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
glow is unchanged.

Checked against PowerPoint's own PDF export of 31 real decks (597 slides):
the affected slides' mean pixel difference drops from ~29 to ~4 (of 255).
This commit is contained in:
Магда М.Г.
2026-10-12 14:52:32 +02:00
committed by Maksim
parent 0ec0cc5e81
commit 8ac6dc2f47
2 changed files with 41 additions and 4 deletions
+13 -4
View File
@@ -668,7 +668,9 @@ impl Renderer {
let fill = if let Some(t) = st.tint { Some(Fill::solid(deckcraft_model::ColorRef::rgb(t))) } else { fill };
// Effects below the shape.
if let Some(e) = &effects {
self.effects_below(ctx, rctx, e, fx_ph, &geo, &fill, m, w, h);
// A shape without a fill (an outline, a frame) glows along its line only.
let filled = is_pic || fill.as_ref().is_some_and(|f| !matches!(f, Fill::None));
self.effects_below(ctx, rctx, e, fx_ph, &geo, &fill, filled, m, w, h);
}
let soft = effects.as_ref().and_then(|e| e.soft_edge).filter(|r| *r > 0.0);
if let Some(r) = soft {
@@ -801,6 +803,7 @@ impl Renderer {
ph: Option<Rgba>,
geo: &Geometry,
fill: &Option<Fill>,
filled: bool,
m: Affine,
w: f64,
h: f64,
@@ -837,9 +840,15 @@ impl Renderer {
ctx.push_layer(None, None, Some((c.a as f32) / 255.0), None, Some(blur(g.radius * 0.35 * k)));
ctx.set_transform(m);
ctx.set_paint(color(c.with_alpha(255), 1.0));
ctx.set_stroke(kurbo::Stroke::new(g.radius.max(0.0) * 1.2).with_join(kurbo::Join::Round));
ctx.stroke_path(&silhouette);
ctx.fill_path(&silhouette);
if filled {
ctx.set_stroke(kurbo::Stroke::new(g.radius.max(0.0) * 1.2).with_join(kurbo::Join::Round));
ctx.stroke_path(&silhouette);
ctx.fill_path(&silhouette);
} else {
// The glow spreads its radius to both sides of the line; the inside stays clear.
ctx.set_stroke(kurbo::Stroke::new(g.radius.max(0.0) * 2.0).with_join(kurbo::Join::Round));
ctx.stroke_path(&silhouette);
}
ctx.pop_layer();
}
if let Some(rf) = &e.reflection {
+28
View File
@@ -124,6 +124,34 @@ fn effects_tables_charts_render() {
assert!(px[0] < 250, "{px:?}");
}
#[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<Fill>| {
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();