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::apply_blur;
18use super::shift::offset_pixels;
19use crate::filter::context::ScratchBuffer;
20use vello_common::color::{AlphaColor, Srgb};
21use vello_common::filter::drop_shadow::DropShadow;
22use vello_common::filter_effects::EdgeMode;
23use vello_common::peniko::color::PremulRgba8;
24#[cfg(not(feature = "std"))]
25use vello_common::peniko::kurbo::common::FloatFuncs as _;
26use vello_common::pixmap::Pixmap;
27
28impl FilterEffect for DropShadow {
29    fn execute_lowp(&self, pixmap: &mut Pixmap, filter_scratch: &mut ScratchBuffer) {
30        apply_drop_shadow(
31            pixmap,
32            self.dx,
33            self.dy,
34            self.std_deviation,
35            self.n_decimations,
36            &self.kernel[..usize::from(self.kernel_size)],
37            self.color,
38            self.edge_mode,
39            self.composite_original,
40            filter_scratch,
41        );
42    }
43
44    fn execute_highp(&self, pixmap: &mut Pixmap, filter_scratch: &mut ScratchBuffer) {
45        // TODO: Currently only lowp is implemented and used for highp as well.
46        // This needs to be updated to use proper high-precision arithmetic.
47        Self::execute_lowp(self, pixmap, filter_scratch);
48    }
49}
50
51/// Apply drop shadow effect.
52///
53/// This is the main entry point that splits the drop shadow operation into well-defined steps:
54/// 1. Offset the shadow pixels
55/// 2. Blur the already-offset shadow
56/// 3. Apply shadow color and optionally composite with original
57fn apply_drop_shadow(
58    pixmap: &mut Pixmap,
59    dx: f32,
60    dy: f32,
61    std_deviation: f32,
62    n_decimations: usize,
63    kernel: &[f32],
64    color: AlphaColor<Srgb>,
65    edge_mode: EdgeMode,
66    composite_original: bool,
67    filter_scratch: &mut ScratchBuffer,
68) {
69    // Clone pixmap to create shadow buffer
70    let mut shadow_pixmap = pixmap.clone();
71
72    // Step 1: Offset the shadow pixels
73    offset_pixels(&mut shadow_pixmap, dx, dy);
74
75    // Step 2: Blur the already-offset shadow
76    if std_deviation > 0.0 {
77        let scratch =
78            filter_scratch.get_scratch_buffer(shadow_pixmap.width(), shadow_pixmap.height());
79        apply_blur(
80            &mut shadow_pixmap,
81            scratch,
82            n_decimations,
83            kernel,
84            edge_mode,
85        );
86    }
87
88    // Step 3: Apply shadow color and optionally composite with original
89    write_colored_shadow(&shadow_pixmap, pixmap, color, composite_original);
90}
91
92/// Apply the shadow color and optionally composite with the original.
93///
94/// The shadow has already been offset and blurred, so this simply applies
95/// the shadow color to the alpha channel and, when requested, composites the
96/// original over it using source-over.
97fn write_colored_shadow(
98    shadow: &Pixmap,
99    dst: &mut Pixmap,
100    color: AlphaColor<Srgb>,
101    composite_original: bool,
102) {
103    let width = dst.width();
104    let height = dst.height();
105
106    // Precompute shadow color components
107    let shadow_r = (color.components[0] * 255.0).round() as u8;
108    let shadow_g = (color.components[1] * 255.0).round() as u8;
109    let shadow_b = (color.components[2] * 255.0).round() as u8;
110
111    for y in 0..height {
112        for x in 0..width {
113            // Sample alpha directly (shadow is already offset)
114            let alpha = shadow.sample(x, y).a;
115
116            // Apply shadow color to alpha
117            let shadow_alpha = (u8_to_norm(alpha) * color.components[3]).min(1.0);
118            let final_alpha = norm_to_u8(shadow_alpha);
119
120            // Premultiply RGB by alpha as required by PremulRgba8
121            let alpha_u16 = u16::from(final_alpha);
122            let premultiply = |channel: u8| ((u16::from(channel) * alpha_u16) / 255) as u8;
123
124            let colored_shadow = PremulRgba8 {
125                r: premultiply(shadow_r),
126                g: premultiply(shadow_g),
127                b: premultiply(shadow_b),
128                a: final_alpha,
129            };
130
131            let result = if composite_original {
132                // Read original and composite: original over shadow
133                compose_src_over(dst.sample(x, y), colored_shadow)
134            } else {
135                colored_shadow
136            };
137
138            dst.set_pixel(x, y, result);
139        }
140    }
141}
142
143/// Composite two pixels using Porter-Duff "source over" operator.
144///
145/// Composes the source pixel over the destination pixel using premultiplied
146/// alpha blending. Returns the composited result.
147///
148/// Formula for premultiplied colors: `result = src + dst * (1 - src_alpha)`
149fn compose_src_over(src: PremulRgba8, dst: PremulRgba8) -> PremulRgba8 {
150    let src_a = u8_to_norm(src.a);
151
152    PremulRgba8 {
153        r: src_over_channel(src.r, dst.r, src_a),
154        g: src_over_channel(src.g, dst.g, src_a),
155        b: src_over_channel(src.b, dst.b, src_a),
156        a: src_over_channel(src.a, dst.a, src_a),
157    }
158}
159
160/// Blend a single channel using Porter-Duff "source over" operator.
161///
162/// For premultiplied colors, the formula is: `result = src + dst * (1 - src_alpha)`
163#[inline]
164fn src_over_channel(src: u8, dst: u8, src_alpha: f32) -> u8 {
165    let result = u8_to_norm(src) + u8_to_norm(dst) * (1.0 - src_alpha);
166    norm_to_u8(result)
167}
168
169/// Convert a u8 color component (0-255) to normalized f32 (0.0-1.0).
170#[inline]
171fn u8_to_norm(value: u8) -> f32 {
172    value as f32 / 255.0
173}
174
175/// Convert a normalized f32 (0.0-1.0) to u8 color component (0-255).
176#[inline]
177fn norm_to_u8(value: f32) -> u8 {
178    (value * 255.0).round() as u8
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use vello_common::color::Srgb;
185
186    /// Test `u8_to_norm` conversion.
187    #[test]
188    fn test_u8_to_norm() {
189        assert_eq!(u8_to_norm(0), 0.0);
190        assert!((u8_to_norm(255) - 1.0).abs() < 1e-6);
191    }
192
193    /// Test `norm_to_u8` conversion.
194    #[test]
195    fn test_norm_to_u8() {
196        assert_eq!(norm_to_u8(0.0), 0);
197        assert_eq!(norm_to_u8(1.0), 255);
198        assert_eq!(norm_to_u8(0.5), 128); // 0.5 * 255 = 127.5 → 128
199    }
200
201    /// Test round-trip conversion u8 → norm → u8.
202    #[test]
203    fn test_conversion_roundtrip() {
204        for value in [0, 1, 50, 127, 128, 200, 254, 255] {
205            let normalized = u8_to_norm(value);
206            let back = norm_to_u8(normalized);
207            assert_eq!(back, value);
208        }
209    }
210
211    /// Test Porter-Duff source-over with fully opaque source.
212    #[test]
213    fn test_compose_src_over_opaque_source() {
214        let src = PremulRgba8 {
215            r: 255,
216            g: 0,
217            b: 0,
218            a: 255,
219        }; // Opaque red
220        let dst = PremulRgba8 {
221            r: 0,
222            g: 255,
223            b: 0,
224            a: 255,
225        }; // Opaque green
226
227        let result = compose_src_over(src, dst);
228        // Opaque source should completely cover destination
229        assert_eq!(result.r, 255);
230        assert_eq!(result.g, 0);
231        assert_eq!(result.b, 0);
232        assert_eq!(result.a, 255);
233    }
234
235    /// Test Porter-Duff source-over with transparent source.
236    #[test]
237    fn test_compose_src_over_transparent_source() {
238        let src = PremulRgba8 {
239            r: 0,
240            g: 0,
241            b: 0,
242            a: 0,
243        };
244        let dst = PremulRgba8 {
245            r: 0,
246            g: 255,
247            b: 0,
248            a: 255,
249        };
250
251        let result = compose_src_over(src, dst);
252        // Transparent source should leave destination unchanged
253        assert_eq!(result.r, 0);
254        assert_eq!(result.g, 255);
255        assert_eq!(result.b, 0);
256        assert_eq!(result.a, 255);
257    }
258
259    /// Test Porter-Duff source-over with semi-transparent source.
260    #[test]
261    fn test_compose_src_over_semi_transparent() {
262        let src = PremulRgba8 {
263            r: 128,
264            g: 0,
265            b: 0,
266            a: 128,
267        }; // 50% red (premul)
268        let dst = PremulRgba8 {
269            r: 0,
270            g: 128,
271            b: 0,
272            a: 128,
273        }; // 50% green (premul)
274
275        let result = compose_src_over(src, dst);
276        // Result should blend src + dst*(1-src_alpha)
277        // r: 128 + 0*(1-0.5) = 128
278        // g: 0 + 128*0.5 = 64
279        // a: 128 + 128*0.5 = 192
280        assert_eq!(
281            result,
282            PremulRgba8 {
283                r: 128,
284                g: 64,
285                b: 0,
286                a: 192,
287            }
288        );
289    }
290
291    /// Test `write_colored_shadow` applies color correctly.
292    #[test]
293    fn test_compose_shadow_color() {
294        let mut shadow_pixmap = Pixmap::new(2, 2);
295        let mut dst_pixmap = Pixmap::new(2, 2);
296
297        // Shadow has alpha=255 at (0,0)
298        shadow_pixmap.set_pixel(
299            0,
300            0,
301            PremulRgba8 {
302                r: 0,
303                g: 0,
304                b: 0,
305                a: 255,
306            },
307        );
308
309        let shadow_color = AlphaColor {
310            components: [1.0, 0.0, 0.0, 1.0], // Red
311            cs: std::marker::PhantomData::<Srgb>,
312        };
313
314        write_colored_shadow(&shadow_pixmap, &mut dst_pixmap, shadow_color, true);
315
316        // Shadow at (0,0) should be red
317        let result = dst_pixmap.sample(0, 0);
318        assert_eq!(result.r, 255);
319        assert_eq!(result.g, 0);
320        assert_eq!(result.b, 0);
321        assert_eq!(result.a, 255);
322    }
323}