diff options
Diffstat (limited to 'vendor/qoi/tests')
-rw-r--r-- | vendor/qoi/tests/common.rs | 12 | ||||
-rw-r--r-- | vendor/qoi/tests/test_chunks.rs | 211 | ||||
-rw-r--r-- | vendor/qoi/tests/test_gen.rs | 313 | ||||
-rw-r--r-- | vendor/qoi/tests/test_misc.rs | 6 | ||||
-rw-r--r-- | vendor/qoi/tests/test_ref.rs | 114 |
5 files changed, 656 insertions, 0 deletions
diff --git a/vendor/qoi/tests/common.rs b/vendor/qoi/tests/common.rs new file mode 100644 index 0000000..cc1eff1 --- /dev/null +++ b/vendor/qoi/tests/common.rs @@ -0,0 +1,12 @@ +#[allow(unused)] +pub fn hash<const N: usize>(px: [u8; N]) -> u8 { + let r = px[0]; + let g = px[1]; + let b = px[2]; + let a = if N >= 4 { px[3] } else { 0xff }; + let rm = r.wrapping_mul(3); + let gm = g.wrapping_mul(5); + let bm = b.wrapping_mul(7); + let am = a.wrapping_mul(11); + rm.wrapping_add(gm).wrapping_add(bm).wrapping_add(am) % 64 +} diff --git a/vendor/qoi/tests/test_chunks.rs b/vendor/qoi/tests/test_chunks.rs new file mode 100644 index 0000000..d465140 --- /dev/null +++ b/vendor/qoi/tests/test_chunks.rs @@ -0,0 +1,211 @@ +mod common; + +use bytemuck::{cast_slice, Pod}; + +use qoi::consts::{ + QOI_HEADER_SIZE, QOI_OP_DIFF, QOI_OP_INDEX, QOI_OP_LUMA, QOI_OP_RGB, QOI_OP_RGBA, QOI_OP_RUN, + QOI_PADDING_SIZE, +}; +use qoi::{decode_to_vec, encode_to_vec}; + +use self::common::hash; + +fn test_chunk<P, E, const N: usize>(pixels: P, expected: E) +where + P: AsRef<[[u8; N]]>, + E: AsRef<[u8]>, + [u8; N]: Pod, +{ + let pixels = pixels.as_ref(); + let expected = expected.as_ref(); + let pixels_raw = cast_slice::<_, u8>(pixels); + let encoded = encode_to_vec(pixels_raw, pixels.len() as _, 1).unwrap(); + let decoded = decode_to_vec(&encoded).unwrap().1; + assert_eq!(pixels_raw, decoded.as_slice(), "roundtrip failed (encoded={:?}))", encoded); + assert!(encoded.len() >= expected.len() + QOI_HEADER_SIZE + QOI_PADDING_SIZE); + assert_eq!(&encoded[QOI_HEADER_SIZE..][..expected.len()], expected); +} + +#[test] +fn test_encode_rgb_3ch() { + test_chunk([[11, 121, 231]], [QOI_OP_RGB, 11, 121, 231]); +} + +#[test] +fn test_encode_rgb_4ch() { + test_chunk([[11, 121, 231, 0xff]], [QOI_OP_RGB, 11, 121, 231]); +} + +#[test] +fn test_encode_rgba() { + test_chunk([[11, 121, 231, 55]], [QOI_OP_RGBA, 11, 121, 231, 55]); +} + +#[test] +fn test_encode_run_start_len1to62_3ch() { + for n in 1..=62 { + let mut v = vec![[0, 0, 0]; n]; + v.push([11, 22, 33]); + test_chunk(v, [QOI_OP_RUN | (n as u8 - 1), QOI_OP_RGB]); + } +} + +#[test] +fn test_encode_run_start_len1to62_4ch() { + for n in 1..=62 { + let mut v = vec![[0, 0, 0, 0xff]; n]; + v.push([11, 22, 33, 44]); + test_chunk(v, [QOI_OP_RUN | (n as u8 - 1), QOI_OP_RGBA]); + } +} + +#[test] +fn test_encode_run_start_63to124_3ch() { + for n in 63..=124 { + let mut v = vec![[0, 0, 0]; n]; + v.push([11, 22, 33]); + test_chunk(v, [QOI_OP_RUN | 61, QOI_OP_RUN | (n as u8 - 63), QOI_OP_RGB]); + } +} + +#[test] +fn test_encode_run_start_len63to124_4ch() { + for n in 63..=124 { + let mut v = vec![[0, 0, 0, 0xff]; n]; + v.push([11, 22, 33, 44]); + test_chunk(v, [QOI_OP_RUN | 61, QOI_OP_RUN | (n as u8 - 63), QOI_OP_RGBA]); + } +} + +#[test] +fn test_encode_run_end_3ch() { + let px = [11, 33, 55]; + test_chunk( + [[1, 99, 2], px, px, px], + [QOI_OP_RGB, 1, 99, 2, QOI_OP_RGB, px[0], px[1], px[2], QOI_OP_RUN | 1], + ); +} + +#[test] +fn test_encode_run_end_4ch() { + let px = [11, 33, 55, 77]; + test_chunk( + [[1, 99, 2, 3], px, px, px], + [QOI_OP_RGBA, 1, 99, 2, 3, QOI_OP_RGBA, px[0], px[1], px[2], px[3], QOI_OP_RUN | 1], + ); +} + +#[test] +fn test_encode_run_mid_3ch() { + let px = [11, 33, 55]; + test_chunk( + [[1, 99, 2], px, px, px, [1, 2, 3]], + [QOI_OP_RGB, 1, 99, 2, QOI_OP_RGB, px[0], px[1], px[2], QOI_OP_RUN | 1], + ); +} + +#[test] +fn test_encode_run_mid_4ch() { + let px = [11, 33, 55, 77]; + test_chunk( + [[1, 99, 2, 3], px, px, px, [1, 2, 3, 4]], + [QOI_OP_RGBA, 1, 99, 2, 3, QOI_OP_RGBA, px[0], px[1], px[2], px[3], QOI_OP_RUN | 1], + ); +} + +#[test] +fn test_encode_index_3ch() { + let px = [101, 102, 103]; + test_chunk( + [px, [1, 2, 3], px], + [QOI_OP_RGB, 101, 102, 103, QOI_OP_RGB, 1, 2, 3, QOI_OP_INDEX | hash(px)], + ); +} + +#[test] +fn test_encode_index_4ch() { + let px = [101, 102, 103, 104]; + test_chunk( + [px, [1, 2, 3, 4], px], + [QOI_OP_RGBA, 101, 102, 103, 104, QOI_OP_RGBA, 1, 2, 3, 4, QOI_OP_INDEX | hash(px)], + ); +} + +#[test] +fn test_encode_index_zero_3ch() { + let px = [0, 0, 0]; + test_chunk([[101, 102, 103], px], [QOI_OP_RGB, 101, 102, 103, QOI_OP_RGB, 0, 0, 0]); +} + +#[test] +fn test_encode_index_zero_0x00_4ch() { + let px = [0, 0, 0, 0]; + test_chunk( + [[101, 102, 103, 104], px], + [QOI_OP_RGBA, 101, 102, 103, 104, QOI_OP_INDEX | hash(px)], + ); +} + +#[test] +fn test_encode_index_zero_0xff_4ch() { + let px = [0, 0, 0, 0xff]; + test_chunk( + [[101, 102, 103, 104], px], + [QOI_OP_RGBA, 101, 102, 103, 104, QOI_OP_RGBA, 0, 0, 0, 0xff], + ); +} + +#[test] +fn test_encode_diff() { + for x in 0..8_u8 { + let x = [x.wrapping_sub(5), x.wrapping_sub(4), x.wrapping_sub(3)]; + for dr in 0..3 { + for dg in 0..3 { + for db in 0..3 { + if dr != 2 || dg != 2 || db != 2 { + let r = x[0].wrapping_add(dr).wrapping_sub(2); + let g = x[1].wrapping_add(dg).wrapping_sub(2); + let b = x[2].wrapping_add(db).wrapping_sub(2); + let d = QOI_OP_DIFF | dr << 4 | dg << 2 | db; + test_chunk( + [[1, 99, 2], x, [r, g, b]], + [QOI_OP_RGB, 1, 99, 2, QOI_OP_RGB, x[0], x[1], x[2], d], + ); + test_chunk( + [[1, 99, 2, 0xff], [x[0], x[1], x[2], 9], [r, g, b, 9]], + [QOI_OP_RGB, 1, 99, 2, QOI_OP_RGBA, x[0], x[1], x[2], 9, d], + ); + } + } + } + } + } +} + +#[test] +fn test_encode_luma() { + for x in (0..200_u8).step_by(4) { + let x = [x.wrapping_mul(3), x.wrapping_sub(5), x.wrapping_sub(7)]; + for dr_g in (0..16).step_by(4) { + for dg in (0..64).step_by(8) { + for db_g in (0..16).step_by(4) { + if dr_g != 8 || dg != 32 || db_g != 8 { + let r = x[0].wrapping_add(dr_g).wrapping_add(dg).wrapping_sub(40); + let g = x[1].wrapping_add(dg).wrapping_sub(32); + let b = x[2].wrapping_add(db_g).wrapping_add(dg).wrapping_sub(40); + let d1 = QOI_OP_LUMA | dg; + let d2 = (dr_g << 4) | db_g; + test_chunk( + [[1, 99, 2], x, [r, g, b]], + [QOI_OP_RGB, 1, 99, 2, QOI_OP_RGB, x[0], x[1], x[2], d1, d2], + ); + test_chunk( + [[1, 99, 2, 0xff], [x[0], x[1], x[2], 9], [r, g, b, 9]], + [QOI_OP_RGB, 1, 99, 2, QOI_OP_RGBA, x[0], x[1], x[2], 9, d1, d2], + ); + } + } + } + } + } +} diff --git a/vendor/qoi/tests/test_gen.rs b/vendor/qoi/tests/test_gen.rs new file mode 100644 index 0000000..08767d4 --- /dev/null +++ b/vendor/qoi/tests/test_gen.rs @@ -0,0 +1,313 @@ +mod common; + +use bytemuck::cast_slice; +use std::borrow::Cow; +use std::fmt::Debug; + +use cfg_if::cfg_if; +use rand::{ + distributions::{Distribution, Standard}, + rngs::StdRng, + Rng, SeedableRng, +}; + +use libqoi::{qoi_decode, qoi_encode}; +use qoi::consts::{ + QOI_HEADER_SIZE, QOI_MASK_2, QOI_OP_DIFF, QOI_OP_INDEX, QOI_OP_LUMA, QOI_OP_RGB, QOI_OP_RGBA, + QOI_OP_RUN, QOI_PADDING_SIZE, +}; +use qoi::{decode_header, decode_to_vec, encode_to_vec}; + +use self::common::hash; + +struct GenState<const N: usize> { + index: [[u8; N]; 64], + pixels: Vec<u8>, + prev: [u8; N], + len: usize, +} + +impl<const N: usize> GenState<N> { + pub fn with_capacity(capacity: usize) -> Self { + Self { + index: [[0; N]; 64], + pixels: Vec::with_capacity(capacity * N), + prev: Self::zero(), + len: 0, + } + } + pub fn write(&mut self, px: [u8; N]) { + self.index[hash(px) as usize] = px; + for i in 0..N { + self.pixels.push(px[i]); + } + self.prev = px; + self.len += 1; + } + + pub fn pick_from_index(&self, rng: &mut impl Rng) -> [u8; N] { + self.index[rng.gen_range(0_usize..64)] + } + + pub fn zero() -> [u8; N] { + let mut px = [0; N]; + if N >= 4 { + px[3] = 0xff; + } + px + } +} + +struct ImageGen { + p_new: f64, + p_index: f64, + p_repeat: f64, + p_diff: f64, + p_luma: f64, +} + +impl ImageGen { + pub fn new_random(rng: &mut impl Rng) -> Self { + let p: [f64; 6] = rng.gen(); + let t = p.iter().sum::<f64>(); + Self { + p_new: p[0] / t, + p_index: p[1] / t, + p_repeat: p[2] / t, + p_diff: p[3] / t, + p_luma: p[4] / t, + } + } + + pub fn generate(&self, rng: &mut impl Rng, channels: usize, min_len: usize) -> Vec<u8> { + match channels { + 3 => self.generate_const::<_, 3>(rng, min_len), + 4 => self.generate_const::<_, 4>(rng, min_len), + _ => panic!(), + } + } + + fn generate_const<R: Rng, const N: usize>(&self, rng: &mut R, min_len: usize) -> Vec<u8> + where + Standard: Distribution<[u8; N]>, + { + let mut s = GenState::<N>::with_capacity(min_len); + let zero = GenState::<N>::zero(); + + while s.len < min_len { + let mut p = rng.gen_range(0.0..1.0); + + if p < self.p_new { + s.write(rng.gen()); + continue; + } + p -= self.p_new; + + if p < self.p_index { + let px = s.pick_from_index(rng); + s.write(px); + continue; + } + p -= self.p_index; + + if p < self.p_repeat { + let px = s.prev; + let n_repeat = rng.gen_range(1_usize..=70); + for _ in 0..n_repeat { + s.write(px); + } + continue; + } + p -= self.p_repeat; + + if p < self.p_diff { + let mut px = s.prev; + px[0] = px[0].wrapping_add(rng.gen_range(0_u8..4).wrapping_sub(2)); + px[1] = px[1].wrapping_add(rng.gen_range(0_u8..4).wrapping_sub(2)); + px[2] = px[2].wrapping_add(rng.gen_range(0_u8..4).wrapping_sub(2)); + s.write(px); + continue; + } + p -= self.p_diff; + + if p < self.p_luma { + let mut px = s.prev; + let vg = rng.gen_range(0_u8..64).wrapping_sub(32); + let vr = rng.gen_range(0_u8..16).wrapping_sub(8).wrapping_add(vg); + let vb = rng.gen_range(0_u8..16).wrapping_sub(8).wrapping_add(vg); + px[0] = px[0].wrapping_add(vr); + px[1] = px[1].wrapping_add(vg); + px[2] = px[2].wrapping_add(vb); + s.write(px); + continue; + } + + s.write(zero); + } + + s.pixels + } +} + +fn format_encoded(encoded: &[u8]) -> String { + let header = decode_header(encoded).unwrap(); + let mut data = &encoded[QOI_HEADER_SIZE..encoded.len() - QOI_PADDING_SIZE]; + let mut s = format!("{}x{}:{} = [", header.width, header.height, header.channels.as_u8()); + while !data.is_empty() { + let b1 = data[0]; + data = &data[1..]; + match b1 { + QOI_OP_RGB => { + s.push_str(&format!("rgb({},{},{})", data[0], data[1], data[2])); + data = &data[3..]; + } + QOI_OP_RGBA => { + s.push_str(&format!("rgba({},{},{},{})", data[0], data[1], data[2], data[3])); + data = &data[4..]; + } + _ => match b1 & QOI_MASK_2 { + QOI_OP_INDEX => s.push_str(&format!("index({})", b1 & 0x3f)), + QOI_OP_RUN => s.push_str(&format!("run({})", b1 & 0x3f)), + QOI_OP_DIFF => s.push_str(&format!( + "diff({},{},{})", + (b1 >> 4) & 0x03, + (b1 >> 2) & 0x03, + b1 & 0x03 + )), + QOI_OP_LUMA => { + let b2 = data[0]; + data = &data[1..]; + s.push_str(&format!("luma({},{},{})", (b2 >> 4) & 0x0f, b1 & 0x3f, b2 & 0x0f)) + } + _ => {} + }, + } + s.push_str(", "); + } + s.pop().unwrap(); + s.pop().unwrap(); + s.push(']'); + s +} + +fn check_roundtrip<E, D, VE, VD, EE, ED>( + msg: &str, mut data: &[u8], channels: usize, encode: E, decode: D, +) where + E: Fn(&[u8], u32) -> Result<VE, EE>, + D: Fn(&[u8]) -> Result<VD, ED>, + VE: AsRef<[u8]>, + VD: AsRef<[u8]>, + EE: Debug, + ED: Debug, +{ + macro_rules! rt { + ($data:expr, $n:expr) => { + decode(encode($data, $n as _).unwrap().as_ref()).unwrap() + }; + } + macro_rules! fail { + ($msg:expr, $data:expr, $decoded:expr, $encoded:expr, $channels:expr) => { + assert!( + false, + "{} roundtrip failed\n\n image: {:?}\ndecoded: {:?}\nencoded: {}", + $msg, + cast_slice::<_, [u8; $channels]>($data.as_ref()), + cast_slice::<_, [u8; $channels]>($decoded.as_ref()), + format_encoded($encoded.as_ref()), + ); + }; + } + + let mut n_pixels = data.len() / channels; + assert_eq!(n_pixels * channels, data.len()); + + // if all ok, return + // ... but if roundtrip check fails, try to reduce the example to the smallest we can find + if rt!(data, n_pixels).as_ref() == data { + return; + } + + // try removing pixels from the beginning + while n_pixels > 1 { + let slice = &data[..data.len() - channels]; + if rt!(slice, n_pixels - 1).as_ref() != slice { + data = slice; + n_pixels -= 1; + } else { + break; + } + } + + // try removing pixels from the end + while n_pixels > 1 { + let slice = &data[channels..]; + if rt!(slice, n_pixels - 1).as_ref() != slice { + data = slice; + n_pixels -= 1; + } else { + break; + } + } + + // try removing pixels from the middle + let mut data = Cow::from(data); + let mut pos = 1; + while n_pixels > 1 && pos < n_pixels - 1 { + let mut vec = data.to_vec(); + for _ in 0..channels { + vec.remove(pos * channels); + } + if rt!(vec.as_slice(), n_pixels - 1).as_ref() != vec.as_slice() { + data = Cow::from(vec); + n_pixels -= 1; + } else { + pos += 1; + } + } + + let encoded = encode(data.as_ref(), n_pixels as _).unwrap(); + let decoded = decode(encoded.as_ref()).unwrap(); + assert_ne!(decoded.as_ref(), data.as_ref()); + if channels == 3 { + fail!(msg, data, decoded, encoded, 3); + } else { + fail!(msg, data, decoded, encoded, 4); + } +} + +#[test] +fn test_generated() { + let mut rng = StdRng::seed_from_u64(0); + + let mut n_pixels = 0; + while n_pixels < 20_000_000 { + let min_len = rng.gen_range(1..=5000); + let channels = rng.gen_range(3..=4); + let gen = ImageGen::new_random(&mut rng); + let img = gen.generate(&mut rng, channels, min_len); + + let encode = |data: &[u8], size| encode_to_vec(data, size, 1); + let decode = |data: &[u8]| decode_to_vec(data).map(|r| r.1); + let encode_c = |data: &[u8], size| qoi_encode(data, size, 1, channels as _); + let decode_c = |data: &[u8]| qoi_decode(data, channels as _).map(|r| r.1); + + check_roundtrip("qoi-rust -> qoi-rust", &img, channels as _, encode, decode); + check_roundtrip("qoi-rust -> qoi.h", &img, channels as _, encode, decode_c); + check_roundtrip("qoi.h -> qoi-rust", &img, channels as _, encode_c, decode); + + let size = (img.len() / channels) as u32; + let encoded = encode(&img, size).unwrap(); + let encoded_c = encode_c(&img, size).unwrap(); + cfg_if! { + if #[cfg(feature = "reference")] { + let eq = encoded.as_slice() == encoded_c.as_ref(); + assert!(eq, "qoi-rust [reference mode] doesn't match qoi.h"); + } else { + let eq = encoded.len() == encoded_c.len(); + assert!(eq, "qoi-rust [non-reference mode] length doesn't match qoi.h"); + } + } + + n_pixels += size; + } +} diff --git a/vendor/qoi/tests/test_misc.rs b/vendor/qoi/tests/test_misc.rs new file mode 100644 index 0000000..720adf4 --- /dev/null +++ b/vendor/qoi/tests/test_misc.rs @@ -0,0 +1,6 @@ +#[test] +fn test_new_encoder() { + // this used to fail due to `Bytes` not being `pub` + let arr = [0u8]; + let _ = qoi::Decoder::new(&arr[..]); +}
\ No newline at end of file diff --git a/vendor/qoi/tests/test_ref.rs b/vendor/qoi/tests/test_ref.rs new file mode 100644 index 0000000..59a7315 --- /dev/null +++ b/vendor/qoi/tests/test_ref.rs @@ -0,0 +1,114 @@ +use std::fs::{self, File}; +use std::path::{Path, PathBuf}; + +use anyhow::{bail, Result}; +use cfg_if::cfg_if; +use walkdir::{DirEntry, WalkDir}; + +use qoi::{decode_to_vec, encode_to_vec}; + +fn find_qoi_png_pairs(root: impl AsRef<Path>) -> Vec<(PathBuf, PathBuf)> { + let root = root.as_ref(); + + let get_ext = + |path: &Path| path.extension().unwrap_or_default().to_string_lossy().to_ascii_lowercase(); + let check_qoi_png_pair = |path: &Path| { + let (qoi, png) = (path.to_path_buf(), path.with_extension("png")); + if qoi.is_file() && get_ext(&qoi) == "qoi" && png.is_file() { + Some((qoi, png)) + } else { + None + } + }; + + let mut out = vec![]; + if let Some(pair) = check_qoi_png_pair(root) { + out.push(pair); + } else if root.is_dir() { + out.extend( + WalkDir::new(root) + .follow_links(true) + .into_iter() + .filter_map(Result::ok) + .map(DirEntry::into_path) + .filter_map(|p| check_qoi_png_pair(&p)), + ) + } + out +} + +struct Image { + pub width: u32, + pub height: u32, + pub channels: u8, + pub data: Vec<u8>, +} + +impl Image { + fn from_png(filename: &Path) -> Result<Self> { + let decoder = png::Decoder::new(File::open(filename)?); + let mut reader = decoder.read_info()?; + let mut buf = vec![0; reader.output_buffer_size()]; + let info = reader.next_frame(&mut buf)?; + let bytes = &buf[..info.buffer_size()]; + Ok(Self { + width: info.width, + height: info.height, + channels: info.color_type.samples() as u8, + data: bytes.to_vec(), + }) + } +} + +fn compare_slices(name: &str, desc: &str, result: &[u8], expected: &[u8]) -> Result<()> { + if result == expected { + Ok(()) + } else { + if let Some(i) = + (0..result.len().min(expected.len())).position(|i| result[i] != expected[i]) + { + bail!( + "{}: {} mismatch at byte {}: expected {:?}, got {:?}", + name, + desc, + i, + &expected[i..(i + 4).min(expected.len())], + &result[i..(i + 4).min(result.len())], + ); + } else { + bail!( + "{}: {} length mismatch: expected {}, got {}", + name, + desc, + expected.len(), + result.len() + ); + } + } +} + +#[test] +fn test_reference_images() -> Result<()> { + let pairs = find_qoi_png_pairs("assets"); + assert!(!pairs.is_empty()); + + for (qoi_path, png_path) in &pairs { + let png_name = png_path.file_name().unwrap_or_default().to_string_lossy(); + let img = Image::from_png(png_path)?; + println!("{} {} {} {}", png_name, img.width, img.height, img.channels); + let encoded = encode_to_vec(&img.data, img.width, img.height)?; + let expected = fs::read(qoi_path)?; + assert_eq!(encoded.len(), expected.len()); // this should match regardless + cfg_if! { + if #[cfg(feature = "reference")] { + compare_slices(&png_name, "encoding", &encoded, &expected)?; + } + } + let (_header1, decoded1) = decode_to_vec(&encoded)?; + let (_header2, decoded2) = decode_to_vec(&expected)?; + compare_slices(&png_name, "decoding [1]", &decoded1, &img.data)?; + compare_slices(&png_name, "decoding [2]", &decoded2, &img.data)?; + } + + Ok(()) +} |