Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions sparse_strips/vello_common/src/filter/color_matrix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! The color matrix filter.

/// Matrix-based color transformation filter.
///
/// The matrix is stored as four rows of five values. Each row computes one output
/// channel (`R`, `G`, `B`, `A`) from the four input channels plus a constant offset.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ColorMatrix {
/// The 4x5 color transformation matrix in row-major order.
pub matrix: [f32; 20],
}

impl ColorMatrix {
/// Create a new color matrix filter.
pub fn new(matrix: [f32; 20]) -> Self {
Self { matrix }
}

/// Return true if this matrix can be applied directly to premultiplied colors.
///
/// A premultiplied-compatible matrix preserves alpha, does not read alpha
/// from the RGB rows, and has no RGB offsets. For this subset, renderers can
/// apply the RGB rows directly to premultiplied RGB and clamp the result to
/// the unchanged alpha channel.
pub fn is_premul_compatible(&self) -> bool {
self.matrix[3] == 0.0
&& self.matrix[4] == 0.0
&& self.matrix[8] == 0.0
&& self.matrix[9] == 0.0
&& self.matrix[13] == 0.0
&& self.matrix[14] == 0.0
&& self.matrix[15] == 0.0
&& self.matrix[16] == 0.0
&& self.matrix[17] == 0.0
&& self.matrix[18] == 1.0
&& self.matrix[19] == 0.0
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::filter_effects::matrices;

#[test]
fn premul_compatible_matrices_are_rgb_only_and_alpha_preserving() {
assert!(ColorMatrix::new(matrices::GRAYSCALE).is_premul_compatible());
assert!(ColorMatrix::new(matrices::SEPIA).is_premul_compatible());
assert!(!ColorMatrix::new(matrices::ALPHA_TO_BLACK).is_premul_compatible());
}

#[test]
fn premul_compatible_matrix_rejects_rgb_offsets_and_alpha_changes() {
let mut offset_matrix = matrices::IDENTITY;
offset_matrix[4] = 0.25;
assert!(!ColorMatrix::new(offset_matrix).is_premul_compatible());

let mut opacity_matrix = matrices::IDENTITY;
opacity_matrix[18] = 0.5;
assert!(!ColorMatrix::new(opacity_matrix).is_premul_compatible());
}
}
27 changes: 26 additions & 1 deletion sparse_strips/vello_common/src/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
//! represent a special representation of each filter to be used as the basis for rendering in
//! `vello_hybrid` and `vello_cpu`.

use crate::filter::color_matrix::ColorMatrix;
use crate::filter::drop_shadow::{DropShadow, transform_shadow_params};
use crate::filter::flood::Flood;
use crate::filter::gaussian_blur::{GaussianBlur, transform_blur_params};
use crate::filter::offset::Offset;
use crate::filter_effects::{Filter, FilterPrimitive};
use crate::kurbo::{Affine, Vec2};

pub mod color_matrix;
pub mod drop_shadow;
pub mod flood;
pub mod gaussian_blur;
Expand All @@ -30,6 +32,8 @@ pub enum PreparedFilter {
Offset(Offset),
/// A drop shadow filter.
DropShadow(DropShadow),
/// A color matrix filter.
ColorMatrix(ColorMatrix),
}

impl PreparedFilter {
Expand Down Expand Up @@ -73,8 +77,9 @@ impl PreparedFilter {

Self::Offset(offset)
}
FilterPrimitive::ColorMatrix { matrix } => Self::ColorMatrix(ColorMatrix::new(*matrix)),
_ => {
// Other primitives like Blend, ColorMatrix, ComponentTransfer, etc.
// Other primitives like Blend, ComponentTransfer, etc.
// are not yet implemented
unimplemented!("Other filter primitives not yet implemented");
}
Expand All @@ -92,3 +97,23 @@ fn transform_offset_params(dx: f32, dy: f32, transform: &Affine) -> (f32, f32) {
let transformed_offset = Vec2::new(a * offset.x + c * offset.y, b * offset.x + d * offset.y);
(transformed_offset.x as f32, transformed_offset.y as f32)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::filter_effects::matrices;

#[test]
fn prepares_color_matrix_filter() {
let filter = Filter::from_primitive(FilterPrimitive::ColorMatrix {
matrix: matrices::SEPIA,
});
let prepared = PreparedFilter::new(&filter, &Affine::IDENTITY);

let PreparedFilter::ColorMatrix(color_matrix) = prepared else {
panic!("expected color matrix filter");
};

assert_eq!(color_matrix.matrix, matrices::SEPIA);
}
}
12 changes: 6 additions & 6 deletions sparse_strips/vello_common/src/filter_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
//! - `Flood` - Solid color fill
//! - `GaussianBlur` - Gaussian blur filter
//! - `DropShadow` - Drop shadow effect (compound primitive)
//! - `ColorMatrix` - Matrix-based color transformation
//! - `Offset` - Translation/shift (single primitive)
//!
//! **Note:** Currently only single primitive filters are supported. Filter graphs with
Expand All @@ -36,7 +37,6 @@
//! `Opacity`, `Saturate`, `Sepia`
//!
//! **Filter Primitives:**
//! - `ColorMatrix` - Matrix-based color transformation
//! - `Composite` - Porter-Duff compositing operations
//! - `Blend` - Blend mode operations
//! - `Morphology` - Dilate/erode operations
Expand Down Expand Up @@ -400,11 +400,6 @@ pub enum FilterPrimitive {
/// Default is `EdgeMode::None` per SVG spec.
edge_mode: EdgeMode,
},
//
// ============================================================
// TODO: The following filter primitives are not yet implemented
// ============================================================
//
/// Matrix-based color transformation.
///
/// Applies a 4x5 matrix transformation to colors, allowing arbitrary
Expand All @@ -425,6 +420,11 @@ pub enum FilterPrimitive {
dy: f32,
},

//
// ============================================================
// TODO: The following filter primitives are not yet implemented
// ============================================================
//
/// Composite two inputs using Porter-Duff compositing operations.
///
/// Combines two input images using standard compositing operators
Expand Down
Loading
Loading