Rework ColorSpace to match the latest spec (0 | 1)
This commit is contained in:
parent
c39844fb98
commit
2dcbdd19c2
5 changed files with 81 additions and 95 deletions
|
@ -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 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue