diff --git a/src/ocrmypdf/pdfinfo/_contentstream.py b/src/ocrmypdf/pdfinfo/_contentstream.py index 23c68a22..e97133fe 100644 --- a/src/ocrmypdf/pdfinfo/_contentstream.py +++ b/src/ocrmypdf/pdfinfo/_contentstream.py @@ -134,7 +134,9 @@ def _normalize_stack(graphobjs): yield (operands, operator) -def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE): +def _interpret_contents( + contentstream: Object, initial_shorthand=UNIT_SQUARE, initial_fill_ink=Ink.mono +): """Interpret the PDF content stream. The stack represents the state of the PDF graphics stack. We track the @@ -163,7 +165,7 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE): """ stack = [] ctm = Matrix(initial_shorthand) - fill_ink = Ink.mono # PDF default fill color is black + fill_ink = initial_fill_ink # PDF default fill color is black fill_space = '/DeviceGray' # current fill colorspace name (for sc/scn) xobject_settings: list[XobjectSettings] = [] inline_images: list[InlineSettings] = [] @@ -216,6 +218,9 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE): fill_ink = _ink_from_components('cmyk', vals) fill_space = '/DeviceCMYK' elif operator == 'cs': + # Selecting a colorspace resets the fill color to that space's + # initial value, which is black for all device colorspaces. + fill_ink = Ink.mono if operands: fill_space = str(operands[0]) elif operator in ('sc', 'scn'): diff --git a/src/ocrmypdf/pdfinfo/_image.py b/src/ocrmypdf/pdfinfo/_image.py index 2891c0b2..f7a34738 100644 --- a/src/ocrmypdf/pdfinfo/_image.py +++ b/src/ocrmypdf/pdfinfo/_image.py @@ -352,13 +352,19 @@ def _find_form_xobject_images(pdf: Pdf, container: Object, contentsinfo: Content # but in practice both Form XObjects and multiple drawing of the # same object are both very rare. ctm_shorthand = settings.shorthand + # A Form XObject inherits the graphics state (including fill color) + # in effect at the Do that draws it, so a mask painted with an + # inherited gray/color fill must carry that classification inward. yield from _process_content_streams( - pdf=pdf, container=form_xobject, shorthand=ctm_shorthand + pdf=pdf, + container=form_xobject, + shorthand=ctm_shorthand, + initial_fill_ink=settings.fill_ink, ) def _process_content_streams( - *, pdf: Pdf, container: Object, shorthand=None + *, pdf: Pdf, container: Object, shorthand=None, initial_fill_ink=Ink.mono ) -> Iterator[VectorMarker | TextMarker | ImageInfo]: """Find all individual instances of images drawn in the container. @@ -399,7 +405,7 @@ def _process_content_streams( else: return - contentsinfo = _interpret_contents(container, initial_shorthand) + contentsinfo = _interpret_contents(container, initial_shorthand, initial_fill_ink) if contentsinfo.found_vector: yield VectorMarker() diff --git a/tests/test_pdfinfo.py b/tests/test_pdfinfo.py index 7cd277dc..d75949c9 100644 --- a/tests/test_pdfinfo.py +++ b/tests/test_pdfinfo.py @@ -440,3 +440,42 @@ def test_imageinfo_ink_black(mask_black_pdf): 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 + + +def test_fill_ink_cs_resets_color_to_black(): + # `cs` resets the fill color to the colorspace's initial value (black), + # so a stale color set before `cs` must not leak to the drawn mask. + assert _ink_of_first_xobject(b"0.8 0.2 0.2 rg /DeviceGray cs /Im0 Do") is Ink.mono + + +def test_imageinfo_ink_inherited_in_form_xobject(outdir): + # A mask drawn inside a Form XObject inherits the fill color set before the + # Do that paints the form; the gray classification must reach the mask. + pdf = pikepdf.Pdf.new() + pdf.add_blank_page(page_size=(72, 72)) + + mask = pikepdf.Stream(pdf, bytes([0x7E] * 8)) + mask.Type = pikepdf.Name.XObject + mask.Subtype = pikepdf.Name.Image + mask.Width = 8 + mask.Height = 8 + mask.ImageMask = True + mask.BitsPerComponent = 1 + + # Form draws the mask with no color of its own, inheriting the caller's. + form = pikepdf.Stream(pdf, b"q 72 0 0 72 0 0 cm /Im0 Do Q") + form.Type = pikepdf.Name.XObject + form.Subtype = pikepdf.Name.Form + form.BBox = [0, 0, 72, 72] + form.Resources = pikepdf.Dictionary(XObject=pikepdf.Dictionary(Im0=mask)) + + fname = pdf.pages[0].add_resource(form, pikepdf.Name.XObject) + pdf.pages[0].Contents = pikepdf.Stream( + pdf, b"0.263 0.263 0.263 rg %s Do" % bytes(fname) + ) + out = outdir / 'form_mask.pdf' + pdf.save(out) + + image = pdfinfo.PdfInfo(out)[0].images[0] + assert image.type_ == 'stencil' + assert image.ink is Ink.gray