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}