diff --git a/.gitignore b/.gitignore index c425a0fd3..f93d076b5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,4 @@ Cargo.lock test.png .idea -.DS_Store -rust-toolchain.toml \ No newline at end of file +.DS_Store \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 200b097e2..1acc52579 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ homepage = "https://github.com/jay3332/ril" readme = "README.md" keywords = ["ril", "imaging", "image", "processing", "editing"] categories = ["encoding", "graphics", "multimedia", "visualization"] +rust-version = "1.67" [dependencies] num-traits = "0.2" @@ -22,11 +23,15 @@ libwebp-sys2 = { version = "^0.1", features = ["1_2", "mux", "demux"], optional fontdue = { version = "^0.7", optional = true } color_quant = { version = "^1.1", optional = true } colorgrad = { version = "^0.6", optional = true, default_features = false } +# todo: switch back to crates release once +# https://github.com/image-rs/image-webp/commit/4020925b7002bac88cda9f951eb725f6a7fcd3d8 +# is released +image-webp = { git = "https://github.com/image-rs/image-webp", optional = true } [features] default = ["resize", "text", "quantize", "gradient"] -all-pure = ["resize", "png", "jpeg", "gif", "text", "quantize"] -all = ["all-pure", "webp"] +all-pure = ["resize", "png", "jpeg", "gif", "text", "quantize", "webp-pure"] +all = ["resize", "png", "jpeg", "gif", "text", "quantize", "webp"] png = ["dep:png"] jpeg = ["dep:jpeg-decoder", "dep:jpeg-encoder"] gif = ["dep:gif"] @@ -36,10 +41,11 @@ text = ["dep:fontdue"] quantize = ["dep:color_quant"] gradient = ["dep:colorgrad"] static = ["libwebp-sys2?/static"] +webp-pure = ["dep:image-webp"] [dev-dependencies] criterion = "^0.4" -image = "^0" +image = "^0.24" imageproc = "^0.23" rusttype = "^0.9" diff --git a/README.md b/README.md index 652e84170..ab95e9b77 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Additionally, we also plan to support the following pixel formats: have actual support 16-bit pixel formats in the future. ## Requirements -MSRV (Minimum Supported Rust Version) is v1.61.0. +MSRV (Minimum Supported Rust Version) is v1.67.1. ## Installation Add the following to your `Cargo.toml` dependencies: @@ -128,12 +128,12 @@ image format support, but adds a lot of dependencies you may not need. For every image encoding that requires a dependency, a corresponding feature can be enabled for it: -| Encoding | Feature | Dependencies | Default? | -|--------------|---------|--------------------------------|----------| -| PNG and APNG | `png` | `png` | no | -| JPEG | `jpeg` | `jpeg-decoder`, `jpeg-encoder` | no | -| GIF | `gif` | `gif` | no | -| WebP | `webp` | `libwebp-sys2` | no | +| Encoding | Feature | Dependencies | Default? | +|--------------|-----------------------|--------------------------------|----------| +| PNG and APNG | `png` | `png` | no | +| JPEG | `jpeg` | `jpeg-decoder`, `jpeg-encoder` | no | +| GIF | `gif` | `gif` | no | +| WebP | `webp` or `webp-pure` | `libwebp-sys2` or `image-webp` | no | Other features: diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..80569f0e2 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.67.1" +components = ["clippy", "rustfmt", "rust-src"] +targets = ["wasm32-unknown-unknown"] diff --git a/src/encodings/mod.rs b/src/encodings/mod.rs index 37b1ee8f4..ee47d81db 100644 --- a/src/encodings/mod.rs +++ b/src/encodings/mod.rs @@ -8,6 +8,8 @@ pub mod jpeg; pub mod png; #[cfg(feature = "webp")] pub mod webp; +#[cfg(feature = "webp-pure")] +pub mod webp_pure; /// Represents an arbitrary color type. Note that this does not store the bit-depth or the type used /// to store the value of each channel, although it can specify the number of channels. diff --git a/src/encodings/webp_pure.rs b/src/encodings/webp_pure.rs new file mode 100644 index 000000000..e2dbf5fae --- /dev/null +++ b/src/encodings/webp_pure.rs @@ -0,0 +1,221 @@ +use crate::{ + encode, ColorType, Decoder, Encoder, Frame, FrameIterator, Image, ImageFormat, OverlayMode, + Pixel, +}; +use std::{ + io::{Cursor, Read, Write}, + marker::PhantomData, + num::NonZeroU32, + result::Result, + time::Duration, +}; + +#[derive(Copy, Clone, Debug, PartialEq, Default)] +pub struct WebPEncoderOptions {} + +pub struct WebPStaticEncoder { + native_color_type: ColorType, + writer: W, + marker: PhantomData

, +} + +impl Encoder for WebPStaticEncoder { + type Config = WebPEncoderOptions; + + fn new( + dest: W, + metadata: impl encode::HasEncoderMetadata, + ) -> crate::Result { + Ok(Self { + native_color_type: metadata.color_type(), + writer: dest, + marker: PhantomData, + }) + } + + fn add_frame(&mut self, frame: &impl encode::FrameLike

) -> crate::Result<()> { + let data = frame + .image() + .data + .iter() + .flat_map(P::as_bytes) + .collect::>(); + let encoder = image_webp::WebPEncoder::new(self.writer.by_ref()); + + encoder + .encode( + &data, + frame.image().width.into(), + frame.image().height.into(), + match self.native_color_type { + ColorType::L => image_webp::ColorType::L8, + ColorType::LA => image_webp::ColorType::La8, + ColorType::Rgb => image_webp::ColorType::Rgb8, + ColorType::Rgba => image_webp::ColorType::Rgba8, + _ => unreachable!(), + }, + ) + .map_err(|e| crate::Error::EncodingError(e.to_string()))?; + Ok(()) + } + + // no-op + fn finish(self) -> crate::Result<()> { + Ok(()) + } +} + +pub struct WebPDecoder { + marker: PhantomData<(P, R)>, +} + +impl Default for WebPDecoder { + fn default() -> Self { + Self::new() + } +} + +impl WebPDecoder { + #[must_use] + pub const fn new() -> Self { + Self { + marker: PhantomData, + } + } +} + +impl Decoder for WebPDecoder { + type Sequence = WebPSequenceDecoder

; + + fn decode(&mut self, stream: R) -> crate::Result> { + let mut decoder = image_webp::WebPDecoder::new(Cursor::new( + stream.bytes().collect::, _>>()?, + )) + .map_err(|e| crate::Error::DecodingError(e.to_string()))?; + + let mut image_buf: Vec = create_image_buffer(&decoder); + decoder + .read_image(&mut image_buf) + .map_err(|e| crate::Error::DecodingError(e.to_string()))?; + + let (width, height) = decoder.dimensions(); + + let data = image_buf_to_pixeldata(&decoder, image_buf).unwrap(); + + Ok(Image { + width: NonZeroU32::new(width).unwrap(), + height: NonZeroU32::new(height).unwrap(), + data, + format: ImageFormat::WebP, + overlay: OverlayMode::default(), + palette: None, + }) + } + + fn decode_sequence(&mut self, stream: R) -> crate::Result { + let decoder = image_webp::WebPDecoder::new(Cursor::new( + stream.bytes().collect::, _>>()?, + )) + .map_err(|e| crate::Error::DecodingError(e.to_string()))?; + + Ok(WebPSequenceDecoder::

{ + marker: PhantomData, + decoder, + }) + } +} + +pub struct WebPSequenceDecoder { + marker: PhantomData

, + decoder: image_webp::WebPDecoder>>, +} + +impl FrameIterator

for WebPSequenceDecoder

{ + fn len(&self) -> u32 { + image_webp::WebPDecoder::num_frames(&self.decoder) + } + + fn is_empty(&self) -> bool { + self.len() == 0 + } + + fn loop_count(&self) -> crate::LoopCount { + match image_webp::WebPDecoder::loop_count(&self.decoder) { + image_webp::LoopCount::Forever => crate::LoopCount::Infinite, + image_webp::LoopCount::Times(n) => { + crate::LoopCount::Exactly((Into::::into(n)) as u32) + } + } + } + + fn into_sequence(self) -> crate::Result> + where + Self: Sized, + { + let loop_count = self.loop_count(); + let frames = self.collect::>>()?; + + Ok(crate::ImageSequence::from_frames(frames).with_loop_count(loop_count)) + } +} + +impl Iterator for WebPSequenceDecoder

{ + type Item = crate::Result>; + + fn next(&mut self) -> Option { + let mut image_buf: Vec = create_image_buffer(&self.decoder); + let (width, height) = self.decoder.dimensions(); + + let frame = self.decoder.read_frame(&mut image_buf); + + match frame { + Err(image_webp::DecodingError::NoMoreFrames) => return None, + Err(_) | Ok(_) => (), + } + + let data = image_buf_to_pixeldata(&self.decoder, image_buf).unwrap(); + + let frame_duration = self.decoder.loop_duration() / self.decoder.num_frames() as u64; + + let frame = Frame::from_image(Image { + width: NonZeroU32::new(width as _).unwrap(), + height: NonZeroU32::new(height as _).unwrap(), + data, + format: ImageFormat::WebP, + overlay: OverlayMode::default(), + palette: None, + }) + .with_delay(Duration::from_millis(frame_duration)) + .with_disposal(crate::DisposalMethod::Background); + Some(Ok(frame)) + } +} + +/// Creates a preallocated [Vec] for the decoder to write to. +fn create_image_buffer(decoder: &image_webp::WebPDecoder>>) -> Vec { + let image_buf_len = decoder + .output_buffer_size() + .ok_or(crate::Error::DecodingError( + "Failed to preallocate buffer for image data".to_string(), + )) + .unwrap(); + vec![0; image_buf_len] +} + +/// Converts the imagebuf from [create_image_buffer()] into a [Result>]. +fn image_buf_to_pixeldata( + decoder: &image_webp::WebPDecoder>>, + image_buf: Vec, +) -> crate::Result> { + let (color_type, pixel_bytes) = if decoder.has_alpha() { + (ColorType::Rgba, 4) + } else { + (ColorType::Rgb, 3) + }; + + image_buf + .as_slice() + .chunks_exact(pixel_bytes) + .map(|chunk| P::from_raw_parts(color_type, 8, chunk)) + .collect::>>() +} diff --git a/src/format.rs b/src/format.rs index 02b6110c3..58cd29e2d 100644 --- a/src/format.rs +++ b/src/format.rs @@ -18,9 +18,14 @@ use crate::encodings::jpeg; use crate::encodings::png; #[cfg(feature = "webp")] use crate::encodings::webp; +#[cfg(feature = "webp-pure")] +use crate::encodings::webp_pure; #[cfg(any(feature = "png", feature = "gif", feature = "jpeg", feature = "webp"))] use crate::{Decoder, Encoder}; +#[cfg(all(feature = "webp-pure", feature = "webp", not(doc)))] +compile_error!("features `ril/webp-pure` and `ril/webp` are mutually exclusive"); + /// Represents the underlying encoding format of an image. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ImageFormat { @@ -167,6 +172,8 @@ impl ImageFormat { Self::Gif => gif::GifEncoder::encode_static(image, dest), #[cfg(feature = "webp")] Self::WebP => webp::WebPStaticEncoder::encode_static(image, dest), + #[cfg(feature = "webp-pure")] + Self::WebP => webp_pure::WebPStaticEncoder::encode_static(image, dest), _ => panic!( "No encoder implementation is found for this image format. \ Did you forget to enable the feature?" @@ -229,6 +236,8 @@ impl ImageFormat { Self::Gif => gif::GifDecoder::new().decode(stream), #[cfg(feature = "webp")] Self::WebP => webp::WebPDecoder::default().decode(stream), + #[cfg(feature = "webp-pure")] + Self::WebP => webp_pure::WebPDecoder::default().decode(stream), _ => panic!( "No encoder implementation is found for this image format. \ Did you forget to enable the feature?" @@ -261,6 +270,8 @@ impl ImageFormat { Self::Gif => Box::new(gif::GifDecoder::new().decode_sequence(stream)?), #[cfg(feature = "webp")] Self::WebP => Box::new(webp::WebPDecoder::default().decode_sequence(stream)?), + #[cfg(feature = "webp-pure")] + Self::WebP => Box::new(webp_pure::WebPDecoder::default().decode_sequence(stream)?), _ => panic!( "No encoder implementation is found for this image format. \ Did you forget to enable the feature?" diff --git a/tests/animated_lossless.webp b/tests/animated_lossless.webp new file mode 100644 index 000000000..97c30366e Binary files /dev/null and b/tests/animated_lossless.webp differ diff --git a/tests/animated_lossy.webp b/tests/animated_lossy.webp new file mode 100644 index 000000000..e19781029 Binary files /dev/null and b/tests/animated_lossy.webp differ diff --git a/tests/animated_sample.webp b/tests/animated_sample.webp deleted file mode 100644 index d0d3524a2..000000000 Binary files a/tests/animated_sample.webp and /dev/null differ diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 000000000..a43f02e15 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,16 @@ +use ril::prelude::*; + +pub const COLORS: [Rgb; 12] = [ + Rgb::new(255, 0, 0), + Rgb::new(255, 128, 0), + Rgb::new(255, 255, 0), + Rgb::new(128, 255, 0), + Rgb::new(0, 255, 0), + Rgb::new(0, 255, 128), + Rgb::new(0, 255, 255), + Rgb::new(0, 128, 255), + Rgb::new(0, 0, 255), + Rgb::new(128, 0, 255), + Rgb::new(255, 0, 255), + Rgb::new(255, 0, 128), +]; diff --git a/tests/out/webp_pure_encode_output.webp b/tests/out/webp_pure_encode_output.webp new file mode 100644 index 000000000..51426b969 Binary files /dev/null and b/tests/out/webp_pure_encode_output.webp differ diff --git a/tests/reference/random_lossless-1.png b/tests/reference/random_lossless-1.png new file mode 100644 index 000000000..4af7ed5af Binary files /dev/null and b/tests/reference/random_lossless-1.png differ diff --git a/tests/reference/random_lossless-2.png b/tests/reference/random_lossless-2.png new file mode 100644 index 000000000..0c4acfdf6 Binary files /dev/null and b/tests/reference/random_lossless-2.png differ diff --git a/tests/reference/random_lossless-3.png b/tests/reference/random_lossless-3.png new file mode 100644 index 000000000..1b8f35b5c Binary files /dev/null and b/tests/reference/random_lossless-3.png differ diff --git a/tests/reference/random_lossy-1.png b/tests/reference/random_lossy-1.png new file mode 100644 index 000000000..b5c5fc2af Binary files /dev/null and b/tests/reference/random_lossy-1.png differ diff --git a/tests/reference/random_lossy-2.png b/tests/reference/random_lossy-2.png new file mode 100644 index 000000000..8c598b604 Binary files /dev/null and b/tests/reference/random_lossy-2.png differ diff --git a/tests/reference/random_lossy-3.png b/tests/reference/random_lossy-3.png new file mode 100644 index 000000000..f9ed3b014 Binary files /dev/null and b/tests/reference/random_lossy-3.png differ diff --git a/tests/test_gif.rs b/tests/test_gif.rs index 4a2480c95..7d40f40a9 100644 --- a/tests/test_gif.rs +++ b/tests/test_gif.rs @@ -1,8 +1,8 @@ -mod test_png; +mod common; +use common::COLORS; use ril::prelude::*; use std::time::Duration; -use test_png::COLORS; #[test] fn test_gif_encode() -> ril::Result<()> { diff --git a/tests/test_png.rs b/tests/test_png.rs index 51b15adb7..24b139153 100644 --- a/tests/test_png.rs +++ b/tests/test_png.rs @@ -1,21 +1,8 @@ +mod common; +use common::COLORS; use ril::prelude::*; use std::time::Duration; -pub const COLORS: [Rgb; 12] = [ - Rgb::new(255, 0, 0), - Rgb::new(255, 128, 0), - Rgb::new(255, 255, 0), - Rgb::new(128, 255, 0), - Rgb::new(0, 255, 0), - Rgb::new(0, 255, 128), - Rgb::new(0, 255, 255), - Rgb::new(0, 128, 255), - Rgb::new(0, 0, 255), - Rgb::new(128, 0, 255), - Rgb::new(255, 0, 255), - Rgb::new(255, 0, 128), -]; - #[test] fn test_static_png() -> ril::Result<()> { let image = Image::::open("tests/sample.png")?; diff --git a/tests/test_webp-pure.rs b/tests/test_webp-pure.rs new file mode 100644 index 000000000..fafde230f --- /dev/null +++ b/tests/test_webp-pure.rs @@ -0,0 +1,63 @@ +#![cfg(feature = "webp-pure")] + +mod common; + +use ril::prelude::*; + +#[test] +fn test_static_webp_encode() -> ril::Result<()> { + let image = Image::from_fn(256, 256, |x, _| L(x as u8)); + + image.save_inferred("tests/out/webp_pure_encode_output.webp") +} + +#[test] +fn test_static_webp_decode() -> ril::Result<()> { + let image = Image::::open("tests/sample.webp")?; + + assert_eq!(image.dimensions(), (256, 256)); + assert_eq!(image.pixel(0, 0), &Rgb::new(255, 0, 0)); + + Ok(()) +} + +#[test] +fn test_animated_webp_lossless() -> ril::Result<()> { + for (i, frame) in ImageSequence::::open("tests/animated_lossless.webp")?.enumerate() { + let frame = frame?.into_image(); + + let reference = + Image::::open(format!("tests/reference/random_lossless-{}.png", i + 1))?; + + frame.pixels().zip(reference.pixels()).for_each(|(a, b)| { + assert_eq!(a, b); + }); + } + + Ok(()) +} + +#[test] +fn test_animated_webp_lossy() -> ril::Result<()> { + for (i, frame) in ImageSequence::::open("tests/animated_lossy.webp")?.enumerate() { + let frame = frame?.into_image(); + + let reference = Image::::open(format!("tests/reference/random_lossy-{}.png", i + 1))?; + + let (width, height) = frame.dimensions(); + + // https://github.com/image-rs/image-webp/blob/4020925b7002bac88cda9f951eb725f6a7fcd3d8/tests/decode.rs#L56-L59 + let num_bytes_different = frame + .pixels() + .zip(reference.pixels()) + .filter(|(a, b)| a != b) + .count(); + + assert!( + 100 * num_bytes_different / ((width * height) as usize) < 5, + "More than 5% of pixels differ" + ); + } + + Ok(()) +} diff --git a/tests/test_webp.rs b/tests/test_webp.rs index a2941a271..7ffe80e21 100644 --- a/tests/test_webp.rs +++ b/tests/test_webp.rs @@ -1,8 +1,9 @@ -mod test_png; +#![cfg(feature = "webp")] +mod common; +use common::COLORS; use ril::prelude::*; use std::time::Duration; -use test_png::COLORS; #[test] fn test_static_webp_encode() -> ril::Result<()> { @@ -35,13 +36,31 @@ fn test_static_webp_decode() -> ril::Result<()> { } #[test] -fn test_animated_webp_decode() -> ril::Result<()> { - for (frame, ref color) in ImageSequence::::open("tests/animated_sample.webp")?.zip(COLORS) - { +fn test_animated_webp_lossless() -> ril::Result<()> { + for (i, frame) in ImageSequence::::open("tests/animated_lossless.webp")?.enumerate() { let frame = frame?.into_image(); - assert_eq!(frame.dimensions(), (256, 256)); - assert_eq!(frame.pixel(0, 0), color); + let reference = + Image::::open(format!("tests/reference/random_lossless-{}.png", i + 1))?; + + frame.pixels().zip(reference.pixels()).for_each(|(a, b)| { + assert_eq!(a, b); + }); + } + + Ok(()) +} + +#[test] +fn test_animated_webp_lossy() -> ril::Result<()> { + for (i, frame) in ImageSequence::::open("tests/animated_lossy.webp")?.enumerate() { + let frame = frame?.into_image(); + + let reference = Image::::open(format!("tests/reference/random_lossy-{}.png", i + 1))?; + + frame.pixels().zip(reference.pixels()).for_each(|(a, b)| { + assert_eq!(a, b); + }); } Ok(())