From cc0e7c4c676210be5a4a05c831c0fa2818dfa3d3 Mon Sep 17 00:00:00 2001 From: Harley Hawk Date: Wed, 21 Oct 2026 01:03:31 -0300 Subject: [PATCH] SVG import: support feBlend filter primitive and transparent backdrops --- crates/svg/src/import/fx.rs | 44 ++++++++++++++++++++++++++-- crates/svg/tests/import_live.rs | 51 +++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/crates/svg/src/import/fx.rs b/crates/svg/src/import/fx.rs index 2f94537f5..2808c5e2f 100644 --- a/crates/svg/src/import/fx.rs +++ b/crates/svg/src/import/fx.rs @@ -2,7 +2,8 @@ //! source, its silhouette blurred, moved or inverted, a colour flood, a coloured silhouette…), //! and the result recognised as one of the effects the canvas draws: Gaussian Blur, Drop Shadow, //! Outer Glow, Inner Glow or Feather. That covers `feGaussianBlur`, `feDropShadow`, the usual -//! shadow chains (offset, blur, flood or color matrix, composite, merge), the object moved and +//! shadow chains (offset, blur, flood or color matrix, composite, merge), blur and shadow graphs +//! with `feBlend` and transparent backdrops (as exported by Figma and others), the object moved and //! blurred with its colors scaled by `feComponentTransfer` (a drop shadow with Darkness) and what //! our export writes, so export → import keeps the effects. @@ -197,15 +198,52 @@ fn eval(f: &Filter, k: f64) -> Option { } Kind::Offset(o) => input(o.input())?.offset(o.dx() as f64, o.dy() as f64)?, Kind::Flood(fl) => V::Flood(color(fl.color()), fl.opacity().get()), + Kind::Blend(b) => { + let (a, b_in) = (input(b.input1())?, input(b.input2())?); + let transparent = |v: &V| matches!(v, V::Flood(_, o) if *o <= 1e-6); + if transparent(&b_in) { + a + } else if transparent(&a) { + b_in + } else if b.mode() == usvg::BlendMode::Normal { + V::Stack(b_in.layers().into_iter().chain(a.layers()).collect()) + } else { + return None; + } + } Kind::Composite(c) => { let (a, b) = (input(c.input1())?, input(c.input2())?); + let transparent = |v: &V| matches!(v, V::Flood(_, o) if *o <= 1e-6); match c.operator() { CompositeOperator::In => a.inside(b)?, - CompositeOperator::Over => V::Stack(b.layers().into_iter().chain(a.layers()).collect()), + CompositeOperator::Over => { + if transparent(&b) { + a + } else if transparent(&a) { + b + } else { + V::Stack(b.layers().into_iter().chain(a.layers()).collect()) + } + } _ => return None, } } - Kind::Merge(m) => V::Stack(m.inputs().iter().map(input).collect::>>()?.into_iter().flat_map(V::layers).collect()), + Kind::Merge(m) => { + let mut layers: Vec = m + .inputs() + .iter() + .map(input) + .collect::>>()? + .into_iter() + .flat_map(V::layers) + .filter(|v| !matches!(v, V::Flood(_, o) if *o <= 1e-6)) + .collect(); + match layers.len() { + 0 => return None, + 1 => layers.remove(0), + _ => V::Stack(layers), + } + } Kind::ColorMatrix(c) => match c.kind() { ColorMatrixKind::Matrix(m) => input(c.input())?.color_matrix(m)?, _ => return None, diff --git a/crates/svg/tests/import_live.rs b/crates/svg/tests/import_live.rs index 08e3a3035..355470468 100644 --- a/crates/svg/tests/import_live.rs +++ b/crates/svg/tests/import_live.rs @@ -164,6 +164,57 @@ fn an_instances_shadow_filter_and_the_art_export_cover_its_symbols_art() { assert!((*xf * Point::ORIGIN).distance(Point::ORIGIN) < 1e-9, "{svg}"); } +#[test] +fn a_figma_blurred_shape_filter_becomes_gaussian_blur() { + let (d, w) = open( + r##""##, + ); + assert!(w.is_empty(), "{w:?}"); + let fx = effects(&d); + assert_eq!(fx.len(), 1); + assert_eq!((fx[0].id.as_str(), &fx[0].params), ("blur.gaussian", &json!({"radius": 6.0}))); +} + +#[test] +fn figma_blurred_shapes_inside_mask_import_with_effects_and_no_warnings() { + let body = r##" + + + + + + + + "##; + let (d, w) = open(body); + assert!(w.is_empty(), "{w:?}"); + let mut blurs = vec![]; + d.walk(|n| { + for e in &n.appearance.effects { + if e.id == "blur.gaussian" { + blurs.push(e.params["radius"].as_f64().unwrap()); + } + } + }); + assert_eq!(blurs, vec![4.0, 8.0]); + + let path = std::path::Path::new("/home/deck/Pictures/ícones e figurinhas/antigravity-color.svg"); + if path.exists() { + let svg = std::fs::read_to_string(path).unwrap(); + let (d, w) = import_with_report(&svg).unwrap(); + assert!(w.is_empty(), "{w:?}"); + let mut count = 0; + d.walk(|n| { + for e in &n.appearance.effects { + if e.id == "blur.gaussian" { + count += 1; + } + } + }); + assert_eq!(count, 11); + } +} + #[test] fn a_gaussian_blur_becomes_the_effect_with_its_radius() { let (d, w) = open(