From 6ec7e4719764b7aa4e860217738ce8f906e92b30 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Wed, 5 Aug 2026 13:45:50 +0200 Subject: [PATCH] :zap: Bound save_layer rects in the vector export path --- render-wasm/src/render/text.rs | 70 +++++++++++++++++++++++++------- render-wasm/src/render/vector.rs | 67 +++++++++++++++++++++++------- render-wasm/src/shapes.rs | 15 ++++++- 3 files changed, 123 insertions(+), 29 deletions(-) diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index c176084688..c1316ad11b 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -329,15 +329,21 @@ fn render_text_on_canvas( layer_opacity: Option, overlay_emoji: bool, ) { + let layer_bounds = shape.layer_bounds(); + if let Some(blur_filter) = blur { let mut blur_paint = Paint::default(); blur_paint.set_image_filter(blur_filter.clone()); - let blur_layer = SaveLayerRec::default().paint(&blur_paint); + let blur_layer = SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&blur_paint); canvas.save_layer(&blur_layer); } if let Some(shadow_paint) = shadow { - let layer_rec = SaveLayerRec::default().paint(shadow_paint); + let layer_rec = SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(shadow_paint); canvas.save_layer(&layer_rec); draw_text( canvas, @@ -351,7 +357,9 @@ fn render_text_on_canvas( if let Some(erode) = skia_safe::image_filters::erode((eps, eps), None, None) { let mut layer_paint = Paint::default(); layer_paint.set_image_filter(erode); - let layer_rec = SaveLayerRec::default().paint(&layer_paint); + let layer_rec = SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&layer_paint); canvas.save_layer(&layer_rec); draw_text( canvas, @@ -582,7 +590,10 @@ fn draw_decoration_stroke( skia::BlendMode::SrcOut }; - canvas.save_layer(&SaveLayerRec::default()); + let outset = stroke_paint.stroke_width().max(0.0); + let layer_bounds = bar.with_outset((outset, outset)); + + canvas.save_layer(&SaveLayerRec::default().bounds(&layer_bounds)); let mut mask_paint = Paint::default(); mask_paint.set_color(skia::Color::BLACK); mask_paint.set_anti_alias(true); @@ -590,7 +601,11 @@ fn draw_decoration_stroke( let mut blend_paint = Paint::default(); blend_paint.set_blend_mode(blend); - canvas.save_layer(&SaveLayerRec::default().paint(&blend_paint)); + canvas.save_layer( + &SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&blend_paint), + ); canvas.draw_rect(bar, stroke_paint); canvas.restore(); canvas.restore(); @@ -705,7 +720,12 @@ pub fn render_emoji_overlay( if let Some(blur_filter) = blur { let mut blur_paint = Paint::default(); blur_paint.set_image_filter(blur_filter.clone()); - canvas.save_layer(&SaveLayerRec::default().paint(&blur_paint)); + let layer_bounds = shape.layer_bounds(); + canvas.save_layer( + &SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&blur_paint), + ); } for (emoji_para, deco_para) in emoji_layout @@ -728,13 +748,17 @@ fn draw_text( layer_opacity: Option, overlay_emoji: bool, ) { + let layer_bounds = shape.layer_bounds(); + if let Some(opacity) = layer_opacity { let mut opacity_paint = Paint::default(); opacity_paint.set_alpha_f(opacity); - let layer_rec = SaveLayerRec::default().paint(&opacity_paint); + let layer_rec = SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&opacity_paint); canvas.save_layer(&layer_rec); } else { - canvas.save_layer(&SaveLayerRec::default()); + canvas.save_layer(&SaveLayerRec::default().bounds(&layer_bounds)); } paint_text_with_emoji_overlay(canvas, shape, paragraph_builder_groups, overlay_emoji); @@ -759,27 +783,41 @@ fn render_masked_stroke_on_canvas( blur: Option<&ImageFilter>, layer_opacity: Option, ) { + let layer_bounds = shape.layer_bounds(); + if let Some(blur_filter) = blur { let mut blur_paint = Paint::default(); blur_paint.set_image_filter(blur_filter.clone()); - canvas.save_layer(&SaveLayerRec::default().paint(&blur_paint)); + canvas.save_layer( + &SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&blur_paint), + ); } if let Some(opacity) = layer_opacity { let mut opacity_paint = Paint::default(); opacity_paint.set_alpha_f(opacity); - canvas.save_layer(&SaveLayerRec::default().paint(&opacity_paint)); + canvas.save_layer( + &SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&opacity_paint), + ); } - canvas.save_layer(&SaveLayerRec::default()); + canvas.save_layer(&SaveLayerRec::default().bounds(&layer_bounds)); - canvas.save_layer(&SaveLayerRec::default()); + canvas.save_layer(&SaveLayerRec::default().bounds(&layer_bounds)); paint_text(canvas, shape, mask_builders); let mut stroke_paint = Paint::default(); stroke_paint.set_blend_mode(stroke_mask_blend); - canvas.save_layer(&SaveLayerRec::default().paint(&stroke_paint)); + canvas.save_layer( + &SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&stroke_paint), + ); paint_text(canvas, shape, stroke_builders); @@ -789,7 +827,11 @@ fn render_masked_stroke_on_canvas( if let Some(fill_builders) = fill_builders { let mut dst_over_paint = Paint::default(); dst_over_paint.set_blend_mode(skia::BlendMode::DstOver); - canvas.save_layer(&SaveLayerRec::default().paint(&dst_over_paint)); + canvas.save_layer( + &SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&dst_over_paint), + ); paint_text(canvas, shape, fill_builders); diff --git a/render-wasm/src/render/vector.rs b/render-wasm/src/render/vector.rs index 23b797023e..a536955c6d 100644 --- a/render-wasm/src/render/vector.rs +++ b/render-wasm/src/render/vector.rs @@ -79,11 +79,14 @@ impl ShapeRenderer for VectorRenderer<'_> { } fn draw_drop_shadows(&mut self, shape: &Shape) -> Result<()> { + let layer_bounds = shape.layer_bounds(); for shadow in shape.drop_shadows_visible() { if let Some(filter) = shadow.get_drop_shadow_filter() { let mut paint = Paint::default(); paint.set_image_filter(filter); - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); + let layer_rec = skia::canvas::SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&paint); self.canvas.save_layer(&layer_rec); let mut fill_paint = Paint::default(); fill_paint.set_anti_alias(true); @@ -99,10 +102,14 @@ impl ShapeRenderer for VectorRenderer<'_> { if !shape.has_fills() { return Ok(()); } + let layer_bounds = shape.layer_bounds(); for shadow in shape.inner_shadows_visible() { let paint = shadow.get_inner_shadow_paint(true, shape.image_filter(1.).as_ref()); - self.canvas - .save_layer(&skia::canvas::SaveLayerRec::default().paint(&paint)); + self.canvas.save_layer( + &skia::canvas::SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&paint), + ); let mut fill_paint = Paint::default(); fill_paint.set_anti_alias(true); fill_paint.set_color(skia::Color::BLACK); @@ -161,9 +168,13 @@ impl ShapeRenderer for VectorRenderer<'_> { }) .collect(); + let layer_bounds = shape.layer_bounds(); for shadow_paint in &drop_shadows { - self.canvas - .save_layer(&skia::canvas::SaveLayerRec::default().paint(shadow_paint)); + self.canvas.save_layer( + &skia::canvas::SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(shadow_paint), + ); text::render_overlay_emoji( self.canvas, @@ -331,7 +342,10 @@ impl ShapeRenderer for VectorRenderer<'_> { if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) { let mut paint = Paint::default(); paint.set_image_filter(filter); - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); + let layer_bounds = shape.layer_bounds(); + let layer_rec = skia::canvas::SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&paint); self.canvas.save_layer(&layer_rec); true } else { @@ -715,7 +729,10 @@ fn render_group( } } - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); + let layer_bounds = element.extrect(tree, scale); + let layer_rec = skia::canvas::SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&paint); canvas.save_layer(&layer_rec); } @@ -726,7 +743,12 @@ fn render_group( // as content, then re-draw the mask silhouette (the group's first child) // with DstIn to clip everything to it. let paint = Paint::default(); - canvas.save_layer(&skia::canvas::SaveLayerRec::default().paint(&paint)); + let subtree_bounds = element.extrect(tree, scale); + canvas.save_layer( + &skia::canvas::SaveLayerRec::default() + .bounds(&subtree_bounds) + .paint(&paint), + ); for child_id in &children { render_tree_inner(shared, canvas, child_id, tree, scale, opts)?; @@ -735,7 +757,11 @@ fn render_group( if let Some(mask_id) = element.mask_id() { let mut mask_paint = Paint::default(); mask_paint.set_blend_mode(skia::BlendMode::DstIn); - canvas.save_layer(&skia::canvas::SaveLayerRec::default().paint(&mask_paint)); + canvas.save_layer( + &skia::canvas::SaveLayerRec::default() + .bounds(&subtree_bounds) + .paint(&mask_paint), + ); render_tree_inner(shared, canvas, mask_id, tree, scale, opts)?; canvas.restore(); // mask layer } @@ -797,7 +823,10 @@ fn render_frame( } } - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); + let layer_bounds = element.extrect(tree, scale); + let layer_rec = skia::canvas::SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&paint); canvas.save_layer(&layer_rec); } @@ -857,13 +886,18 @@ fn render_container_drop_shadows( draw_fills: bool, opts: &TreeOpts, ) -> Result<()> { + let subtree_bounds = element.extrect(tree, scale); for shadow in element.drop_shadows_visible() { let Some(filter) = shadow.get_drop_shadow_filter() else { continue; }; let mut paint = Paint::default(); paint.set_image_filter(filter); - canvas.save_layer(&skia::canvas::SaveLayerRec::default().paint(&paint)); + canvas.save_layer( + &skia::canvas::SaveLayerRec::default() + .bounds(&subtree_bounds) + .paint(&paint), + ); if draw_fills && !element.fills.is_empty() { let mut renderer = VectorRenderer::new(canvas, shared, scale); @@ -902,7 +936,10 @@ fn render_leaf( let mut paint = Paint::default(); paint.set_blend_mode(element.blend_mode().into()); paint.set_alpha_f(element.opacity()); - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); + let layer_bounds = element.layer_bounds(); + let layer_rec = skia::canvas::SaveLayerRec::default() + .bounds(&layer_bounds) + .paint(&paint); canvas.save_layer(&layer_rec); } @@ -1101,7 +1138,8 @@ fn draw_stroke_kind_aware(canvas: &Canvas, shape: &Shape, stroke: &Stroke, paint } StrokeKind::Outer => { canvas.save(); - canvas.save_layer(&skia::canvas::SaveLayerRec::default()); + let layer_bounds = shape.layer_bounds(); + canvas.save_layer(&skia::canvas::SaveLayerRec::default().bounds(&layer_bounds)); draw_shape_geometry(canvas, shape, paint); let mut clear_paint = Paint::default(); clear_paint.set_blend_mode(skia::BlendMode::Clear); @@ -1134,7 +1172,8 @@ fn draw_image_stroke( let container = shape.selrect; canvas.save(); - canvas.save_layer(&skia::canvas::SaveLayerRec::default()); + let layer_bounds = shape.layer_bounds(); + canvas.save_layer(&skia::canvas::SaveLayerRec::default().bounds(&layer_bounds)); // Opaque stroke silhouette; the SrcIn image draw below fills it. draw_stroke_geometry(canvas, scale, shape, stroke, true); diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 47269c5288..dbdeb74be2 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1070,7 +1070,7 @@ impl Shape { extrect } - fn calculate_extrect_uncached(&self, shapes_pool: ShapesPoolRef, scale: f32) -> math::Rect { + fn own_extrect_bounds(&self) -> Bounds { let shape = self; let max_stroke = Stroke::max_bounds_width(shape.strokes.iter(), shape.is_open()); @@ -1096,6 +1096,19 @@ impl Shape { bounds = self.apply_stroke_bounds(bounds, max_stroke); bounds = self.apply_shadow_bounds(bounds); bounds = self.apply_blur_bounds(bounds); + bounds + } + + /// Bound for a `SaveLayerRec` wrapping this shape's own drawing, in + /// untransformed space (callers concatenate [`Self::centered_transform`] + /// first). Includes shadow/blur margins, so it is also a valid input bound + /// for a layer whose paint carries an image filter. + pub fn layer_bounds(&self) -> math::Rect { + self.own_extrect_bounds().to_rect() + } + + fn calculate_extrect_uncached(&self, shapes_pool: ShapesPoolRef, scale: f32) -> math::Rect { + let mut bounds = self.own_extrect_bounds(); bounds = self.apply_children_bounds(bounds, shapes_pool, scale); bounds = self.apply_children_blur(bounds, shapes_pool);