[!NOTE]
This entire codebase — Rust parsers, TypeScript renderers, tests, and tooling — is implemented by AI coding agents, primarily Claude and Codex, through iterative prompting. No human-written application code exists in this repository.
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.
Office Open XML Viewer
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 live framework examples for runnable React, Vue, Svelte, and Solid projects.
Optional, experimental legacy Office add-on 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 for the rationale.
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). 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:
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 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
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.
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 + 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 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.
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:
npm i @silurus/ooxml @silurus/ooxml-legacy-office
Each entry exports a reader for the matching loader or viewer:
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).
The package versions must match exactly; upgrade both together.
No migration is required without the add-on. See the legacy Office guide
for viewer and Node examples, and Experimental legacy Office sources
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.
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.
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 for the full
module list and 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.
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.
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
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:
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.
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 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 |
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.
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
and DOCX API reference.
Progressive PPTX layout
PPTX exposes the same progressive lifecycle on PptxViewer,
PptxScrollViewer, and PptxPresentation.load():
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
and PPTX API reference.
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, fonts, cjkFallback, resourceLimits, the deprecated
maxZipEntryBytes alias, math, dpr) are accepted too.
Font sources
Documents often name fonts the viewer's machine does not have. By default the
canvas falls back to installed fonts and no font is requested from any network
host. Pass fonts, an ordered list of sources, to supply web fonts instead. It
works the same way for every format, engine and Viewer, in main and worker
mode:
import { DocxViewer, fontFiles, googleFonts, officeFontSubstitutions } from '@silurus/ooxml/docx';
// Google Fonts, or a Google-compatible mirror via googleFonts({ cssOrigin }) (stylesheet requests only;
// the mirror's redirects and the font URLs it returns decide where files load from)
new DocxViewer(canvas, { fonts: [googleFonts()] });
// Self-hosted files first, then a mirror for whatever they do not serve
new DocxViewer(canvas, {
fonts: [
fontFiles({ faces: [{ family: 'Carlito', url: '/fonts/Carlito-Regular.woff2' }], substitutes: officeFontSubstitutions }),
googleFonts({ cssOrigin: 'https://fonts.internal.example' }),
],
});
The factories are googleFonts({ cssOrigin? }), cssApi({ urlTemplate, … }),
fontFaceCss({ url }), fontFiles({ faces }) and, for authenticated or
private services, fontResolver(fn), which runs on the main thread. They are
exported from @silurus/ooxml and from each format entry.
- Chaining: sources are consulted per family, in option order. The first
source that has a face for the family claims it. A failed stylesheet or face
keeps the host fallback with a
console.warn, and no other source is tried.
- Lazy subsets: only the
unicode-range subsets that intersect the painted
text are loaded, and installed faces with the authored name are never
replaced.
- Deployment: cross-origin font URLs need CORS. Stylesheet origins need
connect-src and font file origins need font-src in your CSP. Optionally
add <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> for
Google Fonts. Source limits fail closed with
OoxmlError('font-source-limit').
useGoogleFonts: true is deprecated: it still works in 0.90 as
fonts: [googleFonts()], with a warning, and is removed in 0.91. See the
0.90 migration guide for every source kind, the
resolver contract, CSP and CORS requirements, and the limits.
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.
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. Configure fonts (for example
fonts: [googleFonts()] or self-hosted files) 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()):
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.
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 paint from retained page geometry |
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 |
Framework Examples
Runnable TypeScript projects are available for
React,
Vue,
Svelte, and
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 (, , §17.4.7) |
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) | ✅ |
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 |
|
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/ — 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/ — 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 for every removed synchronous helper and its replacement.
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 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.
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
# 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)
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), font-source-limit (a configured font source exceeded a fixed
limit; violation names it), 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.
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:
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:
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 ; 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— controls only console presentation. The library itself does not transmit or persist the report.
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 (included in the npm
tarball) for the full list and license texts. Highlights:
- MathJax + STIX Two Math
(Apache License 2.0) — the equation-rendering engine behind the
opt-in
@silurus/ooxml/math entry described in
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.