From 0309349e1bed8c7cff39247416bbca8d8047c64b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=90=AF?= Date: Mon, 22 Jun 2026 10:59:36 +0800 Subject: [PATCH] fix: add lenient mode for tiff image decoder --- src/decoder/image.rs | 18 ++++++++++++------ src/decoder/mod.rs | 32 ++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 88b63f0..f37b2b1 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. @@ -155,9 +156,10 @@ pub(crate) struct ReadoutLayout { } impl Image { - pub fn from_reader( + pub fn from_reader_with_options( 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().get(0) { + 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..7a3a9b9 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -227,6 +227,7 @@ pub struct TiffHeader { pub byte_order: ByteOrder, pub variant: TiffVariant, pub first_ifd: IfdPointer, + lenient: bool, } /// The representation of a TIFF decoder @@ -257,6 +258,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 @@ -674,7 +677,11 @@ fn fix_endianness(buf: &mut [u8], byte_order: ByteOrder, bit_depth: u8) { } impl TiffHeader { - pub fn parse(mut r: R) -> TiffResult { + pub fn parse(r: R) -> TiffResult { + Self::parse_with_options(r, false) + } + + pub fn parse_with_options(mut r: R, lenient: bool) -> TiffResult { let mut endianess = Vec::with_capacity(2); (&mut r).take(2).read_to_end(&mut endianess)?; let byte_order = match &*endianess { @@ -727,6 +734,7 @@ impl TiffHeader { TiffVariant::Standard }, first_ifd: next_ifd, + lenient, }) } @@ -750,6 +758,7 @@ impl TiffHeader { seen_ifds: cycles::IfdCycles::new(), image: None, scratch: Vec::new(), + lenient: self.lenient, } } } @@ -792,6 +801,11 @@ impl Decoder { Ok(header.open(r)) } + pub fn open_with_options(mut r: R, lenient: bool) -> TiffResult> { + let header = TiffHeader::parse_with_options(&mut r, lenient)?; + Ok(header.open(r)) + } + /// Create a decoder and attempt to parse the first image directory. /// /// To open a file without parsing any directory yet (for instance to retain a [`Decoder`] on @@ -846,7 +860,8 @@ impl Decoder { )); } - let image = Image::from_reader(&mut self.value_reader, &self.directory)?; + let image = + Image::from_reader_with_options(&mut self.value_reader, &self.directory, self.lenient)?; Ok(( self.image.insert(image), &mut self.value_reader, @@ -982,6 +997,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 +1046,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_with_options( + &mut self.value_reader, + &directory, + self.lenient, + )?); self.current_ifd_pointer = Some(offset); self.directory = directory; Ok(()) @@ -1123,7 +1143,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_with_options( + &mut self.value_reader, + &directory, + self.lenient, + )?); self.current_ifd_pointer = Some(offset); self.directory = directory; Ok(())