#![no_std]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
use embedded_graphics::{pixelcolor::Rgb888, prelude::*};
const MAGIC: &[u8] = b"qoif";
const HEADER_LENGTH: usize = 14;
const STREAM_END: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 1];
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Qoi<'a> {
data: &'a [u8],
size: Size,
}
impl<'a> Qoi<'a> {
pub fn new(data: &'a [u8]) -> Result<Self, Error> {
if data.len() < (HEADER_LENGTH + STREAM_END.len()) {
return Err(Error::TruncatedFile);
}
let (header, data) = data.split_at(HEADER_LENGTH);
let (magic, header) = header.split_at(MAGIC.len());
let (data, end) = data.split_at(data.len() - STREAM_END.len());
if magic != MAGIC {
return Err(Error::InvalidMagic);
}
if end != STREAM_END {
return Err(Error::TruncatedFile);
}
let width = u32::from_be_bytes(header[0..4].try_into().unwrap());
let height = u32::from_be_bytes(header[4..8].try_into().unwrap());
let _channels = header[8];
let _colorspace = header[9];
Ok(Self {
data,
size: Size::new(width, height),
})
}
pub fn pixels(&'a self) -> PixelsIter<'a> {
PixelsIter::new(self)
}
}
impl ImageDrawable for Qoi<'_> {
type Color = Rgb888;
fn draw<D>(&self, target: &mut D) -> Result<(), D::Error>
where
D: DrawTarget<Color = Self::Color>,
{
target.fill_contiguous(&self.bounding_box(), self.pixels())
}
fn draw_sub_image<D>(
&self,
target: &mut D,
area: &embedded_graphics::primitives::Rectangle,
) -> Result<(), D::Error>
where
D: DrawTarget<Color = Self::Color>,
{
self.draw(&mut target.translated(-area.top_left).clipped(area))
}
}
impl OriginDimensions for Qoi<'_> {
fn size(&self) -> Size {
self.size
}
}
fn hash_pixel(pixel: Rgb888, alpha: u8) -> u8 {
pixel
.r()
.wrapping_mul(3)
.wrapping_add(pixel.g().wrapping_mul(5))
.wrapping_add(pixel.b().wrapping_mul(7))
.wrapping_add(alpha.wrapping_mul(11))
% 64
}
#[derive(Debug)]
pub struct PixelsIter<'a> {
previous_color: Rgb888,
previous_alpha: u8,
previous_colors: [Rgb888; 64],
previous_alphas: [u8; 64],
data: &'a [u8],
run_length: u8,
}
impl<'a> PixelsIter<'a> {
fn new(qoi: &'a Qoi<'a>) -> Self {
Self {
previous_color: Rgb888::BLACK,
previous_alpha: 255,
previous_colors: [Rgb888::BLACK; 64],
previous_alphas: [0; 64],
data: qoi.data,
run_length: 0,
}
}
}
impl Iterator for PixelsIter<'_> {
type Item = Rgb888;
fn next(&mut self) -> Option<Self::Item> {
if self.run_length > 0 {
self.run_length -= 1;
return Some(self.previous_color);
}
let (byte, rest) = self.data.split_first()?;
self.data = rest;
match byte {
0b11111110 => {
if self.data.len() >= 3 {
self.previous_color = Rgb888::new(self.data[0], self.data[1], self.data[2]);
self.data = &self.data[3..];
} else {
return None;
}
}
0b11111111 => {
if self.data.len() >= 4 {
self.previous_color = Rgb888::new(self.data[0], self.data[1], self.data[2]);
self.previous_alpha = self.data[3];
self.data = &self.data[4..];
} else {
return None;
}
}
_ => match byte & 0b11000000 {
0b00000000 => {
let index = usize::from(byte & 0x3F);
self.previous_color = self.previous_colors[index];
self.previous_alpha = self.previous_alphas[index];
return Some(self.previous_color);
}
0b01000000 => {
let dr = (byte >> 4) & 0x3;
let dg = (byte >> 2) & 0x3;
let db = byte & 0x3;
let r = self.previous_color.r().wrapping_add(dr).wrapping_sub(2);
let g = self.previous_color.g().wrapping_add(dg).wrapping_sub(2);
let b = self.previous_color.b().wrapping_add(db).wrapping_sub(2);
self.previous_color = Rgb888::new(r, g, b);
}
0b10000000 => {
if self.data.len() >= 1 {
let byte2 = self.data[0];
self.data = &self.data[1..];
let dg = (byte & 0x3F).wrapping_sub(32);
let dr = (byte2 >> 4).wrapping_sub(8).wrapping_add(dg);
let db = (byte2 & 0x0F).wrapping_sub(8).wrapping_add(dg);
let r = self.previous_color.r().wrapping_add(dr);
let g = self.previous_color.g().wrapping_add(dg);
let b = self.previous_color.b().wrapping_add(db);
self.previous_color = Rgb888::new(r, g, b);
} else {
return None;
}
}
0b11000000 | _ => {
self.run_length = byte & 0x3F;
return Some(self.previous_color);
}
},
}
let index = usize::from(hash_pixel(self.previous_color, self.previous_alpha));
self.previous_colors[index] = self.previous_color;
self.previous_alphas[index] = self.previous_alpha;
Some(self.previous_color)
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Error {
InvalidMagic,
TruncatedFile,
}
#[cfg(test)]
mod tests {
use embedded_graphics::{image::Image, mock_display::MockDisplay};
use super::*;
#[test]
fn invalid_magic() {
let data = b"not a valid qoi file!!!!!!!!";
assert_eq!(Qoi::new(data), Err(Error::InvalidMagic));
}
#[test]
fn truncated_header() {
let data = b"too short";
assert_eq!(Qoi::new(data), Err(Error::TruncatedFile));
}
#[test]
fn truncated_file() {
let data = include_bytes!("../tests/colors.qoi");
let (_, data) = data.split_last().unwrap();
assert_eq!(Qoi::new(data), Err(Error::TruncatedFile));
}
#[test]
fn image() {
let data = include_bytes!("../tests/colors.qoi");
let qoi = Qoi::new(data).unwrap();
assert_eq!(qoi.size(), Size::new(3, 3));
let mut display = MockDisplay::<Rgb888>::new();
Image::new(&qoi, Point::zero()).draw(&mut display).unwrap();
display.assert_pattern(&[
"RGB", "WWW", "KKK", ]);
}
}