npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@silurus/ooxml

v0.64.3

Published

Browser-based OOXML viewer (docx/xlsx/pptx) — Rust/WASM parser + Canvas renderer

Readme

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.

Office Open XML Viewer

npm version npm downloads VS Code Marketplace license

Live demo

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 | |:---:|:---:|:---:| | docx | xlsx | pptx |

npm install @silurus/ooxml
# or
pnpm add @silurus/ooxml

Bundler note: this package embeds .wasm files. With Vite add vite-plugin-wasm; with webpack use experiments.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, ~4 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 a math engine tree-shake the ~4 MB away entirely.


Quick Start

import { DocxViewer } from '@silurus/ooxml/docx';
import { XlsxViewer } from '@silurus/ooxml/xlsx';
import { PptxViewer } from '@silurus/ooxml/pptx';

// DOCX — caller provides the <canvas>
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 <canvas> + tab bar
const container = document.getElementById('xlsx-container') as HTMLElement;
const xlsx = new XlsxViewer(container);
await xlsx.load('/workbook.xlsx');

// PPTX — caller provides the <canvas>
const canvas = document.getElementById('pptx-canvas') as HTMLCanvasElement;
const pptx = new PptxViewer(canvas);
await pptx.load('/deck.pptx');
pptx.nextSlide();

Rendering equations

OMML equations (m:oMath / m:oMathPara) in .docx, .pptx and .xlsx are rendered with MathJax + STIX Two Math. That engine is ~4 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 ~4 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 every viewer (DocxViewer, PptxViewer, XlsxViewer) and every headless engine (DocxDocument, PptxPresentation, XlsxWorkbook). You inject it once where you create the object — the viewer constructor or the .load() options — and every render reuses it; it is never a per-render argument. (Excel stores "Insert > Equation" as OMML inside the shared DrawingML <xdr:txBody> grammar, so XlsxViewer renders equations embedded in shapes / text boxes the same way.)

Off-main-thread rendering

By default the headless engines parse in a worker but render on the main thread. Pass mode: 'worker' to .load() to parse and render entirely inside a Web Worker — the main thread only paints the returned ImageBitmap via a bitmaprenderer context, keeping it free for scrolling and input. It requires Worker + OffscreenCanvas.

import { PptxPresentation } from '@silurus/ooxml/pptx';

// Render entirely inside a Web Worker — the main thread only paints bitmaps.
const pres = await PptxPresentation.load('/deck.pptx', { mode: 'worker' });
const canvas = document.getElementById('pptx-canvas') as HTMLCanvasElement;
const bitmap = await pres.renderSlideToBitmap(0, { width: 960, dpr: window.devicePixelRatio });
const ctx = canvas.getContext('bitmaprenderer') as ImageBitmapRenderingContext;
ctx.transferFromImageBitmap(bitmap); // consumes the bitmap

The *ToBitmap method exists on all three engines — PptxPresentation.renderSlideToBitmap(slideIndex, opts), DocxDocument.renderPageToBitmap(pageIndex, opts), and XlsxWorkbook.renderViewportToBitmap(sheetIndex, viewport, opts) (the xlsx variant requires opts.width and opts.height, since a worker has no DOM element to measure). They work in both modes — in main mode they render to an internal OffscreenCanvas — so you can write mode-agnostic code.

Notes:

  • The returned ImageBitmap is owned by the caller: transferFromImageBitmap consumes it, or call bitmap.close() when done.
  • The canvas-target methods (renderSlide(canvas), renderPage(canvas), renderViewport(canvas)) are unavailable in worker mode — use the *ToBitmap variants instead.
  • OMML equations require mode: 'main'; in worker mode they are skipped (with a console warning).
  • Trade-off: worker mode keeps the main thread responsive, but each frame is transferred back as an ImageBitmap, so a single render can be marginally slower than mode: 'main'. Choose it for non-blocking UI, not raw speed.

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["&lt;canvas&gt;"]
    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.

Key files

| 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 |


Framework Examples

// 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<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 — useTemplateRef is a 3.5+ feature -->
<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 — 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: `
    <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(); }
}

Add "allowSyntheticDefaultImports": true and configure @angular-builders/custom-webpack (or use esbuild builder) with WASM support in your Angular workspace.

<!-- Svelte 5 — runes syntax ($props, $state) -->
<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, 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 (
    <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.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<HTMLCanvasElement>();
  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 (
    <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>
  );
});

Feature Support

Word (.docx)

| 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 / distribute — CJK both/distribute spread by inter-character pitch, §17.18.44) | ✅ | | | 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 | ✅ | | | Right-to-left text — UAX#9 bidi, w:bidi / w:rtl, complex-script formatting (w:szCs / w:bCs / rFonts@cs, §17.3.2.26), RTL lists and indents | ✅ | | | Japanese kinsoku line breaking (w:kinsoku, §17.15.1.58 — 行頭/行末禁則) | ✅ | | Elements | Tables (with borders, fills, merges, banding, alignment) | ✅ | | | Table auto-layout by preferred widths (w:tblLayout autofit, §17.4.52; min content width) | ✅ | | | Right-to-left table column order (w:bidiVisual, §17.4.1) | ✅ | | | Math equations (OMML m:oMath / m:oMathPara, rendered via MathJax — opt-in @silurus/ooxml/math) | ✅ | | | Images (inline and anchored, with text wrap) | ✅ | | | SVG images (asvg:svgBlip MS-2016 extension — vector drawn from the embedded .svg, raster fallback) | ✅ | | | Text boxes / drawing shapes (wps:txbx, a:prstGeom — 186 preset geometries via the shared engine; connector arrow heads headEnd / tailEnd (§20.1.8.3) and prstDash dash patterns (§20.1.8.48)) | ✅ | | | WMF / EMF metafile images (legacy vector) | ❌ Not planned | | Advanced | Footnotes — reference markers + bottom-of-page bodies with separator rule, numbered (w:footnoteReference / w:footnoteRef, §17.11) | ✅ | | | Endnotes — reference markers + bodies at document end (w:endnoteReference, §17.11) | ✅ | | | w:snapToGrid opt-out of the document grid (§17.3.1.32) | ✅ | | | Track changes (w:ins / w:del — author-coloured underline / strikethrough) | ✅ | | | Comments — author / date / text via the document model (doc.comments, §17.13.4; not drawn on the page) | ✅ | | | Mail merge fields | ❌ Not planned | | Interaction | Text selection (transparent overlay, native copy) | ✅ |


Excel (.xlsx)

| Category | Feature | Status | |----------|---------|--------| | Workbook | Multiple sheets, sheet names | ✅ | | | Sheet tab colors (<sheetPr><tabColor> — theme / tint / indexed / rgb) | ✅ | | Cells | Text, number, boolean, error values | ✅ | | | Formula results (from cached <v>) | ✅ | | | 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 | ✅ | | | Japanese kinsoku line breaking in wrapped cells (行頭/行末禁則, shared core engine) | ✅ | | | Number formats (0.00, %, #,##0, custom date/time) | ✅ | | Structure | Merged cells | ✅ | | | Right-to-left sheets (sheetView rightToLeft, §18.3.1.87 — mirrored grid, headers, selection, scroll) | ✅ | | | Frozen panes | ✅ | | | Row / column sizing (custom widths and heights) | ✅ | | | Hidden rows / columns | ✅ | | Elements | Images (<xdr:twoCellAnchor>) | ✅ | | | SVG images (asvg:svgBlip MS-2016 extension — vector drawn from the embedded .svg, raster fallback) | ✅ | | | Drawing shapes / text boxes (xdr:sp, xdr:txBody — 186 preset geometries via the shared engine, with avLst adjust handles) | ✅ | | | Math equations in shapes (OMML m:oMath / m:oMathPara in xdr:txBody, incl. a14:m / mc:AlternateContent; rendered via MathJax — opt-in @silurus/ooxml/math) | ✅ | | | Charts (bar, line, area, pie, doughnut, radar, scatter / bubble) | ✅ | | | Chart markers (circle / square / diamond / triangle / x / plus / star / dot / dash, per-point <c:dPt> overrides) | ✅ | | | Chart data labels (<c:dLbl> per-point with CELLRANGE / VALUE / SERIESNAME / CATEGORYNAME field references, position l/r/t/b/ctr/outEnd) | ✅ | | | Chart error bars (<c:errBars> X/Y direction, cust / fixedVal / stdErr / stdDev / percentage, dashed/styled lines) | ✅ | | | Chart manual layout (<c:title><c:layout> and <c:plotArea><c:layout>) | ✅ | | | 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 | | | Cell comments / notes (classic xl/commentsN.xml + Office-365 threaded comments — red triangle indicator + author / text via the worksheet model, shown in an Excel-style hover popup) | ✅ | | | Data validation (rules via the worksheet model; list-type dropdown arrow on the selected cell whose click opens a panel showing the allowed values — read-only) | ✅ | | 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) | ✅ |


PowerPoint (.pptx)

| Category | Feature | Status | |----------|---------|--------| | Slides | Slide rendering | ✅ | | | Slide layout / master inheritance | ✅ | | | Slide size (custom dimensions) | ✅ | | | Slide background (solid, gradient, image) | ✅ | | | Slide numbers | ✅ | | | Speaker notes (plain text via getNotes()) | ✅ | | | Animations / transitions | ❌ Not planned | | Element types | Shapes (sp) | ✅ | | | Pictures (pic) | ✅ | | | SVG images (asvg:svgBlip MS-2016 extension — vector drawn from the embedded .svg, PNG fallback) | ✅ | | | 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 (renders the PowerPoint-saved drawing layout dsp:drawing; no native diagram layout engine) | ✅ | | | OLE embedded objects (p:oleObj — preview/icon rendering) | ❌ Not planned | | | Video / audio (poster + interactive playback) | ✅ | | | Ink / handwriting (p:contentPart, raster fallback) | ✅ | | Shape geometry | 186 preset shapes (prstGeom — incl. 3D presets cube / can / bevel / frame) | ✅ | | | Custom geometry (custGeom) on shapes and pictures (clipping) | ✅ | | | Rotation and flip (flipH / flipV) | ✅ | | 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 (<a:ln cmpd="dbl|thinThick|thickThin|tri"> — straight connectors) | ✅ | | | Picture border (a:ln on p:pic) — stroked along the clip silhouette | ✅ | | Shape effects | Drop shadow (outerShdw) | ✅ | | | Glow (glow — radius + colour) | ✅ | | | Inner shadow (innerShdw) | ✅ | | | Soft edge (softEdge) | ✅ | | | Reflection (reflection) | ✅ | | | 3D camera / perspective projection (scene3d camera + rot) on pictures and shapes — projected shape text is drawn but not selectable | ✅ | | | 3D contour edge (sp3d contourW / contourClr) — flat approximation | ⚠️ | | | Bevel shading (sp3d bevelT / bevelB) — distance-field lip lit by lightRig, matte/plastic materials | ✅ | | | 3D extrusion (sp3d extrusionH / extrusionClr) — swept side-wall approximation (visible only under a tilted camera) | ⚠️ | | Text — characters | Bold, italic, strikethrough (incl. dblStrike) | ✅ | | | Underline styles (sng / dbl / dotted / dash / dashLong / dotDash / dotDotDash / wavy / wavyDbl and *Heavy variants) | ✅ | | | Per-run underline colour (uFill / uFillTx) | ✅ | | | Font family, size, color | ✅ | | | East Asian font (rPr > a:ea — separate typeface for CJK glyphs) | ✅ | | | Symbol font runs (a:sym — e.g. Wingdings / Webdings glyphs) | ✅ | | | Caps transform (all / small) | ✅ | | | Letter spacing (spc) | ✅ | | | Superscript / subscript | ✅ | | | Hyperlinks (hlinkClick — theme hlink colour + auto underline) | ✅ | | | Text shadow (rPr > effectLst > outerShdw) | ✅ | | | Text outline (rPr > a:ln) | ✅ | | | Text highlight / marker (a:highlight — §21.1.2.3.4) | ✅ | | | Math equations (OMML m:oMath / m:oMathPara, incl. a14:m / mc:AlternateContent; STIX Two Math via MathJax — opt-in @silurus/ooxml/math) | ✅ | | Text — paragraphs | Horizontal alignment (left / center / right / justify) | ✅ | | | Vertical anchor (top / center / bottom) | ✅ | | | Line spacing (spcPct, spcPts) | ✅ | | | Space before / after paragraph | ✅ | | | Bullet points (character and auto-numbered) | ✅ | | | Tab stops | ✅ | | | Indent / margin | ✅ | | | Vertical text (bodyPr@vert — vert / vert270 / eaVert) | ✅ | | | Right-to-left text — UAX#9 bidi engine, pPr@rtl, RTL bullets, bodyPr@rtlCol column order, tblPr@rtl tables | ✅ | | Text — body | Text padding (insets) | ✅ | | | normAutoFit (shrink to fit) | ✅ | | | spAutoFit (expand box; suppresses wrap when text fits in one line) | ✅ | | | Word wrap / no wrap | ✅ | | | Japanese kinsoku line breaking (a:pPr@eaLnBrk, §21.1.2.2.7 — 行頭/行末禁則, shared core engine) | ✅ | | | Multi-column text body (numCol / spcCol — balanced flow) | ✅ | | | Theme object-default inheritance (<a:objectDefaults><a:txDef\|spDef> bodyPr fallback) | ✅ | | Tables | Cells, rows, columns | ✅ | | | Cell merges (horizontal / vertical) | ✅ | | | Cell borders | ✅ | | | Cell fills (solid / gradient) | ✅ | | | Cell diagonal lines (lnTlToBr / lnBlToTr) | ✅ | | | Table theme styles (74 built-in PowerPoint presets) | ✅ | | Theme | Scheme colors (dk1/lt1/accent1–6) | ✅ | | | Font scheme (+mj-lt, +mn-lt) | ✅ | | | lumMod / lumOff / alpha transforms | ✅ | | Interaction | Text selection (transparent overlay, native copy) | ✅ |


A note on text selection. Across DOCX / PPTX / XLSX, text selection is currently implemented by rendering glyphs to the canvas while overlaying a transparent DOM layer that mirrors the canvas text positions for native browser selection. This dual-layer approach is a deliberate stop-gap: once the Canvas drawElement API (proposed in WICG/html-in-canvas, currently in Chromium Origin Trial) ships across browsers, the project plans to migrate to a single DOM-as-source-of-truth pipeline where the canvas mirrors the DOM directly — eliminating the duplication while keeping z-order correctness and native selection / a11y.


Companion packages

  • packages/markdown/@silurus/ooxml-markdown and the ooxml-md CLI convert .pptx / .docx / .xlsx to GitHub-flavoured markdown via the workspace WASM parsers. Same projection used by the MCP server (~21× smaller than the raw XML on the demo deck, ~8% bigger than a flat-text extractor). Includes a node20-based GitHub Action for bulk repo-wide conversion.
  • packages/node/ — Node-side parsers (@silurus/ooxml-node) exposing parsePptx / parseDocx / parseXlsx / parseXlsxAllSheets against the workspace WASM artifacts, with no DOM or Web Worker dependency. Useful for CI checks, headless rendering pipelines, and CLI tools. Includes an ooxml-thumbnail CLI (pptx-only first pass; requires skia-canvas).
  • packages/vscode-extension/ — VS Code extension (ooxml-viewer) that registers CustomEditorProviders for .docx, .xlsx, and .pptx, and (opt-in) auto-installs and registers the ooxml-mcp-server so AI coding agents in the same window (Copilot Agent mode, Claude, …) can read those files via dedicated tools. The preview is offline by default; an opt-in ooxmlViewer.useGoogleFonts setting (off, and force-disabled in untrusted workspaces) surfaces the library's metric-compatible font substitution, widening the webview CSP to the Google Fonts CDN only while enabled.
  • packages/mcp-server/ — Rust MCP server (ooxml-mcp-server) exposing the parsers as tools for AI agents (Claude, Copilot, Codex, etc.). Provides structured queries (docx_get_structure, xlsx_get_cell_range, pptx_get_slide_structure, …) so agents can inspect OOXML files without shelling out to unzip. Prebuilt binaries are attached to each GitHub Release for macOS / Linux / Windows; the VS Code extension downloads them on demand.

Development

# Install dependencies
pnpm install

# Build all WASM parsers (requires Rust + wasm-pack)
pnpm build:wasm

# Start Storybook dev server (port 6006)
pnpm storybook

# Type-check all packages
pnpm typecheck

# Run visual regression tests (local only — not run in CI)
pnpm vrt
# Adopt the current rendering as the new reference baseline
UPDATE_REFS=1 pnpm vrt

# Build the library
pnpm build

WASM build (individual packages)

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/

Security & Privacy

  • Canvas-only rendering. Documents are decoded and drawn to an HTMLCanvasElement. No script, link, form, or other active content from the source file is executed or injected into the DOM.
  • ZIP decompression cap. Each entry in the source archive is limited to 512 MiB of uncompressed output by default to block zip-bomb DoS. Override per viewer with maxZipEntryBytes (bytes) — raise it for legitimate decks with large embedded media, lower it to tighten the budget for untrusted input:
    new PptxViewer(canvas, { maxZipEntryBytes: 64 * 1024 * 1024 }); // 64 MiB
    Supported uniformly by DocxViewer, PptxViewer, and XlsxViewer. Zero / negative values fall back to the default.
  • No network by default. The library does not send telemetry or analytics, and does not contact third-party services unless you ask it to. In particular, theme webfonts, Office font metric substitutes (Carlito/Caladea), and the script fallback fonts are not loaded from Google Fonts unless you pass useGoogleFonts: true to the relevant Viewer / load(...) options — supported uniformly by DocxViewer, PptxViewer, and XlsxViewer. When enabled, fonts for non-Latin scripts are supplied on demand from Noto families so text does not fall back to tofu: Arabic (Noto Naskh/Sans Arabic), CJK (Noto Sans/Serif KR · SC · TC · JP, picked per document language so shared Han glyphs take the right shapes), Cyrillic (Noto Sans/Serif), Hebrew (Noto Sans/Serif Hebrew, RTL), Thai (Noto Sans Thai) and Devanagari (Noto Sans Devanagari). No font binaries ship in the bundle. Enabling this option causes the end-user's browser to send an HTTP request (IP and User-Agent) to fonts.googleapis.com, which may have GDPR implications for your application — consider self-hosting the required fonts via @font-face instead.
  • XML parsing. Uses roxmltree, which does not resolve external entities (XXE-safe by default).

License

MIT