fix(eps): a masked gradient mesh takes the transparency after its mask

A gradient mesh with an opacity mask has its own state (`Xy`) after the
mask's dictionary, not right after its `/End X!`. The operators of the
mask's art, on `%_` lines, cleared the flag that says a mesh just ended,
so the state was read as the next object's and each masked mesh in a
run drew with the blend mode of the one before it.

The flag now goes back to what it was when a dictionary opened once it
closes, as `after_object` does.
This commit is contained in:
Don Omondi
2026-10-31 04:21:56 +07:00
committed by bflatastic
parent c9dc4fa857
commit a1fc3cfdb1
5 changed files with 34 additions and 4 deletions
+2 -1
View File
@@ -183,7 +183,8 @@ struct Reader<'a> {
blend_budget: usize,
/// A group just ended, its state (`Xy`, `Xd`, `XW`) not yet read.
group_ended: bool,
/// A gradient mesh just ended (`/End X!`), its own state (`Xy`) not yet read.
/// A gradient mesh just ended (`/End X!`), its own state (`Xy`) not yet read: also after its
/// dictionary (its name, its opacity mask and the mask's art).
mesh_ended: bool,
/// A layer's own state (`Xy` after its art, before `LB`), until something says the art moves on.
layer_state: Option<(f32, BlendMode, bool, Knockout)>,
+6 -2
View File
@@ -19,6 +19,7 @@ impl<'a> Reader<'a> {
children: Vec::new(),
hidden,
after: self.after_object,
mesh_ended: self.mesh_ended,
gs: self.gs.clone(),
base,
clips: Vec::new(),
@@ -243,6 +244,7 @@ impl<'a> Reader<'a> {
let art = self.new_node(NodeKind::Group { children, clip }, &f.gs);
self.gs = f.gs;
self.after_object = f.after;
self.mesh_ended = f.mesh_ended;
let Some(Frame { kind: Kind::Obj(o), .. }) = self.frames.last_mut() else {
return Err("mask art ends outside a mask dictionary".into());
};
@@ -258,13 +260,15 @@ impl<'a> Reader<'a> {
return Ok(());
}
let Some(f) = self.frames.pop() else { return Ok(()) };
let Frame { kind, children, base, hidden, gs, after, .. } = f;
let Frame { kind, children, base, hidden, gs, after, mesh_ended, .. } = f;
let Kind::Obj(mut o) = kind else { return Ok(()) };
obj::close(&mut o, &mut self.stack, base);
self.obj_hidden = hidden;
// Art made inside a dictionary (type's paths) isn't kept.
// Art made inside a dictionary (type's paths, a mask's art) isn't kept, and doesn't end what
// follows the object the dictionary is about: a mesh's state comes after its mask.
self.gs = gs;
self.after_object = after;
self.mesh_ended = mesh_ended;
if matches!(self.frames.last(), Some(Frame { kind: Kind::Obj(_), .. })) {
return self.push(V::Obj(Rc::new(*o)));
}
+2
View File
@@ -142,6 +142,8 @@ pub(super) struct Frame {
pub(super) hidden: bool,
/// [`Reader::after_object`] when it opened.
pub(super) after: bool,
/// [`Reader::mesh_ended`] when it opened.
pub(super) mesh_ended: bool,
/// The state when it opened (a container's lock and visibility; a dictionary's state to go
/// back to).
pub(super) gs: GState,
+22
View File
@@ -486,6 +486,28 @@ fn a_gradient_mesh_takes_the_transparency_written_after_it() {
assert_eq!((a[2].opacity, a[2].blend), (0.7, BlendMode::Screen), "a state after the mesh's `Xd` and `XW` is the next object's");
}
#[test]
fn a_masked_gradient_mesh_takes_the_transparency_written_after_its_mask() {
// GUARDS: a mesh with an opacity mask has its state after the mask's dictionary, whose art (on
// `%_` lines) has operators of its own; read as the next object's, each masked mesh in a run
// drew with the blend mode of the one before it.
let corners: String = ["10 20 10 25 1 15 20 1", "10 50 15 50 1 10 45 1", "40 50 40 45 1 35 50 1", "40 20 35 20 1 40 25 1"]
.iter()
.map(|g| format!("%_[0.5 1 {g}] /N X#\n"))
.collect();
let mesh = format!("/Mesh X!\n%_/DeviceGray /CS X#\n{corners}%_/E X#\n/End X!\n");
let mask = "%_/ArtDictionary :\n%_/Mask :\n%_X=\n%_u\n%_1 g\n%_10 20 m\n%_10 50 L\n%_40 50 L\n%_40 20 L\n%_f\n%_U\n%_X+\n\
%_(Art) ,\n%_1 /Bool (Clipping) ,\n%_1 /Bool (Linked) ,\n%_; (AI9_Mask) ,\n%_;\n";
let next = format!("0 1 0 0 0 Xy\n{RED}{}f\n", rect_path(60.0, 20.0, 80.0, 40.0));
for (state, expected) in [("1 0.5 0 0 0 Xy\n", (0.5, BlendMode::Multiply)), ("", (1.0, BlendMode::Overlay))] {
let a = art(&read_ok(&layer(&format!("3 1 0 0 0 Xy\n{mesh}{mask}{state}0 0 Xd\n6 () XW\n{next}"))).doc);
assert_eq!(a.len(), 2, "{a:?}");
assert!(matches!(a[0].kind, NodeKind::Mesh(_)) && a[0].extras().mask.is_some(), "{:?}", a[0]);
assert_eq!((a[0].opacity, a[0].blend), expected, "{state:?}: the state after the mask is the mesh's");
assert_eq!((a[1].opacity, a[1].blend), (1.0, BlendMode::Normal), "the next object's own state");
}
}
#[test]
fn gradients() {
let defs = "%AI5_BeginGradient: (G)\n(G) 1 2 Bd\n[\n<00FF>\n0\n0 0 0 0 1 0 0 2 1 6 %_BS\n\