Unit tests for unpacking each of the op codes
These tests can be used to verify the fundamentals of the decoder: That individual instructions are parsed correctly and converted back into the correct pixel values.
This commit is contained in:
261
src/main.rs
Normal file
261
src/main.rs
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
struct Decoder<'input> {
|
||||||
|
// QOI codec state information
|
||||||
|
back_buffer: [PixelRGBA; 64],
|
||||||
|
prev_pixel: PixelRGBA,
|
||||||
|
|
||||||
|
bytes: &'input [u8], // input byte slice
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'input> Decoder<'input> {
|
||||||
|
fn new(qoi_bytes: &'input [u8]) -> Self {
|
||||||
|
Self {
|
||||||
|
back_buffer: [PixelRGBA::zero(); 64],
|
||||||
|
prev_pixel: PixelRGBA {
|
||||||
|
r: 0,
|
||||||
|
g: 0,
|
||||||
|
b: 0,
|
||||||
|
a: 255,
|
||||||
|
},
|
||||||
|
bytes: qoi_bytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(qoi_bytes: &'input [u8], back_buffer: [PixelRGBA; 64]) -> Self {
|
||||||
|
Self {
|
||||||
|
back_buffer,
|
||||||
|
prev_pixel: PixelRGBA {
|
||||||
|
r: 0, g: 0, b: 0, a: 255,
|
||||||
|
},
|
||||||
|
bytes: qoi_bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(qoi_bytes: &'input [u8], prev_pixel: PixelRGBA) -> Self {
|
||||||
|
Self {
|
||||||
|
back_buffer: [PixelRGBA::zero(); 64],
|
||||||
|
prev_pixel,
|
||||||
|
bytes: qoi_bytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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> Iterator for Decoder<'input> {
|
||||||
|
type Item = PixelRGBA;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
todo!("Implement the Iterator trait for Decoder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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!(Decoder::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);
|
||||||
|
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);
|
||||||
|
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, 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);
|
||||||
|
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 | 0b0011_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);
|
||||||
|
let result: Vec<PixelRGBA> = decoder.collect();
|
||||||
|
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_1111), // 13
|
||||||
|
];
|
||||||
|
|
||||||
|
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, init_pixel);
|
||||||
|
let result: Vec<PixelRGBA> = decoder.collect();
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user