From 45dd24d905710ecc7c8664c158f8c78a764ac1e6 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 08:10:15 -0800 Subject: [PATCH] .ai save: a stroke the object's art style keeps no longer warns A path with an effect, or a path linked to a graphic style, can have a solid stroke a plain line can't draw, such as one with a width profile. When the reader reads the path back from an art style, the path is written with that style, and the style holds the stroke. The path's drawn look still writes the stroke as a filled outline, and the save warned that such strokes are written as filled outlines. The warning is now left out while a drawn look is written, as the effects warning already is. A styled group's style holds no fills or strokes, and its members are written again after the look, so a member's stroke still warns. Strokes that no art style holds (gradient or pattern strokes, for example) still warn. --- crates/eps/src/ai_editing.rs | 2 +- crates/eps/src/ai_editing/tests.rs | 64 +++++++++++++++++++++++++++++ crates/eps/src/ai_editing/writer.rs | 10 ++++- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/crates/eps/src/ai_editing.rs b/crates/eps/src/ai_editing.rs index 3ca8a66b5f..5ab99d2378 100644 --- a/crates/eps/src/ai_editing.rs +++ b/crates/eps/src/ai_editing.rs @@ -162,7 +162,7 @@ //! symbols past 4,096 as their art (symbol art past [`vectorcraft_doc::MAX_SYMBOL_NODES`] objects //! in all left out), instances of symbols the document doesn't have as empty groups, symbols' 9-slice //! guides, export types and dynamic type left out, live objects (placed documents included) as their art, -//! strokes a plain line can't draw +//! strokes a plain line can't draw and the object's art style doesn't hold //! as their filled outlines, several fills or strokes as a group of a path for each, freeform //! gradient swatches, gradient swatches of more than 256 stops, swatches of patterns written as //! tiles and the placement of pattern swatches that move, turn or scale their pattern, the opacity and blending modes of single fills and strokes, opacity masks on diff --git a/crates/eps/src/ai_editing/tests.rs b/crates/eps/src/ai_editing/tests.rs index 1ab878fc62..e9cc0ba669 100644 --- a/crates/eps/src/ai_editing/tests.rs +++ b/crates/eps/src/ai_editing/tests.rs @@ -6629,6 +6629,70 @@ fn every_effect_setting_written_on_an_object_is_read_back() { assert!(matches!(&spot.appearance.fill().unwrap().paint, Paint::Solid { swatch: Some(s), .. } if s == "Ink"), "{spot:?}"); } +/// A drawn look's strokes warn only where its art style leaves them out: a path with a drop shadow and +/// a stroke with a width profile is written with a style that holds the profile, and doesn't warn; +/// a group with a drop shadow whose member has a stroke inside the path still warns. +#[test] +fn a_drawn_looks_strokes_warn_only_where_its_style_leaves_them_out() { + let profile = vectorcraft_doc::WidthProfile { points: vec![(0.0, 1.0, 0.5), (0.5, 0.25, 0.75), (1.0, 0.0, 1.0)] }; + let (n, warnings, p) = read_one(|doc| { + let mut stroke = StrokeLayer::new(black_fill(), 4.0); + stroke.profile = Some(profile.clone()); + let ap = Appearance { items: vec![AppearanceItem::Stroke(stroke)], effects: vec![drop_shadow()], contents_index: None }; + Node::path(doc.alloc_id(), shapes::rectangle(Rect::new(10.0, 10.0, 50.0, 40.0)), ap) + }); + assert!(p.contains("%AI9_BeginArtStyles"), "{p}"); + assert!(!warnings.iter().any(|w| w == writer::STROKES), "{warnings:?}"); + assert_eq!(n.appearance.stroke().and_then(|s| s.profile.clone()), Some(profile)); + let (_, warnings, p) = read_one(|doc| { + let mut n = + Node::path(doc.alloc_id(), shapes::rectangle(Rect::new(10.0, 10.0, 50.0, 40.0)), Appearance::basic(Paint::None, black_fill(), 2.0)); + n.appearance.stroke_mut().unwrap().align = vectorcraft_doc::StrokeAlign::Inside; + let mut g = group(doc, vec![n], false); + g.appearance.effects = vec![drop_shadow()]; + g + }); + assert!(p.contains("%AI9_BeginArtStyles"), "{p}"); + assert!(warnings.iter().any(|w| w == writer::STROKES), "{warnings:?}"); +} + +/// A path linked to a graphic style with a drop shadow and a stroke with a width profile is written +/// with that style, which holds the profile, and doesn't warn; it reads back linked, with its +/// profile. A linked path whose fill is half opaque in Screen mode isn't written with its style, which +/// the reader reads on no object, and warns that the fill's transparency is left out. +#[test] +fn a_linked_paths_drawn_look_warns_only_for_what_its_style_leaves_out() { + let profile = vectorcraft_doc::WidthProfile { points: vec![(0.0, 1.0, 0.5), (0.5, 0.25, 0.75), (1.0, 0.0, 1.0)] }; + let linked = |doc: &mut Document, fill: FillLayer| { + let mut stroke = StrokeLayer::new(black_fill(), 4.0); + stroke.profile = Some(profile.clone()); + let ap = Appearance { + items: vec![AppearanceItem::Fill(fill), AppearanceItem::Stroke(stroke)], + effects: vec![drop_shadow()], + contents_index: None, + }; + let mut g = vectorcraft_doc::GraphicStyle::new("Linked", ap.clone()); + g.id = 7; + doc.graphic_styles.push(g); + let mut n = Node::path(doc.alloc_id(), shapes::rectangle(Rect::new(10.0, 10.0, 50.0, 40.0)), ap); + n.extras_mut().graphic_style = Some(7); + n + }; + let (n, warnings, p) = read_one(|doc| linked(doc, FillLayer::new(solid(Color::rgb(0.2, 0.2, 0.9))))); + assert!(p.contains("1 (Linked) XW"), "{warnings:?}"); + assert!(!warnings.iter().any(|w| w == writer::STROKES), "{warnings:?}"); + assert!(n.extras().graphic_style.is_some(), "{n:?}"); + assert_eq!(n.appearance.stroke().and_then(|s| s.profile.clone()), Some(profile.clone())); + let (_, warnings, p) = read_one(|doc| { + let mut fill = FillLayer::new(solid(Color::rgb(0.2, 0.2, 0.9))); + fill.opacity = 0.5; + fill.blend = BlendMode::Screen; + linked(doc, fill) + }); + assert!(!p.contains("1 (Linked) XW"), "{p}"); + assert!(warnings.iter().any(|w| w == writer::ITEM_TRANSPARENCY), "{warnings:?}"); +} + /// Each effect a graphic style can hold, in a style of its own, reads back with its settings, whatever /// the writer's conversions to the file's units. #[test] diff --git a/crates/eps/src/ai_editing/writer.rs b/crates/eps/src/ai_editing/writer.rs index dea4ad0c24..2f010070d0 100644 --- a/crates/eps/src/ai_editing/writer.rs +++ b/crates/eps/src/ai_editing/writer.rs @@ -51,7 +51,7 @@ pub(super) const CLIP_PAINT: &str = "the fills and strokes of clipping paths tha pub(super) const CLIP_PAINT_MASKS: &str = "the opacity masks of clipping paths whose fills and strokes are written as paths of their own stay on the clipping path, not on those paths, in the editing data for other apps"; pub(super) const LIVE: &str = "blends, envelopes, meshes, repeats, placed documents and intertwines are written as their art in the editing data for other apps"; -const STROKES: &str = "strokes a plain line can't draw (inside or outside the path, variable width, dots, dashes fitted to corners, gradients, patterns, arrowheads) are written as filled outlines, and brushed strokes as their brush's art, in the editing data for other apps"; +pub(super) const STROKES: &str = "strokes a plain line can't draw (inside or outside the path, variable width, dots, dashes fitted to corners, gradients, patterns, arrowheads) are written as filled outlines, and brushed strokes as their brush's art, in the editing data for other apps"; pub(super) const ITEM_TRANSPARENCY: &str = "the opacity and blending modes of single fills and strokes are left out of the editing data for other apps"; pub(super) const LAYER_MASKS: &str = "opacity masks on layers are left out of the editing data for other apps"; @@ -1028,7 +1028,13 @@ impl<'a> Writer<'a> { AppearanceItem::Fill(f) => self.fill_object(path, rule, f, xy), AppearanceItem::Stroke(s) if self.direct_stroke(s) => self.path_object(path, rule, None, Some(s), xy), AppearanceItem::Stroke(s) => { - self.warn(STROKES); + // In a styled path's or compound path's drawn look the stroke is kept by the object's + // art style, which is written only when the reader reads the object's appearance back + // from it. A styled group's style holds no fills or strokes: its members are written + // again after the look, and warn there. + if !self.in_look { + self.warn(STROKES); + } self.wrote_fixed = true; if self.brush_art(path, s, xy) { return;