fix(pptx): emit a structurally valid package on serialize (dangling rels + SVG fallback)#92
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three
serializeDeck(deck, { source })bugs made generated.pptxfiles 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.tsand adds an invariant guard + tests so the output is always a valid OOXML package.Bug 1a — dangling
tagsrelationshipsinjectIntoSlidecopies a slide-referenced tag part under aslidewise_preserved_N_name and points the slide rel there.preserveDeckChromethen runs and its chrome wipe removes the entireppt/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.xml→tag104.xml), which is the part that actually ships.Bug 1b — dangling
notesMasterrelationshipspptxgenjs writes a notesSlide per slide, each rel-linked to
notesMasters/notesMaster1.xml.preserveDeckChromeremoves 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
.pngraster fallbackFor an SVG image pptxgenjs emits a dual blip (
<a:blip>raster +<asvg:svgBlip>vector) but writes the SVGdata:URL verbatim into the.pngfallback part (itsisSvgPngbranch). The extension/content-type are correct; only the bytes are wrong, so PowerPoint renders fine off thesvgBlipwhile every strict consumer rejects the bogus PNG.Fix: the
.pngfallback is replaced with a real rasterized PNG (browser canvas) or a valid transparent PNG (SSR/Node); the<asvg:svgBlip>.svgpart is left intact.Invariant guard
reconcileDanglingRelsruns on everyserializeDeckreturn path: every internal relationship target must resolve to a shipped part. ItnotesMaster,notesSlide,tags,comments,commentAuthors,glossaryDocument) when the owner body doesn't reference the rId, andpruneDanglingContentTypesnow also runs on the source-preservation path, so stale[Content_Types]overrides (pptxgenjs'sslideMaster1..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.pngis valid PNG and the.svgsurvives; 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.pngbytes). Skips in CI.Full suite: 158 passed, 9 skipped (skips are fixture-gated). Typecheck clean. Includes a
patchchangeset.Notes
@resvg/resvg-js) for faithful headless fallbacks is a possible follow-up..context/attachments/and runcd packages/slidewise && npx vitest run corpus-validity.🤖 Generated with Claude Code