{"_id":"@aixa-transformation/aixa-deckrender","name":"@aixa-transformation/aixa-deckrender","dist-tags":{"latest":"1.3.0"},"versions":{"1.3.0":{"name":"@aixa-transformation/aixa-deckrender","version":"1.3.0","packageManager":"bun@1.3.14","description":"Aixa-DeckRender: a TypeScript-first engine from Aixa Transformation for parsing and rendering PPTX files in the browser.","author":"Aixa Transformation","license":"Apache-2.0","homepage":"https://github.com/Aixa-Transformation/Aixa-DeckRender#readme","repository":{"type":"git","url":"git+https://github.com/Aixa-Transformation/Aixa-DeckRender.git"},"bugs":{"url":"https://github.com/Aixa-Transformation/Aixa-DeckRender/issues"},"keywords":["aixa","deckrender","pptx","powerpoint","renderer","ooxml","slides"],"type":"module","main":"./dist/aixa-deckrender.cjs","module":"./dist/aixa-deckrender.es.js","types":"./dist/types/index.d.ts","exports":{".":{"import":{"types":"./dist/types/index.d.ts","default":"./dist/aixa-deckrender.es.js"},"require":{"types":"./dist/types/index.d.cts","default":"./dist/aixa-deckrender.cjs"}},"./browser":{"types":"./dist/types/index.d.ts","import":"./dist/aixa-deckrender.browser.es.js","default":"./dist/aixa-deckrender.browser.es.js"}},"sideEffects":["./src/renderer/chart/echartsRuntime.ts","./dist/aixa-deckrender.es.js","./dist/aixa-deckrender.cjs","./dist/aixa-deckrender.browser.es.js"],"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"scripts":{"dev":"vite","dev:demo":"vite --config vite.config.demo.ts --host 127.0.0.1","dev:api":"cd test/e2e && .venv/bin/python server.py","dev:e2e":"bash ./scripts/dev-e2e.sh","build":"node scripts/build.mjs","build:demo":"vite build --config vite.config.demo.ts","preview":"vite preview","test":"vitest run","test:package":"node scripts/verify-package.mjs","test:browser":"playwright test --config test/browser/playwright.config.ts","test:pdfjs-contract":"node scripts/verify-pdfjs-contract.mjs","test:coverage":"vitest run --coverage","test:watch":"vitest","lint":"eslint src/","lint:fix":"eslint src/ --fix","format":"prettier --write 'src/**/*.ts'","format:check":"prettier --check 'src/**/*.ts'","typecheck":"tsc --noEmit","publint":"publint","knip":"knip","size":"size-limit","prepublishOnly":"bun run build","prepare":"husky"},"size-limit":[{"path":"dist/aixa-deckrender.es.js","limit":"1400 kB","gzip":true},{"path":"dist/aixa-deckrender.browser.es.js","limit":"500 kB","gzip":true}],"lint-staged":{"src/**/*.ts":["eslint --fix","prettier --write"],"*.{json,md,html,css}":["prettier --write"]},"dependencies":{"echarts":"^6.0.0","jszip":"^3.10.1","mtx-decompressor":"^1.4.2"},"peerDependencies":{"pdfjs-dist":">=5 <7"},"peerDependenciesMeta":{"pdfjs-dist":{"optional":true}},"devDependencies":{"@commitlint/cli":"^20.4.2","@commitlint/config-conventional":"^20.4.2","@eslint/js":"^10.0.1","@playwright/test":"^1.61.1","@size-limit/file":"^12.0.0","@vitest/coverage-v8":"^3.2.4","canvas":"^3.2.1","eslint":"^10.0.2","eslint-config-prettier":"^10.1.8","husky":"^9.1.7","jsdom":"^26.0.0","knip":"^5.85.0","lint-staged":"^16.2.7","pdfjs-dist":"^5.4.624","pixelmatch":"^7.1.0","pngjs":"^7.0.0","prettier":"^3.8.1","publint":"^0.3.17","size-limit":"^12.0.0","typescript":"^5.7.0","typescript-eslint":"^8.56.1","vite":"^6.0.0","vitest":"^3.0.0"},"_id":"@aixa-transformation/aixa-deckrender@1.3.0","_integrity":"sha512-Oq77QpDMRxTbCZs3Dsxyw0ZEj6WT/Otw98pIlzs5o5cqnZ0P5Nit7eYcxRc9xnw5z1Nao7Q26jcaYBro7uCARw==","_nodeVersion":"24.3.0","_npmVersion":"10.8.3","shasum":"fc41e8cd2c39509219fc1149844024feee5c5431","dist":{"integrity":"sha512-Oq77QpDMRxTbCZs3Dsxyw0ZEj6WT/Otw98pIlzs5o5cqnZ0P5Nit7eYcxRc9xnw5z1Nao7Q26jcaYBro7uCARw==","shasum":"fc41e8cd2c39509219fc1149844024feee5c5431","tarball":"https://registry.npmjs.org/@aixa-transformation/aixa-deckrender/-/aixa-deckrender-1.3.0.tgz","fileCount":78,"unpackedSize":2833225,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCSc02EsL0Skqf3xNSXV9mJb1hSKTCXDEYV/KAbVvJSaQIhAOc/hzSx9XL50o8eHNkk0NuA0hmqwHtyZMCyYnm55yQa"}]},"_npmUser":{"name":"naveen-aixa","email":"naveen@aixa-transformation.com"},"directories":{},"maintainers":[{"name":"suraj-aixa","email":"suraj@aixa-transformation.com"},{"name":"naveen-aixa","email":"naveen@aixa-transformation.com"},{"name":"salberth","email":"salberth@aixa-transformation.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/aixa-deckrender_1.3.0_1788574581622_0.8389827880840652"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-05T02:16:21.397Z","1.3.0":"2026-09-05T02:16:21.840Z","modified":"2026-09-05T02:16:22.085Z"},"maintainers":[{"name":"suraj-aixa","email":"suraj@aixa-transformation.com"},{"name":"naveen-aixa","email":"naveen@aixa-transformation.com"},{"name":"salberth","email":"salberth@aixa-transformation.com"}],"description":"Aixa-DeckRender: a TypeScript-first engine from Aixa Transformation for parsing and rendering PPTX files in the browser.","homepage":"https://github.com/Aixa-Transformation/Aixa-DeckRender#readme","keywords":["aixa","deckrender","pptx","powerpoint","renderer","ooxml","slides"],"repository":{"type":"git","url":"git+https://github.com/Aixa-Transformation/Aixa-DeckRender.git"},"author":"Aixa Transformation","bugs":{"url":"https://github.com/Aixa-Transformation/Aixa-DeckRender/issues"},"license":"Apache-2.0","readme":"# Aixa-DeckRender\n\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue)](LICENSE) [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![Node](https://img.shields.io/badge/Node.js-≥20-339933?logo=node.js&logoColor=white)](https://nodejs.org/)\n\nAixa-DeckRender is Aixa's browser-native PPTX rendering library. It parses Office Open XML (`.pptx`) files and renders slides as HTML/SVG DOM.\n\nSupports shapes, text, images, tables, charts, SmartArt, groups, backgrounds, gradients, pattern fills, and the full OOXML color pipeline — covering the vast majority of real-world PowerPoint content.\n\n## Rendering Example\n\nA complex slide with charts, text styles, shapes, and SmartArt — PowerPoint ground truth vs browser-rendered output:\n\n<table>\n<tr>\n<th>PowerPoint (Ground Truth)</th>\n<th>Aixa-DeckRender (Browser)</th>\n</tr>\n<tr>\n<td><img src=\"docs/example/1-chart-and-complex/slides/Slide1.png\" alt=\"PowerPoint ground truth\" width=\"480\" /></td>\n<td><img src=\"docs/example/1-chart-and-complex/rendered-result.png\" alt=\"Aixa-DeckRender output\" width=\"480\" /></td>\n</tr>\n</table>\n\n## Visual Regression Testing\n\nThe visual regression suite compares browser rendering against PowerPoint output. Its **452+ cases** cover 187+ preset shapes, 134+ SmartArt layouts, 36+ fill/stroke/gradient variants, and 101 python-pptx cases (text, shape adjustments, composites, charts). Run the suite locally to verify the current checkout.\n\n<img src=\"docs/example/e2e-test-page.png\" alt=\"E2E evaluation dashboard\" width=\"800\" />\n\n<sup>E2E evaluation dashboard: side-by-side ground truth vs rendered output with SSIM, color histogram, and IoU metrics per slide.</sup>\n\n> Ground truth data (PPTX + PDF pairs) is not committed to the repository due to file size. It can be regenerated locally via `scripts/one_shot_full_ground_truth.py` with Microsoft PowerPoint installed (macOS and Windows both supported) — see [`docs/TESTING.md`](docs/TESTING.md) for details.\n\n## Install\n\nThe npm package is `@aixa-transformation/aixa-deckrender` under the Aixa\nTransformation organization.\n\n```bash\nbun add @aixa-transformation/aixa-deckrender\n```\n\nTo build and pack this repository locally, use Bun 1.3.14 and Node.js 20+:\n\n```bash\ngit clone https://github.com/Aixa-Transformation/Aixa-DeckRender.git\ncd Aixa-DeckRender\nbun install --frozen-lockfile\nbun run build\nbun pm pack\n```\n\nInstall the generated tarball into your application (replace the path and version with\nthe filename printed by `bun pm pack`):\n\n```bash\nbun add /path/to/Aixa-DeckRender/aixa-transformation-aixa-deckrender-1.3.0.tgz\n\n# Optional: only needed for SmartArt / EMF files with embedded PDF fallback previews\nbun add pdfjs-dist\n```\n\nBun 1.3.14 manages dependencies and package scripts. The existing build and test tools\nstill use Node.js 20+; the renderer itself runs in the browser.\n\n## Quick Start\n\nFor bundlers and npm-based apps:\n\n```ts\nimport { PptxViewer, RECOMMENDED_ZIP_LIMITS } from '@aixa-transformation/aixa-deckrender';\n\nconst container = document.getElementById('pptx-container')!;\nconst resp = await fetch('/slides/demo.pptx');\n\n// One-liner: parse, build model, and render\nconst viewer = await PptxViewer.open(await resp.arrayBuffer(), container, {\n  zipLimits: RECOMMENDED_ZIP_LIMITS,\n  listOptions: { windowed: true },\n});\n```\n\nFor direct browser usage without a bundler, import the standalone browser ESM build. This\nentry bundles JSZip and ECharts, and replaces Node-style `process.env` checks at build time.\nPDF.js remains optional and is only needed for EMF-embedded PDF fallback previews:\n\n```html\n<script type=\"module\">\n  import {\n    PptxViewer,\n    RECOMMENDED_ZIP_LIMITS,\n  } from '/vendor/aixa-deckrender/aixa-deckrender.browser.es.js';\n\n  const container = document.getElementById('pptx-container');\n  const resp = await fetch('/slides/demo.pptx');\n  await PptxViewer.open(await resp.arrayBuffer(), container, {\n    zipLimits: RECOMMENDED_ZIP_LIMITS,\n  });\n</script>\n```\n\nCopy `dist/aixa-deckrender.browser.es.js` from your local build into your application's\nversioned static assets at the path used above. The entry bundles JSZip and the ECharts\nchart types supported by this renderer; PDF.js remains an optional external asset.\n\nFor large decks, combine windowed mounting with on-demand slide parsing and media decoding:\n\n```ts\nconst viewer = await PptxViewer.open(buffer, container, {\n  zipLimits: RECOMMENDED_ZIP_LIMITS,\n  lazySlides: true,\n  lazyMedia: true,\n  listOptions: { windowed: true, initialSlides: 4, batchSize: 4 },\n});\n```\n\n### Optional PDF.js Fallback for SmartArt/EMF Preview Images\n\nPowerPoint often stores SmartArt or pasted vector artwork as EMF fallback images. This\nlibrary does **not** implement a full EMF/WMF vector renderer. It can render the common\nOffice fallback cases where an EMF contains an embedded PDF preview or bitmap preview.\n\nFor EMF files with embedded PDF previews, install `pdfjs-dist` and pass explicit asset\nURLs. This keeps PDF.js optional and avoids forcing every consumer bundle to include it.\nThis is only needed for EMF-PDF fallback previews; ordinary PPTX rendering does not\nrequire any code changes.\n\n```ts\nimport { PptxViewer } from '@aixa-transformation/aixa-deckrender';\n\nconst pdfjs = {\n  moduleUrl: new URL('pdfjs-dist/build/pdf.min.mjs', import.meta.url).toString(),\n  workerUrl: new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(),\n};\n\nconst viewer = await PptxViewer.open(buffer, container, {\n  pdfjs,\n});\n```\n\nIf your app uses a CDN or pre-copied assets, point those fields at your hosted files.\nSet `pdfjs: false` to disable EMF-PDF fallback rendering entirely. With no `pdfjs`\nconfiguration, the renderer attempts only a best-effort automatic resolution and\notherwise degrades gracefully.\n\n```ts\ntype PdfjsConfig =\n  | {\n      moduleUrl?: string;\n      workerUrl?: string;\n    }\n  | false;\n```\n\nFor a no-bundler deployment, copy the two PDF.js files into your application's static\nassets and pass absolute self-hosted URLs:\n\n```ts\nconst pdfjs = {\n  moduleUrl: '/vendor/pdfjs/pdf.min.mjs',\n  workerUrl: '/vendor/pdfjs/pdf.worker.min.mjs',\n};\n```\n\nThe PDF fallback uses short-lived blob Workers. A restrictive Content Security Policy\nmust allow the chosen asset origin and `blob:` Workers, for example:\n\n```text\nscript-src 'self';\nworker-src 'self' blob:;\nimg-src 'self' data: blob:;\n```\n\nIf a CDN is used instead, pin exact package versions and add only that origin to the\nrelevant CSP directives.\n\nOr with more control over each step:\n\n```ts\nimport {\n  PptxViewer,\n  parseZip,\n  buildPresentation,\n  RECOMMENDED_ZIP_LIMITS,\n} from '@aixa-transformation/aixa-deckrender';\n\nconst container = document.getElementById('pptx-container')!;\nconst viewer = new PptxViewer(container, { fitMode: 'contain' });\n\nconst files = await parseZip(arrayBuffer, RECOMMENDED_ZIP_LIMITS);\nconst presentation = buildPresentation(files);\nviewer.load(presentation);\nawait viewer.renderList({ windowed: true, batchSize: 8 });\n```\n\nTo delay both media decompression and slide node parsing until rendered slides actually need\nthem, use `parseZipLazyMedia()` and pass `{ lazySlides: true }` to `buildPresentation()`:\n\n```ts\nimport {\n  PptxViewer,\n  parseZipLazyMedia,\n  buildPresentation,\n  RECOMMENDED_ZIP_LIMITS,\n} from '@aixa-transformation/aixa-deckrender';\n\nconst files = await parseZipLazyMedia(arrayBuffer, RECOMMENDED_ZIP_LIMITS);\nconst presentation = buildPresentation(files, { lazySlides: true });\n\nconst viewer = new PptxViewer(container);\nviewer.load(presentation);\nawait viewer.renderList({ windowed: true, initialSlides: 4 });\n```\n\n## API\n\n### `PptxViewer` (primary, extends `EventTarget`)\n\n#### `PptxViewer.open(input, container, options?)` — Static Factory\n\nParse, build, and render in one call. Returns a `Promise<PptxViewer>`.\n\n```ts\nconst viewer = await PptxViewer.open(buffer, container, {\n  renderMode: 'list', // 'list' (default) | 'slide'\n  zipLimits: RECOMMENDED_ZIP_LIMITS,\n  listOptions: { windowed: true, batchSize: 8 },\n  signal: abortController.signal, // optional AbortSignal\n  // ...ViewerOptions\n});\n```\n\n#### `new PptxViewer(container, options?)`\n\n| Option               | Type                       | Default       | Description                                                                                                       |\n| -------------------- | -------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------- |\n| `width`              | `number`                   | --            | Container width hint (omit for auto-detect)                                                                       |\n| `fitMode`            | `'contain' \\| 'none'`      | `'contain'`   | Responsive fit or fixed size                                                                                      |\n| `zoomPercent`        | `number`                   | `100`         | Zoom level (10–400)                                                                                               |\n| `scrollContainer`    | `HTMLElement`              | --            | Scroll container for IntersectionObserver root                                                                    |\n| `zipLimits`          | `ZipParseLimits`           | --            | Security limits for ZIP parsing (used by `.open()`). Use `RECOMMENDED_ZIP_LIMITS` for untrusted input.            |\n| `lazyMedia`          | `boolean`                  | `false`       | Decode embedded media on demand instead of during ZIP parsing. Best for large decks with windowed list rendering. |\n| `lazySlides`         | `boolean`                  | `false`       | Parse slide shape/table/chart nodes on demand. Best for large decks with windowed list rendering.                 |\n| `pdfjs`              | `PdfjsConfig`              | --            | Optional PDF.js URLs for EMF-embedded PDF fallback rendering, or `false` to disable it.                           |\n| `embeddedFontLimits` | `EmbeddedFontLimits`       | safe defaults | Optional embedded-font resource limit overrides. Omitted fields retain the built-in defaults.                     |\n| `onSlideChange`      | `(index) => void`          | --            | Shorthand for `slidechange` event                                                                                 |\n| `onSlideRendered`    | `(index, element) => void` | --            | Shorthand for `sliderendered` event                                                                               |\n| `onSlideError`       | `(index, error) => void`   | --            | Shorthand for `slideerror` event                                                                                  |\n| `onSlideUnmounted`   | `(index) => void`          | --            | Shorthand for `slideunmounted` event                                                                              |\n| `onNodeError`        | `(nodeId, error) => void`  | --            | Shorthand for `nodeerror` event                                                                                   |\n| `onRenderStart`      | `() => void`               | --            | Shorthand for `renderstart` event                                                                                 |\n| `onRenderComplete`   | `() => void`               | --            | Shorthand for `rendercomplete` event                                                                              |\n\nAll shorthand callbacks are also available as `EventTarget` events (e.g. `viewer.addEventListener('slidechange', ...)`).\n\nEmbedded font decompression is bounded by default. Trusted applications can provide partial\n`embeddedFontLimits` overrides; see the [performance guide](docs/PERFORMANCE.md#embedded-font-limits)\nfor defaults, examples, and the soft processing-time boundary.\n\n#### Instance Methods\n\n```ts\nviewer.load(presentation);                              // Load a PresentationData model (no render)\nawait viewer.renderList({ windowed: true });             // Render all slides in scrollable list\nawait viewer.renderSlide(0);                             // Render a single slide (no built-in nav UI)\n\n// Load from binary input (parse → build → render). Cleans up previous state on re-open.\nawait viewer.open(buffer, { renderMode: 'list', signal: abortController.signal });\n\nawait viewer.goToSlide(index);                           // Jump to slide (0-based), returns Promise<void>\nawait viewer.goToSlide(index, { behavior: 'instant' }); // Custom ScrollIntoViewOptions (list mode)\nawait viewer.setZoom(150);                               // Runtime zoom (10–400)\nawait viewer.setFitMode('none');                         // Switch fit mode\nconst matches = viewer.searchText('GPU');                 // Search parsed model text\nconst hit = await viewer.highlightSearchResult(matches[0]); // Default node overlay highlight\nhit?.dispose();\nviewer.clearSearchHighlights();                          // Remove active search overlays\n\n// Render a single slide into an external container (React/Vue integration, thumbnails).\n// Returns a SlideHandle; caller owns it and must call handle.dispose() when done.\nconst handle = viewer.renderSlideToContainer(index, container, scale?);\nhandle.dispose();                                        // Clean up slide-specific resources\n\n// Render a lightweight scaled slide preview into an external container.\n// This preserves the original slide layout and uses transform scaling; it is\n// not a bitmap thumbnail generator, so use lazy/windowed mounting for decks.\nconst thumb = viewer.renderThumbnailToContainer(index, sidebarItem, { width: 180 });\nawait thumb?.ready;\nthumb?.dispose();\n\n// Query which slides are currently mounted in the DOM\nviewer.isSlideMounted(index);   // boolean\nviewer.getMountedSlides();      // number[] (sorted)\n\n// Typed event helpers (return `this` for chaining)\nviewer.on('slidechange', (e) => console.log(e.detail.index));\nviewer.off('slidechange', listener);\n\nviewer.destroy();               // Cleanup blob URLs, observers, and DOM\nviewer[Symbol.dispose]();       // TC39 Explicit Resource Management (calls destroy)\n```\n\n#### Text Search\n\n`PptxViewer.searchText(query, options?)` searches the parsed `PresentationData` model,\nnot the rendered DOM. This keeps search available before or after a slide is mounted and\navoids mutating renderer-generated text runs.\n\nString queries are case-insensitive by default. Pass `matchCase: true` when you need\nexact casing. RegExp queries keep their own flags, so `/GPU/` remains case-sensitive\nand `/GPU/i` remains case-insensitive.\n\n```ts\nimport type { TextSearchResult } from '@aixa-transformation/aixa-deckrender';\n\nconst matches: TextSearchResult[] = viewer.searchText('GPU', {\n  matchCase: false,\n  wholeWord: true,\n  snippetRadius: 48,\n});\n\nconst exactMatches = viewer.searchText('GPU', { matchCase: true });\nconst regexMatches = viewer.searchText(/GPU|CPU/i);\n\nfor (const match of matches) {\n  await viewer.goToSlide(match.slideIndex, { behavior: 'smooth', block: 'center' });\n  // Use match.bounds for node-level highlight overlays in your own UI.\n}\n```\n\nEach `TextSearchResult` includes `slideIndex`, `nodeId`, `nodePath`, `nodeType`,\n`textKind`, full `text`, `matchStart`, `matchEnd`, `snippet`, and `bounds`.\n`bounds` is the matched shape or table bounds in intrinsic slide coordinates, so\napplication code can draw node-level highlight overlays on top of a rendered slide.\n\nThe renderer intentionally does not rewrite text nodes for character-level text highlighting.\nCharacter-level text highlighting would require mapping match offsets back to shaped Office\ntext runs and line layout, which is a separate, higher-risk renderer feature. Today the\nstable API boundary is model-level search plus node-level bounds.\n\nFor the common UI case, `highlightSearchResult(result, options?)` draws a node-level\noverlay using a default highlight style. Pass `SearchHighlightOptions` for custom colors,\nspacing, shadows, and class names:\n\n```ts\nconst hit = await viewer.highlightSearchResult(matches[0], {\n  className: 'my-search-hit',\n  borderColor: '#22c55e',\n  backgroundColor: 'rgba(34, 197, 94, 0.18)',\n  borderRadius: 6,\n  borderWidth: 2,\n  boxShadow: '0 0 0 2px rgba(15, 23, 42, 0.35)',\n  padding: 3,\n});\n\n// The caller owns returned highlight handles.\nhit?.dispose();\nviewer.clearSearchHighlights();\n```\n\n#### Scaled Slide Previews\n\n`PptxViewer.renderThumbnailToContainer(index, container, options?)` renders a slide at\nits intrinsic layout size and applies CSS transform scaling inside a clipped wrapper.\nThis avoids the layout drift that can happen if a PPTX slide is rendered directly into a\nsmall thumbnail-sized container.\n\n```ts\nconst thumb = viewer.renderThumbnailToContainer(slideIndex, thumbnailEl, { width: 96 });\nawait thumb?.ready;\n\n// The caller owns externally rendered previews.\nthumb?.dispose();\n```\n\nThis is not a bitmap thumbnail generator: it still creates a scaled DOM/SVG slide\npreview, so large decks should mount previews lazily with `IntersectionObserver` or a\nwindowed list and dispose handles when they scroll out of view.\n\n#### PowerPoint Layout Thumbnails\n\nUse `getLayouts()` to list every layout, including layouts unused by slides. Each\nentry has a stable `id` (its OOXML part path), `name`, and optional `type`,\n`masterPath`, and `masterName` (falling back to the design's theme name). Layouts follow each master's declared order, with\nunlisted layouts included afterward.\n\n```ts\nconst layouts = viewer.getLayouts();\nconst preview = viewer.renderLayoutThumbnailToContainer(layouts[0].id, thumbnailEl, {\n  width: 180,\n  showPlaceholderGuides: true, // Default: show editable-placeholder outlines.\n});\nawait preview?.ready;\npreview?.dispose();\n```\n\nThese previews render the layout's actual background, master artwork, images,\nand editable-placeholder guides. The layout stays at its native dimensions and is scaled for\nthe thumbnail. The returned handle belongs to the caller; dispose external\npreviews before destroying the viewer or loading another presentation.\n\nFor use without a Viewer, render a layout directly from the parsed presentation:\n\n```ts\nimport { getLayouts, renderLayout } from '@aixa-transformation/aixa-deckrender';\n\nconst [layout] = getLayouts(presentation);\nconst preview = renderLayout(presentation, layout.id, { showPlaceholderGuides: true });\nthumbnailEl.appendChild(preview.element);\nawait preview.ready; // Includes asynchronous images and embedded fonts.\n// Later:\npreview.dispose();\npreview.element.remove();\n```\n\n`renderLayout` returns the same `SlideHandle` contract as `renderSlide` and throws\nfor an unknown layout ID. The Viewer method returns `null` when no presentation\nis loaded or the ID does not exist.\n\nPrefer the Viewer method for thumbnails; it keeps guide strokes readable at the\nchosen size. If you apply your own CSS transform to a direct `renderLayout`\nresult, pass that same scale as `guideScale` (default `1`).\n\n#### `ListRenderOptions`\n\n| Option             | Type      | Default | Description                        |\n| ------------------ | --------- | ------- | ---------------------------------- |\n| `windowed`         | `boolean` | `false` | Use IntersectionObserver windowing |\n| `batchSize`        | `number`  | `12`    | Slides per render batch            |\n| `initialSlides`    | `number`  | `4`     | Initial slides to mount (windowed) |\n| `overscanViewport` | `number`  | `1.5`   | Viewport overscan multiplier       |\n\n#### `ZipParseLimits` and Resource Safety\n\n`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:\n\n| Limit                       | Recommended value | What it protects                            |\n| --------------------------- | ----------------- | ------------------------------------------- |\n| `maxEntries`                | `4000`            | Archives with excessive file counts         |\n| `maxEntryUncompressedBytes` | `32 MiB`          | A single oversized XML/media entry          |\n| `maxTotalUncompressedBytes` | `256 MiB`         | Total decompressed archive size             |\n| `maxMediaBytes`             | `192 MiB`         | Total media payload size under `ppt/media/` |\n| `maxConcurrency`            | `8`               | Parallel ZIP entry reads                    |\n\nWhen 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.\n\nRenderer-level guards also apply after ZIP parsing:\n\n- Chart caches do not allocate from oversized `c:ptCount`; chart point indexes are capped at `10,000` per cache.\n- EMF bitmap previews are rejected when decoded size exceeds `16,777,216` pixels, dimensions exceed `8192x8192`, or pixel payload is shorter than the declared bitmap.\n- External audio/video relationships only render for safe `http`/`https` URLs with `TargetMode=\"External\"` and are created with `preload=\"none\"`.\n\n#### Events (`PptxViewerEventMap`)\n\n```ts\nviewer.addEventListener('renderstart', () => {\n  /* render cycle began */\n});\nviewer.addEventListener('rendercomplete', () => {\n  /* render cycle finished (fires even on error) */\n});\nviewer.addEventListener('slidechange', (e) => console.log(e.detail.index));\nviewer.addEventListener('sliderendered', (e) => console.log(e.detail.index, e.detail.element));\nviewer.addEventListener('slideerror', (e) => console.error(e.detail.index, e.detail.error));\nviewer.addEventListener('slideunmounted', (e) => console.log(e.detail.index));\nviewer.addEventListener('nodeerror', (e) => console.warn(e.detail.nodeId, e.detail.error));\n```\n\n`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.\n\n#### Instance Properties (read-only)\n\n```ts\nviewer.presentationData; // PresentationData | null — the parsed model, null before load()\nviewer.slideCount; // number — total slides (0 if not loaded)\nviewer.slideWidth; // number — intrinsic slide width in px\nviewer.slideHeight; // number — intrinsic slide height in px\nviewer.currentSlideIndex; // number — currently active slide (0-based)\nviewer.isRendering; // boolean — true between renderstart and rendercomplete\nviewer.zoomPercent; // number — current zoom level (e.g. 100, 200)\nviewer.fitMode; // FitMode — current fit mode ('contain' | 'none')\n```\n\n### `PptxRenderer` (deprecated v1 compat)\n\n`PptxRenderer` extends `PptxViewer` and provides the legacy `preview(input)` API with built-in nav buttons in slide mode. Migrate to `PptxViewer` for new code.\n\n```ts\nimport { PptxRenderer } from '@aixa-transformation/aixa-deckrender';\n\nconst renderer = new PptxRenderer(container, { mode: 'list', listMountStrategy: 'windowed' });\nawait renderer.preview(buffer); // deprecated — use PptxViewer.open() instead\n```\n\n`PptxRenderer` accepts the same optional `lazyMedia`, `lazySlides`, and `pdfjs`\nconfiguration as `PptxViewer`, so legacy users can enable performance options or\nEMF-PDF fallback rendering without changing APIs.\n\n### Utility Exports\n\n```ts\nimport {\n  parseZip,\n  parseZipLazyMedia,\n  buildPresentation,\n  materializeAllSlideNodes,\n  serializePresentation,\n  buildTextIndex,\n  searchText,\n  searchPresentation,\n  RECOMMENDED_ZIP_LIMITS,\n} from '@aixa-transformation/aixa-deckrender';\n\nconst files = await parseZip(arrayBuffer, RECOMMENDED_ZIP_LIMITS); // PptxFiles\nconst lazyFiles = await parseZipLazyMedia(arrayBuffer, RECOMMENDED_ZIP_LIMITS); // media resolves on demand\nconst presentation = buildPresentation(files); // PresentationData\nconst lazyPresentation = buildPresentation(lazyFiles, { lazySlides: true }); // slide nodes parse on demand\nmaterializeAllSlideNodes(lazyPresentation); // optional: force full model materialization\nconst json = serializePresentation(presentation); // SerializedPresentation (JSON-safe)\nconst index = buildTextIndex(presentation); // TextIndexEntry[]\nconst matches = searchText(index, '算力'); // TextSearchResult[]\nconst directMatches = searchPresentation(presentation, /GPU|CPU/i); // TextSearchResult[]\n```\n\n#### Headless Slide Rendering\n\nFor advanced use cases (server-side screenshot, custom rendering pipeline):\n\n```ts\nimport { renderSlide } from '@aixa-transformation/aixa-deckrender';\nimport type { SlideHandle } from '@aixa-transformation/aixa-deckrender';\n\nconst handle = renderSlide(presentation, presentation.slides[0], {\n  onNodeError: (nodeId, err) => console.warn(nodeId, err),\n  mediaUrlCache: new Map(), // optional shared cache for blob URLs\n  pdfjs, // optional, only for EMF-embedded PDF fallback rendering\n});\ndocument.body.appendChild(handle.element);\n\n// Await async media such as EMF-PDF fallback previews before screenshots/exports.\nawait handle.ready;\n\n// Cancels pending PDF fallbacks and disposes charts + owned blob URLs.\nhandle.dispose();\n```\n\n#### Model Types\n\nAll model types are exported for consumers building custom tooling:\n\n```ts\nimport type {\n  PresentationData,\n  BuildPresentationOptions,\n  SlideData,\n  SlideNode,\n  ThemeData,\n  BaseNodeData,\n  ShapeNodeData,\n  PicNodeData,\n  TableNodeData,\n  GroupNodeData,\n  ChartNodeData,\n  TextBody,\n  TextParagraph,\n  TextRun,\n  Position,\n  Size,\n  NodeType,\n  SerializedPresentation,\n  SerializedSlide,\n  SerializedNode,\n  PptxFiles,\n  ZipParseLimits,\n  FitMode,\n  PreviewInput,\n  ViewerOptions,\n  ListRenderOptions,\n  ThumbnailRenderOptions,\n  LayoutInfo,\n  LayoutRendererOptions,\n  SearchHighlightHandle,\n  SearchHighlightOptions,\n  PptxViewerEventMap,\n  SlideHandle,\n  PdfjsOptions,\n  PdfjsConfig,\n  TextBounds,\n  TextIndexEntry,\n  TextIndexOptions,\n  TextSearchOptions,\n  TextSearchResult,\n} from '@aixa-transformation/aixa-deckrender';\n```\n\n## Rendering Capabilities\n\n### Shapes — 187+ Presets + Custom Geometry\n\nAll commonly used OOXML `DrawingML` preset shapes, organized by category:\n\n| Category          | Count | Highlights                                                     |\n| ----------------- | ----: | -------------------------------------------------------------- |\n| Basic & Geometric |    70 | Rectangles, ovals, polygons, stars, arcs, clouds, gears, etc.  |\n| Flowchart         |    30 | All standard flowchart shapes                                  |\n| Arrows            |    22 | Directional, bent, curved, striped, chevron                    |\n| Stars & Banners   |    17 | N-point stars, explosions, ribbons, scrolls                    |\n| Callouts          |    17 | Rectangular, rounded, oval, cloud, line callout variants       |\n| Connectors        |    12 | Straight, bent, curved (2-5 segments)                          |\n| Action Buttons    |     9 | Multi-path 3D with darken/lighten face modifiers               |\n| Math & Brackets   |    12 | Plus, minus, multiply, division, brackets, braces              |\n| **Multi-path 3D** |   33+ | Bevel, cube, can, ribbons — multi-layer SVG with 3D appearance |\n\nCustom geometry (`<a:custGeom>`) is also supported via a general-purpose OOXML path interpreter.\n\n### Text — 7-Level Style Inheritance\n\nFull OOXML text cascade: master → layout → shape → paragraph → run. Supports theme fonts, numbered/symbol/picture bullets, multi-level indent, vertical text, superscript/subscript, hyperlinks, and per-shape text insets.\n\nMatching PowerPoint line breaks and typography requires the authored fonts to be available\nto the browser. Load your application's webfont faces before rendering, or use supported\nfonts embedded in the presentation. Fonts available only inside Office may need separate\nwebfont setup. Fallback faces can change text width, weight, and baselines.\n\n### Charts via ECharts\n\nPowered by [ECharts](https://echarts.apache.org/). Supports Bar/Column (clustered, stacked, 100% stacked), Line/Area (standard, stacked, 100% stacked), Pie, multi-ring Doughnut, Radar, Scatter, Bubble, and Stock/Candlestick charts, with axis labels, legends, data labels, grid lines, chart color-style palettes, marker symbols, and custom number formats.\n\nThe renderer registers only the ECharts charts, components, features, and Canvas renderer\nthat it uses. Bundler consumers keep ECharts external; the standalone browser entry\ncontains this same tree-shakeable runtime.\n\nOOXML 3D chart elements such as `bar3DChart`, `line3DChart`, `pie3DChart`, `area3DChart`, and `surface3DChart` are parsed as graceful 2D fallbacks where possible. True 3D perspective, depth walls, and surface meshes are not PowerPoint-perfect.\n\n### Fill, Stroke & Color\n\n- **Fills**: solid, linear/radial/rectangular gradient, 52+ pattern fills, image (stretch/tile)\n- **Strokes**: 8 dash styles, 5 arrowhead types, compound lines, line joins\n- **Colors**: full OOXML pipeline — `schemeClr` → `colorMap` remap → theme lookup → modifiers (lumMod, lumOff, tint, shade, alpha, satMod, etc.). All 6 color spaces supported.\n\n### SmartArt, Tables, Images & More\n\n- **SmartArt**: 134+ layouts via PowerPoint fallback data. EMF-embedded PDF previews can be rendered with optional [pdfjs-dist](https://mozilla.github.io/pdf.js/) configuration.\n- **Tables**: OOXML table styles, cell merge, border inheritance\n- **Images**: blob URL with crop, stretch/tile, video/audio placeholders\n- **Groups**: coordinate remapping with recursive child rendering\n- **Backgrounds**: slide → layout → master inheritance chain\n\n## Architecture\n\nThree-layer pipeline: **Parse -> Model -> Render**\n\n```\nArrayBuffer (.pptx)\n  -> ZipParser (jszip extraction)\n  -> XmlParser (DOMParser + SafeXmlNode null-safe wrapper)\n  -> buildPresentation() (assembles slides/layouts/masters/themes with relationship chains)\n  -> SlideRenderer (background -> master shapes -> layout shapes -> slide shapes -> DOM)\n```\n\nKey design decisions:\n\n- **SafeXmlNode**: Null-safe XML traversal — returns empty nodes instead of null, enabling deep chaining without null checks.\n- **Lazy slide parsing**: Optional `lazySlides` mode keeps per-slide nodes deferred until render, search, serialization, or explicit materialization.\n- **Lazy group parsing**: Group children stored as raw XML, parsed during rendering to avoid deep recursion in model layer.\n- **Error isolation**: Per-node try/catch. A failed shape renders as a dashed-red placeholder; the slide continues.\n- **No external CSS**: All styles inline. The library outputs self-contained HTML fragments.\n- **Blob URL lifecycle**: Created for images/media, tracked in `mediaUrlCache`, revoked on `destroy()`.\n\n## Performance\n\nThe default behavior stays eager for compatibility: parse the package, build the full\nmodel, and render according to the selected mode. For large or media-heavy decks, opt\ninto the lazy/windowed path so the first visible slides can render without materializing\nevery slide and every media entry up front.\n\nUse this preset for interactive viewers:\n\n```ts\nconst viewer = await PptxViewer.open(buffer, container, {\n  zipLimits: RECOMMENDED_ZIP_LIMITS,\n  lazySlides: true,\n  lazyMedia: true,\n  listOptions: {\n    windowed: true,\n    batchSize: 8,\n    initialSlides: 4,\n    overscanViewport: 1.5,\n  },\n});\n```\n\nRecommended choices:\n\n| Scenario                              | Recommended options                                     | Main benefit                                 |\n| ------------------------------------- | ------------------------------------------------------- | -------------------------------------------- |\n| User-uploaded PPTX                    | `zipLimits: RECOMMENDED_ZIP_LIMITS`                     | Bounds ZIP parsing work and decoded payloads |\n| Long scrollable viewer                | `listOptions.windowed: true`                            | Keeps off-screen slides out of the DOM       |\n| Large decks with many slide elements  | `lazySlides: true` plus windowed list rendering         | Defers per-slide node parsing until needed   |\n| Media-heavy decks                     | `lazyMedia: true` plus windowed list rendering          | Defers image/audio/video byte decoding       |\n| Export, print, or full comparison job | Eager defaults, or explicitly materialize before export | Ensures all slides are ready in one pass     |\n\nIn local benchmarks, `lazySlides` reduced model build time by roughly 52-66% on medium\nand large decks, and lowered first-window parse + build + render time by roughly 16-22%.\nFor media-heavy windowed viewers, `lazyMedia` reduced initially decompressed media bytes\nby about 72-97%, depending on deck content. These options preserve rendering semantics;\nthey mainly move work from initial load to the moment a slide or media item is actually\nneeded.\n\nManual pipelines can use the same building blocks:\n\n```ts\nconst files = await parseZipLazyMedia(buffer, RECOMMENDED_ZIP_LIMITS);\nconst presentation = buildPresentation(files, { lazySlides: true });\n\nconst viewer = new PptxViewer(container);\nviewer.load(presentation);\nawait viewer.renderList({ windowed: true, initialSlides: 4 });\n```\n\nDetails: [`docs/PERFORMANCE.md`](docs/PERFORMANCE.md)\n\n## Security\n\n- Treat PPTX input as untrusted. Start with `RECOMMENDED_ZIP_LIMITS`, then tighten for your deployment.\n- External hyperlinks are protocol-filtered (no `javascript:`, `data:`, etc.).\n- Reporting: [`docs/SECURITY.md`](docs/SECURITY.md)\n\n## Development\n\nUse Bun 1.3.14 and Node.js 20+. Install from the committed `bun.lock` with\n`bun install --frozen-lockfile`. Run unit tests with `bun run test`, which invokes\nVitest; `bun test` invokes Bun's separate test runner and is not used by this project.\n\n```bash\nbun install --frozen-lockfile\nbun run dev          # Renderer development and evaluation pages\nbun run dev:demo     # Live demo with the bundled sample deck\nbun run test         # Unit tests (vitest)\nbun run test:coverage # Coverage report → coverage/\nbun run build        # Production build\nbun run build:demo   # Static demo build, configured for /Aixa-DeckRender/\nbun pm pack         # Create an installable Aixa Transformation scoped tarball\nbun run test:package # Verify ESM, CJS, and standalone package entries\nbun run test:browser # Real Chromium checks for standalone, charts, and PDF.js\nbun run dev:e2e      # Dev server + Python E2E API server\nbun run lint         # ESLint\nbun run typecheck    # tsc --noEmit\nbun run knip         # Dead code / unused exports detection\n```\n\nRun `bun run dev:demo` and open `http://127.0.0.1:5173/Aixa-DeckRender/` for the local\ndemo. If Vite selects another port, use the URL printed in the terminal.\n`bun run build:demo` prepares static hosting assets; publishing them is a separate step.\n\nRun `bun run dev` for the development pages at `http://localhost:5173`:\n\n| Page                            | Purpose                                   |\n| ------------------------------- | ----------------------------------------- |\n| `/test/pages/index.html`        | Upload preview with search and thumbnails |\n| `/test/pages/render-slide.html` | Single slide at native resolution         |\n| `/test/pages/e2e-compare.html`  | Side-by-side PDF vs HTML with SSIM scores |\n| `/test/pages/export.html`       | Model JSON tree viewer                    |\n\n## Documentation\n\n| Doc                                       | Content                                                                |\n| ----------------------------------------- | ---------------------------------------------------------------------- |\n| [`ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Parse/model/render pipeline design                                     |\n| [`PERFORMANCE.md`](docs/PERFORMANCE.md)   | Tuning options and presets                                             |\n| [`TESTING.md`](docs/TESTING.md)           | Unit/E2E strategy, two-layer metric system, visual regression workflow |\n| [`CONTRIBUTING.md`](docs/CONTRIBUTING.md) | PR checklist, code quality tools, and workflow                         |\n| [`SECURITY.md`](docs/SECURITY.md)         | Vulnerability reporting                                                |\n\n## What's Not Yet Supported\n\n3D effects, true 3D chart perspective/depth/surface meshes, animations/transitions, equations (OMML), full EMF/WMF vector rendering, shadow/reflection/glow effects, embedded OLE objects, and slide notes rendering.\n\n## FAQ\n\n**Does this run on Node.js?**\nNo. Rendering depends on browser DOM APIs.\n\n**Why is my PPTX rendering incomplete?**\nOOXML 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.\n\n**How do I render 100+ slide decks efficiently?**\nUse `windowed: true` in `listOptions`, and enable `lazySlides: true`. For media-heavy\ndecks, also enable `lazyMedia: true`.\n\n## License\n\nApache License 2.0. See [`LICENSE`](LICENSE) and\n[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) for upstream attribution and bundled\ndependency notices.\n","readmeFilename":"README.md","_rev":"1-de7509729cb37dd91609e126b89ec238"}