initial commit (decoder implementation)
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
||||
1217
Cargo.lock
generated
Normal file
10
Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "qoi2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytemuck = "1.16.1"
|
||||
imageproc = "0.25.0"
|
||||
itertools = "0.13.0"
|
||||
thiserror = "1.0.61"
|
||||
BIN
in/dice.png
Normal file
|
After Width: | Height: | Size: 342 KiB |
BIN
in/dice.qoi
Normal file
BIN
in/edgecase.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
in/edgecase.qoi
Normal file
BIN
in/kodim10.png
Normal file
|
After Width: | Height: | Size: 580 KiB |
BIN
in/kodim10.qoi
Normal file
BIN
in/kodim23.png
Normal file
|
After Width: | Height: | Size: 544 KiB |
BIN
in/kodim23.qoi
Normal file
BIN
in/qoi_logo.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
in/qoi_logo.qoi
Normal file
BIN
in/testcard.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
in/testcard.qoi
Normal file
BIN
in/testcard_rgba.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
in/testcard_rgba.qoi
Normal file
BIN
in/wikipedia_008.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
in/wikipedia_008.qoi
Normal file
BIN
out/dice.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
out/edgecase.jpg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
out/edgecase.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
28
src/common.rs
Normal file
@ -0,0 +1,28 @@
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Header {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub channels: Channels,
|
||||
pub colorspace: Colorspace,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Colorspace {
|
||||
Srgb,
|
||||
Linear
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Channels {
|
||||
RGB,
|
||||
RGBA
|
||||
}
|
||||
|
||||
|
||||
pub const MAGIC: [u8; 4] = [b'q', b'o', b'i', b'f'];
|
||||
|
||||
pub fn hash ([r, g, b, a]: [u8; 4]) -> usize {
|
||||
(r as usize * 3 + g as usize * 5 + b as usize * 7 + a as usize * 11) % 64
|
||||
}
|
||||
202
src/decoder.rs
Normal file
@ -0,0 +1,202 @@
|
||||
use imageproc::image::{ RgbImage, RgbaImage };
|
||||
use itertools::Itertools;
|
||||
use crate::common::*;
|
||||
|
||||
impl Header {
|
||||
pub fn read (header: &[u8]) -> Option<Self> {
|
||||
let magic = header[..4].iter().eq(MAGIC.iter());
|
||||
if !magic { return None; }
|
||||
|
||||
let width = u32::from_be_bytes( header[4..8].try_into().ok()?);
|
||||
let height = u32::from_be_bytes(header[8..12].try_into().ok()?);
|
||||
|
||||
let channels = Self::channels(u8::from_be_bytes(header[12..13].try_into().ok()?))?;
|
||||
let colorspace = Self::colorspace(u8::from_be_bytes(header[13..14].try_into().ok()?))?;
|
||||
|
||||
Some(Self { width, height, channels, colorspace })
|
||||
}
|
||||
|
||||
fn colorspace (space: u8) -> Option<Colorspace> {
|
||||
match space {
|
||||
0 => Some(Colorspace::Srgb),
|
||||
1 => Some(Colorspace::Linear),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
fn channels (ch: u8) -> Option<Channels> {
|
||||
match ch {
|
||||
3 => Some(Channels::RGB),
|
||||
4 => Some(Channels::RGBA),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Decoder {
|
||||
fn decode (header: Header, bytes: &[u8]) -> Option<Self> where Self: Sized;
|
||||
}
|
||||
|
||||
impl Decoder for Vec<[u8; 4]> {
|
||||
/// decode a complete slice into array of pixel color values
|
||||
fn decode (header: Header, bytes: &[u8]) -> Option<Self> {
|
||||
if bytes.len() < 8 { return None; }
|
||||
let end = [0, 0, 0, 0, 0, 0, 0, 1];
|
||||
if bytes[(bytes.len() - 8)..] != end { return None; }
|
||||
let bytes = &bytes[..bytes.len() - 8];
|
||||
|
||||
let mut known = [[0; 4]; 64];
|
||||
let mut prev = [0, 0, 0, 255];
|
||||
let mut out = vec![];
|
||||
let mut iter = bytes.iter();
|
||||
|
||||
loop {
|
||||
let Some(&byte) = iter.next() else { break; };
|
||||
let (color, len) = match byte {
|
||||
254 => { // QOI_OP_RGB
|
||||
let (r, g, b) = iter.next_tuple().unwrap();
|
||||
([*r, *g, *b, prev[3]], 1)
|
||||
},
|
||||
255 => { // QOI_OP_RGBA
|
||||
let (r, g, b, a) = iter.next_tuple().unwrap();
|
||||
([*r, *g, *b, *a], 1)
|
||||
},
|
||||
q if q >> 6 == 0 => { // QOI_OP_INDEX
|
||||
(known[q as usize & 63], 1)
|
||||
},
|
||||
q if q >> 6 == 1 => { // QOI_OP_DIFF
|
||||
let dr = ((q & 48) >> 4) as i8 - 2;
|
||||
let dg = ((q & 12) >> 2) as i8 - 2;
|
||||
let db = (q & 3) as i8 - 2;
|
||||
([ prev[0].wrapping_add_signed(dr), prev[1].wrapping_add_signed(dg), prev[2].wrapping_add_signed(db), prev[3]], 1)
|
||||
},
|
||||
q if q >> 6 == 2 => { // QOI_OP_LUMA
|
||||
let extra = *iter.next()?;
|
||||
let dg = (q & 63) as i8 - 32;
|
||||
let dr = ((extra & 240) >> 4) as i8 - 8;
|
||||
let db = (extra & 15) as i8 - 8;
|
||||
([ prev[0].wrapping_add_signed(dr + dg), prev[1].wrapping_add_signed(dg), prev[2].wrapping_add_signed(db + dg), prev[3]], 1)
|
||||
},
|
||||
q if q >> 6 == 3 => { // QOI_OP_RUN
|
||||
let len = ((q & 63) + 1) as usize;
|
||||
(prev.clone(), len)
|
||||
},
|
||||
_ => return None
|
||||
};
|
||||
|
||||
known[hash(color)] = color;
|
||||
prev = color.clone();
|
||||
(0..len).for_each(|_| out.push(color));
|
||||
}
|
||||
|
||||
if header.width as u64 * header.height as u64 != out.len() as u64 { return None; }
|
||||
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for Vec<[u8; 3]> {
|
||||
fn decode (header: Header, bytes: &[u8]) -> Option<Self> where Self: Sized {
|
||||
let pixels: Vec<[u8; 4]> = Decoder::decode(header, bytes)?;
|
||||
let pixels: Vec<[u8; 3]> = pixels.iter().filter_map(|e| (&e[0..3]).try_into().ok()).collect();
|
||||
if pixels.len() as u64 != header.width as u64 * header.height as u64 { return None; }
|
||||
Some(pixels)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for Vec<u8> {
|
||||
fn decode (header: Header, bytes: &[u8]) -> Option<Self> where Self: Sized {
|
||||
let pixels: Vec<[u8; 4]> = Decoder::decode(header, bytes)?;
|
||||
if header.channels == Channels::RGBA {
|
||||
bytemuck::try_cast_vec(pixels).ok()
|
||||
} else {
|
||||
let pixels: Vec<[u8; 3]> = pixels.iter().filter_map(|e| (&e[0..3]).try_into().ok()).collect();
|
||||
if pixels.len() as u64 != header.width as u64 * header.height as u64 { return None; }
|
||||
bytemuck::try_cast_vec(pixels).ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for RgbaImage {
|
||||
fn decode (mut header: Header, bytes: &[u8]) -> Option<Self> where Self: Sized {
|
||||
header.channels = Channels::RGBA;
|
||||
let pixels = Decoder::decode(header, bytes)?;
|
||||
RgbaImage::from_raw(header.width, header.height, pixels)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for RgbImage {
|
||||
fn decode (mut header: Header, bytes: &[u8]) -> Option<Self> where Self: Sized {
|
||||
header.channels = Channels::RGB;
|
||||
let pixels = Decoder::decode(header, bytes)?;
|
||||
RgbImage::from_raw(header.width, header.height, pixels)
|
||||
}
|
||||
}
|
||||
|
||||
/// a generic decoder over a type which implements Decoder trait
|
||||
pub fn decode <T> (bytes: &[u8]) -> Option<T> where T: Decoder {
|
||||
let header = Header::read(&bytes[..14])?;
|
||||
let rest = &bytes[14..];
|
||||
Decoder::decode(header, rest)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::time::Instant;
|
||||
|
||||
use imageproc::image::RgbaImage;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn basic () {
|
||||
let file = std::fs::read("in/dice.qoi").unwrap();
|
||||
let now = Instant::now();
|
||||
|
||||
let header = Header::read(&file[..14]).unwrap();
|
||||
assert_eq!(Header { width: 800, height: 600, channels: Channels::RGBA, colorspace: Colorspace::Srgb }, header);
|
||||
|
||||
let rest = &file[14..];
|
||||
let pixels: Vec<[u8; 4]> = Decoder::decode(header, rest).unwrap();
|
||||
println!("dice decoding done in {}ms", now.elapsed().as_millis());
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
let pixels = bytemuck::try_cast_vec(pixels).unwrap();
|
||||
println!("dice cast done in {}ns", now.elapsed().as_nanos());
|
||||
let rgba = RgbaImage::from_raw(header.width, header.height, pixels).unwrap();
|
||||
rgba.save("out/dice.png").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge () {
|
||||
let file = std::fs::read("in/edgecase.qoi").unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
let header = Header::read(&file[..14]).unwrap();
|
||||
assert_eq!(Header { width: 256, height: 64, channels: Channels::RGBA, colorspace: Colorspace::Srgb }, header);
|
||||
|
||||
let rest = &file[14..];
|
||||
let pixels: Vec<[u8; 4]> = Decoder::decode(header, rest).unwrap();
|
||||
println!("edgecase decoding done in {}ms", now.elapsed().as_millis());
|
||||
|
||||
let pixels = bytemuck::try_cast_vec(pixels).unwrap();
|
||||
let rgba = RgbaImage::from_raw(header.width, header.height, pixels).unwrap();
|
||||
rgba.save("out/edgecase.png").unwrap();
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn edge_as_jpeg () {
|
||||
let file = std::fs::read("in/edgecase.qoi").unwrap();
|
||||
|
||||
let header = Header::read(&file[..14]).unwrap();
|
||||
assert_eq!(Header { width: 256, height: 64, channels: Channels::RGBA, colorspace: Colorspace::Srgb }, header);
|
||||
|
||||
let rest = &file[14..];
|
||||
let rgb = RgbImage::decode(header, rest).unwrap();
|
||||
|
||||
rgb.save_with_format("out/edgecase.jpg", imageproc::image::ImageFormat::Jpeg).unwrap();
|
||||
}
|
||||
}
|
||||
0
src/encoder.rs
Normal file
13
src/lib.rs
Normal file
@ -0,0 +1,13 @@
|
||||
pub mod common;
|
||||
|
||||
pub mod encoder;
|
||||
pub mod decoder;
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
}
|
||||
}
|
||||