From 0d91749c741233ab8b73771930eb13071209273a Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 6 Nov 2026 17:13:38 -0800 Subject: [PATCH] .ai import: drawn looks in pattern and brush art count in the note An object read as its drawn look counted in the import's "came in as their drawn look" note when it was on the page or in a kept symbol, but not when it was in a pattern's art or a brush's art. A file whose only such objects were in a pattern or a brush opened without the note. - A kept pattern's drawn looks count, once for each pattern name: a definition that a later one of the same name replaces no longer counts. - A brush's art counts once a brush that is kept uses it; art no kept brush uses adds nothing. --- crates/eps/src/import/ai/read.rs | 8 +++++ crates/eps/src/import/ai/read/directives.rs | 2 ++ crates/eps/src/import/ai/read/document.rs | 10 +++++- crates/eps/src/import/ai/tests.rs | 38 +++++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index 79febd5f1..025ccc480 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -475,6 +475,12 @@ struct Reader<'a> { warnings: Vec, /// Objects read as their drawn look (several fills or strokes, effects, brushes). drawn_looks: usize, + /// The objects read as their drawn look in the art of [`Reader::brush_art`], by name: counted in + /// [`Reader::drawn_looks`] once a brush that is kept uses the art. + brush_art_looks: HashMap, + /// The objects read as their drawn look in the art of the patterns kept, by the name the document + /// has the pattern by: a pattern defined again counts only its last definition. + pattern_looks: HashMap, /// Nodes read from `%_` lines. hidden_ids: HashSet, /// The blend part each object's dictionary names (`ShapeBlendPathType`, `ShapeBlendPathID`). @@ -679,6 +685,8 @@ impl<'a> Reader<'a> { color_model: None, warnings: Vec::new(), drawn_looks: 0, + brush_art_looks: HashMap::new(), + pattern_looks: HashMap::new(), hidden_ids: HashSet::new(), blend_roles: HashMap::new(), blend_record: None, diff --git a/crates/eps/src/import/ai/read/directives.rs b/crates/eps/src/import/ai/read/directives.rs index a5445bfbf..33af13372 100644 --- a/crates/eps/src/import/ai/read/directives.rs +++ b/crates/eps/src/import/ai/read/directives.rs @@ -507,6 +507,7 @@ impl<'a> Reader<'a> { } else { self.v7_patterns.remove(&head.name); } + self.pattern_looks.insert(def.name.clone(), sub.drawn_looks); self.doc.patterns.push(def); for (key, blob) in sub.doc.images { self.doc.images.entry(key).or_insert(blob); @@ -752,6 +753,7 @@ impl<'a> Reader<'a> { && sub.frames.len() == 1 && 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 node = match art.len() { 1 => art.pop().map(Arc::unwrap_or_clone), diff --git a/crates/eps/src/import/ai/read/document.rs b/crates/eps/src/import/ai/read/document.rs index 2fd918fe6..65d949f0f 100644 --- a/crates/eps/src/import/ai/read/document.rs +++ b/crates/eps/src/import/ai/read/document.rs @@ -914,13 +914,20 @@ impl<'a> Reader<'a> { } /// The brushes read ([`brush::finish_brushes`]) and how many weren't; `None` when the file has - /// no brush objects (the document keeps the default brushes). + /// no brush objects (the document keeps the default brushes). The objects read as their drawn + /// look in the art the brushes use count toward the note on drawn looks, once for each art. fn brush_library(&mut self) -> Option<(Vec, usize)> { if !self.brush_objects { return None; } let drafts = std::mem::take(&mut self.brush_drafts); let (lib, left_out) = brush::finish_brushes(drafts, &self.brush_art, self.brush_order.as_deref()); + let used: std::collections::HashSet = lib.iter().flat_map(|b| b.art()).map(|a| a.id).collect(); + for (name, art) in &self.brush_art { + if used.contains(&art.id) { + self.drawn_looks += self.brush_art_looks.get(name).copied().unwrap_or(0); + } + } Some((lib, left_out + self.brushes_unread)) } @@ -1087,6 +1094,7 @@ impl<'a> Reader<'a> { warnings.push(brush::left_out_note(unread)); } } + self.drawn_looks += self.pattern_looks.values().sum::(); if self.drawn_looks > 0 { warnings.push(format!( "objects with several fills or strokes, effects or brushes came in as their drawn look ({}): their appearance isn't editable yet", diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 08b69f23e..f5eadb931 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -2396,6 +2396,44 @@ fn a_drawn_look_stands_for_its_object() { assert!(r.warnings.iter().any(|w| w.contains("drawn look (1)")), "{:?}", r.warnings); } +/// An object kept as its drawn look: the look (a group), then the object on `%_` lines and its style. +fn drawn_look_art() -> String { + let hidden_rect = rect_path(0.0, 0.0, 10.0, 10.0).replace('\n', "\n%_"); + format!( + "0 Ae\nu\n{RED}{}f\n{}S\nU\n%_{hidden_rect}n\n%_/ArtDictionary :\n%_/XMLUID : (Look) ; (AI10_ArtUID) ,\n%_;\n1 (Look style) XW\n", + rect_path(0.0, 0.0, 10.0, 10.0), + rect_path(0.0, 0.0, 10.0, 10.0), + ) +} + +/// A pattern's art holding an object kept as its drawn look counts in the note, as a symbol's does; +/// a definition the same name replaces later counts no longer. +#[test] +fn a_drawn_look_in_a_patterns_art_is_noted() { + let pattern = |art: &str| format!("%AI3_BeginPattern: (P)\n(P) 0 0 10 10\n0 A\n0 Xw\n{art}E\n%AI3_EndPattern\n"); + let looked = pattern(&drawn_look_art()); + let r = read_ok(&format!("{looked}{}", layer(""))); + assert_eq!(r.doc.patterns.len(), 1, "{:?}", r.warnings); + assert!(r.warnings.iter().any(|w| w.contains("drawn look (1)")), "{:?}", r.warnings); + let plain = pattern(&format!("{RED}{}f\n", rect_path(0.0, 0.0, 10.0, 10.0))); + let r = read_ok(&format!("{looked}{plain}{}", layer(""))); + assert_eq!(r.doc.patterns.len(), 1, "{:?}", r.warnings); + assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{:?}", r.warnings); +} + +/// A brush's art holding an object kept as its drawn look counts in the note once a brush that is +/// kept uses the art; art no brush uses brings no note. +#[test] +fn a_drawn_look_in_a_brushs_art_is_noted_when_a_brush_uses_it() { + let section = format!("%AI8_BeginBrushPattern\n(Dot art)\n0 A\n{}%AI8_EndBrushPattern\n", drawn_look_art()); + let r = read_ok(&format!("{section}{}{}", plugin_object(SCATTER, "Spray", SPRAY), layer(""))); + let lib = vectorcraft_brush::library(&r.doc); + assert!(matches!(brush_named(&lib, "Spray"), vectorcraft_brush::BrushKind::Scatter(_)), "{:?}", r.warnings); + assert!(r.warnings.iter().any(|w| w.contains("drawn look (1)")), "{:?}", r.warnings); + let r = read_ok(&format!("{section}{}{}", plugin_object(CALLIGRAPHIC, "Nib", NIB), layer(""))); + assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{:?}", r.warnings); +} + /// Editing data with one layer holding an image: `matrix` (`[a b c d tx ty]`), its size, colour /// space, kind and alpha channels, its samples, and the size the data comment declares. fn image(matrix: &str, (w, h): (u32, u32), (space, kind, alpha): (&str, u32, u32), data: &[u8], declared: usize) -> Vec {