diff --git a/CHANGELOG.md b/CHANGELOG.md index 72121491a..8f69c9d5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ This changelog also contains important changes in dependencies. - Incorrect transform order on the outermost `svg` element with a `viewBox`. (#899) - Relative positioning of right-to-left text with `dx` and `dy`, including text on a path and text decorations. (#1093) - `text-anchor` now uses the actual extent of a text chunk, which `dx` and `textLength` can change. (#1145) +- Filters inside rotated or skewed groups are now applied in the user space of the group. + This fixes the clipping to filter regions and subregions, as well as the direction of + offsets, blurs, turbulence and tiles, and the rendering of `feImage`. (#949) ## [0.48.1] 2026-08-02 diff --git a/crates/resvg/src/render.rs b/crates/resvg/src/render.rs index c879183f1..ae9a87f6a 100644 --- a/crates/resvg/src/render.rs +++ b/crates/resvg/src/render.rs @@ -59,6 +59,50 @@ fn render_group( return Some(()); } + let rest = if group.filters().is_empty() { + tiny_skia::Transform::identity() + } else { + let sx = transform.sx.hypot(transform.ky); + let sy = transform.kx.hypot(transform.sy); + tiny_skia::Transform::from_row( + transform.sx / sx, + transform.ky / sx, + transform.kx / sy, + transform.sy / sy, + 0.0, + 0.0, + ) + }; + let inv_rest = rest.invert()?; + + let max_bbox = ctx.max_bbox.to_rect().transform(inv_rest)?.round_out()?; + let (layer, ibbox) = + render_layer(group, &Context { max_bbox }, inv_rest.pre_concat(transform))?; + + let paint = tiny_skia::Paint { + shader: tiny_skia::Pattern::new( + layer.as_ref(), + tiny_skia::SpreadMode::Pad, + tiny_skia::FilterQuality::Bilinear, + group.opacity().get(), + tiny_skia::Transform::from_translate(ibbox.x() as f32, ibbox.y() as f32), + ), + blend_mode: convert_blend_mode(group.blend_mode()), + anti_alias: true, + ..tiny_skia::Paint::default() + }; + let path = tiny_skia::PathBuilder::from_rect(ibbox.to_rect()); + pixmap.fill_path(&path, &paint, tiny_skia::FillRule::Winding, rest, None); + + Some(()) +} + +/// Renders an isolated group into a new layer, returning it and its position. +fn render_layer( + group: &usvg::Group, + ctx: &Context, + transform: tiny_skia::Transform, +) -> Option<(tiny_skia::Pixmap, tiny_skia::IntRect)> { let bbox = group.layer_bounding_box().transform(transform)?; let mut ibbox = if group.filters().is_empty() { @@ -124,22 +168,7 @@ fn render_group( crate::mask::apply(mask, ctx, transform, &mut sub_pixmap); } - let paint = tiny_skia::PixmapPaint { - opacity: group.opacity().get(), - blend_mode: convert_blend_mode(group.blend_mode()), - quality: tiny_skia::FilterQuality::Nearest, - }; - - pixmap.draw_pixmap( - ibbox.x(), - ibbox.y(), - sub_pixmap.as_ref(), - &paint, - tiny_skia::Transform::identity(), - None, - ); - - Some(()) + Some((sub_pixmap, ibbox)) } pub fn convert_blend_mode(mode: usvg::BlendMode) -> tiny_skia::BlendMode { diff --git a/crates/resvg/tests/extra/filter-with-transform-on-shape.png b/crates/resvg/tests/extra/filter-with-transform-on-shape.png index c8de330cd..219a9c482 100644 Binary files a/crates/resvg/tests/extra/filter-with-transform-on-shape.png and b/crates/resvg/tests/extra/filter-with-transform-on-shape.png differ diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs index adb80acdc..b89f30762 100644 --- a/crates/resvg/tests/integration/render.rs +++ b/crates/resvg/tests/integration/render.rs @@ -348,6 +348,7 @@ use crate::render; #[test] fn filters_filter_with_multiple_transforms_1() { assert_eq!(render("tests/filters/filter/with-multiple-transforms-1"), 0); } #[test] fn filters_filter_with_multiple_transforms_2() { assert_eq!(render("tests/filters/filter/with-multiple-transforms-2"), 0); } #[test] fn filters_filter_with_region_and_filterUnits_eq_userSpaceOnUse() { assert_eq!(render("tests/filters/filter/with-region-and-filterUnits=userSpaceOnUse"), 0); } +#[test] fn filters_filter_with_region_and_skew_on_intermediate_result() { assert_eq!(render("tests/filters/filter/with-region-and-skew-on-intermediate-result"), 0); } #[test] fn filters_filter_with_region_and_subregion() { assert_eq!(render("tests/filters/filter/with-region-and-subregion"), 0); } #[test] fn filters_filter_with_region_outside_the_canvas() { assert_eq!(render("tests/filters/filter/with-region-outside-the-canvas"), 0); } #[test] fn filters_filter_with_region_outside_the_viewbox() { assert_eq!(render("tests/filters/filter/with-region-outside-the-viewbox"), 0); } @@ -355,6 +356,7 @@ use crate::render; #[test] fn filters_filter_with_subregion_1() { assert_eq!(render("tests/filters/filter/with-subregion-1"), 0); } #[test] fn filters_filter_with_subregion_2() { assert_eq!(render("tests/filters/filter/with-subregion-2"), 0); } #[test] fn filters_filter_with_subregion_3() { assert_eq!(render("tests/filters/filter/with-subregion-3"), 0); } +#[test] fn filters_filter_with_subregion_and_rotation() { assert_eq!(render("tests/filters/filter/with-subregion-and-rotation"), 0); } #[test] fn filters_filter_with_transform_outside_of_canvas() { assert_eq!(render("tests/filters/filter/with-transform-outside-of-canvas"), 0); } #[test] fn filters_filter_without_region_and_filterUnits_eq_userSpaceOnUse() { assert_eq!(render("tests/filters/filter/without-region-and-filterUnits=userSpaceOnUse"), 0); } #[test] fn filters_filter_zero_sized_subregion() { assert_eq!(render("tests/filters/filter/zero-sized-subregion"), 0); } diff --git a/crates/resvg/tests/tests/filters/feDiffuseLighting/complex-transform.png b/crates/resvg/tests/tests/filters/feDiffuseLighting/complex-transform.png index c2d85fef7..c67a6ff44 100644 Binary files a/crates/resvg/tests/tests/filters/feDiffuseLighting/complex-transform.png and b/crates/resvg/tests/tests/filters/feDiffuseLighting/complex-transform.png differ diff --git a/crates/resvg/tests/tests/filters/feFlood/complex-transform.png b/crates/resvg/tests/tests/filters/feFlood/complex-transform.png index da4fe235a..1137ef330 100644 Binary files a/crates/resvg/tests/tests/filters/feFlood/complex-transform.png and b/crates/resvg/tests/tests/filters/feFlood/complex-transform.png differ diff --git a/crates/resvg/tests/tests/filters/feGaussianBlur/complex-transform.png b/crates/resvg/tests/tests/filters/feGaussianBlur/complex-transform.png index 35405b341..ef6ceda36 100644 Binary files a/crates/resvg/tests/tests/filters/feGaussianBlur/complex-transform.png and b/crates/resvg/tests/tests/filters/feGaussianBlur/complex-transform.png differ diff --git a/crates/resvg/tests/tests/filters/feImage/link-on-an-element-with-complex-transform.png b/crates/resvg/tests/tests/filters/feImage/link-on-an-element-with-complex-transform.png index dfbc0e20d..3dbe8db1d 100644 Binary files a/crates/resvg/tests/tests/filters/feImage/link-on-an-element-with-complex-transform.png and b/crates/resvg/tests/tests/filters/feImage/link-on-an-element-with-complex-transform.png differ diff --git a/crates/resvg/tests/tests/filters/feImage/with-subregion-5.png b/crates/resvg/tests/tests/filters/feImage/with-subregion-5.png index 419b907ee..407624697 100644 Binary files a/crates/resvg/tests/tests/filters/feImage/with-subregion-5.png and b/crates/resvg/tests/tests/filters/feImage/with-subregion-5.png differ diff --git a/crates/resvg/tests/tests/filters/feMerge/complex-transform.png b/crates/resvg/tests/tests/filters/feMerge/complex-transform.png index 6d31c5fa3..95d5d112c 100644 Binary files a/crates/resvg/tests/tests/filters/feMerge/complex-transform.png and b/crates/resvg/tests/tests/filters/feMerge/complex-transform.png differ diff --git a/crates/resvg/tests/tests/filters/feOffset/complex-transform.png b/crates/resvg/tests/tests/filters/feOffset/complex-transform.png index 043c0f81d..a6cf6d7f7 100644 Binary files a/crates/resvg/tests/tests/filters/feOffset/complex-transform.png and b/crates/resvg/tests/tests/filters/feOffset/complex-transform.png differ diff --git a/crates/resvg/tests/tests/filters/fePointLight/complex-transform.png b/crates/resvg/tests/tests/filters/fePointLight/complex-transform.png index 81c0075c2..1f16d9fcd 100644 Binary files a/crates/resvg/tests/tests/filters/fePointLight/complex-transform.png and b/crates/resvg/tests/tests/filters/fePointLight/complex-transform.png differ diff --git a/crates/resvg/tests/tests/filters/feSpotLight/complex-transform.png b/crates/resvg/tests/tests/filters/feSpotLight/complex-transform.png index ee04c567c..0f53a3361 100644 Binary files a/crates/resvg/tests/tests/filters/feSpotLight/complex-transform.png and b/crates/resvg/tests/tests/filters/feSpotLight/complex-transform.png differ diff --git a/crates/resvg/tests/tests/filters/feTile/complex-transform.png b/crates/resvg/tests/tests/filters/feTile/complex-transform.png index 8c4d02613..e38988b05 100644 Binary files a/crates/resvg/tests/tests/filters/feTile/complex-transform.png and b/crates/resvg/tests/tests/filters/feTile/complex-transform.png differ diff --git a/crates/resvg/tests/tests/filters/feTurbulence/complex-transform.png b/crates/resvg/tests/tests/filters/feTurbulence/complex-transform.png index 77a13ed0a..073df2678 100644 Binary files a/crates/resvg/tests/tests/filters/feTurbulence/complex-transform.png and b/crates/resvg/tests/tests/filters/feTurbulence/complex-transform.png differ diff --git a/crates/resvg/tests/tests/filters/filter/transform-on-shape-with-filter-region.png b/crates/resvg/tests/tests/filters/filter/transform-on-shape-with-filter-region.png index 7202dabc4..d13fec6de 100644 Binary files a/crates/resvg/tests/tests/filters/filter/transform-on-shape-with-filter-region.png and b/crates/resvg/tests/tests/filters/filter/transform-on-shape-with-filter-region.png differ diff --git a/crates/resvg/tests/tests/filters/filter/transform-on-shape.png b/crates/resvg/tests/tests/filters/filter/transform-on-shape.png index 9e80674fb..415f356f2 100644 Binary files a/crates/resvg/tests/tests/filters/filter/transform-on-shape.png and b/crates/resvg/tests/tests/filters/filter/transform-on-shape.png differ diff --git a/crates/resvg/tests/tests/filters/filter/with-multiple-transforms-1.png b/crates/resvg/tests/tests/filters/filter/with-multiple-transforms-1.png index 4b0e6b89c..97f291804 100644 Binary files a/crates/resvg/tests/tests/filters/filter/with-multiple-transforms-1.png and b/crates/resvg/tests/tests/filters/filter/with-multiple-transforms-1.png differ diff --git a/crates/resvg/tests/tests/filters/filter/with-region-and-skew-on-intermediate-result.png b/crates/resvg/tests/tests/filters/filter/with-region-and-skew-on-intermediate-result.png new file mode 100644 index 000000000..578f51a0c Binary files /dev/null and b/crates/resvg/tests/tests/filters/filter/with-region-and-skew-on-intermediate-result.png differ diff --git a/crates/resvg/tests/tests/filters/filter/with-region-and-skew-on-intermediate-result.svg b/crates/resvg/tests/tests/filters/filter/with-region-and-skew-on-intermediate-result.svg new file mode 100644 index 000000000..de94a6f32 --- /dev/null +++ b/crates/resvg/tests/tests/filters/filter/with-region-and-skew-on-intermediate-result.svg @@ -0,0 +1,20 @@ + + With region and skew on an intermediate result. No red should be visible. + + + + + + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/filters/filter/with-subregion-and-rotation.png b/crates/resvg/tests/tests/filters/filter/with-subregion-and-rotation.png new file mode 100644 index 000000000..99c3ec664 Binary files /dev/null and b/crates/resvg/tests/tests/filters/filter/with-subregion-and-rotation.png differ diff --git a/crates/resvg/tests/tests/filters/filter/with-subregion-and-rotation.svg b/crates/resvg/tests/tests/filters/filter/with-subregion-and-rotation.svg new file mode 100644 index 000000000..ff98b8246 --- /dev/null +++ b/crates/resvg/tests/tests/filters/filter/with-subregion-and-rotation.svg @@ -0,0 +1,14 @@ + + With subregion and rotation. No red should be visible. + + + + + + + + + + + +