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]