diff --git a/src/colorspace.rs b/src/colorspace.rs index 14706c2..02d6861 100644 --- a/src/colorspace.rs +++ b/src/colorspace.rs @@ -1,70 +1,53 @@ -use std::fmt::{self, Debug}; +use std::convert::TryFrom; -#[derive(Copy, Clone, PartialEq, Eq, Hash)] -pub struct ColorSpace { - pub r_linear: bool, - pub g_linear: bool, - pub b_linear: bool, - pub a_linear: bool, +use crate::error::{Error, Result}; +use crate::utils::unlikely; + +/// Image color space. +/// +/// Note: the color space is purely informative. Although it is saved to the +/// file header, it does not affect encoding/decoding in any way. +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +#[repr(u8)] +pub enum ColorSpace { + /// sRGB with linear alpha + Srgb = 0, + /// All channels are linear + Linear = 1, } impl ColorSpace { - pub const SRGB: Self = Self::new(false, false, false, false); - pub const LINEAR: Self = Self::new(true, true, true, true); - pub const SRGB_LINEAR_ALPHA: Self = Self::new(false, false, false, true); - - pub const fn new(r_linear: bool, g_linear: bool, b_linear: bool, a_linear: bool) -> Self { - Self { r_linear, g_linear, b_linear, a_linear } - } - pub const fn is_srgb(self) -> bool { - !self.r_linear && !self.g_linear && !self.b_linear && !self.a_linear + matches!(self, Self::Srgb) } pub const fn is_linear(self) -> bool { - self.r_linear && self.g_linear && self.b_linear && self.a_linear - } - - pub const fn is_srgb_linear_alpha(self) -> bool { - !self.r_linear && !self.g_linear && !self.b_linear && self.a_linear - } - - pub const fn to_u8(self) -> u8 { - (self.r_linear as u8) << 3 - | (self.g_linear as u8) << 2 - | (self.b_linear as u8) << 1 - | (self.a_linear as u8) - } - - pub const fn from_u8(bits: u8) -> Self { - Self::new(bits & 0x08 != 0, bits & 0x04 != 0, bits & 0x02 != 0, bits & 0x01 != 0) + matches!(self, Self::Linear) } } impl Default for ColorSpace { fn default() -> Self { - Self::SRGB - } -} - -impl From for ColorSpace { - fn from(value: u8) -> Self { - Self::from_u8(value) + Self::Srgb } } impl From for u8 { - fn from(value: ColorSpace) -> Self { - value.to_u8() + #[inline] + fn from(colorspace: ColorSpace) -> Self { + colorspace as u8 } } -impl Debug for ColorSpace { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "ColorSpace({}{}{}{})", - self.r_linear as u8, self.g_linear as u8, self.b_linear as u8, self.a_linear as u8 - ) +impl TryFrom for ColorSpace { + type Error = Error; + + #[inline] + fn try_from(colorspace: u8) -> Result { + if unlikely(colorspace | 1 != 1) { + Err(Error::InvalidColorSpace { colorspace }) + } else { + Ok(if colorspace == 0 { Self::Srgb } else { Self::Linear }) + } } } diff --git a/src/decode.rs b/src/decode.rs index 7c34cdf..9f23b3d 100644 --- a/src/decode.rs +++ b/src/decode.rs @@ -123,8 +123,7 @@ pub fn qoi_decode_to_vec( data: impl AsRef<[u8]>, channels: impl MaybeChannels, ) -> Result<(Header, Vec)> { let data = data.as_ref(); - let header = qoi_decode_header(data)?; - header.validate()?; + let header = Header::decode(data)?; let channels = channels.maybe_channels().unwrap_or(header.channels); match (channels, header.channels) { (3, 3) => Ok((header, qoi_decode_impl::<3, false>(data, header.n_pixels())?)), @@ -137,11 +136,5 @@ pub fn qoi_decode_to_vec( #[inline] pub fn qoi_decode_header(data: impl AsRef<[u8]>) -> Result
{ - let data = data.as_ref(); - if unlikely(data.len() < QOI_HEADER_SIZE) { - return Err(Error::InputBufferTooSmall { size: data.len(), required: QOI_HEADER_SIZE }); - } - let mut bytes = [0_u8; QOI_HEADER_SIZE]; - bytes.copy_from_slice(&data[..QOI_HEADER_SIZE]); - Ok(Header::from_bytes(bytes)) + Header::decode(data) } diff --git a/src/encode.rs b/src/encode.rs index 024fd46..5e1efa5 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -1,3 +1,5 @@ +use std::convert::TryInto; + use crate::colorspace::ColorSpace; use crate::consts::{ QOI_HEADER_SIZE, QOI_OP_DIFF, QOI_OP_INDEX, QOI_OP_LUMA, QOI_OP_RGB, QOI_OP_RGBA, QOI_OP_RUN, @@ -66,7 +68,7 @@ where let header = Header { width, height, channels: CHANNELS as u8, colorspace, ..Header::default() }; - buf = buf.write(header.to_bytes()); + buf = buf.write(header.encode()); let mut index = [Pixel::new(); 256]; let mut px_prev = Pixel::new().with_a(0xff); @@ -129,13 +131,18 @@ where } #[inline] -pub fn qoi_encode_to_buf( - mut out: impl AsMut<[u8]>, data: impl AsRef<[u8]>, width: u32, height: u32, channels: u8, - colorspace: impl Into, -) -> Result { +pub fn qoi_encode_to_buf( + mut out: O, data: D, width: u32, height: u32, channels: u8, colorspace: C, +) -> Result +where + O: AsMut<[u8]>, + D: AsRef<[u8]>, + C: TryInto, + Error: From, +{ let out = out.as_mut(); let data = data.as_ref(); - let colorspace = colorspace.into(); + let colorspace = colorspace.try_into()?; match channels { 3 => qoi_encode_impl::<3>(out, data, width, height, colorspace), 4 => qoi_encode_impl::<4>(out, data, width, height, colorspace), @@ -144,12 +151,14 @@ pub fn qoi_encode_to_buf( } #[inline] -pub fn qoi_encode_to_vec( - data: impl AsRef<[u8]>, width: u32, height: u32, channels: u8, - colorspace: impl Into, -) -> Result> { - let data = data.as_ref(); - let colorspace = colorspace.into(); +pub fn qoi_encode_to_vec( + data: D, width: u32, height: u32, channels: u8, colorspace: C, +) -> Result> +where + D: AsRef<[u8]>, + C: TryInto, + Error: From, +{ let size = encode_size_required(width, height, channels); let mut out = vec![0; size]; // note: we could save time here but that won't be safe anymore let size = qoi_encode_to_buf(&mut out, data, width, height, channels, colorspace)?; diff --git a/src/error.rs b/src/error.rs index 2352682..0f710e8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,7 +15,7 @@ pub enum Error { OutputBufferTooSmall { size: usize, required: usize }, InvalidMagic { magic: u32 }, UnexpectedBufferEnd, - // TODO: invalid colorspace + InvalidColorSpace { colorspace: u8 }, } pub type Result = StdResult; @@ -48,6 +48,9 @@ impl Display for Error { Self::UnexpectedBufferEnd => { write!(f, "unexpected input buffer end while decoding") } + Self::InvalidColorSpace { colorspace } => { + write!(f, "invalid color space: {} (expected 0 or 1)", colorspace) + } } } } diff --git a/src/header.rs b/src/header.rs index 3e6071a..e0556f0 100644 --- a/src/header.rs +++ b/src/header.rs @@ -43,12 +43,12 @@ const fn u32_from_be(v: &[u8]) -> u32 { impl Header { #[inline] pub const fn new(width: u32, height: u32, channels: u8) -> Self { - Self { magic: QOI_MAGIC, width, height, channels, colorspace: ColorSpace::from_u8(0) } + Self { magic: QOI_MAGIC, width, height, channels, colorspace: ColorSpace::Srgb } } #[inline] - pub fn with_colorspace(mut self, colorspace: impl Into) -> Self { - self.colorspace = colorspace.into(); + pub fn with_colorspace(mut self, colorspace: ColorSpace) -> Self { + self.colorspace = colorspace; self } @@ -58,7 +58,7 @@ impl Header { } #[inline] - pub(crate) fn to_bytes(&self) -> [u8; QOI_HEADER_SIZE] { + pub(crate) fn encode(&self) -> [u8; QOI_HEADER_SIZE] { let mut out = [0; QOI_HEADER_SIZE]; out[..4].copy_from_slice(&u32_to_be(self.magic)); out[4..8].copy_from_slice(&u32_to_be(self.width)); @@ -69,32 +69,30 @@ impl Header { } #[inline] - pub(crate) fn from_bytes(v: [u8; QOI_HEADER_SIZE]) -> Self { - Self { - magic: u32_from_be(&v[..4]), - width: u32_from_be(&v[4..8]), - height: u32_from_be(&v[8..12]), - channels: v[12], - colorspace: v[13].into(), + pub(crate) fn decode(data: impl AsRef<[u8]>) -> Result { + let data = data.as_ref(); + if unlikely(data.len() < QOI_HEADER_SIZE) { + return Err(Error::InputBufferTooSmall { size: data.len(), required: QOI_HEADER_SIZE }); } + let magic = u32_from_be(&data[..4]); + let width = u32_from_be(&data[4..8]); + let height = u32_from_be(&data[8..12]); + let channels = data[12]; + let colorspace = ColorSpace::try_from(data[13])?; + if unlikely(magic != QOI_MAGIC) { + return Err(Error::InvalidMagic { magic }); + } else if unlikely(height == 0 || width == 0) { + return Err(Error::EmptyImage { width, height }); + } else if unlikely((width as usize) * (height as usize) > QOI_PIXELS_MAX) { + return Err(Error::ImageTooLarge { width, height }); + } else if unlikely(channels != 3 && channels != 4) { + return Err(Error::InvalidChannels { channels }); + } + Ok(Self { magic, width, height, channels, colorspace }) } #[inline] pub const fn n_pixels(&self) -> usize { (self.width as usize).saturating_mul(self.height as usize) } - - #[inline] - pub const fn validate(&self) -> Result<()> { - if unlikely(self.magic != QOI_MAGIC) { - return Err(Error::InvalidMagic { magic: self.magic }); - } else if unlikely(self.height == 0 || self.width == 0) { - return Err(Error::EmptyImage { width: self.width, height: self.height }); - } else if unlikely((self.height as usize) * (self.width as usize) > QOI_PIXELS_MAX) { - return Err(Error::ImageTooLarge { width: self.width, height: self.height }); - } else if unlikely(self.channels < 3 || self.channels > 4) { - return Err(Error::InvalidChannels { channels: self.channels }); - } - Ok(()) - } }