[librsvg: 2/4] Implement saturate filter function




commit 638d5a919b8898316ebcbc82b4e1d73067c30aa5
Author: John Ledbetter <john ledbetter gmail com>
Date:   Wed May 12 10:34:11 2021 -0400

    Implement saturate filter function
    
    `saturate()` takes a single optional `number-percentage` argument,
    similar to `sepia()`; the value is allowed to be any positive number;
    values greater than 1 increase the saturation, while numbers less than
    1 desaturate.
    
    Saturate is implemented as a `FeColorMatrix`.
    
    See:
    * https://www.w3.org/TR/filter-effects/#funcdef-filter-saturate
    * https://www.w3.org/TR/filter-effects/#saturateEquivalent

 src/filter_func.rs   | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 tests/src/filters.rs | 59 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 134 insertions(+)
---
diff --git a/src/filter_func.rs b/src/filter_func.rs
index ca7bc7c6..d6c835d3 100644
--- a/src/filter_func.rs
+++ b/src/filter_func.rs
@@ -19,6 +19,7 @@ use crate::{drawing_ctx::DrawingCtx, filters::component_transfer};
 pub enum FilterFunction {
     Blur(Blur),
     Opacity(Opacity),
+    Saturate(Saturate),
     Sepia(Sepia),
 }
 
@@ -38,6 +39,14 @@ pub struct Opacity {
     proportion: Option<f64>,
 }
 
+/// Parameters for the `saturate()` filter function
+///
+/// https://www.w3.org/TR/filter-effects/#funcdef-filter-saturate
+#[derive(Debug, Clone, PartialEq)]
+pub struct Saturate {
+    proportion: Option<f64>,
+}
+
 /// Parameters for the `sepia()` filter function
 ///
 /// https://www.w3.org/TR/filter-effects/#funcdef-filter-sepia
@@ -84,6 +93,17 @@ fn parse_opacity<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, Pars
     Ok(FilterFunction::Opacity(Opacity { proportion }))
 }
 
+#[allow(clippy::unnecessary_wraps)]
+fn parse_saturate<'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),
+        Err(_) => None,
+    };
+
+    Ok(FilterFunction::Saturate(Saturate { 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)) {
@@ -146,6 +166,39 @@ impl Opacity {
     }
 }
 
+impl Saturate {
+    #[rustfmt::skip]
+    fn matrix(&self) -> nalgebra::Matrix5<f64> {
+        let p = self.proportion.unwrap_or(1.0);
+
+        nalgebra::Matrix5::new(
+            0.213 + 0.787 * p, 0.715 - 0.715 * p, 0.072 - 0.072 * p, 0.0, 0.0,
+            0.213 - 0.213 * p, 0.715 + 0.285 * p, 0.072 - 0.072 * p, 0.0, 0.0,
+            0.213 - 0.213 * p, 0.715 - 0.715 * p, 0.072 + 0.928 * p, 0.0, 0.0,
+            0.0,               0.0,               0.0,               1.0, 0.0,
+            0.0,               0.0,               0.0,               0.0, 1.0,
+        )
+    }
+
+    fn to_filter_spec(&self, params: &NormalizeParams) -> FilterSpec {
+        let user_space_filter = Filter::default().to_user_space(params);
+
+        let saturate = ResolvedPrimitive {
+            primitive: Primitive::default(),
+            params: PrimitiveParams::ColorMatrix(ColorMatrix {
+                matrix: self.matrix(),
+                ..ColorMatrix::default()
+            }),
+        }
+        .into_user_space(params);
+
+        FilterSpec {
+            user_space_filter,
+            primitives: vec![saturate],
+        }
+    }
+}
+
 impl Sepia {
     #[rustfmt::skip]
     fn matrix(&self) -> nalgebra::Matrix5<f64> {
@@ -185,6 +238,7 @@ impl Parse for FilterFunction {
         let fns: Vec<(&str, &dyn Fn(&mut Parser<'i, '_>) -> _)> = vec![
             ("blur", &parse_blur),
             ("opacity", &parse_opacity),
+            ("saturate", &parse_saturate),
             ("sepia", &parse_sepia),
         ];
 
@@ -213,6 +267,7 @@ impl FilterFunction {
         match self {
             FilterFunction::Blur(v) => Ok(v.to_filter_spec(&params)),
             FilterFunction::Opacity(v) => Ok(v.to_filter_spec(&params)),
+            FilterFunction::Saturate(v) => Ok(v.to_filter_spec(&params)),
             FilterFunction::Sepia(v) => Ok(v.to_filter_spec(&params)),
         }
     }
@@ -254,6 +309,21 @@ mod tests {
         );
     }
 
+    #[test]
+    fn parses_saturate() {
+        assert_eq!(
+            FilterFunction::parse_str("saturate()").unwrap(),
+            FilterFunction::Saturate(Saturate { proportion: None })
+        );
+
+        assert_eq!(
+            FilterFunction::parse_str("saturate(50%)").unwrap(),
+            FilterFunction::Saturate(Saturate {
+                proportion: Some(0.50_f32.into()),
+            })
+        );
+    }
+
     #[test]
     fn parses_sepia() {
         assert_eq!(
@@ -301,6 +371,11 @@ mod tests {
         assert!(FilterFunction::parse_str("opacity(foo)").is_err());
     }
 
+    #[test]
+    fn invalid_saturate_yields_error() {
+        assert!(FilterFunction::parse_str("saturate(foo)").is_err());
+    }
+
     #[test]
     fn invalid_sepia_yields_error() {
         assert!(FilterFunction::parse_str("sepia(foo)").is_err());
diff --git a/tests/src/filters.rs b/tests/src/filters.rs
index 66fac65a..61e89927 100644
--- a/tests/src/filters.rs
+++ b/tests/src/filters.rs
@@ -163,6 +163,7 @@ fn opacity_filter_func() {
     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="red"/>
   <rect x="100" y="100" width="200" height="200" fill="lime" filter="opacity(0.75)"/>
 </svg>
 "##,
@@ -192,6 +193,7 @@ fn opacity_filter_func() {
     </filter>
   </defs>
 
+  <rect x="100" y="100" width="200" height="200" fill="red"/>
   <rect x="100" y="100" width="200" height="200" fill="lime" filter="url(#filter)"/>
 </svg>
 "##,
@@ -215,6 +217,63 @@ fn opacity_filter_func() {
         .evaluate(&output_surf, "opacity_filter_func");
 }
 
+#[test]
+fn saturate_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="saturate(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">
+      <feColorMatrix type="saturate" values="0.75" />
+    </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, "saturate_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]