From 1940d3e529e049470e8e9faa09ad9f4ede684ae4 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 8 Nov 2026 14:06:24 -0800 Subject: [PATCH] .ai import: opacity mask art in pattern and brush art takes document ids A pattern's or a brush's art was copied into the document with new ids, but the art of an opacity mask inside it kept the ids of the reader that read the definition. Those ids could repeat ids of objects in the layers. The mask art now takes document ids, as a symbol's art already does. --- crates/eps/src/import/ai/read/directives.rs | 22 ++++++-- crates/eps/src/import/ai/tests.rs | 60 +++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/crates/eps/src/import/ai/read/directives.rs b/crates/eps/src/import/ai/read/directives.rs index 68f981751..ef27e5f74 100644 --- a/crates/eps/src/import/ai/read/directives.rs +++ b/crates/eps/src/import/ai/read/directives.rs @@ -503,7 +503,15 @@ impl<'a> Reader<'a> { let Some(tile) = head.tile.filter(|t| whole && sane(*t) && t.width() > 0.0 && t.height() > 0.0 && !frame.children.is_empty()) else { return; }; - let art: Vec> = frame.children.iter().map(|n| Arc::new(self.doc.reid(n))).collect(); + let art: Vec> = frame + .children + .iter() + .map(|n| { + let mut kept = self.doc.reid(n); + self.reid_masks(&mut kept, 0); + Arc::new(kept) + }) + .collect(); // The objects of the art linked to graphic styles, by their ids in the document. if !sub.style_links.is_empty() { let mut ids = HashMap::new(); @@ -671,7 +679,7 @@ impl<'a> Reader<'a> { named } - /// 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 + /// Give the art of the opacity masks in `n` (a copy of a symbol's, a pattern's or a brush'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. pub(super) fn reid_masks(&mut self, n: &mut Node, depth: usize) { @@ -783,7 +791,15 @@ impl<'a> Reader<'a> { && sub.doc.images.is_empty(); let Some(frame) = sub.frames.pop().filter(|f| whole && !f.children.is_empty()) else { return }; self.brush_art_looks.insert(name.clone(), sub.drawn_looks); - let mut art: Vec> = frame.children.iter().map(|n| Arc::new(self.doc.reid(n))).collect(); + let mut art: Vec> = frame + .children + .iter() + .map(|n| { + let mut kept = self.doc.reid(n); + self.reid_masks(&mut kept, 0); + Arc::new(kept) + }) + .collect(); let node = match art.len() { 1 => art.pop().map(Arc::unwrap_or_clone), _ => Some(Node::group(self.doc.alloc_id(), art)), diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 07236c2c6..5125adae0 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -5974,6 +5974,37 @@ fn an_object_in_a_brushs_art_with_a_style_this_reads_comes_in_as_itself() { assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{:?}", r.warnings); } +/// The art of an opacity mask in a brush's art takes ids of the document, as the rest of the art +/// does: no object of the brush's art shares an id with an object of the layers. +#[test] +fn a_brushs_opacity_mask_art_takes_ids_of_the_document() { + fn ids(n: &Node, out: &mut Vec) { + out.push(n.id); + if let Some(m) = n.extras().mask.as_ref() { + ids(&m.art, out); + } + for c in n.children().into_iter().flatten() { + ids(c, out); + } + } + let square = rect_path(0.0, 0.0, 10.0, 10.0); + let mask_art: String = format!("1 g\n{square}f\n").lines().map(|l| format!("%_{l}\n")).collect(); + let mask = format!("%_/ArtDictionary :\n%_/Mask :\n%_X=\n{mask_art}%_X+\n%_(Art) ,\n%_1 /Bool (Clipping) ,\n%_; (AI9_Mask) ,\n%_;\n"); + let section = format!("%AI8_BeginBrushPattern\n(Dot art)\n0 A\n{RED}{square}f\n{mask}%AI8_EndBrushPattern\n"); + let shapes: String = (0..12).map(|i| format!("{RED}{}f\n", rect_path(f64::from(i) * 20.0, 0.0, f64::from(i) * 20.0 + 10.0, 10.0))).collect(); + let r = read_ok(&format!("{section}{}{}", plugin_object(SCATTER, "Spray", SPRAY), layer(&shapes))); + let lib = vectorcraft_brush::library(&r.doc); + let spray = lib.iter().find(|b| b.name == "Spray").unwrap_or_else(|| panic!("{:?}", r.warnings)); + let art = spray.art(); + assert!(art.iter().any(|n| n.extras().mask.is_some()), "the mask is read: {art:#?}"); + let mut all = Vec::new(); + for n in r.doc.layers.iter().map(|l| &**l).chain(art) { + ids(n, &mut all); + } + let unique: std::collections::HashSet<_> = all.iter().copied().collect(); + assert_eq!(unique.len(), all.len(), "{all:?}"); +} + /// An art styles section inside a symbol's, a pattern's or a brush's art is skipped: the file's own /// art styles still style the layers' objects read after the definition. #[test] @@ -6196,6 +6227,35 @@ fn an_object_in_a_patterns_art_with_a_listed_style_is_linked() { assert_eq!(square.extras().graphic_style, Some(1), "{square:#?}"); } +/// The art of an opacity mask in a pattern's art takes ids of the document, as the rest of the art +/// does: no object of the pattern's art shares an id with an object of the layers. +#[test] +fn a_patterns_opacity_mask_art_takes_ids_of_the_document() { + fn ids(n: &Node, out: &mut Vec) { + out.push(n.id); + if let Some(m) = n.extras().mask.as_ref() { + ids(&m.art, out); + } + for c in n.children().into_iter().flatten() { + ids(c, out); + } + } + let square = rect_path(0.0, 0.0, 10.0, 10.0); + let mask_art: String = format!("1 g\n{square}f\n").lines().map(|l| format!("%_{l}\n")).collect(); + let mask = format!("%_/ArtDictionary :\n%_/Mask :\n%_X=\n{mask_art}%_X+\n%_(Art) ,\n%_1 /Bool (Clipping) ,\n%_; (AI9_Mask) ,\n%_;\n"); + let pattern = format!("%AI3_BeginPattern: (P)\n(P) 0 0 10 10\n0 A\n0 Xw\n{RED}{square}f\n{mask}E\n%AI3_EndPattern\n"); + let shapes: String = (0..12).map(|i| format!("{RED}{}f\n", rect_path(f64::from(i) * 20.0, 0.0, f64::from(i) * 20.0 + 10.0, 10.0))).collect(); + let r = read_ok(&format!("{pattern}{}", layer(&shapes))); + let def = r.doc.pattern("P").unwrap_or_else(|| panic!("{:?}", r.warnings)); + assert!(def.art.iter().any(|n| n.extras().mask.is_some()), "the mask is read: {:#?}", def.art); + let mut all = Vec::new(); + for n in r.doc.layers.iter().chain(&def.art) { + ids(n, &mut all); + } + let unique: std::collections::HashSet<_> = all.iter().copied().collect(); + assert_eq!(unique.len(), all.len(), "{all:?}"); +} + #[test] fn an_object_in_a_version_7_patterns_tile_with_a_listed_style_is_linked() { let defs = styles_and_list(&[fill_style("Blue", "0 0 0 0 0 0 1 Xa\n")], &["Blue"]);