mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(render): a knockout clear follows a shading's /BBox and skips images that don't decode (#2971)
Review on #2974. In the erase pass `set_paint` returned no clip, so a shading fill cleared its whole path while the redraw is clipped to the shading's /BBox (ISO 32000-2 Table 77): red outside the box stayed erased. It now returns the shading's clip there too. And an unmasked image cleared its placement before decoding, so an image that doesn't decode (over MAX_IMAGE_PIXELS, damaged data) erased the group's earlier artwork and drew nothing; the clear now happens only when the image decodes, asked only in a knockout group (Device::in_knockout_group). The stencil case in the test now also checks that the stencil draws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
82023e2ffe
commit
3df8a6386d
@@ -6056,7 +6056,7 @@ trailer << /Root 1 0 R >>
|
||||
4 0 obj << /Length 39 >> stream
|
||||
/Fm1 Do /G gs 0 1 0 rg 10 10 30 30 re f
|
||||
endstream endobj
|
||||
5 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 300 200] /Group << /S /Transparency {group} >> /Resources << /ExtGState << /A << /ca 0.5 >> >> /XObject << /In 6 0 R /Msk 7 0 R /Neu 8 0 R /Img 9 0 R /Sm 10 0 R /Nimg 12 0 R /Deep 13 0 R >> >> /Length {} >> stream
|
||||
5 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 300 200] /Group << /S /Transparency {group} >> /Resources << /ExtGState << /A << /ca 0.5 >> >> /XObject << /In 6 0 R /Msk 7 0 R /Neu 8 0 R /Img 9 0 R /Sm 10 0 R /Nimg 12 0 R /Deep 13 0 R /Shg 16 0 R /Big 17 0 R >> >> /Length {} >> stream
|
||||
{form}
|
||||
endstream endobj
|
||||
6 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 300 200] /Group << /S /Transparency >> /Resources << /ExtGState << /A << /ca 0.5 >> >> >> /Length 34 >> stream
|
||||
@@ -6086,6 +6086,13 @@ endstream endobj
|
||||
14 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 300 200] /Group << /S /Transparency >> /Length 28 >> stream
|
||||
0 0 1 rg 100 50 150 120 re f
|
||||
endstream endobj
|
||||
15 0 obj << /PatternType 2 /Shading << /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 1 0] /Function << /FunctionType 2 /Domain [0 1] /C0 [0 0 1] /C1 [0 0 1] /N 1 >> /Extend [true true] /BBox [100 50 140 170] >> >> endobj
|
||||
16 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 300 200] /Group << /S /Transparency >> /Resources << /Pattern << /P 15 0 R >> >> /Length 38 >> stream
|
||||
/Pattern cs /P scn 100 50 150 120 re f
|
||||
endstream endobj
|
||||
17 0 obj << /Type /XObject /Subtype /Image /Width 20000 /Height 20000 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /ASCIIHexDecode /Length 7 >> stream
|
||||
0000FF>
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF",
|
||||
form.len()
|
||||
@@ -6151,6 +6158,24 @@ trailer << /Root 1 0 R >>
|
||||
// A stencil mask must not erase the red beside it.
|
||||
let stencil = render(ko, &format!("{red}0 0 1 rg q 150 0 0 120 100 50 cm /Msk Do Q"));
|
||||
check(&stencil, 50, 40, [255, 128, 128], "red beside a stencil");
|
||||
// Where the stencil paints (every sample 0), its blue is drawn (review on #2974: the check
|
||||
// above alone passed even if the stencil drew nothing). A stencil composites as it did
|
||||
// before rather than knocking the red out, and its exact colour also depends on #2975 (a
|
||||
// stencil's fill opacity applied twice), so only "blue was drawn here" is pinned.
|
||||
let under = px(&stencil, 130, 100);
|
||||
assert!(under != [255, 128, 128] && under[2] >= 150 && under[0] < 230, "stencil drew nothing: {under:?}");
|
||||
|
||||
// A shading's /BBox clips it (ISO 32000-2 Table 77), so a shading fill's shape is its path
|
||||
// inside the box: the erase pass must clear only that, or the red outside the box but
|
||||
// inside the path stays erased (review on #2974).
|
||||
let shaded = render(ko, &format!("{red}/Shg Do"));
|
||||
check(&shaded, 120, 100, [128, 128, 255], "the shading inside its box");
|
||||
check(&shaded, 155, 100, [255, 128, 128], "red outside the shading's box");
|
||||
|
||||
// An image that isn't decoded (here over the renderer's pixel limit) draws nothing, so it
|
||||
// must not clear what is under its rectangle either (review on #2974).
|
||||
let undecoded = render(ko, &format!("{red}q 160 0 0 100 60 50 cm /Big Do Q"));
|
||||
check(&undecoded, 100, 100, [255, 128, 128], "red under an image that isn't drawn");
|
||||
}
|
||||
|
||||
/// Reading colours replace *document* colours, never annotation colours (issue #1548): a
|
||||
|
||||
Vendored
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+6
@@ -83,6 +83,12 @@ pub trait Device<'a> {
|
||||
/// element replaces what the group drew under its shape, so a device clears that first;
|
||||
/// the element's layer then composites over the cleared area as usual.
|
||||
fn knockout_shape(&mut self, _shape: &BezPath) {}
|
||||
/// PdfCraft patch (39): whether `knockout_shape` would clear anything now, that is whether
|
||||
/// the innermost transparency group is a knockout group. Lets the interpreter skip work it
|
||||
/// only needs for the clear.
|
||||
fn in_knockout_group(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
/// PdfCraft patch (39): whether a transparency group drawn now is an element of a knockout
|
||||
/// group that the device wants cleared under the group's shape first. If so the
|
||||
/// interpreter draws the group's content once between `begin_knockout_erase` and
|
||||
|
||||
+7
-2
@@ -289,8 +289,13 @@ pub(crate) fn draw_image_xobject<'a, 'b>(
|
||||
// PdfCraft patch (39): an image without a mask covers its whole placement rectangle, so in
|
||||
// a knockout group it replaces what the group drew there (#2971). A stencil's or a masked
|
||||
// image's shape is its mask; those composite as before (exact where the mask is opaque or
|
||||
// transparent, which is what masks mostly are).
|
||||
if !x_object.is_mask && !has_alpha {
|
||||
// transparent, which is what masks mostly are). Only an image that decodes clears: one that
|
||||
// doesn't (over `MAX_IMAGE_PIXELS`, damaged data) draws nothing, so it replaces nothing.
|
||||
if !x_object.is_mask
|
||||
&& !has_alpha
|
||||
&& device.in_knockout_group()
|
||||
&& x_object.decoded_raster(None).is_some()
|
||||
{
|
||||
device.knockout_shape(&(transform * Rect::new(0.0, 0.0, width, height).to_path(0.1)));
|
||||
}
|
||||
|
||||
|
||||
Vendored
+12
-1
@@ -1063,7 +1063,14 @@ impl Renderer {
|
||||
if self.erasing {
|
||||
self.ctx.set_blend_mode(knockout_blend_mode());
|
||||
self.ctx.set_paint(AlphaColor::<Srgb>::new([0.0, 0.0, 0.0, 0.0]));
|
||||
return None;
|
||||
// A shading's `/BBox` clips what it paints, so it clips what it clears too.
|
||||
return match paint {
|
||||
Paint::Pattern(p) => match p.as_ref() {
|
||||
Pattern::Shading(s) => s.shading.clip_path.clone(),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
let mut paint_transform = Affine::IDENTITY;
|
||||
let mut clip_path = None;
|
||||
@@ -1727,6 +1734,10 @@ impl<'a> Device<'a> for Renderer {
|
||||
self.ctx.set_blend_mode(knockout_blend_mode());
|
||||
}
|
||||
|
||||
fn in_knockout_group(&mut self) -> bool {
|
||||
self.in_knockout()
|
||||
}
|
||||
|
||||
fn knockout_shape(&mut self, shape: &BezPath) {
|
||||
if !self.in_knockout() {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user