Repository navigation
Decode imported images once and reuse the result - #140
Merged
Merged
Conversation
validateImageFile loaded each file into an <img> and then decoded it again with createImageBitmap, which it threw away. Stack Matrix then drew the <img>, decoding the photo a second time. Raster files now decode with createImageBitmap alone. In Chromium and WebKit it accepts the same formats as <img>, applies the same EXIF orientation, and still rejects damaged files in Chrome. SVGs, which Chrome rejects there, and engines without createImageBitmap still decode in an <img>. The structural truncation check is unchanged. validateImageFile returns that decoded image as a DecodedImage (source, width, height, close). Stack Matrix draws it directly; the main image import closes it, since the preview and swatches decode the committed URL themselves. Refs #131 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vycdev
force-pushed
the
perf/issue-131-import-decoding
branch
from
November 10, 2026 08:44
ef9732c to
bc5f973
Compare
Drawing the decoded bitmap at the default smoothing quality downscaled large photos more coarsely than the <img> did, which moved measured cell colors further from the true area average. Medium quality matches or beats the previous accuracy in Chromium and WebKit. Refs #131 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 10, 2026
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.
Refs #131
Rebased onto
developafter #142 moved the import check intovalidateImageFile(src/lib/imageFile.ts). This replaces the earlier version of this PR.What changed
validateImageFileused to load each file into an<img>and then decode it again withcreateImageBitmap, throwing that bitmap away. Stack Matrix then drew the<img>, which decoded the photo a second time.createImageBitmapalone. SVGs, which Chrome rejects increateImageBitmap, and engines without it still decode in an<img>. The structuralisTruncatedImageDatacheck still runs first, so a truncated file is refused before it is decoded.validateImageFilereturns the decoded image as aDecodedImage(source,width,height,close()) instead of anHTMLImageElement. The caller owns it and closes it, just as it already owns and revokes the URL.imageSmoothingQuality = 'medium'; see Stack Matrix colors below.useImageImport) closes it right away.CanvasPreviewanduseSwatchesstill decode the committed URL themselves; handing them this decode is the next step and is not in this PR.Accepted files are unchanged
In Playwright Chromium and WebKit I compared the old rule (
<img>, thencreateImageBitmapfor non-SVG) with the new one (createImageBitmapfor raster files,<img>for SVG). Each file was tested with its MIME type and with an empty type.The old and new rules gave the same result in every case. A PNG and a JPEG with damaged data but an intact end marker are rejected by
createImageBitmapin Chromium (<img>andimg.decode()both accept them) and accepted by both in WebKit, as before. Firefox and the Windows WebView2 were not tested.EXIF orientation: Stack Matrix now draws a bitmap instead of an
<img>, so rotated phone photos matter here. With no options,createImageBitmapapplies the EXIF orientation exactly as<img>does in Chromium 148 and WebKit 26.4 (same size and the same corner on top). I call it with no options on purpose: passingimageOrientation: 'from-image'throws on older engines that don't recognize that value, which would wrongly reject valid files. Much older engines whosecreateImageBitmapignored EXIF tags are untested.Stack Matrix colors
Stack Matrix measures colors from the photo's pixels, so I checked that drawing a bitmap instead of an
<img>doesn't change them:<img>and through the bitmap.<img>. I measured cells the waysampleStackMatrixPhotodoes (9×9 points in the middle 32% of each cell, trimmed mean) on the 3888×2916 fixture. The bitmap path ended up further from a true area average of the full-resolution pixels.mediumquality matches or beats the old path on both engines (highis worse on WebKit):<img>/ bitmap at default / bitmap atmedium<img>/ bitmap at default / bitmap atmediumWith
medium, re-measuring the same photo gives cell colors about 0.5 levels per channel different from before, on average. This was measured on one natural photo, not a printed calibration board.Cost (3888×2916 JPEG fixture, medians in the app)
Stack Matrix photos load about 35–60% faster in Chromium (and so in the Windows app). On WebKit they are unchanged to about 25 ms slower; the spreads of the runs overlap. The main import's check is shorter, but the total time to show the image doesn't change, because most of that time is spent after the commit, in
CanvasPreviewanduseSwatches.Tests
tests/imageFile.test.ts): the stubs now count decodes. A raster file is decoded once bycreateImageBitmapand never by<img>. A truncated PNG is refused before any decode.createImageBitmaprejections, unreadable bytes, SVGs (<img>only), engines withoutcreateImageBitmap(<img>fallback), and non-images are each covered.@smoke raster imports are decoded once before they are committed: holdscreateImageBitmapfor imported files and checks that it runs exactly once and that no<img>loads the imported file before the check finishes. It fails on the old code.image-import-error.spec.tsandstack-matrix-photo-import.spec.ts: 9/9 on Chromium and 9/9 on WebKit, run withKROMACUT_E2E_FRESH_SERVERS=1.bc5f973: lint, build, unit tests,@smoke, docs, landing, not-found, i18n, and i18n build all pass.npm run lint,npm run build, andnpm test(1112 pass, 1 skipped) all pass.The changelog note is folded into the existing unreleased Unreadable image imports entry. The docs don't describe decode steps, so they are unchanged.
🤖 Generated with Claude Code