Skip to content

fix(pptx): emit a structurally valid package on serialize (dangling rels + SVG fallback)#92

Merged
karthikmudunuri merged 1 commit into
mainfrom
karthikmudunuri/dar-es-salaam
Jun 11, 2026
Merged

fix(pptx): emit a structurally valid package on serialize (dangling rels + SVG fallback)#92
karthikmudunuri merged 1 commit into
mainfrom
karthikmudunuri/dar-es-salaam

Conversation

@karthikmudunuri

Copy link
Copy Markdown
Member

Summary

Three serializeDeck(deck, { source }) bugs made generated .pptx files structurally invalid (missing package parts / corrupt image parts) even when the source template is clean — the breakage is introduced on serialize. PowerPoint flags a repair on open, and stricter consumers (Google Slides import, LibreOffice, thumbnail renderers, OOXML validators) reject the file outright. Reproduced across many customer templates.

This PR fixes all three at their root in deckToPptx.ts and adds an invariant guard + tests so the output is always a valid OOXML package.

Bug 1a — dangling tags relationships

injectIntoSlide copies a slide-referenced tag part under a slidewise_preserved_N_ name and points the slide rel there. preserveDeckChrome then runs and its chrome wipe removes the entire ppt/tags/ prefix (deleting that prefixed copy), re-copying the source tags under their original names — leaving the slide rel pointing at a part that no longer exists.

Fix: the guard re-points the rel at its de-prefixed original (slidewise_preserved_0_tag104.xmltag104.xml), which is the part that actually ships.

Bug 1b — dangling notesMaster relationships

pptxgenjs writes a notesSlide per slide, each rel-linked to notesMasters/notesMaster1.xml. preserveDeckChrome removes that part (it's a chrome prefix) and, when the source has no notes master, never replaces it — leaving every notesSlide's rel dangling.

Fix: the orphaned (implicit, non-body-referenced) relationship is dropped.

Bug 2 — SVG markup written into the .png raster fallback

For an SVG image pptxgenjs emits a dual blip (<a:blip> raster + <asvg:svgBlip> vector) but writes the SVG data: URL verbatim into the .png fallback part (its isSvgPng branch). The extension/content-type are correct; only the bytes are wrong, so PowerPoint renders fine off the svgBlip while every strict consumer rejects the bogus PNG.

Fix: the .png fallback is replaced with a real rasterized PNG (browser canvas) or a valid transparent PNG (SSR/Node); the <asvg:svgBlip> .svg part is left intact.

Invariant guard

reconcileDanglingRels runs on every serializeDeck return path: every internal relationship target must resolve to a shipped part. It

  • repairs recoverable targets (de-prefix),
  • drops only genuinely-optional missing rels (notesMaster, notesSlide, tags, comments, commentAuthors, glossaryDocument) when the owner body doesn't reference the rId, and
  • never drops a critical implicit rel (slideLayout/slideMaster/theme) — it keeps + warns, so it can't trade one corruption for another.

pruneDanglingContentTypes now also runs on the source-preservation path, so stale [Content_Types] overrides (pptxgenjs's slideMaster1..N, leftover notes overrides, and everything that survives when chrome preservation bails on an aspect mismatch) can't invalidate the package either.

Tests

  • dangling-rels.test.ts — 6 unit cases for the guard (repair, safe-drop, critical-rel-kept, External/resolvable untouched, body-referenced kept).
  • svg-raster-fallback.test.ts — asserts every .png is valid PNG and the .svg survives; fails without the fix.
  • chrome-preservation.test.ts — package-wide "no dangling internal rels" assertion, run per available branded fixture.
  • corpus-validity.test.ts — drop any number of decks into .context/attachments/ and it round-trips every one, asserting all four failure modes are gone (dangling rels, dangling Content_Types overrides, parts with no content type, non-PNG .png bytes). Skips in CI.

Full suite: 158 passed, 9 skipped (skips are fixture-gated). Typecheck clean. Includes a patch changeset.

Notes

  • The SVG raster is faithful only in the browser; Node/SSR gets a valid-but-transparent fallback. A Node rasterizer (e.g. @resvg/resvg-js) for faithful headless fallbacks is a possible follow-up.
  • To validate your own corpus: drop the misbehaving decks into .context/attachments/ and run cd packages/slidewise && npx vitest run corpus-validity.

🤖 Generated with Claude Code

Three serializeDeck bugs corrupted the generated .pptx (missing parts /
invalid image bytes) even from clean source templates, triggering a
PowerPoint repair prompt and rejection by stricter consumers (Google
Slides, LibreOffice, OOXML validators):

- Dangling tags relationships: the chrome-preserve path re-pointed a
  slide's tag rel at a slidewise_preserved_* name, then clobbered that
  part by re-copying the source tags under their original names. The rel
  now resolves to the de-prefixed part it should always have pointed at.
- Dangling notesMaster relationships: pptxgenjs writes a notesSlide per
  slide linked to a notes master, which chrome preservation removed
  without a source replacement. The orphaned (implicit, non-body-
  referenced) relationship is now dropped.
- SVG markup in .png raster fallbacks: dual SVG images had the SVG source
  written into the .png fallback. It is now a real rasterized PNG
  (browser) or a valid transparent PNG (SSR/Node); the svgBlip is intact.

Adds a final reconcileDanglingRels invariant guard (repair recoverable
targets, drop only safe-to-remove optional ones, keep critical rels), and
runs pruneDanglingContentTypes on the source path so stale Content_Types
overrides can't invalidate the package either. Includes unit, SVG, and a
whole-corpus validity test net.
@karthikmudunuri karthikmudunuri merged commit 305dc0f into main Jun 11, 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