replace html2canvas with snapdom - #7920
Open
fikrydev wants to merge 11 commits into
Open
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Updates the image-export feature in ExportImage.js to use snapdom for DOM-to-canvas capture (instead of html2canvas), including adapting the clone-mutation logic to snapdom’s API constraints.
Changes:
- Replaced html2canvas capture flow with
snapdom.toCanvas(...)and updated the loader tomw.loader.using('snapdom'). - Added an offscreen “capture clone” path and adjusted clone-fixup utilities to operate on an element root (including self-matching selectors).
- Updated dropdown preloading to call
ensureSnapdomLoaded().
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
fikrydev
force-pushed
the
chore/replace-html2canvas-with-snapdom
branch
from
August 13, 2026 08:17
154bcb9 to
a3e66ef
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (2)
javascript/commons/ExportImage.js:306
wrapper(the element passed tosnapdom.toCanvas) is never inserted into the offscreenrootthat gets appended to the document. SincewrapWithHeaderFooter()movestargetintowrapper, the appendedrootends up containing empty ancestor shells, while the capturedwrapperis detached from the DOM (so computed layout/CSS can be missing and capture can fail/produce 0×0). Insertwrapperback into the ancestor spine before appendingrootso the clone is connected and ancestor-scoped CSS applies.
document.body.appendChild( root );
javascript/commons/ExportImage.js:140
buildAncestorSpine()currently clones all ancestors up to<html>and thengenerateImageBlob()appends that clone tree todocument.body, which nests<html>/<body>elements inside the live<body>. This is invalid DOM and can trigger browser normalization/reparenting in ways that break the offscreen mount and the capture. Stop cloning atdocument.body/documentElementand wrap the spine in a normal<div>that carries the page-level classes needed for ancestor-scoped CSS.
This issue also appears on line 306 of the same file.
while ( ancestor ) {
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
Replace html2canvas with [snapdom](https://github.com/zumerlab/snapdom) for the group table / crosstable / bracket / match list / prize pool / participants / standings image export feature, and rework how the exported image is composed:
@mediaresolution regardless of the exporting device. In practice this caused several regressions: toggle-dependent views (FFA/Swiss standings detail columns, participant card compact mode) always rendered in their "wide desktop" state regardless of the toggle, Match List captures could come out visually broken, and mobile exports were blurrier and much larger in file size than before. The rewrite drops the iframe/fixed-width sandbox entirely — the element is still cloned (rebuilding a shallow ancestor spine sotheme--darkand other ancestor-scoped CSS, plus:has()-driven toggle state, still apply) but the clone stays in the live document at its natural width, so the export matches whatever the exporting device is actually showing.snapdom.toCanvas()call — wrapping, centering and sizing are handled by the browser's layout engine instead of custom math.<img>elements to finish decoding on its own; without this, flags and the footer logo could be rasterized blank, or (worse) an undecoded image could collapse a row's height and push later rows out of place in the capture. All images inside the captured tree are now awaited viadecode()(with a timeout fallback) before the snapshot is taken.scaleanddproptions multiply together into the final canvas size. The initial migration derivedscalefromdevicePixelRatiobut never setdpr, so snapdom multiplied by the real device pixel ratio a second time on top of that — producing exports 4-9x larger than necessary on HiDPI screens.dpris now pinned to1, with all oversampling driven throughscalealone.insetbox-shadow as a structural border/divider technique in several components (tables, standings, crosstables), so shadows can't just be stripped from captures to avoid the fallback. Instead, WebKit/iOS user agents now get a higher oversampling ceiling (3-4x vs. the default 2-3x) to buy back sharpness lost to the resample..brkts-matchlist-collapse-areaelement uses CSSsubgridto inherit its column tracks from its live.brkts-matchlistparent. Reparenting the clone under the header/footer flex wrapper (with the real grid ancestor only reattached further up the spine) left it with no grid to inherit from, collapsing every column into one and squashing the whole match list into a narrow vertical stack. Fixed by baking the parent's already-resolved pixel track widths onto the clone before it's detached, turning it into a self-contained grid.ImageCacheandCanvasComposerclasses (image preloading + hand-rolled canvas compositing, font measuring, text wrapping, gradient/rounded-rect drawing) are gone — that responsibility moved to plain CSS on the composed DOM. Despite the fixes above adding some logic back, the file is still ~150 lines smaller than the html2canvas-based version.How did you test this change?
Test it here: https://fikry.wiki.tldev.eu/rocketleague/FIFAe_World_Cup/2025