fix: inherit fill color into Form XObjects; reset fill color on cs (#1688)
Address final review findings: a mask painted inside a Form XObject now inherits the fill color in effect at the form's Do operator (previously it reset to black, missing gray/color promotion one indirection deep). The cs operator now resets the fill color to black per PDF spec, so a stale color set before cs cannot leak to a subsequently drawn mask.
This commit is contained in:
@@ -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'):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user