Skip to main content

vello_cpu/filter/
drop_shadow.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Drop shadow filter implementation.
5//!
6//! This implements the feDropShadow primitive from SVG Filter Effects 2.
7//! The drop shadow effect is a shorthand for a commonly used sequence of filter operations:
8//! 1. Extract alpha channel
9//! 2. Offset the alpha
10//! 3. Blur the offset alpha
11//! 4. Composite shadow color with blurred alpha
12//! 5. Composite shadow with original graphic
13//!
14//! @see <https://drafts.fxtf.org/filter-effects-2/#feDropShadowElement>
15
16use super::FilterEffect;
17use super::gaussian_blur::{MAX_KERNEL_SIZE, apply_blur, plan_decimated_blur};
18use super::shift::offset_pixels;
19use crate::layer_manager::LayerManager;
20use vello_common::color::{AlphaColor, Srgb};
21use vello_common::filter_effects::EdgeMode;
22use vello_common::peniko::color::PremulRgba8;
23#[cfg(not(feature = "std"))]
24use vello_common::peniko::kurbo::common::FloatFuncs as _;
25use vello_common::pixmap::Pixmap;
26
27pub(crate) struct DropShadow {
28    pub dx: f32,
29    pub dy: f32,
30    pub color: AlphaColor<Srgb>,
31    /// Standard deviation for the blur (for reference/debugging).
32    std_deviation: f32,
33    /// Edge mode for blur sampling.
34    edge_mode: EdgeMode,
35    /// Number of 2x2 decimation levels to use (0 means direct convolution).
36    n_decimations: usize,
37    /// Pre-computed Gaussian kernel weights for the reduced blur.
38    /// Only the first `kernel_size` elements are valid.
39    kernel: [f32; MAX_KERNEL_SIZE],
40    /// Actual length of the kernel (kernel is padded to `MAX_KERNEL_SIZE`).
41    kernel_size: usize,
42}
43
44impl DropShadow {
45    /// Create a new drop shadow filter with the specified parameters.
46    ///
47    /// This precomputes the blur decimation plan and kernel for optimal performance.
48    pub(crate) fn new(
49        dx: f32,
50        dy: f32,
51        std_deviation: f32,
52        edge_mode: EdgeMode,
53        color: AlphaColor<Srgb>,
54    ) -> Self {
55        // Precompute blur plan (same logic as GaussianBlur::new)
56        let (n_decimations, kernel, kernel_size) = plan_decimated_blur(std_deviation);
57
58        Self {
59            dx,
60            dy,
61            color,
62            std_deviation,
63            edge_mode,
64            n_decimations,
65            kernel,
66            kernel_size,
67        }
68    }
69}
70
71impl FilterEffect for DropShadow {
72    fn execute_lowp(&self, pixmap: &mut Pixmap, layer_manager: &mut LayerManager) {
73        apply_drop_shadow(
74            pixmap,
75            self.dx,
76            self.dy,
77            self.std_deviation,
78            self.n_decimations,
79            &self.kernel[..self.kernel_size],
80            self.color,
81            self.edge_mode,
82            layer_manager,
83        );
84    }
85
86    fn execute_highp(&self, pixmap: &mut Pixmap, layer_manager: &mut LayerManager) {
87        // TODO: Currently only lowp is implemented and used for highp as well.
88        // This needs to be updated to use proper high-precision arithmetic.
89        Self::execute_lowp(self, pixmap, layer_manager);
90    }
91}
92
93/// Apply drop shadow effect.
94///
95/// This is the main entry point that splits the drop shadow operation into well-defined steps:
96/// 1. Offset the shadow pixels
97/// 2. Blur the already-offset shadow
98/// 3. Apply shadow color and composite with original
99fn apply_drop_shadow(
100    pixmap: &mut Pixmap,
101    dx: f32,
102    dy: f32,
103    std_deviation: f32,
104    n_decimations: usize,
105    kernel: &[f32],
106    color: AlphaColor<Srgb>,
107    edge_mode: EdgeMode,
108    layer_manager: &mut LayerManager,
109) {
110    // Clone pixmap to create shadow buffer
111    let mut shadow_pixmap = pixmap.clone();
112
113    // Step 1: Offset the shadow pixels
114    offset_pixels(&mut shadow_pixmap, dx, dy);
115
116    // Step 2: Blur the already-offset shadow
117    if std_deviation > 0.0 {
118        let scratch =
119            layer_manager.get_scratch_buffer(shadow_pixmap.width(), shadow_pixmap.height());
120        apply_blur(
121            &mut shadow_pixmap,
122            scratch,
123            n_decimations,
124            kernel,
125            edge_mode,
126        );
127    }
128
129    // Step 3: Apply shadow color and composite with original
130    compose_shadow_direct(&shadow_pixmap, pixmap, color);
131}
132
133/// Apply shadow color and composite with original.
134///
135/// The shadow has already been offset and blurred, so this simply applies
136/// the shadow color to the alpha channel and composites using source-over.
137fn compose_shadow_direct(shadow: &Pixmap, dst: &mut Pixmap, color: AlphaColor<Srgb>) {
138    let width = dst.width();
139    let height = dst.height();
140
141    // Precompute shadow color components
142    let shadow_r = (color.components[0] * 255.0).round() as u8;
143    let shadow_g = (color.components[1] * 255.0).round() as u8;
144    let shadow_b = (color.components[2] * 255.0).round() as u8;
145
146    for y in 0..height {
147        for x in 0..width {
148            // Sample alpha directly (shadow is already offset)
149            let alpha = shadow.sample(x, y).a;
150
151            // Apply shadow color to alpha
152            let shadow_alpha = (u8_to_norm(alpha) * color.components[3]).min(1.0);
153            let final_alpha = norm_to_u8(shadow_alpha);
154
155            // Premultiply RGB by alpha as required by PremulRgba8
156            let alpha_u16 = u16::from(final_alpha);
157            let premultiply = |channel: u8| ((u16::from(channel) * alpha_u16) / 255) as u8;
158
159            let colored_shadow = PremulRgba8 {
160                r: premultiply(shadow_r),
161                g: premultiply(shadow_g),
162                b: premultiply(shadow_b),
163                a: final_alpha,
164            };
165
166            // Read original and composite: original over shadow
167            let original_pixel = dst.sample(x, y);
168            let result = compose_src_over(original_pixel, colored_shadow);
169
170            dst.set_pixel(x, y, result);
171        }
172    }
173}
174
175/// Composite two pixels using Porter-Duff "source over" operator.
176///
177/// Composes the source pixel over the destination pixel using premultiplied
178/// alpha blending. Returns the composited result.
179///
180/// Formula for premultiplied colors: `result = src + dst * (1 - src_alpha)`
181fn compose_src_over(src: PremulRgba8, dst: PremulRgba8) -> PremulRgba8 {
182    let src_a = u8_to_norm(src.a);
183
184    PremulRgba8 {
185        r: src_over_channel(src.r, dst.r, src_a),
186        g: src_over_channel(src.g, dst.g, src_a),
187        b: src_over_channel(src.b, dst.b, src_a),
188        a: src_over_channel(src.a, dst.a, src_a),
189    }
190}
191
192/// Blend a single channel using Porter-Duff "source over" operator.
193///
194/// For premultiplied colors, the formula is: `result = src + dst * (1 - src_alpha)`
195#[inline]
196fn src_over_channel(src: u8, dst: u8, src_alpha: f32) -> u8 {
197    let result = u8_to_norm(src) + u8_to_norm(dst) * (1.0 - src_alpha);
198    norm_to_u8(result)
199}
200
201/// Convert a u8 color component (0-255) to normalized f32 (0.0-1.0).
202#[inline]
203fn u8_to_norm(value: u8) -> f32 {
204    value as f32 / 255.0
205}
206
207/// Convert a normalized f32 (0.0-1.0) to u8 color component (0-255).
208#[inline]
209fn norm_to_u8(value: f32) -> u8 {
210    (value * 255.0).round() as u8
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use vello_common::color::Srgb;
217
218    /// Test `u8_to_norm` conversion.
219    #[test]
220    fn test_u8_to_norm() {
221        assert_eq!(u8_to_norm(0), 0.0);
222        assert!((u8_to_norm(255) - 1.0).abs() < 1e-6);
223    }
224
225    /// Test `norm_to_u8` conversion.
226    #[test]
227    fn test_norm_to_u8() {
228        assert_eq!(norm_to_u8(0.0), 0);
229        assert_eq!(norm_to_u8(1.0), 255);
230        assert_eq!(norm_to_u8(0.5), 128); // 0.5 * 255 = 127.5 → 128
231    }
232
233    /// Test round-trip conversion u8 → norm → u8.
234    #[test]
235    fn test_conversion_roundtrip() {
236        for value in [0, 1, 50, 127, 128, 200, 254, 255] {
237            let normalized = u8_to_norm(value);
238            let back = norm_to_u8(normalized);
239            assert_eq!(back, value);
240        }
241    }
242
243    /// Test Porter-Duff source-over with fully opaque source.
244    #[test]
245    fn test_compose_src_over_opaque_source() {
246        let src = PremulRgba8 {
247            r: 255,
248            g: 0,
249            b: 0,
250            a: 255,
251        }; // Opaque red
252        let dst = PremulRgba8 {
253            r: 0,
254            g: 255,
255            b: 0,
256            a: 255,
257        }; // Opaque green
258
259        let result = compose_src_over(src, dst);
260        // Opaque source should completely cover destination
261        assert_eq!(result.r, 255);
262        assert_eq!(result.g, 0);
263        assert_eq!(result.b, 0);
264        assert_eq!(result.a, 255);
265    }
266
267    /// Test Porter-Duff source-over with transparent source.
268    #[test]
269    fn test_compose_src_over_transparent_source() {
270        let src = PremulRgba8 {
271            r: 0,
272            g: 0,
273            b: 0,
274            a: 0,
275        };
276        let dst = PremulRgba8 {
277            r: 0,
278            g: 255,
279            b: 0,
280            a: 255,
281        };
282
283        let result = compose_src_over(src, dst);
284        // Transparent source should leave destination unchanged
285        assert_eq!(result.r, 0);
286        assert_eq!(result.g, 255);
287        assert_eq!(result.b, 0);
288        assert_eq!(result.a, 255);
289    }
290
291    /// Test Porter-Duff source-over with semi-transparent source.
292    #[test]
293    fn test_compose_src_over_semi_transparent() {
294        let src = PremulRgba8 {
295            r: 128,
296            g: 0,
297            b: 0,
298            a: 128,
299        }; // 50% red (premul)
300        let dst = PremulRgba8 {
301            r: 0,
302            g: 128,
303            b: 0,
304            a: 128,
305        }; // 50% green (premul)
306
307        let result = compose_src_over(src, dst);
308        // Result should blend src + dst*(1-src_alpha)
309        // r: 128 + 0*(1-0.5) = 128
310        // g: 0 + 128*0.5 = 64
311        // a: 128 + 128*0.5 = 192
312        assert_eq!(
313            result,
314            PremulRgba8 {
315                r: 128,
316                g: 64,
317                b: 0,
318                a: 192,
319            }
320        );
321    }
322
323    /// Test `compose_shadow_direct` applies color correctly.
324    #[test]
325    fn test_compose_shadow_color() {
326        let mut shadow_pixmap = Pixmap::new(2, 2);
327        let mut dst_pixmap = Pixmap::new(2, 2);
328
329        // Shadow has alpha=255 at (0,0)
330        shadow_pixmap.set_pixel(
331            0,
332            0,
333            PremulRgba8 {
334                r: 0,
335                g: 0,
336                b: 0,
337                a: 255,
338            },
339        );
340
341        let shadow_color = AlphaColor {
342            components: [1.0, 0.0, 0.0, 1.0], // Red
343            cs: std::marker::PhantomData::<Srgb>,
344        };
345
346        compose_shadow_direct(&shadow_pixmap, &mut dst_pixmap, shadow_color);
347
348        // Shadow at (0,0) should be red
349        let result = dst_pixmap.sample(0, 0);
350        assert_eq!(result.r, 255);
351        assert_eq!(result.g, 0);
352        assert_eq!(result.b, 0);
353        assert_eq!(result.a, 255);
354    }
355}