Repository navigation
fix: decode image XObject samples only when a page draws them - #425
Conversation
Pages that share one /Resources dictionary, as LibreOffice writes them, list every image of the document. Each page decoder defiltered all of them in set(), so every page result held the decoded pixels of images it never paints: a shared dictionary with 8 Flate photos cost ~50 MB per kept page instead of ~8 MB. parse() now reads only the image dictionary. load_pixels() defilters the samples and the soft mask on the first call to a pixel getter, which only Do_image and save_to_file make. Fixes docling-project#424 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jeff Witt <1848307+wittjeff@users.noreply.github.com>
|
✅ DCO Check Passed Thanks @wittjeff, all your commits are properly signed off. 🎉 |
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/
|
|
Tested on the production PDFs behind #424, built from ac2fe96 in the docling-serve 1.38.1 image (local engine, H100 MIG 1g.12gb, OCR + tables + formula/code enrichment + picture classification). Peak RSS of the conversion:
Conversion time is unchanged (within 2%). The Markdown is identical to 7.23.0 for the first two documents; the third differs by 0.04% of words, and docling#5023 alone shows the same difference, so it looks like run-to-run variation. One cost remains. On the synthetic repro from #424 this PR still holds about 55 MB per kept page (5.7 GB for 100 pages, against 1.2 GB unshared); its images compress poorly (a 36 MB file). As far as I can read |
yes, we write every page so we ca parallelize, so this is a feature we do need. |
@PeterStaar-IBM Incoming. |
|
Thanks for merging! To clarify my last paragraph: I wasn't suggesting dropping the per-page write. The idea was to keep it and prune the copied page before it is written, in out_page.removeUnreferencedResources();
QPDFWriter writer(out_pdf);
If that's what "Incoming" is, I'm happy to run the next PR on the same production PDFs. |
Fixes #424.
Problem
Pages that share one
/Resourcesdictionary (as LibreOffice writes them) list every image of the document.pdf_resource<PAGE_XOBJECTS>::set()parses every key, andpdf_resource<PAGE_XOBJECT_IMAGE>::parse()defiltered the samples and the soft mask at once. Thexobject_parse_cacheworks only inside one page decoder, and the threaded parser opens aQPDFper page. So each page result held the decoded pixels of every image in the shared dictionary, whether the page draws it or not.Change
parse()reads only the image dictionary: size, colour space, filters,/Decode, palette.load_pixels() construnsinit_stream_data()andinit_soft_mask_data()on the first call to a pixel getter (get_raw_stream_data(),get_codec_stream_data(),get_decoded_stream_data(),get_soft_mask_data(), thehas_*forms and the soft-mask size getters). OnlyDo_imageandsave_to_file()call these, so a page decodes only the images it draws.QPDFObjectHandlemembers aremutable. One page decoder runs on one thread, and its QPDF document stays alive untilDoruns.extract_bitmap_pixels=falsekeeps its meaning:load_pixels()does nothing.Measurement
20 pages, 8 RGB 1600×1200 images in one
/Resources, drawn only on page 1. RSS growth per keptPageParseResult,DoclingThreadedPdfParser:Tests
tests/test_unit_shared_resource_images.py: an 8-page in-memory PDF (built withtests/pdf_builder.py) with one shared 34 MB Flate image drawn on page 1. It checks that only page 1 has the bitmap and limits the RSS growth of the kept results to 3× one decoded copy. On 7.23.0 it fails with 311 MB; with this PR it passes. It reads/proc/self/statusand skips where that does not exist.6d3458e2: 106 passed. No groundtruth changes.🤖 Generated with Claude Code