Repository: linebender/resvg (language: rust)

Issue:
Embedded grayscale image in SVG fails to render
For example:

```svg
<svg id="svg1" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>Embedded JPEG as image/jpg</title>

    <image id="image1" x="8" y="8" width="64" height="64" xlink:href="data:image/jpg;base64,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"/>

    <rect id="frame" x="1" y="1" width="78" height="78" fill="none" stroke="black"/>
</svg>
```

Reason seems to be that while `zune_jpeg` tries its best to convert to the RGBA format, this won't always be the case, for example for greyscale images, where the output color space will still be luma.


Discussion / hints:

Fix patch:
diff --git a/crates/resvg/src/image.rs b/crates/resvg/src/image.rs
index bcb31584..193201db 100644
--- a/crates/resvg/src/image.rs
+++ b/crates/resvg/src/image.rs
@@ -87,7 +87,25 @@ mod raster_images {
 
         let options = DecoderOptions::default().jpeg_set_out_colorspace(ColorSpace::RGBA);
         let mut decoder = zune_jpeg::JpegDecoder::new_with_options(data, options);
-        let img_data = decoder.decode().ok()?;
+        decoder.decode_headers().ok()?;
+        let output_cs = decoder.get_output_colorspace()?;
+
+        let img_data = {
+            let data = decoder.decode().ok()?;
+            match output_cs {
+                ColorSpace::RGBA => data,
+                // `set_output_color_space` is not guaranteed to actually always set the output space
+                // to RGBA (its docs say "we do not guarantee the decoder can convert to all colorspaces").
+                // In particular, it seems like it doesn't work for luma JPEGs,
+                // so we convert them manually.
+                ColorSpace::Luma => data
+                    .into_iter()
+                    .flat_map(|p| [p, p, p, 255])
+                    .collect::<Vec<_>>(),
+                _ => return None,
+            }
+        };
+
         let info = decoder.info()?;
 
         let size = tiny_skia::IntSize::from_wh(info.width as u32, info.height as u32)?;


Test patch:
diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs
index eb829fe9..692dac12 100644
--- a/crates/resvg/tests/integration/render.rs
+++ b/crates/resvg/tests/integration/render.rs
@@ -1098,7 +1098,9 @@ use crate::render;
 #[test] fn structure_image_embedded_gif() { assert_eq!(render("tests/structure/image/embedded-gif"), 0); }
 #[test] fn structure_image_embedded_jpeg_as_image_jpeg() { assert_eq!(render("tests/structure/image/embedded-jpeg-as-image-jpeg"), 0); }
 #[test] fn structure_image_embedded_jpeg_as_image_jpg() { assert_eq!(render("tests/structure/image/embedded-jpeg-as-image-jpg"), 0); }
+#[test] fn structure_image_embedded_jpeg_luma() { assert_eq!(render("tests/structure/image/embedded-jpeg-luma"), 0); }
 #[test] fn structure_image_embedded_jpeg_without_mime() { assert_eq!(render("tests/structure/image/embedded-jpeg-without-mime"), 0); }
+#[test] fn structure_image_embedded_png_luma() { assert_eq!(render("tests/structure/image/embedded-png-luma"), 0); }
 #[test] fn structure_image_embedded_png() { assert_eq!(render("tests/structure/image/embedded-png"), 0); }
 #[test] fn structure_image_embedded_svg_with_text() { assert_eq!(render("tests/structure/image/embedded-svg-with-text"), 0); }
 #[test] fn structure_image_embedded_svg_without_mime() { assert_eq!(render("tests/structure/image/embedded-svg-without-mime"), 0); }
diff --git a/crates/resvg/tests/tests/structure/image/embedded-jpeg-luma.png b/crates/resvg/tests/tests/structure/image/embedded-jpeg-luma.png
new file mode 100644
index 00000000..774242fd
Binary files /dev/null and b/crates/resvg/tests/tests/structure/image/embedded-jpeg-luma.png differ
diff --git a/crates/resvg/tests/tests/structure/image/embedded-jpeg-luma.svg b/crates/resvg/tests/tests/structure/image/embedded-jpeg-luma.svg
new file mode 100644
index 00000000..7f17fcec
--- /dev/null
+++ b/crates/resvg/tests/tests/structure/image/embedded-jpeg-luma.svg
@@ -0,0 +1,8 @@
+<svg id="svg1" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg"
+     xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>Embedded JPEG with luma</title>
+
+    <image id="image1" x="8" y="8" width="64" height="64" xlink:href="data:image/jpg;base64,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"/>
+
+    <rect id="frame" x="1" y="1" width="78" height="78" fill="none" stroke="black"/>
+</svg>
diff --git a/crates/resvg/tests/tests/structure/image/embedded-png-luma.png b/crates/resvg/tests/tests/structure/image/embedded-png-luma.png
new file mode 100644
index 00000000..c43970e2
Binary files /dev/null and b/crates/resvg/tests/tests/structure/image/embedded-png-luma.png differ
diff --git a/crates/resvg/tests/tests/structure/image/embedded-png-luma.svg b/crates/resvg/tests/tests/structure/image/embedded-png-luma.svg
new file mode 100644
index 00000000..8aa77383
--- /dev/null
+++ b/crates/resvg/tests/tests/structure/image/embedded-png-luma.svg
@@ -0,0 +1,8 @@
+<svg id="svg1" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg"
+     xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>Embedded PNG with luma</title>
+
+    <image id="image1" x="8" y="8" width="64" height="64" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAAAAACIM/FCAAADg0lEQVR4Ae3gAZAkSZIkSRKLqpm7R0REZmZmVlVVVVV3d3d3d/fMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMdHd3d3dXV1VVVVVmZkZGRIS7m5kKz0xmV3d1d3dPz8zMzMxMYuX/Bir/N1D5v4HK/w1U/m+g8n8Dlf8bqPzfQOX/Bir/N1D5v4HK/w1U/m+g8n8Dlf8bqPzfQOX/Bir/N1D5v4HK/w1Uno9z3K/BysJoAIawRtFgEk2kEzWSJG1lkiQkTpM4ZadxWk47TUJi27axnZA2aWzbto1J206wbds2dhrbtm3/AM+Dyv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zdQ+b+Byv8NVP5voPJ/A5X/G6j830Dl/wYq/zcg/m+g8n8Dlf8bqPzfQOX/Bir/N1D5v4HK/w1U/m+g8n8Dlf8bqPzfQOX/Bir/N1D5v4HK/w1U/m+g8n8Dlf8bqPzfwD8C0btoHlhAxXAAAAAASUVORK5CYII="/>
+
+    <rect id="frame" x="1" y="1" width="78" height="78" fill="none" stroke="black"/>
+</svg>