← All formats

PPTX

Render .pptx decks to Canvas, your way.

Every demo below is the real library rendering a sample PowerPoint deck (.pptx) live in your browser — and the exact code that produces it. The same patterns power the Storybook stories.

Story · Demo

Single viewer with navigation

Hand PptxViewer a canvas and it manages parsing, rendering and the current slide. Step through with the built-in nextSlide() / prevSlide().

sample-1.pptx live · WASM
import { PptxViewer } from '@silurus/ooxml/pptx';

// The built-in viewer tracks the current slide for you.
const viewer = new PptxViewer(canvas, { width: 960, useGoogleFonts: true });
await viewer.load('/sample.pptx');

nextBtn.addEventListener('click', () => viewer.nextSlide());
prevBtn.addEventListener('click', () => viewer.prevSlide());

Story · ScrollView

Scroll through every slide

Drive the headless PptxPresentation engine to render each slide into its own canvas, stacked in a scroll container — no built-in viewer required.

sample-1.pptx live · WASM
import { PptxPresentation } from '@silurus/ooxml/pptx';

// Headless engine — render every slide into a canvas you control.
const doc = await PptxPresentation.load('/sample.pptx');

for (let i = 0; i < doc.slideCount; i++) {
  const canvas = document.createElement('canvas');
  scroller.appendChild(canvas);
  await doc.renderSlide(canvas, i, { width: 1100 });
}

Story · ThumbnailGrid

Thumbnail overview

The same engine renders slides at any size. Drop them into a grid at thumbnail width and wire up click handlers for navigation.

sample-1.pptx live · WASM
import { PptxPresentation } from '@silurus/ooxml/pptx';

// Render each slide small, wire up navigation.
const doc = await PptxPresentation.load('/sample.pptx');

for (let i = 0; i < doc.slideCount; i++) {
  const thumb = document.createElement('canvas');
  thumb.addEventListener('click', () => open(i));
  grid.appendChild(thumb);
  await doc.renderSlide(thumb, i, { width: 320 });
}

Story · MasterDetail

Thumbnail rail + large preview

Combine both: a PptxPresentation for the thumbnail rail and a PptxViewer for the detail pane. Click a thumbnail to jump the preview with goToSlide().

sample-1.pptx live · WASM
import { PptxPresentation, PptxViewer } from '@silurus/ooxml/pptx';

// A large preview viewer on the right…
const viewer = new PptxViewer(detailCanvas, { width: 960, enableTextSelection: true });

// …and a thumbnail rail on the left, sharing the same file.
const [doc] = await Promise.all([
  PptxPresentation.load('/sample.pptx'),
  viewer.load('/sample.pptx'),
]);

for (let i = 0; i < doc.slideCount; i++) {
  const thumb = document.createElement('canvas');
  thumb.addEventListener('click', () => viewer.goToSlide(i));  // jump the preview
  rail.appendChild(thumb);
  await doc.renderSlide(thumb, i, { width: 200 });
}

In your framework

Mount it in a canvas

The viewer is a plain TypeScript class — no framework runtime, no peer deps. Create it on mount, call load(), and let it go on unmount.

import { useEffect, useRef } from 'react';
import { PptxViewer } from '@silurus/ooxml/pptx';

export function Viewer({ src }: { src: string }) {
  const ref = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = ref.current;
    if (!canvas) return;
    const viewer = new PptxViewer(canvas, { width: 960 });
    void viewer.load(src);
    return () => viewer.destroy();
  }, [src]);

  return <canvas ref={ref} />;
}

API reference

Options & methods

Every public option and method, straight from the source. Types omitted for brevity are exported from the package — your editor will autocomplete the rest.

PptxViewer

Opinionated single-canvas viewer. Hand it a <canvas>; it owns parsing, rendering and the current slide.

new PptxViewer(canvas: HTMLCanvasElement, options?: PptxViewerOptions)

Options

OptionTypeDefaultDescription
width number 960 Canvas CSS width in px; height is derived from the slide aspect ratio.
dpr number devicePixelRatio Device pixel ratio for the backing store (crispness on HiDPI).
useGoogleFonts boolean false Load metric-compatible webfonts and non-Latin script fallbacks (Noto Arabic / CJK KR·SC·TC·JP / Cyrillic / Hebrew / Thai / Devanagari) from Google Fonts so layout matches Office and non-Latin text never falls back to tofu. Off by default for privacy.
enableTextSelection boolean false Overlay a transparent text layer so users can select & copy slide text.
enableMediaPlayback boolean false Make embedded audio/video interactive (the viewer draws its own play chrome).
hiddenSlideMode 'show' | 'skip' | 'dim' 'show' How hidden slides (`<p:sld show="0">`, §19.3.1.38) are presented. `show` draws them like any other slide; `skip` makes sequential navigation (nextSlide/prevSlide and the initial load) jump over them while keeping absolute indices unchanged (an explicit goToSlide to a hidden slide is still honored); `dim` draws them under a translucent overlay (the PowerPoint thumbnail look).
hiddenSlideDim Partial<DimOptions> { color: '#ffffff', opacity: 0.6 } Overrides for the `dim` overlay, merged over the default white 60% wash. A partial so it stays in sync if DimOptions gains a field.
maxZipEntryBytes number 512 MiB Per-entry ZIP decompression cap (zip-bomb guard). Lower it for untrusted input. Zero / negative values fall back to the default.
math MathRenderer undefined Opt-in OMML equation engine (MathJax + STIX Two Math, ~3 MB). Import it from the separate @silurus/ooxml/math entry — `import { math } from "@silurus/ooxml/math"` — and pass it to render equations. Omit it and equations are skipped, and the engine is left out of your build. When passed, the engine ships as a standalone asset fetched lazily the first time a document contains an equation.
mode 'main' | 'worker' 'main' 'main' renders on the main thread (default). 'worker' renders the whole viewer off the main thread — every frame is produced in a Web Worker and painted via a `bitmaprenderer` context — so document rendering never blocks the UI. Scroll, sheet tabs, zoom and (xlsx) cell selection are unchanged. Requires Worker + OffscreenCanvas. The pptx/docx text-selection overlay and in-document find work in 'worker' mode too (per-run geometry crosses the worker boundary); equations still require 'main'. Trade-off: each frame crosses the worker boundary as an ImageBitmap, so an individual render can be marginally slower than 'main' — the win is a responsive main thread, not raw render speed.
zoomMin / zoomMax number 0.1 / 4 Zoom factor bounds for setScale / fitWidth / fitPage (10%–400%).
onScaleChange (scale: number) => void Called when the zoom factor changes (setScale / fitWidth / fitPage / zoomIn / zoomOut), with the clamped factor (1 = 100%).
onHyperlinkClick (target: HyperlinkTarget) => void Called when a hyperlink is clicked. `target` is `{ kind: 'external', url }` or `{ kind: 'internal', ref, slideIndex? }`. When supplied, the callback fully owns the click (the default external-open / internal-navigation is not run). External URLs are scheme-sanitized (http / https / mailto / tel only); internal targets resolve to a docx bookmark / pptx slide jump / xlsx defined name or cell.
enableHyperlinks boolean true Master switch for hyperlink interactivity. Set `false` to disable it entirely: no hit-testing, no pointer cursor over links, no default navigation, and `onHyperlinkClick` is never called. Links still render exactly as authored but are inert, like plain text.
onSlideChange (index: number, total: number) => void Called after a slide finishes rendering.
onError (err: Error) => void Called on parse or render errors.

Methods

load(source: string | ArrayBuffer): Promise<void> Load from a URL or ArrayBuffer and render the first slide.
goToSlide(index: number): Promise<void> Render a specific slide (0-indexed, clamped).
nextSlide(): Promise<void> Advance one slide.
prevSlide(): Promise<void> Go back one slide.
getScale(): number The current zoom factor (1 = 100%).
setScale(scale: number): Promise<void> Set the absolute zoom factor (1 = 100%), clamped to [zoomMin, zoomMax]; re-renders at the new size and fires onScaleChange when it changes. View-only.
fitWidth(): Promise<void> Fit the content WIDTH to the host container and re-render (routes through setScale). Defers when nothing is loaded or the container is unlaid-out.
fitPage(): Promise<void> Fit the WHOLE content (width and height) inside the container so it is visible without scrolling — takes the tighter of the two fits. Defers when unloaded / unlaid-out.
findText(query: string, opts?: { caseSensitive?: boolean }): Promise<FindMatch<PptxMatchLocation>[]> Full-text search across the whole document; highlights every hit and returns them in document order. Each match carries `matchIndex`, the matched `text`, and its `location`. Case-insensitive by default.
findNext(): Promise<FindMatch<PptxMatchLocation> | null> Move to the next match (wrap-around), navigate to it if needed, and draw it in the active-match colour. Returns the now-active match, or null when there are none. Call findText first.
findPrev(): Promise<FindMatch<PptxMatchLocation> | null> Move to the previous match (wrap-around from first to last).
clearFind(): void Clear all highlights and reset the find state.
get slideIndex(): number Current slide index.
get slideCount(): number Total slides (0 until loaded).
get hiddenSlideMode(): "show" | "skip" | "dim" The current hidden-slide mode.
setHiddenSlideMode(mode: "show" | "skip" | "dim"): Promise<void> Switch the hidden-slide mode at runtime and re-render. Entering `skip` while on a hidden slide advances to the nearest visible slide.
get visibleSlideCount(): number Number of non-hidden slides (the absolute slideCount is unchanged).
getNotes(slideIndex: number): string | null Speaker-notes text for a slide (0-based); null when the slide has no notes part or the index is out of range.
get canvasElement(): HTMLCanvasElement The underlying canvas.
destroy(): void Tear down the worker and release resources.

PptxPresentation

Headless engine — parse once, render any slide into any canvas you supply (scroll views, thumbnail grids, master–detail).

await PptxPresentation.load(source, options?)

Options

OptionTypeDefaultDescription
useGoogleFonts boolean false Load metric-compatible webfonts and non-Latin script fallbacks (Noto Arabic / CJK KR·SC·TC·JP / Cyrillic / Hebrew / Thai / Devanagari) from Google Fonts so layout matches Office and non-Latin text never falls back to tofu. Off by default for privacy.
wasmUrl string | URL bundled asset Override the URL the parser worker fetches the WebAssembly module from. By default each format resolves the `*_parser_bg.wasm` asset that ships next to its bundle (relative to the module URL); set this to serve it from a CDN or a self-hosted path instead (a relative value resolves against the document URL). Pointing it at a mismatched or missing file makes load() reject when the worker instantiates it.
maxZipEntryBytes number 512 MiB Per-entry ZIP decompression cap (zip-bomb guard). Lower it for untrusted input. Zero / negative values fall back to the default.
workerTimeoutMs number unlimited Reject the parse if the worker does not answer within this many ms — an opt-in safety net for a wedged / crashed worker that would otherwise leave load() pending forever. Unlimited by default (a large document with heavy media can legitimately take tens of seconds). A worker that throws or fails to load already rejects immediately regardless; this only covers the "silent, never-responds" case.
math MathRenderer undefined Opt-in OMML equation engine (MathJax + STIX Two Math, ~3 MB). Import it from the separate @silurus/ooxml/math entry — `import { math } from "@silurus/ooxml/math"` — and pass it to render equations. Omit it and equations are skipped, and the engine is left out of your build. When passed, the engine ships as a standalone asset fetched lazily the first time a document contains an equation.
mode 'main' | 'worker' 'main' 'main' parses in a worker and renders on the main thread (default). 'worker' parses AND renders entirely inside the worker; the main thread only paints the ImageBitmap returned by the render*ToBitmap method via a `bitmaprenderer` context. Requires Worker + OffscreenCanvas. The canvas-target render methods are unavailable in 'worker' mode, and equations require 'main'. Trade-off: each frame is transferred from the worker as an ImageBitmap, so a single render can be marginally slower than 'main' — the win is that the main thread never blocks.

Methods

static load(source, options?): Promise<PptxPresentation> Parse a deck from a URL or ArrayBuffer.
get slideCount(): number Total slides.
renderSlide(canvas, index, opts?: { width?, dpr?, onTextRun?, dim? }): Promise<void> Render one slide into the given canvas at the given width. `onTextRun` receives each rendered segment as `PptxTextRunInfo`, including the source shape’s slide-local `shapeId` when authored, so callers can build a transparent selection overlay or stable shape mapping; `dim` (a DimOptions) paints a translucent wash over the finished slide (hidden-slide dimming). Equations render when a `math` engine was passed to `load`. Unavailable in `mode: "worker"` — use renderSlideToBitmap.
renderSlideToBitmap(index, opts?: { width?, dpr?, dim? }): Promise<ImageBitmap> Render one slide and return it as an ImageBitmap (both modes; in worker mode the render runs off the main thread). `dim` paints a translucent overlay over the slide (hidden-slide dimming). Equations are skipped in `mode: "worker"` (they require `mode: "main"`). The bitmap is caller-owned: pass it to `transferFromImageBitmap` (which consumes it) or call `bitmap.close()`.
presentSlide(canvas, index, opts?: { width?, dpr?, onTextRun? }): Promise<PresentationHandle> Render a slide and attach canvas-native audio/video playback, returning a handle with play() / pause() / destroy(). Works in both modes — in `mode: "worker"` the base slide is rendered off the main thread and the video overlay is composited on the main thread; `onTextRun` is unavailable there (it cannot cross the worker boundary).
getNotes(slideIndex: number): string | null Speaker-notes text for a slide (0-based; ECMA-376 §13.3.5). Returns null when the slide has no notes part or the index is out of range.
get slideWidth(): number Slide width in EMU (0 until loaded).
get slideHeight(): number Slide height in EMU (0 until loaded).
get mode(): "main" | "worker" The render mode this engine was loaded with. An injected engine’s mode decides whether slides render via renderSlide (main) or renderSlideToBitmap (worker).
destroy(): void Release the worker.

PptxScrollViewer

Container-owning continuous-scroll viewer. Takes a <div> (not a canvas) and renders the whole deck as one vertically-scrolling, virtualized surface (only the visible window + overscan is mounted). Zoom is view-only.

new PptxScrollViewer(container: HTMLElement, options?: PptxScrollViewerOptions)

Options

OptionTypeDefaultDescription
width number container width Base fit width in CSS px. Default: the container width at first non-zero layout.
gap number 16 Vertical gap (px) between consecutive slides.
paddingTop / paddingBottom number gap Desk padding (px) above the first slide / below the last. Pass 0 for a flush edge.
paddingLeft / paddingRight number gap Horizontal desk gutters (px); also shrink the container-derived fit width so a slide sits inside them at 100%. Pass 0 for a flush edge.
overscan number 1 Slides kept mounted beyond the viewport on each side.
background string undefined CSS background for the scroll surface (the desk behind/between slides). Default transparent (the container shows through).
pageShadow string | false '0 1px 3px rgba(0,0,0,0.2)' CSS box-shadow painted on every slide canvas. A spread-only ring (e.g. `0 0 0 1px #c8ccd0`) gives a crisp 1px border look. `false` disables it (flat slides).
enableZoom boolean true Enable Ctrl/⌘ + wheel (and trackpad pinch) zoom. View-only.
zoomMin / zoomMax number 0.1 / 4 Absolute zoom scale bounds (10%–400%).
enableTextSelection boolean false Overlay a transparent, selectable text layer per slide for native copy. `mode: "main"` only — in worker mode the overlay stays empty and the viewer warns once.
onHyperlinkClick (target: HyperlinkTarget) => void Called when a hyperlink is clicked. `target` is `{ kind: 'external', url }` or `{ kind: 'internal', ref, slideIndex? }`. When supplied, the callback fully owns the click (the default external-open / internal-navigation is not run). External URLs are scheme-sanitized (http / https / mailto / tel only); internal targets resolve to a docx bookmark / pptx slide jump / xlsx defined name or cell.
enableHyperlinks boolean true Master switch for hyperlink interactivity. Set `false` to disable it entirely: no hit-testing, no pointer cursor over links, no default navigation, and `onHyperlinkClick` is never called. Links still render exactly as authored but are inert, like plain text.
presentation PptxPresentation undefined Inject an already-loaded engine to share one parse across panes. When set, load() is unsupported, the engine’s own mode wins, and destroy() does NOT destroy it (the caller owns its lifecycle).
useGoogleFonts boolean false Load metric-compatible webfonts and non-Latin script fallbacks (Noto Arabic / CJK KR·SC·TC·JP / Cyrillic / Hebrew / Thai / Devanagari) from Google Fonts so layout matches Office and non-Latin text never falls back to tofu. Off by default for privacy.
maxZipEntryBytes number 512 MiB Per-entry ZIP decompression cap (zip-bomb guard). Lower it for untrusted input. Zero / negative values fall back to the default.
math MathRenderer undefined Opt-in OMML equation engine (MathJax + STIX Two Math, ~3 MB). Import it from the separate @silurus/ooxml/math entry — `import { math } from "@silurus/ooxml/math"` — and pass it to render equations. Omit it and equations are skipped, and the engine is left out of your build. When passed, the engine ships as a standalone asset fetched lazily the first time a document contains an equation.
dpr number devicePixelRatio Device pixel ratio for the backing store (crispness on HiDPI).
mode 'main' | 'worker' 'main' 'main' parses in a worker and renders on the main thread (default). 'worker' parses AND renders entirely inside the worker; the main thread only paints the ImageBitmap returned by the render*ToBitmap method via a `bitmaprenderer` context. Requires Worker + OffscreenCanvas. The canvas-target render methods are unavailable in 'worker' mode, and equations require 'main'. Trade-off: each frame is transferred from the worker as an ImageBitmap, so a single render can be marginally slower than 'main' — the win is that the main thread never blocks.
onVisibleSlideChange (topIndex: number, total: number) => void Fires when the top-most visible slide changes.
onError (err: Error) => void Called on load errors and async per-slide render failures (a failed slide is left blank rather than crashing the scroll loop).

Methods

load(source: string | ArrayBuffer): Promise<void> Load a deck from a URL or ArrayBuffer and render the first window. Throws when an engine was injected via `presentation`.
scrollToSlide(index: number, opts?: { behavior?: "auto" | "smooth" }): void Scroll so slide index’s top edge sits at the viewport top (index clamped).
setScale(scale: number): void Set the absolute zoom scale at runtime (clamped to zoomMin/zoomMax). Flicker-free. View-only.
relayout(): void Force a re-fit + re-mount of the visible window. Called automatically after load / resize / zoom; use it when the container resizes in a way a ResizeObserver cannot observe (e.g. a late web-font load). Idempotent.
get slideCount(): number Total slides (0 until loaded).
get topVisibleSlide(): number Index of the top-most visible slide.
destroy(): void Tear down the DOM subtree. Destroys a self-loaded engine; an injected one is left intact.