From eae8fc9e128170328b66f3e5e59f53aaee057fb3 Mon Sep 17 00:00:00 2001 From: wfatih Date: Fri, 2 Oct 2026 22:08:43 +0300 Subject: [PATCH] fix(image): flatten transparent images onto white instead of black Signed-off-by: wfatih --- docling/backend/image_backend.py | 18 ++++++++++++++++-- tests/test_backend_image_native.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docling/backend/image_backend.py b/docling/backend/image_backend.py index c5c9de05a1..0deb1866d0 100644 --- a/docling/backend/image_backend.py +++ b/docling/backend/image_backend.py @@ -69,6 +69,20 @@ def _oriented_frame(image: Image.Image) -> tuple[Image.Image, tuple[float, float return oriented, (dpi_x, dpi_y) +def _to_rgb(frame: Image.Image) -> Image.Image: + """Convert a frame to RGB, flattening any transparency onto white. + + A plain ``convert("RGB")`` drops the alpha channel and keeps the colour + stored under it, which is usually black for fully transparent pixels. Dark + content on a transparent background would then become a black page. + """ + if frame.mode in ("RGBA", "LA", "PA") or "transparency" in frame.info: + rgba = frame.convert("RGBA") + background = Image.new("RGBA", rgba.size, (255, 255, 255, 255)) + return Image.alpha_composite(background, rgba).convert("RGB") + return frame.convert("RGB") + + class _ImagePageBackend(PdfPageBackend): def __init__( self, image: Image.Image, page_no: int, dpi: tuple[float, float] @@ -235,11 +249,11 @@ def __init__( img.seek(i) frame, dpi = _oriented_frame(img) self._frame_dpi.append(dpi) - self._frames.append(frame.convert("RGB")) + self._frames.append(_to_rgb(frame)) else: frame, dpi = _oriented_frame(img) self._frame_dpi.append(dpi) - self._frames.append(frame.convert("RGB")) + self._frames.append(_to_rgb(frame)) except Exception as e: for frame in self._frames: frame.close() diff --git a/tests/test_backend_image_native.py b/tests/test_backend_image_native.py index 940a6c0a3b..664d84246c 100644 --- a/tests/test_backend_image_native.py +++ b/tests/test_backend_image_native.py @@ -187,6 +187,35 @@ def test_exif_orientation_swaps_dpi_axes(): assert min(page_backend.get_page_image().getpixel((2, 2))) > 128 +@pytest.mark.parametrize("mode", ["RGBA", "LA", "P"]) +def test_transparent_background_is_flattened_onto_white(mode): + """Transparent pixels become white instead of the colour stored under them. + + Fully transparent pixels usually store black, so dropping the alpha channel + turned dark text on a transparent background into a black page. + """ + img = Image.new("RGBA", (64, 48), (0, 0, 0, 0)) + img.paste((0, 0, 0, 255), (0, 0, 16, 12)) + if mode == "LA": + img = img.convert("LA") + elif mode == "P": + # Index 0 is transparent black, index 1 opaque black. + img = Image.new("P", (64, 48), 0) + img.putpalette([0, 0, 0, 0, 0, 0]) + img.paste(1, (0, 0, 16, 12)) + buf = BytesIO() + img.save(buf, format="PNG", **({"transparency": 0} if mode == "P" else {})) + buf.seek(0) + + page_backend = _get_backend_from_stream( + DocumentStream(name="test.png", stream=buf) + ).load_page(0) + image = page_backend.get_page_image() + + assert image.getpixel((63, 47)) == (255, 255, 255) + assert image.getpixel((2, 2)) == (0, 0, 0) + + def test_get_page_image_full(): """Test getting full page image.""" width, height = 100, 80