pdfkit: share one XObject per pixel-identical bitmap - #26
Merged
Conversation
html2pdf's corpus renders react.dev into 12 pages and 9,095,777 bytes, of which the bulk is 166 image XObjects: a handful of icon SVGs, rasterised and stored as raw FlateDecode RGB samples once per placement. Every DrawImage/DrawPNG/DrawJPEG call built a fresh XObject even when the pixels were identical, so a page that reuses one icon paid for it every time — in file size and in a redundant flate pass. Content-address the image instead. imageKey hashes the *uncompressed* samples together with the parameters that give them meaning (filter, colour space, width, height, bits per component) and the alpha samples that would become the soft mask; the JPEG path keys on the raw DCTDecode bytes. Document.imageFor looks the key up and only builds — and only compresses — on a first sighting, so a duplicate costs a hash and nothing else. The cache lives on the Document, so two pages drawing the same bitmap share one stream and one /Im<i> name; placeImage no longer registers, it only records the resource on the page and paints it. Determinism is preserved by construction: the cache is consulted, never iterated. Object order and /Im<i> numbering still come from the images slice in first-sighting order, exactly as before, and the zero Options still promise byte-identical output for identical input — TestImageDedupStaysDeterministic writes one document twice and two identically built documents once each, and compares bytes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Why
html2pdf's corpus renders
react.devinto 12 pages and 9,095,777 bytes, most of it 166 image XObjects — a handful of icon SVGs, rasterised and stored as raw FlateDecode RGB samples once per placement.DrawImage/DrawPNG/DrawJPEGbuilt a fresh XObject on every call even when the pixels were identical, so a page that reuses one icon paid for it every time: in file size, and in a redundant flate pass.What
imageKeycontent-addresses an image by its uncompressed samples plus the parameters that give them meaning (filter, colour space, width, height, bits per component) and the alpha samples that would become the/SMask. The JPEG path keys on the raw DCTDecode bytes. Lengths go into the digest ahead of the payloads, so no shift of bytes between samples and alpha can forge a match.Document.imageFor(key, build)looks the key up and callsbuildonly on a first sighting — so a duplicate costs a SHA-256 pass and nothing else, and never pays for compression twice.Document, so two pages drawing the same bitmap share one stream and one/Im<i>name.placeImageno longer registers; it records the resource on the page and paints it.Determinism
Preserved by construction: the cache is consulted, never iterated. Object order and
/Im<i>numbering still come from theimagesslice in first-sighting order, exactly as before.TestImageDedupStaysDeterministicwrites one document twice, and two identically-built documents once each, and compares bytes.Tests
Identical
DrawImagecollapses to one/Subtype /Imagewith both placements painting/Im0; distinct images do not collapse; same forDrawPNG(3 draws → 1 image + 1 soft mask) andDrawJPEG(a byte-identical copy, not the same slice, so identity cannot be doing the work); same RGB with different alpha stays two objects; cross-page sharing keeps one stream but a/XObjectdict on each page.go vetclean,gofmtclean on the touched files, coverage 100.0% total with every function at 100.0%, cross-compiles for wasm and windows.🤖 Generated with Claude Code