diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ed3dfaa6a..f91fb60d8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,7 +41,7 @@ jobs: - name: Install toolchain uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.65.0 + toolchain: 1.67.1 - name: Build run: cargo build diff --git a/Cargo.lock b/Cargo.lock index 989aa3619..38190720b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "cfg-if" version = "1.0.0" @@ -129,6 +135,16 @@ dependencies = [ "weezl", ] +[[package]] +name = "image-webp" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "imagesize" version = "0.12.0" @@ -201,11 +217,18 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "resvg" version = "0.42.0" dependencies = [ "gif", + "image-webp", "log", "once_cell", "pico-args", diff --git a/README.md b/README.md index 5b105587d..ee6f888b1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Build Status](https://github.com/RazrFalcon/resvg/workflows/Build/badge.svg) [![Crates.io](https://img.shields.io/crates/v/resvg.svg)](https://crates.io/crates/resvg) [![Documentation](https://docs.rs/resvg/badge.svg)](https://docs.rs/resvg) -[![Rust 1.65+](https://img.shields.io/badge/rust-1.65+-orange.svg)](https://www.rust-lang.org) +[![Rust 1.67.1+](https://img.shields.io/badge/rust-1.67.1+-orange.svg)](https://www.rust-lang.org) *resvg* is an [SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics) rendering library. diff --git a/crates/resvg/Cargo.toml b/crates/resvg/Cargo.toml index 85ec077da..6a817db95 100644 --- a/crates/resvg/Cargo.toml +++ b/crates/resvg/Cargo.toml @@ -16,6 +16,7 @@ required-features = ["text", "system-fonts", "memmap-fonts"] [dependencies] gif = { version = "0.13", optional = true } +image-webp = { version = "0.1.3", optional = true } log = "0.4" pico-args = { version = "0.5", features = ["eq-separator"] } rgb = "0.8" @@ -40,4 +41,4 @@ memmap-fonts = ["usvg/memmap-fonts"] # Enables decoding and rendering of raster images. # When disabled, `image` elements with SVG data will still be rendered. # Adds around 200KiB to your binary. -raster-images = ["gif", "dep:zune-jpeg"] +raster-images = ["gif", "image-webp", "dep:zune-jpeg"] diff --git a/crates/resvg/src/image.rs b/crates/resvg/src/image.rs index e63e0a079..3f4f34958 100644 --- a/crates/resvg/src/image.rs +++ b/crates/resvg/src/image.rs @@ -70,6 +70,9 @@ mod raster_images { usvg::ImageKind::GIF(ref data) => { decode_gif(data).log_none(|| log::warn!("Failed to decode a GIF image.")) } + usvg::ImageKind::WEBP(ref data) => { + decode_webp(data).log_none(|| log::warn!("Failed to decode a WebP image.")) + } } } @@ -107,6 +110,38 @@ mod raster_images { Some(pixmap) } + fn decode_webp(data: &[u8]) -> Option { + let mut decoder = image_webp::WebPDecoder::new(std::io::Cursor::new(data)).ok()?; + let mut first_frame = vec![0; decoder.output_buffer_size()?]; + decoder.read_image(&mut first_frame).ok()?; + + let (w, h) = decoder.dimensions(); + let mut pixmap = tiny_skia::Pixmap::new(w, h)?; + + if decoder.has_alpha() { + rgba_to_pixmap(&first_frame, &mut pixmap); + } else { + rgb_to_pixmap(&first_frame, &mut pixmap); + } + + Some(pixmap) + } + + fn rgb_to_pixmap(data: &[u8], pixmap: &mut tiny_skia::Pixmap) { + use rgb::FromSlice; + + let mut i = 0; + let dst = pixmap.data_mut(); + for p in data.as_rgb() { + dst[i + 0] = p.r; + dst[i + 1] = p.g; + dst[i + 2] = p.b; + dst[i + 3] = 255; + + i += tiny_skia::BYTES_PER_PIXEL; + } + } + fn rgba_to_pixmap(data: &[u8], pixmap: &mut tiny_skia::Pixmap) { use rgb::FromSlice; diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs index 6dfcb61da..987515e2a 100644 --- a/crates/resvg/tests/integration/render.rs +++ b/crates/resvg/tests/integration/render.rs @@ -1101,12 +1101,14 @@ use crate::render; #[test] fn structure_image_embedded_svg_without_mime() { assert_eq!(render("tests/structure/image/embedded-svg-without-mime"), 0); } #[test] fn structure_image_embedded_svg() { assert_eq!(render("tests/structure/image/embedded-svg"), 0); } #[test] fn structure_image_embedded_svgz() { assert_eq!(render("tests/structure/image/embedded-svgz"), 0); } +#[test] fn structure_image_embedded_webp() { assert_eq!(render("tests/structure/image/embedded-webp"), 0); } #[test] fn structure_image_external_gif() { assert_eq!(render("tests/structure/image/external-gif"), 0); } #[test] fn structure_image_external_jpeg() { assert_eq!(render("tests/structure/image/external-jpeg"), 0); } #[test] fn structure_image_external_png() { assert_eq!(render("tests/structure/image/external-png"), 0); } #[test] fn structure_image_external_svg_with_transform() { assert_eq!(render("tests/structure/image/external-svg-with-transform"), 0); } #[test] fn structure_image_external_svg() { assert_eq!(render("tests/structure/image/external-svg"), 0); } #[test] fn structure_image_external_svgz() { assert_eq!(render("tests/structure/image/external-svgz"), 0); } +#[test] fn structure_image_external_webp() { assert_eq!(render("tests/structure/image/external-webp"), 0); } #[test] fn structure_image_float_size() { assert_eq!(render("tests/structure/image/float-size"), 0); } #[test] fn structure_image_image_with_float_size_scaling() { assert_eq!(render("tests/structure/image/image-with-float-size-scaling"), 0); } #[test] fn structure_image_no_height_non_square() { assert_eq!(render("tests/structure/image/no-height-non-square"), 0); } diff --git a/crates/resvg/tests/resources/image.webp b/crates/resvg/tests/resources/image.webp new file mode 100644 index 000000000..2d6c9ca58 Binary files /dev/null and b/crates/resvg/tests/resources/image.webp differ diff --git a/crates/resvg/tests/tests/structure/image/embedded-webp.png b/crates/resvg/tests/tests/structure/image/embedded-webp.png new file mode 100644 index 000000000..fdb3d7704 Binary files /dev/null and b/crates/resvg/tests/tests/structure/image/embedded-webp.png differ diff --git a/crates/resvg/tests/tests/structure/image/embedded-webp.svg b/crates/resvg/tests/tests/structure/image/embedded-webp.svg new file mode 100644 index 000000000..b9e03765a --- /dev/null +++ b/crates/resvg/tests/tests/structure/image/embedded-webp.svg @@ -0,0 +1,47 @@ + + Embedded WebP + + + + + diff --git a/crates/resvg/tests/tests/structure/image/external-webp.png b/crates/resvg/tests/tests/structure/image/external-webp.png new file mode 100644 index 000000000..fdb3d7704 Binary files /dev/null and b/crates/resvg/tests/tests/structure/image/external-webp.png differ diff --git a/crates/resvg/tests/tests/structure/image/external-webp.svg b/crates/resvg/tests/tests/structure/image/external-webp.svg new file mode 100644 index 000000000..21dc2ebf7 --- /dev/null +++ b/crates/resvg/tests/tests/structure/image/external-webp.svg @@ -0,0 +1,8 @@ + + External WebP + + + + + diff --git a/crates/usvg/src/parser/image.rs b/crates/usvg/src/parser/image.rs index 3c0aa9472..e622e20dd 100644 --- a/crates/usvg/src/parser/image.rs +++ b/crates/usvg/src/parser/image.rs @@ -53,7 +53,7 @@ impl ImageHrefResolver<'_> { /// /// base64 encoded data is already decoded. /// - /// The default implementation would try to load JPEG, PNG, GIF, SVG and SVGZ types. + /// The default implementation would try to load JPEG, PNG, GIF, WebP, SVG and SVGZ types. /// Note that it will simply match the `mime` or data's magic. /// The actual images would not be decoded. It's up to the renderer. pub fn default_data_resolver() -> ImageHrefDataResolverFn<'static> { @@ -62,11 +62,13 @@ impl ImageHrefResolver<'_> { "image/jpg" | "image/jpeg" => Some(ImageKind::JPEG(data)), "image/png" => Some(ImageKind::PNG(data)), "image/gif" => Some(ImageKind::GIF(data)), + "image/webp" => Some(ImageKind::WEBP(data)), "image/svg+xml" => load_sub_svg(&data, opts), "text/plain" => match get_image_data_format(&data) { Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(data)), Some(ImageFormat::PNG) => Some(ImageKind::PNG(data)), Some(ImageFormat::GIF) => Some(ImageKind::GIF(data)), + Some(ImageFormat::WEBP) => Some(ImageKind::WEBP(data)), _ => load_sub_svg(&data, opts), }, _ => None, @@ -98,9 +100,10 @@ impl ImageHrefResolver<'_> { Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(Arc::new(data))), Some(ImageFormat::PNG) => Some(ImageKind::PNG(Arc::new(data))), Some(ImageFormat::GIF) => Some(ImageKind::GIF(Arc::new(data))), + Some(ImageFormat::WEBP) => Some(ImageKind::WEBP(Arc::new(data))), Some(ImageFormat::SVG) => load_sub_svg(&data, opts), _ => { - log::warn!("'{}' is not a PNG, JPEG, GIF or SVG(Z) image.", href); + log::warn!("'{}' is not a PNG, JPEG, GIF, WebP or SVG(Z) image.", href); None } } @@ -123,6 +126,7 @@ enum ImageFormat { PNG, JPEG, GIF, + WEBP, SVG, } @@ -298,7 +302,7 @@ pub(crate) fn get_href_data(href: &str, state: &converter::State) -> Option Option { let ext = path.extension().and_then(|e| e.to_str())?.to_lowercase(); @@ -309,12 +313,13 @@ fn get_image_file_format(path: &std::path::Path, data: &[u8]) -> Option Option { match imagesize::image_type(data).ok()? { imagesize::ImageType::Gif => Some(ImageFormat::GIF), imagesize::ImageType::Jpeg => Some(ImageFormat::JPEG), imagesize::ImageType::Png => Some(ImageFormat::PNG), + imagesize::ImageType::Webp => Some(ImageFormat::WEBP), _ => None, } } diff --git a/crates/usvg/src/tree/mod.rs b/crates/usvg/src/tree/mod.rs index cf97b5099..73f8fad2d 100644 --- a/crates/usvg/src/tree/mod.rs +++ b/crates/usvg/src/tree/mod.rs @@ -1424,6 +1424,8 @@ pub enum ImageKind { PNG(Arc>), /// A reference to raw GIF data. Should be decoded by the caller. GIF(Arc>), + /// A reference to raw WebP data. Should be decoded by the caller. + WEBP(Arc>), /// A preprocessed SVG tree. Can be rendered as is. SVG(Tree), } @@ -1431,12 +1433,13 @@ pub enum ImageKind { impl ImageKind { pub(crate) fn actual_size(&self) -> Option { match self { - ImageKind::JPEG(ref data) | ImageKind::PNG(ref data) | ImageKind::GIF(ref data) => { - imagesize::blob_size(data) - .ok() - .and_then(|size| Size::from_wh(size.width as f32, size.height as f32)) - .log_none(|| log::warn!("Image has an invalid size. Skipped.")) - } + ImageKind::JPEG(ref data) + | ImageKind::PNG(ref data) + | ImageKind::GIF(ref data) + | ImageKind::WEBP(ref data) => imagesize::blob_size(data) + .ok() + .and_then(|size| Size::from_wh(size.width as f32, size.height as f32)) + .log_none(|| log::warn!("Image has an invalid size. Skipped.")), ImageKind::SVG(ref svg) => Some(svg.size), } } @@ -1448,6 +1451,7 @@ impl std::fmt::Debug for ImageKind { ImageKind::JPEG(_) => f.write_str("ImageKind::JPEG(..)"), ImageKind::PNG(_) => f.write_str("ImageKind::PNG(..)"), ImageKind::GIF(_) => f.write_str("ImageKind::GIF(..)"), + ImageKind::WEBP(_) => f.write_str("ImageKind::WEBP(..)"), ImageKind::SVG(_) => f.write_str("ImageKind::SVG(..)"), } } diff --git a/crates/usvg/src/writer.rs b/crates/usvg/src/writer.rs index e2d5c6ec6..c36045fd8 100644 --- a/crates/usvg/src/writer.rs +++ b/crates/usvg/src/writer.rs @@ -1073,6 +1073,7 @@ impl XmlWriterExt for XmlWriter { ImageKind::JPEG(ref data) => ("jpeg", data.as_slice()), ImageKind::PNG(ref data) => ("png", data.as_slice()), ImageKind::GIF(ref data) => ("gif", data.as_slice()), + ImageKind::WEBP(ref data) => ("webp", data.as_slice()), ImageKind::SVG(ref tree) => { svg_string = tree.to_string(&WriteOptions::default()); ("svg+xml", svg_string.as_bytes())