diff --git a/crates/effects/src/distort.rs b/crates/effects/src/distort.rs index 3fb10a925..fe3bcdd65 100644 --- a/crates/effects/src/distort.rs +++ b/crates/effects/src/distort.rs @@ -112,6 +112,11 @@ const MAX_REACH: f64 = 1.0e9; /// a 2000-anchor path. Stacked Transform effects multiply their copies (three of 1000 copies make /// a thousand million of one square), so the limit is on the result, not on each effect's count. pub(crate) const MAX_COPY_ANCHORS: usize = 2_000_000; +/// Most image nodes a Transform effect's copies make of an art, the originals included: 1000 +/// copies of a 2000-image art. Stacked Transform effects multiply their copies (three of 1000 +/// copies make a thousand million of one image), so the limit is on the result, not on each +/// effect's count. +pub(crate) const MAX_COPY_IMAGES: usize = 2_000_000; /// Transform effect: scale, rotate and reflect about reference point `reference` of the bounds /// (the 9-point grid, 4 = centre), then move; `copies` more times, each from the last. With @@ -152,7 +157,8 @@ pub(crate) fn transform_within(path: &PathData, b: Rect, p: &Value, seed: u64, m /// The Transform effect's map and how many copies of what it is applied to it makes, for bounds /// `b` (the reference point lies on them), params `p` and the object's `seed` for Random: the map /// applies to the object itself when `copies` is 0, and once more to each of `copies` copies -/// otherwise (see [`transform_within`], and images, which have no anchors to budget). +/// otherwise (see [`transform_within`], and images, whose copies count nodes against +/// [`MAX_COPY_IMAGES`]). pub(crate) fn transform_map(b: Rect, p: &Value, seed: u64) -> (Affine, usize) { let random = flag(p, "random", false); // The share (0..1) of value `k` applied. diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index 689223044..763f30658 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -222,10 +222,24 @@ pub(crate) fn reshape_leaves(art: &mut Node, fx: &[&Effect]) { /// images follow, each mapped once more than the last. Without copies the image itself takes the /// map, as a path does. fn transform_images(n: &mut Node, m: Affine, copies: usize) { + transform_images_within(n, m, copies, crate::distort::MAX_COPY_IMAGES) +} + +/// [`transform_images`] whose copies make at most `max` image nodes of the art, the originals +/// included: each copy adds the image once more, and a Transform after this one copies the lot +/// again, so copies stop where the result would pass `max` (#2042). +pub(crate) fn transform_images_within(n: &mut Node, m: Affine, copies: usize, max: usize) { + let mut room = max.saturating_sub(image_count(n)); + copy_images(n, m, copies, &mut room); +} + +/// The walk of [`transform_images`]: the map and its copies go to each image in document order, +/// each copy taking one from `room` (what the art can still grow by), until the budget is full. +fn copy_images(n: &mut Node, m: Affine, copies: usize, room: &mut usize) { let Some(children) = n.children_mut() else { return }; for c in children.iter_mut() { if !matches!(c.kind, NodeKind::Image(_)) { - transform_images(Arc::make_mut(c), m, copies); + copy_images(Arc::make_mut(c), m, copies, room); continue; } let src = (**c).clone(); @@ -240,12 +254,14 @@ fn transform_images(n: &mut Node, m: Affine, copies: usize) { *c = Arc::new(placed(m)); continue; } + let made = copies.min(*room); + *room -= made; // The copies become image nodes of their own, under a group with the image's id, name and // place in the art (as the runs an outlined text gets). - let mut images: Vec> = Vec::with_capacity(copies + 1); + let mut images: Vec> = Vec::with_capacity(made + 1); images.push(Arc::new(placed(Affine::IDENTITY))); let mut xf = Affine::IDENTITY; - for _ in 0..copies { + for _ in 0..made { xf *= m; images.push(Arc::new(placed(xf))); } @@ -255,6 +271,11 @@ fn transform_images(n: &mut Node, m: Affine, copies: usize) { } } +/// How many image nodes `n` holds below itself. +fn image_count(n: &Node) -> usize { + n.children().into_iter().flatten().map(|c| usize::from(matches!(c.kind, NodeKind::Image(_))) + image_count(c)).sum() +} + /// `n` with type anywhere inside it outlined and live objects evaluated, so geometry effects on /// it reach every member. pub(crate) fn outlined_members(n: &Node) -> Node { diff --git a/crates/effects/src/tests.rs b/crates/effects/src/tests.rs index 5c7026370..30a8cab2f 100644 --- a/crates/effects/src/tests.rs +++ b/crates/effects/src/tests.rs @@ -234,6 +234,49 @@ fn stacked_transform_copies_stay_within_the_anchor_budget() { assert_eq!(run("distort.transform", p, &square()).subpaths.len(), 1001); } +/// #2042: stacked Transform effects multiply their image copies as they do their anchors; the +/// image nodes they make are bounded, whatever each one's count. +#[test] +fn stacked_transform_copies_stay_within_the_image_budget() { + use std::sync::Arc; + + use vectorcraft_doc::{ImageObject, NodeId}; + let id = NodeId(9); + let image = || { + Node::new( + id, + NodeKind::Image(Box::new(ImageObject { + key: "k".into(), + width: 40, + height: 40, + xf: Affine::translate((10.0, 0.0)), + link: None, + placement: Default::default(), + })), + ) + }; + let count = |art: &Node| { + let mut xs = vec![]; + image_xs(art, &mut xs); + xs.len() + }; + let m = Affine::translate((70.0, 0.0)); + let mut art = Node::group(id, vec![Arc::new(image())]); + let mut counts = vec![]; + for _ in 0..3 { + crate::reshape::transform_images_within(&mut art, m, 1000, 100); + counts.push(count(&art)); + } + assert_eq!(counts, [100, 100, 100], "99 copies, then no room for a copy of them"); + // Within the budget every copy is made, and the effect's own budget takes a thousand copies. + let mut art = Node::group(id, vec![Arc::new(image())]); + crate::reshape::transform_images_within(&mut art, m, 24, 100); + assert_eq!(count(&art), 25); + let mut g = Node::group(id, vec![Arc::new(image())]); + g.appearance.effects.push(fx("distort.transform", json!({"moveH": 70, "copies": 1000}))); + assert_eq!(count(&evaluate_container(&g).unwrap()), 1001); +} + #[test] fn transform_copies_stop_before_they_overflow() { let out = run("distort.transform", json!({"scaleH": 100000, "scaleV": 100000, "copies": 1000}), &square()); @@ -268,14 +311,20 @@ fn transform_on_a_group_moves_and_copies_its_images() { xs.sort_by(f64::total_cmp); assert_eq!(xs, [10.0, 80.0, 150.0], "the image and its two copies, 70 pt apart"); // Without copies the image itself takes the transform, as a path does. - let mut g = Node::group(id, vec![Arc::new(Node::new(id, NodeKind::Image(Box::new(ImageObject { - key: "k".into(), - width: 40, - height: 40, - xf: Affine::translate((10.0, 0.0)), - link: None, - placement: Default::default(), - }))))]); + let mut g = Node::group( + id, + vec![Arc::new(Node::new( + id, + NodeKind::Image(Box::new(ImageObject { + key: "k".into(), + width: 40, + height: 40, + xf: Affine::translate((10.0, 0.0)), + link: None, + placement: Default::default(), + })), + ))], + ); g.appearance.effects.push(fx("distort.transform", json!({"scaleH": 50, "scaleV": 50, "reference": 8}))); let art = evaluate_container(&g).unwrap(); let mut xs = vec![];