From 8d68b2fe9d25c0b60ce60485a5dc3cc7cc795a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=B3=D0=B4=D0=B0=20=D0=9C=2E=D0=93=2E?= Date: Wed, 14 Oct 2026 20:35:43 +0200 Subject: [PATCH] Render: an unfilled shape's glow follows its line A glow always filled the shape's whole silhouette, so a frame with no fill (a rounded-rectangle border with a soft glow, common in corporate decks) came out as a solid tinted panel over the slide. PowerPoint glows such a shape along its outline only and leaves the inside clear; a filled shape's or a picture's glow is unchanged. The line glow uses the same width and blur as the outline part of a filled shape's glow. Checked against PowerPoint's own PDF export of 31 real decks (597 slides): 66 slides closer to PowerPoint, none further; the frames' slides go from a mean pixel difference of ~29 to ~2.5-8 (of 255), and the 90th percentile over all slides from 12.9 to 10.0. --- crates/render/src/lib.rs | 6 +++++- crates/render/src/tests.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index df7e43c2..89bb07ef 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1388,7 +1388,11 @@ impl Renderer { ctx.set_paint(color(c.with_alpha(255), 1.0)); ctx.set_stroke(kurbo::Stroke::new(g.radius.max(0.0) * 0.95).with_join(kurbo::Join::Round)); ctx.stroke_path(&silhouette); - ctx.fill_path(&silhouette); + // A shape without a fill (an outline, a frame) glows along its line only: the inside + // stays clear, as PowerPoint draws it. + if content.is_some() || fill_alpha.is_some() { + ctx.fill_path(&silhouette); + } ctx.pop_layer(); } if let Some(rf) = &e.reflection { diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index d7aec2a6..e5f8d450 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -344,6 +344,34 @@ fn horizontal_bars_list_the_first_category_at_the_bottom_unless_reversed() { assert!(draw(true) < 180, "reversed: first category at the top: {}", draw(true)); } +#[test] +fn glow_of_an_unfilled_shape_follows_its_line() { + // A frame (no fill, thin line) with a strong glow: the glow lines its edge, the inside stays + // the slide's white, as PowerPoint draws it. + let glow = |fill: Option| { + let mut sh = Shape { + id: ShapeId(610), + xfrm: Some(Xfrm::new(100.0, 100.0, 400.0, 300.0)), + fill, + line: Some(deckcraft_model::style::Line::solid(ColorRef::rgb(deckcraft_color::Rgba::rgb(0, 176, 240)), 0.25)), + ..Default::default() + }; + sh.effects = Some(deckcraft_model::Effects { + glow: Some(deckcraft_model::style::Glow { color: ColorRef::rgb(deckcraft_color::Rgba::rgb(0, 0, 255)), radius: 8.0 }), + ..Default::default() + }); + render_slide(&deck_with(vec![sh]), 0, &RenderOpts { scale: 1.0, ..Default::default() }) + }; + let img = glow(Some(Fill::None)); + let inside = img.pixel(300, 250); + assert!(inside[0] > 245 && inside[1] > 245, "{inside:?}"); + let edge = img.pixel(300, 97); + assert!(edge[0] < 230, "{edge:?}"); + // A filled shape's glow is still the whole silhouette's (the fill covers its inside). + let img = glow(Some(Fill::solid(ColorRef::rgb(deckcraft_color::Rgba::rgb(255, 255, 255))))); + assert!(img.pixel(300, 94)[0] < 240); +} + #[test] fn placeholder_prompts_only_in_edit_view() { let p = Presentation::default();