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
+5
-1
@@ -23,8 +23,9 @@ quick-error = "2.0.1"
|
|||||||
# Rename to avoid confusion with the feature name.
|
# Rename to avoid confusion with the feature name.
|
||||||
fax34 = { package = "fax", version = "0.2.6", optional = true }
|
fax34 = { package = "fax", version = "0.2.6", optional = true }
|
||||||
flate2 = { version = "1.0.20", optional = true }
|
flate2 = { version = "1.0.20", optional = true }
|
||||||
weezl = { version = "0.1.10", optional = true }
|
weezl = { version = "0.2.1", optional = true }
|
||||||
zstd = { version = "0.13", optional = true }
|
zstd = { version = "0.13", optional = true }
|
||||||
|
zrip-decode = { version = "0.8.2", features = ["paranoid"], optional = true }
|
||||||
zune-jpeg = { version = "0.5.1", optional = true }
|
zune-jpeg = { version = "0.5.1", optional = true }
|
||||||
image-webp = {version = "0.2.4", optional = true }
|
image-webp = {version = "0.2.4", optional = true }
|
||||||
|
|
||||||
@@ -45,6 +46,9 @@ jpeg = ["dep:zune-jpeg"]
|
|||||||
lzw = ["dep:weezl"]
|
lzw = ["dep:weezl"]
|
||||||
webp = ["dep:image-webp"]
|
webp = ["dep:image-webp"]
|
||||||
zstd = ["dep:zstd"]
|
zstd = ["dep:zstd"]
|
||||||
|
# Fully safe Rust but somewhat slower Zstd decoding.
|
||||||
|
# When both zstd and zstd-safe-rust are enabled, zstd takes precedence.
|
||||||
|
zstd-safe-rust = ["dep:zrip-decode"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "lzw"
|
name = "lzw"
|
||||||
|
|||||||
+1
-1
@@ -9,9 +9,9 @@ use tiff::decoder::Decoder;
|
|||||||
fn read_image(image: &[u8]) {
|
fn read_image(image: &[u8]) {
|
||||||
let image = std::io::Cursor::new(image);
|
let image = std::io::Cursor::new(image);
|
||||||
let decoder = Decoder::open(black_box(image));
|
let decoder = Decoder::open(black_box(image));
|
||||||
|
|
||||||
let mut reader = decoder.unwrap();
|
let mut reader = decoder.unwrap();
|
||||||
while reader.more_images() {
|
while reader.more_images() {
|
||||||
|
reader.next_directory().unwrap();
|
||||||
reader.read_image().unwrap();
|
reader.read_image().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -2,13 +2,15 @@ use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingSampleBuffer, Decod
|
|||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let Some(image) = std::env::args_os().nth(1) else {
|
let Some(image) = std::env::args_os().nth(1) else {
|
||||||
eprintln!("Usage: decode FILE");
|
eprintln!("Usage: decode FILE [--lenient]");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let lenient = std::env::args().nth(2).as_deref() == Some("--lenient");
|
||||||
|
|
||||||
let file = std::fs::File::open(image)?;
|
let file = std::fs::File::open(image)?;
|
||||||
let io = std::io::BufReader::new(file);
|
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![]);
|
let mut data = DecodingSampleBuffer::I8(vec![]);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -363,7 +363,7 @@ impl ::std::fmt::Debug for Entry {
|
|||||||
fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
|
fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
|
||||||
fmt.write_str(&format!(
|
fmt.write_str(&format!(
|
||||||
"Entry {{ type_: {:?}, count: {:?}, offset: {:?} }}",
|
"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
|
/// run on samples directly and thus skip the outer byte order entirely, except potentially
|
||||||
/// they have interior byte order treatment.
|
/// they have interior byte order treatment.
|
||||||
pub decompression_to_host_endian: bool,
|
pub decompression_to_host_endian: bool,
|
||||||
|
pub lenient: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Describes how to read a tile-aligned portion of the image.
|
/// Describes how to read a tile-aligned portion of the image.
|
||||||
@@ -158,6 +159,7 @@ impl Image {
|
|||||||
pub fn from_reader<R: Read + Seek>(
|
pub fn from_reader<R: Read + Seek>(
|
||||||
decoder: &mut ValueReader<R>,
|
decoder: &mut ValueReader<R>,
|
||||||
ifd: &Directory,
|
ifd: &Directory,
|
||||||
|
lenient: bool,
|
||||||
) -> TiffResult<Image> {
|
) -> TiffResult<Image> {
|
||||||
let mut tag_reader = TagReader { decoder, ifd };
|
let mut tag_reader = TagReader { decoder, ifd };
|
||||||
|
|
||||||
@@ -500,6 +502,7 @@ impl Image {
|
|||||||
compression_method,
|
compression_method,
|
||||||
CompressionMethod::SgiLog | CompressionMethod::SgiLog24
|
CompressionMethod::SgiLog | CompressionMethod::SgiLog24
|
||||||
),
|
),
|
||||||
|
lenient,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,10 +518,13 @@ impl Image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
|
pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
|
||||||
let is_alpha_extra_samples = matches!(
|
let is_alpha_extra_samples = match self.extra_samples.as_slice().first() {
|
||||||
self.extra_samples.as_slice(),
|
Some(ExtraSamples::AssociatedAlpha) | Some(ExtraSamples::UnassociatedAlpha) => true,
|
||||||
[ExtraSamples::AssociatedAlpha, ..] | [ExtraSamples::UnassociatedAlpha, ..]
|
// 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 {
|
match self.photometric_interpretation {
|
||||||
PhotometricInterpretation::RGB => match self.photometric_samples {
|
PhotometricInterpretation::RGB => match self.photometric_samples {
|
||||||
@@ -669,6 +675,12 @@ impl Image {
|
|||||||
PhotometricInterpretation::TransparencyMask => {
|
PhotometricInterpretation::TransparencyMask => {
|
||||||
Ok(ColorType::Gray(self.bits_per_sample))
|
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")]
|
#[cfg(feature = "zstd")]
|
||||||
CompressionMethod::ZSTD => Box::new(zstd::Decoder::new(reader)?),
|
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)),
|
CompressionMethod::PackBits => Box::new(PackBitsReader::new(reader, compressed_length)),
|
||||||
#[cfg(feature = "deflate")]
|
#[cfg(feature = "deflate")]
|
||||||
CompressionMethod::Deflate | CompressionMethod::OldDeflate => {
|
CompressionMethod::Deflate | CompressionMethod::OldDeflate => {
|
||||||
@@ -1195,12 +1209,20 @@ impl Image {
|
|||||||
super::fix_endianness_and_predict(
|
super::fix_endianness_and_predict(
|
||||||
out_row,
|
out_row,
|
||||||
color_type.bit_depth(),
|
color_type.bit_depth(),
|
||||||
samples,
|
data_samples,
|
||||||
ByteOrder::native(),
|
ByteOrder::native(),
|
||||||
predictor,
|
predictor,
|
||||||
scratch,
|
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(
|
super::post_process_row(
|
||||||
out_row,
|
out_row,
|
||||||
photometric_interpretation,
|
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(
|
super::post_process_row(
|
||||||
tile,
|
tile,
|
||||||
photometric_interpretation,
|
photometric_interpretation,
|
||||||
@@ -1253,6 +1285,8 @@ impl Image {
|
|||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
// No masking here: the floating point predictor requires
|
||||||
|
// IEEEFP samples, which are never depth-promoted.
|
||||||
super::post_process_row(
|
super::post_process_row(
|
||||||
row,
|
row,
|
||||||
photometric_interpretation,
|
photometric_interpretation,
|
||||||
@@ -1285,6 +1319,16 @@ impl Image {
|
|||||||
scratch,
|
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(
|
super::post_process_row(
|
||||||
row,
|
row,
|
||||||
photometric_interpretation,
|
photometric_interpretation,
|
||||||
@@ -1330,6 +1374,16 @@ impl Image {
|
|||||||
scratch,
|
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(
|
super::post_process_row(
|
||||||
row,
|
row,
|
||||||
photometric_interpretation,
|
photometric_interpretation,
|
||||||
|
|||||||
@@ -92,11 +92,7 @@ fn packbits_decode<R: Read, const N: usize>(
|
|||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
let mut code = [0u8; 1];
|
let mut code = [0u8; 1];
|
||||||
|
|
||||||
loop {
|
while let Some(pixels) = buf.get_mut(i..) {
|
||||||
let Some(pixels) = buf.get_mut(i..) else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
if pixels.is_empty() {
|
if pixels.is_empty() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-5
@@ -257,6 +257,8 @@ pub struct Decoder<R> {
|
|||||||
/// Reusable scratch buffer for the floating-point predictor. Stored here so that the
|
/// 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.
|
/// allocation persists across strips/tiles and images, avoiding per-row heap allocation.
|
||||||
scratch: Vec<u8>,
|
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
|
/// 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(
|
fn invert_colors(
|
||||||
buf: &mut [u8],
|
buf: &mut [u8],
|
||||||
color_type: ColorType,
|
color_type: ColorType,
|
||||||
@@ -664,8 +716,8 @@ fn invert_colors(
|
|||||||
/// CIELab (photometric=8) stores a/b as int8 where 0 is neutral.
|
/// CIELab (photometric=8) stores a/b as int8 where 0 is neutral.
|
||||||
/// ICCLab (photometric=9) stores them as uint8 where 128 is neutral.
|
/// ICCLab (photometric=9) stores them as uint8 where 128 is neutral.
|
||||||
/// Adding 128 (wrapping) converts between the two representations.
|
/// Adding 128 (wrapping) converts between the two representations.
|
||||||
/// See libtiff's TIFFCIELabToXYZ for reference:
|
/// See libtiff's `TIFFCIELabToXYZ` for reference (tif_color.c line 45
|
||||||
/// https://gitlab.com/libtiff/libtiff/-/blob/master/libtiff/tif_color.c
|
/// @ 04884181e8903915038be553bfab404302d98ea0).
|
||||||
fn cielab_to_icclab(buf: &mut [u8]) {
|
fn cielab_to_icclab(buf: &mut [u8]) {
|
||||||
for [_, a, b] in buf.as_chunks_mut::<3>().0 {
|
for [_, a, b] in buf.as_chunks_mut::<3>().0 {
|
||||||
*a = a.wrapping_add(128);
|
*a = a.wrapping_add(128);
|
||||||
@@ -784,6 +836,7 @@ impl TiffHeader {
|
|||||||
seen_ifds: cycles::IfdCycles::new(),
|
seen_ifds: cycles::IfdCycles::new(),
|
||||||
image: None,
|
image: None,
|
||||||
scratch: Vec::new(),
|
scratch: Vec::new(),
|
||||||
|
lenient: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -866,6 +919,15 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
self
|
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
|
// 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.
|
// requested in some form or another in a number of issues.
|
||||||
#[expect(clippy::unnecessary_unwrap)] // Lifetimes would overlap due to return.
|
#[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((
|
Ok((
|
||||||
self.image.insert(image),
|
self.image.insert(image),
|
||||||
&mut self.value_reader,
|
&mut self.value_reader,
|
||||||
@@ -1016,6 +1078,7 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
current_ifd_pointer: self.current_ifd_pointer,
|
current_ifd_pointer: self.current_ifd_pointer,
|
||||||
image: core::mem::take(&mut self.image),
|
image: core::mem::take(&mut self.image),
|
||||||
scratch: core::mem::take(&mut self.scratch),
|
scratch: core::mem::take(&mut self.scratch),
|
||||||
|
lenient: self.lenient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1064,7 +1127,11 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
/// error is returned.
|
/// error is returned.
|
||||||
pub fn seek_to_image(&mut self, ifd_index: usize) -> TiffResult<()> {
|
pub fn seek_to_image(&mut self, ifd_index: usize) -> TiffResult<()> {
|
||||||
let (offset, directory) = self.find_nth_ifd(ifd_index)?;
|
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.current_ifd_pointer = Some(offset);
|
||||||
self.directory = directory;
|
self.directory = directory;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1157,7 +1224,11 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
pub fn next_image(&mut self) -> TiffResult<()> {
|
pub fn next_image(&mut self) -> TiffResult<()> {
|
||||||
let next_ifd = self.directory.next();
|
let next_ifd = self.directory.next();
|
||||||
let (offset, directory) = self.read_chained_ifd(self.current_ifd_pointer, next_ifd)?;
|
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.current_ifd_pointer = Some(offset);
|
||||||
self.directory = directory;
|
self.directory = directory;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+4
-1
@@ -295,7 +295,10 @@ pub enum CompressionMethod(u16) unknown(
|
|||||||
}
|
}
|
||||||
|
|
||||||
tags! {
|
tags! {
|
||||||
pub enum PhotometricInterpretation(u16) {
|
pub enum PhotometricInterpretation(u16) unknown(
|
||||||
|
/// A private or extension tag
|
||||||
|
unknown
|
||||||
|
) {
|
||||||
WhiteIsZero = 0,
|
WhiteIsZero = 0,
|
||||||
BlackIsZero = 1,
|
BlackIsZero = 1,
|
||||||
RGB = 2,
|
RGB = 2,
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ macro_rules! test_image_sum {
|
|||||||
fn $name(file: &str, expected_type: ColorType, expected_sum: $sum_ty) {
|
fn $name(file: &str, expected_type: ColorType, expected_sum: $sum_ty) {
|
||||||
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!");
|
||||||
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");
|
decoder.next_image().expect("Cannot read image IFD");
|
||||||
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
||||||
@@ -825,7 +827,7 @@ fn test_predictor_3_gray_f32() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "zstd")]
|
#[cfg(any(feature = "zstd", feature = "zstd-safe-rust"))]
|
||||||
fn test_zstd_compression() {
|
fn test_zstd_compression() {
|
||||||
// gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif
|
// gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif
|
||||||
test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396);
|
test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396);
|
||||||
@@ -975,7 +977,7 @@ fn extra_bits_gray() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extra_bits_rgb() {
|
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]
|
#[test]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -233,3 +233,167 @@ fn test_rgb_u64_predict_roundtrip() {
|
|||||||
fn test_ycbcr_u8_predict_roundtrip() {
|
fn test_ycbcr_u8_predict_roundtrip() {
|
||||||
test_u8_predict_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
|
test_u8_predict_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Extended bit depths (9-15 promoted to u16, 17-31 promoted to u32).
|
||||||
|
//
|
||||||
|
// A horizontal-predicted n-bit sample stores its delta in exactly n bits,
|
||||||
|
// i.e. modulo 2^n, so reconstruction must also be modulo 2^n. The decoder
|
||||||
|
// accumulates in the promoted storage width and reduces back to n bits;
|
||||||
|
// these tests pin that reduction down with exact pixel values, including
|
||||||
|
// deltas that wrap modulo 2^n. The encoder does not support extended
|
||||||
|
// depths, so the TIFFs are hand-built.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Hand-build a minimal little-endian, single-strip, uncompressed grayscale
|
||||||
|
/// TIFF with `Predictor::Horizontal` and the given (possibly non-standard)
|
||||||
|
/// bit depth. `pixels` are the intended sample values; they are converted to
|
||||||
|
/// horizontal differences modulo `2^bits` and bit-packed MSB-first with each
|
||||||
|
/// row padded to a byte boundary — the only representation an n-bit field
|
||||||
|
/// can hold.
|
||||||
|
fn build_gray_hpredict_tiff(
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
bits: u8,
|
||||||
|
photometric: u16,
|
||||||
|
pixels: &[u32],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let max = ((1u64 << bits) - 1) as u32;
|
||||||
|
let row_samples = width as usize;
|
||||||
|
assert_eq!(pixels.len(), row_samples * height as usize);
|
||||||
|
assert!(pixels.iter().all(|&v| v <= max));
|
||||||
|
|
||||||
|
// Per-row horizontal differencing modulo 2^bits.
|
||||||
|
let mut deltas = Vec::with_capacity(pixels.len());
|
||||||
|
for row in pixels.chunks(row_samples) {
|
||||||
|
for (i, &v) in row.iter().enumerate() {
|
||||||
|
deltas.push(if i == 0 {
|
||||||
|
v
|
||||||
|
} else {
|
||||||
|
v.wrapping_sub(row[i - 1]) & max
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bit-pack MSB-first, each row padded to a byte boundary.
|
||||||
|
let row_bytes = (row_samples * bits as usize).div_ceil(8);
|
||||||
|
let mut data = vec![0u8; row_bytes * height as usize];
|
||||||
|
for (r, row) in deltas.chunks(row_samples).enumerate() {
|
||||||
|
for (i, &d) in row.iter().enumerate() {
|
||||||
|
let start = r * row_bytes * 8 + i * bits as usize;
|
||||||
|
for (off, b) in (start..).zip((0..bits).rev()) {
|
||||||
|
if (d >> b) & 1 == 1 {
|
||||||
|
data[off / 8] |= 0x80 >> (off % 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classic little-endian TIFF: header, strip data, then a single IFD.
|
||||||
|
let mut out = Vec::new();
|
||||||
|
out.extend_from_slice(b"II");
|
||||||
|
out.extend_from_slice(&42u16.to_le_bytes());
|
||||||
|
let data_offset = 8u32;
|
||||||
|
let ifd_offset = data_offset + data.len() as u32;
|
||||||
|
out.extend_from_slice(&ifd_offset.to_le_bytes());
|
||||||
|
out.extend_from_slice(&data);
|
||||||
|
|
||||||
|
const SHORT: u16 = 3;
|
||||||
|
const LONG: u16 = 4;
|
||||||
|
let entries: &[(u16, u16, u32)] = &[
|
||||||
|
(256, LONG, width), // ImageWidth
|
||||||
|
(257, LONG, height), // ImageLength
|
||||||
|
(258, SHORT, u32::from(bits)), // BitsPerSample
|
||||||
|
(259, SHORT, 1), // Compression: none
|
||||||
|
(262, SHORT, u32::from(photometric)), // PhotometricInterpretation
|
||||||
|
(273, LONG, data_offset), // StripOffsets
|
||||||
|
(277, SHORT, 1), // SamplesPerPixel
|
||||||
|
(278, LONG, height), // RowsPerStrip
|
||||||
|
(279, LONG, data.len() as u32), // StripByteCounts
|
||||||
|
(317, SHORT, 2), // Predictor: horizontal
|
||||||
|
(339, SHORT, 1), // SampleFormat: unsigned integer
|
||||||
|
];
|
||||||
|
out.extend_from_slice(&(entries.len() as u16).to_le_bytes());
|
||||||
|
for &(tag, ty, value) in entries {
|
||||||
|
out.extend_from_slice(&tag.to_le_bytes());
|
||||||
|
out.extend_from_slice(&ty.to_le_bytes());
|
||||||
|
out.extend_from_slice(&1u32.to_le_bytes());
|
||||||
|
// A count-1 SHORT payload occupies the low bytes of the value field.
|
||||||
|
out.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes()); // no next IFD
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_gray_u16(tiff: Vec<u8>, bits: u8) -> Vec<u16> {
|
||||||
|
let mut decoder = Decoder::open(Cursor::new(tiff)).expect("Cannot create decoder");
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(bits));
|
||||||
|
match decoder.read_image().unwrap() {
|
||||||
|
DecodingSampleBuffer::U16(res) => res,
|
||||||
|
_ => panic!("Wrong data type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_gray_u32(tiff: Vec<u8>, bits: u8) -> Vec<u32> {
|
||||||
|
let mut decoder = Decoder::open(Cursor::new(tiff)).expect("Cannot create decoder");
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(bits));
|
||||||
|
match decoder.read_image().unwrap() {
|
||||||
|
DecodingSampleBuffer::U32(res) => res,
|
||||||
|
_ => panic!("Wrong data type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gray12_hpredict_wraparound_exact() {
|
||||||
|
// Pixel row [4095, 0, 1] encodes as deltas [4095, 1, 1] (mod 2^12).
|
||||||
|
// u16 accumulation yields [4095, 4096, 4097]; reduction modulo 2^12
|
||||||
|
// must restore [4095, 0, 1]. Saturating instead would corrupt the two
|
||||||
|
// wrapped samples to 4095.
|
||||||
|
let tiff = build_gray_hpredict_tiff(3, 1, 12, 1, &[4095, 0, 1]);
|
||||||
|
assert_eq!(decode_gray_u16(tiff, 12), vec![4095, 0, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gray12_hpredict_wraparound_white_is_zero_exact() {
|
||||||
|
// Same stored samples with PhotometricInterpretation = WhiteIsZero:
|
||||||
|
// after the mod-2^12 reduction the decoder inverts as (2^12 - 1) - v.
|
||||||
|
let tiff = build_gray_hpredict_tiff(3, 1, 12, 0, &[4095, 0, 1]);
|
||||||
|
assert_eq!(decode_gray_u16(tiff, 12), vec![0, 4095, 4094]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gray20_hpredict_wraparound_exact() {
|
||||||
|
// The same wrap-around shape on the u32 storage path (17-31 bits).
|
||||||
|
let tiff = build_gray_hpredict_tiff(3, 1, 20, 1, &[0xF_FFFF, 0, 1]);
|
||||||
|
assert_eq!(decode_gray_u32(tiff, 20), vec![0xF_FFFF, 0, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extended_depth_hpredict_random_roundtrip() {
|
||||||
|
// Deterministic pseudo-random pixels across a spread of extended depths;
|
||||||
|
// encode-then-decode must be the identity for every sample, including
|
||||||
|
// every mod-2^n wrap-around the differencing produces. Three rows verify
|
||||||
|
// that prediction restarts per row.
|
||||||
|
let mut state = 0x243F_6A88_85A3_08D3u64;
|
||||||
|
let mut next = move || {
|
||||||
|
state = state
|
||||||
|
.wrapping_mul(6364136223846793005)
|
||||||
|
.wrapping_add(1442695040888963407);
|
||||||
|
(state >> 33) as u32
|
||||||
|
};
|
||||||
|
for &bits in &[9u8, 11, 12, 15] {
|
||||||
|
let max = (1u32 << bits) - 1;
|
||||||
|
let pixels: Vec<u32> = (0..16 * 3).map(|_| next() & max).collect();
|
||||||
|
let tiff = build_gray_hpredict_tiff(16, 3, bits, 1, &pixels);
|
||||||
|
let expected: Vec<u16> = pixels.iter().map(|&v| v as u16).collect();
|
||||||
|
assert_eq!(decode_gray_u16(tiff, bits), expected, "bits={bits}");
|
||||||
|
}
|
||||||
|
for &bits in &[17u8, 20, 25, 31] {
|
||||||
|
let max = ((1u64 << bits) - 1) as u32;
|
||||||
|
let pixels: Vec<u32> = (0..16 * 3).map(|_| next() & max).collect();
|
||||||
|
let tiff = build_gray_hpredict_tiff(16, 3, bits, 1, &pixels);
|
||||||
|
assert_eq!(decode_gray_u32(tiff, bits), pixels, "bits={bits}");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user