From c401151541c3265395ee158b665940c51471bb3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Mon, 19 Oct 2026 12:07:51 +0200 Subject: [PATCH] :bug: Fix mask group drop shadow in PDF export (#12362) --- render-wasm/src/render/pdf.rs | 1 + render-wasm/src/render/vector.rs | 73 +++++++++++++++++++------------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/render-wasm/src/render/pdf.rs b/render-wasm/src/render/pdf.rs index c61fef322b..b5b7366509 100644 --- a/render-wasm/src/render/pdf.rs +++ b/render-wasm/src/render/pdf.rs @@ -42,6 +42,7 @@ pub fn render_to_pdf( let metadata = skia::pdf::Metadata { creator: "Penpot".to_string(), producer: "Penpot (Skia PDF)".to_string(), + raster_dpi: Some(300.0), ..Default::default() }; diff --git a/render-wasm/src/render/vector.rs b/render-wasm/src/render/vector.rs index 15a8dc0c32..7eddda2b8b 100644 --- a/render-wasm/src/render/vector.rs +++ b/render-wasm/src/render/vector.rs @@ -822,34 +822,7 @@ fn render_group( let child_opts = opts.for_children_of(element); if masked { - // Mirror the GPU mask: render all children (including the mask shape) - // as content, then re-draw the mask silhouette (the group's first child) - // with DstIn to clip everything to it. - let paint = Paint::default(); - 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, &child_opts)?; - } - - 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() - .bounds(&subtree_bounds) - .paint(&mask_paint), - ); - render_tree_inner(shared, canvas, mask_id, tree, scale, &child_opts)?; - canvas.restore(); // mask layer - } - - canvas.restore(); // composition layer + render_masked_children(shared, canvas, element, &children, tree, scale, &child_opts)?; } else { for child_id in &children { render_tree_inner(shared, canvas, child_id, tree, scale, &child_opts)?; @@ -863,6 +836,42 @@ fn render_group( Ok(()) } +fn render_masked_children( + shared: &mut RenderResources, + canvas: &Canvas, + element: &Shape, + children: &[Uuid], + tree: ShapesPoolRef, + scale: f32, + child_opts: &TreeOpts, +) -> Result<()> { + let subtree_bounds = element.extrect(tree, scale); + canvas.save_layer( + &skia::canvas::SaveLayerRec::default() + .bounds(&subtree_bounds) + .paint(&Paint::default()), + ); + + for child_id in children { + render_tree_inner(shared, canvas, child_id, tree, scale, child_opts)?; + } + + 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() + .bounds(&subtree_bounds) + .paint(&mask_paint), + ); + render_tree_inner(shared, canvas, mask_id, tree, scale, child_opts)?; + canvas.restore(); // mask layer + } + + canvas.restore(); // composition layer + Ok(()) +} + // --------------------------------------------------------------------------- // Frames // --------------------------------------------------------------------------- @@ -991,8 +1000,12 @@ fn render_container_drop_shadows( let children = children_paint_order(tree, element); let child_opts = opts.for_children_of(element); - for child_id in &children { - render_tree_inner(shared, canvas, child_id, tree, scale, &child_opts)?; + if element.is_masked_group() { + render_masked_children(shared, canvas, element, &children, tree, scale, &child_opts)?; + } else { + for child_id in &children { + render_tree_inner(shared, canvas, child_id, tree, scale, &child_opts)?; + } } canvas.restore();