aboutsummaryrefslogtreecommitdiff
path: root/vendor/qoi/tests/test_gen.rs
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/qoi/tests/test_gen.rs')
-rw-r--r--vendor/qoi/tests/test_gen.rs313
1 files changed, 313 insertions, 0 deletions
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;
+ }
+}