Merge pull request #1426 from storytold/q/1410-ai-compound-paint

Fix #1410: an .ai compound path takes its last path's paint, and paths grouped in it stay its paths
This commit is contained in:
BFlat 2026-10-12 21:10:21 +07:00 committed by GitHub
commit 487cc29b57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 38 additions and 3 deletions

View File

@ -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<Node>) -> Vec<Node> {
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,

View File

@ -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!(