Compare commits
14 Commits
ef977d7990
...
trunk
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b3c58fad5 | |||
| e2ba02f4d1 | |||
| 8ccf6bd475 | |||
| 04f83a55d2 | |||
| 0b2408bda1 | |||
| a6f18e5ea9 | |||
| 7ba2e7760b | |||
| e31aa5decb | |||
| 5ae4aa5823 | |||
| ef5d962621 | |||
| 9cffd665ca | |||
| 431c808b72 | |||
| f4eef2f863 | |||
| d5e7b0c818 |
@@ -5,4 +5,5 @@ edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
[dev-dependencies]
|
||||
png = "0.17.14"
|
||||
|
||||
14
README.md
Normal file
14
README.md
Normal 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.
|
||||
135
src/decoder.rs
135
src/decoder.rs
@@ -35,6 +35,7 @@ fn try_read_u32<I: Iterator<Item=u8>>(bytes: &mut I) -> Result<u32, DecodeError>
|
||||
}
|
||||
|
||||
mod codec_utils {
|
||||
use super::DecodeError;
|
||||
use super::PixelRGBA;
|
||||
pub(crate) fn hash(pixel: PixelRGBA) -> u8 {
|
||||
pixel
|
||||
@@ -45,34 +46,51 @@ mod codec_utils {
|
||||
.wrapping_add(pixel.a.wrapping_mul(11))
|
||||
% 64
|
||||
}
|
||||
|
||||
pub (crate) fn read_u8_rewrap<I: Iterator<Item = u8>>(bytes: &mut I) -> Result<u8, DecodeError> {
|
||||
if let Some(byte) = bytes.next() {
|
||||
return Ok(byte);
|
||||
} else {
|
||||
return Err(DecodeError::EarlyIteratorExhaustion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct Decoder<I: Iterator<Item = u8>> {
|
||||
pub struct Decoder<I: Iterator<Item = u8>> {
|
||||
// Image metadata
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub channels: u8,
|
||||
pub colorspace: u8,
|
||||
|
||||
// QOI codec state information
|
||||
back_buffer: [PixelRGBA; 64],
|
||||
prev_pixel: PixelRGBA,
|
||||
|
||||
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>
|
||||
where
|
||||
I: Iterator<Item = u8>,
|
||||
{
|
||||
fn new(bytes: I) -> Self {
|
||||
Self {
|
||||
pub fn try_new(mut bytes: I) -> Result<Self, DecodeError> {
|
||||
let _ = try_read_magic(&mut bytes)?;
|
||||
Ok(Self {
|
||||
width: try_read_u32(&mut bytes)?,
|
||||
height: try_read_u32(&mut bytes)?,
|
||||
channels: codec_utils::read_u8_rewrap(&mut bytes)?,
|
||||
colorspace: codec_utils::read_u8_rewrap(&mut bytes)?,
|
||||
back_buffer: [PixelRGBA::zero(); 64],
|
||||
prev_pixel: PixelRGBA {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 255,
|
||||
},
|
||||
prev_pixel: PixelRGBA { r:0, g:0, b:0, a: 255},
|
||||
bytes,
|
||||
run_len: 0,
|
||||
}
|
||||
output_count: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,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)
|
||||
@@ -176,8 +201,12 @@ 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: size,
|
||||
height: 1,
|
||||
channels: 4,
|
||||
colorspace: 0,
|
||||
back_buffer,
|
||||
prev_pixel: PixelRGBA {
|
||||
r: 0,
|
||||
@@ -187,17 +216,41 @@ 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: size,
|
||||
height: 1,
|
||||
channels: 4,
|
||||
colorspace: 0,
|
||||
back_buffer: [PixelRGBA::zero(); 64],
|
||||
prev_pixel,
|
||||
bytes,
|
||||
run_len: 0,
|
||||
output_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// The decoder includes image metadata, now, and construction attempts
|
||||
// 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, size: u32) -> Self {
|
||||
Self {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +305,7 @@ mod test {
|
||||
},
|
||||
];
|
||||
|
||||
let decoder = Decoder::new(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);
|
||||
}
|
||||
@@ -298,7 +351,7 @@ mod test {
|
||||
},
|
||||
];
|
||||
|
||||
let decoder = Decoder::new(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);
|
||||
}
|
||||
@@ -333,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);
|
||||
}
|
||||
@@ -364,7 +417,7 @@ mod test {
|
||||
PixelRGBA::new(0, 0, 0, 255),
|
||||
];
|
||||
|
||||
let decoder = Decoder::new(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);
|
||||
}
|
||||
@@ -383,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);
|
||||
}
|
||||
@@ -402,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);
|
||||
}
|
||||
@@ -430,7 +483,7 @@ mod test {
|
||||
PixelRGBA::new(37, 19, 28, 255),
|
||||
];
|
||||
|
||||
let decoder = Decoder::new(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);
|
||||
}
|
||||
@@ -444,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");
|
||||
@@ -460,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");
|
||||
@@ -481,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);
|
||||
}
|
||||
@@ -523,7 +576,7 @@ mod test {
|
||||
PixelRGBA::new(0xFF, 0xFF, 0xFF, 0xFF),
|
||||
];
|
||||
|
||||
let mut decoder = Decoder::new(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 {
|
||||
@@ -592,7 +645,7 @@ mod test {
|
||||
39, // run x3
|
||||
38, // final RGBA
|
||||
];
|
||||
let mut decoder = Decoder::new(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() {
|
||||
@@ -626,4 +679,38 @@ mod test {
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
/// Try decoding an image consisting of a single pixel.
|
||||
#[test]
|
||||
fn decode_1_pixel_image() {
|
||||
let input: [u8; 26] = [
|
||||
0x71, 0x6f, 0x69, 0x66, // 'qoif' magic bytes
|
||||
0x00, 0x00, 0x00, 0x01, // u32 width
|
||||
0x00, 0x00, 0x00, 0x01, // u32 height
|
||||
0x03, // u8 channels (3 for RGB mode)
|
||||
0x00, // u8 colorspace (0 for sRGB w/ linear alpha)
|
||||
// One single blue pixel, QOI_OP_RGB
|
||||
0xFE, 0x00, 0x00, 0xFF,
|
||||
// footer
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01
|
||||
];
|
||||
|
||||
let mut decoder = Decoder::try_new(input.into_iter())
|
||||
.expect("Failed to initialize decoder from byte array");
|
||||
|
||||
// Check the metadata
|
||||
assert_eq!(decoder.width, 1);
|
||||
assert_eq!(decoder.height, 1);
|
||||
assert_eq!(decoder.channels, 3);
|
||||
assert_eq!(decoder.colorspace, 0);
|
||||
|
||||
// Grab the one single pixel and check that it's solid blue.
|
||||
let pixel = decoder.next().expect("Couldn't get pixel from decoder");
|
||||
assert_eq!(pixel, PixelRGBA::new(0, 0, 255, 255));
|
||||
|
||||
// Assert that there are no more pixels.
|
||||
let pixel = decoder.next();
|
||||
assert!(pixel.is_none());
|
||||
}
|
||||
}
|
||||
25
src/main.rs
25
src/main.rs
@@ -1,25 +0,0 @@
|
||||
|
||||
use qoicodec::DecodeError;
|
||||
use qoicodec::try_read_magic;
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
fn main() {
|
||||
let file = File::open("qoi_test_images/dice.qoi").unwrap();
|
||||
let mut bytes = file.bytes()
|
||||
.map(|maybe_bytes|
|
||||
{
|
||||
match maybe_bytes {
|
||||
Ok(byte) => byte,
|
||||
Err(oops) => panic!("Oops, the file failed to load: {}", oops)
|
||||
}
|
||||
});
|
||||
if let Err(e) = try_read_magic(&mut bytes) {
|
||||
match e {
|
||||
DecodeError::Magic => panic!("QOI Magic bytes are bad!"),
|
||||
DecodeError::EarlyIteratorExhaustion => panic!("File iterator exhausted earlier than expected."),
|
||||
_ => panic!("Unhandled error reading magic: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
todo!("The rest of the main function");
|
||||
}
|
||||
108
tests/codec.rs
Normal file
108
tests/codec.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
use qoicodec::DecodeError;
|
||||
use qoicodec::Decoder;
|
||||
use qoicodec::PixelRGBA;
|
||||
|
||||
use std::io::BufWriter;
|
||||
use std::path::Path;
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TestError {
|
||||
SetupFailure, // an error during test initialization (missing file, no write permission for output)
|
||||
CodecFailure(DecodeError),
|
||||
}
|
||||
|
||||
impl From<DecodeError> for TestError {
|
||||
fn from(err: DecodeError) -> Self {
|
||||
Self::CodecFailure(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// There are test images provided by the QOI spec author: https://qoiformat.org/qoi_test_images.zip
|
||||
/// This function looks for them in `./qoi_test_images/` and turns them all
|
||||
/// into PNGs. Manual inspection is required to evaluate the test result.
|
||||
fn decode_qoi_samples() -> Result<(), TestError> {
|
||||
let file = File::open("qoi_test_images/dice.qoi").map_err(|_io_err| TestError::SetupFailure)?;
|
||||
|
||||
// TODO: feed the Result<> forward, don't let a panic path be given to an unaware caller.
|
||||
let mut bytes = file.bytes()
|
||||
.map(|maybe_bytes|
|
||||
{
|
||||
match maybe_bytes {
|
||||
Ok(byte) => byte,
|
||||
Err(oops) => panic!("Oops, the file failed to load: {}", oops)
|
||||
}
|
||||
});
|
||||
let decoder = Decoder::try_new(bytes)?;
|
||||
|
||||
let test_out_path = Path::new("test_output.png");
|
||||
let file = File::create(test_out_path).unwrap();
|
||||
let ref mut w = BufWriter::new(file);
|
||||
|
||||
let mut encoder = png::Encoder::new(w, decoder.width, decoder.height);
|
||||
encoder.set_color(match decoder.channels {
|
||||
3 => png::ColorType::Rgb,
|
||||
4 => png::ColorType::Rgba,
|
||||
_ => panic!("Bad colorspace in qoicodec::Decoder!")
|
||||
});
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
encoder.set_source_gamma(png::ScaledFloat::from_scaled(45455));
|
||||
encoder.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2));
|
||||
let source_chromaticities = png::SourceChromaticities::new(
|
||||
(0.31270, 0.32900),
|
||||
(0.64000, 0.33000),
|
||||
(0.30000, 0.60000),
|
||||
(0.15000, 0.06000),
|
||||
);
|
||||
encoder.set_source_chromaticities(source_chromaticities);
|
||||
let mut writer = encoder.write_header().unwrap();
|
||||
|
||||
let data: Vec<u8> = PixelIterExpander::try_new(decoder).unwrap().collect();
|
||||
let result = writer.write_image_data(&data);
|
||||
if let Err(res) = result {
|
||||
panic!("{}", res);
|
||||
}
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
struct PixelIterExpander <I: Iterator<Item = qoicodec::PixelRGBA>> {
|
||||
wrapped_iterator: I,
|
||||
current_pixel: qoicodec::PixelRGBA,
|
||||
color_element_idx: u8,
|
||||
}
|
||||
|
||||
impl <I: std::iter::Iterator<Item = PixelRGBA>> PixelIterExpander <I> {
|
||||
fn try_new(mut pixels: I) -> Option<Self> {
|
||||
let px = pixels.next()?;
|
||||
|
||||
Some(Self {
|
||||
wrapped_iterator: pixels,
|
||||
current_pixel: px,
|
||||
color_element_idx: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl <I: Iterator<Item=PixelRGBA>> Iterator for PixelIterExpander<I> {
|
||||
type Item = u8;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// at index 4, reset counter and get next pixel
|
||||
if self.color_element_idx == 4 {
|
||||
self.color_element_idx = 0;
|
||||
self.current_pixel = self.wrapped_iterator.next()?;
|
||||
}
|
||||
|
||||
// increment pixel value, match over index and return that byte
|
||||
self.color_element_idx += 1;
|
||||
match self.color_element_idx {
|
||||
1 => Some(self.current_pixel.r),
|
||||
2 => Some(self.current_pixel.g),
|
||||
3 => Some(self.current_pixel.b),
|
||||
4 => Some(self.current_pixel.a),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user