Repository: linebender/resvg (language: rust)

Issue:
Support more values for imageRendering
As a standard SVG attribute, only `optimizeSpeed` and `optimizeQuality` are supported (see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/image-rendering), however, as mentioned in the note there, it can also be used as a CSS property with the following possible values:

[smooth](https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#smooth)

    The image should be scaled with an algorithm that maximizes the appearance of the image. In particular, scaling algorithms that "smooth" colors are acceptable, such as bilinear interpolation. This is intended for images such as photos.
[high-quality](https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#high-quality)

    Identical to smooth, but with a preference for higher-quality scaling. If system resources are constrained, images with high-quality should be prioritized over those with any other value, when considering which images to degrade the quality of and to what degree.
[crisp-edges](https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#crisp-edges)

    The image is scaled with an algorithm such as "nearest neighbor" that preserves contrast and edges in the image. Generally intended for images such as pixel art or line drawings, no blurring or color smoothing occurs.
[pixelated](https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#pixelated)

    The image is scaled with the "nearest neighbor" or similar algorithm to the nearest integer multiple of the original image size, then uses smooth interpolation to bring the image to the final desired size. This is intended to preserve a "pixelated" look without introducing scaling artifacts when the upscaled resolution isn't an integer multiple of the original.

Here is an example SVG that uses `pixelated`:
![test](https://github.com/user-attachments/assets/6dad07a1-0381-4147-be00-874c2d83c181)



Discussion / hints:
Yes, SVG is endless.

Fix patch:
diff --git a/crates/resvg/src/image.rs b/crates/resvg/src/image.rs
index 3f4f34958..bcb315841 100644
--- a/crates/resvg/src/image.rs
+++ b/crates/resvg/src/image.rs
@@ -57,6 +57,7 @@ fn render_vector(
 #[cfg(feature = "raster-images")]
 mod raster_images {
     use crate::OptionLog;
+    use usvg::ImageRendering;
 
     fn decode_raster(image: &usvg::ImageKind) -> Option<tiny_skia::Pixmap> {
         match image {
@@ -169,10 +170,14 @@ mod raster_images {
         let rect = tiny_skia::Size::from_wh(raster.width() as f32, raster.height() as f32)?
             .to_rect(0.0, 0.0)?;
 
-        let mut quality = tiny_skia::FilterQuality::Bicubic;
-        if rendering_mode == usvg::ImageRendering::OptimizeSpeed {
-            quality = tiny_skia::FilterQuality::Nearest;
-        }
+        let quality = match rendering_mode {
+            ImageRendering::OptimizeQuality => tiny_skia::FilterQuality::Bicubic,
+            ImageRendering::OptimizeSpeed => tiny_skia::FilterQuality::Nearest,
+            ImageRendering::Smooth => tiny_skia::FilterQuality::Bilinear,
+            ImageRendering::HighQuality => tiny_skia::FilterQuality::Bicubic,
+            ImageRendering::CrispEdges => tiny_skia::FilterQuality::Nearest,
+            ImageRendering::Pixelated => tiny_skia::FilterQuality::Nearest,
+        };
 
         let pattern = tiny_skia::Pattern::new(
             raster.as_ref(),
diff --git a/crates/resvg/src/main.rs b/crates/resvg/src/main.rs
index 45ced6017..b30252354 100644
--- a/crates/resvg/src/main.rs
+++ b/crates/resvg/src/main.rs
@@ -162,7 +162,7 @@ OPTIONS:
                                 geometricPrecision]
   --image-rendering HINT        Selects the default image rendering method
                                 [default: optimizeQuality]
-                                [possible values: optimizeQuality, optimizeSpeed]
+                                [possible values: optimizeQuality, optimizeSpeed, smooth, high-quality, crisp-edges, pixelated]
   --resources-dir DIR           Sets a directory that will be used during
                                 relative paths resolving.
                                 Expected to be the same as the directory that
diff --git a/crates/usvg/src/main.rs b/crates/usvg/src/main.rs
index 56a53b146..1ecc02c62 100644
--- a/crates/usvg/src/main.rs
+++ b/crates/usvg/src/main.rs
@@ -43,7 +43,7 @@ OPTIONS:
                                     geometricPrecision]
   --image-rendering HINT            Selects the default image rendering method
                                     [default: optimizeQuality]
-                                    [possible values: optimizeQuality, optimizeSpeed]
+                                    [possible values: optimizeQuality, optimizeSpeed, smooth, high-quality, crisp-edges, pixelated]
   --resources-dir DIR               Sets a directory that will be used during
                                     relative paths resolving.
                                     Expected to be the same as the directory that
diff --git a/crates/usvg/src/parser/svgtree/mod.rs b/crates/usvg/src/parser/svgtree/mod.rs
index 540a8b45e..edb36f191 100644
--- a/crates/usvg/src/parser/svgtree/mod.rs
+++ b/crates/usvg/src/parser/svgtree/mod.rs
@@ -1020,6 +1020,10 @@ impl<'a, 'input: 'a> FromValue<'a, 'input> for ImageRendering {
         match value {
             "auto" | "optimizeQuality" => Some(ImageRendering::OptimizeQuality),
             "optimizeSpeed" => Some(ImageRendering::OptimizeSpeed),
+            "smooth" => Some(ImageRendering::Smooth),
+            "high-quality" => Some(ImageRendering::HighQuality),
+            "crisp-edges" => Some(ImageRendering::CrispEdges),
+            "pixelated" => Some(ImageRendering::Pixelated),
             _ => None,
         }
     }
diff --git a/crates/usvg/src/parser/svgtree/parse.rs b/crates/usvg/src/parser/svgtree/parse.rs
index dfb04fb7b..0c2328ba2 100644
--- a/crates/usvg/src/parser/svgtree/parse.rs
+++ b/crates/usvg/src/parser/svgtree/parse.rs
@@ -242,6 +242,13 @@ pub(crate) fn parse_svg_element<'input>(
         // For some reason those properties are allowed only inside a `style` attribute and CSS.
         if matches!(aid, AId::MixBlendMode | AId::Isolation | AId::FontKerning) {
             continue;
+        } else if aid == AId::ImageRendering
+            && matches!(
+                attr.value(),
+                "smooth" | "high-quality" | "crisp-edges" | "pixelated"
+            )
+        {
+            continue;
         }
 
         append_attribute(parent_id, tag_name, aid, attr.value_storage().clone(), doc);
diff --git a/crates/usvg/src/tree/mod.rs b/crates/usvg/src/tree/mod.rs
index a964c11cd..481cda9ce 100644
--- a/crates/usvg/src/tree/mod.rs
+++ b/crates/usvg/src/tree/mod.rs
@@ -170,6 +170,11 @@ impl std::str::FromStr for TextRendering {
 pub enum ImageRendering {
     OptimizeQuality,
     OptimizeSpeed,
+    // The following can only appear as presentation attributes.
+    Smooth,
+    HighQuality,
+    CrispEdges,
+    Pixelated,
 }
 
 impl Default for ImageRendering {
@@ -185,6 +190,10 @@ impl std::str::FromStr for ImageRendering {
         match s {
             "optimizeQuality" => Ok(ImageRendering::OptimizeQuality),
             "optimizeSpeed" => Ok(ImageRendering::OptimizeSpeed),
+            "smooth" => Ok(ImageRendering::Smooth),
+            "high-quality" => Ok(ImageRendering::HighQuality),
+            "crisp-edges" => Ok(ImageRendering::CrispEdges),
+            "pixelated" => Ok(ImageRendering::Pixelated),
             _ => Err("invalid"),
         }
     }
diff --git a/crates/usvg/src/writer.rs b/crates/usvg/src/writer.rs
index c36045fd8..9b0e74467 100644
--- a/crates/usvg/src/writer.rs
+++ b/crates/usvg/src/writer.rs
@@ -647,6 +647,18 @@ fn write_element(node: &Node, is_clip_path: bool, opt: &WriteOptions, xml: &mut
                 ImageRendering::OptimizeSpeed => {
                     xml.write_svg_attribute(AId::ImageRendering, "optimizeSpeed");
                 }
+                ImageRendering::Smooth => {
+                    xml.write_attribute(AId::Style.to_str(), "image-rendering:smooth");
+                }
+                ImageRendering::HighQuality => {
+                    xml.write_attribute(AId::Style.to_str(), "image-rendering:high-quality");
+                }
+                ImageRendering::CrispEdges => {
+                    xml.write_attribute(AId::Style.to_str(), "image-rendering:crisp-edges");
+                }
+                ImageRendering::Pixelated => {
+                    xml.write_attribute(AId::Style.to_str(), "image-rendering:pixelated");
+                }
             }
 
             xml.write_image_data(&img.kind);
diff --git a/docs/svg2-changelog.md b/docs/svg2-changelog.md
index c6c2638c1..0ed6ba4e3 100644
--- a/docs/svg2-changelog.md
+++ b/docs/svg2-changelog.md
@@ -196,7 +196,7 @@ Basically everything from [CSS Text Module Level 3](https://www.w3.org/TR/css-te
 - [x] An [`isolation`](https://www.w3.org/TR/compositing-1/#isolation) property.
 - [ ] `left`, `center` and `right` variants to `refX` and `refY` properties of the [`marker`](https://www.w3.org/TR/SVG2/painting.html#MarkerElement) element.
 - [x] An `auto-start-reverse` variant to [`orient`](https://www.w3.org/TR/SVG2/painting.html#OrientAttribute) property of the [`marker`](https://www.w3.org/TR/SVG2/painting.html#MarkerElement) element
-
+- [x] The `image-rendering` can appear as a presentation attribute with additional possible values. Currently, there is only best-effort support for "pixelated".
 ### Changed
 
 - [x] Markers can be set on all shapes and not only on `path`.


Test patch:
diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs
index 8d585dc9e..7dadd41b1 100644
--- a/crates/resvg/tests/integration/render.rs
+++ b/crates/resvg/tests/integration/render.rs
@@ -740,6 +740,7 @@ use crate::render;
 #[test] fn painting_fill_opacity_with_pattern() { assert_eq!(render("tests/painting/fill-opacity/with-pattern"), 0); }
 #[test] fn painting_fill_rule_evenodd() { assert_eq!(render("tests/painting/fill-rule/evenodd"), 0); }
 #[test] fn painting_fill_rule_nonzero() { assert_eq!(render("tests/painting/fill-rule/nonzero"), 0); }
+#[test] fn painting_image_rendering_high_quality() { assert_eq!(render("tests/painting/image-rendering/high-quality"), 0); }
 #[test] fn painting_image_rendering_on_feImage() { assert_eq!(render("tests/painting/image-rendering/on-feImage"), 0); }
 #[test] fn painting_image_rendering_optimizeSpeed_on_SVG() { assert_eq!(render("tests/painting/image-rendering/optimizeSpeed-on-SVG"), 0); }
 #[test] fn painting_image_rendering_optimizeSpeed() { assert_eq!(render("tests/painting/image-rendering/optimizeSpeed"), 0); }
diff --git a/crates/resvg/tests/tests/painting/image-rendering/high-quality.png b/crates/resvg/tests/tests/painting/image-rendering/high-quality.png
new file mode 100644
index 000000000..c1874e84c
Binary files /dev/null and b/crates/resvg/tests/tests/painting/image-rendering/high-quality.png differ
diff --git a/crates/resvg/tests/tests/painting/image-rendering/high-quality.svg b/crates/resvg/tests/tests/painting/image-rendering/high-quality.svg
new file mode 100644
index 000000000..392667f06
--- /dev/null
+++ b/crates/resvg/tests/tests/painting/image-rendering/high-quality.svg
@@ -0,0 +1,62 @@
+<svg id="svg1" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg"
+     xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>`optimizeSpeed`</title>
+
+    <image id="image1" x="8" y="8" width="64" height="64" style="image-rendering:high-quality"
+           xlink:href="data:image/png;base64,
+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
+QmCC"/>
+
+    <rect id="frame" x="1" y="1" width="78" height="78" fill="none" stroke="black"/>
+</svg>