From b33cdfd14feffbbbcf3983ee0fe15cbf7f7822a5 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 6 Nov 2026 18:37:12 -0800 Subject: [PATCH] .ai import: a clipping path's fill read apart keeps its own ids A fill read apart from a clipping path took a copy of the clipping path's opacity mask whose art kept the original's ids, so the document held two objects with one id. The copy of the mask's art, and the subpaths of a fill read apart from a compound clipping path, now get ids of their own and stay linked to the same swatches. --- crates/eps/src/import/ai/read/containers.rs | 15 ++++--- crates/eps/src/import/ai/read/directives.rs | 4 +- crates/eps/src/import/ai/tests.rs | 46 +++++++++++++++++---- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/crates/eps/src/import/ai/read/containers.rs b/crates/eps/src/import/ai/read/containers.rs index abe4eaf27..e2fc2ac8b 100644 --- a/crates/eps/src/import/ai/read/containers.rs +++ b/crates/eps/src/import/ai/read/containers.rs @@ -332,17 +332,22 @@ impl<'a> Reader<'a> { *clipping = false; } let mut fill = Node::new(self.doc.alloc_id(), kind); - // A compound clipping path's subpaths: copies of their own that don't clip. + // A compound clipping path's subpaths: copies with ids of their own, linked to the same swatches, that don't clip. for c in fill.children_mut().into_iter().flatten() { - let c = Arc::make_mut(c); - self.reid(c); - if let NodeKind::Path { clipping, .. } = &mut c.kind { + let mut copy = self.doc.reid(c); + self.reid_masks(&mut copy, 0); + if let NodeKind::Path { clipping, .. } = &mut copy.kind { *clipping = false; } + *c = Arc::new(copy); } (fill.opacity, fill.blend, fill.isolate, fill.knockout, fill.knockout_shape, fill.visible, fill.locked) = (n.opacity, n.blend, n.isolate, n.knockout, n.knockout_shape, n.visible, n.locked); - if let Some(mask) = n.extras().mask.clone() { + // The opacity mask's art is a copy with ids of its own, linked to the same swatches. + if let Some(mut mask) = n.extras().mask.clone() { + let mut art = self.doc.reid(&mask.art); + self.reid_masks(&mut art, 0); + mask.art = Arc::new(art); fill.extras_mut().mask = Some(mask); } let (mut fills, mut rest) = ((*n.appearance).clone(), (*n.appearance).clone()); diff --git a/crates/eps/src/import/ai/read/directives.rs b/crates/eps/src/import/ai/read/directives.rs index aedb753d6..307105f95 100644 --- a/crates/eps/src/import/ai/read/directives.rs +++ b/crates/eps/src/import/ai/read/directives.rs @@ -648,10 +648,10 @@ impl<'a> Reader<'a> { named } - /// Give the art of the opacity masks in `n` (a copy of a symbol's art with ids of this + /// Give the art of the opacity masks in `n` (a copy of a symbol's art or of a clipping path with ids of this /// document, [`vectorcraft_doc::Document::reid`], which leaves masks as they are) ids of this /// document too. - fn reid_masks(&mut self, n: &mut Node, depth: usize) { + pub(super) fn reid_masks(&mut self, n: &mut Node, depth: usize) { if depth > MAX_DEPTH { return; } diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 0cb243692..f85b67e0c 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -1809,22 +1809,52 @@ fn a_compound_path_holding_its_drawn_look_paints_its_paths_apart() { fn clipping_paths_beside_a_compound_paths_look_keep_their_fills() { let (a, b) = (rect_path(0.0, 0.0, 20.0, 20.0), rect_path(5.0, 5.0, 25.0, 25.0)); let look = format!("0 Ae\nu\n{RED}{}f\n0 0 0 0 0 0 1 Xa\n{}f\nU\n9 () XW\n", rect_path(0.0, 0.0, 30.0, 30.0), rect_path(10.0, 10.0, 15.0, 15.0)); - let mask_art: String = format!("1 g\n{}f\n", rect_path(0.0, 0.0, 10.0, 10.0)).lines().map(|l| format!("%_{l}\n")).collect(); + let mask_art: String = + format!("0 0 0 0 (Global white) 1 0 Xk\n{}f\n", rect_path(0.0, 0.0, 10.0, 10.0)).lines().map(|l| format!("%_{l}\n")).collect(); let mask = format!("%_/ArtDictionary :\n%_/Mask :\n%_X=\n{mask_art}%_X+\n%_(Art) ,\n%_; (AI9_Mask) ,\n%_;\n"); let children = |second: &str, after_first: &str| { let body = format!("0 Ae\n*u\n{RED}{a}h\nW\nf\n{after_first}{second}{b}h\nW\nf\n{look}*U\n"); - let a = art(&read_ok(&layer(&body)).doc); + let doc = read_ok(&layer(&body)).doc; + // Every object, opacity masks' art included, has an id of its own. + fn ids(n: &Node, seen: &mut std::collections::HashSet) { + assert!(seen.insert(n.id), "{:?} appears twice", n.id); + if let Some(m) = &n.extras().mask { + ids(&m.art, seen); + } + for c in n.children().into_iter().flatten() { + ids(c, seen); + } + } + let mut seen = std::collections::HashSet::new(); + doc.layers.iter().for_each(|l| ids(l, &mut seen)); + let a = art(&doc); let NodeKind::Group { children, clip: true } = &a[0].kind else { panic!("{a:#?}") }; - children.iter().map(|c| (c.kind.clone(), fill(c).color(), c.extras().mask.is_some())).collect::>() + // An opacity mask: the swatch its art's fill is linked to. + let mask = |c: &Node| { + c.extras().mask.as_ref().map(|m| { + let mut swatch = None; + m.art.walk(&mut |p| { + if let Paint::Solid { swatch: Some(s), .. } = fill(p) { + swatch = Some(s); + } + }); + swatch + }) + }; + children.iter().map(|c| (c.kind.clone(), fill(c).color(), mask(c))).collect::>() }; let (red, blue) = (Some(Color::rgb(1.0, 0.0, 0.0)), Some(Color::rgb(0.0, 0.0, 1.0))); - let paths = |c: &[(NodeKind, Option, bool)]| { + // A child's kind, fill color and, with an opacity mask, the swatch its art is linked to. + type Child = (NodeKind, Option, Option>); + let paths = |c: &[Child]| { assert!(matches!(c[0].0, NodeKind::Compound { .. }) && c.len() == 4, "{c:?}"); - c[..3].iter().map(|(k, c, m)| (matches!(k, NodeKind::Path { clipping: false, .. }), *c, *m)).collect::>() + c[..3].iter().map(|(k, c, m)| (matches!(k, NodeKind::Path { clipping: false, .. }), *c, m.clone())).collect::>() }; - assert_eq!(paths(&children("", "")), [(false, None, false), (true, red, false), (true, red, false)]); - assert_eq!(paths(&children("0 0 0 0 0 0 1 Xa\n", "")), [(false, None, false), (true, red, false), (true, blue, false)]); - assert_eq!(paths(&children("0 0 0 0 0 0 1 Xa\n", &mask)), [(false, None, false), (true, red, true), (true, blue, false)]); + assert_eq!(paths(&children("", "")), [(false, None, None), (true, red, None), (true, red, None)]); + assert_eq!(paths(&children("0 0 0 0 0 0 1 Xa\n", "")), [(false, None, None), (true, red, None), (true, blue, None)]); + // The fill's copy of the mask stays linked to the mask art's global swatch. + let linked = Some(Some("Global white".to_string())); + assert_eq!(paths(&children("0 0 0 0 0 0 1 Xa\n", &mask)), [(false, None, None), (true, red, linked), (true, blue, None)]); } #[test]