mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
Merge pull request #3941 from fabian-co/feat/ai-palette-swatches
Open .ai files with their swatches (palette, colour groups, unused inks); Swatches panel keeps the art's swatch after deselecting
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -22,6 +22,7 @@ mod lex;
|
||||
mod mesh;
|
||||
mod obj;
|
||||
mod paint;
|
||||
mod palette;
|
||||
mod read;
|
||||
mod style;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
//! 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" => {
|
||||
// Capped as swatches are: a file of many `Pg` would make as many empty groups.
|
||||
if let Some(name) = first_text(&vals)
|
||||
&& out.groups.len() < MAX_SWATCHES
|
||||
{
|
||||
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"]);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -687,7 +687,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, salt: &'static str) {
|
||||
widgets::subheader(ui, tl!("Swatch Tiles"));
|
||||
let query = if pstate(ui.ctx(), "swatch-show-find") { widgets::search_field(ui, find_id(), tl!("Find")) } else { String::new() };
|
||||
let items = entries(app, kind, &query);
|
||||
let active_swatch = active_swatch(app);
|
||||
let active_swatch = kept_swatch(app, ui, active_swatch(app));
|
||||
let focus = focus_swatch(app, ui, active_swatch.as_deref());
|
||||
let selected = selection(app, ui);
|
||||
// Without a selection the swatch of the active paint is highlighted.
|
||||
@@ -773,6 +773,29 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, salt: &'static str) {
|
||||
/// The document and the objects selected in it.
|
||||
type SelectedArt = (u64, Vec<u64>);
|
||||
|
||||
/// The swatch highlighted for the active paint (`active`): with art selected, its swatch; once the
|
||||
/// art is deselected, still that swatch, until another document shows or the paints for new art
|
||||
/// change (a swatch or colour picked with nothing selected). Deselecting doesn't jump to the
|
||||
/// swatch of the paint new art gets (White).
|
||||
fn kept_swatch(app: &VectorcraftApp, ui: &Ui, active: Option<String>) -> Option<String> {
|
||||
let Some(st) = app.session.active() else { return active };
|
||||
let p = &app.session.paint;
|
||||
// The document, and the paints new art gets, which picking with nothing selected changes.
|
||||
let defaults = format!("{}|{:?}|{:?}|{}", st.uid, p.fill, p.stroke, app.session.fill_active);
|
||||
if !st.selection.objects.is_empty() {
|
||||
set_pstate(ui.ctx(), "swatch-kept", Some((defaults, active.clone())));
|
||||
return active;
|
||||
}
|
||||
match pstate::<Option<(String, Option<String>)>>(ui.ctx(), "swatch-kept") {
|
||||
Some((d, kept)) if d == defaults => kept,
|
||||
Some(_) => {
|
||||
set_pstate(ui.ctx(), "swatch-kept", None::<(String, Option<String>)>);
|
||||
active
|
||||
}
|
||||
None => active,
|
||||
}
|
||||
}
|
||||
|
||||
/// The swatch the tiles scroll to this frame, once: the active paint's swatch when it changed, or
|
||||
/// when other art was selected. Selecting other art also clears the panel's own selection, so its
|
||||
/// swatch is the one highlighted.
|
||||
@@ -1905,6 +1928,38 @@ mod tests {
|
||||
assert_eq!(focus_swatch(&app, &egui::Ui::new(ctx.clone(), egui::Id::new("t"), egui::UiBuilder::new()), Some("Last Ink")), None);
|
||||
}
|
||||
|
||||
/// Deselecting art keeps its swatch highlighted (not the White new art gets) until the paints for
|
||||
/// new art change; selecting other art highlights its swatch.
|
||||
#[test]
|
||||
fn deselecting_keeps_the_arts_swatch_highlighted() {
|
||||
let mut app = app();
|
||||
app.run("swatch.new", json!({"name": "Ink 185", "color": "#e4002b", "spot": true})).unwrap();
|
||||
let id = app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap()["id"].clone();
|
||||
app.run("paint.setFill", json!({"ids": [id], "swatch": "Ink 185"})).unwrap();
|
||||
app.run("select.none", json!({})).unwrap();
|
||||
// New art's paints back to the defaults, as for art that came in with a file.
|
||||
app.run("paint.default", json!({})).unwrap();
|
||||
let ctx = context();
|
||||
let ui = egui::Ui::new(ctx.clone(), egui::Id::new("t"), egui::UiBuilder::new());
|
||||
let shown = |app: &VectorcraftApp| kept_swatch(app, &ui, active_swatch(app));
|
||||
assert_ne!(shown(&app).as_deref(), Some("Ink 185"), "nothing selected yet: new art's paint");
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
assert_eq!(shown(&app).as_deref(), Some("Ink 185"));
|
||||
app.run("select.none", json!({})).unwrap();
|
||||
assert_eq!(shown(&app).as_deref(), Some("Ink 185"), "kept after deselecting");
|
||||
assert_eq!(shown(&app).as_deref(), Some("Ink 185"), "and on later frames");
|
||||
// A colour picked with nothing selected is new art's: its swatch shows.
|
||||
app.run("paint.setFill", json!({"swatch": "Black"})).unwrap();
|
||||
assert_eq!(shown(&app).as_deref(), Some("Black"));
|
||||
// Other art selected (a white one) shows its swatch, and keeps it once deselected.
|
||||
let white = app.run("shape.rectangle", json!({"x": 60, "y": 0, "width": 50, "height": 50})).unwrap()["id"].clone();
|
||||
app.run("paint.setFill", json!({"ids": [white.clone()], "swatch": "White"})).unwrap();
|
||||
app.run("select.set", json!({"ids": [white]})).unwrap();
|
||||
assert_eq!(shown(&app).as_deref(), Some("White"));
|
||||
app.run("select.none", json!({})).unwrap();
|
||||
assert_eq!(shown(&app).as_deref(), Some("White"));
|
||||
}
|
||||
|
||||
/// A selected image, or text in a colour no swatch has, highlights no swatch (not None); text in
|
||||
/// a swatch's colour highlights that swatch.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user