Repository navigation
Conversation
Contributor
|
✅ DCO Check Passed Thanks @wittjeff, all your commits are properly signed off. 🎉 |
Contributor
Merge Protections🟢 Merge protection satisfied — ready to merge. Show 1 satisfied protection🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
|
wittjeff
force-pushed
the
fix/drop-unreferenced-page-resources
branch
from
October 27, 2026 21:05
675fefb to
1b42ab6
Compare
The threaded parser writes every page to a standalone one-page PDF and keeps that buffer with the page result. When pages share one /Resources dictionary (docling-project#424), each buffer carried the compressed stream of every image in it, drawn or not. Before the buffer is written, remove the /Font and /XObject entries that no content stream of the page can resolve. The walk covers the page content, form XObjects, Type3 glyph procedures, tiling patterns, soft-mask groups and annotation appearance streams, since the stream decoder resolves names from all of them against the page resources. It visits each stream and dictionary once and edits only the /Font and /XObject dictionaries, so shared dictionaries stay shared. qpdf's removeUnreferencedResources() gives every form its own copy of a shared /Resources: on page 2 of a regression PDF whose 2298 figure forms share one /Resources, qpdf 12.4.2 spends 12 s on the page instead of 0.4 s and writes 94 MB instead of 1.5 MB. The page is written unchanged when a stream cannot be parsed, when qpdf fails to resolve an object, or when streams nest deeper than 32 levels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jeff Witt <1848307+wittjeff@users.noreply.github.com>
wittjeff
force-pushed
the
fix/drop-unreferenced-page-resources
branch
from
October 27, 2026 21:08
1b42ab6 to
67aa711
Compare
This branch has not been deployed
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.
Follow-up to #425, as requested in #425 (comment) and #425 (comment).
Problem
The threaded parser writes every page to a standalone one-page PDF, so that pages can be decoded in parallel, and each page result keeps that buffer. The copy of the page takes along everything its
/Resourcesnames. When all pages share one/Resources(LibreOffice output, #424), every page buffer carries the compressed stream of every image in the document, whether the page draws it or not. On the #424 repro this was about 55 MB per kept page.Change
get_thread_safe_page_buffer()now removes, from the copied page, the/Fontand/XObjectentries that no content stream of that page can use, beforeQPDFWriter::write(). The page belongs to the one-pageout_pdf, so the source document and the other pages do not change. QPDFWriter does not write streams that are no longer reachable.The new
src/parse/qpdf/page_resources.hdoes the walk. It does not callQPDFPageObjectHelper::removeUnreferencedResources(), for two reasons:/Resources, and the copies write the other resource categories (/ExtGState, ...) inline again. On page 2 of2508.13113v2.pdffrom the regression set, 2298 figure forms share one/Resources:qpdf 12.4.2 --pages . 2 -- --remove-unreferenced-resources=yestakes 11.9 s instead of 0.4 s and writes 94 MB instead of 1.5 MB, which is the opposite of what the page buffer needs. The walk here removes entries in place, so shared dictionaries stay shared. It also visits each stream and each dictionary once: a walk withforEachFormXObject()over the unchanged dictionaries calls its action 5.27 million times on that page, and a first version of this PR that did so took the file from 31 s to over an hour./Resources(9.6.5), tiling pattern cells, soft-mask groups and annotation appearance streams. qpdf scans only the page content and form XObjects, so it can remove an entry that docling-parse still uses.The walk collects every name operand from all of these streams. An entry whose name occurs in none of them cannot be resolved, so it is removed. The page is written as before when a stream cannot be parsed (qpdf throws or warns), when qpdf cannot resolve an object, or when streams nest deeper than 32 levels. The walk also reaches forms that are never drawn, so the depth cap keeps a crafted chain of them from exhausting the stack.
Results
mainand by 1.1 MB with this change.main: 71.0 s onmain, 70.4 s with this change.2508.13113v2.pdf: 30.8 s onmain, 30.2 s with this change.Tests
New in
tests/test_unit_shared_resource_images.py:test_pages_that_do_not_draw_a_shared_image_do_not_hold_its_stream: the RSS bound above (fails onmain).test_type3_glyph_keeps_an_image_it_takes_from_the_page_resources: a Type3 glyph draws an image that only the page resources name. The image must stay.test_forms_that_share_one_xobject_dictionary_are_visited_once: 1000 forms share one/XObjectdictionary; the page must parse in a few seconds.Unit tests and the regression suite pass on HF revision
6d3458e2.@lazariv, thank you for the offer: a run on the production PDFs behind #424 would be very welcome.
🤖 Generated with Claude Code