Merge pull request #3316 from odysseexp/fix_svg_gradients

SVG import: support feBlend filter primitive and transparent backdrops
This commit is contained in:
BFlat
2026-10-21 16:17:05 +07:00
committed by GitHub
3 changed files with 80 additions and 3 deletions
+4
View File
@@ -140,3 +140,7 @@ sync_github_name = false
[people.KaboomFox]
sync_github_name = true
[people.odysseexp]
real_name = "Harley Paiva"
sync_github_name = true
+41 -3
View File
@@ -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<V> {
}
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::<Option<Vec<_>>>()?.into_iter().flat_map(V::layers).collect()),
Kind::Merge(m) => {
let mut layers: Vec<V> = m
.inputs()
.iter()
.map(input)
.collect::<Option<Vec<_>>>()?
.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,
+35
View File
@@ -164,6 +164,41 @@ 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##"<defs><filter id="b"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="3"/></filter></defs><rect x="50" y="30" width="60" height="40" fill="#08f" filter="url(#b)"/>"##,
);
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##"<mask id="m"><circle cx="50" cy="50" r="40" fill="#fff"/></mask>
<g mask="url(#m)">
<g filter="url(#b1)"><circle cx="30" cy="30" r="20" fill="#ff0"/></g>
<g filter="url(#b2)"><circle cx="70" cy="70" r="20" fill="#00f"/></g>
</g>
<defs>
<filter id="b1"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="2"/></filter>
<filter id="b2"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="4"/></filter>
</defs>"##;
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]);
}
#[test]
fn a_gaussian_blur_becomes_the_effect_with_its_radius() {
let (d, w) = open(