diff --git a/Cargo.toml b/Cargo.toml index d1531f7..752c199 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ harness = false name = "encode" harness = false + [[test]] name = "decode_libtiffpic" path = "tests/decode_libtiffpic.rs" diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 5383302..4593ccb 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -85,6 +85,8 @@ pub(crate) struct Image { pub chunk_offsets: Vec, pub chunk_bytes: Vec, pub chroma_subsampling: (u16, u16), + /// FillOrder: 1 = MSB-to-LSB (default), 2 = LSB-to-MSB (common in fax images) + pub fill_order: u16, /// The color map (palette) for `PhotometricInterpretation::RGBPalette`. /// Contains `3 * 2^BitsPerSample` u16 entries: R values, then G values, then B values. pub color_map: Option>, @@ -135,6 +137,10 @@ pub(crate) struct ReadoutLayout { /// Bits per sample in the encoded data. pub tiff_bits_per_sample: u8, + /// Bits per sample in the output buffer, promoted to a standard width. + /// For sub-byte depths (1-8): same as tiff_bits_per_sample. + /// For 9-16: 16. For 17-32: 32. For 33-64: 64. + pub output_bits_per_sample: u8, /// Number of samples in the encoded data. pub tiff_samples: u16, /// Dimensions of the underlying rectangular chunks (tile or strips). @@ -169,6 +175,7 @@ impl Image { chunk_offsets: Vec::new(), chunk_bytes: Vec::new(), chroma_subsampling: (2, 2), + fill_order: 1, color_map: None, decompression_to_host_endian: false, } @@ -329,6 +336,12 @@ impl Image { (2, 2) }; + let fill_order = tag_reader + .find_tag(Tag::FillOrder)? + .map(Value::into_u16) + .transpose()? + .unwrap_or(1); + let planes = match planar_config { PlanarConfiguration::Chunky => 1, PlanarConfiguration::Planar => samples, @@ -506,6 +519,7 @@ impl Image { chunk_offsets, chunk_bytes, chroma_subsampling, + fill_order, color_map, decompression_to_host_endian: matches!( compression_method, @@ -677,6 +691,7 @@ impl Image { #[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>, dimensions: (u32, u32), samples: u16, + #[cfg_attr(not(feature = "fax"), allow(unused_variables))] fill_order: u16, ) -> TiffResult> { Ok(match compression_method { CompressionMethod::None => Box::new(reader), @@ -770,10 +785,18 @@ impl Image { Box::new(Cursor::new(data)) } #[cfg(feature = "fax")] + CompressionMethod::Fax3 => Box::new(super::stream::Group3Reader::new( + dimensions, + reader, + compressed_length, + fill_order, + )?), + #[cfg(feature = "fax")] CompressionMethod::Fax4 => Box::new(super::stream::Group4Reader::new( dimensions, reader, compressed_length, + fill_order, )?), #[cfg(feature = "webp")] CompressionMethod::WebP => Box::new(super::stream::WebPReader::new( @@ -872,24 +895,40 @@ impl Image { let data_dimensions = (width, height); + // Output bits per sample: sub-byte depths stay packed, supra-byte non-standard + // depths are promoted to the next standard width (matching sample_type() mapping). + let output_bits_per_sample: u8 = match tiff_bits_per_sample { + 0..=8 => tiff_bits_per_sample, + 9..=16 => 16, + 17..=32 => 32, + _ => 64, + }; + + // Packed row size for reading from the file (uses tiff_bits_per_sample). let tiff_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample)) .checked_mul(u64::from(tiff_samples)) .ok_or(TiffError::LimitsExceeded)?; let tiff_row_bytes: usize = tiff_row_bits.div_ceil(8).try_into()?; - let chunk_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample)) - .checked_mul(u64::from(data_samples)) - .ok_or(TiffError::LimitsExceeded)?; + // Output buffer sizes use output_bits_per_sample (the promoted width). + let chunk_row_bits = (u64::from(tiff_chunk_dimensions.0) + * u64::from(output_bits_per_sample)) + .checked_mul(u64::from(data_samples)) + .ok_or(TiffError::LimitsExceeded)?; let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?; - let data_row_bits = (u64::from(data_dimensions.0) * u64::from(tiff_bits_per_sample)) + let data_row_bits = (u64::from(data_dimensions.0) * u64::from(output_bits_per_sample)) .checked_mul(u64::from(data_samples)) .ok_or(TiffError::LimitsExceeded)?; let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?; + // Clamp the chunk height to the image height: a chunk never contributes more + // rows than the image contains. Without this, RowsPerStrip = u32::MAX overflows + // usize on 32-bit platforms. + let clamped_chunk_height = tiff_chunk_dimensions.1.min(data_dimensions.1); let chunk_col_stride: usize = data_row_bits .div_ceil(8) - .checked_mul(u64::from(tiff_chunk_dimensions.1)) + .checked_mul(u64::from(clamped_chunk_height)) .ok_or(TiffError::LimitsExceeded)? .try_into()?; @@ -938,6 +977,7 @@ impl Image { chunk_col_stride, plane_stride, tiff_bits_per_sample, + output_bits_per_sample, tiff_samples, tiff_chunk_dimensions, tiff_row_bytes, @@ -1013,7 +1053,12 @@ impl Image { bit_depth: n, num_samples: _, } if n == 8 || n == 16 || n == 32 || n == 64 => {} - ColorType::Gray(n) + ColorType::RGB(n) + | ColorType::RGBA(n) + | ColorType::CMYK(n) + | ColorType::CMYKA(n) + | ColorType::YCbCr(n) + | ColorType::Gray(n) | ColorType::Palette(n) | ColorType::Multiband { bit_depth: n, @@ -1031,6 +1076,26 @@ impl Image { )); } }, + // Extended bit depths (9-15, 17-31): packed N-bit samples unpacked to standard width. + // Horizontal predictor is supported (operates on unpacked standard-width samples). + // FloatingPoint predictor is not meaningful for non-standard integer depths. + ColorType::RGB(n) + | ColorType::RGBA(n) + | ColorType::CMYK(n) + | ColorType::CMYKA(n) + | ColorType::YCbCr(n) + | ColorType::Gray(n) + | ColorType::Multiband { + bit_depth: n, + num_samples: _, + } if n <= 32 => match self.predictor { + Predictor::None | Predictor::Horizontal => {} + Predictor::FloatingPoint => { + return Err(TiffError::UnsupportedError( + TiffUnsupportedError::FloatingPointPredictor(color_type), + )); + } + }, type_ => { return Err(TiffError::UnsupportedError( TiffUnsupportedError::UnsupportedColorType(type_), @@ -1102,9 +1167,52 @@ impl Image { self.jpeg_tables.as_deref().map(|a| &**a), chunk_dims, self.samples, + self.fill_order, )?; - if is_output_chunk_rows && is_all_bits { + // Extended bit depths (9-15, 17-31): packed N-bit samples must be unpacked to + // standard-width output samples before endianness fix and predictor. + let needs_unpacking = layout.tiff_bits_per_sample != layout.output_bits_per_sample + && layout.tiff_bits_per_sample > 8; + + if needs_unpacking { + if !is_all_bits { + // Packed extended depths with extra samples to discard is not yet supported. + return Err(TiffError::UnsupportedError( + TiffUnsupportedError::InterpretationWithBits( + self.photometric_interpretation, + vec![self.bits_per_sample; self.samples as usize], + ), + )); + } + + let tiff_bps = layout.tiff_bits_per_sample; + let out_bps = layout.output_bits_per_sample; + let samples_per_data_row = data_dims.0 as usize * samples as usize; + + let mut packed = vec![0u8; chunk_row_bytes]; + + for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) { + reader.read_exact(&mut packed)?; + + let out_row = &mut row[..data_row_bytes]; + unpack_bits(&packed, out_row, tiff_bps, samples_per_data_row, out_bps); + + // Unpacked values are in native byte order, so pass native to skip + // endianness conversion while still applying the predictor. + super::fix_endianness_and_predict( + out_row, + color_type.bit_depth(), + samples, + ByteOrder::native(), + predictor, + ); + + if photometric_interpretation == PhotometricInterpretation::WhiteIsZero { + super::invert_colors(out_row, color_type, self.sample_format)?; + } + } + } else if is_output_chunk_rows && is_all_bits { // Here we can read directly into the output buffer itself. let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize]; reader.read_exact(tile)?; @@ -1229,6 +1337,108 @@ impl Image { } } +/// Unpack N-bit packed samples (MSB-first) into standard-width output samples in native byte +/// order. Packed bits are read from `src` and expanded samples are written to `dst`. +/// +/// `bits_per_sample` is the packed depth (e.g. 10, 12, 14). +/// `num_samples` is the number of samples to extract. +/// `output_width` is the output sample width in bits (16 or 32). +/// +/// # Panics +/// +/// Panics if `dst` is too small for `num_samples` output samples. +fn unpack_bits( + src: &[u8], + dst: &mut [u8], + bits_per_sample: u8, + num_samples: usize, + output_width: u8, +) { + // Dispatch to const-generic specializations so the compiler can unroll the + // byte-gathering loop and precompute shift/mask constants. + macro_rules! dispatch { + ($($bps:literal),+) => { + match bits_per_sample { + $( $bps => unpack_bits_const::<$bps>(src, dst, num_samples, output_width), )+ + _ => unpack_bits_const::<0>(src, dst, num_samples, output_width), + } + }; + } + // Cover every bit depth from 9..=32 that could reach this path, plus 0 as fallback. + dispatch!( + 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32 + ); +} + +/// Inner loop with const BPS so the compiler can optimize the bit extraction. +/// BPS=0 is the fallback for unexpected bit depths (uses runtime value). +#[inline(always)] +fn unpack_bits_const( + src: &[u8], + dst: &mut [u8], + num_samples: usize, + output_width: u8, +) { + debug_assert!(output_width == 16 || output_width == 32); + + match output_width { + 16 => { + debug_assert!(dst.len() >= num_samples * 2); + let mut bit_offset: usize = 0; + for i in 0..num_samples { + let val = if BPS > 0 { + extract_bits_msb_const::(src, bit_offset) + } else { + extract_bits_msb_const::<16>(src, bit_offset) // unreachable fallback + }; + dst[i * 2..i * 2 + 2].copy_from_slice(&(val as u16).to_ne_bytes()); + bit_offset += if BPS > 0 { BPS as usize } else { 16 }; + } + } + 32 => { + debug_assert!(dst.len() >= num_samples * 4); + let mut bit_offset: usize = 0; + for i in 0..num_samples { + let val = if BPS > 0 { + extract_bits_msb_const::(src, bit_offset) + } else { + extract_bits_msb_const::<32>(src, bit_offset) // unreachable fallback + }; + dst[i * 4..i * 4 + 4].copy_from_slice(&val.to_ne_bytes()); + bit_offset += if BPS > 0 { BPS as usize } else { 32 }; + } + } + _ => unreachable!(), + } +} + +/// Extract a value of `BPS` bits starting at `bit_offset` from an MSB-first packed byte stream. +/// Uses a single wide read + shift/mask for branchless extraction. +#[inline(always)] +fn extract_bits_msb_const(src: &[u8], bit_offset: usize) -> u32 { + let byte_idx = bit_offset / 8; + let bit_idx = bit_offset % 8; + let mask: u64 = (1u64 << BPS) - 1; + + // Read a u64 from as many bytes as available, big-endian, without going out of bounds. + // For BPS <= 32 (+ up to 7 bits offset = 39 bits max), we need at most 5 bytes, + // but reading 8 at once is faster and the branch is only taken near row ends. + let remaining = src.len() - byte_idx; + let acc = if remaining >= 8 { + u64::from_be_bytes(src[byte_idx..byte_idx + 8].try_into().unwrap()) + } else { + // Near the end of the buffer — read available bytes into the high bits of a u64. + let mut buf = [0u8; 8]; + buf[..remaining].copy_from_slice(&src[byte_idx..]); + u64::from_be_bytes(buf) + }; + + // The value starts at bit_idx from the MSB of our u64. Shift it to the LSB and mask. + let shift = 64 - bit_idx - BPS as usize; + ((acc >> shift) & mask) as u32 +} + impl ReadoutLayout { pub(crate) fn samples_per_out_texel(&self) -> u16 { match self.planar_config { @@ -1241,7 +1451,7 @@ impl ReadoutLayout { // pixel data. pub(crate) fn chunk_row_bytes(&self, width: u32) -> TiffResult { let data_samples = self.samples_per_out_texel(); - let data_row_bits = (u64::from(width) * u64::from(self.tiff_bits_per_sample)) + let data_row_bits = (u64::from(width) * u64::from(self.output_bits_per_sample)) .checked_mul(u64::from(data_samples)) .ok_or(TiffError::LimitsExceeded)?; Ok(data_row_bits.div_ceil(8).try_into()?) @@ -1259,8 +1469,12 @@ impl ReadoutLayout { let data_row_bytes = u64::try_from(row_stride)?; + // Use the original image height (from plane_stride / row_stride) to clamp + // the chunk height, matching the clamping in readout_for_size(). + let image_height = self.plane_stride.checked_div(self.row_stride).unwrap_or(0) as u32; + let clamped_chunk_height = self.tiff_chunk_dimensions.1.min(image_height); let chunk_col_stride = data_row_bytes - .checked_mul(u64::from(self.tiff_chunk_dimensions.1)) + .checked_mul(u64::from(clamped_chunk_height)) .ok_or(TiffError::LimitsExceeded)? .try_into()?; diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index c4ecf93..81a227c 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -740,6 +740,58 @@ fn invert_colors( *x = !*x; } } + (ColorType::Gray(n @ (3 | 5 | 6 | 7)), SampleFormat::Uint) => { + // Non-standard sub-byte depths where samples may cross byte boundaries. + // Byte-wise NOT would corrupt samples spanning two bytes, so we invert + // each N-bit sample individually within the packed stream. + let max = (1u8 << n) - 1; + let total_bits = buf.len() * 8; + let mut bit_offset = 0; + while bit_offset + n as usize <= total_bits { + let byte_idx = bit_offset / 8; + let bit_in_byte = bit_offset % 8; + + if bit_in_byte + n as usize <= 8 { + // Sample fits within one byte + let shift = 8 - bit_in_byte - n as usize; + let val = (buf[byte_idx] >> shift) & max; + let mask = max << shift; + buf[byte_idx] = (buf[byte_idx] & !mask) | ((max - val) << shift); + } else { + // Sample spans two bytes + let combined = ((buf[byte_idx] as u16) << 8) + | buf.get(byte_idx + 1).copied().unwrap_or(0) as u16; + let shift = 16 - bit_in_byte - n as usize; + let max16 = max as u16; + let val = ((combined >> shift) & max16) as u8; + let new_combined = + (combined & !(max16 << shift)) | ((max - val) as u16) << shift; + buf[byte_idx] = (new_combined >> 8) as u8; + if byte_idx + 1 < buf.len() { + buf[byte_idx + 1] = new_combined as u8; + } + } + + bit_offset += n as usize; + } + } + (ColorType::Gray(n @ 9..=15), SampleFormat::Uint) => { + // Extended depths promoted to u16: invert within the valid bit range. + // Byte-wise NOT would flip the upper padding bits, so we use (max - v). + let max = (1u16 << n) - 1; + for chunk in buf.as_chunks_mut::<2>().0 { + let v = u16::from_ne_bytes(*chunk); + *chunk = (max - v).to_ne_bytes(); + } + } + (ColorType::Gray(n @ 17..=31), SampleFormat::Uint) => { + // Extended depths promoted to u32: invert within the valid bit range. + let max = (1u32 << n) - 1; + for chunk in buf.as_chunks_mut::<4>().0 { + let v = u32::from_ne_bytes(*chunk); + *chunk = (max - v).to_ne_bytes(); + } + } (ColorType::Gray(32), SampleFormat::IEEEFP) => { for x in buf.as_chunks_mut::<4>().0 { let v = f32::from_ne_bytes(*x); diff --git a/src/decoder/stream.rs b/src/decoder/stream.rs index cf67d5d..9fb5e4e 100644 --- a/src/decoder/stream.rs +++ b/src/decoder/stream.rs @@ -256,9 +256,24 @@ impl Read for PackBitsReader { } } +/// Iterator adapter that reverses bits in each byte for FillOrder=2 (LSB-to-MSB). +#[cfg(feature = "fax")] +type FaxBytes = + core::iter::Map>>, fn(io::Result) -> io::Result>; + +#[cfg(feature = "fax")] +fn fax_byte_iter(reader: R, len: u64, fill_order: u16) -> FaxBytes { + let map_fn: fn(io::Result) -> io::Result = if fill_order == 2 { + |r| r.map(u8::reverse_bits) + } else { + |r| r + }; + io::BufReader::new(reader.take(len)).bytes().map(map_fn) +} + #[cfg(feature = "fax")] pub struct Group4Reader { - decoder: fax34::decoder::Group4Decoder>>>, + decoder: fax34::decoder::Group4Decoder>, line_buf: io::Cursor>, height: u32, width: u16, @@ -271,13 +286,14 @@ impl Group4Reader { dimensions: (u32, u32), reader: R, compressed_length: u64, + fill_order: u16, ) -> crate::TiffResult { let width = u16::try_from(dimensions.0)?; let height = dimensions.1; Ok(Self { decoder: fax34::decoder::Group4Decoder::new( - io::BufReader::new(reader.take(compressed_length)).bytes(), + fax_byte_iter(reader, compressed_length, fill_order), width, )?, line_buf: io::Cursor::new(Vec::with_capacity(width.into())), @@ -356,6 +372,141 @@ impl Read for Group4Reader { } } +/// Owning byte reader for Group 3 decoding: the compressed data is consumed +/// into an iterator so the reader doesn't borrow the buffer. +#[cfg(feature = "fax")] +type OwnedBitReader = fax34::ByteReader< + core::iter::Map, fn(u8) -> Result>, +>; + +/// Decodes CCITT Group 3 (T.4) 1D compressed data line by line. +/// +/// The fax crate's `Group3Decoder` has a bug where `new()` doesn't consume the +/// initial EOL marker, causing its lookup tables to destructively consume bits +/// from the EOL when trying to decode run-length codes. We work around this by +/// using the crate's lower-level `white::decode` / `black::decode` functions +/// and handling EOL markers ourselves. +#[cfg(feature = "fax")] +pub struct Group3Reader { + reader: OwnedBitReader, + line_buf: io::Cursor>, + height: u32, + width: u16, + y: u32, +} + +#[cfg(feature = "fax")] +impl Group3Reader { + pub fn new( + dimensions: (u32, u32), + reader: R, + compressed_length: u64, + fill_order: u16, + ) -> crate::TiffResult { + let width = u16::try_from(dimensions.0)?; + let height = dimensions.1; + + // Buffer all compressed data and apply FillOrder bit reversal + let mut compressed = vec![0u8; compressed_length as usize]; + reader.take(compressed_length).read_exact(&mut compressed)?; + if fill_order == 2 { + for b in &mut compressed { + *b = b.reverse_bits(); + } + } + + // Create an owning bit reader (Vec consumed by into_iter, no borrow issues) + let map_fn: fn(u8) -> Result = Ok; + let mut bit_reader = fax34::ByteReader::new(compressed.into_iter().map(map_fn)).unwrap(); + + // Skip fill bits + initial EOL marker + Self::skip_eol(&mut bit_reader); + + Ok(Self { + reader: bit_reader, + line_buf: io::Cursor::new(Vec::with_capacity(width.into())), + width, + height, + y: 0, + }) + } + + /// Skip zero fill bits followed by the terminating '1' bit of an EOL code. + fn skip_eol(reader: &mut impl fax34::BitReader) { + while reader.peek(1) == Some(0) { + let _ = reader.consume(1); + } + if reader.peek(1) == Some(1) { + let _ = reader.consume(1); + } + } + + /// Decode a full run-length value (terminal + optional makeup codes). + fn with_markup( + decoder: fn(&mut R) -> Option, + reader: &mut R, + ) -> Option { + let mut sum = 0u16; + while let Some(n) = decoder(reader) { + sum += n; + if n < 64 { + return Some(sum); + } + } + None + } +} + +#[cfg(feature = "fax")] +impl Read for Group3Reader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if self.line_buf.position() as usize == self.line_buf.get_ref().len() + && self.y < self.height + { + // Decode one line of alternating white/black run-lengths + let mut transitions = Vec::new(); + let mut a0 = 0u16; + let mut is_white = true; + loop { + let run = if is_white { + Self::with_markup(fax34::maps::white::decode, &mut self.reader) + } else { + Self::with_markup(fax34::maps::black::decode, &mut self.reader) + }; + match run { + Some(len) => { + a0 += len; + transitions.push(a0); + is_white = !is_white; + } + None => break, + } + } + + self.y += 1; + + // Convert transitions to packed bilevel output + let bytes_per_line = usize::from(self.width).div_ceil(8); + let buffer = self.line_buf.get_mut(); + buffer.clear(); + buffer.resize(bytes_per_line, 0u8); + + let pels = fax34::decoder::pels(&transitions, self.width); + for (i, c) in pels.enumerate() { + if c == fax34::Color::Black { + buffer[i / 8] |= 1 << (7 - (i % 8)); + } + } + self.line_buf.set_position(0); + + // Skip fill bits + line-ending EOL + Self::skip_eol(&mut self.reader); + } + + self.line_buf.read(buf) + } +} + #[cfg(feature = "webp")] pub struct WebPReader { inner: Cursor>, diff --git a/tests/decode_images.rs b/tests/decode_images.rs index 3575773..d6339a1 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -1,6 +1,7 @@ extern crate tiff; use tiff::decoder::{ifd, Decoder, DecodingResult}; +use tiff::tags::{ByteOrder, Type}; use tiff::ColorType; use std::fs::File; @@ -8,6 +9,7 @@ use std::io::{Read, Seek}; use std::path::PathBuf; const TEST_IMAGE_DIR: &str = "./tests/images/"; +const SEQ_IMAGE_DIR: &str = "./tests/images/seq/"; macro_rules! test_image_sum { ($name:ident, $buffer:ident, $sum_ty:ty) => { @@ -40,9 +42,198 @@ 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); +/// Compute a CRC32 hash of the full decoded TIFF output including metadata. +/// +/// Hashes: page count, and for each page: dimensions, color type, and decoded +/// sample data (in little-endian byte order for multi-byte types). This catches +/// wrong dimensions, wrong color type, byte-order errors, bit-alignment errors, +/// stride/padding errors, and pixel value errors. +fn compute_tiff_hash(reader: R) -> u32 { + let mut decoder = Decoder::open(reader).expect("Cannot create decoder"); + let mut hasher = crc32fast::Hasher::new(); + + let mut page = 0u32; + loop { + decoder.next_image().expect("Cannot read image IFD"); + let (width, height) = decoder.dimensions().unwrap(); + let color_type = decoder.colortype().unwrap(); + let mut result = decoder.read_image().unwrap(); + + // Hash page index + dimensions + hasher.update(&page.to_le_bytes()); + hasher.update(&width.to_le_bytes()); + hasher.update(&height.to_le_bytes()); + + // Hash color type (discriminant + fields) + 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:?}"), + } + + // Hash decoded data: TIFF type tag (u16 repr) + LE sample bytes. + // Map each variant to the corresponding TIFF Type for byte-order normalization. + let sample_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 + }; + hasher.update(&sample_type.to_u16().to_le_bytes()); + + let mut buf = result.as_buffer(0); + let bytes = buf.as_bytes_mut(); + ByteOrder::native().convert(sample_type, bytes, ByteOrder::LittleEndian); + hasher.update(bytes); + + page += 1; + if !decoder.more_images() { + break; + } + } + + // Hash total page count at the end + hasher.update(&page.to_le_bytes()); + hasher.finalize() +} + +/// Verify all TIFF files in tests/images/seq/ against their filename-embedded CRC32 hash. +/// +/// Each file is named `-<8hex>.tiff` where the last segment before `.tiff` +/// is the expected CRC32 hash of (dimensions, color type, decoded data) for all pages. +#[test] +fn verify_all_seq_images() { + let seq_dir = std::path::Path::new(SEQ_IMAGE_DIR); + if !seq_dir.is_dir() { + panic!("seq image directory not found: {}", seq_dir.display()); + } + + let mut files: Vec<_> = std::fs::read_dir(seq_dir) + .expect("Cannot read seq image directory") + .filter_map(|e| e.ok()) + .filter(|e| { + e.path() + .extension() + .is_some_and(|ext| ext == "tiff" || ext == "tif") + }) + .collect(); + files.sort_by_key(|e| e.file_name()); + + assert!( + !files.is_empty(), + "No .tiff files found in {}", + seq_dir.display() + ); + + let mut failures = Vec::new(); + for entry in &files { + let path = entry.path(); + let stem = path.file_stem().unwrap().to_str().unwrap(); + + // Extract expected hash: last '-'-delimited segment + let expected_hash = stem + .rsplit('-') + .next() + .expect("filename must contain a '-' before the hash"); + + // Validate it looks like an 8-char hex string + if expected_hash.len() != 8 || !expected_hash.chars().all(|c| c.is_ascii_hexdigit()) { + failures.push(format!( + "{}: filename does not end with 8-char hex hash (got '{}')", + stem, expected_hash + )); + continue; + } + + let file = File::open(&path).expect("Cannot open test image"); + let actual_hash = format!("{:08x}", compute_tiff_hash(file)); + + if actual_hash != expected_hash { + failures.push(format!( + "{}: expected hash {} but got {}", + stem, expected_hash, actual_hash + )); + } + } + + if !failures.is_empty() { + panic!( + "{} of {} seq images failed hash verification:\n {}", + failures.len(), + files.len(), + failures.join("\n ") + ); + } + + eprintln!("Verified {} seq images", files.len()); +} + +/// Helper: print the correct hash-suffixed filename for every *.tiff in tests/images/seq/. +/// Run with: cargo test print_seq_hashes -- --ignored --nocapture +#[test] +#[ignore] +fn print_seq_hashes() { + let dir = std::path::Path::new(SEQ_IMAGE_DIR); + let mut files: Vec<_> = std::fs::read_dir(dir) + .expect("Cannot read image directory") + .filter_map(|e| e.ok()) + .filter(|e| { + e.file_name() + .to_str() + .is_some_and(|n| n.starts_with("seq-") && n.ends_with(".tiff")) + }) + .collect(); + files.sort_by_key(|e| e.file_name()); + + for entry in &files { + let path = entry.path(); + let name = path.file_name().unwrap().to_str().unwrap(); + let file = File::open(&path).expect("Cannot open test image"); + let hash = format!("{:08x}", compute_tiff_hash(file)); + // Strip the old hash (last -XXXXXXXX before .tiff) if present + let stem = path.file_stem().unwrap().to_str().unwrap(); + let base = match stem.rsplit_once('-') { + Some((prefix, suffix)) + if suffix.len() == 8 && suffix.chars().all(|c| c.is_ascii_hexdigit()) => + { + prefix + } + _ => stem, + }; + let new_name = format!("{base}-{hash}.tiff"); + if new_name != name { + eprintln!("RENAME: {name} -> {new_name}"); + } else { + eprintln!("OK: {name}"); + } + } +} + /// Tests that a decoder can be constructed for an image and the color type /// read from the IFD and is of the appropriate type, but the type is /// unsupported. +#[allow(dead_code)] fn test_image_color_type_unsupported(file: &str, expected_type: ColorType) { let path = PathBuf::from(TEST_IMAGE_DIR).join(file); let img_file = File::open(path).expect("Cannot find test image!"); @@ -121,7 +312,7 @@ fn test_gray_u8() { #[test] fn test_gray_u12() { - test_image_color_type_unsupported("12bit.cropped.tiff", ColorType::Gray(12)); + test_image_sum_u16("12bit.cropped.tiff", ColorType::Gray(12), 10973); } #[test] @@ -160,7 +351,7 @@ fn test_rgb_u8() { #[test] fn test_rgb_u12() { - test_image_color_type_unsupported("12bit.cropped.rgb.tiff", ColorType::RGB(12)); + test_image_sum_u16("12bit.cropped.rgb.tiff", ColorType::RGB(12), 32919); } #[test] @@ -809,3 +1000,56 @@ fn test_decode_huge_g4() { panic!("Unexpected decoding result type"); } } + +// ---- WhiteIsZero tests across bit depths ---- + +// WhiteIsZero + extended bit depth (12-bit gray, synthetic 4x2 image) +#[test] +fn test_gray_u12_white_is_zero() { + // Values: [0, 1000, 2000, 4095, 100, 500, 3000, 3500] + // After WhiteIsZero inversion: [4095, 3095, 2095, 0, 3995, 3595, 1095, 595] + test_image_sum_u16("miniswhite-1c-12b.tiff", ColorType::Gray(12), 18565); +} + +// WhiteIsZero + non-standard sub-byte depths (3-bit and 6-bit, synthetic 8x2 images) +// These verify per-sample inversion for packed depths where samples cross byte boundaries. +#[test] +fn test_gray_u3_white_is_zero() { + test_image_sum_u8("miniswhite-1c-3b.tiff", ColorType::Gray(3), 765); +} + +#[test] +fn test_gray_u6_white_is_zero() { + // Values: [0, 10, 20, 30, 40, 50, 60, 63] × 2 rows + // After inversion: [63, 53, 43, 33, 23, 13, 3, 0] × 2 rows + test_image_sum_u8("miniswhite-1c-6b.tiff", ColorType::Gray(6), 2124); +} + +// ---- Non-standard sub-byte depths (BlackIsZero) ---- +// Verify that odd bit depths (3, 5, 7) decode without error. + +#[test] +fn test_gray_u3() { + // 8x2 image, values [0..7, 7..0], packed to 6 bytes + test_image_sum_u8("minisblack-1c-3b.tiff", ColorType::Gray(3), 765); +} + +#[test] +fn test_gray_u5() { + // 8x2 image, packed to 10 bytes + test_image_sum_u8("minisblack-1c-5b.tiff", ColorType::Gray(5), 1275); +} + +#[test] +fn test_gray_u7() { + // 8x2 image, packed to 14 bytes + test_image_sum_u8("minisblack-1c-7b.tiff", ColorType::Gray(7), 1711); +} + +// ---- Horizontal predictor + extended bit depth ---- + +#[test] +fn test_gray_u12_hpredict() { + // Delta-encoded 12-bit values: [100, 200, 300, 400, 1000, 2000, 3000, 4000] + test_image_sum_u16("hpredict-1c-12b.tiff", ColorType::Gray(12), 11000); +} diff --git a/tests/decode_libtiffpic.rs b/tests/decode_libtiffpic.rs index 180c5c0..2479fd7 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(0xc2f30c6f)), // 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(0x304b606b)), + ("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(0)), // LogLuv decoder output varies across platforms + ("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}"), + } + } +} diff --git a/tests/gen_seq_tiff.c b/tests/gen_seq_tiff.c new file mode 100644 index 0000000..89f5661 --- /dev/null +++ b/tests/gen_seq_tiff.c @@ -0,0 +1,1351 @@ +/* + * gen_seq_tiff.c — Generate sequential-value TIFF test images using libtiff. + * + * Each image is 16x16 with sample values cycling 0..max. + * Any single-bit decode error changes the pixel-value checksum. + * + * Usage: ./gen_seq_tiff + * + * Compile: cc -O2 -o gen_seq_tiff gen_seq_tiff.c -ltiff -lm + */ + +#include +#include +#include +#include +#include +#include + +#define W 16 +#define H 16 +#define NPIX (W * H) + +/* Pack a single sample value into an MSB-first bit stream at the given bit offset. */ +static void pack_value(uint8_t *buf, int *bit_offset, uint32_t val, int bits) +{ + /* Write bits MSB-first */ + for (int i = bits - 1; i >= 0; i--) { + int byte_idx = *bit_offset / 8; + int bit_in_byte = *bit_offset % 8; + if ((val >> i) & 1) + buf[byte_idx] |= (uint8_t)(0x80 >> bit_in_byte); + (*bit_offset)++; + } +} + +/* Write a single-channel grayscale TIFF with N-bit packed samples. + * libtiff expects packed MSB-first data for all non-standard bit depths. */ +static int write_gray(const char *path, int bits, + uint16_t photometric, uint16_t predictor) +{ + TIFF *tif = TIFFOpen(path, "w"); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, photometric); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + if (predictor != PREDICTOR_NONE) + TIFFSetField(tif, TIFFTAG_PREDICTOR, predictor); + + uint32_t max_val = (1u << bits) - 1; + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + int bit_offset = 0; + + for (int x = 0; x < W; x++) { + uint32_t val = (y * W + x) % (max_val + 1); + /* For WhiteIsZero, store inverted values (decoder will invert back) */ + if (photometric == PHOTOMETRIC_MINISWHITE) + val = max_val - val; + pack_value(buf, &bit_offset, val, bits); + } + + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); + TIFFClose(tif); + return -1; + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write a 3-channel RGB TIFF with N-bit packed samples, contiguous layout. */ +static int write_rgb_contig(const char *path, int bits) +{ + TIFF *tif = TIFFOpen(path, "w"); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + + uint32_t max_val = (1u << bits) - 1; + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + int bit_offset = 0; + + for (int x = 0; x < W; x++) { + for (int c = 0; c < 3; c++) { + uint32_t idx = (y * W + x) * 3 + c; + uint32_t val = idx % (max_val + 1); + pack_value(buf, &bit_offset, val, bits); + } + } + + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); + TIFFClose(tif); + return -1; + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write a 3-channel RGB TIFF with N-bit packed samples, planar (separate) layout. */ +static int write_rgb_planar(const char *path, int bits) +{ + TIFF *tif = TIFFOpen(path, "w"); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_SEPARATE); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + + uint32_t max_val = (1u << bits) - 1; + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int s = 0; s < 3; s++) { + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + int bit_offset = 0; + + for (int x = 0; x < W; x++) { + /* Plane 0: cycling 0..max, plane 1: +100, plane 2: +200 */ + uint32_t val = ((y * W + x) + s * 100) % (max_val + 1); + pack_value(buf, &bit_offset, val, bits); + } + + if (TIFFWriteScanline(tif, buf, y, (uint16_t)s) < 0) { + fprintf(stderr, "Error writing scanline %d plane %d to %s\n", + y, s, path); + _TIFFfree(buf); + TIFFClose(tif); + return -1; + } + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write a standard-depth grayscale TIFF with specified compression. + * Works for 8-bit and 16-bit depths using native sample storage. */ +static int write_gray_compressed(const char *path, int bits, + uint16_t compression, uint16_t predictor, + const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); + TIFFSetField(tif, TIFFTAG_COMPRESSION, compression); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + if (predictor != PREDICTOR_NONE) + TIFFSetField(tif, TIFFTAG_PREDICTOR, predictor); + + uint32_t max_val = (bits < 32) ? ((1u << bits) - 1) : 0xFFFFFFFFu; + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + for (int x = 0; x < W; x++) { + uint32_t val = (y * W + x) % (max_val + 1); + if (bits == 8) { + buf[x] = (uint8_t)val; + } else if (bits == 16) { + ((uint16_t *)buf)[x] = (uint16_t)val; + } else if (bits == 32) { + ((uint32_t *)buf)[x] = val; + } + } + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); TIFFClose(tif); return -1; + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write a standard-depth RGB TIFF with specified compression. */ +static int write_rgb_compressed(const char *path, int bits, + uint16_t compression, const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + TIFFSetField(tif, TIFFTAG_COMPRESSION, compression); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + + uint32_t max_val = (bits < 32) ? ((1u << bits) - 1) : 0xFFFFFFFFu; + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + for (int x = 0; x < W; x++) { + for (int c = 0; c < 3; c++) { + uint32_t idx = (y * W + x) * 3 + c; + uint32_t val = idx % (max_val + 1); + if (bits == 8) { + buf[x * 3 + c] = (uint8_t)val; + } else if (bits == 16) { + ((uint16_t *)buf)[x * 3 + c] = (uint16_t)val; + } + } + } + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); TIFFClose(tif); return -1; + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write a tiled grayscale TIFF. Uses 8x8 tiles. */ +static int write_gray_tiled(const char *path, int bits, const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + int tw = 16, th = 16; + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_TILEWIDTH, tw); + TIFFSetField(tif, TIFFTAG_TILELENGTH, th); + + uint32_t max_val = (bits < 32) ? ((1u << bits) - 1) : 0xFFFFFFFFu; + tsize_t tile_size = TIFFTileSize(tif); + uint8_t *tile_buf = (uint8_t *)_TIFFmalloc(tile_size); + if (!tile_buf) { TIFFClose(tif); return -1; } + + for (int ty = 0; ty < H; ty += th) { + for (int tx = 0; tx < W; tx += tw) { + memset(tile_buf, 0, tile_size); + for (int row = 0; row < th; row++) { + for (int col = 0; col < tw; col++) { + int y = ty + row, x = tx + col; + uint32_t val = (y * W + x) % (max_val + 1); + if (bits == 8) { + tile_buf[row * tw + col] = (uint8_t)val; + } else if (bits == 16) { + ((uint16_t *)tile_buf)[row * tw + col] = (uint16_t)val; + } + } + } + if (TIFFWriteTile(tif, tile_buf, tx, ty, 0, 0) < 0) { + fprintf(stderr, "Error writing tile at (%d,%d) to %s\n", tx, ty, path); + _TIFFfree(tile_buf); TIFFClose(tif); return -1; + } + } + } + + _TIFFfree(tile_buf); + TIFFClose(tif); + return 0; +} + +/* Write a tiled RGB TIFF. Uses 8x8 tiles. */ +static int write_rgb_tiled(const char *path, int bits, const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + int tw = 16, th = 16; + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_TILEWIDTH, tw); + TIFFSetField(tif, TIFFTAG_TILELENGTH, th); + + uint32_t max_val = (bits < 32) ? ((1u << bits) - 1) : 0xFFFFFFFFu; + tsize_t tile_size = TIFFTileSize(tif); + uint8_t *tile_buf = (uint8_t *)_TIFFmalloc(tile_size); + if (!tile_buf) { TIFFClose(tif); return -1; } + + for (int ty = 0; ty < H; ty += th) { + for (int tx = 0; tx < W; tx += tw) { + memset(tile_buf, 0, tile_size); + for (int row = 0; row < th; row++) { + for (int col = 0; col < tw; col++) { + int y = ty + row, x = tx + col; + for (int c = 0; c < 3; c++) { + uint32_t idx = (y * W + x) * 3 + c; + uint32_t val = idx % (max_val + 1); + if (bits == 8) { + tile_buf[(row * tw + col) * 3 + c] = (uint8_t)val; + } else if (bits == 16) { + ((uint16_t *)tile_buf)[(row * tw + col) * 3 + c] = (uint16_t)val; + } + } + } + } + if (TIFFWriteTile(tif, tile_buf, tx, ty, 0, 0) < 0) { + fprintf(stderr, "Error writing tile at (%d,%d) to %s\n", tx, ty, path); + _TIFFfree(tile_buf); TIFFClose(tif); return -1; + } + } + } + + _TIFFfree(tile_buf); + TIFFClose(tif); + return 0; +} + +/* Write a grayscale float TIFF (32-bit or 64-bit). Values are (y*W+x)/255.0 */ +static int write_gray_float(const char *path, int float_bits, const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, float_bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + for (int x = 0; x < W; x++) { + double val = (double)(y * W + x) / 255.0; + if (float_bits == 32) { + ((float *)buf)[x] = (float)val; + } else { + ((double *)buf)[x] = val; + } + } + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); TIFFClose(tif); return -1; + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write an RGB float TIFF (32-bit). Values are idx/767.0 where idx = (y*W+x)*3+c */ +static int write_rgb_float(const char *path, int float_bits, const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, float_bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + for (int x = 0; x < W; x++) { + for (int c = 0; c < 3; c++) { + double val = (double)((y * W + x) * 3 + c) / 767.0; + if (float_bits == 32) { + ((float *)buf)[x * 3 + c] = (float)val; + } else { + ((double *)buf)[x * 3 + c] = val; + } + } + } + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); TIFFClose(tif); return -1; + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write a signed integer grayscale TIFF. Values cycle -128..127 for 8-bit, + * -32768..32767 for 16-bit. */ +static int write_gray_signed(const char *path, int bits, const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_INT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + for (int x = 0; x < W; x++) { + int32_t pixel_idx = y * W + x; + if (bits == 8) { + /* Cycle -128..127: signed_val = (pixel_idx % 256) - 128 */ + int8_t val = (int8_t)((pixel_idx % 256) - 128); + ((int8_t *)buf)[x] = val; + } else if (bits == 16) { + /* Same pattern but 16-bit signed: cycle through -128..127 for simplicity */ + int16_t val = (int16_t)((pixel_idx % 256) - 128); + ((int16_t *)buf)[x] = val; + } else if (bits == 32) { + int32_t val = (int32_t)((pixel_idx % 256) - 128); + ((int32_t *)buf)[x] = val; + } + } + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); TIFFClose(tif); return -1; + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write a 4-channel CMYK TIFF. Values cycle 0..max per channel. */ +static int write_cmyk(const char *path, int bits, const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 4); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_SEPARATED); + TIFFSetField(tif, TIFFTAG_INKSET, INKSET_CMYK); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + + uint32_t max_val = (bits < 32) ? ((1u << bits) - 1) : 0xFFFFFFFFu; + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + for (int x = 0; x < W; x++) { + for (int c = 0; c < 4; c++) { + uint32_t idx = (y * W + x) * 4 + c; + uint32_t val = idx % (max_val + 1); + if (bits == 8) { + buf[x * 4 + c] = (uint8_t)val; + } else if (bits == 16) { + ((uint16_t *)buf)[x * 4 + c] = (uint16_t)val; + } + } + } + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); TIFFClose(tif); return -1; + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write a palette (indexed color) TIFF with 8-bit indices and a known colormap. + * Colormap: R[i] = i*257, G[i] = (255-i)*257, B[i] = (i*37 % 256)*257 + * (multiplied by 257 for 16-bit colormap entries per TIFF spec). */ +static int write_palette(const char *path, int bits, const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + int ncolors = 1 << bits; + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_PALETTE); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + + /* Build colormap — 3 arrays of ncolors uint16 entries */ + uint16_t *r = (uint16_t *)calloc(ncolors, sizeof(uint16_t)); + uint16_t *g = (uint16_t *)calloc(ncolors, sizeof(uint16_t)); + uint16_t *b = (uint16_t *)calloc(ncolors, sizeof(uint16_t)); + if (!r || !g || !b) { + free(r); free(g); free(b); TIFFClose(tif); return -1; + } + for (int i = 0; i < ncolors; i++) { + r[i] = (uint16_t)((i % 256) * 257); + g[i] = (uint16_t)(((255 - (i % 256))) * 257); + b[i] = (uint16_t)(((i * 37) % 256) * 257); + } + TIFFSetField(tif, TIFFTAG_COLORMAP, r, g, b); + + /* Indices cycle 0..ncolors-1 */ + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { free(r); free(g); free(b); TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + if (bits == 8) { + for (int x = 0; x < W; x++) + buf[x] = (uint8_t)((y * W + x) % ncolors); + } else if (bits == 4) { + /* Pack two 4-bit indices per byte, MSB first */ + for (int x = 0; x < W; x++) { + uint8_t idx = (uint8_t)((y * W + x) % ncolors); + if (x % 2 == 0) + buf[x / 2] = (uint8_t)(idx << 4); + else + buf[x / 2] |= idx; + } + } else if (bits == 1) { + /* Pack 1-bit indices */ + int bit_offset = 0; + for (int x = 0; x < W; x++) { + uint32_t idx = (y * W + x) % ncolors; + pack_value(buf, &bit_offset, idx, 1); + } + } + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); free(r); free(g); free(b); TIFFClose(tif); return -1; + } + } + + _TIFFfree(buf); + free(r); free(g); free(b); + TIFFClose(tif); + return 0; +} + +/* Write an RGBA TIFF with associated alpha (pre-multiplied). */ +static int write_rgba(const char *path, int bits, const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + uint16_t extra_samples[] = { EXTRASAMPLE_ASSOCALPHA }; + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 4); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + TIFFSetField(tif, TIFFTAG_EXTRASAMPLES, 1, extra_samples); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + + uint32_t max_val = (bits < 32) ? ((1u << bits) - 1) : 0xFFFFFFFFu; + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + for (int x = 0; x < W; x++) { + for (int c = 0; c < 4; c++) { + uint32_t idx = (y * W + x) * 4 + c; + uint32_t val = idx % (max_val + 1); + if (bits == 8) { + buf[x * 4 + c] = (uint8_t)val; + } else if (bits == 16) { + ((uint16_t *)buf)[x * 4 + c] = (uint16_t)val; + } + } + } + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); TIFFClose(tif); return -1; + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write a standard-depth grayscale TIFF with configurable RowsPerStrip (multi-strip). */ +static int write_gray_multistrip(const char *path, int bits, int rows_per_strip) +{ + TIFF *tif = TIFFOpen(path, "w"); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, rows_per_strip); + + uint32_t max_val = (bits < 32) ? ((1u << bits) - 1) : 0xFFFFFFFFu; + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + for (int x = 0; x < W; x++) { + uint32_t val = (y * W + x) % (max_val + 1); + if (bits == 8) + buf[x] = (uint8_t)val; + else if (bits == 16) + ((uint16_t *)buf)[x] = (uint16_t)val; + } + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); TIFFClose(tif); return -1; + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write a standard-depth RGB TIFF with configurable RowsPerStrip. */ +static int write_rgb_multistrip(const char *path, int bits, int rows_per_strip) +{ + TIFF *tif = TIFFOpen(path, "w"); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, rows_per_strip); + + uint32_t max_val = (bits < 32) ? ((1u << bits) - 1) : 0xFFFFFFFFu; + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + for (int x = 0; x < W; x++) { + for (int c = 0; c < 3; c++) { + uint32_t idx = (y * W + x) * 3 + c; + uint32_t val = idx % (max_val + 1); + if (bits == 8) + buf[x * 3 + c] = (uint8_t)val; + else if (bits == 16) + ((uint16_t *)buf)[x * 3 + c] = (uint16_t)val; + } + } + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); TIFFClose(tif); return -1; + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write a tiled grayscale TIFF with compression. */ +static int write_gray_tiled_comp(const char *path, int bits, + uint16_t compression, const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + int tw = 16, th = 16; + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); + TIFFSetField(tif, TIFFTAG_COMPRESSION, compression); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_TILEWIDTH, tw); + TIFFSetField(tif, TIFFTAG_TILELENGTH, th); + + uint32_t max_val = (bits < 32) ? ((1u << bits) - 1) : 0xFFFFFFFFu; + tsize_t tile_size = TIFFTileSize(tif); + uint8_t *tile_buf = (uint8_t *)_TIFFmalloc(tile_size); + if (!tile_buf) { TIFFClose(tif); return -1; } + + for (int ty = 0; ty < H; ty += th) { + for (int tx = 0; tx < W; tx += tw) { + memset(tile_buf, 0, tile_size); + for (int row = 0; row < th; row++) { + for (int col = 0; col < tw; col++) { + int y = ty + row, x = tx + col; + uint32_t val = (y * W + x) % (max_val + 1); + if (bits == 8) + tile_buf[row * tw + col] = (uint8_t)val; + else if (bits == 16) + ((uint16_t *)tile_buf)[row * tw + col] = (uint16_t)val; + } + } + if (TIFFWriteTile(tif, tile_buf, tx, ty, 0, 0) < 0) { + fprintf(stderr, "Error writing tile at (%d,%d) to %s\n", tx, ty, path); + _TIFFfree(tile_buf); TIFFClose(tif); return -1; + } + } + } + + _TIFFfree(tile_buf); + TIFFClose(tif); + return 0; +} + +/* Write a grayscale float TIFF with compression + predictor. + * Values are (y*W+x)/255.0 */ +static int write_gray_float_comp(const char *path, int float_bits, + uint16_t compression, uint16_t predictor, + const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, float_bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); + TIFFSetField(tif, TIFFTAG_COMPRESSION, compression); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + if (predictor != PREDICTOR_NONE) + TIFFSetField(tif, TIFFTAG_PREDICTOR, predictor); + + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + for (int x = 0; x < W; x++) { + double val = (double)(y * W + x) / 255.0; + if (float_bits == 32) + ((float *)buf)[x] = (float)val; + else + ((double *)buf)[x] = val; + } + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); TIFFClose(tif); return -1; + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write a signed integer RGB TIFF. Values cycle -128..127 per channel. */ +static int write_rgb_signed(const char *path, int bits, const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_INT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + for (int x = 0; x < W; x++) { + for (int c = 0; c < 3; c++) { + int32_t idx = (y * W + x) * 3 + c; + if (bits == 8) { + int8_t val = (int8_t)((idx % 256) - 128); + ((int8_t *)buf)[x * 3 + c] = val; + } else if (bits == 16) { + int16_t val = (int16_t)((idx % 256) - 128); + ((int16_t *)buf)[x * 3 + c] = val; + } + } + } + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d to %s\n", y, path); + _TIFFfree(buf); TIFFClose(tif); return -1; + } + } + + _TIFFfree(buf); + TIFFClose(tif); + return 0; +} + +/* Write a multi-page TIFF with 3 gray pages. Page n has values offset by n*50. */ +static int write_multipage(const char *path, int bits, const char *mode) +{ + TIFF *tif = TIFFOpen(path, mode); + if (!tif) { fprintf(stderr, "Cannot open %s\n", path); return -1; } + + uint32_t max_val = (bits < 32) ? ((1u << bits) - 1) : 0xFFFFFFFFu; + int npages = 3; + + for (int page = 0; page < npages; page++) { + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + TIFFSetField(tif, TIFFTAG_SUBFILETYPE, FILETYPE_PAGE); + TIFFSetField(tif, TIFFTAG_PAGENUMBER, (uint16_t)page, (uint16_t)npages); + + tsize_t scanline_size = TIFFScanlineSize(tif); + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (int y = 0; y < H; y++) { + memset(buf, 0, scanline_size); + for (int x = 0; x < W; x++) { + uint32_t val = ((y * W + x) + page * 50) % (max_val + 1); + if (bits == 8) { + buf[x] = (uint8_t)val; + } else if (bits == 16) { + ((uint16_t *)buf)[x] = (uint16_t)val; + } + } + if (TIFFWriteScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, "Error writing scanline %d page %d to %s\n", y, page, path); + _TIFFfree(buf); TIFFClose(tif); return -1; + } + } + _TIFFfree(buf); + + if (!TIFFWriteDirectory(tif)) { + fprintf(stderr, "Error writing directory page %d to %s\n", page, path); + TIFFClose(tif); return -1; + } + } + + TIFFClose(tif); + return 0; +} + +int main(int argc, char **argv) +{ + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + const char *dir = argv[1]; + char path[512]; + int ok = 1; + + printf("Generating libtiff test images in %s/\n\n", dir); + + /* Sub-byte BlackIsZero */ + int subbyte_biz[] = {3, 5, 7}; + for (int i = 0; i < 3; i++) { + int b = subbyte_biz[i]; + snprintf(path, sizeof(path), "%s/seq-1c-%db.tiff", dir, b); + if (write_gray(path, b, PHOTOMETRIC_MINISBLACK, PREDICTOR_NONE) == 0) + printf(" OK seq-1c-%db.tiff\n", b); + else { printf(" FAIL seq-1c-%db.tiff\n", b); ok = 0; } + } + + /* Sub-byte WhiteIsZero */ + int subbyte_wiz[] = {3, 5, 6, 7}; + for (int i = 0; i < 4; i++) { + int b = subbyte_wiz[i]; + snprintf(path, sizeof(path), "%s/seq-1c-%db-miniswhite.tiff", dir, b); + if (write_gray(path, b, PHOTOMETRIC_MINISWHITE, PREDICTOR_NONE) == 0) + printf(" OK seq-1c-%db-miniswhite.tiff\n", b); + else { printf(" FAIL seq-1c-%db-miniswhite.tiff\n", b); ok = 0; } + } + + /* Extended BlackIsZero */ + int ext_bits[] = {10, 12, 14, 24}; + for (int i = 0; i < 4; i++) { + int b = ext_bits[i]; + snprintf(path, sizeof(path), "%s/seq-1c-%db.tiff", dir, b); + if (write_gray(path, b, PHOTOMETRIC_MINISBLACK, PREDICTOR_NONE) == 0) + printf(" OK seq-1c-%db.tiff\n", b); + else { printf(" FAIL seq-1c-%db.tiff\n", b); ok = 0; } + } + + /* Extended WhiteIsZero */ + for (int i = 0; i < 4; i++) { + int b = ext_bits[i]; + snprintf(path, sizeof(path), "%s/seq-1c-%db-miniswhite.tiff", dir, b); + if (write_gray(path, b, PHOTOMETRIC_MINISWHITE, PREDICTOR_NONE) == 0) + printf(" OK seq-1c-%db-miniswhite.tiff\n", b); + else { printf(" FAIL seq-1c-%db-miniswhite.tiff\n", b); ok = 0; } + } + + /* Extended HPredict — libtiff may not support predictor for packed non-standard depths. + * Try it and report failure gracefully. */ + for (int i = 0; i < 4; i++) { + int b = ext_bits[i]; + snprintf(path, sizeof(path), "%s/seq-1c-%db-hpredict.tiff", dir, b); + if (write_gray(path, b, PHOTOMETRIC_MINISBLACK, PREDICTOR_HORIZONTAL) == 0) + printf(" OK seq-1c-%db-hpredict.tiff\n", b); + else { printf(" FAIL seq-1c-%db-hpredict.tiff (may not be supported)\n", b); } + } + + /* RGB Contiguous */ + for (int i = 0; i < 4; i++) { + int b = ext_bits[i]; + snprintf(path, sizeof(path), "%s/seq-3c-%db-contig.tiff", dir, b); + if (write_rgb_contig(path, b) == 0) + printf(" OK seq-3c-%db-contig.tiff\n", b); + else { printf(" FAIL seq-3c-%db-contig.tiff\n", b); ok = 0; } + } + + /* RGB Planar */ + for (int i = 0; i < 4; i++) { + int b = ext_bits[i]; + snprintf(path, sizeof(path), "%s/seq-3c-%db-planar.tiff", dir, b); + if (write_rgb_planar(path, b) == 0) + printf(" OK seq-3c-%db-planar.tiff\n", b); + else { printf(" FAIL seq-3c-%db-planar.tiff\n", b); ok = 0; } + } + + printf("\n--- Standard depths with compression ---\n"); + + /* Gray 8-bit: LZW, Deflate, PackBits */ + { + struct { const char *suffix; uint16_t comp; } comps[] = { + {"lzw", COMPRESSION_LZW}, + {"deflate", COMPRESSION_DEFLATE}, + {"packbits", COMPRESSION_PACKBITS}, + }; + for (int i = 0; i < 3; i++) { + snprintf(path, sizeof(path), "%s/seq-1c-8b-%s.tiff", dir, comps[i].suffix); + if (write_gray_compressed(path, 8, comps[i].comp, PREDICTOR_NONE, "w") == 0) + printf(" OK seq-1c-8b-%s.tiff\n", comps[i].suffix); + else { printf(" FAIL seq-1c-8b-%s.tiff\n", comps[i].suffix); ok = 0; } + } + } + + /* Gray 16-bit: LZW, Deflate */ + { + struct { const char *suffix; uint16_t comp; } comps[] = { + {"lzw", COMPRESSION_LZW}, + {"deflate", COMPRESSION_DEFLATE}, + }; + for (int i = 0; i < 2; i++) { + snprintf(path, sizeof(path), "%s/seq-1c-16b-%s.tiff", dir, comps[i].suffix); + if (write_gray_compressed(path, 16, comps[i].comp, PREDICTOR_NONE, "w") == 0) + printf(" OK seq-1c-16b-%s.tiff\n", comps[i].suffix); + else { printf(" FAIL seq-1c-16b-%s.tiff\n", comps[i].suffix); ok = 0; } + } + } + + /* Gray 8-bit LZW with horizontal predictor */ + snprintf(path, sizeof(path), "%s/seq-1c-8b-lzw-hpredict.tiff", dir); + if (write_gray_compressed(path, 8, COMPRESSION_LZW, PREDICTOR_HORIZONTAL, "w") == 0) + printf(" OK seq-1c-8b-lzw-hpredict.tiff\n"); + else { printf(" FAIL seq-1c-8b-lzw-hpredict.tiff\n"); ok = 0; } + + /* RGB 8-bit: LZW */ + snprintf(path, sizeof(path), "%s/seq-3c-8b-lzw.tiff", dir); + if (write_rgb_compressed(path, 8, COMPRESSION_LZW, "w") == 0) + printf(" OK seq-3c-8b-lzw.tiff\n"); + else { printf(" FAIL seq-3c-8b-lzw.tiff\n"); ok = 0; } + + printf("\n--- Tiled images ---\n"); + + /* Gray tiled: 8-bit and 16-bit */ + snprintf(path, sizeof(path), "%s/seq-1c-8b-tiled.tiff", dir); + if (write_gray_tiled(path, 8, "w") == 0) + printf(" OK seq-1c-8b-tiled.tiff\n"); + else { printf(" FAIL seq-1c-8b-tiled.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-1c-16b-tiled.tiff", dir); + if (write_gray_tiled(path, 16, "w") == 0) + printf(" OK seq-1c-16b-tiled.tiff\n"); + else { printf(" FAIL seq-1c-16b-tiled.tiff\n"); ok = 0; } + + /* RGB tiled: 8-bit */ + snprintf(path, sizeof(path), "%s/seq-3c-8b-tiled.tiff", dir); + if (write_rgb_tiled(path, 8, "w") == 0) + printf(" OK seq-3c-8b-tiled.tiff\n"); + else { printf(" FAIL seq-3c-8b-tiled.tiff\n"); ok = 0; } + + printf("\n--- BigTIFF ---\n"); + + /* BigTIFF: gray 8-bit and RGB 16-bit */ + snprintf(path, sizeof(path), "%s/seq-1c-8b-bigtiff.tiff", dir); + if (write_gray_compressed(path, 8, COMPRESSION_NONE, PREDICTOR_NONE, "w8") == 0) + printf(" OK seq-1c-8b-bigtiff.tiff\n"); + else { printf(" FAIL seq-1c-8b-bigtiff.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-3c-16b-bigtiff.tiff", dir); + if (write_rgb_compressed(path, 16, COMPRESSION_NONE, "w8") == 0) + printf(" OK seq-3c-16b-bigtiff.tiff\n"); + else { printf(" FAIL seq-3c-16b-bigtiff.tiff\n"); ok = 0; } + + printf("\n--- Float samples ---\n"); + + /* Gray float: 32-bit and 64-bit */ + snprintf(path, sizeof(path), "%s/seq-1c-32f.tiff", dir); + if (write_gray_float(path, 32, "w") == 0) + printf(" OK seq-1c-32f.tiff\n"); + else { printf(" FAIL seq-1c-32f.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-1c-64f.tiff", dir); + if (write_gray_float(path, 64, "w") == 0) + printf(" OK seq-1c-64f.tiff\n"); + else { printf(" FAIL seq-1c-64f.tiff\n"); ok = 0; } + + /* RGB float: 32-bit */ + snprintf(path, sizeof(path), "%s/seq-3c-32f.tiff", dir); + if (write_rgb_float(path, 32, "w") == 0) + printf(" OK seq-3c-32f.tiff\n"); + else { printf(" FAIL seq-3c-32f.tiff\n"); ok = 0; } + + printf("\n--- Signed integer ---\n"); + + /* Signed gray: 8-bit, 16-bit, 32-bit */ + snprintf(path, sizeof(path), "%s/seq-1c-i8.tiff", dir); + if (write_gray_signed(path, 8, "w") == 0) + printf(" OK seq-1c-i8.tiff\n"); + else { printf(" FAIL seq-1c-i8.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-1c-i16.tiff", dir); + if (write_gray_signed(path, 16, "w") == 0) + printf(" OK seq-1c-i16.tiff\n"); + else { printf(" FAIL seq-1c-i16.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-1c-i32.tiff", dir); + if (write_gray_signed(path, 32, "w") == 0) + printf(" OK seq-1c-i32.tiff\n"); + else { printf(" FAIL seq-1c-i32.tiff\n"); ok = 0; } + + printf("\n--- CMYK ---\n"); + + snprintf(path, sizeof(path), "%s/seq-4c-8b-cmyk.tiff", dir); + if (write_cmyk(path, 8, "w") == 0) + printf(" OK seq-4c-8b-cmyk.tiff\n"); + else { printf(" FAIL seq-4c-8b-cmyk.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-4c-16b-cmyk.tiff", dir); + if (write_cmyk(path, 16, "w") == 0) + printf(" OK seq-4c-16b-cmyk.tiff\n"); + else { printf(" FAIL seq-4c-16b-cmyk.tiff\n"); ok = 0; } + + printf("\n--- Palette (indexed) ---\n"); + + snprintf(path, sizeof(path), "%s/seq-1c-8b-palette.tiff", dir); + if (write_palette(path, 8, "w") == 0) + printf(" OK seq-1c-8b-palette.tiff\n"); + else { printf(" FAIL seq-1c-8b-palette.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-1c-4b-palette.tiff", dir); + if (write_palette(path, 4, "w") == 0) + printf(" OK seq-1c-4b-palette.tiff\n"); + else { printf(" FAIL seq-1c-4b-palette.tiff\n"); ok = 0; } + + printf("\n--- RGBA (associated alpha) ---\n"); + + snprintf(path, sizeof(path), "%s/seq-4c-8b-rgba.tiff", dir); + if (write_rgba(path, 8, "w") == 0) + printf(" OK seq-4c-8b-rgba.tiff\n"); + else { printf(" FAIL seq-4c-8b-rgba.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-4c-16b-rgba.tiff", dir); + if (write_rgba(path, 16, "w") == 0) + printf(" OK seq-4c-16b-rgba.tiff\n"); + else { printf(" FAIL seq-4c-16b-rgba.tiff\n"); ok = 0; } + + printf("\n--- Multi-page ---\n"); + + snprintf(path, sizeof(path), "%s/seq-1c-8b-multipage.tiff", dir); + if (write_multipage(path, 8, "w") == 0) + printf(" OK seq-1c-8b-multipage.tiff\n"); + else { printf(" FAIL seq-1c-8b-multipage.tiff\n"); ok = 0; } + + printf("\n--- Big-endian ---\n"); + + snprintf(path, sizeof(path), "%s/seq-1c-8b-bigendian.tiff", dir); + if (write_gray_compressed(path, 8, COMPRESSION_NONE, PREDICTOR_NONE, "wb") == 0) + printf(" OK seq-1c-8b-bigendian.tiff\n"); + else { printf(" FAIL seq-1c-8b-bigendian.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-1c-16b-bigendian.tiff", dir); + if (write_gray_compressed(path, 16, COMPRESSION_NONE, PREDICTOR_NONE, "wb") == 0) + printf(" OK seq-1c-16b-bigendian.tiff\n"); + else { printf(" FAIL seq-1c-16b-bigendian.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-3c-8b-bigendian.tiff", dir); + if (write_rgb_compressed(path, 8, COMPRESSION_NONE, "wb") == 0) + printf(" OK seq-3c-8b-bigendian.tiff\n"); + else { printf(" FAIL seq-3c-8b-bigendian.tiff\n"); ok = 0; } + + printf("\n--- Standard sub-byte depths ---\n"); + + /* 1-bit, 2-bit, 4-bit BlackIsZero */ + { + int stdbits[] = {1, 2, 4}; + for (int i = 0; i < 3; i++) { + int b = stdbits[i]; + snprintf(path, sizeof(path), "%s/seq-1c-%db.tiff", dir, b); + if (write_gray(path, b, PHOTOMETRIC_MINISBLACK, PREDICTOR_NONE) == 0) + printf(" OK seq-1c-%db.tiff\n", b); + else { printf(" FAIL seq-1c-%db.tiff\n", b); ok = 0; } + } + } + + /* 1-bit, 4-bit WhiteIsZero */ + { + int wbits[] = {1, 4}; + for (int i = 0; i < 2; i++) { + int b = wbits[i]; + snprintf(path, sizeof(path), "%s/seq-1c-%db-miniswhite.tiff", dir, b); + if (write_gray(path, b, PHOTOMETRIC_MINISWHITE, PREDICTOR_NONE) == 0) + printf(" OK seq-1c-%db-miniswhite.tiff\n", b); + else { printf(" FAIL seq-1c-%db-miniswhite.tiff\n", b); ok = 0; } + } + } + + printf("\n--- Multi-strip (RowsPerStrip=4) ---\n"); + + snprintf(path, sizeof(path), "%s/seq-1c-8b-multistrip.tiff", dir); + if (write_gray_multistrip(path, 8, 4) == 0) + printf(" OK seq-1c-8b-multistrip.tiff\n"); + else { printf(" FAIL seq-1c-8b-multistrip.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-1c-16b-multistrip.tiff", dir); + if (write_gray_multistrip(path, 16, 4) == 0) + printf(" OK seq-1c-16b-multistrip.tiff\n"); + else { printf(" FAIL seq-1c-16b-multistrip.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-3c-8b-multistrip.tiff", dir); + if (write_rgb_multistrip(path, 8, 4) == 0) + printf(" OK seq-3c-8b-multistrip.tiff\n"); + else { printf(" FAIL seq-3c-8b-multistrip.tiff\n"); ok = 0; } + + printf("\n--- Tiled + compressed ---\n"); + + snprintf(path, sizeof(path), "%s/seq-1c-8b-tiled-lzw.tiff", dir); + if (write_gray_tiled_comp(path, 8, COMPRESSION_LZW, "w") == 0) + printf(" OK seq-1c-8b-tiled-lzw.tiff\n"); + else { printf(" FAIL seq-1c-8b-tiled-lzw.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-1c-8b-tiled-deflate.tiff", dir); + if (write_gray_tiled_comp(path, 8, COMPRESSION_DEFLATE, "w") == 0) + printf(" OK seq-1c-8b-tiled-deflate.tiff\n"); + else { printf(" FAIL seq-1c-8b-tiled-deflate.tiff\n"); ok = 0; } + + printf("\n--- Float + predictor ---\n"); + + snprintf(path, sizeof(path), "%s/seq-1c-32f-deflate-fpredict.tiff", dir); + if (write_gray_float_comp(path, 32, COMPRESSION_DEFLATE, PREDICTOR_FLOATINGPOINT, "w") == 0) + printf(" OK seq-1c-32f-deflate-fpredict.tiff\n"); + else { printf(" FAIL seq-1c-32f-deflate-fpredict.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-1c-64f-deflate-fpredict.tiff", dir); + if (write_gray_float_comp(path, 64, COMPRESSION_DEFLATE, PREDICTOR_FLOATINGPOINT, "w") == 0) + printf(" OK seq-1c-64f-deflate-fpredict.tiff\n"); + else { printf(" FAIL seq-1c-64f-deflate-fpredict.tiff\n"); ok = 0; } + + printf("\n--- Unassociated alpha ---\n"); + + /* Reuse write_rgba but with unassociated alpha */ + { + snprintf(path, sizeof(path), "%s/seq-4c-8b-rgba-unassoc.tiff", dir); + TIFF *tif = TIFFOpen(path, "w"); + if (tif) { + uint16_t extra[] = { EXTRASAMPLE_UNASSALPHA }; + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, W); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, H); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 4); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 8); + TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + TIFFSetField(tif, TIFFTAG_EXTRASAMPLES, 1, extra); + TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, H); + tsize_t sl = TIFFScanlineSize(tif); + uint8_t *b2 = (uint8_t *)_TIFFmalloc(sl); + int good = 1; + if (b2) { + for (int y = 0; y < H && good; y++) { + memset(b2, 0, sl); + for (int x = 0; x < W; x++) + for (int c = 0; c < 4; c++) { + uint32_t idx = (y * W + x) * 4 + c; + b2[x * 4 + c] = (uint8_t)(idx % 256); + } + if (TIFFWriteScanline(tif, b2, y, 0) < 0) good = 0; + } + _TIFFfree(b2); + } else good = 0; + TIFFClose(tif); + if (good) printf(" OK seq-4c-8b-rgba-unassoc.tiff\n"); + else { printf(" FAIL seq-4c-8b-rgba-unassoc.tiff\n"); ok = 0; } + } else { printf(" FAIL seq-4c-8b-rgba-unassoc.tiff\n"); ok = 0; } + } + + printf("\n--- Sub-byte RGB ---\n"); + + /* 5-bit and 7-bit RGB contiguous */ + { + int rgb_sub[] = {5, 7}; + for (int i = 0; i < 2; i++) { + int b = rgb_sub[i]; + snprintf(path, sizeof(path), "%s/seq-3c-%db-contig.tiff", dir, b); + if (write_rgb_contig(path, b) == 0) + printf(" OK seq-3c-%db-contig.tiff\n", b); + else { printf(" FAIL seq-3c-%db-contig.tiff\n", b); ok = 0; } + } + } + + printf("\n--- Signed RGB ---\n"); + + snprintf(path, sizeof(path), "%s/seq-3c-i8.tiff", dir); + if (write_rgb_signed(path, 8, "w") == 0) + printf(" OK seq-3c-i8.tiff\n"); + else { printf(" FAIL seq-3c-i8.tiff\n"); ok = 0; } + + snprintf(path, sizeof(path), "%s/seq-3c-i16.tiff", dir); + if (write_rgb_signed(path, 16, "w") == 0) + printf(" OK seq-3c-i16.tiff\n"); + else { printf(" FAIL seq-3c-i16.tiff\n"); ok = 0; } + + printf("\n--- Float f64 RGB ---\n"); + + snprintf(path, sizeof(path), "%s/seq-3c-64f.tiff", dir); + if (write_rgb_float(path, 64, "w") == 0) + printf(" OK seq-3c-64f.tiff\n"); + else { printf(" FAIL seq-3c-64f.tiff\n"); ok = 0; } + + printf("\n--- Tiled BigTIFF ---\n"); + + snprintf(path, sizeof(path), "%s/seq-1c-8b-tiled-bigtiff.tiff", dir); + if (write_gray_tiled_comp(path, 8, COMPRESSION_NONE, "w8") == 0) + printf(" OK seq-1c-8b-tiled-bigtiff.tiff\n"); + else { printf(" FAIL seq-1c-8b-tiled-bigtiff.tiff\n"); ok = 0; } + + printf("\nDone. %s\n", ok ? "All succeeded." : "Some failed!"); + return ok ? 0 : 1; +} diff --git a/tests/images/hpredict-1c-12b.tiff b/tests/images/hpredict-1c-12b.tiff new file mode 100644 index 0000000..de2fcc8 Binary files /dev/null and b/tests/images/hpredict-1c-12b.tiff differ diff --git a/tests/images/minisblack-1c-3b.tiff b/tests/images/minisblack-1c-3b.tiff new file mode 100644 index 0000000..1ea0bfb Binary files /dev/null and b/tests/images/minisblack-1c-3b.tiff differ diff --git a/tests/images/minisblack-1c-5b.tiff b/tests/images/minisblack-1c-5b.tiff new file mode 100644 index 0000000..1bf6702 Binary files /dev/null and b/tests/images/minisblack-1c-5b.tiff differ diff --git a/tests/images/minisblack-1c-7b.tiff b/tests/images/minisblack-1c-7b.tiff new file mode 100644 index 0000000..f933419 Binary files /dev/null and b/tests/images/minisblack-1c-7b.tiff differ diff --git a/tests/images/miniswhite-1c-12b.tiff b/tests/images/miniswhite-1c-12b.tiff new file mode 100644 index 0000000..a85842e Binary files /dev/null and b/tests/images/miniswhite-1c-12b.tiff differ diff --git a/tests/images/miniswhite-1c-3b.tiff b/tests/images/miniswhite-1c-3b.tiff new file mode 100644 index 0000000..f40b0dc Binary files /dev/null and b/tests/images/miniswhite-1c-3b.tiff differ diff --git a/tests/images/miniswhite-1c-6b.tiff b/tests/images/miniswhite-1c-6b.tiff new file mode 100644 index 0000000..f076125 Binary files /dev/null and b/tests/images/miniswhite-1c-6b.tiff differ diff --git a/tests/images/seq/seq-1c-10b-6d739fa2.tiff b/tests/images/seq/seq-1c-10b-6d739fa2.tiff new file mode 100644 index 0000000..1553c9a Binary files /dev/null and b/tests/images/seq/seq-1c-10b-6d739fa2.tiff differ diff --git a/tests/images/seq/seq-1c-10b-hpredict-6d739fa2.tiff b/tests/images/seq/seq-1c-10b-hpredict-6d739fa2.tiff new file mode 100644 index 0000000..1553c9a Binary files /dev/null and b/tests/images/seq/seq-1c-10b-hpredict-6d739fa2.tiff differ diff --git a/tests/images/seq/seq-1c-10b-miniswhite-6d739fa2.tiff b/tests/images/seq/seq-1c-10b-miniswhite-6d739fa2.tiff new file mode 100644 index 0000000..95536e9 Binary files /dev/null and b/tests/images/seq/seq-1c-10b-miniswhite-6d739fa2.tiff differ diff --git a/tests/images/seq/seq-1c-12b-47c39b31.tiff b/tests/images/seq/seq-1c-12b-47c39b31.tiff new file mode 100644 index 0000000..8e588b6 Binary files /dev/null and b/tests/images/seq/seq-1c-12b-47c39b31.tiff differ diff --git a/tests/images/seq/seq-1c-12b-hpredict-47c39b31.tiff b/tests/images/seq/seq-1c-12b-hpredict-47c39b31.tiff new file mode 100644 index 0000000..8e588b6 Binary files /dev/null and b/tests/images/seq/seq-1c-12b-hpredict-47c39b31.tiff differ diff --git a/tests/images/seq/seq-1c-12b-miniswhite-47c39b31.tiff b/tests/images/seq/seq-1c-12b-miniswhite-47c39b31.tiff new file mode 100644 index 0000000..0fb0453 Binary files /dev/null and b/tests/images/seq/seq-1c-12b-miniswhite-47c39b31.tiff differ diff --git a/tests/images/seq/seq-1c-14b-e883657f.tiff b/tests/images/seq/seq-1c-14b-e883657f.tiff new file mode 100644 index 0000000..ad5bf09 Binary files /dev/null and b/tests/images/seq/seq-1c-14b-e883657f.tiff differ diff --git a/tests/images/seq/seq-1c-14b-hpredict-e883657f.tiff b/tests/images/seq/seq-1c-14b-hpredict-e883657f.tiff new file mode 100644 index 0000000..ad5bf09 Binary files /dev/null and b/tests/images/seq/seq-1c-14b-hpredict-e883657f.tiff differ diff --git a/tests/images/seq/seq-1c-14b-miniswhite-e883657f.tiff b/tests/images/seq/seq-1c-14b-miniswhite-e883657f.tiff new file mode 100644 index 0000000..4e5cdfc Binary files /dev/null and b/tests/images/seq/seq-1c-14b-miniswhite-e883657f.tiff differ diff --git a/tests/images/seq/seq-1c-16b-bigendian-68f373a0.tiff b/tests/images/seq/seq-1c-16b-bigendian-68f373a0.tiff new file mode 100644 index 0000000..dfd127b Binary files /dev/null and b/tests/images/seq/seq-1c-16b-bigendian-68f373a0.tiff differ diff --git a/tests/images/seq/seq-1c-16b-deflate-68f373a0.tiff b/tests/images/seq/seq-1c-16b-deflate-68f373a0.tiff new file mode 100644 index 0000000..af814d8 Binary files /dev/null and b/tests/images/seq/seq-1c-16b-deflate-68f373a0.tiff differ diff --git a/tests/images/seq/seq-1c-16b-lzw-68f373a0.tiff b/tests/images/seq/seq-1c-16b-lzw-68f373a0.tiff new file mode 100644 index 0000000..c096523 Binary files /dev/null and b/tests/images/seq/seq-1c-16b-lzw-68f373a0.tiff differ diff --git a/tests/images/seq/seq-1c-16b-multistrip-68f373a0.tiff b/tests/images/seq/seq-1c-16b-multistrip-68f373a0.tiff new file mode 100644 index 0000000..58b842a Binary files /dev/null and b/tests/images/seq/seq-1c-16b-multistrip-68f373a0.tiff differ diff --git a/tests/images/seq/seq-1c-16b-tiled-68f373a0.tiff b/tests/images/seq/seq-1c-16b-tiled-68f373a0.tiff new file mode 100644 index 0000000..fedb5bf Binary files /dev/null and b/tests/images/seq/seq-1c-16b-tiled-68f373a0.tiff differ diff --git a/tests/images/seq/seq-1c-1b-71f6a21a.tiff b/tests/images/seq/seq-1c-1b-71f6a21a.tiff new file mode 100644 index 0000000..d3c1543 Binary files /dev/null and b/tests/images/seq/seq-1c-1b-71f6a21a.tiff differ diff --git a/tests/images/seq/seq-1c-1b-miniswhite-71f6a21a.tiff b/tests/images/seq/seq-1c-1b-miniswhite-71f6a21a.tiff new file mode 100644 index 0000000..e91ccbd Binary files /dev/null and b/tests/images/seq/seq-1c-1b-miniswhite-71f6a21a.tiff differ diff --git a/tests/images/seq/seq-1c-24b-072a9dc9.tiff b/tests/images/seq/seq-1c-24b-072a9dc9.tiff new file mode 100644 index 0000000..f3a250e Binary files /dev/null and b/tests/images/seq/seq-1c-24b-072a9dc9.tiff differ diff --git a/tests/images/seq/seq-1c-24b-hpredict-072a9dc9.tiff b/tests/images/seq/seq-1c-24b-hpredict-072a9dc9.tiff new file mode 100644 index 0000000..f3a250e Binary files /dev/null and b/tests/images/seq/seq-1c-24b-hpredict-072a9dc9.tiff differ diff --git a/tests/images/seq/seq-1c-24b-miniswhite-072a9dc9.tiff b/tests/images/seq/seq-1c-24b-miniswhite-072a9dc9.tiff new file mode 100644 index 0000000..47902a2 Binary files /dev/null and b/tests/images/seq/seq-1c-24b-miniswhite-072a9dc9.tiff differ diff --git a/tests/images/seq/seq-1c-2b-58b25f76.tiff b/tests/images/seq/seq-1c-2b-58b25f76.tiff new file mode 100644 index 0000000..1f59ba7 Binary files /dev/null and b/tests/images/seq/seq-1c-2b-58b25f76.tiff differ diff --git a/tests/images/seq/seq-1c-32f-390fe673.tiff b/tests/images/seq/seq-1c-32f-390fe673.tiff new file mode 100644 index 0000000..b77d270 Binary files /dev/null and b/tests/images/seq/seq-1c-32f-390fe673.tiff differ diff --git a/tests/images/seq/seq-1c-32f-deflate-fpredict-390fe673.tiff b/tests/images/seq/seq-1c-32f-deflate-fpredict-390fe673.tiff new file mode 100644 index 0000000..a5c272f Binary files /dev/null and b/tests/images/seq/seq-1c-32f-deflate-fpredict-390fe673.tiff differ diff --git a/tests/images/seq/seq-1c-3b-ef237c07.tiff b/tests/images/seq/seq-1c-3b-ef237c07.tiff new file mode 100644 index 0000000..ac08203 Binary files /dev/null and b/tests/images/seq/seq-1c-3b-ef237c07.tiff differ diff --git a/tests/images/seq/seq-1c-3b-miniswhite-ef237c07.tiff b/tests/images/seq/seq-1c-3b-miniswhite-ef237c07.tiff new file mode 100644 index 0000000..b4c2c6b Binary files /dev/null and b/tests/images/seq/seq-1c-3b-miniswhite-ef237c07.tiff differ diff --git a/tests/images/seq/seq-1c-4b-fb92dcae.tiff b/tests/images/seq/seq-1c-4b-fb92dcae.tiff new file mode 100644 index 0000000..0a8aa99 Binary files /dev/null and b/tests/images/seq/seq-1c-4b-fb92dcae.tiff differ diff --git a/tests/images/seq/seq-1c-4b-miniswhite-fb92dcae.tiff b/tests/images/seq/seq-1c-4b-miniswhite-fb92dcae.tiff new file mode 100644 index 0000000..e282a37 Binary files /dev/null and b/tests/images/seq/seq-1c-4b-miniswhite-fb92dcae.tiff differ diff --git a/tests/images/seq/seq-1c-4b-palette-85108c5a.tiff b/tests/images/seq/seq-1c-4b-palette-85108c5a.tiff new file mode 100644 index 0000000..f16f7fd Binary files /dev/null and b/tests/images/seq/seq-1c-4b-palette-85108c5a.tiff differ diff --git a/tests/images/seq/seq-1c-5b-73098d17.tiff b/tests/images/seq/seq-1c-5b-73098d17.tiff new file mode 100644 index 0000000..b3faa00 Binary files /dev/null and b/tests/images/seq/seq-1c-5b-73098d17.tiff differ diff --git a/tests/images/seq/seq-1c-5b-miniswhite-73098d17.tiff b/tests/images/seq/seq-1c-5b-miniswhite-73098d17.tiff new file mode 100644 index 0000000..210cabc Binary files /dev/null and b/tests/images/seq/seq-1c-5b-miniswhite-73098d17.tiff differ diff --git a/tests/images/seq/seq-1c-64f-afa8560e.tiff b/tests/images/seq/seq-1c-64f-afa8560e.tiff new file mode 100644 index 0000000..e8a9683 Binary files /dev/null and b/tests/images/seq/seq-1c-64f-afa8560e.tiff differ diff --git a/tests/images/seq/seq-1c-64f-deflate-fpredict-afa8560e.tiff b/tests/images/seq/seq-1c-64f-deflate-fpredict-afa8560e.tiff new file mode 100644 index 0000000..825f10d Binary files /dev/null and b/tests/images/seq/seq-1c-64f-deflate-fpredict-afa8560e.tiff differ diff --git a/tests/images/seq/seq-1c-6b-miniswhite-79cafbb6.tiff b/tests/images/seq/seq-1c-6b-miniswhite-79cafbb6.tiff new file mode 100644 index 0000000..f7e91b8 Binary files /dev/null and b/tests/images/seq/seq-1c-6b-miniswhite-79cafbb6.tiff differ diff --git a/tests/images/seq/seq-1c-7b-9c61ba70.tiff b/tests/images/seq/seq-1c-7b-9c61ba70.tiff new file mode 100644 index 0000000..733f954 Binary files /dev/null and b/tests/images/seq/seq-1c-7b-9c61ba70.tiff differ diff --git a/tests/images/seq/seq-1c-7b-miniswhite-9c61ba70.tiff b/tests/images/seq/seq-1c-7b-miniswhite-9c61ba70.tiff new file mode 100644 index 0000000..4753443 Binary files /dev/null and b/tests/images/seq/seq-1c-7b-miniswhite-9c61ba70.tiff differ diff --git a/tests/images/seq/seq-1c-8b-bigendian-20f3db0c.tiff b/tests/images/seq/seq-1c-8b-bigendian-20f3db0c.tiff new file mode 100644 index 0000000..4c7cf1f Binary files /dev/null and b/tests/images/seq/seq-1c-8b-bigendian-20f3db0c.tiff differ diff --git a/tests/images/seq/seq-1c-8b-bigtiff-20f3db0c.tiff b/tests/images/seq/seq-1c-8b-bigtiff-20f3db0c.tiff new file mode 100644 index 0000000..ee1555f Binary files /dev/null and b/tests/images/seq/seq-1c-8b-bigtiff-20f3db0c.tiff differ diff --git a/tests/images/seq/seq-1c-8b-deflate-20f3db0c.tiff b/tests/images/seq/seq-1c-8b-deflate-20f3db0c.tiff new file mode 100644 index 0000000..759017e Binary files /dev/null and b/tests/images/seq/seq-1c-8b-deflate-20f3db0c.tiff differ diff --git a/tests/images/seq/seq-1c-8b-lzw-20f3db0c.tiff b/tests/images/seq/seq-1c-8b-lzw-20f3db0c.tiff new file mode 100644 index 0000000..14f51fb Binary files /dev/null and b/tests/images/seq/seq-1c-8b-lzw-20f3db0c.tiff differ diff --git a/tests/images/seq/seq-1c-8b-lzw-hpredict-20f3db0c.tiff b/tests/images/seq/seq-1c-8b-lzw-hpredict-20f3db0c.tiff new file mode 100644 index 0000000..e59ea69 Binary files /dev/null and b/tests/images/seq/seq-1c-8b-lzw-hpredict-20f3db0c.tiff differ diff --git a/tests/images/seq/seq-1c-8b-multipage-adeefdcc.tiff b/tests/images/seq/seq-1c-8b-multipage-adeefdcc.tiff new file mode 100644 index 0000000..20369b5 Binary files /dev/null and b/tests/images/seq/seq-1c-8b-multipage-adeefdcc.tiff differ diff --git a/tests/images/seq/seq-1c-8b-multistrip-20f3db0c.tiff b/tests/images/seq/seq-1c-8b-multistrip-20f3db0c.tiff new file mode 100644 index 0000000..4fd3fbf Binary files /dev/null and b/tests/images/seq/seq-1c-8b-multistrip-20f3db0c.tiff differ diff --git a/tests/images/seq/seq-1c-8b-packbits-20f3db0c.tiff b/tests/images/seq/seq-1c-8b-packbits-20f3db0c.tiff new file mode 100644 index 0000000..1c821df Binary files /dev/null and b/tests/images/seq/seq-1c-8b-packbits-20f3db0c.tiff differ diff --git a/tests/images/seq/seq-1c-8b-palette-89b39bc3.tiff b/tests/images/seq/seq-1c-8b-palette-89b39bc3.tiff new file mode 100644 index 0000000..735d5a2 Binary files /dev/null and b/tests/images/seq/seq-1c-8b-palette-89b39bc3.tiff differ diff --git a/tests/images/seq/seq-1c-8b-tiled-20f3db0c.tiff b/tests/images/seq/seq-1c-8b-tiled-20f3db0c.tiff new file mode 100644 index 0000000..6da0688 Binary files /dev/null and b/tests/images/seq/seq-1c-8b-tiled-20f3db0c.tiff differ diff --git a/tests/images/seq/seq-1c-8b-tiled-bigtiff-20f3db0c.tiff b/tests/images/seq/seq-1c-8b-tiled-bigtiff-20f3db0c.tiff new file mode 100644 index 0000000..b427bb6 Binary files /dev/null and b/tests/images/seq/seq-1c-8b-tiled-bigtiff-20f3db0c.tiff differ diff --git a/tests/images/seq/seq-1c-8b-tiled-deflate-20f3db0c.tiff b/tests/images/seq/seq-1c-8b-tiled-deflate-20f3db0c.tiff new file mode 100644 index 0000000..aac05d0 Binary files /dev/null and b/tests/images/seq/seq-1c-8b-tiled-deflate-20f3db0c.tiff differ diff --git a/tests/images/seq/seq-1c-8b-tiled-lzw-20f3db0c.tiff b/tests/images/seq/seq-1c-8b-tiled-lzw-20f3db0c.tiff new file mode 100644 index 0000000..e577eeb Binary files /dev/null and b/tests/images/seq/seq-1c-8b-tiled-lzw-20f3db0c.tiff differ diff --git a/tests/images/seq/seq-1c-i16-63af2488.tiff b/tests/images/seq/seq-1c-i16-63af2488.tiff new file mode 100644 index 0000000..cccb499 Binary files /dev/null and b/tests/images/seq/seq-1c-i16-63af2488.tiff differ diff --git a/tests/images/seq/seq-1c-i32-99fddec2.tiff b/tests/images/seq/seq-1c-i32-99fddec2.tiff new file mode 100644 index 0000000..4a2fcc1 Binary files /dev/null and b/tests/images/seq/seq-1c-i32-99fddec2.tiff differ diff --git a/tests/images/seq/seq-1c-i8-f8446bbe.tiff b/tests/images/seq/seq-1c-i8-f8446bbe.tiff new file mode 100644 index 0000000..821c8d3 Binary files /dev/null and b/tests/images/seq/seq-1c-i8-f8446bbe.tiff differ diff --git a/tests/images/seq/seq-3c-10b-contig-d08d5dc0.tiff b/tests/images/seq/seq-3c-10b-contig-d08d5dc0.tiff new file mode 100644 index 0000000..5093c23 Binary files /dev/null and b/tests/images/seq/seq-3c-10b-contig-d08d5dc0.tiff differ diff --git a/tests/images/seq/seq-3c-10b-planar-c82e8ab6.tiff b/tests/images/seq/seq-3c-10b-planar-c82e8ab6.tiff new file mode 100644 index 0000000..8d9fb47 Binary files /dev/null and b/tests/images/seq/seq-3c-10b-planar-c82e8ab6.tiff differ diff --git a/tests/images/seq/seq-3c-12b-contig-e6f40b4a.tiff b/tests/images/seq/seq-3c-12b-contig-e6f40b4a.tiff new file mode 100644 index 0000000..6c19ffe Binary files /dev/null and b/tests/images/seq/seq-3c-12b-contig-e6f40b4a.tiff differ diff --git a/tests/images/seq/seq-3c-12b-planar-e29e8e25.tiff b/tests/images/seq/seq-3c-12b-planar-e29e8e25.tiff new file mode 100644 index 0000000..926576b Binary files /dev/null and b/tests/images/seq/seq-3c-12b-planar-e29e8e25.tiff differ diff --git a/tests/images/seq/seq-3c-14b-contig-f4dcc6cc.tiff b/tests/images/seq/seq-3c-14b-contig-f4dcc6cc.tiff new file mode 100644 index 0000000..2abbf27 Binary files /dev/null and b/tests/images/seq/seq-3c-14b-contig-f4dcc6cc.tiff differ diff --git a/tests/images/seq/seq-3c-14b-planar-4dde706b.tiff b/tests/images/seq/seq-3c-14b-planar-4dde706b.tiff new file mode 100644 index 0000000..1b9fc08 Binary files /dev/null and b/tests/images/seq/seq-3c-14b-planar-4dde706b.tiff differ diff --git a/tests/images/seq/seq-3c-16b-bigtiff-1b40ca6e.tiff b/tests/images/seq/seq-3c-16b-bigtiff-1b40ca6e.tiff new file mode 100644 index 0000000..71232bd Binary files /dev/null and b/tests/images/seq/seq-3c-16b-bigtiff-1b40ca6e.tiff differ diff --git a/tests/images/seq/seq-3c-24b-contig-27b9f8ce.tiff b/tests/images/seq/seq-3c-24b-contig-27b9f8ce.tiff new file mode 100644 index 0000000..674c266 Binary files /dev/null and b/tests/images/seq/seq-3c-24b-contig-27b9f8ce.tiff differ diff --git a/tests/images/seq/seq-3c-24b-planar-6296c0c9.tiff b/tests/images/seq/seq-3c-24b-planar-6296c0c9.tiff new file mode 100644 index 0000000..1385467 Binary files /dev/null and b/tests/images/seq/seq-3c-24b-planar-6296c0c9.tiff differ diff --git a/tests/images/seq/seq-3c-32f-9a471c2b.tiff b/tests/images/seq/seq-3c-32f-9a471c2b.tiff new file mode 100644 index 0000000..ab46c0c Binary files /dev/null and b/tests/images/seq/seq-3c-32f-9a471c2b.tiff differ diff --git a/tests/images/seq/seq-3c-5b-contig-09f197f4.tiff b/tests/images/seq/seq-3c-5b-contig-09f197f4.tiff new file mode 100644 index 0000000..c3e9860 Binary files /dev/null and b/tests/images/seq/seq-3c-5b-contig-09f197f4.tiff differ diff --git a/tests/images/seq/seq-3c-64f-9fff098a.tiff b/tests/images/seq/seq-3c-64f-9fff098a.tiff new file mode 100644 index 0000000..4b7725a Binary files /dev/null and b/tests/images/seq/seq-3c-64f-9fff098a.tiff differ diff --git a/tests/images/seq/seq-3c-7b-contig-2e4f43c5.tiff b/tests/images/seq/seq-3c-7b-contig-2e4f43c5.tiff new file mode 100644 index 0000000..1756461 Binary files /dev/null and b/tests/images/seq/seq-3c-7b-contig-2e4f43c5.tiff differ diff --git a/tests/images/seq/seq-3c-8b-bigendian-8743c999.tiff b/tests/images/seq/seq-3c-8b-bigendian-8743c999.tiff new file mode 100644 index 0000000..be659fe Binary files /dev/null and b/tests/images/seq/seq-3c-8b-bigendian-8743c999.tiff differ diff --git a/tests/images/seq/seq-3c-8b-lzw-8743c999.tiff b/tests/images/seq/seq-3c-8b-lzw-8743c999.tiff new file mode 100644 index 0000000..2e98747 Binary files /dev/null and b/tests/images/seq/seq-3c-8b-lzw-8743c999.tiff differ diff --git a/tests/images/seq/seq-3c-8b-multistrip-8743c999.tiff b/tests/images/seq/seq-3c-8b-multistrip-8743c999.tiff new file mode 100644 index 0000000..8a5528d Binary files /dev/null and b/tests/images/seq/seq-3c-8b-multistrip-8743c999.tiff differ diff --git a/tests/images/seq/seq-3c-8b-tiled-8743c999.tiff b/tests/images/seq/seq-3c-8b-tiled-8743c999.tiff new file mode 100644 index 0000000..1fcbbe9 Binary files /dev/null and b/tests/images/seq/seq-3c-8b-tiled-8743c999.tiff differ diff --git a/tests/images/seq/seq-3c-i16-f7fcf423.tiff b/tests/images/seq/seq-3c-i16-f7fcf423.tiff new file mode 100644 index 0000000..20f416e Binary files /dev/null and b/tests/images/seq/seq-3c-i16-f7fcf423.tiff differ diff --git a/tests/images/seq/seq-3c-i8-d7550ce4.tiff b/tests/images/seq/seq-3c-i8-d7550ce4.tiff new file mode 100644 index 0000000..497d583 Binary files /dev/null and b/tests/images/seq/seq-3c-i8-d7550ce4.tiff differ diff --git a/tests/images/seq/seq-4c-16b-cmyk-c6e52592.tiff b/tests/images/seq/seq-4c-16b-cmyk-c6e52592.tiff new file mode 100644 index 0000000..b3e2a08 Binary files /dev/null and b/tests/images/seq/seq-4c-16b-cmyk-c6e52592.tiff differ diff --git a/tests/images/seq/seq-4c-16b-rgba-5181991f.tiff b/tests/images/seq/seq-4c-16b-rgba-5181991f.tiff new file mode 100644 index 0000000..4f657fa Binary files /dev/null and b/tests/images/seq/seq-4c-16b-rgba-5181991f.tiff differ diff --git a/tests/images/seq/seq-4c-8b-cmyk-352ac1da.tiff b/tests/images/seq/seq-4c-8b-cmyk-352ac1da.tiff new file mode 100644 index 0000000..ff64ee7 Binary files /dev/null and b/tests/images/seq/seq-4c-8b-cmyk-352ac1da.tiff differ diff --git a/tests/images/seq/seq-4c-8b-rgba-50969cda.tiff b/tests/images/seq/seq-4c-8b-rgba-50969cda.tiff new file mode 100644 index 0000000..ee203fe Binary files /dev/null and b/tests/images/seq/seq-4c-8b-rgba-50969cda.tiff differ diff --git a/tests/images/seq/seq-4c-8b-rgba-unassoc-50969cda.tiff b/tests/images/seq/seq-4c-8b-rgba-unassoc-50969cda.tiff new file mode 100644 index 0000000..76c8694 Binary files /dev/null and b/tests/images/seq/seq-4c-8b-rgba-unassoc-50969cda.tiff differ diff --git a/tests/verify_seq_tiff.c b/tests/verify_seq_tiff.c new file mode 100644 index 0000000..2f177ba --- /dev/null +++ b/tests/verify_seq_tiff.c @@ -0,0 +1,460 @@ +/* + * verify_seq_tiff.c — Read seq-* test TIFFs with libtiff and verify decoded values. + * + * This is the authoritative cross-check: libtiff wrote these files, and now + * libtiff reads them back. The checksums computed here must match what + * image-tiff's Rust decoder produces (in decode_images.rs tests). + * + * For sub-byte depths (1-7 bit): sums packed bytes (matching image-tiff behavior). + * For extended depths (9-31 bit): unpacks MSB-first to u16/u32, sums those. + * For standard depths: sums native sample values. + * For float: sums as float/double. + * For signed: sums as int64. + * + * Usage: ./verify_seq_tiff + * Compile: cc -O2 -o verify_seq_tiff verify_seq_tiff.c -ltiff -lm + */ + +#include +#include +#include +#include +#include +#include + +#define W 16 +#define H 16 + +/* Extract N bits from MSB-first packed byte stream starting at bit_offset. */ +static uint32_t extract_bits_msb(const uint8_t *buf, int bit_offset, int nbits) +{ + uint32_t val = 0; + for (int i = nbits - 1; i >= 0; i--) { + int byte_idx = bit_offset / 8; + int bit_in_byte = bit_offset % 8; + if (buf[byte_idx] & (0x80 >> bit_in_byte)) + val |= (1u << i); + bit_offset++; + } + return val; +} + +typedef enum { + SUM_U8_PACKED, /* Sum raw packed bytes (sub-byte depths) */ + SUM_U8, /* Sum u8 samples */ + SUM_U16, /* Sum u16 samples */ + SUM_U16_UNPACK, /* Unpack from packed bits, sum as u16 */ + SUM_U32, /* Sum u32 samples */ + SUM_U32_UNPACK, /* Unpack from packed bits, sum as u32 */ + SUM_U64, /* Sum u64 samples */ + SUM_I8, /* Sum i8 samples */ + SUM_I16, /* Sum i16 samples */ + SUM_I32, /* Sum i32 samples */ + SUM_F32, /* Sum f32 samples */ + SUM_F64, /* Sum f64 samples */ +} SumMode; + +typedef struct { + const char *filename; + SumMode mode; + int bits_per_sample; + int samples_per_pixel; + /* Expected checksums — same as Rust test expectations */ + union { + uint64_t u; + int64_t i; + double f; + } expected; +} TestCase; + +static int verify_stripped(const char *dir, const TestCase *tc) +{ + char path[512]; + snprintf(path, sizeof(path), "%s/%s", dir, tc->filename); + + TIFF *tif = TIFFOpen(path, "r"); + if (!tif) { + fprintf(stderr, " SKIP %s (cannot open)\n", tc->filename); + return 0; /* skip, not fail */ + } + + uint32_t w, h; + uint16_t bps, spp, sf, pc; + TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &w); + TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &h); + TIFFGetField(tif, TIFFTAG_BITSPERSAMPLE, &bps); + TIFFGetField(tif, TIFFTAG_SAMPLESPERPIXEL, &spp); + if (!TIFFGetField(tif, TIFFTAG_SAMPLEFORMAT, &sf)) + sf = SAMPLEFORMAT_UINT; + if (!TIFFGetField(tif, TIFFTAG_PLANARCONFIG, &pc)) + pc = PLANARCONFIG_CONTIG; + + int is_tiled = TIFFIsTiled(tif); + tsize_t scanline_size = TIFFScanlineSize(tif); + + /* For planar images, read_image() only reads plane 0 */ + int nplanes = (pc == PLANARCONFIG_SEPARATE) ? 1 : 1; + /* spp for checksum is always actual spp for contig, 1 for planar (first plane) */ + int eff_spp = (pc == PLANARCONFIG_SEPARATE) ? 1 : spp; + + uint64_t sum_u = 0; + int64_t sum_i = 0; + double sum_f = 0.0; + + if (is_tiled) { + uint32_t tw, th; + TIFFGetField(tif, TIFFTAG_TILEWIDTH, &tw); + TIFFGetField(tif, TIFFTAG_TILELENGTH, &th); + tsize_t tile_size = TIFFTileSize(tif); + uint8_t *tile_buf = (uint8_t *)_TIFFmalloc(tile_size); + if (!tile_buf) { TIFFClose(tif); return -1; } + + /* Assemble tiles into a full image buffer, then checksum */ + int img_bytes; + switch (tc->mode) { + case SUM_U8_PACKED: + case SUM_U8: img_bytes = w * h * eff_spp; break; + case SUM_U16: + case SUM_U16_UNPACK: img_bytes = w * h * eff_spp * 2; break; + case SUM_U32: + case SUM_U32_UNPACK: + case SUM_F32: img_bytes = w * h * eff_spp * 4; break; + case SUM_U64: + case SUM_F64: img_bytes = w * h * eff_spp * 8; break; + case SUM_I8: img_bytes = w * h * eff_spp; break; + case SUM_I16: img_bytes = w * h * eff_spp * 2; break; + case SUM_I32: img_bytes = w * h * eff_spp * 4; break; + default: img_bytes = w * h * eff_spp; break; + } + uint8_t *img = (uint8_t *)calloc(1, img_bytes); + if (!img) { _TIFFfree(tile_buf); TIFFClose(tif); return -1; } + + int bytes_per_sample = bps / 8; + if (bytes_per_sample == 0) bytes_per_sample = 1; + int row_bytes = w * eff_spp * bytes_per_sample; + + for (uint32_t ty = 0; ty < h; ty += th) { + for (uint32_t tx = 0; tx < w; tx += tw) { + TIFFReadTile(tif, tile_buf, tx, ty, 0, 0); + /* Copy tile data into image buffer */ + for (uint32_t row = 0; row < th && (ty + row) < h; row++) { + int src_off = row * tw * eff_spp * bytes_per_sample; + int dst_off = ((ty + row) * w + tx) * eff_spp * bytes_per_sample; + int copy_w = tw; + if (tx + tw > w) copy_w = w - tx; + memcpy(img + dst_off, tile_buf + src_off, + copy_w * eff_spp * bytes_per_sample); + } + } + } + + /* Now checksum the assembled image */ + int npixels = w * h * eff_spp; + switch (tc->mode) { + case SUM_U8_PACKED: + case SUM_U8: + for (int j = 0; j < npixels; j++) sum_u += img[j]; + break; + case SUM_U16: + for (int j = 0; j < npixels; j++) sum_u += ((uint16_t *)img)[j]; + break; + case SUM_U32: + for (int j = 0; j < npixels; j++) sum_u += ((uint32_t *)img)[j]; + break; + case SUM_I8: + for (int j = 0; j < npixels; j++) sum_i += ((int8_t *)img)[j]; + break; + case SUM_F32: + for (int j = 0; j < npixels; j++) sum_f += ((float *)img)[j]; + break; + case SUM_F64: + for (int j = 0; j < npixels; j++) sum_f += ((double *)img)[j]; + break; + default: break; + } + + free(img); + _TIFFfree(tile_buf); + } else { + /* Stripped image */ + uint8_t *buf = (uint8_t *)_TIFFmalloc(scanline_size); + if (!buf) { TIFFClose(tif); return -1; } + + for (uint32_t y = 0; y < h; y++) { + if (TIFFReadScanline(tif, buf, y, 0) < 0) { + fprintf(stderr, " ERROR reading scanline %u of %s\n", y, tc->filename); + _TIFFfree(buf); TIFFClose(tif); return -1; + } + + switch (tc->mode) { + case SUM_U8_PACKED: + /* Sum raw packed bytes — matches image-tiff sub-byte output */ + for (tsize_t j = 0; j < scanline_size; j++) + sum_u += buf[j]; + break; + case SUM_U8: + for (uint32_t x = 0; x < w * eff_spp; x++) + sum_u += buf[x]; + break; + case SUM_U16: + for (uint32_t x = 0; x < w * eff_spp; x++) + sum_u += ((uint16_t *)buf)[x]; + break; + case SUM_U16_UNPACK: { + /* Unpack N-bit MSB-first packed samples to u16 */ + for (uint32_t x = 0; x < w * eff_spp; x++) { + uint32_t val = extract_bits_msb(buf, x * bps, bps); + sum_u += val; + } + break; + } + case SUM_U32: + for (uint32_t x = 0; x < w * eff_spp; x++) + sum_u += ((uint32_t *)buf)[x]; + break; + case SUM_U32_UNPACK: { + for (uint32_t x = 0; x < w * eff_spp; x++) { + uint32_t val = extract_bits_msb(buf, x * bps, bps); + sum_u += val; + } + break; + } + case SUM_U64: + for (uint32_t x = 0; x < w * eff_spp; x++) + sum_u += ((uint64_t *)buf)[x]; + break; + case SUM_I8: + for (uint32_t x = 0; x < w * eff_spp; x++) + sum_i += ((int8_t *)buf)[x]; + break; + case SUM_I16: + for (uint32_t x = 0; x < w * eff_spp; x++) + sum_i += ((int16_t *)buf)[x]; + break; + case SUM_I32: + for (uint32_t x = 0; x < w * eff_spp; x++) + sum_i += ((int32_t *)buf)[x]; + break; + case SUM_F32: { + float *fp = (float *)buf; + for (uint32_t x = 0; x < w * eff_spp; x++) + sum_f += fp[x]; + break; + } + case SUM_F64: { + double *dp = (double *)buf; + for (uint32_t x = 0; x < w * eff_spp; x++) + sum_f += dp[x]; + break; + } + } + } + _TIFFfree(buf); + } + + TIFFClose(tif); + + /* Compare checksums */ + int pass = 0; + char got_str[64], exp_str[64]; + + switch (tc->mode) { + case SUM_U8_PACKED: + case SUM_U8: + case SUM_U16: + case SUM_U16_UNPACK: + case SUM_U32: + case SUM_U32_UNPACK: + case SUM_U64: + snprintf(got_str, sizeof(got_str), "%llu", (unsigned long long)sum_u); + snprintf(exp_str, sizeof(exp_str), "%llu", (unsigned long long)tc->expected.u); + pass = (sum_u == tc->expected.u); + break; + case SUM_I8: + case SUM_I16: + case SUM_I32: + snprintf(got_str, sizeof(got_str), "%lld", (long long)sum_i); + snprintf(exp_str, sizeof(exp_str), "%lld", (long long)tc->expected.i); + pass = (sum_i == tc->expected.i); + break; + case SUM_F32: + snprintf(got_str, sizeof(got_str), "%.10g", sum_f); + snprintf(exp_str, sizeof(exp_str), "%.10g", tc->expected.f); + /* Allow small float rounding tolerance */ + pass = (fabs(sum_f - tc->expected.f) < 0.01); + break; + case SUM_F64: + snprintf(got_str, sizeof(got_str), "%.15g", sum_f); + snprintf(exp_str, sizeof(exp_str), "%.15g", tc->expected.f); + pass = (fabs(sum_f - tc->expected.f) < 1e-10); + break; + } + + if (pass) { + printf(" PASS %-45s sum=%s\n", tc->filename, got_str); + } else { + printf(" FAIL %-45s got=%s expected=%s\n", tc->filename, got_str, exp_str); + } + + return pass ? 0 : 1; +} + +int main(int argc, char **argv) +{ + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + const char *dir = argv[1]; + + /* These expected values MUST match the Rust test expectations in decode_images.rs. + * If they don't, either the libtiff decoder or the image-tiff decoder has a bug. */ + TestCase cases[] = { + /* --- Sub-byte BlackIsZero (packed byte sums) --- */ + {"seq-1c-3b.tiff", SUM_U8_PACKED, 3, 1, {.u = 5792}}, + {"seq-1c-5b.tiff", SUM_U8_PACKED, 5, 1, {.u = 18944}}, + {"seq-1c-7b.tiff", SUM_U8_PACKED, 7, 1, {.u = 24992}}, + + /* --- Sub-byte WhiteIsZero (packed byte sums) --- + * libtiff returns RAW stored values (inverted), unlike image-tiff which + * inverts back to BlackIsZero. These sums verify libtiff round-trips + * the stored data correctly. image-tiff's inversion is verified separately + * by the Rust tests (which expect the same sums as BlackIsZero). */ + {"seq-1c-3b-miniswhite.tiff", SUM_U8_PACKED, 3, 1, {.u = 18688}}, + {"seq-1c-5b-miniswhite.tiff", SUM_U8_PACKED, 5, 1, {.u = 21856}}, + {"seq-1c-6b-miniswhite.tiff", SUM_U8_PACKED, 6, 1, {.u = 23232}}, + {"seq-1c-7b-miniswhite.tiff", SUM_U8_PACKED, 7, 1, {.u = 32128}}, + + /* --- Extended BlackIsZero (unpacked u16/u32 sums) --- */ + {"seq-1c-10b.tiff", SUM_U16_UNPACK, 10, 1, {.u = 32640}}, + {"seq-1c-12b.tiff", SUM_U16_UNPACK, 12, 1, {.u = 32640}}, + {"seq-1c-14b.tiff", SUM_U16_UNPACK, 14, 1, {.u = 32640}}, + {"seq-1c-24b.tiff", SUM_U32_UNPACK, 24, 1, {.u = 32640}}, + + /* --- Extended WhiteIsZero (unpacked sums, decoder inverts) --- */ + /* NOTE: libtiff does NOT invert WhiteIsZero — it returns raw stored values. + * image-tiff inverts them. So for WIZ, libtiff sum ≠ image-tiff sum. + * We verify these separately by checking the raw packed sum. */ + + /* --- Extended HPredict --- */ + {"seq-1c-10b-hpredict.tiff", SUM_U16_UNPACK, 10, 1, {.u = 32640}}, + {"seq-1c-12b-hpredict.tiff", SUM_U16_UNPACK, 12, 1, {.u = 32640}}, + {"seq-1c-14b-hpredict.tiff", SUM_U16_UNPACK, 14, 1, {.u = 32640}}, + {"seq-1c-24b-hpredict.tiff", SUM_U32_UNPACK, 24, 1, {.u = 32640}}, + + /* --- Extended RGB Contiguous (unpacked sums) --- */ + {"seq-3c-10b-contig.tiff", SUM_U16_UNPACK, 10, 3, {.u = 294528}}, + {"seq-3c-12b-contig.tiff", SUM_U16_UNPACK, 12, 3, {.u = 294528}}, + {"seq-3c-14b-contig.tiff", SUM_U16_UNPACK, 14, 3, {.u = 294528}}, + {"seq-3c-24b-contig.tiff", SUM_U32_UNPACK, 24, 3, {.u = 294528}}, + + /* --- Extended RGB Planar (first plane only, unpacked) --- */ + {"seq-3c-10b-planar.tiff", SUM_U16_UNPACK, 10, 3, {.u = 32640}}, + {"seq-3c-12b-planar.tiff", SUM_U16_UNPACK, 12, 3, {.u = 32640}}, + {"seq-3c-14b-planar.tiff", SUM_U16_UNPACK, 14, 3, {.u = 32640}}, + {"seq-3c-24b-planar.tiff", SUM_U32_UNPACK, 24, 3, {.u = 32640}}, + + /* --- Standard depths with compression --- */ + {"seq-1c-8b-lzw.tiff", SUM_U8, 8, 1, {.u = 32640}}, + {"seq-1c-8b-deflate.tiff", SUM_U8, 8, 1, {.u = 32640}}, + {"seq-1c-8b-packbits.tiff", SUM_U8, 8, 1, {.u = 32640}}, + {"seq-1c-16b-lzw.tiff", SUM_U16, 16, 1, {.u = 32640}}, + {"seq-1c-16b-deflate.tiff", SUM_U16, 16, 1, {.u = 32640}}, + {"seq-1c-8b-lzw-hpredict.tiff", SUM_U8, 8, 1, {.u = 32640}}, + {"seq-3c-8b-lzw.tiff", SUM_U8, 8, 3, {.u = 97920}}, + + /* --- Tiled --- */ + {"seq-1c-8b-tiled.tiff", SUM_U8, 8, 1, {.u = 32640}}, + {"seq-1c-16b-tiled.tiff", SUM_U16, 16, 1, {.u = 32640}}, + {"seq-3c-8b-tiled.tiff", SUM_U8, 8, 3, {.u = 97920}}, + + /* --- BigTIFF --- */ + {"seq-1c-8b-bigtiff.tiff", SUM_U8, 8, 1, {.u = 32640}}, + {"seq-3c-16b-bigtiff.tiff", SUM_U16, 16, 3, {.u = 294528}}, + + /* --- Float --- */ + {"seq-1c-32f.tiff", SUM_F32, 32, 1, {.f = 128.0}}, + {"seq-1c-64f.tiff", SUM_F64, 64, 1, {.f = 128.0}}, + {"seq-3c-32f.tiff", SUM_F32, 32, 3, {.f = 384.0}}, + + /* --- Signed integer --- */ + {"seq-1c-i8.tiff", SUM_I8, 8, 1, {.i = -128}}, + {"seq-1c-i16.tiff", SUM_I16, 16, 1, {.i = -128}}, + {"seq-1c-i32.tiff", SUM_I32, 32, 1, {.i = -128}}, + + /* --- CMYK --- */ + {"seq-4c-8b-cmyk.tiff", SUM_U8, 8, 4, {.u = 130560}}, + {"seq-4c-16b-cmyk.tiff", SUM_U16, 16, 4, {.u = 523776}}, + + /* --- Palette (raw packed indices) --- */ + {"seq-1c-8b-palette.tiff", SUM_U8, 8, 1, {.u = 32640}}, + {"seq-1c-4b-palette.tiff", SUM_U8_PACKED, 4, 1, {.u = 15360}}, + + /* --- RGBA --- */ + {"seq-4c-8b-rgba.tiff", SUM_U8, 8, 4, {.u = 130560}}, + {"seq-4c-16b-rgba.tiff", SUM_U16, 16, 4, {.u = 523776}}, + + /* --- Multi-page (first page only) --- */ + {"seq-1c-8b-multipage.tiff", SUM_U8, 8, 1, {.u = 32640}}, + + /* --- Big-endian --- */ + {"seq-1c-8b-bigendian.tiff", SUM_U8, 8, 1, {.u = 32640}}, + {"seq-1c-16b-bigendian.tiff", SUM_U16, 16, 1, {.u = 32640}}, + {"seq-3c-8b-bigendian.tiff", SUM_U8, 8, 3, {.u = 97920}}, + + /* --- Standard sub-byte (packed byte sums) --- */ + {"seq-1c-1b.tiff", SUM_U8_PACKED, 1, 1, {.u = 2720}}, + {"seq-1c-2b.tiff", SUM_U8_PACKED, 2, 1, {.u = 1728}}, + {"seq-1c-4b.tiff", SUM_U8_PACKED, 4, 1, {.u = 15360}}, + + /* --- Multi-strip --- */ + {"seq-1c-8b-multistrip.tiff", SUM_U8, 8, 1, {.u = 32640}}, + {"seq-1c-16b-multistrip.tiff", SUM_U16, 16, 1, {.u = 32640}}, + {"seq-3c-8b-multistrip.tiff", SUM_U8, 8, 3, {.u = 97920}}, + + /* --- Tiled + compressed --- */ + {"seq-1c-8b-tiled-lzw.tiff", SUM_U8, 8, 1, {.u = 32640}}, + {"seq-1c-8b-tiled-deflate.tiff", SUM_U8, 8, 1, {.u = 32640}}, + + /* --- Float + predictor --- */ + {"seq-1c-32f-deflate-fpredict.tiff", SUM_F32, 32, 1, {.f = 128.0}}, + {"seq-1c-64f-deflate-fpredict.tiff", SUM_F64, 64, 1, {.f = 128.0}}, + + /* --- Unassociated alpha --- */ + {"seq-4c-8b-rgba-unassoc.tiff", SUM_U8, 8, 4, {.u = 130560}}, + + /* --- Sub-byte RGB (packed byte sums) --- */ + {"seq-3c-5b-contig.tiff", SUM_U8_PACKED, 5, 3, {.u = 56832}}, + {"seq-3c-7b-contig.tiff", SUM_U8_PACKED, 7, 3, {.u = 74976}}, + + /* --- Signed RGB --- */ + {"seq-3c-i8.tiff", SUM_I8, 8, 3, {.i = -384}}, + {"seq-3c-i16.tiff", SUM_I16, 16, 3, {.i = -384}}, + + /* --- Float f64 RGB --- */ + {"seq-3c-64f.tiff", SUM_F64, 64, 3, {.f = 384.0}}, + + /* --- Tiled BigTIFF --- */ + {"seq-1c-8b-tiled-bigtiff.tiff", SUM_U8, 8, 1, {.u = 32640}}, + }; + + int ncases = sizeof(cases) / sizeof(cases[0]); + int failures = 0; + + printf("Verifying %d test images with libtiff decoder in %s/\n\n", ncases, dir); + + /* Suppress libtiff warnings for non-standard tag 317 (Predictor) */ + TIFFSetWarningHandler(NULL); + + for (int i = 0; i < ncases; i++) { + int ret = verify_stripped(dir, &cases[i]); + if (ret != 0) failures++; + } + + printf("\n%d/%d passed", ncases - failures, ncases); + if (failures > 0) + printf(", %d FAILED", failures); + printf("\n"); + + return failures > 0 ? 1 : 0; +}