Skip to content

Segfault in get_image(scale=3.0) on a scanned CCITT page (regression in 7.10.0) #365

Open

Description

@tyler-waite

Summary

PageParseResult.get_image(scale=3.0) segfaults on page 7 of a public scanned PDF. The same page renders at 2.99 and at 3.01, and every other page of the document renders at 3.0. It's a regression: 7.9.0 renders the page, 7.10.0 through 7.22.1 segfault.

We hit it through Docling: RapidOCR renders every page at its default scale=3.0, so a full conversion with OCR on crashes the whole process.

Reproduce

The PDF is a Honda engine owner's manual (39 pages, Acrobat Capture 3.0 "Searchable Image"):
https://cdn.powerequipment.honda.com/engines/pdf/manuals/31ZH7600.pdf (sha256 cf9ddf8f0890ed28082d9c5dfbea383635755dbbc56d56c0c6c68d15e64c6141)

# pip install docling-parse==7.22.1
# python -X faulthandler repro.py 31ZH7600.pdf 3.0 7
import sys

from docling_parse.pdf_parser import DoclingThreadedPdfParser, RenderConfig, ThreadedPdfParserConfig

path, scale = sys.argv[1], float(sys.argv[2])
pages = [int(p) for p in sys.argv[3:]] or None

render = RenderConfig()
render.scale = 1.0
parser = DoclingThreadedPdfParser(parser_config=ThreadedPdfParserConfig(threads=1, render_config=render))
key = parser.load(path, page_numbers=pages)
for result in parser.iterate_results():
    n = result.page_number
    print(f"page {n}: size {result.page_width:.2f} x {result.page_height:.2f} pt, rendering at {scale}", flush=True)
    img = result.get_image(scale=scale)
    print(f"page {n}: ok {img.size}", flush=True)
parser.unload(key)
print("done")

Output on 7.22.1 (python:3.12-slim, linux/arm64; linux/amd64 crashes the same way):

page 7: size 426.24 x 598.56 pt, rendering at 3.0
Fatal Python error: Segmentation fault

Current thread 0x0000ffff97a874c0 (most recent call first):
  File ".../docling_parse/pdf_parser.py", line 1378 in _render_image_at_scale
  File ".../docling_parse/pdf_parser.py", line 1443 in get_image
  File "/w/repro.py", line 18 in <module>

It doesn't depend on threading: threads=1, a single page (page_numbers=[7]), and no other work in the process.

What varies

Page 7 at different scales (7.22.1):

scale 2.0 2.5 2.8 2.9 2.95 2.99 3.0 3.01 3.05 3.1 3.5 4.0
page 7 ok ok ok ok ok ok SIGSEGV ok ok ok ok ok

Versions (page 7, scale 3.0):

docling-parse 7.0.0 7.5.0 7.8.0 7.8.1 7.9.0 7.10.0 7.12.1 7.18.0 7.21.0 7.22.1
result ok ok ok ok ok (1278×1794) SIGSEGV SIGSEGV SIGSEGV SIGSEGV SIGSEGV

7.10.0 is #305 (2229782), which changed canvas sizing and added the rotation path in src/render/blend2d_renderer.h. That might be a place to start. We haven't located the faulting line.

About the page: its crop box is 426.24 × 598.56 pt, and its media box is 624.24 × 796.56 pt. It has no /Rotate. Its only content is one 2592×3312 1-bit /CCITTFaxDecode image (/K -1, /Uncompressed true, /DeviceGray) plus an invisible OCR text layer. Page 6 of the same file has the same structure (a 2568×3299 CCITT G4 image) and renders at every scale above.

Environment

  • docling-parse 7.22.1 (also 7.10.0 to 7.21.0), from PyPI wheels
  • Python 3.12, Debian (python:3.12-slim), linux/arm64 native, and linux/amd64
  • Through Docling 2.131.0 (ThreadedDoclingParseDocumentBackend, RapidOCR default scale=3.0), the full pipeline crashes the same way; PyPdfiumDocumentBackend converts the file.

Activity

  1. wittjeff commented on Oct 6, 2026

    @wittjeff
    Contributor

    Thanks for the precise report, it made this straightforward to pin down.

    Root cause. This is a bug in Blend2D's JIT analytic fill, not in docling-parse code. The page is one CCITT bitmap placed with a 0.17° skew, so it is blitted through the affine path. At scale 3.0 the canvas is 1279 px wide; 1279 % 256 = 255, which puts the clip's x_end cell on bit 63 of the last 64-bit coverage word in Blend2D's rasterizer. The skewed top edge crosses the right clip edge within row 0 and flags every cell of that word. Blend2D's FillAnalyticPart then re-enters its bit-scan loop with a stale span length (0, left over from the constant-coverage loop) instead of 64, computes a zero-length span, and its per-pixel do/while loop runs 2^32 times until the destination pointer leaves the heap. Details, a 30-line standalone reproducer, and the gdb trace are in blend2d/blend2d#267; the fix is blend2d/blend2d#268.

    Why only 3.0, and why only since 7.10.0. The condition is canvas_width % 256 in 252..255 plus an image edge that reaches the clip edge. 2.99 and 3.01 give 1275 and 1283 px canvases, which are outside that range (2.992..3.0 all crash, 2.991 and 3.001 do not). Affine image blits exist since v5.9; 7.9.0 rendered the page only because its int-truncated crop box produced a 1278 x 1794 canvas with the top edge 1.7 px higher, which kept the skewed edge above the canvas. #305 moved the geometry by about 2 px into the failing configuration. The same crash reproduces with Blend2D alone for any pattern or image fill in this configuration, independent of CPU features, thread count, sampling quality, or source image size.

    Fix. #387 carries the Blend2D fix as a patch on the pinned commit until the pin moves past the upstream merge, and adds a regression test with a synthetic page on the crashing geometry. With it, page 7 renders at 3.0, all 39 pages render, and the patched output is byte-identical to Blend2D's reference pipeline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions