Skip to content

fix(clone): drop <picture> <source> elements so the inlined <img> src is not overridden (#462) - #465

Merged
tinchox5 merged 1 commit into
zumerlab:mainfrom
dylan1951:fix/picture-source-blank-render
Jul 23, 2026
Merged

fix(clone): drop <picture> <source> elements so the inlined <img> src is not overridden (#462)#465
tinchox5 merged 1 commit into
zumerlab:mainfrom
dylan1951:fix/picture-source-blank-render

Conversation

@dylan1951

Copy link
Copy Markdown

Closes #462.

Problem

A <picture>'s <source> elements out-rank the <img>'s own src. They were cloned verbatim, so inside the exported SVG the picture selection algorithm re-selected an external URL — and SVG-as-image cannot load external resources. The photo never painted, however correctly the <img> had been inlined.

The signature is a large SVG that rasterizes to a tiny PNG: everything embedded, nothing drawn.

Why the existing machinery doesn't cover it

  • freezeImgSrcset (src/utils/clone.helpers.js) already sets the cloned <img>'s src = original.currentSrc and strips srcset/sizes — but only on the <img>. The sibling <source> elements go through the generic child-clone path with their external srcsets intact.
  • pictureResolver (src/modules/pictureResolver.js) only acts on a <picture> whose <img src> is a lazy-load placeholder (isPlaceholderSrc); it continues for a normally-loaded picture, and runs against the live DOM before cloning.

So a fully-loaded <picture> reaches the SVG with its <source> list intact.

Fix

Skip <source> children of a <picture> in deepClone:

if (tag === 'source' && node.parentElement?.localName === 'picture') {
  return null
}

Because freezeImgSrcset has already frozen the clone to the variant the live page chose, the sources carry nothing still needed. Art direction is preserved — the chosen crop comes from currentSrc, not from re-evaluating the sources. Capturing the same carousel at a 1600px viewport embeds the 2200×1200 crops; at 500px it embeds the 600×1000 crops.

The check is deliberately narrow: <source> inside <audio>/<video> is untouched, and there's a test covering that.

Results

Real WordPress hero carousel, real assets, identical 723 KB SVG both runs:

<source> in SVG embedded images PNG photo painted
before 4 229 KB + 280 KB 48 KB 0 %
after 0 229 KB + 280 KB 2046 KB 100 %

Tests

New deepClone — <picture> sources block in __tests__/core.clone.test.js:

  • <source> children of a <picture> are dropped and the inlined <img> src survives — fails on main, passes with the fix
  • a <source> outside a <picture> is kept — guards against over-reach; passes either way by design

Full suite: 685 passed / 1 skipped, against 683 / 1 on main — no regressions. npm run lint and npm run test:types clean. Verified on Chromium; I don't have the Firefox/WebKit Playwright binaries locally.

…not overridden

A <picture>'s <source> out-ranks its <img>'s own src. The sources were cloned
verbatim, so the exported SVG re-selected an external URL at rasterization time —
and svg-as-image may not load external resources. The photo never painted, however
correctly the <img> had been inlined, producing a large SVG that rasterized to a
small blank PNG.

Skip <source> children of a <picture> when cloning. The <img> clone is already
frozen to the variant the live page chose (freezeImgSrcset sets src =
original.currentSrc and strips srcset/sizes), so the sources carry nothing still
needed and art direction is preserved.

Verified against a WordPress hero carousel whose slides use <picture> with a
catch-all <source media="(min-width: 0px)">, which made the failure deterministic.
With the site's real assets and an identical 723 KB SVG: 48 KB PNG with 0% of the
photo painted before, 2046 KB and 100% after. Capturing at a 1600px viewport embeds
the 2200x1200 crops; at 500px it embeds the 600x1000 crops.

Closes zumerlab#462

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tinchox5
tinchox5 merged commit c2ce711 into zumerlab:main Jul 23, 2026
@tinchox5

Copy link
Copy Markdown
Member

Merged, thanks. Good instinct scoping the fix to <picture>'s <source> specifically instead of touching <source> generally — the added test guarding <audio>/<video> sources is exactly the kind of thing that keeps a fix from becoming an over-reach.

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.

<picture> <source> elements override the inlined <img> src, so pictures rasterize blank

2 participants