From 0d4b5979832129794da4a6402fbf4a2ad32d2c83 Mon Sep 17 00:00:00 2001 From: "Ayberk A." <105879357+AyberkA0@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:04:01 +0300 Subject: [PATCH] AI import: a gradient stop naming a process colour leaves its swatch process (#1771) A colour named by a gradient stop (`Bs`) was collected with the colours the colour operators define, in file order, and the first definition of a name made its swatch. A stop can't say whether its colour is an ink, so it reads as a spot colour; and since the file defines its gradients before the art, a global process colour set with `Xk` that a gradient also names came in as a spot swatch, which PDF export then writes as a Separation. The stops' named colours are now kept apart and make swatches only after the colour operators' colours, so `Xk`'s definition wins and a name only a stop uses still gets its swatch. The stop's values were already read right since the lexer reads a `%_BS` line's own operands. --- crates/eps/src/import/ai/read.rs | 12 ++++++++++-- crates/eps/src/import/ai/tests.rs | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index d12a569e3..deb91345f 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -461,7 +461,11 @@ struct Reader<'a> { instance: Option, pending_fill: Option, pending_stroke: Option, + /// The named colours the colour operators define (`x`, `Xx`, `Xk`…). spots: Vec, + /// The named colours gradient stops name. A stop doesn't say whether its colour is an ink or + /// a global process colour, so these make swatches only for names no colour operator defines. + stop_spots: Vec, raster: Option, /// The placed file being read (`%AI5_BeginPlace` … `~`). place: Option, @@ -626,6 +630,7 @@ impl<'a> Reader<'a> { pending_fill: None, pending_stroke: None, spots: Vec::new(), + stop_spots: Vec::new(), raster: None, place: None, foreign_data: None, @@ -879,6 +884,7 @@ impl<'a> Reader<'a> { self.doc.images.entry(key).or_insert(blob); } self.spots.extend(sub.spots); + self.stop_spots.extend(sub.stop_spots); self.cmyk_colors += sub.cmyk_colors; self.rgb_colors += sub.rgb_colors; for w in &sub.warnings { @@ -1155,7 +1161,7 @@ impl<'a> Reader<'a> { let vals = self.take_head(vals); if let Some(d) = self.gradient_def.as_mut() { match w { - "Bs" => d.stop(&vals, &mut self.spots), + "Bs" => d.stop(&vals, &mut self.stop_spots), "BD" => { if let Some((name, g)) = self.gradient_def.take().and_then(GradientDef::finish) { self.gradients.insert(name, g); @@ -2321,7 +2327,9 @@ impl<'a> Reader<'a> { if let Some(u) = self.ruler_units.and_then(ruler_unit) { doc.units = u; } - for n in &self.spots { + // The colour operators' definitions first: a gradient, defined before the art, names a + // global process colour as a stop and would otherwise make it an ink (#1771). + for n in self.spots.iter().chain(&self.stop_spots) { if doc.swatch(&n.name).is_none() { doc.swatches.push(Swatch { name: n.name.clone(), paint: Paint::solid(n.color), global: true, spot: n.spot }); } diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 0ab7b1b67..3bd312297 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -173,6 +173,29 @@ fn global_process_colours_are_global_swatches_at_their_tint() { assert_eq!(doc.swatch("Black global").unwrap().paint.color(), Some(Color::cmyk(0.0, 0.0, 0.0, 1.0)), "the swatch is the full colour"); } +/// A gradient stop naming a global process colour doesn't make it an ink (#1771): the colour +/// operator's definition (`Xk`) makes the swatch, though the gradient, defined first, names the +/// colour first. The lines are the ones posted in #1771, with a neutral name. +#[test] +fn a_gradient_stop_naming_a_process_colour_leaves_its_swatch_process() { + let defs = "%AI5_BeginGradient: (Brand)\n(Brand) 0 2 Bd\n[\n<00FF>\n0\n\ + 0.930000007152557 0.100000001490116 0 0 1 1 6 50 0 %_BS\n\ + %_0.930000007152557 0.100000001490116 0 0 (Test Cyan) 0 0 5 1 6 50 0 Bs\n\ + %_0 0 0 0 1 0 0 2 1 6 50 100 Bs\nBD\n%AI5_EndGradient\n"; + let art_ = format!("0.930000007152557 0.100000001490116 0 0 (Test Cyan) 0 0 Xk\n{}f\n", rect_path(0.0, 0.0, 10.0, 10.0)); + let near = + |c: Option| matches!(c, Some(Color::Cmyk { c, m, y, k }) if (c - 0.93).abs() < 1e-3 && (m - 0.1).abs() < 1e-3 && y == 0.0 && k == 0.0); + let doc = read_ok(&format!("{defs}{}", layer(&art_))).doc; + let sw = doc.swatch("Test Cyan").unwrap(); + assert!(sw.global && !sw.spot, "a global process colour, not an ink: {sw:?}"); + assert!(near(sw.paint.color()), "C=93 M=10: {sw:?}"); + assert!(near(fill(&art(&doc)[0]).color()), "the fill: {:?}", fill(&art(&doc)[0])); + // Named only by the stop, the colour is still a swatch of its own, at the stop's values. + let doc = read_ok(&format!("{defs}{}", layer(&format!("{}f\n", rect_path(0.0, 0.0, 10.0, 10.0))))).doc; + let sw = doc.swatch("Test Cyan").unwrap(); + assert!(near(sw.paint.color()), "{sw:?}"); +} + /// A pattern's definition (`%AI3_BeginPattern`): its name and tile (8 points square here, y up), /// then its art on `%_` lines (a group of the tile's unpainted box and a dot, a round-capped /// stroke of no length), `E`.