diff --git a/crates/eps/src/import/ai/paint.rs b/crates/eps/src/import/ai/paint.rs index 94c65ef89..b624a2247 100644 --- a/crates/eps/src/import/ai/paint.rs +++ b/crates/eps/src/import/ai/paint.rs @@ -422,6 +422,18 @@ mod tests { assert_eq!((geom.start, geom.end), (Point::new(10.0, 10.0), Point::new(50.0, 10.0))); } + #[test] + fn gradient_stops_beyond_the_cap_are_ignored() { + let mut d = GradientDef::begin(&[V::Str(b"H".to_vec()), V::Num(0.0)]).unwrap(); + let mut spots = vec![]; + for i in 0..257 { + let ramp = i as f64 / 256.0 * 100.0; + d.stop(&nums(&[0.2, 0.4, 0.6, 0.0, 1.0, 1.0, 6.0, 50.0, ramp]), &mut spots); + } + let (_, g) = d.finish().unwrap(); + assert_eq!(g.stops.len(), MAX_STOPS); + } + /// A linear gradient of three stops as newer files place it: `Bg` with numbers that aren't in /// art space, `Xm` for the whole ramp and a `Bm` for each pair of stops. #[test] diff --git a/crates/eps/src/import/ai/style.rs b/crates/eps/src/import/ai/style.rs index 68739fb05..389b0657e 100644 --- a/crates/eps/src/import/ai/style.rs +++ b/crates/eps/src/import/ai/style.rs @@ -764,6 +764,20 @@ mod tests { assert_eq!(feather.params["radius"].as_f64(), Some(5.0)); } + #[test] + fn art_styles_clear_operands_beyond_the_cap() { + let mut data = Vec::from( + b"/KnownStyle :\n(Soft) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n/BasicFilter :\n(Adobe Feather) 1 0 /Filter ,\n1 /Visible ,\n", + ); + for _ in 0..80 { + data.extend_from_slice(b"1 "); + } + data.extend_from_slice(b"/Dictionary : /NotRecorded ,\n; /Dict ;\n /Part ,\n;\n"); + let styles = art_styles(&data); + let [feather] = styles["Soft"].effects.as_slice() else { panic!() }; + assert_eq!(feather.params["radius"].as_f64(), Some(5.0)); + } + #[test] fn art_styles_ignore_filters_beyond_the_cap() { const UNIT: &[u8] = b"/BasicFilter :\n(Adobe Feather) 1 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n; /Dict ;\n /Part ,\n"; @@ -781,14 +795,80 @@ mod tests { fn hostile_dash_offsets_and_width_profiles_are_tamed() { let mut f = Filter::default(); paint_op(&mut f, "d", &[V::Arr(vec![V::Num(5.0)]), V::Num(f64::NAN)]); - assert_eq!(f.dash.map(|d| d.offset), Some(0.0)); + assert_eq!(f.dash.as_ref().map(|d| d.offset), Some(0.0)); // Positions outside 0..=1 and out of order, a negative width, a non-finite one. let arrays = HashMap::from([ ("BSVariableWidthTValuesArray".to_string(), vec![1.5, -0.2, 0.5, 0.7]), ("BSCenteredWidthsArray".to_string(), vec![-1.0, 2.0, 0.5, f64::INFINITY]), ]); assert_eq!(width_profile(&arrays).map(|p| p.points), Some(vec![(0.0, 2.0, 2.0), (0.5, 0.5, 0.5), (1.0, 0.0, 0.0)])); + let ok = (0..64).map(|_| V::Num(2.0)).collect::>(); + paint_op(&mut f, "d", &[V::Arr(ok), V::Num(0.0)]); + assert_eq!(f.dash.as_ref().map(|d| d.pattern.len()), Some(64)); + let huge = (0..65).map(|_| V::Num(1.0)).collect::>(); + paint_op(&mut f, "d", &[V::Arr(huge), V::Num(0.0)]); + assert_eq!(f.dash, None); + let long = (0..1025).map(|i| i as f64 / 1024.0).collect::>(); + let wide = vec![1.0; 1025]; + let huge = HashMap::from([("BSVariableWidthTValuesArray".to_string(), long), ("BSCenteredWidthsArray".to_string(), wide)]); + assert_eq!(width_profile(&huge), None); } + + #[test] + fn hostile_stroke_widths_are_sanitized() { + let mut f = Filter::default(); + paint_op(&mut f, "w", &[V::Num(12.0)]); + assert_eq!(f.width, 12.0); + paint_op(&mut f, "w", &[V::Num(-1.0)]); + assert_eq!(f.width, 12.0); + paint_op(&mut f, "w", &[V::Num(f64::INFINITY)]); + assert_eq!(f.width, 12.0); + } + + #[test] + fn hostile_stroke_paint_flags_ignore_short_operands() { + let mut f = Filter { paints: (true, true), ..Filter::default() }; + paint_op(&mut f, "Xd", &[V::Num(1.0)]); + assert_eq!(f.paints, (true, true)); + paint_op(&mut f, "Xd", &[V::Num(0.0), V::Num(1.0)]); + assert_eq!(f.paints, (false, true)); + paint_op(&mut f, "Xd", &[V::Num(2.0), V::Num(0.0)]); + assert_eq!(f.paints, (true, false)); + } + + #[test] + fn hostile_stroke_miter_limits_are_capped() { + let mut f = Filter::default(); + paint_op(&mut f, "M", &[V::Num(900.0)]); + assert_eq!(f.miter, 500.0); + paint_op(&mut f, "M", &[V::Num(0.5)]); + assert_eq!(f.miter, 500.0); + } + + #[test] + fn hostile_stroke_caps_and_joins_ignore_invalid_values() { + let mut f = Filter::default(); + paint_op(&mut f, "J", &[V::Num(99.0)]); + assert_eq!(f.cap, LineCap::Butt); + paint_op(&mut f, "J", &[V::Num(2.0)]); + assert_eq!(f.cap, LineCap::Square); + paint_op(&mut f, "j", &[V::Num(99.0)]); + assert_eq!(f.join, LineJoin::Miter); + paint_op(&mut f, "j", &[V::Num(1.0)]); + assert_eq!(f.join, LineJoin::Round); + } + + #[test] + fn hostile_blend_opacities_are_clamped() { + let mut f = Filter::default(); + paint_op(&mut f, "Xy", &[V::Num(3.0), V::Num(1.5)]); + assert_eq!(f.xy, Some((3, 1.0))); + paint_op(&mut f, "Xy", &[V::Num(-1.0), V::Num(0.25)]); + assert_eq!(f.xy, Some((0, 0.25))); + paint_op(&mut f, "Xy", &[V::Num(0.0), V::Num(f64::NAN)]); + assert_eq!(f.xy, Some((0, 0.25))); + } + /// A basic filter `name` whose dictionary holds `body`. fn basic(name: &str, body: &str) -> String { format!("/BasicFilter :\n({name}) 0 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n{body}; /Dict ;\n /Part ,\n") diff --git a/crates/pathops/src/budget.rs b/crates/pathops/src/budget.rs index ab7ead8a6..01559ce8a 100644 --- a/crates/pathops/src/budget.rs +++ b/crates/pathops/src/budget.rs @@ -15,14 +15,18 @@ pub const MAX_SEGMENT_PAIRS: usize = 1 << 22; /// Fenwick tree of counts over its slots. struct Counts(Vec); -// `isolate_lowest_one` is still unstable in Rust 1.96, and the workspace builds on 1.95 (rust-version). -#[allow(clippy::manual_isolate_lowest_one)] +#[inline] +fn fenwick_step(i: usize) -> usize { + // Same as `i.isolate_lowest_one()`; that API is still unstable on the workspace MSRV. + 1 << i.trailing_zeros() +} + impl Counts { fn add(&mut self, slot: usize, d: isize) { let mut i = slot + 1; while let Some(c) = self.0.get_mut(i) { *c += d; - i += i & i.wrapping_neg(); + i += fenwick_step(i); } } @@ -31,7 +35,7 @@ impl Counts { let (mut i, mut s) = (end.min(self.0.len().saturating_sub(1)), 0); while i > 0 { s += self.0.get(i).copied().unwrap_or(0); - i -= i & i.wrapping_neg(); + i -= fenwick_step(i); } s }