Merge pull request #5800 from dotCooCoo/feat/ai-global-name-suffix

.ai import: gradients and patterns are named up to the newline in their names
This commit is contained in:
BFlat
2026-11-05 21:18:35 +07:00
committed by GitHub
11 changed files with 645 additions and 48 deletions
+28
View File
@@ -3346,6 +3346,34 @@ proptest! {
survive("hostile .ai patterns", || Some(vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, false, 1.0).0))?;
}
/// A `.ai` whose gradients and patterns have names with newlines (blank before one, several,
/// repeated, one shown like another's numbered name), used by fills and a palette, damaged or
/// hostile.
#[test]
fn ai_global_names_with_newlines_never_panic(
names in prop::collection::vec(prop::sample::select(vec!["Sky\\n1", "Sky\\n2", "Sky", "Sky 2", "\\n3", "\\n", "Sky\\n\\n1", "\\012x"]), 1..6),
cut in 0usize..3_000,
edits in prop::collection::vec((0usize..3_000, prop::sample::select(vec!['\\', 'n', '(', ')', ' ', '\n', 'B', 'p', 'E', '%'])), 0..10),
) {
use vectorcraft_testkit::ai;
let mut setup = String::new();
let mut art = String::new();
let mut cells = String::new();
for name in &names {
setup.push_str(&format!(
"%AI5_BeginGradient: ({name})\n({name}) 0 2 Bd\n[\n%_1 0 50 0 Bs\n%_0 0 50 100 Bs\nBD\n%AI5_EndGradient\n\
%AI3_BeginPattern: ({name})\n({name}) 0 0 6 6\n%_0 A\n%_0 0 m\n%_0 6 L\n%_6 6 L\n%_6 0 L\n%_0 0 0 1 k\n%_f\nE\n%AI3_EndPattern\n"
));
art.push_str(&format!("Bb\n1 ({name}) 0 0 0 50 1 0 0 1 0 0 Bg\n{}f\n0 BB\n", ai::rect(10.0, 10.0, 40.0, 40.0)));
art.push_str(&format!("({name}) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n{}f\n", ai::rect(50.0, 50.0, 90.0, 90.0)));
cells.push_str(&format!("Bb\n2 ({name}) 0 0 0 1 1 0 0 1 0 0 1 Bg\n0 BB\nPc\n({name}) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\nPc\n"));
}
setup.push_str(&format!("%AI5_BeginPalette\n0 0 Pb\n{cells}PB\n%AI5_EndPalette\n"));
let data = mutate_text(&ai::editing_data(100.0, 100.0, &format!("{setup}{}", ai::layer("L", &art))), cut, &edits);
let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&data)].concat();
survive("hostile .ai global names", || Some(vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, false, 1.0).0))?;
}
/// A `.ai` whose images (binary samples with an alpha channel, the operators that end a raster,
/// then a name and an opacity mask) and placed file (an empty relative folder, a preview) are
/// damaged or hostile, bytes of the samples included.
+204 -9
View File
@@ -18,15 +18,20 @@
//! `Bm` for each pair in the order of the ramp (`Bc`, around them, are the caps past the first and
//! last stops). Without `Xm`, a radial gradient's one `Bm` takes the unit circle onto it.
//!
//! - The name shown for a gradient's or a pattern's name in the file is the part before its first
//! newline (`\n`) ([`ShownNames`]).
//!
//! "AI7 spec": Adobe Illustrator File Format Specification, version 7.0 (1998), listed by PRONOM at
//! https://www.nationalarchives.gov.uk/PRONOM/fmt/423; §3.5 documents the gradient operators.
//! https://www.nationalarchives.gov.uk/PRONOM/fmt/423; §3.5 documents the gradient operators and §4
//! the names of global objects.
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use kurbo::{Affine, Point, Vec2};
use vectorcraft_color::gradient::{Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop};
use vectorcraft_color::{Color, Paint};
use super::lex::{Lexer, Tok};
use super::obj::V;
/// Largest number of stops read in one gradient.
@@ -189,6 +194,157 @@ impl GradientDef {
}
}
/// The names of the named colors in the tokens [`Self::token`] is given: those the color operators
/// (`x`, `Xx`, `Xk` and their stroke forms) and gradient stops (`Bs` between `Bd` and `BD`) use,
/// also in the color and tile strings of patterns in the version 7 form (`(…) @`, `(…) &`). It
/// also keeps the names of the gradients `BD` finishes ([`GradientDef::finish`]).
#[derive(Default)]
pub(super) struct ColorNames {
pub names: HashSet<String>,
pub gradients: Vec<String>,
vals: Vec<V>,
def: Option<GradientDef>,
spots: Vec<Named>,
}
impl ColorNames {
pub fn token(&mut self, tok: Tok<'_>) {
self.take(tok, true);
}
/// [`Self::token`], with the names in the strings of a version 7 pattern's layer when `outer`.
fn take(&mut self, tok: Tok<'_>, outer: bool) {
let w = match tok {
Tok::Num(n) => {
if self.vals.len() < MAX_STOPS {
self.vals.push(V::Num(n));
}
return;
}
Tok::Str(s) | Tok::Hex(s) => {
if self.vals.len() < MAX_STOPS {
self.vals.push(V::Str(s));
}
return;
}
Tok::Word(w) => w,
_ => return,
};
let vals = std::mem::take(&mut self.vals);
match w {
b"Bd" => self.def = GradientDef::begin(&vals),
b"BD" => {
if let Some((name, _)) = self.def.take().and_then(GradientDef::finish) {
self.gradients.push(name);
}
}
b"Bs" => {
if let Some(d) = self.def.as_mut() {
d.stop(&vals, &mut self.spots);
}
}
b"x" | b"X" | b"Xx" | b"XX" | b"Xk" | b"XK" => {
if let Some((_, Some(n))) = std::str::from_utf8(w).ok().and_then(|op| color_op(op, &vals)) {
self.spots.push(n);
}
}
b"@" | b"&" if outer => {
for v in &vals {
if let V::Str(s) = v {
let mut inner = ColorNames::default();
let mut lex = Lexer::new(s);
while let Some(t) = lex.next_token() {
inner.take(t.tok, false);
}
self.names.extend(inner.names);
}
}
}
_ => {}
}
self.names.extend(self.spots.drain(..).map(|n| n.name));
}
}
/// A kind of global object.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Global {
Gradient,
Pattern,
}
/// The names the gradients and patterns get in the document, by their kind and names in the file.
/// The name shown for a name in the file is the part before its first newline (AI7 spec §4):
/// `(Sky\n1)` is shown as "Sky". Gradients and patterns share their swatches' names. A name with a
/// newline is shown as the part before it unless that name is reserved (a custom color's, or a
/// gradient's or pattern's name without a newline) or an object defined earlier (of either kind)
/// got it; then it is the first free "Sky 2", "Sky 3"… A name shown as nothing (`(\n3)`) is shown
/// as "Gradient" or "Pattern", numbered the same way. A name without a newline is kept. An object
/// defined again under the same name in the file keeps the name it got.
#[derive(Debug, Default)]
pub(super) struct ShownNames {
gradients: HashMap<String, String>,
patterns: HashMap<String, String>,
taken: HashSet<String>,
/// The number to try first for each shown name already taken.
next: HashMap<String, usize>,
}
impl ShownNames {
fn by_file(&self, kind: Global) -> &HashMap<String, String> {
match kind {
Global::Gradient => &self.gradients,
Global::Pattern => &self.patterns,
}
}
/// The name the object of `kind` defined as `name` in the file got ([`Self::define`]), or
/// `name` for one not defined.
pub fn get<'n>(&'n self, kind: Global, name: &'n str) -> &'n str {
self.by_file(kind).get(name).map_or(name, String::as_str)
}
/// Keep `name` from the objects defined after.
pub fn reserve(&mut self, name: &str) {
self.taken.insert(name.to_string());
}
/// The name the object of `kind` defined as `name` in the file gets.
pub fn define(&mut self, kind: Global, name: &str) -> String {
if let Some(shown) = self.by_file(kind).get(name) {
return shown.clone();
}
let shown = match name.split_once('\n') {
None => name.to_string(),
Some((base, _)) => {
let base = match (base.is_empty(), kind) {
(false, _) => base,
(true, Global::Gradient) => "Gradient",
(true, Global::Pattern) => "Pattern",
};
if self.taken.contains(base) {
// The first free number from the one after the last number this base got. Of
// the numbers `from..=tries`, at least one is free.
let from = self.next.get(base).copied().unwrap_or(2);
let tries = from.saturating_add(self.taken.len());
let i = (from..tries).find(|i| !self.taken.contains(&format!("{base} {i}"))).unwrap_or(tries);
self.next.insert(base.to_string(), i.saturating_add(1));
format!("{base} {i}")
} else {
base.to_string()
}
}
};
self.taken.insert(shown.clone());
let by_file = match kind {
Global::Gradient => &mut self.gradients,
Global::Pattern => &mut self.patterns,
};
by_file.insert(name.to_string(), shown.clone());
shown
}
}
/// A gradient instance on an object, between `Bb` and `BB`.
#[derive(Clone, Debug, Default)]
pub(super) struct Instance {
@@ -267,8 +423,9 @@ impl Instance {
self.xm.is_some() || self.bm.is_some()
}
/// The paint, through `to_doc` (art space → the document), when the gradient is defined.
pub fn paint(&self, defs: &HashMap<String, Gradient>, to_doc: Affine) -> Option<Paint> {
/// The paint, through `to_doc` (art space → the document), when the gradient is defined, linked
/// to the gradient's swatch by the name `shown` gave it.
pub fn paint(&self, defs: &HashMap<String, Gradient>, shown: &ShownNames, to_doc: Affine) -> Option<Paint> {
let name = self.name.as_ref()?;
let gradient = defs.get(name)?.clone();
let radial = gradient.kind == GradientKind::Radial;
@@ -319,7 +476,8 @@ impl Instance {
let focal = (self.hilight.hypot() > 1e-9).then_some(focal).filter(|p| p.is_finite());
let geom = GradientGeom { start, end, aspect, focal: if radial { focal } else { None } };
let angle = -(end - start).atan2().to_degrees();
Some(Paint::Gradient(Box::new(GradientPaint { geom: Some(geom), angle, swatch: Some(name.clone()), ..GradientPaint::new(gradient) })))
let swatch = Some(shown.get(Global::Gradient, name).to_string());
Some(Paint::Gradient(Box::new(GradientPaint { geom: Some(geom), angle, swatch, ..GradientPaint::new(gradient) })))
}
}
@@ -415,22 +573,59 @@ mod tests {
bg.extend(nums(&[100.0, 100.0, 0.0, 50.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0]));
i.bg(&bg);
i.bm(&nums(&[50.0, 0.0, 0.0, -50.0, 100.0, 100.0]));
let Some(Paint::Gradient(gp)) = i.paint(&defs, Affine::IDENTITY) else { panic!() };
let shown = ShownNames::default();
let Some(Paint::Gradient(gp)) = i.paint(&defs, &shown, Affine::IDENTITY) else { panic!() };
assert_eq!(gp.swatch.as_deref(), Some("G"));
let geom = gp.geom.unwrap();
assert_eq!(geom.start, Point::new(100.0, 100.0));
assert_eq!(geom.end, Point::new(150.0, 100.0));
assert!((geom.aspect - 1.0).abs() < 1e-9);
assert!(i.paint(&HashMap::new(), Affine::IDENTITY).is_none());
assert!(i.paint(&HashMap::new(), &shown, Affine::IDENTITY).is_none());
// Newer files give the matrix with `Xm`; the `Bm`s after it are bands of a cached ramp.
let mut i = Instance::begin(&[]);
i.bg(&bg);
i.xm(&nums(&[40.0, 0.0, 0.0, -40.0, 10.0, 10.0]));
i.bm(&nums(&[5.0, 0.0, 0.0, -40.0, 30.0, 10.0]));
let Some(Paint::Gradient(gp)) = i.paint(&defs, Affine::IDENTITY) else { panic!() };
let Some(Paint::Gradient(gp)) = i.paint(&defs, &shown, Affine::IDENTITY) else { panic!() };
let geom = gp.geom.unwrap();
assert_eq!((geom.start, geom.end), (Point::new(10.0, 10.0), Point::new(50.0, 10.0)));
}
/// A name is shown up to its first newline, numbered when a reserved name or an earlier object
/// of either kind has it; a name defined again keeps the name it got, and a blank one gets its
/// kind's stand-in. A name without a newline is kept.
#[test]
fn global_names_are_shown_up_to_their_newline() {
let mut shown = ShownNames::default();
// A color's name, and the names without a newline the file defines.
for name in ["Ink", "Sky", "Sky 2", "Sea"] {
shown.reserve(name);
}
let got: Vec<String> = [
(Global::Gradient, "Sky\n1"),
(Global::Gradient, "Sky\n2"),
(Global::Gradient, "Sky"),
(Global::Gradient, "Sky\n1"),
(Global::Pattern, "Sky\n1"),
(Global::Gradient, "Sea\nA\nB"),
(Global::Gradient, "\n3"),
(Global::Pattern, "\n4"),
(Global::Gradient, "Sky 2"),
(Global::Pattern, "Ink\n1"),
(Global::Gradient, "Ink"),
(Global::Pattern, "Sea"),
(Global::Pattern, "Tide\n1"),
(Global::Pattern, ""),
]
.iter()
.map(|(kind, n)| shown.define(*kind, n))
.collect();
assert_eq!(got, ["Sky 3", "Sky 4", "Sky", "Sky 3", "Sky 5", "Sea 2", "Gradient", "Pattern", "Sky 2", "Ink 2", "Ink", "Sea", "Tide", ""]);
assert_eq!(shown.get(Global::Gradient, "Sky\n2"), "Sky 4");
assert_eq!(shown.get(Global::Pattern, "Sky\n1"), "Sky 5");
assert_eq!(shown.get(Global::Pattern, "Sky\n2"), "Sky\n2", "not a pattern's name");
}
#[test]
fn gradient_stops_beyond_the_cap_are_ignored() {
let mut d = GradientDef::begin(&[V::Str(b"H".to_vec()), V::Num(0.0)]).unwrap();
@@ -481,7 +676,7 @@ mod tests {
// From the stop at 20 to the one at 60, then on to the one at 100.
i.bm(&nums(&[0.0, -80.0, -30.0, 0.0, 10.0, 260.0]));
i.bm(&nums(&[0.0, -80.0, -30.0, 0.0, 10.0, 180.0]));
let Some(Paint::Gradient(gp)) = i.paint(&defs, Affine::IDENTITY) else { panic!() };
let Some(Paint::Gradient(gp)) = i.paint(&defs, &ShownNames::default(), Affine::IDENTITY) else { panic!() };
gp.geom.unwrap()
};
for xm in [true, false] {
+8
View File
@@ -63,6 +63,14 @@ impl Palette {
pub fn is_empty(&self) -> bool {
self.swatches.is_empty() && self.groups.iter().all(|(_, s)| s.is_empty())
}
/// The names the palette gives its color groups and swatches, except the name a swatch takes
/// from its gradient or pattern.
pub fn own_names(&self) -> impl Iterator<Item = &str> {
let named = |e: &Entry| !matches!(&e.cell, Cell::Gradient(n) | Cell::Pattern(n) if *n == e.name);
let swatches = self.swatches.iter().chain(self.groups.iter().flat_map(|(_, s)| s)).filter(move |e| named(e));
self.groups.iter().map(|(name, _)| name.as_str()).chain(swatches.map(|e| e.name.as_str()))
}
}
/// The palette of `data`: what follows `%AI5_BeginPalette`, up to its end.
+131 -4
View File
@@ -110,7 +110,7 @@ use super::super::ate::CANVAS_CENTRE;
use super::super::graphics::HAIRLINE;
use super::lex::{Lexer, Tok};
use super::obj::{self, Obj, V};
use super::paint::{self, GradientDef, Instance, Named};
use super::paint::{self, Global, GradientDef, Instance, Named, ShownNames};
use crate::ps;
mod art;
@@ -210,7 +210,7 @@ pub(super) fn remember_xml_id(xml_ids: &mut HashMap<String, Option<NodeId>>, byt
/// when `large_canvas` is set: an EPS's page is drawn at the full size, but a `.ai`'s PDF part may
/// keep the reduced one, which the PDF import reads as it is (#2031).
pub fn read(data: &[u8], large_canvas: bool) -> Result<Structure, String> {
let mut r = Reader::new(data);
let mut r = Reader::with_names(data);
r.large_canvas = large_canvas.then_some(1.0);
r.run()?;
r.finish()
@@ -220,13 +220,129 @@ pub fn read(data: &[u8], large_canvas: bool) -> Result<Structure, String> {
/// data): the program's header gives the author and the custom colors the editing data's header
/// doesn't.
pub fn read_beside(data: &[u8], large_canvas: bool, ps: &[u8]) -> Result<Structure, String> {
let mut r = Reader::new(data);
let header = Reader::header_of(ps);
let mut r = Reader::with_names(data);
r.large_canvas = large_canvas.then_some(1.0);
// The program's custom colors have swatches of their names, which no gradient or pattern takes.
for n in &header.custom_colors {
r.global_names.reserve(&n.name);
}
r.run()?;
r.program_header(ps);
r.program_header(header);
r.finish()
}
/// The names the gradients and patterns of the editing data `data` get in the document
/// ([`ShownNames`]), with the names its other swatches can have reserved first: Registration, the
/// default swatches of both color modes when the palette the reader keeps is missing or empty, the
/// names that palette gives, the named colors, and the names without a newline of the gradients
/// and patterns. Names come only from what the reader reads: not from the alternates of versioned
/// content, the prolog, resources, sections it skips (such as symbols and the text document) or
/// what follows the trailer. The palette the reader keeps is the last one outside those and the
/// sections it reads on their own.
fn swatch_names(data: &[u8]) -> ShownNames {
/// The sections the reader reads on their own whose definitions and colors it keeps.
const READ_APART: &[&str] = &["BeginPattern", "BeginBrushPattern", "BeginPluginObject", "BeginArtStyles", "BeginArtStyleList"];
/// Whether the marker `m` ends what began with `begin` and ends with `end`, `depth` of its kind
/// open inside it.
fn closes(begin: &str, end: &str, depth: &mut usize, m: &str) -> bool {
if !begin.is_empty() && m == begin {
*depth += 1;
} else if m == end {
match depth.checked_sub(1) {
Some(d) => *depth = d,
None => return true,
}
}
false
}
let mut names = ShownNames::default();
names.reserve(vectorcraft_color::swatch::REGISTRATION);
let mut colors = paint::ColorNames::default();
let mut lex = Lexer::new(data);
// What the reader skips wherever it is (the marker of one of its kind nested in it, its end
// marker, how many are open inside it, and where reading goes on when it doesn't end), the
// top-level section being passed over (the first three, and whether the reader reads it on its
// own), and where the palette being found starts.
let mut skip: Option<(&str, &str, usize, Option<usize>)> = None;
let mut section: Option<(&str, &str, usize, bool)> = None;
let (mut palette_at, mut palette) = (None, None);
// A pattern's name is the first string of its definition.
let mut head = false;
loop {
let before = lex.offset();
let Some(t) = lex.next_token() else {
// Versioned content that doesn't end is read whole, as the reader reads it.
match skip.take() {
Some((.., Some(at))) => {
lex.seek(at);
continue;
}
_ => break,
}
};
match &t.tok {
Tok::Comment(c) => {
let line = String::from_utf8_lossy(c);
let m = marker(&line);
if let Some((begin, end, depth, _)) = skip.as_mut() {
if closes(begin, end, depth, m) {
skip = None;
}
} else if let Some(at) = palette_at {
if m == "EndPalette" {
palette = Some(lex.slice(at, before));
palette_at = None;
}
} else if m.starts_with("Alternate_Content") && section.is_none_or(|s| s.3) {
skip = Some(("Begin_Content_if_version_gt", "End_Versioned_Content", 0, Some(lex.offset())));
} else if m == "%BeginProlog" {
skip = Some(("", "%EndProlog", 0, None));
} else if m == "%BeginResource" {
skip = Some(("", "%EndResource", 0, None));
} else if let Some((begin, end, depth, _)) = section.as_mut() {
if closes(begin, end, depth, m) {
section = None;
}
} else if m == "BeginPalette" {
palette_at = Some(lex.offset());
} else if let Some((begin, end)) = types::SKIPPED.iter().find(|(b, _)| *b == m) {
section = Some((begin, end, 0, READ_APART.contains(begin)));
head = m == "BeginPattern";
} else if matches!(m, "%PageTrailer" | "%Trailer") {
break;
}
continue;
}
// A palette's names are those of the palette kept, as the palette reads them.
_ if skip.is_some() || palette_at.is_some() || section.is_some_and(|s| !s.3) => continue,
Tok::Str(s) | Tok::Hex(s) if std::mem::take(&mut head) => {
let text = obj::text(s);
if !text.contains('\n') {
names.reserve(&text);
}
}
_ => {}
}
colors.token(t.tok);
}
colors.gradients.iter().filter(|n| !n.contains('\n')).for_each(|n| names.reserve(n));
let palette = palette.map(super::palette::palette).unwrap_or_default();
if palette.is_empty() {
for mode in [ColorMode::Rgb, ColorMode::Cmyk] {
let (swatches, groups) = vectorcraft_color::default_swatches(mode.model());
for g in &groups {
names.reserve(&g.name);
g.swatches.iter().for_each(|s| names.reserve(&s.name));
}
swatches.iter().for_each(|s| names.reserve(&s.name));
}
}
palette.own_names().for_each(|n| names.reserve(n));
colors.names.iter().for_each(|n| names.reserve(n));
names
}
struct Reader<'a> {
lex: Lexer<'a>,
stack: Vec<V>,
@@ -258,6 +374,8 @@ struct Reader<'a> {
/// The Objects on Path records of the `/Document` dictionary and the ids of the objects.
on_path: on_path::OnPath,
gradients: HashMap<String, Gradient>,
/// The names the gradients and patterns get in the document, by their names in the file.
global_names: ShownNames,
gradient_def: Option<GradientDef>,
instance: Option<Instance>,
pending_fill: Option<Paint>,
@@ -428,6 +546,14 @@ pub(crate) fn is_look_slot(name: Option<&str>) -> bool {
}
impl<'a> Reader<'a> {
/// A reader of the editing data `data` as a whole, with the names its gradients and patterns
/// get in the document found first ([`swatch_names`]).
fn with_names(data: &'a [u8]) -> Self {
let mut r = Self::new(data);
r.global_names = swatch_names(data);
r
}
fn new(data: &'a [u8]) -> Self {
let mut doc = Document::new(1.0, 1.0);
doc.layers.clear();
@@ -452,6 +578,7 @@ impl<'a> Reader<'a> {
variables: VariableRead::default(),
on_path: on_path::OnPath::default(),
gradients: HashMap::new(),
global_names: ShownNames::default(),
gradient_def: None,
instance: None,
pending_fill: None,
+1 -1
View File
@@ -361,7 +361,7 @@ impl<'a> Reader<'a> {
let (cx, cy) = ((t[0] + t[2]) / 2.0, (t[1] + t[3]) / 2.0);
to_doc *= Affine::new([1.0, 0.0, 0.0, -1.0, CANVAS_CENTRE - cx, CANVAS_CENTRE + cy]);
}
match i.paint(&self.gradients, to_doc) {
match i.paint(&self.gradients, &self.global_names, to_doc) {
Some(p) if i.stroke => self.pending_stroke = Some(p),
Some(p) => self.pending_fill = Some(p),
None => {
+16 -6
View File
@@ -4,7 +4,7 @@ use super::types::{
FLIP, GState, Kind, MAX_HEX, MAX_NOTE, MAX_PATTERNS, MAX_PLACED_FOLDER, MAX_STACK, MAX_SUBREADERS, PatternHead, Place, Raster, SKIPPED,
VERSIONED_UNENDED,
};
use super::{Arc, Lexer, Node, PatternDef, Reader, Rect, Tok, V, marker, obj};
use super::{Arc, Global, Lexer, Node, PatternDef, Reader, Rect, Tok, V, marker, obj};
impl<'a> Reader<'a> {
pub(super) fn push(&mut self, v: V) -> Result<(), String> {
@@ -310,6 +310,8 @@ impl<'a> Reader<'a> {
_ => return,
};
if !name.trim().is_empty() && !self.custom_colors.iter().any(|n| n.name == name) {
// A gradient or pattern defined after it does not take its swatch's name.
self.global_names.reserve(&name);
self.custom_colors.push(super::Named { name, color, tint: 1.0, spot: true });
}
}
@@ -362,13 +364,15 @@ impl<'a> Reader<'a> {
let mut sub = Reader::new(section);
sub.subreader_depth = self.subreader_depth + 1;
sub.pattern = Some(PatternHead::default());
// The gradients defined so far, lent (and those it defines kept).
// The gradients defined so far and the names given, lent (and those it defines kept).
sub.gradients = std::mem::take(&mut self.gradients);
sub.global_names = std::mem::take(&mut self.global_names);
sub.nodes = self.nodes;
sub.template = self.template;
let read = sub.open(Kind::Group, false).and_then(|()| sub.run());
self.nodes = sub.nodes;
self.gradients = std::mem::take(&mut sub.gradients);
self.global_names = std::mem::take(&mut sub.global_names);
// Unread art in the definition, hidden or not, leaves the tile incomplete.
let whole = read.is_ok()
&& sub.unsupported.is_empty()
@@ -387,6 +391,7 @@ impl<'a> Reader<'a> {
sub.subreader_depth = self.subreader_depth + 1;
sub.pattern = Some(PatternHead { taken: true, name: def.name.clone(), tile: Some(def.tile) });
sub.gradients = std::mem::take(&mut self.gradients);
sub.global_names = std::mem::take(&mut self.global_names);
sub.nodes = self.nodes;
sub.template = self.template;
let mut read = sub.open(Kind::Group, false);
@@ -398,6 +403,7 @@ impl<'a> Reader<'a> {
}
self.nodes = sub.nodes;
self.gradients = std::mem::take(&mut sub.gradients);
self.global_names = std::mem::take(&mut sub.global_names);
let whole = read.is_ok()
&& sub.unsupported.is_empty()
&& sub.unknown.is_empty()
@@ -473,15 +479,17 @@ impl<'a> Reader<'a> {
return;
};
let art: Vec<Arc<Node>> = frame.children.iter().map(|n| Arc::new(self.doc.reid(n))).collect();
let mut def = PatternDef::new(&head.name, art);
// Fills and the tables look the pattern up by its name in the file; the document has it by
// the name it shows.
let mut def = PatternDef::new(&self.global_names.define(Global::Pattern, &head.name), art);
def.tile = FLIP.transform_rect_bbox(tile);
if self.pattern_tiles.insert(def.name.clone(), def.tile).is_some() {
if self.pattern_tiles.insert(head.name.clone(), def.tile).is_some() {
self.doc.patterns.retain(|p| p.name != def.name);
}
if v7 {
self.v7_patterns.insert(def.name.clone());
self.v7_patterns.insert(head.name);
} else {
self.v7_patterns.remove(&def.name);
self.v7_patterns.remove(&head.name);
}
self.doc.patterns.push(def);
for (key, blob) in sub.doc.images {
@@ -570,12 +578,14 @@ impl<'a> Reader<'a> {
// The gradients and patterns defined so far, lent.
sub.gradients = std::mem::take(&mut self.gradients);
sub.pattern_tiles = std::mem::take(&mut self.pattern_tiles);
sub.global_names = std::mem::take(&mut self.global_names);
sub.nodes = self.nodes;
sub.template = self.template;
let read = sub.open(Kind::Group, false).and_then(|()| sub.run());
self.nodes = sub.nodes;
self.gradients = std::mem::take(&mut sub.gradients);
self.pattern_tiles = std::mem::take(&mut sub.pattern_tiles);
self.global_names = std::mem::take(&mut sub.global_names);
// Images aren't kept: the document's images don't count a brush's art among their users. Art
// read with a note (something left out of it) isn't kept either.
let whole = read.is_ok()
+32 -18
View File
@@ -4,7 +4,7 @@ use super::containers::drop_commented_copies;
use super::ops::appearance_from_art_style;
use super::types::{FLIP, Kind, MAX_DEPTH};
use super::{
Affine, Arc, Artboard, CANVAS_CENTRE, ColorMode, Document, HashMap, Node, NodeId, NodeKind, Obj, OpacityMask, Paint, Point, Reader, Rect,
Affine, Arc, Artboard, CANVAS_CENTRE, ColorMode, Document, Global, HashMap, Node, NodeId, NodeKind, Obj, OpacityMask, Paint, Point, Reader, Rect,
Structure, Swatch, Unit, V, obj,
};
use vectorcraft_doc::{DataSet, DataValue, GraphicStyle, Guide, ImageBlob, PatternDef, TextWrap, Variable, VariableKind, Variables};
@@ -231,7 +231,7 @@ fn repoint(n: &mut Node, renamed: &std::collections::BTreeMap<String, String>) {
/// editing data defines, which are named apart from `patterns` ([`beside`]); `None` when the
/// palette is empty or reading stops before it.
pub fn palette(data: &[u8], patterns: &[PatternDef]) -> Option<PaletteSwatches> {
let mut r = Reader::new(data);
let mut r = Reader::with_names(data);
// The palette, the gradients and the patterns it names come before the art, so an error in the
// art leaves them read.
let _ = r.run();
@@ -281,7 +281,7 @@ pub struct Brushes {
pub fn brushes(data: &[u8], patterns: &[PatternDef]) -> Option<Brushes> {
// Brush objects are plug-in objects: data without one isn't read again.
memchr::memmem::find(data, b"BeginPluginObject")?;
let mut r = Reader::new(data);
let mut r = Reader::with_names(data);
// The brushes come before the art, so an error in the art leaves them read.
let _ = r.run();
let (mut lib, unread) = r.brush_library()?;
@@ -406,10 +406,23 @@ pub fn variables(data: &[u8]) -> VariableRead {
}
impl<'a> Reader<'a> {
/// Take the author and custom colors that the header of the program `ps` gives (its comment
/// lines up to `%%EndComments`), where the editing data's own header gives none: the program of
/// an EPS whose editing data is kept apart from it.
pub(super) fn program_header(&mut self, ps: &[u8]) {
/// Take the author and custom colors that the header of a program gives (`outer`, from
/// [`Self::header_of`]), where the editing data's own header gives none: the program of an EPS
/// whose editing data is kept apart from it.
pub(super) fn program_header(&mut self, outer: Reader<'_>) {
if self.author.is_none() {
self.author = outer.author;
}
for n in outer.custom_colors {
if self.custom_colors.len() < super::directives::MAX_CUSTOM_COLORS && !self.custom_colors.iter().any(|c| c.name == n.name) {
self.custom_colors.push(n);
}
}
}
/// A reader that has read the header of the program `ps` (its comment lines up to
/// `%%EndComments`).
pub(super) fn header_of(ps: &[u8]) -> Reader<'_> {
let mut end = 0;
for line in ps.split_inclusive(|&b| b == b'\n' || b == b'\r') {
if !line.starts_with(b"%") && !line.trim_ascii().is_empty() {
@@ -422,14 +435,7 @@ impl<'a> Reader<'a> {
}
let mut outer = Reader::new(ps.get(..end).unwrap_or_default());
let _ = outer.run();
if self.author.is_none() {
self.author = outer.author;
}
for n in outer.custom_colors {
if self.custom_colors.len() < super::directives::MAX_CUSTOM_COLORS && !self.custom_colors.iter().any(|c| c.name == n.name) {
self.custom_colors.push(n);
}
}
outer
}
}
@@ -665,18 +671,26 @@ impl<'a> Reader<'a> {
return None;
}
let palette = std::mem::take(&mut self.palette);
let swatch = |e: crate::import::ai::palette::Entry| -> Option<Swatch> {
let swatch = |mut e: crate::import::ai::palette::Entry| -> Option<Swatch> {
use crate::import::ai::palette::Cell;
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.
Cell::Gradient(g) => {
let mut 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) => {
patterns.iter().find(|d| d.name == p)?;
(Paint::Pattern { pattern: p, xf: Affine::IDENTITY }, false, false)
let pattern = self.global_names.get(Global::Pattern, &p).to_string();
patterns.iter().find(|d| d.name == pattern)?;
if e.name == p {
e.name.clone_from(&pattern);
}
(Paint::Pattern { pattern, xf: Affine::IDENTITY }, false, false)
}
};
Some(Swatch { name: e.name, paint, global, spot })
+18 -9
View File
@@ -5,8 +5,8 @@ use super::types::{
line_width,
};
use super::{
Affine, Appearance, AppearanceItem, Arc, BlendMode, Dash, FillLayer, FillRule, Gradient, GradientDef, Graph, HashMap, HashSet, Instance,
Knockout, LayerColor, LineCap, LineJoin, Node, NodeId, NodeKind, Paint, Reader, Rect, StrokeLayer, V, obj, paint,
Affine, Appearance, AppearanceItem, Arc, BlendMode, Dash, FillLayer, FillRule, Global, Gradient, GradientDef, Graph, HashMap, HashSet, Instance,
Knockout, LayerColor, LineCap, LineJoin, Node, NodeId, NodeKind, Paint, Reader, Rect, ShownNames, StrokeLayer, V, obj, paint,
};
impl<'a> Reader<'a> {
@@ -157,6 +157,7 @@ impl<'a> Reader<'a> {
"Bs" => d.stop(&vals, &mut self.stop_spots),
"BD" => {
if let Some((name, g)) = self.gradient_def.take().and_then(GradientDef::finish) {
self.global_names.define(Global::Gradient, &name);
self.gradients.insert(name, g);
}
}
@@ -478,7 +479,7 @@ impl<'a> Reader<'a> {
self.unread_pattern(w);
return;
};
let p = Paint::Pattern { pattern: name.unwrap_or_default(), xf };
let p = Paint::Pattern { pattern: name.as_deref().map(|n| self.global_names.get(Global::Pattern, n).to_string()).unwrap_or_default(), xf };
if w == "p" {
self.pending_unread_fill = false;
self.gs.fill = p;
@@ -707,7 +708,13 @@ impl<'a> Reader<'a> {
object,
look,
st,
&StylePaints { gradients: &self.gradients, patterns: &self.pattern_tiles, v7_patterns: &self.v7_patterns, space },
&StylePaints {
gradients: &self.gradients,
patterns: &self.pattern_tiles,
names: &self.global_names,
v7_patterns: &self.v7_patterns,
space,
},
)
}) {
// Strokes and effects at the document's scale, as the art (a Large Canvas copy's is scaled up).
@@ -841,11 +848,12 @@ fn pattern_controls(n: &[f64]) -> Option<Affine> {
xf.as_coeffs().iter().all(|v| v.is_finite()).then_some(xf)
}
/// What a style's fills paint with: the file's gradients and pattern tiles, the patterns defined in
/// the version 7 form, and art space → the document.
/// What a style's fills paint with: the file's gradients and pattern tiles with the names they get
/// in the document, the patterns defined in the version 7 form, and art space → the document.
struct StylePaints<'a> {
gradients: &'a HashMap<String, Gradient>,
patterns: &'a HashMap<String, Rect>,
names: &'a ShownNames,
v7_patterns: &'a HashSet<String>,
space: Affine,
}
@@ -915,7 +923,8 @@ fn styled(object: &Node, look: &Node, st: &crate::import::ai::style::ArtStyle, p
let mut layer = match item {
StyleItem::Fill(paint) => AppearanceItem::Fill(FillLayer::new(paint.clone())),
StyleItem::Gradient { name, angle } => {
let paint = match (name.as_ref().and_then(|n| placed.get(n)), gradients, own_gradient) {
// The drawn look's gradients are keyed by the names they got in the document.
let paint = match (name.as_ref().and_then(|n| placed.get(paints.names.get(Global::Gradient, n))), gradients, own_gradient) {
// Other objects than type: the object's own, as they always were.
(_, 1, Some(own)) if !slot => own.paint.clone(),
(Some(p), ..) => p.clone(),
@@ -930,7 +939,7 @@ fn styled(object: &Node, look: &Node, st: &crate::import::ai::style::ArtStyle, p
};
AppearanceItem::Fill(FillLayer::new(paint))
}
StyleItem::Pattern { name, matrix, controls } => match placed.get(&format!("{PATTERN_KEY}{name}")) {
StyleItem::Pattern { name, matrix, controls } => match placed.get(&format!("{PATTERN_KEY}{}", paints.names.get(Global::Pattern, name))) {
Some(p) => AppearanceItem::Fill(FillLayer::new(p.clone())),
None => {
let tile = paints.patterns.get(name).copied()?;
@@ -939,7 +948,7 @@ fn styled(object: &Node, look: &Node, st: &crate::import::ai::style::ArtStyle, p
matrix = Some(matrix.unwrap_or(Affine::IDENTITY) * pattern_controls(controls)?);
}
let xf = pattern_xf(paints.space, matrix, tile)?;
AppearanceItem::Fill(FillLayer::new(Paint::Pattern { pattern: name.clone(), xf }))
AppearanceItem::Fill(FillLayer::new(Paint::Pattern { pattern: paints.names.get(Global::Pattern, name).to_string(), xf }))
}
},
StyleItem::Stroke { .. } => stroke_layer(item)?,
+165
View File
@@ -605,6 +605,36 @@ fn a_gradient_stop_naming_a_process_colour_leaves_its_swatch_process() {
assert!(near(sw.paint.color()), "{sw:?}");
}
/// A gradient whose name in the file is shown as the name of a color a later gradient's stop names
/// is numbered: the color keeps its swatch.
#[test]
fn a_gradient_shown_as_a_stops_color_name_is_numbered() {
let first = "%AI5_BeginGradient: (Test Cyan\\n1)\n(Test Cyan\\n1) 0 2 Bd\n[\n%_1 0 50 0 Bs\n%_0 0 50 100 Bs\nBD\n%AI5_EndGradient\n";
let brand = "%AI5_BeginGradient: (Brand)\n(Brand) 0 2 Bd\n[\n\
%_0.93 0.1 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!("Bb\n1 (Test Cyan\\n1) 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!("{first}{brand}{}", layer(&art_))).doc;
let Paint::Gradient(g) = fill(&art(&doc)[0]) else { panic!("{:?}", fill(&art(&doc)[0])) };
assert_eq!(g.swatch.as_deref(), Some("Test Cyan 2"));
let sw = doc.swatch("Test Cyan").unwrap();
assert!(matches!(sw.paint, Paint::Solid { .. }) && sw.global, "{sw:?}");
}
/// A gradient whose name has no newline keeps it when a gradient shown as that name is defined
/// first: the one shown up to its newline is numbered.
#[test]
fn a_gradient_name_without_a_newline_is_kept_whichever_comes_first() {
let gradient = |name: &str| format!("%AI5_BeginGradient: ({name})\n({name}) 0 2 Bd\n[\n%_1 0 50 0 Bs\n%_0 0 50 100 Bs\nBD\n%AI5_EndGradient\n");
let filled = |name: &str| format!("Bb\n1 ({name}) 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("Sky\\n1"), gradient("Sky"), layer(&format!("{}{}", filled("Sky\\n1"), filled("Sky"))))).doc;
let links: Vec<_> = art(&doc).iter().map(|n| if let Paint::Gradient(g) = fill(n) { g.swatch.clone() } else { None }).collect();
assert_eq!(links, [Some("Sky 2".to_string()), Some("Sky".to_string())]);
// A gradient without stops isn't kept, and its name stays free.
let empty = "%AI5_BeginGradient: (Sky)\n(Sky) 0 0 Bd\n[\nBD\n%AI5_EndGradient\n";
let doc = read_ok(&format!("{empty}{}{}", gradient("Sky\\n1"), layer(&filled("Sky\\n1")))).doc;
assert!(matches!(fill(&art(&doc)[0]), Paint::Gradient(g) if g.swatch.as_deref() == Some("Sky")));
}
/// 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`.
@@ -688,6 +718,116 @@ fn patterns_are_pattern_swatches_and_fill_with_them() {
assert!(s.warnings.is_empty(), "{:?}", s.warnings);
}
/// A pattern's name in the file is shown up to its newline (AI7 spec §4): two patterns named
/// `Dots` apart from what follows the newline are "Dots" and "Dots 2", each fill paints with its
/// own, and one defined again under its name in the file replaces the first.
#[test]
fn pattern_names_are_shown_up_to_their_newline() {
let def = |name: &str, tile: u32| DOTS.replace("(Dots)", &format!("({name})")).replace("0 0 8 8\n", &format!("0 0 {tile} {tile}\n"));
let filled = |name: &str| format!("({name}) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n{}f\n", rect_path(10.0, 20.0, 70.0, 80.0));
let body = format!(
"{}{}{}{}",
def("Dots\\n1", 8),
def("Dots\\n2", 8),
def("Dots\\n2", 9),
layer(&format!("{}{}", filled("Dots\\n2"), filled("Dots\\n1")))
);
let s = read_ok(&body);
let doc = &s.doc;
let tiles: Vec<_> = doc.patterns.iter().map(|p| (p.name.as_str(), p.tile.width())).collect();
assert_eq!(tiles, [("Dots", 8.0), ("Dots 2", 9.0)]);
for name in ["Dots", "Dots 2"] {
assert!(matches!(&doc.swatch(name).unwrap().paint, Paint::Pattern { pattern, .. } if pattern == name), "{name}");
}
let fills: Vec<_> = art(doc).iter().map(|n| if let Paint::Pattern { pattern, .. } = fill(n) { pattern } else { String::new() }).collect();
assert_eq!(fills, ["Dots 2", "Dots"]);
assert!(s.warnings.is_empty(), "{:?}", s.warnings);
// Without a palette (or with an empty one) the document keeps its default swatches, and a named
// color the art uses has its swatch: a pattern takes none of their names.
let empty = "%AI5_BeginPalette\n0 0 Pb\nPB\n%AI5_EndPalette\n";
let ink = format!("0 1 1 0 (Ink) 0 x\n{}f\n", rect_path(0.0, 0.0, 5.0, 5.0));
for palette in ["", empty] {
let body =
format!("{}{}{palette}{}", def("Black\\n1", 8), def("Ink\\n1", 8), layer(&format!("{}{}{ink}", filled("Black\\n1"), filled("Ink\\n1"))));
let doc = read_ok(&body).doc;
for (solid, pattern) in [("Black", "Black 2"), ("Ink", "Ink 2")] {
assert!(matches!(&doc.swatch(pattern).unwrap().paint, Paint::Pattern { pattern: p, .. } if p == pattern), "{palette:?}");
assert!(matches!(doc.swatch(solid).unwrap().paint, Paint::Solid { .. }), "{palette:?}");
}
let fills: Vec<_> = art(&doc).iter().map(|n| fill(n)).collect();
assert!(matches!(&fills[..2], [Paint::Pattern { pattern: a, .. }, Paint::Pattern { pattern: b, .. }] if a == "Black 2" && b == "Ink 2"));
}
// Nor the name of a custom color the program of an EPS gives, or None's.
let ps = b"%!PS-Adobe-3.0\n%%CMYKCustomColor: 0 0 1 0 (Outer Ink)\n%%EndComments\n";
let data = stream(&format!("{}{}{}", def("Outer Ink\\n1", 8), def("[None]\\n1", 8), layer(&filled("Outer Ink\\n1"))));
let doc = super::read_beside(data.as_bytes(), true, ps).unwrap().doc;
assert_eq!(doc.patterns.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["Outer Ink 2", "[None] 2"]);
assert!(matches!(doc.swatch("Outer Ink").unwrap().paint, Paint::Solid { .. }));
assert!(matches!(fill(&art(&doc)[0]), Paint::Pattern { pattern, .. } if pattern == "Outer Ink 2"));
}
/// A pattern shown as the name of one of the document's other swatches is numbered: a swatch or a
/// color group of the palette the reader keeps (under any version's prefix, a swatch it names by
/// its values too), Registration, and a custom color of the editing data's own header. A pattern
/// whose name has no newline keeps it, whichever is defined first. The default swatches go when a
/// palette replaces them, and their names are free then; a palette only in versioned content that
/// isn't read replaces nothing.
#[test]
fn pattern_names_are_numbered_around_the_documents_swatches() {
let def = |name: &str| DOTS.replace("(Dots)", &format!("({name})"));
let palette = "%AI12_BeginPalette\n0 0 Pb\n0 0 0 0.5 k\n(Mist)\nPc\n1 (Greys) 1 Pg\n0 0 0 0.2 k\nPc\nPB\n%AI12_EndPalette\n";
let header = "%%CMYKCustomColor: 0 0 1 0 (Head Ink)\n";
let doc = |defs: &[&str], palette: &str| {
let body = format!("{}{palette}{}", defs.iter().map(|n| def(n)).collect::<String>(), layer(""));
read(stream_with_header(header, &body).as_bytes()).unwrap().doc
};
let names = |d: &Document| d.patterns.iter().map(|p| p.name.clone()).collect::<Vec<_>>();
let d = doc(&["Mist\\n1", "Greys\\n1", "[Registration]\\n1", "Head Ink\\n1", "Red\\n1", "Sky\\n1", "Sky"], palette);
let generated = d.swatch_groups[0].swatches[0].name.clone();
let all: Vec<&str> = d.swatches_iter().map(|s| s.name.as_str()).collect();
assert!(d.swatch("Mist").is_some() && d.swatch("Head Ink").is_some(), "{all:?}");
assert!(matches!(&d.swatch("Red").unwrap().paint, Paint::Pattern { pattern, .. } if pattern == "Red"), "{all:?}");
assert_eq!(names(&d), ["Mist 2", "Greys 2", "[Registration] 2", "Head Ink 2", "Red", "Sky 2", "Sky"]);
let d = doc(&[&format!("{generated}\\n1")], palette);
assert_eq!(names(&d), [format!("{generated} 2")]);
// Without a palette the default swatches stay, and so they do when the only palette is in
// versioned content that isn't read.
let versioned = format!("%AI17_Begin_Content_if_version_gt:17 1\n%AI17_Alternate_Content\n{palette}%AI17_End_Versioned_Content\n");
for palette in ["", &versioned] {
let d = doc(&["Red\\n1"], palette);
assert!(matches!(d.swatch("Red").unwrap().paint, Paint::Solid { .. }), "{palette:?}");
assert_eq!(names(&d), ["Red 2"], "{palette:?}");
}
// A pattern and a color in versioned content that isn't read take no name.
let unread = format!(
"%AI17_Begin_Content_if_version_gt:17 1\n%AI17_Alternate_Content\n{}{}%AI17_End_Versioned_Content\n",
def("Sky"),
layer("0 1 1 0 (Sea) 0 x\n")
);
let d = doc(&["Sky\\n1", "Sea\\n1"], &unread);
assert_eq!(names(&d), ["Sky", "Sea"]);
// Nor does a color of a palette that a later one replaces, or of a palette's alternate.
let replaced = "%AI5_BeginPalette\n0 0 Pb\n0 1 1 0 (Sky) 0 x\n(Sky)\nPc\nPB\n%AI5_EndPalette\n\
%AI5_BeginPalette\n0 0 Pb\n%AI17_Begin_Content_if_version_gt:17 1\n0 0 0 1 k\n(Ink)\nPc\n\
%AI17_Alternate_Content\n0 1 0 0 (Sea) 0 x\n(Sea)\nPc\n%AI17_End_Versioned_Content\nPB\n%AI5_EndPalette\n";
let d = doc(&["Sky\\n1", "Sea\\n1"], replaced);
assert_eq!(names(&d), ["Sky", "Sea"]);
// Versioned content that doesn't end is read whole, its colors too.
let unended = format!("%AI17_Begin_Content_if_version_gt:17 1\n%AI17_Alternate_Content\n{}", layer("0 1 1 0 (Sea) 0 x\n"));
let d = doc(&["Sea\\n1"], &unended);
assert_eq!(names(&d), ["Sea 2"]);
}
/// 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() {
let palette = "%AI5_BeginPalette\n0 0 Pb\n(Dots\\n1) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\nPc\nPB\n%AI5_EndPalette\n";
let doc = read_ok(&format!("{}{palette}{}", DOTS.replace("(Dots)", "(Dots\\n1)"), layer(""))).doc;
let names: Vec<&str> = doc.swatches.iter().map(|s| s.name.as_str()).collect();
assert_eq!(names, ["[None]", "Dots"]);
assert!(matches!(&doc.swatch("Dots").unwrap().paint, Paint::Pattern { pattern, .. } if pattern == "Dots"));
}
/// Where a definition draws its tile doesn't move the fill: the tile's lower left is the pattern's
/// origin, so the same dots drawn at 30..38 × 40..48 tile as they do at 0..8 × 0..8.
#[test]
@@ -855,6 +995,17 @@ fn a_version_7_patterns_underbar_fills_the_tile_box() {
assert!(def.art[1].appearance.stroke().is_some());
}
/// A version 7 pattern shown as the name of a custom color its own layer strings use is numbered:
/// the color keeps its swatch.
#[test]
fn a_version_7_pattern_shown_as_its_colors_name_is_numbered() {
let layers = "(0 O 0 R 0 0 0 1 \\(Ink\\) 0 x) @ (2 2 m 9 2 L 9 9 L f) &\n";
let s = pattern_read7(&pattern7("Ink\\n1", layers), &pattern_layer7("Art", ""));
assert_eq!(s.doc.patterns.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["Ink 2"]);
assert!(matches!(&s.doc.swatch("Ink 2").unwrap().paint, Paint::Pattern { pattern, .. } if pattern == "Ink 2"));
assert!(matches!(s.doc.swatch("Ink").unwrap().paint, Paint::Solid { .. }));
}
/// A layer's strings are read with their escapes undone: `\r`, `\n`, an octal tab, a backslash at
/// a line's end, and escaped parentheses around a custom color's name.
#[test]
@@ -995,6 +1146,20 @@ fn a_style_pattern_fill_is_placed_as_a_direct_one() {
}
}
/// An art style's pattern fill paints with the pattern under the name it is shown with.
#[test]
fn a_style_pattern_fill_paints_with_the_shown_name() {
let look = "0 Ae\nu\n/AI11Text :\n1 /FreeUndo ,\n0 /FrameIndex ,\n0 /StoryIndex ,\n;\nU\n";
let own = "%_/AI11Text :\n%_0 /FreeUndo ,\n%_0 /FrameIndex ,\n%_0 /StoryIndex ,\n%_;\n";
let defs = format!("{}{}", pattern7("Grid", "(0 g) @ _ &\n"), pattern7("Grid\\n1", "(0 g) @ _ &\n"));
let s = read_ok(&format!("{}{defs}{}", tiled_style("Grid\\n1", "0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0]"), layer(&format!("{look}{own}1 (Tiled) XW\n"))));
let pattern = |n: &Node| match fill(n) {
Paint::Pattern { pattern, .. } => pattern,
p => panic!("{p:?} {:?} {:?}", s.unread, s.warnings),
};
assert_eq!(pattern(&art(&s.doc)[0]), "Grid 2");
}
#[test]
fn compound_paths_and_clipping() {
let art_ = format!(
@@ -446,6 +446,47 @@ fn a_version_7_palette_keeps_its_swatches_without_names() {
assert_eq!(d.swatch("Gray K=0").unwrap().paint.color(), Some(Color::gray(0.0)), "1 g is white");
}
/// A gradient's name in the file is shown up to its newline (the AI7 spec's §4): two gradients
/// named `Sky` apart from what follows the newline come in as swatches "Sky" and "Sky 2", and each
/// object keeps its own gradient, linked to its own swatch. A palette swatch that has the name
/// "Sky" of its own keeps it, and the gradients take the next free names.
#[test]
fn gradient_names_are_shown_up_to_their_newline() {
let gradient = |name: &str, gray: f64| {
format!("%AI5_BeginGradient: ({name})\n({name}) 0 2 Bd\n[\n%_{gray} 0 50 0 Bs\n%_0 0 50 100 Bs\nBD\n%AI5_EndGradient\n")
};
let cell = |name: &str| format!("Bb\n2 ({name}) 0 0 0 1 1 0 0 1 0 0 1 Bg\n0 BB\nPc\n");
let fill = |name: &str, x: u32| format!("Bb\n1 ({name}) 0 0 0 100 1 0 0 1 0 0 Bg\n{}\n0 BB\n", tri(x));
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\n{}{}LB\n%AI5_EndLayer--\n",
fill("Sky\\n2", 10),
fill("Sky\\n1", 30)
);
let first_stop = |p: &Paint| match p {
Paint::Gradient(g) => (g.swatch.clone(), g.gradient.stops.first().map(|s| s.color)),
_ => panic!("{p:?}"),
};
for (own, first, second) in [("", "Sky", "Sky 2"), ("0 1 1 0 (Sky) 0 x\nPc\n", "Sky 2", "Sky 3")] {
let setup = format!(
"{}{}%AI5_BeginPalette\n0 0 Pb\nPn\nPc\n{own}{}{}PB\n%AI5_EndPalette\n",
gradient("Sky\\n1", 1.0),
gradient("Sky\\n2", 0.5),
cell("Sky\\n1"),
cell("Sky\\n2")
);
let r = import(&stream7(&setup, &body)).unwrap();
let d = &r.document;
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))));
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)))]);
}
}
/// A version 8 file's paths keep their notes and Show Center.
#[test]
fn a_legacy_files_paths_keep_their_notes_and_show_center() {
File diff suppressed because one or more lines are too long