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() {
|
||||||
|
}
|
||||||
|
}
|
||||||