Files
qoicodec/src/main.rs
Robert Garrett b9aa3cf28f Tests for prev_pixel and backbuffer
Gotta check that the previous pixel is recorded and the backbuffer is
filled. There are errors in the decoder that revolve around this.
2024-10-14 21:07:29 -05:00

554 lines
18 KiB
Rust

fn main() {
println!("Hello, world!");
}
#[derive(Clone, Copy, Default, Debug, PartialEq)]
struct PixelRGBA {
r: u8,
g: u8,
b: u8,
a: u8,
}
impl PixelRGBA {
fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
fn zero() -> Self {
Self::new(0, 0, 0, 0)
}
}
const QOI_OP_RGB: u8 = 0b1111_1110;
const QOI_OP_RGBA: u8 = 0b1111_1111;
const QOI_OP_INDEX: u8 = 0b0000_0000;
const QOI_OP_DIFF: u8 = 0b0100_0000;
const QOI_OP_LUMA: u8 = 0b1000_0000;
const QOI_OP_RUN: u8 = 0b1100_0000;
const QOI_OP_SMALL_MASK: u8 = 0b1100_0000; // mask for the small op codes
struct Decoder<I: Iterator<Item = u8>> {
// QOI codec state information
back_buffer: [PixelRGBA; 64],
prev_pixel: PixelRGBA,
bytes: I,
run_len: u8,
}
impl<I> Decoder<I>
where
I: Iterator<Item = u8>,
{
fn new(bytes: I) -> Self {
Self {
back_buffer: [PixelRGBA::zero(); 64],
prev_pixel: PixelRGBA {
r: 0,
g: 0,
b: 0,
a: 255,
},
bytes,
run_len: 0,
}
}
// A hack to unit test the index lookup behavior. A partial test can be done
// to verify the basic indexing principles by preloading a known buffer and
// then extracting back-referenced data out of it. A complete test should
// feed in other valid operations that populate the backbuffer, and then index
// op codes to demonstrate the indexing operations.
fn new_with_backbuffer(bytes: I, back_buffer: [PixelRGBA; 64]) -> Self {
Self {
back_buffer,
prev_pixel: PixelRGBA {
r: 0,
g: 0,
b: 0,
a: 255,
},
bytes,
run_len: 0,
}
}
// A hack to unit test the run behavior. Same idea as the new_with_backbuffer()
// function, but for testing a run of pixels.
fn new_with_previous_pixel(bytes: I, prev_pixel: PixelRGBA) -> Self {
Self {
back_buffer: [PixelRGBA::zero(); 64],
prev_pixel,
bytes,
run_len: 0,
}
}
pub(crate) fn peek_prev_pixel(&self) -> &PixelRGBA {
&self.prev_pixel
}
pub(crate) fn peek_backbuffer(&self, idx: usize) -> &PixelRGBA {
&self.back_buffer[idx]
}
}
mod codec_utils {
use super::PixelRGBA;
pub(crate) fn hash(pixel: PixelRGBA) -> u8 {
pixel
.r
.wrapping_mul(3)
.wrapping_add(pixel.g.wrapping_mul(5))
.wrapping_add(pixel.b.wrapping_mul(7))
.wrapping_add(pixel.a.wrapping_mul(11))
% 64
}
}
impl<'input, I> Iterator for Decoder<I>
where
I: Iterator<Item = u8>,
{
type Item = PixelRGBA;
fn next(&mut self) -> Option<Self::Item> {
if self.run_len > 0 {
self.run_len -= 1;
return Some(self.prev_pixel);
} else {
// Two kinds of patterns to match:
// 1. Whole byte tag -- RGB and RGBA
// 2. Partial byte tag -- 2 front bits of Index, Diff, Luma, and Run
let byte = self.bytes.next()?;
if byte == QOI_OP_RGB {
return Some(PixelRGBA {
r: self.bytes.next()?,
g: self.bytes.next()?,
b: self.bytes.next()?,
a: self.prev_pixel.a,
});
} else if byte == QOI_OP_RGBA {
return Some(PixelRGBA {
r: self.bytes.next()?,
g: self.bytes.next()?,
b: self.bytes.next()?,
a: self.bytes.next()?,
});
} else {
match byte & QOI_OP_SMALL_MASK {
QOI_OP_INDEX => {
let idx = (byte & !QOI_OP_SMALL_MASK) as usize;
return Some(self.back_buffer[idx]);
}
QOI_OP_DIFF => {
let dr = ((byte & 0b0011_0000) >> 4).wrapping_sub(2);
let dg = ((byte & 0b0000_1100) >> 2).wrapping_sub(2);
let db = (byte & 0b0000_0011).wrapping_sub(2);
let result = PixelRGBA {
r: self.prev_pixel.r.wrapping_add(dr),
g: self.prev_pixel.g.wrapping_add(dg),
b: self.prev_pixel.b.wrapping_add(db),
a: self.prev_pixel.a,
};
self.prev_pixel = result;
return Some(result);
}
QOI_OP_LUMA => {
let dg = (byte & !QOI_OP_SMALL_MASK).wrapping_sub(32);
let packed = self.bytes.next()?;
let drdg = ((packed & 0b1111_0000) >> 4).wrapping_sub(8);
let dbdg = (packed & 0b0000_1111).wrapping_sub(8);
let dr = drdg.wrapping_add(dg);
let db = dbdg.wrapping_add(dg);
let result = PixelRGBA {
r: self.prev_pixel.r.wrapping_add(dr),
g: self.prev_pixel.g.wrapping_add(dg),
b: self.prev_pixel.b.wrapping_add(db),
a: self.prev_pixel.a,
};
self.prev_pixel = result;
return Some(result);
}
QOI_OP_RUN => {
self.run_len = byte & !QOI_OP_SMALL_MASK;
// storage bias of -1, so a +1 should be on the end here.
// However, I'm immediately popping off the first occurrence
// and returning a PixelRGBA, so the count is also immediatly
// dropped by 1
return Some(self.prev_pixel);
}
_ => panic!("bad op code{}", byte),
}
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test] // this is mostly just to drive the function. Make sure it wraps or crashes in debug.
fn test_backref_hash_function() {
let pixel = PixelRGBA {
r: 100,
g: 80,
b: 90,
a: 255,
};
let expected = 39;
assert_eq!(codec_utils::hash(pixel), expected);
}
#[test]
fn decoder_unpack_rgb() {
// compressed RGB values should be expanded back out to RGBA
// with an assumed alpha of 0xFF.
let compressed = [
QOI_OP_RGB, 0xFF, 0xFF, 0xFF, QOI_OP_RGB, 0x7F, 0x00, 0xAD, QOI_OP_RGB, 0x00, 0x00,
0x00,
];
let expected = [
PixelRGBA {
r: 0xFF,
g: 0xFF,
b: 0xFF,
a: 0xFF,
},
PixelRGBA {
r: 0x7F,
g: 0x00,
b: 0xAD,
a: 0xFF,
},
PixelRGBA {
r: 0x00,
g: 0x00,
b: 0x00,
a: 0xFF,
},
];
let decoder = Decoder::new(compressed.into_iter());
let result = decoder.collect::<Vec<PixelRGBA>>();
assert_eq!(result, expected);
}
#[test]
fn decoder_unpack_rgba() {
let compressed = [
QOI_OP_RGBA,
0xFF,
0xFF,
0xFF,
0xFF,
QOI_OP_RGBA,
0x7F,
0x7F,
0x7F,
0xFF,
QOI_OP_RGBA,
0x10,
0x20,
0x30,
0x40,
];
let expected = [
PixelRGBA {
r: 0xFF,
g: 0xFF,
b: 0xFF,
a: 0xFF,
},
PixelRGBA {
r: 0x7f,
g: 0x7f,
b: 0x7f,
a: 0xFF,
},
PixelRGBA {
r: 0x10,
g: 0x20,
b: 0x30,
a: 0x40,
},
];
let decoder = Decoder::new(compressed.into_iter());
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
#[test]
fn decoder_unpack_index() {
let mut backbuffer = [PixelRGBA::zero(); 64];
backbuffer[0] = PixelRGBA::new(255, 255, 255, 255);
backbuffer[1] = PixelRGBA::new(0, 255, 0, 255);
backbuffer[2] = PixelRGBA::new(255, 0, 255, 255);
backbuffer[3] = PixelRGBA::new(0, 0, 0, 0);
backbuffer[10] = PixelRGBA::new(10, 10, 10, 0);
backbuffer[11] = PixelRGBA::new(0xF0, 0x2C, 0xAF, 0xFF);
let compressed = [
(QOI_OP_INDEX | 0),
(QOI_OP_INDEX | 11),
(QOI_OP_INDEX | 1),
(QOI_OP_INDEX | 2),
(QOI_OP_INDEX | 3),
(QOI_OP_INDEX | 10),
(QOI_OP_INDEX | 42),
];
let expected = [
PixelRGBA::new(255, 255, 255, 255),
PixelRGBA::new(0xF0, 0x2C, 0xAF, 0xFF),
PixelRGBA::new(0, 0xFF, 0, 0xFF),
PixelRGBA::new(255, 0, 255, 255),
PixelRGBA::new(0, 0, 0, 0),
PixelRGBA::new(10, 10, 10, 0),
PixelRGBA::zero(),
];
let decoder = Decoder::new_with_backbuffer(compressed.into_iter(), backbuffer);
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
#[test]
fn decoder_unpack_diff() {
// DIFF components are 2 bit values with a bias of 2.
// i.e. : 0b00 is -2, and 0b11 is +1
let compressed = [
(QOI_OP_DIFF | 0b0011_1111), // (1, 1, 1)
(QOI_OP_DIFF | 0b0011_1010), // (1, 0, 0)
(QOI_OP_DIFF | 0b0010_1110), // (0, 1, 0)
(QOI_OP_DIFF | 0b0010_1011), // (0, 0, 1)
(QOI_OP_DIFF | 0b0011_1011), // (1, 0, 1)
(QOI_OP_DIFF | 0b0001_1001), // (-1, 0, -1)
(QOI_OP_DIFF | 0b0000_0000), // (-2, -2, -2)
];
// the codec begins with a pixel at (0, 0, 0, 255), so these results
// are diffs from that.
let expected = [
PixelRGBA::new(1, 1, 1, 255),
PixelRGBA::new(2, 1, 1, 255),
PixelRGBA::new(2, 2, 1, 255),
PixelRGBA::new(2, 2, 2, 255),
PixelRGBA::new(3, 2, 3, 255),
PixelRGBA::new(2, 2, 2, 255),
PixelRGBA::new(0, 0, 0, 255),
];
let decoder = Decoder::new(compressed.into_iter());
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
#[test]
fn decoder_unpack_diff_rollover() {
let init_pixel = PixelRGBA::new(255, 255, 255, 255);
let compressed = [
(QOI_OP_DIFF | 0b0011_1111), // +1s
(QOI_OP_DIFF | 0b0011_1111), // +1s
(QOI_OP_DIFF | 0b0010_1010), // +0s, could have been an index or a run, probably
];
let expected = [
PixelRGBA::new(0, 0, 0, 255), // 255 rollover to 0
PixelRGBA::new(1, 1, 1, 255), // +1s
PixelRGBA::new(1, 1, 1, 255), // holds at 1s
];
let decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel);
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
#[test]
fn decoder_unpack_diff_rollunder() {
let init_pixel = PixelRGBA::new(0, 0, 0, 255);
let compressed = [
(QOI_OP_DIFF | 0b0001_0101), // -1s
(QOI_OP_DIFF | 0b0001_0101), // -1s
(QOI_OP_DIFF | 0b0010_1010), // 0s
];
let expected = [
PixelRGBA::new(255, 255, 255, 255),
PixelRGBA::new(254, 254, 254, 255),
PixelRGBA::new(254, 254, 254, 255),
];
let decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel);
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
#[test]
fn decoder_unpack_luma() {
// red and blue diffs are relative to the green channel as (dr - dg) and (db - dg)
// Their finished diffs need to invert this operation.
// Diff(dg, dr-dg, db-dg) and Pix (dr, dg, db)
let compressed = [
(QOI_OP_LUMA | 0b0011_1111),
(0b1111_1111), // Diff( 31, 7, 7) -> Pix (38, 31, 38)
(QOI_OP_LUMA | 0b0010_0000),
(0b1000_1000), // Diff( 0, 0, 0) -> Pix (0, 0, 0)
(QOI_OP_LUMA | 0b0010_0001),
(0b1111_1111), // Diff( 1, 7, 7) -> Pix (8, 1, 8)
(QOI_OP_LUMA | 0b0001_0011),
(0b1100_0011), // Diff(-13, 4, -5) -> Pix (-9, -13, -18)
];
let expected = [
PixelRGBA::new(38, 31, 38, 255),
PixelRGBA::new(38, 31, 38, 255),
PixelRGBA::new(46, 32, 46, 255),
PixelRGBA::new(37, 19, 28, 255),
];
let decoder = Decoder::new(compressed.into_iter());
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
#[test]
fn decoder_unpack_luma_rollover() {
let init_pixel = PixelRGBA::new(255, 255, 255, 255);
let compressed = [
(QOI_OP_LUMA | 0b0011_1111),
(0b1111_1111), // Diff (31, 7, 7) -> Pix (38, 31, 38)
];
let expected = PixelRGBA::new(37, 30, 37, 255);
let mut decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel);
let result = decoder
.next()
.expect("Oops, didn't get a Pixel back from the Decoder");
assert_eq!(result, expected);
}
#[test]
fn decoder_unpack_luma_rollunder() {
let init_pixel = PixelRGBA::new(0, 0, 0, 255);
let compressed = [
(QOI_OP_LUMA | 0b0001_0011),
(0b1100_0011), // Diff(-13, 4, -5) -> Pix (-9, -13, -18)
];
let expected = PixelRGBA::new(247, 243, 238, 255);
let mut decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel);
let result = decoder
.next()
.expect("Oops, didn't get a Pixel back from the Decoder");
assert_eq!(result, expected);
}
#[test]
fn decoder_unpack_run() {
let compressed = [
(QOI_OP_RUN | 0b0000_0000), // 1 -- bias of -1, so all zeros is a run of 1 pixel
(QOI_OP_RUN | 0b0000_1100), // 13 // 0b1111? no it isn't. what?
];
let init_pixel = PixelRGBA::new(50, 100, 150, 200);
let expected = [init_pixel; 14];
// the run instructions should really have been collapsed into just one, but
// lets pretend an encoder did this for some reason. The decoder can still
// unpack this correctly, it's just a sub-optimal compression is all.
let decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel);
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
#[test]
fn decoder_prev_pixel_verify() {
let compressed = [
QOI_OP_RGB, 0x10, 0x10, 0x10,
QOI_OP_RGBA, 0x20, 0x20, 0x20, 0x20,
(QOI_OP_INDEX | 1),
(QOI_OP_DIFF | 0b0011_1111),
(QOI_OP_LUMA | 0b0011_111), 0b0011_1111,
(QOI_OP_RUN | 2),
QOI_OP_RGBA, 0xFF, 0xFF, 0xFF, 0xFF,
];
let expected = [
PixelRGBA::new(0, 0, 0, 0xFF), // init
PixelRGBA::new(0x10, 0x10, 0x10, 0xFF), // RGB
PixelRGBA::new(0x20, 0x20, 0x20, 0xFF), // RGBA
PixelRGBA::new(0, 0, 0, 0), // INDEX -- this doubles as a small test for the backbuffer operation
PixelRGBA::new(0x1, 0x1, 0x1, 0x0), // DIFF
PixelRGBA::new(0x27, 0x20, 0x27, 0x0), // LUMA
PixelRGBA::new(0x27, 0x20, 0x27, 0x0), // RUN 1
PixelRGBA::new(0x27, 0x20, 0x27, 0x0), // RUN 2
PixelRGBA::new(0x27, 0x20, 0x27, 0x0), // RUN 3
// final OP_RGBA is just to flush out the OP_RUN prev_pixel value
];
let mut decoder = Decoder::new(compressed.into_iter());
let mut result = Vec::<PixelRGBA>::new();
loop {
if let Some(_) = decoder.next() {
result.push(*decoder.peek_prev_pixel());
} else {
break;
}
}
assert_eq!(result, expected);
}
#[test]
fn decoder_backbuffer_verify() {
let compressed = [
QOI_OP_RGB, 0x10, 0x10, 0x10,
QOI_OP_RGBA, 0x20, 0x20, 0x20, 0x20,
(QOI_OP_INDEX | 1),
(QOI_OP_DIFF | 0b0011_1111),
(QOI_OP_LUMA | 0b0011_111), 0b0011_1111,
(QOI_OP_RUN | 2),
QOI_OP_RGBA, 0xFF, 0xFF, 0xFF, 0xFF,
];
// these are the indices where we're expecting each pixel to land.
// Each pixel gets put into this backbuffer as it's en/de-coded.
// For RGB and RGBA, it'll simply assign a value into the index.
// For INDEX, the write can be skipped, and the expected index will be
// the same as the one in the op code. By definition, it has to be.
// OP_DIFF & OP_LUMA need to consider the value in the backbuffer, as
// they'll be using it to compute the new pixel.
//
let indices = [
31, // Pix (16, 16, 16, 255)
0, // Pix (32, 32, 32, 32)
1, // Pix (0, 0, 0, 0)
3, // current state: Pix (1, 1, 1, 0)
38, // current state: Pix(39, 32, 39, 0)
38, // run x1
38, // run x2
38, // run x3
];
let mut decoder = Decoder::new(compressed.into_iter());
let mut iters = 0;
loop {
if let Some(pixel) = decoder.next() {
// pixel has been decompressed, so it should be in the backbuffer by this point
// query it out:
let stored_px = decoder.peek_backbuffer(indices[iters]);
// and compare it to the value returned from iteration
assert_eq!(&pixel, stored_px);
} else {
break;
}
iters += 1;
}
}
}