Render: cap a stroke's dash count so a tiny dash on a huge shape can't abort on open

This commit is contained in:
Nikita Rakitin
2026-10-14 03:14:28 +03:00
parent 457406a7a5
commit ca2af8fe30
2 changed files with 53 additions and 0 deletions
+25
View File
@@ -1442,6 +1442,7 @@ pub(crate) fn stroke(
if let Some(d) = &line.dash {
let pat: Vec<f64> = 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<f64>, cap: kurbo::Cap, width: f64) -> Vec<f64> {
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<f64>) -> Vec<f64> {
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<PathEl> = path.elements().to_vec();
let mut pts: Vec<Point> = vec![];
+28
View File
@@ -744,3 +744,31 @@ fn a_3d_shapes_face_is_lit_even_without_a_bevel() {
let img = render(vec![shape(r#"<a:scene3d><a:camera prst="orthographicFront"/><a:lightRig rig="threePt" dir="t"/></a:scene3d>"#)]);
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:?}");
}