From 87f918f58c3758152035383ff370c039d6a3adee Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Fri, 5 Jun 2026 00:57:08 -0700 Subject: [PATCH] feat: expose fill-color ink classification on ImageInfo (#1688) --- src/ocrmypdf/pdfinfo/_image.py | 26 +++++++++++++-- tests/test_pdfinfo.py | 61 ++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/ocrmypdf/pdfinfo/_image.py b/src/ocrmypdf/pdfinfo/_image.py index 3ae8fef0..2891c0b2 100644 --- a/src/ocrmypdf/pdfinfo/_image.py +++ b/src/ocrmypdf/pdfinfo/_image.py @@ -36,6 +36,7 @@ from ocrmypdf.pdfinfo._types import ( UNIT_SQUARE, Colorspace, Encoding, + Ink, ) logger = logging.getLogger() @@ -61,10 +62,12 @@ class ImageInfo: pdfimage: Object | None = None, inline: PdfInlineImage | None = None, shorthand=None, + fill_ink: Ink | None = None, ): """Initialize an ImageInfo.""" self._name = str(name) self._shorthand = shorthand + self._fill_ink = fill_ink pim: PdfInlineImage | PdfImage @@ -175,6 +178,17 @@ class ImageInfo: """Type of image, either 'image' or 'stencil'.""" return self._type + @property + def ink(self) -> Ink | None: + """Fill-color classification for stencil masks, else None. + + A stencil (image mask) is painted with the current fill color; this + reports whether that color is mono/gray/color so the rasterizer can + choose a device that does not discard the distinction. Non-stencil + images return None. + """ + return self._fill_ink if self._type == 'stencil' else None + @property def width(self) -> int: """Width of the image in pixels.""" @@ -249,7 +263,10 @@ def _find_inline_images(contentsinfo: ContentsInfo) -> Iterator[ImageInfo]: """Find inline images in the contentstream.""" for n, inline in enumerate(contentsinfo.inline_images): yield ImageInfo( - name=f'inline-{n:02d}', shorthand=inline.shorthand, inline=inline.iimage + name=f'inline-{n:02d}', + shorthand=inline.shorthand, + inline=inline.iimage, + fill_ink=inline.fill_ink, ) @@ -300,7 +317,12 @@ def _find_regular_images( # these from our DPI calculation for the page. continue - yield ImageInfo(name=draw.name, pdfimage=pdfimage, shorthand=draw.shorthand) + yield ImageInfo( + name=draw.name, + pdfimage=pdfimage, + shorthand=draw.shorthand, + fill_ink=draw.fill_ink, + ) def _find_form_xobject_images(pdf: Pdf, container: Object, contentsinfo: ContentsInfo): diff --git a/tests/test_pdfinfo.py b/tests/test_pdfinfo.py index d0323f5b..8d5d0708 100644 --- a/tests/test_pdfinfo.py +++ b/tests/test_pdfinfo.py @@ -371,3 +371,64 @@ def test_fill_ink_tolerates_malformed_color_operands(body): ) def test_ink_from_components(space, comps, expected): assert _ink_from_components(space, comps) is Ink[expected] + + +def _make_image_mask_pdf(path, content_fill: bytes): + """Build a 1-page PDF with one 8x8 image mask painted with content_fill. + + content_fill is the color operator sequence emitted before drawing the + mask, e.g. b"0.263 0.263 0.263 rg". + """ + pdf = pikepdf.Pdf.new() + pdf.add_blank_page(page_size=(72, 72)) + # 8x8 1-bpc mask, each row padded to a byte (1 byte per row). + mask_bytes = bytes([0x7E] * 8) + mask = pikepdf.Stream(pdf, mask_bytes) + mask.Type = pikepdf.Name.XObject + mask.Subtype = pikepdf.Name.Image + mask.Width = 8 + mask.Height = 8 + mask.ImageMask = True + mask.BitsPerComponent = 1 + name = pdf.pages[0].add_resource(mask, pikepdf.Name.XObject) + pdf.pages[0].Contents = pikepdf.Stream( + pdf, b"q 72 0 0 72 0 0 cm %s %s Do Q" % (content_fill, bytes(name)) + ) + pdf.save(path) + return path + + +@pytest.fixture +def mask_gray_pdf(outdir): + return _make_image_mask_pdf(outdir / 'mask_gray.pdf', b"0.263 0.263 0.263 rg") + + +@pytest.fixture +def mask_rgb_pdf(outdir): + return _make_image_mask_pdf(outdir / 'mask_rgb.pdf', b"0.8 0.2 0.2 rg") + + +@pytest.fixture +def mask_black_pdf(outdir): + return _make_image_mask_pdf(outdir / 'mask_black.pdf', b"0 g") + + +def test_imageinfo_ink_gray(mask_gray_pdf): + image = pdfinfo.PdfInfo(mask_gray_pdf)[0].images[0] + assert image.type_ == 'stencil' + assert image.ink is Ink.gray + + +def test_imageinfo_ink_color(mask_rgb_pdf): + image = pdfinfo.PdfInfo(mask_rgb_pdf)[0].images[0] + assert image.ink is Ink.color + + +def test_imageinfo_ink_black(mask_black_pdf): + image = pdfinfo.PdfInfo(mask_black_pdf)[0].images[0] + assert image.ink is Ink.mono + + +def test_imageinfo_ink_none_for_regular_image(eight_by_eight_regular_image): + image = pdfinfo.PdfInfo(eight_by_eight_regular_image)[0].images[0] + assert image.ink is None