From 5bcece6208d9898f01fabbfcb1734306b7623795 Mon Sep 17 00:00:00 2001 From: Teo Mal <76534902+teolines@users.noreply.github.com> Date: Mon, 28 Sep 2026 06:39:50 +0000 Subject: [PATCH] Keep filter inputs aligned with the filter region Filter inputs could retain the full layer dimensions while outputs used the smaller filter region, causing out-of-bounds writes. Crop the source to the working region, adjust its coordinate origin, and place the result back at the original offset. This also preserves clipping for chained filters with different regions. Fixes #1141. --- CHANGELOG.md | 1 + crates/resvg/src/filter/mod.rs | 29 +++++++++++++----- crates/resvg/tests/integration/render.rs | 2 ++ .../tiny-element-with-spot-light.png | Bin 0 -> 355 bytes .../tiny-element-with-spot-light.svg | 14 +++++++++ .../two-urls-with-different-regions.png | Bin 0 -> 1150 bytes .../two-urls-with-different-regions.svg | 16 ++++++++++ 7 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 crates/resvg/tests/tests/filters/feDiffuseLighting/tiny-element-with-spot-light.png create mode 100644 crates/resvg/tests/tests/filters/feDiffuseLighting/tiny-element-with-spot-light.svg create mode 100644 crates/resvg/tests/tests/filters/filter-functions/two-urls-with-different-regions.png create mode 100644 crates/resvg/tests/tests/filters/filter-functions/two-urls-with-different-regions.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 30a71c972..867c8ad82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This changelog also contains important changes in dependencies. - `dominant-baseline` is now correctly inherited by nested `` elements, so a nested span stays on the same baseline as its siblings. (#864) +- Panics and incorrect clipping when a filter region is smaller than its input. (#1141) ## [0.48.1] 2026-08-02 diff --git a/crates/resvg/src/filter/mod.rs b/crates/resvg/src/filter/mod.rs index 30f8680c5..8b6fd138e 100644 --- a/crates/resvg/src/filter/mod.rs +++ b/crates/resvg/src/filter/mod.rs @@ -338,7 +338,7 @@ pub fn apply( source: &mut tiny_skia::Pixmap, ) { let result = apply_inner(filter, ts, source); - let result = result.and_then(|image| apply_to_canvas(image, source)); + let result = result.and_then(|(image, region)| apply_to_canvas(image, region, source)); // Clear on error. if result.is_err() { @@ -357,8 +357,8 @@ pub fn apply( fn apply_inner( filter: &usvg::filter::Filter, ts: usvg::Transform, - source: &mut tiny_skia::Pixmap, -) -> Result { + source: &tiny_skia::Pixmap, +) -> Result<(Image, IntRect), Error> { let region = filter .rect() .transform(ts) @@ -372,6 +372,17 @@ fn apply_inner( IntRect::from_xywh(0, 0, source.width(), source.height()).ok_or(Error::InvalidRegion)?; let region = crate::geom::fit_to_rect(region, source_rect).ok_or(Error::InvalidRegion)?; + let cropped_source = if region != source_rect { + // TODO: Avoid copying? + Some(source.copy_region(region)?) + } else { + None + }; + let source = cropped_source.as_ref().unwrap_or(source); + let output_region = region; + let ts = ts.post_translate(-region.x() as f32, -region.y() as f32); + let region = region.translate_to(0, 0).ok_or(Error::InvalidRegion)?; + let mut results: Vec = Vec::new(); for primitive in filter.primitives() { @@ -514,7 +525,7 @@ fn apply_inner( } if let Some(res) = results.pop() { - Ok(res.image) + Ok((res.image, output_region)) } else { Err(Error::NoResults) } @@ -1097,13 +1108,17 @@ fn transform_light_source( source } -fn apply_to_canvas(input: Image, pixmap: &mut tiny_skia::Pixmap) -> Result<(), Error> { +fn apply_to_canvas( + input: Image, + region: IntRect, + pixmap: &mut tiny_skia::Pixmap, +) -> Result<(), Error> { let input = input.into_color_space(usvg::filter::ColorInterpolation::SRGB)?; pixmap.fill(tiny_skia::Color::TRANSPARENT); pixmap.draw_pixmap( - 0, - 0, + region.x(), + region.y(), input.as_ref().as_ref(), &tiny_skia::PixmapPaint::default(), tiny_skia::Transform::identity(), diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs index e55675256..a556ff5dd 100644 --- a/crates/resvg/tests/integration/render.rs +++ b/crates/resvg/tests/integration/render.rs @@ -142,6 +142,7 @@ use crate::render; #[test] fn filters_feDiffuseLighting_surfaceScale_eq_0() { assert_eq!(render("tests/filters/feDiffuseLighting/surfaceScale=0"), 0); } #[test] fn filters_feDiffuseLighting_surfaceScale_eq_1_33() { assert_eq!(render("tests/filters/feDiffuseLighting/surfaceScale=1.33"), 0); } #[test] fn filters_feDiffuseLighting_surfaceScale_eq_5() { assert_eq!(render("tests/filters/feDiffuseLighting/surfaceScale=5"), 0); } +#[test] fn filters_feDiffuseLighting_tiny_element_with_spot_light() { assert_eq!(render("tests/filters/feDiffuseLighting/tiny-element-with-spot-light"), 0); } #[test] fn filters_feDisplacementMap_simple_case() { assert_eq!(render("tests/filters/feDisplacementMap/simple-case"), 0); } #[test] fn filters_feDistantLight_default_attributes() { assert_eq!(render("tests/filters/feDistantLight/default-attributes"), 0); } #[test] fn filters_feDistantLight_negative_azimuth_and_elevation() { assert_eq!(render("tests/filters/feDistantLight/negative-azimuth-and-elevation"), 0); } @@ -393,6 +394,7 @@ use crate::render; #[test] fn filters_filter_functions_one_invalid_url_in_list() { assert_eq!(render("tests/filters/filter-functions/one-invalid-url-in-list"), 0); } #[test] fn filters_filter_functions_two_drop_shadow_function() { assert_eq!(render("tests/filters/filter-functions/two-drop-shadow-function"), 0); } #[test] fn filters_filter_functions_two_exact_urls() { assert_eq!(render("tests/filters/filter-functions/two-exact-urls"), 0); } +#[test] fn filters_filter_functions_two_urls_with_different_regions() { assert_eq!(render("tests/filters/filter-functions/two-urls-with-different-regions"), 0); } #[test] fn filters_filter_functions_two_urls() { assert_eq!(render("tests/filters/filter-functions/two-urls"), 0); } #[test] fn filters_filter_functions_url_and_grayscale() { assert_eq!(render("tests/filters/filter-functions/url-and-grayscale"), 0); } #[test] fn filters_flood_color_hsla_color() { assert_eq!(render("tests/filters/flood-color/hsla-color"), 0); } diff --git a/crates/resvg/tests/tests/filters/feDiffuseLighting/tiny-element-with-spot-light.png b/crates/resvg/tests/tests/filters/feDiffuseLighting/tiny-element-with-spot-light.png new file mode 100644 index 0000000000000000000000000000000000000000..08c03cd813ca0910c19663652251fb6b7f780709 GIT binary patch literal 355 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&7G|JGckpC4ASD{$6XFV_F@X>x!xEqShYp})K=bWMN_hmA0i*jn%jyCl%^#m`tGw4qdK@&J^i)h8l8xK bZLV$qTn!VS+&!PW928WZu6{1-oD!M + Tiny element with a spot light + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/filters/filter-functions/two-urls-with-different-regions.png b/crates/resvg/tests/tests/filters/filter-functions/two-urls-with-different-regions.png new file mode 100644 index 0000000000000000000000000000000000000000..9a7732c65f293a3c76b9e12c0be03bbfe9e1f65a GIT binary patch literal 1150 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4mO}jWo=(60|Sezr;B4q#hka-F8aQ95OBEo zaWeOU$rlc3cX};Q)?m~!@>5x2(y(RvoW-))e|O7=Z$5wKvgLHfh}+le>gJuVlyBJj z_x}63xN|>c8n(tvUv>TUQ@e=k5w$!A7X9OOXh~p{Y+_B}5SySF;m|fDI2+G5TjsI< zYkONJ*Urm*LO@5sc|!xwK_(-X0q1;6th_CA_`UVH>1nxUgUDS2A2L{6kSsr~{ES%x zFE@waAPTB)l?J!pGPn-S*t_xm-Spx<#)#8TpBKrm+0GboqwL&e%k#_|ww=#^nj~O- U=d1HwV7bEJ>FVdQ&MBb@04+FS;{X5v literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/filters/filter-functions/two-urls-with-different-regions.svg b/crates/resvg/tests/tests/filters/filter-functions/two-urls-with-different-regions.svg new file mode 100644 index 000000000..3ec6ab8ce --- /dev/null +++ b/crates/resvg/tests/tests/filters/filter-functions/two-urls-with-different-regions.svg @@ -0,0 +1,16 @@ + + Two urls with different filter regions + + + + + + + + + + + + + +