Home Business & Finance Silurus/ooxml: Pixel-faithful Office documents, rendered...
Business & Finance

Silurus/ooxml: Pixel-faithful Office documents, rendered in the browser

Key Points

This entire codebase — Rust parsers, TypeScript renderers, tests, and tooling — was implemented by Claude (Anthropic's AI assistant) through iterative prompting. No human-written application code exists in this repository. A browser-based viewer for Office Open XML documents that renders to an HTML Canvas element.

This entire codebase — Rust parsers, TypeScript renderers, tests, and tooling — was implemented by Claude (Anthropic's AI assistant) through iterative prompting. No human-written application code exists in this repository. A browser-based viewer for Office Open XML documents that renders to an HTML Canvas element. The parsers are written in Rust and compiled to WebAssembly; the renderers use the Canvas 2D API. Each format also exposes a headless engine (DocxDocument / XlsxWorkbook / PptxPresentation ) 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 Examples section in the Storybook demo. | DOCX | XLSX | PPTX | |---|---|---| npm install @silurus/ooxml # or pnpm add @silurus/ooxml Bundler note: this package embeds .wasm files. With Vite addvite-plugin-wasm ; with webpack useexperiments.asyncWebAssembly . Bundle size note: the package is ESM-only ( .mjs ). npm's Unpacked Size sums all four entry bundles, including the opt-in math engine (MathJax + STIX Two Math, ~3 MB). What actually lands in your app is much smaller — import only the format you need (e.g.@silurus/ooxml/pptx ). The math engine is a separate entry (@silurus/ooxml/math ): it is bundled only if you import it and pass it to a viewer (see Rendering equations). Viewers that never receive amath engine — and all xlsx usage — tree-shake the ~3 MB away entirely. import { DocxViewer } from '@silurus/ooxml/docx'; import { XlsxViewer } from '@silurus/ooxml/xlsx'; import { PptxViewer } from '@silurus/ooxml/pptx'; // DOCX — caller provides the const canvas = document.getElementById('docx-canvas') as HTMLCanvasElement; const docx = new DocxViewer(canvas); await docx.load('/document.docx'); docx.nextPage(); // XLSX — viewer manages its own + tab bar const container = document.getElementById('xlsx-container') as HTMLElement; const xlsx = new XlsxViewer(container); await xlsx.load('/workbook.xlsx'); // PPTX — caller provides the const canvas = document.getElementById('pptx-canvas') as HTMLCanvasElement; const pptx = new PptxViewer(canvas); await pptx.load('/deck.pptx'); pptx.nextSlide(); OMML equations (m:oMath / m:oMathPara ) in .docx / .pptx are rendered with MathJax + STIX Two Math. That engine is ~3 MB, so it is opt-in: import the math engine from the separate @silurus/ooxml/math entry and pass it to the viewer. Pass it and equations render; omit it and the engine is referenced nowhere, so a bundler tree-shakes the ~3 MB away entirely (equations are simply skipped). It is fully self-contained: no network, no cross-origin requests. import { DocxViewer } from '@silurus/ooxml/docx'; import { math } from '@silurus/ooxml/math'; const canvas = document.getElementById('docx-canvas') as HTMLCanvasElement; const docx = new DocxViewer(canvas, { math }); // ← equations now render await docx.load('/paper-with-equations.docx'); The same math engine works for PptxViewer and the headless DocxDocument / PptxPresentation APIs (which take math in their options). xlsx has no equation support and never references the engine. Architecture diagram flowchart TB subgraph build["🦀 Build-time (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["🌐 Runtime (Browser)"] subgraph core_pkg["@silurus/ooxml-core (shared primitives)"] CORE["renderChart · resolveFill · applyStroke\nbuildCustomPath · autoResize · shared types"] end subgraph docx_pkg["@silurus/ooxml · docx"] DV["DocxViewer"] --> DD["DocxDocument"] DD --> DW["worker.ts\n〈Web Worker — parse only〉"] DD --> DR["renderer.ts\n〈Canvas 2D — main thread〉"] end subgraph xlsx_pkg["@silurus/ooxml · xlsx"] XV["XlsxViewer"] --> XB["XlsxWorkbook"] XB --> XW["worker.ts\n〈Web Worker — parse only〉"] XB --> XR["renderer.ts\n〈Canvas 2D — main thread〉"] end subgraph pptx_pkg["@silurus/ooxml · pptx"] PV["PptxViewer"] --> PP["PptxPresentation"] PP --> PW["worker.ts\n〈Web Worker — parse only〉"] PP --> PR["renderer.ts\n〈Canvas 2D — main thread〉"] end DR -. uses .-> CORE XR -. uses .-> CORE PR -. uses .-> CORE end docx_wasm --> DW xlsx_wasm --> XW pptx_wasm --> PW DR --> canvas[""] XR --> canvas PR --> canvas All three formats follow the same shape: the worker parses the .docx / .xlsx / .pptx archive via WASM and posts a JSON model back to the main thread, where the renderer draws to the canvas. Rendering stays on the main thread so the canvas shares the document's FontFaceSet — an OffscreenCanvas in a worker has its own font registry and would silently fall back to a system font, producing subtly different text measurements (and wrap positions) from the installed theme webfonts. @silurus/ooxml-core holds the cross-format primitives that the three renderers all depend on: a unified chart renderer (bar / line / area / radar / waterfall), shape helpers (resolveFill , applyStroke , buildCustomPath , hexToRgba ), the autoResize viewer utility, and the shared type definitions. | File | Role | |---|---| packages/docx/parser/src/lib.rs | Rust WASM parser — DOCX ZIP → Document JSON | packages/xlsx/parser/src/lib.rs | Rust WASM parser — XLSX ZIP → Workbook JSON | packages/pptx/parser/src/lib.rs | Rust WASM parser — PPTX ZIP → Presentation JSON | packages/docx/src/renderer.ts | Canvas 2D rendering engine with text layout (main thread) | packages/xlsx/src/renderer.ts | Canvas 2D rendering engine with virtual scroll (main thread) | packages/pptx/src/renderer.ts | Canvas 2D rendering engine (main thread) | packages/*/src/worker.ts | Web Worker: WASM init and parsing only (one per format) | packages/*/src/viewer.ts | Public Viewer API — canvas lifecycle, navigation | packages/core/src/index.ts | Cross-format primitives — chart renderer, shape helpers, autoResize , shared types | React 19 // React 19.1 — vite-plugin-wasm required in vite.config.ts import { useEffect, useRef, useState } from 'react'; import { PptxViewer } from '@silurus/ooxml/pptx'; export function PptxViewerComponent({ src }: { src: string }) { const canvasRef = useRef(null); const viewerRef = useRef(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 ( viewerRef.current?.prevSlide()}>‹ Prev {slide.current + 1} / {slide.total} viewerRef.current?.nextSlide()}>Next › ); } Vue 3.5 Angular 19 // Angular 19 — standalone component with signal-based state import { Component, ElementRef, viewChild, signal, AfterViewInit, } from '@angular/core'; import { PptxViewer } from '@silurus/ooxml/pptx'; @Component({ selector: 'app-pptx-viewer', standalone: true, template: ` ‹ Prev {{ current() + 1 }} / {{ total() }} Next › `, }) export class PptxViewerComponent implements AfterViewInit { canvasEl = viewChild.required>('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(); } } Add "allowSyntheticDefaultImports": true and configure@angular-builders/custom-webpack (or useesbuild builder) with WASM support in your Angular workspace. Svelte 5 viewer?.prevSlide()}>‹ Prev {current + 1} / {total} viewer?.nextSlide()}>Next › SolidJS 1.9 // SolidJS 1.9 import { createSignal, onMount, onCleanup } 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); }); onCleanup(() => { /* viewer?.destroy?.() */ }); return ( viewer?.prevSlide()}>‹ Prev {current() + 1} / {total()} viewer?.nextSlide()}>Next › ); } Qwik 2 // Qwik 2.0 — dynamic import to keep WASM out of SSR bundle 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(); const current = useSignal(0); const total = useSignal(0); let viewer: PptxViewerType | undefined; // useVisibleTask$ runs only in the browser, never during 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 ( viewer?.prevSlide()}>‹ Prev {current.value + 1} / {total.value} viewer?.nextSlide()}>Next › ); }); | Category | Feature | Status | |---|---|---| | Document | Page rendering | ✅ | | Page size and margins | ✅ | | | Headers / footers (default / first / even) | ✅ | | | Section breaks (continuous / nextPage / oddPage / evenPage) | ✅ | | | Text | Paragraphs | ✅ | | Bold, italic, underline, strikethrough | ✅ | | | Font family, size, color | ✅ | | | Hyperlinks | ✅ | | Superscript / subscript (w:vertAlign ) | ✅ | | Ruby annotations / furigana (w:ruby ) | ✅ | | | Formatting | Paragraph alignment (left/center/right/justify) | ✅ | | Line spacing (auto / atLeast / exact) | ✅ | | Line grid (w:docGrid , §17.6.5) | ✅ | | | Margin collapsing between paragraphs | ✅ | | | Indents and tab stops | ✅ | | | Lists (bullet and numbered) | ✅ | | | Paragraph styles (Heading 1–9, Normal, custom) | ✅ | | Table style w:pPr cascade (§17.7.6) | ✅ | | Table style borders / shading / banding (tblStylePr , cnfStyle , §17.4.7) | ✅ | | | Table of contents (TOC field) — dot leaders, right-aligned page numbers | ✅ | | | keepNext / keepLines / widowControl | ✅ | | | Elements | Tables (with borders, fills, merges, banding, alignment) | ✅ | Math equations (OMML m:oMath / m:oMathPara , rendered via MathJax — opt-in @silurus/ooxml/math ) | ✅ | | | Images (inline and anchored, with text wrap) | ✅ | | | Text boxes / drawing shapes | ✅ | | | WMF / EMF metafile images (legacy vector) | ❌ Not planned | | | Advanced | Footnote / endnote reference markers | ✅ | Track changes (w:ins / w:del — author-coloured underline / strikethrough) | ✅ | | | Comments / footnote bodies (parsed, not yet rendered inline) | || | Mail merge fields | ❌ Not planned | | | Interaction | Text selection (transparent overlay, native copy) | ✅ | | Category | Feature | Status | |---|---|---| | Workbook | Multiple sheets, sheet names | ✅ | Sheet tab colors ( — theme / tint / indexed / rgb) | ✅ | | | Cells | Text, number, boolean, error values | ✅ | Formula results (from cached ) | ✅ | | | Dates (ECMA-376 date format codes) | ✅ | | | Rich text (per-run formatting) | ✅ | | | Formatting | Bold, italic, underline (single / double / singleAccounting / doubleAccounting ), strikethrough | ✅ | Superscript / subscript (vertAlign ) | ✅ | | | Font family, size, color | ✅ | | | Cell background color (solid + gradient) | ✅ | | Pattern fills (gray125 / gray0625 / lightGray / mediumGray / darkGray and the 12 light* / dark* directional hatches) | ✅ | | | Borders (thin, medium, thick, hair, double, dashed, dotted, dashDotDot, …) | ✅ | | Diagonal borders (diagonalUp / diagonalDown , single + double) | ✅ | | | Horizontal / vertical alignment | ✅ | | | Text wrapping | ✅ | | Number formats (0.00 , % , #,##0 , custom date/time) | ✅ | | | Structure | Merged cells | ✅ | | Frozen panes | ✅ | | | Row / column sizing (custom widths and heights) | ✅ | | | Hidden rows / columns | ✅ | | | Elements | Images ( ) | ✅ | Drawing shapes / text boxes (xdr:sp , xdr:txBody ) | ✅ | | | Charts (bar, line, area, radar, scatter / bubble) | ✅ | | Chart markers (circle / square / diamond / triangle / x / plus / star / dot / dash, per-point overrides) | ✅ | | Chart data labels ( per-point with CELLRANGE / VALUE / SERIESNAME / CATEGORYNAME field references, position l /r /t /b /ctr /outEnd ) | ✅ | | Chart error bars ( X/Y direction, cust / fixedVal / stdErr / stdDev / percentage , dashed/styled lines) | ✅ | | Chart manual layout ( and ) | ✅ | | Sparklines (x14:sparklineGroup — line / column / win-loss, with markers and high/low/first/last/negative highlights) | ✅ | | | Advanced | Conditional formatting (cellIs , colorScale , dataBar , iconSet , top10 , aboveAverage ) | ✅ | | Slicers (static, Office 2010 extension) | ✅ | | | Pivot tables | ❌ Not planned | | | Data validation / comments | ❌ Not planned | | | Interaction | Cell selection (single / range / row / column / all) | ✅ | | Excel-style row / column header highlight on selection | ✅ | | | Shift+click to extend, Ctrl+C to copy as TSV | ✅ | | | Text selection inside cells (transparent overlay) | ✅ | | onSelectionChange callback, getCellAt(x, y) API | ✅ | | Zoom slider (Excel-style, right of the tab bar, 10–400% with 100% centered; showZoomSlider option) | ✅ | | Category | Feature | Status | |---|---|---| | Slides | Slide rendering | ✅ | | Slide layout / master inheritance | ✅ | | | Slide size (custom dimensions) | ✅ | | | Slide background (solid, gradient, image) | ✅ | | | Slide numbers | ✅ | | | Notes pages | ❌ | | | Animations / transitions | ❌ Not planned | | | Element types | Shapes (sp ) | ✅ | Pictures (pic ) | ✅ | | Groups (grpSp ) with nested transforms | ✅ | | Connectors (cxnSp ) | ✅ | | Tables (tbl in graphicFrame ) | ✅ | | | Charts (bar, line, area, radar, waterfall) | ✅ | | | Charts (pie, doughnut) | ✅ | | Charts (scatter — scatterStyle marker / line / smooth variants) | ✅ | | Charts (bubble — bubbleSize per-point area scaling) | ✅ | | | SmartArt | ❌ | | | OLE objects | ❌ | | | Video / audio (poster + interactive playback) | ✅ | | Ink / handwriting (p:contentPart , raster fallback) | ✅ | | | Shape geometry | 130+ preset shapes (prstGeom ) | ✅ | Custom geometry (custGeom ) on shapes and pictures (clipping) | ✅ | | | Rotation and flip (flipH / flipV) | ✅ | | | 3D preset shapes | ❌ | | | Fills | Solid fill (solidFill ) | ✅ | Linear / radial gradient (gradFill ) | ✅ | | No fill (noFill ) | ✅ | | Pattern fill (pattFill ) — 30 preset bitmaps incl. pct5–pct90 / horz / vert / cross / diag / grid / brick / check / trellis | ✅ | | Image fill on shapes (blipFill in sp ) | ✅ | | | Strokes | Solid line color and width | ✅ | | Dash / dot styles | ✅ | | Arrow heads (headEnd / tailEnd ) | ✅ | | | Compound / double lines (`
Office (ORG) Claude (PERSON) Office Open XML (ORG) WebAssembly (ORG) the Canvas 2D API (ORG) DocxDocument / XlsxWorkbook / PptxPresentation (ORG) UI (ORG) Storybook (EVENT) Bundler (PERSON) Vite (PERSON) ESM (ORG) npm (PERSON) MathJax (ORG) MB (ORG) HTMLElement (ORG)
Originally published by Hacker News Read original →