From b1645b006eafbe9899ba8c726612a628527b33ae Mon Sep 17 00:00:00 2001 From: Naseq Date: Tue, 27 Oct 2026 17:12:07 +0430 Subject: [PATCH] Render: skip shapes whose outline is not finite or far larger than any screen (#1165) An ellipse at the EMU limit, compounding group scales or custom-geometry guides could reach vello's flattener with an outline billions of pixels long; it sizes its buffer by that length and the allocation aborts the process. geometry::drawable() rejects a shape with a non-finite path point, then measures the outline's control box (which always holds the curve, unlike the tight bounding box when its extremum overflows) unioned with the frame; past 1e7 device pixels the shape is skipped with a warning. Rebased onto the render module split: the guard now lives in geometry.rs and renderer.rs. Co-Authored-By: Claude Opus 5.5 --- crates/render/src/geometry.rs | 18 +++++++++++ crates/render/src/renderer.rs | 8 ++++- crates/render/src/tests.rs | 56 +++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/crates/render/src/geometry.rs b/crates/render/src/geometry.rs index 12b22796..b7ca7e70 100644 --- a/crates/render/src/geometry.rs +++ b/crates/render/src/geometry.rs @@ -199,3 +199,21 @@ pub(crate) fn scale_of(m: Affine) -> f64 { pub(crate) fn blur(sigma: f64) -> Filter { Filter::from_primitive(FilterPrimitive::GaussianBlur { std_deviation: sigma.clamp(0.0, 500.0) as f32, edge_mode: EdgeMode::None }) } + +/// The widest a shape may reach on screen, in pixels, and still be drawn: ten thousand screens +/// across, far past any real deck, and still cheap for vello to flatten (#1165). +const MAX_DEVICE_EXTENT: f64 = 1e7; + +/// Whether a shape's geometry can go to vello. Geometry far beyond any screen (a size at the EMU +/// limit, group scales compounding, custom-geometry guides) makes vello's flattener size a buffer +/// by its on-screen length, and the allocation aborts the process (#1165). Each path is checked +/// for finite points first: a bounding box drops NaN, and a union with the frame would hide it. +/// The extent is the control box, which always holds the curve: the tight bounding box can miss +/// a curve's extremum when finding it overflows. +pub(crate) fn drawable(geo: &Geometry, m: Affine, w: f64, h: f64) -> bool { + if !geo.paths.iter().all(|p| p.path.is_finite()) { + return false; + } + let dev = m.transform_rect_bbox(geo.outline().control_box().union(Rect::new(0.0, 0.0, w, h))); + dev.is_finite() && dev.width().max(dev.height()) <= MAX_DEVICE_EXTENT +} diff --git a/crates/render/src/renderer.rs b/crates/render/src/renderer.rs index 510cb6c5..d865e323 100644 --- a/crates/render/src/renderer.rs +++ b/crates/render/src/renderer.rs @@ -1,4 +1,6 @@ -use super::geometry::{Caster, blur, border_outside, camera_turn, color, fill_opacity, outside_line, scale_of, shadow_local, shape_geometry}; +use super::geometry::{ + Caster, blur, border_outside, camera_turn, color, drawable, fill_opacity, outside_line, scale_of, shadow_local, shape_geometry, +}; use super::opts::{RenderOpts, ShapeState, SlideFields}; use super::raster::Image; use super::slide::{SlidePart, needs_filters, output_size}; @@ -542,6 +544,10 @@ impl Renderer { None => (self.scaled_geometry(s, m, depth, w, h), Vec::new()), }; let fade = |i: usize| fades.get(i).copied().unwrap_or(deckcraft_geom::morph::Fade { fill: 1.0, stroke: 1.0 }); + if !drawable(&geo, m, w, h) { + log::warn!("render: shape {} skipped: its outline is not finite or far larger than any screen", s.id.0); + return; + } let (fill, fill_ph) = resolve::fill(rctx, s); let (line, line_ph) = resolve::line(rctx, s); let (effects, fx_ph) = resolve::effects(rctx, s); diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index bcddf4c9..4be2639c 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -2951,3 +2951,59 @@ fn pie_data_labels_take_the_chart_text_colour_when_it_has_one() { assert!(count(&coloured, &red) > 30, "the labels take the chart's red: {} red pixels", count(&coloured, &red)); assert!(count(&plain, &white) > count(&coloured, &white), "the labels were white before"); } + +/// #1165: an ellipse sized at the EMU limit, crossing the slide, made vello's flattener allocate a +/// buffer from its on-screen length and abort. It is skipped; the rest of the slide still draws. +#[test] +fn a_shape_far_larger_than_any_screen_is_skipped_not_flattened() { + let huge = Shape { + id: ShapeId(501), + geom: Geom::preset("ellipse"), + xfrm: Some(Xfrm::new(-5e17, -5e17, 1e18, 1e18)), + style: Some(ShapeStyle::accent(deckcraft_color::SchemeSlot::Accent2)), + ..Default::default() + }; + let normal = Shape { + id: ShapeId(502), + xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 100.0)), + style: Some(ShapeStyle::accent(deckcraft_color::SchemeSlot::Accent1)), + ..Default::default() + }; + let p = deck_with(vec![huge, normal]); + let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() }); + let accent = p.masters[0].theme.colors.get(deckcraft_color::SchemeSlot::Accent1); + let px = img.pixel(200, 150); + assert!((px[0] as i32 - accent.r as i32).abs() <= 2, "{px:?} vs {accent:?}"); + assert_eq!(img.pixel(50, 50), [255, 255, 255, 255], "the huge ellipse is not drawn"); +} + +/// #1165: a path with a NaN point is not drawn, even when the shape's frame is finite and small +/// (a bounding box, or a union with the frame, would hide the NaN). +#[test] +fn a_shape_whose_outline_is_not_finite_is_not_drawable() { + let geo = |pts: &[(f64, f64)]| { + let mut path = kurbo::BezPath::new(); + path.move_to(pts[0]); + for p in &pts[1..] { + path.line_to(*p); + } + deckcraft_geom::preset::Geometry { + paths: vec![deckcraft_geom::preset::SubPath { path, fill: deckcraft_geom::preset::FillMode::Norm, stroke: true, even_odd: false }], + text_rect: kurbo::Rect::ZERO, + handles: vec![], + sites: vec![], + } + }; + let m = kurbo::Affine::IDENTITY; + assert!(crate::geometry::drawable(&geo(&[(0.0, 0.0), (100.0, 50.0)]), m, 100.0, 50.0)); + assert!(!crate::geometry::drawable(&geo(&[(0.0, 0.0), (f64::NAN, 50.0)]), m, 100.0, 50.0)); + assert!(!crate::geometry::drawable(&geo(&[(0.0, 0.0), (f64::INFINITY, 50.0)]), m, 100.0, 50.0)); + assert!(!crate::geometry::drawable(&geo(&[(0.0, 0.0), (1e18, 50.0)]), m, 100.0, 50.0)); + // A quadratic whose extremum overflows kurbo's bounding box: its control point still counts. + let mut path = kurbo::BezPath::new(); + path.move_to((0.0, 0.0)); + path.quad_to((1e308, 0.0), (0.0, 0.0)); + let mut quad = geo(&[(0.0, 0.0), (1.0, 1.0)]); + quad.paths[0].path = path; + assert!(!crate::geometry::drawable(&quad, m, 100.0, 50.0)); +}