
2026/06/08 2:22
Silurus/ooxml:ブラウザ上で表示されるピクセル忠実な Office ドキュメント
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
@silurus/ooxml は、完全に AI が生成した(人間が書かれたアプリコードなし)Rust/WASM シートであり、クライアント側 WebAssembly パーサーを Web Workers で実行することでブラウザ上で DOCX、XLSX、PPTX ファイルを高忠実度でレンダリングします。このスレッドセーフなアーキテクチャは、メインスレッドでレンダリングを行いながら詳細なグラフィックを Canvas 2D に描画し、一貫した FontFaceSet の使用を保証します。このパッケージは ESM 専用であり、Vite バンダーなどのバンドラーを使用する場合は vite-plugin-wasm を、webpack では experiments.asyncWebAssembly を設定することで WebAssembly モジュールを正しく処理する必要があります。
数学式は、OMML 方程式の DOCX ドキュメントを安全に解析するための MathJax + STIX Two Math エンジン(約 3 MB で、インポートされない場合は tree-shaken)をオプトインしてサポートします。レンダリング機能には以下が含まれます:
- DOCX:ヘッダー/フッター付きのページレイアウト、変更追跡、コメント(解析済み)、ネイティブテキスト選択。
- XLSX:複数シートを持つワークブック、スパークライン、グラフマーカー/誤差棒、固定ペイン、Excel 風ズームスライダー。
- PPTX:スライド背景、画像埋め込みによる SmartArt、インク/手書き、および広範な形状・テーブルスタイル。
テキスト選択は現在、ネイティブ Canvas
drawElement API が利用されるまでの暫定的な透明 DOM オーバーレイを使用しています。デフォルトではネットワークリクエストが発生しません。ZIP ボムに対しては maxZipEntryBytes で保護され、XML パーソル化は XXE セーフです。Google Fonts は useGoogleFonts: true が設定されるまで自動で読み込まれません。
関連パッケージには、WASM ベースのコンバーターである
@silurus/ooxml-markdown、ヘッドレスパーサーである @silurus/ooxml-node、および自動インストールされる MCP サーバーを備えた VS Code 拡張機能が含まれます。開発コマンドは pnpm build:wasm、pnpm storybook、視覚的回帰テスト用には pnpm vrt です。このプロジェクトは MIT ライセンスの下でリリースされています。本文
@silurus/ooxml:ブラウザベースの Office ファイルビューア
概要と構成
- 実装方法: コードベース全体は、反復的なプロンプティングを通じて Claude(Anthropic の AI)によって作成されています。人間が書かれたアプリケーションコードは含まれていません。
- 機能: Office Open XML (.docx, .xlsx, .pptx) ファイルをブラウザ上で読み込み、HTML Canvas 要素にレンダリングします。
- アーキテクチャ:
- パーサー: Rust で記述され、WebAssembly (WASM) にコンパイルされています。
- レンダー: Canvas 2D API を使用します。
- ヘッドレスエンジン:
、DocxDocument
、XlsxWorkbook
は公開されており、ビルトインビューアに依存せず、独自の UI(スクロール、サムネイル、マスター/ディテールなど)を構成できます。PptxPresentation
| フォーマット | 動作状態 |
|---|---|
| DOCX | ✅ |
| XLSX | ✅ |
| PPTX | ✅ |
インストール
npm install @silurus/ooxml # または pnpm add @silurus/ooxml
重要:バンドラー設定と注意
-
WASM 埋め込みの対応
- Vite:
を追加してください。vite-plugin-wasm - webpack:
を使用してください。experiments.asyncWebAssembly
- Vite:
-
バンドルサイズについて
- このパッケージは ESM (
) 専用です。.mjs - npm の「Unpacked Size」は約 3 MB です(数学エンジン
を含むオプションエントリ)。MathJax + STIX Two Math - アプリに組み込まれる実際のサイズは小さく、必要なフォーマットだけをインポートすれば済みます(例:
)。@silurus/ooxml/pptx - 数学エンジンは独立したパッケージ (
) です。これらを受け取りない限り、バンドルからは自動的に除外(ツリーシェイク)されます。@silurus/ooxml/math
- このパッケージは ESM (
クイックスタート
DOCX (ワード)
呼び出し側が Canvas を提供する必要あり。
import { DocxViewer } from '@silurus/ooxml/docx'; const canvas = document.getElementById('docx-canvas') as HTMLCanvasElement; const docx = new DocxViewer(canvas); await docx.load('/document.docx'); docx.nextPage(); // ページ移動
XLSX (エクセル)
ビューア自体が Canvas とタブバーを管理します。
import { XlsxViewer } from '@silurus/ooxml/xlsx'; const container = document.getElementById('xlsx-container') as HTMLElement; const xlsx = new XlsxViewer(container); await xlsx.load('/workbook.xlsx');
PPTX (パワーポイント)
呼び出し側が Canvas を提供する必要あり。
import { PptxViewer } from '@silurus/ooxml/pptx'; const canvas = document.getElementById('pptx-canvas') as HTMLCanvasElement; const pptx = new PptxViewer(canvas); await pptx.load('/deck.pptx'); pptx.nextSlide(); // スライド移動
方程式のレンダリング (MathJax + STIX Two Math)
.docx や .pptx 内の OMML 方程式 (m:oMath) は、MathJax と STIX Two Math を使用してレンダリングされます。
- エンジンサイズ: 約 3 MB(オプション)。
- 導入方法:
パッケージを別エントリとしてインポートし、ビューアに渡す必要があります。@silurus/ooxml/math- インポート → レンダリング
- インポートなし → エクストラバンドルなし(方程式はスキップ)
- 適用対象:
,DocxViewer
,PptxViewer
,DocxDocument
(オプション引数PptxPresentation
)。math - 例外:
は方程式をサポートしていません。XlsxViewer
import { DocxViewer } from '@silurus/ooxml/docx'; import { math } from '@silurus/ooxml/math'; // 数学エンジンを読み込む const canvas = document.getElementById('docx-canvas') as HTMLCanvasElement; // ← ここに math オプションを渡すことで方程式がレンダリングされます const docx = new DocxViewer(canvas, { math }); await docx.load('/paper-with-equations.docx');
アーキテクチャ
ビルドとランタイムの流れ
flowchart TB subgraph build["🦀 ビルド時 (Rust → WebAssembly)"] direction LR docx_rs["packages/docx/parser/src/lib.rs"] xlsx_rs["packages/xlsx/parser/src/lib.rs"] pptx_rs["packages/pptx/parser/src/lib.rs"] docx_rs -- wasm-pack --> docx_wasm["docx_parser.wasm"] xlsx_rs -- wasm-pack --> xlsx_wasm["xlsx_parser.wasm"] pptx_rs -- wasm-pack --> pptx_wasm["pptx_parser.wasm"] end subgraph browser["🌐 ランタイム (ブラウザ)"] subgraph core_pkg["@silurus/ooxml-core (共有プリミティブ)"] CORE["renderChart<br/>resolveFill·applyStroke<br/>buildCustomPath<br/>autoResize<br/>共有型定義"] end subgraph docx_pkg["@silurus/ooxml · docx"] DV["DocxViewer"] --> DD["DocxDocument"] DD --> DW["worker.ts\n(WASM 初期化・解析)"] DD --> DR["renderer.ts\n(Canvas 2D レンダリング)"] end subgraph xlsx_pkg["@silurus/ooxml · xlsx"] XV["XlsxViewer"] --> XB["XlsxWorkbook"] XB --> XW["worker.ts\n(WASM 初期化・解析)"] XB --> XR["renderer.ts\n(Canvas 2D レンダリング)"] end subgraph pptx_pkg["@silurus/ooxml · pptx"] PV["PptxViewer"] --> PP["PptxPresentation"] PP --> PW["worker.ts\n(WASM 初期化・解析)"] PP --> PR["renderer.ts\n(Canvas 2D レンダリング)"] end DR -. uses .-> CORE XR -. uses .-> CORE PR -. uses .-> CORE DW -- WASM --> docx_wasm XW -- WASM --> xlsx_wasm PW -- WASM --> pptx_wasm DR --> canvas["<canvas>"] XR --> canvas PR --> canvas end
読み込み処理の仕組み
- ワーカー (Web Worker): Rust WASM を使用して
/.docx
/.xlsx
アーカイブを解析し、JSON モデルとして結果をポストします。.pptx - メインスレッド (Renderer): JSON モデルに基づいて Canvas 上に描画を行います。
- メインスレッド上のため、
を直接共有可能。FontFaceSet - ワーカー内の
は独自のフォント設定を持つため、システムフォントにフォールバックすると文字幅が微妙に異なる可能性があります。OffscreenCanvas
- メインスレッド上のため、
主要なファイル構成
| ファイル | ロール |
|---|---|
| Rust WASM パーサー: ZIP → JSON モデル変換 |
| メインスレッド レンダラー: Canvas 2D 描画エンジン |
| Web Worker: WASM 初期化と解析専用(1 つのフォーマットあたり) |
| 公開 API: キャンバスライフサイクル・ナビゲーション制御 |
| 共有プリミティブ: チャートレンダラー、形状ヘルパー ( など)、 |
フレームワーク例 (React, Vue, Angular, Svelte, SolidJS, Qwik)
以下はスライドショー (
PptxViewer) の実装例です。
React 19
import { useEffect, useRef, useState } from 'react'; import { PptxViewer } from '@silurus/ooxml/pptx'; export function PptxViewerComponent({ src }: { src: string }) { const canvasRef = useRef<HTMLCanvasElement>(null); const viewerRef = useRef<PptxViewer | null>(null); const [slide, setSlide] = useState({ current: 0, total: 0 }); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const viewer = new PptxViewer(canvas, { onSlideChange: (i, total) => setSlide({ current: i, total }), }); viewerRef.current = viewer; viewer.load(src); }, [src]); return ( <div> <canvas ref={canvasRef} style={{ width: 800 }} /> <button onClick={() => viewerRef.current?.prevSlide()}>‹ Prev</button> <span> {slide.current + 1} / {slide.total} </span> <button onClick={() => viewerRef.current?.nextSlide()}>Next ›</button> </div> ); }
Vue 3.5
<script setup lang="ts"> import { useTemplateRef, onMounted, ref } from 'vue'; import { PptxViewer } from '@silurus/ooxml/pptx'; const props = defineProps<{ src: string }>(); const canvas = useTemplateRef<HTMLCanvasElement>('canvas'); let viewer: PptxViewer | null = null; const current = ref(0); const total = ref(0); onMounted(async () => { viewer = new PptxViewer(canvas.value!, { onSlideChange: (i, t) => { current.value = i; total.value = t; }, }); await viewer.load(props.src); }); </script> <template> <div> <canvas ref="canvas" style="width: 800px" /> <button @click="viewer?.prevSlide()">‹ Prev</button> <span> {{ current + 1 }} / {{ total }} </span> <button @click="viewer?.nextSlide()">Next ›</button> </div> </template>
Angular 19
import { Component, ElementRef, viewChild, signal, AfterViewInit } from '@angular/core'; import { PptxViewer } from '@silurus/ooxml/pptx'; @Component({ selector: 'app-pptx-viewer', standalone: true, template: ` <div> <canvas #canvas style="width: 800px"></canvas> <button (click)="prev()">‹ Prev</button> <span> {{ current() + 1 }} / {{ total() }} </span> <button (click)="next()">Next ›</button> </div> `, }) export class PptxViewerComponent implements AfterViewInit { canvasEl = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas'); current = signal(0); total = signal(0); private viewer?: PptxViewer; ngAfterViewInit(): void { this.viewer = new PptxViewer(this.canvasEl().nativeElement, { onSlideChange: (i, t) => { this.current.set(i); this.total.set(t); }, }); this.viewer.load('/deck.pptx'); } prev(): void { this.viewer?.prevSlide(); } next(): void { this.viewer?.nextSlide(); } }
Svelte 5
<script lang="ts"> import { onMount } from 'svelte'; import { PptxViewer } from '@silurus/ooxml/pptx'; let { src }: { src: string } = $props(); let canvas: HTMLCanvasElement; let viewer: PptxViewer; let current = $state(0); let total = $state(0); onMount(async () => { viewer = new PptxViewer(canvas, { onSlideChange: (i, t) => { current = i; total = t; }, }); await viewer.load(src); }); </script> <div> <canvas bind:this={canvas} style="width: 800px"></canvas> <button onclick={() => viewer?.prevSlide()}>‹ Prev</button> <span> {current + 1} / {total} </span> <button onclick={() => viewer?.nextSlide()}>Next ›</button> </div>
SolidJS 1.9
import { createSignal, onMount } from 'solid-js'; import { PptxViewer } from '@silurus/ooxml/pptx'; export function PptxViewerComponent(props: { src: string }) { let canvasEl!: HTMLCanvasElement; let viewer: PptxViewer | undefined; const [current, setCurrent] = createSignal(0); const [total, setTotal ] = createSignal(0); onMount(async () => { viewer = new PptxViewer(canvasEl, { onSlideChange: (i, t) => { setCurrent(i); setTotal(t); }, }); await viewer.load(props.src); }); return ( <div> <canvas ref={canvasEl} style={{ width: '800px' }} /> <button onClick={() => viewer?.prevSlide()}>‹ Prev</button> <span> {current() + 1} / {total()} </span> <button onClick={() => viewer?.nextSlide()}>Next ›</button> </div> ); }
Qwik 2
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik'; import type { PptxViewer as PptxViewerType } from '@silurus/ooxml/pptx'; export const PptxViewerComponent = component$<{ src: string }>(({ src }) => { const canvasRef = useSignal<HTMLCanvasElement>(); const current = useSignal(0); const total = useSignal(0); let viewer: PptxViewerType | undefined; // ブラウザ環境のみ実行(SSR ではスキップ) useVisibleTask$(async () => { if (!canvasRef.value) return; const { PptxViewer } = await import('@silurus/ooxml/pptx'); viewer = new PptxViewer(canvasRef.value, { onSlideChange: (i, t) => { current.value = i; total.value = t; }, }); await viewer.load(src); }); return ( <div> <canvas ref={canvasRef} style={{ width: '800px' }} /> <button onClick$={() => viewer?.prevSlide()}>‹ Prev</button> <span> {current.value + 1} / {total.value} </span> <button onClick$={() => viewer?.nextSlide()}>Next ›</button> </div> ); });
機能サポート詳述
Word (.docx)
- ドキュメント: ページレンダリング、サイズ/余白、ヘッダー/フッター(ページ番号・偶奇)、セクション分離
- テキスト: フォント設定、太字/イタリック/アンダーライン/取り消し線、ハイパーリンク、上付き/下付き文字、ルビ注記 (ふりがな)
- フォートメット: 行間隔、グリッド、インデント/タブストップ、リスト(箇条書き・番号)、スタイル(見出し、Normal)、テーブルスタイル(枠線/塗りつぶし)、目次(TOC)
- 要素: テーブル(マージ/帯柄)、数学方程式 (OMML) (MathJax 使用)、画像(インライン/テキスト回り込み)、テキストボックス、WMF/EMF(予定未定)、変更追跡、コメント(解析済みで非表示)
- 高度機能: 変更追跡(著者色付き)、メールマージフィールド(予定未定)
Excel (.xlsx)
- ワークブック: 複数シート、シートタブの色設定
- セル: テキスト/数値/ブール値/エラー、式計算結果、日付形式、リッチテキスト
- フォーマット: フォント設定、背景色(ソリッド・グラデーション)、パターン塗りつぶし、枠線(破線・点線など)、アライメント、折り返し、数値形式(通貨/パーセントなど)
- 構造: マージセル、フローリングパン、非表示行/列の管理
- 要素: 画像、描画形状、チャート(棒/折れ線/エリア/レーダー/散布図/泡グラフ)、スライサー(2010 互換)
- チャート詳細: マーカー種別変更、データラベル、誤差棒、スパークライン(Excel Sparkline)、手動レイアウト
- 高度機能: 条件付きフォーマット(セル基準・色スケール・ダータバーなど)
PowerPoint (.pptx)
- スライド: レイアウト継承、サイズ変更、背景設定、スライド番号
- 要素タイプ: 形状、画像(グループ化/変換可)、コンネクタ、テーブル、チャート、SmartArt(非対応)、OLE オブジェクト(非対応)、メディア再生、インク書き込み
- 形状ジオメトリ: 130+ プリセット形状、カスタムパス、回転/反転、3D(非対応)
- 塗りつぶし/ストローク: ソリッド/グラデーション/パターン (30 種)、画像埋め込み、破線・矢印・二重ライン
- 形状効果: ドロップシャドウ、グロー、反射、ボベル(部分実装)
- テキスト: フォント設定(東アジア対応)、太字/イタリック/アンダーラインスタイル、数学方程式、ハイパーリンク、アウトライン、数学式
- パラグラフィ: 水平/垂直配置、行間隔、箇条書き、インデント、縦書き (bodyPr@vert)、右から左配置 (rtl)
- テキストレイアウト: マルチカラム、収縮自動調整 (normAutoFit/spAutoFit)、折り返し制御、テキストパディング
- テーマ: スキーム色(16 色)、フォントスキーム、アルファ/輝度変換
- 相互作用: テキスト選択機能
注記:テキスト選択の実装
現在のテキスト選択機能は、Canvas グリフレンダリング +透過性 DOM レヤーのオーバーレイという二重構造で実装されています。これは応急処置であり、将来 WICG/html-in-canvas の
drawElement API が標準化された時点で、単一パイプラインへの移行が予定されています。
サブパッケージとツール
- packages/markdown/: Office ファイルを Markdown へ変換する CLI とライブラリ(GitHub Actions などでの大規模変換に有用)。
- packages/node/: Node.js 環境用のサーバーサイドパーサー(Web Worker 不要)。CI チェックやヘッドレスレンダリングパイプライン向け。
CLI も含む。ooxml-thumbnail - packages/vscode-extension/: VS Code 拡張機能(
)。ファイル選択時に自動で解析し、AI エージェントへ構造を提供する。ooxml-viewer - packages/mcp-server/: AI エージェント(Claude, Copilot など)向けの MCP サーバー。ファイルの構造をツールとして提供 (
等)。Mac/Win/Linux のプリビルトバイナリ付き。docx_get_structure
開発方法
依存関係とビルド
# 依存関係のインストール pnpm install # WASM パーサーの構築(Rust + wasm-pack 必須) pnpm build:wasm # Storybook デバッグモード (ポート 6006) pnpm storybook # 型チェックの実行 pnpm typecheck # 視覚的回復テスト (Visual Regression Test) pnpm vrt UPDATE_REFS=1 pnpm vrt # ベースラインの更新 # ライブラリ全体のビルド pnpm build
WASM の個別構築手順
cd packages/docx/parser && wasm-pack build --target web && cp pkg/docx_parser_bg.wasm pkg/docx_parser.js ../src/wasm/ cd packages/xlsx/parser && wasm-pack build --target web && cp pkg/xlsx_parser_bg.wasm pkg/xlsx_parser.js ../src/wasm/ cd packages/pptx/parser && wasm-pack build --target web && cp pkg/pptx_parser_bg.wasm pkg/pptx_parser.js ../src/wasm/
セキュリティとプライバシー
- Canvas 専用レンダリング: ドキュメントは解読され Canvas に描画されます。HTML/JS/XSS な攻撃や外部コンテンツの注入は行われません。
- ZIP デコンプレッション制限:
- 各 ZIP エントリへのデフォルト制限は 512 MiB。DoS 防御のため。
- オーバーライド:
オプション(バイト単位)。maxZipEntryBytesnew PptxViewer(canvas, { maxZipEntryBytes: 64 * 1024 * 1024 }); // 64 MiB
- ネットワーク通信:
- デフォルトはオフ。テレメトリやサードパーティサービスへの連絡なし。
- Google Fonts (テーマフォント) もデフォルトで読み込まれません。
を指定すると HTTP リクエストが発生する(注意)。useGoogleFonts: true
- XML パーシング: 外部エンティティ解決を行わない (
使用)。XXE のリスクは排除されています。roxmltree
ライセンス
MIT