Compare commits
No commits in common. "64c4499569a79a7b7abe8b2e2324c19cc78e8e18" and "3904ae05cf1a1f76b01d6b31ee69a53acd6fd916" have entirely different histories.
64c4499569
...
3904ae05cf
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -744,7 +744,6 @@ dependencies = [
|
|||||||
name = "qoi2"
|
name = "qoi2"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"imageproc",
|
"imageproc",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
|
|||||||
@ -4,7 +4,6 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.86"
|
|
||||||
bytemuck = "1.16.1"
|
bytemuck = "1.16.1"
|
||||||
imageproc = "0.25.0"
|
imageproc = "0.25.0"
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
@TODO replace Options with custom Errors
|
|
||||||
@TODO encoder wrappers for RgbaImage/RgbImage
|
|
||||||
@TODO write tests for the entire image set
|
|
||||||
@TODO write qoi-x-qoi and x-qoi-x tests to ensure correctness
|
|
||||||
BIN
in/1kb.png
BIN
in/1kb.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 KiB |
BIN
in/1kb.qoi
BIN
in/1kb.qoi
Binary file not shown.
BIN
out/1kb.qoi
BIN
out/1kb.qoi
Binary file not shown.
BIN
out/dice.qoi
BIN
out/dice.qoi
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 250 KiB |
BIN
out/kodim23.qoi
BIN
out/kodim23.qoi
Binary file not shown.
@ -14,103 +14,15 @@ 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),
|
|
||||||
}
|
|
||||||
|
|
||||||
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, _]: [i16; 4], [r1, g1, b1, _]: [i16; 4]) -> [i16; 3] {
|
|
||||||
[diff(r0, r1), diff(g0, g1), diff(b0, b1)]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn diff_ops (left: [u8; 4], right: [u8; 4]) -> Option<Ops> {
|
|
||||||
if left[3] != right[3] { return None; }
|
|
||||||
let left: [i16; 4] = left.map(i16::from);
|
|
||||||
let right: [i16; 4] = right.map(i16::from);
|
|
||||||
|
|
||||||
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 let Some(luma) = new_luma(diff) {
|
|
||||||
Some(luma)
|
|
||||||
} else { None }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_luma ([r, g, b]: [i16; 3]) -> Option<Ops> {
|
|
||||||
let drdg = r - g;
|
|
||||||
let dbdg = b - g;
|
|
||||||
if (-32..32).contains(&g) && (-8..8).contains(&drdg) && (-8..8).contains(&dbdg) {
|
|
||||||
return Some(Ops::Luma([drdg as i8, g as i8, dbdg as i8]));
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn diff (lhs: i16, rhs: i16) -> i16 {
|
|
||||||
let mut diff = lhs - rhs;
|
|
||||||
if diff.abs() > 128 {
|
|
||||||
diff = 256 - rhs + lhs + 1 * match diff.is_negative() {
|
|
||||||
true => 1,
|
|
||||||
false => -1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
diff
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cast_with_bias (num: i8, bias: i8) -> u8 {
|
|
||||||
(num + bias) as u8
|
|
||||||
}
|
|
||||||
|
|||||||
@ -11,11 +11,19 @@ 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 = Colorspace::from_repr(u8::from_be_bytes(header[13..14].try_into().ok()?))?;
|
let colorspace = Self::colorspace(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),
|
||||||
@ -57,16 +65,17 @@ impl Decoder for Vec<[u8; 4]> {
|
|||||||
(known[q as usize & 63], 1)
|
(known[q as usize & 63], 1)
|
||||||
},
|
},
|
||||||
q if q >> 6 == 1 => { // QOI_OP_DIFF
|
q if q >> 6 == 1 => { // QOI_OP_DIFF
|
||||||
let mut diff = [((q & 48) >> 4) as i8 - 2, ((q & 12) >> 2) as i8 - 2, (q & 3) as i8 - 2, 0].into_iter();
|
let dr = ((q & 48) >> 4) as i8 - 2;
|
||||||
(prev.map(|p| p.wrapping_add_signed(diff.next().unwrap())), 1)
|
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
|
q if q >> 6 == 2 => { // QOI_OP_LUMA
|
||||||
let extra = *iter.next()?;
|
let extra = *iter.next()?;
|
||||||
let dg = (q & 63) as i8 - 32;
|
let dg = (q & 63) as i8 - 32;
|
||||||
let dr = ((extra & 240) >> 4) as i8 - 8 + dg;
|
let dr = ((extra & 240) >> 4) as i8 - 8;
|
||||||
let db = (extra & 15) as i8 - 8 + dg;
|
let db = (extra & 15) as i8 - 8;
|
||||||
let mut diff = [dr, dg, db, 0].into_iter();
|
([ prev[0].wrapping_add_signed(dr + dg), prev[1].wrapping_add_signed(dg), prev[2].wrapping_add_signed(db + dg), prev[3]], 1)
|
||||||
(prev.map(|p| p.wrapping_add_signed(diff.next().unwrap())), 1)
|
|
||||||
},
|
},
|
||||||
q if q >> 6 == 3 => { // QOI_OP_RUN
|
q if q >> 6 == 3 => { // QOI_OP_RUN
|
||||||
let len = ((q & 63) + 1) as usize;
|
let len = ((q & 63) + 1) as usize;
|
||||||
@ -190,12 +199,4 @@ mod test {
|
|||||||
|
|
||||||
rgb.save_with_format("out/edgecase.jpg", imageproc::image::ImageFormat::Jpeg).unwrap();
|
rgb.save_with_format("out/edgecase.jpg", imageproc::image::ImageFormat::Jpeg).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dice_generic() {
|
|
||||||
let file = std::fs::read("in/dice.qoi").unwrap();
|
|
||||||
let rgba: RgbaImage = decode(&file).unwrap();
|
|
||||||
rgba.save("out/dice_generic.png").unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
147
src/encoder.rs
147
src/encoder.rs
@ -1,147 +0,0 @@
|
|||||||
use std::mem::discriminant;
|
|
||||||
|
|
||||||
use itertools::Itertools;
|
|
||||||
use crate::common::*;
|
|
||||||
|
|
||||||
|
|
||||||
impl Ops {
|
|
||||||
fn write_encoded (self, buf: &mut 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::Rgb([r, g, b]) => buf.extend_from_slice(&[254, r, g, b]),
|
|
||||||
Ops::Rgba([r, g, b, a]) => buf.extend_from_slice(&[255, r, g, b, a]),
|
|
||||||
Ops::Index(idx) => buf.push(idx as u8),
|
|
||||||
Ops::Run(len) => buf.push((3 << 6) | (len - 1)),
|
|
||||||
Ops::Diff([dr, dg, db]) => buf.push((1 << 6) | ((cd(dr) << 4) | (cd(dg) << 2) | cd(db))),
|
|
||||||
Ops::Luma([dr, dg, db]) => buf.extend_from_slice(&[(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::with_capacity(header.width as usize * header.height as usize);
|
|
||||||
let mut cur = Ops::Rgba([0, 0, 0, 255]);
|
|
||||||
|
|
||||||
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, write it, and start a new one
|
|
||||||
Ops::Run(run) if run >= 62 => {
|
|
||||||
cur = Ops::Run(62); // clamp for safety, or return None? @Q
|
|
||||||
cur.write_encoded(&mut out);
|
|
||||||
cur = Ops::Run(1);
|
|
||||||
},
|
|
||||||
// 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)) {
|
|
||||||
cur.write_encoded(&mut out);
|
|
||||||
}
|
|
||||||
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, a], last) {
|
|
||||||
cur = ops;
|
|
||||||
} else {
|
|
||||||
cur = header.channels.ops_color(&rgba[..header.channels.num()])?;
|
|
||||||
}
|
|
||||||
cur.write_encoded(&mut out);
|
|
||||||
last = rgba;
|
|
||||||
known[hash] = rgba;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if an image buffer ends with a run, flush it as well
|
|
||||||
if discriminant(&cur) == discriminant(&Ops::Run(0)) {
|
|
||||||
cur.write_encoded(&mut out);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn kodim23 () {
|
|
||||||
let file = imageproc::image::io::Reader::open("in/kodim23.png").unwrap().decode().unwrap();
|
|
||||||
let file = file.into_rgb8().into_raw();
|
|
||||||
let header = Header { width: 768, height: 512, channels: Channels::RGB, colorspace: Colorspace::Srgb };
|
|
||||||
|
|
||||||
|
|
||||||
let inst = Instant::now();
|
|
||||||
let encoded = encode_full(header, &file).unwrap();
|
|
||||||
println!("kodim23 encoding done in {} ms", inst.elapsed().as_millis());
|
|
||||||
let _write = std::fs::write("out/kodim23.qoi", &encoded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,3 @@
|
|||||||
#![feature(decl_macro)]
|
|
||||||
|
|
||||||
pub mod common;
|
pub mod common;
|
||||||
|
|
||||||
pub mod encoder;
|
pub mod encoder;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user