Compare commits

..

3 Commits

Author SHA1 Message Date
5b3c58fad5 Create a readme, incl. instructions for test exec
It's about time I have a README for this thing. I need somewhere to put
the test setup instructions, making now as good a time as any to get
around to it.
2025-10-14 13:23:43 -05:00
e2ba02f4d1 Make test-case magic ctor's image size-aware.
Those construction functions were setting an image size of 0x0, which is
no longer acceptable because of the iteration limiter. There is now an
extra argument for the expected pixel count.
2025-10-13 11:02:45 -05:00
8ccf6bd475 Track decode progress and stop at W*H pixels
I was thinking about doing some partial parsing where I retrieve a few
extra bytes to see if it's a footer, but that's really hard with an
iterator as an input stream.

Instead, I'm just going to count how many pixels the decoder has output
and stop iterating at that point.

Now to fix the test cases that don't have an image size assigned.
2025-10-13 10:56:23 -05:00
2 changed files with 48 additions and 21 deletions

14
README.md Normal file
View File

@@ -0,0 +1,14 @@
# QOI Codec Crate
The [Quite Okay Image format](https://qoiformat.org/) is an image compression scheme whose specification fits on one single page.
This crate is my (work in progress) implementation of that specification. At this time (v0.1) it can only decode some of the test images. Some don't work correctly and some don't work at all.
## Running the tests
1. Download the sample data set: https://qoiformat.org/qoi_test_images.zip
2. Unpack them into the project folder at `./qoi_test_images/`
The "integration test" in `tests/codec.rs` reads a QOI file and decodes it into a PNG. Since these images are not part of the repo, test will fail without this manual step.
Other tests, like those inside the regular source tree will run just fine. E.g.: The module `crate::decoder::test` will run properly with no additional setup actions. All data is contained as constants inside the source code.

View File

@@ -70,6 +70,8 @@ pub struct Decoder<I: Iterator<Item = u8>> {
bytes: I,
run_len: u8,
// Counter to ensure we stop after getting to Self::width * Self::height pixels
output_count: usize,
}
impl<I> Decoder<I>
@@ -87,6 +89,7 @@ where
prev_pixel: PixelRGBA { r:0, g:0, b:0, a: 255},
bytes,
run_len: 0,
output_count: 0,
})
}
}
@@ -98,6 +101,13 @@ where
type Item = PixelRGBA;
fn next(&mut self) -> Option<Self::Item> {
// count pixels. When we reach WIDTH x HEIGHT, check for footer and quit.
if self.output_count == (self.width * self.height) as usize {
return None;
} else {
self.output_count += 1;
}
if self.run_len > 0 {
self.run_len -= 1;
Some(self.prev_pixel)
@@ -191,10 +201,10 @@ mod test {
// 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 {
fn new_with_backbuffer(bytes: I, back_buffer: [PixelRGBA; 64], size: u32) -> Self {
Self {
width: 0,
height: 0,
width: size,
height: 1,
channels: 4,
colorspace: 0,
back_buffer,
@@ -206,21 +216,23 @@ mod test {
},
bytes,
run_len: 0,
output_count: 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 {
fn new_with_previous_pixel(bytes: I, prev_pixel: PixelRGBA, size: u32) -> Self {
Self {
width: 0,
height: 0,
width: size,
height: 1,
channels: 4,
colorspace: 0,
back_buffer: [PixelRGBA::zero(); 64],
prev_pixel,
bytes,
run_len: 0,
output_count: 0,
}
}
@@ -228,16 +240,17 @@ mod test {
// to extract it from the input iterator. For the decoder tests, this
// needs to be skipped. Thus, we get *another* magic constructor
// available only to the test module.
fn new_with_no_metadata(bytes: I) -> Self {
fn new_with_no_metadata(bytes: I, size: u32) -> Self {
Self {
width: 0,
height: 0,
width: size,
height: 1,
channels: 4,
colorspace: 0,
back_buffer: [PixelRGBA::zero(); 64],
prev_pixel: PixelRGBA { r: 0, g: 0, b: 0, a: 255 },
bytes,
run_len: 0,
output_count: 0,
}
}
@@ -292,7 +305,7 @@ mod test {
},
];
let decoder = Decoder::new_with_no_metadata(compressed.into_iter());
let decoder = Decoder::new_with_no_metadata(compressed.into_iter(), expected.len() as u32);
let result = decoder.collect::<Vec<PixelRGBA>>();
assert_eq!(result, expected);
}
@@ -338,7 +351,7 @@ mod test {
},
];
let decoder = Decoder::new_with_no_metadata(compressed.into_iter());
let decoder = Decoder::new_with_no_metadata(compressed.into_iter(), expected.len() as u32);
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
@@ -373,7 +386,7 @@ mod test {
PixelRGBA::zero(),
];
let decoder = Decoder::new_with_backbuffer(compressed.into_iter(), backbuffer);
let decoder = Decoder::new_with_backbuffer(compressed.into_iter(), backbuffer, expected.len() as u32);
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
@@ -404,7 +417,7 @@ mod test {
PixelRGBA::new(0, 0, 0, 255),
];
let decoder = Decoder::new_with_no_metadata(compressed.into_iter());
let decoder = Decoder::new_with_no_metadata(compressed.into_iter(), expected.len() as u32);
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
@@ -423,7 +436,7 @@ mod test {
PixelRGBA::new(1, 1, 1, 255), // holds at 1s
];
let decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel);
let decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel, expected.len() as u32);
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
@@ -442,7 +455,7 @@ mod test {
PixelRGBA::new(254, 254, 254, 255),
];
let decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel);
let decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel, expected.len() as u32);
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
@@ -470,7 +483,7 @@ mod test {
PixelRGBA::new(37, 19, 28, 255),
];
let decoder = Decoder::new_with_no_metadata(compressed.into_iter());
let decoder = Decoder::new_with_no_metadata(compressed.into_iter(), expected.len() as u32);
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
@@ -484,7 +497,7 @@ mod test {
];
let expected = PixelRGBA::new(37, 30, 37, 255);
let mut decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel);
let mut decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel, 1);
let result = decoder
.next()
.expect("Oops, didn't get a Pixel back from the Decoder");
@@ -500,7 +513,7 @@ mod test {
];
let expected = PixelRGBA::new(247, 243, 238, 255);
let mut decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel);
let mut decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel, 1);
let result = decoder
.next()
.expect("Oops, didn't get a Pixel back from the Decoder");
@@ -521,7 +534,7 @@ mod test {
// 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 decoder = Decoder::new_with_previous_pixel(compressed.into_iter(), init_pixel, expected.len() as u32);
let result: Vec<PixelRGBA> = decoder.collect();
assert_eq!(result, expected);
}
@@ -563,7 +576,7 @@ mod test {
PixelRGBA::new(0xFF, 0xFF, 0xFF, 0xFF),
];
let mut decoder = Decoder::new_with_no_metadata(compressed.into_iter());
let mut decoder = Decoder::new_with_no_metadata(compressed.into_iter(), expected.len() as u32);
let mut result = Vec::<PixelRGBA>::new();
loop {
@@ -632,7 +645,7 @@ mod test {
39, // run x3
38, // final RGBA
];
let mut decoder = Decoder::new_with_no_metadata(compressed.into_iter());
let mut decoder = Decoder::new_with_no_metadata(compressed.into_iter(), indices.len() as u32);
let mut iters = 0;
loop {
if let Some(pixel) = decoder.next() {