From 6feec01d85d9d932f6089e7ec69b67a98344c308 Mon Sep 17 00:00:00 2001 From: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:49:58 +0300 Subject: [PATCH] eps: a layer's own transparency in the editing data is read (#2003) --- crates/eps/src/import/ai/read.rs | 34 +++++++++++++++++++++++-------- crates/eps/src/import/ai/tests.rs | 21 +++++++++++++++++++ 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index ef19fd644..3d9024fee 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -373,6 +373,8 @@ struct Reader<'a> { hidden_ids: HashSet, /// A group just ended, its state (`Xy`, `Xd`, `XW`) not yet read. group_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)>, /// Painted copies of clipping paths that have a fill (drawn under what they clip). clip_fills: HashSet, /// Groups closed as an object's drawn look (`9 () XW`). @@ -524,6 +526,7 @@ impl<'a> Reader<'a> { clip_fills: HashSet::new(), looks: HashSet::new(), group_ended: false, + layer_state: None, styles: HashMap::new(), mesh: None, cmyk_cache: HashMap::new(), @@ -915,6 +918,10 @@ impl<'a> Reader<'a> { layer.name = Some(a.name.clone()); layer.visible = a.visible; layer.locked = a.locked; + // The layer's own opacity, blend and knockout, written after its art (#2003). + if let Some((opacity, blend, isolate, knockout)) = self.layer_state.take() { + (layer.opacity, layer.blend, layer.isolate, layer.knockout) = (opacity, blend, isolate, knockout); + } match self.frames.last_mut() { Some(parent) => { parent.children.push(layer); @@ -969,9 +976,11 @@ impl<'a> Reader<'a> { ";" => return self.end_obj(), _ => {} } - // What follows an object (its name, transparency and style) ends with anything else. - if !matches!(w, "Xy" | "Xd" | "XW" | "BB") { + // What follows an object (its name, transparency and style) ends with anything else. The + // state block of a layer whose art ended (`Xy`, `Xd`, `XW`, then `LB`) is read at the `LB`. + if !matches!(w, "Xy" | "Xd" | "XW" | "BB" | "LB") { self.after_object = false; + self.layer_state = None; } let group_ended = std::mem::take(&mut self.group_ended); let vals = self.operands(); @@ -1479,13 +1488,20 @@ impl<'a> Reader<'a> { }; gs.knockout_shape = rest.get(2).is_some_and(|v| *v != 0.0); let gs = self.gs.clone(); - if let Some(n) = self.last_object() - && matches!(n.kind, NodeKind::Group { .. } | NodeKind::Compound { .. }) - { - n.opacity = gs.opacity; - n.blend = gs.blend; - n.isolate = gs.isolate; - n.knockout = gs.knockout; + if self.last_object().is_some_and(|n| matches!(n.kind, NodeKind::Group { .. } | NodeKind::Compound { .. })) { + if let Some(n) = self.last_object() { + n.opacity = gs.opacity; + n.blend = gs.blend; + n.isolate = gs.isolate; + n.knockout = gs.knockout; + } + self.layer_state = None; + } else { + // After a layer's art and before its `LB`: the layer's own state (#2003). Kept until + // an operator that isn't part of the state block (or the `LB` that takes it) says the + // art moves on, so a path's state before its paint isn't read as its layer's. + self.layer_state = matches!(self.frames.last(), Some(Frame { kind: Kind::Layer(_), .. })) + .then_some((gs.opacity, gs.blend, gs.isolate, gs.knockout)); } } diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 974f717d3..3b7c0e414 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -303,6 +303,27 @@ fn a_group_whose_state_has_no_transparency_takes_the_current_one() { assert_eq!((a[1].opacity, a[1].children().unwrap()[0].opacity), (1.0, 0.5)); } +#[test] +fn a_layer_keeps_its_own_transparency() { + // Illustrator writes a layer's own opacity, blend and knockout after its art, before `LB` + // (`Xy`, `Xd`, then `n () XW`); they belong to the layer, not to its last object (#2003). + let art_ = format!("{RED}{}f\n0 0.35 0 2 0 Xy\n0 0 Xd\n7 () XW\n", rect_path(0.0, 0.0, 10.0, 10.0)); + let doc = read_ok(&layer(&art_)).doc; + let l = &doc.layers[0]; + assert_eq!((l.opacity, l.blend), (0.35, BlendMode::Normal), "the layer's own state"); + assert_eq!((l.isolate, l.knockout), (false, Knockout::Neutral)); + // The art inside keeps its own state. + assert_eq!(art(&doc)[0].opacity, 1.0); + // A layer whose last object is a group keeps that group's state apart from the layer's. + let art_ = format!( + "0 Ae\nu\n0 0.5 0 0 0 Xy\n{}f\nU\n0 0 Xd\n6 () XW\n", + rect_path(0.0, 0.0, 10.0, 10.0) + ); + let doc = read_ok(&layer(&art_)).doc; + assert_eq!(doc.layers[0].opacity, 1.0, "no layer state written: the layer stays opaque"); + assert_eq!(art(&doc)[0].opacity, 0.5, "the group's own state"); +} + #[test] fn transparency_of_objects_and_groups() { let art_ = format!(