diff --git a/Cargo.toml b/Cargo.toml index 8252569..eabaa1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,17 @@ [package] -name = "qoi" +name = "qoicoubeh" version = "0.4.1" description = "VERY fast encoder/decoder for QOI (Quite Okay Image) format" -authors = ["Ivan Smirnov "] +authors = [ + "Ivan Smirnov ", + "Marc-André Lureau ", +] edition = "2021" readme = "README.md" license = "MIT/Apache-2.0" -repository = "https://github.com/aldanor/qoi-rust" -homepage = "https://github.com/aldanor/qoi-rust" -documentation = "https://docs.rs/qoi" +repository = "https://github.com/elmarco/qoi-rust" +homepage = "https://github.com/elmarco/qoi-rust" +documentation = "https://docs.rs/qoicoubeh" categories = ["multimedia::images", "multimedia::encoding"] keywords = ["qoi", "graphics", "image", "encoding"] exclude = [ diff --git a/README.md b/README.md index b258adc..42d0d81 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ -# [qoi](https://crates.io/crates/qoi) +# [qoicoubeh](https://crates.io/crates/qoicoubeh) -[![Build](https://github.com/aldanor/qoi-rust/workflows/CI/badge.svg)](https://github.com/aldanor/qoi-rust/actions?query=branch%3Amaster) -[![Latest Version](https://img.shields.io/crates/v/qoi.svg)](https://crates.io/crates/qoi) -[![Documentation](https://img.shields.io/docsrs/qoi)](https://docs.rs/qoi) +[![Build](https://github.com/elmarco/qoi-rust/workflows/CI/badge.svg)](https://github.com/elmarco/qoi-rust/actions?query=branch%3Amaster) +[![Latest Version](https://img.shields.io/crates/v/qoicoubeh.svg)](https://crates.io/crates/qoicoubeh) +[![Documentation](https://img.shields.io/docsrs/qoicoubeh)](https://docs.rs/qoicoubeh) [![Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance) +> **⚠️ WARNING ⚠️** +> This project is a fork of [qoi-rust](https://github.com/aldanor/qoi-rust) until the maintainer resumes activity. + Fast encoder/decoder for [QOI image format](https://qoiformat.org/), implemented in pure and safe Rust. - One of the [fastest](#benchmarks) QOI encoders/decoders out there. @@ -34,7 +37,7 @@ assert_eq!(decoded, pixels); ``` decode:Mp/s encode:Mp/s decode:MB/s encode:MB/s qoi.h 282.9 225.3 978.3 778.9 -qoi-rust 427.4 290.0 1477.7 1002.9 +qoicoubeh 427.4 290.0 1477.7 1002.9 ``` - Reference C implementation: diff --git a/bench/Cargo.toml b/bench/Cargo.toml index d1a04d6..f93f5e5 100644 --- a/bench/Cargo.toml +++ b/bench/Cargo.toml @@ -9,7 +9,7 @@ publish = false [dependencies] # internal libqoi = { path = "../libqoi" } -qoi = { path = ".." } +qoicoubeh = { path = ".." } # external anyhow = "1.0" bytemuck = "1.7" diff --git a/bench/src/main.rs b/bench/src/main.rs index e7dbeaa..87cc142 100644 --- a/bench/src/main.rs +++ b/bench/src/main.rs @@ -135,7 +135,7 @@ impl Codec for CodecQoiRust { type Output = Vec; fn name() -> &'static str { - "qoi-rust" + "qoicoubeh" } fn encode(img: &Image) -> Result> { @@ -215,7 +215,7 @@ impl ImageBench { let (decoded, t_decode) = timeit(|| C::decode(encoded.as_ref(), img)); let decoded = decoded?; let roundtrip = decoded.as_ref() == img.data.as_slice(); - if C::name() == "qoi-rust" { + if C::name() == "qoicoubeh" { assert!(roundtrip, "{}: decoded data doesn't roundtrip", C::name()); } else { ensure!(roundtrip, "{}: decoded data doesn't roundtrip", C::name()); @@ -405,7 +405,7 @@ struct Args { #[structopt(short, long)] average: bool, /// Simple totals, no fancy tables. - #[structopt(short, long)] + #[structopt(long)] simple: bool, } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index f9c04dc..8657b24 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -10,7 +10,7 @@ cargo-fuzz = true [dependencies] # internal -qoi-fast = { path = ".." } +qoicoubeh = { path = ".." } # external libfuzzer-sys = "0.4" diff --git a/fuzz/fuzz_targets/decode.rs b/fuzz/fuzz_targets/decode.rs index 62a1c3b..9dc1c0a 100644 --- a/fuzz/fuzz_targets/decode.rs +++ b/fuzz/fuzz_targets/decode.rs @@ -24,7 +24,7 @@ fuzz_target!(|input: (u16, u16, bool, &[u8])| { channels, 0, ]; - vec.extend(&*data); + vec.extend(data); vec.extend(&[0, 0, 0, 0, 0, 0, 0, 1]); let header_expected = Header { diff --git a/fuzz/fuzz_targets/encode.rs b/fuzz/fuzz_targets/encode.rs index aedb0c3..ef4113c 100644 --- a/fuzz/fuzz_targets/encode.rs +++ b/fuzz/fuzz_targets/encode.rs @@ -1,7 +1,7 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use qoi::{encode_size_limit, encode_to_vec}; +use qoi::{encode_max_len, encode_to_vec}; fuzz_target!(|input: (bool, u8, &[u8])| { let (is_4, w_frac, data) = input; @@ -18,7 +18,7 @@ fuzz_target!(|input: (bool, u8, &[u8])| { let out = encode_to_vec(&data[..(w * h * channels as usize)], w as u32, h as u32); if w * h != 0 { let out = out.unwrap(); - assert!(out.len() <= encode_size_limit(w as u32, h as u32, channels)); + assert!(out.len() <= encode_max_len(w as u32, h as u32, channels)); } else { assert!(out.is_err()); } diff --git a/src/decode.rs b/src/decode.rs index be97803..3c134ce 100644 --- a/src/decode.rs +++ b/src/decode.rs @@ -35,6 +35,11 @@ where let mut px = Pixel::::new().with_a(0xff); let mut px_rgba: Pixel<4>; + if matches!(data, [QOI_OP_RUN..=QOI_OP_RUN_END, ..]) { + px_rgba = px.as_rgba(0xff); + index[px_rgba.hash_index() as usize] = px_rgba; + } + while let [px_out, ptail @ ..] = pixels { pixels = ptail; match data { @@ -178,6 +183,7 @@ where let (phead, ptail) = pixels.split_at_mut(run); // can't panic phead.fill(px.into()); pixels = ptail; + index[px.hash_index() as usize] = px; continue; } QOI_OP_DIFF..=QOI_OP_DIFF_END => { @@ -381,7 +387,11 @@ impl Decoder { if unlikely(buf.len() < size) { return Err(Error::OutputBufferTooSmall { size: buf.len(), required: size }); } - self.reader.decode_image(buf, self.channels.as_u8(), self.header.channels.as_u8())?; + self.reader.decode_image( + &mut buf[..size], + self.channels.as_u8(), + self.header.channels.as_u8(), + )?; Ok(size) } diff --git a/src/encode.rs b/src/encode.rs index 0ed8476..57d69dd 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -1,6 +1,5 @@ #[cfg(any(feature = "std", feature = "alloc"))] use alloc::{vec, vec::Vec}; -use core::convert::TryFrom; #[cfg(feature = "std")] use std::io::Write; @@ -10,13 +9,16 @@ use crate::consts::{QOI_HEADER_SIZE, QOI_OP_INDEX, QOI_OP_RUN, QOI_PADDING, QOI_ use crate::error::{Error, Result}; use crate::header::Header; use crate::pixel::{Pixel, SupportedChannels}; -use crate::types::{Channels, ColorSpace}; +use crate::types::{Channels, ColorSpace, RawChannels}; #[cfg(feature = "std")] use crate::utils::GenericWriter; use crate::utils::{unlikely, BytesMut, Writer}; #[allow(clippy::cast_possible_truncation, unused_assignments, unused_variables)] -fn encode_impl(mut buf: W, data: &[u8]) -> Result +fn encode_impl( + mut buf: W, data: &[u8], width: usize, height: usize, stride: usize, + read_px: impl Fn(&mut Pixel, &[u8]), +) -> Result where Pixel: SupportedChannels, [u8; N]: Pod, @@ -30,59 +32,57 @@ where let mut px = Pixel::::new().with_a(0xff); let mut index_allowed = false; - let n_pixels = data.len() / N; + let n_pixels = width * height; - for (i, chunk) in data.chunks_exact(N).enumerate() { - px.read(chunk); - if px == px_prev { - run += 1; - if run == 62 || unlikely(i == n_pixels - 1) { - buf = buf.write_one(QOI_OP_RUN | (run - 1))?; - run = 0; - } - } else { - if run != 0 { - #[cfg(not(feature = "reference"))] - { - // credits for the original idea: @zakarumych (had to be fixed though) - buf = buf.write_one(if run == 1 && index_allowed { - QOI_OP_INDEX | hash_prev - } else { - QOI_OP_RUN | (run - 1) - })?; - } - #[cfg(feature = "reference")] - { + let mut i = 0; + for row in data.chunks(stride).take(height) { + let pixel_row = &row[..width * R]; + for chunk in pixel_row.chunks_exact(R) { + read_px(&mut px, chunk); + if px == px_prev { + run += 1; + if run == 62 || unlikely(i == n_pixels - 1) { buf = buf.write_one(QOI_OP_RUN | (run - 1))?; + run = 0; } - run = 0; - } - index_allowed = true; - let px_rgba = px.as_rgba(0xff); - hash_prev = px_rgba.hash_index(); - let index_px = &mut index[hash_prev as usize]; - if *index_px == px_rgba { - buf = buf.write_one(QOI_OP_INDEX | hash_prev)?; } else { - *index_px = px_rgba; - buf = px.encode_into(px_prev, buf)?; + if run != 0 { + #[cfg(not(feature = "reference"))] + { + // credits for the original idea: @zakarumych (had to be fixed though) + buf = buf.write_one(if run == 1 && index_allowed { + QOI_OP_INDEX | hash_prev + } else { + QOI_OP_RUN | (run - 1) + })?; + } + #[cfg(feature = "reference")] + { + buf = buf.write_one(QOI_OP_RUN | (run - 1))?; + } + run = 0; + } + index_allowed = true; + let px_rgba = px.as_rgba(0xff); + hash_prev = px_rgba.hash_index(); + let index_px = &mut index[hash_prev as usize]; + if *index_px == px_rgba { + buf = buf.write_one(QOI_OP_INDEX | hash_prev)?; + } else { + *index_px = px_rgba; + buf = px.encode_into(px_prev, buf)?; + } + px_prev = px; } - px_prev = px; + i += 1; } } + assert_eq!(i, n_pixels); buf = buf.write_many(&QOI_PADDING)?; Ok(cap.saturating_sub(buf.capacity())) } -#[inline] -fn encode_impl_all(out: W, data: &[u8], channels: Channels) -> Result { - match channels { - Channels::Rgb => encode_impl::<_, 3>(out, data), - Channels::Rgba => encode_impl::<_, 4>(out, data), - } -} - /// The maximum number of bytes the encoded image will take. /// /// Can be used to pre-allocate the buffer to encode the image into. @@ -113,30 +113,102 @@ pub fn encode_to_vec(data: impl AsRef<[u8]>, width: u32, height: u32) -> Result< Encoder::new(&data, width, height)?.encode_to_vec() } +pub struct EncoderBuilder<'a> { + data: &'a [u8], + width: u32, + height: u32, + stride: Option, + raw_channels: Option, + colorspace: Option, +} + +impl<'a> EncoderBuilder<'a> { + /// Creates a new encoder builder from a given array of pixel data and image dimensions. + pub fn new(data: &'a (impl AsRef<[u8]> + ?Sized), width: u32, height: u32) -> Self { + Self { + data: data.as_ref(), + width, + height, + stride: None, + raw_channels: None, + colorspace: None, + } + } + + /// Set the stride of the pixel data. + pub const fn stride(mut self, stride: usize) -> Self { + self.stride = Some(stride); + self + } + + /// Set the input format of the pixel data. + pub const fn raw_channels(mut self, raw_channels: RawChannels) -> Self { + self.raw_channels = Some(raw_channels); + self + } + + /// Set the colorspace. + pub const fn colorspace(mut self, colorspace: ColorSpace) -> Self { + self.colorspace = Some(colorspace); + self + } + + /// Build the encoder. + pub fn build(self) -> Result> { + let EncoderBuilder { data, width, height, stride, raw_channels, colorspace } = self; + + let size = data.len(); + let no_stride = stride.is_none(); + let stride = stride.unwrap_or( + size.checked_div(height as usize) + .ok_or(Error::InvalidImageDimensions { width, height })?, + ); + let raw_channels = raw_channels.unwrap_or(if stride == width as usize * 3 { + RawChannels::Rgb + } else { + RawChannels::Rgba + }); + + if stride < width as usize * raw_channels.bytes_per_pixel() { + return Err(Error::InvalidImageLength { size, width, height }); + } + if stride * (height - 1) as usize + width as usize * raw_channels.bytes_per_pixel() < size { + return Err(Error::InvalidImageLength { size, width, height }); + } + if no_stride && size != width as usize * height as usize * raw_channels.bytes_per_pixel() { + return Err(Error::InvalidImageLength { size, width, height }); + } + + let channels = raw_channels.into(); + let colorspace = colorspace.unwrap_or_default(); + + Ok(Encoder { + data, + stride, + raw_channels, + header: Header::try_new(self.width, self.height, channels, colorspace)?, + }) + } +} + /// Encode QOI images into buffers or into streams. pub struct Encoder<'a> { data: &'a [u8], + stride: usize, + raw_channels: RawChannels, header: Header, } impl<'a> Encoder<'a> { /// Creates a new encoder from a given array of pixel data and image dimensions. + /// The data must be in RGB(A) order, without fill borders (extra stride). /// /// The number of channels will be inferred automatically (the valid values /// are 3 or 4). The color space will be set to sRGB by default. #[inline] #[allow(clippy::cast_possible_truncation)] pub fn new(data: &'a (impl AsRef<[u8]> + ?Sized), width: u32, height: u32) -> Result { - let data = data.as_ref(); - let mut header = - Header::try_new(width, height, Channels::default(), ColorSpace::default())?; - let size = data.len(); - let n_channels = size / header.n_pixels(); - if header.n_pixels() * n_channels != size { - return Err(Error::InvalidImageLength { size, width, height }); - } - header.channels = Channels::try_from(n_channels.min(0xff) as u8)?; - Ok(Self { data, header }) + EncoderBuilder::new(data, width, height).build() } /// Returns a new encoder with modified color space. @@ -181,7 +253,7 @@ impl<'a> Encoder<'a> { } let (head, tail) = buf.split_at_mut(QOI_HEADER_SIZE); // can't panic head.copy_from_slice(&self.header.encode()); - let n_written = encode_impl_all(BytesMut::new(tail), self.data, self.header.channels)?; + let n_written = self.encode_impl_all(BytesMut::new(tail))?; Ok(QOI_HEADER_SIZE + n_written) } @@ -203,8 +275,62 @@ impl<'a> Encoder<'a> { #[inline] pub fn encode_to_stream(&self, writer: &mut W) -> Result { writer.write_all(&self.header.encode())?; - let n_written = - encode_impl_all(GenericWriter::new(writer), self.data, self.header.channels)?; + let n_written = self.encode_impl_all(GenericWriter::new(writer))?; Ok(n_written + QOI_HEADER_SIZE) } + + #[inline] + fn encode_impl_all(&self, out: W) -> Result { + let width = self.header.width as usize; + let height = self.header.height as usize; + let stride = self.stride; + match self.raw_channels { + RawChannels::Rgb => { + encode_impl::<_, 3, 3>(out, self.data, width, height, stride, Pixel::read) + } + RawChannels::Bgr => { + encode_impl::<_, 3, 3>(out, self.data, width, height, stride, |px, c| { + px.update_rgb(c[2], c[1], c[0]); + }) + } + RawChannels::Rgba => { + encode_impl::<_, 4, 4>(out, self.data, width, height, stride, Pixel::read) + } + RawChannels::Argb => { + encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| { + px.update_rgba(c[1], c[2], c[3], c[0]); + }) + } + RawChannels::Rgbx => { + encode_impl::<_, 3, 4>(out, self.data, width, height, stride, |px, c| { + px.read(&c[..3]); + }) + } + RawChannels::Xrgb => { + encode_impl::<_, 3, 4>(out, self.data, width, height, stride, |px, c| { + px.update_rgb(c[1], c[2], c[3]); + }) + } + RawChannels::Bgra => { + encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| { + px.update_rgba(c[2], c[1], c[0], c[3]); + }) + } + RawChannels::Abgr => { + encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| { + px.update_rgba(c[3], c[2], c[1], c[0]); + }) + } + RawChannels::Bgrx => { + encode_impl::<_, 3, 4>(out, self.data, width, height, stride, |px, c| { + px.update_rgb(c[2], c[1], c[0]); + }) + } + RawChannels::Xbgr => { + encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| { + px.update_rgb(c[3], c[2], c[1]); + }) + } + } + } } diff --git a/src/error.rs b/src/error.rs index c550613..3bcd1df 100644 --- a/src/error.rs +++ b/src/error.rs @@ -22,6 +22,8 @@ pub enum Error { UnexpectedBufferEnd, /// Invalid stream end marker encountered when decoding InvalidPadding, + /// Invalid stride + InvalidStride { stride: usize }, #[cfg(feature = "std")] /// Generic I/O error from the wrapped reader/writer IoError(std::io::Error), @@ -57,6 +59,9 @@ impl Display for Error { Self::InvalidPadding => { write!(f, "invalid padding (stream end marker mismatch)") } + Self::InvalidStride { stride } => { + write!(f, "invalid stride: {stride}") + } #[cfg(feature = "std")] Self::IoError(ref err) => { write!(f, "i/o error: {err}") diff --git a/src/lib.rs b/src/lib.rs index 3fb7a35..f169db5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ //! ``` //! decode:Mp/s encode:Mp/s decode:MB/s encode:MB/s //! qoi.h 282.9 225.3 978.3 778.9 -//! qoi-rust 427.4 290.0 1477.7 1002.9 +//! qoicoubeh 427.4 290.0 1477.7 1002.9 //! ``` //! //! - Reference C implementation: @@ -88,8 +88,8 @@ pub use crate::decode::{decode_header, decode_to_buf, Decoder}; #[cfg(any(feature = "alloc", feature = "std"))] pub use crate::encode::encode_to_vec; -pub use crate::encode::{encode_max_len, encode_to_buf, Encoder}; +pub use crate::encode::{encode_max_len, encode_to_buf, Encoder, EncoderBuilder}; pub use crate::error::{Error, Result}; pub use crate::header::Header; -pub use crate::types::{Channels, ColorSpace}; +pub use crate::types::{Channels, ColorSpace, RawChannels}; diff --git a/src/pixel.rs b/src/pixel.rs index fb87d5a..1ffe074 100644 --- a/src/pixel.rs +++ b/src/pixel.rs @@ -5,6 +5,7 @@ use bytemuck::{cast, Pod}; #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[repr(transparent)] +// in RGBA order pub struct Pixel([u8; N]); impl Pixel { diff --git a/src/types.rs b/src/types.rs index 81229d4..da44f24 100644 --- a/src/types.rs +++ b/src/types.rs @@ -111,3 +111,58 @@ impl TryFrom for Channels { } } } + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum RawChannels { + Rgb, + Bgr, + Rgba, + Argb, + Rgbx, + Xrgb, + Bgra, + Abgr, + Bgrx, + Xbgr, +} + +impl From for RawChannels { + fn from(value: Channels) -> Self { + match value { + Channels::Rgb => Self::Rgb, + Channels::Rgba => Self::Rgba, + } + } +} + +impl From for Channels { + fn from(value: RawChannels) -> Self { + match value { + RawChannels::Rgb + | RawChannels::Bgr + | RawChannels::Rgbx + | RawChannels::Xrgb + | RawChannels::Bgrx + | RawChannels::Xbgr => Self::Rgb, + RawChannels::Rgba | RawChannels::Argb | RawChannels::Bgra | RawChannels::Abgr => { + Self::Rgba + } + } + } +} + +impl RawChannels { + pub(crate) const fn bytes_per_pixel(self) -> usize { + match self { + Self::Rgb | Self::Bgr => 3, + Self::Rgba + | Self::Argb + | Self::Rgbx + | Self::Xrgb + | Self::Bgra + | Self::Abgr + | Self::Bgrx + | Self::Xbgr => 4, + } + } +} diff --git a/tests/test_gen.rs b/tests/test_gen.rs index fd015c5..4994fe3 100644 --- a/tests/test_gen.rs +++ b/tests/test_gen.rs @@ -287,9 +287,9 @@ fn test_generated() { let encode_c = |data: &[u8], size| qoi_encode(data, size, 1, channels as _); let decode_c = |data: &[u8]| qoi_decode(data, channels as _).map(|r| r.1); - check_roundtrip("qoi-rust -> qoi-rust", &img, channels as _, encode, decode); - check_roundtrip("qoi-rust -> qoi.h", &img, channels as _, encode, decode_c); - check_roundtrip("qoi.h -> qoi-rust", &img, channels as _, encode_c, decode); + check_roundtrip("qoicoubeh -> qoicoubeh", &img, channels as _, encode, decode); + check_roundtrip("qoicoubeh -> qoi.h", &img, channels as _, encode, decode_c); + check_roundtrip("qoi.h -> qoicoubeh", &img, channels as _, encode_c, decode); let size = (img.len() / channels) as u32; let encoded = encode(&img, size).unwrap(); @@ -297,10 +297,10 @@ fn test_generated() { cfg_if! { if #[cfg(feature = "reference")] { let eq = encoded.as_slice() == encoded_c.as_ref(); - assert!(eq, "qoi-rust [reference mode] doesn't match qoi.h"); + assert!(eq, "qoicoubeh [reference mode] doesn't match qoi.h"); } else { let eq = encoded.len() == encoded_c.len(); - assert!(eq, "qoi-rust [non-reference mode] length doesn't match qoi.h"); + assert!(eq, "qoicoubeh [non-reference mode] length doesn't match qoi.h"); } } diff --git a/tests/test_misc.rs b/tests/test_misc.rs index 0a93f61..0f38662 100644 --- a/tests/test_misc.rs +++ b/tests/test_misc.rs @@ -1,10 +1,50 @@ +#![cfg_attr(not(feature = "std"), allow(unused_imports, dead_code))] + use qoi::{ - consts::{QOI_OP_RGB, QOI_OP_RUN}, - decode_to_vec, Channels, ColorSpace, Header, Result, + consts::{QOI_OP_INDEX, QOI_OP_RGB, QOI_OP_RUN}, + decode_to_vec, Channels, ColorSpace, Decoder, Error, Header, RawChannels, Result, }; +const ONE_PIXEL_QOI_IMAGE: [u8; 23] = [ + 0x71, 0x6f, 0x69, 0x66, // magic + 0x00, 0x00, 0x00, 0x01, // width + 0x00, 0x00, 0x00, 0x01, // height + 0x04, // number of channels + 0x00, // colorspace + 0x55, // QOI_OP_DIFF + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // padding +]; + +const ONE_PIXEL_QOI_HEADER: Header = + Header { width: 1, height: 1, channels: Channels::Rgba, colorspace: ColorSpace::Srgb }; + +#[cfg(feature = "std")] #[test] -fn test_new_encoder() { +fn test_decode_stream_to_exact_sized_buffer() -> Result<()> { + let mut decoder = Decoder::from_stream(&ONE_PIXEL_QOI_IMAGE[..])?; + assert_eq!(decoder.header(), &ONE_PIXEL_QOI_HEADER); + + let mut out = vec![0u8; decoder.required_buf_len()]; + let n_written = decoder.decode_to_buf(&mut out)?; + assert_eq!(n_written, 4); + Ok(()) +} + +#[cfg(feature = "std")] +#[test] +fn test_decode_stream_to_larger_buffer() -> Result<()> { + let mut decoder = Decoder::from_stream(&ONE_PIXEL_QOI_IMAGE[..])?; + assert_eq!(decoder.header(), &ONE_PIXEL_QOI_HEADER); + + let mut out = vec![0u8; decoder.required_buf_len() + 16]; + let n_written = decoder.decode_to_buf(&mut out)?; + assert_eq!(n_written, 4); + assert_eq!(&out[4..], &[0_u8; 16]); + Ok(()) +} + +#[test] +fn test_new_decoder() { // this used to fail due to `Bytes` not being `pub` let arr = [0u8]; let _ = qoi::Decoder::new(&arr[..]); @@ -22,9 +62,133 @@ fn test_start_with_qoi_op_run() -> Result<()> { Ok(()) } +#[test] +fn test_start_with_qoi_op_run_and_use_index() -> Result<()> { + let header = Header::try_new(4, 1, Channels::Rgba, ColorSpace::Linear)?; + let mut qoi_data: Vec<_> = header.encode().into_iter().collect(); + qoi_data.extend([QOI_OP_RUN | 1, QOI_OP_RGB, 10, 20, 30, QOI_OP_INDEX | 53]); + qoi_data.extend([0; 7]); + qoi_data.push(1); + let (_, decoded) = decode_to_vec(&qoi_data)?; + assert_eq!(decoded, vec![0, 0, 0, 255, 0, 0, 0, 255, 10, 20, 30, 255, 0, 0, 0, 255]); + Ok(()) +} + #[cfg(target_endian = "big")] #[test] fn test_big_endian() { // so we can see it in the CI logs assert_eq!(u16::to_be_bytes(1), [0, 1]); } + +#[test] +fn test_new_encoder() { + let arr3 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; // 2 * 2 * 3 + let arr4 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; // 2 * 2 * 4 + + let enc = qoi::Encoder::new(&arr3, 2, 2).unwrap(); + assert_eq!(enc.channels(), Channels::Rgb); + + let enc = qoi::Encoder::new(&arr4, 2, 2).unwrap(); + assert_eq!(enc.channels(), Channels::Rgba); + + assert!(matches!( + qoi::Encoder::new(&arr3, 3, 3), + Err(Error::InvalidImageLength { size: 12, width: 3, height: 3 }) + )); + + assert!(matches!( + qoi::Encoder::new(&arr3, 1, 1), + Err(Error::InvalidImageLength { size: 12, width: 1, height: 1 }) + )); + + let enc = qoi::EncoderBuilder::new(&arr3, 2, 2) + .stride(2 * 3) + .raw_channels(RawChannels::Bgr) + .build() + .unwrap(); + assert_eq!(enc.channels(), Channels::Rgb); + let qoi = enc.encode_to_vec().unwrap(); + let (_header, res) = decode_to_vec(qoi).unwrap(); + assert_eq!(res, [2, 1, 0, 5, 4, 3, 8, 7, 6, 11, 10, 9]); + + let enc = qoi::EncoderBuilder::new(&arr4, 2, 2) + .stride(2 * 4) + .raw_channels(RawChannels::Rgba) + .build() + .unwrap(); + assert_eq!(enc.channels(), Channels::Rgba); + let qoi = enc.encode_to_vec().unwrap(); + let (_header, res) = decode_to_vec(qoi).unwrap(); + assert_eq!(res, arr4); + + let enc = qoi::EncoderBuilder::new(&arr4, 2, 2) + .stride(2 * 4) + .raw_channels(RawChannels::Bgra) + .build() + .unwrap(); + assert_eq!(enc.channels(), Channels::Rgba); + let qoi = enc.encode_to_vec().unwrap(); + let (_header, res) = decode_to_vec(qoi).unwrap(); + assert_eq!(res, [2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15]); + + let enc = qoi::EncoderBuilder::new(&arr4, 2, 2) + .stride(2 * 4) + .raw_channels(RawChannels::Rgbx) + .build() + .unwrap(); + assert_eq!(enc.channels(), Channels::Rgb); + let qoi = enc.encode_to_vec().unwrap(); + let (_header, res) = decode_to_vec(qoi).unwrap(); + assert_eq!(res, [0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14]); + + let enc = qoi::EncoderBuilder::new(&arr4, 2, 2) + .stride(2 * 4) + .raw_channels(RawChannels::Xrgb) + .build() + .unwrap(); + assert_eq!(enc.channels(), Channels::Rgb); + let qoi = enc.encode_to_vec().unwrap(); + let (_header, res) = decode_to_vec(qoi).unwrap(); + assert_eq!(res, [1, 2, 3, 5, 6, 7, 9, 10, 11, 13, 14, 15]); + + let enc = qoi::EncoderBuilder::new(&arr4, 2, 2) + .stride(2 * 4) + .raw_channels(RawChannels::Bgra) + .build() + .unwrap(); + assert_eq!(enc.channels(), Channels::Rgba); + let qoi = enc.encode_to_vec().unwrap(); + let (_header, res) = decode_to_vec(qoi).unwrap(); + assert_eq!(res, [2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15]); + + let enc = qoi::EncoderBuilder::new(&arr4, 2, 2) + .stride(2 * 4) + .raw_channels(RawChannels::Abgr) + .build() + .unwrap(); + assert_eq!(enc.channels(), Channels::Rgba); + let qoi = enc.encode_to_vec().unwrap(); + let (_header, res) = decode_to_vec(qoi).unwrap(); + assert_eq!(res, [3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12]); + + let enc = qoi::EncoderBuilder::new(&arr4, 2, 2) + .stride(2 * 4) + .raw_channels(RawChannels::Bgrx) + .build() + .unwrap(); + assert_eq!(enc.channels(), Channels::Rgb); + let qoi = enc.encode_to_vec().unwrap(); + let (_header, res) = decode_to_vec(qoi).unwrap(); + assert_eq!(res, [2, 1, 0, 6, 5, 4, 10, 9, 8, 14, 13, 12]); + + let enc = qoi::EncoderBuilder::new(&arr4, 2, 2) + .stride(2 * 4) + .raw_channels(RawChannels::Xbgr) + .build() + .unwrap(); + assert_eq!(enc.channels(), Channels::Rgb); + let qoi = enc.encode_to_vec().unwrap(); + let (_header, res) = decode_to_vec(qoi).unwrap(); + assert_eq!(res, [3, 2, 1, 7, 6, 5, 11, 10, 9, 15, 14, 13]); +}