From 003234a05666d2ba15765c6826adfe9284bbc06b Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 12 Oct 2026 20:25:50 +0700 Subject: [PATCH] Fix #1410: an .ai compound path takes its last path's paint, and paths grouped in it stay its paths --- crates/eps/src/import/ai/read.rs | 22 +++++++++++++++++++--- crates/eps/src/import/ai/tests.rs | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index 124701eb..a30403d0 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -702,13 +702,17 @@ impl<'a> Reader<'a> { "*U" => self.close(what, |k| matches!(k, Kind::Compound))?, _ => self.close(what, |k| matches!(k, Kind::Clip))?, }; - let Frame { kind, children: mut kids, gs, clips, hidden: opened_hidden, .. } = f; + let Frame { kind, children: kids, gs, clips, hidden: opened_hidden, .. } = f; // A compound path of clipping paths clips its clipping group. let clipping = matches!(kind, Kind::Compound) && !clips.is_empty(); let node = match kind { Kind::Compound => { - let Some(first) = kids.first() else { return Ok(()) }; - let (appearance, opacity, blend, rule) = (first.appearance.clone(), first.opacity, first.blend, path_rule(first)); + // A group in a compound path (`*u u … U *U`) only holds its paths. + let mut kids = ungrouped(kids); + // The compound path's paint is its last path's (a gradient's instance wraps only + // that one); the last painted one, should the last have none. + let Some(shown) = kids.iter().rev().find(|k| !k.appearance.items.is_empty()).or(kids.last()) else { return Ok(()) }; + let (appearance, opacity, blend, rule) = (shown.appearance.clone(), shown.opacity, shown.blend, path_rule(shown)); for k in &mut kids { k.opacity = 1.0; k.blend = BlendMode::Normal; @@ -1716,6 +1720,18 @@ fn close(bp: &mut BezPath) { } /// The fill rule of a path or compound node. +/// `kids` with the members of each plain group among them in the group's place, at any depth. +fn ungrouped(kids: Vec) -> Vec { + let mut out = Vec::with_capacity(kids.len()); + for k in kids { + match k.kind { + NodeKind::Group { children, clip: false } => out.extend(ungrouped(children.into_iter().map(Arc::unwrap_or_clone).collect())), + _ => out.push(k), + } + } + out +} + fn path_rule(n: &Node) -> FillRule { match &n.kind { NodeKind::Path { rule, .. } | NodeKind::Compound { rule, .. } => *rule, diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index dd8fdd1a..dbc0dc56 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -244,6 +244,25 @@ fn compound_paths_and_clipping() { assert!(*clip && matches!(children[0].kind, NodeKind::Path { clipping: true, .. })); } +/// #1410: a compound path shows its last path's paint, and paths grouped in it stay its paths. +#[test] +fn compound_path_takes_its_last_paths_paint() { + let paths = format!("0 g\n{}f\n{RED}{}f\n", rect_path(0.0, 0.0, 30.0, 30.0), rect_path(10.0, 10.0, 20.0, 20.0)); + let bodies = [ + format!("0 Ae\n*u\n{paths}*U\n"), + // The paths in a group. + format!("0 Ae\n*u\n0 Ae\nu\n{paths}U\n*U\n"), + // The last path unpainted. + format!("*u\n{paths}{}n\n*U\n", rect_path(1.0, 1.0, 2.0, 2.0)), + ]; + for body in bodies { + let a = art(&read_ok(&layer(&body)).doc); + let NodeKind::Compound { children, .. } = &a[0].kind else { panic!("{:?}", a[0].kind) }; + assert!(children.len() >= 2 && children.iter().all(|c| matches!(c.kind, NodeKind::Path { .. })), "{body}"); + assert_eq!(fill(&a[0]).color(), Some(Color::rgb(1.0, 0.0, 0.0)), "{body}"); + } +} + #[test] fn transparency_of_objects_and_groups() { let art_ = format!(