hayro: port PdfCraft's page transparency group (#2972)

The page group composites over a transparent backdrop and is then put on the background, so a blend mode over an empty part of the page keeps its color. With a color replacement, and on a plate render, the background stays inside the group as its backdrop, so plate films are as before. Ledger hayro (18); the PdfCraft raster test is adapted in upstream_raster.rs.
This commit is contained in:
aiongg committed 2026-10-24 07:38:08 +00:00
1 parent 7c7ec9c8ca
commit 30ee2f832f
4 files changed
+67 -13

No files matched your search

+34
View File
@@ -618,3 +618,37 @@ fn visibility_expressions_decide_what_a_membership_dictionary_shows() {
let deep = format!("<< /Type /OCMD /OCGs [5 0 R] /VE {}5 0 R{} >>", "[/Not ".repeat(41), "]".repeat(41));
assert!(!render(&deep, &[5]), "too deep: /OCGs decides");
}
/// PdfCraft ledger hayro (18) (#2972): a page's objects composite with a transparent backdrop
/// and the page group is then put on the paper, so a blend mode only blends where something
/// was drawn. A green fill with `/BM /Screen` (or Lighten, Difference…) half over a red square
/// takes the mode's color over the red and stays plain green over the empty page.
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
fn blend_modes_over_an_empty_part_of_the_page_keep_their_color() {
for (mode, over_red) in [
("Normal", Some([0, 255, 0])),
("Multiply", Some([0, 0, 0])),
("Screen", Some([255, 255, 0])),
("Lighten", Some([255, 255, 0])),
("Difference", Some([255, 255, 0])),
("Exclusion", Some([255, 255, 0])),
("ColorDodge", Some([255, 0, 0])),
("Overlay", Some([255, 0, 0])),
// The non-separable modes mix hue, saturation and luminosity; over an empty part of
// the page they too keep the source color.
("Hue", None),
("Saturation", None),
("Color", None),
("Luminosity", None),
] {
let page = format!("/MediaBox [0 0 300 200] /Resources << /ExtGState << /G1 << /Type /ExtGState /BM /{mode} >> >> >>");
let p = render(&one_page(&page, b"1 0 0 rg 20 20 150 150 re f /G1 gs 0 1 0 rg 100 50 150 120 re f", b""));
// Pixel rows run top down: PDF (210, 100) and (130, 100) are rows 100 from the top.
assert_eq!(pixel(&p, 210, 100), [0, 255, 0, 255], "{mode} over the empty page");
if let Some(over_red) = over_red {
assert_eq!(pixel(&p, 130, 100)[..3], over_red, "{mode} over the red square");
}
assert_eq!(pixel(&p, 280, 10), [255, 255, 255, 255], "{mode}: the paper stays white");
}
}
+7 -2
View File
@@ -210,8 +210,13 @@ each marked `PdfCraft patch` in the source: hayro-interpret (40) (PdfCraft #2987
content membership dictionary with a visibility expression (`/VE`: `/And`, `/Or` and `/Not`
over groups the document lists in `/OCProperties /OCGs`) shows its content when the
expression is true, ahead of `/OCGs` and `/P`; a malformed expression, or one nested deeper
than 32 levels, is ignored and `/OCGs` and `/P` decide. Its PdfCraft raster test is adapted in
`tests/upstream_raster.rs`.
than 32 levels, is ignored and `/OCGs` and `/P` decide; and hayro (18) (PdfCraft #2972), the
page renders as one transparency group over a transparent backdrop that is then put on the
background, so a blend mode over an empty part of the page keeps its color; with a color
replacement (reading colors) the background stays inside the group as its backdrop. A plate
render (`InterpreterSettings::plate`) also keeps the background inside, marked `Craft patch`,
so blend modes on a separation's film blend with the paper. The PdfCraft raster tests of both
are adapted in `tests/upstream_raster.rs`.
This remains the patched 0.7 backend. It retains the existing Bradford Lab conversion,
floating component precision and codec versions, rather than claiming full 0.8
+1 -1
View File
@@ -62,7 +62,7 @@
"path": "src/lib.rs",
"source_sha256": "682db2a32fbf4d875c0e9dd7ac5dc875a6b09ade375efbce6f104de9197de078",
"source_git_blob": "6333ed5808ae0cf9cf158e4730e973c1d7089bab",
"sha256": "682db2a32fbf4d875c0e9dd7ac5dc875a6b09ade375efbce6f104de9197de078"
"sha256": "8396b90516603079bb43012621e8089621fec58fdd3d9c9aa9a8d2aa840265da"
},
{
"path": "src/renderer.rs",
+25 -10
View File
@@ -254,15 +254,22 @@ pub fn render_into<'a>(
interpreter_settings.preview,
);
// PdfCraft patch (18): the page renders as one transparency group with the background
// painted *inside* it, as the initial backdrop. A group isolates what it wraps from what
// was painted before it, so with the background outside, a Multiply highlight blended
// only with the group's transparent backdrop and stayed its own colour — over a dark
// reading-colours background the whole quad became one flat bar and the words under it
// vanished. Inside the group the highlight multiplies with the background too: the words
// keep the highlight's colour and the band between them takes the background (white
// backgrounds are unaffected — multiplying with white is the identity).
device.push_transparency_group(1.0, None, BlendMode::Normal);
// PdfCraft patch (18): the page renders as one transparency group (the page group). Its
// objects composite with an initially transparent backdrop, and the group is then put on
// the paper (ISO 32000-2, page group), so a blend mode only blends where something was
// drawn: a /BM /Screen fill over an empty part of the page keeps its color (#2970).
// Under reading colors the background is painted *inside* the group instead, as its
// initial backdrop, so a Multiply highlight blends with the dark background: outside it,
// the highlight kept its own flat color and the words under it vanished into the band
// (#1548). Reading colors replace the page's colors anyway; for document colors the
// background stays outside, or every separable mode but Normal, Multiply, Darken and
// HardLight would blend with the white paper. Craft patch: a plate render keeps the
// background inside as well, so blend modes on a separation's film blend with the paper.
let backdrop_inside =
render_settings.color_replacement.is_some() || interpreter_settings.plate.is_some();
if backdrop_inside {
device.push_transparency_group(1.0, None, BlendMode::Normal);
}
device.ctx.set_paint(render_settings.bg_color);
device
@@ -274,11 +281,19 @@ pub fn render_into<'a>(
path: clip_path,
fill: FillRule::NonZero,
});
if !backdrop_inside {
device.push_transparency_group(1.0, None, BlendMode::Normal);
}
interpret_page(page, &mut state, &mut device);
if !backdrop_inside {
device.pop_transparency_group();
}
device.pop_clip_path();
device.pop_transparency_group();
if backdrop_inside {
device.pop_transparency_group();
}
let mut resources = vello_cpu::Resources::default();
device.ctx.render_to_buffer(