Merge remote-tracking branch 'origin/main' into palette-cielab-transparency-lzw
This commit is contained in:
commit
d0c9198016
12 files changed
+320
-24
No files matched your search
+1
-1
@@ -363,7 +363,7 @@ impl ::std::fmt::Debug for Entry {
|
||||
fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
|
||||
fmt.write_str(&format!(
|
||||
"Entry {{ type_: {:?}, count: {:?}, offset: {:?} }}",
|
||||
self.type_, self.count, &self.offset
|
||||
self.type_, self.count, self.offset
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
+59
-5
@@ -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<R: Read + Seek>(
|
||||
decoder: &mut ValueReader<R>,
|
||||
ifd: &Directory,
|
||||
lenient: bool,
|
||||
) -> TiffResult<Image> {
|
||||
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<ColorType> {
|
||||
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 {
|
||||
@@ -669,6 +675,12 @@ impl Image {
|
||||
PhotometricInterpretation::TransparencyMask => {
|
||||
Ok(ColorType::Gray(self.bits_per_sample))
|
||||
}
|
||||
PhotometricInterpretation::Unknown(_) => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
self.photometric_interpretation,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,6 +727,8 @@ impl Image {
|
||||
)),
|
||||
#[cfg(feature = "zstd")]
|
||||
CompressionMethod::ZSTD => Box::new(zstd::Decoder::new(reader)?),
|
||||
#[cfg(all(not(feature = "zstd"), feature = "zstd-safe-rust"))]
|
||||
CompressionMethod::ZSTD => Box::new(zrip_decode::streaming::FrameDecoder::new(reader)),
|
||||
CompressionMethod::PackBits => Box::new(PackBitsReader::new(reader, compressed_length)),
|
||||
#[cfg(feature = "deflate")]
|
||||
CompressionMethod::Deflate | CompressionMethod::OldDeflate => {
|
||||
@@ -1195,12 +1209,20 @@ impl Image {
|
||||
super::fix_endianness_and_predict(
|
||||
out_row,
|
||||
color_type.bit_depth(),
|
||||
samples,
|
||||
data_samples,
|
||||
ByteOrder::native(),
|
||||
predictor,
|
||||
scratch,
|
||||
);
|
||||
|
||||
// An n-bit predictor delta is stored modulo 2^n, but the
|
||||
// reconstruction above accumulated it in the promoted u16/u32
|
||||
// storage width. Reduce back modulo the declared depth so both
|
||||
// the inverted and non-inverted paths see exact, in-range
|
||||
// values (2^n divides the storage modulus, so one mask after
|
||||
// the row is exact).
|
||||
super::mask_samples_to_bit_depth(out_row, tiff_bps, out_bps, self.sample_format);
|
||||
|
||||
super::post_process_row(
|
||||
out_row,
|
||||
photometric_interpretation,
|
||||
@@ -1224,6 +1246,16 @@ impl Image {
|
||||
);
|
||||
}
|
||||
|
||||
// No-op in this branch: promoted layouts (declared depth < storage
|
||||
// width) are handled exclusively by the unpacking branch above.
|
||||
// Called in every integer branch so they cannot diverge.
|
||||
super::mask_samples_to_bit_depth(
|
||||
tile,
|
||||
layout.tiff_bits_per_sample,
|
||||
layout.output_bits_per_sample,
|
||||
self.sample_format,
|
||||
);
|
||||
|
||||
super::post_process_row(
|
||||
tile,
|
||||
photometric_interpretation,
|
||||
@@ -1253,6 +1285,8 @@ impl Image {
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
// No masking here: the floating point predictor requires
|
||||
// IEEEFP samples, which are never depth-promoted.
|
||||
super::post_process_row(
|
||||
row,
|
||||
photometric_interpretation,
|
||||
@@ -1285,6 +1319,16 @@ impl Image {
|
||||
scratch,
|
||||
);
|
||||
|
||||
// No-op in this branch: promoted layouts only occur in the
|
||||
// unpacking branch. Called so the integer branches cannot
|
||||
// diverge.
|
||||
super::mask_samples_to_bit_depth(
|
||||
row,
|
||||
layout.tiff_bits_per_sample,
|
||||
layout.output_bits_per_sample,
|
||||
self.sample_format,
|
||||
);
|
||||
|
||||
super::post_process_row(
|
||||
row,
|
||||
photometric_interpretation,
|
||||
@@ -1330,6 +1374,16 @@ impl Image {
|
||||
scratch,
|
||||
);
|
||||
|
||||
// No-op in this branch: promoted layouts only occur in the
|
||||
// unpacking branch. Called so the integer branches cannot
|
||||
// diverge.
|
||||
super::mask_samples_to_bit_depth(
|
||||
row,
|
||||
layout.tiff_bits_per_sample,
|
||||
layout.output_bits_per_sample,
|
||||
self.sample_format,
|
||||
);
|
||||
|
||||
super::post_process_row(
|
||||
row,
|
||||
photometric_interpretation,
|
||||
|
||||
@@ -92,11 +92,7 @@ fn packbits_decode<R: Read, const N: usize>(
|
||||
let mut i = 0;
|
||||
let mut code = [0u8; 1];
|
||||
|
||||
loop {
|
||||
let Some(pixels) = buf.get_mut(i..) else {
|
||||
break;
|
||||
};
|
||||
|
||||
while let Some(pixels) = buf.get_mut(i..) {
|
||||
if pixels.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
+76
-5
@@ -257,6 +257,8 @@ pub struct Decoder<R> {
|
||||
/// 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<u8>,
|
||||
/// 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
|
||||
@@ -566,6 +568,56 @@ fn fix_endianness_and_predict(
|
||||
}
|
||||
}
|
||||
|
||||
/// Reduce promoted extended-depth samples (9–15 bits → `u16`, 17–31 bits →
|
||||
/// `u32`) modulo their declared bit depth, i.e. mask with `(1 << n) - 1`.
|
||||
///
|
||||
/// An n-bit horizontal-predicted sample stores its delta in exactly n bits,
|
||||
/// i.e. modulo `2^n`, so reconstruction is also defined modulo `2^n`. The
|
||||
/// predictor accumulates `prev + delta` in the promoted storage width
|
||||
/// (`u16`/`u32`, see `rev_hpredict_nsamp`); since `2^n` divides the storage
|
||||
/// modulus, masking once after the predictor yields exactly the mod-`2^n`
|
||||
/// value for every sample — including legitimate wrap-arounds (a 9-bit
|
||||
/// `511, 0` pair is stored as delta `1`, and `511 + 1 = 512` masks back to
|
||||
/// `0`). Masking also bounds any out-of-range sample in malformed input, so
|
||||
/// all downstream consumers (`invert_colors`' `max - v` and the non-inverted
|
||||
/// output path) see in-range values. For samples already in range it is a
|
||||
/// no-op.
|
||||
///
|
||||
/// This runs in every integer decode branch of `expand_chunk` so the
|
||||
/// branches cannot diverge. Outside the unpacking branch it reduces to the
|
||||
/// two guards below and touches nothing: promoted layouts (`declared <
|
||||
/// storage`) exist only there — standard depths have `declared == storage`,
|
||||
/// and sub-byte depths keep byte-granular storage.
|
||||
#[inline]
|
||||
fn mask_samples_to_bit_depth(
|
||||
buf: &mut [u8],
|
||||
declared_bits: u8,
|
||||
storage_bits: u8,
|
||||
sample_format: SampleFormat,
|
||||
) {
|
||||
// Only unsigned samples are promoted with zero-extension; signed
|
||||
// extended depths would need sign extension instead and are not
|
||||
// supported by the unpacking path.
|
||||
if sample_format != SampleFormat::Uint {
|
||||
return;
|
||||
}
|
||||
match storage_bits {
|
||||
16 if declared_bits < 16 => {
|
||||
let mask = (1u16 << declared_bits) - 1;
|
||||
for chunk in buf.as_chunks_mut::<2>().0 {
|
||||
*chunk = (u16::from_ne_bytes(*chunk) & mask).to_ne_bytes();
|
||||
}
|
||||
}
|
||||
32 if declared_bits < 32 => {
|
||||
let mask = (1u32 << declared_bits) - 1;
|
||||
for chunk in buf.as_chunks_mut::<4>().0 {
|
||||
*chunk = (u32::from_ne_bytes(*chunk) & mask).to_ne_bytes();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn invert_colors(
|
||||
buf: &mut [u8],
|
||||
color_type: ColorType,
|
||||
@@ -664,8 +716,8 @@ fn invert_colors(
|
||||
/// CIELab (photometric=8) stores a/b as int8 where 0 is neutral.
|
||||
/// ICCLab (photometric=9) stores them as uint8 where 128 is neutral.
|
||||
/// Adding 128 (wrapping) converts between the two representations.
|
||||
/// See libtiff's TIFFCIELabToXYZ for reference:
|
||||
/// https://gitlab.com/libtiff/libtiff/-/blob/master/libtiff/tif_color.c
|
||||
/// See libtiff's `TIFFCIELabToXYZ` for reference (tif_color.c line 45
|
||||
/// @ 04884181e8903915038be553bfab404302d98ea0).
|
||||
fn cielab_to_icclab(buf: &mut [u8]) {
|
||||
for [_, a, b] in buf.as_chunks_mut::<3>().0 {
|
||||
*a = a.wrapping_add(128);
|
||||
@@ -784,6 +836,7 @@ impl TiffHeader {
|
||||
seen_ifds: cycles::IfdCycles::new(),
|
||||
image: None,
|
||||
scratch: Vec::new(),
|
||||
lenient: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -866,6 +919,15 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
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<R> {
|
||||
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.
|
||||
@@ -880,7 +942,7 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
));
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -1016,6 +1078,7 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
current_ifd_pointer: self.current_ifd_pointer,
|
||||
image: core::mem::take(&mut self.image),
|
||||
scratch: core::mem::take(&mut self.scratch),
|
||||
lenient: self.lenient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1064,7 +1127,11 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
/// 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(())
|
||||
@@ -1157,7 +1224,11 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
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(())
|
||||
|
||||
+4
-1
@@ -295,7 +295,10 @@ pub enum CompressionMethod(u16) unknown(
|
||||
}
|
||||
|
||||
tags! {
|
||||
pub enum PhotometricInterpretation(u16) {
|
||||
pub enum PhotometricInterpretation(u16) unknown(
|
||||
/// A private or extension tag
|
||||
unknown
|
||||
) {
|
||||
WhiteIsZero = 0,
|
||||
BlackIsZero = 1,
|
||||
RGB = 2,
|
||||
|
||||
Reference in new issue
Block a user