diff --git a/crates/metafile/src/cliprect.rs b/crates/metafile/src/cliprect.rs
index fd9dfc14a..bc7221541 100644
--- a/crates/metafile/src/cliprect.rs
+++ b/crates/metafile/src/cliprect.rs
@@ -11,6 +11,10 @@ use crate::geom::{Op, P};
/// Line segments per Bézier when a crossing shape is flattened.
const CURVE_STEPS: usize = 16;
+/// Point-against-edge tests one clipped item may cost (all its subpaths together); an item over
+/// it is dropped, as a single polygon over it already was.
+const MAX_CLIP_WORK: usize = 1_000_000;
+
/// The bounding box `[l, t, r, b]` of the points in `ops` (control points included).
pub(crate) fn ops_bbox(ops: &[Op]) -> Option<[f64; 4]> {
let mut b: Option<[f64; 4]> = None;
@@ -100,12 +104,12 @@ 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()) > 1_000_000 {
+ if poly.len().saturating_mul(clip.len()) > MAX_CLIP_WORK {
return Vec::new();
}
let mut poly = poly.to_vec();
for i in 0..clip.len() {
- if poly.len().saturating_mul(clip.len() - i) > 1_000_000 {
+ if poly.len().saturating_mul(clip.len() - i) > MAX_CLIP_WORK {
return Vec::new();
}
let (a, b) = (clip[i], clip[(i + 1) % clip.len()]);
@@ -158,6 +162,9 @@ pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec {
return Vec::new();
}
let subs = flatten(&ops);
+ if too_much_work(&subs, clip) {
+ return Vec::new();
+ }
let mut out = Vec::new();
if fill.is_some() {
let f = clip_fill(&subs, clip);
@@ -181,7 +188,11 @@ pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec {
if disjoint(b, cb) {
return Vec::new();
}
- let f = clip_fill(&flatten(&ops), clip);
+ let subs = flatten(&ops);
+ if too_much_work(&subs, clip) {
+ return Vec::new();
+ }
+ let f = clip_fill(&subs, clip);
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(),
@@ -230,6 +241,12 @@ pub(crate) fn flatten(ops: &[Op]) -> Vec<(Vec, bool)> {
subs
}
+/// Whether cutting every subpath of one item by `clip` would exceed [`MAX_CLIP_WORK`]; the
+/// per-polygon check alone lets many small subpaths each spend the whole allowance.
+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 {
let mut ops = Vec::new();
for (pts, _) in subs {
diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs
index 29fab81ec..ff6559ec1 100644
--- a/crates/metafile/src/tests.rs
+++ b/crates/metafile/src/tests.rs
@@ -2074,3 +2074,18 @@ fn clip_polygon_work_bound() {
let clipped = crate::cliprect::clip_polygon(&poly, &clip);
assert!(clipped.is_empty(), "excessive polygon clip product rejected");
}
+
+#[test]
+fn clip_work_is_bounded_across_subpaths() {
+ use crate::canvas::Raw;
+ use crate::geom::Op;
+ // 512-gon clip; 4,000 small triangles crossing its edge: each under the per-polygon cap, together
+ // far over it, for both the fill and the stroke.
+ 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();
+ let tri = |x: f64| [Op::M((x, 95.0)), Op::L((x + 1.0, 95.0)), Op::L((x, 300.0)), Op::Z];
+ let many: Vec = (0..4000).flat_map(|i| tri(i as f64 % 200.0)).collect();
+ let raw = |ops| Raw::Path { ops, fill: Some([0, 0, 0, 255]), stroke: Some(([0, 0, 0, 255], 1.0)), even_odd: false, region: None };
+ 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");
+}