Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ 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)
- `feDisplacementMap` applied `scale` twice, displacing almost everything out of the filter region,
and read the displacement map with premultiplied colors. (#1019)

## [0.48.1] 2026-08-02

Expand Down
11 changes: 6 additions & 5 deletions crates/resvg/src/filter/displacement_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ use usvg::filter::{ColorChannel, DisplacementMap};
/// - `map` pixels should have a **unpremultiplied alpha**.
/// - `src` pixels can have any alpha method.
///
/// `sx` and `sy` indicate canvas scale.
/// `scale_x` and `scale_y` are the `scale` attribute already converted
/// into the canvas coordinate system, i.e. the maximum displacement in pixels.
///
/// `src`, `map` and `dest` dimensions must match.
pub fn apply(
fe: &DisplacementMap,
sx: f32,
sy: f32,
scale_x: f32,
scale_y: f32,
src: ImageRef,
map: ImageRef,
dest: ImageRefMut,
Expand All @@ -42,8 +43,8 @@ pub fn apply(

let dx = calc_offset(fe.x_channel_selector());
let dy = calc_offset(fe.y_channel_selector());
let ox = (x as f32 + dx * sx * fe.scale()).round() as i32;
let oy = (y as f32 + dy * sy * fe.scale()).round() as i32;
let ox = (x as f32 + dx * scale_x).round() as i32;
let oy = (y as f32 + dy * scale_y).round() as i32;

// TODO: we should use some kind of anti-aliasing when offset is on a pixel border

Expand Down
6 changes: 5 additions & 1 deletion crates/resvg/src/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -976,10 +976,14 @@ fn apply_displacement_map(
input2: Image,
) -> Result<Image, Error> {
let pixmap1 = input1.into_color_space(cs)?.take()?;
let pixmap2 = input2.into_color_space(cs)?.take()?;
let mut pixmap2 = input2.into_color_space(cs)?.take()?;

// The displacement map must be read using non-premultiplied color values.
demultiply_alpha(pixmap2.data_mut().as_rgba_mut());

let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?;

// `scale` is in user units, convert it into pixels.
let (sx, sy) = match scale_coordinates(fe.scale(), fe.scale(), ts) {
Some(v) => v,
None => return Ok(Image::from_image(pixmap1, cs)),
Expand Down
5 changes: 5 additions & 0 deletions crates/resvg/tests/integration/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,12 @@ use crate::render;
#[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_primitiveUnits_eq_objectBoundingBox() { assert_eq!(render("tests/filters/feDisplacementMap/primitiveUnits=objectBoundingBox"), 0); }
#[test] fn filters_feDisplacementMap_scale_eq_0() { assert_eq!(render("tests/filters/feDisplacementMap/scale=0"), 0); }
#[test] fn filters_feDisplacementMap_semi_transparent_map() { assert_eq!(render("tests/filters/feDisplacementMap/semi-transparent-map"), 0); }
#[test] fn filters_feDisplacementMap_simple_case() { assert_eq!(render("tests/filters/feDisplacementMap/simple-case"), 0); }
#[test] fn filters_feDisplacementMap_turbulence_map() { assert_eq!(render("tests/filters/feDisplacementMap/turbulence-map"), 0); }
#[test] fn filters_feDisplacementMap_uniform_map() { assert_eq!(render("tests/filters/feDisplacementMap/uniform-map"), 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); }
#[test] fn filters_feDistantLight_only_azimuth() { assert_eq!(render("tests/filters/feDistantLight/only-azimuth"), 0); }
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions crates/resvg/tests/tests/filters/feDisplacementMap/scale=0.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions crates/resvg/tests/tests/filters/feDisplacementMap/uniform-map.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions crates/usvg/src/parser/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -721,8 +721,8 @@ fn convert_displacement_map(fe: SvgNode, scale: Size, primitives: &[Primitive])
_ => ColorChannel::A,
};

// TODO: should probably split scale to scale_x and scale_y,
// but resvg doesn't support displacement map anyway...
// TODO: should probably split scale to scale_x and scale_y
// to handle `primitiveUnits=objectBoundingBox` with a non-square bbox.
let scale = (scale.width() + scale.height()) / 2.0;

Kind::DisplacementMap(DisplacementMap {
Expand Down
Loading