fix(render): a pattern before an isolated CMYK group no longer moves it

From the review of #5228. The finished CMYK group was drawn through
whatever paint transform a preceding shading pattern left on the
context, so its cyan half showed magenta. Hayro patch (27) now resets
the paint transform before drawing the group's pixels.

Two regression tests, both reported in that review:
- a pattern painted before the group (fails without the reset);
- the Color blend mode over an empty CMYK group, swept over all 256 K
  levels (failed on nine levels before #5199's vello_cpu fix was merged
  in; passes with it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
This commit is contained in:
KenM76
2026-10-28 20:40:16 -04:00
co-authored by Claude Opus 5.5
parent e518e27778
commit 367f7d5432
2 changed files with 68 additions and 0 deletions
@@ -214,3 +214,69 @@ trailer << /Root 1 0 R >>
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:?}");
}
/// A minimal page with `page` as its content, a shading pattern `/P` and an isolated
/// `DeviceCMYK` group `/G` over `bbox` drawing `group`; the RGB of device pixel (`x`, `y`).
fn cmyk_group_pixel(media: [u32; 2], page: &str, bbox: [u32; 4], group: &str, group_resources: &str, (x, y): (u32, u32)) -> Vec<u8> {
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 {} {}] /Contents 4 0 R /Resources << /XObject << /G 5 0 R >> /Pattern << /P 6 0 R >> >> >> endobj
4 0 obj << /Length {} >> stream
{page}
endstream endobj
5 0 obj << /Type /XObject /Subtype /Form /BBox [{} {} {} {}] /Group << /S /Transparency /CS /DeviceCMYK /I true >> /Resources << {group_resources} >> /Length {} >> stream
{group}
endstream endobj
6 0 obj << /Type /Pattern /PatternType 2 /Matrix [2 0 0 2 30 40] /Shading << /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 10 0] /Extend [true true] /Function << /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [0 0 1] /N 1 >> >> >> endobj
trailer << /Root 1 0 R >>
%%EOF",
media[0],
media[1],
page.len(),
bbox[0],
bbox[1],
bbox[2],
bbox[3],
group.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(), "{:?}", p.error);
p.rgba[((y * p.width + x) * 4) as usize..][..3].to_vec()
}
/// From the review of #5228: a shading pattern painted before the group left its pattern
/// transform on the context, and the finished group was drawn through it, so the cyan half
/// of the group showed magenta. The group must look the same whether or not a pattern was
/// painted first.
#[test]
fn a_pattern_painted_before_an_isolated_cmyk_group_does_not_move_it() {
let group = "0 1 0 0 k 20 20 30 60 re f 1 0 0 0 k 50 20 30 60 re f";
let render = |page: &str| cmyk_group_pixel([100, 100], page, [20, 20, 80, 80], group, "", (55, 50));
let after_pattern = render("/Pattern cs /P scn 0 0 5 5 re f /G Do");
let alone = render("/G Do");
assert!(alone[2] > 150 && alone[0] < 100, "the control is cyan: {alone:?}");
assert!(alone.iter().zip(&after_pattern).all(|(a, b)| a.abs_diff(*b) <= 3), "after a pattern {after_pattern:?}, alone {alone:?}");
}
/// From the review of #5228: the Color blend mode over an isolated group's empty backdrop
/// keeps the source colour (ISO 32000-2 §11.3.5.3 with nothing beneath). The K ink pass
/// draws gray, and vello_cpu's nonseparable blend of a gray darkened some K levels to
/// near black. Every 8-bit K level is checked against a plain fill of the same ink.
#[test]
fn the_color_blend_mode_over_an_empty_cmyk_group_keeps_every_k_level() {
let mut wrong = Vec::new();
for level in 0..=255u8 {
let k = format!("0 0 0 {} k", f32::from(level) / 255.0);
let page = format!("{k} 0 0 20 20 re f /G Do");
let group = format!("/B gs {k} 20 0 20 20 re f");
let plain = cmyk_group_pixel([40, 20], &page, [20, 0, 40, 20], &group, "/ExtGState << /B << /BM /Color >> >>", (10, 10));
let blended = cmyk_group_pixel([40, 20], &page, [20, 0, 40, 20], &group, "/ExtGState << /B << /BM /Color >> >>", (30, 10));
if plain.iter().zip(&blended).any(|(a, b)| a.abs_diff(*b) > 3) {
wrong.push((level, plain, blended));
}
}
assert!(wrong.is_empty(), "K levels drawn wrong (level, plain, blended): {wrong:?}");
}
+2
View File
@@ -1893,6 +1893,8 @@ impl<'a> Device<'a> for Renderer {
// must not apply to the finished pixels a second time.
self.set_soft_mask(None);
self.set_blend_mode(BlendMode::Normal);
// A pattern painted earlier leaves its paint transform on the context.
self.ctx.set_paint_transform(Affine::IDENTITY);
self.draw_pixmap(Arc::new(result), ImageQuality::Low, Affine::translate((x0, y0)));
true
}