mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 03:05:44 +00:00
Save As .ai: bound the symbol art the editing data writes
Instances of symbols that hold instances of other symbols multiply, and the editing data wrote every level for each instance above it, limited only by nesting depth: eight levels of 100 instances each could hang a save or run out of memory. The writer now counts the objects of symbol art it writes, symbol instances and inline graphics alike, and leaves out instances past 16,000,000 objects in all, as the PDF part does, with a warning.
This commit is contained in:
@@ -39,8 +39,9 @@
|
||||
//! loss a warning: type as its outlines and its inline graphics' art, gradients as their average color, pattern fills as their
|
||||
//! tiles cut to the shape, clipping groups and clipping layers as their art cut to the clipping
|
||||
//! path (the clipping path's fill cut too, its stroke not), images as their outlines filled with
|
||||
//! their average color, symbols and live objects as their art, strokes a plain line can't draw as
|
||||
//! their filled outlines, several fills or strokes as a group of a path for each, gradient and
|
||||
//! their average color, symbols and live objects as their art (symbol art past
|
||||
//! [`vectorcraft_doc::MAX_SYMBOL_NODES`] objects in all left out), strokes a plain line can't draw
|
||||
//! as their filled outlines, several fills or strokes as a group of a path for each, gradient and
|
||||
//! pattern swatches, transparency and live effects other than geometry effects left out.
|
||||
//!
|
||||
//! The data streams hold the program compressed with Zstandard after `%AI24_ZStandard_Data`, with
|
||||
|
||||
@@ -651,6 +651,31 @@ fn inline_graphics_in_type_are_written_as_their_art() {
|
||||
assert!(at(&blue) < at(&red), "the stroke above the characters is over the inline graphic");
|
||||
}
|
||||
|
||||
/// Nested symbols whose instances multiply stop at [`vectorcraft_doc::MAX_SYMBOL_NODES`] objects of
|
||||
/// symbol art in all, with a warning.
|
||||
#[test]
|
||||
fn nested_symbol_instances_stop_at_the_symbol_art_limit() {
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let layer = doc.layers[0].id;
|
||||
// Empty paths: counted, but written as nothing, so the test stays small; and one filled square.
|
||||
let empty = Arc::new(Node::path(doc.alloc_id(), PathData::new(vec![]), Appearance::default()));
|
||||
let mut members = vec![empty; 4999];
|
||||
let square = shapes::rectangle(Rect::new(0.0, 0.0, 1.0, 1.0));
|
||||
members.push(Arc::new(Node::path(doc.alloc_id(), square, Appearance::basic(solid(Color::gray(0.5)), Paint::None, 0.0))));
|
||||
let leaves = Node::group(doc.alloc_id(), members);
|
||||
doc.symbols.push(Symbol { name: "Leaf".into(), art: Arc::new(leaves) });
|
||||
let leaf = Arc::new(Node::new(doc.alloc_id(), NodeKind::SymbolInstance { symbol: "Leaf".into(), xf: Affine::IDENTITY }));
|
||||
let trees = Node::group(doc.alloc_id(), vec![leaf; 5000]);
|
||||
doc.symbols.push(Symbol { name: "Tree".into(), art: Arc::new(trees) });
|
||||
let tree = Node::new(doc.alloc_id(), NodeKind::SymbolInstance { symbol: "Tree".into(), xf: Affine::IDENTITY });
|
||||
doc.insert(Some(layer), usize::MAX, tree).unwrap();
|
||||
let out = export_ai_editing(&doc, &opts()).unwrap();
|
||||
assert!(out.warnings.iter().any(|w| w == writer::SYMBOL_LIMIT), "{:?}", out.warnings);
|
||||
// 5,001 objects for the tree and each leaf: 3,198 leaves fit in 16,000,000.
|
||||
let text = String::from_utf8(out.program).unwrap();
|
||||
assert_eq!(text.matches("\r\nf\r\n").count(), 3198, "the squares of the leaves written");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stained_symbol_instance_is_written_in_its_stained_colors() {
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
|
||||
@@ -36,6 +36,7 @@ const STROKES: &str = "strokes a plain line can't draw (inside or outside the pa
|
||||
pub(super) const TRANSPARENCY: &str = "transparency (opacity, blending modes, opacity masks) is left out of the editing data for other apps";
|
||||
pub(super) const EFFECTS: &str = "live effects other than geometry effects are left out of the editing data for other apps";
|
||||
const SWATCHES: &str = "gradient and pattern swatches are left out of the editing data for other apps";
|
||||
pub(super) const SYMBOL_LIMIT: &str = "symbol instances past 16,000,000 objects of symbol art in all are left out of the editing data for other apps";
|
||||
const TEMPLATES: &str = "template layers are written as dimmed layers that don't print in the editing data for other apps";
|
||||
|
||||
pub(super) struct Writer<'a> {
|
||||
@@ -57,6 +58,8 @@ pub(super) struct Writer<'a> {
|
||||
next: HashMap<String, u64>,
|
||||
/// The document's brushes, read the first time a brushed stroke is written.
|
||||
brushes: Option<Vec<vectorcraft_brush::Brush>>,
|
||||
/// The objects of the symbol art written so far, within [`vectorcraft_doc::MAX_SYMBOL_NODES`].
|
||||
symbol_nodes: u64,
|
||||
}
|
||||
|
||||
impl<'a> Writer<'a> {
|
||||
@@ -73,9 +76,23 @@ impl<'a> Writer<'a> {
|
||||
ids: HashSet::new(),
|
||||
next: HashMap::new(),
|
||||
brushes: None,
|
||||
symbol_nodes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the art of a symbol, `art`, may be written: the objects of the symbol art written
|
||||
/// so far stay within [`vectorcraft_doc::MAX_SYMBOL_NODES`], as in the PDF part (instances of
|
||||
/// symbols that hold instances multiply). Past it the art is left out, with a warning.
|
||||
fn take_symbol(&mut self, art: &Node) -> bool {
|
||||
let total = self.symbol_nodes.saturating_add(art.count() as u64);
|
||||
if total > vectorcraft_doc::MAX_SYMBOL_NODES {
|
||||
self.warn(SYMBOL_LIMIT);
|
||||
return false;
|
||||
}
|
||||
self.symbol_nodes = total;
|
||||
true
|
||||
}
|
||||
|
||||
fn warn(&mut self, w: &str) {
|
||||
if !self.warnings.iter().any(|x| x == w) {
|
||||
self.warnings.push(w.to_string());
|
||||
@@ -234,6 +251,7 @@ impl<'a> Writer<'a> {
|
||||
self.line("u");
|
||||
if let Some(sym) = self.doc.symbols.iter().find(|s| &s.name == symbol)
|
||||
&& self.nest < MAX_NEST
|
||||
&& self.take_symbol(&sym.art)
|
||||
{
|
||||
// Stained by the instance's fill, as the canvas draws it.
|
||||
let mut art = vectorcraft_brush::instance_art(&sym.art, n);
|
||||
@@ -540,6 +558,9 @@ impl<'a> Writer<'a> {
|
||||
for ig in &layout.inlines {
|
||||
let Some(art) = t.runs.get(ig.run).and_then(|r| r.inline.as_ref()) else { continue };
|
||||
let Some(sym) = doc.symbols.iter().find(|x| x.name == art.symbol) else { continue };
|
||||
if !self.take_symbol(&sym.art) {
|
||||
continue;
|
||||
}
|
||||
self.warn(LIVE);
|
||||
let mut art_node = (*sym.art).clone();
|
||||
// Strokes scale with the art, as on the canvas.
|
||||
|
||||
Reference in New Issue
Block a user