Merge pull request #2029 from midasmatterhorn/fix/issue-2003-ai-layer-opacity

eps: a layer's own transparency in the editing data is read (#2003)
This commit is contained in:
BFlat
2026-10-16 03:58:21 +07:00
committed by GitHub
2 changed files with 43 additions and 9 deletions
+25 -9
View File
@@ -373,6 +373,8 @@ struct Reader<'a> {
hidden_ids: HashSet<NodeId>,
/// 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<NodeId>,
/// 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));
}
}
+18
View File
@@ -303,6 +303,24 @@ 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!(