Repository navigation
Conversation
…ature A certification or FieldMDP signature carries /Reference[*]/Data, which Acrobat and iText point at the catalog. to_json() followed it into the page tree and the structure tree, and its cycle guard, a set of unparse() texts of the objects on the current path copied into every call, expanded every page again for each /Pg in the structure tree. Loading a signed, tagged document took minutes and gigabytes before any page was decoded. - stop at nested /Catalog, /Pages, /Page, /StructTreeRoot and /StructElem dictionaries and write a reference, as for the /Parent and /P keys - key the cycle guard on QPDFObjGen for indirect objects only, on one set passed by reference and unwound on return - pass max_level down the recursion; the callers that passed 16 use the default 32, which was the effective depth before 33-page tagged PDF with one DocMDP signature, load(page_range=(1, 1)): 48.6 s and 2.0 GB before, 0.06 s and 85 MB after. Fixes docling-project#399 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Jeff Witt <1848307+wittjeff@users.noreply.github.com>
process_document_from_bytesio() built the form, metadata, language and outline JSON on every load. They are now built on the first call to get_annotations(), get_meta_xml() or get_table_of_contents(), under thread_safe_buffer_mutex, since a worker of the threaded parser may be copying a page out of qpdf_document at that moment. A conversion that only reads pages no longer pays for them. Refs docling-project#399 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Jeff Witt <1848307+wittjeff@users.noreply.github.com>
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/
|
PeterStaar-IBM
self-requested a review
October 22, 2026 15:13
Member
|
@wittjeff can you fix the merge conflict? |
Resolves the conflict in src/parse/pdf_decoders/document.h with docling-project#415, which added qpdf_reported_damage next to json_annots: both are kept. Signed-off-by: Jeff Witt <1848307+wittjeff@users.noreply.github.com>
Member
|
@wittjeff We seem to have another merge conflict (just merged another of your PR's). |
Resolve the document.h conflict with docling-project#414: keep the load_error fields and the lazy-annotations comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jeff Witt <1848307+wittjeff@users.noreply.github.com>
Contributor
Author
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.
Fixes #399.
Problem
load()on a signed, tagged PDF spent minutes and gigabytes before any page was decoded, holding the GIL. Removing/AcroFormfrom the catalog made it instant, andpage_rangemade no difference.Cause
process_document_from_bytesio()built the document annotations eagerly, which runsto_json()on/AcroForm./Reference[*]/Data, which Acrobat and iText point at the catalog.to_json()followed that reference into the page tree and the structure tree.to_json()detected cycles with a set ofunparse()texts of the objects on the current path, copied into every recursive call. A shared object was expanded once per path, so every/Pgin the structure tree expanded its page again, fonts included.max_levelwas also not passed down the recursion.Measured on a 33-page tagged PDF from the regression corpus (
11586287754644715199-16.pdf: 15.6k structure elements, 6.4k/Pgreferences) with one synthetic DocMDP signature added with pikepdf:/Reference/Data→ catalog/StructTreeRootremoved(
DoclingThreadedPdfParser().load(path, page_range=(1, 1)), peak RSS of the process.) A 91-page untagged PDF with the same signature loaded in 0.02 s before and after: the structure tree is the multiplier.Change
to_json()stops at nested dictionaries whose/Typeis/Catalog,/Pages,/Page,/StructTreeRootor/StructElemand writes"[skipping /Catalog object '1 0 R']", the way it already skips the/Parentand/Pkeys. A page or the catalog is never form or annotation content.QPDFObjGenon one set passed by reference and unwound on return: nounparse(), no copies. Direct objects are not tracked, since they cannot close a cycle. The output for every existing case is unchanged; a cycle still yields"[ref to previous object: 'N 0 R']".max_levelis passed down the recursion. The callers that passed 16 now use the default 32, which was the effective depth before; the two font-program callers keep their explicit 2 (descriptor_jsonandstream_dict_jsonare not read anywhere).get_annotations(),get_meta_xml()orget_table_of_contents()call, underthread_safe_buffer_mutex, instead of during load. A conversion that only reads pages no longer pays for them. Theextract_doc_annotationstiming is recorded when they are first requested.Points 1 to 3 are one commit, point 4 a second one, so it can be dropped on its own if you prefer to keep the eager build.
Not included
Releasing the GIL during the native load, as suggested in the issue. After this change the load is qpdf's parse only. Releasing the GIL there needs a mutex around the per-key decoder maps against concurrent
load()calls on one parser, which is a separate change.Tests
tests/test_unit_signed_form_json.py: an in-memory signed, tagged fixture. Asserts that the form export keeps the signature fields and never reaches the page tree or the structure tree, with both parsers.tests/test_unit_*.py+tests/test_regression_parse.pyagainst the pinned dataset: 269 passed, 6 skipped (pre-existing), 0 failed.🤖 Generated with Claude Code