Skip to content

fix(pptx/render): emit valid SVG <image> for image-fill backgrounds#100

Merged
karthikmudunuri merged 3 commits into
mainfrom
karthikmudunuri/open-release-pr
Jun 12, 2026
Merged

fix(pptx/render): emit valid SVG <image> for image-fill backgrounds#100
karthikmudunuri merged 3 commits into
mainfrom
karthikmudunuri/open-release-pr

Conversation

@karthikmudunuri

Copy link
Copy Markdown
Member

Problem

renderDeckToSvg rendered a slide's image-fill background as a CSS background shorthand dropped into an SVG fill="…" attribute:

<rect ... fill="center / cover no-repeat url("data:image/jpeg;base64,/9j/4AAQ…")"/>

That is not valid SVG — it's a non-paint value, and the nested unescaped quotes terminate the attribute. Browsers parse it leniently, but every strict SVG rasteriser (@resvg/resvg-js, librsvg, batik) rejects it:

SVG data parsing failed cause invalid attribute at 1:216 ... expected space not 'd'

This was the single blocker to a Chromium-free render path (parsePptx → renderDeckToSvg → resvg → PNG).

Root cause

The pptx importer stores image backgrounds as a CSS background shorthand (pptxToDeck.ts:3592: center / cover no-repeat url("…")). The renderer's image-ref detection (isImageRef) only inspected the start of the value (startsWith("data:image"), /^url\(/), so the shorthand wasn't recognised as an image and fell through to solidFrom, which returned the whole shorthand as a "named colour" → fill="…". renderBackground already had a correct <image> branch; it just never reached it.

Fix

  • isImageRef now matches a url(...) anywhere in the value (gradients have no url(, so they're unaffected).
  • imageHref extracts the URL from url(...) wherever it appears (data URLs use a )-free base64 alphabet, so non-greedy-to-first-) is safe).
  • New imageFitPreserve: coverxMidYMid slice, containxMidYMid meet, wired into renderBackground.
  • Image-fill element fills now also yield valid SVG via solidFrom (which delegates to isImageRef), though only backgrounds use this form today.

Tests

Three new lock-in tests (9/9 green):

  1. Image-fill background → real <image> with the data-URL xlink:href, preserveAspectRatio="xMidYMid slice", and no fill="…url(…".
  2. contain background → preserveAspectRatio="xMidYMid meet".
  3. Strict-parser lock-in: every rendered slide passes fast-xml-parser's XMLValidator (the non-browser equivalent of the resvg check), with an image-background slide as the regression case. Verified the validator rejects the old malformed output, so the guard is meaningful.

tsc --noEmit clean. Changeset: patch.

renderDeckToSvg rendered a slide's image-fill background as a CSS
background shorthand inside fill="…" (center / cover no-repeat
url("data:image…")) — invalid SVG that strict rasterisers (resvg,
librsvg) reject, blocking a Chromium-free render path. The renderer now
recognises a url(...) anywhere in the value and emits a real <image>
element (slice for cover, meet for contain). Adds a strict-XML-parser
lock-in test over an image-background fixture.
…svg-js

XMLValidator proves well-formedness only; resvg is the actual consumer.
Add @resvg/resvg-js as a devDependency and drive an image-background
slide through the package's default rasteriser (no injected hook),
asserting a valid PNG of the expected dimensions (320x180). resvg threw
on the old fill="...url(data:...)..." output, so this is a true guard.
@karthikmudunuri

Copy link
Copy Markdown
Member Author

Added a real-rasteriser guard on top of the XMLValidator well-formedness check.

@resvg/resvg-js is now a devDependency, and a new test drives an image-background slide through the package's default rasteriser path (no injected hook → the package's own dynamic @resvg/resvg-js import in defaultRasterize), end-to-end. It asserts a valid PNG signature and decodes the IHDR to confirm resvg produced a correctly-sized raster (1920×1080 capped at width 320 → 320×180), rather than just a non-empty buffer.

Confirmed resvg threw on the old fill="…url(data:…)…" output with the exact bug-report error (expected space not 'd'), so this is a true regression guard, not a no-op. All 10 render tests pass (environment: node, so the native addon loads).

…resvg/resvg-js"

This reverts commit 3f9d393. Keep the resvg renderability check on the
consumer side (the host serializer already ships @resvg/resvg-js) rather
than dragging a platform-native binary into the package's CI. The bug was
an SVG-validity (well-formedness) regression, which the pure-JS
XMLValidator lock-in already catches — it was confirmed to reject the old
malformed output. The package CI stays native-dep-free.
@karthikmudunuri

Copy link
Copy Markdown
Member Author

Reverted the resvg test + native devDependency (commit b6bca12). Keeping the package CI native-dep-free.

Rationale (agreed split):

Layer Guard Dependency
Package CI XMLValidator (valid SVG / well-formedness) pure JS — kept
Host (consumer) resvg actually rasterises already ships @resvg/resvg-js

This bug was an SVG validity regression (nested quotes / non-paint fill breaking attribute parsing) — exactly what fast-xml-parser's XMLValidator catches, with no native binary. That lock-in was confirmed to reject the old malformed output, so it's a real guard. The renderability round-trip belongs on the consumer side, where resvg already lives, and will be added there alongside the Chromium-free renderer.

Net diff is back to three files (changeset + XMLValidator test + renderDeck.ts fix); no pnpm-lock.yaml change. All 9 render tests pass. Good to merge as-is.

@karthikmudunuri karthikmudunuri merged commit 94e348b 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