//! Decodes of the libjpeg-turbo test images (tests/data, see SOURCE.md there). use craft_jpeg::{Decoded, Limits, Samples, decode}; fn fixture(name: &str) -> Vec { let path = format!("{}/tests/data/{name}", env!("CARGO_MANIFEST_DIR")); std::fs::read(&path).unwrap_or_else(|e| panic!("{path}: {e}")) } fn decode_clean(name: &str) -> Decoded { let img = decode(&fixture(name), &Limits::default(), None).unwrap_or_else(|e| panic!("{name}: {e}")); assert!(!img.damaged, "{name}"); img } fn decode_fixture(name: &str) -> Decoded { let img = decode_clean(name); assert_eq!((img.width, img.height, img.components), (227, 149, 3), "{name}"); img } /// Mean and maximum absolute difference. fn diff(a: impl Iterator, b: impl Iterator) -> (f64, f64) { let (mut sum, mut max, mut n) = (0.0, 0.0f64, 0usize); for (a, b) in a.zip(b) { let d = (a - b).abs(); sum += d; max = max.max(d); n += 1; } (sum / n.max(1) as f64, max) } /// testimgari-huffman.jpg is testimgari.jpg transcoded by jpegtran: the same coefficients, only /// the entropy coding differs, so the samples must be identical. #[test] fn arithmetic_coded_image_matches_its_huffman_transcode() { let huffman = decode_fixture("testimgari-huffman.jpg"); let arithmetic = decode_fixture("testimgari.jpg"); assert!(matches!(huffman.samples, Samples::U8(_))); assert!(huffman.samples == arithmetic.samples, "testimgari.jpg decodes differently from testimgari-huffman.jpg"); } /// The arithmetic and progressive arithmetic transcodes jpegtran made of testorig.jpg and /// testorig12.jpg hold their coefficients, so they decode to exactly the same samples. #[test] fn arithmetic_transcodes_match_their_huffman_sources() { for (source, transcodes) in [ ("testorig.jpg", &["testorig-ari-progressive.jpg", "testorig-ari-progressive-restart.jpg"]), ("testorig12.jpg", &["testorig12-ari.jpg", "testorig12-ari-progressive.jpg"]), ] { let reference = decode_fixture(source); for name in transcodes { assert!(decode_fixture(name).samples == reference.samples, "{name} decodes differently from {source}"); } } } /// libjpeg's arithmetic encoder drops the trailing zero bytes of a scan, so the white lower /// part of whitetail-ari.jpg decodes from zeros supplied at the EOI marker. It must decode /// whole and match its Huffman transcode. #[test] fn arithmetic_white_tail_decodes_from_implied_zeros() { let arithmetic = decode_clean("whitetail-ari.jpg"); assert_eq!((arithmetic.width, arithmetic.height, arithmetic.components), (512, 512, 1)); assert!(decode_clean("whitetail-huffman.jpg").samples == arithmetic.samples, "whitetail-ari.jpg decodes differently from whitetail-huffman.jpg"); } /// The 8-bit path against zune-jpeg. Both use centered linear chroma upsampling; the residual is /// float against integer IDCT and color conversion. #[test] fn baseline_image_matches_zune_jpeg() { let bytes = fixture("testorig.jpg"); let mut zune = zune_jpeg::JpegDecoder::new(zune_jpeg::zune_core::bytestream::ZCursor::new(&bytes[..])); let reference = zune.decode().unwrap_or_else(|e| panic!("zune-jpeg: {e:?}")); let img = decode_fixture("testorig.jpg"); let Samples::U8(px) = &img.samples else { panic!("testorig.jpg: expected 8-bit samples") }; assert_eq!(px.len(), reference.len()); let (mean, max) = diff(px.iter().map(|&v| f64::from(v)), reference.iter().map(|&v| f64::from(v))); eprintln!("testorig.jpg vs zune-jpeg: mean {mean:.3}, max {max}"); assert!(mean < MAX_MEAN_8 && max <= MAX_ABS_8, "mean {mean}, max {max}"); } // Observed: mean 0.52, max 3. const MAX_MEAN_8: f64 = 0.75; const MAX_ABS_8: f64 = 4.0; /// Parse a binary PPM with 16-bit big-endian samples. fn ppm16(bytes: &[u8]) -> (u32, u32, u32, Vec) { let mut fields = Vec::new(); let mut at = 0; while fields.len() < 4 { while bytes.get(at).is_some_and(u8::is_ascii_whitespace) { at += 1; } let start = at; while bytes.get(at).is_some_and(|b| !b.is_ascii_whitespace()) { at += 1; } fields.push(std::str::from_utf8(&bytes[start..at]).unwrap_or_default().to_owned()); } assert_eq!(fields[0], "P6"); let num = |s: &str| s.parse::().unwrap_or_else(|e| panic!("PPM header {s}: {e}")); let (w, h, maxval) = (num(&fields[1]), num(&fields[2]), num(&fields[3])); let data = &bytes[at + 1..]; let px: Vec = data.as_chunks::<2>().0.iter().map(|&c| u16::from_be_bytes(c)).collect(); assert_eq!(px.len(), (w * h * 3) as usize); (w, h, maxval, px) } /// testorig12.jpg against libjpeg-turbo's decode (testorig12.ppm, 12-bit RGB; SOURCE.md says how /// it was made). libjpeg-turbo uses an integer IDCT and integer color conversion, craft-jpeg /// floats; the comparison is on the 12-bit scale. #[test] fn twelve_bit_image_matches_libjpeg_turbo() { let (w, h, maxval, reference) = ppm16(&fixture("testorig12.ppm")); assert_eq!((w, h, maxval), (227, 149, 4095)); let img = decode_fixture("testorig12.jpg"); let Samples::U16(px) = &img.samples else { panic!("testorig12.jpg: expected 16-bit samples") }; assert_eq!(px.len(), reference.len()); let (mean, max) = diff(px.iter().map(|&v| f64::from(v) * 4095.0 / 65535.0), reference.iter().map(|&v| f64::from(v))); eprintln!("testorig12.jpg vs libjpeg-turbo (12-bit units): mean {mean:.3}, max {max:.2}"); assert!(mean < MAX_MEAN_12 && max <= MAX_ABS_12, "mean {mean}, max {max}"); } // Observed: mean 0.50, max 2.46 (of 4095). const MAX_MEAN_12: f64 = 0.75; const MAX_ABS_12: f64 = 4.0; /// Every band of each Huffman-coded 8-bit test image, read in order, joined. fn bands_of(bytes: &[u8]) -> (Vec, bool) { let bands = craft_jpeg::Bands::new(bytes, &Limits::default(), None).unwrap_or_else(|e| panic!("bands: {e}")); let mut reader = bands.reader(); let mut out = Vec::new(); while let Some(band) = reader.next_band().unwrap_or_else(|e| panic!("band: {e}")) { out.extend_from_slice(band.samples); } (out, reader.damaged()) } /// The sequential Huffman-coded 8-bit test images (4:2:0 colour, gray, and gray from the /// arithmetic white-tail test) decode in bands to exactly the whole-image decode's samples. #[test] fn baseline_images_decode_in_bands_as_whole() { for name in ["testorig.jpg", "testimgari-huffman.jpg", "whitetail-huffman.jpg"] { let bytes = fixture(name); let whole = decode_clean(name); let Samples::U8(px) = &whole.samples else { panic!("{name}: expected 8-bit samples") }; let (bands, damaged) = bands_of(&bytes); assert!(!damaged, "{name}"); assert!(&bands == px, "{name}: bands differ from the whole-image decode"); } }