From fa1db7809ab2ea2e26495bbdc7a7978eacffd231 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 07:09:54 -0400 Subject: [PATCH] Render: a picture's shadow covers the whole picture The shadow of a picture is the picture recoloured to the shadow colour, but drawing the picture left its pixel-space transform set, so the recolouring rectangle (sized in points) covered only part of a picture whose bitmap is bigger than it is shown: a 400 px photo at 240 pt cast a shadow of its top-left corner only. The shape's transform is restored before recolouring. Wild synthetic set: framed pictures with shadows 7.67 -> 5.95, rotated picture with a shadow 10.67 -> 10.07. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/render/src/lib.rs | 3 +++ crates/render/src/tests_wild.rs | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 4e84051..c4da515 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1224,6 +1224,9 @@ impl Renderer { Some(pf) => { ctx.push_clip_layer(&silhouette); paint::picture(ctx, rctx.pres, pf, Rect::new(0.0, 0.0, w, h), sm, None); + // Drawing the picture leaves its own pixel transform set: back to the shape's, + // or the recolouring below covers only part of a large picture's shadow. + ctx.set_transform(sm); ctx.push_layer(None, Some(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::SrcIn)), None, None, None); ctx.set_paint(color(c.with_alpha(255), 1.0)); ctx.fill_rect(&Rect::new(0.0, 0.0, w, h).inflate(1.0, 1.0)); diff --git a/crates/render/src/tests_wild.rs b/crates/render/src/tests_wild.rs index 9a885c9..846bce6 100644 --- a/crates/render/src/tests_wild.rs +++ b/crates/render/src/tests_wild.rs @@ -598,3 +598,37 @@ fn table_borders_take_their_dashes_and_diagonals_are_drawn() { // The diagonal from top-left to bottom-right passes through the middle. assert!(close(img.pixel(200, 150), Rgba::rgb(0, 0, 255), 40), "{:?}", img.pixel(200, 150)); } + +#[test] +fn a_large_pictures_shadow_is_whole() { + // A 400 px picture shown at 240 pt: its shadow was recoloured in the picture's pixel space and + // covered only part of it. + let mut counts = vec![]; + for side in [8u32, 400] { + let mut p = Presentation::default(); + let media = p.add_media("image1.png", "image/png", png(side, side * 3 / 4, [0, 0, 255])); + let mut fx = offset_shadow(); + if let Some(o) = &mut fx.outer_shadow { + o.blur = 8.0; + } + let sh = Shape { + id: ShapeId(15), + xfrm: Some(Xfrm { rot: 20.0, ..Xfrm::new(360.0, 300.0, 240.0, 160.0) }), + kind: ShapeKind::Picture { fill: deckcraft_model::style::PictureFill { media, ..Default::default() } }, + effects: Some(fx), + ..Default::default() + }; + std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![sh]; + let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() }); + counts.push( + (0..img.height) + .flat_map(|y| (0..img.width).map(move |x| (x, y))) + .filter(|(x, y)| { + let q = img.pixel(*x, *y); + q[0] < 200 && q[2] < 200 + }) + .count(), + ); + } + assert!(counts[1] > 3000 && (counts[0] as i64 - counts[1] as i64).abs() < 300, "{counts:?}"); +}