Repository navigation
fix(render): decode reduced bitmaps again for a finer re-render - #429
Merged
Merged
Conversation
Signed-off-by: ExtReMLapin <3909752+ExtReMLapin@users.noreply.github.com>
Caching every non-default-scale render keeps one full page image per live page. Docling asks for full-page images at images_scale in preprocessing and at scale 2 for tables, and caches those itself in Page._image_cache, so the parser cache only duplicated them. On a 40-page scanned document with images_scale=2 that added about 330 MiB at peak. Keep the render only when the request has a cropbox, which is the OCR case this cache is for. A full-page request still reuses a cached render at the same scale. Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
The render pipeline decodes oversampled /DCTDecode and /JPXDecode images at 1/2, 1/4 or 1/8 resolution, sized for the configured render scale. PageParseResult.get_image(scale=...) or get_image(canvas_size=...) renders the same instructions again; at a finer scale it upscaled those reduced samples, so the page came out blurred (e.g. OCR crops at scale 3 after a scale-1 render). A bitmap instruction decoded at a reduced resolution now keeps a re-decoder that holds the image's encoded bytes, which the page keeps anyway. The renderer asks for samples sized for its own resolution and draws those when the stored ones are too coarse. A render at the configured scale or below does not decode again. Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
Contributor
|
✅ DCO Check Passed Thanks @cau-git, 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
previously approved these changes
Oct 28, 2026
The re-decoder was installed whenever a reduction was requested. Only /DCTDecode and /JPXDecode honour that request; /FlateDecode, raw, CCITT and JBIG2 images, and streams QPDF already decoded, come back at full resolution. A finer get_image(scale=...) then decoded those images again and drew the same pixels: ~14 ms and 12 MiB more for a 2048x2048 RGB Flate image, ~39 ms for an A4 300 dpi CCITT scan. Install it only when the decoded samples are smaller than the stored image. Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
Member
Author
|
@yqliving FYI. |
PeterStaar-IBM
approved these changes
Oct 28, 2026
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.
Stacked on #405. The first two commits are #405. Review only the last two commits, from
fix(render): decode reduced bitmaps again for a finer re-renderon. A squash merge removes the overlap.Problem
Since #333, the render pipeline decodes oversampled
/DCTDecodeand/JPXDecodeimages at 1/2, 1/4 or 1/8 resolution, sized for the configured render scale. The page keeps only these reduced samples.PageParseResult.get_image(scale=...)orget_image(canvas_size=...)at a finer scale renders the same instructions again, so it upscales the reduced samples. The result is blurred.Example: a full-page 300 DPI scan, parsed with the threaded parser at render scale 1 (the default in Docling). The scan is decoded at 1/4 resolution. Docling's OCR then crops the page at scale 3, and the OCR input is about as sharp as a 77 DPI image. The workaround is
render_scale=3, which makes every full-page render 3x.This is very likely also the cause of docling-project/docling#4350 (picture and page images at
images_scale>render_scale).Fix
render_bitmapasks for samples sized for its own resolution (scale, or the effective scale of acanvas_sizerender). It decodes the image again only when the stored samples are too coarse.scale,canvas_sizeandcropboxrequests, and the C++ apps./DCTDecode,/JPXDecode). Flate, uncompressed, CCITT and JBIG2 images already decode at full resolution and are drawn from their stored samples at any scale. Without this check, a finer re-render decoded them again with no change: about 14 ms for a 2048x2048 Flate image and about 39 ms for an A4 300 dpi CCITT scan.The re-decode costs about 0.6 s per call on a full-page scan. #405 keeps the scaled render for cropped requests, so OCR crops on one page pay this cost only once.
Results
Scanned one-page invoice, threaded parser at render scale 1, then
get_image(scale=3):10 OCR-like crops at scale 3 on the same page:
Docling end-to-end on a 40-page synthetic scan (macOS arm64): 22–23 s per conversion with this PR, against 31 s with the fix only.
Tests
tests/test_unit_rerender_reduced_bitmaps.py: renders a synthetic JPEG page at scale 1, callsget_image(scale=3), and compares it with a native scale-3 render. It fails on main and passes with the fix.Limits
/Mask,/Indexedcolours or image masks are never decoded at reduced resolution, so they do not need this.