summaryrefslogtreecommitdiff
path: root/rsvg/src/filters
diff options
context:
space:
mode:
Diffstat (limited to 'rsvg/src/filters')
-rw-r--r--rsvg/src/filters/blend.rs178
-rw-r--r--rsvg/src/filters/bounds.rs121
-rw-r--r--rsvg/src/filters/color_matrix.rs342
-rw-r--r--rsvg/src/filters/component_transfer.rs458
-rw-r--r--rsvg/src/filters/composite.rs179
-rw-r--r--rsvg/src/filters/context.rs405
-rw-r--r--rsvg/src/filters/convolve_matrix.rs354
-rw-r--r--rsvg/src/filters/displacement_map.rs195
-rw-r--r--rsvg/src/filters/drop_shadow.rs88
-rw-r--r--rsvg/src/filters/error.rs78
-rw-r--r--rsvg/src/filters/flood.rs70
-rw-r--r--rsvg/src/filters/gaussian_blur.rs282
-rw-r--r--rsvg/src/filters/image.rs211
-rw-r--r--rsvg/src/filters/lighting.rs1090
-rw-r--r--rsvg/src/filters/merge.rs217
-rw-r--r--rsvg/src/filters/mod.rs381
-rw-r--r--rsvg/src/filters/morphology.rs200
-rw-r--r--rsvg/src/filters/offset.rs100
-rw-r--r--rsvg/src/filters/tile.rs109
-rw-r--r--rsvg/src/filters/turbulence.rs484
20 files changed, 5542 insertions, 0 deletions
diff --git a/rsvg/src/filters/blend.rs b/rsvg/src/filters/blend.rs
new file mode 100644
index 00000000..30b0bdf7
--- /dev/null
+++ b/rsvg/src/filters/blend.rs
@@ -0,0 +1,178 @@
+use cssparser::Parser;
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementTrait};
+use crate::error::*;
+use crate::node::{CascadedValues, Node};
+use crate::parsers::{Parse, ParseValue};
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::surface_utils::shared_surface::Operator;
+use crate::xml::Attributes;
+
+use super::bounds::BoundsBuilder;
+use super::context::{FilterContext, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams,
+ ResolvedPrimitive,
+};
+
+/// Enumeration of the possible blending modes.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
+enum Mode {
+ Normal,
+ Multiply,
+ Screen,
+ Darken,
+ Lighten,
+ Overlay,
+ ColorDodge,
+ ColorBurn,
+ HardLight,
+ SoftLight,
+ Difference,
+ Exclusion,
+ HslHue,
+ HslSaturation,
+ HslColor,
+ HslLuminosity,
+}
+
+enum_default!(Mode, Mode::Normal);
+
+/// The `feBlend` filter primitive.
+#[derive(Default)]
+pub struct FeBlend {
+ base: Primitive,
+ params: Blend,
+}
+
+/// Resolved `feBlend` primitive for rendering.
+#[derive(Clone, Default)]
+pub struct Blend {
+ in1: Input,
+ in2: Input,
+ mode: Mode,
+ color_interpolation_filters: ColorInterpolationFilters,
+}
+
+impl ElementTrait for FeBlend {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ let (in1, in2) = self.base.parse_two_inputs(attrs, session);
+ self.params.in1 = in1;
+ self.params.in2 = in2;
+
+ for (attr, value) in attrs.iter() {
+ if let expanded_name!("", "mode") = attr.expanded() {
+ set_attribute(&mut self.params.mode, attr.parse(value), session);
+ }
+ }
+ }
+}
+
+impl Blend {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ let input_1 = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in1,
+ self.color_interpolation_filters,
+ )?;
+ let input_2 = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in2,
+ self.color_interpolation_filters,
+ )?;
+ let bounds: IRect = bounds_builder
+ .add_input(&input_1)
+ .add_input(&input_2)
+ .compute(ctx)
+ .clipped
+ .into();
+
+ let surface = input_1
+ .surface()
+ .compose(input_2.surface(), bounds, self.mode.into())?;
+
+ Ok(FilterOutput { surface, bounds })
+ }
+}
+
+impl FilterEffect for FeBlend {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ let cascaded = CascadedValues::new_from_node(node);
+ let values = cascaded.get();
+
+ let mut params = self.params.clone();
+ params.color_interpolation_filters = values.color_interpolation_filters();
+
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::Blend(params),
+ }])
+ }
+}
+
+impl Parse for Mode {
+ fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ Ok(parse_identifiers!(
+ parser,
+ "normal" => Mode::Normal,
+ "multiply" => Mode::Multiply,
+ "screen" => Mode::Screen,
+ "darken" => Mode::Darken,
+ "lighten" => Mode::Lighten,
+ "overlay" => Mode::Overlay,
+ "color-dodge" => Mode::ColorDodge,
+ "color-burn" => Mode::ColorBurn,
+ "hard-light" => Mode::HardLight,
+ "soft-light" => Mode::SoftLight,
+ "difference" => Mode::Difference,
+ "exclusion" => Mode::Exclusion,
+ "hue" => Mode::HslHue,
+ "saturation" => Mode::HslSaturation,
+ "color" => Mode::HslColor,
+ "luminosity" => Mode::HslLuminosity,
+ )?)
+ }
+}
+
+impl From<Mode> for Operator {
+ #[inline]
+ fn from(x: Mode) -> Self {
+ use Mode::*;
+
+ match x {
+ Normal => Operator::Over,
+ Multiply => Operator::Multiply,
+ Screen => Operator::Screen,
+ Darken => Operator::Darken,
+ Lighten => Operator::Lighten,
+ Overlay => Operator::Overlay,
+ ColorDodge => Operator::ColorDodge,
+ ColorBurn => Operator::ColorBurn,
+ HardLight => Operator::HardLight,
+ SoftLight => Operator::SoftLight,
+ Difference => Operator::Difference,
+ Exclusion => Operator::Exclusion,
+ HslHue => Operator::HslHue,
+ HslSaturation => Operator::HslSaturation,
+ HslColor => Operator::HslColor,
+ HslLuminosity => Operator::HslLuminosity,
+ }
+ }
+}
diff --git a/rsvg/src/filters/bounds.rs b/rsvg/src/filters/bounds.rs
new file mode 100644
index 00000000..6a6dd9d2
--- /dev/null
+++ b/rsvg/src/filters/bounds.rs
@@ -0,0 +1,121 @@
+//! Filter primitive subregion computation.
+use crate::rect::Rect;
+use crate::transform::Transform;
+
+use super::context::{FilterContext, FilterInput};
+
+/// A helper type for filter primitive subregion computation.
+pub struct BoundsBuilder {
+ /// Filter primitive properties.
+ x: Option<f64>,
+ y: Option<f64>,
+ width: Option<f64>,
+ height: Option<f64>,
+
+ /// The transform to use when generating the rect
+ transform: Transform,
+
+ /// The inverse transform used when adding rects
+ inverse: Transform,
+
+ /// Whether one of the input nodes is standard input.
+ standard_input_was_referenced: bool,
+
+ /// The current bounding rectangle.
+ rect: Option<Rect>,
+}
+
+/// A filter primitive's subregion.
+pub struct Bounds {
+ /// Primitive's subregion, clipped to the filter effects region.
+ pub clipped: Rect,
+
+ /// Primitive's subregion, unclipped.
+ pub unclipped: Rect,
+}
+
+impl BoundsBuilder {
+ /// Constructs a new `BoundsBuilder`.
+ #[inline]
+ pub fn new(
+ x: Option<f64>,
+ y: Option<f64>,
+ width: Option<f64>,
+ height: Option<f64>,
+ transform: Transform,
+ ) -> Self {
+ // We panic if transform is not invertible. This is checked in the caller.
+ Self {
+ x,
+ y,
+ width,
+ height,
+ transform,
+ inverse: transform.invert().unwrap(),
+ standard_input_was_referenced: false,
+ rect: None,
+ }
+ }
+
+ /// Adds a filter primitive input to the bounding box.
+ #[inline]
+ pub fn add_input(mut self, input: &FilterInput) -> Self {
+ // If a standard input was referenced, the default value is the filter effects region
+ // regardless of other referenced inputs. This means we can skip computing the bounds.
+ if self.standard_input_was_referenced {
+ return self;
+ }
+
+ match *input {
+ FilterInput::StandardInput(_) => {
+ self.standard_input_was_referenced = true;
+ }
+ FilterInput::PrimitiveOutput(ref output) => {
+ let input_rect = self.inverse.transform_rect(&Rect::from(output.bounds));
+ self.rect = Some(self.rect.map_or(input_rect, |r| input_rect.union(&r)));
+ }
+ }
+
+ self
+ }
+
+ /// Returns the final exact bounds, both with and without clipping to the effects region.
+ pub fn compute(self, ctx: &FilterContext) -> Bounds {
+ let effects_region = ctx.effects_region();
+
+ // The default value is the filter effects region converted into
+ // the ptimitive coordinate system.
+ let mut rect = match self.rect {
+ Some(r) if !self.standard_input_was_referenced => r,
+ _ => self.inverse.transform_rect(&effects_region),
+ };
+
+ // If any of the properties were specified, we need to respect them.
+ // These replacements are possible because of the primitive coordinate system.
+ if self.x.is_some() || self.y.is_some() || self.width.is_some() || self.height.is_some() {
+ if let Some(x) = self.x {
+ let w = rect.width();
+ rect.x0 = x;
+ rect.x1 = rect.x0 + w;
+ }
+ if let Some(y) = self.y {
+ let h = rect.height();
+ rect.y0 = y;
+ rect.y1 = rect.y0 + h;
+ }
+ if let Some(width) = self.width {
+ rect.x1 = rect.x0 + width;
+ }
+ if let Some(height) = self.height {
+ rect.y1 = rect.y0 + height;
+ }
+ }
+
+ // Convert into the surface coordinate system.
+ let unclipped = self.transform.transform_rect(&rect);
+
+ let clipped = unclipped.intersection(&effects_region).unwrap_or_default();
+
+ Bounds { clipped, unclipped }
+ }
+}
diff --git a/rsvg/src/filters/color_matrix.rs b/rsvg/src/filters/color_matrix.rs
new file mode 100644
index 00000000..88eb6f11
--- /dev/null
+++ b/rsvg/src/filters/color_matrix.rs
@@ -0,0 +1,342 @@
+use cssparser::Parser;
+use markup5ever::{expanded_name, local_name, namespace_url, ns, QualName};
+use nalgebra::{Matrix3, Matrix4x5, Matrix5, Vector5};
+
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementTrait};
+use crate::error::*;
+use crate::node::{CascadedValues, Node};
+use crate::parsers::{NumberList, Parse, ParseValue};
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::surface_utils::{
+ iterators::Pixels, shared_surface::ExclusiveImageSurface, ImageSurfaceDataExt, Pixel,
+};
+use crate::util::clamp;
+use crate::xml::Attributes;
+
+use super::bounds::BoundsBuilder;
+use super::context::{FilterContext, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams,
+ ResolvedPrimitive,
+};
+
+/// Color matrix operation types.
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+enum OperationType {
+ Matrix,
+ Saturate,
+ HueRotate,
+ LuminanceToAlpha,
+}
+
+enum_default!(OperationType, OperationType::Matrix);
+
+/// The `feColorMatrix` filter primitive.
+#[derive(Default)]
+pub struct FeColorMatrix {
+ base: Primitive,
+ params: ColorMatrix,
+}
+
+/// Resolved `feColorMatrix` primitive for rendering.
+#[derive(Clone)]
+pub struct ColorMatrix {
+ pub in1: Input,
+ pub matrix: Matrix5<f64>,
+ pub color_interpolation_filters: ColorInterpolationFilters,
+}
+
+impl Default for ColorMatrix {
+ fn default() -> ColorMatrix {
+ ColorMatrix {
+ in1: Default::default(),
+ color_interpolation_filters: Default::default(),
+
+ // nalgebra's Default for Matrix5 is all zeroes, so we actually need this :(
+ matrix: Matrix5::identity(),
+ }
+ }
+}
+
+impl ElementTrait for FeColorMatrix {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.params.in1 = self.base.parse_one_input(attrs, session);
+
+ // First, determine the operation type.
+ let mut operation_type = Default::default();
+ for (attr, value) in attrs
+ .iter()
+ .filter(|(attr, _)| attr.expanded() == expanded_name!("", "type"))
+ {
+ set_attribute(&mut operation_type, attr.parse(value), session);
+ }
+
+ // Now read the matrix correspondingly.
+ //
+ // Here we cannot assume that ColorMatrix::default() has provided the correct
+ // initial value for the matrix itself, since the initial value for the matrix
+ // (i.e. the value to which it should fall back if the `values` attribute is in
+ // error) depends on the operation_type.
+ //
+ // So, for each operation_type, first initialize the proper default matrix, then
+ // try to parse the value.
+
+ use OperationType::*;
+
+ self.params.matrix = match operation_type {
+ Matrix => ColorMatrix::default_matrix(),
+ Saturate => ColorMatrix::saturate_matrix(1.0),
+ HueRotate => ColorMatrix::hue_rotate_matrix(0.0),
+ LuminanceToAlpha => ColorMatrix::luminance_to_alpha_matrix(),
+ };
+
+ for (attr, value) in attrs
+ .iter()
+ .filter(|(attr, _)| attr.expanded() == expanded_name!("", "values"))
+ {
+ match operation_type {
+ Matrix => parse_matrix(&mut self.params.matrix, attr, value, session),
+ Saturate => parse_saturate_matrix(&mut self.params.matrix, attr, value, session),
+ HueRotate => parse_hue_rotate_matrix(&mut self.params.matrix, attr, value, session),
+ LuminanceToAlpha => {
+ parse_luminance_to_alpha_matrix(&mut self.params.matrix, attr, value, session)
+ }
+ }
+ }
+ }
+}
+
+fn parse_matrix(dest: &mut Matrix5<f64>, attr: QualName, value: &str, session: &Session) {
+ let parsed: Result<NumberList<20, 20>, _> = attr.parse(value);
+
+ match parsed {
+ Ok(NumberList(v)) => {
+ let matrix = Matrix4x5::from_row_slice(&v);
+ let mut matrix = matrix.fixed_resize(0.0);
+ matrix[(4, 4)] = 1.0;
+ *dest = matrix;
+ }
+
+ Err(e) => {
+ rsvg_log!(session, "element feColorMatrix with type=\"matrix\", expected a values attribute with 20 numbers: {}", e);
+ }
+ }
+}
+
+fn parse_saturate_matrix(dest: &mut Matrix5<f64>, attr: QualName, value: &str, session: &Session) {
+ let parsed: Result<f64, _> = attr.parse(value);
+
+ match parsed {
+ Ok(s) => {
+ *dest = ColorMatrix::saturate_matrix(s);
+ }
+
+ Err(e) => {
+ rsvg_log!(session, "element feColorMatrix with type=\"saturate\", expected a values attribute with 1 number: {}", e);
+ }
+ }
+}
+
+fn parse_hue_rotate_matrix(
+ dest: &mut Matrix5<f64>,
+ attr: QualName,
+ value: &str,
+ session: &Session,
+) {
+ let parsed: Result<f64, _> = attr.parse(value);
+
+ match parsed {
+ Ok(degrees) => {
+ *dest = ColorMatrix::hue_rotate_matrix(degrees.to_radians());
+ }
+
+ Err(e) => {
+ rsvg_log!(session, "element feColorMatrix with type=\"hueRotate\", expected a values attribute with 1 number: {}", e);
+ }
+ }
+}
+
+fn parse_luminance_to_alpha_matrix(
+ _dest: &mut Matrix5<f64>,
+ _attr: QualName,
+ _value: &str,
+ session: &Session,
+) {
+ // There's nothing to parse, since our caller already supplied the default value,
+ // and type="luminanceToAlpha" does not takes a `values` attribute. So, just warn
+ // that the value is being ignored.
+
+ rsvg_log!(
+ session,
+ "ignoring \"values\" attribute for feColorMatrix with type=\"luminanceToAlpha\""
+ );
+}
+
+impl ColorMatrix {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ let input_1 = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in1,
+ self.color_interpolation_filters,
+ )?;
+ let bounds: IRect = bounds_builder
+ .add_input(&input_1)
+ .compute(ctx)
+ .clipped
+ .into();
+
+ let mut surface = ExclusiveImageSurface::new(
+ ctx.source_graphic().width(),
+ ctx.source_graphic().height(),
+ input_1.surface().surface_type(),
+ )?;
+
+ surface.modify(&mut |data, stride| {
+ for (x, y, pixel) in Pixels::within(input_1.surface(), bounds) {
+ let alpha = f64::from(pixel.a) / 255f64;
+
+ let pixel_vec = if alpha == 0.0 {
+ Vector5::new(0.0, 0.0, 0.0, 0.0, 1.0)
+ } else {
+ Vector5::new(
+ f64::from(pixel.r) / 255f64 / alpha,
+ f64::from(pixel.g) / 255f64 / alpha,
+ f64::from(pixel.b) / 255f64 / alpha,
+ alpha,
+ 1.0,
+ )
+ };
+ let mut new_pixel_vec = Vector5::zeros();
+ self.matrix.mul_to(&pixel_vec, &mut new_pixel_vec);
+
+ let new_alpha = clamp(new_pixel_vec[3], 0.0, 1.0);
+
+ let premultiply = |x: f64| ((clamp(x, 0.0, 1.0) * new_alpha * 255f64) + 0.5) as u8;
+
+ let output_pixel = Pixel {
+ r: premultiply(new_pixel_vec[0]),
+ g: premultiply(new_pixel_vec[1]),
+ b: premultiply(new_pixel_vec[2]),
+ a: ((new_alpha * 255f64) + 0.5) as u8,
+ };
+
+ data.set_pixel(stride, output_pixel, x, y);
+ }
+ });
+
+ Ok(FilterOutput {
+ surface: surface.share()?,
+ bounds,
+ })
+ }
+
+ /// Compute a `type="hueRotate"` matrix.
+ ///
+ /// <https://drafts.fxtf.org/filter-effects/#element-attrdef-fecolormatrix-values>
+ #[rustfmt::skip]
+ pub fn hue_rotate_matrix(radians: f64) -> Matrix5<f64> {
+ let (sin, cos) = radians.sin_cos();
+
+ let a = Matrix3::new(
+ 0.213, 0.715, 0.072,
+ 0.213, 0.715, 0.072,
+ 0.213, 0.715, 0.072,
+ );
+
+ let b = Matrix3::new(
+ 0.787, -0.715, -0.072,
+ -0.213, 0.285, -0.072,
+ -0.213, -0.715, 0.928,
+ );
+
+ let c = Matrix3::new(
+ -0.213, -0.715, 0.928,
+ 0.143, 0.140, -0.283,
+ -0.787, 0.715, 0.072,
+ );
+
+ let top_left = a + b * cos + c * sin;
+
+ let mut matrix = top_left.fixed_resize(0.0);
+ matrix[(3, 3)] = 1.0;
+ matrix[(4, 4)] = 1.0;
+ matrix
+ }
+
+ /// Compute a `type="luminanceToAlpha"` matrix.
+ ///
+ /// <https://drafts.fxtf.org/filter-effects/#element-attrdef-fecolormatrix-values>
+ #[rustfmt::skip]
+ fn luminance_to_alpha_matrix() -> Matrix5<f64> {
+ Matrix5::new(
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.2126, 0.7152, 0.0722, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 1.0,
+ )
+ }
+
+ /// Compute a `type="saturate"` matrix.
+ ///
+ /// <https://drafts.fxtf.org/filter-effects/#element-attrdef-fecolormatrix-values>
+ #[rustfmt::skip]
+ fn saturate_matrix(s: f64) -> Matrix5<f64> {
+ Matrix5::new(
+ 0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0.0, 0.0,
+ 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0.0, 0.0,
+ 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 1.0,
+ )
+ }
+
+ /// Default for `type="matrix"`.
+ ///
+ /// <https://drafts.fxtf.org/filter-effects/#element-attrdef-fecolormatrix-values>
+ fn default_matrix() -> Matrix5<f64> {
+ Matrix5::identity()
+ }
+}
+
+impl FilterEffect for FeColorMatrix {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ let cascaded = CascadedValues::new_from_node(node);
+ let values = cascaded.get();
+
+ let mut params = self.params.clone();
+ params.color_interpolation_filters = values.color_interpolation_filters();
+
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::ColorMatrix(params),
+ }])
+ }
+}
+
+impl Parse for OperationType {
+ fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ Ok(parse_identifiers!(
+ parser,
+ "matrix" => OperationType::Matrix,
+ "saturate" => OperationType::Saturate,
+ "hueRotate" => OperationType::HueRotate,
+ "luminanceToAlpha" => OperationType::LuminanceToAlpha,
+ )?)
+ }
+}
diff --git a/rsvg/src/filters/component_transfer.rs b/rsvg/src/filters/component_transfer.rs
new file mode 100644
index 00000000..6f26f683
--- /dev/null
+++ b/rsvg/src/filters/component_transfer.rs
@@ -0,0 +1,458 @@
+use std::cmp::min;
+
+use cssparser::Parser;
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementData, ElementTrait};
+use crate::error::*;
+use crate::node::{CascadedValues, Node, NodeBorrow};
+use crate::parsers::{NumberList, Parse, ParseValue};
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::surface_utils::{
+ iterators::Pixels, shared_surface::ExclusiveImageSurface, ImageSurfaceDataExt, Pixel,
+};
+use crate::util::clamp;
+use crate::xml::Attributes;
+
+use super::bounds::BoundsBuilder;
+use super::context::{FilterContext, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams,
+ ResolvedPrimitive,
+};
+
+/// The `feComponentTransfer` filter primitive.
+#[derive(Default)]
+pub struct FeComponentTransfer {
+ base: Primitive,
+ params: ComponentTransfer,
+}
+
+/// Resolved `feComponentTransfer` primitive for rendering.
+#[derive(Clone, Default)]
+pub struct ComponentTransfer {
+ pub in1: Input,
+ pub functions: Functions,
+ pub color_interpolation_filters: ColorInterpolationFilters,
+}
+
+impl ElementTrait for FeComponentTransfer {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.params.in1 = self.base.parse_one_input(attrs, session);
+ }
+}
+
+/// Component transfer function types.
+#[derive(Clone, Debug, PartialEq)]
+pub enum FunctionType {
+ Identity,
+ Table,
+ Discrete,
+ Linear,
+ Gamma,
+}
+
+impl Parse for FunctionType {
+ fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ Ok(parse_identifiers!(
+ parser,
+ "identity" => FunctionType::Identity,
+ "table" => FunctionType::Table,
+ "discrete" => FunctionType::Discrete,
+ "linear" => FunctionType::Linear,
+ "gamma" => FunctionType::Gamma,
+ )?)
+ }
+}
+
+/// The compute function parameters.
+struct FunctionParameters {
+ table_values: Vec<f64>,
+ slope: f64,
+ intercept: f64,
+ amplitude: f64,
+ exponent: f64,
+ offset: f64,
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct Functions {
+ pub r: FeFuncR,
+ pub g: FeFuncG,
+ pub b: FeFuncB,
+ pub a: FeFuncA,
+}
+
+/// The compute function type.
+type Function = fn(&FunctionParameters, f64) -> f64;
+
+/// The identity component transfer function.
+fn identity(_: &FunctionParameters, value: f64) -> f64 {
+ value
+}
+
+/// The table component transfer function.
+fn table(params: &FunctionParameters, value: f64) -> f64 {
+ let n = params.table_values.len() - 1;
+ let k = (value * (n as f64)).floor() as usize;
+
+ let k = min(k, n); // Just in case.
+
+ if k == n {
+ return params.table_values[k];
+ }
+
+ let vk = params.table_values[k];
+ let vk1 = params.table_values[k + 1];
+ let k = k as f64;
+ let n = n as f64;
+
+ vk + (value - k / n) * n * (vk1 - vk)
+}
+
+/// The discrete component transfer function.
+fn discrete(params: &FunctionParameters, value: f64) -> f64 {
+ let n = params.table_values.len();
+ let k = (value * (n as f64)).floor() as usize;
+
+ params.table_values[min(k, n - 1)]
+}
+
+/// The linear component transfer function.
+fn linear(params: &FunctionParameters, value: f64) -> f64 {
+ params.slope * value + params.intercept
+}
+
+/// The gamma component transfer function.
+fn gamma(params: &FunctionParameters, value: f64) -> f64 {
+ params.amplitude * value.powf(params.exponent) + params.offset
+}
+
+/// Common values for `feFuncX` elements
+///
+/// The elements `feFuncR`, `feFuncG`, `feFuncB`, `feFuncA` all have the same parameters; this structure
+/// contains them. Later we define newtypes on this struct as [`FeFuncR`], etc.
+#[derive(Clone, Debug, PartialEq)]
+pub struct FeFuncCommon {
+ pub function_type: FunctionType,
+ pub table_values: Vec<f64>,
+ pub slope: f64,
+ pub intercept: f64,
+ pub amplitude: f64,
+ pub exponent: f64,
+ pub offset: f64,
+}
+
+impl Default for FeFuncCommon {
+ #[inline]
+ fn default() -> Self {
+ Self {
+ function_type: FunctionType::Identity,
+ table_values: Vec::new(),
+ slope: 1.0,
+ intercept: 0.0,
+ amplitude: 1.0,
+ exponent: 1.0,
+ offset: 0.0,
+ }
+ }
+}
+
+// All FeFunc* elements are defined here; they just delegate their attributes
+// to the FeFuncCommon inside.
+macro_rules! impl_func {
+ ($(#[$attr:meta])*
+ $name:ident
+ ) => {
+ #[derive(Clone, Debug, Default, PartialEq)]
+ pub struct $name(pub FeFuncCommon);
+
+ impl ElementTrait for $name {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.0.set_attributes(attrs, session);
+ }
+ }
+ };
+}
+
+impl_func!(
+ /// The `feFuncR` element.
+ FeFuncR
+);
+
+impl_func!(
+ /// The `feFuncG` element.
+ FeFuncG
+);
+
+impl_func!(
+ /// The `feFuncB` element.
+ FeFuncB
+);
+
+impl_func!(
+ /// The `feFuncA` element.
+ FeFuncA
+);
+
+impl FeFuncCommon {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "type") => {
+ set_attribute(&mut self.function_type, attr.parse(value), session)
+ }
+ expanded_name!("", "tableValues") => {
+ // #691: Limit list to 256 to mitigate malicious SVGs
+ let mut number_list = NumberList::<0, 256>(Vec::new());
+ set_attribute(&mut number_list, attr.parse(value), session);
+ self.table_values = number_list.0;
+ }
+ expanded_name!("", "slope") => {
+ set_attribute(&mut self.slope, attr.parse(value), session)
+ }
+ expanded_name!("", "intercept") => {
+ set_attribute(&mut self.intercept, attr.parse(value), session)
+ }
+ expanded_name!("", "amplitude") => {
+ set_attribute(&mut self.amplitude, attr.parse(value), session)
+ }
+ expanded_name!("", "exponent") => {
+ set_attribute(&mut self.exponent, attr.parse(value), session)
+ }
+ expanded_name!("", "offset") => {
+ set_attribute(&mut self.offset, attr.parse(value), session)
+ }
+
+ _ => (),
+ }
+ }
+
+ // The table function type with empty table_values is considered
+ // an identity function.
+ match self.function_type {
+ FunctionType::Table | FunctionType::Discrete => {
+ if self.table_values.is_empty() {
+ self.function_type = FunctionType::Identity;
+ }
+ }
+ _ => (),
+ }
+ }
+
+ fn function_parameters(&self) -> FunctionParameters {
+ FunctionParameters {
+ table_values: self.table_values.clone(),
+ slope: self.slope,
+ intercept: self.intercept,
+ amplitude: self.amplitude,
+ exponent: self.exponent,
+ offset: self.offset,
+ }
+ }
+
+ fn function(&self) -> Function {
+ match self.function_type {
+ FunctionType::Identity => identity,
+ FunctionType::Table => table,
+ FunctionType::Discrete => discrete,
+ FunctionType::Linear => linear,
+ FunctionType::Gamma => gamma,
+ }
+ }
+}
+
+macro_rules! func_or_default {
+ ($func_node:ident, $func_type:ident) => {
+ match $func_node {
+ Some(ref f) => match &*f.borrow_element_data() {
+ ElementData::$func_type(e) => (**e).clone(),
+ _ => unreachable!(),
+ },
+ _ => $func_type::default(),
+ }
+ };
+}
+
+macro_rules! get_func_x_node {
+ ($func_node:ident, $func_type:ident) => {
+ $func_node
+ .children()
+ .rev()
+ .filter(|c| c.is_element())
+ .find(|c| matches!(*c.borrow_element_data(), ElementData::$func_type(_)))
+ };
+}
+
+impl ComponentTransfer {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ let input_1 = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in1,
+ self.color_interpolation_filters,
+ )?;
+ let bounds: IRect = bounds_builder
+ .add_input(&input_1)
+ .compute(ctx)
+ .clipped
+ .into();
+
+ // Create the output surface.
+ let mut surface = ExclusiveImageSurface::new(
+ ctx.source_graphic().width(),
+ ctx.source_graphic().height(),
+ input_1.surface().surface_type(),
+ )?;
+
+ fn compute_func(func: &FeFuncCommon) -> impl Fn(u8, f64, f64) -> u8 {
+ let compute = func.function();
+ let params = func.function_parameters();
+
+ move |value, alpha, new_alpha| {
+ let value = f64::from(value) / 255f64;
+
+ let unpremultiplied = if alpha == 0f64 { 0f64 } else { value / alpha };
+
+ let new_value = compute(&params, unpremultiplied);
+ let new_value = clamp(new_value, 0f64, 1f64);
+
+ ((new_value * new_alpha * 255f64) + 0.5) as u8
+ }
+ }
+
+ let compute_r = compute_func(&self.functions.r.0);
+ let compute_g = compute_func(&self.functions.g.0);
+ let compute_b = compute_func(&self.functions.b.0);
+
+ // Alpha gets special handling since everything else depends on it.
+ let compute_a = self.functions.a.0.function();
+ let params_a = self.functions.a.0.function_parameters();
+ let compute_a = |alpha| compute_a(&params_a, alpha);
+
+ // Do the actual processing.
+ surface.modify(&mut |data, stride| {
+ for (x, y, pixel) in Pixels::within(input_1.surface(), bounds) {
+ let alpha = f64::from(pixel.a) / 255f64;
+ let new_alpha = compute_a(alpha);
+
+ let output_pixel = Pixel {
+ r: compute_r(pixel.r, alpha, new_alpha),
+ g: compute_g(pixel.g, alpha, new_alpha),
+ b: compute_b(pixel.b, alpha, new_alpha),
+ a: ((new_alpha * 255f64) + 0.5) as u8,
+ };
+
+ data.set_pixel(stride, output_pixel, x, y);
+ }
+ });
+
+ Ok(FilterOutput {
+ surface: surface.share()?,
+ bounds,
+ })
+ }
+}
+
+impl FilterEffect for FeComponentTransfer {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ let cascaded = CascadedValues::new_from_node(node);
+ let values = cascaded.get();
+
+ let mut params = self.params.clone();
+ params.functions = get_functions(node)?;
+ params.color_interpolation_filters = values.color_interpolation_filters();
+
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::ComponentTransfer(params),
+ }])
+ }
+}
+
+/// Takes a feComponentTransfer and walks its children to produce the feFuncX arguments.
+fn get_functions(node: &Node) -> Result<Functions, FilterResolveError> {
+ let func_r_node = get_func_x_node!(node, FeFuncR);
+ let func_g_node = get_func_x_node!(node, FeFuncG);
+ let func_b_node = get_func_x_node!(node, FeFuncB);
+ let func_a_node = get_func_x_node!(node, FeFuncA);
+
+ let r = func_or_default!(func_r_node, FeFuncR);
+ let g = func_or_default!(func_g_node, FeFuncG);
+ let b = func_or_default!(func_b_node, FeFuncB);
+ let a = func_or_default!(func_a_node, FeFuncA);
+
+ Ok(Functions { r, g, b, a })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::document::Document;
+
+ #[test]
+ fn extracts_functions() {
+ let document = Document::load_from_bytes(
+ br#"<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg">
+ <filter id="filter">
+ <feComponentTransfer id="component_transfer">
+ <!-- no feFuncR so it should get the defaults -->
+
+ <feFuncG type="table" tableValues="0.0 1.0 2.0"/>
+
+ <feFuncB type="table"/>
+ <!-- duplicate this to test that last-one-wins -->
+ <feFuncB type="discrete" tableValues="0.0, 1.0" slope="1.0" intercept="2.0" amplitude="3.0" exponent="4.0" offset="5.0"/>
+
+ <!-- no feFuncA so it should get the defaults -->
+ </feComponentTransfer>
+ </filter>
+</svg>
+"#
+ );
+
+ let component_transfer = document.lookup_internal_node("component_transfer").unwrap();
+ let functions = get_functions(&component_transfer).unwrap();
+
+ assert_eq!(
+ functions,
+ Functions {
+ r: FeFuncR::default(),
+
+ g: FeFuncG(FeFuncCommon {
+ function_type: FunctionType::Table,
+ table_values: vec![0.0, 1.0, 2.0],
+ ..FeFuncCommon::default()
+ }),
+
+ b: FeFuncB(FeFuncCommon {
+ function_type: FunctionType::Discrete,
+ table_values: vec![0.0, 1.0],
+ slope: 1.0,
+ intercept: 2.0,
+ amplitude: 3.0,
+ exponent: 4.0,
+ offset: 5.0,
+ ..FeFuncCommon::default()
+ }),
+
+ a: FeFuncA::default(),
+ }
+ );
+ }
+}
diff --git a/rsvg/src/filters/composite.rs b/rsvg/src/filters/composite.rs
new file mode 100644
index 00000000..c5c02af1
--- /dev/null
+++ b/rsvg/src/filters/composite.rs
@@ -0,0 +1,179 @@
+use cssparser::Parser;
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementTrait};
+use crate::error::*;
+use crate::node::{CascadedValues, Node};
+use crate::parsers::{Parse, ParseValue};
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::surface_utils::shared_surface::Operator as SurfaceOperator;
+use crate::xml::Attributes;
+
+use super::bounds::BoundsBuilder;
+use super::context::{FilterContext, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams,
+ ResolvedPrimitive,
+};
+
+/// Enumeration of the possible compositing operations.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
+pub enum Operator {
+ Over,
+ In,
+ Out,
+ Atop,
+ Xor,
+ Arithmetic,
+}
+
+enum_default!(Operator, Operator::Over);
+
+/// The `feComposite` filter primitive.
+#[derive(Default)]
+pub struct FeComposite {
+ base: Primitive,
+ params: Composite,
+}
+
+/// Resolved `feComposite` primitive for rendering.
+#[derive(Clone, Default)]
+pub struct Composite {
+ pub in1: Input,
+ pub in2: Input,
+ pub operator: Operator,
+ pub k1: f64,
+ pub k2: f64,
+ pub k3: f64,
+ pub k4: f64,
+ pub color_interpolation_filters: ColorInterpolationFilters,
+}
+
+impl ElementTrait for FeComposite {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ let (in1, in2) = self.base.parse_two_inputs(attrs, session);
+ self.params.in1 = in1;
+ self.params.in2 = in2;
+
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "operator") => {
+ set_attribute(&mut self.params.operator, attr.parse(value), session)
+ }
+ expanded_name!("", "k1") => {
+ set_attribute(&mut self.params.k1, attr.parse(value), session)
+ }
+ expanded_name!("", "k2") => {
+ set_attribute(&mut self.params.k2, attr.parse(value), session)
+ }
+ expanded_name!("", "k3") => {
+ set_attribute(&mut self.params.k3, attr.parse(value), session)
+ }
+ expanded_name!("", "k4") => {
+ set_attribute(&mut self.params.k4, attr.parse(value), session)
+ }
+ _ => (),
+ }
+ }
+ }
+}
+
+impl Composite {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ let input_1 = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in1,
+ self.color_interpolation_filters,
+ )?;
+ let input_2 = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in2,
+ self.color_interpolation_filters,
+ )?;
+ let bounds: IRect = bounds_builder
+ .add_input(&input_1)
+ .add_input(&input_2)
+ .compute(ctx)
+ .clipped
+ .into();
+
+ let surface = if self.operator == Operator::Arithmetic {
+ input_1.surface().compose_arithmetic(
+ input_2.surface(),
+ bounds,
+ self.k1,
+ self.k2,
+ self.k3,
+ self.k4,
+ )?
+ } else {
+ input_1
+ .surface()
+ .compose(input_2.surface(), bounds, self.operator.into())?
+ };
+
+ Ok(FilterOutput { surface, bounds })
+ }
+}
+
+impl FilterEffect for FeComposite {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ let cascaded = CascadedValues::new_from_node(node);
+ let values = cascaded.get();
+
+ let mut params = self.params.clone();
+ params.color_interpolation_filters = values.color_interpolation_filters();
+
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::Composite(params),
+ }])
+ }
+}
+
+impl Parse for Operator {
+ fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ Ok(parse_identifiers!(
+ parser,
+ "over" => Operator::Over,
+ "in" => Operator::In,
+ "out" => Operator::Out,
+ "atop" => Operator::Atop,
+ "xor" => Operator::Xor,
+ "arithmetic" => Operator::Arithmetic,
+ )?)
+ }
+}
+
+impl From<Operator> for SurfaceOperator {
+ #[inline]
+ fn from(x: Operator) -> SurfaceOperator {
+ use Operator::*;
+
+ match x {
+ Over => SurfaceOperator::Over,
+ In => SurfaceOperator::In,
+ Out => SurfaceOperator::Out,
+ Atop => SurfaceOperator::Atop,
+ Xor => SurfaceOperator::Xor,
+
+ _ => panic!("can't convert Operator::Arithmetic to a shared_surface::Operator"),
+ }
+ }
+}
diff --git a/rsvg/src/filters/context.rs b/rsvg/src/filters/context.rs
new file mode 100644
index 00000000..a09160ab
--- /dev/null
+++ b/rsvg/src/filters/context.rs
@@ -0,0 +1,405 @@
+use once_cell::sync::OnceCell;
+use std::collections::HashMap;
+use std::rc::Rc;
+
+use crate::bbox::BoundingBox;
+use crate::coord_units::CoordUnits;
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::filter::UserSpaceFilter;
+use crate::paint_server::UserSpacePaintSource;
+use crate::parsers::CustomIdent;
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::{IRect, Rect};
+use crate::surface_utils::shared_surface::{SharedImageSurface, SurfaceType};
+use crate::transform::Transform;
+
+use super::error::FilterError;
+use super::Input;
+
+/// A filter primitive output.
+#[derive(Debug, Clone)]
+pub struct FilterOutput {
+ /// The surface after the filter primitive was applied.
+ pub surface: SharedImageSurface,
+
+ /// The filter primitive subregion.
+ pub bounds: IRect,
+}
+
+/// A filter primitive result.
+#[derive(Debug, Clone)]
+pub struct FilterResult {
+ /// The name of this result: the value of the `result` attribute.
+ pub name: Option<CustomIdent>,
+
+ /// The output.
+ pub output: FilterOutput,
+}
+
+/// An input to a filter primitive.
+#[derive(Debug, Clone)]
+pub enum FilterInput {
+ /// One of the standard inputs.
+ StandardInput(SharedImageSurface),
+ /// Output of another filter primitive.
+ PrimitiveOutput(FilterOutput),
+}
+
+/// The filter rendering context.
+pub struct FilterContext {
+ /// Paint source for primitives which have an input value equal to `StrokePaint`.
+ stroke_paint: Rc<UserSpacePaintSource>,
+ /// Paint source for primitives which have an input value equal to `FillPaint`.
+ fill_paint: Rc<UserSpacePaintSource>,
+
+ /// The source graphic surface.
+ source_surface: SharedImageSurface,
+ /// Output of the last filter primitive.
+ last_result: Option<FilterOutput>,
+ /// Surfaces of the previous filter primitives by name.
+ previous_results: HashMap<CustomIdent, FilterOutput>,
+
+ /// Input surface for primitives that require an input of `BackgroundImage` or `BackgroundAlpha`. Computed lazily.
+ background_surface: OnceCell<Result<SharedImageSurface, FilterError>>,
+
+ // Input surface for primitives that require an input of `StrokePaint`, Computed lazily.
+ stroke_paint_surface: OnceCell<Result<SharedImageSurface, FilterError>>,
+
+ // Input surface for primitives that require an input of `FillPaint`, Computed lazily.
+ fill_paint_surface: OnceCell<Result<SharedImageSurface, FilterError>>,
+
+ /// Primtive units
+ primitive_units: CoordUnits,
+ /// The filter effects region.
+ effects_region: Rect,
+
+ /// The filter element affine matrix.
+ ///
+ /// If `filterUnits == userSpaceOnUse`, equal to the drawing context matrix, so, for example,
+ /// if the target node is in a group with `transform="translate(30, 20)"`, this will be equal
+ /// to a matrix that translates to 30, 20 (and does not scale). Note that the target node
+ /// bounding box isn't included in the computations in this case.
+ ///
+ /// If `filterUnits == objectBoundingBox`, equal to the target node bounding box matrix
+ /// multiplied by the drawing context matrix, so, for example, if the target node is in a group
+ /// with `transform="translate(30, 20)"` and also has `x="1", y="1", width="50", height="50"`,
+ /// this will be equal to a matrix that translates to 31, 21 and scales to 50, 50.
+ ///
+ /// This is to be used in conjunction with setting the viewbox size to account for the scaling.
+ /// For `filterUnits == userSpaceOnUse`, the viewbox will have the actual resolution size, and
+ /// for `filterUnits == objectBoundingBox`, the viewbox will have the size of 1, 1.
+ _affine: Transform,
+
+ /// The filter primitive affine matrix.
+ ///
+ /// See the comments for `_affine`, they largely apply here.
+ paffine: Transform,
+}
+
+impl FilterContext {
+ /// Creates a new `FilterContext`.
+ pub fn new(
+ filter: &UserSpaceFilter,
+ stroke_paint: Rc<UserSpacePaintSource>,
+ fill_paint: Rc<UserSpacePaintSource>,
+ source_surface: &SharedImageSurface,
+ draw_transform: Transform,
+ node_bbox: BoundingBox,
+ ) -> Result<Self, FilterError> {
+ // The rect can be empty (for example, if the filter is applied to an empty group).
+ // However, with userSpaceOnUse it's still possible to create images with a filter.
+ let bbox_rect = node_bbox.rect.unwrap_or_default();
+
+ let affine = match filter.filter_units {
+ CoordUnits::UserSpaceOnUse => draw_transform,
+ CoordUnits::ObjectBoundingBox => Transform::new_unchecked(
+ bbox_rect.width(),
+ 0.0,
+ 0.0,
+ bbox_rect.height(),
+ bbox_rect.x0,
+ bbox_rect.y0,
+ )
+ .post_transform(&draw_transform),
+ };
+
+ let paffine = match filter.primitive_units {
+ CoordUnits::UserSpaceOnUse => draw_transform,
+ CoordUnits::ObjectBoundingBox => Transform::new_unchecked(
+ bbox_rect.width(),
+ 0.0,
+ 0.0,
+ bbox_rect.height(),
+ bbox_rect.x0,
+ bbox_rect.y0,
+ )
+ .post_transform(&draw_transform),
+ };
+
+ if !(affine.is_invertible() && paffine.is_invertible()) {
+ return Err(FilterError::InvalidParameter(
+ "transform is not invertible".to_string(),
+ ));
+ }
+
+ let effects_region = {
+ let mut bbox = BoundingBox::new();
+ let other_bbox = BoundingBox::new()
+ .with_transform(affine)
+ .with_rect(filter.rect);
+
+ // At this point all of the previous viewbox and matrix business gets converted to pixel
+ // coordinates in the final surface, because bbox is created with an identity transform.
+ bbox.insert(&other_bbox);
+
+ // Finally, clip to the width and height of our surface.
+ let (width, height) = (source_surface.width(), source_surface.height());
+ let rect = Rect::from_size(f64::from(width), f64::from(height));
+ let other_bbox = BoundingBox::new().with_rect(rect);
+ bbox.clip(&other_bbox);
+
+ bbox.rect.unwrap()
+ };
+
+ Ok(Self {
+ stroke_paint,
+ fill_paint,
+ source_surface: source_surface.clone(),
+ last_result: None,
+ previous_results: HashMap::new(),
+ background_surface: OnceCell::new(),
+ stroke_paint_surface: OnceCell::new(),
+ fill_paint_surface: OnceCell::new(),
+ primitive_units: filter.primitive_units,
+ effects_region,
+ _affine: affine,
+ paffine,
+ })
+ }
+
+ /// Returns the surface corresponding to the source graphic.
+ #[inline]
+ pub fn source_graphic(&self) -> &SharedImageSurface {
+ &self.source_surface
+ }
+
+ /// Returns the surface corresponding to the background image snapshot.
+ fn background_image(&self, draw_ctx: &DrawingCtx) -> Result<SharedImageSurface, FilterError> {
+ let res = self.background_surface.get_or_init(|| {
+ draw_ctx
+ .get_snapshot(self.source_surface.width(), self.source_surface.height())
+ .map_err(FilterError::Rendering)
+ });
+
+ res.as_ref().map(|s| s.clone()).map_err(|e| e.clone())
+ }
+
+ /// Returns a surface filled with the current stroke's paint, for `StrokePaint` inputs in primitives.
+ ///
+ /// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#attr-valuedef-in-strokepaint>
+ fn stroke_paint_image(
+ &self,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<SharedImageSurface, FilterError> {
+ let res = self.stroke_paint_surface.get_or_init(|| {
+ Ok(draw_ctx.get_paint_source_surface(
+ self.source_surface.width(),
+ self.source_surface.height(),
+ acquired_nodes,
+ &self.stroke_paint,
+ )?)
+ });
+
+ res.as_ref().map(|s| s.clone()).map_err(|e| e.clone())
+ }
+
+ /// Returns a surface filled with the current fill's paint, for `FillPaint` inputs in primitives.
+ ///
+ /// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#attr-valuedef-in-fillpaint>
+ fn fill_paint_image(
+ &self,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<SharedImageSurface, FilterError> {
+ let res = self.fill_paint_surface.get_or_init(|| {
+ Ok(draw_ctx.get_paint_source_surface(
+ self.source_surface.width(),
+ self.source_surface.height(),
+ acquired_nodes,
+ &self.fill_paint,
+ )?)
+ });
+
+ res.as_ref().map(|s| s.clone()).map_err(|e| e.clone())
+ }
+
+ /// Converts this `FilterContext` into the surface corresponding to the output of the filter
+ /// chain.
+ ///
+ /// The returned surface is in the sRGB color space.
+ // TODO: sRGB conversion should probably be done by the caller.
+ #[inline]
+ pub fn into_output(self) -> Result<SharedImageSurface, cairo::Error> {
+ match self.last_result {
+ Some(FilterOutput { surface, bounds }) => surface.to_srgb(bounds),
+ None => SharedImageSurface::empty(
+ self.source_surface.width(),
+ self.source_surface.height(),
+ SurfaceType::AlphaOnly,
+ ),
+ }
+ }
+
+ /// Stores a filter primitive result into the context.
+ #[inline]
+ pub fn store_result(&mut self, result: FilterResult) {
+ if let Some(name) = result.name {
+ self.previous_results.insert(name, result.output.clone());
+ }
+
+ self.last_result = Some(result.output);
+ }
+
+ /// Returns the paffine matrix.
+ #[inline]
+ pub fn paffine(&self) -> Transform {
+ self.paffine
+ }
+
+ /// Returns the primitive units.
+ #[inline]
+ pub fn primitive_units(&self) -> CoordUnits {
+ self.primitive_units
+ }
+
+ /// Returns the filter effects region.
+ #[inline]
+ pub fn effects_region(&self) -> Rect {
+ self.effects_region
+ }
+
+ /// Get a filter primitive's default input as if its `in=\"...\"` were not specified.
+ ///
+ /// Per <https://drafts.fxtf.org/filter-effects/#element-attrdef-filter-primitive-in>,
+ /// "References to non-existent results will be treated as if no result was
+ /// specified". That is, fall back to the last result in the filter chain, or if this
+ /// is the first in the chain, just use SourceGraphic.
+ fn get_unspecified_input(&self) -> FilterInput {
+ if let Some(output) = self.last_result.as_ref() {
+ FilterInput::PrimitiveOutput(output.clone())
+ } else {
+ FilterInput::StandardInput(self.source_graphic().clone())
+ }
+ }
+
+ /// Retrieves the filter input surface according to the SVG rules.
+ fn get_input_raw(
+ &self,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ in_: &Input,
+ ) -> Result<FilterInput, FilterError> {
+ match *in_ {
+ Input::Unspecified => Ok(self.get_unspecified_input()),
+
+ Input::SourceGraphic => Ok(FilterInput::StandardInput(self.source_graphic().clone())),
+
+ Input::SourceAlpha => self
+ .source_graphic()
+ .extract_alpha(self.effects_region().into())
+ .map_err(FilterError::CairoError)
+ .map(FilterInput::StandardInput),
+
+ Input::BackgroundImage => self
+ .background_image(draw_ctx)
+ .map(FilterInput::StandardInput),
+
+ Input::BackgroundAlpha => self
+ .background_image(draw_ctx)
+ .and_then(|surface| {
+ surface
+ .extract_alpha(self.effects_region().into())
+ .map_err(FilterError::CairoError)
+ })
+ .map(FilterInput::StandardInput),
+
+ Input::FillPaint => self
+ .fill_paint_image(acquired_nodes, draw_ctx)
+ .map(FilterInput::StandardInput),
+
+ Input::StrokePaint => self
+ .stroke_paint_image(acquired_nodes, draw_ctx)
+ .map(FilterInput::StandardInput),
+
+ Input::FilterOutput(ref name) => {
+ let input = match self.previous_results.get(name).cloned() {
+ Some(filter_output) => {
+ // Happy path: we found a previous primitive's named output, so pass it on.
+ FilterInput::PrimitiveOutput(filter_output)
+ }
+
+ None => {
+ // Fallback path: we didn't find a primitive's output by the
+ // specified name, so fall back to using an unspecified output.
+ // Per the spec, "References to non-existent results will be
+ // treated as if no result was specified." -
+ // https://drafts.fxtf.org/filter-effects/#element-attrdef-filter-primitive-in
+ self.get_unspecified_input()
+ }
+ };
+
+ Ok(input)
+ }
+ }
+ }
+
+ /// Retrieves the filter input surface according to the SVG rules.
+ ///
+ /// The surface will be converted to the color space specified by `color_interpolation_filters`.
+ pub fn get_input(
+ &self,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ in_: &Input,
+ color_interpolation_filters: ColorInterpolationFilters,
+ ) -> Result<FilterInput, FilterError> {
+ let raw = self.get_input_raw(acquired_nodes, draw_ctx, in_)?;
+
+ // Convert the input surface to the desired format.
+ let (surface, bounds) = match raw {
+ FilterInput::StandardInput(ref surface) => (surface, self.effects_region().into()),
+ FilterInput::PrimitiveOutput(FilterOutput {
+ ref surface,
+ ref bounds,
+ }) => (surface, *bounds),
+ };
+
+ let surface = match color_interpolation_filters {
+ ColorInterpolationFilters::Auto => Ok(surface.clone()),
+ ColorInterpolationFilters::LinearRgb => surface.to_linear_rgb(bounds),
+ ColorInterpolationFilters::Srgb => surface.to_srgb(bounds),
+ };
+
+ surface
+ .map_err(FilterError::CairoError)
+ .map(|surface| match raw {
+ FilterInput::StandardInput(_) => FilterInput::StandardInput(surface),
+ FilterInput::PrimitiveOutput(ref output) => {
+ FilterInput::PrimitiveOutput(FilterOutput { surface, ..*output })
+ }
+ })
+ }
+}
+
+impl FilterInput {
+ /// Retrieves the surface from `FilterInput`.
+ #[inline]
+ pub fn surface(&self) -> &SharedImageSurface {
+ match *self {
+ FilterInput::StandardInput(ref surface) => surface,
+ FilterInput::PrimitiveOutput(FilterOutput { ref surface, .. }) => surface,
+ }
+ }
+}
diff --git a/rsvg/src/filters/convolve_matrix.rs b/rsvg/src/filters/convolve_matrix.rs
new file mode 100644
index 00000000..096ad043
--- /dev/null
+++ b/rsvg/src/filters/convolve_matrix.rs
@@ -0,0 +1,354 @@
+use cssparser::Parser;
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+use nalgebra::{DMatrix, Dyn, VecStorage};
+
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementTrait};
+use crate::error::*;
+use crate::node::{CascadedValues, Node};
+use crate::parsers::{NumberList, NumberOptionalNumber, Parse, ParseValue};
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::surface_utils::{
+ iterators::{PixelRectangle, Pixels},
+ shared_surface::ExclusiveImageSurface,
+ EdgeMode, ImageSurfaceDataExt, Pixel,
+};
+use crate::util::clamp;
+use crate::xml::Attributes;
+
+use super::bounds::BoundsBuilder;
+use super::context::{FilterContext, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams,
+ ResolvedPrimitive,
+};
+
+/// The `feConvolveMatrix` filter primitive.
+#[derive(Default)]
+pub struct FeConvolveMatrix {
+ base: Primitive,
+ params: ConvolveMatrix,
+}
+
+/// Resolved `feConvolveMatrix` primitive for rendering.
+#[derive(Clone)]
+pub struct ConvolveMatrix {
+ in1: Input,
+ order: NumberOptionalNumber<u32>,
+ kernel_matrix: NumberList<0, 400>, // #691: Limit list to 400 (20x20) to mitigate malicious SVGs
+ divisor: f64,
+ bias: f64,
+ target_x: Option<u32>,
+ target_y: Option<u32>,
+ edge_mode: EdgeMode,
+ kernel_unit_length: Option<(f64, f64)>,
+ preserve_alpha: bool,
+ color_interpolation_filters: ColorInterpolationFilters,
+}
+
+impl Default for ConvolveMatrix {
+ /// Constructs a new `ConvolveMatrix` with empty properties.
+ #[inline]
+ fn default() -> ConvolveMatrix {
+ ConvolveMatrix {
+ in1: Default::default(),
+ order: NumberOptionalNumber(3, 3),
+ kernel_matrix: NumberList(Vec::new()),
+ divisor: 0.0,
+ bias: 0.0,
+ target_x: None,
+ target_y: None,
+ edge_mode: EdgeMode::Duplicate,
+ kernel_unit_length: None,
+ preserve_alpha: false,
+ color_interpolation_filters: Default::default(),
+ }
+ }
+}
+
+impl ElementTrait for FeConvolveMatrix {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.params.in1 = self.base.parse_one_input(attrs, session);
+
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "order") => {
+ set_attribute(&mut self.params.order, attr.parse(value), session)
+ }
+ expanded_name!("", "kernelMatrix") => {
+ set_attribute(&mut self.params.kernel_matrix, attr.parse(value), session)
+ }
+ expanded_name!("", "divisor") => {
+ set_attribute(&mut self.params.divisor, attr.parse(value), session)
+ }
+ expanded_name!("", "bias") => {
+ set_attribute(&mut self.params.bias, attr.parse(value), session)
+ }
+ expanded_name!("", "targetX") => {
+ set_attribute(&mut self.params.target_x, attr.parse(value), session)
+ }
+ expanded_name!("", "targetY") => {
+ set_attribute(&mut self.params.target_y, attr.parse(value), session)
+ }
+ expanded_name!("", "edgeMode") => {
+ set_attribute(&mut self.params.edge_mode, attr.parse(value), session)
+ }
+ expanded_name!("", "kernelUnitLength") => {
+ let v: Result<NumberOptionalNumber<f64>, _> = attr.parse(value);
+ match v {
+ Ok(NumberOptionalNumber(x, y)) => {
+ self.params.kernel_unit_length = Some((x, y));
+ }
+
+ Err(e) => {
+ rsvg_log!(session, "ignoring attribute with invalid value: {}", e);
+ }
+ }
+ }
+ expanded_name!("", "preserveAlpha") => {
+ set_attribute(&mut self.params.preserve_alpha, attr.parse(value), session);
+ }
+
+ _ => (),
+ }
+ }
+ }
+}
+
+impl ConvolveMatrix {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ #![allow(clippy::many_single_char_names)]
+
+ let input_1 = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in1,
+ self.color_interpolation_filters,
+ )?;
+ let mut bounds: IRect = bounds_builder
+ .add_input(&input_1)
+ .compute(ctx)
+ .clipped
+ .into();
+ let original_bounds = bounds;
+
+ let target_x = match self.target_x {
+ Some(x) if x >= self.order.0 => {
+ return Err(FilterError::InvalidParameter(
+ "targetX must be less than orderX".to_string(),
+ ))
+ }
+ Some(x) => x,
+ None => self.order.0 / 2,
+ };
+
+ let target_y = match self.target_y {
+ Some(y) if y >= self.order.1 => {
+ return Err(FilterError::InvalidParameter(
+ "targetY must be less than orderY".to_string(),
+ ))
+ }
+ Some(y) => y,
+ None => self.order.1 / 2,
+ };
+
+ let mut input_surface = if self.preserve_alpha {
+ // preserve_alpha means we need to premultiply and unpremultiply the values.
+ input_1.surface().unpremultiply(bounds)?
+ } else {
+ input_1.surface().clone()
+ };
+
+ let scale = self
+ .kernel_unit_length
+ .and_then(|(x, y)| {
+ if x <= 0.0 || y <= 0.0 {
+ None
+ } else {
+ Some((x, y))
+ }
+ })
+ .map(|(dx, dy)| ctx.paffine().transform_distance(dx, dy));
+
+ if let Some((ox, oy)) = scale {
+ // Scale the input surface to match kernel_unit_length.
+ let (new_surface, new_bounds) = input_surface.scale(bounds, 1.0 / ox, 1.0 / oy)?;
+
+ input_surface = new_surface;
+ bounds = new_bounds;
+ }
+
+ let cols = self.order.0 as usize;
+ let rows = self.order.1 as usize;
+ let number_of_elements = cols * rows;
+ let numbers = self.kernel_matrix.0.clone();
+
+ if numbers.len() != number_of_elements && numbers.len() != 400 {
+ // "If the result of orderX * orderY is not equal to the the number of entries
+ // in the value list, the filter primitive acts as a pass through filter."
+ //
+ // https://drafts.fxtf.org/filter-effects/#element-attrdef-feconvolvematrix-kernelmatrix
+ rsvg_log!(
+ draw_ctx.session(),
+ "feConvolveMatrix got {} elements when it expected {}; ignoring it",
+ numbers.len(),
+ number_of_elements
+ );
+ return Ok(FilterOutput {
+ surface: input_1.surface().clone(),
+ bounds: original_bounds,
+ });
+ }
+
+ let matrix = DMatrix::from_data(VecStorage::new(Dyn(rows), Dyn(cols), numbers));
+
+ let divisor = if self.divisor != 0.0 {
+ self.divisor
+ } else {
+ let d = matrix.iter().sum();
+
+ if d != 0.0 {
+ d
+ } else {
+ 1.0
+ }
+ };
+
+ let mut surface = ExclusiveImageSurface::new(
+ input_surface.width(),
+ input_surface.height(),
+ input_1.surface().surface_type(),
+ )?;
+
+ surface.modify(&mut |data, stride| {
+ for (x, y, pixel) in Pixels::within(&input_surface, bounds) {
+ // Compute the convolution rectangle bounds.
+ let kernel_bounds = IRect::new(
+ x as i32 - target_x as i32,
+ y as i32 - target_y as i32,
+ x as i32 - target_x as i32 + self.order.0 as i32,
+ y as i32 - target_y as i32 + self.order.1 as i32,
+ );
+
+ // Do the convolution.
+ let mut r = 0.0;
+ let mut g = 0.0;
+ let mut b = 0.0;
+ let mut a = 0.0;
+
+ for (x, y, pixel) in
+ PixelRectangle::within(&input_surface, bounds, kernel_bounds, self.edge_mode)
+ {
+ let kernel_x = (kernel_bounds.x1 - x - 1) as usize;
+ let kernel_y = (kernel_bounds.y1 - y - 1) as usize;
+
+ r += f64::from(pixel.r) / 255.0 * matrix[(kernel_y, kernel_x)];
+ g += f64::from(pixel.g) / 255.0 * matrix[(kernel_y, kernel_x)];
+ b += f64::from(pixel.b) / 255.0 * matrix[(kernel_y, kernel_x)];
+
+ if !self.preserve_alpha {
+ a += f64::from(pixel.a) / 255.0 * matrix[(kernel_y, kernel_x)];
+ }
+ }
+
+ // If preserve_alpha is true, set a to the source alpha value.
+ if self.preserve_alpha {
+ a = f64::from(pixel.a) / 255.0;
+ } else {
+ a = a / divisor + self.bias;
+ }
+
+ let clamped_a = clamp(a, 0.0, 1.0);
+
+ let compute = |x| {
+ let x = x / divisor + self.bias * a;
+
+ let x = if self.preserve_alpha {
+ // Premultiply the output value.
+ clamp(x, 0.0, 1.0) * clamped_a
+ } else {
+ clamp(x, 0.0, clamped_a)
+ };
+
+ ((x * 255.0) + 0.5) as u8
+ };
+
+ let output_pixel = Pixel {
+ r: compute(r),
+ g: compute(g),
+ b: compute(b),
+ a: ((clamped_a * 255.0) + 0.5) as u8,
+ };
+
+ data.set_pixel(stride, output_pixel, x, y);
+ }
+ });
+
+ let mut surface = surface.share()?;
+
+ if let Some((ox, oy)) = scale {
+ // Scale the output surface back.
+ surface = surface.scale_to(
+ ctx.source_graphic().width(),
+ ctx.source_graphic().height(),
+ original_bounds,
+ ox,
+ oy,
+ )?;
+
+ bounds = original_bounds;
+ }
+
+ Ok(FilterOutput { surface, bounds })
+ }
+}
+
+impl FilterEffect for FeConvolveMatrix {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ let cascaded = CascadedValues::new_from_node(node);
+ let values = cascaded.get();
+
+ let mut params = self.params.clone();
+ params.color_interpolation_filters = values.color_interpolation_filters();
+
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::ConvolveMatrix(params),
+ }])
+ }
+}
+
+impl Parse for EdgeMode {
+ fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ Ok(parse_identifiers!(
+ parser,
+ "duplicate" => EdgeMode::Duplicate,
+ "wrap" => EdgeMode::Wrap,
+ "none" => EdgeMode::None,
+ )?)
+ }
+}
+
+// Used for the preserveAlpha attribute
+impl Parse for bool {
+ fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ Ok(parse_identifiers!(
+ parser,
+ "false" => false,
+ "true" => true,
+ )?)
+ }
+}
diff --git a/rsvg/src/filters/displacement_map.rs b/rsvg/src/filters/displacement_map.rs
new file mode 100644
index 00000000..f0cead68
--- /dev/null
+++ b/rsvg/src/filters/displacement_map.rs
@@ -0,0 +1,195 @@
+use cssparser::Parser;
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementTrait};
+use crate::error::*;
+use crate::node::{CascadedValues, Node};
+use crate::parsers::{Parse, ParseValue};
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::surface_utils::{iterators::Pixels, shared_surface::ExclusiveImageSurface};
+use crate::xml::Attributes;
+
+use super::bounds::BoundsBuilder;
+use super::context::{FilterContext, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams,
+ ResolvedPrimitive,
+};
+
+/// Enumeration of the color channels the displacement map can source.
+#[derive(Clone, Copy)]
+enum ColorChannel {
+ R,
+ G,
+ B,
+ A,
+}
+
+enum_default!(ColorChannel, ColorChannel::A);
+
+/// The `feDisplacementMap` filter primitive.
+#[derive(Default)]
+pub struct FeDisplacementMap {
+ base: Primitive,
+ params: DisplacementMap,
+}
+
+/// Resolved `feDisplacementMap` primitive for rendering.
+#[derive(Clone, Default)]
+pub struct DisplacementMap {
+ in1: Input,
+ in2: Input,
+ scale: f64,
+ x_channel_selector: ColorChannel,
+ y_channel_selector: ColorChannel,
+ color_interpolation_filters: ColorInterpolationFilters,
+}
+
+impl ElementTrait for FeDisplacementMap {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ let (in1, in2) = self.base.parse_two_inputs(attrs, session);
+ self.params.in1 = in1;
+ self.params.in2 = in2;
+
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "scale") => {
+ set_attribute(&mut self.params.scale, attr.parse(value), session)
+ }
+ expanded_name!("", "xChannelSelector") => {
+ set_attribute(
+ &mut self.params.x_channel_selector,
+ attr.parse(value),
+ session,
+ );
+ }
+ expanded_name!("", "yChannelSelector") => {
+ set_attribute(
+ &mut self.params.y_channel_selector,
+ attr.parse(value),
+ session,
+ );
+ }
+ _ => (),
+ }
+ }
+ }
+}
+
+impl DisplacementMap {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ // https://www.w3.org/TR/filter-effects/#feDisplacementMapElement
+ // "The color-interpolation-filters property only applies to
+ // the in2 source image and does not apply to the in source
+ // image. The in source image must remain in its current color
+ // space.
+
+ let input_1 = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in1,
+ ColorInterpolationFilters::Auto,
+ )?;
+ let displacement_input = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in2,
+ self.color_interpolation_filters,
+ )?;
+ let bounds: IRect = bounds_builder
+ .add_input(&input_1)
+ .add_input(&displacement_input)
+ .compute(ctx)
+ .clipped
+ .into();
+
+ // Displacement map's values need to be non-premultiplied.
+ let displacement_surface = displacement_input.surface().unpremultiply(bounds)?;
+
+ let (sx, sy) = ctx.paffine().transform_distance(self.scale, self.scale);
+
+ let mut surface = ExclusiveImageSurface::new(
+ ctx.source_graphic().width(),
+ ctx.source_graphic().height(),
+ input_1.surface().surface_type(),
+ )?;
+
+ surface.draw(&mut |cr| {
+ for (x, y, displacement_pixel) in Pixels::within(&displacement_surface, bounds) {
+ let get_value = |channel| match channel {
+ ColorChannel::R => displacement_pixel.r,
+ ColorChannel::G => displacement_pixel.g,
+ ColorChannel::B => displacement_pixel.b,
+ ColorChannel::A => displacement_pixel.a,
+ };
+
+ let process = |x| f64::from(x) / 255.0 - 0.5;
+
+ let dx = process(get_value(self.x_channel_selector));
+ let dy = process(get_value(self.y_channel_selector));
+
+ let x = f64::from(x);
+ let y = f64::from(y);
+ let ox = sx * dx;
+ let oy = sy * dy;
+
+ // Doing this in a loop doesn't look too bad performance wise, and allows not to
+ // manually implement bilinear or other interpolation.
+ cr.rectangle(x, y, 1.0, 1.0);
+ cr.reset_clip();
+ cr.clip();
+
+ input_1.surface().set_as_source_surface(&cr, -ox, -oy)?;
+ cr.paint()?;
+ }
+
+ Ok(())
+ })?;
+
+ Ok(FilterOutput {
+ surface: surface.share()?,
+ bounds,
+ })
+ }
+}
+
+impl FilterEffect for FeDisplacementMap {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ let cascaded = CascadedValues::new_from_node(node);
+ let values = cascaded.get();
+
+ let mut params = self.params.clone();
+ params.color_interpolation_filters = values.color_interpolation_filters();
+
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::DisplacementMap(params),
+ }])
+ }
+}
+
+impl Parse for ColorChannel {
+ fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ Ok(parse_identifiers!(
+ parser,
+ "R" => ColorChannel::R,
+ "G" => ColorChannel::G,
+ "B" => ColorChannel::B,
+ "A" => ColorChannel::A,
+ )?)
+ }
+}
diff --git a/rsvg/src/filters/drop_shadow.rs b/rsvg/src/filters/drop_shadow.rs
new file mode 100644
index 00000000..a7003f10
--- /dev/null
+++ b/rsvg/src/filters/drop_shadow.rs
@@ -0,0 +1,88 @@
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+
+use crate::document::AcquiredNodes;
+use crate::element::{set_attribute, ElementTrait};
+use crate::filter_func::drop_shadow_primitives;
+use crate::node::{CascadedValues, Node};
+use crate::paint_server::resolve_color;
+use crate::parsers::{NumberOptionalNumber, ParseValue};
+use crate::session::Session;
+use crate::xml::Attributes;
+
+use super::{FilterEffect, FilterResolveError, Input, Primitive, ResolvedPrimitive};
+
+/// The `feDropShadow` element.
+#[derive(Default)]
+pub struct FeDropShadow {
+ base: Primitive,
+ params: DropShadow,
+}
+
+/// Resolved `feDropShadow` parameters for rendering.
+#[derive(Clone)]
+pub struct DropShadow {
+ pub in1: Input,
+ pub dx: f64,
+ pub dy: f64,
+ pub std_deviation: NumberOptionalNumber<f64>,
+}
+
+impl Default for DropShadow {
+ /// Defaults come from <https://www.w3.org/TR/filter-effects/#feDropShadowElement>
+ fn default() -> Self {
+ Self {
+ in1: Default::default(),
+ dx: 2.0,
+ dy: 2.0,
+ std_deviation: NumberOptionalNumber(2.0, 2.0),
+ }
+ }
+}
+
+impl ElementTrait for FeDropShadow {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.params.in1 = self.base.parse_one_input(attrs, session);
+
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "dx") => {
+ set_attribute(&mut self.params.dx, attr.parse(value), session);
+ }
+
+ expanded_name!("", "dy") => {
+ set_attribute(&mut self.params.dy, attr.parse(value), session);
+ }
+
+ expanded_name!("", "stdDeviation") => {
+ set_attribute(&mut self.params.std_deviation, attr.parse(value), session);
+ }
+
+ _ => (),
+ }
+ }
+ }
+}
+
+impl FilterEffect for FeDropShadow {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ let cascaded = CascadedValues::new_from_node(node);
+ let values = cascaded.get();
+
+ let color = resolve_color(
+ &values.flood_color().0,
+ values.flood_opacity().0,
+ values.color().0,
+ );
+
+ Ok(drop_shadow_primitives(
+ self.params.dx,
+ self.params.dy,
+ self.params.std_deviation,
+ color,
+ ))
+ }
+}
diff --git a/rsvg/src/filters/error.rs b/rsvg/src/filters/error.rs
new file mode 100644
index 00000000..1b1f9bc1
--- /dev/null
+++ b/rsvg/src/filters/error.rs
@@ -0,0 +1,78 @@
+use std::fmt;
+
+use crate::error::RenderingError;
+
+/// An enumeration of errors that can occur during filter primitive rendering.
+#[derive(Debug, Clone)]
+pub enum FilterError {
+ /// The filter was passed invalid input (the `in` attribute).
+ InvalidInput,
+ /// The filter was passed an invalid parameter.
+ InvalidParameter(String),
+ /// The filter input surface has an unsuccessful status.
+ BadInputSurfaceStatus(cairo::Error),
+ /// A Cairo error.
+ ///
+ /// This means that either a failed intermediate surface creation or bad intermediate surface
+ /// status.
+ CairoError(cairo::Error),
+ /// Error from the rendering backend.
+ Rendering(RenderingError),
+ /// A lighting filter input surface is too small.
+ LightingInputTooSmall,
+}
+
+/// Errors that can occur while resolving a `FilterSpec`.
+#[derive(Debug)]
+pub enum FilterResolveError {
+ /// An `uri(#foo)` reference does not point to a `<filter>` element.
+ ReferenceToNonFilterElement,
+ /// A lighting filter has none or multiple light sources.
+ InvalidLightSourceCount,
+ /// Child node was in error.
+ ChildNodeInError,
+}
+
+impl fmt::Display for FilterError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match *self {
+ FilterError::InvalidInput => write!(f, "invalid value of the `in` attribute"),
+ FilterError::InvalidParameter(ref s) => write!(f, "invalid parameter value: {s}"),
+ FilterError::BadInputSurfaceStatus(ref status) => {
+ write!(f, "invalid status of the input surface: {status}")
+ }
+ FilterError::CairoError(ref status) => write!(f, "Cairo error: {status}"),
+ FilterError::Rendering(ref e) => write!(f, "Rendering error: {e}"),
+ FilterError::LightingInputTooSmall => write!(
+ f,
+ "lighting filter input surface is too small (less than 2×2 pixels)"
+ ),
+ }
+ }
+}
+
+impl fmt::Display for FilterResolveError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match *self {
+ FilterResolveError::ReferenceToNonFilterElement => {
+ write!(f, "reference to a non-filter element")
+ }
+ FilterResolveError::InvalidLightSourceCount => write!(f, "invalid light source count"),
+ FilterResolveError::ChildNodeInError => write!(f, "child node was in error"),
+ }
+ }
+}
+
+impl From<cairo::Error> for FilterError {
+ #[inline]
+ fn from(x: cairo::Error) -> Self {
+ FilterError::CairoError(x)
+ }
+}
+
+impl From<RenderingError> for FilterError {
+ #[inline]
+ fn from(e: RenderingError) -> Self {
+ FilterError::Rendering(e)
+ }
+}
diff --git a/rsvg/src/filters/flood.rs b/rsvg/src/filters/flood.rs
new file mode 100644
index 00000000..4ebf0257
--- /dev/null
+++ b/rsvg/src/filters/flood.rs
@@ -0,0 +1,70 @@
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::ElementTrait;
+use crate::node::{CascadedValues, Node};
+use crate::paint_server::resolve_color;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::xml::Attributes;
+
+use super::bounds::BoundsBuilder;
+use super::context::{FilterContext, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Primitive, PrimitiveParams, ResolvedPrimitive,
+};
+
+/// The `feFlood` filter primitive.
+#[derive(Default)]
+pub struct FeFlood {
+ base: Primitive,
+}
+
+/// Resolved `feFlood` primitive for rendering.
+pub struct Flood {
+ pub color: cssparser::RGBA,
+}
+
+impl ElementTrait for FeFlood {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.base.parse_no_inputs(attrs, session);
+ }
+}
+
+impl Flood {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ let bounds: IRect = bounds_builder.compute(ctx).clipped.into();
+ rsvg_log!(draw_ctx.session(), "(feFlood bounds={:?}", bounds);
+
+ let surface = ctx.source_graphic().flood(bounds, self.color)?;
+
+ Ok(FilterOutput { surface, bounds })
+ }
+}
+
+impl FilterEffect for FeFlood {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ let cascaded = CascadedValues::new_from_node(node);
+ let values = cascaded.get();
+
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::Flood(Flood {
+ color: resolve_color(
+ &values.flood_color().0,
+ values.flood_opacity().0,
+ values.color().0,
+ ),
+ }),
+ }])
+ }
+}
diff --git a/rsvg/src/filters/gaussian_blur.rs b/rsvg/src/filters/gaussian_blur.rs
new file mode 100644
index 00000000..b56fc9ef
--- /dev/null
+++ b/rsvg/src/filters/gaussian_blur.rs
@@ -0,0 +1,282 @@
+use std::cmp::min;
+use std::f64;
+
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+use nalgebra::{DMatrix, Dyn, VecStorage};
+
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementTrait};
+use crate::node::{CascadedValues, Node};
+use crate::parsers::{NumberOptionalNumber, ParseValue};
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::surface_utils::{
+ shared_surface::{BlurDirection, Horizontal, SharedImageSurface, Vertical},
+ EdgeMode,
+};
+use crate::xml::Attributes;
+
+use super::bounds::BoundsBuilder;
+use super::context::{FilterContext, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams,
+ ResolvedPrimitive,
+};
+
+/// The maximum gaussian blur kernel size.
+///
+/// The value of 500 is used in webkit.
+const MAXIMUM_KERNEL_SIZE: usize = 500;
+
+/// The `feGaussianBlur` filter primitive.
+#[derive(Default)]
+pub struct FeGaussianBlur {
+ base: Primitive,
+ params: GaussianBlur,
+}
+
+/// Resolved `feGaussianBlur` primitive for rendering.
+#[derive(Clone)]
+pub struct GaussianBlur {
+ pub in1: Input,
+ pub std_deviation: NumberOptionalNumber<f64>,
+ pub color_interpolation_filters: ColorInterpolationFilters,
+}
+
+// We need this because NumberOptionalNumber doesn't impl Default
+impl Default for GaussianBlur {
+ fn default() -> GaussianBlur {
+ GaussianBlur {
+ in1: Default::default(),
+ std_deviation: NumberOptionalNumber(0.0, 0.0),
+ color_interpolation_filters: Default::default(),
+ }
+ }
+}
+
+impl ElementTrait for FeGaussianBlur {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.params.in1 = self.base.parse_one_input(attrs, session);
+
+ for (attr, value) in attrs.iter() {
+ if let expanded_name!("", "stdDeviation") = attr.expanded() {
+ set_attribute(&mut self.params.std_deviation, attr.parse(value), session);
+ }
+ }
+ }
+}
+
+/// Computes a gaussian kernel line for the given standard deviation.
+fn gaussian_kernel(std_deviation: f64) -> Vec<f64> {
+ assert!(std_deviation > 0.0);
+
+ // Make sure there aren't any infinities.
+ let maximal_deviation = (MAXIMUM_KERNEL_SIZE / 2) as f64 / 3.0;
+
+ // Values further away than std_deviation * 3 are too small to contribute anything meaningful.
+ let radius = ((std_deviation.min(maximal_deviation) * 3.0) + 0.5) as usize;
+ // Clamp the radius rather than diameter because `MAXIMUM_KERNEL_SIZE` might be even and we
+ // want an odd-sized kernel.
+ let radius = min(radius, (MAXIMUM_KERNEL_SIZE - 1) / 2);
+ let diameter = radius * 2 + 1;
+
+ let mut kernel = Vec::with_capacity(diameter);
+
+ let gauss_point = |x: f64| (-x.powi(2) / (2.0 * std_deviation.powi(2))).exp();
+
+ // Fill the matrix by doing numerical integration approximation from -2*std_dev to 2*std_dev,
+ // sampling 50 points per pixel. We do the bottom half, mirror it to the top half, then compute
+ // the center point. Otherwise asymmetric quantization errors will occur. The formula to
+ // integrate is e^-(x^2/2s^2).
+ for i in 0..diameter / 2 {
+ let base_x = (diameter / 2 + 1 - i) as f64 - 0.5;
+
+ let mut sum = 0.0;
+ for j in 1..=50 {
+ let r = base_x + 0.02 * f64::from(j);
+ sum += gauss_point(r);
+ }
+
+ kernel.push(sum / 50.0);
+ }
+
+ // We'll compute the middle point later.
+ kernel.push(0.0);
+
+ // Mirror the bottom half to the top half.
+ for i in 0..diameter / 2 {
+ let x = kernel[diameter / 2 - 1 - i];
+ kernel.push(x);
+ }
+
+ // Find center val -- calculate an odd number of quanta to make it symmetric, even if the
+ // center point is weighted slightly higher than others.
+ let mut sum = 0.0;
+ for j in 0..=50 {
+ let r = -0.5 + 0.02 * f64::from(j);
+ sum += gauss_point(r);
+ }
+ kernel[diameter / 2] = sum / 51.0;
+
+ // Normalize the distribution by scaling the total sum to 1.
+ let sum = kernel.iter().sum::<f64>();
+ kernel.iter_mut().for_each(|x| *x /= sum);
+
+ kernel
+}
+
+/// Returns a size of the box blur kernel to approximate the gaussian blur.
+fn box_blur_kernel_size(std_deviation: f64) -> usize {
+ let d = (std_deviation * 3.0 * (2.0 * f64::consts::PI).sqrt() / 4.0 + 0.5).floor();
+ let d = d.min(MAXIMUM_KERNEL_SIZE as f64);
+ d as usize
+}
+
+/// Applies three box blurs to approximate the gaussian blur.
+///
+/// This is intended to be used in two steps, horizontal and vertical.
+fn three_box_blurs<B: BlurDirection>(
+ surface: &SharedImageSurface,
+ bounds: IRect,
+ std_deviation: f64,
+) -> Result<SharedImageSurface, FilterError> {
+ let d = box_blur_kernel_size(std_deviation);
+ if d == 0 {
+ return Ok(surface.clone());
+ }
+
+ let surface = if d % 2 == 1 {
+ // Odd kernel sizes just get three successive box blurs.
+ let mut surface = surface.clone();
+
+ for _ in 0..3 {
+ surface = surface.box_blur::<B>(bounds, d, d / 2)?;
+ }
+
+ surface
+ } else {
+ // Even kernel sizes have a more interesting scheme.
+ let surface = surface.box_blur::<B>(bounds, d, d / 2)?;
+ let surface = surface.box_blur::<B>(bounds, d, d / 2 - 1)?;
+
+ let d = d + 1;
+ surface.box_blur::<B>(bounds, d, d / 2)?
+ };
+
+ Ok(surface)
+}
+
+/// Applies the gaussian blur.
+///
+/// This is intended to be used in two steps, horizontal and vertical.
+fn gaussian_blur(
+ input_surface: &SharedImageSurface,
+ bounds: IRect,
+ std_deviation: f64,
+ vertical: bool,
+) -> Result<SharedImageSurface, FilterError> {
+ let kernel = gaussian_kernel(std_deviation);
+ let (rows, cols) = if vertical {
+ (kernel.len(), 1)
+ } else {
+ (1, kernel.len())
+ };
+ let kernel = DMatrix::from_data(VecStorage::new(Dyn(rows), Dyn(cols), kernel));
+
+ Ok(input_surface.convolve(
+ bounds,
+ ((cols / 2) as i32, (rows / 2) as i32),
+ &kernel,
+ EdgeMode::None,
+ )?)
+}
+
+impl GaussianBlur {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ let input_1 = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in1,
+ self.color_interpolation_filters,
+ )?;
+ let bounds: IRect = bounds_builder
+ .add_input(&input_1)
+ .compute(ctx)
+ .clipped
+ .into();
+
+ let NumberOptionalNumber(std_x, std_y) = self.std_deviation;
+
+ // "A negative value or a value of zero disables the effect of
+ // the given filter primitive (i.e., the result is the filter
+ // input image)."
+ if std_x <= 0.0 && std_y <= 0.0 {
+ return Ok(FilterOutput {
+ surface: input_1.surface().clone(),
+ bounds,
+ });
+ }
+
+ let (std_x, std_y) = ctx.paffine().transform_distance(std_x, std_y);
+
+ // The deviation can become negative here due to the transform.
+ let std_x = std_x.abs();
+ let std_y = std_y.abs();
+
+ // Performance TODO: gaussian blur is frequently used for shadows, operating on SourceAlpha
+ // (so the image is alpha-only). We can use this to not waste time processing the other
+ // channels.
+
+ // Horizontal convolution.
+ let horiz_result_surface = if std_x >= 2.0 {
+ // The spec says for deviation >= 2.0 three box blurs can be used as an optimization.
+ three_box_blurs::<Horizontal>(input_1.surface(), bounds, std_x)?
+ } else if std_x != 0.0 {
+ gaussian_blur(input_1.surface(), bounds, std_x, false)?
+ } else {
+ input_1.surface().clone()
+ };
+
+ // Vertical convolution.
+ let output_surface = if std_y >= 2.0 {
+ // The spec says for deviation >= 2.0 three box blurs can be used as an optimization.
+ three_box_blurs::<Vertical>(&horiz_result_surface, bounds, std_y)?
+ } else if std_y != 0.0 {
+ gaussian_blur(&horiz_result_surface, bounds, std_y, true)?
+ } else {
+ horiz_result_surface
+ };
+
+ Ok(FilterOutput {
+ surface: output_surface,
+ bounds,
+ })
+ }
+}
+
+impl FilterEffect for FeGaussianBlur {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ let cascaded = CascadedValues::new_from_node(node);
+ let values = cascaded.get();
+
+ let mut params = self.params.clone();
+ params.color_interpolation_filters = values.color_interpolation_filters();
+
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::GaussianBlur(params),
+ }])
+ }
+}
diff --git a/rsvg/src/filters/image.rs b/rsvg/src/filters/image.rs
new file mode 100644
index 00000000..eaeb08f9
--- /dev/null
+++ b/rsvg/src/filters/image.rs
@@ -0,0 +1,211 @@
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+
+use crate::aspect_ratio::AspectRatio;
+use crate::document::{AcquiredNodes, NodeId};
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementTrait};
+use crate::href::{is_href, set_href};
+use crate::node::{CascadedValues, Node};
+use crate::parsers::ParseValue;
+use crate::properties::ComputedValues;
+use crate::rect::Rect;
+use crate::session::Session;
+use crate::surface_utils::shared_surface::SharedImageSurface;
+use crate::viewbox::ViewBox;
+use crate::xml::Attributes;
+
+use super::bounds::{Bounds, BoundsBuilder};
+use super::context::{FilterContext, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Primitive, PrimitiveParams, ResolvedPrimitive,
+};
+
+/// The `feImage` filter primitive.
+#[derive(Default)]
+pub struct FeImage {
+ base: Primitive,
+ params: ImageParams,
+}
+
+#[derive(Clone, Default)]
+struct ImageParams {
+ aspect: AspectRatio,
+ href: Option<String>,
+}
+
+/// Resolved `feImage` primitive for rendering.
+pub struct Image {
+ aspect: AspectRatio,
+ source: Source,
+ feimage_values: Box<ComputedValues>,
+}
+
+/// What a feImage references for rendering.
+enum Source {
+ /// Nothing is referenced; ignore the filter.
+ None,
+
+ /// Reference to a node.
+ Node(Node),
+
+ /// Reference to an external image. This is just a URL.
+ ExternalImage(String),
+}
+
+impl Image {
+ /// Renders the filter if the source is an existing node.
+ fn render_node(
+ &self,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ bounds: Rect,
+ referenced_node: &Node,
+ ) -> Result<SharedImageSurface, FilterError> {
+ // https://www.w3.org/TR/filter-effects/#feImageElement
+ //
+ // The filters spec says, "... otherwise [rendering a referenced object], the
+ // referenced resource is rendered according to the behavior of the use element."
+ // I think this means that we use the same cascading mode as <use>, i.e. the
+ // referenced object inherits its properties from the feImage element.
+ let cascaded =
+ CascadedValues::new_from_values(referenced_node, &self.feimage_values, None, None);
+
+ let image = draw_ctx.draw_node_to_surface(
+ referenced_node,
+ acquired_nodes,
+ &cascaded,
+ ctx.paffine(),
+ ctx.source_graphic().width(),
+ ctx.source_graphic().height(),
+ )?;
+
+ let surface = ctx.source_graphic().paint_image(bounds, &image, None)?;
+
+ Ok(surface)
+ }
+
+ /// Renders the filter if the source is an external image.
+ fn render_external_image(
+ &self,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ _draw_ctx: &DrawingCtx,
+ bounds: &Bounds,
+ url: &str,
+ ) -> Result<SharedImageSurface, FilterError> {
+ // FIXME: translate the error better here
+ let image = acquired_nodes
+ .lookup_image(url)
+ .map_err(|_| FilterError::InvalidInput)?;
+
+ let rect = self.aspect.compute(
+ &ViewBox::from(Rect::from_size(
+ f64::from(image.width()),
+ f64::from(image.height()),
+ )),
+ &bounds.unclipped,
+ );
+
+ let surface = ctx
+ .source_graphic()
+ .paint_image(bounds.clipped, &image, Some(rect))?;
+
+ Ok(surface)
+ }
+}
+
+impl ElementTrait for FeImage {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.base.parse_no_inputs(attrs, session);
+
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "preserveAspectRatio") => {
+ set_attribute(&mut self.params.aspect, attr.parse(value), session);
+ }
+
+ // "path" is used by some older Adobe Illustrator versions
+ ref a if is_href(a) || *a == expanded_name!("", "path") => {
+ set_href(a, &mut self.params.href, Some(value.to_string()));
+ }
+
+ _ => (),
+ }
+ }
+ }
+}
+
+impl Image {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ let bounds = bounds_builder.compute(ctx);
+
+ let surface = match &self.source {
+ Source::None => return Err(FilterError::InvalidInput),
+
+ Source::Node(node) => {
+ if let Ok(acquired) = acquired_nodes.acquire_ref(node) {
+ self.render_node(
+ ctx,
+ acquired_nodes,
+ draw_ctx,
+ bounds.clipped,
+ acquired.get(),
+ )?
+ } else {
+ return Err(FilterError::InvalidInput);
+ }
+ }
+
+ Source::ExternalImage(ref href) => {
+ self.render_external_image(ctx, acquired_nodes, draw_ctx, &bounds, href)?
+ }
+ };
+
+ Ok(FilterOutput {
+ surface,
+ bounds: bounds.clipped.into(),
+ })
+ }
+}
+
+impl FilterEffect for FeImage {
+ fn resolve(
+ &self,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ let cascaded = CascadedValues::new_from_node(node);
+ let feimage_values = cascaded.get().clone();
+
+ let source = match self.params.href {
+ None => Source::None,
+
+ Some(ref s) => {
+ if let Ok(node_id) = NodeId::parse(s) {
+ acquired_nodes
+ .acquire(&node_id)
+ .map(|acquired| Source::Node(acquired.get().clone()))
+ .unwrap_or(Source::None)
+ } else {
+ Source::ExternalImage(s.to_string())
+ }
+ }
+ };
+
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::Image(Image {
+ aspect: self.params.aspect,
+ source,
+ feimage_values: Box::new(feimage_values),
+ }),
+ }])
+ }
+}
diff --git a/rsvg/src/filters/lighting.rs b/rsvg/src/filters/lighting.rs
new file mode 100644
index 00000000..ed39e78b
--- /dev/null
+++ b/rsvg/src/filters/lighting.rs
@@ -0,0 +1,1090 @@
+//! Lighting filters and light nodes.
+
+use float_cmp::approx_eq;
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+use nalgebra::{Vector2, Vector3};
+use num_traits::identities::Zero;
+use rayon::prelude::*;
+use std::cmp::max;
+
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementData, ElementTrait};
+use crate::filters::{
+ bounds::BoundsBuilder,
+ context::{FilterContext, FilterOutput},
+ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams,
+ ResolvedPrimitive,
+};
+use crate::node::{CascadedValues, Node, NodeBorrow};
+use crate::paint_server::resolve_color;
+use crate::parsers::{NonNegative, NumberOptionalNumber, ParseValue};
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::surface_utils::{
+ shared_surface::{ExclusiveImageSurface, SharedImageSurface, SurfaceType},
+ ImageSurfaceDataExt, Pixel,
+};
+use crate::transform::Transform;
+use crate::unit_interval::UnitInterval;
+use crate::util::clamp;
+use crate::xml::Attributes;
+
+/// The `feDiffuseLighting` filter primitives.
+#[derive(Default)]
+pub struct FeDiffuseLighting {
+ base: Primitive,
+ params: DiffuseLightingParams,
+}
+
+#[derive(Clone)]
+pub struct DiffuseLightingParams {
+ in1: Input,
+ surface_scale: f64,
+ kernel_unit_length: Option<(f64, f64)>,
+ diffuse_constant: NonNegative,
+}
+
+impl Default for DiffuseLightingParams {
+ fn default() -> Self {
+ Self {
+ in1: Default::default(),
+ surface_scale: 1.0,
+ kernel_unit_length: None,
+ diffuse_constant: NonNegative(1.0),
+ }
+ }
+}
+
+/// The `feSpecularLighting` filter primitives.
+#[derive(Default)]
+pub struct FeSpecularLighting {
+ base: Primitive,
+ params: SpecularLightingParams,
+}
+
+#[derive(Clone)]
+pub struct SpecularLightingParams {
+ in1: Input,
+ surface_scale: f64,
+ kernel_unit_length: Option<(f64, f64)>,
+ specular_constant: NonNegative,
+ specular_exponent: f64,
+}
+
+impl Default for SpecularLightingParams {
+ fn default() -> Self {
+ Self {
+ in1: Default::default(),
+ surface_scale: 1.0,
+ kernel_unit_length: None,
+ specular_constant: NonNegative(1.0),
+ specular_exponent: 1.0,
+ }
+ }
+}
+
+/// Resolved `feDiffuseLighting` primitive for rendering.
+pub struct DiffuseLighting {
+ params: DiffuseLightingParams,
+ light: Light,
+}
+
+/// Resolved `feSpecularLighting` primitive for rendering.
+pub struct SpecularLighting {
+ params: SpecularLightingParams,
+ light: Light,
+}
+
+/// A light source before applying affine transformations, straight out of the SVG.
+#[derive(Debug, PartialEq)]
+enum UntransformedLightSource {
+ Distant(FeDistantLight),
+ Point(FePointLight),
+ Spot(FeSpotLight),
+}
+
+/// A light source with affine transformations applied.
+enum LightSource {
+ Distant {
+ azimuth: f64,
+ elevation: f64,
+ },
+ Point {
+ origin: Vector3<f64>,
+ },
+ Spot {
+ origin: Vector3<f64>,
+ direction: Vector3<f64>,
+ specular_exponent: f64,
+ limiting_cone_angle: Option<f64>,
+ },
+}
+
+impl UntransformedLightSource {
+ fn transform(&self, paffine: Transform) -> LightSource {
+ match *self {
+ UntransformedLightSource::Distant(ref l) => l.transform(),
+ UntransformedLightSource::Point(ref l) => l.transform(paffine),
+ UntransformedLightSource::Spot(ref l) => l.transform(paffine),
+ }
+ }
+}
+
+struct Light {
+ source: UntransformedLightSource,
+ lighting_color: cssparser::RGBA,
+ color_interpolation_filters: ColorInterpolationFilters,
+}
+
+impl Light {
+ /// Returns the color and unit (or null) vector from the image sample to the light.
+ #[inline]
+ pub fn color_and_vector(
+ &self,
+ source: &LightSource,
+ x: f64,
+ y: f64,
+ z: f64,
+ ) -> (cssparser::RGBA, Vector3<f64>) {
+ let vector = match *source {
+ LightSource::Distant { azimuth, elevation } => {
+ let azimuth = azimuth.to_radians();
+ let elevation = elevation.to_radians();
+ Vector3::new(
+ azimuth.cos() * elevation.cos(),
+ azimuth.sin() * elevation.cos(),
+ elevation.sin(),
+ )
+ }
+ LightSource::Point { origin } | LightSource::Spot { origin, .. } => {
+ let mut v = origin - Vector3::new(x, y, z);
+ let _ = v.try_normalize_mut(0.0);
+ v
+ }
+ };
+
+ let color = match *source {
+ LightSource::Spot {
+ direction,
+ specular_exponent,
+ limiting_cone_angle,
+ ..
+ } => {
+ let minus_l_dot_s = -vector.dot(&direction);
+ match limiting_cone_angle {
+ _ if minus_l_dot_s <= 0.0 => cssparser::RGBA::transparent(),
+ Some(a) if minus_l_dot_s < a.to_radians().cos() => {
+ cssparser::RGBA::transparent()
+ }
+ _ => {
+ let factor = minus_l_dot_s.powf(specular_exponent);
+ let compute = |x| (clamp(f64::from(x) * factor, 0.0, 255.0) + 0.5) as u8;
+
+ cssparser::RGBA {
+ red: compute(self.lighting_color.red),
+ green: compute(self.lighting_color.green),
+ blue: compute(self.lighting_color.blue),
+ alpha: 255,
+ }
+ }
+ }
+ }
+ _ => self.lighting_color,
+ };
+
+ (color, vector)
+ }
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct FeDistantLight {
+ azimuth: f64,
+ elevation: f64,
+}
+
+impl FeDistantLight {
+ fn transform(&self) -> LightSource {
+ LightSource::Distant {
+ azimuth: self.azimuth,
+ elevation: self.elevation,
+ }
+ }
+}
+
+impl ElementTrait for FeDistantLight {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "azimuth") => {
+ set_attribute(&mut self.azimuth, attr.parse(value), session)
+ }
+ expanded_name!("", "elevation") => {
+ set_attribute(&mut self.elevation, attr.parse(value), session)
+ }
+ _ => (),
+ }
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct FePointLight {
+ x: f64,
+ y: f64,
+ z: f64,
+}
+
+impl FePointLight {
+ fn transform(&self, paffine: Transform) -> LightSource {
+ let (x, y) = paffine.transform_point(self.x, self.y);
+ let z = transform_dist(paffine, self.z);
+
+ LightSource::Point {
+ origin: Vector3::new(x, y, z),
+ }
+ }
+}
+
+impl ElementTrait for FePointLight {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session),
+ expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session),
+ expanded_name!("", "z") => set_attribute(&mut self.z, attr.parse(value), session),
+ _ => (),
+ }
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct FeSpotLight {
+ x: f64,
+ y: f64,
+ z: f64,
+ points_at_x: f64,
+ points_at_y: f64,
+ points_at_z: f64,
+ specular_exponent: f64,
+ limiting_cone_angle: Option<f64>,
+}
+
+// We need this because, per the spec, the initial values for all fields are 0.0
+// except for specular_exponent, which is 1.
+impl Default for FeSpotLight {
+ fn default() -> FeSpotLight {
+ FeSpotLight {
+ x: 0.0,
+ y: 0.0,
+ z: 0.0,
+ points_at_x: 0.0,
+ points_at_y: 0.0,
+ points_at_z: 0.0,
+ specular_exponent: 1.0,
+ limiting_cone_angle: None,
+ }
+ }
+}
+
+impl FeSpotLight {
+ fn transform(&self, paffine: Transform) -> LightSource {
+ let (x, y) = paffine.transform_point(self.x, self.y);
+ let z = transform_dist(paffine, self.z);
+ let (points_at_x, points_at_y) =
+ paffine.transform_point(self.points_at_x, self.points_at_y);
+ let points_at_z = transform_dist(paffine, self.points_at_z);
+
+ let origin = Vector3::new(x, y, z);
+ let mut direction = Vector3::new(points_at_x, points_at_y, points_at_z) - origin;
+ let _ = direction.try_normalize_mut(0.0);
+
+ LightSource::Spot {
+ origin,
+ direction,
+ specular_exponent: self.specular_exponent,
+ limiting_cone_angle: self.limiting_cone_angle,
+ }
+ }
+}
+
+impl ElementTrait for FeSpotLight {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session),
+ expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session),
+ expanded_name!("", "z") => set_attribute(&mut self.z, attr.parse(value), session),
+ expanded_name!("", "pointsAtX") => {
+ set_attribute(&mut self.points_at_x, attr.parse(value), session)
+ }
+ expanded_name!("", "pointsAtY") => {
+ set_attribute(&mut self.points_at_y, attr.parse(value), session)
+ }
+ expanded_name!("", "pointsAtZ") => {
+ set_attribute(&mut self.points_at_z, attr.parse(value), session)
+ }
+
+ expanded_name!("", "specularExponent") => {
+ set_attribute(&mut self.specular_exponent, attr.parse(value), session);
+ }
+
+ expanded_name!("", "limitingConeAngle") => {
+ set_attribute(&mut self.limiting_cone_angle, attr.parse(value), session);
+ }
+
+ _ => (),
+ }
+ }
+ }
+}
+
+/// Applies the `primitiveUnits` coordinate transformation to a non-x or y distance.
+#[inline]
+fn transform_dist(t: Transform, d: f64) -> f64 {
+ d * (t.xx.powi(2) + t.yy.powi(2)).sqrt() / std::f64::consts::SQRT_2
+}
+
+impl ElementTrait for FeDiffuseLighting {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.params.in1 = self.base.parse_one_input(attrs, session);
+
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "surfaceScale") => {
+ set_attribute(&mut self.params.surface_scale, attr.parse(value), session);
+ }
+ expanded_name!("", "kernelUnitLength") => {
+ let v: Result<NumberOptionalNumber<f64>, _> = attr.parse(value);
+ match v {
+ Ok(NumberOptionalNumber(x, y)) => {
+ self.params.kernel_unit_length = Some((x, y));
+ }
+
+ Err(e) => {
+ rsvg_log!(session, "ignoring attribute with invalid value: {}", e);
+ }
+ }
+ }
+ expanded_name!("", "diffuseConstant") => {
+ set_attribute(
+ &mut self.params.diffuse_constant,
+ attr.parse(value),
+ session,
+ );
+ }
+ _ => (),
+ }
+ }
+ }
+}
+
+impl DiffuseLighting {
+ #[inline]
+ fn compute_factor(&self, normal: Normal, light_vector: Vector3<f64>) -> f64 {
+ let k = if normal.normal.is_zero() {
+ // Common case of (0, 0, 1) normal.
+ light_vector.z
+ } else {
+ let mut n = normal
+ .normal
+ .map(|x| f64::from(x) * self.params.surface_scale / 255.);
+ n.component_mul_assign(&normal.factor);
+ let normal = Vector3::new(n.x, n.y, 1.0);
+
+ normal.dot(&light_vector) / normal.norm()
+ };
+
+ self.params.diffuse_constant.0 * k
+ }
+}
+
+impl ElementTrait for FeSpecularLighting {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.params.in1 = self.base.parse_one_input(attrs, session);
+
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "surfaceScale") => {
+ set_attribute(&mut self.params.surface_scale, attr.parse(value), session);
+ }
+ expanded_name!("", "kernelUnitLength") => {
+ let v: Result<NumberOptionalNumber<f64>, _> = attr.parse(value);
+ match v {
+ Ok(NumberOptionalNumber(x, y)) => {
+ self.params.kernel_unit_length = Some((x, y));
+ }
+
+ Err(e) => {
+ rsvg_log!(session, "ignoring attribute with invalid value: {}", e);
+ }
+ }
+ }
+ expanded_name!("", "specularConstant") => {
+ set_attribute(
+ &mut self.params.specular_constant,
+ attr.parse(value),
+ session,
+ );
+ }
+ expanded_name!("", "specularExponent") => {
+ set_attribute(
+ &mut self.params.specular_exponent,
+ attr.parse(value),
+ session,
+ );
+ }
+ _ => (),
+ }
+ }
+ }
+}
+
+impl SpecularLighting {
+ #[inline]
+ fn compute_factor(&self, normal: Normal, light_vector: Vector3<f64>) -> f64 {
+ let h = light_vector + Vector3::new(0.0, 0.0, 1.0);
+ let h_norm = h.norm();
+
+ if h_norm == 0.0 {
+ return 0.0;
+ }
+
+ let n_dot_h = if normal.normal.is_zero() {
+ // Common case of (0, 0, 1) normal.
+ h.z / h_norm
+ } else {
+ let mut n = normal
+ .normal
+ .map(|x| f64::from(x) * self.params.surface_scale / 255.);
+ n.component_mul_assign(&normal.factor);
+ let normal = Vector3::new(n.x, n.y, 1.0);
+ normal.dot(&h) / normal.norm() / h_norm
+ };
+
+ if approx_eq!(f64, self.params.specular_exponent, 1.0) {
+ self.params.specular_constant.0 * n_dot_h
+ } else {
+ self.params.specular_constant.0 * n_dot_h.powf(self.params.specular_exponent)
+ }
+ }
+}
+
+macro_rules! impl_lighting_filter {
+ ($lighting_type:ty, $params_name:ident, $alpha_func:ident) => {
+ impl $params_name {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ let input_1 = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.params.in1,
+ self.light.color_interpolation_filters,
+ )?;
+ let mut bounds: IRect = bounds_builder
+ .add_input(&input_1)
+ .compute(ctx)
+ .clipped
+ .into();
+ let original_bounds = bounds;
+
+ let scale = self
+ .params
+ .kernel_unit_length
+ .and_then(|(x, y)| {
+ if x <= 0.0 || y <= 0.0 {
+ None
+ } else {
+ Some((x, y))
+ }
+ })
+ .map(|(dx, dy)| ctx.paffine().transform_distance(dx, dy));
+
+ let mut input_surface = input_1.surface().clone();
+
+ if let Some((ox, oy)) = scale {
+ // Scale the input surface to match kernel_unit_length.
+ let (new_surface, new_bounds) =
+ input_surface.scale(bounds, 1.0 / ox, 1.0 / oy)?;
+
+ input_surface = new_surface;
+ bounds = new_bounds;
+ }
+
+ let (bounds_w, bounds_h) = bounds.size();
+
+ // Check if the surface is too small for normal computation. This case is
+ // unspecified; WebKit doesn't render anything in this case.
+ if bounds_w < 2 || bounds_h < 2 {
+ return Err(FilterError::LightingInputTooSmall);
+ }
+
+ let (ox, oy) = scale.unwrap_or((1.0, 1.0));
+
+ let source = self.light.source.transform(ctx.paffine());
+
+ let mut surface = ExclusiveImageSurface::new(
+ input_surface.width(),
+ input_surface.height(),
+ SurfaceType::from(self.light.color_interpolation_filters),
+ )?;
+
+ {
+ let output_stride = surface.stride() as usize;
+ let mut output_data = surface.data();
+ let output_slice = &mut *output_data;
+
+ let compute_output_pixel =
+ |output_slice: &mut [u8], base_y, x, y, normal: Normal| {
+ let pixel = input_surface.get_pixel(x, y);
+
+ let scaled_x = f64::from(x) * ox;
+ let scaled_y = f64::from(y) * oy;
+ let z = f64::from(pixel.a) / 255.0 * self.params.surface_scale;
+
+ let (color, vector) =
+ self.light.color_and_vector(&source, scaled_x, scaled_y, z);
+
+ // compute the factor just once for the three colors
+ let factor = self.compute_factor(normal, vector);
+ let compute =
+ |x| (clamp(factor * f64::from(x), 0.0, 255.0) + 0.5) as u8;
+
+ let r = compute(color.red);
+ let g = compute(color.green);
+ let b = compute(color.blue);
+ let a = $alpha_func(r, g, b);
+
+ let output_pixel = Pixel { r, g, b, a };
+
+ output_slice.set_pixel(output_stride, output_pixel, x, y - base_y);
+ };
+
+ // Top left.
+ compute_output_pixel(
+ output_slice,
+ 0,
+ bounds.x0 as u32,
+ bounds.y0 as u32,
+ Normal::top_left(&input_surface, bounds),
+ );
+
+ // Top right.
+ compute_output_pixel(
+ output_slice,
+ 0,
+ bounds.x1 as u32 - 1,
+ bounds.y0 as u32,
+ Normal::top_right(&input_surface, bounds),
+ );
+
+ // Bottom left.
+ compute_output_pixel(
+ output_slice,
+ 0,
+ bounds.x0 as u32,
+ bounds.y1 as u32 - 1,
+ Normal::bottom_left(&input_surface, bounds),
+ );
+
+ // Bottom right.
+ compute_output_pixel(
+ output_slice,
+ 0,
+ bounds.x1 as u32 - 1,
+ bounds.y1 as u32 - 1,
+ Normal::bottom_right(&input_surface, bounds),
+ );
+
+ if bounds_w >= 3 {
+ // Top row.
+ for x in bounds.x0 as u32 + 1..bounds.x1 as u32 - 1 {
+ compute_output_pixel(
+ output_slice,
+ 0,
+ x,
+ bounds.y0 as u32,
+ Normal::top_row(&input_surface, bounds, x),
+ );
+ }
+
+ // Bottom row.
+ for x in bounds.x0 as u32 + 1..bounds.x1 as u32 - 1 {
+ compute_output_pixel(
+ output_slice,
+ 0,
+ x,
+ bounds.y1 as u32 - 1,
+ Normal::bottom_row(&input_surface, bounds, x),
+ );
+ }
+ }
+
+ if bounds_h >= 3 {
+ // Left column.
+ for y in bounds.y0 as u32 + 1..bounds.y1 as u32 - 1 {
+ compute_output_pixel(
+ output_slice,
+ 0,
+ bounds.x0 as u32,
+ y,
+ Normal::left_column(&input_surface, bounds, y),
+ );
+ }
+
+ // Right column.
+ for y in bounds.y0 as u32 + 1..bounds.y1 as u32 - 1 {
+ compute_output_pixel(
+ output_slice,
+ 0,
+ bounds.x1 as u32 - 1,
+ y,
+ Normal::right_column(&input_surface, bounds, y),
+ );
+ }
+ }
+
+ if bounds_w >= 3 && bounds_h >= 3 {
+ // Interior pixels.
+ let first_row = bounds.y0 as u32 + 1;
+ let one_past_last_row = bounds.y1 as u32 - 1;
+ let first_pixel = (first_row as usize) * output_stride;
+ let one_past_last_pixel = (one_past_last_row as usize) * output_stride;
+
+ output_slice[first_pixel..one_past_last_pixel]
+ .par_chunks_mut(output_stride)
+ .zip(first_row..one_past_last_row)
+ .for_each(|(slice, y)| {
+ for x in bounds.x0 as u32 + 1..bounds.x1 as u32 - 1 {
+ compute_output_pixel(
+ slice,
+ y,
+ x,
+ y,
+ Normal::interior(&input_surface, bounds, x, y),
+ );
+ }
+ });
+ }
+ }
+
+ let mut surface = surface.share()?;
+
+ if let Some((ox, oy)) = scale {
+ // Scale the output surface back.
+ surface = surface.scale_to(
+ ctx.source_graphic().width(),
+ ctx.source_graphic().height(),
+ original_bounds,
+ ox,
+ oy,
+ )?;
+
+ bounds = original_bounds;
+ }
+
+ Ok(FilterOutput { surface, bounds })
+ }
+ }
+
+ impl FilterEffect for $lighting_type {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ let mut sources = node.children().rev().filter(|c| {
+ c.is_element()
+ && matches!(
+ *c.borrow_element_data(),
+ ElementData::FeDistantLight(_)
+ | ElementData::FePointLight(_)
+ | ElementData::FeSpotLight(_)
+ )
+ });
+
+ let source_node = sources.next();
+ if source_node.is_none() || sources.next().is_some() {
+ return Err(FilterResolveError::InvalidLightSourceCount);
+ }
+
+ let source_node = source_node.unwrap();
+
+ let source = match &*source_node.borrow_element_data() {
+ ElementData::FeDistantLight(l) => {
+ UntransformedLightSource::Distant((**l).clone())
+ }
+ ElementData::FePointLight(l) => UntransformedLightSource::Point((**l).clone()),
+ ElementData::FeSpotLight(l) => UntransformedLightSource::Spot((**l).clone()),
+ _ => unreachable!(),
+ };
+
+ let cascaded = CascadedValues::new_from_node(node);
+ let values = cascaded.get();
+
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::$params_name($params_name {
+ params: self.params.clone(),
+ light: Light {
+ source,
+ lighting_color: resolve_color(
+ &values.lighting_color().0,
+ UnitInterval::clamp(1.0),
+ values.color().0,
+ ),
+ color_interpolation_filters: values.color_interpolation_filters(),
+ },
+ }),
+ }])
+ }
+ }
+ };
+}
+
+const fn diffuse_alpha(_r: u8, _g: u8, _b: u8) -> u8 {
+ 255
+}
+
+fn specular_alpha(r: u8, g: u8, b: u8) -> u8 {
+ max(max(r, g), b)
+}
+
+impl_lighting_filter!(FeDiffuseLighting, DiffuseLighting, diffuse_alpha);
+
+impl_lighting_filter!(FeSpecularLighting, SpecularLighting, specular_alpha);
+
+/// 2D normal and factor stored separately.
+///
+/// The normal needs to be multiplied by `surface_scale * factor / 255` and
+/// normalized with 1 as the z component.
+/// pub for the purpose of accessing this from benchmarks.
+#[derive(Debug, Clone, Copy)]
+pub struct Normal {
+ pub factor: Vector2<f64>,
+ pub normal: Vector2<i16>,
+}
+
+impl Normal {
+ #[inline]
+ fn new(factor_x: f64, nx: i16, factor_y: f64, ny: i16) -> Normal {
+ // Negative nx and ny to account for the different coordinate system.
+ Normal {
+ factor: Vector2::new(factor_x, factor_y),
+ normal: Vector2::new(-nx, -ny),
+ }
+ }
+
+ /// Computes and returns the normal vector for the top left pixel for light filters.
+ #[inline]
+ pub fn top_left(surface: &SharedImageSurface, bounds: IRect) -> Normal {
+ // Surface needs to be at least 2×2.
+ assert!(bounds.width() >= 2);
+ assert!(bounds.height() >= 2);
+
+ let get = |x, y| i16::from(surface.get_pixel(x, y).a);
+ let (x, y) = (bounds.x0 as u32, bounds.y0 as u32);
+
+ let center = get(x, y);
+ let right = get(x + 1, y);
+ let bottom = get(x, y + 1);
+ let bottom_right = get(x + 1, y + 1);
+
+ Self::new(
+ 2. / 3.,
+ -2 * center + 2 * right - bottom + bottom_right,
+ 2. / 3.,
+ -2 * center - right + 2 * bottom + bottom_right,
+ )
+ }
+
+ /// Computes and returns the normal vector for the top row pixels for light filters.
+ #[inline]
+ pub fn top_row(surface: &SharedImageSurface, bounds: IRect, x: u32) -> Normal {
+ assert!(x as i32 > bounds.x0);
+ assert!((x as i32) + 1 < bounds.x1);
+ assert!(bounds.height() >= 2);
+
+ let get = |x, y| i16::from(surface.get_pixel(x, y).a);
+ let y = bounds.y0 as u32;
+
+ let left = get(x - 1, y);
+ let center = get(x, y);
+ let right = get(x + 1, y);
+ let bottom_left = get(x - 1, y + 1);
+ let bottom = get(x, y + 1);
+ let bottom_right = get(x + 1, y + 1);
+
+ Self::new(
+ 1. / 3.,
+ -2 * left + 2 * right - bottom_left + bottom_right,
+ 1. / 2.,
+ -left - 2 * center - right + bottom_left + 2 * bottom + bottom_right,
+ )
+ }
+
+ /// Computes and returns the normal vector for the top right pixel for light filters.
+ #[inline]
+ pub fn top_right(surface: &SharedImageSurface, bounds: IRect) -> Normal {
+ // Surface needs to be at least 2×2.
+ assert!(bounds.width() >= 2);
+ assert!(bounds.height() >= 2);
+
+ let get = |x, y| i16::from(surface.get_pixel(x, y).a);
+ let (x, y) = (bounds.x1 as u32 - 1, bounds.y0 as u32);
+
+ let left = get(x - 1, y);
+ let center = get(x, y);
+ let bottom_left = get(x - 1, y + 1);
+ let bottom = get(x, y + 1);
+
+ Self::new(
+ 2. / 3.,
+ -2 * left + 2 * center - bottom_left + bottom,
+ 2. / 3.,
+ -left - 2 * center + bottom_left + 2 * bottom,
+ )
+ }
+
+ /// Computes and returns the normal vector for the left column pixels for light filters.
+ #[inline]
+ pub fn left_column(surface: &SharedImageSurface, bounds: IRect, y: u32) -> Normal {
+ assert!(y as i32 > bounds.y0);
+ assert!((y as i32) + 1 < bounds.y1);
+ assert!(bounds.width() >= 2);
+
+ let get = |x, y| i16::from(surface.get_pixel(x, y).a);
+ let x = bounds.x0 as u32;
+
+ let top = get(x, y - 1);
+ let top_right = get(x + 1, y - 1);
+ let center = get(x, y);
+ let right = get(x + 1, y);
+ let bottom = get(x, y + 1);
+ let bottom_right = get(x + 1, y + 1);
+
+ Self::new(
+ 1. / 2.,
+ -top + top_right - 2 * center + 2 * right - bottom + bottom_right,
+ 1. / 3.,
+ -2 * top - top_right + 2 * bottom + bottom_right,
+ )
+ }
+
+ /// Computes and returns the normal vector for the interior pixels for light filters.
+ #[inline]
+ pub fn interior(surface: &SharedImageSurface, bounds: IRect, x: u32, y: u32) -> Normal {
+ assert!(x as i32 > bounds.x0);
+ assert!((x as i32) + 1 < bounds.x1);
+ assert!(y as i32 > bounds.y0);
+ assert!((y as i32) + 1 < bounds.y1);
+
+ let get = |x, y| i16::from(surface.get_pixel(x, y).a);
+
+ let top_left = get(x - 1, y - 1);
+ let top = get(x, y - 1);
+ let top_right = get(x + 1, y - 1);
+ let left = get(x - 1, y);
+ let right = get(x + 1, y);
+ let bottom_left = get(x - 1, y + 1);
+ let bottom = get(x, y + 1);
+ let bottom_right = get(x + 1, y + 1);
+
+ Self::new(
+ 1. / 4.,
+ -top_left + top_right - 2 * left + 2 * right - bottom_left + bottom_right,
+ 1. / 4.,
+ -top_left - 2 * top - top_right + bottom_left + 2 * bottom + bottom_right,
+ )
+ }
+
+ /// Computes and returns the normal vector for the right column pixels for light filters.
+ #[inline]
+ pub fn right_column(surface: &SharedImageSurface, bounds: IRect, y: u32) -> Normal {
+ assert!(y as i32 > bounds.y0);
+ assert!((y as i32) + 1 < bounds.y1);
+ assert!(bounds.width() >= 2);
+
+ let get = |x, y| i16::from(surface.get_pixel(x, y).a);
+ let x = bounds.x1 as u32 - 1;
+
+ let top_left = get(x - 1, y - 1);
+ let top = get(x, y - 1);
+ let left = get(x - 1, y);
+ let center = get(x, y);
+ let bottom_left = get(x - 1, y + 1);
+ let bottom = get(x, y + 1);
+
+ Self::new(
+ 1. / 2.,
+ -top_left + top - 2 * left + 2 * center - bottom_left + bottom,
+ 1. / 3.,
+ -top_left - 2 * top + bottom_left + 2 * bottom,
+ )
+ }
+
+ /// Computes and returns the normal vector for the bottom left pixel for light filters.
+ #[inline]
+ pub fn bottom_left(surface: &SharedImageSurface, bounds: IRect) -> Normal {
+ // Surface needs to be at least 2×2.
+ assert!(bounds.width() >= 2);
+ assert!(bounds.height() >= 2);
+
+ let get = |x, y| i16::from(surface.get_pixel(x, y).a);
+ let (x, y) = (bounds.x0 as u32, bounds.y1 as u32 - 1);
+
+ let top = get(x, y - 1);
+ let top_right = get(x + 1, y - 1);
+ let center = get(x, y);
+ let right = get(x + 1, y);
+
+ Self::new(
+ 2. / 3.,
+ -top + top_right - 2 * center + 2 * right,
+ 2. / 3.,
+ -2 * top - top_right + 2 * center + right,
+ )
+ }
+
+ /// Computes and returns the normal vector for the bottom row pixels for light filters.
+ #[inline]
+ pub fn bottom_row(surface: &SharedImageSurface, bounds: IRect, x: u32) -> Normal {
+ assert!(x as i32 > bounds.x0);
+ assert!((x as i32) + 1 < bounds.x1);
+ assert!(bounds.height() >= 2);
+
+ let get = |x, y| i16::from(surface.get_pixel(x, y).a);
+ let y = bounds.y1 as u32 - 1;
+
+ let top_left = get(x - 1, y - 1);
+ let top = get(x, y - 1);
+ let top_right = get(x + 1, y - 1);
+ let left = get(x - 1, y);
+ let center = get(x, y);
+ let right = get(x + 1, y);
+
+ Self::new(
+ 1. / 3.,
+ -top_left + top_right - 2 * left + 2 * right,
+ 1. / 2.,
+ -top_left - 2 * top - top_right + left + 2 * center + right,
+ )
+ }
+
+ /// Computes and returns the normal vector for the bottom right pixel for light filters.
+ #[inline]
+ pub fn bottom_right(surface: &SharedImageSurface, bounds: IRect) -> Normal {
+ // Surface needs to be at least 2×2.
+ assert!(bounds.width() >= 2);
+ assert!(bounds.height() >= 2);
+
+ let get = |x, y| i16::from(surface.get_pixel(x, y).a);
+ let (x, y) = (bounds.x1 as u32 - 1, bounds.y1 as u32 - 1);
+
+ let top_left = get(x - 1, y - 1);
+ let top = get(x, y - 1);
+ let left = get(x - 1, y);
+ let center = get(x, y);
+
+ Self::new(
+ 2. / 3.,
+ -top_left + top - 2 * left + 2 * center,
+ 2. / 3.,
+ -top_left - 2 * top + left + 2 * center,
+ )
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::document::Document;
+
+ #[test]
+ fn extracts_light_source() {
+ let document = Document::load_from_bytes(
+ br#"<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg">
+ <filter id="filter">
+ <feDiffuseLighting id="diffuse_distant">
+ <feDistantLight azimuth="0.0" elevation="45.0"/>
+ </feDiffuseLighting>
+
+ <feSpecularLighting id="specular_point">
+ <fePointLight x="1.0" y="2.0" z="3.0"/>
+ </feSpecularLighting>
+
+ <feDiffuseLighting id="diffuse_spot">
+ <feSpotLight x="1.0" y="2.0" z="3.0"
+ pointsAtX="4.0" pointsAtY="5.0" pointsAtZ="6.0"
+ specularExponent="7.0" limitingConeAngle="8.0"/>
+ </feDiffuseLighting>
+ </filter>
+</svg>
+"#,
+ );
+ let mut acquired_nodes = AcquiredNodes::new(&document);
+
+ let node = document.lookup_internal_node("diffuse_distant").unwrap();
+ let lighting = borrow_element_as!(node, FeDiffuseLighting);
+ let resolved = lighting.resolve(&mut acquired_nodes, &node).unwrap();
+ let ResolvedPrimitive { params, .. } = resolved.first().unwrap();
+ let diffuse_lighting = match params {
+ PrimitiveParams::DiffuseLighting(l) => l,
+ _ => unreachable!(),
+ };
+ assert_eq!(
+ diffuse_lighting.light.source,
+ UntransformedLightSource::Distant(FeDistantLight {
+ azimuth: 0.0,
+ elevation: 45.0,
+ })
+ );
+
+ let node = document.lookup_internal_node("specular_point").unwrap();
+ let lighting = borrow_element_as!(node, FeSpecularLighting);
+ let resolved = lighting.resolve(&mut acquired_nodes, &node).unwrap();
+ let ResolvedPrimitive { params, .. } = resolved.first().unwrap();
+ let specular_lighting = match params {
+ PrimitiveParams::SpecularLighting(l) => l,
+ _ => unreachable!(),
+ };
+ assert_eq!(
+ specular_lighting.light.source,
+ UntransformedLightSource::Point(FePointLight {
+ x: 1.0,
+ y: 2.0,
+ z: 3.0,
+ })
+ );
+
+ let node = document.lookup_internal_node("diffuse_spot").unwrap();
+ let lighting = borrow_element_as!(node, FeDiffuseLighting);
+ let resolved = lighting.resolve(&mut acquired_nodes, &node).unwrap();
+ let ResolvedPrimitive { params, .. } = resolved.first().unwrap();
+ let diffuse_lighting = match params {
+ PrimitiveParams::DiffuseLighting(l) => l,
+ _ => unreachable!(),
+ };
+ assert_eq!(
+ diffuse_lighting.light.source,
+ UntransformedLightSource::Spot(FeSpotLight {
+ x: 1.0,
+ y: 2.0,
+ z: 3.0,
+ points_at_x: 4.0,
+ points_at_y: 5.0,
+ points_at_z: 6.0,
+ specular_exponent: 7.0,
+ limiting_cone_angle: Some(8.0),
+ })
+ );
+ }
+}
diff --git a/rsvg/src/filters/merge.rs b/rsvg/src/filters/merge.rs
new file mode 100644
index 00000000..0f762fdd
--- /dev/null
+++ b/rsvg/src/filters/merge.rs
@@ -0,0 +1,217 @@
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementData, ElementTrait};
+use crate::node::{CascadedValues, Node, NodeBorrow};
+use crate::parsers::ParseValue;
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::surface_utils::shared_surface::{Operator, SharedImageSurface, SurfaceType};
+use crate::xml::Attributes;
+
+use super::bounds::BoundsBuilder;
+use super::context::{FilterContext, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams,
+ ResolvedPrimitive,
+};
+
+/// The `feMerge` filter primitive.
+pub struct FeMerge {
+ base: Primitive,
+}
+
+/// The `<feMergeNode>` element.
+#[derive(Clone, Default)]
+pub struct FeMergeNode {
+ in1: Input,
+}
+
+/// Resolved `feMerge` primitive for rendering.
+pub struct Merge {
+ pub merge_nodes: Vec<MergeNode>,
+}
+
+/// Resolved `feMergeNode` for rendering.
+#[derive(Debug, Default, PartialEq)]
+pub struct MergeNode {
+ pub in1: Input,
+ pub color_interpolation_filters: ColorInterpolationFilters,
+}
+
+impl Default for FeMerge {
+ /// Constructs a new `Merge` with empty properties.
+ #[inline]
+ fn default() -> FeMerge {
+ FeMerge {
+ base: Default::default(),
+ }
+ }
+}
+
+impl ElementTrait for FeMerge {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.base.parse_no_inputs(attrs, session);
+ }
+}
+
+impl ElementTrait for FeMergeNode {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ for (attr, value) in attrs.iter() {
+ if let expanded_name!("", "in") = attr.expanded() {
+ set_attribute(&mut self.in1, attr.parse(value), session);
+ }
+ }
+ }
+}
+
+impl MergeNode {
+ fn render(
+ &self,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ bounds: IRect,
+ output_surface: Option<SharedImageSurface>,
+ ) -> Result<SharedImageSurface, FilterError> {
+ let input = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in1,
+ self.color_interpolation_filters,
+ )?;
+
+ if output_surface.is_none() {
+ return Ok(input.surface().clone());
+ }
+
+ input
+ .surface()
+ .compose(&output_surface.unwrap(), bounds, Operator::Over)
+ .map_err(FilterError::CairoError)
+ }
+}
+
+impl Merge {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ // Compute the filter bounds, taking each feMergeNode's input into account.
+ let mut bounds_builder = bounds_builder;
+ for merge_node in &self.merge_nodes {
+ let input = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &merge_node.in1,
+ merge_node.color_interpolation_filters,
+ )?;
+ bounds_builder = bounds_builder.add_input(&input);
+ }
+
+ let bounds: IRect = bounds_builder.compute(ctx).clipped.into();
+
+ // Now merge them all.
+ let mut output_surface = None;
+ for merge_node in &self.merge_nodes {
+ output_surface = merge_node
+ .render(ctx, acquired_nodes, draw_ctx, bounds, output_surface)
+ .ok();
+ }
+
+ let surface = match output_surface {
+ Some(s) => s,
+ None => SharedImageSurface::empty(
+ ctx.source_graphic().width(),
+ ctx.source_graphic().height(),
+ SurfaceType::AlphaOnly,
+ )?,
+ };
+
+ Ok(FilterOutput { surface, bounds })
+ }
+}
+
+impl FilterEffect for FeMerge {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::Merge(Merge {
+ merge_nodes: resolve_merge_nodes(node)?,
+ }),
+ }])
+ }
+}
+
+/// Takes a feMerge and walks its children to produce a list of feMergeNode arguments.
+fn resolve_merge_nodes(node: &Node) -> Result<Vec<MergeNode>, FilterResolveError> {
+ let mut merge_nodes = Vec::new();
+
+ for child in node.children().filter(|c| c.is_element()) {
+ let cascaded = CascadedValues::new_from_node(&child);
+ let values = cascaded.get();
+
+ if let ElementData::FeMergeNode(merge_node) = &*child.borrow_element_data() {
+ merge_nodes.push(MergeNode {
+ in1: merge_node.in1.clone(),
+ color_interpolation_filters: values.color_interpolation_filters(),
+ });
+ }
+ }
+
+ Ok(merge_nodes)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::document::Document;
+
+ #[test]
+ fn extracts_parameters() {
+ let document = Document::load_from_bytes(
+ br#"<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg">
+ <filter id="filter">
+ <feMerge id="merge">
+ <feMergeNode in="SourceGraphic"/>
+ <feMergeNode in="SourceAlpha" color-interpolation-filters="sRGB"/>
+ </feMerge>
+ </filter>
+</svg>
+"#,
+ );
+ let mut acquired_nodes = AcquiredNodes::new(&document);
+
+ let node = document.lookup_internal_node("merge").unwrap();
+ let merge = borrow_element_as!(node, FeMerge);
+ let resolved = merge.resolve(&mut acquired_nodes, &node).unwrap();
+ let ResolvedPrimitive { params, .. } = resolved.first().unwrap();
+ let params = match params {
+ PrimitiveParams::Merge(m) => m,
+ _ => unreachable!(),
+ };
+ assert_eq!(
+ &params.merge_nodes[..],
+ vec![
+ MergeNode {
+ in1: Input::SourceGraphic,
+ color_interpolation_filters: Default::default(),
+ },
+ MergeNode {
+ in1: Input::SourceAlpha,
+ color_interpolation_filters: ColorInterpolationFilters::Srgb,
+ },
+ ]
+ );
+ }
+}
diff --git a/rsvg/src/filters/mod.rs b/rsvg/src/filters/mod.rs
new file mode 100644
index 00000000..f0fee772
--- /dev/null
+++ b/rsvg/src/filters/mod.rs
@@ -0,0 +1,381 @@
+//! Entry point for the CSS filters infrastructure.
+
+use cssparser::{BasicParseError, Parser};
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+use std::rc::Rc;
+use std::time::Instant;
+
+use crate::bbox::BoundingBox;
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementTrait};
+use crate::error::{ParseError, RenderingError};
+use crate::filter::UserSpaceFilter;
+use crate::length::*;
+use crate::node::Node;
+use crate::paint_server::UserSpacePaintSource;
+use crate::parsers::{CustomIdent, Parse, ParseValue};
+use crate::properties::ColorInterpolationFilters;
+use crate::session::Session;
+use crate::surface_utils::shared_surface::{SharedImageSurface, SurfaceType};
+use crate::transform::Transform;
+use crate::xml::Attributes;
+
+mod bounds;
+use self::bounds::BoundsBuilder;
+
+pub mod context;
+use self::context::{FilterContext, FilterOutput, FilterResult};
+
+mod error;
+use self::error::FilterError;
+pub use self::error::FilterResolveError;
+
+/// A filter primitive interface.
+pub trait FilterEffect: ElementTrait {
+ fn resolve(
+ &self,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError>;
+}
+
+pub mod blend;
+pub mod color_matrix;
+pub mod component_transfer;
+pub mod composite;
+pub mod convolve_matrix;
+pub mod displacement_map;
+pub mod drop_shadow;
+pub mod flood;
+pub mod gaussian_blur;
+pub mod image;
+pub mod lighting;
+pub mod merge;
+pub mod morphology;
+pub mod offset;
+pub mod tile;
+pub mod turbulence;
+
+pub struct FilterSpec {
+ pub user_space_filter: UserSpaceFilter,
+ pub primitives: Vec<UserSpacePrimitive>,
+}
+
+/// Resolved parameters for each filter primitive.
+///
+/// These gather all the data that a primitive may need during rendering:
+/// the `feFoo` element's attributes, any computed values from its properties,
+/// and parameters extracted from the element's children (for example,
+/// `feMerge` gathers info from its `feMergNode` children).
+pub enum PrimitiveParams {
+ Blend(blend::Blend),
+ ColorMatrix(color_matrix::ColorMatrix),
+ ComponentTransfer(component_transfer::ComponentTransfer),
+ Composite(composite::Composite),
+ ConvolveMatrix(convolve_matrix::ConvolveMatrix),
+ DiffuseLighting(lighting::DiffuseLighting),
+ DisplacementMap(displacement_map::DisplacementMap),
+ Flood(flood::Flood),
+ GaussianBlur(gaussian_blur::GaussianBlur),
+ Image(image::Image),
+ Merge(merge::Merge),
+ Morphology(morphology::Morphology),
+ Offset(offset::Offset),
+ SpecularLighting(lighting::SpecularLighting),
+ Tile(tile::Tile),
+ Turbulence(turbulence::Turbulence),
+}
+
+impl PrimitiveParams {
+ /// Returns a human-readable name for a primitive.
+ #[rustfmt::skip]
+ fn name(&self) -> &'static str {
+ use PrimitiveParams::*;
+ match self {
+ Blend(..) => "feBlend",
+ ColorMatrix(..) => "feColorMatrix",
+ ComponentTransfer(..) => "feComponentTransfer",
+ Composite(..) => "feComposite",
+ ConvolveMatrix(..) => "feConvolveMatrix",
+ DiffuseLighting(..) => "feDiffuseLighting",
+ DisplacementMap(..) => "feDisplacementMap",
+ Flood(..) => "feFlood",
+ GaussianBlur(..) => "feGaussianBlur",
+ Image(..) => "feImage",
+ Merge(..) => "feMerge",
+ Morphology(..) => "feMorphology",
+ Offset(..) => "feOffset",
+ SpecularLighting(..) => "feSpecularLighting",
+ Tile(..) => "feTile",
+ Turbulence(..) => "feTurbulence",
+ }
+ }
+}
+
+/// The base filter primitive node containing common properties.
+#[derive(Default, Clone)]
+pub struct Primitive {
+ pub x: Option<Length<Horizontal>>,
+ pub y: Option<Length<Vertical>>,
+ pub width: Option<ULength<Horizontal>>,
+ pub height: Option<ULength<Vertical>>,
+ pub result: Option<CustomIdent>,
+}
+
+pub struct ResolvedPrimitive {
+ pub primitive: Primitive,
+ pub params: PrimitiveParams,
+}
+
+/// A fully resolved filter primitive in user-space coordinates.
+pub struct UserSpacePrimitive {
+ x: Option<f64>,
+ y: Option<f64>,
+ width: Option<f64>,
+ height: Option<f64>,
+ result: Option<CustomIdent>,
+
+ params: PrimitiveParams,
+}
+
+/// An enumeration of possible inputs for a filter primitive.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub enum Input {
+ Unspecified,
+ SourceGraphic,
+ SourceAlpha,
+ BackgroundImage,
+ BackgroundAlpha,
+ FillPaint,
+ StrokePaint,
+ FilterOutput(CustomIdent),
+}
+
+enum_default!(Input, Input::Unspecified);
+
+impl Parse for Input {
+ fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ parser
+ .try_parse(|p| {
+ parse_identifiers!(
+ p,
+ "SourceGraphic" => Input::SourceGraphic,
+ "SourceAlpha" => Input::SourceAlpha,
+ "BackgroundImage" => Input::BackgroundImage,
+ "BackgroundAlpha" => Input::BackgroundAlpha,
+ "FillPaint" => Input::FillPaint,
+ "StrokePaint" => Input::StrokePaint,
+ )
+ })
+ .or_else(|_: BasicParseError<'_>| {
+ let ident = CustomIdent::parse(parser)?;
+ Ok(Input::FilterOutput(ident))
+ })
+ }
+}
+
+impl ResolvedPrimitive {
+ pub fn into_user_space(self, params: &NormalizeParams) -> UserSpacePrimitive {
+ let x = self.primitive.x.map(|l| l.to_user(params));
+ let y = self.primitive.y.map(|l| l.to_user(params));
+ let width = self.primitive.width.map(|l| l.to_user(params));
+ let height = self.primitive.height.map(|l| l.to_user(params));
+
+ UserSpacePrimitive {
+ x,
+ y,
+ width,
+ height,
+ result: self.primitive.result,
+ params: self.params,
+ }
+ }
+}
+
+impl UserSpacePrimitive {
+ /// Validates attributes and returns the `BoundsBuilder` for bounds computation.
+ #[inline]
+ fn get_bounds(&self, ctx: &FilterContext) -> BoundsBuilder {
+ BoundsBuilder::new(self.x, self.y, self.width, self.height, ctx.paffine())
+ }
+}
+
+impl Primitive {
+ fn parse_standard_attributes(
+ &mut self,
+ attrs: &Attributes,
+ session: &Session,
+ ) -> (Input, Input) {
+ let mut input_1 = Input::Unspecified;
+ let mut input_2 = Input::Unspecified;
+
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session),
+ expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session),
+ expanded_name!("", "width") => {
+ set_attribute(&mut self.width, attr.parse(value), session)
+ }
+ expanded_name!("", "height") => {
+ set_attribute(&mut self.height, attr.parse(value), session)
+ }
+ expanded_name!("", "result") => {
+ set_attribute(&mut self.result, attr.parse(value), session)
+ }
+ expanded_name!("", "in") => set_attribute(&mut input_1, attr.parse(value), session),
+ expanded_name!("", "in2") => {
+ set_attribute(&mut input_2, attr.parse(value), session)
+ }
+ _ => (),
+ }
+ }
+
+ (input_1, input_2)
+ }
+
+ pub fn parse_no_inputs(&mut self, attrs: &Attributes, session: &Session) {
+ let (_, _) = self.parse_standard_attributes(attrs, session);
+ }
+
+ pub fn parse_one_input(&mut self, attrs: &Attributes, session: &Session) -> Input {
+ let (input_1, _) = self.parse_standard_attributes(attrs, session);
+ input_1
+ }
+
+ pub fn parse_two_inputs(&mut self, attrs: &Attributes, session: &Session) -> (Input, Input) {
+ self.parse_standard_attributes(attrs, session)
+ }
+}
+
+/// Applies a filter and returns the resulting surface.
+pub fn render(
+ filter: &FilterSpec,
+ stroke_paint_source: Rc<UserSpacePaintSource>,
+ fill_paint_source: Rc<UserSpacePaintSource>,
+ source_surface: SharedImageSurface,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ transform: Transform,
+ node_bbox: BoundingBox,
+) -> Result<SharedImageSurface, RenderingError> {
+ let session = draw_ctx.session().clone();
+
+ FilterContext::new(
+ &filter.user_space_filter,
+ stroke_paint_source,
+ fill_paint_source,
+ &source_surface,
+ transform,
+ node_bbox,
+ )
+ .and_then(|mut filter_ctx| {
+ // the message has an unclosed parenthesis; we'll close it below.
+ rsvg_log!(
+ session,
+ "(rendering filter with effects_region={:?}",
+ filter_ctx.effects_region()
+ );
+ for user_space_primitive in &filter.primitives {
+ let start = Instant::now();
+
+ match render_primitive(user_space_primitive, &filter_ctx, acquired_nodes, draw_ctx) {
+ Ok(output) => {
+ let elapsed = start.elapsed();
+ rsvg_log!(
+ session,
+ "(rendered filter primitive {} in\n {} seconds)",
+ user_space_primitive.params.name(),
+ elapsed.as_secs() as f64 + f64::from(elapsed.subsec_nanos()) / 1e9
+ );
+
+ filter_ctx.store_result(FilterResult {
+ name: user_space_primitive.result.clone(),
+ output,
+ });
+ }
+
+ Err(err) => {
+ rsvg_log!(
+ session,
+ "(filter primitive {} returned an error: {})",
+ user_space_primitive.params.name(),
+ err
+ );
+
+ // close the opening parenthesis from the message at the start of this function
+ rsvg_log!(session, ")");
+
+ // Exit early on Cairo errors. Continue rendering otherwise.
+ if let FilterError::CairoError(status) = err {
+ return Err(FilterError::CairoError(status));
+ }
+ }
+ }
+ }
+
+ // close the opening parenthesis from the message at the start of this function
+ rsvg_log!(session, ")");
+
+ Ok(filter_ctx.into_output()?)
+ })
+ .or_else(|err| match err {
+ FilterError::CairoError(status) => {
+ // Exit early on Cairo errors
+ Err(RenderingError::from(status))
+ }
+
+ _ => {
+ // ignore other filter errors and just return an empty surface
+ Ok(SharedImageSurface::empty(
+ source_surface.width(),
+ source_surface.height(),
+ SurfaceType::AlphaOnly,
+ )?)
+ }
+ })
+}
+
+#[rustfmt::skip]
+fn render_primitive(
+ primitive: &UserSpacePrimitive,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+) -> Result<FilterOutput, FilterError> {
+ use PrimitiveParams::*;
+
+ let bounds_builder = primitive.get_bounds(ctx);
+
+ // Note that feDropShadow is not handled here. When its FilterElement::resolve() is called,
+ // it returns a series of lower-level primitives (flood, blur, offset, etc.) that make up
+ // the drop-shadow effect.
+
+ match primitive.params {
+ Blend(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ ColorMatrix(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ ComponentTransfer(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ Composite(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ ConvolveMatrix(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ DiffuseLighting(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ DisplacementMap(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ Flood(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ GaussianBlur(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ Image(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ Merge(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ Morphology(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ Offset(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ SpecularLighting(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ Tile(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ Turbulence(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx),
+ }
+}
+
+impl From<ColorInterpolationFilters> for SurfaceType {
+ fn from(c: ColorInterpolationFilters) -> Self {
+ match c {
+ ColorInterpolationFilters::LinearRgb => SurfaceType::LinearRgb,
+ _ => SurfaceType::SRgb,
+ }
+ }
+}
diff --git a/rsvg/src/filters/morphology.rs b/rsvg/src/filters/morphology.rs
new file mode 100644
index 00000000..1ff7ddaa
--- /dev/null
+++ b/rsvg/src/filters/morphology.rs
@@ -0,0 +1,200 @@
+use std::cmp::{max, min};
+
+use cssparser::Parser;
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementTrait};
+use crate::error::*;
+use crate::node::Node;
+use crate::parsers::{NumberOptionalNumber, Parse, ParseValue};
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::surface_utils::{
+ iterators::{PixelRectangle, Pixels},
+ shared_surface::ExclusiveImageSurface,
+ EdgeMode, ImageSurfaceDataExt, Pixel,
+};
+use crate::xml::Attributes;
+
+use super::bounds::BoundsBuilder;
+use super::context::{FilterContext, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams,
+ ResolvedPrimitive,
+};
+
+/// Enumeration of the possible morphology operations.
+#[derive(Clone)]
+enum Operator {
+ Erode,
+ Dilate,
+}
+
+enum_default!(Operator, Operator::Erode);
+
+/// The `feMorphology` filter primitive.
+#[derive(Default)]
+pub struct FeMorphology {
+ base: Primitive,
+ params: Morphology,
+}
+
+/// Resolved `feMorphology` primitive for rendering.
+#[derive(Clone)]
+pub struct Morphology {
+ in1: Input,
+ operator: Operator,
+ radius: NumberOptionalNumber<f64>,
+}
+
+// We need this because NumberOptionalNumber doesn't impl Default
+impl Default for Morphology {
+ fn default() -> Morphology {
+ Morphology {
+ in1: Default::default(),
+ operator: Default::default(),
+ radius: NumberOptionalNumber(0.0, 0.0),
+ }
+ }
+}
+
+impl ElementTrait for FeMorphology {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.params.in1 = self.base.parse_one_input(attrs, session);
+
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "operator") => {
+ set_attribute(&mut self.params.operator, attr.parse(value), session);
+ }
+ expanded_name!("", "radius") => {
+ set_attribute(&mut self.params.radius, attr.parse(value), session);
+ }
+ _ => (),
+ }
+ }
+ }
+}
+
+impl Morphology {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ // Although https://www.w3.org/TR/filter-effects/#propdef-color-interpolation-filters does not mention
+ // feMorphology as being one of the primitives that does *not* use that property,
+ // the SVG1.1 test for filters-morph-01-f.svg fails if we pass the value from the ComputedValues here (that
+ // document does not specify the color-interpolation-filters property, so it defaults to linearRGB).
+ // So, we pass Auto, which will get resolved to SRGB, and that makes that test pass.
+ //
+ // I suppose erosion/dilation doesn't care about the color space of the source image?
+
+ let input_1 = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in1,
+ ColorInterpolationFilters::Auto,
+ )?;
+ let bounds: IRect = bounds_builder
+ .add_input(&input_1)
+ .compute(ctx)
+ .clipped
+ .into();
+
+ let NumberOptionalNumber(rx, ry) = self.radius;
+
+ if rx <= 0.0 && ry <= 0.0 {
+ return Ok(FilterOutput {
+ surface: input_1.surface().clone(),
+ bounds,
+ });
+ }
+
+ let (rx, ry) = ctx.paffine().transform_distance(rx, ry);
+
+ // The radii can become negative here due to the transform.
+ // Additionally The radii being excessively large causes cpu hangups
+ let (rx, ry) = (rx.abs().min(10.0), ry.abs().min(10.0));
+
+ let mut surface = ExclusiveImageSurface::new(
+ ctx.source_graphic().width(),
+ ctx.source_graphic().height(),
+ input_1.surface().surface_type(),
+ )?;
+
+ surface.modify(&mut |data, stride| {
+ for (x, y, _pixel) in Pixels::within(input_1.surface(), bounds) {
+ // Compute the kernel rectangle bounds.
+ let kernel_bounds = IRect::new(
+ (f64::from(x) - rx).floor() as i32,
+ (f64::from(y) - ry).floor() as i32,
+ (f64::from(x) + rx).ceil() as i32 + 1,
+ (f64::from(y) + ry).ceil() as i32 + 1,
+ );
+
+ // Compute the new pixel values.
+ let initial = match self.operator {
+ Operator::Erode => u8::max_value(),
+ Operator::Dilate => u8::min_value(),
+ };
+
+ let mut output_pixel = Pixel {
+ r: initial,
+ g: initial,
+ b: initial,
+ a: initial,
+ };
+
+ for (_x, _y, pixel) in
+ PixelRectangle::within(input_1.surface(), bounds, kernel_bounds, EdgeMode::None)
+ {
+ let op = match self.operator {
+ Operator::Erode => min,
+ Operator::Dilate => max,
+ };
+
+ output_pixel.r = op(output_pixel.r, pixel.r);
+ output_pixel.g = op(output_pixel.g, pixel.g);
+ output_pixel.b = op(output_pixel.b, pixel.b);
+ output_pixel.a = op(output_pixel.a, pixel.a);
+ }
+
+ data.set_pixel(stride, output_pixel, x, y);
+ }
+ });
+
+ Ok(FilterOutput {
+ surface: surface.share()?,
+ bounds,
+ })
+ }
+}
+
+impl FilterEffect for FeMorphology {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ _node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::Morphology(self.params.clone()),
+ }])
+ }
+}
+
+impl Parse for Operator {
+ fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ Ok(parse_identifiers!(
+ parser,
+ "erode" => Operator::Erode,
+ "dilate" => Operator::Dilate,
+ )?)
+ }
+}
diff --git a/rsvg/src/filters/offset.rs b/rsvg/src/filters/offset.rs
new file mode 100644
index 00000000..5b15b583
--- /dev/null
+++ b/rsvg/src/filters/offset.rs
@@ -0,0 +1,100 @@
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementTrait};
+use crate::node::Node;
+use crate::parsers::ParseValue;
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::xml::Attributes;
+
+use super::bounds::BoundsBuilder;
+use super::context::{FilterContext, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams,
+ ResolvedPrimitive,
+};
+
+/// The `feOffset` filter primitive.
+#[derive(Default)]
+pub struct FeOffset {
+ base: Primitive,
+ params: Offset,
+}
+
+/// Resolved `feOffset` primitive for rendering.
+#[derive(Clone, Default)]
+pub struct Offset {
+ pub in1: Input,
+ pub dx: f64,
+ pub dy: f64,
+}
+
+impl ElementTrait for FeOffset {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.params.in1 = self.base.parse_one_input(attrs, session);
+
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "dx") => {
+ set_attribute(&mut self.params.dx, attr.parse(value), session)
+ }
+ expanded_name!("", "dy") => {
+ set_attribute(&mut self.params.dy, attr.parse(value), session)
+ }
+ _ => (),
+ }
+ }
+ }
+}
+
+impl Offset {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ // https://www.w3.org/TR/filter-effects/#ColorInterpolationFiltersProperty
+ //
+ // "Note: The color-interpolation-filters property just has an
+ // effect on filter operations. Therefore, it has no effect on
+ // filter primitives like feOffset"
+ //
+ // This is why we pass Auto here.
+ let input_1 = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in1,
+ ColorInterpolationFilters::Auto,
+ )?;
+ let bounds: IRect = bounds_builder
+ .add_input(&input_1)
+ .compute(ctx)
+ .clipped
+ .into();
+ rsvg_log!(draw_ctx.session(), "(feOffset bounds={:?}", bounds);
+
+ let (dx, dy) = ctx.paffine().transform_distance(self.dx, self.dy);
+
+ let surface = input_1.surface().offset(bounds, dx, dy)?;
+
+ Ok(FilterOutput { surface, bounds })
+ }
+}
+
+impl FilterEffect for FeOffset {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ _node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::Offset(self.params.clone()),
+ }])
+ }
+}
diff --git a/rsvg/src/filters/tile.rs b/rsvg/src/filters/tile.rs
new file mode 100644
index 00000000..fb50ce81
--- /dev/null
+++ b/rsvg/src/filters/tile.rs
@@ -0,0 +1,109 @@
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::ElementTrait;
+use crate::node::Node;
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::xml::Attributes;
+
+use super::bounds::BoundsBuilder;
+use super::context::{FilterContext, FilterInput, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams,
+ ResolvedPrimitive,
+};
+
+/// The `feTile` filter primitive.
+#[derive(Default)]
+pub struct FeTile {
+ base: Primitive,
+ params: Tile,
+}
+
+/// Resolved `feTile` primitive for rendering.
+#[derive(Clone, Default)]
+pub struct Tile {
+ in1: Input,
+}
+
+impl ElementTrait for FeTile {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.params.in1 = self.base.parse_one_input(attrs, session);
+ }
+}
+
+impl Tile {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ acquired_nodes: &mut AcquiredNodes<'_>,
+ draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ // https://www.w3.org/TR/filter-effects/#ColorInterpolationFiltersProperty
+ //
+ // "Note: The color-interpolation-filters property just has an
+ // effect on filter operations. Therefore, it has no effect on
+ // filter primitives like [...], feTile"
+ //
+ // This is why we pass Auto here.
+ let input_1 = ctx.get_input(
+ acquired_nodes,
+ draw_ctx,
+ &self.in1,
+ ColorInterpolationFilters::Auto,
+ )?;
+
+ // feTile doesn't consider its inputs in the filter primitive subregion calculation.
+ let bounds: IRect = bounds_builder.compute(ctx).clipped.into();
+
+ let surface = match input_1 {
+ FilterInput::StandardInput(input_surface) => input_surface,
+ FilterInput::PrimitiveOutput(FilterOutput {
+ surface: input_surface,
+ bounds: input_bounds,
+ }) => {
+ if input_bounds.is_empty() {
+ rsvg_log!(
+ draw_ctx.session(),
+ "(feTile with empty input_bounds; returning just the input surface)"
+ );
+
+ input_surface
+ } else {
+ rsvg_log!(
+ draw_ctx.session(),
+ "(feTile bounds={:?}, input_bounds={:?})",
+ bounds,
+ input_bounds
+ );
+
+ let tile_surface = input_surface.tile(input_bounds)?;
+
+ ctx.source_graphic().paint_image_tiled(
+ bounds,
+ &tile_surface,
+ input_bounds.x0,
+ input_bounds.y0,
+ )?
+ }
+ }
+ };
+
+ Ok(FilterOutput { surface, bounds })
+ }
+}
+
+impl FilterEffect for FeTile {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ _node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::Tile(self.params.clone()),
+ }])
+ }
+}
diff --git a/rsvg/src/filters/turbulence.rs b/rsvg/src/filters/turbulence.rs
new file mode 100644
index 00000000..9e76a2a6
--- /dev/null
+++ b/rsvg/src/filters/turbulence.rs
@@ -0,0 +1,484 @@
+use cssparser::Parser;
+use markup5ever::{expanded_name, local_name, namespace_url, ns};
+
+use crate::document::AcquiredNodes;
+use crate::drawing_ctx::DrawingCtx;
+use crate::element::{set_attribute, ElementTrait};
+use crate::error::*;
+use crate::node::{CascadedValues, Node};
+use crate::parsers::{NumberOptionalNumber, Parse, ParseValue};
+use crate::properties::ColorInterpolationFilters;
+use crate::rect::IRect;
+use crate::session::Session;
+use crate::surface_utils::{
+ shared_surface::{ExclusiveImageSurface, SurfaceType},
+ ImageSurfaceDataExt, Pixel, PixelOps,
+};
+use crate::util::clamp;
+use crate::xml::Attributes;
+
+use super::bounds::BoundsBuilder;
+use super::context::{FilterContext, FilterOutput};
+use super::{
+ FilterEffect, FilterError, FilterResolveError, Primitive, PrimitiveParams, ResolvedPrimitive,
+};
+
+/// Enumeration of the tile stitching modes.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
+enum StitchTiles {
+ Stitch,
+ NoStitch,
+}
+
+enum_default!(StitchTiles, StitchTiles::NoStitch);
+
+/// Enumeration of the noise types.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
+enum NoiseType {
+ FractalNoise,
+ Turbulence,
+}
+
+enum_default!(NoiseType, NoiseType::Turbulence);
+
+/// The `feTurbulence` filter primitive.
+#[derive(Default)]
+pub struct FeTurbulence {
+ base: Primitive,
+ params: Turbulence,
+}
+
+/// Resolved `feTurbulence` primitive for rendering.
+#[derive(Clone)]
+pub struct Turbulence {
+ base_frequency: NumberOptionalNumber<f64>,
+ num_octaves: i32,
+ seed: f64,
+ stitch_tiles: StitchTiles,
+ type_: NoiseType,
+ color_interpolation_filters: ColorInterpolationFilters,
+}
+
+impl Default for Turbulence {
+ /// Constructs a new `Turbulence` with empty properties.
+ #[inline]
+ fn default() -> Turbulence {
+ Turbulence {
+ base_frequency: NumberOptionalNumber(0.0, 0.0),
+ num_octaves: 1,
+ seed: 0.0,
+ stitch_tiles: Default::default(),
+ type_: Default::default(),
+ color_interpolation_filters: Default::default(),
+ }
+ }
+}
+
+impl ElementTrait for FeTurbulence {
+ fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
+ self.base.parse_no_inputs(attrs, session);
+
+ for (attr, value) in attrs.iter() {
+ match attr.expanded() {
+ expanded_name!("", "baseFrequency") => {
+ set_attribute(&mut self.params.base_frequency, attr.parse(value), session);
+ }
+ expanded_name!("", "numOctaves") => {
+ set_attribute(&mut self.params.num_octaves, attr.parse(value), session);
+ }
+ // Yes, seed needs to be parsed as a number and then truncated.
+ expanded_name!("", "seed") => {
+ set_attribute(&mut self.params.seed, attr.parse(value), session);
+ }
+ expanded_name!("", "stitchTiles") => {
+ set_attribute(&mut self.params.stitch_tiles, attr.parse(value), session);
+ }
+ expanded_name!("", "type") => {
+ set_attribute(&mut self.params.type_, attr.parse(value), session)
+ }
+ _ => (),
+ }
+ }
+ }
+}
+
+// Produces results in the range [1, 2**31 - 2].
+// Algorithm is: r = (a * r) mod m
+// where a = 16807 and m = 2**31 - 1 = 2147483647
+// See [Park & Miller], CACM vol. 31 no. 10 p. 1195, Oct. 1988
+// To test: the algorithm should produce the result 1043618065
+// as the 10,000th generated number if the original seed is 1.
+const RAND_M: i32 = 2147483647; // 2**31 - 1
+const RAND_A: i32 = 16807; // 7**5; primitive root of m
+const RAND_Q: i32 = 127773; // m / a
+const RAND_R: i32 = 2836; // m % a
+
+fn setup_seed(mut seed: i32) -> i32 {
+ if seed <= 0 {
+ seed = -(seed % (RAND_M - 1)) + 1;
+ }
+ if seed > RAND_M - 1 {
+ seed = RAND_M - 1;
+ }
+ seed
+}
+
+fn random(seed: i32) -> i32 {
+ let mut result = RAND_A * (seed % RAND_Q) - RAND_R * (seed / RAND_Q);
+ if result <= 0 {
+ result += RAND_M;
+ }
+ result
+}
+
+const B_SIZE: usize = 0x100;
+const PERLIN_N: i32 = 0x1000;
+
+#[derive(Clone, Copy)]
+struct NoiseGenerator {
+ base_frequency: (f64, f64),
+ num_octaves: i32,
+ stitch_tiles: StitchTiles,
+ type_: NoiseType,
+
+ tile_width: f64,
+ tile_height: f64,
+
+ lattice_selector: [usize; B_SIZE + B_SIZE + 2],
+ gradient: [[[f64; 2]; B_SIZE + B_SIZE + 2]; 4],
+}
+
+#[derive(Clone, Copy)]
+struct StitchInfo {
+ width: usize, // How much to subtract to wrap for stitching.
+ height: usize,
+ wrap_x: usize, // Minimum value to wrap.
+ wrap_y: usize,
+}
+
+impl NoiseGenerator {
+ fn new(
+ seed: i32,
+ base_frequency: (f64, f64),
+ num_octaves: i32,
+ type_: NoiseType,
+ stitch_tiles: StitchTiles,
+ tile_width: f64,
+ tile_height: f64,
+ ) -> Self {
+ let mut rv = Self {
+ base_frequency,
+ num_octaves,
+ type_,
+ stitch_tiles,
+
+ tile_width,
+ tile_height,
+
+ lattice_selector: [0; B_SIZE + B_SIZE + 2],
+ gradient: [[[0.0; 2]; B_SIZE + B_SIZE + 2]; 4],
+ };
+
+ let mut seed = setup_seed(seed);
+
+ for k in 0..4 {
+ for i in 0..B_SIZE {
+ rv.lattice_selector[i] = i;
+ for j in 0..2 {
+ seed = random(seed);
+ rv.gradient[k][i][j] =
+ ((seed % (B_SIZE + B_SIZE) as i32) - B_SIZE as i32) as f64 / B_SIZE as f64;
+ }
+ let s = (rv.gradient[k][i][0] * rv.gradient[k][i][0]
+ + rv.gradient[k][i][1] * rv.gradient[k][i][1])
+ .sqrt();
+ rv.gradient[k][i][0] /= s;
+ rv.gradient[k][i][1] /= s;
+ }
+ }
+ for i in (1..B_SIZE).rev() {
+ let k = rv.lattice_selector[i];
+ seed = random(seed);
+ let j = seed as usize % B_SIZE;
+ rv.lattice_selector[i] = rv.lattice_selector[j];
+ rv.lattice_selector[j] = k;
+ }
+ for i in 0..B_SIZE + 2 {
+ rv.lattice_selector[B_SIZE + i] = rv.lattice_selector[i];
+ for k in 0..4 {
+ for j in 0..2 {
+ rv.gradient[k][B_SIZE + i][j] = rv.gradient[k][i][j];
+ }
+ }
+ }
+
+ rv
+ }
+
+ fn noise2(&self, color_channel: usize, vec: [f64; 2], stitch_info: Option<StitchInfo>) -> f64 {
+ #![allow(clippy::many_single_char_names)]
+
+ const BM: usize = 0xff;
+
+ let s_curve = |t| t * t * (3. - 2. * t);
+ let lerp = |t, a, b| a + t * (b - a);
+
+ let t = vec[0] + f64::from(PERLIN_N);
+ let mut bx0 = t as usize;
+ let mut bx1 = bx0 + 1;
+ let rx0 = t.fract();
+ let rx1 = rx0 - 1.0;
+ let t = vec[1] + f64::from(PERLIN_N);
+ let mut by0 = t as usize;
+ let mut by1 = by0 + 1;
+ let ry0 = t.fract();
+ let ry1 = ry0 - 1.0;
+
+ // If stitching, adjust lattice points accordingly.
+ if let Some(stitch_info) = stitch_info {
+ if bx0 >= stitch_info.wrap_x {
+ bx0 -= stitch_info.width;
+ }
+ if bx1 >= stitch_info.wrap_x {
+ bx1 -= stitch_info.width;
+ }
+ if by0 >= stitch_info.wrap_y {
+ by0 -= stitch_info.height;
+ }
+ if by1 >= stitch_info.wrap_y {
+ by1 -= stitch_info.height;
+ }
+ }
+ bx0 &= BM;
+ bx1 &= BM;
+ by0 &= BM;
+ by1 &= BM;
+ let i = self.lattice_selector[bx0];
+ let j = self.lattice_selector[bx1];
+ let b00 = self.lattice_selector[i + by0];
+ let b10 = self.lattice_selector[j + by0];
+ let b01 = self.lattice_selector[i + by1];
+ let b11 = self.lattice_selector[j + by1];
+ let sx = s_curve(rx0);
+ let sy = s_curve(ry0);
+ let q = self.gradient[color_channel][b00];
+ let u = rx0 * q[0] + ry0 * q[1];
+ let q = self.gradient[color_channel][b10];
+ let v = rx1 * q[0] + ry0 * q[1];
+ let a = lerp(sx, u, v);
+ let q = self.gradient[color_channel][b01];
+ let u = rx0 * q[0] + ry1 * q[1];
+ let q = self.gradient[color_channel][b11];
+ let v = rx1 * q[0] + ry1 * q[1];
+ let b = lerp(sx, u, v);
+ lerp(sy, a, b)
+ }
+
+ fn turbulence(&self, color_channel: usize, point: [f64; 2], tile_x: f64, tile_y: f64) -> f64 {
+ let mut stitch_info = None;
+ let mut base_frequency = self.base_frequency;
+
+ // Adjust the base frequencies if necessary for stitching.
+ if self.stitch_tiles == StitchTiles::Stitch {
+ // When stitching tiled turbulence, the frequencies must be adjusted
+ // so that the tile borders will be continuous.
+ if base_frequency.0 != 0.0 {
+ let freq_lo = (self.tile_width * base_frequency.0).floor() / self.tile_width;
+ let freq_hi = (self.tile_width * base_frequency.0).ceil() / self.tile_width;
+ if base_frequency.0 / freq_lo < freq_hi / base_frequency.0 {
+ base_frequency.0 = freq_lo;
+ } else {
+ base_frequency.0 = freq_hi;
+ }
+ }
+ if base_frequency.1 != 0.0 {
+ let freq_lo = (self.tile_height * base_frequency.1).floor() / self.tile_height;
+ let freq_hi = (self.tile_height * base_frequency.1).ceil() / self.tile_height;
+ if base_frequency.1 / freq_lo < freq_hi / base_frequency.1 {
+ base_frequency.1 = freq_lo;
+ } else {
+ base_frequency.1 = freq_hi;
+ }
+ }
+
+ // Set up initial stitch values.
+ let width = (self.tile_width * base_frequency.0 + 0.5) as usize;
+ let height = (self.tile_height * base_frequency.1 + 0.5) as usize;
+ stitch_info = Some(StitchInfo {
+ width,
+ wrap_x: (tile_x * base_frequency.0) as usize + PERLIN_N as usize + width,
+ height,
+ wrap_y: (tile_y * base_frequency.1) as usize + PERLIN_N as usize + height,
+ });
+ }
+
+ let mut sum = 0.0;
+ let mut vec = [point[0] * base_frequency.0, point[1] * base_frequency.1];
+ let mut ratio = 1.0;
+ for _ in 0..self.num_octaves {
+ if self.type_ == NoiseType::FractalNoise {
+ sum += self.noise2(color_channel, vec, stitch_info) / ratio;
+ } else {
+ sum += (self.noise2(color_channel, vec, stitch_info)).abs() / ratio;
+ }
+ vec[0] *= 2.0;
+ vec[1] *= 2.0;
+ ratio *= 2.0;
+ if let Some(stitch_info) = stitch_info.as_mut() {
+ // Update stitch values. Subtracting PerlinN before the multiplication and
+ // adding it afterward simplifies to subtracting it once.
+ stitch_info.width *= 2;
+ stitch_info.wrap_x = 2 * stitch_info.wrap_x - PERLIN_N as usize;
+ stitch_info.height *= 2;
+ stitch_info.wrap_y = 2 * stitch_info.wrap_y - PERLIN_N as usize;
+ }
+ }
+ sum
+ }
+}
+
+impl Turbulence {
+ pub fn render(
+ &self,
+ bounds_builder: BoundsBuilder,
+ ctx: &FilterContext,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ _draw_ctx: &mut DrawingCtx,
+ ) -> Result<FilterOutput, FilterError> {
+ let bounds: IRect = bounds_builder.compute(ctx).clipped.into();
+
+ let affine = ctx.paffine().invert().unwrap();
+
+ let seed = clamp(
+ self.seed.trunc(), // per the spec, round towards zero
+ f64::from(i32::min_value()),
+ f64::from(i32::max_value()),
+ ) as i32;
+
+ // "Negative values are unsupported" -> set to the initial value which is 0.0
+ //
+ // https://drafts.fxtf.org/filter-effects/#element-attrdef-feturbulence-basefrequency
+ let base_frequency = {
+ let NumberOptionalNumber(base_freq_x, base_freq_y) = self.base_frequency;
+ let x = base_freq_x.max(0.0);
+ let y = base_freq_y.max(0.0);
+ (x, y)
+ };
+
+ let noise_generator = NoiseGenerator::new(
+ seed,
+ base_frequency,
+ self.num_octaves,
+ self.type_,
+ self.stitch_tiles,
+ f64::from(bounds.width()),
+ f64::from(bounds.height()),
+ );
+
+ // The generated color values are in the color space determined by
+ // color-interpolation-filters.
+ let surface_type = SurfaceType::from(self.color_interpolation_filters);
+
+ let mut surface = ExclusiveImageSurface::new(
+ ctx.source_graphic().width(),
+ ctx.source_graphic().height(),
+ surface_type,
+ )?;
+
+ surface.modify(&mut |data, stride| {
+ for y in bounds.y_range() {
+ for x in bounds.x_range() {
+ let point = affine.transform_point(f64::from(x), f64::from(y));
+ let point = [point.0, point.1];
+
+ let generate = |color_channel| {
+ let v = noise_generator.turbulence(
+ color_channel,
+ point,
+ f64::from(x - bounds.x0),
+ f64::from(y - bounds.y0),
+ );
+
+ let v = match self.type_ {
+ NoiseType::FractalNoise => (v * 255.0 + 255.0) / 2.0,
+ NoiseType::Turbulence => v * 255.0,
+ };
+
+ (clamp(v, 0.0, 255.0) + 0.5) as u8
+ };
+
+ let pixel = Pixel {
+ r: generate(0),
+ g: generate(1),
+ b: generate(2),
+ a: generate(3),
+ }
+ .premultiply();
+
+ data.set_pixel(stride, pixel, x as u32, y as u32);
+ }
+ }
+ });
+
+ Ok(FilterOutput {
+ surface: surface.share()?,
+ bounds,
+ })
+ }
+}
+
+impl FilterEffect for FeTurbulence {
+ fn resolve(
+ &self,
+ _acquired_nodes: &mut AcquiredNodes<'_>,
+ node: &Node,
+ ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> {
+ let cascaded = CascadedValues::new_from_node(node);
+ let values = cascaded.get();
+
+ let mut params = self.params.clone();
+ params.color_interpolation_filters = values.color_interpolation_filters();
+
+ Ok(vec![ResolvedPrimitive {
+ primitive: self.base.clone(),
+ params: PrimitiveParams::Turbulence(params),
+ }])
+ }
+}
+
+impl Parse for StitchTiles {
+ fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ Ok(parse_identifiers!(
+ parser,
+ "stitch" => StitchTiles::Stitch,
+ "noStitch" => StitchTiles::NoStitch,
+ )?)
+ }
+}
+
+impl Parse for NoiseType {
+ fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ Ok(parse_identifiers!(
+ parser,
+ "fractalNoise" => NoiseType::FractalNoise,
+ "turbulence" => NoiseType::Turbulence,
+ )?)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn turbulence_rng() {
+ let mut r = 1;
+ r = setup_seed(r);
+
+ for _ in 0..10_000 {
+ r = random(r);
+ }
+
+ assert_eq!(r, 1043618065);
+ }
+}