Skip to content

fix: return None from pil_image when the reference cannot be decoded - #916

Open
simpleqt wants to merge 1 commit into
docling-project:mainfrom
simpleqt:fix/get-image-unopenable-ref
Open

simpleqt wants to merge 1 commit into
docling-project:mainfrom
simpleqt:fix/get-image-unopenable-ref

Conversation

@simpleqt

Copy link
Copy Markdown
Contributor

fix: return None from pil_image when the reference cannot be decoded

ImageRef.pil_image is annotated PILImage.Image | None, and
FloatingItem.get_image documents that it returns None when no image is
available — but both forwarded PILImage.open without a guard:

>>> ref = ImageRef(dpi=72, mimetype="image/svg+xml", size=Size(width=120, height=80),
...                uri=AnyUrl("data:image/svg+xml;base64,PHN2ZyB4bWxucz0i..."))
>>> ref.pil_image
PIL.UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x...>

>>> item.get_image(doc)          # FloatingItem.get_image -> return self.image.pil_image
PIL.UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x...>

Three ways to reach it today:

  • an inline data:image/svg+xml reference — Pillow has no SVG decoder, and a
    Markdown or HTML backend that keeps such an image as a reference produces
    exactly this ref
  • a data: payload that is not image data at all
  • a local image file that is no longer readable

The consequence is not a missing image, which callers already skip, but an
exception from a method that promises None:
BaseItemAndImageEnrichmentModel.prepare_element calls element.get_image(...)
to feed the picture-description and classification models, and anything else
walking the pictures of a document inherits the crash.

The fix catches UnidentifiedImageError and returns None, which is what the
annotations and the docstring already promise.

Refusal to read a source at all stays an error. A blocked file: URI and a
payload past max_image_decoded_size keep raising ValueError; those are
pinned by test_image_ref_blocks_file_scheme and
test_image_ref_blocks_oversized_base64, which still pass. Only the decoder
failing to identify the data is swallowed.

Testing

Two tests in tests/test_docling_doc.py:

  • test_get_image_of_an_undecodable_ref_returns_none — a document with an inline
    SVG reference; asserts the payload is still referenced (image is not None),
    that pil_image is now None, and that PictureItem.get_image(doc) returns
    None instead of raising
  • test_pil_image_of_a_corrupt_data_uri_returns_none — a data:image/png
    payload that is not image data

With the new tests but the source at the base revision:
22 failed, 106 passed; with the fix: 20 failed, 108 passed. The delta is
exactly these two tests, and the 20 remaining failures are pre-existing (they
fail identically on the unmodified base commit — a Windows path artifact in the
test_load_local_images_* family and test_save_to_disk).

ruff at the pre-commit revision (0.16.10) passes check and format --check
on both changed files.

Unrelated to this change: running the suite rewrites
tests/data/doc/constructed_doc.dt in the working tree (a line-ending artifact
on Windows). It is not part of this commit.

Why this is here rather than in docling

This surfaced while fixing docling#4742
in docling, where an inline data:image/svg+xml image was silently dropped
because create_image_ref handed the payload to PIL.Image.open and returned
None when it failed. Keeping the payload as an ImageRef is the natural fix
there, but it only works once get_image() returns None for a ref that cannot
be decoded — otherwise it converts silent data loss into a crash in the picture
enrichment path. I withdrew that PR
(docling#4925) rather
than ship the regression; this is the part that belongs here, and it is a defect
on its own for corrupt data URIs and unreadable local files.

AI assistance: this change was produced with AI assistance (an autonomous coding
agent); the analysis, the reproduction, the before/after test runs and the lint
runs were executed locally, and a human reviewed the result before submission.

`ImageRef.pil_image` is annotated `PILImage.Image | None` and
`FloatingItem.get_image` documents that it returns None when no image is
available, but both forwarded `PILImage.open` without a guard:

  >>> ref = ImageRef(dpi=72, mimetype="image/svg+xml", size=Size(width=120, height=80),
  ...                uri=AnyUrl("data:image/svg+xml;base64,..."))
  >>> ref.pil_image
  PIL.UnidentifiedImageError: cannot identify image file

An inline `data:image/svg+xml` image is the case that surfaced this: Pillow
has no SVG decoder, so a Markdown or HTML backend that keeps such an image as a
reference produces a ref that raises. `FloatingItem.get_image` then raised
instead of returning None, and its callers that skip a picture without an image
broke - the picture enrichment models through `BaseItemAndImageEnrichmentModel
.prepare_element`, and every consumer walking the pictures of such a document.
A `data:` payload that simply is not image data, and a local image file that
is no longer readable, reach the same path today.

Catch UnidentifiedImageError and return None, which is what the annotations and
the docstring already promise. Refusal to read a source at all stays an error:
a blocked `file:` URI and a payload past `max_image_decoded_size` keep
raising ValueError, as their existing tests require.

Signed-off-by: simpleqt <2015900818@qq.com>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 13:26
@github-actions

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

Thanks @simpleqt, all your commits are properly signed off. 🎉

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@mergify

mergify Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 1 of 2 protections blocking · waiting on 👀 reviews

Protection Waiting on
🔴 Require two reviewer for test updates 👀 reviews
🟢 Enforce conventional commit —

🔴 Require two reviewer for test updates

Waiting for

  • #approved-reviews-by >= 2
This rule is failing.

When test data is updated, we require two reviewers

  • #approved-reviews-by >= 2

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)(?:\(.+\))?(!)?:

This branch has not been deployed

No deployments
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.

2 participants