diff --git a/in/1kb.png b/in/1kb.png new file mode 100644 index 0000000..2dc8bf3 Binary files /dev/null and b/in/1kb.png differ diff --git a/out/1kb.qoi b/out/1kb.qoi new file mode 100644 index 0000000..da76eaa Binary files /dev/null and b/out/1kb.qoi differ diff --git a/out/dice.qoi b/out/dice.qoi new file mode 100644 index 0000000..2358313 Binary files /dev/null and b/out/dice.qoi differ diff --git a/src/common.rs b/src/common.rs index 7f15149..edc0863 100644 --- a/src/common.rs +++ b/src/common.rs @@ -14,15 +14,85 @@ pub enum Colorspace { Linear } +impl Colorspace { + pub fn from_repr (i: u8) -> Option { + 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)] pub enum Channels { RGB, RGBA } +impl Channels { + pub fn num (&self) -> usize { + match self { + Self::RGBA => 4, + Self::RGB => 3, + } + } + + pub fn ops_color (&self, bytes: &[u8]) -> Option { + 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 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 } + +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 { + 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 + } +} diff --git a/src/decoder.rs b/src/decoder.rs index 5611f61..0a8d665 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -11,19 +11,11 @@ impl Header { 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()?))?; + let colorspace = Colorspace::from_repr(u8::from_be_bytes(header[13..14].try_into().ok()?))?; Some(Self { width, height, channels, colorspace }) } - fn colorspace (space: u8) -> Option { - match space { - 0 => Some(Colorspace::Srgb), - 1 => Some(Colorspace::Linear), - _ => None - } - } - fn channels (ch: u8) -> Option { match ch { 3 => Some(Channels::RGB), diff --git a/src/encoder.rs b/src/encoder.rs index e69de29..3a1dbf7 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -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 { + // 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> { + 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> { + 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> { + 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); + } +} diff --git a/src/lib.rs b/src/lib.rs index 20955d0..71091b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(decl_macro)] + pub mod common; pub mod encoder;