Skip to content

feat: add stabilizationAttempts option for blank large images in WebKit captures - #591

Open
Kebechet wants to merge 3 commits into
bubkoo:masterfrom
Kebechet:feat/stabilization-attempts
Open

feat: add stabilizationAttempts option for blank large images in WebKit captures#591
Kebechet wants to merge 3 commits into
bubkoo:masterfrom
Kebechet:feat/stabilization-attempts

Conversation

@Kebechet

@Kebechet Kebechet commented Aug 6, 2026

Copy link
Copy Markdown

Problem

On WebKit (iOS Safari / WKWebView), capturing a subtree that contains large images intermittently produces an output where those images are blank while text, backgrounds, and small images render fine — the long-standing class of reports around #361.

We measured this on a real device (iPhone 11, iOS 18.7, Safari 26.2) with a self-verifying fixture page: 13 fixtures (small PNG/SVG files, a data-URL image, a lazy image, a CSS background-image, an inline SVG, two SVG logos, and four 1200×1200 incompressible-noise PNGs as data URLs), captured at pixelRatio: 4, with pixel-sampled verdicts per fixture, cold and warm:

pipeline cold capture immediate re-capture
1.11.11 toPng 4 large images blank ok
1.11.13 toPng 4 large images blank 4 large images blank
pre-decode every embedded image, then draw 4 large images blank mostly blank
capture until two consecutive results identical 1st attempt blank → final complete (converged at attempt 2–3) ok

Three findings worth pinning down, because they invalidate the workarounds usually suggested in the issue threads:

  1. Only large images are affected — every small fixture, including SVG-in-<img>, rendered fine on this WebKit build.
  2. A plain retry is not reliable — a second standalone capture can come back blank again (see the 1.11.13 row).
  3. Pre-decoding the embedded images does not help — WebKit's SVG-image subresource loader does not share the page's decoded-image cache, so warming Image objects outside the SVG context changes nothing. The only reliable signal is comparing actual capture output.

Fix

An opt-in stabilizationAttempts option (default 1 = existing behaviour, zero cost for current users). When greater than 1, toCanvas repeats the capture until two consecutive results are identical and returns the last one, capped at the given number of attempts. All rasterizing entry points (toPng, toJpeg, toBlob, toPixelData) inherit it through toCanvas; toSvg is untouched.

Measured cost on the device above: the heavy capture went from ~1.0 s (single attempt, blank images) to ~2.0 s (converged, complete) with stabilizationAttempts: 3.

Subtrees with genuinely dynamic content (a running clock, an animation) never produce two identical captures and will take all attempts — documented on the option.

Tests

test/spec/stabilization.spec.ts:

  • a stabilized capture renders correctly (reference-image comparison);
  • for stable content the stabilized result is identical to a single capture;
  • stabilization works through toBlob.

Related: #589 fixes the complementary WebKit failure where a capture hangs forever because HTMLImageElement.decode() rejects under the same memory-pressure conditions.

@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.16667% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.50%. Comparing base (d9b2fcf) to head (7939228).
⚠️ Report is 26 commits behind head on master.

Files with missing lines Patch % Lines
src/index.ts 79.16% 2 Missing and 3 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master     #591   +/-   ##
=======================================
  Coverage   66.50%   66.50%           
=======================================
  Files          10       10           
  Lines         612      612           
  Branches      150      150           
=======================================
  Hits          407      407           
  Misses        144      144           
  Partials       61       61           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in capture “stabilization” mechanism to mitigate intermittent blank large images in WebKit-based captures by repeating rasterization until two consecutive outputs match (bounded by a new stabilizationAttempts option).

Changes:

  • Introduces stabilizationAttempts in Options with inline documentation.
  • Refactors toCanvas to perform repeated captures and stop early on convergence.
  • Adds a dedicated spec covering stabilized captures and propagation through toBlob.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/index.ts Implements the multi-attempt stabilization loop in toCanvas (via a new internal captureCanvas).
src/types.ts Adds the new Options.stabilizationAttempts API documentation.
test/spec/stabilization.spec.ts Adds tests validating stabilized rendering and option propagation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/index.ts Outdated
Comment thread src/index.ts Outdated
Comment thread test/spec/stabilization.spec.ts Outdated
Kebechet added a commit to Kebechet/Blazor.HtmlToImage that referenced this pull request Aug 6, 2026
Measured on a real iPhone: WebKit rasterizes the capture's intermediate SVG
before large embedded images have decoded, blanking them in the output, and a
plain retry or pre-decoding does not recover it - only comparing consecutive
captures until they agree does. Captures now repeat until two consecutive
results are identical (StabilizeAttempts, default 3), and every capture is
bounded by CaptureTimeoutMs because upstream's createImage never settles when
Safari rejects HTMLImageElement.decode() under memory pressure.

Both mitigations are proposed upstream:
bubkoo/html-to-image#589 (hang)
bubkoo/html-to-image#591 (stabilization)
bubkoo/html-to-image#590 (the toBlob type/quality
deviation this wrapper already works around)
Post-merge cleanup tracked in
#1
rpsjosh1958 pushed a commit to rpsjosh1958/influencer-drop-shop that referenced this pull request Sep 10, 2026
Two real bugs in the rasterization pipeline (cors-image.tsx,
store-promo-card.tsx):
- CorsImage fired its onLoad callback the instant the base64 string was
  computed, before the actual <img> tag existed in the DOM — the
  "ready to download" gate could flip true before images had really
  painted, worse with more concurrent images (Rack's logo + up to 3
  products). Moved onLoad to the real rendered <img> element.
- Rack's product grid used CSS Grid, which html-to-image's SVG-based
  rasterizer doesn't reliably preserve track sizing for — silently
  collapsing tiles to zero height in the export even though the images
  had loaded. Replaced with flexbox (same visual layout), matching the
  technique already used everywhere else in these cards.

Separately, on iOS Safari specifically: confirmed via html-to-image's
own issue tracker (bubkoo/html-to-image#591, maintainer-tested on a
real iPhone) that WebKit has a documented bug where capturing a subtree
with large images intermittently returns them blank — a WebKit
SVG-image-loader quirk present in the installed 1.11.13, not a timing
bug on our side. A single retry isn't reliable; the only fix that
reliably works is re-capturing until two consecutive results are
byte-identical. That fix isn't released upstream yet, so replicated it
directly in both share-modal.tsx and store-share-modal.tsx's download
handlers — effectively free on browsers without the bug (converges on
the first comparison), and should resolve it on iOS Safari.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants