mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 06:31:19 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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)?,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user