diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 637f0ae..c20700c 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -785,7 +785,12 @@ 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 picture (or picture fill) casts its shadow from its pixels, not its frame. + let content = match (&s.kind, &fill) { + (ShapeKind::Picture { fill: pf }, _) | (_, Some(Fill::Picture(pf))) => Some(paint::resolved(rctx, pf, None).into_owned()), + _ => None, + }; + self.effects_below(ctx, rctx, e, fx_ph, &geo, &fill, content.as_ref(), m, w, h); } let soft = effects.as_ref().and_then(|e| e.soft_edge).filter(|r| *r > 0.0); if let Some(r) = soft { @@ -959,6 +964,7 @@ impl Renderer { ph: Option, geo: &Geometry, fill: &Option, + content: Option<&deckcraft_model::style::PictureFill>, m: Affine, w: f64, h: f64, @@ -986,8 +992,24 @@ impl Renderer { // Blurred by a third of the blur radius, as PowerPoint's export shows. ctx.push_layer(None, None, Some((c.a as f32) / 255.0), None, Some(blur(sh.blur / 3.0 * k))); ctx.set_transform(sm); - ctx.set_paint(color(c.with_alpha(255), 1.0)); - ctx.fill_path(&silhouette); + match content { + // As PowerPoint draws it: the shadow is the picture's own coverage (a logo on a + // transparent background casts the logo's shadow): the picture, clipped to the + // shape, recoloured to the shadow colour. + Some(pf) => { + ctx.push_clip_layer(&silhouette); + paint::picture(ctx, rctx.pres, pf, Rect::new(0.0, 0.0, w, h), sm, None); + 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)); + ctx.pop_layer(); + ctx.pop_layer(); + } + None => { + ctx.set_paint(color(c.with_alpha(255), 1.0)); + ctx.fill_path(&silhouette); + } + } ctx.pop_layer(); } if let Some(g) = &e.glow { diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index f2a241b..5ccefb9 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -1506,3 +1506,50 @@ fn group_child_scale_does_not_scale_line_widths() { assert!(d * 20 < drawn, "{name}: {d} differing values against {drawn} inked pixels"); } } + +#[test] +fn a_picture_casts_its_shadow_from_its_pixels() { + // PowerPoint shades only where a picture paints: a logo on a transparent background casts the + // logo's shadow, not its frame's. Here the left half of the image is transparent. + let png = { + let img = image::RgbaImage::from_fn(8, 4, |x, _| if x < 4 { image::Rgba([0, 0, 0, 0]) } else { image::Rgba([20, 160, 20, 255]) }); + let mut out = std::io::Cursor::new(vec![]); + image::DynamicImage::ImageRgba8(img).write_to(&mut out, image::ImageFormat::Png).unwrap(); + out.into_inner() + }; + let shadow = deckcraft_model::style::Shadow { + color: ColorRef::rgb(deckcraft_color::Rgba::BLACK), + blur: 0.0, + dist: 20.0, + dir: 90.0, + inner: false, + sx: 1.0, + sy: 1.0, + kx: 0.0, + ky: 0.0, + align: String::new(), + rotate_with_shape: false, + }; + for as_fill in [false, true] { + let mut p = Presentation::default(); + let media = p.add_media("logo.png", "image/png", png.clone()); + let pf = deckcraft_model::style::PictureFill { media, ..Default::default() }; + let mut sh = Shape { id: ShapeId(600), xfrm: Some(Xfrm::new(100.0, 100.0, 400.0, 200.0)), ..Default::default() }; + if as_fill { + sh.fill = Some(Fill::Picture(pf)); + sh.line = Some(deckcraft_model::style::Line { fill: Some(Fill::None), ..Default::default() }); + } else { + sh.kind = ShapeKind::Picture { fill: pf }; + } + sh.effects = Some(deckcraft_model::Effects { outer_shadow: Some(shadow.clone()), ..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() }); + // Below the picture, 10 pt down: shadow under the opaque right half only. + let left = img.pixel(200, 310); + let right = img.pixel(400, 310); + assert!(left[0] > 240, "no shadow under the transparent half ({as_fill}): {left:?}"); + assert!(right[0] < 40, "shadow under the opaque half ({as_fill}): {right:?}"); + // The transparent half shows the slide, not a shadow behind it. + assert!(img.pixel(200, 200)[0] > 240, "{as_fill}"); + } +}