[librsvg: 1/9] Implement grayscale filter function




commit bab15965189812a2b120c575aca6166cff3c7578
Author: John Ledbetter <john ledbetter gmail com>
Date:   Sat May 15 13:34:51 2021 -0400

    Implement grayscale filter function
    
    The grayscale function takes a single optional `<number-percentage>`
    argument which is the proportion of the conversion.
    
    The default value is 1, which is completely grayscale.
    
    The color matrix operation to convert to grayscale is the same as a
    saturation conversion with an argument of (1 - x).
    
    * https://www.w3.org/TR/filter-effects/#funcdef-filter-grayscale
    * https://www.w3.org/TR/filter-effects/#grayscaleEquivalent

 src/filter_func.rs   | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 tests/src/filters.rs | 20 +++++++++++++++++++
 2 files changed, 75 insertions(+)
---
diff --git a/src/filter_func.rs b/src/filter_func.rs
index d6c835d3..8ce3725e 100644
--- a/src/filter_func.rs
+++ b/src/filter_func.rs
@@ -18,6 +18,7 @@ use crate::{drawing_ctx::DrawingCtx, filters::component_transfer};
 #[derive(Debug, Clone, PartialEq)]
 pub enum FilterFunction {
     Blur(Blur),
+    Grayscale(Grayscale),
     Opacity(Opacity),
     Saturate(Saturate),
     Sepia(Sepia),
@@ -31,6 +32,14 @@ pub struct Blur {
     std_deviation: Option<Length<Both>>,
 }
 
+/// Parameters for the `grayscale()` filter function
+///
+/// https://www.w3.org/TR/filter-effects/#funcdef-filter-grayscale
+#[derive(Debug, Clone, PartialEq)]
+pub struct Grayscale {
+    proportion: Option<f64>,
+}
+
 /// Parameters for the `opacity()` filter function
 ///
 /// https://www.w3.org/TR/filter-effects/#funcdef-filter-opacity
@@ -82,6 +91,17 @@ fn parse_blur<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseEr
     }))
 }
 
+#[allow(clippy::unnecessary_wraps)]
+fn parse_grayscale<'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::Grayscale(Grayscale { proportion }))
+}
+
 #[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)) {
@@ -138,6 +158,19 @@ impl Blur {
     }
 }
 
+impl Grayscale {
+    fn to_filter_spec(&self, params: &NormalizeParams) -> FilterSpec {
+        // grayscale is implemented as the inverse of a saturate operation,
+        // with the input clamped to the range [0, 1] by the parser.
+        let p = 1.0 - self.proportion.unwrap_or(1.0);
+        let saturate = Saturate {
+            proportion: Some(p),
+        };
+
+        saturate.to_filter_spec(params)
+    }
+}
+
 impl Opacity {
     fn to_filter_spec(&self, params: &NormalizeParams) -> FilterSpec {
         let p = self.proportion.unwrap_or(1.0);
@@ -237,6 +270,7 @@ impl Parse for FilterFunction {
         let loc = parser.current_source_location();
         let fns: Vec<(&str, &dyn Fn(&mut Parser<'i, '_>) -> _)> = vec![
             ("blur", &parse_blur),
+            ("grayscale", &parse_grayscale),
             ("opacity", &parse_opacity),
             ("saturate", &parse_saturate),
             ("sepia", &parse_sepia),
@@ -266,6 +300,7 @@ impl FilterFunction {
 
         match self {
             FilterFunction::Blur(v) => Ok(v.to_filter_spec(&params)),
+            FilterFunction::Grayscale(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)),
@@ -294,6 +329,21 @@ mod tests {
         );
     }
 
+    #[test]
+    fn parses_grayscale() {
+        assert_eq!(
+            FilterFunction::parse_str("grayscale()").unwrap(),
+            FilterFunction::Grayscale(Grayscale { proportion: None })
+        );
+
+        assert_eq!(
+            FilterFunction::parse_str("grayscale(50%)").unwrap(),
+            FilterFunction::Grayscale(Grayscale {
+                proportion: Some(0.50_f32.into()),
+            })
+        );
+    }
+
     #[test]
     fn parses_opacity() {
         assert_eq!(
@@ -366,6 +416,11 @@ mod tests {
         assert!(FilterFunction::parse_str("blur(42 43)").is_err());
     }
 
+    #[test]
+    fn invalid_grayscale_yields_error() {
+        assert!(FilterFunction::parse_str("grayscale(foo)").is_err());
+    }
+
     #[test]
     fn invalid_opacity_yields_error() {
         assert!(FilterFunction::parse_str("opacity(foo)").is_err());
diff --git a/tests/src/filters.rs b/tests/src/filters.rs
index c283c74e..868bbed4 100644
--- a/tests/src/filters.rs
+++ b/tests/src/filters.rs
@@ -158,6 +158,26 @@ test_compare_render_output!(
 "##,
 );
 
+test_compare_render_output!(
+    grayscale_filter_func,
+    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="grayscale(0.75)"/>
+</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.25" />
+    </filter>
+  </defs>
+
+  <rect x="100" y="100" width="200" height="200" fill="lime" filter="url(#filter)"/>
+</svg>
+"##,
+);
+
 test_compare_render_output!(
 opacity_filter_func,
     br##"<?xml version="1.0" encoding="UTF-8"?>


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