Open .ai files with their swatches: the palette, its colour groups and unused spot colours

The editing data's palette (%AI5_BeginPalette) was skipped, so a .ai file
opened with the default swatches plus only the spot colours its art used:
inks the art doesn't use (a Pantone set kept in the file) and colour groups
were lost. The palette is now read in order (process, spot in CMYK, RGB or
Lab, and global colours, gradients and patterns, colour groups; versioned
content read once) and replaces the default swatches.

Xs/Xz (Registration at a tint, as printer's marks use it) are read as the
Registration swatch: before, art painted with them stopped the editing data
from being read at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Fabian Grajales
2026-10-24 17:46:20 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 1a6bc267d5
commit 1adbf259f7
6 changed files with 343 additions and 7 deletions
@@ -82,6 +82,37 @@ fn an_illustrator_ai_file_opens_from_its_editing_data() {
assert_ne!(names(&s.doc().unwrap().doc.layers), ["Art", "Hidden"]);
}
/// A `.ai` file opens with the swatches its palette holds, spot colours the art doesn't use and colour
/// groups included, in place of the default swatches; art painted with Registration (`XZ`) still
/// opens from the editing data.
#[test]
fn an_ai_file_opens_with_the_swatches_of_its_palette() {
let palette = "%AI5_BeginPalette\n0 0 Pb\n0 0 0 0 1 1 1 Xa\n(White)\nPc\n0.04 0.3 0 0 81.96 21 -8 (Pink ink) 0 2 Xx\n(Pink ink)\nPc\n\
0.06 0.98 1 0 (Red ink) 0 x\n(Red ink)\nPc\n1 (Inks) 1 Pg\n0.02 0.1 1 0 (Yellow ink) 0 x\n(Yellow ink)\nPc\nPB\n%AI5_EndPalette\n";
// The square the page draws, in the red ink, after a stroke colour of Registration.
let art = format!("0 0 0 0 0 0 0 ([Registration]) 0 1 XZ\n0.06 0.98 1 0 (Red ink) 0 x\n{}f\n", ai::rect(10.0, 50.0, 40.0, 90.0));
let data = ai::editing_data(200.0, 100.0, &format!("{palette}{}", ai::layer("Art", &art)));
let mut s = Session::new();
let r = open(&mut s, "inks.ai", &ai::ai(&compressed(data.as_bytes()), "1 0 0 rg 10 50 30 40 re f"), json!({})).unwrap();
assert_eq!(r["format"], json!("ai"), "{r}");
let doc = &s.doc().unwrap().doc;
assert_eq!(names(&doc.layers), ["Art"], "opened from the editing data: {r}");
let swatches: Vec<&str> = doc.swatches.iter().map(|s| s.name.as_str()).collect();
assert_eq!(swatches, ["[None]", "White", "Pink ink", "Red ink"]);
let pink = doc.swatch("Pink ink").unwrap();
assert!(pink.spot && pink.global);
assert_eq!(pink.paint.color(), Some(vectorcraft_color::Color::lab(81.96, 21.0, -8.0)), "a Lab ink keeps its Lab values");
let groups: Vec<&str> = doc.swatch_groups.iter().map(|g| g.name.as_str()).collect();
assert_eq!(groups, ["Inks"]);
assert!(doc.swatch("Yellow ink").is_some_and(|s| s.spot), "an ink the art doesn't use");
let red = &doc.layers[0].children().unwrap()[0];
assert!(
matches!(red.appearance.fill().map(|f| &f.paint), Some(vectorcraft_color::Paint::Solid { swatch: Some(n), .. }) if n == "Red ink"),
"{:?}",
red.appearance.fill()
);
}
/// A 1-bit RGB image opens from the editing data in the colors its PDF part shows.
#[test]
fn a_one_bit_rgb_image_opens_from_the_editing_data_in_its_colors() {
+23 -1
View File
@@ -2495,6 +2495,13 @@ fn ascii85(data: &[u8]) -> String {
/// A pattern's definition in editing data: its name and tile, its art on `%_` lines, its end.
const PATTERN: &str = "%AI3_BeginPattern: (Dots)\n(Dots) 0 0 6 6\n%_0 A\n%_0 Ae\n%_u\n%_0 0 m\n%_0 6 L\n%_6 6 L\n%_6 0 L\n%_n\n%_0 0 0 1 K\n%_1 J 1 w\n%_3 3 m\n%_3 3 L\n%_S\n%_U\n%_9 () XW\nE\n%AI3_EndPattern\n";
/// The swatches in editing data: process, spot (one the art doesn't use), global, a gradient that
/// isn't defined and a pattern, a colour group, and versioned content read once.
const PALETTE: &str = "%AI5_BeginPalette\n0 0 Pb\n%AI17_Begin_Content_if_version_gt:24 15\n1 1 1 1 ([Registration]) 0 Xs\n([Registration])\nPc\n\
0 0 0 1 k\n(Black)\nPc\n0.1 0.9 0.8 0 (Unused ink) 0 x\n(Unused ink)\nPc\n0 0 0 1 0 0 0 (Black global) 0 1 Xk\n(Black global)\nPc\n\
Bb\n2 (G) 0 0 0 1 1 0 0 1 0 0 1 Bg\n0 BB\n(G)\nPc\n(Dots) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n(Dots)\nPc\n\
%AI17_Alternate_Content\n0 0 0 1 k\n(Black)\nPc\n%AI17_End_Versioned_Content\n1 (Greys) 1 Pg\n0 0 0 0.5 k\n(K50)\nPc\nPB\n%AI5_EndPalette\n";
/// The editing data of a file with every sort of object the layers reader knows, and some it doesn't.
fn editing_text() -> String {
let square = |x: u32| format!("0 0 1 0 k\n{x} 10 m\n{} 10 L\n{} 14 L\n{x} 14 L\nf", x + 4, x + 4);
@@ -2520,6 +2527,7 @@ fn editing_text() -> String {
"Bb\n1 (G) 0 0 0 1 1 0 0 1 0 0 1 Bg\n1 0 0 1 0 0 Xm\n0 0 0 0 0 0 Bm\n1 0 0 1 5 5 Bm\n0 BB".into(),
"0 1 w 2 J 0 j 4 M [3 2]0 d 1 D".into(),
"0 0 0 1 (Black global) 0.4 0 Xk".into(),
"0 0 0 0 0 0 0 ([Registration]) 0.1 1 Xz".into(),
"(Dots) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p".into(),
square(60),
// A 2 × 1 CMYK image with an alpha channel, its samples in hex.
@@ -2532,7 +2540,7 @@ fn editing_text() -> String {
format!("%AI5_BeginLayer\n{visible} 1 1 1 0 0 1 0 79 128 255 0 50 0 Lb\n({name}) Ln\n{body}\nLB\n%AI5_EndLayer--\n")
};
format!(
"%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 100 100\n%%HiResBoundingBox: 0 0 100 100\n%AI3_Cropmarks: 0 0 100 100\n{PATTERN}{}{}%%Trailer\n",
"%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 100 100\n%%HiResBoundingBox: 0 0 100 100\n%AI3_Cropmarks: 0 0 100 100\n{PATTERN}{PALETTE}{}{}%%Trailer\n",
layer("One", 1, &format!("{body}\n{}", layer("Sub", 1, &square(70)))),
layer("Two", 0, &square(80))
)
@@ -2552,6 +2560,20 @@ fn editing_eps(editing: &str) -> Vec<u8> {
.into_bytes()
}
/// The fuzzed editing data, as it is, opens with its palette's swatches in its order and groups,
/// the spot colour the art doesn't use included (the gradient isn't defined: left out).
#[test]
fn the_editing_fixture_reads_its_palette() {
let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&editing_text())].concat();
let (doc, warnings, layers) = vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, true, 1.0);
assert!(layers, "{warnings:?}");
let swatches: Vec<&str> = doc.swatches.iter().map(|s| s.name.as_str()).collect();
assert_eq!(swatches, ["[None]", "Black", "Unused ink", "Black global", "Dots"]);
assert!(doc.swatch("Unused ink").is_some_and(|s| s.spot && s.global));
let groups: Vec<(&str, usize)> = doc.swatch_groups.iter().map(|g| (g.name.as_str(), g.swatches.len())).collect();
assert_eq!(groups, [("Greys", 1)]);
}
/// The fuzzed editing data, as it is, reads its CMYK image with its ink amounts and its alpha
/// channel as an opacity mask.
#[test]
+1
View File
@@ -22,6 +22,7 @@ mod lex;
mod mesh;
mod obj;
mod paint;
mod palette;
mod read;
mod style;
+28 -2
View File
@@ -4,8 +4,10 @@
//! (`c m y k (name) tint`; tint 0 is the full colour), `Xa`/`XA` RGB (`c m y k r g b`, the CMYK
//! values its equivalent), `Xx`/`XX` a named colour of any model
//! (`c m y k [r g b | L a b] (name) tint type`, type 0 CMYK, 1 RGB, 2 Lab), `Xk`/`XK` a global
//! process colour (its operands as `Xx`'s: a global swatch that isn't an ink of its own). Lower
//! case paints fills, upper case strokes.
//! process colour (its operands as `Xx`'s: a global swatch that isn't an ink of its own),
//! `Xs`/`XS` (`c m y k (name) tint`) and `Xz`/`XZ` (`c m y k r g b (name) tint type`) the
//! Registration colour at a tint, as written in files users sent. Lower case paints fills, upper
//! case strokes.
//! - Gradient definitions: `(name) type stops Bd`, its stops as `colour style [opacity 6]
//! midpoint ramp Bs` (style 0 grey, 1 CMYK, 2 RGB with its CMYK, 3 and 4 named colours), `BD`.
//! - A gradient on an object: `[flag] Bb` (1: the stroke), `flag (name) x y angle length a b c d
@@ -60,6 +62,7 @@ pub(super) fn color_op(op: &str, vals: &[V]) -> Option<(Paint, Option<Named>)> {
"x" => named(vals, Some(false)),
"xx" => named(vals, None),
"xk" => named(vals, None).map(|(p, n)| (p, n.map(|n| Named { spot: false, ..n }))),
"xs" | "xz" => registration(vals),
_ => None,
}
}
@@ -92,6 +95,15 @@ fn named(vals: &[V], rgb: Option<bool>) -> Option<(Paint, Option<Named>)> {
Some((paint, Some(Named { name, color, tint, spot: true })))
}
/// `… (name) tint Xs` / `… (name) tint type Xz`: the built-in Registration swatch at the tint (its
/// own colour, whatever values the operands give).
fn registration(vals: &[V]) -> Option<(Paint, Option<Named>)> {
let at = vals.iter().rposition(|v| matches!(v, V::Str(_)))?;
let tint = 1.0 - vals.get(at + 1).and_then(V::num).unwrap_or(0.0).clamp(0.0, 1.0) as f32;
let full = Color::cmyk(1.0, 1.0, 1.0, 1.0);
Some((Paint::Solid { color: full.tinted(tint), swatch: Some(vectorcraft_color::swatch::REGISTRATION.into()), tint }, None))
}
/// The colour of a gradient stop's colour values and style.
fn stop_color(style: f64, comps: &[V]) -> Option<(Color, Option<Named>)> {
let nums: Vec<f32> = comps.iter().filter_map(V::num).map(|v| v as f32).collect();
@@ -318,6 +330,20 @@ mod tests {
v.iter().map(|n| V::Num(*n)).collect()
}
#[test]
fn registration_paints_with_the_registration_swatch_at_its_tint() {
let mut v = nums(&[0.75, 0.68, 0.67, 0.9, 0.0, 0.0, 0.0]);
v.extend([V::Str(b"[Registration]".to_vec()), V::Num(0.1), V::Num(1.0)]);
let (p, named) = color_op("XZ", &v).unwrap();
assert!(named.is_none());
let Paint::Solid { swatch, tint, color } = p else { panic!() };
assert_eq!((swatch.as_deref(), color), (Some("[Registration]"), Color::cmyk(1.0, 1.0, 1.0, 1.0).tinted(0.9)));
assert!((tint - 0.9).abs() < 1e-6);
let mut v = nums(&[1.0, 1.0, 1.0, 1.0]);
v.extend([V::Str(b"[Registration]".to_vec()), V::Num(0.0)]);
assert!(matches!(color_op("Xs", &v), Some((Paint::Solid { tint, .. }, None)) if tint == 1.0));
}
#[test]
fn colours() {
let (p, _) = color_op("g", &nums(&[0.7])).unwrap();
+216
View File
@@ -0,0 +1,216 @@
//! The swatches of the editing data: the section between `%AI5_BeginPalette` and
//! `%AI5_EndPalette`, as written in files users sent.
//!
//! - `0 0 Pb` starts the swatches and `PB` ends them.
//! - Each swatch is its paint, then `(name) Pc`. The paint is a colour operator (`g`, `k`, `Xa`: a
//! process colour; `x`, `Xx`: a spot colour; `Xk`: a global process colour), a gradient
//! (`Bb` … `flag (gradient) … Bg` … `BB`) or a pattern (`(pattern) … p`). `Xs` and `Xz` paint
//! with Registration, which every document has.
//! - `flag (name) count Pg` starts a colour group: the swatches up to the next `Pg` or `PB` are
//! its own.
//! - Newer files write the swatches between `%AI17_Begin_Content_if_version_gt` and
//! `%AI17_Alternate_Content`, then again for older versions up to `%AI17_End_Versioned_Content`:
//! the first are read, the others skipped.
//!
//! Anything else in the section (an operator it doesn't know) is left alone: the swatches around
//! it still come in.
use vectorcraft_color::Color;
use super::lex::{Lexer, Tok};
use super::obj::V;
use super::paint::{self, Named};
/// Largest number of swatches read from one palette.
const MAX_SWATCHES: usize = 20_000;
/// Largest number of values kept between two operators.
const MAX_VALUES: usize = 64;
/// A swatch's paint, as the palette gives it.
#[derive(Clone, Debug, PartialEq)]
pub(super) enum Cell {
/// A colour: its full colour, whether it's global and whether it's an ink of its own.
Color { color: Color, global: bool, spot: bool },
/// The gradient defined under this name (`%AI5_BeginGradient`).
Gradient(String),
/// The pattern defined under this name (`%AI3_BeginPattern`).
Pattern(String),
}
/// A swatch of the palette: its name and paint.
#[derive(Clone, Debug, PartialEq)]
pub(super) struct Entry {
pub name: String,
pub cell: Cell,
}
/// The palette: the swatches outside any group, then each colour group's, in order.
#[derive(Clone, Debug, Default, PartialEq)]
pub(super) struct Palette {
pub swatches: Vec<Entry>,
pub groups: Vec<(String, Vec<Entry>)>,
}
impl Palette {
pub fn is_empty(&self) -> bool {
self.swatches.is_empty() && self.groups.iter().all(|(_, s)| s.is_empty())
}
fn len(&self) -> usize {
self.swatches.len() + self.groups.iter().map(|(_, s)| s.len()).sum::<usize>()
}
}
/// The palette of `data`: what follows `%AI5_BeginPalette`, up to its end.
pub(super) fn palette(data: &[u8]) -> Palette {
let mut lex = Lexer::new(data);
let mut out = Palette::default();
let mut vals: Vec<V> = Vec::new();
// The paint of the swatch being read.
let mut cell: Option<Cell> = None;
// Inside a gradient's `Bb` … `BB`.
let mut in_gradient = false;
// Inside versioned content's alternate, which repeats what came before it.
let mut alternate = false;
while let Some(t) = lex.next_token() {
if let Tok::Comment(c) = t.tok {
let line = String::from_utf8_lossy(c);
if line.contains("_Alternate_Content") {
alternate = true;
} else if line.contains("_End_Versioned_Content") || line.contains("_Begin_Content_if_version") {
alternate = false;
}
continue;
}
if alternate {
continue;
}
let w = match t.tok {
Tok::Num(v) => {
push(&mut vals, V::Num(v));
continue;
}
Tok::Str(s) | Tok::Hex(s) => {
push(&mut vals, V::Str(s));
continue;
}
Tok::Word(w) => std::str::from_utf8(w).unwrap_or("?"),
_ => continue,
};
match w {
// Array brackets: their numbers are a pattern's matrix, which the swatch doesn't need.
"[" | "]" => continue,
"Bb" => {
in_gradient = true;
cell = None;
}
"Bg" if in_gradient => {
if let Some(name) = last_text(&vals) {
cell = Some(Cell::Gradient(name));
}
}
"BB" => in_gradient = false,
"p" => cell = first_text(&vals).map(Cell::Pattern),
"Xs" | "Xz" | "XS" | "XZ" => cell = None,
"g" | "k" | "x" | "Xa" | "Xx" | "Xk" => {
cell = paint::color_op(w, &vals).and_then(|(p, named)| match named {
Some(Named { color, spot, .. }) => Some(Cell::Color { color, global: true, spot }),
None => p.color().map(|color| Cell::Color { color, global: false, spot: false }),
});
}
"Pg" => {
if let Some(name) = first_text(&vals) {
out.groups.push((name, Vec::new()));
}
}
"Pc" => {
if let (Some(name), Some(c)) = (last_text(&vals), cell.take())
&& !name.starts_with('[')
&& out.len() < MAX_SWATCHES
{
let e = Entry { name, cell: c };
match out.groups.last_mut() {
Some((_, g)) => g.push(e),
None => out.swatches.push(e),
}
}
cell = None;
}
_ => {}
}
vals.clear();
}
out
}
fn push(vals: &mut Vec<V>, v: V) {
if vals.len() < MAX_VALUES {
vals.push(v);
}
}
fn first_text(vals: &[V]) -> Option<String> {
vals.iter().find(|v| matches!(v, V::Str(_))).and_then(V::text)
}
fn last_text(vals: &[V]) -> Option<String> {
vals.iter().rev().find(|v| matches!(v, V::Str(_))).and_then(V::text)
}
#[cfg(test)]
mod tests {
use super::*;
fn color(name: &str, color: Color, global: bool, spot: bool) -> Entry {
Entry { name: name.into(), cell: Cell::Color { color, global, spot } }
}
#[test]
fn a_palette_gives_its_swatches_in_order() {
let p = palette(
b"0 0 Pb\n1 1 1 1 ([Registration]) 0 Xs\n([Registration])\nPc\n\
0 0 0 1 k\n(Black)\nPc\n\
0.1 0.9 0.8 0 (Ink 185) 0 x\n(Ink 185)\nPc\n\
0 0.5 1 0 1 0.5 0 (Brand) 0 1 Xk\n(Brand)\nPc\n\
0.5 0.5 0.5 0 50 40 -20 (Lab ink) 0 2 Xx\n(Lab ink)\nPc\n\
Bb\n2 (Sky) 0 0 0 1 1 0 0 1 0 0 1 Bg\n0 BB\n(Sky)\nPc\n\
(Dots) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n(Dots)\nPc\nPB\n",
);
assert!(p.groups.is_empty());
assert_eq!(
p.swatches,
vec![
color("Black", Color::cmyk(0.0, 0.0, 0.0, 1.0), false, false),
color("Ink 185", Color::cmyk(0.1, 0.9, 0.8, 0.0), true, true),
color("Brand", Color::rgb(1.0, 0.5, 0.0), true, false),
color("Lab ink", Color::lab(50.0, 40.0, -20.0), true, true),
Entry { name: "Sky".into(), cell: Cell::Gradient("Sky".into()) },
Entry { name: "Dots".into(), cell: Cell::Pattern("Dots".into()) },
]
);
}
#[test]
fn colour_groups_hold_the_swatches_after_them() {
let p = palette(
b"0 0 Pb\n0 0 0 0 k\n(White)\nPc\n1 (Greys) 1 Pg\n0 0 0 0.5 k\n(K50)\nPc\n0 (Inks) 1 Pg\n0 1 1 0 (Red ink) 0 x\n(Red ink)\nPc\nPB\n",
);
assert_eq!(p.swatches.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), ["White"]);
let groups: Vec<(&str, Vec<&str>)> = p.groups.iter().map(|(n, s)| (n.as_str(), s.iter().map(|e| e.name.as_str()).collect())).collect();
assert_eq!(groups, [("Greys", vec!["K50"]), ("Inks", vec!["Red ink"])]);
}
#[test]
fn versioned_swatches_are_read_once() {
let p = palette(
b"0 0 Pb\n%AI17_Begin_Content_if_version_gt:24 15\n0 1 1 0 (Red ink) 0 x\n(Red ink)\nPc\n\
%AI17_Alternate_Content\n0 1 1 0 (Red ink) 0 x\n(Red ink)\nPc\n%AI17_End_Versioned_Content\nPB\n",
);
assert_eq!(p.swatches.len(), 1);
}
#[test]
fn what_it_doesnt_know_leaves_the_other_swatches() {
let p = palette(b"0 0 Pb\n1 2 3 Zz\n(Odd)\nPc\n(no paint) Pc\n0 0 0 1 k\n(Black)\nPc\nPB\n");
assert_eq!(p.swatches.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), ["Black"]);
}
}
+44 -4
View File
@@ -38,7 +38,10 @@
//! - Type in the legacy format (version 7 specification §15): `type To` … `TO`, read by
//! [`legacy_text`].
//!
//! Sections the reader doesn't need (brushes, symbols, styles, swatches, filters) are skipped;
//! - Swatches: the palette (`%AI5_BeginPalette`), read by [`super::palette`], replaces the
//! default swatches.
//!
//! Sections the reader doesn't need (brushes, symbols, styles, filters) are skipped;
//! type, symbols, fills with a pattern it can't read, placed files without a preview and operators
//! it doesn't know are errors.
@@ -518,6 +521,8 @@ struct Reader<'a> {
mesh: Option<(super::mesh::MeshReader, bool)>,
/// The art styles (`%AI9_BeginArtStyles`), by name.
styles: HashMap<String, super::style::ArtStyle>,
/// The swatches (`%AI5_BeginPalette`).
palette: super::palette::Palette,
/// See [`Structure::restyled`].
restyled: Vec<Rect>,
/// CMYK samples converted, by value.
@@ -599,7 +604,6 @@ const SKIPPED: &[(&str, &str)] = &[
("BeginPluginObject", "EndPluginObject"),
("BeginArtStyles", "EndArtStyles"),
("BeginArtStyleList", "EndArtStyleList"),
("BeginPalette", "EndPalette"),
("BeginSymbolList", "EndSymbolList"),
("BeginTextDocument", "EndTextDocument"),
("BeginEncoding", "EndEncoding"),
@@ -668,6 +672,7 @@ impl<'a> Reader<'a> {
group_ended: false,
layer_state: None,
styles: HashMap::new(),
palette: super::palette::Palette::default(),
restyled: vec![],
mesh: None,
cmyk_cache: HashMap::new(),
@@ -760,6 +765,11 @@ impl<'a> Reader<'a> {
self.styles = super::style::art_styles(section);
return Ok(());
}
if m == "BeginPalette" {
let section = self.section("EndPalette")?;
self.palette = super::palette::palette(section);
return Ok(());
}
if m == "BeginEncoding" {
self.type_state.encoding_comment(line);
}
@@ -1241,7 +1251,7 @@ impl<'a> Reader<'a> {
self.paint(&op, hidden, true)?;
}
// Colours.
"g" | "G" | "k" | "K" | "x" | "X" | "Xa" | "XA" | "Xx" | "XX" | "Xk" | "XK" => self.color(w, &vals),
"g" | "G" | "k" | "K" | "x" | "X" | "Xa" | "XA" | "Xx" | "XX" | "Xk" | "XK" | "Xs" | "XS" | "Xz" | "XZ" => self.color(w, &vals),
"p" | "P" => self.pattern_paint(w, &vals),
// A pattern's definition ends.
"E" if self.pattern.is_some() => self.done = true,
@@ -1726,7 +1736,9 @@ impl<'a> Reader<'a> {
fn color(&mut self, w: &str, vals: &[V]) {
let Some((p, named)) = paint::color_op(w, vals) else { return };
match p.color() {
// Registration (`Xs`, `Xz`) says nothing of the document's colour model.
let registration = matches!(w, "Xs" | "XS" | "Xz" | "XZ");
match p.color().filter(|_| !registration) {
Some(vectorcraft_color::Color::Rgb { .. }) => self.rgb_colors += 1,
Some(vectorcraft_color::Color::Cmyk { .. }) => self.cmyk_colors += 1,
_ => {}
@@ -2378,6 +2390,34 @@ impl<'a> Reader<'a> {
if let Some(u) = self.ruler_units.and_then(ruler_unit) {
doc.units = u;
}
// The file's own swatches in place of the defaults, in its order and colour groups.
if !self.palette.is_empty() {
let palette = std::mem::take(&mut self.palette);
let swatch = |e: super::palette::Entry| -> Option<Swatch> {
use super::palette::Cell;
let (paint, global, spot) = match e.cell {
Cell::Color { color, global, spot } => (Paint::solid(color), global, spot),
Cell::Gradient(g) => {
let mut gp = vectorcraft_color::gradient::GradientPaint::new(self.gradients.get(&g)?.clone());
gp.swatch = Some(e.name.clone());
(Paint::Gradient(Box::new(gp)), false, false)
}
Cell::Pattern(p) => {
doc.patterns.iter().find(|d| d.name == p)?;
(Paint::Pattern { pattern: p, xf: Affine::IDENTITY }, false, false)
}
};
Some(Swatch { name: e.name, paint, global, spot })
};
let mut swatches = vec![Swatch { name: "[None]".into(), paint: Paint::None, global: false, spot: false }];
swatches.extend(palette.swatches.into_iter().filter_map(swatch));
let groups: Vec<_> = palette
.groups
.into_iter()
.map(|(name, s)| vectorcraft_color::swatch::SwatchGroup { name, swatches: s.into_iter().filter_map(swatch).collect() })
.collect();
(doc.swatches, doc.swatch_groups) = (swatches.into(), groups.into());
}
// 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) {