[librsvg: 1/4] Implement opacity filter function




commit 6c37a0ac21acd39b5e0d4cb8f417f686a6c6288d
Author: John Ledbetter <john ledbetter gmail com>
Date:   Wed May 12 10:19:39 2021 -0400

    Implement opacity filter function
    
    `opacity()` takes a single optional `number-percentage` argument,
    similar to `sepia`(); the value must be clamped to 0-1 and values
    less than 0 are invalid.
    
    Opacity is implemented as a `FeComponentTransfer` that modifies the
    alpha component.
    
    See:
    * https://www.w3.org/TR/filter-effects/#funcdef-filter-opacity
    * https://www.w3.org/TR/filter-effects/#opacityEquivalent

 src/filter_func.rs                | 88 +++++++++++++++++++++++++++++++++++----
 src/filters/component_transfer.rs | 36 ++++++++--------
 tests/src/filters.rs              | 59 ++++++++++++++++++++++++++
 3 files changed, 157 insertions(+), 26 deletions(-)
---
diff --git a/src/filter_func.rs b/src/filter_func.rs
index a0ebeea1..ca7bc7c6 100644
--- a/src/filter_func.rs
+++ b/src/filter_func.rs
@@ -1,6 +1,5 @@
 use cssparser::Parser;
 
-use crate::drawing_ctx::DrawingCtx;
 use crate::error::*;
 use crate::filter::Filter;
 use crate::filters::{
@@ -11,6 +10,7 @@ use crate::length::*;
 use crate::parsers::{NumberOrPercentage, Parse};
 use crate::properties::ComputedValues;
 use crate::{coord_units::CoordUnits, filters::color_matrix::ColorMatrix};
+use crate::{drawing_ctx::DrawingCtx, filters::component_transfer};
 
 /// CSS Filter functions from the Filter Effects Module Level 1
 ///
@@ -18,6 +18,7 @@ use crate::{coord_units::CoordUnits, filters::color_matrix::ColorMatrix};
 #[derive(Debug, Clone, PartialEq)]
 pub enum FilterFunction {
     Blur(Blur),
+    Opacity(Opacity),
     Sepia(Sepia),
 }
 
@@ -29,6 +30,14 @@ pub struct Blur {
     std_deviation: Option<Length<Both>>,
 }
 
+/// Parameters for the `opacity()` filter function
+///
+/// https://www.w3.org/TR/filter-effects/#funcdef-filter-opacity
+#[derive(Debug, Clone, PartialEq)]
+pub struct Opacity {
+    proportion: Option<f64>,
+}
+
 /// Parameters for the `sepia()` filter function
 ///
 /// https://www.w3.org/TR/filter-effects/#funcdef-filter-sepia
@@ -64,6 +73,17 @@ fn parse_blur<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseEr
     }))
 }
 
+#[allow(clippy::unnecessary_wraps)]
+fn parse_opacity<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseError<'i>> {
+    let proportion = match parser.try_parse(|p| NumberOrPercentage::parse(p)) {
+        Ok(NumberOrPercentage { value }) if value < 0.0 => None,
+        Ok(NumberOrPercentage { value }) => Some(value.clamp(0.0, 1.0)),
+        Err(_) => None,
+    };
+
+    Ok(FilterFunction::Opacity(Opacity { proportion }))
+}
+
 #[allow(clippy::unnecessary_wraps)]
 fn parse_sepia<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseError<'i>> {
     let proportion = match parser.try_parse(|p| NumberOrPercentage::parse(p)) {
@@ -98,6 +118,34 @@ impl Blur {
     }
 }
 
+impl Opacity {
+    fn to_filter_spec(&self, params: &NormalizeParams) -> FilterSpec {
+        let p = self.proportion.unwrap_or(1.0);
+        let user_space_filter = Filter::default().to_user_space(params);
+
+        let opacity = ResolvedPrimitive {
+            primitive: Primitive::default(),
+            params: PrimitiveParams::ComponentTransfer(component_transfer::ComponentTransfer {
+                functions: crate::filters::component_transfer::Functions {
+                    a: component_transfer::FeFuncA {
+                        function_type: component_transfer::FunctionType::Table,
+                        table_values: vec![0.0, p],
+                        ..component_transfer::FeFuncA::default()
+                    },
+                    ..component_transfer::Functions::default()
+                },
+                ..component_transfer::ComponentTransfer::default()
+            }),
+        }
+        .into_user_space(params);
+
+        FilterSpec {
+            user_space_filter,
+            primitives: vec![opacity],
+        }
+    }
+}
+
 impl Sepia {
     #[rustfmt::skip]
     fn matrix(&self) -> nalgebra::Matrix5<f64> {
@@ -134,13 +182,16 @@ impl Sepia {
 impl Parse for FilterFunction {
     fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, crate::error::ParseError<'i>> {
         let loc = parser.current_source_location();
-
-        if let Ok(func) = parser.try_parse(|p| parse_function(p, "blur", parse_blur)) {
-            return Ok(func);
-        }
-
-        if let Ok(func) = parser.try_parse(|p| parse_function(p, "sepia", parse_sepia)) {
-            return Ok(func);
+        let fns: Vec<(&str, &dyn Fn(&mut Parser<'i, '_>) -> _)> = vec![
+            ("blur", &parse_blur),
+            ("opacity", &parse_opacity),
+            ("sepia", &parse_sepia),
+        ];
+
+        for (filter_name, parse_fn) in fns {
+            if let Ok(func) = parser.try_parse(|p| parse_function(p, filter_name, parse_fn)) {
+                return Ok(func);
+            }
         }
 
         return Err(loc.new_custom_error(ValueErrorKind::parse_error("expected filter function")));
@@ -161,6 +212,7 @@ impl FilterFunction {
 
         match self {
             FilterFunction::Blur(v) => Ok(v.to_filter_spec(&params)),
+            FilterFunction::Opacity(v) => Ok(v.to_filter_spec(&params)),
             FilterFunction::Sepia(v) => Ok(v.to_filter_spec(&params)),
         }
     }
@@ -187,6 +239,21 @@ mod tests {
         );
     }
 
+    #[test]
+    fn parses_opacity() {
+        assert_eq!(
+            FilterFunction::parse_str("opacity()").unwrap(),
+            FilterFunction::Opacity(Opacity { proportion: None })
+        );
+
+        assert_eq!(
+            FilterFunction::parse_str("opacity(50%)").unwrap(),
+            FilterFunction::Opacity(Opacity {
+                proportion: Some(0.50_f32.into()),
+            })
+        );
+    }
+
     #[test]
     fn parses_sepia() {
         assert_eq!(
@@ -229,6 +296,11 @@ mod tests {
         assert!(FilterFunction::parse_str("blur(42 43)").is_err());
     }
 
+    #[test]
+    fn invalid_opacity_yields_error() {
+        assert!(FilterFunction::parse_str("opacity(foo)").is_err());
+    }
+
     #[test]
     fn invalid_sepia_yields_error() {
         assert!(FilterFunction::parse_str("sepia(foo)").is_err());
diff --git a/src/filters/component_transfer.rs b/src/filters/component_transfer.rs
index f879f1f5..799306c8 100644
--- a/src/filters/component_transfer.rs
+++ b/src/filters/component_transfer.rs
@@ -34,9 +34,9 @@ pub struct FeComponentTransfer {
 /// Resolved `feComponentTransfer` primitive for rendering.
 #[derive(Clone, Default)]
 pub struct ComponentTransfer {
-    in1: Input,
-    functions: Functions,
-    color_interpolation_filters: ColorInterpolationFilters,
+    pub in1: Input,
+    pub functions: Functions,
+    pub color_interpolation_filters: ColorInterpolationFilters,
 }
 
 impl SetAttributes for FeComponentTransfer {
@@ -48,7 +48,7 @@ impl SetAttributes for FeComponentTransfer {
 
 /// Pixel components that can be influenced by `feComponentTransfer`.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum Channel {
+pub enum Channel {
     R,
     G,
     B,
@@ -57,7 +57,7 @@ enum Channel {
 
 /// Component transfer function types.
 #[derive(Clone, Debug, PartialEq)]
-enum FunctionType {
+pub enum FunctionType {
     Identity,
     Table,
     Discrete,
@@ -89,11 +89,11 @@ struct FunctionParameters {
 }
 
 #[derive(Clone, Debug, Default, PartialEq)]
-struct Functions {
-    r: FeFuncR,
-    g: FeFuncG,
-    b: FeFuncB,
-    a: FeFuncA,
+pub struct Functions {
+    pub r: FeFuncR,
+    pub g: FeFuncG,
+    pub b: FeFuncB,
+    pub a: FeFuncA,
 }
 
 /// The compute function type.
@@ -156,14 +156,14 @@ macro_rules! func_x {
     ($func_name:ident, $channel:expr) => {
         #[derive(Clone, Debug, PartialEq)]
         pub struct $func_name {
-            channel: Channel,
-            function_type: FunctionType,
-            table_values: Vec<f64>,
-            slope: f64,
-            intercept: f64,
-            amplitude: f64,
-            exponent: f64,
-            offset: f64,
+            pub channel: Channel,
+            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 $func_name {
diff --git a/tests/src/filters.rs b/tests/src/filters.rs
index ca4a8fa4..66fac65a 100644
--- a/tests/src/filters.rs
+++ b/tests/src/filters.rs
@@ -156,6 +156,65 @@ fn blur_filter_func() {
         .evaluate(&output_surf, "blur_filter_func");
 }
 
+#[test]
+fn opacity_filter_func() {
+    // Create an element with a filter function, and compare it to the
+    // supposed equivalent using the <filter> element.
+    let svg = load_svg(
+        br##"<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg"; xmlns:xlink="http://www.w3.org/1999/xlink"; width="400" 
height="400">
+  <rect x="100" y="100" width="200" height="200" fill="lime" filter="opacity(0.75)"/>
+</svg>
+"##,
+    ).unwrap();
+
+    let output_surf = render_document(
+        &svg,
+        SurfaceSize(400, 400),
+        |_| (),
+        cairo::Rectangle {
+            x: 0.0,
+            y: 0.0,
+            width: 400.0,
+            height: 400.0,
+        },
+    )
+    .unwrap();
+
+    let reference = load_svg(
+        br##"<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg"; xmlns:xlink="http://www.w3.org/1999/xlink"; width="400" 
height="400">
+  <defs>
+    <filter id="filter">
+      <feComponentTransfer>
+        <feFuncA type="table" tableValues="0 0.75" />
+      </feComponentTransfer>
+    </filter>
+  </defs>
+
+  <rect x="100" y="100" width="200" height="200" fill="lime" filter="url(#filter)"/>
+</svg>
+"##,
+    ).unwrap();
+
+    let reference_surf = render_document(
+        &reference,
+        SurfaceSize(400, 400),
+        |_| (),
+        cairo::Rectangle {
+            x: 0.0,
+            y: 0.0,
+            width: 400.0,
+            height: 400.0,
+        },
+    )
+    .unwrap();
+
+    Reference::from_surface(reference_surf.into_image_surface().unwrap())
+        .compare(&output_surf)
+        .evaluate(&output_surf, "opacity_filter_func");
+}
+
 #[test]
 fn sepia_filter_func() {
     // Create an element with a filter function, and compare it to the


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]