From cae5b356c732b30726230ac19a802bf096d3ad27 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 23:29:51 -0800 Subject: [PATCH] .ai import: a palette gradient swatch's paint is not linked to itself The editing-data reader set the paint of each gradient swatch in the palette to link to the swatch's own name. The default gradient swatches and a gradient swatch given a new gradient with swatch.edit hold their gradient without a link, so a document saved as .ai and opened again had swatches that differed from the ones it was saved with. The swatch now holds the gradient alone. Art filled with the gradient is not changed: it still links to a swatch by the gradient's name. The fix also applies to a .ai whose layers are not read, such as a gradient library, where only the palette comes in. Three tests cover a hand-written palette with a gradient swatch named by its gradient and one with a name of its own, a new document's four gradient swatches written by the editing-data writer and read back, and a gradient library without layers. --- crates/eps/src/ai_editing/tests.rs | 31 ++++++++++++++++++++++ crates/eps/src/import/ai/read/document.rs | 5 ++-- crates/eps/src/import/ai/tests.rs | 20 ++++++++++++++ crates/eps/src/import/tests_illustrator.rs | 6 ++--- crates/eps/src/import/tests_native.rs | 16 +++++++++++ 5 files changed, 73 insertions(+), 5 deletions(-) diff --git a/crates/eps/src/ai_editing/tests.rs b/crates/eps/src/ai_editing/tests.rs index 42d3c7d73..dec514d5c 100644 --- a/crates/eps/src/ai_editing/tests.rs +++ b/crates/eps/src/ai_editing/tests.rs @@ -2047,6 +2047,37 @@ fn gradients_and_patterns_are_read_back() { same_lines(&gradient_looks(&read_back(&doc)), &gradient_looks(&doc)); } +/// A new document's gradient swatches read back with the paint they were written with, not linked +/// to a swatch, and art filled with one of them reads back linked to it. +#[test] +fn gradient_swatches_read_back_unlinked() { + let mut doc = Document::new(200.0, 100.0); + let Some(Paint::Gradient(sunset)) = doc.swatch("Sunset").map(|s| s.paint.clone()) else { panic!("no Sunset swatch") }; + let layer = doc.layers[0].id; + let linked = gradient_paint(sunset.gradient.clone(), None, Some("Sunset")); + let rect = painted(&mut doc, shapes::rectangle(Rect::new(10.0, 10.0, 90.0, 50.0)), vec![AppearanceItem::Fill(FillLayer::new(linked))]); + doc.insert(Some(layer), usize::MAX, rect).unwrap(); + let back = read_back(&doc); + let gradients = |d: &Document| -> Vec { + d.swatches_iter() + .filter_map(|s| match &s.paint { + Paint::Gradient(g) => { + Some(format!("{} {:?} stops={} geom={:?} swatch={:?}", s.name, g.gradient.kind, g.gradient.stops.len(), g.geom, g.swatch)) + } + _ => None, + }) + .collect() + }; + let want = gradients(&doc); + assert_eq!(want.len(), 4, "{want:?}"); + assert_eq!(gradients(&back), want); + let link = back.layers[0].children().and_then(|c| c.first()).map(|n| match n.appearance.items.first().map(AppearanceItem::paint) { + Some(Paint::Gradient(g)) => g.swatch.clone(), + other => panic!("{other:?}"), + }); + assert_eq!(link, Some(Some("Sunset".to_string()))); +} + #[test] fn gradients_and_patterns_write_the_same_bytes_again() { let doc = gradients_and_patterns(); diff --git a/crates/eps/src/import/ai/read/document.rs b/crates/eps/src/import/ai/read/document.rs index 8a79f53b1..f416cc496 100644 --- a/crates/eps/src/import/ai/read/document.rs +++ b/crates/eps/src/import/ai/read/document.rs @@ -820,12 +820,13 @@ impl<'a> Reader<'a> { let (paint, global, spot) = match e.cell { Cell::Color { color, global, spot } => (Paint::solid(color), global, spot), // A swatch named by its gradient or pattern takes the name that got in the document. + // A gradient swatch's own paint does not link to a swatch; art painted with the + // gradient links to it. Cell::Gradient(g) => { - let mut gp = vectorcraft_color::gradient::GradientPaint::new(self.gradients.get(&g)?.clone()); + let gp = vectorcraft_color::gradient::GradientPaint::new(self.gradients.get(&g)?.clone()); if e.name == g { e.name = self.global_names.get(Global::Gradient, &g).to_string(); } - gp.swatch = Some(e.name.clone()); (Paint::Gradient(Box::new(gp)), false, false) } Cell::Pattern(p) => { diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index a82cd50eb..93e09a183 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -906,6 +906,26 @@ fn a_color_named_as_a_palette_gradient_or_pattern_is_numbered() { assert_eq!(links, [Some("Brand 3".to_string()), Some("Brand 2".to_string())]); } +/// A palette's gradient swatch holds its gradient without a link to a swatch, whether it is named +/// by its gradient or has a name of its own, and the art filled with the gradient links to the +/// swatch named by the gradient. +#[test] +fn a_palette_gradient_swatch_is_not_linked_to_itself() { + let gradient = "%AI5_BeginGradient: (Sky)\n(Sky) 0 2 Bd\n[\n%_1 0 50 0 Bs\n%_0 0 50 100 Bs\nBD\n%AI5_EndGradient\n"; + let palette = "%AI5_BeginPalette\n0 0 Pb\nBb\n2 (Sky) 0 0 0 1 1 0 0 1 0 0 1 Bg\n0 BB\n(Sky)\nPc\n\ + Bb\n2 (Sky) 0 0 0 1 1 0 0 1 0 0 1 Bg\n0 BB\n(Brand)\nPc\nPB\n%AI5_EndPalette\n"; + let filled = format!("Bb\n1 (Sky) 0 0 0 10 1 0 0 1 0 0 Bg\n{}f\n0 BB\n", rect_path(0.0, 0.0, 10.0, 10.0)); + let doc = read_ok(&format!("{gradient}{palette}{}", layer(&filled))).doc; + for name in ["Sky", "Brand"] { + let sw = doc.swatch(name).unwrap_or_else(|| panic!("no {name} swatch")); + let Paint::Gradient(g) = &sw.paint else { panic!("{sw:?}") }; + assert_eq!(g.swatch, None, "{name}'s own paint"); + assert_eq!(g.geom, None); + assert_eq!(g.gradient.stops.len(), 2); + } + assert!(matches!(fill(&art(&doc)[0]), Paint::Gradient(g) if g.swatch.as_deref() == Some("Sky"))); +} + /// A palette's pattern swatch named by its pattern takes the name the pattern is shown with. #[test] fn a_palette_pattern_swatch_takes_its_patterns_shown_name() { diff --git a/crates/eps/src/import/tests_illustrator.rs b/crates/eps/src/import/tests_illustrator.rs index b896063aa..70363dfaf 100644 --- a/crates/eps/src/import/tests_illustrator.rs +++ b/crates/eps/src/import/tests_illustrator.rs @@ -444,7 +444,7 @@ fn a_version_7_palette_keeps_its_swatches_without_names() { let spot = d.swatch("Spot A").unwrap(); assert!(spot.spot && spot.global, "{spot:?}"); assert!(d.swatches.iter().filter(|s| s.name != "Spot A").all(|s| !s.spot && !s.global)); - assert!(matches!(&d.swatch("B&W").unwrap().paint, Paint::Gradient(g) if g.swatch.as_deref() == Some("B&W"))); + assert!(matches!(&d.swatch("B&W").unwrap().paint, Paint::Gradient(g) if g.swatch.is_none())); assert_eq!(d.swatch("Gray K=0").unwrap().paint.color(), Some(Color::gray(0.0)), "1 g is white"); } @@ -481,8 +481,8 @@ fn gradient_names_are_shown_up_to_their_newline() { let names: Vec<&str> = d.swatches.iter().map(|s| s.name.as_str()).collect(); let expected: Vec<&str> = ["[None]"].into_iter().chain((!own.is_empty()).then_some("Sky")).chain([first, second]).collect(); assert_eq!(names, expected, "{:?}", r.warnings); - assert_eq!(first_stop(&d.swatch(first).unwrap().paint), (Some(first.into()), Some(Color::gray(0.0)))); - assert_eq!(first_stop(&d.swatch(second).unwrap().paint), (Some(second.into()), Some(Color::gray(0.5)))); + assert_eq!(first_stop(&d.swatch(first).unwrap().paint), (None, Some(Color::gray(0.0)))); + assert_eq!(first_stop(&d.swatch(second).unwrap().paint), (None, Some(Color::gray(0.5)))); let art = d.layers[0].children().unwrap(); let fills: Vec<_> = art.iter().map(|n| first_stop(&n.appearance.fill().unwrap().paint)).collect(); assert_eq!(fills, [(Some(second.into()), Some(Color::gray(0.5))), (Some(first.into()), Some(Color::gray(0.0)))]); diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 9dad1eee7..32c4ff796 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -1331,6 +1331,22 @@ fn an_ai_file_that_comes_in_as_its_pdf_part_keeps_the_swatches_of_its_palette() assert!(d.swatch("Red ink").is_some_and(|s| s.spot)); } +/// A `.ai` gradient library (a palette of gradient swatches and no layers) gets the palette's +/// gradient swatches, each holding its gradient without a link to a swatch. +#[test] +fn an_ai_file_without_layers_keeps_the_gradients_of_its_palette_unlinked() { + let sky = "%AI5_BeginGradient: (Sky)\n(Sky) 0 2 Bd\n[\n%_1 0 50 0 Bs\n%_0 0 50 100 Bs\nBD\n%AI5_EndGradient\n"; + let palette = "%AI5_BeginPalette\n0 0 Pb\nBb\n2 (Sky) 0 0 0 1 1 0 0 1 0 0 1 Bg\n0 BB\n(Sky)\nPc\nPB\n%AI5_EndPalette\n"; + let art = native(&format!("{sky}{palette}")); + let (d, warnings, read) = crate::layered_ai(&private(&art), pdf_part(10.0, 86.0), vec![], false, true, 1.0); + assert!(!read, "{warnings:?}"); + let swatches: Vec<&str> = d.swatches.iter().map(|s| s.name.as_str()).collect(); + assert_eq!(swatches, ["[None]", "Sky"], "{warnings:?}"); + let Some(vectorcraft_color::Paint::Gradient(g)) = d.swatch("Sky").map(|s| &s.paint) else { panic!("{:?}", d.swatch("Sky")) }; + assert_eq!(g.swatch, None, "the swatch's own paint"); + assert_eq!(g.gradient.stops.len(), 2); +} + /// A `.ai` pattern library (a palette of pattern swatches and no layers) whose PDF part defines no /// pattern gets the patterns its editing data defines with the palette's pattern swatches. #[test]