diff --git a/Cargo.toml b/Cargo.toml index 05e12aee5..3ff770b5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ril" authors = ["jay3332"] -version = "0.10.0" +version = "0.11.0" license = "MIT" edition = "2021" description = "Rust Imaging Library: A performant and high-level image processing crate for Rust" @@ -22,12 +22,14 @@ 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 } +qoi = { version = "^0.4", optional = true } [features] default = ["resize", "text", "quantize", "gradient"] -all-pure = ["resize", "png", "jpeg", "gif", "text", "quantize"] +all-pure = ["resize", "png", "qoi", "jpeg", "gif", "text", "quantize"] all = ["all-pure", "webp"] png = ["dep:png"] +qoi = ["dep:qoi"] jpeg = ["dep:jpeg-decoder", "dep:jpeg-encoder"] gif = ["dep:gif"] webp = ["dep:libwebp-sys2"] diff --git a/src/encodings/mod.rs b/src/encodings/mod.rs index 37b1ee8f4..e40b83a83 100644 --- a/src/encodings/mod.rs +++ b/src/encodings/mod.rs @@ -6,6 +6,8 @@ pub mod gif; pub mod jpeg; #[cfg(feature = "png")] pub mod png; +#[cfg(feature = "qoi")] +pub mod qoi; #[cfg(feature = "webp")] pub mod webp; diff --git a/src/encodings/qoi.rs b/src/encodings/qoi.rs new file mode 100644 index 000000000..5fbb2ab4a --- /dev/null +++ b/src/encodings/qoi.rs @@ -0,0 +1,93 @@ +use crate::encode::{FrameLike, HasEncoderMetadata}; +use crate::{ + pixel::Dynamic, ColorType, Decoder, Encoder, Image, Pixel, SingleFrameIterator, +}; +use core::marker::PhantomData; +use qoi::{Channels, ColorSpace, Decoder as QDecoder, Encoder as QEncoder}; +use std::io::{Read, Write}; + +impl From for ColorType { + fn from(value: Channels) -> ColorType { + match value { + Channels::Rgb => ColorType::Rgb, + Channels::Rgba => ColorType::Rgba, + } + } +} + +/// A QOI encoder interface over [`qoi::Encoder`]. +pub struct QoiEncoder { + config: ColorSpace, + writer: W, + _marker: PhantomData

, +} + +impl Encoder for QoiEncoder { + type Config = ColorSpace; + + fn new(writer: W, metadata: impl HasEncoderMetadata) -> crate::Result { + Ok(Self { + config: metadata.config(), + writer, + _marker: PhantomData, + }) + } + + fn add_frame(&mut self, frame: &impl FrameLike

) -> crate::Result<()> { + let image = frame.image(); + let data = image.data.iter(); + // Convert the pixels to RGB or RGBA, then to bytes + let data: Box<[u8]> = if P::COLOR_TYPE.has_alpha() { + data.map(P::as_rgba).flat_map(|p| p.as_bytes()).collect() + } else { + data.map(P::as_rgb).flat_map(|p| p.as_bytes()).collect() + }; + // Write to stream + QEncoder::new(&data, image.width(), image.height())? + .with_colorspace(self.config) + .encode_to_stream(&mut self.writer)?; + Ok(()) + } + + fn finish(self) -> crate::Result<()> { + Ok(()) + } +} + +pub struct QoiDecoder { + _marker: PhantomData<(P, R)>, +} + +impl QoiDecoder { + /// Create a new decoder that decodes into the given pixel type. + #[must_use] + pub const fn new() -> Self { + Self { + _marker: PhantomData, + } + } +} + +impl Decoder for QoiDecoder { + type Sequence = SingleFrameIterator

; + + fn decode(&mut self, stream: R) -> crate::Result> { + let mut decoder = QDecoder::from_stream(stream)?; + // Decode the header + let header = decoder.header().to_owned(); + + // Convert the pixels + let pixels = decoder + .decode_to_vec()? + // Since qoi::Channels is #[repr(u8)], this works + .chunks(header.channels as usize) + .map(|chunk| P::from_dynamic(Dynamic::from_bytes(chunk))) + .collect::>(); + + Ok(Image::from_pixels(header.width, pixels)) + } + + fn decode_sequence(&mut self, stream: R) -> crate::Result { + self.decode(stream).map(SingleFrameIterator::new) + } +} diff --git a/src/error.rs b/src/error.rs index c5fd3e055..4886dfb9d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -188,3 +188,50 @@ impl From for Error { } } } + +#[cfg(feature = "qoi")] +impl From for Error { + fn from(value: qoi::Error) -> Self { + use crate::Error::*; + use qoi::Error::*; + match value { + InvalidMagic { .. } => DecodingError("invalid magic number".to_string()), + InvalidChannels { channels } => EncodingError(format!( + "qoi only supports either 3 or 4 channels, got {channels}" + )), + InvalidColorSpace { .. } => { + DecodingError("colorspace of image is malformed".to_string()) + } + InvalidImageDimensions { width, height } => { + if width.min(height) == 0 { + EmptyImageError + } else { + EncodingError(format!( + "image dimensions of {} by {} are not valid, must be below 400Mp", + width, height + )) + } + } + InvalidImageLength { + size, + width, + height, + } => IncompatibleImageData { + width, + height, + received: size, + }, + OutputBufferTooSmall { size, required } => EncodingError(format!( + "buffer of size {} is too small to hold image of size {}", + size, required + )), + UnexpectedBufferEnd => { + DecodingError("buffer reached end before decoding was finished".to_string()) + } + InvalidPadding => DecodingError( + "incorrectly placed stream end marker encountered during decoding".to_string(), + ), + qoi::Error::IoError(error) => Error::IoError(error), + } + } +} diff --git a/src/format.rs b/src/format.rs index 1efc801b9..9efb764f2 100644 --- a/src/format.rs +++ b/src/format.rs @@ -16,9 +16,17 @@ use crate::encodings::gif; use crate::encodings::jpeg; #[cfg(feature = "png")] use crate::encodings::png; +#[cfg(feature = "qoi")] +use crate::encodings::qoi; #[cfg(feature = "webp")] use crate::encodings::webp; -#[cfg(any(feature = "png", feature = "gif", feature = "jpeg", feature = "webp"))] +#[cfg(any( + feature = "png", + feature = "gif", + feature = "jpeg", + feature = "webp", + feature = "qoi" +))] use crate::{Decoder, Encoder}; /// Represents the underlying encoding format of an image. @@ -47,6 +55,9 @@ pub enum ImageFormat { /// The image is encoded in the WebP format. WebP, + + /// The image is encoded in the QOI format. + Qoi, } impl Default for ImageFormat { @@ -87,6 +98,7 @@ impl ImageFormat { "bmp" => Self::Bmp, "tiff" => Self::Tiff, "webp" => Self::WebP, + "qoi" => Self::Qoi, _ => Self::Unknown, }, ) @@ -119,6 +131,8 @@ impl ImageFormat { "image/bmp" => Self::Bmp, "image/tiff" => Self::Tiff, "image/webp" => Self::WebP, + // Not official, but in the spec + "image/qoi" => Self::Qoi, _ => Self::Unknown, } } @@ -141,6 +155,8 @@ impl ImageFormat { && sample[9] != 0x52 { Self::Tiff + } else if sample.starts_with(b"qoif") { + Self::Qoi } else { Self::Unknown } @@ -154,7 +170,13 @@ impl ImageFormat { /// # Panics /// * No encoder implementation is found for this image encoding. #[cfg_attr( - not(any(feature = "png", feature = "gif", feature = "jpeg", feature = "webp")), + not(any( + feature = "png", + feature = "gif", + feature = "jpeg", + feature = "webp", + feature = "qoi" + )), allow(unused_variables, unreachable_code) )] pub fn run_encoder(&self, image: &Image

, dest: impl Write) -> Result<()> { @@ -167,6 +189,8 @@ impl ImageFormat { Self::Gif => gif::GifEncoder::encode_static(image, dest), #[cfg(feature = "webp")] Self::WebP => webp::WebPStaticEncoder::encode_static(image, dest), + #[cfg(feature = "qoi")] + Self::Qoi => qoi::QoiEncoder::encode_static(image, dest), _ => panic!( "No encoder implementation is found for this image format. \ Did you forget to enable the feature?" @@ -183,7 +207,13 @@ impl ImageFormat { /// # Panics /// * No encoder implementation is found for this image encoding. #[cfg_attr( - not(any(feature = "png", feature = "gif", feature = "jpeg", feature = "webp")), + not(any( + feature = "png", + feature = "gif", + feature = "jpeg", + feature = "webp", + feature = "qoi" + )), allow(unused_variables, unreachable_code) )] pub fn run_sequence_encoder( @@ -200,6 +230,8 @@ impl ImageFormat { Self::Gif => gif::GifEncoder::encode_sequence(seq, dest), #[cfg(feature = "webp")] Self::WebP => webp::WebPMuxEncoder::encode_sequence(seq, dest), + #[cfg(feature = "qoi")] + Self::Qoi => qoi::QoiEncoder::encode_sequence(seq, dest), _ => panic!( "No encoder implementation is found for this image format. \ Did you forget to enable the feature?" @@ -215,7 +247,13 @@ impl ImageFormat { /// # Panics /// * No decoder implementation is found for this image encoding. #[cfg_attr( - not(any(feature = "png", feature = "gif", feature = "jpeg", feature = "webp")), + not(any( + feature = "png", + feature = "gif", + feature = "jpeg", + feature = "webp", + feature = "qoi" + )), allow(unused_variables, unreachable_code) )] #[allow(clippy::needless_pass_by_value)] // would require a major refactor @@ -228,7 +266,9 @@ impl ImageFormat { #[cfg(feature = "gif")] Self::Gif => gif::GifDecoder::new().decode(stream), #[cfg(feature = "webp")] - Self::WebP => webp::WebPDecoder::default().decode(stream), + Self::WebP => webp::WebPDecoder::new().decode(stream), + #[cfg(feature = "qoi")] + Self::Qoi => qoi::QoiDecoder::new().decode(stream), _ => panic!( "No encoder implementation is found for this image format. \ Did you forget to enable the feature?" @@ -244,7 +284,13 @@ impl ImageFormat { /// # Panics /// * No decoder implementation is found for this image encoding. #[cfg_attr( - not(any(feature = "png", feature = "gif", feature = "jpeg", feature = "webp")), + not(any( + feature = "png", + feature = "gif", + feature = "jpeg", + feature = "webp", + feature = "qoi" + )), allow(unused_variables, unreachable_code) )] #[allow(clippy::needless_pass_by_value)] // would require a major refactor @@ -260,7 +306,9 @@ impl ImageFormat { #[cfg(feature = "gif")] Self::Gif => Box::new(gif::GifDecoder::new().decode_sequence(stream)?), #[cfg(feature = "webp")] - Self::WebP => Box::new(webp::WebPDecoder::default().decode_sequence(stream)?), + Self::WebP => Box::new(webp::WebPDecoder::new().decode_sequence(stream)?), + #[cfg(feature = "qoi")] + Self::Qoi => Box::new(qoi::QoiDecoder::new().decode_sequence(stream)?), _ => panic!( "No encoder implementation is found for this image format. \ Did you forget to enable the feature?" @@ -281,6 +329,7 @@ impl Display for ImageFormat { Self::Bmp => "bmp", Self::Tiff => "tiff", Self::WebP => "webp", + Self::Qoi => "qoi", Self::Unknown => "", } ) diff --git a/tests/out/animated_webp_encode_output.webp b/tests/out/animated_webp_encode_output.webp index 618369068..16e1866bf 100644 Binary files a/tests/out/animated_webp_encode_output.webp and b/tests/out/animated_webp_encode_output.webp differ diff --git a/tests/out/apng_encode_output.png b/tests/out/apng_encode_output.png index 6ebf2c016..08750cc39 100644 Binary files a/tests/out/apng_encode_output.png and b/tests/out/apng_encode_output.png differ diff --git a/tests/out/gh_17.png b/tests/out/gh_17.png new file mode 100644 index 000000000..3d5b7fce4 Binary files /dev/null and b/tests/out/gh_17.png differ diff --git a/tests/out/gif_encode_output.gif b/tests/out/gif_encode_output.gif index b3c12af44..593d7ec07 100644 Binary files a/tests/out/gif_encode_output.gif and b/tests/out/gif_encode_output.gif differ diff --git a/tests/out/png_encode_output.png b/tests/out/png_encode_output.png index 7fd046a0c..976b342d1 100644 Binary files a/tests/out/png_encode_output.png and b/tests/out/png_encode_output.png differ diff --git a/tests/out/png_palette_encode_output.png b/tests/out/png_palette_encode_output.png index 7fa110fcc..7220d6e51 100644 Binary files a/tests/out/png_palette_encode_output.png and b/tests/out/png_palette_encode_output.png differ diff --git a/tests/out/png_palette_mutation_output.png b/tests/out/png_palette_mutation_output.png index a6e146bee..3f6c24fdc 100644 Binary files a/tests/out/png_palette_mutation_output.png and b/tests/out/png_palette_mutation_output.png differ diff --git a/tests/out/qoi_encode_output_rgb.qoi b/tests/out/qoi_encode_output_rgb.qoi new file mode 100644 index 000000000..6c8a8da88 Binary files /dev/null and b/tests/out/qoi_encode_output_rgb.qoi differ diff --git a/tests/out/qoi_encode_output_rgb_conv.qoi b/tests/out/qoi_encode_output_rgb_conv.qoi new file mode 100644 index 000000000..f9364cf06 Binary files /dev/null and b/tests/out/qoi_encode_output_rgb_conv.qoi differ diff --git a/tests/out/qoi_encode_output_rgba.qoi b/tests/out/qoi_encode_output_rgba.qoi new file mode 100644 index 000000000..6d46c6ed5 Binary files /dev/null and b/tests/out/qoi_encode_output_rgba.qoi differ diff --git a/tests/out/qoi_encode_output_rgba_conv.qoi b/tests/out/qoi_encode_output_rgba_conv.qoi new file mode 100644 index 000000000..e13841284 Binary files /dev/null and b/tests/out/qoi_encode_output_rgba_conv.qoi differ diff --git a/tests/out/resize_gradient_output_control.png b/tests/out/resize_gradient_output_control.png index 994e3778f..76371e782 100644 Binary files a/tests/out/resize_gradient_output_control.png and b/tests/out/resize_gradient_output_control.png differ diff --git a/tests/out/resize_gradient_output_resized.png b/tests/out/resize_gradient_output_resized.png index 875384265..80f16d769 100644 Binary files a/tests/out/resize_gradient_output_resized.png and b/tests/out/resize_gradient_output_resized.png differ diff --git a/tests/out/text_gradient_output.png b/tests/out/text_gradient_output.png index f11dfb47d..fe0233e19 100644 Binary files a/tests/out/text_gradient_output.png and b/tests/out/text_gradient_output.png differ diff --git a/tests/out/text_render_output.png b/tests/out/text_render_output.png index e80e6b7a4..963ebdd57 100644 Binary files a/tests/out/text_render_output.png and b/tests/out/text_render_output.png differ diff --git a/tests/sample_rgb.qoi b/tests/sample_rgb.qoi new file mode 100644 index 000000000..4e1774ad5 Binary files /dev/null and b/tests/sample_rgb.qoi differ diff --git a/tests/sample_rgba.qoi b/tests/sample_rgba.qoi new file mode 100644 index 000000000..37552305f Binary files /dev/null and b/tests/sample_rgba.qoi differ diff --git a/tests/test_qoi.rs b/tests/test_qoi.rs new file mode 100644 index 000000000..4fbdc9452 --- /dev/null +++ b/tests/test_qoi.rs @@ -0,0 +1,33 @@ +use ril::prelude::*; + +#[test] +fn test_qoi_rgb() -> ril::Result<()> { + let image = Image::::open("tests/sample_rgb.qoi")?; + assert_eq!(image.dimensions(), (1024, 1024)); + + image.save_inferred("tests/out/qoi_encode_output_rgb.qoi") +} + +#[test] +fn test_qoi_rgba() -> ril::Result<()> { + let image = Image::::open("tests/sample_rgba.qoi")?; + assert_eq!(image.dimensions(), (1024, 1024)); + + image.save_inferred("tests/out/qoi_encode_output_rgba.qoi") +} + +#[test] +fn test_qoi_rgba_conv() -> ril::Result<()> { + let image = Image::::open("tests/sample_rgba.qoi")?; + assert_eq!(image.dimensions(), (1024, 1024)); + + image.save_inferred("tests/out/qoi_encode_output_rgb_conv.qoi") +} + +#[test] +fn test_qoi_rgb_conv() -> ril::Result<()> { + let image = Image::::open("tests/sample_rgb.qoi")?; + assert_eq!(image.dimensions(), (1024, 1024)); + + image.save_inferred("tests/out/qoi_encode_output_rgba_conv.qoi") +}