> [!NOTE]
> **This entire codebase โ Rust parsers, TypeScript renderers, tests, and tooling โ is implemented by AI coding agents, primarily [Claude](https://claude.ai) and [Codex](https://openai.com/codex/)**, through iterative prompting. No human-written application code exists in this repository.
<details>
<summary><b>Why this project exists โ a note from the author</b></summary>
<br>
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.
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.
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.
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.
</details>
<p align="center">
<img src="docs/images/icon.png" alt="office-open-xml-viewer" width="160" height="160">
</p>
# Office Open XML Viewer
[](https://www.npmjs.com/package/@silurus/ooxml)
[](https://www.npmjs.com/package/@silurus/ooxml)
[](https://marketplace.visualstudio.com/items?itemName=silurus.office-open-xml-viewer)
[](./LICENSE)
**[Live demo](https://ooxml.silurus.dev)**
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 [live framework examples](https://ooxml.silurus.dev/frameworks/) for runnable React, Vue, Svelte, and Solid projects.
Optional, experimental [legacy Office add-on](https://ooxml.silurus.dev/legacy-office/) reads a supported subset of `.doc`, `.xls` and `.ppt` with the same viewers.
## Project scope: read-only viewing
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](https://github.com/yukiyokotani/office-open-xml-viewer/issues/496) for the rationale.
| DOCX | XLSX | PPTX |
|:---:|:---:|:---:|
|  |  |  |
```bash
npm install @silurus/ooxml
# or
pnpm add @silurus/ooxml
```
> **Bundler note**: the Rust parsers ship as real `.wasm` asset files next to the
> JavaScript, referenced with the standard `new URL('โฆ', import.meta.url)` 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">` with no bundler at all. Two setups need a hand:
>
> - **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'] }` to your `vite.config` โ
> production builds are unaffected.
> - **esbuild / Angular CLI** (whose application builder is esbuild-based):
> `new URL` asset references are not processed
> ([esbuild#795](https://github.com/evanw/esbuild/issues/795)). Copy the
> `.wasm` into your served output and point the viewer at it with the
> `wasmUrl` load option. For Angular CLI, copy the required
> `*_parser_bg.wasm` asset from `node_modules/@silurus/ooxml/dist` into the
> served output and pass its public URL to the viewer.
>
> `wasmUrl` also serves the parser WASM from a CDN or any path you control:
>
> ```typescript
> new DocxViewer(canvas, { wasmUrl: 'https://cdn.example.com/docx_parser_bg.wasm' });
> ```
> **Bundle size note**: the package is ESM-only (`.mjs`). npm's *Unpacked Size*
> includes 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](https://ooxml.silurus.dev/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.
---
## Quick Start
```typescript
import { DocxViewer } from '@silurus/ooxml/docx';
import { XlsxSheetViewer, XlsxViewer, XlsxWorkbook } from '@silurus/ooxml/xlsx';
import { PptxViewer } from '@silurus/ooxml/pptx';
// DOCX โ caller provides the <canvas>
const docxCanvas = document.getElementById('docx-canvas') as HTMLCanvasElement;
const docx = new DocxViewer(docxCanvas);
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');
xlsx.setSelection('B2:D5'); // A1 strings describe geometry; the normalized upper-left is ActiveCell
// Excel keeps Selection and ActiveCell separate. Use structured state when the
// active cell or Shift-extension anchor is not the area's upper-left cell.
xlsx.setSelection({
areas: [{ kind: 'cells', top: 2, left: 2, bottom: 5, right: 4 }],
activeAreaIndex: 0,
activeCell: { row: 3, col: 3 },
extensionAnchor: { row: 2, col: 2 },
});
// Read-only, serializable context for an AI/MCP request. Populated cells are
// bounded and detached; formulas and Viewer-formatted display text are retained.
const context = xlsx.getSelectionContext({
maxCells: 1_000,
maxTextCharacters: 1_048_576,
});
// XLSX active-sheet surface only โ caller provides the <canvas>
const sheetCanvas = document.getElementById('xlsx-canvas') as HTMLCanvasElement;
const sheet = new XlsxSheetViewer(sheetCanvas);
await sheet.load('/workbook.xlsx');
await sheet.goToSheet(1);
// Share one parsed workbook across independently scrollable sheet canvases,
// including canvases created in same-origin popup windows.
const workbook = await XlsxWorkbook.load('/workbook.xlsx');
const firstCanvas = document.getElementById('xlsx-first-canvas') as HTMLCanvasElement;
const secondCanvas = document.getElementById('xlsx-second-canvas') as HTMLCanvasElement;
const firstSheet = XlsxSheetViewer.fromWorkbook(firstCanvas, workbook);
const secondSheet = XlsxSheetViewer.fromWorkbook(secondCanvas, workbook);
await Promise.all([
firstSheet.goToSheet(0),
secondSheet.goToSheet(1),
]);
// Each viewer borrows the workbook; the caller closes it after the viewers.
firstSheet.destroy();
secondSheet.destroy();
workbook.destroy();
// PPTX โ caller provides the <canvas>
const pptxCanvas = document.getElementById('pptx-canvas') as HTMLCanvasElement;
const pptx = new PptxViewer(pptxCanvas);
await pptx.load('/deck.pptx');
pptx.nextSlide();
```
`XlsxSheetViewer` also offers a deliberately small delimited-text preview convenience
that reuses the Excel-style sheet surface. It has the same source and ownership
lifecycle as XLSX loading: a string is fetched as a URL, an `ArrayBuffer` is
caller-owned, a successful reload replaces and disposes the previous source,
and `destroy()` releases the active source. There is no format auto-detection or
general-purpose tabular model, and `XlsxWorkbook` / `XlsxViewer` remain
OOXML-only. Every delimited field stays text, so leading zeroes, long identifiers,
dates, and `=...` values are displayed without type or formula inference.
This convenience covers delimiter-based text regardless of filename extension;
fixed-width PRN, DIF, and SYLK remain separate formats and are not parsed here.
```typescript
const csvFile = document.querySelector('input[type=file]') as HTMLInputElement;
const csvBytes = await (csvFile.files?.[0] as File).arrayBuffer();
await sheet.load(csvBytes, { format: 'csv' });
// String sources are URLs, just like sheet.load('/workbook.xlsx').
await sheet.load('/export.tsv', { format: 'tsv', encoding: 'windows-1252' });
// Extensions such as .txt, .dat, and .psv do not imply one delimiter.
await sheet.load('/report.dat', { format: 'delimited-text', delimiter: '|' });
```
### Rendering equations
OMML equations (`m:oMath` / `m:oMathPara`) in `.docx`, `.pptx` and `.xlsx` are rendered with
[MathJax](https://www.mathjax.org/) + [STIX Two Math](https://github.com/stipub/stixfonts).
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 asset is not fetched or evaluated (equations are simply skipped;
the on-demand render-worker asset retains a small loader). When you *do* pass it, the ~4 MB engine ships
as a **standalone asset file** next to the bundle rather than an inline data URL, and is
fetched **on demand โ only the first time a document actually contains an equation**, so
equation-free documents never pay for it. It is fully self-contained: served from your own
origin, no cross-origin requests.
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.
```typescript
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.)
### Experimental legacy DOC, XLS, and PPT sources
Legacy binary Office files can be read directly into the ordinary document,
workbook and presentation models through the experimental
`@silurus/ooxml-legacy-office` add-on. Its readers are separate opt-in entries,
one per format, passed in like the other opt-in modules (`{ legacyDoc }`,
`{ legacyXls }` or `{ legacyPpt }`). Install matching versions of both packages:
```sh
npm i @silurus/ooxml @silurus/ooxml-legacy-office
```
Each entry exports a reader for the matching loader or viewer:
```typescript
import { DocxViewer } from '@silurus/ooxml/docx';
import { legacyDoc } from '@silurus/ooxml-legacy-office/doc';
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
const viewer = new DocxViewer(canvas, { legacyDoc });
await viewer.load(docOrDocxBytes);
```
`legacyXls` (`@silurus/ooxml-legacy-office/xls`) and `legacyPpt`
(`@silurus/ooxml-legacy-office/ppt`) work the same way for XLSX and PPTX; for
custom asset URLs create the reader with `createLegacyDoc({ wasmUrl, moduleUrl })`
(likewise `createLegacyXls` and `createLegacyPpt`). A reader
claims only its own binary family; every other input takes the unchanged OOXML
path, and without a source a legacy file still rejects with the typed
`legacy-binary-format` error. Importing a reader fetches nothing: its
self-contained source module and WASM load in the parser worker (or in Node)
only when a claimed file is opened, and no OOXML package is generated. The
readers are narrow and experimental and reject unsupported content rather than
showing a partial document. They do not execute macros, fields, formulas, actions
or embedded objects. Claimed content outside the supported subset rejects with
`OoxmlError.code === 'unsupported-content'` (`details.format` names the source
format); offer conversion for that code. Resource limits, encryption and parser
crashes stay distinct from it; some native reader budgets surface as untyped
errors (see [Experimental legacy Office sources](docs/legacy-office-conversion.md)).
The package versions must match exactly; upgrade both together.
No migration is required without the add-on. See the [legacy Office guide](https://ooxml.silurus.dev/legacy-office/)
for viewer and Node examples, and [Experimental legacy Office sources](docs/legacy-office-conversion.md)
for the full support boundaries.
### Optional rendering modules
Classic DrawingML 2-D chart families are included in every format entry.
Microsoft ChartEx, model-space 3-D charts and offline country-level Region Maps
are separate entries. TIFF decoding is also a separate entry shared by DOCX,
XLSX and PPTX. Inject the modules once in the same load options object as
`math`; omitting one keeps its main-mode implementation out of the ordinary
format graph. The built-in modules work in both main and worker modes. Without `chartEx`,
ChartEx families show the standard unsupported-chart placeholder. Without
`threeD`, 3-D chart groups
fall back to their canonical 2-D family. Without `regionMap`, Region Maps show
the standard unsupported-chart placeholder. Without `tiff`, recognized TIFF
parts report a render error instead of relying on the browser decoder or being
silently omitted. The code-size boundary applies to
the default main-mode application graph. The separately loaded render-worker
asset stays self-contained for broad bundler compatibility and therefore
contains its built-in optional renderer implementations.
```typescript
import { XlsxViewer } from '@silurus/ooxml/xlsx';
import { threeD } from '@silurus/ooxml/three-d';
import { regionMap } from '@silurus/ooxml/region-map';
import { chartEx } from '@silurus/ooxml/chart-ex';
import { tiff } from '@silurus/ooxml/tiff';
const container = document.getElementById('xlsx-container') as HTMLElement;
const workbookViewer = new XlsxViewer(container, {
threeD,
regionMap,
chartEx,
tiff,
mode: 'worker',
});
await workbookViewer.load('/workbook-with-advanced-charts.xlsx');
```
The Region Map renderer is deterministic and network-free. It uses a pinned
Natural Earth country dataset, supports authored world projections and
two/three-stop value ramps, and fails closed for cached identities or
sub-country/view-specific layouts that the bounded offline model cannot yet
represent safely. The specification/Office evidence boundary for automatic
chart behavior is documented in
[Chart compatibility evidence and scope](docs/chart-compatibility-evidence.md).
The bounded TIFF 6.0 codec decodes the first IFD/page of stripped, top-left
images in the bilevel, 8-bit grayscale, RGB, RGBA and process-CMYK classes,
uncompressed or LZW-compressed (8-bit classes optionally with the horizontal
differencing predictor), plus 1-bit CCITT Group 4. Unsupported or malformed
first-page classes fail with a diagnostic render error. It is not a
general-purpose TIFF library, but as a small
by-product the same `tiff.render()` method can provide a simple preview of a
supported standalone TIFF file. Its optional `TiffRenderOptions` argument can
request a bounded display-sized bitmap; the Office viewers supply those targets
automatically for layout-backed image paints. Try Yours and the VS Code extension enable
every first-party optional module; library applications can choose only the entries they need.
See [Production decisions](https://ooxml.silurus.dev/production/) for the full
module list and [Bundle size](https://ooxml.silurus.dev/bundle-size/) for current
measurements.
### 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 normally parse **and** render inside a Web
Worker โ the main thread presents the returned `ImageBitmap` via a
`bitmaprenderer` context, keeping it free for scrolling and input. It requires
`Worker` + `OffscreenCanvas`.
```typescript
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.
- The built-in math, ChartEx, 3-D chart, and Region Map renderers work in both
modes through the same `math`, `chartEx`, `threeD`, and `regionMap` options.
Custom renderer objects are main-realm code and therefore use the feature's
documented fallback in `mode: 'worker'`.
- A DOCX document that requires browser-only OpenType vertical-glyph selection
automatically uses effective main mode for correct shaping. Read
`document.mode` after loading when your integration needs to observe this
fallback.
- 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.
### Continuous scroll viewers
`DocxScrollViewer` and `PptxScrollViewer` render the whole document as one
vertically-scrolling, PDF-reader-style surface instead of a single page/slide at
a time. Unlike `DocxViewer` / `PptxViewer` (which take a `<canvas>`), the scroll
viewers take a **container** `<div>` โ they own the scroll host, virtualize the
page/slide list (only the visible window plus a small overscan is mounted), and
recycle canvases as you scroll.
```typescript
import { DocxScrollViewer } from '@silurus/ooxml/docx';
const container = document.getElementById('docx-scroll') as HTMLElement;
const viewer = new DocxScrollViewer(container);
await viewer.load('/document.docx');
// viewer.scrollToPage(3);
// viewer.pageCount, viewer.topVisiblePage
```
```typescript
import { PptxScrollViewer } from '@silurus/ooxml/pptx';
const container = document.getElementById('pptx-scroll') as HTMLElement;
const viewer = new PptxScrollViewer(container);
await viewer.load('/deck.pptx');
// viewer.scrollToSlide(2);
// viewer.slideCount, viewer.topVisibleSlide
```
The container must have a bounded height (e.g. `height: 100vh` or a flex child)
so the viewer can size its scroll host to it. Base zoom fits the widest available
DOCX page, or the PPTX slide width, to the container and re-fits on resize. A
progressively loaded DOCX re-fits if a wider page appears; a `0`-width container
defers layout until it has width. Call `destroy()` to tear down (a self-loaded
engine is destroyed with it; a borrowed one is not โ see below).
Pass `refitOnResize: false` when the viewport must not determine the document's
physical display size. An explicit pre-load `setScale(1)` then keeps the same
authored font size at roughly the same on-screen size for portrait pages,
landscape pages, and slides; users can still zoom or call `fitWidth()` /
`fitPage()` explicitly.
**Desk appearance.** The viewer paints each page/slide on its own white canvas
with a soft drop shadow, over a transparent "desk". Style the desk and the sheet
gaps without any wrapper CSS:
```typescript
const viewer = new DocxScrollViewer(container, {
background: '#f3f4f6', // the desk behind / between pages
gap: 24, // vertical gap between pages
paddingTop: 32, // desk padding above the first page
pageShadow: '0 0 0 1px #c8ccd0', // crisp 1px "border" look (box-shadow never shifts layout)
// pageShadow: false, // flat pages, no shadow
});
```
`paddingBottom`, `paddingLeft` and `paddingRight` each default to `gap`, so the
sheet sits inside a uniform desk margin; pass `0` for a flush edge.
**Zoom.** `Ctrl`/`โ` + mouse-wheel (and trackpad pinch) zooms the surface;
bare-wheel still scrolls natively. Zoom is flicker-free โ a rapid gesture shows a
CSS preview and settles into a crisp re-render when it pauses. Bounds are the
absolute scale factors `zoomMin` / `zoomMax` (default `0.1` / `4`), and
`setScale(scale)` sets it programmatically. When fitting needs a scale below
`zoomMin`, that fitted scale becomes the effective minimum so users can zoom in
and still return to the original fit. Pass `enableZoom: false` to disable.
**Text selection and find.** Pass `enableTextSelection: true` to overlay a
transparent, selectable text layer per page/slide for native copy. It works in
both `mode: 'main'` and `mode: 'worker'`; worker rendering returns the retained
text-run geometry beside each bitmap. `findText()`, `findNext()`, `findPrev()`,
and `clearFind()` search the complete document, including virtualized pages or
slides outside the mounted window. Set `findHighlightColors: { match, active }`
on any viewer to override the two overlay backgrounds with CSS colors; use an
alpha color when the canvas text should remain visible through the highlight.
**Selection context for AI/MCP.** Every Viewer exposes one read-only query,
`getSelectionContext()`, for handing the user's current focus to an external
assistant. The result is a detached, JSON-serializable snapshot discriminated by
`format` and `kind`; it never exposes a mutable document model or sends data over
the network. Text, run locators, and populated XLSX cells have hard resource
limits. Check `truncated` / `truncationReasons` before building a prompt.
```typescript
const docx = new DocxViewer(docxCanvas, {
enableTextSelection: true,
enableElementSelection: true,
onSelectionContextChange(context) {
// kind === 'text' or 'element': selected text or a clicked drawing
updateAskAiButton(context);
},
});
const pptx = new PptxViewer(pptxCanvas, {
enableTextSelection: true,
enableElementSelection: true,
onSelectionContextChange(context) {
// Text selection wins while it exists; otherwise a slide-element click
// yields kind === 'element' with compact bounds, provenance and content.
updateAskAiButton(context);
},
});
const spreadsheet = new XlsxViewer(container, {
enableElementSelection: true,
onSelectionContextChange(context) {
updateAskAiButton(context);
},
onContextMenu: async ({ originalEvent, getContext }) => {
// Browser-menu control must happen synchronously, before the first await.
originalEvent.preventDefault();
const { clientX, clientY } = originalEvent;
const context = await getContext();
openContextMenu({ clientX, clientY, context });
},
});
const spreadsheetContext = spreadsheet.getSelectionContext({ maxCells: 1_000 });
// kind === 'range': selection state, values and formulas.
// kind === 'element': a clicked chart, picture or shape.
```
All three formats expose the same `onSelectionContextChange(context)` handoff;
callers may instead query on demand. `enableElementSelection` is an independent,
explicit opt-in because it enables object selection and draws a non-editable outline
around the focused object; adding the callback alone never enables object
hit-testing. XLSX separately retains
`onSelectionStateChange` for canonical UI state such as ActiveCell and multiple
areas. Its context callback is frame-coalesced so drag selection does not build
one snapshot per pointer event.
`onContextMenu` is also common to every Viewer. It receives the real browser
event synchronously so the host can call `preventDefault()`, plus a clearly
asynchronous `getContext()` lookup for the right-click target. The lookup starts
on the first call and is memoized. Omitting the callback installs no listener and leaves
the native browser menu unchanged.
`DocxDocument.getElementContextAt()` and
`PptxPresentation.getElementContextAt()` provide the identical compact element
query for custom/headless page or slide surfaces in both modes. PPTX
element provenance is limited to `master | layout | slide`; editor tree indexes,
archive paths, save/round-trip handles, and mutation APIs are deliberately absent.
When switching on `kind`, retain a default branch so a future read-only focus kind
can be added without changing the transport envelope.
See the [selection-context guide](docs/selection-context.md) for the complete
contract, resource bounds, PPTX hit-testing semantics, and extension policy.
**Hyperlinks.** For DOCX/PPTX the link hit regions live on the text-selection
overlay, so hyperlink interaction requires `enableTextSelection: true`; when that
overlay is enabled, links are interactive by default. XLSX hit-tests cells
directly, so links are interactive out of the box. An external link opens in a
new tab (scheme-sanitized to `http` / `https` / `mailto` / `tel`, `noopener`),
and an internal target navigates within the document (docx bookmark, pptx slide
jump, xlsx defined name or cell reference). XLSX references may switch sheets
and then scroll the destination cell into view; a range navigates to its first
cell. Pass `onHyperlinkClick(target)` to take over the click yourself.
Pass `enableHyperlinks: false` to disable hyperlink interactivity entirely โ no
hit-testing, no pointer cursor over links, no default navigation, and
`onHyperlinkClick` is never called; links still render as authored but are inert.
This applies to every viewer that supports hyperlinks (`DocxViewer`,
`DocxScrollViewer`, `PptxViewer`, `PptxScrollViewer`, `XlsxViewer`,
`XlsxSheetViewer`).
**Choose one loading mode.** For the normal one-view case, construct a Viewer and
call `viewer.load(source)`: the Viewer owns the parsed engine, may replace it on a
later load, and destroys it during teardown. For masterโdetail, multi-pane, or
multi-window UIs, create the Viewer from an already-loaded headless engine so
every view shares **one** parse. The two modes are mutually exclusive: on a
Viewer returned by a `from*()` factory, `load()` is unsupported, the engine's
own `mode` wins, and `destroy()` leaves the borrowed engine intact โ the caller
owns its lifecycle.
| Use case | Acquisition | Engine owner |
| --- | --- | --- |
| One Viewer | `new Viewer(target)` then `viewer.load(source)` | Viewer |
| Shared parse | `Engine.load(source)` then `Viewer.fromDocument / fromPresentation / fromWorkbook(...)` | Caller |
```typescript
import { DocxDocument, DocxScrollViewer, DocxViewer } from '@silurus/ooxml/docx';
const document = await DocxDocument.load('/document.docx'); // parse once
const scroll = DocxScrollViewer.fromDocument(container, document);
const page = DocxViewer.fromDocument(canvas, document);
await page.goToPage(0);
// ...also drive a thumbnail grid or more panes from the same document.
page.destroy();
scroll.destroy(); // the borrowed document is NOT destroyed โ you own it
document.destroy(); // release it yourself when every pane is gone
```
`PptxViewer` and `PptxScrollViewer` use `fromPresentation(...)`; `XlsxViewer`
and `XlsxSheetViewer` use `fromWorkbook(...)`.
### Progressive DOCX layout
For a large DOCX file, set `progressiveLayout: true` to resolve `load()` when the
opening pages are paintable while the same paginator continues in the background.
It is available on `DocxViewer`, `DocxScrollViewer`, and `DocxDocument.load()` in
both render modes. Pair it with `mode: 'worker'` when the remaining layout and
paint work should also stay off the UI thread.
```typescript
import { DocxScrollViewer } from '@silurus/ooxml/docx';
const container = document.querySelector('#document') as HTMLElement;
const pager = document.querySelector('#pager') as HTMLElement;
const viewer = new DocxScrollViewer(container, {
progressiveLayout: true,
mode: 'worker',
onVisiblePageChange(pageIndex, availablePages, layoutComplete) {
pager.textContent =
`Page ${pageIndex + 1} of ${availablePages}${layoutComplete ? '' : 'โฆ'}`;
pager.setAttribute('aria-busy', String(!layoutComplete));
},
});
await viewer.load('/document.docx');
// Print, export, and final-page-count UI need the converged layout.
await viewer.waitUntilLayoutComplete();
console.log('Final page count:', viewer.pageCount);
```
While `layoutComplete` is false, `pageCount` and the callback's `total` argument
mean pages available so far, not the final total. The page callbacks fire again
when that count grows, even if the visible page does not change. Await
`waitUntilLayoutComplete()` before printing, exporting, or snapshotting a final count.
In-document `NUMPAGES` fields are repainted with their authoritative value after
pagination converges. See the [progressive layout guide](https://ooxml.silurus.dev/docx#progressive-layout)
and [DOCX API reference](https://ooxml.silurus.dev/api/docx).
### Progressive PPTX layout
PPTX exposes the same progressive lifecycle on `PptxViewer`,
`PptxScrollViewer`, and `PptxPresentation.load()`:
```typescript
import { PptxScrollViewer } from '@silurus/ooxml/pptx';
const viewer = new PptxScrollViewer(container, {
progressiveLayout: true,
mode: 'worker',
onVisibleSlideChange(slideIndex, slideCount, layoutComplete) {
pager.textContent = `Slide ${slideIndex + 1} of ${slideCount}`;
pager.setAttribute('aria-busy', String(!layoutComplete));
},
});
await viewer.load('/presentation.pptx');
await viewer.waitUntilLayoutComplete();
```
Unlike DOCX pagination, a PPTX bootstrap already provides the final slide list
and uniform dimensions. `slideCount` and the ScrollViewer's scroll extent are
therefore stable from first paint; `availableSlideCount` grows as the paintable
opening prefix is prepared. Scrolling ahead shows a loading state without
changing the scrollbar length. See the [PPTX progressive layout guide](https://ooxml.silurus.dev/pptx#progressive-layout)
and [PPTX API reference](https://ooxml.silurus.dev/api/pptx).
For presentations, `enableMediaPlayback: true` makes embedded audio and video
interactive inside the real viewport plus `mediaOverscan` slides. Other mounted
slides remain static and selectable, avoiding offscreen media blobs and
animation loops.
Both viewers also expose `relayout()` (force a re-fit when the container resizes
in a way a `ResizeObserver` cannot see โ e.g. a late web-font load),
`onVisiblePageChange` (fires when the top-most visible page, provisional DOCX
page count, or completion state changes), `onVisibleSlideChange` (fires when the
top-most visible slide or PPTX completion state changes; its slide count is
final from first paint), and `onError` (async per-page render failures are routed
here instead of crashing the scroll loop). The parse/render knobs from the
headless engines (`mode`, `useGoogleFonts`, `cjkFallback`, `resourceLimits`, the deprecated
`maxZipEntryBytes` alias, `math`, `dpr`) are accepted too.
### CJK fallback region
All document engines and viewers accept `cjkFallback: 'auto' | 'sc' | 'tc' | 'hk' | 'jp' | 'kr'`.
The same Unicode Han character can have different regional glyph shapes. This
option chooses which regional fallback family is tried first when Han text
reaches font fallback and the document has not already identified a region. It
does not replace the document's requested font.
```ts
const viewer = new DocxViewer(canvas, { cjkFallback: 'sc' });
```
Omitting the option is equivalent to `'auto'`: at load time, resolve the first
usable CJK language from `<html lang>`, then `navigator.languages` in order,
then `navigator.language`. If none is available, use `'jp'`. For Chinese,
explicit Hans/Hant maps to SC/TC and takes precedence over region; without an
explicit script, HK/MO maps to HK and TW maps to TC. Bare `zh` maps to SC.
Japanese and Korean map to JP and KR.
The resolved preference is shared with workers and remains fixed for that load.
The requested font remains first. Recognized regional CJK font names, an East
Asian run language where the format provides one, and unambiguous Kana or
Hangul can determine the region before `cjkFallback` is consulted. Text without
Han keeps its existing font route. For example, `cjkFallback: 'sc'` selects SC
for otherwise unqualified Han text, but a Meiryo run or text containing Kana
continues to select JP.
No migration is required. Set an explicit region when the fallback choice must
not depend on the browser locale, server, or Node host. This makes the regional
choice deterministic; it does not by itself guarantee pixel-identical output
across machines because the available fonts still matter. `cjkFallback` does
not download fonts. Use `useGoogleFonts: true` or provide local/self-hosted
fallback fonts when the same fallback faces must be available everywhere.
HK retains the existing sans-only webfont support. This option covers document
text, spreadsheet cells/shapes, and slide text; embedded chart and equation
renderers retain their own font policies. XLSX automatic script inference uses
the shared-string table; inline cell strings and shape text do not contribute
to that workbook-level inference.
### Markdown export
Every headless engine can produce a best-effort, text-focused GitHub-flavoured
markdown projection for LLM ingestion, full-text search, or diffing. Explicit
headings, lists, and tables are preserved where available, but visual layout,
fonts, positioning, and inferred relationships between shapes are intentionally
dropped. Treat the result as full-text extraction, not an authoritative semantic
or reading-order representation.
Review comments are kept out of the document body and collected in a final
`## Review comments` appendix. Comment text is quoted, replies use nested quotes,
and only reliable locations (such as a slide number or worksheet cell) are
reported. Speaker notes remain separate from review comments.
The projection is compiled into each format's existing parser WASM; there is no
separate markdown WASM to load. `toMarkdown()` works in both `mode: 'main'` and
`mode: 'worker'` (it runs off the archive opened at `load()`):
```typescript
import { DocxDocument } from '@silurus/ooxml/docx';
const doc = await DocxDocument.load('/document.docx');
const md = await doc.toMarkdown();
```
`PptxPresentation.toMarkdown()` (title slides โ `#` headings, body โ nested
bullets, speaker notes kept with their slide) and `XlsxWorkbook.toMarkdown()`
(each sheet โ a `## SheetName` pipe table) are the twins.
The repository also contains a low-level adapter and CLI for workspace tooling.
They are internal implementation utilities, not separately published packages;
installed applications should use the format model's `toMarkdown()` method.
---
<details>
<summary><strong>Architecture diagram</strong></summary>
```mermaid
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)"]
VIEWER["DOCX / XLSX / PPTX Viewer"] --> ENGINE["format headless engine"]
ENGINE -->|mode: main| PARSE["parser worker\nใWASM parseใ"]
PARSE --> MODEL["validated document model"]
MODEL --> MAIN["layout + Canvas paint\nใmain threadใ"]
ENGINE -->|mode: worker| RENDER_WORKER["render-worker.ts\nใWASM parse + layout + paintใ"]
RENDER_WORKER --> BITMAP["ImageBitmap"]
MAIN --> CANVAS["<canvas>"]
BITMAP --> CANVAS
CORE["@silurus/ooxml-core\nshared layout and paint primitives"]
MAIN -. uses .-> CORE
RENDER_WORKER -. uses .-> CORE
end
docx_wasm --> PARSE
xlsx_wasm --> PARSE
pptx_wasm --> PARSE
docx_wasm --> RENDER_WORKER
xlsx_wasm --> RENDER_WORKER
pptx_wasm --> RENDER_WORKER
```
All three formats use the same public mode boundary. In the default `main` mode,
WASM parsing runs in a Worker and the validated model is laid out and painted on
the main thread. In `worker` mode, parsing, layout and Canvas paint run in a
render Worker, which returns an `ImageBitmap` for presentation. Built-in optional
renderers use the same injection options in both modes. `@silurus/ooxml-core`
owns the layout, paint and type primitives shared by DOCX, XLSX and PPTX.
### 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` | DOCX Canvas 2D rendering and text layout |
| `packages/xlsx/src/renderer.ts` | XLSX Canvas 2D viewport rendering |
| `packages/pptx/src/renderer.ts` | PPTX Canvas 2D rendering |
| `packages/*/src/worker.ts` | Main-mode parser Worker entry |
| `packages/*/src/render-worker.ts` | Worker-mode parser, layout and Canvas paint entry |
| `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 |
</details>
---
## Framework Examples
Runnable TypeScript projects are available for
[React](https://ooxml.silurus.dev/frameworks/react/),
[Vue](https://ooxml.silurus.dev/frameworks/vue/),
[Svelte](https://ooxml.silurus.dev/frameworks/svelte/), and
[Solid](https://ooxml.silurus.dev/frameworks/solid/). Each guide embeds the
complete StackBlitz project and supports selecting a local DOCX, XLSX, or PPTX
file without uploading it.
---
## 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) | โ
|
| | Page borders (`w:pgBorders`, ยง17.6.10 โ standard line styles, offsetFrom / display / zOrder; art borders not yet supported) | โ
|
| | Line numbering (`w:lnNumType`, ยง17.6.8) | โ
|
| | Section vertical alignment (`w:vAlign`, ยง17.6.22) | โ
|
| **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) | โ
|
| | Document grid (`w:docGrid`, ยง17.6.5 โ line pitch + East Asian character grid / ๅญ่ฉฐใ) | โ
|
| | Margin collapsing between paragraphs | โ
|
| | Indents and tab stops | โ
|
| | Multi-column section layout (`w:cols`, ยง17.6.4 โ newspaper-flow columns; full-width floats span all columns) | โ
|
| | Lists (bullet and numbered, multi-level `%N` markers ยง17.9.11) | โ
|
| | 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 โ ่ก้ ญ/่กๆซ็ฆๅ) | โ
|
| | Vertical writing (็ธฆๆธใ โ UAX#50 vertical glyph forms, ็ธฆไธญๆจช tate-chu-yoko runs, ใใ upper-right positioning; ยง17.3.2 vertical text) | โ
|
| **Elements** | Tables (with borders, fills, merges, banding, alignment) | โ
|
| | Table auto-layout by preferred widths (`w:tblLayout` autofit, ยง17.4.52; min content width) | โ
|
| | Table indent (`w:tblInd`, ยง17.4.50) | โ
|
| | Right-to-left table column order (`w:bidiVisual`, ยง17.4.1) | โ
|
| | Charts (embedded DrawingML `c:chart` โ bar / line / area / pie / doughnut / radar / scatter, via the shared core chart renderer; data labels honour `dLblPos`, ยง21.2.2.48) | โ
|
| | ChartEx (waterfall / histogram / Pareto / funnel / box & whisker / treemap / sunburst) | โ
opt-in |
| | Math equations (OMML `m:oMath` / `m:oMathPara`, rendered via MathJax โ opt-in `@silurus/ooxml/math`) | โ
|
| | Images (inline and anchored, with text wrap and adaptive display-sized decoding for oversized rasters) | โ
|
| | TIFF images (opt-in `@silurus/ooxml/tiff`; bounded bilevel, grayscale, RGB(A) and process-CMYK, uncompressed or LZW, plus CCITT Group 4) | โ
|
| | SVG images (`asvg:svgBlip` MS-2016 extension โ vector drawn from the embedded `.svg`, raster fallback) | โ
|
| | Text boxes / drawing shapes (inline and anchored `wps:wsp` / `wps:txbx`, including solid, gradient, and image fills; `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)). Text-box paragraphs run through the **same line-layout engine as body text**, so kinsoku ่ก้ ญ/่กๆซ็ฆๅ (ยง17.15.1.58โ60), UAX#9 bidi (`w:bidi`, ยง17.3.1.6), justification (ยง17.18.44) and tab stops (ยง17.3.1.37) all apply inside a box | โ
|
| | WMF **and EMF** metafile images (legacy vector, incl. inside text boxes) โ rasterized via a built-in player: windowโviewport mapping (MS-EMF map modes, world transform), pens/brushes, poly/rect/ellipse, text-out, path clipping, and embedded DIB blits | โ
|
| | Legacy VML content โ positioned shapes, text boxes, image previews, and authored text wrapping | โ
|
| | OLE embedded objects (`w:object` โ the baked VML `v:imagedata` preview is drawn; the embedded app is not run) | โ
|
| **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) | โ
|
| | Page-number formats (`w:pgNumType` restart / format ยง17.6.12; PAGE `\*` switches โ decimal / roman / letter / hex / ordinal-dash / hebrew2 / koreanLegal, ยง17.18.59) | โ
|
| | Field date/time pictures (`TIME` / `DATE` field `\@` format, ยง17.16.5.72 / .16) | โ
|
| | `w:snapToGrid` opt-out of the document grid (ยง17.3.1.32) | โ
|
| | Track changes (ยง17.13.5 `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo`) โ the default render is the FINAL state (deletions and moved-away text hidden); the opt-in markup view (`showTrackedChanges`) draws author-coloured underline / strikethrough plus margin change bars, and body-story revision records are available as data | โ
|
| | Comments (ยง17.13.4) โ opt-in margin balloons (`comments: true`): commented ranges tinted, threaded replies via `commentsExtended.xml`, resolved threads hidden, click-to-select stacking; also available as data (`doc.comments`, `doc.commentAnchorRanges()`) | โ
|
| | Markdown export (`DocxDocument.toMarkdown()` โ headings, lists, tables, footnotes / comments) | โ
|
| | Mail merge fields | โ Not planned |
| **Interaction** | Text selection, including table-cell text (transparent overlay, native copy) | โ
|
| | Bounded read-only text/element context (`getSelectionContext()`, page/source locators, element selection, AI/MCP callback) | โ
|
| | In-document find (`findText` / `findNext` / `findPrev` / `clearFind` โ full-text search, all hits highlighted, each match tagged with its page) | โ
|
| | Runtime zoom (`getScale` / `setScale` / `fitWidth` / `fitPage`) | โ
|
| | Clickable hyperlinks (overlay hit-test, `onHyperlinkClick`; internal bookmark / anchor navigation) | โ
|
| | Continuous scroll viewer (`DocxScrollViewer` โ virtualized page list, desk background / shadow, Ctrl/โ+wheel zoom, borrowed-engine factory) | โ
|
| **Loading** | Password-protected files ([MS-OFFCRYPTO] Agile Encryption โ `load(bytes, { password })`, decrypted client-side via WebCrypto; legacy Standard / Extensible encryption โ typed `unsupported-encryption`) | โ
|
---
### 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 (cached `<v>` only; formulas, including `TODAY()` / `NOW()`, are never recalculated) | โ
|
| | Dates (ECMA-376 date format codes) | โ
|
| | Rich text (per-run formatting) | โ
|
| | East-Asian furigana (`<rPh>` ยง18.4.6 + `<phoneticPr>` ยง18.4.3 โ drawn when a cell opts in via `ph="1"`; row-level `<row ph>` inheritance) | โ
|
| **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 | โ
|
| | Row / column outline grouping (`outlineLevel` / `collapsed` ยง18.3.1.73 / .13, `<outlinePr>` โ gutter brackets, +/โ collapse, numbered level buttons; view-only) | โ
|
| **Elements** | Images (`<xdr:twoCellAnchor>`, with adaptive display-sized decoding for oversized rasters) | โ
|
| | TIFF images (opt-in `@silurus/ooxml/tiff`; bounded bilevel, grayscale, RGB(A) and process-CMYK, uncompressed or LZW, plus CCITT Group 4) | โ
|
| | OLE embedded objects (`<oleObjects>` โ the legacy VML `v:imagedata` preview keyed by `oleObject@shapeId` is drawn; an image-typed `objectPr` target is preferred when present, and the embedded app is not run) | โ
|
| | 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) | โ
|
| | ChartEx (waterfall / histogram / Pareto / funnel / box & whisker / treemap / sunburst) | โ
opt-in |
| | Chart markers (circle / square / diamond / triangle / x / plus / star / dot / dash, per-point `<c:dPt>` overrides; markers-only scatter series draw a marker legend key) | โ
|
| | 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`) | โ
|
| | Conditional-formatting formulas (`expression`, text / blanks / errors rules โ partial evaluator over cached values; complete evaluation tracked in [#1547](https://github.com/yukiyokotani/office-open-xml-viewer/issues/1547)). A rule the evaluator cannot evaluate neither formats nor stops lower rules | โ ๏ธ Partial |
| | Slicers (static, Office 2010 extension) | โ
|
| | Pivot tables (saved worksheet output renders unchanged; read-only metadata is exposed. Refresh, recalculation, filtering, restructuring, and interactivity are unsupported) | โ ๏ธ Partial |
| | Cell comments / notes (classic `xl/commentsN.xml` + Office-365 threaded comments โ red triangle indicator + author / text via the worksheet model; pointer or keyboard users can open the popup, with a polite screen-reader status) | โ
|
| | 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) | โ
|
| | Markdown export (`XlsxWorkbook.toMarkdown()` โ each sheet as a `## SheetName` pipe table) | โ
|
| **Interaction** | Cell selection (single / range / row / column / all / multiple areas; `setSelection('B2:D5')` or canonical structured state) | โ
|
| | Excel-style row / column header highlight on selection | โ
|
| | Shift+click to extend, Ctrl/โ+drag to add another area, Ctrl+C to copy as TSV | โ
|
| | Text selection inside cells (transparent overlay) | โ
|
| | `onSelectionStateChange`, bounded range/element `getSelectionContext()` / `copySelection()`, chart/picture/shape selection, `getCellAt(x, y)` | โ
|
| | Zoom slider (Excel-style, right of the tab bar, 10โ400% with 100% centered; `showZoomSlider` option) | โ
|
| | Ctrl/โ + mouse-wheel and trackpad-pinch zoom (in addition to the slider) | โ
|
| | Runtime fit / zoom API (`fitWidth` / `fitPage` / `getScale` / `setScale`, in addition to the slider) | โ
|
| | In-document find (`findText` / `findNext` / `findPrev` / `clearFind` โ matches tagged with sheet + cell) | โ
|
| | Clickable hyperlinks (`onHyperlinkClick`; internal defined-name / sheet-and-cell navigation, ranges use the first cell) | โ
|
| | Drag-to-resize columns / rows by dragging header borders (`resizable` option, default on) โ **view-only: changes the on-screen view only and never modifies the loaded file** | โ
|
| | Customizable cell-selection color (`selectionColor` option, `setSelectionColor()`) | โ
|
| **Loading** | Password-protected files ([MS-OFFCRYPTO] Agile Encryption โ `load(bytes, { password })`, decrypted client-side via WebCrypto; legacy Standard / Extensible encryption โ typed `unsupported-encryption`) | โ
|
---
### 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()`) | โ
|
| | Markdown export (`PptxPresentation.toMarkdown()` โ title slides โ headings, body โ nested bullets, notes / comments collated) | โ
|
| | Animations / transitions | โ Not planned |
| **Element types** | Shapes (`sp`) | โ
|
| | Pictures (`pic`, with adaptive display-sized decoding for oversized rasters) | โ
|
| | TIFF images (opt-in `@silurus/ooxml/tiff`; bounded bilevel, grayscale, RGB(A) and process-CMYK, uncompressed or LZW, plus CCITT Group 4) | โ
|
| | 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) | โ
|
| | Charts (pie, doughnut) | โ
|
| | Charts (scatter โ `scatterStyle` marker / line / smooth variants) | โ
|
| | Charts (bubble โ `bubbleSize` per-point area scaling) | โ
|
| | Charts (ordered classic combo groups โ observed bar/line/area, scatter/bubble, and stock/line combinations; unsupported mixes fail closed) | โ
|
| | ChartEx (waterfall / histogram / Pareto / funnel / box & whisker / treemap / sunburst) | โ
opt-in |
| | Charts (stock โ high / low / close candlesticks) | โ
|
| | SmartArt (renders the PowerPoint-saved drawing layout `dsp:drawing`, or a staged fallback to a text list when no drawing part is present; no native diagram layout engine) | โ
|
| | OLE embedded objects (`p:oleObj` โ the baked preview `p:pic` is drawn; the embedded app is not run) | โ
|
| | 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`) | โ
|
| | WordArt text warps (`a:prstTxWarp`, ยง20.1.9.19 โ all 40 presets, per-glyph envelope fit incl. Follow Path) | โ
|
| | 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, auto-numbered, and picture `a:buBlip` ยง21.1.2.4.2) | โ
|
| | 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 defaults (`<a:objectDefaults>`) treated as new-object templates only, as PowerPoint does | โ
|
| **Tables** | Cells, rows, columns | โ
|
| | Table background (`a:tblBg`, beneath cell fills; optional `TableElement.background: Fill` in parsed models) | โ
|
| | 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, including table-cell text (transparent overlay, native copy) | โ
|
| | Bounded text/element selection context (`getSelectionContext()`, element selection, master/layout/slide provenance, main + worker) | โ
|
| | Comments โ opt-in slide-side cards (`comments: true`), authored target markers and highlights, replies and resolved state; also available per slide through the presentation model | โ
|
| | In-document find (`findText` / `findNext` / `findPrev` / `clearFind` โ matches tagged with slide) | โ
|
| | Runtime zoom (`getScale` / `setScale` / `fitWidth` / `fitPage`) | โ
|
| | Clickable hyperlinks (`onHyperlinkClick`; internal slide-jump navigation) | โ
|
| | Continuous scroll viewer (`PptxScrollViewer` โ virtualized slide list, desk background / shadow, Ctrl/โ+wheel zoom, borrowed-engine factory) | โ
|
| **Loading** | Password-protected files ([MS-OFFCRYPTO] Agile Encryption โ `load(bytes, { password })`, decrypted client-side via WebCrypto; legacy Standard / Extensible encryption โ typed `unsupported-encryption`) | โ
|
---
> **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](https://chromestatus.com/feature/6051647656558592) (proposed in [WICG/html-in-canvas](https://github.com/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/`](packages/markdown/)** โ internal workspace adapter and `ooxml-md` development CLI for the same GitHub-flavoured Markdown projection exposed by each format model's `toMarkdown()` method.
- **[`packages/node/`](packages/node/)** โ the implementation behind the public Node-only `@silurus/ooxml/node` subpath. Its canonical APIs are the explicitly owned, bounded `openPptxPresentation`, `openDocxDocument`, and `openXlsxWorkbook` sessions. Async `materializePptxPresentation`, `materializeDocxDocument`, `materializeXlsxWorkbookIndex`, `materializeXlsxWorksheet`, and `materializeXlsxWorkbook` are provided when a complete caller-owned graph is actually needed. Each `open*` call returns an explicit, idempotent `close()`-able session; PPTX streams `slides()`, DOCX completes format-required sequential pagination before streaming `pages()`, and XLSX parses its workbook index once before sequential `worksheetRows(sheetIndex)` streams reuse the retained archive. Useful for CI checks and headless rendering pipelines; canvas rendering accepts a user-supplied backend such as `skia-canvas` without making it a runtime dependency.
See the [0.75 to 0.76 migration guide](docs/migration-0.76.md) for every removed synchronous helper and its replacement.
- **[`packages/vscode-extension/`](packages/vscode-extension/)** โ VS Code extension (`ooxml-viewer`) that registers `CustomEditorProvider`s for `.docx`, `.xlsx`, and `.pptx`, and (opt-in) auto-installs and registers the `ooxml-mcp-server` for GitHub Copilot Chat in Agent mode, including active Viewer selection. Claude Code and Codex can configure the same binary separately for path-based file tools, but do not receive the active selection bridge. The preview is offline by default; an opt-in `ooxmlViewer.useGoogleFonts` setting (off, and force-disabled in untrusted workspaces) loads optional webfont substitutes and script fallbacks, widening the webview CSP to the Google Fonts CDN only while enabled.
- **[`packages/mcp-server/`](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](https://github.com/yukiyokotani/office-open-xml-viewer/releases) for macOS / Linux / Windows; the VS Code extension downloads them on demand.
---
## Development
```bash
# 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
# Visual regression tests (local only โ not run in CI). `pnpm vrt` compares
# against a baseline checkout captured with `pnpm vrt:snapshot`; see AGENTS.md
# for the required VRT_BASELINE_REVISION / VRT_BASELINE_CHECKOUT variables.
pnpm vrt
# Fidelity ratchet against the committed demo references
pnpm vrt:fidelity
# Build the library
pnpm build
```
### WASM build (individual packages)
```bash
pnpm --filter @silurus/ooxml-docx wasm
pnpm --filter @silurus/ooxml-xlsx wasm
pnpm --filter @silurus/ooxml-pptx wasm
```
## Error handling
Headless APIs (`DocxDocument`, `XlsxWorkbook`, and `PptxPresentation`) and
Viewer APIs report failures from awaitable operations by rejecting the returned
Promise. This includes `viewer.load()` parsing and its initial render, whether
or not the Viewer has an `onError(error)` callback. A failure is never delivered
through both channels.
All three formats use a module Web Worker for parsing, including the default
`mode: 'main'`; the mode selects where rendering runs, not whether parsing uses
a Worker. The supported browser setup requires a page with a normal,
non-opaque origin where Worker loading is permitted. An iframe with
`sandbox="allow-scripts"` but no `allow-same-origin` has an opaque origin. Worker
loading may fail there depending on the browser and Worker script, so this
embedding setup is not supported even if it happens to work in one browser.
There is no classic-Worker or no-Worker fallback for this configuration.
For a regular iframe, omit `sandbox`. To retain sandbox restrictions, serve
the viewer page from a dedicated origin distinct from the parent and use, for
example,
`<iframe sandbox="allow-scripts allow-same-origin" src="https://viewer.example.net/viewer.html"></iframe>`.
Here `allow-same-origin` retains the **iframe page's** origin; it does not give
the iframe the parent's origin. This separate-origin setup requires a `src`
URL; `srcdoc` inherits the parent's origin when origin sandboxing is disabled.
If module scripts or other resources are fetched across origins, configure
CORS as required; the page's CSP must also allow its module Worker and required
assets. The Worker error message includes an opaque-origin hint when the
browser gives no detail, but it cannot identify every Worker failure.
Use `onError` for later Viewer-managed work that has no directly awaitable
result, such as virtualized scroll-view rendering or embedded-media playback.
Those failures are logged with `console.error` when the callback is omitted.
`PptxPresentation.presentSlide()` follows the same boundary: initialization
rejects its Promise, while `PresentSlideOptions.onError` observes only media
decode or playback failures after the presentation handle has been returned.
Stable failures can be narrowed without parsing message strings:
- `OoxmlError` โ typed input and source failures. Its `code` is `encrypted`,
`invalid-password`, `unsupported-encryption`, `legacy-binary-format`,
`not-ooxml`, `unsupported-image-effect` (a picture-effect chain that is
malformed or cannot be drawn), `invalid-numbering` (DOCX numbering that Word
cannot open), or `unsupported-content`. For `unsupported-content`, offer conversion: the configured
source cannot represent this input. Frozen `details` contain only its
`format` identifier and `reason: 'source-subset'`.
- `OoxmlResourceLimitError` (`code === 'ooxml-resource-limit'`) โ a measured
package or format resource crossed a configurable limit or hard ceiling.
`details.violation` contains the resource, metric, limit, observed value, and
usage snapshot.
- `OoxmlDecodedImageLimitError`
(`code === 'ooxml-decoded-image-limit'`) โ a raster crossed an image pixel or
active decoded-byte ceiling. Its `metric`, `limit`, and `observed` properties
are stable.
- `TiffDecodeError` (`code === 'ooxml-tiff-decode'`) โ a recognized TIFF part
is malformed, uses a class the configured codec does not support, or fails
during bitmap handoff. Its message is diagnostic rather than a stable subtype.
Omitting the optional TIFF codec is not an error; the affected image is shown
as an unavailable-image placeholder while the rest of the document renders.
- An otherwise ordinary `Error` may carry `code === 'parser-crashed'` for a
recognized WASM trap. This does not mean โOOMโ: panic, allocation failure,
stack overflow, and other traps can be indistinguishable at the current WASM
boundary.
All other configuration, fetch, parser, renderer, worker, and media failures
remain `Error`, `TypeError`, or `RangeError` values without a stable code. Their
messages are diagnostic text, not a programmatic API.
```ts
import {
DocxViewer,
OoxmlDecodedImageLimitError,
OoxmlError,
OoxmlResourceLimitError,
TiffDecodeError,
} from '@silurus/ooxml/docx';
const viewer = new DocxViewer(canvas, {
// Background failures after an awaited operation has completed.
onError(error) {
if (error instanceof OoxmlResourceLimitError) {
const { limit, observed } = error.details.violation;
showTooLargeMessage({ limit, observed });
} else if (error instanceof OoxmlDecodedImageLimitError) {
showImageTooLargeMessage(error);
} else if (error instanceof TiffDecodeError) {
showUnsupportedImageMessage();
} else if (error instanceof OoxmlError) {
handleContainerError(error.code);
} else {
reportUnexpectedError(error);
}
},
});
try {
await viewer.load(file);
} catch (error) {
reportUnexpectedError(error);
}
```
## 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.
- **Bounded OOXML package expansion.** DOCX, XLSX, and PPTX use the same resource policy. By default, one archive entry may inflate to at most 128 MiB, the distinct entries visited during one package session may inflate to at most 256 MiB, and an archive may contain at most 4,096 entries. Override these budgets with a plain `resourceLimits` object on a viewer or `load(...)` call:
```ts
new XlsxViewer(container, {
resourceLimits: {
maxArchiveEntryBytes: 64 * 1024 * 1024,
maxTotalInflatedBytes: 192 * 1024 * 1024,
maxArchiveEntries: 2048,
},
});
```
`maxArchiveEntryBytes` applies to every XML, text, image, media, and other package part that the parser reads. `maxTotalInflatedBytes` counts the largest amount actually read from each distinct part during the lifetime of the loaded package; reading the same part again does not consume that budget twice. `maxArchiveEntries` bounds central-directory entries before the ZIP library allocates its owned index. Set an individual field to `null` to disable that configurable budget. Internal hard safety ceilings still apply, so disabling a budget does not make arbitrary archives acceptable. Values other than `null` must be positive safe integers; byte fields are expressed in bytes and the entry field is a count.
A violation rejects with `OoxmlResourceLimitError` (`code === 'ooxml-resource-limit'`). Its structured `details.violation` reports the resource, metric, limit, observed value, usage snapshot, and part name when a particular part caused the failure. The deprecated `maxZipEntryBytes` option remains as a compatibility alias for `resourceLimits.maxArchiveEntryBytes`, but is scheduled for removal in a future breaking release; new code should use `resourceLimits`.
Applications can collect the same data as a machine-readable `OoxmlResourceMetrics` report without enabling console output. This is useful for choosing limits from representative files in the application's own domain:
```ts
new DocxViewer(canvas, {
onResourceMetrics(metrics) {
// Send only under your application's own consent/privacy policy.
analytics.record('ooxml-resource-usage', metrics);
},
});
```
After resource options validate, the callback also runs for failed document loads, for which no document/viewer engine is returned. It is not awaited; callback exceptions and rejected promises are ignored and never change the load result. On a successful load, every DOCX/XLSX/PPTX engine and Viewer also exposes `await getResourceMetrics()`; unlike the one-shot callback, this probes the archive owner and returns a fresh snapshot after lazy sheet, slide, image, font, or media access. A failed or unresponsive probe rejects within a bounded timeout rather than presenting stale counters as current. Collection for that getter is always activeโ`debug` controls only console presentation. The library itself does not transmit or persist the report.
For interactive inspection, pass `debug: true`. The same report is formatted as one compact, Ratatui-inspired console card with the compressed source size, largest observed inflated entry, distinct inflated total, entry count, configured limits, and timing checkpoints:
```ts
new DocxViewer(canvas, {
debug: true,
resourceLimits: {
maxArchiveEntryBytes: 128 * 1024 * 1024,
maxTotalInflatedBytes: 256 * 1024 * 1024,
maxArchiveEntries: 4096,
},
});
```
Browser DevTools receive typography-only `%c` styling (a fixed-width font stack, fixed size and line height, disabled ligatures, zero letter spacing, and preserved whitespace) so Unicode borders and gauges stay aligned. The library does not set foreground or background colors. Node and Worker consoles receive the same card as one plain argument without CSS or ANSI escapes.
The report is content-free by construction: it does not include source URLs, filenames, package paths, document text, passwords, or raw error messages. It still contains document-derived sizes, counts, and timings, so applications remain responsible for consent, retention, and telemetry policy. The initial browser callback covers the underlying document/workbook/presentation factory and does not wait for a Viewer's first canvas paint; use `getResourceMetrics()` for the latest observed package counters. Bounded Node sessions accept both `onResourceMetrics` and `debug`. DOCX/PPTX report successful terminal metrics when their one-pass stream completes or the session is explicitly closed; XLSX reports success when the reusable workbook session is explicitly closed. Open-time and session-operation failures report immediately.
Image decoding uses a separate adaptive resource policy shared by all three formats. Ordinary browser-decodable rasters are assigned a geometry-weighted share of the default 128 MiB decoded budget before source extraction. Each source can then flow directly from extraction to decode: it keeps native resolution when it fits its share and otherwise uses up to a 2x canvas/DPR grid when that share has headroom, avoiding visibly soft 1x intermediates for small placed artwork. If the complete set of display grids itself exceeds the budget, adaptive mode reduces them by one uniform quality ratio. This avoids an all-paint inspection barrier without making display-sized downsampling the default. Natural-size consumers, pixel effects that require the authored grid, and formats that cannot be decoder-resized remain on their guarded format-specific paths under the hard surface ceilings. Image-bearing paints for the same loaded document are serialized so overlapping paints cannot each consume the full budget, while the admitted paint still runs up to two decodes concurrently. Applications with a known environment can override the aggregate budget on Viewer or per-render options:
```ts
new PptxViewer(canvas, {
imageResources: {
decodedByteBudget: 256 * 1024 * 1024,
strategy: 'adaptive', // or 'strict' to reject instead of reducing quality
resolution: 'native-if-fit', // or 'display' to minimize retained pixels
},
});
```
`resolution` defaults to `'native-if-fit'`. Use `'display'` when minimizing retained raster pixels is more important than preserving source-resolution sampling. `decodedByteBudget` accepts a positive safe integer from 4 bytes through 512 MiB. This configures planned and retained decoded RGBA ownership; it does not measure browser decoder intermediates or disable the encoded-source, per-axis, or per-surface hard safety ceilings. A strict aggregate crossing or any hard-ceiling crossing rejects with `OoxmlDecodedImageLimitError` (`code === 'ooxml-decoded-image-limit'`) instead of silently omitting the image.
The package counters and raster-image guards are deterministic admission limits, not exact JavaScript/WASM process-memory accounting. XML trees, document models, canvas backing stores, browser decoder overhead, renderer state, and browser-managed SVG/vector parse or decoded storage can still require several times the measured input. SVG has no portable decoded-byte measure or explicit browser release primitive; the library count-bounds its cache and revokes owned object URLs, but cannot charge it as RGBA bytes. The defaults therefore reduce risk but cannot promise that an OOM is impossible on every device. Running parse and render work in `mode: 'worker'` can contain many failures away from the main UI thread, but a Worker is not a separate operating-system process or a strict memory sandbox.
A measured limit crossing is reported as `OoxmlResourceLimitError`. A residual WASM failure that reaches a recognized trap-shaped boundary is reported conservatively as `parser-crashed`, not `parser-oom`: with the current aborting Rust/WASM boundary, panic, allocation failure, explicit `unreachable`, and stack overflow can lose their distinct causes and converge on the same generic runtime error. Inferring OOM from an exception class or message would misclassify some parser defects as large-file failures. Reliable OOM classification would require preserving a structured cause before the trap across every relevant allocation path; it cannot be recovered from the generic trap afterward. The WebAssembly JavaScript embedding also permits implementation-defined stack/OOM failures, including an indistinguishable plain `Error` or process termination, so converting and poisoning every engine-level failure cannot be guaranteed.
- **Bounded XLSX worksheets.** Each XLSX worksheet model is checked against logical budgets. The defaults are 100,000 rows, 250,000 cell records, 32 MiB of owned UTF-8 string content, and 64 MiB of structural JSON. Override them with `xlsxWorksheetLimits` on `XlsxViewer`, `XlsxSheetViewer`, or `XlsxWorkbook.load(...)`:
```ts
new XlsxViewer(container, {
xlsxWorksheetLimits: { maxRows: 110000, maxCells: 300000 },
});
```
An omitted or `undefined` option, an empty object, and any omitted field use the defaults. Supplied fields (`maxRows`, `maxCells`, `maxOwnedUtf8Bytes`, `maxJsonBytes`) must be positive safe integers no greater than `Number.MAX_SAFE_INTEGER - 1`. `null` is invalid. The option is validated before any load effects and is snapshotted for each load. DOCX and PPTX ignore it.
Row and cell budgets count retained records. They do not count grid coordinates or unique indexes. `maxOwnedUtf8Bytes` counts every string owned by `Cell.value`, including typed-value discriminators and rich-text and phonetic strings, plus formula text. Shared strings are charged per cell after resolution.
`maxJsonBytes` covers the worksheet model and its covered ancillary data. Browser and Node builds measure the UTF-8 length of `JSON.stringify` over the assembled structure. The native Rust path measures its complete `serde_json` UTF-8 output, which is a separately documented boundary.
For each dimension, the aggregate derived worksheet cache uses the larger of its default and the worksheet limit. The cache defaults are 200,000 rows, 500,000 cells, 64 MiB owned, and 128 MiB JSON. Each renderer coordinate index independently allows `max(250000, maxCells)` entries, charged per unique key. Delimited-text parsing counts every parsed field, including blanks, against `maxCells`; its fixed 64 MiB input limit is separate. The non-retaining Node `worksheetRows` path keeps streaming semantics, so cumulative budgets apply only to models it materializes.
ZIP, input, wire, representation, and copy limits remain independent. Raising these budgets does not promise that a whole 251,000-cell selection can be copied to the clipboard. The budgets are logical counters, not physical memory measurements. Increasing them accepts additional out-of-memory risk; the library does not guarantee that an allocation failure can be caught. With unchanged defaults no migration is needed. The one exception: errors from these adjustable guards now report `configurable: true`, whether the default or an override applied. Hard-ceiling errors remain `configurable: false`.
- **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`, `XlsxViewer`, and `XlsxSheetViewer`. 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, plus Noto Sans HK, 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. This sends the end-user's IP and User-Agent to `fonts.googleapis.com`, which may have GDPR implications.
- **XML parsing.** Uses `roxmltree`, which does not resolve external entities (XXE-safe by default).
- **Encrypted OOXML ([MS-OFFCRYPTO] Agile Encryption).** Password-protected `.docx` / `.xlsx` / `.pptx` files are OLE2/CFB containers, not ZIPs. Pass `password` to `load(...)` and the file is decrypted **client-side** via WebCrypto โ no bytes and no password leave the browser:
```ts
const doc = await DocxDocument.load(bytes, { password: 'secret' });
```
Key derivation (SHA-512 spin, commonly 100,000 iterations) and AES-CBC segment decryption run on the main thread and add roughly a second before parsing. Failures are typed [`OoxmlError`](packages/core/src/errors/ooxml-error.ts)s: no `password` on an encrypted file โ `encrypted`, wrong `password` โ `invalid-password`, a non-Agile scheme (legacy **Standard** / **Extensible** encryption, or an encrypted legacy binary `.doc`/`.xls`/`.ppt`) โ `unsupported-encryption`. **Note:** decryption recovers the plaintext but does **not** verify the file's HMAC data-integrity tag ([MS-OFFCRYPTO] ยง2.3.4.14), so tampering with the ciphertext is not detected โ treat decrypted output from untrusted sources with the same care as any other input.
## License
MIT
## Third-Party Notices
The library's own code is MIT-licensed. It also bundles a small set of
permissively-licensed third-party components โ see
[THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) (included in the npm
tarball) for the full list and license texts. Highlights:
- **[MathJax](https://www.mathjax.org/) + STIX Two Math**
(Apache License 2.0) โ the equation-rendering engine behind the
opt-in `@silurus/ooxml/math` entry described in
[Rendering equations](#rendering-equations). It ships in the tarball as
a standalone ~4 MB asset but is never loaded by a consuming app unless
that app imports `@silurus/ooxml/math` and the viewer is handed a
document that actually contains an equation.
- **Rust crate dependencies** of the WASM parsers (docx/pptx/xlsx) โ all
MIT / Apache-2.0 (or compatible permissive licenses), no copyleft.