.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.
This commit is contained in:
dotCooCoo
2026-11-09 00:35:12 +07:00
committed by bflatastic
parent cb3e8262bc
commit cae5b356c7
5 changed files with 73 additions and 5 deletions
+31
View File
@@ -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<String> {
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();
+3 -2
View File
@@ -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) => {
+20
View File
@@ -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() {
+3 -3
View File
@@ -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)))]);
+16
View File
@@ -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]