Skip to content

fix(render): decode reduced bitmaps again for a finer re-render - #429

Merged
cau-git merged 4 commits into
mainfrom
fix/rerender-reduced-bitmap-resolution
Oct 28, 2026
Merged

cau-git merged 4 commits into
mainfrom
fix/rerender-reduced-bitmap-resolution

Conversation

@cau-git

@cau-git cau-git commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

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-render on. A squash merge removes the overlap.

Problem

Since #333, the render pipeline decodes oversampled /DCTDecode and /JPXDecode images 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=...) or get_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

  • A bitmap instruction that was decoded at a reduced resolution keeps a re-decoder. The re-decoder holds the encoded bytes, which the page keeps anyway. It does not keep full-resolution pixels.
  • render_bitmap asks for samples sized for its own resolution (scale, or the effective scale of a canvas_size render). It decodes the image again only when the stored samples are too coarse.
  • A render at the configured scale or below does not decode again.
  • The fix is in the renderer, so it covers scale, canvas_size and cropbox requests, and the C++ apps.
  • The re-decoder is kept only for samples that the codec actually decoded at a reduced resolution (/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):

Mean abs gray difference vs a native scale-3 render
Before 4.89
After 0.00

10 OCR-like crops at scale 3 on the same page:

Time Output
main ~0.7 s blurred
this fix without #405 6.4 s sharp
this PR (fix + #405) 0.70 s sharp

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

  • New test tests/test_unit_rerender_reduced_bitmaps.py: renders a synthetic JPEG page at scale 1, calls get_image(scale=3), and compares it with a native scale-3 render. It fails on main and passes with the fix.
  • Full suite on the fix commit: 411 passed, 2 skipped. Render tests on this stacked tree: 39 passed.
  • No ground-truth data changes: native-scale output does not change.

Limits

  • Images with a soft mask, /Mask, /Indexed colours or image masks are never decoded at reduced resolution, so they do not need this.
  • Re-decoded samples are not cached. A re-render at a new finer scale decodes again.

ExtReMLapin and others added 3 commits October 28, 2026 13:32
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>
@github-actions

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

Thanks @cau-git, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 Merge protection satisfied — ready to merge.

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

PeterStaar-IBM
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>
@cau-git

cau-git commented Oct 28, 2026

Copy link
Copy Markdown
Member Author

@yqliving FYI.

@cau-git
cau-git merged commit 436ff5a into main Oct 28, 2026
47 checks passed
@cau-git
cau-git deleted the fix/rerender-reduced-bitmap-resolution branch October 28, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants