encoder impl (not entirely correct yet)

This commit is contained in:
YK 2024-07-10 17:47:33 +03:00
parent 5b6c75de6a
commit 5c12875e81
7 changed files with 207 additions and 9 deletions

BIN
in/1kb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
out/1kb.qoi Normal file

Binary file not shown.

BIN
out/dice.qoi Normal file

Binary file not shown.

View File

@ -14,15 +14,85 @@ pub enum Colorspace {
Linear Linear
} }
impl Colorspace {
pub fn from_repr (i: u8) -> Option<Self> {
match i {
0 => Some(Self::Srgb),
1 => Some(Self::Linear),
_ => None
}
}
pub fn to_repr (&self) -> u8 {
match self {
Self::Srgb => 0,
Self::Linear => 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum Channels { pub enum Channels {
RGB, RGB,
RGBA RGBA
} }
impl Channels {
pub fn num (&self) -> usize {
match self {
Self::RGBA => 4,
Self::RGB => 3,
}
}
pub fn ops_color (&self, bytes: &[u8]) -> Option<Ops> {
match self {
Channels::RGB => Some(Ops::Rgb(bytes.try_into().ok()?)),
Channels::RGBA => Some(Ops::Rgba(bytes.try_into().ok()?)),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Ops {
Index (usize),
Run (u8),
Luma ([i8; 3]),
Diff ([i8; 3]),
Rgb (RgbColor),
Rgba (RgbaColor),
Nop
}
type RgbColor = [u8; 3];
type RgbaColor = [u8; 4];
pub const MAGIC: [u8; 4] = [b'q', b'o', b'i', b'f']; pub const MAGIC: [u8; 4] = [b'q', b'o', b'i', b'f'];
pub fn hash ([r, g, b, a]: [u8; 4]) -> usize { 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 (r as usize * 3 + g as usize * 5 + b as usize * 7 + a as usize * 11) % 64
} }
pub fn calc_diff ([r0, g0, b0]: [u8; 3], [r1, g1, b1]: [u8; 3]) -> [i16; 3] {
[idiff!(r0, r1), idiff!(g0, g1), idiff!(b0, b1)]
}
pub fn diff_ops (left: [u8; 3], right: [u8; 3]) -> Option<Ops> {
let diff = calc_diff(left, right);
if diff.iter().all(|d| (-2..2).contains(d)) {
Some(Ops::Diff(diff.map(|e| e as i8)))
} else if (-32..32).contains(&diff[1]) && (-8..8).contains(&(diff[0] - diff[1])) && (-8..8).contains(&(diff[2] - diff[1])) {
Some(Ops::Luma([(diff[0] - diff[1]) as i8, diff[1] as i8, (diff[2] - diff[1]) as i8]))
} else { None }
}
pub fn cast_with_bias (num: i8, bias: i8) -> u8 {
(num + bias) as u8
}
pub macro idiff {
($t1: expr, $t2: expr) => {
$t1 as i16 - $t2 as i16
}
}

View File

@ -11,19 +11,11 @@ impl Header {
let height = u32::from_be_bytes(header[8..12].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 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()?))?; let colorspace = Colorspace::from_repr(u8::from_be_bytes(header[13..14].try_into().ok()?))?;
Some(Self { width, height, channels, colorspace }) 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> { fn channels (ch: u8) -> Option<Channels> {
match ch { match ch {
3 => Some(Channels::RGB), 3 => Some(Channels::RGB),

View File

@ -0,0 +1,134 @@
use std::mem::discriminant;
use itertools::Itertools;
use crate::common::*;
impl Ops {
// @TODO don't alloc here, get a mut reference and extend here
fn format (self) -> Vec<u8> {
// cast_diff
let cd = |diff: i8| cast_with_bias(diff, 2);
// cast_luma_green
let clg = |diff: i8| cast_with_bias(diff, 32);
// cast_luma_red_blue
let clrb = |diff: i8| cast_with_bias(diff, 8);
match self {
Ops::Nop => vec![],
Ops::Rgb([r, g, b]) => vec![254, r, g, b],
Ops::Rgba([r, g, b, a]) => vec![255, r, g, b, a],
Ops::Index(idx) => vec![0b1111 & idx as u8],
Ops::Run(len) => vec![(3 << 6) | (len - 1)],
Ops::Diff([dr, dg, db]) => vec![(1 << 6) | ((cd(dr) << 4) | (cd(dg) << 2) | cd(db)) ],
Ops::Luma([dr, dg, db]) => vec![(2 << 6) | clg(dg), (clrb(dr) << 4) | clrb(db)],
}
}
}
fn encode_header (header: Header) -> Option<Vec<u8>> {
let mut out = vec![];
out.extend_from_slice(&MAGIC);
out.extend_from_slice(&header.width.to_be_bytes());
out.extend_from_slice(&header.height.to_be_bytes());
out.extend_from_slice(&(header.channels.num() as u8).to_be_bytes());
out.extend_from_slice(&header.colorspace.to_repr().to_be_bytes());
if out.len() != 14 { return None; }
Some(out)
}
fn encode_body (header: Header, data: &[u8]) -> Option<Vec<u8>> {
let mut known = [[0u8; 4]; 64];
let mut last = [0u8, 0, 0, 255];
let mut out = vec![];
let mut cur = Ops::Nop;
for mut chunk in &data.iter().chunks(header.channels.num()) {
let (&r, &g, &b) = chunk.next_tuple()?;
// if we need only 3 channels, we use a placeholder 255 for alpha and ignore it during write
let a = *chunk.next().unwrap_or(&255);
let rgba = [r, g, b, a];
// If current pixel value is the same as of the previous pixel, we need to start/continue a run
if rgba == last {
match cur {
// if run is in bounds 0..62
Ops::Run(run) if run < 62 => cur = Ops::Run(run + 1),
// if run is full, commit it to out vec, and start a new one
Ops::Run(run) if run >= 62 => {
cur = Ops::Run(1);
out.extend_from_slice(&Ops::Run(62).format());
},
// otherwise (default case, when encountering a match for the first time) start a run
_ => cur = Ops::Run(1),
}
} else {
// if current Op is Run (meaning we need to and it since there's no match)
if discriminant(&cur) == discriminant(&Ops::Run(0)) {
out.extend_from_slice(&cur.format());
}
let hash = hash(rgba);
// INDEX -> DIFF -> LUMA -> RGB(A)
if known[hash] == rgba {
cur = Ops::Index(hash);
} else if let Some(ops) = diff_ops([r, g, b], [last[0], last[1], last[2]]) {
cur = ops;
} else {
cur = header.channels.ops_color(&rgba[..header.channels.num()])?;
}
out.extend_from_slice(&cur.format());
last = rgba;
known[hash] = rgba;
}
}
Some(out)
}
fn encode_full (header: Header, bytes: &[u8]) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(header.width as usize * header.height as usize * 4);
out.extend_from_slice(&encode_header(header)?);
out.extend_from_slice(&encode_body(header, bytes)?);
out.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 1]);
Some(out)
}
#[cfg(test)]
mod test {
use std::time::Instant;
use super::*;
#[test]
fn basic () {
let file = imageproc::image::io::Reader::open("in/dice.png").unwrap().decode().unwrap();
let file = file.into_rgba8().into_raw();
let header = Header { width: 800, height: 600, channels: Channels::RGBA, colorspace: Colorspace::Srgb };
let inst = Instant::now();
let encoded = encode_full(header, &file).unwrap();
println!("dice encoding done in {} ms", inst.elapsed().as_millis());
let _write = std::fs::write("out/dice.qoi", &encoded);
}
#[test]
fn _1kb () {
let file = imageproc::image::io::Reader::open("in/1kb.png").unwrap().decode().unwrap();
let file = file.into_rgba8().into_raw();
let header = Header { width: 17, height: 14, channels: Channels::RGBA, colorspace: Colorspace::Srgb };
let inst = Instant::now();
let encoded = encode_full(header, &file).unwrap();
println!("1kb encoding done in {} ms", inst.elapsed().as_millis());
let _write = std::fs::write("out/1kb.qoi", &encoded);
}
}

View File

@ -1,3 +1,5 @@
#![feature(decl_macro)]
pub mod common; pub mod common;
pub mod encoder; pub mod encoder;