Skip to content

Hold the bytes of a vector image only when they were fetched - #869

Draft
gravitypdf-bot wants to merge 1 commit into
gravitypdffrom
fix/860-vector-look-ahead-cost
Draft

gravitypdf-bot wants to merge 1 commit into
gravitypdffrom
fix/860-vector-look-ahead-cost

Conversation

@gravitypdf-bot

@gravitypdf-bot gravitypdf-bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Fixes #860, with the small option only. The benchmarks below show the other two options aren't worth their risk.

The change

ImageProcessor::$vectorData held the bytes of every SVG and WMF until the document ended, so a later pass could parse them again without fetching them again. A data URI or imageVars image brings its bytes on every call, so that copy was never needed. getImage() now holds only what the fetcher returned.

A fetched file is still held, and still fetched once. Output doesn't change.

Is the copy really unused?

getImage() rewrites $file to the md5 by reference, so a later call with the bare md5 would have needed the stored bytes. Reading every caller:

  • Tag/Img.php, Tag/Input.php, Tag/Meter.php and _setListMarker() keep the md5 in $objattr['file'], which the print code only uses to index formobjects and images. It never goes back to getImage().
  • Image() takes $file by value, so backgrounds and watermarks don't see the md5.
  • getImage($this->noImageFile) passes a public property by reference, so a data URI or imageVars image there does become its md5, and later calls pass that. The snapshot holds the property and formobjects alike, so they unwind together and the md5 never outlives its form object.

VectorImageSourceTest pins this in CI:

  • Routes: <img> (alone, in tables, in a repeating header), block backgrounds, list markers and watermarks, each through a moved kept block and through the contents with an index, from a data URI and from imageVars. A data URI or imageVars image is never fetched, and the same routes with the image in noImageFile never fetch its md5.
  • Snapshot: noImageFile is restored with its form object.
  • Callers: a scan of src lists every getImage() call, so a new one fails until it's checked.

I confirmed these fail when they should, by breaking a copy of the code: leaving noImageFile out of the snapshot fails 21 tests, and making Tag/Img.php keep the md5 and pass it on the next pass fails the <img> routes. A new getImage() call failed the scan in the same way on the previous revision of this file.

What they can't do is prove the absence of a route nobody has written. The scan sees calls, not an existing call's $file being stored somewhere new. <input type="image"> isn't in the routes either: with an SVG it raises Undefined array key "cs" with or without this change, which is a separate bug. Its call is in the scan.

I also ran the full suite once on a copy of the code from before this change that logs each read of a stored md5, and none occurred. 58 extra documents, 29 for each source, gave identical PDF hashes with and without the change.

The issue also asked whether a data URI is held twice, as key and value. It isn't: the key is the md5.

Memory

Fetched bytes only, against current gravitypdf. Hot font cache, 10 runs each, PHP 8.5. Time and PDF size didn't change.

Document Current This PR
275 KB data URI, toc-preHTML + index 20.92 MB 20.57 MB
1.4 MB data URI, toc-preHTML + index 40.73 MB 38.99 MB
6.9 MB data URI, toc-preHTML + index 140.48 MB 131.73 MB
500 small data URIs, kept 25.53 MB 24.52 MB

These figures come from a scratch copy with the same store-only-fetched change, not this exact commit. The SVGs are synthetic.

Why not the rest of #860

Hot font cache, 10 runs each, one process per run. "Before" is 81a78b6f, the first parent of the #857 merge; "after" is 9e1090e7. Before isn't a fair baseline: its output has the bug, with the gradients missing from most of these documents, so it does less work. Other agents were running, so timings carry tens of ms of noise.

Document Before #857 After #857
110-page report: 20 KB SVG in the contents, index, 19 kept figures 638 ms, 22.36 MB 669 ms, 22.85 MB
1.4 MB SVG, moved kept block 286 ms, 20.04 MB 444 ms, 21.51 MB
1.4 MB SVG, toc-preHTML + index 288 ms, 22.11 MB 636 ms, 23.58 MB
6.9 MB SVG, toc-preHTML + index 1220 ms, 45.37 MB 2795 ms, 50.27 MB
500 small SVGs, toc-preHTML + index 293 ms, 20.92 MB 527 ms, 26.76 MB
20 distinct 275 KB SVGs, every block moves 1048 ms, 31.03 MB 1681 ms, 36.96 MB
1.4 MB WMF, toc-preHTML + index 308 ms, 23.38 MB 382 ms, 24.72 MB
  • Avoiding the re-parse: a parse costs about 0.11 ms per KB, at most twice extra per unique image, and the report's time difference is within noise. Keeping form objects live means tracking gradient and graphics state numbers across passes, which is the fragile area Keep what a look-ahead draws when the pass is unwound #857 had to fix.
  • Holding fetched bytes only while a snapshot is open: this saves about the size of the unique vector files, 0.5 MB on the report. It would need new tracking, because a kept block that stays on its page never restores its snapshot.

Real-world SVGs, other PHP versions, and SVGs with text or embedded images weren't measured.

Tests

testAnSvgIsHeldOnlyWhenItWasFetched renders through an unwound pass from a data URI, an imageVars entry and a file, checking what's held and that the gradient survives. It fails without the fix. VectorImageSourceTest is described above. There's no snapshot, as this doesn't change rendering.

Run locally on PHP 8.5: composer test (13579 tests, 31 skipped) and composer cs pass, and PHPStan 2.2.17 reports nothing for ImageProcessor, though CI pins another version. PHP 5.6 wasn't run locally, so that's left to CI.

A data URI or an imageVars entry brings its bytes on every call, so the copy kept for the next pass is not needed. getImage() writes the md5 of those bytes back to $file, so tests pin that no route asks for that md5 once an unwound pass has removed the form object, and list every call to getImage().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gravitypdf-bot
gravitypdf-bot force-pushed the fix/860-vector-look-ahead-cost branch from cda5131 to eaff6df Compare November 6, 2026 07:59
@jakejackson1 jakejackson1 added bug Something isn't working create-upstream-pr labels Nov 6, 2026
@jakejackson1
jakejackson1 marked this pull request as draft November 6, 2026 08:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working create-upstream-pr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reduce the parsing and memory an unwound pass costs for SVG and WMF images

2 participants