mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 21:48:09 -06:00
fix(render): an isolated CMYK group inside a knockout element is erased, not painted
main's knockout patch (hayro (22)) draws each element of a knockout group once in erase mode, to clear what the group drew under the element's shape. An isolated DeviceCMYK group nested in such an element still took the CMYK path in that pass, and drew its finished pixels with Normal blending over what should have been cleared. draw_isolated_cmyk_group now declines while erasing, so the group's contents are interpreted in erase mode as before. The new test fails without the fix (the centre shows red through the magenta: [240, 63, 168] against [245, 127, 197] for the DeviceRGB control). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
41277846a3
commit
6fcfb3f98d
@@ -168,3 +168,49 @@ endstream endobj
|
||||
let non_isolated_rgb = render(Some("/CS /DeviceRGB /I false /K false"), 1.0);
|
||||
assert!(distance(&px(&non_isolated_rgb, 110.0, 110.0, 1.0), &green) <= 3, "a non-isolated nested group inherits CMYK");
|
||||
}
|
||||
|
||||
/// An element of a knockout group is first drawn in erase mode, which clears what the group
|
||||
/// drew under the element's shape. An isolated `DeviceCMYK` group nested in that element must
|
||||
/// clear too: when the erase pass drew it through the CMYK path, it painted over the red
|
||||
/// beneath instead of clearing it. The page must match the same element with its nested group
|
||||
/// blending in `DeviceRGB`, which never takes the CMYK path.
|
||||
#[test]
|
||||
fn an_isolated_cmyk_group_inside_a_knockout_element_is_erased_like_any_other() {
|
||||
let render = |inner_cs: &str| {
|
||||
let knockout = "1 0 0 rg 20 20 160 160 re f /El Do";
|
||||
let element = "/Cm Do";
|
||||
let inner = "/A gs 0 1 0 0 k 60 60 80 80 re f";
|
||||
let pdf = format!(
|
||||
"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R /Resources << /XObject << /Ko 5 0 R >> >> >> endobj
|
||||
4 0 obj << /Length 7 >> stream
|
||||
/Ko Do
|
||||
endstream endobj
|
||||
5 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 200 200] /Group << /S /Transparency /I true /K true >> /Resources << /XObject << /El 6 0 R >> >> /Length {} >> stream
|
||||
{knockout}
|
||||
endstream endobj
|
||||
6 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 200 200] /Group << /S /Transparency >> /Resources << /XObject << /Cm 7 0 R >> >> /Length {} >> stream
|
||||
{element}
|
||||
endstream endobj
|
||||
7 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 200 200] /Group << /S /Transparency /CS {inner_cs} /I true >> /Resources << /ExtGState << /A << /ca 0.5 >> >> >> /Length {} >> stream
|
||||
{inner}
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF",
|
||||
knockout.len(),
|
||||
element.len(),
|
||||
inner.len()
|
||||
);
|
||||
let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default());
|
||||
let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() });
|
||||
assert!(p.error.is_none(), "{inner_cs}: {:?}", p.error);
|
||||
// The centre of the inner square, (100, 100), is also the centre of the page.
|
||||
p.rgba[((100 * p.width + 100) * 4) as usize..][..3].to_vec()
|
||||
};
|
||||
let rgb = render("/DeviceRGB");
|
||||
let cmyk = render("/DeviceCMYK");
|
||||
assert!(rgb[2] > 150, "the RGB control shows half-opacity magenta over white, not red: {rgb:?}");
|
||||
assert!(rgb.iter().zip(&cmyk).all(|(a, b)| a.abs_diff(*b) <= 3), "CMYK {cmyk:?}, RGB {rgb:?}");
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-2
@@ -1832,9 +1832,10 @@ impl<'a> Device<'a> for Renderer {
|
||||
/// result goes into the layer the interpreter pushed for the group, which composites it
|
||||
/// with the group's opacity, soft mask and blend mode as before. Declined (and so drawn as
|
||||
/// before) under reading colours, inside a pass (a nested group then blends in the pass's
|
||||
/// own CMYK), and for contents a pass has no tints for.
|
||||
/// own CMYK), while erasing for a knockout group (the contents must clear, not paint), and
|
||||
/// for contents a pass has no tints for.
|
||||
fn draw_isolated_cmyk_group(&mut self, group: &hayro_interpret::CmykGroup<'a, '_>) -> bool {
|
||||
if self.color_replacement.is_some() || self.ink.pass.is_some() {
|
||||
if self.color_replacement.is_some() || self.ink.pass.is_some() || self.erasing {
|
||||
return false;
|
||||
}
|
||||
let canvas = Rect::new(0.0, 0.0, f64::from(self.ctx.width()), f64::from(self.ctx.height()));
|
||||
|
||||
Reference in New Issue
Block a user