From 1e270fa64bc88062a82a72573b34a8c68c646f32 Mon Sep 17 00:00:00 2001 From: edco <5522594+edco@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:21:32 +1000 Subject: [PATCH] Add kernelUnitLength support to feDiffuseLighting, feSpecularLighting, and feConvolveMatrix - Parse kernelUnitLength in crates/usvg/src/parser/filter.rs for DiffuseLighting, SpecularLighting, and ConvolveMatrix - Add kernel_unit_length field and accessors to usvg AST structs - Write kernelUnitLength attribute in crates/usvg/src/writer.rs - Implement resolution-independent differential step and bilinear sampling in resvg lighting and convolve_matrix filter primitives - Add unit and integration tests for kernelUnitLength parsing and rendering --- .gitignore | 1 + Cargo.lock | 12 -- crates/resvg/src/filter/convolve_matrix.rs | 138 ++++++++++++++++----- crates/resvg/src/filter/lighting.rs | 114 ++++++++++++++--- crates/resvg/src/filter/mod.rs | 7 +- crates/resvg/tests/integration/extra.rs | 74 +++++++++++ crates/usvg/src/parser/filter.rs | 95 +++++++++----- crates/usvg/src/tree/filter.rs | 26 +++- crates/usvg/src/writer.rs | 18 +++ crates/usvg/tests/parser.rs | 73 +++++++++++ 10 files changed, 463 insertions(+), 95 deletions(-) diff --git a/.gitignore b/.gitignore index 97e0980f0..d7a58b0fc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ target .vscode tools/build-* **/diffs +.cargo diff --git a/Cargo.lock b/Cargo.lock index 1bfa2662f..44818c654 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - [[package]] name = "arrayvec" version = "0.7.8" @@ -653,10 +647,7 @@ dependencies = [ [[package]] name = "tiny-skia" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea" dependencies = [ - "arrayref", "arrayvec", "bytemuck", "cfg-if", @@ -668,10 +659,7 @@ dependencies = [ [[package]] name = "tiny-skia-path" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" dependencies = [ - "arrayref", "bytemuck", "strict-num", ] diff --git a/crates/resvg/src/filter/convolve_matrix.rs b/crates/resvg/src/filter/convolve_matrix.rs index bd14dfe32..e1edde0da 100644 --- a/crates/resvg/src/filter/convolve_matrix.rs +++ b/crates/resvg/src/filter/convolve_matrix.rs @@ -3,8 +3,39 @@ use super::{ImageRefMut, f32_bound}; use rgb::RGBA8; +use usvg::ApproxEqUlps; use usvg::filter::{ConvolveMatrix, EdgeMode}; +#[inline] +fn pixel_at_f32(src: &ImageRefMut, fx: f32, fy: f32) -> (f32, f32, f32, f32) { + let fx = fx.clamp(0.0, (src.width - 1) as f32); + let fy = fy.clamp(0.0, (src.height - 1) as f32); + let x0 = fx.floor() as u32; + let y0 = fy.floor() as u32; + let x1 = (x0 + 1).min(src.width - 1); + let y1 = (y0 + 1).min(src.height - 1); + let dx = fx - x0 as f32; + let dy = fy - y0 as f32; + + let p00 = src.pixel_at(x0, y0); + let p10 = src.pixel_at(x1, y0); + let p01 = src.pixel_at(x0, y1); + let p11 = src.pixel_at(x1, y1); + + let interp = |c00: u8, c10: u8, c01: u8, c11: u8| -> f32 { + let top = c00 as f32 * (1.0 - dx) + c10 as f32 * dx; + let bottom = c01 as f32 * (1.0 - dx) + c11 as f32 * dx; + top * (1.0 - dy) + bottom * dy + }; + + ( + interp(p00.r, p10.r, p01.r, p11.r), + interp(p00.g, p10.g, p01.g, p11.g), + interp(p00.b, p10.b, p01.b, p11.b), + interp(p00.a, p10.a, p01.a, p11.a), + ) +} + /// Applies a convolve matrix. /// /// Input image pixels should have a **premultiplied alpha** when `preserve_alpha=false`. @@ -12,7 +43,7 @@ use usvg::filter::{ConvolveMatrix, EdgeMode}; /// # Allocations /// /// This method will allocate a copy of the `src` image as a back buffer. -pub fn apply(matrix: &ConvolveMatrix, src: ImageRefMut) { +pub fn apply(matrix: &ConvolveMatrix, ts: usvg::Transform, src: ImageRefMut) { fn bound(min: i32, val: i32, max: i32) -> i32 { core::cmp::max(min, core::cmp::min(max, val)) } @@ -20,6 +51,15 @@ pub fn apply(matrix: &ConvolveMatrix, src: ImageRefMut) { let width_max = src.width as i32 - 1; let height_max = src.height as i32 - 1; + let (step_x, step_y) = if let Some((kx, ky)) = matrix.kernel_unit_length() { + let (sx, sy) = ts.get_scale(); + (kx.get() * sx, ky.get() * sy) + } else { + (1.0, 1.0) + }; + + let use_unit_step = step_x.approx_eq_ulps(&1.0, 4) && step_y.approx_eq_ulps(&1.0, 4); + let mut buf = vec![RGBA8::default(); src.data.len()]; let mut buf = ImageRefMut::new(src.width, src.height, &mut buf); let mut x = 0; @@ -31,44 +71,82 @@ pub fn apply(matrix: &ConvolveMatrix, src: ImageRefMut) { let mut new_a = 0.0; for oy in 0..matrix.matrix().rows() { for ox in 0..matrix.matrix().columns() { - let mut tx = x as i32 - matrix.matrix().target_x() as i32 + ox as i32; - let mut ty = y as i32 - matrix.matrix().target_y() as i32 + oy as i32; + let k = matrix.matrix().get( + matrix.matrix().columns() - ox - 1, + matrix.matrix().rows() - oy - 1, + ); + + if use_unit_step { + let mut tx = x as i32 - matrix.matrix().target_x() as i32 + ox as i32; + let mut ty = y as i32 - matrix.matrix().target_y() as i32 + oy as i32; - match matrix.edge_mode() { - EdgeMode::None => { - if tx < 0 || tx > width_max || ty < 0 || ty > height_max { - continue; + match matrix.edge_mode() { + EdgeMode::None => { + if tx < 0 || tx > width_max || ty < 0 || ty > height_max { + continue; + } + } + EdgeMode::Duplicate => { + tx = bound(0, tx, width_max); + ty = bound(0, ty, height_max); + } + EdgeMode::Wrap => { + while tx < 0 { + tx += src.width as i32; + } + tx %= src.width as i32; + + while ty < 0 { + ty += src.height as i32; + } + ty %= src.height as i32; } } - EdgeMode::Duplicate => { - tx = bound(0, tx, width_max); - ty = bound(0, ty, height_max); + + let p = src.pixel_at(tx as u32, ty as u32); + new_r += (p.r as f32) / 255.0 * k; + new_g += (p.g as f32) / 255.0 * k; + new_b += (p.b as f32) / 255.0 * k; + + if !matrix.preserve_alpha() { + new_a += (p.a as f32) / 255.0 * k; } - EdgeMode::Wrap => { - while tx < 0 { - tx += src.width as i32; + } else { + let mut fx = + x as f32 - matrix.matrix().target_x() as f32 * step_x + ox as f32 * step_x; + let mut fy = + y as f32 - matrix.matrix().target_y() as f32 * step_y + oy as f32 * step_y; + + match matrix.edge_mode() { + EdgeMode::None => { + if fx < 0.0 + || fx > width_max as f32 + || fy < 0.0 + || fy > height_max as f32 + { + continue; + } } - tx %= src.width as i32; - - while ty < 0 { - ty += src.height as i32; + EdgeMode::Duplicate => { + fx = fx.clamp(0.0, width_max as f32); + fy = fy.clamp(0.0, height_max as f32); + } + EdgeMode::Wrap => { + let w = src.width as f32; + let h = src.height as f32; + fx = fx.rem_euclid(w); + fy = fy.rem_euclid(h); } - ty %= src.height as i32; } - } - let k = matrix.matrix().get( - matrix.matrix().columns() - ox - 1, - matrix.matrix().rows() - oy - 1, - ); + let (pr, pg, pb, pa) = pixel_at_f32(&src, fx, fy); + new_r += (pr / 255.0) * k; + new_g += (pg / 255.0) * k; + new_b += (pb / 255.0) * k; - let p = src.pixel_at(tx as u32, ty as u32); - new_r += (p.r as f32) / 255.0 * k; - new_g += (p.g as f32) / 255.0 * k; - new_b += (p.b as f32) / 255.0 * k; - - if !matrix.preserve_alpha() { - new_a += (p.a as f32) / 255.0 * k; + if !matrix.preserve_alpha() { + new_a += (pa / 255.0) * k; + } } } } diff --git a/crates/resvg/src/filter/lighting.rs b/crates/resvg/src/filter/lighting.rs index f09fa387f..c11a9d078 100644 --- a/crates/resvg/src/filter/lighting.rs +++ b/crates/resvg/src/filter/lighting.rs @@ -121,6 +121,50 @@ impl Normal { } } +#[inline] +fn alpha_at_f32(img: ImageRef, fx: f32, fy: f32) -> f32 { + let fx = fx.clamp(0.0, (img.width - 1) as f32); + let fy = fy.clamp(0.0, (img.height - 1) as f32); + let x0 = fx.floor() as u32; + let y0 = fy.floor() as u32; + let x1 = (x0 + 1).min(img.width - 1); + let y1 = (y0 + 1).min(img.height - 1); + let dx = fx - x0 as f32; + let dy = fy - y0 as f32; + + let a00 = img.alpha_at(x0, y0) as f32; + let a10 = img.alpha_at(x1, y0) as f32; + let a01 = img.alpha_at(x0, y1) as f32; + let a11 = img.alpha_at(x1, y1) as f32; + + let top = a00 * (1.0 - dx) + a10 * dx; + let bottom = a01 * (1.0 - dx) + a11 * dx; + top * (1.0 - dy) + bottom * dy +} + +#[inline] +fn normal_at(img: ImageRef, x: u32, y: u32, step_x: f32, step_y: f32) -> Normal { + let fx = x as f32; + let fy = y as f32; + + let a00 = alpha_at_f32(img, fx - step_x, fy - step_y); + let a10 = alpha_at_f32(img, fx, fy - step_y); + let a20 = alpha_at_f32(img, fx + step_x, fy - step_y); + let a01 = alpha_at_f32(img, fx - step_x, fy); + let a21 = alpha_at_f32(img, fx + step_x, fy); + let a02 = alpha_at_f32(img, fx - step_x, fy + step_y); + let a12 = alpha_at_f32(img, fx, fy + step_y); + let a22 = alpha_at_f32(img, fx + step_x, fy + step_y); + + let nx = -a00 + a20 - 2.0 * a01 + 2.0 * a21 - a02 + a22; + let ny = -a00 - 2.0 * a10 - a20 + a02 + 2.0 * a12 + a22; + + Normal { + factor: Vector2::new(1.0 / (4.0 * step_x), 1.0 / (4.0 * step_y)), + normal: Vector2::new(-nx, -ny), + } +} + /// Renders a diffuse lighting. /// /// - `src` pixels can have any alpha method, since only the alpha channel is used. @@ -132,16 +176,25 @@ impl Normal { pub fn diffuse_lighting( fe: &DiffuseLighting, light_source: LightSource, + ts: usvg::Transform, src: ImageRef, dest: ImageRefMut, ) { debug_assert!(src.width == dest.width && src.height == dest.height); + let (surface_scale, step_x, step_y) = if let Some((kx, ky)) = fe.kernel_unit_length() { + let (sx, sy) = ts.get_scale(); + let sz = (ts.sx * ts.sx + ts.sy * ts.sy).sqrt() / core::f32::consts::SQRT_2; + (fe.surface_scale() * sz, kx.get() * sx, ky.get() * sy) + } else { + (fe.surface_scale(), 1.0, 1.0) + }; + let light_factor = |normal: Normal, light_vector: Vector3| { let k = if normal.normal.approx_zero() { light_vector.z } else { - let mut n = normal.normal * (fe.surface_scale() / 255.0); + let mut n = normal.normal * (surface_scale / 255.0); n.x *= normal.factor.x; n.y *= normal.factor.y; @@ -155,8 +208,10 @@ pub fn diffuse_lighting( apply( light_source, - fe.surface_scale(), + surface_scale, fe.lighting_color(), + step_x, + step_y, &light_factor, calc_diffuse_alpha, src, @@ -175,11 +230,20 @@ pub fn diffuse_lighting( pub fn specular_lighting( fe: &SpecularLighting, light_source: LightSource, + ts: usvg::Transform, src: ImageRef, dest: ImageRefMut, ) { debug_assert!(src.width == dest.width && src.height == dest.height); + let (surface_scale, step_x, step_y) = if let Some((kx, ky)) = fe.kernel_unit_length() { + let (sx, sy) = ts.get_scale(); + let sz = (ts.sx * ts.sx + ts.sy * ts.sy).sqrt() / core::f32::consts::SQRT_2; + (fe.surface_scale() * sz, kx.get() * sx, ky.get() * sy) + } else { + (fe.surface_scale(), 1.0, 1.0) + }; + let light_factor = |normal: Normal, light_vector: Vector3| { let h = light_vector + Vector3::new(0.0, 0.0, 1.0); let h_length = h.length(); @@ -196,7 +260,7 @@ pub fn specular_lighting( n_dot_h.powf(fe.specular_exponent()) } } else { - let mut n = normal.normal * (fe.surface_scale() / 255.0); + let mut n = normal.normal * (surface_scale / 255.0); n.x *= normal.factor.x; n.y *= normal.factor.y; @@ -215,8 +279,10 @@ pub fn specular_lighting( apply( light_source, - fe.surface_scale(), + surface_scale, fe.lighting_color(), + step_x, + step_y, &light_factor, calc_specular_alpha, src, @@ -228,6 +294,8 @@ fn apply( light_source: LightSource, surface_scale: f32, lighting_color: Color, + step_x: f32, + step_y: f32, light_factor: &dyn Fn(Normal, Vector3) -> f32, calc_alpha: fn(u8, u8, u8) -> u8, src: ImageRef, @@ -284,24 +352,32 @@ fn apply( *dest.pixel_at_mut(nx, ny) = RGBA8 { b, g, r, a }; }; - calc(0, 0, top_left_normal(src)); - calc(width - 1, 0, top_right_normal(src)); - calc(0, height - 1, bottom_left_normal(src)); - calc(width - 1, height - 1, bottom_right_normal(src)); + if step_x.approx_eq_ulps(&1.0, 4) && step_y.approx_eq_ulps(&1.0, 4) { + calc(0, 0, top_left_normal(src)); + calc(width - 1, 0, top_right_normal(src)); + calc(0, height - 1, bottom_left_normal(src)); + calc(width - 1, height - 1, bottom_right_normal(src)); - for x in 1..width - 1 { - calc(x, 0, top_row_normal(src, x)); - calc(x, height - 1, bottom_row_normal(src, x)); - } + for x in 1..width - 1 { + calc(x, 0, top_row_normal(src, x)); + calc(x, height - 1, bottom_row_normal(src, x)); + } - for y in 1..height - 1 { - calc(0, y, left_column_normal(src, y)); - calc(width - 1, y, right_column_normal(src, y)); - } + for y in 1..height - 1 { + calc(0, y, left_column_normal(src, y)); + calc(width - 1, y, right_column_normal(src, y)); + } - for y in 1..height - 1 { - for x in 1..width - 1 { - calc(x, y, interior_normal(src, x, y)); + for y in 1..height - 1 { + for x in 1..width - 1 { + calc(x, y, interior_normal(src, x, y)); + } + } + } else { + for y in 0..height { + for x in 0..width { + calc(x, y, normal_at(src, x, y, step_x, step_y)); + } } } } diff --git a/crates/resvg/src/filter/mod.rs b/crates/resvg/src/filter/mod.rs index 30f8680c5..1526b6c7b 100644 --- a/crates/resvg/src/filter/mod.rs +++ b/crates/resvg/src/filter/mod.rs @@ -432,7 +432,7 @@ fn apply_inner( } usvg::filter::Kind::ConvolveMatrix(fe) => { let input = get_input(fe.input(), region, source, &results)?; - apply_convolve_matrix(fe, cs, input) + apply_convolve_matrix(fe, cs, ts, input) } usvg::filter::Kind::Morphology(fe) => { let input = get_input(fe.input(), region, source, &results)?; @@ -919,6 +919,7 @@ fn apply_color_matrix( fn apply_convolve_matrix( fe: &usvg::filter::ConvolveMatrix, cs: usvg::filter::ColorInterpolation, + ts: usvg::Transform, input: Image, ) -> Result { let mut pixmap = input.into_color_space(cs)?.take()?; @@ -927,7 +928,7 @@ fn apply_convolve_matrix( demultiply_alpha(pixmap.data_mut().as_rgba_mut()); } - convolve_matrix::apply(fe, pixmap.as_image_ref_mut()); + convolve_matrix::apply(fe, ts, pixmap.as_image_ref_mut()); Ok(Image::from_image(pixmap, cs)) } @@ -1031,6 +1032,7 @@ fn apply_diffuse_lighting( lighting::diffuse_lighting( fe, light_source, + ts, input.as_ref().as_image_ref(), pixmap.as_image_ref_mut(), ); @@ -1052,6 +1054,7 @@ fn apply_specular_lighting( lighting::specular_lighting( fe, light_source, + ts, input.as_ref().as_image_ref(), pixmap.as_image_ref_mut(), ); diff --git a/crates/resvg/tests/integration/extra.rs b/crates/resvg/tests/integration/extra.rs index 6470b0a81..9ad61ffee 100644 --- a/crates/resvg/tests/integration/extra.rs +++ b/crates/resvg/tests/integration/extra.rs @@ -80,3 +80,77 @@ fn render_node_filter_on_empty_group() { fn render_node_filter_with_transform_on_shape() { assert_eq!(render_node("extra/filter-with-transform-on-shape", "g1"), 0); } + +#[test] +fn kernel_unit_length_diffuse_lighting() { + let svg = " + + + + + + + + + "; + + let tree = usvg::Tree::from_str(svg, &usvg::Options::default()).unwrap(); + let mut pixmap = tiny_skia::Pixmap::new(100, 100).unwrap(); + resvg::render( + &tree, + tiny_skia::Transform::identity(), + &mut pixmap.as_mut(), + ); + + // Verify non-empty rendering and diffuse shading + let non_zero_pixels = pixmap.data().iter().filter(|&&b| b != 0).count(); + assert!(non_zero_pixels > 0); +} + +#[test] +fn kernel_unit_length_specular_lighting() { + let svg = " + + + + + + + + + "; + + let tree = usvg::Tree::from_str(svg, &usvg::Options::default()).unwrap(); + let mut pixmap = tiny_skia::Pixmap::new(100, 100).unwrap(); + resvg::render( + &tree, + tiny_skia::Transform::identity(), + &mut pixmap.as_mut(), + ); + + let non_zero_pixels = pixmap.data().iter().filter(|&&b| b != 0).count(); + assert!(non_zero_pixels > 0); +} + +#[test] +fn kernel_unit_length_convolve_matrix() { + let svg = " + + + + + + + "; + + let tree = usvg::Tree::from_str(svg, &usvg::Options::default()).unwrap(); + let mut pixmap = tiny_skia::Pixmap::new(100, 100).unwrap(); + resvg::render( + &tree, + tiny_skia::Transform::identity(), + &mut pixmap.as_mut(), + ); + + let non_zero_pixels = pixmap.data().iter().filter(|&&b| b != 0).count(); + assert!(non_zero_pixels > 0); +} diff --git a/crates/usvg/src/parser/filter.rs b/crates/usvg/src/parser/filter.rs index 52985c891..ee24b047e 100644 --- a/crates/usvg/src/parser/filter.rs +++ b/crates/usvg/src/parser/filter.rs @@ -7,7 +7,7 @@ use std::collections::HashSet; use std::str::FromStr; use std::sync::Arc; -use strict_num::PositiveF32; +use strict_num::{NonZeroPositiveF32, PositiveF32}; use svgtypes::{AspectRatio, Length, LengthUnit as Unit}; use crate::{ @@ -333,33 +333,32 @@ fn collect_children( None => break, }; - let kind = - match tag_name { - EId::FeDropShadow => convert_drop_shadow(child, scale, &primitives), - EId::FeGaussianBlur => convert_gaussian_blur(child, scale, &primitives), - EId::FeOffset => convert_offset(child, scale, &primitives), - EId::FeBlend => convert_blend(child, &primitives), - EId::FeFlood => convert_flood(child), - EId::FeComposite => convert_composite(child, &primitives), - EId::FeMerge => convert_merge(child, &primitives), - EId::FeTile => convert_tile(child, &primitives), - EId::FeImage => convert_image(child, filter_subregion, state, cache), - EId::FeComponentTransfer => convert_component_transfer(child, &primitives), - EId::FeColorMatrix => convert_color_matrix(child, &primitives), - EId::FeConvolveMatrix => convert_convolve_matrix(child, &primitives) - .unwrap_or_else(create_dummy_primitive), - EId::FeMorphology => convert_morphology(child, scale, &primitives), - EId::FeDisplacementMap => convert_displacement_map(child, scale, &primitives), - EId::FeTurbulence => convert_turbulence(child), - EId::FeDiffuseLighting => convert_diffuse_lighting(child, &primitives) - .unwrap_or_else(create_dummy_primitive), - EId::FeSpecularLighting => convert_specular_lighting(child, &primitives) - .unwrap_or_else(create_dummy_primitive), - tag_name => { - log::warn!("'{}' is not a valid filter primitive. Skipped.", tag_name); - continue; - } - }; + let kind = match tag_name { + EId::FeDropShadow => convert_drop_shadow(child, scale, &primitives), + EId::FeGaussianBlur => convert_gaussian_blur(child, scale, &primitives), + EId::FeOffset => convert_offset(child, scale, &primitives), + EId::FeBlend => convert_blend(child, &primitives), + EId::FeFlood => convert_flood(child), + EId::FeComposite => convert_composite(child, &primitives), + EId::FeMerge => convert_merge(child, &primitives), + EId::FeTile => convert_tile(child, &primitives), + EId::FeImage => convert_image(child, filter_subregion, state, cache), + EId::FeComponentTransfer => convert_component_transfer(child, &primitives), + EId::FeColorMatrix => convert_color_matrix(child, &primitives), + EId::FeConvolveMatrix => convert_convolve_matrix(child, scale, &primitives) + .unwrap_or_else(create_dummy_primitive), + EId::FeMorphology => convert_morphology(child, scale, &primitives), + EId::FeDisplacementMap => convert_displacement_map(child, scale, &primitives), + EId::FeTurbulence => convert_turbulence(child), + EId::FeDiffuseLighting => convert_diffuse_lighting(child, scale, &primitives) + .unwrap_or_else(create_dummy_primitive), + EId::FeSpecularLighting => convert_specular_lighting(child, scale, &primitives) + .unwrap_or_else(create_dummy_primitive), + tag_name => { + log::warn!("'{}' is not a valid filter primitive. Skipped.", tag_name); + continue; + } + }; let color_interpolation = child .find_attribute(AId::ColorInterpolationFilters) @@ -645,7 +644,7 @@ fn convert_composite(fe: SvgNode, primitives: &[Primitive]) -> Kind { }) } -fn convert_convolve_matrix(fe: SvgNode, primitives: &[Primitive]) -> Option { +fn convert_convolve_matrix(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Option { fn parse_target(target: Option, order: u32) -> Option { let default_target = (order as f32 / 2.0).floor() as u32; let target = target.unwrap_or(default_target as f32) as i32; @@ -701,6 +700,7 @@ fn convert_convolve_matrix(fe: SvgNode, primitives: &[Primitive]) -> Option Option Result, ()> { + if let Some(value) = fe.attribute::<&str>(AId::KernelUnitLength) { + let mut s = svgtypes::NumberListParser::from(value); + let x = match s.next() { + Some(Ok(n)) => n as f32, + _ => return Err(()), + }; + let y = match s.next() { + Some(Ok(n)) => n as f32, + _ => x, + }; + + if let (Some(x), Some(y)) = ( + NonZeroPositiveF32::new(x * scale.width()), + NonZeroPositiveF32::new(y * scale.height()), + ) { + Ok(Some((x, y))) + } else { + Err(()) + } + } else { + Ok(None) + } +} + fn convert_displacement_map(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Kind { let parse_channel = |aid| match fe.attribute(aid).unwrap_or("A") { "R" => ColorChannel::R, @@ -879,18 +908,20 @@ fn convert_image_inner( Some(Kind::Image(Image { root })) } -fn convert_diffuse_lighting(fe: SvgNode, primitives: &[Primitive]) -> Option { +fn convert_diffuse_lighting(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Option { let light_source = convert_light_source(fe)?; + let kernel_unit_length = parse_kernel_unit_length(fe, scale).ok()?; Some(Kind::DiffuseLighting(DiffuseLighting { input: resolve_input(fe, AId::In, primitives), surface_scale: fe.attribute(AId::SurfaceScale).unwrap_or(1.0), diffuse_constant: fe.attribute(AId::DiffuseConstant).unwrap_or(1.0), lighting_color: convert_lighting_color(fe), light_source, + kernel_unit_length, })) } -fn convert_specular_lighting(fe: SvgNode, primitives: &[Primitive]) -> Option { +fn convert_specular_lighting(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Option { let light_source = convert_light_source(fe)?; let specular_exponent = fe.attribute(AId::SpecularExponent).unwrap_or(1.0); @@ -900,6 +931,7 @@ fn convert_specular_lighting(fe: SvgNode, primitives: &[Primitive]) -> Option Option, } impl ConvolveMatrix { @@ -391,6 +392,13 @@ impl ConvolveMatrix { pub fn preserve_alpha(&self) -> bool { self.preserve_alpha } + + /// Intended distance in current filter units for dx and dy in the convolve matrix calculations. + /// + /// `kernelUnitLength` in the SVG. + pub fn kernel_unit_length(&self) -> Option<(NonZeroPositiveF32, NonZeroPositiveF32)> { + self.kernel_unit_length + } } /// A convolve matrix representation. @@ -692,6 +700,7 @@ pub struct DiffuseLighting { pub(crate) diffuse_constant: f32, pub(crate) lighting_color: Color, pub(crate) light_source: LightSource, + pub(crate) kernel_unit_length: Option<(NonZeroPositiveF32, NonZeroPositiveF32)>, } impl DiffuseLighting { @@ -727,6 +736,13 @@ impl DiffuseLighting { pub fn light_source(&self) -> LightSource { self.light_source } + + /// Intended distance in current filter units for dx and dy in the surface normal calculations. + /// + /// `kernelUnitLength` in the SVG. + pub fn kernel_unit_length(&self) -> Option<(NonZeroPositiveF32, NonZeroPositiveF32)> { + self.kernel_unit_length + } } /// A specular lighting filter primitive. @@ -740,6 +756,7 @@ pub struct SpecularLighting { pub(crate) specular_exponent: f32, pub(crate) lighting_color: Color, pub(crate) light_source: LightSource, + pub(crate) kernel_unit_length: Option<(NonZeroPositiveF32, NonZeroPositiveF32)>, } impl SpecularLighting { @@ -784,6 +801,13 @@ impl SpecularLighting { pub fn light_source(&self) -> LightSource { self.light_source } + + /// Intended distance in current filter units for dx and dy in the surface normal calculations. + /// + /// `kernelUnitLength` in the SVG. + pub fn kernel_unit_length(&self) -> Option<(NonZeroPositiveF32, NonZeroPositiveF32)> { + self.kernel_unit_length + } } /// A light source kind. diff --git a/crates/usvg/src/writer.rs b/crates/usvg/src/writer.rs index e5dc9ddc0..d38bd65f1 100644 --- a/crates/usvg/src/writer.rs +++ b/crates/usvg/src/writer.rs @@ -372,6 +372,12 @@ fn write_filters(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { "false" }, ); + if let Some((kx, ky)) = matrix.kernel_unit_length { + xml.write_attribute_fmt( + AId::KernelUnitLength.to_str(), + format_args!("{} {}", kx.get(), ky.get()), + ); + } xml.end_element(); } @@ -464,6 +470,12 @@ fn write_filters(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { xml.write_svg_attribute(AId::SurfaceScale, &light.surface_scale); xml.write_svg_attribute(AId::DiffuseConstant, &light.diffuse_constant); xml.write_color(AId::LightingColor, light.lighting_color); + if let Some((kx, ky)) = light.kernel_unit_length { + xml.write_attribute_fmt( + AId::KernelUnitLength.to_str(), + format_args!("{} {}", kx.get(), ky.get()), + ); + } write_light_source(&light.light_source, xml); xml.end_element(); @@ -477,6 +489,12 @@ fn write_filters(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { xml.write_svg_attribute(AId::SpecularConstant, &light.specular_constant); xml.write_svg_attribute(AId::SpecularExponent, &light.specular_exponent); xml.write_color(AId::LightingColor, light.lighting_color); + if let Some((kx, ky)) = light.kernel_unit_length { + xml.write_attribute_fmt( + AId::KernelUnitLength.to_str(), + format_args!("{} {}", kx.get(), ky.get()), + ); + } write_light_source(&light.light_source, xml); xml.end_element(); diff --git a/crates/usvg/tests/parser.rs b/crates/usvg/tests/parser.rs index e1672d4a2..1be2d98cd 100644 --- a/crates/usvg/tests/parser.rs +++ b/crates/usvg/tests/parser.rs @@ -702,3 +702,76 @@ fn resolve_fr_from_referenced_radial_gradient() { assert_eq!(rg.fr().get(), 25.0); } + +#[test] +fn filter_kernel_unit_length_parsing() { + let svg = " + + + + + + + + + + + + + "; + + let tree = usvg::Tree::from_str(svg, &usvg::Options::default()).unwrap(); + let usvg::Node::Group(group) = &tree.root().children()[0] else { + unreachable!() + }; + let primitives = group.filters()[0].primitives(); + assert_eq!(primitives.len(), 3); + + let usvg::filter::Kind::DiffuseLighting(dl) = primitives[0].kind() else { + unreachable!() + }; + let (kx, ky) = dl.kernel_unit_length().unwrap(); + assert_eq!(kx.get(), 2.0); + assert_eq!(ky.get(), 4.0); + + let usvg::filter::Kind::SpecularLighting(sl) = primitives[1].kind() else { + unreachable!() + }; + let (kx, ky) = sl.kernel_unit_length().unwrap(); + assert_eq!(kx.get(), 3.0); + assert_eq!(ky.get(), 3.0); + + let usvg::filter::Kind::ConvolveMatrix(cm) = primitives[2].kind() else { + unreachable!() + }; + let (kx, ky) = cm.kernel_unit_length().unwrap(); + assert_eq!(kx.get(), 1.5); + assert_eq!(ky.get(), 2.5); +} + +#[test] +fn filter_kernel_unit_length_invalid() { + let svg = " + + + + + + + + + + + + "; + + let tree = usvg::Tree::from_str(svg, &usvg::Options::default()).unwrap(); + let usvg::Node::Group(group) = &tree.root().children()[0] else { + unreachable!() + }; + let primitives = group.filters()[0].primitives(); + // Primitives with invalid kernelUnitLength should be replaced by dummy primitives (skipped) + for p in primitives { + assert!(matches!(p.kind(), usvg::filter::Kind::Flood(_))); + } +}