From b77f32b2df30376875dc50d1a675e5f8903488b9 Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Fri, 3 May 2024 09:53:26 -0500 Subject: [PATCH] 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. --- src/main.rs | 261 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 src/main.rs diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b93ff73 --- /dev/null +++ b/src/main.rs @@ -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 { + 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::>(); + 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 = 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 = 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 = 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 = 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 = decoder.collect(); + assert_eq!(result, expected); + } +}