mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 07:11:31 +00:00
Affinity import: count path segments and masks in master page copies
Master page copies were charged one unit per shape whatever its number of segments, and nothing for attached vector or pixel masks, so a master holding one huge curve or mask could be rebuilt on thousands of pages within the copy allowance. Copy costs now count every subpath and segment of a shape's path and its vector mask, and a pixel mask's pixels. A node's vector mask also skipped the document range check the node's own art passes; its control points are now checked as mapped into the document (non-finite ones included) and an object whose mask reaches beyond the range is left out, as one whose art does.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
use super::nodes::MAX_DEPTH;
|
||||
use super::{Kind, af};
|
||||
|
||||
/// What copying a subtree takes: its nodes, characters and table cells (`work`), and its pixels.
|
||||
/// What copying a subtree takes: its nodes, characters, table cells and path segments (`work`),
|
||||
/// and its pixels.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub(super) struct CopyCost {
|
||||
pub(super) work: u64,
|
||||
@@ -40,8 +41,60 @@ pub(super) fn copy_cost(n: &af::Node, depth: usize) -> CopyCost {
|
||||
let own = match &n.kind {
|
||||
Kind::Text(t) => CopyCost { work: 1 + len(&t.runs), pixels: 0 },
|
||||
Kind::Table(t) => CopyCost { work: 1 + t.cells.iter().map(|c| 1 + len(&c.runs)).sum::<u64>(), pixels: 0 },
|
||||
Kind::Image(img) => CopyCost { work: 1, pixels: u64::from(img.width) * u64::from(img.height) },
|
||||
Kind::Image(img) => CopyCost { work: 1, pixels: pixels(img) },
|
||||
// Each copy builds its own path: a curve of many segments costs them all.
|
||||
Kind::Shape { path, .. } | Kind::Artboard { path, .. } => CopyCost { work: 1 + segments(path), pixels: 0 },
|
||||
_ => CopyCost { work: 1, pixels: 0 },
|
||||
};
|
||||
n.children.iter().fold(own, |c, ch| c.plus(copy_cost(ch, depth + 1)))
|
||||
// A vector mask is a path of its own, and a pixel mask an image of its own, in every copy.
|
||||
let mask = CopyCost {
|
||||
work: n.mask.as_ref().map_or(0, segments) + u64::from(n.pixel_mask.is_some()),
|
||||
pixels: n.pixel_mask.as_ref().map_or(0, pixels),
|
||||
};
|
||||
n.children.iter().fold(own.plus(mask), |c, ch| c.plus(copy_cost(ch, depth + 1)))
|
||||
}
|
||||
|
||||
/// The subpaths and segments of `p`.
|
||||
fn segments(p: &af::Path) -> u64 {
|
||||
p.subpaths.iter().map(|s| 1 + s.segments.len() as u64).sum()
|
||||
}
|
||||
|
||||
fn pixels(img: &af::Image) -> u64 {
|
||||
u64::from(img.width) * u64::from(img.height)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn node(kind: Kind, mask: Option<af::Path>) -> af::Node {
|
||||
af::Node {
|
||||
class: vectorcraft_affinity::stream::Tag::of(b"ShpN"),
|
||||
name: String::new(),
|
||||
visible: true,
|
||||
locked: false,
|
||||
opacity: 1.0,
|
||||
blend: af::Blend::Normal,
|
||||
kind,
|
||||
mask,
|
||||
pixel_mask: None,
|
||||
effects: vec![],
|
||||
children: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn path(n: usize) -> af::Path {
|
||||
let p = af::Point { x: 0.0, y: 0.0 };
|
||||
af::Path { subpaths: vec![af::SubPath { start: p, segments: vec![[p; 3]; n], closed: false }] }
|
||||
}
|
||||
|
||||
/// A curve, and a vector mask, cost every segment each copy rebuilds: a shape of a million
|
||||
/// segments shown on many pages can't pass as one object.
|
||||
#[test]
|
||||
fn paths_and_masks_cost_their_segments() {
|
||||
let shape = |n| Kind::Shape { path: path(n), fills: vec![], strokes: vec![], even_odd: false };
|
||||
assert!(copy_cost(&node(shape(1_000_000), None), 0).work > 1_000_000);
|
||||
assert!(copy_cost(&node(Kind::Group, Some(path(1_000_000))), 0).work > 1_000_000);
|
||||
assert!(copy_cost(&node(shape(3), None), 0).work < 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,15 @@ pub(super) fn affine(a: af::Affine) -> Affine {
|
||||
Affine::new(a.0)
|
||||
}
|
||||
|
||||
/// Are all of `p`'s points, mapped by `to_doc`, finite and within `MAX_COORD` of the origin?
|
||||
pub(super) fn path_in_range(p: &af::Path, to_doc: Affine) -> bool {
|
||||
let ok = |q: &af::Point| {
|
||||
let d = to_doc * kurbo::Point::new(q.x, q.y);
|
||||
[d.x, d.y].iter().all(|v| v.is_finite() && v.abs() <= crate::MAX_COORD)
|
||||
};
|
||||
p.subpaths.iter().all(|s| ok(&s.start) && s.segments.iter().flatten().all(ok))
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
pub(super) fn layer_with(&mut self, name: Option<&str>, children: Vec<Arc<Node>>) -> Arc<Node> {
|
||||
let id = self.doc.alloc_id();
|
||||
@@ -107,6 +116,12 @@ impl Builder {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// So must its vector mask, which becomes a clipping path: every control point, not just
|
||||
// the finite ones a bounding box takes.
|
||||
if n.mask.as_ref().is_some_and(|m| !path_in_range(m, to_doc)) {
|
||||
self.warn("objects beyond the document range VectorCraft supports (left out)");
|
||||
return None;
|
||||
}
|
||||
let n = &*self.overlaid(n);
|
||||
// Register before descending so nested artboards keep their document order. Inside an
|
||||
// embedded document an artboard is just its content's frame.
|
||||
@@ -252,3 +267,25 @@ impl Builder {
|
||||
Some(Arc::new(out))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn path(points: &[(f64, f64)]) -> af::Path {
|
||||
let p = |&(x, y): &(f64, f64)| af::Point { x, y };
|
||||
let start = p(&points[0]);
|
||||
let segments = points[1..].iter().map(|q| [start, p(q), p(q)]).collect();
|
||||
af::Path { subpaths: vec![af::SubPath { start, segments, closed: true }] }
|
||||
}
|
||||
|
||||
/// A mask's control points are checked as mapped into the document, NaN ones included.
|
||||
#[test]
|
||||
fn mask_paths_must_stay_in_the_document_range() {
|
||||
let k = Affine::scale(0.75);
|
||||
assert!(path_in_range(&path(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0)]), k));
|
||||
assert!(!path_in_range(&path(&[(0.0, 0.0), (1.0e7, 0.0)]), k));
|
||||
assert!(!path_in_range(&path(&[(0.0, 0.0), (f64::NAN, 0.0)]), k));
|
||||
assert!(!path_in_range(&path(&[(0.0, 0.0), (1.0, 1.0)]), Affine::scale(1.0e9)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user