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.
This commit is contained in:
Магда М.Г.
2026-10-14 20:44:01 +02:00
parent 0c2c433ade
commit 8d68b2fe9d
2 changed files with 33 additions and 1 deletions
+5 -1
View File
@@ -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 {
+28
View File
@@ -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<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();