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,