diff --git a/crates/metafile/src/cliprect.rs b/crates/metafile/src/cliprect.rs
index bc7221541..5ecfdfc2f 100644
--- a/crates/metafile/src/cliprect.rs
+++ b/crates/metafile/src/cliprect.rs
@@ -104,14 +104,22 @@ fn is_rect(clip: &[P]) -> bool {
/// Sutherland–Hodgman: `poly` cut by convex `clip`.
pub(crate) fn clip_polygon(poly: &[P], clip: &[P]) -> Vec
{
- if poly.len().saturating_mul(clip.len()) > MAX_CLIP_WORK {
- return Vec::new();
+ clip_polygon_within(poly, clip, &mut { MAX_CLIP_WORK }).unwrap_or_default()
+}
+
+/// [`clip_polygon`] charging each point visited against each clip edge to `budget`: the polygon
+/// can gain a vertex at every edge, so its cost isn't known from its input size alone. `None`
+/// once the budget is spent.
+fn clip_polygon_within(poly: &[P], clip: &[P], budget: &mut usize) -> Option> {
+ if poly.len().saturating_mul(clip.len()) > *budget {
+ return None;
}
let mut poly = poly.to_vec();
for i in 0..clip.len() {
- if poly.len().saturating_mul(clip.len() - i) > MAX_CLIP_WORK {
- return Vec::new();
+ if poly.len().saturating_mul(clip.len() - i) > *budget {
+ return None;
}
+ *budget -= poly.len();
let (a, b) = (clip[i], clip[(i + 1) % clip.len()]);
let keep = |p: P| cross(a, b, p) >= 0.0;
let cut = |p: P, q: P| {
@@ -137,7 +145,7 @@ pub(crate) fn clip_polygon(poly: &[P], clip: &[P]) -> Vec {
break;
}
}
- poly
+ Some(poly)
}
fn disjoint(b: [f64; 4], c: [f64; 4]) -> bool {
@@ -167,7 +175,7 @@ pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec {
}
let mut out = Vec::new();
if fill.is_some() {
- let f = clip_fill(&subs, clip);
+ let Some(f) = clip_fill(&subs, clip) else { return Vec::new() };
if !f.is_empty() {
out.push(Raw::Path { ops: f, fill, stroke: None, even_odd, region: region.clone() });
}
@@ -192,7 +200,7 @@ pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec {
if too_much_work(&subs, clip) {
return Vec::new();
}
- let f = clip_fill(&subs, clip);
+ let f = clip_fill(&subs, clip).unwrap_or_default();
if f.is_empty() { Vec::new() } else { vec![Raw::Pattern { ops: f, even_odd, tile, region }] }
}
Raw::Bitmap { rect, img, region } => crop(rect, img, clip).map(|(rect, img)| vec![Raw::Bitmap { rect, img, region }]).unwrap_or_default(),
@@ -247,17 +255,20 @@ fn too_much_work(subs: &[(Vec, bool)], clip: &[P]) -> bool {
subs.iter().map(|s| s.0.len() + 1).sum::().saturating_mul(clip.len()) > MAX_CLIP_WORK
}
-fn clip_fill(subs: &[(Vec, bool)], clip: &[P]) -> Vec {
+/// The clipped fill of every subpath, its clipping work shared by all of them: `None` (the whole
+/// item goes) once [`MAX_CLIP_WORK`] is spent.
+fn clip_fill(subs: &[(Vec, bool)], clip: &[P]) -> Option> {
let mut ops = Vec::new();
+ let mut budget = MAX_CLIP_WORK;
for (pts, _) in subs {
- let poly = clip_polygon(pts, clip);
+ let poly = clip_polygon_within(pts, clip, &mut budget)?;
if poly.len() >= 3 {
ops.push(Op::M(poly[0]));
ops.extend(poly[1..].iter().map(|p| Op::L(*p)));
ops.push(Op::Z);
}
}
- ops
+ Some(ops)
}
fn clip_stroke(subs: &[(Vec, bool)], clip: &[P]) -> Vec {
diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs
index ff6559ec1..3ad3a7886 100644
--- a/crates/metafile/src/tests.rs
+++ b/crates/metafile/src/tests.rs
@@ -2089,3 +2089,20 @@ fn clip_work_is_bounded_across_subpaths() {
assert!(crate::cliprect::clip_raw(raw(many), &clip).is_empty(), "the whole item's clipping work is bounded");
assert_eq!(crate::cliprect::clip_raw(raw(tri(100.0).to_vec()), &clip).len(), 2, "a small crossing shape is still clipped");
}
+
+/// Clipping a fill can add a vertex at every clip edge, so a polygon's work isn't its input size:
+/// shapes that pass the up-front estimate but grow while clipped are charged as they grow, against
+/// one budget for the whole item.
+#[test]
+fn growing_fill_clips_share_one_budget() {
+ use crate::canvas::Raw;
+ use crate::geom::Op;
+ let clip: Vec<(f64, f64)> =
+ (0..512).map(|i| (i as f64 / 512.0 * std::f64::consts::TAU).sin_cos()).map(|(s, c)| (100.0 + 100.0 * c, 100.0 + 100.0 * s)).collect();
+ // A triangle around the whole circle: clipped, it becomes the 512-gon, about 130,000 steps.
+ let tri = [Op::M((-1000.0, -1000.0)), Op::L((1000.0, -1000.0)), Op::L((100.0, 1000.0)), Op::Z];
+ let raw = |n: usize| Raw::Path { ops: tri.repeat(n), fill: Some([0, 0, 0, 255]), stroke: None, even_odd: false, region: None };
+ // 400 of them pass the estimate (400 x 4 x 512 is under a million) but cost about 52 million.
+ assert!(crate::cliprect::clip_raw(raw(400), &clip).is_empty(), "the growing work is charged");
+ assert_eq!(crate::cliprect::clip_raw(raw(2), &clip).len(), 1, "a couple of them are still clipped");
+}