initial commit (decoder implementation)

This commit is contained in:
YK 2024-07-05 16:30:52 +03:00
commit 3904ae05cf
26 changed files with 1471 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1217
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

10
Cargo.toml Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

BIN
in/dice.qoi Normal file

Binary file not shown.

BIN
in/edgecase.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
in/edgecase.qoi Normal file

Binary file not shown.

BIN
in/kodim10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

BIN
in/kodim10.qoi Normal file

Binary file not shown.

BIN
in/kodim23.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

BIN
in/kodim23.qoi Normal file

Binary file not shown.

BIN
in/qoi_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
in/qoi_logo.qoi Normal file

Binary file not shown.

BIN
in/testcard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
in/testcard.qoi Normal file

Binary file not shown.

BIN
in/testcard_rgba.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
in/testcard_rgba.qoi Normal file

Binary file not shown.

BIN
in/wikipedia_008.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
in/wikipedia_008.qoi Normal file

Binary file not shown.

BIN
out/dice.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

BIN
out/edgecase.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
out/edgecase.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

28
src/common.rs Normal file
View 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
View 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
View File

13
src/lib.rs Normal file
View File

@ -0,0 +1,13 @@
pub mod common;
pub mod encoder;
pub mod decoder;
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
}
}