diff options
Diffstat (limited to 'rsvg/src/filters')
-rw-r--r-- | rsvg/src/filters/blend.rs | 178 | ||||
-rw-r--r-- | rsvg/src/filters/bounds.rs | 121 | ||||
-rw-r--r-- | rsvg/src/filters/color_matrix.rs | 342 | ||||
-rw-r--r-- | rsvg/src/filters/component_transfer.rs | 458 | ||||
-rw-r--r-- | rsvg/src/filters/composite.rs | 179 | ||||
-rw-r--r-- | rsvg/src/filters/context.rs | 405 | ||||
-rw-r--r-- | rsvg/src/filters/convolve_matrix.rs | 354 | ||||
-rw-r--r-- | rsvg/src/filters/displacement_map.rs | 195 | ||||
-rw-r--r-- | rsvg/src/filters/drop_shadow.rs | 88 | ||||
-rw-r--r-- | rsvg/src/filters/error.rs | 78 | ||||
-rw-r--r-- | rsvg/src/filters/flood.rs | 70 | ||||
-rw-r--r-- | rsvg/src/filters/gaussian_blur.rs | 282 | ||||
-rw-r--r-- | rsvg/src/filters/image.rs | 211 | ||||
-rw-r--r-- | rsvg/src/filters/lighting.rs | 1090 | ||||
-rw-r--r-- | rsvg/src/filters/merge.rs | 217 | ||||
-rw-r--r-- | rsvg/src/filters/mod.rs | 381 | ||||
-rw-r--r-- | rsvg/src/filters/morphology.rs | 200 | ||||
-rw-r--r-- | rsvg/src/filters/offset.rs | 100 | ||||
-rw-r--r-- | rsvg/src/filters/tile.rs | 109 | ||||
-rw-r--r-- | rsvg/src/filters/turbulence.rs | 484 |
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(¶ms, 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(¶ms_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!( + ¶ms.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); + } +} |