mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 09:29:44 -06:00
Merge pull request #5123 from dotCooCoo/fix/ai-os22-g6
.ai import: version 7 palettes keep their swatches without names
This commit is contained in:
@@ -2623,11 +2623,12 @@ fn ascii85(data: &[u8]) -> String {
|
||||
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\
|
||||
/// isn't defined and a pattern, a colour group, versioned content read once, and the None swatch
|
||||
/// and a swatch without a name, as version 7 files write them.
|
||||
const PALETTE: &str = "%AI5_BeginPalette\n0 0 Pb\nPn\nPc\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";
|
||||
%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\n0.75 g\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 {
|
||||
@@ -2697,8 +2698,9 @@ fn the_editing_fixture_reads_its_palette() {
|
||||
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)]);
|
||||
let groups: Vec<(&str, Vec<&str>)> =
|
||||
doc.swatch_groups.iter().map(|g| (g.name.as_str(), g.swatches.iter().map(|s| s.name.as_str()).collect())).collect();
|
||||
assert_eq!(groups, [("Greys", vec!["K50", "Gray K=25"])], "the swatch without a name is named by its values");
|
||||
}
|
||||
|
||||
/// The fuzzed editing data, as it is, reads its CMYK image with its ink amounts and its alpha
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
//! `%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.
|
||||
//! - Each swatch is its paint, then an optional `(name)`, then `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.
|
||||
//! - A swatch without a name (version 7 files write none) is named by its spot colour, gradient or
|
||||
//! pattern, or by its values as [`color_name`] gives them. `Pn` (the None swatch) and a `Pc` with
|
||||
//! no paint before it add nothing: the document has `[None]` already.
|
||||
//! - `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
|
||||
@@ -13,9 +16,14 @@
|
||||
//! 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.
|
||||
//! it still come in, and a paint before it makes no swatch.
|
||||
//!
|
||||
//! "AI7 spec": Adobe Illustrator File Format Specification, version 7.0 (1998), listed by PRONOM at
|
||||
//! https://www.nationalarchives.gov.uk/PRONOM/fmt/423; §11 documents the palette operators, and its
|
||||
//! example writes the swatches without names.
|
||||
|
||||
use vectorcraft_color::Color;
|
||||
use vectorcraft_doc::swatches::color_name;
|
||||
|
||||
use super::lex::{Lexer, Tok};
|
||||
use super::obj::V;
|
||||
@@ -55,9 +63,6 @@ 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.
|
||||
@@ -65,8 +70,10 @@ 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;
|
||||
// The paint of the swatch being read, with the name it gets when `Pc` gives none.
|
||||
let mut cell: Option<Entry> = None;
|
||||
// How many swatches have been read.
|
||||
let mut count = 0usize;
|
||||
// Inside a gradient's `Bb` … `BB`.
|
||||
let mut in_gradient = false;
|
||||
// How deep in versioned content, and the depth of the block whose later alternatives (which
|
||||
@@ -111,19 +118,20 @@ pub(super) fn palette(data: &[u8]) -> Palette {
|
||||
}
|
||||
"Bg" if in_gradient => {
|
||||
if let Some(name) = last_text(&vals) {
|
||||
cell = Some(Cell::Gradient(name));
|
||||
cell = Some(Entry { cell: Cell::Gradient(name.clone()), name });
|
||||
}
|
||||
}
|
||||
"BB" => in_gradient = false,
|
||||
"p" => cell = first_text(&vals).map(Cell::Pattern),
|
||||
"p" => cell = first_text(&vals).map(|name| Entry { cell: Cell::Pattern(name.clone()), name }),
|
||||
"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 }),
|
||||
Some(Named { name, color, spot, .. }) => Some(Entry { name, cell: Cell::Color { color, global: true, spot } }),
|
||||
None => p.color().map(|color| Entry { name: color_name(color), cell: Cell::Color { color, global: false, spot: false } }),
|
||||
});
|
||||
}
|
||||
"Pg" => {
|
||||
cell = None;
|
||||
// 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
|
||||
@@ -132,19 +140,27 @@ pub(super) fn palette(data: &[u8]) -> Palette {
|
||||
}
|
||||
}
|
||||
"Pc" => {
|
||||
if let (Some(name), Some(c)) = (last_text(&vals), cell.take())
|
||||
&& !name.starts_with('[')
|
||||
&& out.len() < MAX_SWATCHES
|
||||
// The name before `Pc`, or the one the paint gives; `[Registration]` and the other
|
||||
// built-in swatches every document has are left out.
|
||||
let name = last_text(&vals);
|
||||
if let Some(mut e) = cell.take()
|
||||
&& !name.as_ref().is_some_and(|n| n.starts_with('['))
|
||||
&& count < MAX_SWATCHES
|
||||
{
|
||||
let e = Entry { name, cell: c };
|
||||
if let Some(name) = name {
|
||||
e.name = name;
|
||||
}
|
||||
count += 1;
|
||||
match out.groups.last_mut() {
|
||||
Some((_, g)) => g.push(e),
|
||||
None => out.swatches.push(e),
|
||||
}
|
||||
}
|
||||
cell = None;
|
||||
}
|
||||
_ => {}
|
||||
// A gradient's other operators (its focal point, its matrices) are part of its paint.
|
||||
_ if in_gradient => {}
|
||||
// Anything else ends the paint before it, which no `Pc` then makes a swatch of.
|
||||
_ => cell = None,
|
||||
}
|
||||
vals.clear();
|
||||
}
|
||||
@@ -235,4 +251,71 @@ mod tests {
|
||||
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"]);
|
||||
}
|
||||
|
||||
/// An operator it doesn't know after a paint ends that paint: the `Pc` after it makes no swatch.
|
||||
#[test]
|
||||
fn a_paint_before_an_unknown_operator_makes_no_swatch() {
|
||||
let p = palette(b"0 0 Pb\n0 1 1 0 k\nZz\n(Odd)\nPc\n0.5 g\n1 (Group) 1 Pg\nPc\n0 0 0 1 k\n(Black)\nPc\nPB\n");
|
||||
assert_eq!(p.swatches.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), Vec::<&str>::new());
|
||||
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, [("Group", vec!["Black"])]);
|
||||
}
|
||||
|
||||
/// The palette of the AI7 spec's example (§11): `Pn`, then each swatch's paint and `Pc` with no
|
||||
/// name, then a last `Pc`. Each swatch is named by its spot colour or gradient, or by its values;
|
||||
/// `1 g` is white (§5.4.1).
|
||||
#[test]
|
||||
fn swatches_without_names_are_named_by_their_paint() {
|
||||
let p = palette(
|
||||
b"0 0 Pb\nPn\nPc\n1 g\nPc\n0 g\nPc\n0 0 0 0 k\nPc\n0.75 g\nPc\n\
|
||||
Bb\n2 (B&W) 0 0 0 1 1 0 0 1 0 0 1 Bg\n0 BB\nPc\n0.25 0 0 0 k\nPc\n\
|
||||
0 1 1 0 (Spot A) 0 x\nPc\n0.75 g\nPc\n(Dots) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\nPc\nPc\nPB\n",
|
||||
);
|
||||
assert!(p.groups.is_empty());
|
||||
assert_eq!(
|
||||
p.swatches,
|
||||
vec![
|
||||
color("Gray K=0", Color::gray(0.0), false, false),
|
||||
color("Gray K=100", Color::gray(1.0), false, false),
|
||||
color("C=0 M=0 Y=0 K=0", Color::cmyk(0.0, 0.0, 0.0, 0.0), false, false),
|
||||
color("Gray K=25", Color::gray(0.25), false, false),
|
||||
Entry { name: "B&W".into(), cell: Cell::Gradient("B&W".into()) },
|
||||
color("C=25 M=0 Y=0 K=0", Color::cmyk(0.25, 0.0, 0.0, 0.0), false, false),
|
||||
color("Spot A", Color::cmyk(0.0, 1.0, 1.0, 0.0), true, true),
|
||||
color("Gray K=25", Color::gray(0.25), false, false),
|
||||
Entry { name: "Dots".into(), cell: Cell::Pattern("Dots".into()) },
|
||||
]
|
||||
);
|
||||
// RGB and global process colours too.
|
||||
let p = palette(b"0 0 Pb\n0 0 0 0 1 0.5 0 Xa\nPc\n0 0.5 1 0 1 0.5 0 (Brand) 0 1 Xk\nPc\nPB\n");
|
||||
assert_eq!(p.swatches.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), ["R=255 G=128 B=0", "Brand"]);
|
||||
}
|
||||
|
||||
/// Named and unnamed swatches in one palette: the named ones keep their names, and built-in
|
||||
/// swatches are still left out.
|
||||
#[test]
|
||||
fn named_swatches_keep_their_names_beside_unnamed_ones() {
|
||||
let p = palette(
|
||||
b"0 0 Pb\n1 1 1 1 ([Registration]) 0 Xs\n([Registration])\nPc\n0 0 0 1 k\n(Black)\nPc\n0 0 0 0.5 k\nPc\n\
|
||||
1 (Greys) 1 Pg\n0.4 g\n(Mid)\nPc\n0.4 g\nPc\nPB\n",
|
||||
);
|
||||
assert_eq!(p.swatches.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), ["Black", "C=0 M=0 Y=0 K=50"]);
|
||||
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!["Mid", "Gray K=60"])]);
|
||||
}
|
||||
|
||||
/// No more than `MAX_SWATCHES` swatches, counted across the colour groups.
|
||||
#[test]
|
||||
fn swatches_are_capped_across_groups() {
|
||||
let mut data = b"0 0 Pb\n".to_vec();
|
||||
for i in 0..MAX_SWATCHES / 1000 + 1 {
|
||||
data.extend(format!("1 (G{i}) 1 Pg\n").as_bytes());
|
||||
data.extend("0.5 g\nPc\n".repeat(1000).as_bytes());
|
||||
}
|
||||
data.extend(b"PB\n");
|
||||
let p = palette(&data);
|
||||
assert_eq!(p.groups.len(), MAX_SWATCHES / 1000 + 1);
|
||||
assert_eq!(p.swatches.len() + p.groups.iter().map(|(_, s)| s.len()).sum::<usize>(), MAX_SWATCHES);
|
||||
assert!(p.groups.last().is_some_and(|(_, s)| s.is_empty()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::clipnest::MAX_NEST;
|
||||
use vectorcraft_doc::{LayerColor, LineCap, Node, NodeKind};
|
||||
use vectorcraft_geom::Rect;
|
||||
@@ -26,6 +27,16 @@ fn ai(header: &str, ugroup: &str, body: &str) -> Vec<u8> {
|
||||
/// published specification documents; see the `import` module docs).
|
||||
const AI8: &str = "%%Creator: Adobe Illustrator(R) 8.0\n%AI5_FileFormat 4.0\n";
|
||||
|
||||
/// The editing text of a version 7 file (`%AI5_FileFormat 3`), no prolog: `setup` in its setup,
|
||||
/// then `body`.
|
||||
fn stream7(setup: &str, body: &str) -> Vec<u8> {
|
||||
format!(
|
||||
"%!PS-Adobe-3.0\n%%Creator: a script\n%%BoundingBox: 0 0 100 100\n%AI5_FileFormat 3\n%AI3_Cropmarks: 0 0 100 100\n%%EndComments\n\
|
||||
%%BeginProlog\n%%EndProlog\n%%BeginSetup\n{setup}%%EndSetup\n{body}%%PageTrailer\n%%Trailer\n%%EOF\n"
|
||||
)
|
||||
.into_bytes()
|
||||
}
|
||||
|
||||
/// A triangle filled at `x`.
|
||||
fn tri(x: u32) -> String {
|
||||
format!("{x} {x} m {} {x} L {} {} L f", x + 5, x + 5, x + 5)
|
||||
@@ -209,6 +220,32 @@ fn a_legacy_file_with_its_prolog_comes_in_through_its_layers() {
|
||||
assert_eq!(page.document.layers[0].name.as_deref(), Some("Layer 1"));
|
||||
}
|
||||
|
||||
/// A version 7 palette writes no swatch names (the AI7 spec's §11 example): its swatches come in
|
||||
/// named by their paint, in its order, in place of the defaults.
|
||||
#[test]
|
||||
fn a_version_7_palette_keeps_its_swatches_without_names() {
|
||||
let setup = "%AI5_BeginGradient: (B&W)\n(B&W) 0 2 Bd\n[\n%_1 0 50 0 Bs\n%_0 0 50 100 Bs\nBD\n%AI5_EndGradient\n\
|
||||
%AI5_BeginPalette\n0 0 Pb\nPn\nPc\n1 g\nPc\n0 g\nPc\n0 0 0 0 k\nPc\n0.75 g\nPc\n\
|
||||
Bb\n2 (B&W) 0 0 0 1 1 0 0 1 0 0 1 Bg\n0 BB\nPc\n0.25 0 0 0 k\nPc\n0 1 1 0 (Spot A) 0 x\nPc\n0.75 g\nPc\nPc\nPB\n%AI5_EndPalette\n";
|
||||
let body = format!("%AI5_BeginLayer\n1 1 1 1 0 0 1 -1 79 128 255 0 50 0 Lb\n(Art) Ln\n0 A\n0 Xw\n0 0 0 1 k\n{}\nLB\n%AI5_EndLayer--\n", tri(10));
|
||||
let r = import(&stream7(setup, &body)).unwrap();
|
||||
let d = &r.document;
|
||||
assert_eq!(d.layers.len(), 1, "{:?}", r.warnings);
|
||||
let names: Vec<&str> = d.swatches.iter().map(|s| s.name.as_str()).collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
["[None]", "Gray K=0", "Gray K=100", "C=0 M=0 Y=0 K=0", "Gray K=25", "B&W", "C=25 M=0 Y=0 K=0", "Spot A", "Gray K=25"],
|
||||
"{:?}",
|
||||
r.warnings
|
||||
);
|
||||
assert!(d.swatch_groups.is_empty());
|
||||
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_eq!(d.swatch("Gray K=0").unwrap().paint.color(), Some(Color::gray(0.0)), "1 g is white");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbalanced_and_runaway_groups_are_read_safely() {
|
||||
// Ends without a beginning end nothing; groups left open hold the rest.
|
||||
|
||||
@@ -49,6 +49,8 @@ private editing data. VectorCraft:
|
||||
A PDF-compatible `.ai` opens on its PDF part's trim boxes (its artboards), the bleed boxes round them
|
||||
becoming the document's bleed, so art in the bleed shows outside the artboard, within the bleed guide (#1266).
|
||||
A PDF saved with its editing data (`AIPDFPrivateData`) opens the same way.
|
||||
The swatches of a version 7 `.ai` palette, which carry no names, come in named by their spot color, gradient or
|
||||
values.
|
||||
- **Writes** a PDF-compatible `.ai` (Save As › `ai`): Illustrator opens it as plain art. It never writes
|
||||
Illustrator's private editing data, which is undocumented.
|
||||
- **Writes** a legacy `.ai` of version 7 (Save As › `ai` › Version: Legacy (version 7), `aiVersion: "7"`): the
|
||||
|
||||
Reference in New Issue
Block a user