From 4c6746a6223385ce284de243d0cca4b913617450 Mon Sep 17 00:00:00 2001 From: Craig Bester Date: Mon, 5 Sep 2022 13:35:28 +0200 Subject: [PATCH 1/8] Add WebpEncoder --- src/codecs/webp/encoder.rs | 107 +++++++++++++++++++++++++++++++++++++ src/codecs/webp/mod.rs | 25 ++++++--- src/image.rs | 8 ++- src/io/free_functions.rs | 4 ++ src/lib.rs | 2 +- 5 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 src/codecs/webp/encoder.rs diff --git a/src/codecs/webp/encoder.rs b/src/codecs/webp/encoder.rs new file mode 100644 index 0000000000..c298108c7f --- /dev/null +++ b/src/codecs/webp/encoder.rs @@ -0,0 +1,107 @@ +//! Encoding of WebP images. +/// +/// Uses the simple encoding API from the [libwebp] library. +/// +/// [libwebp]: https://developers.google.com/speed/webp/docs/api#simple_encoding_api +use std::io::Write; + +use libwebp::{Encoder, PixelLayout, WebPMemory}; + +use crate::ImageResult; +use crate::{ColorType, ImageEncoder}; + +/// WebP Encoder. +pub struct WebpEncoder { + inner: W, + quality: WebpQuality, +} + +/// WebP encoder quality. +#[derive(Debug, Copy, Clone)] +pub enum WebpQuality { + /// Lossless encoding. + Lossless, + /// Lossy quality from 0 to 100. 0 = low quality, small size; 100 = high quality, large size. + Lossy(f32), +} + +impl WebpQuality { + /// Minimum lossy quality value (0). + pub const MIN: f32 = 0f32; + /// Maximum lossy quality value (100). + pub const MAX: f32 = 100f32; + + /// Clamps lossy quality between 0 and 100. + fn clamp(self) -> Self { + match self { + WebpQuality::Lossy(quality) => WebpQuality::Lossy(quality.clamp(Self::MIN, Self::MAX)), + lossless => lossless, + } + } +} + +impl WebpEncoder { + /// Create a new encoder that writes its output to `w`. + /// + /// Defaults to lossy encoding with maximum quality. + pub fn new(w: W) -> Self { + WebpEncoder::new_with_quality(w, WebpQuality::Lossy(WebpQuality::MAX)) + } + + /// Create a new encoder with specified quality, that writes its output to `w`. + /// + /// Lossy qualities are clamped between 0 and 100. + pub fn new_with_quality(w: W, quality: WebpQuality) -> Self { + Self { + inner: w, + quality: quality.clamp(), + } + } + + /// Encode image data with the indicated color type. + /// + /// The encoder requires all data to be RGB8 or RGBA8, it will be converted + /// internally if necessary. + pub fn encode( + mut self, + data: &[u8], + width: u32, + height: u32, + color: ColorType, + ) -> ImageResult<()> { + // TODO: convert color types internally. + let layout: PixelLayout = match color { + ColorType::Rgb8 => PixelLayout::Rgb, + ColorType::Rgba8 => PixelLayout::Rgba, + _ => unimplemented!("Color type not yet supported"), + }; + + // Call the library to encode the image. + let encoder = Encoder::new(data, layout, width, height); + let output: WebPMemory = match self.quality { + WebpQuality::Lossless => encoder.encode_lossless(), + WebpQuality::Lossy(quality) => encoder.encode(quality), + }; + + // TODO: how to check if any errors occurred, maybe if the memory is empty? + // Can errors occur? + // let data = result.map_err(|err| { + // ImageError::Encoding(EncodingError::new(ImageFormat::WebP.into(), err)) + // })?; + + self.inner.write_all(&output)?; + Ok(()) + } +} + +impl ImageEncoder for WebpEncoder { + fn write_image( + self, + buf: &[u8], + width: u32, + height: u32, + color_type: ColorType, + ) -> ImageResult<()> { + self.encode(buf, width, height, color_type) + } +} diff --git a/src/codecs/webp/mod.rs b/src/codecs/webp/mod.rs index 99b94a4f37..de183b27cb 100644 --- a/src/codecs/webp/mod.rs +++ b/src/codecs/webp/mod.rs @@ -1,15 +1,28 @@ -//! Decoding of WebP Images +//! Decoding and Encoding of WebP Images +#[cfg(feature = "webp-encoder")] +pub use self::encoder::{WebpEncoder, WebpQuality}; + +#[cfg(feature = "webp-encoder")] +mod encoder; + +#[cfg(feature = "webp")] pub use self::decoder::WebPDecoder; +#[cfg(feature = "webp")] mod decoder; -mod loop_filter; -mod transform; - +#[cfg(feature = "webp")] +mod extended; +#[cfg(feature = "webp")] mod huffman; +#[cfg(feature = "webp")] +mod loop_filter; +#[cfg(feature = "webp")] mod lossless; +#[cfg(feature = "webp")] mod lossless_transform; +#[cfg(feature = "webp")] +mod transform; -mod extended; - +#[cfg(feature = "webp")] pub mod vp8; diff --git a/src/image.rs b/src/image.rs index 59c00a4c39..678e652b0b 100644 --- a/src/image.rs +++ b/src/image.rs @@ -217,7 +217,7 @@ impl ImageFormat { ImageFormat::Pnm => true, ImageFormat::Farbfeld => true, ImageFormat::Avif => true, - ImageFormat::WebP => false, + ImageFormat::WebP => true, ImageFormat::Hdr => false, ImageFormat::OpenExr => true, ImageFormat::Dds => false, @@ -302,6 +302,10 @@ pub enum ImageOutputFormat { /// An image in AVIF Format Avif, + #[cfg(feature = "webp-encoder")] + /// An image in WebP Format. + WebP, + /// A value for signalling an error: An unsupported format was requested // Note: When TryFrom is stabilized, this value should not be needed, and // a TryInto should be used instead of an Into. @@ -334,6 +338,8 @@ impl From for ImageOutputFormat { #[cfg(feature = "avif-encoder")] ImageFormat::Avif => ImageOutputFormat::Avif, + #[cfg(feature = "webp-encoder")] + ImageFormat::WebP => ImageOutputFormat::WebP, f => ImageOutputFormat::Unsupported(format!("{:?}", f)), } diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index 5c3dd91f28..1d9683080f 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -242,6 +242,10 @@ pub(crate) fn write_buffer_impl( ImageOutputFormat::Avif => { avif::AvifEncoder::new(buffered_write).write_image(buf, width, height, color) } + #[cfg(feature = "webp-encoder")] + ImageOutputFormat::WebP => { + webp::WebpEncoder::new(buffered_write).write_image(buf, width, height, color) + } image::ImageOutputFormat::Unsupported(msg) => Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( diff --git a/src/lib.rs b/src/lib.rs index a902d61056..0417e56854 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -244,7 +244,7 @@ pub mod codecs { pub mod tga; #[cfg(feature = "tiff")] pub mod tiff; - #[cfg(feature = "webp")] + #[cfg(any(feature = "webp", feature = "webp-encoder"))] pub mod webp; } From 7cd762fe0fdbf70871c11dd4ffdfe64d186d0539 Mon Sep 17 00:00:00 2001 From: Craig Bester Date: Mon, 5 Sep 2022 22:17:14 +0200 Subject: [PATCH 2/8] Change default WebP encoding quality to 80 --- Cargo.toml | 7 +++++- src/codecs/webp/encoder.rs | 49 +++++++++++++++++++++----------------- src/codecs/webp/mod.rs | 2 +- src/io/free_functions.rs | 2 +- 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f0dbdacbce..34225950ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ dav1d = { version = "0.6.0", optional = true } dcv-color-primitives = { version = "0.4.0", optional = true } color_quant = "1.1" exr = { version = "1.5.0", optional = true } +libwebp = { package = "webp", version = "0.2.2", default-features = false, optional = true } [dev-dependencies] crc32fast = "1.2.0" @@ -64,7 +65,6 @@ default = ["gif", "jpeg", "ico", "png", "pnm", "tga", "tiff", "webp", "bmp", "hd ico = ["bmp", "png"] pnm = [] tga = [] -webp = [] bmp = [] hdr = ["scoped_threadpool"] dxt = [] @@ -72,6 +72,11 @@ dds = ["dxt"] farbfeld = [] openexr = ["exr"] +# Enables WebP decoder support. +webp = [] +# Non-default, not included in `webp`. Requires native dependency libwebp. +webp-encoder = ["libwebp"] + # Enables multi-threading. # Requires latest stable Rust. jpeg_rayon = ["jpeg/rayon"] diff --git a/src/codecs/webp/encoder.rs b/src/codecs/webp/encoder.rs index c298108c7f..e7acfdf9e4 100644 --- a/src/codecs/webp/encoder.rs +++ b/src/codecs/webp/encoder.rs @@ -7,51 +7,54 @@ use std::io::Write; use libwebp::{Encoder, PixelLayout, WebPMemory}; -use crate::ImageResult; +use crate::error::EncodingError; use crate::{ColorType, ImageEncoder}; +use crate::{ImageError, ImageFormat, ImageResult}; /// WebP Encoder. -pub struct WebpEncoder { +pub struct WebPEncoder { inner: W, - quality: WebpQuality, + quality: WebPQuality, } /// WebP encoder quality. #[derive(Debug, Copy, Clone)] -pub enum WebpQuality { +pub enum WebPQuality { /// Lossless encoding. Lossless, /// Lossy quality from 0 to 100. 0 = low quality, small size; 100 = high quality, large size. Lossy(f32), } -impl WebpQuality { +impl WebPQuality { /// Minimum lossy quality value (0). - pub const MIN: f32 = 0f32; + pub const MIN: f32 = 0_f32; /// Maximum lossy quality value (100). - pub const MAX: f32 = 100f32; + pub const MAX: f32 = 100_f32; + /// Default lossy quality, providing reasonable quality and file size (80). + pub const DEFAULT: f32 = 80_f32; /// Clamps lossy quality between 0 and 100. fn clamp(self) -> Self { match self { - WebpQuality::Lossy(quality) => WebpQuality::Lossy(quality.clamp(Self::MIN, Self::MAX)), + WebPQuality::Lossy(quality) => WebPQuality::Lossy(quality.clamp(Self::MIN, Self::MAX)), lossless => lossless, } } } -impl WebpEncoder { +impl WebPEncoder { /// Create a new encoder that writes its output to `w`. /// /// Defaults to lossy encoding with maximum quality. pub fn new(w: W) -> Self { - WebpEncoder::new_with_quality(w, WebpQuality::Lossy(WebpQuality::MAX)) + WebPEncoder::new_with_quality(w, WebPQuality::Lossy(WebPQuality::DEFAULT)) } /// Create a new encoder with specified quality, that writes its output to `w`. /// /// Lossy qualities are clamped between 0 and 100. - pub fn new_with_quality(w: W, quality: WebpQuality) -> Self { + pub fn new_with_quality(w: W, quality: WebPQuality) -> Self { Self { inner: w, quality: quality.clamp(), @@ -76,25 +79,27 @@ impl WebpEncoder { _ => unimplemented!("Color type not yet supported"), }; - // Call the library to encode the image. + // Call the native libwebp library to encode the image. let encoder = Encoder::new(data, layout, width, height); - let output: WebPMemory = match self.quality { - WebpQuality::Lossless => encoder.encode_lossless(), - WebpQuality::Lossy(quality) => encoder.encode(quality), + let encoded: WebPMemory = match self.quality { + WebPQuality::Lossless => encoder.encode_lossless(), + WebPQuality::Lossy(quality) => encoder.encode(quality), }; - // TODO: how to check if any errors occurred, maybe if the memory is empty? - // Can errors occur? - // let data = result.map_err(|err| { - // ImageError::Encoding(EncodingError::new(ImageFormat::WebP.into(), err)) - // })?; + // TODO: how to check if any errors occurred? Can errors occur? + if encoded.is_empty() { + return Err(ImageError::Encoding(EncodingError::new( + ImageFormat::WebP.into(), + "encoding failed, output empty", + ))); + } - self.inner.write_all(&output)?; + self.inner.write_all(&encoded)?; Ok(()) } } -impl ImageEncoder for WebpEncoder { +impl ImageEncoder for WebPEncoder { fn write_image( self, buf: &[u8], diff --git a/src/codecs/webp/mod.rs b/src/codecs/webp/mod.rs index de183b27cb..b38faedfce 100644 --- a/src/codecs/webp/mod.rs +++ b/src/codecs/webp/mod.rs @@ -1,7 +1,7 @@ //! Decoding and Encoding of WebP Images #[cfg(feature = "webp-encoder")] -pub use self::encoder::{WebpEncoder, WebpQuality}; +pub use self::encoder::{WebPEncoder, WebPQuality}; #[cfg(feature = "webp-encoder")] mod encoder; diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index 1d9683080f..fcb492fe33 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -244,7 +244,7 @@ pub(crate) fn write_buffer_impl( } #[cfg(feature = "webp-encoder")] ImageOutputFormat::WebP => { - webp::WebpEncoder::new(buffered_write).write_image(buf, width, height, color) + webp::WebPEncoder::new(buffered_write).write_image(buf, width, height, color) } image::ImageOutputFormat::Unsupported(msg) => Err(ImageError::Unsupported( From cfc0ea981d3aa5033c9a8b79b33ff080be00ede8 Mon Sep 17 00:00:00 2001 From: Craig Bester Date: Tue, 6 Sep 2022 20:37:20 +0200 Subject: [PATCH 3/8] Change WebPQuality to opaque struct --- src/codecs/webp/encoder.rs | 59 +++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/src/codecs/webp/encoder.rs b/src/codecs/webp/encoder.rs index e7acfdf9e4..01bc530eeb 100644 --- a/src/codecs/webp/encoder.rs +++ b/src/codecs/webp/encoder.rs @@ -8,6 +8,7 @@ use std::io::Write; use libwebp::{Encoder, PixelLayout, WebPMemory}; use crate::error::EncodingError; +use crate::ImageFormat::WebP; use crate::{ColorType, ImageEncoder}; use crate::{ImageError, ImageFormat, ImageResult}; @@ -17,48 +18,54 @@ pub struct WebPEncoder { quality: WebPQuality, } -/// WebP encoder quality. #[derive(Debug, Copy, Clone)] -pub enum WebPQuality { - /// Lossless encoding. +enum Quality { Lossless, - /// Lossy quality from 0 to 100. 0 = low quality, small size; 100 = high quality, large size. - Lossy(f32), + Lossy(u8), } +/// WebP encoder quality. +#[derive(Debug, Copy, Clone)] +pub struct WebPQuality(Quality); + impl WebPQuality { /// Minimum lossy quality value (0). - pub const MIN: f32 = 0_f32; + pub const MIN: u8 = 0; /// Maximum lossy quality value (100). - pub const MAX: f32 = 100_f32; + pub const MAX: u8 = 100; /// Default lossy quality, providing reasonable quality and file size (80). - pub const DEFAULT: f32 = 80_f32; + pub const DEFAULT: u8 = 80; - /// Clamps lossy quality between 0 and 100. - fn clamp(self) -> Self { - match self { - WebPQuality::Lossy(quality) => WebPQuality::Lossy(quality.clamp(Self::MIN, Self::MAX)), - lossless => lossless, - } + /// Lossless encoding. + pub fn lossless() -> Self { + Self(Quality::Lossless) + } + + /// Lossy quality. 0 = low quality, small size; 100 = high quality, large size. + /// + /// Values are clamped from 0 to 100. + pub fn lossy(quality: u8) -> Self { + Self(Quality::lossy(quality.clamp(Self::MIN, Self::MAX))) + } +} + +impl Default for WebPQuality { + fn default() -> Self { + Self::lossy(WebPQuality::DEFAULT) } } impl WebPEncoder { /// Create a new encoder that writes its output to `w`. /// - /// Defaults to lossy encoding with maximum quality. + /// Defaults to lossy encoding, see [`WebPQuality::DEFAULT`]. pub fn new(w: W) -> Self { - WebPEncoder::new_with_quality(w, WebPQuality::Lossy(WebPQuality::DEFAULT)) + WebPEncoder::new_with_quality(w, WebPQuality::default()) } - /// Create a new encoder with specified quality, that writes its output to `w`. - /// - /// Lossy qualities are clamped between 0 and 100. + /// Create a new encoder with the specified quality, that writes its output to `w`. pub fn new_with_quality(w: W, quality: WebPQuality) -> Self { - Self { - inner: w, - quality: quality.clamp(), - } + Self { inner: w, quality } } /// Encode image data with the indicated color type. @@ -81,9 +88,9 @@ impl WebPEncoder { // Call the native libwebp library to encode the image. let encoder = Encoder::new(data, layout, width, height); - let encoded: WebPMemory = match self.quality { - WebPQuality::Lossless => encoder.encode_lossless(), - WebPQuality::Lossy(quality) => encoder.encode(quality), + let encoded: WebPMemory = match self.quality.0 { + Quality::Lossless => encoder.encode_lossless(), + Quality::Lossy(quality) => encoder.encode(quality as f32), }; // TODO: how to check if any errors occurred? Can errors occur? From 809b726aca503003fd2d8c676529c8ac5e37d4cf Mon Sep 17 00:00:00 2001 From: Craig Bester Date: Wed, 7 Sep 2022 21:19:29 +0200 Subject: [PATCH 4/8] Add fuzz tests, fix panics --- .github/workflows/rust.yml | 2 +- README.md | 2 +- src/codecs/webp/encoder.rs | 152 ++++++++++++++++++++++++++++++++----- src/lib.rs | 2 +- 4 files changed, 138 insertions(+), 20 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8bf3f1eea8..d775cd0ecc 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - features: [gif, jpeg, png, tiff, ico, pnm, tga, webp, bmp, hdr, dxt, dds, farbfeld, openexr, jpeg_rayon, ''] + features: [gif, jpeg, png, tiff, ico, pnm, tga, webp, bmp, hdr, dxt, dds, farbfeld, openexr, jpeg_rayon, webp-encoder, ''] # we are using the cross project for cross compilation to mips: # https://github.com/cross-rs/cross diff --git a/README.md b/README.md index af4acd930c..2d13325c88 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ All image processing functions provided operate on types that implement the `Gen | BMP | Yes | Rgb8, Rgba8, Gray8, GrayA8 | | ICO | Yes | Yes | | TIFF | Baseline(no fax support) + LZW + PackBits | Rgb8, Rgba8, Gray8 | -| WebP | Yes | No | +| WebP | Yes | Rgb8, Rgba8 | | AVIF | Only 8-bit | Lossy | | PNM | PBM, PGM, PPM, standard PAM | Yes | | DDS | DXT1, DXT3, DXT5 | No | diff --git a/src/codecs/webp/encoder.rs b/src/codecs/webp/encoder.rs index 01bc530eeb..bf0d82f6f6 100644 --- a/src/codecs/webp/encoder.rs +++ b/src/codecs/webp/encoder.rs @@ -7,8 +7,9 @@ use std::io::Write; use libwebp::{Encoder, PixelLayout, WebPMemory}; -use crate::error::EncodingError; -use crate::ImageFormat::WebP; +use crate::error::{ + EncodingError, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, +}; use crate::{ColorType, ImageEncoder}; use crate::{ImageError, ImageFormat, ImageResult}; @@ -18,22 +19,22 @@ pub struct WebPEncoder { quality: WebPQuality, } +/// WebP encoder quality. +#[derive(Debug, Copy, Clone)] +pub struct WebPQuality(Quality); + #[derive(Debug, Copy, Clone)] enum Quality { Lossless, Lossy(u8), } -/// WebP encoder quality. -#[derive(Debug, Copy, Clone)] -pub struct WebPQuality(Quality); - impl WebPQuality { /// Minimum lossy quality value (0). pub const MIN: u8 = 0; /// Maximum lossy quality value (100). pub const MAX: u8 = 100; - /// Default lossy quality, providing reasonable quality and file size (80). + /// Default lossy quality (80), providing a balance of quality and file size. pub const DEFAULT: u8 = 80; /// Lossless encoding. @@ -41,11 +42,11 @@ impl WebPQuality { Self(Quality::Lossless) } - /// Lossy quality. 0 = low quality, small size; 100 = high quality, large size. + /// Lossy encoding. 0 = low quality, small size; 100 = high quality, large size. /// /// Values are clamped from 0 to 100. pub fn lossy(quality: u8) -> Self { - Self(Quality::lossy(quality.clamp(Self::MIN, Self::MAX))) + Self(Quality::Lossy(quality.clamp(Self::MIN, Self::MAX))) } } @@ -70,8 +71,7 @@ impl WebPEncoder { /// Encode image data with the indicated color type. /// - /// The encoder requires all data to be RGB8 or RGBA8, it will be converted - /// internally if necessary. + /// The encoder requires image data be Rgb8 or Rgba8. pub fn encode( mut self, data: &[u8], @@ -79,13 +79,28 @@ impl WebPEncoder { height: u32, color: ColorType, ) -> ImageResult<()> { - // TODO: convert color types internally. - let layout: PixelLayout = match color { - ColorType::Rgb8 => PixelLayout::Rgb, - ColorType::Rgba8 => PixelLayout::Rgba, - _ => unimplemented!("Color type not yet supported"), + // TODO: convert color types internally? + let (layout, stride) = match color { + ColorType::Rgb8 => (PixelLayout::Rgb, 3), + ColorType::Rgba8 => (PixelLayout::Rgba, 4), + _ => { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::WebP.into(), + UnsupportedErrorKind::Color(color.into()), + ), + )) + } }; + // Validate dimensions upfront to avoid panics. + let expected_len = stride * (width * height) as u64; + if expected_len > data.len() as u64 { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::DimensionMismatch, + ))); + } + // Call the native libwebp library to encode the image. let encoder = Encoder::new(data, layout, width, height); let encoded: WebPMemory = match self.quality.0 { @@ -93,7 +108,7 @@ impl WebPEncoder { Quality::Lossy(quality) => encoder.encode(quality as f32), }; - // TODO: how to check if any errors occurred? Can errors occur? + // The simple encoding API in libwebp does not return errors. if encoded.is_empty() { return Err(ImageError::Encoding(EncodingError::new( ImageFormat::WebP.into(), @@ -117,3 +132,106 @@ impl ImageEncoder for WebPEncoder { self.encode(buf, width, height, color_type) } } + +#[cfg(test)] +mod tests { + use crate::codecs::webp::{WebPEncoder, WebPQuality}; + use crate::{ColorType, ImageEncoder}; + + #[test] + fn webp_lossless_deterministic() { + // 1x1 8-bit image buffer containing a single red pixel. + let rgb: &[u8] = &[255, 0, 0]; + let rgba: &[u8] = &[255, 0, 0, 128]; + for (color, img, expected) in [ + ( + ColorType::Rgb8, + rgb, + [ + 82, 73, 70, 70, 28, 0, 0, 0, 87, 69, 66, 80, 86, 80, 56, 76, 15, 0, 0, 0, 47, + 0, 0, 0, 0, 7, 16, 253, 143, 254, 7, 34, 162, 255, 1, 0, + ], + ), + ( + ColorType::Rgba8, + rgba, + [ + 82, 73, 70, 70, 28, 0, 0, 0, 87, 69, 66, 80, 86, 80, 56, 76, 15, 0, 0, 0, 47, + 0, 0, 0, 16, 7, 16, 253, 143, 2, 6, 34, 162, 255, 1, 0, + ], + ), + ] { + // Encode it into a memory buffer. + let mut encoded_img = Vec::new(); + { + let encoder = + WebPEncoder::new_with_quality(&mut encoded_img, WebPQuality::lossless()); + encoder + .write_image(&img, 1, 1, color) + .expect("image encoding failed"); + } + + // WebP encoding should be deterministic. + assert_eq!(encoded_img, expected); + } + } + + #[derive(Debug, Clone)] + struct MockImage { + width: u32, + height: u32, + color: ColorType, + data: Vec, + } + + impl quickcheck::Arbitrary for MockImage { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + // Limit to small, non-empty images <= 512x512. + let width = u32::arbitrary(g) % 512 + 1; + let height = u32::arbitrary(g) % 512 + 1; + let (color, stride) = if bool::arbitrary(g) { + (ColorType::Rgb8, 3) + } else { + (ColorType::Rgba8, 4) + }; + let size = width * height * stride; + let data: Vec = (0..size).map(|_| u8::arbitrary(g)).collect(); + MockImage { + width, + height, + color, + data, + } + } + } + + quickcheck! { + fn fuzz_webp_valid_image(image: MockImage, quality: u8) -> bool { + // Check valid images do not panic. + let mut buffer = Vec::::new(); + for webp_quality in [WebPQuality::lossless(), WebPQuality::lossy(quality)] { + buffer.clear(); + let encoder = WebPEncoder::new_with_quality(&mut buffer, webp_quality); + if !encoder + .write_image(&image.data, image.width, image.height, image.color) + .is_ok() { + return false; + } + } + true + } + + fn fuzz_webp_no_panic(data: Vec, width: u8, height: u8, quality: u8) -> bool { + // Check random (usually invalid) parameters do not panic. + let mut buffer = Vec::::new(); + for webp_quality in [WebPQuality::lossless(), WebPQuality::lossy(quality)] { + buffer.clear(); + let encoder = WebPEncoder::new_with_quality(&mut buffer, webp_quality); + // Ignore errors. + let _ = encoder + .write_image(&image.data, image.width, image.height, image.color); + } + true + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 0417e56854..8fe29330cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -185,7 +185,7 @@ pub mod flat; /// | BMP | Yes | Rgb8, Rgba8, Gray8, GrayA8 | /// | ICO | Yes | Yes | /// | TIFF | Baseline(no fax support) + LZW + PackBits | Rgb8, Rgba8, Gray8 | -/// | WebP | Yes | No | +/// | WebP | Yes | Rgb8, Rgba8 | /// | AVIF | Only 8-bit | Lossy | /// | PNM | PBM, PGM, PPM, standard PAM | Yes | /// | DDS | DXT1, DXT3, DXT5 | No | From ce5c8cacb92a616a17f0baec1e0ac63aca4bbdaa Mon Sep 17 00:00:00 2001 From: Craig Bester Date: Wed, 7 Sep 2022 21:22:57 +0200 Subject: [PATCH 5/8] Undo reordering of modules --- src/codecs/webp/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/codecs/webp/mod.rs b/src/codecs/webp/mod.rs index b38faedfce..f9f10b595a 100644 --- a/src/codecs/webp/mod.rs +++ b/src/codecs/webp/mod.rs @@ -12,7 +12,7 @@ pub use self::decoder::WebPDecoder; #[cfg(feature = "webp")] mod decoder; #[cfg(feature = "webp")] -mod extended; +mod transform; #[cfg(feature = "webp")] mod huffman; #[cfg(feature = "webp")] @@ -22,7 +22,7 @@ mod lossless; #[cfg(feature = "webp")] mod lossless_transform; #[cfg(feature = "webp")] -mod transform; +mod extended; #[cfg(feature = "webp")] pub mod vp8; From 9d808727d9d9a43bfeaa31624bff5b4b1c331fc9 Mon Sep 17 00:00:00 2001 From: Craig Bester Date: Wed, 7 Sep 2022 21:27:56 +0200 Subject: [PATCH 6/8] Fix fuzz test --- src/codecs/webp/encoder.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/codecs/webp/encoder.rs b/src/codecs/webp/encoder.rs index bf0d82f6f6..e20c55f8b4 100644 --- a/src/codecs/webp/encoder.rs +++ b/src/codecs/webp/encoder.rs @@ -224,12 +224,14 @@ mod tests { fn fuzz_webp_no_panic(data: Vec, width: u8, height: u8, quality: u8) -> bool { // Check random (usually invalid) parameters do not panic. let mut buffer = Vec::::new(); - for webp_quality in [WebPQuality::lossless(), WebPQuality::lossy(quality)] { - buffer.clear(); - let encoder = WebPEncoder::new_with_quality(&mut buffer, webp_quality); - // Ignore errors. - let _ = encoder - .write_image(&image.data, image.width, image.height, image.color); + for color in [ColorType::Rgb8, ColorType::Rgba8] { + for webp_quality in [WebPQuality::lossless(), WebPQuality::lossy(quality)] { + buffer.clear(); + let encoder = WebPEncoder::new_with_quality(&mut buffer, webp_quality); + // Ignore errors. + let _ = encoder + .write_image(&data, width as u32, height as u32, color); + } } true } From 08029aa90593cd788489aeecaddd4fb245db66ad Mon Sep 17 00:00:00 2001 From: Craig Bester Date: Wed, 7 Sep 2022 21:33:02 +0200 Subject: [PATCH 7/8] Format imports --- src/codecs/webp/encoder.rs | 3 +-- src/codecs/webp/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/codecs/webp/encoder.rs b/src/codecs/webp/encoder.rs index e20c55f8b4..6ecef9364d 100644 --- a/src/codecs/webp/encoder.rs +++ b/src/codecs/webp/encoder.rs @@ -10,8 +10,7 @@ use libwebp::{Encoder, PixelLayout, WebPMemory}; use crate::error::{ EncodingError, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, }; -use crate::{ColorType, ImageEncoder}; -use crate::{ImageError, ImageFormat, ImageResult}; +use crate::{ColorType, ImageEncoder, ImageError, ImageFormat, ImageResult}; /// WebP Encoder. pub struct WebPEncoder { diff --git a/src/codecs/webp/mod.rs b/src/codecs/webp/mod.rs index f9f10b595a..b38faedfce 100644 --- a/src/codecs/webp/mod.rs +++ b/src/codecs/webp/mod.rs @@ -12,7 +12,7 @@ pub use self::decoder::WebPDecoder; #[cfg(feature = "webp")] mod decoder; #[cfg(feature = "webp")] -mod transform; +mod extended; #[cfg(feature = "webp")] mod huffman; #[cfg(feature = "webp")] @@ -22,7 +22,7 @@ mod lossless; #[cfg(feature = "webp")] mod lossless_transform; #[cfg(feature = "webp")] -mod extended; +mod transform; #[cfg(feature = "webp")] pub mod vp8; From 708cdec502bb6a1c4c14b2b8e820950392328537 Mon Sep 17 00:00:00 2001 From: Craig Bester Date: Wed, 7 Sep 2022 22:34:53 +0200 Subject: [PATCH 8/8] Add webp-encoder feature to GitHub Actions test --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d775cd0ecc..01bbd3284d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: rust: ["1.56.1", stable, beta, nightly] - features: [gif, jpeg, png, tiff, ico, pnm, tga, webp, bmp, hdr, dxt, dds, farbfeld, openexr, jpeg_rayon, ''] + features: [gif, jpeg, png, tiff, ico, pnm, tga, webp, bmp, hdr, dxt, dds, farbfeld, openexr, jpeg_rayon, webp-encoder, ''] steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1