diff --git a/tests/decode_images.rs b/tests/decode_images.rs index eaf414e..d6339a1 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -10,7 +10,6 @@ use std::path::PathBuf; const TEST_IMAGE_DIR: &str = "./tests/images/"; const SEQ_IMAGE_DIR: &str = "./tests/images/seq/"; -const LIBTIFFPIC_DEPTH_DIR: &str = "./tests/libtiffpic/depth/"; macro_rules! test_image_sum { ($name:ident, $buffer:ident, $sum_ty:ty) => { @@ -43,32 +42,6 @@ test_image_sum!(test_image_sum_u64, U64, u64); test_image_sum!(test_image_sum_f32, F32, f32); test_image_sum!(test_image_sum_f64, F64, f64); -/// Like test_image_sum but reads from an arbitrary directory. -macro_rules! test_image_sum_in { - ($name:ident, $buffer:ident, $sum_ty:ty) => { - fn $name(dir: &str, file: &str, expected_type: ColorType, expected_sum: $sum_ty) { - let path = PathBuf::from(dir).join(file); - let img_file = File::open(path).expect("Cannot find test image!"); - let mut decoder = Decoder::open(img_file).expect("Cannot create decoder"); - - decoder.next_image().expect("Cannot read image IFD"); - assert_eq!(decoder.colortype().unwrap(), expected_type); - let img_res = decoder.read_image().unwrap(); - - match img_res { - DecodingResult::$buffer(res) => { - let sum: $sum_ty = res.into_iter().map(<$sum_ty>::from).sum(); - assert_eq!(sum, expected_sum); - } - _ => panic!("Wrong bit depth"), - } - } - }; -} - -test_image_sum_in!(test_depth_sum_u16, U16, u64); -test_image_sum_in!(test_depth_sum_u32, U32, u64); - /// Compute a CRC32 hash of the full decoded TIFF output including metadata. /// /// Hashes: page count, and for each page: dimensions, color type, and decoded @@ -1028,114 +1001,6 @@ fn test_decode_huge_g4() { } } -// ---- Extended bit depth tests (9-31 bits per sample) ---- -// -// These test the unpacking of packed N-bit samples to standard-width output. -// All flower-* images are 73x43 pixels from the libtiffpic depth suite. - -// Gray extended depths (contiguous, BlackIsZero) -#[test] -fn test_gray_u10() { - test_depth_sum_u16( - LIBTIFFPIC_DEPTH_DIR, - "flower-minisblack-10.tif", - ColorType::Gray(10), - 1175406, - ); -} - -#[test] -fn test_gray_u14() { - test_depth_sum_u16( - LIBTIFFPIC_DEPTH_DIR, - "flower-minisblack-14.tif", - ColorType::Gray(14), - 18847564, - ); -} - -#[test] -fn test_gray_u24() { - test_depth_sum_u32( - LIBTIFFPIC_DEPTH_DIR, - "flower-minisblack-24.tif", - ColorType::Gray(24), - 19201687050, - ); -} - -// RGB contiguous extended depths -#[test] -fn test_rgb_contig_u10() { - test_depth_sum_u16( - LIBTIFFPIC_DEPTH_DIR, - "flower-rgb-contig-10.tif", - ColorType::RGB(10), - 3302314, - ); -} - -#[test] -fn test_rgb_contig_u14() { - test_depth_sum_u16( - LIBTIFFPIC_DEPTH_DIR, - "flower-rgb-contig-14.tif", - ColorType::RGB(14), - 52955900, - ); -} - -#[test] -fn test_rgb_contig_u24() { - test_depth_sum_u32( - LIBTIFFPIC_DEPTH_DIR, - "flower-rgb-contig-24.tif", - ColorType::RGB(24), - 53926969278, - ); -} - -// RGB planar extended depths (read_image returns first plane only) -#[test] -fn test_rgb_planar_u10() { - test_depth_sum_u16( - LIBTIFFPIC_DEPTH_DIR, - "flower-rgb-planar-10.tif", - ColorType::RGB(10), - 1214176, - ); -} - -#[test] -fn test_rgb_planar_u12() { - test_depth_sum_u16( - LIBTIFFPIC_DEPTH_DIR, - "flower-rgb-planar-12.tif", - ColorType::RGB(12), - 4865028, - ); -} - -#[test] -fn test_rgb_planar_u14() { - test_depth_sum_u16( - LIBTIFFPIC_DEPTH_DIR, - "flower-rgb-planar-14.tif", - ColorType::RGB(14), - 19468331, - ); -} - -#[test] -fn test_rgb_planar_u24() { - test_depth_sum_u32( - LIBTIFFPIC_DEPTH_DIR, - "flower-rgb-planar-24.tif", - ColorType::RGB(24), - 19835471019, - ); -} - // ---- WhiteIsZero tests across bit depths ---- // WhiteIsZero + extended bit depth (12-bit gray, synthetic 4x2 image) diff --git a/tests/decode_libtiffpic.rs b/tests/decode_libtiffpic.rs index 180c5c0..37324fb 100644 --- a/tests/decode_libtiffpic.rs +++ b/tests/decode_libtiffpic.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use std::{fs::File, path}; use tiff::decoder::{Decoder, DecodingResult}; +use tiff::tags::{ByteOrder, Type}; +use tiff::ColorType; use libtest_mimic::{Arguments, Failed, Trial}; use walkdir::WalkDir; @@ -9,95 +11,202 @@ use walkdir::WalkDir; const REFERENCE_DIR: &str = "tests/libtiffpic"; // `find test/libtiffpic -type f | sort` -const FILES: &[(&str, bool)] = &[ - ("tests/libtiffpic/caspian.tif", true), - ("tests/libtiffpic/cramps.tif", true), - ("tests/libtiffpic/cramps-tile.tif", true), - ("tests/libtiffpic/CVS/Entries", false), - ("tests/libtiffpic/CVS/Repository", false), - ("tests/libtiffpic/CVS/Root", false), - ("tests/libtiffpic/depth/CVS/Entries", false), - ("tests/libtiffpic/depth/CVS/Repository", false), - ("tests/libtiffpic/depth/CVS/Root", false), - ("tests/libtiffpic/depth/flower-minisblack-02.tif", true), - ("tests/libtiffpic/depth/flower-minisblack-04.tif", true), - ("tests/libtiffpic/depth/flower-minisblack-06.tif", true), - ("tests/libtiffpic/depth/flower-minisblack-08.tif", true), - ("tests/libtiffpic/depth/flower-minisblack-10.tif", true), - ("tests/libtiffpic/depth/flower-minisblack-12.tif", true), - ("tests/libtiffpic/depth/flower-minisblack-14.tif", true), - ("tests/libtiffpic/depth/flower-minisblack-16.tif", true), - ("tests/libtiffpic/depth/flower-minisblack-24.tif", true), - ("tests/libtiffpic/depth/flower-minisblack-32.tif", true), - ("tests/libtiffpic/depth/flower-palette-02.tif", true), - ("tests/libtiffpic/depth/flower-palette-04.tif", true), - ("tests/libtiffpic/depth/flower-palette-08.tif", true), - ("tests/libtiffpic/depth/flower-palette-16.tif", true), - ("tests/libtiffpic/depth/flower-rgb-contig-02.tif", true), - ("tests/libtiffpic/depth/flower-rgb-contig-04.tif", true), - ("tests/libtiffpic/depth/flower-rgb-contig-08.tif", true), - ("tests/libtiffpic/depth/flower-rgb-contig-10.tif", true), - ("tests/libtiffpic/depth/flower-rgb-contig-12.tif", true), - ("tests/libtiffpic/depth/flower-rgb-contig-14.tif", true), - ("tests/libtiffpic/depth/flower-rgb-contig-16.tif", true), - ("tests/libtiffpic/depth/flower-rgb-contig-24.tif", true), - ("tests/libtiffpic/depth/flower-rgb-contig-32.tif", true), - ("tests/libtiffpic/depth/flower-rgb-planar-02.tif", true), - ("tests/libtiffpic/depth/flower-rgb-planar-04.tif", true), - ("tests/libtiffpic/depth/flower-rgb-planar-08.tif", true), - ("tests/libtiffpic/depth/flower-rgb-planar-10.tif", true), - ("tests/libtiffpic/depth/flower-rgb-planar-12.tif", true), - ("tests/libtiffpic/depth/flower-rgb-planar-14.tif", true), - ("tests/libtiffpic/depth/flower-rgb-planar-16.tif", true), - ("tests/libtiffpic/depth/flower-rgb-planar-24.tif", true), - ("tests/libtiffpic/depth/flower-rgb-planar-32.tif", true), +// +// Each entry maps a file path to its expected behavior: +// None = not a TIFF file, skip entirely +// Some(0) = TIFF with unsupported features, decode-only smoke test +// Some(nonzero) = TIFF with full CRC32 hash verification of decoded output +const FILES: &[(&str, Option)] = &[ + ("tests/libtiffpic/caspian.tif", Some(0x4ea8d4e4)), + ("tests/libtiffpic/cramps.tif", Some(0xdfa2fba4)), + ("tests/libtiffpic/cramps-tile.tif", Some(0xdfa2fba4)), + ("tests/libtiffpic/CVS/Entries", None), + ("tests/libtiffpic/CVS/Repository", None), + ("tests/libtiffpic/CVS/Root", None), + ("tests/libtiffpic/depth/CVS/Entries", None), + ("tests/libtiffpic/depth/CVS/Repository", None), + ("tests/libtiffpic/depth/CVS/Root", None), + ( + "tests/libtiffpic/depth/flower-minisblack-02.tif", + Some(0x1c90f515), + ), + ( + "tests/libtiffpic/depth/flower-minisblack-04.tif", + Some(0x0f4813cb), + ), + ( + "tests/libtiffpic/depth/flower-minisblack-06.tif", + Some(0xf0c0a901), + ), + ( + "tests/libtiffpic/depth/flower-minisblack-08.tif", + Some(0xe797b15b), + ), + ( + "tests/libtiffpic/depth/flower-minisblack-10.tif", + Some(0xdee442ec), + ), + ( + "tests/libtiffpic/depth/flower-minisblack-12.tif", + Some(0x4810c4e9), + ), + ( + "tests/libtiffpic/depth/flower-minisblack-14.tif", + Some(0x7e03cf03), + ), + ( + "tests/libtiffpic/depth/flower-minisblack-16.tif", + Some(0x8e0add32), + ), + ( + "tests/libtiffpic/depth/flower-minisblack-24.tif", + Some(0x5cf29313), + ), + ( + "tests/libtiffpic/depth/flower-minisblack-32.tif", + Some(0x80025eee), + ), + ( + "tests/libtiffpic/depth/flower-palette-02.tif", + Some(0xaeb21cff), + ), + ( + "tests/libtiffpic/depth/flower-palette-04.tif", + Some(0x339d428b), + ), + ( + "tests/libtiffpic/depth/flower-palette-08.tif", + Some(0xb6b203e9), + ), + ( + "tests/libtiffpic/depth/flower-palette-16.tif", + Some(0x4e02308f), + ), + ( + "tests/libtiffpic/depth/flower-rgb-contig-02.tif", + Some(0x8f16cf35), + ), + ( + "tests/libtiffpic/depth/flower-rgb-contig-04.tif", + Some(0x95e97f94), + ), + ( + "tests/libtiffpic/depth/flower-rgb-contig-08.tif", + Some(0x98134e7c), + ), + ( + "tests/libtiffpic/depth/flower-rgb-contig-10.tif", + Some(0xa2ad8f4d), + ), + ( + "tests/libtiffpic/depth/flower-rgb-contig-12.tif", + Some(0xfad187fc), + ), + ( + "tests/libtiffpic/depth/flower-rgb-contig-14.tif", + Some(0x93fdc170), + ), + ( + "tests/libtiffpic/depth/flower-rgb-contig-16.tif", + Some(0x25e1066b), + ), + ( + "tests/libtiffpic/depth/flower-rgb-contig-24.tif", + Some(0x25cdf743), + ), + ( + "tests/libtiffpic/depth/flower-rgb-contig-32.tif", + Some(0x195dde9c), + ), + ( + "tests/libtiffpic/depth/flower-rgb-planar-02.tif", + Some(0xcc0281ca), + ), + ( + "tests/libtiffpic/depth/flower-rgb-planar-04.tif", + Some(0x867134b2), + ), + ( + "tests/libtiffpic/depth/flower-rgb-planar-08.tif", + Some(0x2341af83), + ), + ( + "tests/libtiffpic/depth/flower-rgb-planar-10.tif", + Some(0x79211047), + ), + ( + "tests/libtiffpic/depth/flower-rgb-planar-12.tif", + Some(0x14a0ff72), + ), + ( + "tests/libtiffpic/depth/flower-rgb-planar-14.tif", + Some(0xc077389d), + ), + ( + "tests/libtiffpic/depth/flower-rgb-planar-16.tif", + Some(0xd0849282), + ), + ( + "tests/libtiffpic/depth/flower-rgb-planar-24.tif", + Some(0x433d333b), + ), + ( + "tests/libtiffpic/depth/flower-rgb-planar-32.tif", + Some(0xd0780b23), + ), ( "tests/libtiffpic/depth/flower-separated-contig-08.tif", - true, + Some(0x97fb03a6), ), ( "tests/libtiffpic/depth/flower-separated-contig-16.tif", - true, + Some(0xb6ee0135), ), ( "tests/libtiffpic/depth/flower-separated-planar-08.tif", - true, + Some(0x502e93fa), ), ( "tests/libtiffpic/depth/flower-separated-planar-16.tif", - true, + Some(0x50e580d9), ), - ("tests/libtiffpic/depth/genimages", false), - ("tests/libtiffpic/depth/README.txt", false), - ("tests/libtiffpic/depth/summary.txt", false), - ("tests/libtiffpic/dscf0013.tif", true), - ("tests/libtiffpic/fax2d.tif", true), + ("tests/libtiffpic/depth/genimages", None), + ("tests/libtiffpic/depth/README.txt", None), + ("tests/libtiffpic/depth/summary.txt", None), + ("tests/libtiffpic/dscf0013.tif", Some(0)), // unsupported YCbCr chroma subsampling + ("tests/libtiffpic/fax2d.tif", Some(0)), // unsupported Fax3 compression // No support for direct CCITT Group 3 1D format - ("tests/libtiffpic/g3test.g3", false), - ("tests/libtiffpic/g3test.tif", true), - ("tests/libtiffpic/jello.tif", true), - ("tests/libtiffpic/jim___ah.tif", true), - ("tests/libtiffpic/jim___cg.tif", true), - ("tests/libtiffpic/jim___dg.tif", true), - ("tests/libtiffpic/jim___gg.tif", true), - ("tests/libtiffpic/ladoga.tif", true), - ("tests/libtiffpic/off_l16.tif", true), - ("tests/libtiffpic/off_luv24.tif", true), - ("tests/libtiffpic/off_luv32.tif", true), - ("tests/libtiffpic/oxford.tif", true), - ("tests/libtiffpic/pc260001.tif", true), - ("tests/libtiffpic/quad-jpeg.tif", true), - ("tests/libtiffpic/quad-lzw.tif", true), - ("tests/libtiffpic/quad-tile.tif", true), - ("tests/libtiffpic/README", false), - ("tests/libtiffpic/smallliz.tif", true), - ("tests/libtiffpic/strike.tif", true), - ("tests/libtiffpic/text.tif", true), - ("tests/libtiffpic/ycbcr-cat.tif", true), - ("tests/libtiffpic/zackthecat.tif", true), + ("tests/libtiffpic/g3test.g3", None), + ("tests/libtiffpic/g3test.tif", Some(0)), // unsupported Fax3 compression + ("tests/libtiffpic/jello.tif", Some(0xb03dbda9)), + ("tests/libtiffpic/jim___ah.tif", Some(0x81a27b7f)), + ("tests/libtiffpic/jim___cg.tif", Some(0xfd31747c)), + ("tests/libtiffpic/jim___dg.tif", Some(0x8c6a5e8f)), + ("tests/libtiffpic/jim___gg.tif", Some(0x8c6a5e8f)), + ("tests/libtiffpic/ladoga.tif", Some(0x9b118511)), + ("tests/libtiffpic/off_l16.tif", Some(0xd434a2d9)), + ("tests/libtiffpic/off_luv24.tif", Some(0x2bee71a2)), + ("tests/libtiffpic/off_luv32.tif", Some(0xffaa1e2e)), + ("tests/libtiffpic/oxford.tif", Some(0x355d2e3e)), + ("tests/libtiffpic/pc260001.tif", Some(0xdb56b753)), + ("tests/libtiffpic/quad-jpeg.tif", Some(0xcba22475)), + ("tests/libtiffpic/quad-lzw.tif", Some(0)), // invalid LZW stream + ("tests/libtiffpic/quad-tile.tif", Some(0x6055f9ee)), + ("tests/libtiffpic/README", None), + ("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported YCbCr chroma subsampling + ("tests/libtiffpic/strike.tif", Some(0x016b0f6a)), + ("tests/libtiffpic/text.tif", Some(0)), // unsupported compression + ("tests/libtiffpic/ycbcr-cat.tif", Some(0)), // unsupported YCbCr chroma subsampling + ("tests/libtiffpic/zackthecat.tif", Some(0)), // unsupported YCbCr chroma subsampling ]; fn main() { + let args: Vec = std::env::args().collect(); + if args.iter().any(|a| a == "--print-hashes") { + print_reference_hashes(); + return; + } + let mut candidates: HashMap<_, _> = FILES.iter().copied().collect(); let mut trials = Vec::new(); @@ -114,22 +223,19 @@ fn main() { let path = path.replace(std::path::MAIN_SEPARATOR, "/"); // always use UNIX path separators // Mark this path as consumed in either case by removing it. - let Some(should_decode) = candidates.remove(path.as_str()) else { + let Some(expected_hash) = candidates.remove(path.as_str()) else { panic!("File {} not in reference list", path); }; - // Check if we should turn it into a test case. - if !should_decode { + // None = non-image file, skip + let Some(expected_hash) = expected_hash else { continue; - } + }; let path = path.to_string(); trials.push(Trial::test( path.to_string(), - move || -> Result<(), Failed> { - decode_libtiffpic(&path) - .map_err(|e| Failed::from(format_args!("Decoding failed for {}: {}", path, e))) - }, + move || -> Result<(), Failed> { decode_and_verify(&path, expected_hash) }, )); } @@ -145,17 +251,114 @@ fn main() { libtest_mimic::run(&args, trials).exit(); } -fn decode_libtiffpic(path: &str) -> Result<(), tiff::TiffError> { - let path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path); +/// Map DecodingResult variant to a TIFF Type for byte-order normalization. +fn sample_type_of(result: &DecodingResult) -> Type { + match result { + DecodingResult::U8(_) => Type::BYTE, + DecodingResult::I8(_) => Type::SBYTE, + DecodingResult::U16(_) => Type::SHORT, + DecodingResult::I16(_) => Type::SSHORT, + DecodingResult::U32(_) => Type::LONG, + DecodingResult::I32(_) => Type::SLONG, + DecodingResult::F32(_) => Type::FLOAT, + DecodingResult::U64(_) => Type::LONG8, + DecodingResult::I64(_) => Type::SLONG8, + DecodingResult::F64(_) => Type::DOUBLE, + DecodingResult::F16(_) => Type::SHORT, // f16 is 2 bytes like SHORT + } +} +/// Hash color type into the hasher (discriminant + fields). +fn hash_color_type(hasher: &mut crc32fast::Hasher, color_type: ColorType) { + match color_type { + ColorType::Gray(n) => hasher.update(&[0, n]), + ColorType::RGB(n) => hasher.update(&[1, n]), + ColorType::Palette(n) => hasher.update(&[2, n]), + ColorType::GrayA(n) => hasher.update(&[3, n]), + ColorType::RGBA(n) => hasher.update(&[4, n]), + ColorType::CMYK(n) => hasher.update(&[5, n]), + ColorType::CMYKA(n) => hasher.update(&[6, n]), + ColorType::YCbCr(n) => hasher.update(&[7, n]), + ColorType::Lab(n) => hasher.update(&[8, n]), + ColorType::Multiband { + bit_depth, + num_samples, + } => { + hasher.update(&[9, bit_depth]); + hasher.update(&num_samples.to_le_bytes()); + } + other => panic!("compute_tiff_hash: unhandled ColorType {other:?}"), + } +} + +/// Compute a CRC32 hash of the full decoded TIFF output using read_image_to_buffer. +/// +/// Hashes per page: page index, dimensions, color type, TIFF sample type tag, +/// and decoded sample data normalized to little-endian byte order. +fn compute_hash(path: &std::path::Path) -> Result { let img_file = File::open(path)?; let mut decoder = Decoder::open(img_file)?; - + let mut hasher = crc32fast::Hasher::new(); let mut buffer = DecodingResult::U8(vec![]); + + let mut page = 0u32; while decoder.more_images() { decoder.next_image()?; - let _layout = decoder.read_image_to_buffer(&mut buffer); + let (width, height) = decoder.dimensions()?; + let color_type = decoder.colortype()?; + let _layout = decoder.read_image_to_buffer(&mut buffer)?; + + hasher.update(&page.to_le_bytes()); + hasher.update(&width.to_le_bytes()); + hasher.update(&height.to_le_bytes()); + hash_color_type(&mut hasher, color_type); + + let sample_type = sample_type_of(&buffer); + hasher.update(&sample_type.to_u16().to_le_bytes()); + + let mut buf = buffer.as_buffer(0); + let bytes = buf.as_bytes_mut(); + ByteOrder::native().convert(sample_type, bytes, ByteOrder::LittleEndian); + hasher.update(bytes); + + page += 1; + } + + hasher.update(&page.to_le_bytes()); + Ok(hasher.finalize()) +} + +fn decode_and_verify(path: &str, expected_hash: u32) -> Result<(), Failed> { + let full_path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path); + + if expected_hash == 0 { + // Smoke test: try to decode, tolerate errors (unsupported formats) + let _ = compute_hash(&full_path); + return Ok(()); + } + + let actual_hash = compute_hash(&full_path) + .map_err(|e| Failed::from(format!("Decoding failed for {path}: {e}")))?; + + if actual_hash != expected_hash { + return Err(Failed::from(format!( + "{path}: expected hash 0x{expected_hash:08x} but got 0x{actual_hash:08x}" + ))); } Ok(()) } + +/// Print reference hashes for all decodable files (use --print-hashes). +fn print_reference_hashes() { + for &(path, expected) in FILES { + if expected.is_none() { + continue; + } + let full_path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path); + match compute_hash(&full_path) { + Ok(hash) => eprintln!("(\"{path}\", Some(0x{hash:08x})),"), + Err(e) => eprintln!("// {path}: FAILED: {e}"), + } + } +}