From 3fd1dfed361bd8a8f13bfaf7342c8a84c8487390 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 11:17:52 +0000 Subject: [PATCH] metafile: bound clipping work across all subpaths of one drawn item --- crates/metafile/src/cliprect.rs | 23 ++++++++++++++++++++--- crates/metafile/src/tests.rs | 15 +++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) 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"); +}