Rework ColorSpace to match the latest spec (0 | 1)

This commit is contained in:
Ivan Smirnov 2021-12-31 13:34:27 +03:00
parent c39844fb98
commit 2dcbdd19c2
5 changed files with 81 additions and 95 deletions

View file

@ -1,70 +1,53 @@
use std::fmt::{self, Debug}; use std::convert::TryFrom;
#[derive(Copy, Clone, PartialEq, Eq, Hash)] use crate::error::{Error, Result};
pub struct ColorSpace { use crate::utils::unlikely;
pub r_linear: bool,
pub g_linear: bool, /// Image color space.
pub b_linear: bool, ///
pub a_linear: bool, /// 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 { 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 { 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 { pub const fn is_linear(self) -> bool {
self.r_linear && self.g_linear && self.b_linear && self.a_linear matches!(self, Self::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)
} }
} }
impl Default for ColorSpace { impl Default for ColorSpace {
fn default() -> Self { fn default() -> Self {
Self::SRGB Self::Srgb
}
}
impl From<u8> for ColorSpace {
fn from(value: u8) -> Self {
Self::from_u8(value)
} }
} }
impl From<ColorSpace> for u8 { impl From<ColorSpace> for u8 {
fn from(value: ColorSpace) -> Self { #[inline]
value.to_u8() fn from(colorspace: ColorSpace) -> Self {
colorspace as u8
} }
} }
impl Debug for ColorSpace { impl TryFrom<u8> for ColorSpace {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { type Error = Error;
write!(
f, #[inline]
"ColorSpace({}{}{}{})", fn try_from(colorspace: u8) -> Result<Self> {
self.r_linear as u8, self.g_linear as u8, self.b_linear as u8, self.a_linear as u8 if unlikely(colorspace | 1 != 1) {
) Err(Error::InvalidColorSpace { colorspace })
} else {
Ok(if colorspace == 0 { Self::Srgb } else { Self::Linear })
}
} }
} }

View file

@ -123,8 +123,7 @@ pub fn qoi_decode_to_vec(
data: impl AsRef<[u8]>, channels: impl MaybeChannels, data: impl AsRef<[u8]>, channels: impl MaybeChannels,
) -> Result<(Header, Vec<u8>)> { ) -> Result<(Header, Vec<u8>)> {
let data = data.as_ref(); let data = data.as_ref();
let header = qoi_decode_header(data)?; let header = Header::decode(data)?;
header.validate()?;
let channels = channels.maybe_channels().unwrap_or(header.channels); let channels = channels.maybe_channels().unwrap_or(header.channels);
match (channels, header.channels) { match (channels, header.channels) {
(3, 3) => Ok((header, qoi_decode_impl::<3, false>(data, header.n_pixels())?)), (3, 3) => Ok((header, qoi_decode_impl::<3, false>(data, header.n_pixels())?)),
@ -137,11 +136,5 @@ pub fn qoi_decode_to_vec(
#[inline] #[inline]
pub fn qoi_decode_header(data: impl AsRef<[u8]>) -> Result<Header> { pub fn qoi_decode_header(data: impl AsRef<[u8]>) -> Result<Header> {
let data = data.as_ref(); Header::decode(data)
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))
} }

View file

@ -1,3 +1,5 @@
use std::convert::TryInto;
use crate::colorspace::ColorSpace; use crate::colorspace::ColorSpace;
use crate::consts::{ use crate::consts::{
QOI_HEADER_SIZE, QOI_OP_DIFF, QOI_OP_INDEX, QOI_OP_LUMA, QOI_OP_RGB, QOI_OP_RGBA, QOI_OP_RUN, 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 = let header =
Header { width, height, channels: CHANNELS as u8, colorspace, ..Header::default() }; 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 index = [Pixel::new(); 256];
let mut px_prev = Pixel::new().with_a(0xff); let mut px_prev = Pixel::new().with_a(0xff);
@ -129,13 +131,18 @@ where
} }
#[inline] #[inline]
pub fn qoi_encode_to_buf( pub fn qoi_encode_to_buf<O, D, C>(
mut out: impl AsMut<[u8]>, data: impl AsRef<[u8]>, width: u32, height: u32, channels: u8, mut out: O, data: D, width: u32, height: u32, channels: u8, colorspace: C,
colorspace: impl Into<ColorSpace>, ) -> Result<usize>
) -> Result<usize> { where
O: AsMut<[u8]>,
D: AsRef<[u8]>,
C: TryInto<ColorSpace>,
Error: From<C::Error>,
{
let out = out.as_mut(); let out = out.as_mut();
let data = data.as_ref(); let data = data.as_ref();
let colorspace = colorspace.into(); let colorspace = colorspace.try_into()?;
match channels { match channels {
3 => qoi_encode_impl::<3>(out, data, width, height, colorspace), 3 => qoi_encode_impl::<3>(out, data, width, height, colorspace),
4 => qoi_encode_impl::<4>(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] #[inline]
pub fn qoi_encode_to_vec( pub fn qoi_encode_to_vec<D, C>(
data: impl AsRef<[u8]>, width: u32, height: u32, channels: u8, data: D, width: u32, height: u32, channels: u8, colorspace: C,
colorspace: impl Into<ColorSpace>, ) -> Result<Vec<u8>>
) -> Result<Vec<u8>> { where
let data = data.as_ref(); D: AsRef<[u8]>,
let colorspace = colorspace.into(); C: TryInto<ColorSpace>,
Error: From<C::Error>,
{
let size = encode_size_required(width, height, channels); 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 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)?; let size = qoi_encode_to_buf(&mut out, data, width, height, channels, colorspace)?;

View file

@ -15,7 +15,7 @@ pub enum Error {
OutputBufferTooSmall { size: usize, required: usize }, OutputBufferTooSmall { size: usize, required: usize },
InvalidMagic { magic: u32 }, InvalidMagic { magic: u32 },
UnexpectedBufferEnd, UnexpectedBufferEnd,
// TODO: invalid colorspace InvalidColorSpace { colorspace: u8 },
} }
pub type Result<T> = StdResult<T, Error>; pub type Result<T> = StdResult<T, Error>;
@ -48,6 +48,9 @@ impl Display for Error {
Self::UnexpectedBufferEnd => { Self::UnexpectedBufferEnd => {
write!(f, "unexpected input buffer end while decoding") write!(f, "unexpected input buffer end while decoding")
} }
Self::InvalidColorSpace { colorspace } => {
write!(f, "invalid color space: {} (expected 0 or 1)", colorspace)
}
} }
} }
} }

View file

@ -43,12 +43,12 @@ const fn u32_from_be(v: &[u8]) -> u32 {
impl Header { impl Header {
#[inline] #[inline]
pub const fn new(width: u32, height: u32, channels: u8) -> Self { 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] #[inline]
pub fn with_colorspace(mut self, colorspace: impl Into<ColorSpace>) -> Self { pub fn with_colorspace(mut self, colorspace: ColorSpace) -> Self {
self.colorspace = colorspace.into(); self.colorspace = colorspace;
self self
} }
@ -58,7 +58,7 @@ impl Header {
} }
#[inline] #[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]; let mut out = [0; QOI_HEADER_SIZE];
out[..4].copy_from_slice(&u32_to_be(self.magic)); out[..4].copy_from_slice(&u32_to_be(self.magic));
out[4..8].copy_from_slice(&u32_to_be(self.width)); out[4..8].copy_from_slice(&u32_to_be(self.width));
@ -69,32 +69,30 @@ impl Header {
} }
#[inline] #[inline]
pub(crate) fn from_bytes(v: [u8; QOI_HEADER_SIZE]) -> Self { pub(crate) fn decode(data: impl AsRef<[u8]>) -> Result<Self> {
Self { let data = data.as_ref();
magic: u32_from_be(&v[..4]), if unlikely(data.len() < QOI_HEADER_SIZE) {
width: u32_from_be(&v[4..8]), return Err(Error::InputBufferTooSmall { size: data.len(), required: QOI_HEADER_SIZE });
height: u32_from_be(&v[8..12]),
channels: v[12],
colorspace: v[13].into(),
} }
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] #[inline]
pub const fn n_pixels(&self) -> usize { pub const fn n_pixels(&self) -> usize {
(self.width as usize).saturating_mul(self.height as 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(())
}
} }