mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
SVG import: support feBlend filter primitive and transparent backdrops
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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##"<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]);
|
||||
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user