Skip to content

fix(pptx): harden serialize output — media dedupe, DEFLATE, SVG fallback#94

Merged
karthikmudunuri merged 1 commit into
mainfrom
karthikmudunuri/pptx-edit-corruption-fix
Jun 12, 2026
Merged

fix(pptx): harden serialize output — media dedupe, DEFLATE, SVG fallback#94
karthikmudunuri merged 1 commit into
mainfrom
karthikmudunuri/pptx-edit-corruption-fix

Conversation

@karthikmudunuri

Copy link
Copy Markdown
Member

TL;DR

After a small edit, saving a deck ballooned it from 1.5 MB to ~6 MB and produced images that break outside PowerPoint. This hardens the serializer so a round-trip stays valid and compact — dedupe shared media, DEFLATE the package, and emit a valid SVG raster fallback (with an optional rasterizeSvg hook so our headless pipeline can rasterize faithfully).

Summary

  • Media de-duplication — shared images (icons/logos/backgrounds) were copied once per reference under slidewise_preserved_N_ names; one image was written 9×. Copies are now keyed on the immutable source path and written once, with every referencing rel pointing at that single copy. New PreservedPartRegistry / preserveSourcePart, threaded through injectIntoSlide, copyPartDependencies, and injectSlideBg.
  • CompressiongenerateAsync shipped with JSZip's default STORE (0%), so multi-MB slide XML went out raw. Now DEFLATE (level 6), matching the source archive. Combined with dedupe: ~6 MB → ~1.1 MB on the repro deck.
  • F1 — transparent-PNG fallback CRC — the last-resort 1×1 PNG had a bad IDAT CRC (decodes leniently, rejected by strict PNG/OOXML validators). Replaced with a CRC-correct constant.
  • F2 — headless SVG rasterization — added an optional rasterizeSvg hook to SerializeOptions (exported SvgRasterizer). On Node/SSR (no canvas) the SVG raster fallback degraded to a blank transparent PNG; a host can now inject e.g. @resvg/resvg-js. Order: host hook → browser canvas → transparent fallback; non-PNG / throwing output is ignored.

Review focus

  • New public API: SerializeOptions.rasterizeSvg + SvgRasterizer type (optional, backward-compatible). Chose a host-injected hook over a hard @resvg/resvg-js dependency because the library is isomorphic and a native dep would break the browser bundle.
  • Dedup correctness: rel targets are recomputed relative to each owner via relativeTarget; the dependency walk now runs once per source part across the whole serialize (reg.depsWalked).
  • Compression: DEFLATE on already-compressed media (PNG/JPEG) is a negligible-CPU no-op; the win is on XML.

Test plan

  • media-dedup.test.ts — shared image written once + both rels resolve it; package is DEFLATE-compressed (both fail pre-fix)
  • svg-fallback.test.ts — F1 emits CRC-correct PNG on headless path; F2 uses host rasterizer; bogus/throwing rasterizer ignored (all fail pre-fix)
  • Full slidewise suite: 165 passing, 9 skipped; repo typecheck clean
  • End-to-end on the real repro deck: ~6 MB → ~1.1 MB, 0 duplicate media, 24 PNGs all CRC-valid, 0 SVG-in-PNG, re-parses cleanly

serializeDeck round-trips produced invalid / oversized packages:

- Media de-duplication: shared images (icons, logos, backgrounds) were
  copied once PER reference under slidewise_preserved_N_ names — a 1.5 MB
  deck ballooned to ~6 MB with one image written 9×. Copies are now keyed
  on the immutable source path and written once; every referencing rel
  points at that single copy (new PreservedPartRegistry / preserveSourcePart,
  threaded through injectIntoSlide, copyPartDependencies, injectSlideBg).

- Compression: the package shipped with JSZip's default STORE (0%), so
  multi-MB slide XML went out raw. generateAsync now uses DEFLATE, matching
  the source archive. With dedupe: ~6 MB → ~1.1 MB on the repro deck.

- SVG raster fallback (1.19.1 follow-ups):
  - F1: the last-resort transparent PNG had a bad IDAT CRC (decodes in
    lenient readers, rejected by strict PNG/OOXML validators). Replaced
    with a CRC-correct 1×1 transparent PNG.
  - F2: added an optional `rasterizeSvg` hook to SerializeOptions so the
    headless Node/SSR path can emit a faithful raster (host injects e.g.
    @resvg/resvg-js) instead of a blank transparent PNG. Resolution order:
    host hook → browser canvas → transparent fallback. Non-PNG/throwing
    hook output is ignored.

Tests: media-dedup.test.ts (dedupe + DEFLATE) and svg-fallback.test.ts
(F1 CRC + F2 hook); both verified to fail pre-fix. Full suite 165 passing;
repo typecheck clean.
@karthikmudunuri karthikmudunuri merged commit 017ce51 into main Jun 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant