diff options
author | Valentin Popov <valentin@popov.link> | 2024-01-08 00:21:28 +0300 |
---|---|---|
committer | Valentin Popov <valentin@popov.link> | 2024-01-08 00:21:28 +0300 |
commit | 1b6a04ca5504955c571d1c97504fb45ea0befee4 (patch) | |
tree | 7579f518b23313e8a9748a88ab6173d5e030b227 /vendor/image/src/codecs/webp/encoder.rs | |
parent | 5ecd8cf2cba827454317368b68571df0d13d7842 (diff) | |
download | fparkan-1b6a04ca5504955c571d1c97504fb45ea0befee4.tar.xz fparkan-1b6a04ca5504955c571d1c97504fb45ea0befee4.zip |
Initial vendor packages
Signed-off-by: Valentin Popov <valentin@popov.link>
Diffstat (limited to 'vendor/image/src/codecs/webp/encoder.rs')
-rw-r--r-- | vendor/image/src/codecs/webp/encoder.rs | 242 |
1 files changed, 242 insertions, 0 deletions
diff --git a/vendor/image/src/codecs/webp/encoder.rs b/vendor/image/src/codecs/webp/encoder.rs new file mode 100644 index 0000000..0383046 --- /dev/null +++ b/vendor/image/src/codecs/webp/encoder.rs @@ -0,0 +1,242 @@ +//! 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::error::{ + EncodingError, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, +}; +use crate::flat::SampleLayout; +use crate::{ColorType, ImageEncoder, ImageError, ImageFormat, ImageResult}; + +/// WebP Encoder. +pub struct WebPEncoder<W> { + inner: W, + quality: WebPQuality, +} + +/// WebP encoder quality. +#[derive(Debug, Copy, Clone)] +pub struct WebPQuality(Quality); + +#[derive(Debug, Copy, Clone)] +enum Quality { + Lossless, + Lossy(u8), +} + +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 (80), providing a balance of quality and file size. + pub const DEFAULT: u8 = 80; + + /// Lossless encoding. + pub fn lossless() -> Self { + Self(Quality::Lossless) + } + + /// 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))) + } +} + +impl Default for WebPQuality { + fn default() -> Self { + Self::lossy(WebPQuality::DEFAULT) + } +} + +impl<W: Write> WebPEncoder<W> { + /// Create a new encoder that writes its output to `w`. + /// + /// Defaults to lossy encoding, see [`WebPQuality::DEFAULT`]. + pub fn new(w: W) -> Self { + WebPEncoder::new_with_quality(w, WebPQuality::default()) + } + + /// 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 } + } + + /// Encode image data with the indicated color type. + /// + /// The encoder requires image data be Rgb8 or Rgba8. + pub fn encode( + mut self, + data: &[u8], + width: u32, + height: u32, + color: ColorType, + ) -> ImageResult<()> { + // TODO: convert color types internally? + let layout = match color { + ColorType::Rgb8 => PixelLayout::Rgb, + ColorType::Rgba8 => PixelLayout::Rgba, + _ => { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::WebP.into(), + UnsupportedErrorKind::Color(color.into()), + ), + )) + } + }; + + // Validate dimensions upfront to avoid panics. + if width == 0 + || height == 0 + || !SampleLayout::row_major_packed(color.channel_count(), width, height) + .fits(data.len()) + { + 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 { + Quality::Lossless => encoder.encode_lossless(), + Quality::Lossy(quality) => encoder.encode(quality as f32), + }; + + // The simple encoding API in libwebp does not return errors. + if encoded.is_empty() { + return Err(ImageError::Encoding(EncodingError::new( + ImageFormat::WebP.into(), + "encoding failed, output empty", + ))); + } + + self.inner.write_all(&encoded)?; + Ok(()) + } +} + +impl<W: Write> ImageEncoder for WebPEncoder<W> { + fn write_image( + self, + buf: &[u8], + width: u32, + height: u32, + color_type: ColorType, + ) -> ImageResult<()> { + 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<u8>, + } + + 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<u8> = (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::<u8>::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<u8>, width: u8, height: u8, quality: u8) -> bool { + // Check random (usually invalid) parameters do not panic. + let mut buffer = Vec::<u8>::new(); + 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 + } + } +} |