From 7f69b1eaa0119e982843a59a3b7dd0f504ee0717 Mon Sep 17 00:00:00 2001 From: simpleqt <2015900818@qq.com> Date: Wed, 21 Oct 2026 21:25:04 +0800 Subject: [PATCH] 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,...")) >>> 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> --- docling_core/types/doc/common/reference.py | 19 +++++++-- tests/test_docling_doc.py | 46 ++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/docling_core/types/doc/common/reference.py b/docling_core/types/doc/common/reference.py index d99040bfb..73659c6d8 100644 --- a/docling_core/types/doc/common/reference.py +++ b/docling_core/types/doc/common/reference.py @@ -118,7 +118,14 @@ def _load_local_image(self, base_dir: Path | None = None) -> None: @property def pil_image(self) -> PILImage.Image | None: - """Return the PIL Image.""" + """Return the PIL Image, or None if its data cannot be decoded. + + A URI may point at data that Pillow cannot open: an inline + ``data:image/svg+xml`` (Pillow has no SVG decoder), a ``data:`` payload + that is not an image, or a local image file that is no longer readable. + Refusal to read a source at all stays an error - a blocked ``file:`` + URI and an oversized payload keep raising :class:`ValueError`. + """ if self._pil is not None: return self._pil @@ -134,7 +141,10 @@ def pil_image(self) -> PILImage.Image | None: max_size=settings.max_image_decoded_size, label="Image file", ) - self._pil = PILImage.open(local_path) + try: + self._pil = PILImage.open(local_path) + except PILImage.UnidentifiedImageError: + return None elif isinstance(self.uri, AnyUrl) and self.uri.scheme == "data": encoded_img = str(self.uri).split(",")[1] decoded_img = base64.b64decode(encoded_img) @@ -142,7 +152,10 @@ def pil_image(self) -> PILImage.Image | None: if len(decoded_img) > settings.max_image_decoded_size: raise ValueError(f"Decoded image exceeds size limit of {settings.max_image_decoded_size} bytes.") - self._pil = PILImage.open(BytesIO(decoded_img)) + try: + self._pil = PILImage.open(BytesIO(decoded_img)) + except PILImage.UnidentifiedImageError: + return None # else: HTTP(S) and other remote schemes are intentionally not fetched. # If remote fetch is enabled, it must use host allowlists and block # private, link-local, and cloud-metadata addresses (SSRF controls). diff --git a/tests/test_docling_doc.py b/tests/test_docling_doc.py index 1972c199e..a2a4f27d0 100644 --- a/tests/test_docling_doc.py +++ b/tests/test_docling_doc.py @@ -798,6 +798,52 @@ def test_pil_image(): assert reloaded_image.tobytes() == fig_image.tobytes() +def test_get_image_of_an_undecodable_ref_returns_none(): + """A reference whose data Pillow cannot open must not raise. + + ``FloatingItem.get_image`` is typed and documented as returning ``None`` when + no image is available, but it forwarded ``ImageRef.pil_image``, which called + ``PILImage.open`` unguarded. An inline ``data:image/svg+xml`` reference (which + a Markdown or HTML backend produces when the inline image is an SVG) made + ``get_image`` raise ``UnidentifiedImageError`` instead of returning ``None``, + so callers that skip a picture without an image crashed - the picture + enrichment models through ``prepare_element``, and any consumer that walks + the pictures of such a document. + """ + svg = ( + '' + '' + ) + svg_data_uri = "data:image/svg+xml;base64," + base64.b64encode(svg.encode()).decode() + + doc = DoclingDocument(name="Untitled 1") + doc.add_picture( + image=ImageRef( + dpi=72, + mimetype="image/svg+xml", + size=Size(width=120, height=80), + uri=AnyUrl(svg_data_uri), + ) + ) + + picture = doc.pictures[0] + assert picture.image is not None # the payload is still referenced + assert picture.image.pil_image is None # but there are no pixels to decode + assert picture.get_image(doc=doc) is None + + +def test_pil_image_of_a_corrupt_data_uri_returns_none(): + """The same holds for a data URI that simply is not image data.""" + image_ref = ImageRef( + dpi=72, + mimetype="image/png", + size=Size(width=1, height=1), + uri=AnyUrl("data:image/png;base64," + base64.b64encode(b"not an image").decode()), + ) + + assert image_ref.pil_image is None + + def test_image_ref(): data_uri = { "dpi": 72,