mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 11:54:03 +00:00
Merge pull request #5189 from dotCooCoo/fix/ai-os22-g1
.ai import: version 7 pattern definitions come in as pattern swatches
This commit is contained in:
@@ -3601,6 +3601,40 @@ proptest! {
|
||||
}
|
||||
}
|
||||
|
||||
/// A pattern's definition in the version 7 form (§3.4): a layer whose `_ &` fills the tile's box,
|
||||
/// and one whose tile string has CR-separated lines, comments and escapes.
|
||||
const LEGACY_PATTERN: &str = "%AI3_BeginPattern: (P)\n(P) 0 0 10 10 [\n%AI3_Tile\n(0 O 0 R 0 0 0 0.5 k) @ _ &\n\
|
||||
(0 O 1 R 0 0 0 1 \\(Ink\\) 0 X) @ (\r%AI6_BeginPatternLayer\r1 w u\r0 5 m\\r10 5 L\rS\rU\r%AI6_EndPatternLayer\r) &\n] E\n%AI3_EndPattern\n";
|
||||
|
||||
/// [`NAMED_PROLOG_AI`] with `pattern` in its setup, filling the square.
|
||||
fn legacy_pattern_ai(pattern: &str) -> String {
|
||||
NAMED_PROLOG_AI
|
||||
.replace("%AI5_Begin_NonPrinting", &format!("{pattern}%AI5_Begin_NonPrinting"))
|
||||
.replace("\n10 10 m\n", "\n(P) 5 5 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n10 10 m\n")
|
||||
.replace("10 10 L\nS\n", "10 10 L\nB\n")
|
||||
}
|
||||
|
||||
/// The fixture, as it is, opens with the pattern filling the square.
|
||||
#[test]
|
||||
fn the_legacy_pattern_fixture_opens_with_its_pattern() {
|
||||
let l = vectorcraft_engine::cmd::fileio::load("x.ai", legacy_pattern_ai(LEGACY_PATTERN).as_bytes()).unwrap();
|
||||
assert_eq!(l.doc.pattern("P").map(|p| p.art.len()), Some(2), "{:?}", l.warnings);
|
||||
let square = &l.doc.layers[0].children().unwrap()[0];
|
||||
assert!(matches!(square.appearance.fill().map(|f| &f.paint), Some(vectorcraft_color::Paint::Pattern { .. })));
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(config())]
|
||||
|
||||
/// That `.ai` with its pattern's definition damaged (its strings, `@`, `&`, `_`, the list's
|
||||
/// brackets and its `E`): read, without the pattern where it can't be read whole, or refused.
|
||||
#[test]
|
||||
fn ps_form_ai_legacy_pattern_never_panics(cut in 0usize..500, edits in prop::collection::vec((0usize..500, prop::sample::select(vec!['0', '9', '-', '.', ' ', '\n', '\r', '(', ')', '\\', '[', ']', '%', '@', '&', '_', 'E', 'u', 'U', 'q', 'Q', 'm', 'f', 'S', 'k', 'X'])), 0..12)) {
|
||||
let text = legacy_pattern_ai(&mutate_text(LEGACY_PATTERN, cut, &edits));
|
||||
survive("PostScript-form .ai with a damaged version 7 pattern", || vectorcraft_engine::cmd::fileio::load("x.ai", text.as_bytes()).ok().map(|l| l.doc))?;
|
||||
}
|
||||
}
|
||||
|
||||
/// An operand of a layer's options (`Lb`): a flag, a colour index or component, a percentage, or
|
||||
/// anything else.
|
||||
fn arb_layer_operand() -> impl Strategy<Value = String> {
|
||||
|
||||
@@ -127,6 +127,9 @@ impl Reader<'_> {
|
||||
/// `type To`: a text object begins.
|
||||
pub(super) fn begin_text(&mut self, vals: &[V], hidden: bool) {
|
||||
self.end_path_quietly();
|
||||
if self.pattern.is_some() {
|
||||
self.unreadable("type in a pattern", false);
|
||||
}
|
||||
let kind = nums::<1>(vals).map_or(0, |[k]| k as i64);
|
||||
self.text = Some(LegacyText { kind, gs: self.gs.clone(), hidden, ..LegacyText::default() });
|
||||
}
|
||||
|
||||
@@ -52,9 +52,13 @@
|
||||
//! lines and `1 (style) XW`: the drawn look is kept, named after the object.
|
||||
//! - Patterns: `%AI3_BeginPattern: (name)`, `(name) llx lly urx ury` (the tile, in the pattern's
|
||||
//! space, y up), the tile's art (on `%_` lines in newer files), `E`, `%AI3_EndPattern`; each is
|
||||
//! read by a reader of its own and becomes a pattern swatch. A fill or stroke with one:
|
||||
//! read by a reader of its own and becomes a pattern swatch. In the version 7 form (§3.4) a list
|
||||
//! of layers follows the tile, `[ (colors) @ (tile) & … ] E`, each layer's tile read from its
|
||||
//! string with the colors' state (`_ &`: the tile's box filled). A fill or stroke with one:
|
||||
//! `(name) x y scaleX scaleY angle reflect axis shear shearAxis [matrix] p` (`P`), the matrix
|
||||
//! taking the pattern's space to art space (the other operands are what it is made of).
|
||||
//! taking the pattern's space to art space (the other operands are what it is made of). With a
|
||||
//! pattern defined in the version 7 form, the other operands are applied in order (§5.4.1) before
|
||||
//! the initial matrix.
|
||||
//!
|
||||
//! - Type in the legacy format (version 7 specification §15): `type To` … `TO`, read by
|
||||
//! [`legacy_text`].
|
||||
@@ -327,6 +331,9 @@ struct Reader<'a> {
|
||||
pattern: Option<PatternHead>,
|
||||
/// The tile (y down) of each pattern read, by name.
|
||||
pattern_tiles: HashMap<String, Rect>,
|
||||
/// The patterns read from a definition in the version 7 form, whose fills apply their other
|
||||
/// operands before the matrix (see [`Reader::pattern_paint`]).
|
||||
v7_patterns: HashSet<String>,
|
||||
/// The legacy text object being read (`To` … `TO`), and the modal type attributes.
|
||||
text: Option<legacy_text::LegacyText>,
|
||||
type_state: legacy_text::TypeState,
|
||||
@@ -431,6 +438,7 @@ impl<'a> Reader<'a> {
|
||||
ruler_units: None,
|
||||
pattern: None,
|
||||
pattern_tiles: HashMap::new(),
|
||||
v7_patterns: HashSet::new(),
|
||||
text: None,
|
||||
type_state: legacy_text::TypeState::default(),
|
||||
graph: None,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::art::{four, sane, two};
|
||||
use super::types::{
|
||||
FLIP, Kind, MAX_HEX, MAX_NOTE, MAX_PATTERNS, MAX_PLACED_FOLDER, MAX_STACK, MAX_SUBREADERS, PatternHead, Place, Raster, SKIPPED, VERSIONED_UNENDED,
|
||||
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, Node, PatternDef, Reader, Rect, Tok, V, marker};
|
||||
|
||||
@@ -82,10 +83,14 @@ impl<'a> Reader<'a> {
|
||||
return Ok(());
|
||||
}
|
||||
let m = marker(line);
|
||||
// A pattern's definition (one inside another's isn't read).
|
||||
if m == "BeginPattern" && self.pattern.is_none() {
|
||||
// A pattern's definition. One inside another's isn't read, and the outer tile is then incomplete.
|
||||
if m == "BeginPattern" {
|
||||
let section = self.section("EndPattern")?;
|
||||
self.define_pattern(section);
|
||||
if self.pattern.is_some() {
|
||||
self.unsupported.insert("patterns inside patterns".into());
|
||||
} else {
|
||||
self.define_pattern(section);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if m == "BeginSymbol" {
|
||||
@@ -322,6 +327,11 @@ impl<'a> Reader<'a> {
|
||||
if self.pattern_tiles.len() >= MAX_PATTERNS || self.subreader_depth >= MAX_SUBREADERS {
|
||||
return;
|
||||
}
|
||||
match legacy_pattern(section) {
|
||||
Legacy::No => {}
|
||||
Legacy::Malformed => return,
|
||||
Legacy::Yes(def) => return self.define_legacy_pattern(&def),
|
||||
}
|
||||
let mut sub = Reader::new(section);
|
||||
sub.subreader_depth = self.subreader_depth + 1;
|
||||
sub.pattern = Some(PatternHead::default());
|
||||
@@ -339,6 +349,98 @@ impl<'a> Reader<'a> {
|
||||
&& sub.unread.is_empty()
|
||||
&& sub.hidden_unread.is_empty()
|
||||
&& sub.frames.len() == 1;
|
||||
self.keep_pattern(sub, whole, false);
|
||||
}
|
||||
|
||||
/// A definition in the version 7 form ([`legacy_pattern`]), read layer by layer into one group:
|
||||
/// each layer's colors (`@`) set the state its tile string (`&`) is read with, or the fill of the
|
||||
/// tile's box (`_ &`). Each layer starts from the state the definition starts with.
|
||||
fn define_legacy_pattern(&mut self, def: &LegacyPattern) {
|
||||
let mut sub = Reader::new(b"");
|
||||
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.nodes = self.nodes;
|
||||
sub.template = self.template;
|
||||
let mut read = sub.open(Kind::Group, false);
|
||||
for layer in &def.layers {
|
||||
if read.is_err() {
|
||||
break;
|
||||
}
|
||||
read = sub.pattern_layer(layer, def.tile);
|
||||
}
|
||||
self.nodes = sub.nodes;
|
||||
self.gradients = std::mem::take(&mut sub.gradients);
|
||||
let whole = read.is_ok()
|
||||
&& sub.unsupported.is_empty()
|
||||
&& sub.unknown.is_empty()
|
||||
&& sub.unread.is_empty()
|
||||
&& sub.hidden_unread.is_empty()
|
||||
&& sub.frames.len() == 1;
|
||||
self.keep_pattern(sub, whole, true);
|
||||
}
|
||||
|
||||
/// One layer of a definition in the version 7 form, read into the definition's group.
|
||||
fn pattern_layer(&mut self, layer: &'a LegacyLayer, tile: Rect) -> Result<(), String> {
|
||||
self.gs = GState::default();
|
||||
self.pending_fill = None;
|
||||
self.pending_stroke = None;
|
||||
self.pending_unread_fill = false;
|
||||
self.pending_unread_stroke = false;
|
||||
self.instance = None;
|
||||
self.end_path_quietly();
|
||||
// The color part has the overprint and the colors only.
|
||||
let mut lex = super::Lexer::new(&layer.color);
|
||||
while let Some(t) = lex.next_token() {
|
||||
match t.tok {
|
||||
Tok::Num(_) | Tok::Str(_) | Tok::Hex(_) => {}
|
||||
Tok::Word(w) if LAYER_COLOR_OPS.contains(&w) => {}
|
||||
_ => return Err("a pattern layer's colors hold something else".into()),
|
||||
}
|
||||
}
|
||||
self.run_string(&layer.color)?;
|
||||
match &layer.tile {
|
||||
Some(art) => self.run_string(art)?,
|
||||
None => {
|
||||
let corner = |x: f64, y: f64| [V::Num(x), V::Num(y)];
|
||||
self.path_op("m", &corner(tile.x0, tile.y0))?;
|
||||
for (x, y) in [(tile.x0, tile.y1), (tile.x1, tile.y1), (tile.x1, tile.y0)] {
|
||||
self.path_op("L", &corner(x, y))?;
|
||||
}
|
||||
self.paint("f", false, false)?;
|
||||
}
|
||||
}
|
||||
// A path the layer leaves unpainted paints nothing.
|
||||
self.end_path_quietly();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run this reader on one of a pattern layer's strings, which must end everything it starts.
|
||||
fn run_string(&mut self, bytes: &'a [u8]) -> Result<(), String> {
|
||||
let depth = self.frames.len();
|
||||
self.lex = super::Lexer::new(bytes);
|
||||
let read = self.run();
|
||||
let open = self.done
|
||||
|| self.frames.len() != depth
|
||||
|| self.stack.iter().any(|v| matches!(v, V::Mark))
|
||||
|| !self.marks.is_empty()
|
||||
|| self.text.is_some()
|
||||
|| self.raster.is_some()
|
||||
|| self.place.is_some()
|
||||
|| self.mesh.is_some()
|
||||
|| self.gradient_def.is_some();
|
||||
self.stack.clear();
|
||||
self.marks.clear();
|
||||
read?;
|
||||
if open {
|
||||
return Err("a pattern layer doesn't end what it starts".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Keep the pattern `sub` read, when it read it `whole`, as a pattern swatch; `v7` when it was
|
||||
/// defined in the version 7 form.
|
||||
fn keep_pattern(&mut self, mut sub: Reader<'_>, whole: bool, v7: bool) {
|
||||
let (Some(head), Some(frame)) = (sub.pattern.take(), sub.frames.pop()) else { return };
|
||||
let Some(tile) = head.tile.filter(|t| whole && sane(*t) && t.width() > 0.0 && t.height() > 0.0 && !frame.children.is_empty()) else {
|
||||
return;
|
||||
@@ -349,6 +451,11 @@ impl<'a> Reader<'a> {
|
||||
if self.pattern_tiles.insert(def.name.clone(), def.tile).is_some() {
|
||||
self.doc.patterns.retain(|p| p.name != def.name);
|
||||
}
|
||||
if v7 {
|
||||
self.v7_patterns.insert(def.name.clone());
|
||||
} else {
|
||||
self.v7_patterns.remove(&def.name);
|
||||
}
|
||||
self.doc.patterns.push(def);
|
||||
for (key, blob) in sub.doc.images {
|
||||
self.doc.images.entry(key).or_insert(blob);
|
||||
@@ -404,6 +511,117 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The operators a pattern layer's colors (`@`) may hold: overprint, gray, process and custom colors.
|
||||
const LAYER_COLOR_OPS: &[&[u8]] = &[b"O", b"R", b"g", b"G", b"k", b"K", b"x", b"X"];
|
||||
|
||||
/// The longest string a pattern layer may have (the format's limit, 64K bytes).
|
||||
const MAX_LAYER_STRING: usize = 65_535;
|
||||
|
||||
/// Most layers in one pattern's definition.
|
||||
const MAX_LAYERS: usize = 65_536;
|
||||
|
||||
/// A pattern's definition in the version 7 form: its head, then a list of layers painted in order,
|
||||
/// the first the lowest.
|
||||
struct LegacyPattern {
|
||||
name: String,
|
||||
/// The tile in the pattern's space (y up).
|
||||
tile: Rect,
|
||||
layers: Vec<LegacyLayer>,
|
||||
}
|
||||
|
||||
/// `(colors) @ (tile) &`, or `(colors) @ _ &` (no tile: the tile's box is filled with the colors' fill).
|
||||
struct LegacyLayer {
|
||||
color: Vec<u8>,
|
||||
tile: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
enum Legacy {
|
||||
/// Another form: the definition's art follows its head as operators.
|
||||
No,
|
||||
/// The version 7 form, which can't be read.
|
||||
Malformed,
|
||||
Yes(LegacyPattern),
|
||||
}
|
||||
|
||||
/// The next token that isn't a comment; `Err` at a pattern's definition inside this one.
|
||||
fn next_operand<'s>(lex: &mut super::Lexer<'s>) -> Result<Option<super::super::lex::Token<'s>>, ()> {
|
||||
while let Some(t) = lex.next_token() {
|
||||
match t.tok {
|
||||
Tok::Comment(c) if marker(&String::from_utf8_lossy(c)) == "BeginPattern" => return Err(()),
|
||||
Tok::Comment(_) => {}
|
||||
_ => return Ok(Some(t)),
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// A pattern's definition in the version 7 form: `(name) llx lly urx ury [`, then for each layer
|
||||
/// `(colors) @` and `(tile) &` or `_ &`, then `] E` (version 7 specification §3.4). It is that form
|
||||
/// when a `[` that isn't on a `%_` line follows the head.
|
||||
fn legacy_pattern(section: &[u8]) -> Legacy {
|
||||
let mut lex = super::Lexer::new(section);
|
||||
let mut next = || next_operand(&mut lex).ok().flatten().map(|t| (t.tok, t.hidden));
|
||||
let name = match next() {
|
||||
Some((Tok::Str(s), _)) => super::obj::text(&s),
|
||||
_ => return Legacy::No,
|
||||
};
|
||||
let mut tile = [0.0; 4];
|
||||
for v in &mut tile {
|
||||
match next() {
|
||||
Some((Tok::Num(n), _)) => *v = n,
|
||||
_ => return Legacy::No,
|
||||
}
|
||||
}
|
||||
if !matches!(next(), Some((Tok::Word(b"["), false))) {
|
||||
return Legacy::No;
|
||||
}
|
||||
let [x0, y0, x1, y1] = tile;
|
||||
let mut layers = vec![];
|
||||
let string = |t: Option<(Tok, bool)>| match t {
|
||||
Some((Tok::Str(s) | Tok::Hex(s), _)) if s.len() <= MAX_LAYER_STRING => Some(s),
|
||||
_ => None,
|
||||
};
|
||||
let word = |t: Option<(Tok, bool)>, w: &[u8]| matches!(t, Some((Tok::Word(x), _)) if x == w);
|
||||
loop {
|
||||
// Before the first layer, anything but a string or `] E` is the newer form's art (`[]0 d`).
|
||||
let first = layers.is_empty();
|
||||
let color = match next() {
|
||||
Some((Tok::Word(b"]"), _)) if !first => break,
|
||||
Some((Tok::Word(b"]"), _)) => {
|
||||
return match next() {
|
||||
Some((Tok::Word(b"E"), _)) => Legacy::Yes(LegacyPattern { name, tile: Rect::new(x0, y0, x1, y1), layers }),
|
||||
_ => Legacy::No,
|
||||
};
|
||||
}
|
||||
t @ Some((Tok::Str(_) | Tok::Hex(_), _)) => match string(t) {
|
||||
Some(s) => s,
|
||||
None => return Legacy::Malformed,
|
||||
},
|
||||
_ if first => return Legacy::No,
|
||||
_ => return Legacy::Malformed,
|
||||
};
|
||||
if !word(next(), b"@") {
|
||||
return Legacy::Malformed;
|
||||
}
|
||||
let tile = match next() {
|
||||
Some((Tok::Word(b"_"), _)) => None,
|
||||
t => match string(t) {
|
||||
Some(s) => Some(s),
|
||||
None => return Legacy::Malformed,
|
||||
},
|
||||
};
|
||||
if !word(next(), b"&") || layers.len() >= MAX_LAYERS {
|
||||
return Legacy::Malformed;
|
||||
}
|
||||
layers.push(LegacyLayer { color, tile });
|
||||
}
|
||||
// `] E` ends the definition: nothing but comments may follow it.
|
||||
if !word(next(), b"E") || !matches!(next_operand(&mut lex), Ok(None)) {
|
||||
return Legacy::Malformed;
|
||||
}
|
||||
Legacy::Yes(LegacyPattern { name, tile: Rect::new(x0, y0, x1, y1), layers })
|
||||
}
|
||||
|
||||
/// How many custom colors the header may define.
|
||||
pub(super) const MAX_CUSTOM_COLORS: usize = 1000;
|
||||
|
||||
|
||||
@@ -402,7 +402,8 @@ impl<'a> Reader<'a> {
|
||||
|
||||
/// `(name) x y scaleX scaleY angle reflect axis shear shearAxis [matrix] p` (`P`: the stroke's):
|
||||
/// the pattern swatch `name`, its space taken to art space by the matrix. A pattern this hasn't
|
||||
/// read can't be drawn.
|
||||
/// read can't be drawn. For a pattern defined in the version 7 form, the other operands are
|
||||
/// applied in order before the initial matrix ([`pattern_controls`]).
|
||||
fn pattern_paint(&mut self, w: &str, vals: &[V]) {
|
||||
// In the setup it paints nothing (and the art's space isn't fixed yet).
|
||||
if self.frames.is_empty() {
|
||||
@@ -413,7 +414,16 @@ impl<'a> Reader<'a> {
|
||||
self.unread_pattern(w);
|
||||
return;
|
||||
};
|
||||
let matrix = vals.iter().rev().find_map(|v| if let V::Arr(a) = v { paint::affine(&Self::nums(a)) } else { None });
|
||||
let mut matrix = vals.iter().rev().find_map(|v| if let V::Arr(a) = v { paint::affine(&Self::nums(a)) } else { None });
|
||||
if name.as_ref().is_some_and(|n| self.v7_patterns.contains(n)) {
|
||||
let controls: Vec<f64> =
|
||||
vals.iter().skip_while(|v| !matches!(v, V::Str(_))).skip(1).take_while(|v| !matches!(v, V::Arr(_))).filter_map(V::num).collect();
|
||||
let Some(controls) = pattern_controls(&controls) else {
|
||||
self.unread_pattern(w);
|
||||
return;
|
||||
};
|
||||
matrix = Some(matrix.unwrap_or(Affine::IDENTITY) * controls);
|
||||
}
|
||||
let Some(xf) = pattern_xf(self.space(), matrix, tile) else {
|
||||
self.unread_pattern(w);
|
||||
return;
|
||||
@@ -596,11 +606,14 @@ impl<'a> Reader<'a> {
|
||||
return;
|
||||
}
|
||||
// A style read here: the object, painted as it says.
|
||||
if let Some(mut appearance) = self
|
||||
.styles
|
||||
.get(&style)
|
||||
.and_then(|st| styled(object, look, st, &StylePaints { gradients: &self.gradients, patterns: &self.pattern_tiles, space }))
|
||||
{
|
||||
if let Some(mut appearance) = self.styles.get(&style).and_then(|st| {
|
||||
styled(
|
||||
object,
|
||||
look,
|
||||
st,
|
||||
&StylePaints { gradients: &self.gradients, patterns: &self.pattern_tiles, 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).
|
||||
let k = space.determinant().abs().sqrt();
|
||||
if k.is_finite() && k > 0.0 && (k - 1.0).abs() > 1e-9 {
|
||||
@@ -694,10 +707,33 @@ fn pattern_xf(space: Affine, matrix: Option<Affine>, tile: Rect) -> Option<Affin
|
||||
(xf.determinant().abs() >= 1e-12 && 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, and art space → the document.
|
||||
/// A pattern fill's `x y scaleX scaleY angle reflect axis shear shearAxis` as one transform of the
|
||||
/// pattern's space, applied before its initial matrix: the shear (by `shear` degrees along the line
|
||||
/// at `shearAxis` degrees), the reflection about the line at `axis` degrees when `reflect` isn't
|
||||
/// 0, the counterclockwise rotation by `angle` degrees, the scale, then the move to `x y`. `None`
|
||||
/// unless there are nine finite numbers.
|
||||
fn pattern_controls(n: &[f64]) -> Option<Affine> {
|
||||
let &[x, y, sx, sy, angle, reflect, axis, shear, shear_axis] = n else { return None };
|
||||
if !n.iter().all(|v| v.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let turn = |deg: f64| Affine::rotate(deg.to_radians());
|
||||
let reflection = if reflect != 0.0 { turn(axis) * Affine::scale_non_uniform(1.0, -1.0) * turn(-axis) } else { Affine::IDENTITY };
|
||||
let shearing = if shear != 0.0 {
|
||||
turn(shear_axis) * Affine::new([1.0, 0.0, shear.to_radians().tan(), 1.0, 0.0, 0.0]) * turn(-shear_axis)
|
||||
} else {
|
||||
Affine::IDENTITY
|
||||
};
|
||||
let xf = Affine::translate((x, y)) * Affine::scale_non_uniform(sx, sy) * turn(angle) * reflection * shearing;
|
||||
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.
|
||||
struct StylePaints<'a> {
|
||||
gradients: &'a HashMap<String, Gradient>,
|
||||
patterns: &'a HashMap<String, Rect>,
|
||||
v7_patterns: &'a HashSet<String>,
|
||||
space: Affine,
|
||||
}
|
||||
|
||||
@@ -778,11 +814,15 @@ fn styled(object: &Node, look: &Node, st: &crate::import::ai::style::ArtStyle, p
|
||||
};
|
||||
AppearanceItem::Fill(FillLayer::new(paint))
|
||||
}
|
||||
StyleItem::Pattern { name, matrix } => match placed.get(&format!("{PATTERN_KEY}{name}")) {
|
||||
StyleItem::Pattern { name, matrix, controls } => match placed.get(&format!("{PATTERN_KEY}{name}")) {
|
||||
Some(p) => AppearanceItem::Fill(FillLayer::new(p.clone())),
|
||||
None => {
|
||||
let tile = paints.patterns.get(name).copied()?;
|
||||
let xf = pattern_xf(paints.space, matrix.and_then(|m| paint::affine(&m)), tile)?;
|
||||
let mut matrix = matrix.and_then(|m| paint::affine(&m));
|
||||
if paints.v7_patterns.contains(name) {
|
||||
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 }))
|
||||
}
|
||||
},
|
||||
|
||||
@@ -133,10 +133,12 @@ pub(super) enum StyleItem {
|
||||
name: Option<String>,
|
||||
angle: f64,
|
||||
},
|
||||
/// A fill with pattern swatch `name`, its space taken to art space by `matrix`.
|
||||
/// A fill with pattern swatch `name`, its space taken to art space by `matrix`; `controls`, the
|
||||
/// numbers before the matrix, apply first when the pattern was defined in the version 7 form.
|
||||
Pattern {
|
||||
name: String,
|
||||
matrix: Option<[f64; 6]>,
|
||||
controls: Vec<f64>,
|
||||
},
|
||||
Stroke {
|
||||
paint: Paint,
|
||||
@@ -199,8 +201,8 @@ struct Filter {
|
||||
gradient: bool,
|
||||
/// The gradient's swatch and angle (`Bg`).
|
||||
gradient_of: Option<(Option<String>, f64)>,
|
||||
/// `(name) … [matrix] p`.
|
||||
pattern: Option<(String, Option<[f64; 6]>)>,
|
||||
/// `(name) … [matrix] p`, and the numbers between the name and the matrix.
|
||||
pattern: Option<(String, Option<[f64; 6]>, Vec<f64>)>,
|
||||
/// `mode opacity … Xy`.
|
||||
xy: Option<(usize, f32)>,
|
||||
/// `… isolate knockout … Xy`.
|
||||
@@ -518,8 +520,10 @@ fn paint_op(f: &mut Filter, op: &str, vals: &[V]) {
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let controls: Vec<f64> =
|
||||
vals.iter().skip_while(|v| !matches!(v, V::Str(_))).skip(1).take_while(|v| !matches!(v, V::Arr(_))).filter_map(V::num).collect();
|
||||
if let Some(name) = name {
|
||||
f.pattern = Some((name, matrix));
|
||||
f.pattern = Some((name, matrix, controls));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -998,7 +1002,7 @@ fn paint_item(f: &Filter) -> Result<Option<StyleItem>, ()> {
|
||||
match f.name.as_str() {
|
||||
FILL if !f.paints.1 => Ok(None),
|
||||
FILL => match (&f.pattern, &f.gradient_of, &f.fill) {
|
||||
(Some((name, matrix)), ..) => Ok(Some(StyleItem::Pattern { name: name.clone(), matrix: *matrix })),
|
||||
(Some((name, matrix, controls)), ..) => Ok(Some(StyleItem::Pattern { name: name.clone(), matrix: *matrix, controls: controls.clone() })),
|
||||
(None, Some((name, angle)), _) => Ok(Some(StyleItem::Gradient { name: name.clone(), angle: *angle })),
|
||||
(None, None, _) if f.gradient => Ok(Some(StyleItem::Gradient { name: None, angle: 0.0 })),
|
||||
(None, None, Some(p)) => Ok(Some(StyleItem::Fill(p.clone()))),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use kurbo::{Point, Rect};
|
||||
use kurbo::{Affine, Point, Rect};
|
||||
use vectorcraft_color::gradient::GradientKind;
|
||||
use vectorcraft_color::{BlendMode, Color, Paint};
|
||||
use vectorcraft_doc::{AppearanceItem, ColorMode, Document, Guide, Knockout, LayerColor, LineCap, LineJoin, Node, NodeKind, Unit};
|
||||
@@ -525,7 +525,7 @@ fn a_gradient_in_a_patterns_definition_is_placed_from_the_canvas() {
|
||||
/// has that isn't read, where it shows. Its definition costs nothing else.
|
||||
#[test]
|
||||
fn a_pattern_it_cant_read_is_an_error_only_where_it_fills() {
|
||||
let unread = "%AI3_BeginPattern: (Old)\n(Old) 0 0 8 8 [\n(0 O 0 R 0 g) @\n(\n0 0 m 8 8 L S\n) &\n] E\n%AI3_EndPattern\n\
|
||||
let unread = "%AI3_BeginPattern: (Old)\n(Old) 0 0 8 8 [\n(0 O 0 R 0 g) @\n(\n0 0 m 8 8 L frobnicate S\n) &\n] E\n%AI3_EndPattern\n\
|
||||
%AI3_BeginPattern: (Odd)\n(Odd) 0 0 8 8\n%_U\nE\n%AI3_EndPattern\n%AI3_BeginPattern: (Flat)\n(Flat) 0 0 8 0\n%_0 0 m\n%_8 0 L\n%_S\nE\n%AI3_EndPattern\n";
|
||||
let s = read_ok(&format!("{unread}{}", layer(&(rect_path(0.0, 0.0, 1.0, 1.0) + "f\n"))));
|
||||
assert!(s.doc.patterns.is_empty() && s.doc.swatches.iter().all(|w| !matches!(w.paint, Paint::Pattern { .. })));
|
||||
@@ -565,6 +565,231 @@ fn a_pattern_painting_with_an_unread_pattern_is_not_kept() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A pattern's definition inside another's isn't read, so the outer one isn't kept either.
|
||||
#[test]
|
||||
fn a_pattern_with_a_definition_inside_it_isnt_kept() {
|
||||
let tile = rect_path(0.0, 0.0, 8.0, 8.0).replace('\n', "\n%_");
|
||||
let inner = "%AI3_BeginPattern: (In)\n(In) 0 0 1 1\n%_0 0 m\n%_1 1 L\n%_S\nE\n%AI3_EndPattern\n";
|
||||
let def = format!("%AI3_BeginPattern: (Out)\n(Out) 0 0 8 8\n{inner}%_{tile}f\nE\n%AI3_EndPattern\n");
|
||||
let s = read_ok(&format!("{def}{}", layer("")));
|
||||
assert!(s.doc.patterns.is_empty(), "{:?}", s.doc.patterns.iter().map(|p| &p.name).collect::<Vec<_>>());
|
||||
// Without the definition inside it, it is kept.
|
||||
let s = read_ok(&format!("{}{}", def.replace(inner, ""), layer("")));
|
||||
assert!(s.doc.pattern("Out").is_some());
|
||||
}
|
||||
|
||||
/// A definition whose art follows its head as operators is still read when that art starts with a
|
||||
/// dash array (`[]0 d`) off `%_` lines, which isn't a version 7 layer list.
|
||||
#[test]
|
||||
fn a_pattern_whose_art_starts_with_a_dash_array_is_read() {
|
||||
let def = format!("%AI3_BeginPattern: (Dash)\n(Dash) 0 0 8 8 []0 d\n{}f\nE\n%AI3_EndPattern\n", rect_path(0.0, 0.0, 8.0, 8.0));
|
||||
let s = read_ok(&format!("{def}{}", layer("")));
|
||||
assert_eq!(s.doc.pattern("Dash").map(|p| p.art.len()), Some(1));
|
||||
}
|
||||
|
||||
/// Editing data in the version 7 form: `%AI5_FileFormat 2.0`, the art size around the template
|
||||
/// box's center (an artboard 200 by 100 from the origin), `setup` (pattern definitions), then `body`.
|
||||
fn pattern_stream7(setup: &str, body: &str) -> String {
|
||||
format!(
|
||||
"%!PS-Adobe-3.0\n%%Creator: test\n%AI5_FileFormat 2.0\n%AI5_ArtSize: 200 100\n%AI3_TemplateBox: 100 50 100 50\n%%EndComments\n%%BeginProlog\n%%EndProlog\n%%BeginSetup\n{setup}%%EndSetup\n{body}\n%%PageTrailer\ngsave annotatepage grestore showpage\n%%Trailer\n%%EOF\n"
|
||||
)
|
||||
}
|
||||
|
||||
/// One layer named `name` with the version 7 form's ten operands before `Lb`, holding `art`.
|
||||
fn pattern_layer7(name: &str, art: &str) -> String {
|
||||
format!("%AI5_BeginLayer\n1 1 1 1 0 0 0 79 128 255 Lb\n({name}) Ln\n0 A\n{art}LB\n%AI5_EndLayer--\n")
|
||||
}
|
||||
|
||||
/// A definition of `name` in the version 7 form: its 20-point tile, then `layers`, the layer list.
|
||||
fn pattern7(name: &str, layers: &str) -> String {
|
||||
format!("%AI3_BeginPattern: ({name})\n({name}) 0 0 20 20 [\n%AI3_Tile\n{layers}] E\n%AI3_EndPattern\n")
|
||||
}
|
||||
|
||||
fn pattern_read7(setup: &str, body: &str) -> Structure {
|
||||
match read(pattern_stream7(setup, body).as_bytes()) {
|
||||
Ok(s) => s,
|
||||
Err(e) => panic!("{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A definition in the version 7 form (§3.4): each layer's colors (`@`) paint its tile's art (`&`),
|
||||
/// the first layer the lowest. The operator lines in the strings end in CR, and each layer starts
|
||||
/// from the definition's state, so the second layer's stroke doesn't take the first one's width.
|
||||
#[test]
|
||||
fn version_7_pattern_layers_paint_in_order_with_their_own_colors() {
|
||||
let layers = "(0 O 0 R 1 0 0 0 k\r1 0 0 0 K) @\n(\r%AI6_BeginPatternLayer\r0 J 0 j 3 w 4 M []0 d\r2 2 m\r9 2 L\r9 9 L\r2 9 L\rf\r%AI6_EndPatternLayer\r) &\n\
|
||||
(0 O 1 R 0 0 0 1 k\r0 0 0 1 K) @\n(\r2 12 m\r18 12 L\rS\r) &\n";
|
||||
let fill_with = format!("(Grid) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n{}f\n", rect_path(10.0, 10.0, 90.0, 90.0));
|
||||
let s = pattern_read7(&pattern7("Grid", layers), &pattern_layer7("Art", &fill_with));
|
||||
assert!(s.unread.is_empty() && s.warnings.is_empty(), "{:?} {:?}", s.unread, s.warnings);
|
||||
let def = s.doc.pattern("Grid").unwrap();
|
||||
assert_eq!(def.tile, Rect::new(0.0, -20.0, 20.0, 0.0), "the tile, y down");
|
||||
assert_eq!(def.art.len(), 2, "one path for each layer");
|
||||
let (square, line) = (&def.art[0], &def.art[1]);
|
||||
assert_eq!(square.geometric_bounds(), Some(Rect::new(2.0, -9.0, 9.0, -2.0)));
|
||||
assert_eq!(fill(square).color(), Some(Color::cmyk(1.0, 0.0, 0.0, 0.0)));
|
||||
assert!(square.appearance.stroke().is_none());
|
||||
assert!(line.appearance.fill().is_none());
|
||||
let stroke = line.appearance.stroke().unwrap();
|
||||
assert_eq!(stroke.paint.color(), Some(Color::cmyk(0.0, 0.0, 0.0, 1.0)));
|
||||
assert_eq!(stroke.width, 1.0, "the first layer's width stays with it");
|
||||
assert!(stroke.overprint);
|
||||
assert!(matches!(fill(&art(&s.doc)[0]), Paint::Pattern { pattern, .. } if pattern == "Grid"));
|
||||
}
|
||||
|
||||
/// `_ &` fills the tile's box with the layer's fill color, without a stroke (§3.4).
|
||||
#[test]
|
||||
fn a_version_7_patterns_underbar_fills_the_tile_box() {
|
||||
let layers = "(0 O 0 R 0 1 0 0 k 0 0 0 1 K) @ _ &\n(0 0 0 1 k 0 0 0 1 K) @ (2 2 m 18 18 L S) &\n";
|
||||
let s = pattern_read7(&pattern7("Back", layers), &pattern_layer7("Art", ""));
|
||||
let def = s.doc.pattern("Back").unwrap();
|
||||
let back = &def.art[0];
|
||||
assert_eq!(back.geometric_bounds(), Some(def.tile));
|
||||
assert_eq!(fill(back).color(), Some(Color::cmyk(0.0, 1.0, 0.0, 0.0)));
|
||||
assert!(back.appearance.stroke().is_none());
|
||||
assert!(def.art[1].appearance.stroke().is_some());
|
||||
}
|
||||
|
||||
/// 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]
|
||||
fn a_version_7_patterns_strings_are_read_with_their_escapes_undone() {
|
||||
let layers = "(0 O 0 R 0 0 0 1 \\(Ink \\(A\\)\\) 0 x) @\n(2 2 m\\r9 2 L\\n9 9 L\\0112 9 \\\nL f) &\n";
|
||||
let s = pattern_read7(&pattern7("Ink", layers), &pattern_layer7("Art", ""));
|
||||
let def = s.doc.pattern("Ink").unwrap();
|
||||
assert_eq!(def.art.len(), 1);
|
||||
assert_eq!(def.art[0].geometric_bounds(), Some(Rect::new(2.0, -9.0, 9.0, -2.0)));
|
||||
assert!(s.doc.swatch("Ink (A)").is_some(), "{:?}", s.doc.swatches.iter().map(|w| &w.name).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
/// A definition in the version 7 form that can't be read whole isn't kept, and a fill with it is
|
||||
/// something the file has that isn't read: an operator it doesn't know, type, a definition inside
|
||||
/// it, colors with other operators, a layer without its tile or its `&`, a string longer than the
|
||||
/// format allows, an `E`, a group or an array a layer doesn't end, more than 65,536 layers, a list
|
||||
/// without its `]` or its `E`, anything after its `E`.
|
||||
#[test]
|
||||
fn a_version_7_pattern_it_cant_read_whole_isnt_kept() {
|
||||
let line = "(0 0 m 8 8 L S) &\n";
|
||||
let long = format!("(0 g) @ ({}0 0 m 8 8 L S) &\n", " ".repeat(65_536));
|
||||
let open = "%AI3_BeginPattern: (P)\n(P) 0 0 20 20 [\n(0 g) @ (0 0 m 8 8 L S) &\n";
|
||||
let defs = [
|
||||
pattern7("P", "(0 g) @ (0 0 m 8 8 L frobnicate S) &\n"),
|
||||
pattern7("P", "(0 g) @ (0 To 1 0 0 1 2 2 0 Tp TP (a) Tx TO) &\n"),
|
||||
pattern7("P", "(0 g) @ (\r%AI3_BeginPattern: (In)\r(In) 0 0 1 1\rE\r%AI3_EndPattern\r0 0 m 8 8 L S) &\n"),
|
||||
pattern7("P", &format!("%AI3_BeginPattern: (In)\n(In) 0 0 1 1 [\n] E\n%AI3_EndPattern\n(0 g) @ {line}")),
|
||||
pattern7("P", &format!("(0 g 0 0 m) @ {line}")),
|
||||
pattern7("P", "(0 g) @\n"),
|
||||
pattern7("P", "(0 g) @ _\n"),
|
||||
pattern7("P", &long),
|
||||
pattern7("P", "(0 g) @ (0 0 m 8 8 L S E) &\n"),
|
||||
pattern7("P", "(0 g) @ (u 0 0 m 8 8 L S) &\n"),
|
||||
pattern7("P", "(0 g) @ (0 0 m 8 8 L S [) &\n"),
|
||||
pattern7("P", "(0 g) @ ([ 0 0 m 8 8 L S) &\n"),
|
||||
pattern7("P", &"(0 g) @ _ &\n".repeat(65_537)),
|
||||
format!("{open}E\n%AI3_EndPattern\n"),
|
||||
format!("{open}]\n%AI3_EndPattern\n"),
|
||||
format!("{open}] E\n0 0 m 8 8 L S\n%AI3_EndPattern\n"),
|
||||
];
|
||||
let fill_with = format!("(P) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n{}f\n", rect_path(0.0, 0.0, 1.0, 1.0));
|
||||
for (i, def) in defs.iter().enumerate() {
|
||||
assert!(pattern_read7(def, &pattern_layer7("Art", "")).doc.patterns.is_empty(), "case {i}");
|
||||
assert_unread(&pattern_read7(def, &pattern_layer7("Art", &fill_with)), "pattern fills");
|
||||
}
|
||||
// The same definition, whole, is kept.
|
||||
let s = pattern_read7(&pattern7("P", &format!("(0 g) @ {line}")), &pattern_layer7("Art", &fill_with));
|
||||
assert!(s.doc.pattern("P").is_some() && s.unread.is_empty());
|
||||
}
|
||||
|
||||
/// With a pattern defined in the version 7 form, a fill's other operands are applied in order
|
||||
/// before its initial matrix (§5.4.1): the shear along its axis, the reflection about the line at
|
||||
/// its angle, the rotation, the scale, then the offset. Identity ones move the tiles by the offset
|
||||
/// alone. `P` places a stroke's tiles the same way.
|
||||
#[test]
|
||||
fn a_pattern_fill_applies_its_offset_scale_rotation_reflection_and_shear() {
|
||||
let def = pattern7("Grid", "(0 g) @ _ &\n");
|
||||
// The operands, and the pattern's space → art space they make (a b c d e f).
|
||||
let cases = [
|
||||
("10 15 1 1 0 0 0 0 0 [1 0 0 1 0 0]", [1.0, 0.0, 0.0, 1.0, 10.0, 15.0]),
|
||||
("25 30 1.5 .75 30 1 45 10 0 [1 0 0 1 0 0]", [-0.75, 0.649519, 1.166793, 0.489528, 25.0, 30.0]),
|
||||
("-5 7 2 .5 -45 0 0 -15 45 [1 0 0 1 0 0]", [1.793151, -0.353553, 1.035276, 0.353553, -5.0, 7.0]),
|
||||
("3 -4 1.2 .8 20 1 30 12 25 [.8 .1 -.2 1.1 4 -6]", [-0.028557, 0.816531, 1.054026, 0.117492, 7.2, -10.1]),
|
||||
];
|
||||
// Art space → the document (the artboard is 100 high), and a tile's art (y down from its top
|
||||
// left) → the pattern's space.
|
||||
let space = Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 100.0]);
|
||||
let tile = Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 20.0]);
|
||||
for (ops, want) in cases {
|
||||
for (op, paint) in [("p", "f"), ("P", "S")] {
|
||||
let s = pattern_read7(&def, &pattern_layer7("Art", &format!("(Grid) {ops} {op}\n{}{paint}\n", rect_path(10.0, 10.0, 90.0, 90.0))));
|
||||
let node = &art(&s.doc)[0];
|
||||
let p = if op == "p" { fill(node) } else { node.appearance.stroke().unwrap().paint.clone() };
|
||||
let Paint::Pattern { xf, .. } = p else { panic!("{ops} {op}: {p:?}") };
|
||||
let got = (space.inverse() * xf * tile.inverse()).as_coeffs();
|
||||
assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() < 1e-6), "{ops} {op}: {got:?}");
|
||||
}
|
||||
}
|
||||
// The tiles' corners: with identity operands but the offset, the tile's top left (0, 20) is at
|
||||
// (10, 35) in art space; with the last ones, its lower right (20, 0) is at (6.628860, 6.230623).
|
||||
let placed = |ops: &str| {
|
||||
let s = pattern_read7(&def, &pattern_layer7("Art", &format!("(Grid) {ops} p\n{}f\n", rect_path(10.0, 10.0, 90.0, 90.0))));
|
||||
let Paint::Pattern { xf, .. } = fill(&art(&s.doc)[0]) else { panic!() };
|
||||
xf
|
||||
};
|
||||
assert_eq!(placed(cases[0].0) * Point::ZERO, Point::new(10.0, 65.0));
|
||||
assert!((placed(cases[3].0) * Point::new(20.0, 20.0)).distance(Point::new(6.628860, 93.769377)) < 1e-5);
|
||||
// Without its nine operands, a fill with the pattern isn't read.
|
||||
let r = pattern_read7(&def, &pattern_layer7("Art", &format!("(Grid) 0 0 1 1 0 0 0 0 [1 0 0 1 0 0] p\n{}f\n", rect_path(10.0, 10.0, 90.0, 90.0))));
|
||||
assert_unread(&r, "pattern fills");
|
||||
}
|
||||
|
||||
/// With a pattern defined in the newer form (art on `%_` lines), the matrix alone places the tiles
|
||||
/// (#1025): the other operands don't move them, and a fill without all nine is still read.
|
||||
#[test]
|
||||
fn a_newer_pattern_fill_is_placed_by_its_matrix_alone() {
|
||||
let placed = |ops: &str| {
|
||||
let s = read_ok(&format!("{DOTS}{}", layer(&format!("(Dots) {ops} p\n{}f\n", rect_path(10.0, 20.0, 70.0, 80.0)))));
|
||||
assert!(s.unread.is_empty() && s.warnings.is_empty(), "{ops}: {:?} {:?}", s.unread, s.warnings);
|
||||
let Paint::Pattern { xf, .. } = fill(&art(&s.doc)[0]) else { panic!("{ops}: {:?}", fill(&art(&s.doc)[0])) };
|
||||
xf
|
||||
};
|
||||
let plain = placed("0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0]");
|
||||
assert_eq!(plain * Point::ZERO, Point::new(0.0, 92.0));
|
||||
assert_eq!(placed("25 30 1.5 .75 30 1 45 10 0 [1 0 0 1 0 0]"), plain);
|
||||
assert_eq!(placed("0 0 1 1 0 0 0 0 [1 0 0 1 0 0]"), plain);
|
||||
}
|
||||
|
||||
/// An art style `Tiled` whose fill is `(name) {ops} p`.
|
||||
fn tiled_style(name: &str, ops: &str) -> String {
|
||||
format!(
|
||||
"%AI9_BeginArtStyles\n/KnownStyle :\n(Tiled) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
|
||||
/CompoundFilter :\n(Stack Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
|
||||
/BasicFilter :\n(Fill Style Filter) 0 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n/FillStyle : 0 O\n({name}) {ops} p\n0 1 Xd\n/Def ;\n (FillStyle) ,\n; /Dict ;\n /Part ,\n\
|
||||
/BasicFilter :\n(Conduit Filter) 0 0 /Filter ,\n1 /Visible ,\n;\n /Part ,\n;\n /Part ,\n;\n%AI9_EndArtStyles\n"
|
||||
)
|
||||
}
|
||||
|
||||
/// A fill set through an art style on a text object places the tiles as a `p` fill with the same
|
||||
/// operands does: with the other operands before the matrix for a pattern defined in the version 7
|
||||
/// form, by the matrix alone for one defined in the newer form.
|
||||
#[test]
|
||||
fn a_style_pattern_fill_is_placed_as_a_direct_one() {
|
||||
let rect = rect_path(10.0, 20.0, 70.0, 80.0);
|
||||
// The text object's drawn look (without a pattern to take the placement from), then the object.
|
||||
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 ops = "25 30 1.5 .75 30 1 45 10 0 [.8 .1 -.2 1.1 4 -6]";
|
||||
for (name, def) in [("Grid", pattern7("Grid", "(0 g) @ _ &\n")), ("Dots", DOTS.to_string())] {
|
||||
let styled = read_ok(&format!("{}{def}{}", tiled_style(name, ops), layer(&format!("{look}{own}1 (Tiled) XW\n"))));
|
||||
let direct = read_ok(&format!("{def}{}", layer(&format!("({name}) {ops} p\n{rect}f\n"))));
|
||||
let xf = |s: &Structure| match fill(&art(&s.doc)[0]) {
|
||||
Paint::Pattern { xf, .. } => xf,
|
||||
p => panic!("{name}: {p:?} {:?} {:?}", s.unread, s.warnings),
|
||||
};
|
||||
let (got, want) = (xf(&styled).as_coeffs(), xf(&direct).as_coeffs());
|
||||
assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() < 1e-9), "{name}: {got:?} {want:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_paths_and_clipping() {
|
||||
let art_ = format!(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use kurbo::Point;
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::clipnest::MAX_NEST;
|
||||
use vectorcraft_doc::{LayerColor, LineCap, Node, NodeKind};
|
||||
@@ -123,6 +124,31 @@ fn a_postscript_ai_that_names_its_prolog_without_including_it_opens_with_its_lay
|
||||
assert_eq!(tree(d.layers[1].children().unwrap()), "p");
|
||||
}
|
||||
|
||||
/// A `.ai` in PostScript form with a pattern defined in the version 7 form (§3.4): the layer keeps
|
||||
/// its name, and the pattern is a swatch that fills the path from its offset.
|
||||
#[test]
|
||||
fn a_postscript_ai_with_a_version_7_pattern_fills_with_it() {
|
||||
let pattern = "%AI3_BeginPattern: (Stripes)\n(Stripes) 0 0 10 10 [\n%AI3_Tile\n(0 O 0 R 0 0 0 0.5 k) @ _ &\n\
|
||||
(0 O 0 R 0 0 0 1 K) @ (\r1 w\r0 5 m\r10 5 L\rS\r) &\n] E\n%AI3_EndPattern\n";
|
||||
let setup = "Adobe_IllustratorA_AI3 /initialize get exec\n";
|
||||
let style = "0 i 1 J 1 j 0.8 w 4 M []0 d\n0 D\n";
|
||||
let src = NAMED_PROLOG
|
||||
.replace(setup, &format!("{setup}{pattern}"))
|
||||
.replace(style, &format!("{style}(Stripes) 5 5 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n"))
|
||||
.replace("10 10 L\nS\n", "10 10 L\nB\n");
|
||||
let r = import(src.as_bytes()).unwrap();
|
||||
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
|
||||
let d = &r.document;
|
||||
assert_eq!(d.layers[0].name.as_deref(), Some("SECTION::Cut"));
|
||||
assert!(matches!(&d.swatch("Stripes").unwrap().paint, Paint::Pattern { pattern, .. } if pattern == "Stripes"));
|
||||
assert_eq!(d.pattern("Stripes").unwrap().art.len(), 2);
|
||||
let square = &d.layers[0].children().unwrap()[0];
|
||||
let Some(Paint::Pattern { pattern, xf }) = square.appearance.fill().map(|f| f.paint.clone()) else { panic!("{:?}", square.appearance) };
|
||||
assert_eq!(pattern, "Stripes");
|
||||
// The tile's top left, (0, 10) in its space, is at (5, 15) in art space: 85 down the 100-point page.
|
||||
assert_eq!(xf * Point::ZERO, Point::new(5.0, 85.0));
|
||||
}
|
||||
|
||||
/// A `.ai` in PostScript form with a graph on each of two layers (version 7 specification §17): its
|
||||
/// data on `%_` lines, then its drawn art in a group, each object followed by `%_… Go`.
|
||||
fn graphs_on_layers() -> String {
|
||||
|
||||
@@ -104,7 +104,9 @@ private editing data. VectorCraft:
|
||||
without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868. Graphs (`%_Gs` … `%_GS`)
|
||||
come in as their drawn art, as plain groups, paths and type, without their data or settings, with a note.
|
||||
without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868, with its paragraphs' letter
|
||||
and word spacing (`TC`, `TW`), hanging punctuation (`Tq`) and hyphenation (`Th`).
|
||||
and word spacing (`TC`, `TW`), hanging punctuation (`Tq`) and hyphenation (`Th`). Its patterns in
|
||||
the version 7 form (a list of `@` and `&` layers, `_ &` for the tile's background) open as pattern swatches,
|
||||
with a fill's offset, scale, rotation, reflection and shear applied before its matrix.
|
||||
- Estimate: **~65%** for real exchange with Illustrator users (open: ~75%; save: PDF-compatible, or legacy
|
||||
version 7 with layers and groups; not yet tested by users who hand files back).
|
||||
Closing the open-side issues: 10–20 h with users' files.
|
||||
|
||||
Reference in New Issue
Block a user