Silurus/ooxml: documenti Office con fedeltà al pixel, visualizzati nel browser
Sintesi redazionale: URL dell'articolo: https://github.com/yukiyokotani/office-open-xml-viewer URL dei commenti: https://news.ycombinator.com/item?id=48436863 Punti: 54 # Commenti: 21. Fonte originale: https://github.com/yukiyokotani/office-open-xml-viewer
<p>Note</p><p>**This entire codebase — Rust parsers, TypeScript renderers, tests, and tooling — is implemented by AI coding agents, primarily Claude and Codex**, through iterative prompting. No human-written application code exists in this repository.</p><p>**Why this project exists — a note from the author**</p><p>OOXML's behavior is defined by a written specification (ECMA-376 / ISO-29500), and there is a clear answer to compare against: Word, Excel, and PowerPoint themselves. In principle, anyone with enough patience could have built a faithful viewer — the spec says what to implement, and the Office applications show whether you got it right.</p><p>In practice, it didn't happen. For more than a decade, no free, open-source library reached a rendering quality good enough for real use. There are a few commercial libraries with decent fidelity (and editing support), but their pricing makes them hard to adopt casually. I think the reason is simply cost: the specification is huge, and reading and implementing it faithfully takes far more effort than volunteers can afford.</p><p>Generative AI changed that. A viewer is an unusually good fit for AI-driven iterative development ("vibe coding"): there is a spec to read and a correct output to aim for, so the work comes down to interpreting the specification and refining the rendering until it matches. Limiting the scope to viewing also avoids the most serious risk an Office library can carry — corrupting a user's files.</p><p>So I'm building this library with AI coding agents, spec-first, and keeping it free to use. For some documents it already reproduces the desktop Office applications more faithfully than commercial libraries — and sometimes even the official Microsoft 365 web apps.</p><p>A browser-based viewer for Office Open XML documents that renders to an HTML Canvas element.<br>The parsers are written in Rust and compiled to WebAssembly; the renderers use the Canvas 2D API.<br>Each format also exposes a headless engine (`DocxDocument`</p><p>/ `XlsxWorkbook`</p><p>/ `PptxPresentation`</p><p>) that renders into any caller-supplied canvas, so you can compose your own UI — scroll views, thumbnail grids, master-detail panes — instead of being locked into the built-in viewer. See the live framework examples for runnable React, Vue, Svelte, and Solid projects.</p><p>This project is intentionally a **read-only viewer**. Editing interfaces, mutation APIs, editable or lossless document models, saving / round-tripping, and partial editing of DOCX, XLSX, or PPTX files are out of scope. Read-only interactions such as selection, copy, search, shape IDs, hit testing, and annotation or external-tool integrations remain in scope, and editing-focused forks are welcome. See #496 for the rationale.</p><p>| DOCX | XLSX | PPTX |<br>|---|---|---|</p><p>```<br>npm install @silurus/ooxml<br># or<br>pnpm add @silurus/ooxml<br>```</p><p>Bundler note: the Rust parsers ship as real`.wasm`</p><p>asset files next to the JavaScript, referenced with the standard`new URL('…', import.meta.url)`</p><p>form and fetched (streaming-compiled) at load time. Verified to work with zero config:webpack 5,Next.js(Turbopack, dev and build),Vite 8(dev and build),Vite 7 production builds, and a plain`<script type="module">`</p><p>with no bundler at all. Two setups need a hand:</p><p>Vite 7 dev server: the dependency optimizer rewrites the asset reference into its own cache path and the load fails (fixed in Vite 8). Add`optimizeDeps: { exclude: ['@silurus/ooxml'] }`</p><p>to your`vite.config`</p><p>— production builds are unaffected.esbuild / Angular CLI(whose application builder is esbuild-based):`new URL`</p><p>asset references are not processed (esbuild#795). Copy the`.wasm`</p><p>into your served output and point the viewer at it with the`wasmUrl`</p><p>load option. For Angular CLI, copy the required`*_parser_bg.wasm`</p><p>asset from`node_modules/@silurus/ooxml/dist`</p><p>into the served output and pass its public URL to the viewer.</p><p>`wasmUrl`</p><p>also serves the parser WASM from a CDN or any path you control:new DocxViewer(canvas, { wasmUrl: 'https://cdn.example.com/docx_parser_bg.wasm' });</p><p>Bundle size note: the package is ESM-only (`.mjs`</p><p>). npm'sUnpacked Sizeincludes every entry and optional asset, so it is not the size of a format-specific application graph. See the current production measurements on the stable Bundle size page. Import only the format and optional renderer entries your application needs. In main mode, MathJax, ChartEx, 3-D, Region Map and TIFF implementations remain outside the graph unless imported; the separately loaded worker asset is self-contained.</p><p>```<br>import { DocxViewer } from '@silurus/ooxml/docx';<br>import { XlsxSheetViewer, XlsxViewer, XlsxWorkbook } from '@silurus/ooxml/xlsx';<br>import { PptxViewer } from '@silurus/ooxml/pptx';<br>// DOCX — caller provides the <canvas><br>const docxCanvas = document.getElementById('docx-canvas') as HTMLCanvasElement;<br>const docx = new DocxViewer(docxCanvas);<br>await docx.load('/document.docx');<br>docx.nextPage();<br>// XLSX — viewer manages its own <canvas> + tab bar<br>const container = document.getElementById('xlsx-container') as HTMLElement;<br>const xlsx = new XlsxViewer(container);<br>await xlsx.load('/workbook.xlsx');<br>xlsx.setSelection('B2:D5'); // A1 strings describe geometry; the normalized upper-left is ActiveCell<br>// Excel keeps Selection and ActiveCell separate. Use structured state when the<br>// active cell or Shift-extension anchor is not the area's upper-left cell.<br>xlsx.setSelection({<br>areas: [{ kind: 'cells', top: 2, left: 2, bottom: 5, right: 4 }],<br>activeAreaIndex: 0,<br>activeCell: { row: 3, col: 3 },<br>extensionAnchor: { row: 2, col: 2 },<br>});<br>// Read-only, serializable context for an AI/MCP request. Populated cells are<br>// bounded and detached; formulas and Viewer-formatted display text are retained.<br>const context = xlsx.getSelectionContext({<br>maxCells: 1_000,<br>maxTextCharacters: 1_048_576,<br>});<br>// XLSX active-sheet surface only — caller provides the <canvas><br>const sheetCanvas = document.getElementById('xlsx-canvas') as HTMLCanvasElement;<br>const sheet = new XlsxSheetViewer(sheetCanvas);<br>await sheet.load('/workbook.xlsx');<br>await sheet.goToSheet(1);<br>// Share one parsed workbook across independently scrollable sheet canvases,<br>// including canvases created in same-origin popup windows.<br>const workbook = await XlsxWorkbook.load('/workbook.xlsx');<br>const firstCanvas = document.getElementById('xlsx-first-canvas') as HTMLCanvasElement;<br>const secondCanvas = document.getElementById('xlsx-second-canvas') as HTMLCanvasElement;<br>const firstSheet = XlsxSheetViewer.fromWorkbook(firstCanvas, workbook);<br>const secondSheet = XlsxSheetViewer.fromWorkbook(secondCanvas, workbook);<br>await Promise.all([<br>firstSheet.goToSheet(0),<br>secondSheet.goToSheet(1),<br>]);<br>// Each viewer borrows the workbook; the caller closes it after the viewers.<br>firstSheet.destroy();<br>secondSheet.destroy();<br>workbook.destroy();<br>// PPTX — caller provides the <canvas><br>const pptxCanvas = document.getElementById('pptx-canvas') as HTMLCanvasElement;<br>const pptx = new PptxViewer(pptxCanvas);<br>await pptx.load('/deck.pptx');<br>pptx.nextSlide();<br>```</p><p>`XlsxSheetViewer`</p><p>also offers a deliberately small delimited-text preview convenience<br>that reuses the Excel-style sheet surface. It has the same source and ownership<br>lifecycle as XLSX loading: a string is fetched as a URL, an `ArrayBuffer`</p><p>is<br>caller-owned, a successful reload replaces and disposes the previous source,<br>and `destroy()`</p><p>releases the active source. There is no format auto-detection or<br>general-purpose tabular model, and `XlsxWorkbook`</p><p>/ `XlsxViewer`</p><p>remain<br>OOXML-only. Every delimited field stays text, so leading zeroes, long identifiers,<br>dates, and `=...`</p><p>values are displayed without type or formula inference.<br>This convenience covers delimiter-based text regardless of filename extension;<br>fixed-width PRN, DIF, and SYLK remain separate formats and are not parsed here.</p><p>```<br>const csvFile = document.querySelector('input[type=file]') as HTMLInputElement;<br>const csvBytes = await (csvFile.files?.[0] as File).arrayBuffer();<br>await sheet.load(csvBytes, { format: 'csv' });<br>// String sources are URLs, just like sheet.load('/workbook.xlsx').<br>await sheet.load('/export.tsv', { format: 'tsv', encoding: 'windows-1252' });<br>// Extensions such as .txt, .dat, and .psv do not imply one delimiter.<br>await sheet.load('/report.dat', { format: 'delimited-text', delimiter: '|' });<br>```</p><p>OMML equations (`m:oMath`</p><p>/ `m:oMathPara`</p><p>) in `.docx`</p><p>, `.pptx`</p><p>and `.xlsx`</p><p>are rendered with<br>MathJax + STIX Two Math.<br>That engine is ~4 MB, so it is **opt-in**: import the `math`</p><p>engine from the separate<br>`@silurus/ooxml/math`</p><p>entry and pass it to the viewer. Pass it and equations render;<br>omit it and the engine asset is not fetched or evaluated (equations are simply skipped;<br>the on-demand render-worker asset retains a small loader). When you *do* pass it, the ~4 MB engine ships<br>as a **standalone asset file** next to the bundle rather than an inline data URL, and is<br>fetched **on demand — only the first time a document actually contains an equation**, so<br>equation-free documents never pay for it. It is fully self-contained: served from your own<br>origin, no cross-origin requests.</p><p>Accented Latin letters use STIX Two Math paths in normal, italic, bold and bold-italic equations. Less common Cyrillic, phonetic and dingbat characters use a system-font fallback, so their appearance can vary by platform.</p><p>```<br>import { DocxViewer } from '@silurus/ooxml/docx';<br>import { math } from '@silurus/ooxml/math';<br>const canvas = document.getElementById('docx-canvas') as HTMLCanvasElement;<br>const docx = new DocxViewer(canvas, { math }); // ← equations now render<br>await docx.load('/paper-with-equations.docx');<br>```</p><p>The same `math`</p><p>engine works for every viewer (`DocxViewer`</p><p>, `PptxViewer`</p><p>,<br>`XlsxViewer`</p><p>) and every headless engine (`DocxDocument`</p><p>, `PptxPresentation`</p><p>,<br>`XlsxWorkbook`</p><p>). You inject it **once** where you create the object — the viewer<br>constructor or the `.load()`</p><p>options — and every render reuses it; it is never a<br>per-render argument. (Excel stores "Insert > Equation" as OMML inside the shared<br>DrawingML `<xdr:txBody>`</p><p>grammar, so `XlsxViewer`</p><p>renders equations embedded in<br>shapes / text boxes the same way.)</p><p>Legacy binary Office files can be read directly into the ordinary workbook<br>and presentation models through **model sources**, a format-generic<br>`modelSources`</p><p>load option. The legacy readers are separate opt-in entries,<br>one per format; each returns a model source for the matching loader or viewer:</p><p>```<br>import { PptxViewer } from '@silurus/ooxml/pptx';<br>import { legacyPptSource } from '@silurus/ooxml/legacy-ppt';<br>const canvas = document.querySelector('canvas') as HTMLCanvasElement;<br>const viewer = new PptxViewer(canvas, { modelSources: [legacyPptSource()] });<br>await viewer.load(pptOrPptxBytes);<br>```</p><p>`legacyXlsSource()`</p><p>(`@silurus/ooxml/legacy-xls`</p><p>) works the same way for<br>XLSX. A source claims only its own binary family; every other input takes the<br>unchanged OOXML path, and without a source a legacy file still rejects with<br>the typed `legacy-binary-format`</p><p>error. Creating a source fetches nothing: its<br>self-contained source module and WASM load in the parser worker (or in Node)<br>only when a claimed file is opened, and no OOXML package is generated. The<br>readers are narrow and experimental and reject unsupported content rather than<br>guessing; see Experimental legacy Office sources.</p><p>Classic DrawingML 2-D chart families are included in every format entry.<br>Microsoft ChartEx, model-space 3-D charts and offline country-level Region Maps<br>are separate entries. TIFF decoding is also a separate entry shared by DOCX,<br>XLSX and PPTX. Inject the modules once in the same load options object as<br>`math`</p><p>; omitting one keeps its main-mode implementation out of the ordinary<br>format graph. The built-in modules work in both main and worker modes. Without `chartEx`</p><p>,<br>ChartEx families show the standard unsupported-chart placeholder. Without<br>`threeD`</p><p>, 3-D chart groups<br>fall back to their canonical 2-D family. Without `regionMap`</p><p>_(testo troncato)_</p>
