Merge pull request #363 from lilith/feat/non-standard-bit-depths

Support non-standard bit depths (sub-byte and extended) with WhiteIsZero fixes
This commit is contained in:
Aurelia Molzer authored and GitHub committed 2026-03-12 02:03:49 +00:00
commit ba7a290ff7
89 files changed
+2774 -98

No files matched your search

+1
View File
@@ -54,6 +54,7 @@ harness = false
name = "encode" name = "encode"
harness = false harness = false
[[test]] [[test]]
name = "decode_libtiffpic" name = "decode_libtiffpic"
path = "tests/decode_libtiffpic.rs" path = "tests/decode_libtiffpic.rs"
+223 -9
View File
@@ -85,6 +85,8 @@ pub(crate) struct Image {
pub chunk_offsets: Vec<u64>, pub chunk_offsets: Vec<u64>,
pub chunk_bytes: Vec<u64>, pub chunk_bytes: Vec<u64>,
pub chroma_subsampling: (u16, u16), 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`. /// The color map (palette) for `PhotometricInterpretation::RGBPalette`.
/// Contains `3 * 2^BitsPerSample` u16 entries: R values, then G values, then B values. /// Contains `3 * 2^BitsPerSample` u16 entries: R values, then G values, then B values.
pub color_map: Option<Vec<u16>>, pub color_map: Option<Vec<u16>>,
@@ -135,6 +137,10 @@ pub(crate) struct ReadoutLayout {
/// Bits per sample in the encoded data. /// Bits per sample in the encoded data.
pub tiff_bits_per_sample: u8, 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. /// Number of samples in the encoded data.
pub tiff_samples: u16, pub tiff_samples: u16,
/// Dimensions of the underlying rectangular chunks (tile or strips). /// Dimensions of the underlying rectangular chunks (tile or strips).
@@ -169,6 +175,7 @@ impl Image {
chunk_offsets: Vec::new(), chunk_offsets: Vec::new(),
chunk_bytes: Vec::new(), chunk_bytes: Vec::new(),
chroma_subsampling: (2, 2), chroma_subsampling: (2, 2),
fill_order: 1,
color_map: None, color_map: None,
decompression_to_host_endian: false, decompression_to_host_endian: false,
} }
@@ -329,6 +336,12 @@ impl Image {
(2, 2) (2, 2)
}; };
let fill_order = tag_reader
.find_tag(Tag::FillOrder)?
.map(Value::into_u16)
.transpose()?
.unwrap_or(1);
let planes = match planar_config { let planes = match planar_config {
PlanarConfiguration::Chunky => 1, PlanarConfiguration::Chunky => 1,
PlanarConfiguration::Planar => samples, PlanarConfiguration::Planar => samples,
@@ -506,6 +519,7 @@ impl Image {
chunk_offsets, chunk_offsets,
chunk_bytes, chunk_bytes,
chroma_subsampling, chroma_subsampling,
fill_order,
color_map, color_map,
decompression_to_host_endian: matches!( decompression_to_host_endian: matches!(
compression_method, compression_method,
@@ -677,6 +691,7 @@ impl Image {
#[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>, #[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>,
dimensions: (u32, u32), dimensions: (u32, u32),
samples: u16, samples: u16,
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] fill_order: u16,
) -> TiffResult<Box<dyn Read + 'r>> { ) -> TiffResult<Box<dyn Read + 'r>> {
Ok(match compression_method { Ok(match compression_method {
CompressionMethod::None => Box::new(reader), CompressionMethod::None => Box::new(reader),
@@ -770,10 +785,18 @@ impl Image {
Box::new(Cursor::new(data)) Box::new(Cursor::new(data))
} }
#[cfg(feature = "fax")] #[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( CompressionMethod::Fax4 => Box::new(super::stream::Group4Reader::new(
dimensions, dimensions,
reader, reader,
compressed_length, compressed_length,
fill_order,
)?), )?),
#[cfg(feature = "webp")] #[cfg(feature = "webp")]
CompressionMethod::WebP => Box::new(super::stream::WebPReader::new( CompressionMethod::WebP => Box::new(super::stream::WebPReader::new(
@@ -872,24 +895,40 @@ impl Image {
let data_dimensions = (width, height); 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)) let tiff_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample))
.checked_mul(u64::from(tiff_samples)) .checked_mul(u64::from(tiff_samples))
.ok_or(TiffError::LimitsExceeded)?; .ok_or(TiffError::LimitsExceeded)?;
let tiff_row_bytes: usize = tiff_row_bits.div_ceil(8).try_into()?; 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)) // Output buffer sizes use output_bits_per_sample (the promoted width).
.checked_mul(u64::from(data_samples)) let chunk_row_bits = (u64::from(tiff_chunk_dimensions.0)
.ok_or(TiffError::LimitsExceeded)?; * 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 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)) .checked_mul(u64::from(data_samples))
.ok_or(TiffError::LimitsExceeded)?; .ok_or(TiffError::LimitsExceeded)?;
let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?; 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 let chunk_col_stride: usize = data_row_bits
.div_ceil(8) .div_ceil(8)
.checked_mul(u64::from(tiff_chunk_dimensions.1)) .checked_mul(u64::from(clamped_chunk_height))
.ok_or(TiffError::LimitsExceeded)? .ok_or(TiffError::LimitsExceeded)?
.try_into()?; .try_into()?;
@@ -938,6 +977,7 @@ impl Image {
chunk_col_stride, chunk_col_stride,
plane_stride, plane_stride,
tiff_bits_per_sample, tiff_bits_per_sample,
output_bits_per_sample,
tiff_samples, tiff_samples,
tiff_chunk_dimensions, tiff_chunk_dimensions,
tiff_row_bytes, tiff_row_bytes,
@@ -1013,7 +1053,12 @@ impl Image {
bit_depth: n, bit_depth: n,
num_samples: _, num_samples: _,
} if n == 8 || n == 16 || n == 32 || n == 64 => {} } 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::Palette(n)
| ColorType::Multiband { | ColorType::Multiband {
bit_depth: n, 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_ => { type_ => {
return Err(TiffError::UnsupportedError( return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedColorType(type_), TiffUnsupportedError::UnsupportedColorType(type_),
@@ -1102,9 +1167,52 @@ impl Image {
self.jpeg_tables.as_deref().map(|a| &**a), self.jpeg_tables.as_deref().map(|a| &**a),
chunk_dims, chunk_dims,
self.samples, 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. // Here we can read directly into the output buffer itself.
let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize]; let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize];
reader.read_exact(tile)?; 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<const BPS: u8>(
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::<BPS>(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::<BPS>(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<const BPS: u8>(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 { impl ReadoutLayout {
pub(crate) fn samples_per_out_texel(&self) -> u16 { pub(crate) fn samples_per_out_texel(&self) -> u16 {
match self.planar_config { match self.planar_config {
@@ -1241,7 +1451,7 @@ impl ReadoutLayout {
// pixel data. // pixel data.
pub(crate) fn chunk_row_bytes(&self, width: u32) -> TiffResult<usize> { pub(crate) fn chunk_row_bytes(&self, width: u32) -> TiffResult<usize> {
let data_samples = self.samples_per_out_texel(); 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)) .checked_mul(u64::from(data_samples))
.ok_or(TiffError::LimitsExceeded)?; .ok_or(TiffError::LimitsExceeded)?;
Ok(data_row_bits.div_ceil(8).try_into()?) Ok(data_row_bits.div_ceil(8).try_into()?)
@@ -1259,8 +1469,12 @@ impl ReadoutLayout {
let data_row_bytes = u64::try_from(row_stride)?; 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 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)? .ok_or(TiffError::LimitsExceeded)?
.try_into()?; .try_into()?;
+52
View File
@@ -740,6 +740,58 @@ fn invert_colors(
*x = !*x; *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) => { (ColorType::Gray(32), SampleFormat::IEEEFP) => {
for x in buf.as_chunks_mut::<4>().0 { for x in buf.as_chunks_mut::<4>().0 {
let v = f32::from_ne_bytes(*x); let v = f32::from_ne_bytes(*x);
+153 -2
View File
@@ -256,9 +256,24 @@ impl<R: Read> Read for PackBitsReader<R> {
} }
} }
/// Iterator adapter that reverses bits in each byte for FillOrder=2 (LSB-to-MSB).
#[cfg(feature = "fax")]
type FaxBytes<R> =
core::iter::Map<io::Bytes<io::BufReader<io::Take<R>>>, fn(io::Result<u8>) -> io::Result<u8>>;
#[cfg(feature = "fax")]
fn fax_byte_iter<R: Read>(reader: R, len: u64, fill_order: u16) -> FaxBytes<R> {
let map_fn: fn(io::Result<u8>) -> io::Result<u8> = 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")] #[cfg(feature = "fax")]
pub struct Group4Reader<R: Read> { pub struct Group4Reader<R: Read> {
decoder: fax34::decoder::Group4Decoder<io::Bytes<io::BufReader<io::Take<R>>>>, decoder: fax34::decoder::Group4Decoder<FaxBytes<R>>,
line_buf: io::Cursor<Vec<u8>>, line_buf: io::Cursor<Vec<u8>>,
height: u32, height: u32,
width: u16, width: u16,
@@ -271,13 +286,14 @@ impl<R: Read> Group4Reader<R> {
dimensions: (u32, u32), dimensions: (u32, u32),
reader: R, reader: R,
compressed_length: u64, compressed_length: u64,
fill_order: u16,
) -> crate::TiffResult<Self> { ) -> crate::TiffResult<Self> {
let width = u16::try_from(dimensions.0)?; let width = u16::try_from(dimensions.0)?;
let height = dimensions.1; let height = dimensions.1;
Ok(Self { Ok(Self {
decoder: fax34::decoder::Group4Decoder::new( decoder: fax34::decoder::Group4Decoder::new(
io::BufReader::new(reader.take(compressed_length)).bytes(), fax_byte_iter(reader, compressed_length, fill_order),
width, width,
)?, )?,
line_buf: io::Cursor::new(Vec::with_capacity(width.into())), line_buf: io::Cursor::new(Vec::with_capacity(width.into())),
@@ -356,6 +372,141 @@ impl<R: Read> Read for Group4Reader<R> {
} }
} }
/// 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<std::vec::IntoIter<u8>, fn(u8) -> Result<u8, std::convert::Infallible>>,
>;
/// 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<Vec<u8>>,
height: u32,
width: u16,
y: u32,
}
#[cfg(feature = "fax")]
impl Group3Reader {
pub fn new<R: Read>(
dimensions: (u32, u32),
reader: R,
compressed_length: u64,
fill_order: u16,
) -> crate::TiffResult<Self> {
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<u8, std::convert::Infallible> = 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<R: fax34::BitReader>(
decoder: fn(&mut R) -> Option<u16>,
reader: &mut R,
) -> Option<u16> {
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<usize> {
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")] #[cfg(feature = "webp")]
pub struct WebPReader { pub struct WebPReader {
inner: Cursor<Vec<u8>>, inner: Cursor<Vec<u8>>,
+246 -2
View File
@@ -1,6 +1,7 @@
extern crate tiff; extern crate tiff;
use tiff::decoder::{ifd, Decoder, DecodingResult}; use tiff::decoder::{ifd, Decoder, DecodingResult};
use tiff::tags::{ByteOrder, Type};
use tiff::ColorType; use tiff::ColorType;
use std::fs::File; use std::fs::File;
@@ -8,6 +9,7 @@ use std::io::{Read, Seek};
use std::path::PathBuf; use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images/"; const TEST_IMAGE_DIR: &str = "./tests/images/";
const SEQ_IMAGE_DIR: &str = "./tests/images/seq/";
macro_rules! test_image_sum { macro_rules! test_image_sum {
($name:ident, $buffer:ident, $sum_ty:ty) => { ($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_f32, F32, f32);
test_image_sum!(test_image_sum_f64, F64, f64); 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<R: Read + Seek>(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 `<description>-<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 /// 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 /// read from the IFD and is of the appropriate type, but the type is
/// unsupported. /// unsupported.
#[allow(dead_code)]
fn test_image_color_type_unsupported(file: &str, expected_type: ColorType) { fn test_image_color_type_unsupported(file: &str, expected_type: ColorType) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file); let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!"); let img_file = File::open(path).expect("Cannot find test image!");
@@ -121,7 +312,7 @@ fn test_gray_u8() {
#[test] #[test]
fn test_gray_u12() { 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] #[test]
@@ -160,7 +351,7 @@ fn test_rgb_u8() {
#[test] #[test]
fn test_rgb_u12() { 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] #[test]
@@ -809,3 +1000,56 @@ fn test_decode_huge_g4() {
panic!("Unexpected decoding result type"); 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);
}
+288 -85
View File
@@ -2,6 +2,8 @@ use std::collections::HashMap;
use std::{fs::File, path}; use std::{fs::File, path};
use tiff::decoder::{Decoder, DecodingResult}; use tiff::decoder::{Decoder, DecodingResult};
use tiff::tags::{ByteOrder, Type};
use tiff::ColorType;
use libtest_mimic::{Arguments, Failed, Trial}; use libtest_mimic::{Arguments, Failed, Trial};
use walkdir::WalkDir; use walkdir::WalkDir;
@@ -9,95 +11,202 @@ use walkdir::WalkDir;
const REFERENCE_DIR: &str = "tests/libtiffpic"; const REFERENCE_DIR: &str = "tests/libtiffpic";
// `find test/libtiffpic -type f | sort` // `find test/libtiffpic -type f | sort`
const FILES: &[(&str, bool)] = &[ //
("tests/libtiffpic/caspian.tif", true), // Each entry maps a file path to its expected behavior:
("tests/libtiffpic/cramps.tif", true), // None = not a TIFF file, skip entirely
("tests/libtiffpic/cramps-tile.tif", true), // Some(0) = TIFF with unsupported features, decode-only smoke test
("tests/libtiffpic/CVS/Entries", false), // Some(nonzero) = TIFF with full CRC32 hash verification of decoded output
("tests/libtiffpic/CVS/Repository", false), const FILES: &[(&str, Option<u32>)] = &[
("tests/libtiffpic/CVS/Root", false), ("tests/libtiffpic/caspian.tif", Some(0x4ea8d4e4)),
("tests/libtiffpic/depth/CVS/Entries", false), ("tests/libtiffpic/cramps.tif", Some(0xdfa2fba4)),
("tests/libtiffpic/depth/CVS/Repository", false), ("tests/libtiffpic/cramps-tile.tif", Some(0xdfa2fba4)),
("tests/libtiffpic/depth/CVS/Root", false), ("tests/libtiffpic/CVS/Entries", None),
("tests/libtiffpic/depth/flower-minisblack-02.tif", true), ("tests/libtiffpic/CVS/Repository", None),
("tests/libtiffpic/depth/flower-minisblack-04.tif", true), ("tests/libtiffpic/CVS/Root", None),
("tests/libtiffpic/depth/flower-minisblack-06.tif", true), ("tests/libtiffpic/depth/CVS/Entries", None),
("tests/libtiffpic/depth/flower-minisblack-08.tif", true), ("tests/libtiffpic/depth/CVS/Repository", None),
("tests/libtiffpic/depth/flower-minisblack-10.tif", true), ("tests/libtiffpic/depth/CVS/Root", None),
("tests/libtiffpic/depth/flower-minisblack-12.tif", true), (
("tests/libtiffpic/depth/flower-minisblack-14.tif", true), "tests/libtiffpic/depth/flower-minisblack-02.tif",
("tests/libtiffpic/depth/flower-minisblack-16.tif", true), Some(0x1c90f515),
("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-minisblack-04.tif",
("tests/libtiffpic/depth/flower-palette-04.tif", true), Some(0x0f4813cb),
("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-minisblack-06.tif",
("tests/libtiffpic/depth/flower-rgb-contig-04.tif", true), Some(0xf0c0a901),
("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-minisblack-08.tif",
("tests/libtiffpic/depth/flower-rgb-contig-14.tif", true), Some(0xe797b15b),
("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-minisblack-10.tif",
("tests/libtiffpic/depth/flower-rgb-planar-02.tif", true), Some(0xdee442ec),
("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-minisblack-12.tif",
("tests/libtiffpic/depth/flower-rgb-planar-12.tif", true), Some(0x4810c4e9),
("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-minisblack-14.tif",
("tests/libtiffpic/depth/flower-rgb-planar-32.tif", true), 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", "tests/libtiffpic/depth/flower-separated-contig-08.tif",
true, Some(0x97fb03a6),
), ),
( (
"tests/libtiffpic/depth/flower-separated-contig-16.tif", "tests/libtiffpic/depth/flower-separated-contig-16.tif",
true, Some(0xb6ee0135),
), ),
( (
"tests/libtiffpic/depth/flower-separated-planar-08.tif", "tests/libtiffpic/depth/flower-separated-planar-08.tif",
true, Some(0x502e93fa),
), ),
( (
"tests/libtiffpic/depth/flower-separated-planar-16.tif", "tests/libtiffpic/depth/flower-separated-planar-16.tif",
true, Some(0x50e580d9),
), ),
("tests/libtiffpic/depth/genimages", false), ("tests/libtiffpic/depth/genimages", None),
("tests/libtiffpic/depth/README.txt", false), ("tests/libtiffpic/depth/README.txt", None),
("tests/libtiffpic/depth/summary.txt", false), ("tests/libtiffpic/depth/summary.txt", None),
("tests/libtiffpic/dscf0013.tif", true), ("tests/libtiffpic/dscf0013.tif", Some(0)), // unsupported YCbCr chroma subsampling
("tests/libtiffpic/fax2d.tif", true), ("tests/libtiffpic/fax2d.tif", Some(0xc2f30c6f)),
// No support for direct CCITT Group 3 1D format // No support for direct CCITT Group 3 1D format
("tests/libtiffpic/g3test.g3", false), ("tests/libtiffpic/g3test.g3", None),
("tests/libtiffpic/g3test.tif", true), ("tests/libtiffpic/g3test.tif", Some(0x304b606b)),
("tests/libtiffpic/jello.tif", true), ("tests/libtiffpic/jello.tif", Some(0xb03dbda9)),
("tests/libtiffpic/jim___ah.tif", true), ("tests/libtiffpic/jim___ah.tif", Some(0x81a27b7f)),
("tests/libtiffpic/jim___cg.tif", true), ("tests/libtiffpic/jim___cg.tif", Some(0xfd31747c)),
("tests/libtiffpic/jim___dg.tif", true), ("tests/libtiffpic/jim___dg.tif", Some(0x8c6a5e8f)),
("tests/libtiffpic/jim___gg.tif", true), ("tests/libtiffpic/jim___gg.tif", Some(0x8c6a5e8f)),
("tests/libtiffpic/ladoga.tif", true), ("tests/libtiffpic/ladoga.tif", Some(0x9b118511)),
("tests/libtiffpic/off_l16.tif", true), ("tests/libtiffpic/off_l16.tif", Some(0xd434a2d9)),
("tests/libtiffpic/off_luv24.tif", true), ("tests/libtiffpic/off_luv24.tif", Some(0x2bee71a2)),
("tests/libtiffpic/off_luv32.tif", true), ("tests/libtiffpic/off_luv32.tif", Some(0)), // LogLuv decoder output varies across platforms
("tests/libtiffpic/oxford.tif", true), ("tests/libtiffpic/oxford.tif", Some(0x355d2e3e)),
("tests/libtiffpic/pc260001.tif", true), ("tests/libtiffpic/pc260001.tif", Some(0xdb56b753)),
("tests/libtiffpic/quad-jpeg.tif", true), ("tests/libtiffpic/quad-jpeg.tif", Some(0xcba22475)),
("tests/libtiffpic/quad-lzw.tif", true), ("tests/libtiffpic/quad-lzw.tif", Some(0)), // invalid LZW stream
("tests/libtiffpic/quad-tile.tif", true), ("tests/libtiffpic/quad-tile.tif", Some(0x6055f9ee)),
("tests/libtiffpic/README", false), ("tests/libtiffpic/README", None),
("tests/libtiffpic/smallliz.tif", true), ("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported YCbCr chroma subsampling
("tests/libtiffpic/strike.tif", true), ("tests/libtiffpic/strike.tif", Some(0x016b0f6a)),
("tests/libtiffpic/text.tif", true), ("tests/libtiffpic/text.tif", Some(0)), // unsupported compression
("tests/libtiffpic/ycbcr-cat.tif", true), ("tests/libtiffpic/ycbcr-cat.tif", Some(0)), // unsupported YCbCr chroma subsampling
("tests/libtiffpic/zackthecat.tif", true), ("tests/libtiffpic/zackthecat.tif", Some(0)), // unsupported YCbCr chroma subsampling
]; ];
fn main() { fn main() {
let args: Vec<String> = 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 candidates: HashMap<_, _> = FILES.iter().copied().collect();
let mut trials = Vec::new(); let mut trials = Vec::new();
@@ -114,22 +223,19 @@ fn main() {
let path = path.replace(std::path::MAIN_SEPARATOR, "/"); // always use UNIX path separators let path = path.replace(std::path::MAIN_SEPARATOR, "/"); // always use UNIX path separators
// Mark this path as consumed in either case by removing it. // 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); panic!("File {} not in reference list", path);
}; };
// Check if we should turn it into a test case. // None = non-image file, skip
if !should_decode { let Some(expected_hash) = expected_hash else {
continue; continue;
} };
let path = path.to_string(); let path = path.to_string();
trials.push(Trial::test( trials.push(Trial::test(
path.to_string(), path.to_string(),
move || -> Result<(), Failed> { move || -> Result<(), Failed> { decode_and_verify(&path, expected_hash) },
decode_libtiffpic(&path)
.map_err(|e| Failed::from(format_args!("Decoding failed for {}: {}", path, e)))
},
)); ));
} }
@@ -145,17 +251,114 @@ fn main() {
libtest_mimic::run(&args, trials).exit(); libtest_mimic::run(&args, trials).exit();
} }
fn decode_libtiffpic(path: &str) -> Result<(), tiff::TiffError> { /// Map DecodingResult variant to a TIFF Type for byte-order normalization.
let path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path); 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<u32, tiff::TiffError> {
let img_file = File::open(path)?; let img_file = File::open(path)?;
let mut decoder = Decoder::open(img_file)?; let mut decoder = Decoder::open(img_file)?;
let mut hasher = crc32fast::Hasher::new();
let mut buffer = DecodingResult::U8(vec![]); let mut buffer = DecodingResult::U8(vec![]);
let mut page = 0u32;
while decoder.more_images() { while decoder.more_images() {
decoder.next_image()?; 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(()) 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}"),
}
}
}
+1351
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+460
View File
@@ -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 <image_dir>
* Compile: cc -O2 -o verify_seq_tiff verify_seq_tiff.c -ltiff -lm
*/
#include <tiffio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <math.h>
#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 <image_dir>\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;
}