mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
.ai import: symbols and their instances open live
The reader measured a symbol's definition only to find the page art an
instance covered, so every file with a placed symbol instance opened
with that instance as drawn art, or as its page.
- Each `%AI14_BeginSymbol` definition is read by a reader of its own into
a symbol of the document, with its art (in the symbol's space, y down),
the registration point its dictionary gives
(`AI13PatternRegistrationType`, 1 to 9 from the top left) and the
instances of other symbols in its art. A definition whose art isn't
read whole isn't kept: type, placed files, non-native art, pattern
fills, a gradient or an image the reader can't read or place, and
anything else the reader doesn't read. Its instances come in as the
page draws them, within the box of the art that is read. A definition
that holds an instance of a symbol the file doesn't have isn't kept
either, nor is a definition holding an instance of one of those, and
their instances aren't read. A definition after the first layer isn't
read.
- `%AI24_BeginSymbolList` orders the symbols as the Symbols panel lists
them. A name read twice is numbered ("Dot 2"), and instances name the
first symbol with the file's name.
- `/SymbolInstance` with `/SymbolRef` and `/RTransformMatrix` comes in as
a live instance, with the hide and lock set before it and the opacity,
blending mode, isolation and knockout of the `Xy` after it, as a
group's. An instance with appearance changes of its own
(`AI19SymbolAppearanceMap`), of a symbol that isn't read, or placed
only by `/Matrix` or `/Transform`, comes in as the page draws it.
- Symbol art takes the gradients and art styles read before it, and an
object in it linked to a listed graphic style keeps the link.
- In a symbol's art, the matrices of instances, gradients and images are
read in the canvas's space, y up, about the canvas point of the ruler
origin. Without `%AI3_TemplateBox` an image can't be placed, so the
symbol isn't kept.
- At most 4,096 definitions are read, and a symbol list names at most
16,384 symbols.
Tests: reader tests for each case above, and fuzz cases for damaged and
hostile symbol data.
This commit is contained in:
@@ -3621,6 +3621,114 @@ proptest! {
|
||||
Some(d)
|
||||
})?;
|
||||
}
|
||||
|
||||
/// A `.ai` whose symbols are damaged or hostile: their definitions, the symbol list and the
|
||||
/// instances (on the page and in a symbol's art), cut short or edited.
|
||||
#[test]
|
||||
fn ai_symbols_never_panic(cut in 0usize..2_000, edits in prop::collection::vec((0usize..2_000, prop::sample::select(vec!['0', '9', '-', '.', 'e', ' ', '\n', '(', ')', '/', ':', ';', ',', '%', 'u', 'U', 'A', 'Z'])), 0..12)) {
|
||||
let text = mutate_text(&symbols_text(), cut, &edits);
|
||||
let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&text)].concat();
|
||||
survive("mutated .ai symbols", || Some(vectorcraft_eps::layered_ai(&private, Document::new(200.0, 100.0), vec![], false, false, 1.0).0))?;
|
||||
survive("mutated .ai symbols, trusted", || Some(vectorcraft_eps::layered_ai(&private, Document::new(200.0, 100.0), vec![], false, true, 1.0).0))?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Editing data with two symbols (S2's art holds an instance of S1), the symbol list and a layer of
|
||||
/// instances, as the format writes them.
|
||||
fn symbols_text() -> String {
|
||||
use vectorcraft_testkit::ai;
|
||||
let def = |name: &str, art: &str, reg: u32| {
|
||||
format!(
|
||||
"%AI14_BeginSymbol\n({name})\n0 A\n0 Xw\n0 Ae\nu\n{art}U\n%_/ArtDictionary :\n%_{reg} /Int (AI13PatternRegistrationType) ,\n%_;\n%_\n9 () XW\n%AI10_EndSymbol\n"
|
||||
)
|
||||
};
|
||||
let inst = |name: &str, m: &str| format!("/SymbolInstance :\n({name}) /SymbolRef ,\n{m} /RTransformMatrix\n;\n9 () XW\n");
|
||||
let defs = def("S1", "0 1 1 0 1 0 0 Xa\n-2 -2 m\n2 -2 L\n2 2 L\n-2 2 L\nf\n", 5) + &def("S2", &inst("S1", "1 0 0 -1 -8081 8237"), 1);
|
||||
let list = "%AI24_BeginSymbolList\n(S2)\n(S1)\n%AI24_EndSymbolList\n";
|
||||
let page = ai::layer(
|
||||
"L",
|
||||
&format!(
|
||||
"1 Xw\n{}0 Xw\n{}1 0.5 0 2 0 Xy\n0 0 Xd\n6 () XW\n0 Ae\nq\n{}10 10 m\n30 10 L\n30 30 L\nh\nW\nn\nQ\n",
|
||||
inst("S1", "1 0 0 -1 20 50"),
|
||||
inst("S2", "2 0 0 -2 100 50"),
|
||||
inst("S1", "1 0 0 -1 20 20")
|
||||
),
|
||||
);
|
||||
let data = ai::editing_data(
|
||||
200.0,
|
||||
100.0,
|
||||
&format!("%AI5_Begin_NonPrinting\n{defs}%AI5_End_NonPrinting--\n%AI5_Begin_NonPrinting\n{list}%AI5_End_NonPrinting--\n{page}"),
|
||||
);
|
||||
data.replacen("%%EndComments", "%AI3_TemplateBox: 100.5 50.5 100.5 50.5\n%%EndComments", 1)
|
||||
}
|
||||
|
||||
/// The data [`ai_symbols_never_panic`] damages, as it is, opens with its symbols and instances live.
|
||||
#[test]
|
||||
fn the_symbols_fuzz_data_opens_live() {
|
||||
let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&symbols_text())].concat();
|
||||
let (doc, warnings, layered) = vectorcraft_eps::layered_ai(&private, Document::new(200.0, 100.0), vec![], false, true, 1.0);
|
||||
assert!(layered, "{warnings:?}");
|
||||
let names: Vec<&str> = doc.symbols.iter().map(|s| s.name.as_str()).collect();
|
||||
assert_eq!(names, ["S2", "S1"]);
|
||||
let mut instances = 0;
|
||||
doc.walk(|n| instances += usize::from(matches!(n.kind, vectorcraft_doc::NodeKind::SymbolInstance { .. })));
|
||||
assert_eq!(instances, 3);
|
||||
}
|
||||
|
||||
/// Symbols the reader must stop at: sections that don't end, references and matrices it can't
|
||||
/// use, definitions nested deeper than it reads, and symbols that hold each other.
|
||||
#[test]
|
||||
fn ai_symbol_limits_never_panic() {
|
||||
use vectorcraft_testkit::ai;
|
||||
let inst = |name: &str, m: &str| format!("/SymbolInstance :\n{name} /SymbolRef ,\n{m} /RTransformMatrix\n;\n");
|
||||
let def = |name: &str, art: &str| format!("%AI14_BeginSymbol\n({name})\n0 A\n0 Xw\n0 Ae\nu\n{art}U\n%AI10_EndSymbol\n");
|
||||
let mut nested = String::new();
|
||||
for i in 0..400 {
|
||||
nested.push_str(&format!("%AI14_BeginSymbol\n(N{i})\n0 Ae\nu\n"));
|
||||
}
|
||||
for _ in 0..400 {
|
||||
nested.push_str("U\n%AI10_EndSymbol\n");
|
||||
}
|
||||
let mut chain = String::new();
|
||||
for i in 0..300 {
|
||||
chain.push_str(&def(&format!("C{i}"), &inst(&format!("(C{})", i + 1), "1 0 0 -1 0 0")));
|
||||
}
|
||||
let cases = [
|
||||
format!("%AI14_BeginSymbol\n(Open)\n0 Ae\nu\n{}", ai::layer("L", &inst("(Open)", "1 0 0 -1 20 50"))),
|
||||
format!("%AI24_BeginSymbolList\n(A)\n(B)\n{}", ai::layer("L", "")),
|
||||
format!(
|
||||
"{}{}",
|
||||
def("S", "-2 -2 m\n2 2 L\nS\n"),
|
||||
ai::layer("L", &(inst("", "1 0 0 -1 20 50") + &inst("/SymbolRef", "1 0 0 -1 20 50") + &inst("(S)", "")))
|
||||
),
|
||||
format!(
|
||||
"{}{}",
|
||||
def("S", "-2 -2 m\n2 2 L\nS\n"),
|
||||
ai::layer(
|
||||
"L",
|
||||
&(inst("(S)", "1 0 0") + &inst("(S)", "1 0 0 -1 20 50 7") + &inst("(S)", "1e400 0 0 -1 20 50") + &inst("(S)", "0 0 0 0 20 50"))
|
||||
)
|
||||
),
|
||||
format!("{nested}{}", ai::layer("L", &inst("(N0)", "1 0 0 -1 20 50"))),
|
||||
format!("{chain}{}", ai::layer("L", &inst("(C0)", "1 0 0 -1 20 50"))),
|
||||
format!(
|
||||
"{}{}{}",
|
||||
def("S1", &inst("(S2)", "1 0 0 -1 0 0")),
|
||||
def("S2", &inst("(S1)", "1 0 0 -1 0 0")),
|
||||
ai::layer("L", &inst("(S1)", "1 0 0 -1 20 50"))
|
||||
),
|
||||
format!("{}{}", def("S1", &inst("(S2)", "1 0 0 -1 0 0")), def("S2", &inst("(S1)", "1 0 0 -1 0 0"))),
|
||||
];
|
||||
for body in cases {
|
||||
let data = ai::editing_data(200.0, 100.0, &body).replacen("%%EndComments", "%AI3_TemplateBox: 100.5 50.5 100.5 50.5\n%%EndComments", 1);
|
||||
let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&data)].concat();
|
||||
survive("hostile .ai symbols", || Some(vectorcraft_eps::layered_ai(&private, Document::new(200.0, 100.0), vec![], false, false, 1.0).0))
|
||||
.unwrap();
|
||||
survive("hostile .ai symbols, trusted", || {
|
||||
Some(vectorcraft_eps::layered_ai(&private, Document::new(200.0, 100.0), vec![], false, true, 1.0).0)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// The data [`ai_objects_on_path_never_panics`] damages, as it is, opens with its Objects on Path group.
|
||||
@@ -4329,6 +4437,15 @@ fn arb_ai_token() -> impl Strategy<Value = String> {
|
||||
"\n%\n",
|
||||
"\n%AI5_EndPlace\n",
|
||||
"\n%%PageTrailer\n",
|
||||
"\n%AI14_BeginSymbol\n(S1)\n0 A 0 Xw 0 Ae\n",
|
||||
"\n%AI10_EndSymbol\n",
|
||||
"\n%AI24_BeginSymbolList\n(S1)\n",
|
||||
"\n%AI24_EndSymbolList\n",
|
||||
"/SymbolInstance :",
|
||||
"(S1) /SymbolRef ,",
|
||||
"/RTransformMatrix",
|
||||
"5 /Int (AI13PatternRegistrationType) ,",
|
||||
"; (AI19SymbolAppearanceMap) ,",
|
||||
])
|
||||
.prop_map(str::to_string),
|
||||
]
|
||||
|
||||
@@ -87,8 +87,18 @@
|
||||
//! style's appearance and transparency is linked to it. A `1` naming a style the list doesn't
|
||||
//! have is the object's own appearance, not a link.
|
||||
//!
|
||||
//! Sections the reader doesn't need (symbols, styles, filters) are skipped;
|
||||
//! type, symbols, fills with a pattern it can't read, placed files without a preview and operators
|
||||
//! - Symbols: `%AI14_BeginSymbol` (or `%AI10_`), `(name)`, `0 A 0 Xw 0 Ae`, the art (in the
|
||||
//! symbol's space, y down), a dictionary whose `AI13PatternRegistrationType` is the registration
|
||||
//! point (1 to 9, from the top left, row by row), `%AI10_EndSymbol`; each is read by a reader of
|
||||
//! its own into the document's symbols. `%AI24_BeginSymbolList` (or `%AI10_`) lists their names
|
||||
//! in the Symbols panel's order. An instance: `/SymbolInstance : (name) /SymbolRef , a b c d tx
|
||||
//! ty /RTransformMatrix ;`, the matrix taking the symbol's art to art space; in a symbol's art,
|
||||
//! it takes it to art space about the canvas point of art space's origin, as a pattern's
|
||||
//! gradients are. Its hide and lock come before it (`Xw`, `A`) and its transparency (`Xy`) after
|
||||
//! it.
|
||||
//!
|
||||
//! Sections the reader doesn't need (styles, filters) are skipped; type, instances of symbols it
|
||||
//! can't read, fills with a pattern it can't read, placed files without a preview and operators
|
||||
//! it doesn't know are errors. So is data that ends inside a layer, a group or a compound path.
|
||||
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
@@ -127,7 +137,7 @@ use art::preview_png;
|
||||
pub use document::{BindingRef, Brushes, PaletteSwatches, VariableRead, artboards, bindings_note, brushes, graphic_styles, palette, variables};
|
||||
pub(crate) use document::{slot_frame, slot_of};
|
||||
pub use types::IMAGE_TOO_LARGE;
|
||||
use types::{BLEND_BUDGET, FLIP, Frame, GState, Graph, Kind, PatternHead, PendingEnvelope, Place, Raster};
|
||||
use types::{BLEND_BUDGET, FLIP, Frame, GState, Graph, Kind, PatternHead, PendingEnvelope, Place, Raster, SymbolHead};
|
||||
#[cfg(test)]
|
||||
pub use types::{GRAPH_PLAIN, GRAPH_UNENDED, LINKED_PLACEHOLDERS, NON_NATIVE_ART, NON_NATIVE_ART_UNREAD, VERSIONED_UNENDED};
|
||||
pub(super) use types::{blend_mode, blend_name};
|
||||
@@ -405,7 +415,24 @@ struct Reader<'a> {
|
||||
/// What layers that don't show have that this reader doesn't read.
|
||||
hidden_unread: BTreeSet<String>,
|
||||
unread: Vec<(NodeId, Vec<String>)>,
|
||||
/// The art boxes (symbol space) of symbols whose definition can be measured but not read
|
||||
/// whole, by their name in the file: their instances come in as the page draws them.
|
||||
symbol_bounds: HashMap<String, Rect>,
|
||||
/// The symbols read, by their name in the file → their name in the document.
|
||||
symbol_names: HashMap<String, String>,
|
||||
/// The names the symbols read have in the document, and for each name read more than once,
|
||||
/// the number its next copy is tried with.
|
||||
symbol_taken: HashSet<String>,
|
||||
symbol_suffix: HashMap<String, usize>,
|
||||
/// The art boxes (symbol space) of symbols read, by their name in the file, once measured for
|
||||
/// an instance that comes in as the page draws it.
|
||||
symbol_art_bounds: HashMap<String, Option<Rect>>,
|
||||
/// The symbol list's names, in the Symbols panel's order (`%AI24_BeginSymbolList`).
|
||||
symbol_list: Option<Vec<String>>,
|
||||
/// The symbols read were checked for instances of symbols the file doesn't have.
|
||||
symbols_checked: bool,
|
||||
/// This reader reads a symbol's definition (see [`Reader::define_symbol`]).
|
||||
symbol: Option<SymbolHead>,
|
||||
/// Depth shared by pattern and symbol readers.
|
||||
subreader_depth: usize,
|
||||
pending_unread_fill: bool,
|
||||
@@ -600,6 +627,13 @@ impl<'a> Reader<'a> {
|
||||
hidden_unread: BTreeSet::new(),
|
||||
unread: Vec::new(),
|
||||
symbol_bounds: HashMap::new(),
|
||||
symbol_names: HashMap::new(),
|
||||
symbol_taken: HashSet::new(),
|
||||
symbol_suffix: HashMap::new(),
|
||||
symbol_art_bounds: HashMap::new(),
|
||||
symbol_list: None,
|
||||
symbols_checked: false,
|
||||
symbol: None,
|
||||
subreader_depth: 0,
|
||||
pending_unread_fill: false,
|
||||
pending_unread_stroke: false,
|
||||
|
||||
@@ -16,6 +16,7 @@ impl<'a> Reader<'a> {
|
||||
if self.pattern.is_some() {
|
||||
self.unreadable("type in a pattern", false);
|
||||
}
|
||||
self.left_out_of_symbol();
|
||||
// `1 /FreeUndo`: type in a drawn look. Its story is one of another text document
|
||||
// (`/AI11UndoFreeTextDocument`), which counts its stories on its own, so its slot has none.
|
||||
// In the look of a text object that has an appearance, the object itself follows the look,
|
||||
@@ -46,6 +47,11 @@ impl<'a> Reader<'a> {
|
||||
/// group. Without data, or with a PDF that can't be read, nothing is drawn.
|
||||
pub(super) fn foreign_art(&mut self, o: &Obj) -> Result<(), String> {
|
||||
let Some(text) = self.foreign_data.take() else { return Ok(()) };
|
||||
// Where a symbol's art puts it isn't known.
|
||||
if self.symbol.is_some() {
|
||||
self.left_out_of_symbol();
|
||||
return Ok(());
|
||||
}
|
||||
// One `%` starts each line; the rest, `_` included, is data.
|
||||
let mut encoded = String::with_capacity(text.len());
|
||||
for line in text.split(|b| matches!(b, b'\r' | b'\n')) {
|
||||
@@ -165,6 +171,9 @@ impl<'a> Reader<'a> {
|
||||
self.path_kinds.clear();
|
||||
self.points = 0;
|
||||
self.clip_next = false;
|
||||
if let Some(head) = self.symbol.as_mut() {
|
||||
(head.missing_fill, head.missing_stroke) = (false, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint the current path (`op`), as a guide when `guide`.
|
||||
@@ -177,6 +186,7 @@ impl<'a> Reader<'a> {
|
||||
self.set_instance(&i);
|
||||
}
|
||||
let (fill_paint, stroke_paint) = (self.pending_fill.take(), self.pending_stroke.take());
|
||||
let missing = self.symbol.as_mut().map_or((false, false), |h| (std::mem::take(&mut h.missing_fill), std::mem::take(&mut h.missing_stroke)));
|
||||
if bp.elements().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -196,6 +206,10 @@ impl<'a> Reader<'a> {
|
||||
"b" => (true, true),
|
||||
_ => (false, false),
|
||||
};
|
||||
// A symbol's art painted with a gradient the file doesn't define is left out.
|
||||
if (missing.0 && fill) || (missing.1 && stroke) {
|
||||
self.left_out_of_symbol();
|
||||
}
|
||||
// A gradient instance's paint replaces the missing pattern.
|
||||
let unread = (fill && self.pending_unread_fill && fill_paint.is_none()) || (stroke && self.pending_unread_stroke && stroke_paint.is_none());
|
||||
let mut items = Vec::new();
|
||||
@@ -353,15 +367,20 @@ impl<'a> Reader<'a> {
|
||||
pub(super) fn set_instance(&mut self, i: &Instance) {
|
||||
let mut to_doc = self.space();
|
||||
// In a pattern's definition a gradient's `Xm` and `Bm` are written y down about the canvas point of art space's
|
||||
// origin, as the file's PDF part draws them.
|
||||
if self.pattern.is_some()
|
||||
// origin, as the file's PDF part draws them; in a symbol's, as the instances in its art are.
|
||||
if (self.pattern.is_some() || self.symbol.is_some())
|
||||
&& i.has_matrix()
|
||||
&& let Some(t) = self.template
|
||||
{
|
||||
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, &self.global_names, to_doc) {
|
||||
let paint = i.paint(&self.gradients, &self.global_names, to_doc);
|
||||
if let Some(head) = self.symbol.as_mut() {
|
||||
let missing = if i.stroke { &mut head.missing_stroke } else { &mut head.missing_fill };
|
||||
*missing = paint.is_none();
|
||||
}
|
||||
match paint {
|
||||
Some(p) if i.stroke => self.pending_stroke = Some(p),
|
||||
Some(p) => self.pending_fill = Some(p),
|
||||
None => {
|
||||
@@ -421,22 +440,22 @@ impl<'a> Reader<'a> {
|
||||
self.image_link = None;
|
||||
let space = self.raster.as_ref().and_then(|r| r.space.clone()).unwrap_or_default();
|
||||
let Some((m, n)) = Self::image_operands(&vals) else {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
};
|
||||
let [_, _, _, _, w, h, bits, kind, alpha, _, binary, rest @ ..] = n.as_slice() else {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
};
|
||||
// ImageMask 1: the image is transparent and takes a color (version 7 specification §8.1).
|
||||
let image_mask = rest.first() == Some(&1.0);
|
||||
let (w, h) = (*w as u64, *h as u64);
|
||||
if w.saturating_mul(h) > MAX_PIXELS {
|
||||
self.warn(IMAGE_TOO_LARGE);
|
||||
self.image_left_out(IMAGE_TOO_LARGE);
|
||||
return Ok(());
|
||||
}
|
||||
if w == 0 || h == 0 || ![1.0, 8.0].contains(bits) {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
}
|
||||
let channels: usize = match space.as_str() {
|
||||
@@ -462,7 +481,7 @@ impl<'a> Reader<'a> {
|
||||
// An alpha channel follows the colour samples, one byte a pixel.
|
||||
let alpha_len = if *alpha > 0.0 { (w * h) as usize } else { 0 };
|
||||
let (Some(samples), opacity) = (data.get(..need), data.get(need..need.saturating_add(alpha_len))) else {
|
||||
self.warn("an image whose data is cut short was left out");
|
||||
self.image_left_out("an image whose data is cut short was left out");
|
||||
return Ok(());
|
||||
};
|
||||
let opacity = opacity.filter(|_| alpha_len > 0);
|
||||
@@ -503,12 +522,12 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
}
|
||||
let Some(img) = image::RgbaImage::from_raw(w as u32, h as u32, rgba) else {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
};
|
||||
let mut png = vec![];
|
||||
if img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).is_err() {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
}
|
||||
self.add_raster(ImageBlob::new("image/png", png), None, &m, (w as u32, h as u32), None)
|
||||
@@ -529,7 +548,7 @@ impl<'a> Reader<'a> {
|
||||
let mut png = vec![];
|
||||
let written = image::GrayImage::from_raw(w, h, gray).map(|img| img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png));
|
||||
if !matches!(written, Some(Ok(()))) {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
}
|
||||
let paint = self.mask_paint();
|
||||
@@ -556,25 +575,29 @@ impl<'a> Reader<'a> {
|
||||
let path = self.image_link.take();
|
||||
let operands = Self::image_operands(&vals);
|
||||
let (Some(path), Some((m, n))) = (path, operands) else {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
};
|
||||
let (&[a, b, c, d, tx, ty], &[_, _, _, _, w, h, bits, ..]) = (m.as_slice(), n.as_slice()) else {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
};
|
||||
if !(1.0..=MAX_SIDE).contains(&w) || !(1.0..=MAX_SIDE).contains(&h) || ![1.0, 8.0].contains(&bits) {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
}
|
||||
let (w, h) = (w as u32, h as u32);
|
||||
let xf = self.space() * Affine::new([a, b, c, d, tx, ty]) * FLIP;
|
||||
let Some(to_doc) = self.image_space() else {
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
};
|
||||
let xf = to_doc * Affine::new([a, b, c, d, tx, ty]) * FLIP;
|
||||
if !xf.as_coeffs().iter().all(|v| v.is_finite()) || xf.determinant().abs() < 1e-12 {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
}
|
||||
let Some(png) = missing_placeholder() else {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
};
|
||||
// The placeholder is all the document has of the file: what a missing link shows.
|
||||
@@ -603,13 +626,16 @@ impl<'a> Reader<'a> {
|
||||
// downwards, (u, -v): pixels → (u, -v) → art space. Flipping the matrix's result instead
|
||||
// gives the same place only when the image isn't rotated (b = c = 0).
|
||||
let [a, b, c, d, tx, ty] = m else {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
};
|
||||
let Some(to_doc) = self.image_space() else {
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
};
|
||||
let to_doc = self.space();
|
||||
let xf = to_doc * Affine::new([*a, *b, *c, *d, *tx, *ty]) * FLIP;
|
||||
if !xf.as_coeffs().iter().all(|v| v.is_finite()) || xf.determinant().abs() < 1e-12 {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
self.image_left_out("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
}
|
||||
let mask = alpha.and_then(|a| self.alpha_mask(a, w, h, xf));
|
||||
@@ -619,6 +645,24 @@ impl<'a> Reader<'a> {
|
||||
self.add_image_colorized(im, mask, colorize, false)
|
||||
}
|
||||
|
||||
/// An image that isn't read is left out, with `warning`; in a symbol's art, the symbol isn't
|
||||
/// read whole, so its instances come in as the page draws them.
|
||||
fn image_left_out(&mut self, warning: &str) {
|
||||
if self.symbol.is_some() {
|
||||
self.left_out_of_symbol();
|
||||
} else {
|
||||
self.warn(warning);
|
||||
}
|
||||
}
|
||||
|
||||
/// Note art of a symbol's definition that is left out: the symbol isn't kept, and its
|
||||
/// instances come in as the page draws them, within the box of the art that is read.
|
||||
pub(super) fn left_out_of_symbol(&mut self) {
|
||||
if let Some(head) = self.symbol.as_mut() {
|
||||
head.left_out = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// An image's alpha channel (`w` × `h` bytes) as a grayscale image over the image (`xf`): its
|
||||
/// opacity mask, as in PDF import. `None` when it hides nothing.
|
||||
fn alpha_mask(&mut self, alpha: &[u8], w: u32, h: u32, xf: Affine) -> Option<OpacityMask> {
|
||||
@@ -665,6 +709,12 @@ impl<'a> Reader<'a> {
|
||||
/// shows the preview the data keeps of it, as a linked image whose file is missing does. One
|
||||
/// without a preview, a path or a matrix isn't read.
|
||||
pub(super) fn end_place(&mut self, hidden: bool) -> Result<(), String> {
|
||||
// Where a symbol's art puts a placed file's pixels isn't known.
|
||||
if self.symbol.is_some() {
|
||||
self.place = None;
|
||||
self.left_out_of_symbol();
|
||||
return Ok(());
|
||||
}
|
||||
let (m, b, path, png, w, h, folder) = match self.place.take() {
|
||||
Some(Place { broken: true, .. }) => {
|
||||
self.unreadable("placed files", hidden);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use super::document::slot_of;
|
||||
use super::ops::{paints_apart, path_rule};
|
||||
use super::types::{Frame, GState, Kind, MAX_DEPTH, MAX_NODES, PendingEnvelope};
|
||||
use super::{AppearanceItem, Arc, BezPath, BlendMode, FillRule, HashSet, LayerColor, Node, NodeId, NodeKind, Obj, Paint, PathEl, Rc, Reader, V, obj};
|
||||
use super::{
|
||||
AppearanceItem, Arc, BezPath, BlendMode, FillRule, HashMap, HashSet, LayerColor, Node, NodeId, NodeKind, Obj, Paint, PathEl, Rc, Reader, V, obj,
|
||||
};
|
||||
use vectorcraft_doc::TextWrap;
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
@@ -13,6 +15,7 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
if matches!(kind, Kind::Layer(_)) {
|
||||
self.ensure_space();
|
||||
self.check_symbols();
|
||||
}
|
||||
let base = if matches!(kind, Kind::Obj(_)) { self.stack.len() } else { self.frames.last().map_or(0, |f| f.base) };
|
||||
self.frames.push(Frame {
|
||||
@@ -548,24 +551,141 @@ impl<'a> Reader<'a> {
|
||||
Ok(Some(id))
|
||||
}
|
||||
|
||||
/// A symbol instance (`/SymbolInstance`): the symbol its `SymbolRef` names, placed by its
|
||||
/// `RTransformMatrix`, hidden and locked as the state says. In a symbol's art the matrix is
|
||||
/// written about the canvas point of art space's origin, and the name is the file's until the
|
||||
/// symbols are checked ([`Self::check_symbols`]). An instance of a symbol that wasn't read, one
|
||||
/// whose appearance differs from its symbol's, or one placed only by a `Matrix` or `Transform`,
|
||||
/// comes in as the page draws it, within the symbol's box placed by the matrix when the box is
|
||||
/// known.
|
||||
fn symbol_instance(&mut self, o: &Obj) -> Result<(), String> {
|
||||
let bounds = o.get("SymbolRef").and_then(|v| v.iter().find_map(V::text)).and_then(|name| self.symbol_bounds.get(&name).copied());
|
||||
let matrix = o
|
||||
.get("Matrix")
|
||||
.or_else(|| o.get("Transform"))
|
||||
.map(|v| v.iter().find_map(|item| if let V::Arr(a) = item { Some(Self::nums(a)) } else { None }).unwrap_or_else(|| Self::nums(v)));
|
||||
let box_at_canvas = match (bounds, matrix.as_deref()) {
|
||||
(Some(b), Some([a, c, d, e, x, y])) if [*a, *c, *d, *e, *x, *y].iter().all(|n| n.is_finite()) => {
|
||||
Some(self.space().transform_rect_bbox(super::Affine::new([*a, *c, *d, *e, *x, *y]).transform_rect_bbox(b)))
|
||||
}
|
||||
let name = o.text("SymbolRef");
|
||||
let affine = |n: &[f64]| match n {
|
||||
[a, b, c, d, e, f] if [a, b, c, d, e, f].iter().all(|v| v.is_finite()) => Some(super::Affine::new([*a, *b, *c, *d, *e, *f])),
|
||||
_ => None,
|
||||
};
|
||||
match box_at_canvas {
|
||||
Some(b) => self.unread_box("symbols", b, self.obj_hidden).map(|_| ()),
|
||||
None => {
|
||||
self.unreadable("symbols", self.obj_hidden);
|
||||
Ok(())
|
||||
let live = affine(&o.nums("RTransformMatrix"));
|
||||
let boxed_only = live.is_none();
|
||||
let matrix = live.or_else(|| {
|
||||
o.get("Matrix")
|
||||
.or_else(|| o.get("Transform"))
|
||||
.map(|v| v.iter().find_map(|item| if let V::Arr(a) = item { Some(Self::nums(a)) } else { None }).unwrap_or_else(|| Self::nums(v)))
|
||||
.and_then(|n| affine(&n))
|
||||
});
|
||||
let hidden = self.obj_hidden;
|
||||
let place = if self.symbol.is_some() {
|
||||
self.canvas_origin().map(|o| super::Affine::new([1.0, 0.0, 0.0, -1.0, o.x, o.y]))
|
||||
} else {
|
||||
Some(self.space())
|
||||
};
|
||||
let (Some(name), Some(m), Some(place)) = (name, matrix, place) else {
|
||||
self.unreadable("symbols", hidden);
|
||||
return Ok(());
|
||||
};
|
||||
let xf = place * m;
|
||||
let usable = xf.determinant().abs() >= 1e-12 && xf.as_coeffs().iter().all(|v| v.is_finite());
|
||||
let symbol = if self.symbol.is_some() { Some(name.clone()) } else { self.symbol_names.get(&name).cloned() };
|
||||
match symbol.filter(|_| usable && !boxed_only && !overrides(o)) {
|
||||
Some(symbol) => {
|
||||
// Its transparency is the `Xy` after it, as a group's is.
|
||||
let gs = self.gs.clone();
|
||||
let node = self.new_node(NodeKind::SymbolInstance { symbol, xf }, &gs);
|
||||
self.add(node, hidden)
|
||||
}
|
||||
None => {
|
||||
let bounds = self.symbol_art_bounds(&name).or_else(|| self.symbol_bounds.get(&name).copied());
|
||||
match bounds.map(|b| xf.transform_rect_bbox(b)).filter(|_| usable && self.symbol.is_none()) {
|
||||
Some(b) => self.unread_box("symbols", b, hidden).map(|_| ()),
|
||||
None => {
|
||||
self.unreadable("symbols", hidden);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The art box (symbol space) of the symbol read under `name` (its name in the file), measured
|
||||
/// once.
|
||||
fn symbol_art_bounds(&mut self, name: &str) -> Option<super::Rect> {
|
||||
if let Some(b) = self.symbol_art_bounds.get(name) {
|
||||
return *b;
|
||||
}
|
||||
let named = self.symbol_names.get(name)?;
|
||||
let boxes = self.doc.symbol_boxes();
|
||||
let b = self.doc.symbols.iter().find(|s| &s.name == named).and_then(|s| s.art.visual_bounds_with(&boxes));
|
||||
drop(boxes);
|
||||
self.symbol_art_bounds.insert(name.to_string(), b);
|
||||
b
|
||||
}
|
||||
|
||||
/// Check the symbols read for instances in their art of symbols the file doesn't have, before
|
||||
/// the first layer: a symbol with one isn't kept, and so on for the symbols holding its
|
||||
/// instances. The instances in the art kept then name the symbols as the document does.
|
||||
pub(super) fn check_symbols(&mut self) {
|
||||
if std::mem::replace(&mut self.symbols_checked, true) {
|
||||
return;
|
||||
}
|
||||
// Each symbol's name in the file (none for one named as an earlier one), and the names its
|
||||
// instances use.
|
||||
let files: HashMap<&str, &str> = self.symbol_names.iter().map(|(file, named)| (named.as_str(), file.as_str())).collect();
|
||||
let defs: Vec<(Option<String>, Vec<String>)> = self
|
||||
.doc
|
||||
.symbols
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let mut uses = vec![];
|
||||
symbol_uses(&s.art, &mut uses, 0);
|
||||
(files.get(s.name.as_str()).map(|f| f.to_string()), uses.into_iter().map(str::to_string).collect())
|
||||
})
|
||||
.collect();
|
||||
// A symbol goes when one of its instances names a symbol that isn't kept: first those
|
||||
// naming one the file doesn't have, then, one at a time, those using a symbol that went.
|
||||
let mut kept: Vec<bool> = vec![true; defs.len()];
|
||||
let mut users: HashMap<&str, Vec<usize>> = HashMap::new();
|
||||
let mut going: Vec<usize> = vec![];
|
||||
for (i, (_, uses)) in defs.iter().enumerate() {
|
||||
for u in uses {
|
||||
users.entry(u.as_str()).or_default().push(i);
|
||||
}
|
||||
if uses.iter().any(|u| !self.symbol_names.contains_key(u)) {
|
||||
going.push(i);
|
||||
}
|
||||
}
|
||||
while let Some(i) = going.pop() {
|
||||
match kept.get_mut(i) {
|
||||
Some(k) if *k => *k = false,
|
||||
_ => continue,
|
||||
}
|
||||
if let Some(file) = defs.get(i).and_then(|(file, _)| file.as_deref()) {
|
||||
going.extend(users.get(file).into_iter().flatten().copied().filter(|j| kept.get(*j).copied().unwrap_or(false)));
|
||||
}
|
||||
}
|
||||
drop(users);
|
||||
if kept.iter().any(|k| !k) {
|
||||
let left: Vec<(Option<String>, String)> = defs
|
||||
.iter()
|
||||
.zip(&self.doc.symbols)
|
||||
.zip(&kept)
|
||||
.filter(|(_, k)| !**k)
|
||||
.map(|(((file, _), s), _)| (file.clone(), s.name.clone()))
|
||||
.collect();
|
||||
let mut i = 0;
|
||||
self.doc.symbols.retain(|_| {
|
||||
i += 1;
|
||||
kept.get(i - 1).copied().unwrap_or(false)
|
||||
});
|
||||
// Their art's box isn't known (the instances that made them go have none), so their
|
||||
// instances have no box either.
|
||||
for (file, named) in left {
|
||||
self.doc.set_symbol_registration(&named, vectorcraft_doc::SymbolRegistration::CENTER);
|
||||
let Some(file) = file else { continue };
|
||||
self.symbol_names.remove(&file);
|
||||
}
|
||||
}
|
||||
let names = self.symbol_names.clone();
|
||||
for s in &mut self.doc.symbols {
|
||||
rename_uses(Arc::make_mut(&mut s.art), &names, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -793,6 +913,68 @@ pub(super) fn drop_commented_copies(layers: &mut [Arc<Node>], hidden: &HashSet<N
|
||||
/// A node and its index among its siblings.
|
||||
type Indexed = (usize, Node);
|
||||
|
||||
/// The names of the symbols the instances in `node`, and in the art of its opacity masks, use.
|
||||
fn symbol_uses<'n>(node: &'n Node, out: &mut Vec<&'n str>, depth: usize) {
|
||||
if depth > MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &node.kind {
|
||||
out.push(symbol);
|
||||
}
|
||||
if let Some(m) = &node.extras().mask {
|
||||
symbol_uses(&m.art, out, depth + 1);
|
||||
}
|
||||
for c in node.children().into_iter().flatten() {
|
||||
symbol_uses(c, out, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// The instances in `node`, and in the art of its opacity masks, named by `names` (a name in the
|
||||
/// file → the symbol's in the document).
|
||||
fn rename_uses(node: &mut Node, names: &HashMap<String, String>, depth: usize) {
|
||||
if depth > MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &mut node.kind
|
||||
&& let Some(named) = names.get(symbol.as_str())
|
||||
{
|
||||
symbol.clone_from(named);
|
||||
}
|
||||
if let Some(m) = node.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||||
rename_uses(Arc::make_mut(&mut m.art), names, depth + 1);
|
||||
}
|
||||
for c in node.children_mut().into_iter().flatten() {
|
||||
rename_uses(Arc::make_mut(c), names, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a symbol instance's dictionary carries appearance of its own (a dynamic symbol's
|
||||
/// edits), which isn't read.
|
||||
fn overrides(o: &Obj) -> bool {
|
||||
const KEYS: [&str; 5] = [
|
||||
"AI19SymbolAppearanceMap",
|
||||
"AI19SymbolOverriddenSymbolMap",
|
||||
"AI Local Art of Symbol Instance",
|
||||
"AI15 Layer In Symbol",
|
||||
"AI15 Color Of Layer In Symbol",
|
||||
];
|
||||
// In a dictionary or array nested in it, as a keyed entry's value, in an array or among the
|
||||
// values closed without a key.
|
||||
fn has(o: &Obj, depth: usize) -> bool {
|
||||
depth <= 8
|
||||
&& (o.entries.iter().any(|(k, v)| k.as_deref().is_some_and(|k| KEYS.contains(&k)) || v.iter().any(|v| in_value(v, depth + 1)))
|
||||
|| o.content.iter().any(|v| in_value(v, depth + 1)))
|
||||
}
|
||||
fn in_value(v: &V, depth: usize) -> bool {
|
||||
match v {
|
||||
V::Obj(c) => has(c, depth),
|
||||
V::Arr(items) => depth <= 8 && items.iter().any(|v| in_value(v, depth + 1)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
has(o, 0)
|
||||
}
|
||||
|
||||
/// Close the subpath `bp` ends with, if it has one open.
|
||||
pub(super) fn close(bp: &mut BezPath) {
|
||||
if bp.elements().last().is_some_and(|e| !matches!(e, PathEl::ClosePath)) {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::super::brush;
|
||||
use super::art::{four, sane, two};
|
||||
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,
|
||||
FLIP, GState, Kind, MAX_DEPTH, MAX_HEX, MAX_LISTED, MAX_NODES, MAX_NOTE, MAX_PATTERNS, MAX_PLACED_FOLDER, MAX_STACK, MAX_SUBREADERS, MAX_SYMBOLS,
|
||||
PatternHead, Place, Raster, SKIPPED, SymbolHead, VERSIONED_UNENDED,
|
||||
};
|
||||
use super::{Arc, Global, Lexer, Node, PatternDef, Reader, Rect, Tok, V, marker, obj};
|
||||
use super::{Arc, Global, Lexer, Node, NodeKind, PatternDef, Reader, Rect, Tok, V, marker, obj};
|
||||
use vectorcraft_doc::{NodeId, Symbol, SymbolRegistration};
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
pub(super) fn push(&mut self, v: V) -> Result<(), String> {
|
||||
@@ -85,7 +88,7 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
let m = marker(line);
|
||||
// A pattern's definition. One inside another's isn't read, and the outer tile is then incomplete.
|
||||
if m == "BeginPattern" {
|
||||
if m == "BeginPattern" && self.symbol.is_none() {
|
||||
let section = self.section("EndPattern")?;
|
||||
if self.pattern.is_some() {
|
||||
self.unsupported.insert("patterns inside patterns".into());
|
||||
@@ -96,7 +99,12 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
if m == "BeginSymbol" {
|
||||
let section = self.section("EndSymbol")?;
|
||||
self.symbol_bounds(section);
|
||||
self.define_symbol(section);
|
||||
return Ok(());
|
||||
}
|
||||
if m == "BeginSymbolList" {
|
||||
let section = self.section("EndSymbolList")?;
|
||||
self.symbol_list(section);
|
||||
return Ok(());
|
||||
}
|
||||
// Brushes and their art (those in a pattern's or a brush's art aren't read).
|
||||
@@ -504,32 +512,165 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a definition's box for an instance whose art must come from the page.
|
||||
fn symbol_bounds(&mut self, section: &[u8]) {
|
||||
if self.symbol_bounds.len() >= MAX_PATTERNS || self.subreader_depth >= MAX_SUBREADERS {
|
||||
/// A symbol's definition (`section`: what follows `%AI14_BeginSymbol`), read by a reader of its
|
||||
/// own into a symbol of the document, its art in the symbol's space (y down), named as the file
|
||||
/// names it unless another symbol has that name. A definition inside another one or inside a
|
||||
/// pattern's isn't read, nor is one after the first layer (the symbols were checked there, see
|
||||
/// [`Reader::check_symbols`]). One it can't read whole (an operator it doesn't know, type, unbalanced
|
||||
/// groups, art it leaves out) isn't kept: its instances come in as the page draws them, within
|
||||
/// its art's box when that can be measured.
|
||||
fn define_symbol(&mut self, section: &[u8]) {
|
||||
if self.symbol.is_some()
|
||||
|| self.pattern.is_some()
|
||||
|| self.symbols_checked
|
||||
|| self.subreader_depth >= MAX_SUBREADERS
|
||||
|| self.doc.symbols.len() + self.symbol_bounds.len() >= MAX_SYMBOLS
|
||||
{
|
||||
return;
|
||||
}
|
||||
let mut lex = super::Lexer::new(section);
|
||||
let name = match lex.next_token().map(|t| t.tok) {
|
||||
Some(Tok::Str(bytes)) => super::obj::text(&bytes),
|
||||
let name = match Lexer::new(section).next_token().map(|t| t.tok) {
|
||||
Some(Tok::Str(bytes)) => obj::text(&bytes),
|
||||
_ => return,
|
||||
};
|
||||
let mut sub = Reader::new(section);
|
||||
sub.subreader_depth = self.subreader_depth + 1;
|
||||
sub.cropmarks = self.cropmarks;
|
||||
sub.symbol = Some(SymbolHead::default());
|
||||
sub.template = self.template;
|
||||
// The gradients, art styles and patterns defined so far and the names given, lent.
|
||||
sub.gradients = std::mem::take(&mut self.gradients);
|
||||
sub.global_names = std::mem::take(&mut self.global_names);
|
||||
sub.styles = std::mem::take(&mut self.styles);
|
||||
sub.pattern_tiles = std::mem::take(&mut self.pattern_tiles);
|
||||
sub.v7_patterns.clone_from(&self.v7_patterns);
|
||||
sub.nodes = self.nodes;
|
||||
let read = sub.open(Kind::Group, false).and_then(|()| sub.run());
|
||||
self.nodes = sub.nodes;
|
||||
if read.is_err() || !sub.unsupported.is_empty() || !sub.unknown.is_empty() || sub.frames.len() != 1 {
|
||||
self.gradients = std::mem::take(&mut sub.gradients);
|
||||
self.global_names = std::mem::take(&mut sub.global_names);
|
||||
self.styles = std::mem::take(&mut sub.styles);
|
||||
self.pattern_tiles = std::mem::take(&mut sub.pattern_tiles);
|
||||
// Art stopped by a trailer, or with an array, type, image, placed file, gradient mesh or
|
||||
// gradient still open at the end (as [`Reader::run_string`] checks), isn't whole.
|
||||
let open = sub.done
|
||||
|| sub.stack.iter().any(|v| matches!(v, V::Mark))
|
||||
|| !sub.marks.is_empty()
|
||||
|| sub.text.is_some()
|
||||
|| sub.raster.is_some()
|
||||
|| sub.place.is_some()
|
||||
|| sub.mesh.is_some()
|
||||
|| sub.gradient_def.is_some();
|
||||
let head = sub.symbol.take().unwrap_or_default();
|
||||
let registration = head.registration;
|
||||
let measured = read.is_ok() && !open && sub.unsupported.is_empty() && sub.unknown.is_empty() && sub.frames.len() == 1;
|
||||
let Some(frame) = sub.frames.pop().filter(|_| measured) else { return };
|
||||
let mut kids = frame.children;
|
||||
// Where a symbol's art puts a pattern's tiles isn't known.
|
||||
let whole = !head.left_out && sub.unread.is_empty() && sub.hidden_unread.is_empty() && !kids.iter().any(|n| paints_pattern(n, 0));
|
||||
if !whole {
|
||||
let boxes = self.doc.symbol_boxes();
|
||||
let bounds = kids.iter().fold(None, |acc, n| vectorcraft_geom::union_opt(acc, n.visual_bounds_with(&boxes)));
|
||||
drop(boxes);
|
||||
if let Some(b) = bounds.filter(|b| sane(*b) && b.area() > 0.0) {
|
||||
self.symbol_bounds.insert(name, b);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(frame) = sub.frames.pop() else { return };
|
||||
let bounds = frame.children.iter().fold(None, |acc, n| vectorcraft_geom::union_opt(acc, n.visual_bounds()));
|
||||
let Some(bounds) = bounds.map(|b| sub.space().inverse().transform_rect_bbox(b)) else { return };
|
||||
if sane(bounds) && bounds.area() > 0.0 {
|
||||
self.symbol_bounds.insert(name, bounds);
|
||||
let art = match (kids.len(), kids.pop()) {
|
||||
(1, Some(one)) => one,
|
||||
(_, last) => Node::group(self.doc.alloc_id(), kids.into_iter().chain(last).map(Arc::new).collect()),
|
||||
};
|
||||
// The group a definition holds its art in, when it holds one object and isn't linked to a
|
||||
// graphic style.
|
||||
let linked = sub.style_links.iter().any(|(id, _)| *id == art.id);
|
||||
let art = match symbol_art_member(&art).filter(|_| !linked).cloned() {
|
||||
Some(m) => Arc::unwrap_or_clone(m),
|
||||
None => art,
|
||||
};
|
||||
let read_art = art;
|
||||
let mut art = self.doc.reid(&read_art);
|
||||
self.reid_masks(&mut art, 0);
|
||||
// The objects of the art linked to graphic styles, by their ids in the document.
|
||||
if !sub.style_links.is_empty() {
|
||||
let mut ids = HashMap::new();
|
||||
pair_ids(&read_art, &art, &mut ids, 0);
|
||||
for (id, style) in &sub.style_links {
|
||||
if let Some(new) = ids.get(id)
|
||||
&& self.style_links.len() < MAX_NODES
|
||||
{
|
||||
self.style_links.push((*new, style.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let named = self.unique_symbol_name(if name.is_empty() { "Symbol" } else { &name });
|
||||
self.doc.symbols.push(Symbol { name: named.clone(), art: Arc::new(art), dynamic: false });
|
||||
if let Some(reg) = registration.and_then(symbol_registration) {
|
||||
self.doc.set_symbol_registration(&named, reg);
|
||||
}
|
||||
self.symbol_names.entry(name).or_insert(named);
|
||||
for (key, blob) in sub.doc.images {
|
||||
self.doc.images.entry(key).or_insert(blob);
|
||||
}
|
||||
self.spots.extend(sub.spots);
|
||||
self.stop_spots.extend(sub.stop_spots);
|
||||
self.cmyk_colors += sub.cmyk_colors;
|
||||
self.rgb_colors += sub.rgb_colors;
|
||||
self.drawn_looks += sub.drawn_looks;
|
||||
for w in &sub.warnings {
|
||||
self.warn(w);
|
||||
}
|
||||
}
|
||||
|
||||
/// `base`, or the first free "`base` N" (N from 2) when a symbol read has that name, as
|
||||
/// [`vectorcraft_doc::Document::unique_symbol_name`] names it, without looking at every symbol
|
||||
/// for each N.
|
||||
fn unique_symbol_name(&mut self, base: &str) -> String {
|
||||
let mut named = base.to_string();
|
||||
if self.symbol_taken.contains(&named) {
|
||||
let next = self.symbol_suffix.entry(named.clone()).or_insert(2);
|
||||
while self.symbol_taken.contains(&format!("{base} {next}")) {
|
||||
*next += 1;
|
||||
}
|
||||
named = format!("{base} {next}");
|
||||
*next += 1;
|
||||
}
|
||||
self.symbol_taken.insert(named.clone());
|
||||
named
|
||||
}
|
||||
|
||||
/// Give the art of the opacity masks in `n` (a copy of a symbol's art with ids of this
|
||||
/// document, [`vectorcraft_doc::Document::reid`], which leaves masks as they are) ids of this
|
||||
/// document too.
|
||||
fn reid_masks(&mut self, n: &mut Node, depth: usize) {
|
||||
if depth > MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||||
let mut art = self.doc.reid(&m.art);
|
||||
self.reid_masks(&mut art, depth + 1);
|
||||
m.art = Arc::new(art);
|
||||
}
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
self.reid_masks(Arc::make_mut(c), depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// The symbol list (`section`: what follows `%AI24_BeginSymbolList`): the symbols' names in the
|
||||
/// Symbols panel's order. The first list is kept.
|
||||
fn symbol_list(&mut self, section: &[u8]) {
|
||||
if self.symbol.is_some() || self.pattern.is_some() || self.symbol_list.is_some() {
|
||||
return;
|
||||
}
|
||||
let mut lex = Lexer::new(section);
|
||||
let mut names = vec![];
|
||||
while let Some(t) = lex.next_token() {
|
||||
if let Tok::Str(bytes) = t.tok {
|
||||
if names.len() >= MAX_LISTED {
|
||||
break;
|
||||
}
|
||||
names.push(obj::text(&bytes));
|
||||
}
|
||||
}
|
||||
self.symbol_list = Some(names);
|
||||
}
|
||||
|
||||
/// A plug-in object (`section`: what follows `%AI8_BeginPluginObject`): the brush manager's
|
||||
@@ -788,6 +929,64 @@ fn ps_string(v: &[u8]) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The one member of `n` when `n` is a plain group holding only it (not clipping, unnamed, shown,
|
||||
/// unlocked, opaque, Normal, not isolated, knockout neutral, no knockout shape, appearance, opacity
|
||||
/// mask or other extras such as object attributes): a symbol's art written as such a group
|
||||
/// (definitions hold a group) is that member.
|
||||
pub(crate) fn symbol_art_member(n: &Node) -> Option<&Arc<Node>> {
|
||||
let NodeKind::Group { children, clip: false } = &n.kind else { return None };
|
||||
let [one] = children.as_slice() else { return None };
|
||||
let plain = n.name.as_deref().is_none_or(str::is_empty)
|
||||
&& n.visible
|
||||
&& !n.locked
|
||||
&& n.opacity == 1.0
|
||||
&& n.blend == vectorcraft_color::BlendMode::Normal
|
||||
&& !n.isolate
|
||||
&& n.knockout == vectorcraft_doc::Knockout::Neutral
|
||||
&& !n.knockout_shape
|
||||
&& n.appearance.items.is_empty()
|
||||
&& n.appearance.effects.is_empty()
|
||||
&& *n.extras() == vectorcraft_doc::NodeExtras::default();
|
||||
plain.then_some(one)
|
||||
}
|
||||
|
||||
/// Whether `n`, an object in it or the art of an opacity mask in it is filled or stroked with a
|
||||
/// pattern.
|
||||
fn paints_pattern(n: &Node, depth: usize) -> bool {
|
||||
if depth > MAX_DEPTH {
|
||||
return true;
|
||||
}
|
||||
let pattern = |p: &super::Paint| matches!(p, super::Paint::Pattern { .. });
|
||||
n.appearance.items.iter().any(|item| match item {
|
||||
super::AppearanceItem::Fill(f) => pattern(&f.paint),
|
||||
super::AppearanceItem::Stroke(s) => pattern(&s.paint),
|
||||
}) || n.extras().mask.as_ref().is_some_and(|m| paints_pattern(&m.art, depth + 1))
|
||||
|| n.children().into_iter().flatten().any(|c| paints_pattern(c, depth + 1))
|
||||
}
|
||||
|
||||
/// Each id of `old` and its descendants → the id of the object in its place in `new`, a copy of
|
||||
/// `old` with new ids ([`vectorcraft_doc::Document::reid`]).
|
||||
fn pair_ids(old: &Node, new: &Node, ids: &mut HashMap<NodeId, NodeId>, depth: usize) {
|
||||
ids.insert(old.id, new.id);
|
||||
if depth > MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
if let (Some(a), Some(b)) = (old.extras().mask.as_ref(), new.extras().mask.as_ref()) {
|
||||
pair_ids(&a.art, &b.art, ids, depth + 1);
|
||||
}
|
||||
if let (Some(a), Some(b)) = (old.children(), new.children()) {
|
||||
for (a, b) in a.iter().zip(b) {
|
||||
pair_ids(a, b, ids, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The registration point `AI13PatternRegistrationType` `v` gives a symbol: 1 to 9 from the top
|
||||
/// left, row by row (5 the center); `None` for other numbers.
|
||||
pub(super) fn symbol_registration(v: f64) -> Option<SymbolRegistration> {
|
||||
(v.fract() == 0.0 && (1.0..=9.0).contains(&v)).then(|| SymbolRegistration(v as u8 - 1))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -805,9 +1004,11 @@ mod tests {
|
||||
assert_eq!(ps_string(longest.as_bytes()).map(|s| s.len()), Some(MAX_STRING));
|
||||
}
|
||||
|
||||
/// Definitions nested in a definition aren't read, however deep, and none is read past the
|
||||
/// limit of nested readers.
|
||||
#[test]
|
||||
fn deeply_nested_symbol_definitions_stop_at_subreader_limit() {
|
||||
let mut definition = String::from("(Outer) 0 Ae\n");
|
||||
let mut definition = String::from("(Outer) 0 A 0 Xw 0 Ae\n");
|
||||
for i in 0..300 {
|
||||
definition.push_str(&format!("%AI14_BeginSymbol\n(S{i}) 0 Ae\n"));
|
||||
}
|
||||
@@ -817,8 +1018,34 @@ mod tests {
|
||||
}
|
||||
let mut reader = Reader::new(b"");
|
||||
reader.subreader_depth = MAX_SUBREADERS;
|
||||
reader.symbol_bounds(definition.as_bytes());
|
||||
assert!(reader.symbol_bounds.is_empty());
|
||||
reader.define_symbol(definition.as_bytes());
|
||||
assert!(reader.doc.symbols.is_empty());
|
||||
reader.subreader_depth = 0;
|
||||
reader.define_symbol(definition.as_bytes());
|
||||
let names: Vec<&str> = reader.doc.symbols.iter().map(|s| s.name.as_str()).collect();
|
||||
assert_eq!(names, ["Outer"]);
|
||||
}
|
||||
|
||||
/// Definitions past the limit aren't read, however they are named.
|
||||
#[test]
|
||||
fn symbol_definitions_stop_at_the_limit_even_with_one_name() {
|
||||
let mut reader = Reader::new(b"");
|
||||
for _ in 0..MAX_SYMBOLS + 10 {
|
||||
reader.define_symbol(b"(Same) 0 A 0 Xw 0 Ae\nu\nU\n");
|
||||
}
|
||||
assert_eq!(reader.doc.symbols.len(), MAX_SYMBOLS);
|
||||
assert_eq!(reader.doc.symbols.last().map(|s| s.name.as_str()), Some(format!("Same {MAX_SYMBOLS}").as_str()));
|
||||
}
|
||||
|
||||
/// The registration points 1 to 9 run from the top left, row by row; other numbers give none.
|
||||
#[test]
|
||||
fn symbol_registration_numbers_run_row_by_row() {
|
||||
assert_eq!(symbol_registration(1.0), Some(SymbolRegistration(0)));
|
||||
assert_eq!(symbol_registration(5.0), Some(SymbolRegistration::CENTER));
|
||||
assert_eq!(symbol_registration(9.0), Some(SymbolRegistration(8)));
|
||||
for v in [0.0, 10.0, 2.5, -1.0, f64::NAN] {
|
||||
assert_eq!(symbol_registration(v), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::{
|
||||
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};
|
||||
use vectorcraft_doc::{DataSet, DataValue, GraphicStyle, Guide, ImageBlob, PatternDef, Symbol, TextWrap, Variable, VariableKind, Variables};
|
||||
|
||||
/// `%AI5_RulerUnits` → [`Unit`] (Illustrator 7 spec table 3; centimeters is 4; pixels is 6 in later files).
|
||||
fn ruler_unit(v: u8) -> Option<Unit> {
|
||||
@@ -576,6 +576,13 @@ impl<'a> Reader<'a> {
|
||||
f.children.pop();
|
||||
return;
|
||||
}
|
||||
// The dictionary after a symbol's art: its registration point.
|
||||
if self.frames.len() == 1
|
||||
&& let Some(head) = self.symbol.as_mut()
|
||||
&& let Some(r) = o.nums("AI13PatternRegistrationType").first()
|
||||
{
|
||||
head.registration = Some(*r);
|
||||
}
|
||||
self.constraint_art(o);
|
||||
// A blend's key object or spine.
|
||||
if let Some(kind) = o.text("ShapeBlendPathType")
|
||||
@@ -725,9 +732,50 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
|
||||
/// Art space → where the art being read goes: the document, or while a pattern's definition is
|
||||
/// read, the pattern's space (y down).
|
||||
/// read, the pattern's space (y down). A symbol's definition is written in the symbol's space.
|
||||
pub(crate) fn space(&mut self) -> Affine {
|
||||
if self.pattern.is_some() { FLIP } else { self.ensure_space() }
|
||||
if self.pattern.is_some() {
|
||||
FLIP
|
||||
} else if self.symbol.is_some() {
|
||||
Affine::IDENTITY
|
||||
} else {
|
||||
self.ensure_space()
|
||||
}
|
||||
}
|
||||
|
||||
/// The space an image's matrix is in → where the art being read goes: [`Self::space`], and in a
|
||||
/// symbol's art (outside a pattern's definition) the canvas's space turned y up about art space's
|
||||
/// origin there, as an instance's matrix in it is; `None` there without a template box.
|
||||
pub(super) fn image_space(&mut self) -> Option<Affine> {
|
||||
if self.pattern.is_none() && self.symbol.is_some() {
|
||||
return self.canvas_origin().map(|o| Affine::new([1.0, 0.0, 0.0, -1.0, o.x, o.y]));
|
||||
}
|
||||
Some(self.space())
|
||||
}
|
||||
|
||||
/// The canvas point of art space's origin, from the template box's center (the canvas's): what
|
||||
/// a pattern's gradients and the instances in a symbol's art are written about.
|
||||
pub(super) fn canvas_origin(&self) -> Option<Point> {
|
||||
let t = self.template?;
|
||||
let (cx, cy) = ((t[0] + t[2]) / 2.0, (t[1] + t[3]) / 2.0);
|
||||
let origin = Point::new(CANVAS_CENTRE - cx, CANVAS_CENTRE + cy);
|
||||
(origin.x.is_finite() && origin.y.is_finite()).then_some(origin)
|
||||
}
|
||||
|
||||
/// The symbols in the symbol list's order, those it doesn't name after them as they were
|
||||
/// defined; names it has twice or that no symbol has don't count.
|
||||
fn order_symbols(&mut self) {
|
||||
let Some(list) = self.symbol_list.take() else { return };
|
||||
let mut slots: Vec<Option<Symbol>> = std::mem::take(&mut self.doc.symbols).into_iter().map(Some).collect();
|
||||
let at: HashMap<String, usize> = slots.iter().enumerate().filter_map(|(i, s)| s.as_ref().map(|s| (s.name.clone(), i))).collect();
|
||||
let mut ordered = Vec::with_capacity(slots.len());
|
||||
for name in &list {
|
||||
if let Some(s) = self.symbol_names.get(name).and_then(|named| at.get(named)).and_then(|i| slots.get_mut(*i)).and_then(Option::take) {
|
||||
ordered.push(s);
|
||||
}
|
||||
}
|
||||
ordered.extend(slots.into_iter().flatten());
|
||||
self.doc.symbols = ordered;
|
||||
}
|
||||
|
||||
/// The swatches of the palette, `[None]` first, in its order, and its color groups; a gradient
|
||||
@@ -834,6 +882,8 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
let to_doc = self.ensure_space();
|
||||
let artboard = self.first_board();
|
||||
self.check_symbols();
|
||||
self.order_symbols();
|
||||
drop_commented_copies(&mut self.doc.layers, &self.hidden_ids);
|
||||
super::on_path::apply(&mut self.doc, &self.on_path);
|
||||
let mut doc = std::mem::replace(&mut self.doc, Document::new(1.0, 1.0));
|
||||
@@ -947,6 +997,9 @@ impl<'a> Reader<'a> {
|
||||
let links: HashMap<NodeId, u32> = self.style_links.iter().filter_map(|(node, name)| Some((*node, *ids.get(name)?))).collect();
|
||||
if !links.is_empty() {
|
||||
link_styles(&mut doc.layers, &links, &styles, 0);
|
||||
for symbol in &mut doc.symbols {
|
||||
link_styles(std::slice::from_mut(&mut symbol.art), &links, &styles, 0);
|
||||
}
|
||||
}
|
||||
doc.graphic_styles = styles.into();
|
||||
}
|
||||
@@ -969,6 +1022,11 @@ impl<'a> Reader<'a> {
|
||||
doc.variables = variables.variables.into();
|
||||
}
|
||||
let (bindings, bindings_skipped) = (variables.bindings, variables.bindings_skipped);
|
||||
// Symbols that hold instances of themselves can't be drawn: those instances go before the
|
||||
// layers are compared with the page.
|
||||
if !doc.symbols.is_empty() {
|
||||
doc.limit_depth_noting(&mut warnings);
|
||||
}
|
||||
Ok(Structure {
|
||||
doc,
|
||||
to_doc,
|
||||
|
||||
@@ -520,10 +520,16 @@ impl<'a> Reader<'a> {
|
||||
self.gs.fill = p;
|
||||
self.pending_fill = None;
|
||||
self.pending_unread_fill = false;
|
||||
if let Some(head) = self.symbol.as_mut() {
|
||||
head.missing_fill = false;
|
||||
}
|
||||
} else {
|
||||
self.gs.stroke = p;
|
||||
self.pending_stroke = None;
|
||||
self.pending_unread_stroke = false;
|
||||
if let Some(head) = self.symbol.as_mut() {
|
||||
head.missing_stroke = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,14 +591,18 @@ impl<'a> Reader<'a> {
|
||||
let gs = &self.gs;
|
||||
let state = (gs.opacity, gs.blend, gs.isolate, gs.knockout, gs.knockout_shape);
|
||||
let Some(Frame { kind: Kind::Layer(_), children, .. }) = self.frames.last_mut() else { return };
|
||||
let Some(n) = children.last_mut().filter(|n| matches!(n.kind, NodeKind::Group { .. } | NodeKind::Compound { .. })) else { return };
|
||||
let Some(n) =
|
||||
children.last_mut().filter(|n| matches!(n.kind, NodeKind::Group { .. } | NodeKind::Compound { .. } | NodeKind::SymbolInstance { .. }))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
(n.opacity, n.blend, n.isolate, n.knockout, n.knockout_shape) = before;
|
||||
self.layer_state = Some(state);
|
||||
}
|
||||
|
||||
/// `mode opacity isolate knockout shape Xy`; after a group's end, the group's too, and right
|
||||
/// after a gradient mesh's `/End X!` or a blend's record (before their `Xd` and `6 () XW`),
|
||||
/// theirs, unless the layer's `LB` follows: then it is the layer's (see `end_layer`).
|
||||
/// `mode opacity isolate knockout shape Xy`; after a group's end or a symbol instance, theirs
|
||||
/// too, and right after a gradient mesh's `/End X!` or a blend's record (before their `Xd` and
|
||||
/// `6 () XW`), theirs, unless the layer's `LB` follows: then it is the layer's (see `end_layer`).
|
||||
fn transparency(&mut self, vals: &[V], mesh_ended: bool) {
|
||||
let n = Self::nums(vals);
|
||||
let [mode, opacity, rest @ ..] = n.as_slice() else { return };
|
||||
@@ -622,6 +632,12 @@ impl<'a> Reader<'a> {
|
||||
// The layer's instead when the layer's `7 () XW` follows (`layer_state_block`).
|
||||
self.container_undo = in_layer.then_some(before);
|
||||
self.layer_state = None;
|
||||
} else if let Some(n) = self.last_object().filter(|n| matches!(n.kind, NodeKind::SymbolInstance { .. })) {
|
||||
// A symbol instance's, as a group's.
|
||||
let before = (n.opacity, n.blend, n.isolate, n.knockout, n.knockout_shape);
|
||||
(n.opacity, n.blend, n.isolate, n.knockout, n.knockout_shape) = (gs.opacity, gs.blend, gs.isolate, gs.knockout, gs.knockout_shape);
|
||||
self.container_undo = in_layer.then_some(before);
|
||||
self.layer_state = None;
|
||||
} else {
|
||||
// After a layer's art and before its `LB`: the layer's own state (#2003). Kept until
|
||||
// an operator that isn't part of the state block (or the `LB` that takes it) says the
|
||||
@@ -969,7 +985,9 @@ fn styled(object: &Node, look: &Node, st: &crate::import::ai::style::ArtStyle, p
|
||||
use crate::import::ai::style::StyleItem;
|
||||
// A text object's slot (an empty group standing for it) takes the style as the type will.
|
||||
let slot = slot_of(object.name.as_deref()).is_some();
|
||||
// A symbol instance draws its symbol's art: it keeps its drawn look.
|
||||
if !st.plain
|
||||
|| matches!(object.kind, NodeKind::SymbolInstance { .. })
|
||||
|| (!slot && !st.simple)
|
||||
|| (object.is_container() && !slot && !matches!(object.kind, NodeKind::Compound { .. }) && !st.items.is_empty())
|
||||
|| (st.pathfinder
|
||||
|
||||
@@ -40,6 +40,12 @@ pub(crate) const FLIP: Affine = Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 0.0]);
|
||||
/// Most patterns read (others can't fill art).
|
||||
pub(super) const MAX_PATTERNS: usize = 4096;
|
||||
|
||||
/// Most symbol definitions read (instances of others come in as the page draws them).
|
||||
pub(super) const MAX_SYMBOLS: usize = 4096;
|
||||
|
||||
/// Most names a symbol list orders.
|
||||
pub(super) const MAX_LISTED: usize = 4 * MAX_SYMBOLS;
|
||||
|
||||
/// Most characters of a path's note read (the version 7 specification allows 254, §5.3.1).
|
||||
pub(super) const MAX_NOTE: usize = 1024;
|
||||
|
||||
@@ -227,6 +233,22 @@ pub(super) struct PatternHead {
|
||||
pub(super) tile: Option<Rect>,
|
||||
}
|
||||
|
||||
/// What a reader of a symbol's definition has read of the symbol besides its art.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct SymbolHead {
|
||||
/// `AI13PatternRegistrationType` in the dictionary after the art.
|
||||
pub(super) registration: Option<f64>,
|
||||
/// Art the reader left out (type, a placed file, non-native art, an image it can't read or
|
||||
/// place, a gradient it can't read): the symbol isn't kept, and its instances come in as the
|
||||
/// page draws them, within the box of the rest.
|
||||
pub(super) left_out: bool,
|
||||
/// A gradient instance the reader can't make a paint of (a gradient the file doesn't define,
|
||||
/// or one it can't place) is the next path's fill or stroke: the art is left out when that
|
||||
/// path paints it.
|
||||
pub(super) missing_fill: bool,
|
||||
pub(super) missing_stroke: bool,
|
||||
}
|
||||
|
||||
/// An image's operands, from `XN` and before `XI`.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct Raster {
|
||||
@@ -314,6 +336,8 @@ pub(super) const IGNORED: &[&str] = &[
|
||||
];
|
||||
|
||||
/// Sections skipped whole, by their markers without the `AI<version>_` prefix: begin → end.
|
||||
/// Patterns, symbols and the symbol list are read before this list is looked at; their markers
|
||||
/// stay here for sections of the same kind nested in them to be skipped.
|
||||
pub(super) const SKIPPED: &[(&str, &str)] = &[
|
||||
("BeginPattern", "EndPattern"),
|
||||
("BeginBrushPattern", "EndBrushPattern"),
|
||||
|
||||
@@ -3953,6 +3953,362 @@ fn sections_it_skips() {
|
||||
%_/Document :\n%_/Binary : /ASCII85Decode ,\n%_(]<[%\n%;;;~>\n%_; (Profile) ,\n%_;\n";
|
||||
let doc = read_ok(&format!("{body}{}", layer(&(rect_path(0.0, 0.0, 1.0, 1.0) + "f\n")))).doc;
|
||||
assert_eq!(art(&doc).len(), 1);
|
||||
// A symbol whose art can't be read isn't kept.
|
||||
assert!(doc.symbols.is_empty());
|
||||
}
|
||||
|
||||
/// Symbol definitions as the editing data writes them, in a section that doesn't print.
|
||||
fn symbols(defs: &str) -> String {
|
||||
format!("%AI5_Begin_NonPrinting\n{defs}%AI5_End_NonPrinting--\n")
|
||||
}
|
||||
|
||||
/// A symbol named `name` whose art is `art` (in the symbol's space, y down), with the registration
|
||||
/// point `registration` (1 to 9 from the top left, row by row).
|
||||
fn symbol(name: &str, art: &str, registration: u32) -> String {
|
||||
format!(
|
||||
"%AI14_BeginSymbol\n({name})\n0 A\n0 Xw\n0 Ae\nu\n{art}U\n%_/ArtDictionary :\n%_{registration} /Int (AI13PatternRegistrationType) ,\n%_;\n%_\n9 () XW\n%AI10_EndSymbol\n"
|
||||
)
|
||||
}
|
||||
|
||||
/// An instance of the symbol named `name`, `m` taking the symbol's art to art space.
|
||||
fn instance(name: &str, m: [f64; 6]) -> String {
|
||||
let [a, b, c, d, e, f] = m;
|
||||
format!("/SymbolInstance :\n({name}) /SymbolRef ,\n{a} {b} {c} {d} {e} {f} /RTransformMatrix\n;\n")
|
||||
}
|
||||
|
||||
/// A red square of 4 points about the origin, as a symbol's art.
|
||||
fn dot() -> String {
|
||||
format!("{RED}{}f\n", rect_path(-2.0, -2.0, 2.0, 2.0))
|
||||
}
|
||||
|
||||
/// The visual box of `n` in `doc`, its instances measured by their symbols' art.
|
||||
fn instance_box(doc: &Document, n: &Node) -> Rect {
|
||||
n.visual_bounds_with(&doc.symbol_boxes()).expect("a box")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_symbol_comes_in_once_and_its_instances_stay_live() {
|
||||
let body = symbols(&symbol("Dot", &dot(), 5))
|
||||
+ &layer(&(instance("Dot", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]) + &instance("Dot", [2.0, 0.0, 0.0, -2.0, 100.0, 50.0])));
|
||||
let mut doc = read_ok(&body).doc;
|
||||
assert_eq!(doc.symbols.len(), 1);
|
||||
assert_eq!(doc.symbols[0].name, "Dot");
|
||||
assert!(doc.symbol_registration("Dot").is_center());
|
||||
let a = art(&doc);
|
||||
assert_eq!(a.len(), 2);
|
||||
let xfs: Vec<_> = a
|
||||
.iter()
|
||||
.map(|n| if let NodeKind::SymbolInstance { symbol, xf } = &n.kind { (symbol.clone(), *xf) } else { panic!("{:?}", n.kind) })
|
||||
.collect();
|
||||
assert_eq!(xfs[0].0, "Dot");
|
||||
assert_ne!(xfs[0].1, xfs[1].1);
|
||||
assert_eq!(instance_box(&doc, &a[0]), Rect::new(18.0, 48.0, 22.0, 52.0));
|
||||
assert_eq!(instance_box(&doc, &a[1]), Rect::new(96.0, 46.0, 104.0, 54.0));
|
||||
// Editing the symbol's art changes both instances.
|
||||
let wider = read_ok(&(symbols(&symbol("Dot", &format!("{RED}{}f\n", rect_path(-4.0, -2.0, 4.0, 2.0)), 5)) + &layer(""))).doc;
|
||||
doc.symbols[0].art = wider.symbols[0].art.clone();
|
||||
assert_eq!(instance_box(&doc, &a[0]), Rect::new(16.0, 48.0, 24.0, 52.0));
|
||||
assert_eq!(instance_box(&doc, &a[1]), Rect::new(92.0, 46.0, 108.0, 54.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_instance_in_a_symbols_art_is_placed_from_the_canvas() {
|
||||
// The template box's center is (100.5, 50.5): art space's origin is at canvas (8091, 8242).
|
||||
let nested = instance("Dot", [1.0, 0.0, 0.0, -1.0, -8081.0, 8237.0]);
|
||||
let defs = symbols(&(symbol("Dot", &dot(), 5) + &symbol("Pair", &nested, 1)));
|
||||
let body = format!("%AI3_TemplateBox: 100.5 50.5 100.5 50.5\n{defs}{}", layer(&instance("Pair", [1.0, 0.0, 0.0, -1.0, 50.0, 50.0])));
|
||||
let doc = read_ok(&body).doc;
|
||||
let names: Vec<&str> = doc.symbols.iter().map(|s| s.name.as_str()).collect();
|
||||
assert_eq!(names, ["Dot", "Pair"]);
|
||||
assert_eq!(doc.symbol_registration("Pair"), vectorcraft_doc::SymbolRegistration(0));
|
||||
// Dot sits at (10, 5) in Pair's art, which the page instance puts at (50, 50).
|
||||
let a = art(&doc);
|
||||
assert_eq!(instance_box(&doc, &a[0]), Rect::new(58.0, 53.0, 62.0, 57.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_image_in_a_symbols_art_is_placed_from_the_canvas() {
|
||||
// The template box's center is (100.5, 50.5): art space's origin is at canvas (8091, 8242). The
|
||||
// image's matrix is in the canvas's space turned y up about that point, as an instance's in a
|
||||
// symbol's art is: its top left pixel goes to (-5, -2.5) in the symbol's art.
|
||||
let m = "[ 5 0 0 5 -8096 8244.5 ]";
|
||||
let raster = format!(
|
||||
"%AI5_File:\n%AI5_BeginRaster\n() 1 XG\n/DeviceRGB XN\n{m} 2 1 0 Xh\n{m} 0 0 2 1 2 1 8 3 0 0 1 0 4 4 0 0\n\
|
||||
%%BeginData: 6\rXI\nABCDEF%%EndData\r\nXH\r\n%AI5_EndRaster\r\nN\n"
|
||||
);
|
||||
let body = format!("%AI3_TemplateBox: 100.5 50.5 100.5 50.5\n{}{}", symbols(&symbol("Pic", &raster, 5)), layer(""));
|
||||
let doc = read_ok(&body).doc;
|
||||
assert_eq!(doc.symbols.len(), 1);
|
||||
let NodeKind::Image(im) = &doc.symbols[0].art.kind else { panic!("{:?}", doc.symbols[0].art.kind) };
|
||||
for ((u, v), want) in [((0.0, 0.0), (-5.0, -2.5)), ((2.0, 0.0), (5.0, -2.5)), ((0.0, 1.0), (-5.0, 2.5))] {
|
||||
let got = im.xf * Point::new(u, v);
|
||||
assert!((got - Point::new(want.0, want.1)).hypot() < 1e-9, "pixel ({u}, {v}): {got:?}, want {want:?}");
|
||||
}
|
||||
// A linked image (`XG` and `XF`) is placed the same way.
|
||||
let linked = raster7("(a.tif) 0 XG\n", m, (2, 1), "XF", "");
|
||||
let doc = read_ok(&format!("%AI3_TemplateBox: 100.5 50.5 100.5 50.5\n{}{}", symbols(&symbol("Link", &linked, 5)), layer(""))).doc;
|
||||
let NodeKind::Image(link) = &doc.symbols[0].art.kind else { panic!("{:?}", doc.symbols[0].art.kind) };
|
||||
assert!(link.link.is_some());
|
||||
assert!((link.xf * Point::new(0.0, 0.0) - Point::new(-5.0, -2.5)).hypot() < 1e-9, "{:?}", link.xf);
|
||||
// Without a template box the image can't be placed, so the symbol isn't read whole: its
|
||||
// instance comes in as the page draws it.
|
||||
let group = format!("0 Ae\nu\n{}{RED}{}f\nU\n", instance("Pic", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]), rect_path(10.0, 0.0, 14.0, 4.0));
|
||||
let r = read_ok(&(symbols(&symbol("Pic", &raster, 5)) + &layer(&group)));
|
||||
assert!(r.doc.symbols.is_empty(), "{:?}", r.doc.symbols.iter().map(|s| &s.name).collect::<Vec<_>>());
|
||||
assert_eq!(r.unread.len(), 1, "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_instance_keeps_its_hide_lock_transparency_and_clip() {
|
||||
let m = [1.0, 0.0, 0.0, -1.0, 20.0, 50.0];
|
||||
let art_ = format!(
|
||||
"1 Xw\n{}0 Xw\n1 A\n{}0 A\n{}1 0.5 1 1 1 Xy\n0 0 Xd\n6 () XW\n0 Ae\nq\n{}{}h\nW\nn\nQ\n0 0.25 0 0 0 Xy\n{}",
|
||||
instance("Dot", m),
|
||||
instance("Dot", m),
|
||||
instance("Dot", m),
|
||||
instance("Dot", m),
|
||||
rect_path(19.0, 49.0, 21.0, 51.0),
|
||||
instance("Dot", m),
|
||||
);
|
||||
let doc = read_ok(&(symbols(&symbol("Dot", &dot(), 5)) + &layer(&art_))).doc;
|
||||
let a = art(&doc);
|
||||
assert_eq!(a.len(), 5);
|
||||
assert!(!a[0].visible && !a[0].locked);
|
||||
assert!(a[1].visible && a[1].locked);
|
||||
// An instance's transparency is the `Xy` after it, as a group's is; one set before it isn't its.
|
||||
assert_eq!((a[2].opacity, a[2].blend, a[2].isolate, a[2].knockout, a[2].knockout_shape), (0.5, BlendMode::Multiply, true, Knockout::On, true));
|
||||
assert_eq!((a[1].opacity, a[1].blend, a[1].isolate, a[1].knockout), (1.0, BlendMode::Normal, false, Knockout::Neutral));
|
||||
assert_eq!((a[3].opacity, a[4].opacity), (0.25, 1.0));
|
||||
let NodeKind::Group { children, clip: true } = &a[3].kind else { panic!("a clipping group: {:?}", a[3].kind) };
|
||||
assert!(matches!(children[0].kind, NodeKind::Path { clipping: true, .. }));
|
||||
assert!(matches!(children[1].kind, NodeKind::SymbolInstance { .. }));
|
||||
}
|
||||
|
||||
/// The art of an opacity mask in a symbol's art takes ids of the document, as the rest of the art
|
||||
/// does: no two objects of the document, in its layers or its symbols, share an id.
|
||||
#[test]
|
||||
fn a_symbols_opacity_mask_art_takes_ids_of_the_document() {
|
||||
fn ids(n: &Node, out: &mut Vec<vectorcraft_doc::NodeId>) {
|
||||
out.push(n.id);
|
||||
if let Some(m) = n.extras().mask.as_ref() {
|
||||
ids(&m.art, out);
|
||||
}
|
||||
for c in n.children().into_iter().flatten() {
|
||||
ids(c, out);
|
||||
}
|
||||
}
|
||||
let square = rect_path(-2.0, -2.0, 2.0, 2.0);
|
||||
let mask_art: String = format!("1 g\n{square}f\n").lines().map(|l| format!("%_{l}\n")).collect();
|
||||
let mask = format!("%_/ArtDictionary :\n%_/Mask :\n%_X=\n{mask_art}%_X+\n%_(Art) ,\n%_1 /Bool (Clipping) ,\n%_; (AI9_Mask) ,\n%_;\n");
|
||||
let defs = symbols(&symbol("Masked", &format!("{RED}{square}f\n{mask}{}", dot()), 5));
|
||||
let shapes: String = (0..6).map(|i| format!("{RED}{}f\n", rect_path(f64::from(i) * 10.0, 0.0, f64::from(i) * 10.0 + 4.0, 4.0))).collect();
|
||||
let doc = read_ok(&(defs + &layer(&format!("{shapes}{}", instance("Masked", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]))))).doc;
|
||||
let mut all = Vec::new();
|
||||
for n in doc.layers.iter().chain(doc.symbols.iter().map(|s| &s.art)) {
|
||||
ids(n, &mut all);
|
||||
}
|
||||
assert!(doc.symbols.iter().any(|s| s.art.children().into_iter().flatten().any(|c| c.extras().mask.is_some())), "the mask is read");
|
||||
let unique: std::collections::HashSet<_> = all.iter().copied().collect();
|
||||
assert_eq!(unique.len(), all.len(), "{all:?}");
|
||||
}
|
||||
|
||||
/// A layer's `Xy` right after its last object, an instance, before the layer's `0 0 Xd` and
|
||||
/// `7 () XW`, is the layer's; the instance's own state block (`6 () XW`) keeps it the instance's.
|
||||
#[test]
|
||||
fn a_layers_state_after_its_last_symbol_instance_is_the_layers() {
|
||||
let last = instance("Dot", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]);
|
||||
let doc = read_ok(&(symbols(&symbol("Dot", &dot(), 5)) + &layer(&format!("{last}1 0.6 0 2 0 Xy\n0 0 Xd\n7 () XW\n")))).doc;
|
||||
assert_eq!((doc.layers[0].opacity, doc.layers[0].blend), (0.6, BlendMode::Multiply));
|
||||
assert_eq!((art(&doc)[0].opacity, art(&doc)[0].blend), (1.0, BlendMode::Normal));
|
||||
let doc = read_ok(&(symbols(&symbol("Dot", &dot(), 5)) + &layer(&format!("{last}1 0.6 0 2 0 Xy\n0 0 Xd\n6 () XW\n")))).doc;
|
||||
assert_eq!(doc.layers[0].opacity, 1.0);
|
||||
assert_eq!((art(&doc)[0].opacity, art(&doc)[0].blend), (0.6, BlendMode::Multiply));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_symbol_list_orders_the_symbols_and_names_stay_apart() {
|
||||
let defs = symbol("A", &dot(), 5) + &symbol("B", &dot(), 5) + &symbol("A", &dot(), 5) + &symbol("C", &dot(), 9);
|
||||
for list in [
|
||||
"%AI24_BeginSymbolList\n(C)\n(Ghost)\n(B)\n(C)\n%AI24_EndSymbolList\n",
|
||||
"%AI10_BeginSymbolList\n(C)\n(Ghost)\n(B)\n(C)\n%AI10_EndSymbolList\n",
|
||||
] {
|
||||
let doc = read_ok(&(symbols(&format!("{defs}{list}")) + &layer(&instance("A", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0])))).doc;
|
||||
let names: Vec<&str> = doc.symbols.iter().map(|s| s.name.as_str()).collect();
|
||||
assert_eq!(names, ["C", "B", "A", "A 2"], "{list}");
|
||||
assert_eq!(doc.symbol_registration("C"), vectorcraft_doc::SymbolRegistration(8));
|
||||
// An instance names the first symbol the file gives that name.
|
||||
assert!(matches!(&art(&doc)[0].kind, NodeKind::SymbolInstance { symbol, .. } if symbol == "A"));
|
||||
}
|
||||
}
|
||||
|
||||
/// A symbol whose art is filled with a pattern isn't kept: its instances come in as the page draws
|
||||
/// them, within the box of its art. The pattern keeps the name it was numbered with.
|
||||
#[test]
|
||||
fn a_symbol_with_a_pattern_fill_isnt_kept() {
|
||||
let pattern = DOTS.replace("(Dots)", "(Red\\n1)");
|
||||
let lawn = symbol("Lawn", &format!("(Red\\n1) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n{}f\n", rect_path(-2.0, -2.0, 2.0, 2.0)), 5);
|
||||
let r = read_ok(&format!("{pattern}{}{}", symbols(&lawn), layer(&instance("Lawn", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]))));
|
||||
assert_eq!(r.doc.patterns.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["Red 2"]);
|
||||
assert!(r.doc.symbols.is_empty());
|
||||
assert_unread(&r, "symbols");
|
||||
let (id, _) = &r.unread[0];
|
||||
assert_eq!(r.doc.node(*id).and_then(|n| n.geometric_bounds()), Some(Rect::new(18.0, 48.0, 22.0, 52.0)));
|
||||
// A pattern chosen and then replaced by a color before the art is painted fills nothing.
|
||||
let solid = symbol("Solid", &format!("(Red\\n1) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n{RED}{}f\n", rect_path(-2.0, -2.0, 2.0, 2.0)), 5);
|
||||
let r = read_ok(&format!("{pattern}{}{}", symbols(&solid), layer(&instance("Solid", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]))));
|
||||
assert_eq!(r.doc.symbols.len(), 1);
|
||||
assert!(r.unread.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_instance_of_a_symbol_it_doesnt_have_is_the_unread_object() {
|
||||
let missing = instance("Absent", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]);
|
||||
// Alone on the layer, the file isn't read: the page has no box for it.
|
||||
let e = read(stream(&(symbols(&symbol("Dot", &dot(), 5)) + &layer(&missing))).as_bytes()).unwrap_err();
|
||||
assert!(e.contains("symbols"), "{e}");
|
||||
// In a group, the group is the unread object.
|
||||
let group = format!("0 Ae\nu\n{missing}{RED}{}f\nU\n", rect_path(10.0, 0.0, 14.0, 4.0));
|
||||
let r = read_ok(&(symbols(&symbol("Dot", &dot(), 5)) + &layer(&group)));
|
||||
assert_unread(&r, "symbols");
|
||||
// Nor is a symbol whose art holds an instance of one the file doesn't have: the page has no box
|
||||
// for its instances.
|
||||
let holder = symbol("Holder", &(dot() + &instance("Absent", [1.0, 0.0, 0.0, -1.0, 0.0, 0.0])), 5);
|
||||
let body =
|
||||
format!("%AI3_TemplateBox: 100.5 50.5 100.5 50.5\n{}{}", symbols(&holder), layer(&instance("Holder", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0])));
|
||||
let e = read(stream(&body).as_bytes()).unwrap_err();
|
||||
assert!(e.contains("symbols"), "{e}");
|
||||
// An instance whose matrix doesn't have six finite numbers isn't read either.
|
||||
let bad = "/SymbolInstance :\n(Dot) /SymbolRef ,\n1 0 0 -1 20 /RTransformMatrix\n;\n";
|
||||
let r = read_ok(&(symbols(&symbol("Dot", &dot(), 5)) + &layer(&format!("0 Ae\nu\n{bad}U\n"))));
|
||||
assert_unread(&r, "symbols");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_instance_whose_appearance_differs_from_its_symbols_comes_in_as_the_page_draws_it() {
|
||||
let edited = "/SymbolInstance :\n(Dot) /SymbolRef ,\n1 0 0 -1 20 50 /RTransformMatrix\n,\n/Dictionary :\n; (AI19SymbolAppearanceMap) ,\n;\n";
|
||||
let r = read_ok(&(symbols(&symbol("Dot", &dot(), 5)) + &layer(edited)));
|
||||
assert_unread(&r, "symbols");
|
||||
let (id, _) = &r.unread[0];
|
||||
assert_eq!(r.doc.node(*id).and_then(|n| n.geometric_bounds()), Some(Rect::new(18.0, 48.0, 22.0, 52.0)), "the box is the symbol's, placed");
|
||||
// The appearance changes are found in a dictionary that an array holds as a value without a key.
|
||||
let in_array = "/SymbolInstance :\n(Dot) /SymbolRef ,\n1 0 0 -1 20 50 /RTransformMatrix\n,\n\
|
||||
/Array :\n/Dictionary :\n/Dictionary :\n; (AI19SymbolAppearanceMap) ,\n;\n;\n; (Edits) ,\n;\n";
|
||||
let r = read_ok(&(symbols(&symbol("Dot", &dot(), 5)) + &layer(in_array)));
|
||||
assert_unread(&r, "symbols");
|
||||
// So does an instance of a symbol whose art isn't read whole, within that art's box.
|
||||
let partial = symbol("Partial", &format!("{}f\n1 Xw\n1 2 frobnicate\n0 Xw\n", rect_path(-2.0, -2.0, 2.0, 2.0)), 5);
|
||||
let r = read_ok(&(symbols(&partial) + &layer(&instance("Partial", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]))));
|
||||
assert!(r.doc.symbols.is_empty());
|
||||
assert_unread(&r, "symbols");
|
||||
let (id, _) = &r.unread[0];
|
||||
assert_eq!(r.doc.node(*id).and_then(|n| n.geometric_bounds()), Some(Rect::new(18.0, 48.0, 22.0, 52.0)));
|
||||
}
|
||||
|
||||
/// A symbol whose art holds an image that is left out (no matrix, a matrix with no area, data cut
|
||||
/// short) isn't kept, so its instances don't draw it without the image.
|
||||
#[test]
|
||||
fn a_symbol_with_an_image_it_leaves_out_isnt_kept() {
|
||||
for (what, image) in [
|
||||
("no matrix", raster7("", "", (1, 1), "XI", "%FF0000\n")),
|
||||
("a matrix with no area", raster7("", "[ 0 0 0 0 0 0 ]", (1, 1), "XI", "%FF0000\n")),
|
||||
("data cut short", raster7("", "[ 1 0 0 1 0 0 ]", (2, 2), "XI", "%FF0000\n")),
|
||||
("no end", raster7("", "[ 1 0 0 1 0 0 ]", (1, 1), "XI", "%FF0000\n").replace("%AI5_EndRaster\n", "")),
|
||||
("art after a trailer", format!("%%PageTrailer\n{}", raster7("", "[ 1 0 0 1 0 0 ]", (1, 1), "XI", "%FF0000\n"))),
|
||||
] {
|
||||
// With a template box, so that the image could be placed.
|
||||
let defs = symbols(&symbol("Photo", &(dot() + &image), 5));
|
||||
let group = format!("0 Ae\nu\n{}{RED}{}f\nU\n", instance("Photo", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]), rect_path(10.0, 0.0, 14.0, 4.0));
|
||||
let r = read_ok(&format!("%AI3_TemplateBox: 100.5 50.5 100.5 50.5\n{defs}{}", layer(&group)));
|
||||
assert!(r.doc.symbols.is_empty(), "{what}");
|
||||
assert_eq!(r.unread.len(), 1, "{what}: {:?}", r.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
/// A symbol's art written as a group holding one object stays that group when the group is tagged
|
||||
/// (`XT`), so the tag isn't lost.
|
||||
#[test]
|
||||
fn a_symbols_tagged_group_keeps_its_tag() {
|
||||
let def = format!(
|
||||
"%AI14_BeginSymbol\n(Tagged)\n0 A\n0 Xw\n0 Ae\nu\n{}U\n/AdobeURL (https://example.com/a) XT\n%_/ArtDictionary :\n%_5 /Int (AI13PatternRegistrationType) ,\n%_;\n%_\n9 () XW\n%AI10_EndSymbol\n",
|
||||
dot()
|
||||
);
|
||||
let doc = read_ok(&(symbols(&def) + &layer(&instance("Tagged", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0])))).doc;
|
||||
let art = &doc.symbols[0].art;
|
||||
assert!(matches!(art.kind, NodeKind::Group { .. }), "{:?}", art.kind);
|
||||
assert_eq!(tags(art).0, "https://example.com/a");
|
||||
}
|
||||
|
||||
/// A definition whose art a trailer stops isn't kept, though the art before the trailer is whole.
|
||||
#[test]
|
||||
fn a_symbol_cut_off_by_a_trailer_isnt_kept() {
|
||||
let cut = format!("%AI14_BeginSymbol\n(Cut)\n0 A\n0 Xw\n0 Ae\n{}%%PageTrailer\n{}%AI10_EndSymbol\n", dot(), dot());
|
||||
let group = format!("0 Ae\nu\n{}{RED}{}f\nU\n", instance("Cut", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]), rect_path(10.0, 0.0, 14.0, 4.0));
|
||||
let r = read_ok(&(symbols(&cut) + &layer(&group)));
|
||||
assert!(r.doc.symbols.is_empty(), "{:?}", r.doc.symbols.iter().map(|s| &s.name).collect::<Vec<_>>());
|
||||
assert_unread(&r, "symbols");
|
||||
}
|
||||
|
||||
/// A symbol whose art holds type, a placed file, non-native art, an image it can't place or a
|
||||
/// gradient the file doesn't define isn't kept. Its instances come in as the page draws them, within the box of the rest of its art, an
|
||||
/// instance right on a layer as well as one in a group.
|
||||
#[test]
|
||||
fn a_symbols_art_it_leaves_out_gives_its_instances_the_box_of_the_rest() {
|
||||
let typed = "/AI11Text :\n0 /FreeUndo ,\n0 /FrameIndex ,\n0 /StoryIndex ,\n;\n".to_string();
|
||||
let placed = "%AI5_BeginPlace\n%AI5_EndPlace\n".to_string();
|
||||
let foreign =
|
||||
"/ForeignObject :\n1 /Version ,\n2 0 0 2 10 20 /RTransform ,\n10 20 /Origin ,\n1 2 5 8 /Bounds ,\n/Data ,\n%,u@!!/MSk8%41#ocdN=10d&\n%1H@<P2`V<S,pbuU7L]\\~>\n;\n"
|
||||
.to_string();
|
||||
let image = raster7("", "[ 1 0 0 1 0 0 ]", (1, 1), "XI", "%FF0000\n");
|
||||
let gradient = format!("Bb\n1 (Nowhere) 0 0 0 10 1 0 0 1 0 0 Bg\n{}f\n0 BB\n", rect_path(-2.0, -2.0, 2.0, 2.0));
|
||||
let template = "%AI3_TemplateBox: 100.5 50.5 100.5 50.5\n";
|
||||
for (what, left, head) in [
|
||||
("type", typed, template),
|
||||
("a placed file", placed, template),
|
||||
("non-native art", foreign, template),
|
||||
("an image without a template box", image, ""),
|
||||
("a gradient the file doesn't define", gradient, template),
|
||||
] {
|
||||
let defs = symbols(&symbol("Mixed", &(dot() + &left), 5));
|
||||
let body = format!("{head}{defs}{}", layer(&instance("Mixed", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0])));
|
||||
let r = read(stream(&body).as_bytes()).unwrap_or_else(|e| panic!("{what}: {e}"));
|
||||
assert!(r.doc.symbols.is_empty(), "{what}");
|
||||
assert_unread(&r, "symbols");
|
||||
let (id, _) = &r.unread[0];
|
||||
assert_eq!(r.doc.node(*id).and_then(|n| n.geometric_bounds()), Some(Rect::new(18.0, 48.0, 22.0, 52.0)), "{what}");
|
||||
}
|
||||
// A gradient the file doesn't define on a path that isn't painted paints nothing.
|
||||
// Nor does one on a path that a group's start drops, for the paths after it.
|
||||
let square = rect_path(-2.0, -2.0, 2.0, 2.0);
|
||||
for (what, unpainted) in [
|
||||
("not painted", format!("Bb\n1 (Nowhere) 0 0 0 10 1 0 0 1 0 0 Bg\n{square}n\n0 BB\n")),
|
||||
("dropped", format!("Bb\n1 (Nowhere) 0 0 0 10 1 0 0 1 0 0 Bg\n{square}0 BB\nu\n{square}f\nU\n")),
|
||||
] {
|
||||
let defs = symbols(&symbol("Kept", &(dot() + &unpainted), 5));
|
||||
let r = read_ok(&format!("{template}{defs}{}", layer(&instance("Kept", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]))));
|
||||
assert_eq!(r.doc.symbols.len(), 1, "{what}: {:?}", r.warnings);
|
||||
assert!(r.unread.is_empty(), "{what}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbols_that_hold_each_other_lose_the_instances_that_close_the_loop() {
|
||||
let m = [1.0, 0.0, 0.0, -1.0, 0.0, 0.0];
|
||||
let defs = symbols(&(symbol("S1", &instance("S2", m), 5) + &symbol("S2", &instance("S1", m), 5)));
|
||||
let r =
|
||||
read_ok(&format!("%AI3_TemplateBox: 100.5 50.5 100.5 50.5\n{defs}{}", layer(&(dot() + &instance("S1", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0])))));
|
||||
assert!(r.warnings.contains(&vectorcraft_doc::depth::symbols_note()), "{:?}", r.warnings);
|
||||
assert!(!r.doc.clone().limit_depth(), "nothing is left that draws itself");
|
||||
}
|
||||
|
||||
/// A definition after the first layer isn't read: the symbols were checked before that layer, and
|
||||
/// one read later could hold an instance of a symbol the file doesn't have.
|
||||
#[test]
|
||||
fn a_symbol_defined_after_the_first_layer_is_not_read() {
|
||||
let late = symbols(&symbol("Late", &(dot() + &instance("Absent", [1.0, 0.0, 0.0, -1.0, 0.0, 0.0])), 5));
|
||||
let group = format!("0 Ae\nu\n{}{RED}{}f\nU\n", instance("Late", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]), rect_path(10.0, 0.0, 14.0, 4.0));
|
||||
let r = read_ok(&format!("%AI3_TemplateBox: 100.5 50.5 100.5 50.5\n{}{late}{}", layer(&dot()), layer(&group)));
|
||||
assert!(r.doc.symbols.is_empty(), "{:?}", r.doc.symbols.iter().map(|s| &s.name).collect::<Vec<_>>());
|
||||
assert_unread(&r, "symbols");
|
||||
}
|
||||
|
||||
/// A filled square at x = `10 × i` (5 pt wide), to tell the alternatives of versioned content apart.
|
||||
@@ -4573,6 +4929,35 @@ fn a_style_name_outside_ascii_links_its_objects() {
|
||||
assert_eq!(art(&r.doc)[0].extras().graphic_style, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_object_in_a_symbols_art_with_a_listed_style_is_linked() {
|
||||
let defs = styles_and_list(&[fill_style("Blue", "0 0 0 0 0 0 1 Xa\n")], &["Blue"]);
|
||||
let body =
|
||||
format!("{defs}{}{}", symbols(&symbol("Tile", &styled_square("Blue"), 5)), layer(&instance("Tile", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0])));
|
||||
let r = read_ok(&body);
|
||||
assert_eq!(style_names(&r.doc), [("Blue".to_string(), 1)]);
|
||||
let tile = &r.doc.symbols[0].art;
|
||||
assert!(matches!(tile.kind, NodeKind::Path { .. }), "{tile:#?}");
|
||||
assert_eq!(fill(tile).color(), Some(Color::rgb(0.0, 0.0, 1.0)));
|
||||
assert_eq!(tile.extras().graphic_style, Some(1), "{tile:#?}");
|
||||
// A symbol's art written as a group holding one object stays that group when the group is
|
||||
// linked to the style.
|
||||
let empty = "/KnownStyle :\n(Empty) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
|
||||
/BasicFilter :\n(Conduit Filter) 0 0 /Filter ,\n1 /Visible ,\n;\n /Part ,\n;\n";
|
||||
let defs = styles_and_list(&[empty.to_string()], &["Empty"]);
|
||||
let square = rect_path(0.0, 0.0, 10.0, 10.0);
|
||||
let hidden = square.replace('\n', "\n%_");
|
||||
// The styled group is the definition's own group, without one around it.
|
||||
let group = format!(
|
||||
"%AI14_BeginSymbol\n(Group)\n0 A\n0 Xw\n0 Ae\nu\n{RED}{square}f\nU\n%_u\n%_{hidden}f\n%_U\n1 (Empty) XW\n\
|
||||
%_/ArtDictionary :\n%_5 /Int (AI13PatternRegistrationType) ,\n%_;\n%_\n%AI10_EndSymbol\n"
|
||||
);
|
||||
let body = format!("{defs}{}{}", symbols(&group), layer(&instance("Group", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0])));
|
||||
let art = &read_ok(&body).doc.symbols[0].art;
|
||||
assert!(matches!(art.kind, NodeKind::Group { .. }), "{art:#?}");
|
||||
assert_eq!(art.extras().graphic_style, Some(1), "{art:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_style_list_the_default_graphic_styles_stay() {
|
||||
let defaults = style_names(&Document::new(1.0, 1.0));
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1206,6 +1206,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
A `.ai` or PDF whose editing data holds a readable variable library opens with its variable definitions, data-set rows, text, visibility and linked-file values, active row and lock in the Variables panel, whether its layers are read or it comes in as its PDF part; graph values and malformed entries are skipped with a note. With its layers read, the objects its text, visibility and image variables are bound to (type for a text variable, any object for a visibility variable, a linked image for an image variable, found by their XML ids) come in bound, so selecting a data set changes them; locked and hidden objects, objects in a locked or hidden layer or group and objects on a locked artboard keep their bindings, and a data set leaves the locked ones as they are. Bindings whose object didn't come in or is of another kind are skipped with a note, as are graph bindings and every binding of a file that comes in as its PDF part (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`).
|
||||
A `.ai` or EPS opened from its editing data keeps each artboard's center mark, cross hairs, video safe areas and video ruler pixel aspect ratio, and the file's Document Raster Effects Settings; the author comes from the header's `%%For` (the editing data's, else an EPS's own) when the file's File Info gives none, the header's custom colors that no swatch has come in as spot swatches, a named color the file defines takes the place of a default swatch of that name when the file has no palette, and an older file's `%%TemplateBox` places its artboard. A value of another type or out of range keeps its default (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`).
|
||||
A `.ai` opened from its editing data opens its Objects on Path groups live, with their path, their objects in order along it, Pivot and Rotate, and their art where the file draws it; a group whose spacing has been dragged, or with options an Objects on Path group doesn't hold, opens as a plain group (`crates/eps/src/import/ai/tests.rs`).
|
||||
`.ai` symbol definitions and placed instances open as live symbols, in the Symbols panel's order; an instance of a symbol that isn't read, or whose appearance differs from its symbol's, comes in as the page draws it (`crates/eps/src/import/ai/tests.rs`).
|
||||
A `.ai` or EPS whose editing data has brushes opens with the file's Calligraphic, Scatter, Art and Bristle brushes in the Brushes panel in place of the default ones, in the file's order: the Calligraphic size, roundness and angle with their variation, the Scatter size, spacing, scatter and rotation, the Bristle settings, and the Scatter and Art brushes' art. A brush library `.ai` (brushes and no layers) opens from its PDF part with its brushes, so Other Library lists them. Pattern brushes and brushes that can't be read are left out with a note; brushed paths still come in as their drawn look (part of #372 and #637; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`).
|
||||
In a `.ai` opened from its editing data, a path or type object with Offset Path, Outline Object or Outline Stroke on the whole object opens as itself with those effects live, in their order and shown or hidden; on a fill or stroke of type they stay on that fill or stroke. Offset Path keeps its Offset, Joins and Miter Limit, and Outline Stroke takes its width from the stroke. An Offset Path with a setting missing, out of range or not read keeps the drawn art, as does a path with a Path effect on one fill or stroke (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`).
|
||||
In a `.ai` opened from its editing data, an object with a Tweak that moves nothing or Pucker & Bloat at 0 opens as itself with the effect live. A path, compound path or group whose only effect is Transform without copies, or a Free Distort whose corners make a parallelogram, opens as itself with it live, and so does an unstroked path or compound path with straight segments whose only effect is Twist. A gradient or pattern, an image, type, a clipping group, an opacity mask or effects inside the object, or a stroke with a Transform that scales, rotates or reflects, keep the drawn art (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/ai/read/ops.rs`).
|
||||
|
||||
Reference in New Issue
Block a user