@aiden0z/pptx-renderer
v1.3.0
Published
Parse and render PPTX files in the browser with a TypeScript-first engine.
Maintainers
Readme
pptx-renderer
A high-fidelity, browser-native PPTX renderer that parses Office Open XML (.pptx) files and renders slides as HTML/SVG DOM.
Supports shapes, text, images, tables, charts, SmartArt, bounded OMML equations, groups, backgrounds, gradients, pattern fills, bounded ordinary-shape outer shadows and reflections, bounded static DrawingML 3D, and OOXML color inheritance. Rendering fidelity depends on the source features and available preview data; see the support boundaries below.
Rendering Example
A complex slide with charts, text styles, shapes, and SmartArt — PowerPoint ground truth vs browser-rendered output:
Visual Regression Testing
Visual regression suites compare selected shape, SmartArt, fill/stroke, text, table, and chart cases against PowerPoint output. Each API evaluation records the renderer revision, browser, optional font profile, and SHA-256 fingerprints of its PPTX and ground truth so results can be compared against the same inputs. Eligible single-chart Cartesian slides also expose plot-bound, data-ink, series-color, and region diagnostics so page whitespace cannot hide a missing or displaced series. A passing aggregate or local score does not establish semantic correctness or full PowerPoint parity; structural assertions and targeted browser/native inspection complement the metrics.
E2E evaluation dashboard: side-by-side ground truth vs rendered output with SSIM, color histogram, and IoU metrics per slide.
Ground truth binaries (PPTX/PDF/PNG) stay in the ignored
test/e2e/testdata/tree. Tracked case definitions and coverage metadata keep that local corpus reproducible. Generate shape/SmartArt corpora withscripts/one_shot_full_ground_truth.pyor focused text/table/chart/composite cases withscripts/generate_pypptx_cases.py; both macOS and Windows PowerPoint are supported. Seedocs/TESTING.md.
On macOS, native exports use one fixed ignored oracle-runtime directory and target the requested
presentation by its exact full path. Keep the interactive PowerPoint session available and inspect
pending dialogs when automation fails. This lets local oracle runs coexist with other open
presentations without treating the active window as the export target.
Install
npm install @aiden0z/pptx-renderer
# or
pnpm add @aiden0z/pptx-renderer
# Optional: only needed for SmartArt / EMF files with embedded PDF fallback previews
pnpm add pdfjs-distRequires Node.js 20+ for development. Runtime is browser-only.
Quick Start
For bundlers and npm-based apps:
import { PptxViewer, RECOMMENDED_ZIP_LIMITS } from '@aiden0z/pptx-renderer';
const container = document.getElementById('pptx-container')!;
const resp = await fetch('/slides/demo.pptx');
// One-liner: parse, build model, and render
const viewer = await PptxViewer.open(await resp.arrayBuffer(), container, {
zipLimits: RECOMMENDED_ZIP_LIMITS,
listOptions: { windowed: true },
});For direct browser usage without a bundler, import the standalone browser ESM build. This
entry bundles JSZip and ECharts, and replaces Node-style process.env checks at build time.
PDF.js remains optional and is only needed for EMF-embedded PDF fallback previews:
<script type="module">
import {
PptxViewer,
RECOMMENDED_ZIP_LIMITS,
} from '/vendor/pptx-renderer/aiden0z-pptx-renderer.browser.es.js';
const container = document.getElementById('pptx-container');
const resp = await fetch('/slides/demo.pptx');
await PptxViewer.open(await resp.arrayBuffer(), container, {
zipLimits: RECOMMENDED_ZIP_LIMITS,
});
</script>Copy the standalone artifact from the published package into your application's versioned static assets. The entry bundles JSZip and the ECharts chart types supported by this renderer; PDF.js remains an optional external asset. A pinned CDN URL can also be used, but the project does not depend on or deploy through a CDN provider.
For large decks, combine windowed mounting with on-demand slide parsing and media decoding:
const viewer = await PptxViewer.open(buffer, container, {
zipLimits: RECOMMENDED_ZIP_LIMITS,
lazySlides: true,
lazyMedia: true,
listOptions: { windowed: true, initialSlides: 4, batchSize: 4 },
});Optional PDF.js Fallback for SmartArt/EMF Preview Images
PowerPoint often stores SmartArt or pasted vector artwork as EMF fallback images. This library does not implement a full EMF/WMF vector renderer. It can render the common Office fallback cases where an EMF contains an embedded PDF preview or bitmap preview.
For EMF files with embedded PDF previews, install pdfjs-dist and pass explicit asset
URLs. This keeps PDF.js optional and avoids forcing every consumer bundle to include it.
This is only needed for EMF-PDF fallback previews; ordinary PPTX rendering does not
require any code changes.
import { PptxViewer } from '@aiden0z/pptx-renderer';
const pdfjs = {
moduleUrl: new URL('pdfjs-dist/build/pdf.min.mjs', import.meta.url).toString(),
workerUrl: new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(),
};
const viewer = await PptxViewer.open(buffer, container, {
pdfjs,
});If your app uses a CDN or pre-copied assets, point those fields at your hosted files.
Set pdfjs: false to disable EMF-PDF fallback rendering entirely. With no pdfjs
configuration, the renderer attempts only a best-effort automatic resolution and
otherwise degrades gracefully.
type PdfjsConfig =
| {
moduleUrl?: string;
workerUrl?: string;
}
| false;For a no-bundler deployment, copy the two PDF.js files into your application's static assets and pass absolute self-hosted URLs:
const pdfjs = {
moduleUrl: '/vendor/pdfjs/pdf.min.mjs',
workerUrl: '/vendor/pdfjs/pdf.worker.min.mjs',
};The PDF fallback uses short-lived blob Workers. A restrictive Content Security Policy
must allow the chosen asset origin and blob: Workers, for example:
script-src 'self';
worker-src 'self' blob:;
img-src 'self' data: blob:;If a CDN is used instead, pin exact package versions and add only that origin to the relevant CSP directives.
Or with more control over each step:
import {
PptxViewer,
parseZip,
buildPresentation,
RECOMMENDED_ZIP_LIMITS,
} from '@aiden0z/pptx-renderer';
const container = document.getElementById('pptx-container')!;
const viewer = new PptxViewer(container, { fitMode: 'contain' });
const files = await parseZip(arrayBuffer, RECOMMENDED_ZIP_LIMITS);
const presentation = buildPresentation(files);
viewer.load(presentation);
await viewer.renderList({ windowed: true, batchSize: 8 });To delay both media decompression and slide node parsing until rendered slides actually need
them, use parseZipLazyMedia() and pass { lazySlides: true } to buildPresentation():
import {
PptxViewer,
parseZipLazyMedia,
buildPresentation,
RECOMMENDED_ZIP_LIMITS,
} from '@aiden0z/pptx-renderer';
const files = await parseZipLazyMedia(arrayBuffer, RECOMMENDED_ZIP_LIMITS);
const presentation = buildPresentation(files, { lazySlides: true });
const viewer = new PptxViewer(container);
viewer.load(presentation);
await viewer.renderList({ windowed: true, initialSlides: 4 });API
PptxViewer (primary, extends EventTarget)
PptxViewer.open(input, container, options?) — Static Factory
Parse, build, and render in one call. Returns a Promise<PptxViewer>.
const viewer = await PptxViewer.open(buffer, container, {
renderMode: 'list', // 'list' (default) | 'slide'
zipLimits: RECOMMENDED_ZIP_LIMITS,
listOptions: { windowed: true, batchSize: 8 },
signal: abortController.signal, // optional AbortSignal
// ...ViewerOptions
});new PptxViewer(container, options?)
| Option | Type | Default | Description |
| -------------------- | --------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------- |
| width | number | -- | Container width hint (omit for auto-detect) |
| fitMode | 'contain' \| 'none' | 'contain' | Responsive fit or fixed size |
| zoomPercent | number | 100 | Zoom level (10–400) |
| scrollContainer | HTMLElement | -- | Scroll container for IntersectionObserver root |
| zipLimits | ZipParseLimits | -- | Security limits for ZIP parsing (used by .open()). Use RECOMMENDED_ZIP_LIMITS for untrusted input. |
| lazyMedia | boolean | false | Decode embedded media on demand instead of during ZIP parsing. Best for large decks with windowed list rendering. |
| lazySlides | boolean | false | Parse slide shape/table/chart nodes on demand. Best for large decks with windowed list rendering. |
| pdfjs | PdfjsConfig | -- | Optional PDF.js URLs for EMF-embedded PDF fallback rendering, or false to disable it. |
| embeddedFontLimits | EmbeddedFontLimits | safe defaults | Optional embedded-font resource limit overrides. Omitted fields retain the built-in defaults. |
| fontFaces | readonly FontFaceConfig[] | -- | Host-provided font faces for typefaces referenced by the PPTX but not embedded in it. |
| onSlideChange | (index) => void | -- | Shorthand for slidechange event |
| onSlideRendered | (index, element) => void | -- | Shorthand for sliderendered event |
| onSlideError | (index, error) => void | -- | Shorthand for slideerror event |
| onSlideUnmounted | (index) => void | -- | Shorthand for slideunmounted event |
| onNodeError | (nodeId, error) => void | -- | Shorthand for nodeerror event |
| onRenderStart | () => void | -- | Shorthand for renderstart event |
| onRenderComplete | () => void | -- | Shorthand for rendercomplete event |
All shorthand callbacks are also available as EventTarget events (e.g. viewer.addEventListener('slidechange', ...)).
Embedded font decompression is bounded by default. Trusted applications can provide partial
embeddedFontLimits overrides; see the performance guide
for defaults, examples, and the soft processing-time boundary.
For decks that reference fonts unavailable in the browser, provide regular/bold faces before
layout through fontFaces. Sources follow the browser FontFace API and can be font bytes or a
CSS url(...) source (subject to the host page's CSP and CORS policy):
const [regular, bold] = await Promise.all([
fetch('/fonts/brand-sans-regular.woff2').then((response) => response.arrayBuffer()),
fetch('/fonts/brand-sans-bold.woff2').then((response) => response.arrayBuffer()),
]);
const viewer = new PptxViewer(container, {
fontFaces: [
{ family: 'Brand Sans', source: regular, descriptors: { weight: '400' } },
{ family: 'Brand Sans', source: bold, descriptors: { weight: '700' } },
],
});Instance Methods
viewer.load(presentation); // Load a PresentationData model (no render)
await viewer.renderList({ windowed: true }); // Render all slides in scrollable list
await viewer.renderSlide(0); // Render a single slide (no built-in nav UI)
// Load from binary input (parse → build → render). Cleans up previous state on re-open.
await viewer.open(buffer, { renderMode: 'list', signal: abortController.signal });
await viewer.goToSlide(index); // Jump to slide (0-based), returns Promise<void>
await viewer.goToSlide(index, { behavior: 'instant' }); // Custom ScrollIntoViewOptions (list mode)
await viewer.setZoom(150); // Runtime zoom (10–400)
await viewer.setFitMode('none'); // Switch fit mode
const matches = viewer.searchText('GPU'); // Search parsed model text
const hit = await viewer.highlightSearchResult(matches[0]); // Default node overlay highlight
hit?.dispose();
viewer.clearSearchHighlights(); // Remove active search overlays
// Render a single slide into an external container (React/Vue integration, thumbnails).
// Returns a SlideHandle; caller owns it and must call handle.dispose() when done.
const handle = viewer.renderSlideToContainer(index, container, scale?);
handle.dispose(); // Clean up slide-specific resources
// Render a lightweight scaled slide preview into an external container.
// This preserves the original slide layout and uses transform scaling; it is
// not a bitmap thumbnail generator, so use lazy/windowed mounting for decks.
const thumb = viewer.renderThumbnailToContainer(index, sidebarItem, { width: 180 });
await thumb?.ready;
thumb?.dispose();
// Query which slides are currently mounted in the DOM
viewer.isSlideMounted(index); // boolean
viewer.getMountedSlides(); // number[] (sorted)
// Typed event helpers (return `this` for chaining)
viewer.on('slidechange', (e) => console.log(e.detail.index));
viewer.off('slidechange', listener);
viewer.destroy(); // Cleanup blob URLs, observers, and DOM
viewer[Symbol.dispose](); // TC39 Explicit Resource Management (calls destroy)Text Search
PptxViewer.searchText(query, options?) searches the parsed PresentationData model,
not the rendered DOM. This keeps search available before or after a slide is mounted and
avoids mutating renderer-generated text runs.
String queries are case-insensitive by default. Pass matchCase: true when you need
exact casing. RegExp queries keep their own flags, so /GPU/ remains case-sensitive
and /GPU/i remains case-insensitive.
import type { TextSearchResult } from '@aiden0z/pptx-renderer';
const matches: TextSearchResult[] = viewer.searchText('GPU', {
matchCase: false,
wholeWord: true,
snippetRadius: 48,
});
const exactMatches = viewer.searchText('GPU', { matchCase: true });
const regexMatches = viewer.searchText(/GPU|CPU/i);
for (const match of matches) {
await viewer.goToSlide(match.slideIndex, { behavior: 'smooth', block: 'center' });
// Use match.bounds for node-level highlight overlays in your own UI.
}Each TextSearchResult includes slideIndex, nodeId, nodePath, nodeType,
textKind, full text, matchStart, matchEnd, snippet, and bounds.
bounds is the matched shape or table bounds in intrinsic slide coordinates, so
application code can draw node-level highlight overlays on top of a rendered slide.
The renderer intentionally does not rewrite text nodes for character-level text highlighting. Character-level text highlighting would require mapping match offsets back to shaped Office text runs and line layout, which is a separate, higher-risk renderer feature. Today the stable API boundary is model-level search plus node-level bounds.
For the common UI case, highlightSearchResult(result, options?) draws a node-level
overlay using a default highlight style. Pass SearchHighlightOptions for custom colors,
spacing, shadows, and class names:
const hit = await viewer.highlightSearchResult(matches[0], {
className: 'my-search-hit',
borderColor: '#22c55e',
backgroundColor: 'rgba(34, 197, 94, 0.18)',
borderRadius: 6,
borderWidth: 2,
boxShadow: '0 0 0 2px rgba(15, 23, 42, 0.35)',
padding: 3,
});
// The caller owns returned highlight handles.
hit?.dispose();
viewer.clearSearchHighlights();Scaled Slide Previews
PptxViewer.renderThumbnailToContainer(index, container, options?) renders a slide at
its intrinsic layout size and applies CSS transform scaling inside a clipped wrapper.
This avoids the layout drift that can happen if a PPTX slide is rendered directly into a
small thumbnail-sized container.
const thumb = viewer.renderThumbnailToContainer(slideIndex, thumbnailEl, { width: 96 });
await thumb?.ready;
// The caller owns externally rendered previews.
thumb?.dispose();This is not a bitmap thumbnail generator: it still creates a scaled DOM/SVG slide
preview, so large decks should mount previews lazily with IntersectionObserver or a
windowed list and dispose handles when they scroll out of view.
ListRenderOptions
| Option | Type | Default | Description |
| ------------------ | --------- | ------- | ---------------------------------- |
| windowed | boolean | false | Use IntersectionObserver windowing |
| batchSize | number | 12 | Slides per render batch |
| initialSlides | number | 4 | Initial slides to mount (windowed) |
| overscanViewport | number | 1.5 | Viewport overscan multiplier |
ZipParseLimits and Resource Safety
parseZip(buffer) defaults to no ZIP limits for backward compatibility. For files from users or other untrusted sources, pass RECOMMENDED_ZIP_LIMITS or stricter values:
| Limit | Recommended value | What it protects |
| --------------------------- | ----------------- | ------------------------------------------- |
| maxEntries | 4000 | Archives with excessive file counts |
| maxEntryUncompressedBytes | 32 MiB | A single oversized XML/media entry |
| maxTotalUncompressedBytes | 256 MiB | Total decompressed archive size |
| maxMediaBytes | 192 MiB | Total media payload size under ppt/media/ |
| maxConcurrency | 8 | Parallel ZIP entry reads |
When ZIP metadata does not expose a reliable uncompressed size, the parser falls back to the actual decoded entry size before accepting the entry. This keeps maxEntryUncompressedBytes, maxTotalUncompressedBytes, and maxMediaBytes effective for XML/text entries and media entries alike.
Renderer-level guards also apply after ZIP parsing:
- Chart caches do not allocate from oversized
c:ptCount; chart point indexes are capped at10,000per cache. - EMF bitmap previews are rejected when decoded size exceeds
16,777,216pixels, dimensions exceed8192x8192, or pixel payload is shorter than the declared bitmap. - External audio/video relationships only render for safe
http/httpsURLs withTargetMode="External"and are created withpreload="none".
Events (PptxViewerEventMap)
viewer.addEventListener('renderstart', () => {
/* render cycle began */
});
viewer.addEventListener('rendercomplete', () => {
/* render cycle finished (fires even on error) */
});
viewer.addEventListener('slidechange', (e) => console.log(e.detail.index));
viewer.addEventListener('sliderendered', (e) => console.log(e.detail.index, e.detail.element));
viewer.addEventListener('slideerror', (e) => console.error(e.detail.index, e.detail.error));
viewer.addEventListener('slideunmounted', (e) => console.log(e.detail.index));
viewer.addEventListener('nodeerror', (e) => console.warn(e.detail.nodeId, e.detail.error));slidechange fires both on goToSlide() navigation and after each render cycle (initial render included). renderstart/rendercomplete bracket every render cycle (renderList, renderSlide, setZoom, setFitMode). When calls overlap, the newer render request supersedes older queued or batched work, so stale list batches stop before appending more DOM.
Instance Properties (read-only)
viewer.presentationData; // PresentationData | null — the parsed model, null before load()
viewer.slideCount; // number — total slides (0 if not loaded)
viewer.slideWidth; // number — intrinsic slide width in px
viewer.slideHeight; // number — intrinsic slide height in px
viewer.currentSlideIndex; // number — currently active slide (0-based)
viewer.isRendering; // boolean — true between renderstart and rendercomplete
viewer.zoomPercent; // number — current zoom level (e.g. 100, 200)
viewer.fitMode; // FitMode — current fit mode ('contain' | 'none')PptxRenderer (deprecated v1 compat)
PptxRenderer extends PptxViewer and provides the legacy preview(input) API with built-in nav buttons in slide mode. Migrate to PptxViewer for new code.
import { PptxRenderer } from '@aiden0z/pptx-renderer';
const renderer = new PptxRenderer(container, { mode: 'list', listMountStrategy: 'windowed' });
await renderer.preview(buffer); // deprecated — use PptxViewer.open() insteadPptxRenderer accepts the same optional lazyMedia, lazySlides, and pdfjs
configuration as PptxViewer, so legacy users can enable performance options or
EMF-PDF fallback rendering without changing APIs.
Utility Exports
import {
parseZip,
parseZipLazyMedia,
buildPresentation,
materializeAllSlideNodes,
serializePresentation,
buildTextIndex,
searchText,
searchPresentation,
RECOMMENDED_ZIP_LIMITS,
} from '@aiden0z/pptx-renderer';
const files = await parseZip(arrayBuffer, RECOMMENDED_ZIP_LIMITS); // PptxFiles
const lazyFiles = await parseZipLazyMedia(arrayBuffer, RECOMMENDED_ZIP_LIMITS); // media resolves on demand
const presentation = buildPresentation(files); // PresentationData
const lazyPresentation = buildPresentation(lazyFiles, { lazySlides: true }); // slide nodes parse on demand
materializeAllSlideNodes(lazyPresentation); // optional: force full model materialization
const json = serializePresentation(presentation); // SerializedPresentation (JSON-safe)
const index = buildTextIndex(presentation); // TextIndexEntry[]
const matches = searchText(index, '算力'); // TextSearchResult[]
const directMatches = searchPresentation(presentation, /GPU|CPU/i); // TextSearchResult[]Headless Slide Rendering
For advanced use cases (server-side screenshot, custom rendering pipeline):
import { renderSlide } from '@aiden0z/pptx-renderer';
import type { SlideHandle } from '@aiden0z/pptx-renderer';
const handle = renderSlide(presentation, presentation.slides[0], {
onNodeError: (nodeId, err) => console.warn(nodeId, err),
mediaUrlCache: new Map(), // optional shared cache for blob URLs
pdfjs, // optional, only for EMF-embedded PDF fallback rendering
fontFaces, // optional host-provided FontFaceConfig[]
});
document.body.appendChild(handle.element);
// Await async media such as EMF-PDF fallback previews before screenshots/exports.
await handle.ready;
// Cancels pending PDF fallbacks and disposes charts + owned blob URLs.
handle.dispose();Model Types
All model types are exported for consumers building custom tooling:
import type {
PresentationData,
BuildPresentationOptions,
SlideData,
SlideNode,
ThemeData,
BaseNodeData,
ShapeNodeData,
PicNodeData,
TableNodeData,
GroupNodeData,
ChartNodeData,
TextBody,
TextParagraph,
TextRun,
Position,
Size,
NodeType,
SerializedPresentation,
SerializedSlide,
SerializedNode,
PptxFiles,
ZipParseLimits,
FitMode,
PreviewInput,
ViewerOptions,
FontFaceConfig,
ListRenderOptions,
ThumbnailRenderOptions,
SearchHighlightHandle,
SearchHighlightOptions,
PptxViewerEventMap,
SlideHandle,
PdfjsOptions,
PdfjsConfig,
TextBounds,
TextIndexEntry,
TextIndexOptions,
TextSearchOptions,
TextSearchResult,
} from '@aiden0z/pptx-renderer';Rendering Capabilities
Shapes and geometry
The renderer covers the commonly used DrawingML preset families and numeric custom geometry,
including multi-path shapes, connectors, arrows, callouts, action buttons, and flowcharts.
The spec-compiled runtime currently contains 29 definitions: all 28 zero-adjustment flowcharts plus
donut, whose OOXML default is adj=25000 with a supported 0..50000 adjustment range. Other
presets continue to use their handwritten implementations. Arbitrary symbolic guides in custom
geometry are not yet supported.
Text
Text rendering follows the master, layout, placeholder, shape, paragraph, and run cascade. It
supports theme fonts, CJK text, bullets, hyperlinks, vertical text, superscript/subscript, Office
percentage spacing, common wrap/overflow combinations, and selected spAutoFit growth. Font
availability remains part of visual-test provenance.
Tables
The common table path supports built-in and document table styles, first/last/banded options, variable row and column sizes, horizontal and vertical merges, cell margins and anchors, CJK/mixed text, conditional and merged borders, explicit border clearing, and direct cell overrides. The claim is bounded by an eight-case native PowerPoint matrix; uncommon producer quirks, diagonal borders, and arbitrary style combinations may still differ.
Charts
ECharts renders bar/column, line, area, pie, doughnut, radar, scatter, bubble, stock/candlestick, supported combo charts, and secondary axes. Sparse and literal sources, explicit zeros, common labels, legends, markers, and number formats are supported. Chart rendering remains approximate because Office plot-area, axis, label, and legend layout can differ. OOXML 3D charts fall back to a 2D representation where possible.
Equations
A bounded OMML subset renders as browser-native Presentation MathML: runs, bar/no-bar/skewed/linear fractions, radicals, subscript/superscript, delimiters, n-ary operators, matrices, and functions. The direct path is selected only when the complete math subtree is recognized; otherwise the package-authored MCE fallback shape or graphic frame is used. Per-token rich formula styling and unknown OMML constructs remain outside the direct subset. No formula-specific runtime dependency is required.
Effects and static 3D
Ordinary-shape outer shadows and reflections have native-validated lanes for the combinations in the tracked effect matrices. Other effect combinations use the existing approximation or flat fallback.
The static DrawingML 3D path parses scene, camera, lighting, bevel, contour, and material properties. Native-validated rendering covers selected top-bevel silhouettes, zero-depth camera planes, cropped pictures, live text, one custom-geometry family, one two-picture group, and selected edge-on bottom bevel material values. It uses SVG, Canvas, and projection math rather than a mesh engine. General extrusion, arbitrary cameras and lighting, arbitrary group scenes, 3D charts, and embedded Office 3D models are not supported.
SmartArt, media, groups, and compatible content
- SmartArt: renders available diagram fallback data; layout fidelity varies.
- Images: raster and SVG previews, crop and geometry clipping, common image effects, and browser-supported audio/video.
- OLE/EMF previews: uses package-provided bitmap or embedded-PDF previews when available; PDF previews require optional
pdfjs-dist. - Groups: recursively remaps child coordinates and preserves supported transforms.
- Compatible content: selects one supported
mc:Choice, otherwise itsmc:Fallback, while preserving branch order. - Backgrounds and color: resolves slide/layout/master backgrounds, theme colors, color maps, and common modifiers.
Architecture
Three-layer pipeline: Parse -> Model -> Render
ArrayBuffer (.pptx)
-> ZipParser (jszip extraction)
-> XmlParser (DOMParser + SafeXmlNode null-safe wrapper)
-> buildPresentation() (assembles slides/layouts/masters/themes with relationship chains)
-> SlideRenderer (background -> master shapes -> layout shapes -> slide shapes -> DOM)Key design decisions:
- SafeXmlNode: Null-safe XML traversal — returns empty nodes instead of null, enabling deep chaining without null checks.
- Lazy slide parsing: Optional
lazySlidesmode keeps per-slide nodes deferred until render, search, serialization, or explicit materialization. - Lazy group parsing: Group children stored as raw XML, parsed during rendering to avoid deep recursion in model layer.
- Error isolation: Per-node try/catch. A failed shape renders as a dashed-red placeholder; the slide continues.
- No external CSS: All styles inline. The library outputs self-contained HTML fragments.
- Blob URL lifecycle: Created for images/media, tracked in
mediaUrlCache, revoked ondestroy().
Performance
The default behavior stays eager for compatibility: parse the package, build the full model, and render according to the selected mode. For large or media-heavy decks, opt into the lazy/windowed path so the first visible slides can render without materializing every slide and every media entry up front.
Use this preset for interactive viewers:
const viewer = await PptxViewer.open(buffer, container, {
zipLimits: RECOMMENDED_ZIP_LIMITS,
lazySlides: true,
lazyMedia: true,
listOptions: {
windowed: true,
batchSize: 8,
initialSlides: 4,
overscanViewport: 1.5,
},
});Recommended choices:
| Scenario | Recommended options | Main benefit |
| ------------------------------------- | ------------------------------------------------------- | -------------------------------------------- |
| User-uploaded PPTX | zipLimits: RECOMMENDED_ZIP_LIMITS | Bounds ZIP parsing work and decoded payloads |
| Long scrollable viewer | listOptions.windowed: true | Keeps off-screen slides out of the DOM |
| Large decks with many slide elements | lazySlides: true plus windowed list rendering | Defers per-slide node parsing until needed |
| Media-heavy decks | lazyMedia: true plus windowed list rendering | Defers image/audio/video byte decoding |
| Export, print, or full comparison job | Eager defaults, or explicitly materialize before export | Ensures all slides are ready in one pass |
In local benchmarks, lazySlides reduced model build time by roughly 52-66% on medium
and large decks, and lowered first-window parse + build + render time by roughly 16-22%.
For media-heavy windowed viewers, lazyMedia reduced initially decompressed media bytes
by about 72-97%, depending on deck content. These options preserve rendering semantics;
they mainly move work from initial load to the moment a slide or media item is actually
needed.
Manual pipelines can use the same building blocks:
const files = await parseZipLazyMedia(buffer, RECOMMENDED_ZIP_LIMITS);
const presentation = buildPresentation(files, { lazySlides: true });
const viewer = new PptxViewer(container);
viewer.load(presentation);
await viewer.renderList({ windowed: true, initialSlides: 4 });Details: docs/PERFORMANCE.md
Security
- Treat PPTX input as untrusted. Start with
RECOMMENDED_ZIP_LIMITS, then tighten for your deployment. - External hyperlinks are protocol-filtered (no
javascript:,data:, etc.). - Reporting:
docs/SECURITY.md
Development
pnpm install
pnpm dev # Vite dev server
pnpm test # Unit tests (vitest)
pnpm test:coverage # Coverage report → coverage/
pnpm build # Production build
pnpm test:package # Verify package entries, packlist boundaries, and notice links
pnpm test:browser # Real Chromium checks for standalone, charts, and PDF.js
pnpm dev:e2e # Dev server + Python E2E API server
pnpm geometry:check # Verify pinned OOXML geometry compile/evaluate/emit output
pnpm lint # ESLint
pnpm typecheck # tsc --noEmit
pnpm knip # Dead code / unused exports detectionDev pages at http://127.0.0.1:5173:
| Page | Purpose |
| ------------------------------- | ----------------------------------------- |
| /test/pages/index.html | Upload preview with search and thumbnails |
| /test/pages/render-slide.html | Single slide at native resolution |
| /test/pages/e2e-compare.html | Side-by-side PDF vs HTML with SSIM scores |
| /test/pages/export.html | Model JSON tree viewer |
Documentation
| Doc | Content |
| ----------------------------------------- | ---------------------------------------------------------------------- |
| ARCHITECTURE.md | Parse/model/render pipeline design |
| PERFORMANCE.md | Tuning options and presets |
| TESTING.md | Unit/E2E strategy, two-layer metric system, visual regression workflow |
| CONTRIBUTING.md | PR checklist, code quality tools, and workflow |
| SECURITY.md | Vulnerability reporting |
What's Not Yet Supported
The renderer does not provide general DrawingML extrusion, true 3D charts, embedded Office 3D
models, animation playback or transitions, a complete OMML engine, arbitrary EMF/WMF vector
rendering, executable/editable OLE objects, or slide-note rendering. Unsupported 3D and effect
combinations retain a flat or approximate rendering path. Package-provided OLE, equation, and EMF
previews may still render when a compatible fallback is available. Exact bounded scopes are recorded
in test/e2e/oracle/capabilities.json.
FAQ
Does this run on Node.js? No. Rendering depends on browser DOM APIs.
Why is my PPTX rendering incomplete? OOXML is a vast spec. Please open a compatibility issue with a minimal PPTX sample — the visual regression pipeline makes it straightforward to add coverage for new cases.
How do I render 100+ slide decks efficiently?
Use windowed: true in listOptions, and enable lazySlides: true. For media-heavy
decks, also enable lazyMedia: true.
License
Apache License 2.0. See LICENSE.
