Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions docling/backend/image_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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()
Expand Down
29 changes: 29 additions & 0 deletions tests/test_backend_image_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading