diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index eb139555..b1d9fce0 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1442,6 +1442,7 @@ pub(crate) fn stroke( if let Some(d) = &line.dash { let pat: Vec = d.pattern().iter().map(|v| v * lw.max(0.75)).collect(); let pat = dash_with_caps(pat, cap, sw); + let pat = limit_dash_count(path, pat); if !pat.is_empty() && pat.iter().all(|v| *v > 0.0) { if cap == kurbo::Cap::Square { sk = sk.with_caps(kurbo::Cap::Butt); @@ -1582,6 +1583,30 @@ fn dash_with_caps(mut pat: Vec, cap: kurbo::Cap, width: f64) -> Vec { pat } +/// The most dashes one stroke is cut into. Stroking expands every dash into its own outline, so a +/// tiny dash on a huge shape (a 1 EMU-wide `custDash` on a 2147483647 EMU rectangle) would +/// otherwise need gigabytes and abort the app when the slide is drawn. Past this count the +/// pattern is scaled up evenly, which looks the same: the dashes are far below a pixel by then. +const MAX_DASHES: f64 = 20_000.0; + +fn limit_dash_count(path: &BezPath, pat: Vec) -> Vec { + use kurbo::ParamCurveArclen; + let period: f64 = pat.iter().sum(); + if !(period.is_finite() && period > 0.0) { + return pat; + } + let len: f64 = path.segments().map(|s| s.arclen(1e-2)).sum(); + let dashes = len / period; + if !len.is_finite() { + return vec![]; + } + if dashes <= MAX_DASHES { + return pat; + } + let k = dashes / MAX_DASHES; + pat.into_iter().map(|v| v * k).collect() +} + fn end_points(path: &BezPath) -> Option<((Point, Point), (Point, Point))> { let els: Vec = path.elements().to_vec(); let mut pts: Vec = vec![]; diff --git a/crates/render/src/tests_wild.rs b/crates/render/src/tests_wild.rs index 7e7f0eac..672e7dcd 100644 --- a/crates/render/src/tests_wild.rs +++ b/crates/render/src/tests_wild.rs @@ -744,3 +744,31 @@ fn a_3d_shapes_face_is_lit_even_without_a_bevel() { let img = render(vec![shape(r#""#)]); assert!(close(img.pixel(175, 150), base, 1)); } + +#[test] +fn a_tiny_dash_on_a_huge_shape_draws_without_running_out_of_memory() { + // Found by fuzzing the pptx round trip: a 1 EMU `custDash` on a 2147483647 EMU square asked + // the stroker for hundreds of thousands of dashes and aborted on a 7.5 GB allocation. + let sh = Shape { + id: ShapeId(12), + xfrm: Some(Xfrm::new(0.0, 0.0, 169_093.0, 169_093.0)), + geom: Geom::preset("rect"), + line: Some(Line { + fill: solid(RED), + width: Some(0.0001), + dash: Some(deckcraft_model::style::Dash::Custom { pattern: vec![(1.0, 1.0)] }), + ..Default::default() + }), + ..Default::default() + }; + let img = render_slide(&deck_with(vec![sh]), 0, &RenderOpts { scale: 0.25, ..Default::default() }); + assert!(img.width > 0); + // The pattern is scaled up only past the limit; ordinary lines keep theirs. + let mut p = BezPath::new(); + p.move_to((0.0, 0.0)); + p.line_to((100.0, 0.0)); + assert_eq!(limit_dash_count(&p, vec![1.0, 1.0]), vec![1.0, 1.0]); + p.line_to((4.0e6, 0.0)); + let pat = limit_dash_count(&p, vec![1.0, 1.0]); + assert!((4.0e6 / (pat[0] + pat[1]) - MAX_DASHES).abs() < 1.0, "{pat:?}"); +}