Skip to main content

vello_cpu/filter/
mod.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Filter effects implementation for `vello_cpu`.
5//!
6//! This module provides CPU-based implementations of SVG filter effects,
7//! supporting both low-precision (u8) and high-precision (f32) rendering paths.
8//! Filters are applied to layers through the layer manager, which handles
9//! intermediate storage.
10
11mod drop_shadow;
12mod flood;
13mod gaussian_blur;
14mod offset;
15mod shift;
16
17pub(crate) use drop_shadow::DropShadow;
18pub(crate) use flood::Flood;
19pub(crate) use gaussian_blur::GaussianBlur;
20pub(crate) use offset::Offset;
21
22use crate::layer_manager::LayerManager;
23use vello_common::filter_effects::{Filter, FilterPrimitive};
24use vello_common::kurbo::{Affine, Vec2};
25use vello_common::pixmap::Pixmap;
26use vello_common::util::extract_scales;
27
28/// Trait for filter effects that can be applied to layers.
29///
30/// Each filter implements this trait with both u8 and f32 variants
31/// to support different rendering backends and precision requirements.
32/// The low-precision path (`execute_lowp`) uses 8-bit color channels for
33/// better performance, while the high-precision path (`execute_highp`)
34/// uses 32-bit floating-point for higher quality at the cost of speed.
35pub(crate) trait FilterEffect {
36    /// Apply the low-precision (u8) version of the filter.
37    ///
38    /// # Arguments
39    /// * `pixmap` - The target pixmap containing rendering metadata
40    /// * `layer_manager` - Manager for allocating and accessing intermediate layers
41    fn execute_lowp(&self, pixmap: &mut Pixmap, layer_manager: &mut LayerManager);
42
43    /// Apply the high-precision (f32) version of the filter.
44    ///
45    /// # Arguments
46    /// * `pixmap` - The target pixmap containing rendering metadata
47    /// * `layer_manager` - Manager for allocating and accessing intermediate layers
48    fn execute_highp(&self, pixmap: &mut Pixmap, layer_manager: &mut LayerManager);
49}
50
51/// Apply the low-precision (u8) version of a filter effect to a layer.
52///
53/// This function dispatches filter primitives from a filter graph to their
54/// corresponding CPU implementations using 8-bit color channels.
55///
56/// # Arguments
57/// * `filter` - The filter containing the graph of primitives to apply
58/// * `pixmap` - The target pixmap containing rendering metadata
59/// * `layer_manager` - Manager for allocating and accessing intermediate layers
60/// * `transform` - The transformation matrix to extract scale from for filter parameters
61///
62/// # Limitations
63/// Currently only supports filter graphs with a single primitive.
64/// Multi-primitive filter graphs are not yet implemented.
65pub(crate) fn filter_lowp(
66    filter: &Filter,
67    pixmap: &mut Pixmap,
68    layer_manager: &mut LayerManager,
69    transform: Affine,
70) {
71    // Multi-primitive filter graphs are not yet implemented.
72    if filter.graph.primitives.len() != 1 {
73        unimplemented!("Multi-primitive filter graphs are not yet supported");
74    }
75
76    match &filter.graph.primitives[0] {
77        FilterPrimitive::Flood { color } => {
78            let flood = Flood::new(*color);
79            flood.execute_lowp(pixmap, layer_manager);
80        }
81        FilterPrimitive::GaussianBlur {
82            std_deviation,
83            edge_mode,
84        } => {
85            let scaled_std_dev = transform_blur_params(*std_deviation, &transform);
86            let blur = GaussianBlur::new(scaled_std_dev, *edge_mode);
87            blur.execute_lowp(pixmap, layer_manager);
88        }
89        FilterPrimitive::DropShadow {
90            dx,
91            dy,
92            std_deviation,
93            color,
94            edge_mode,
95        } => {
96            let (scaled_dx, scaled_dy, scaled_std_dev) =
97                transform_shadow_params(*dx, *dy, *std_deviation, &transform);
98            let drop_shadow =
99                DropShadow::new(scaled_dx, scaled_dy, scaled_std_dev, *edge_mode, *color);
100            drop_shadow.execute_lowp(pixmap, layer_manager);
101        }
102        FilterPrimitive::Offset { dx, dy } => {
103            let (scaled_dx, scaled_dy) = transform_offset_params(*dx, *dy, &transform);
104            Offset::new(scaled_dx, scaled_dy).execute_lowp(pixmap, layer_manager);
105        }
106        _ => {
107            // Other primitives like Blend, ColorMatrix, ComponentTransfer, etc.
108            // are not yet implemented
109            unimplemented!("Other filter primitives not yet implemented");
110        }
111    }
112}
113
114/// Apply the high-precision (f32) version of a filter effect to a layer.
115///
116/// This function dispatches filter primitives from a filter graph to their
117/// corresponding CPU implementations using 32-bit floating-point color channels.
118///
119/// # Arguments
120/// * `filter` - The filter containing the graph of primitives to apply
121/// * `pixmap` - The target pixmap containing rendering metadata
122/// * `layer_manager` - Manager for allocating and accessing intermediate layers
123/// * `transform` - The transformation matrix to extract scale from for filter parameters
124///
125/// # Limitations
126/// Currently only supports filter graphs with a single primitive.
127/// Multi-primitive filter graphs are not yet implemented.
128pub(crate) fn filter_highp(
129    filter: &Filter,
130    pixmap: &mut Pixmap,
131    layer_manager: &mut LayerManager,
132    transform: Affine,
133) {
134    // Multi-primitive filter graphs are not yet implemented.
135    if filter.graph.primitives.len() != 1 {
136        unimplemented!("Multi-primitive filter graphs are not yet supported");
137    }
138
139    match &filter.graph.primitives[0] {
140        FilterPrimitive::Flood { color } => {
141            let flood = Flood::new(*color);
142            flood.execute_highp(pixmap, layer_manager);
143        }
144        FilterPrimitive::GaussianBlur {
145            std_deviation,
146            edge_mode,
147        } => {
148            let scaled_std_dev = transform_blur_params(*std_deviation, &transform);
149            let blur = GaussianBlur::new(scaled_std_dev, *edge_mode);
150            blur.execute_highp(pixmap, layer_manager);
151        }
152        FilterPrimitive::DropShadow {
153            dx,
154            dy,
155            std_deviation,
156            color,
157            edge_mode,
158        } => {
159            let (scaled_dx, scaled_dy, scaled_std_dev) =
160                transform_shadow_params(*dx, *dy, *std_deviation, &transform);
161            let drop_shadow =
162                DropShadow::new(scaled_dx, scaled_dy, scaled_std_dev, *edge_mode, *color);
163            drop_shadow.execute_highp(pixmap, layer_manager);
164        }
165        FilterPrimitive::Offset { dx, dy } => {
166            let (scaled_dx, scaled_dy) = transform_offset_params(*dx, *dy, &transform);
167            Offset::new(scaled_dx, scaled_dy).execute_highp(pixmap, layer_manager);
168        }
169        _ => {
170            // Other primitives like Blend, ColorMatrix, ComponentTransfer, etc.
171            // are not yet implemented
172            unimplemented!("Other filter primitives not yet implemented");
173        }
174    }
175}
176
177/// Transform an offset's dx/dy using the affine transformation's linear part.
178///
179/// # Returns
180/// A tuple of (`scaled_dx`, `scaled_dy`) in device space.
181fn transform_offset_params(dx: f32, dy: f32, transform: &Affine) -> (f32, f32) {
182    let offset = Vec2::new(dx as f64, dy as f64);
183    let [a, b, c, d, _, _] = transform.as_coeffs();
184    let transformed_offset = Vec2::new(a * offset.x + c * offset.y, b * offset.x + d * offset.y);
185    (transformed_offset.x as f32, transformed_offset.y as f32)
186}
187
188/// Transform a drop shadow's offset and standard deviation using the affine transformation.
189///
190/// Applies the full linear transformation (rotation, scale, and shear) to the offset vector,
191/// and scales the blur standard deviation uniformly.
192///
193/// # Arguments
194/// * `dx` - Horizontal offset in user space
195/// * `dy` - Vertical offset in user space
196/// * `std_deviation` - Blur standard deviation in user space
197/// * `transform` - The transformation matrix to apply
198///
199/// # Returns
200/// A tuple of (`scaled_dx`, `scaled_dy`, `scaled_std_dev`) in device space
201fn transform_shadow_params(
202    dx: f32,
203    dy: f32,
204    std_deviation: f32,
205    transform: &Affine,
206) -> (f32, f32, f32) {
207    // Transform the offset vector by the full transformation matrix
208    // to correctly handle rotation, scale, and shear.
209    // We use the linear part only (no translation) since this is a vector offset.
210    let offset = Vec2::new(dx as f64, dy as f64);
211    let [a, b, c, d, _, _] = transform.as_coeffs();
212    let transformed_offset = Vec2::new(a * offset.x + c * offset.y, b * offset.x + d * offset.y);
213    let scaled_dx = transformed_offset.x as f32;
214    let scaled_dy = transformed_offset.y as f32;
215
216    // Scale the blur radius uniformly
217    let scaled_std_dev = transform_blur_params(std_deviation, transform);
218
219    (scaled_dx, scaled_dy, scaled_std_dev)
220}
221
222/// Scale a blur's standard deviation uniformly based on the transformation.
223///
224/// Extracts the scale factors from the transformation matrix using SVD and
225/// averages them to get a uniform scale factor for the blur radius.
226///
227/// # Arguments
228/// * `std_deviation` - The blur standard deviation in user space
229/// * `transform` - The transformation matrix to extract scale from
230///
231/// # Returns
232/// The scaled standard deviation in device space
233fn transform_blur_params(std_deviation: f32, transform: &Affine) -> f32 {
234    let (scale_x, scale_y) = extract_scales(transform);
235    let uniform_scale = (scale_x + scale_y) / 2.0;
236    // TODO: Support separate std_deviation for x and y axes (std_deviation_x, std_deviation_y)
237    // to properly handle non-uniform scaling. This would eliminate the need for uniform_scale
238    // and allow blur to scale independently along each axis.
239    std_deviation * uniform_scale
240}