diff --git a/examples/decode.rs b/examples/decode.rs index 9594e40..6b52533 100644 --- a/examples/decode.rs +++ b/examples/decode.rs @@ -2,13 +2,15 @@ use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingSampleBuffer, Decod fn main() -> Result<(), Box> { let Some(image) = std::env::args_os().nth(1) else { - eprintln!("Usage: decode FILE"); + eprintln!("Usage: decode FILE [--lenient]"); return Ok(()); }; + let lenient = std::env::args().nth(2).as_deref() == Some("--lenient"); + let file = std::fs::File::open(image)?; let io = std::io::BufReader::new(file); - let mut reader = Decoder::open(io)?; + let mut reader = Decoder::open(io)?.with_lenient(lenient); let mut data = DecodingSampleBuffer::I8(vec![]); diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 88b63f0..b90e25c 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -97,6 +97,7 @@ pub(crate) struct Image { /// run on samples directly and thus skip the outer byte order entirely, except potentially /// they have interior byte order treatment. pub decompression_to_host_endian: bool, + pub lenient: bool, } /// Describes how to read a tile-aligned portion of the image. @@ -158,6 +159,7 @@ impl Image { pub fn from_reader( decoder: &mut ValueReader, ifd: &Directory, + lenient: bool, ) -> TiffResult { let mut tag_reader = TagReader { decoder, ifd }; @@ -500,6 +502,7 @@ impl Image { compression_method, CompressionMethod::SgiLog | CompressionMethod::SgiLog24 ), + lenient, }) } @@ -515,10 +518,13 @@ impl Image { } pub(crate) fn colortype(&self) -> TiffResult { - let is_alpha_extra_samples = matches!( - self.extra_samples.as_slice(), - [ExtraSamples::AssociatedAlpha, ..] | [ExtraSamples::UnassociatedAlpha, ..] - ); + let is_alpha_extra_samples = match self.extra_samples.as_slice().first() { + Some(ExtraSamples::AssociatedAlpha) | Some(ExtraSamples::UnassociatedAlpha) => true, + // In lenient mode, keep unspecified alpha interpretation logic compatible with libtiff + // See Issue #341 + Some(ExtraSamples::Unspecified) if self.lenient && self.samples > 3 => true, + _ => false, + }; match self.photometric_interpretation { PhotometricInterpretation::RGB => match self.photometric_samples { @@ -1192,7 +1198,7 @@ impl Image { super::fix_endianness_and_predict( out_row, color_type.bit_depth(), - samples, + data_samples, ByteOrder::native(), predictor, scratch, diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index cfc19ef..f2f39af 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -257,6 +257,8 @@ pub struct Decoder { /// Reusable scratch buffer for the floating-point predictor. Stored here so that the /// allocation persists across strips/tiles and images, avoiding per-row heap allocation. scratch: Vec, + /// Leniency flag that allows libtiff-like TIFF color interpretation behavior + lenient: bool, } /// All the information needed to read and interpret byte slices from the underlying file, i.e. to @@ -750,6 +752,7 @@ impl TiffHeader { seen_ifds: cycles::IfdCycles::new(), image: None, scratch: Vec::new(), + lenient: false, } } } @@ -832,6 +835,15 @@ impl Decoder { self } + /// Use a lenient decoder. + /// + /// Configuring this make the parser respect libtiff's behavior to maintain compatibility with + /// some older softwares (e.g. some old Adobe suite products). + pub fn with_lenient(mut self, lenient: bool) -> Decoder { + self.lenient = lenient; + self + } + // FIXME: this tuple would be a good candidate for a proxy, like IfdDecoder. That has been // requested in some form or another in a number of issues. #[expect(clippy::unnecessary_unwrap)] // Lifetimes would overlap due to return. @@ -846,7 +858,7 @@ impl Decoder { )); } - let image = Image::from_reader(&mut self.value_reader, &self.directory)?; + let image = Image::from_reader(&mut self.value_reader, &self.directory, self.lenient)?; Ok(( self.image.insert(image), &mut self.value_reader, @@ -982,6 +994,7 @@ impl Decoder { current_ifd_pointer: self.current_ifd_pointer, image: core::mem::take(&mut self.image), scratch: core::mem::take(&mut self.scratch), + lenient: self.lenient, } } @@ -1030,7 +1043,11 @@ impl Decoder { /// error is returned. pub fn seek_to_image(&mut self, ifd_index: usize) -> TiffResult<()> { let (offset, directory) = self.find_nth_ifd(ifd_index)?; - self.image = Some(Image::from_reader(&mut self.value_reader, &directory)?); + self.image = Some(Image::from_reader( + &mut self.value_reader, + &directory, + self.lenient, + )?); self.current_ifd_pointer = Some(offset); self.directory = directory; Ok(()) @@ -1123,7 +1140,11 @@ impl Decoder { pub fn next_image(&mut self) -> TiffResult<()> { let next_ifd = self.directory.next(); let (offset, directory) = self.read_chained_ifd(self.current_ifd_pointer, next_ifd)?; - self.image = Some(Image::from_reader(&mut self.value_reader, &directory)?); + self.image = Some(Image::from_reader( + &mut self.value_reader, + &directory, + self.lenient, + )?); self.current_ifd_pointer = Some(offset); self.directory = directory; Ok(()) diff --git a/tests/decode_images.rs b/tests/decode_images.rs index a68380a..d2cafd3 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -16,7 +16,9 @@ macro_rules! test_image_sum { fn $name(file: &str, expected_type: ColorType, expected_sum: $sum_ty) { let path = PathBuf::from(TEST_IMAGE_DIR).join(file); let img_file = File::open(path).expect("Cannot find test image!"); - let mut decoder = Decoder::open(img_file).expect("Cannot create decoder"); + let mut decoder = Decoder::open(img_file) + .expect("Cannot create decoder") + .with_lenient(true); decoder.next_image().expect("Cannot read image IFD"); assert_eq!(decoder.colortype().unwrap(), expected_type); @@ -975,7 +977,7 @@ fn extra_bits_gray() { #[test] fn extra_bits_rgb() { - test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGB(8), 0); + test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGBA(8), 64); } #[test]