Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cad95f255e | ||
|
|
988a5a0c07 | ||
|
|
5942111bb5 | ||
|
|
e27f6fe00d | ||
|
|
46b19cdacf | ||
|
|
c27fd94e1d | ||
|
|
f28f5fc830 | ||
|
|
3436a2222b | ||
|
|
31919c7bfc | ||
|
|
d0c9198016 | ||
|
|
8f83f0fa82 | ||
|
|
f3f9ff1244 | ||
|
|
a7dda6f4b1 | ||
|
|
35c14f1e3c | ||
|
|
a9cf5e17b9 | ||
|
|
6276defac6 | ||
|
|
dee0c2fb11 | ||
|
|
11133cb1e3 | ||
|
|
8ad05b85db | ||
|
|
4a9e1d152b | ||
|
|
1c6001ea5d | ||
|
|
c0043c1a84 | ||
|
|
c7a1176381 | ||
|
|
a658c09f09 | ||
|
|
0309349e1b | ||
|
|
279851a9a9 | ||
|
|
22b4c39254 | ||
|
|
f11a1c5393 | ||
|
|
c4285aea4a | ||
|
|
9924ea2721 | ||
|
|
30e04f3381 | ||
|
|
dfd0191403 | ||
|
|
19caae4f53 | ||
|
|
61dbcc1392 | ||
|
|
d9d4f0a905 |
No files matched your search
@@ -1,5 +1,12 @@
|
||||
# Version 0.12.0 (unreleased)
|
||||
|
||||
Fixes:
|
||||
- Decoding a tiled image whose width is not a multiple of the tile width no
|
||||
longer corrupts the columns below the right border tiles when extra samples
|
||||
are discarded (e.g. RGB with an unspecified extra sample). The horizontal
|
||||
tile padding was written past the end of the output row, spilling into the
|
||||
leftmost columns of the following row.
|
||||
|
||||
Additions:
|
||||
- Added the `TiffHeader` type. It can be used to create a `Decoder` in a state
|
||||
before the first IFD and allows opening files at an arbitrary first IFD
|
||||
|
||||
@@ -25,6 +25,7 @@ fax34 = { package = "fax", version = "0.2.6", optional = true }
|
||||
flate2 = { version = "1.0.20", optional = true }
|
||||
weezl = { version = "0.2.1", 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 }
|
||||
image-webp = {version = "0.2.4", optional = true }
|
||||
|
||||
@@ -45,6 +46,9 @@ jpeg = ["dep:zune-jpeg"]
|
||||
lzw = ["dep:weezl"]
|
||||
webp = ["dep:image-webp"]
|
||||
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]]
|
||||
name = "lzw"
|
||||
|
||||
+4
-2
@@ -2,13 +2,15 @@ use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingSampleBuffer, Decod
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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![]);
|
||||
|
||||
|
||||
+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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
+132
-42
@@ -1,8 +1,9 @@
|
||||
use super::ifd::Value;
|
||||
use super::stream::PackBitsReader;
|
||||
use super::stream::{PackBitsReader, ReadSamples};
|
||||
use super::tag_reader::TagReader;
|
||||
use super::ChunkType;
|
||||
use super::{predict_f16, predict_f32, predict_f64, ValueReader};
|
||||
use crate::decoder::stream::CompressionEngines;
|
||||
use crate::tags::{
|
||||
ByteOrder, CompressionMethod, ExtraSamples, PhotometricInterpretation, PlanarConfiguration,
|
||||
Predictor, SampleFormat, Tag,
|
||||
@@ -11,7 +12,7 @@ use crate::{
|
||||
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
|
||||
};
|
||||
|
||||
use std::io::{self, Cursor, Read, Seek};
|
||||
use std::io::{Cursor, Read, Seek};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -97,6 +98,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 +160,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 +503,7 @@ impl Image {
|
||||
compression_method,
|
||||
CompressionMethod::SgiLog | CompressionMethod::SgiLog24
|
||||
),
|
||||
lenient,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -515,10 +519,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 {
|
||||
@@ -591,20 +598,30 @@ impl Image {
|
||||
),
|
||||
)),
|
||||
},
|
||||
// Unsupported due to inherently heterogeneous sample types. This is represented as:
|
||||
// ```
|
||||
// struct TiffLab /* Interpretation 8* {
|
||||
// pub L: u8, // SampleFormat::Uint
|
||||
// pub a: i8, // SampleFormat::Int
|
||||
// pub b: i8, // SampleFormat::Int
|
||||
// }
|
||||
// ```
|
||||
PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
PhotometricInterpretation::CIELab,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
)),
|
||||
// CIE L*a*b* (TIFF6 Section 23, pp 110-115) encodes a/b as signed integers
|
||||
// (int8 at 8-bit, int16 at 16-bit) with 0 neutral, while ICC L*a*b*
|
||||
// (photometric=9) uses unsigned samples with the neutral at the middle of
|
||||
// the range. `SampleFormat` applies uniformly to all samples, so it can't
|
||||
// describe the L=unsigned + a/b=signed mix; instead we take a/b as signed
|
||||
// from the photometric interpretation (requiring `SampleFormat::Uint`, like
|
||||
// the ICCLab arm) and re-bias them to the unsigned ICCLab encoding in
|
||||
// post-processing (see `cielab_to_icclab`). libtiff does the same, decoding
|
||||
// both depths (`putcontig8bitCIELab8` / `putcontig8bitCIELab16`, selected by
|
||||
// `initCIELabConversion` in tif_getimage.c line 2216
|
||||
// @ 04884181e8903915038be553bfab404302d98ea0).
|
||||
PhotometricInterpretation::CIELab => match self.photometric_samples {
|
||||
3 if matches!(self.bits_per_sample, 8 | 16)
|
||||
&& matches!(self.sample_format, SampleFormat::Uint) =>
|
||||
{
|
||||
Ok(ColorType::Lab(self.bits_per_sample))
|
||||
}
|
||||
_ => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
PhotometricInterpretation::CIELab,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
)),
|
||||
},
|
||||
PhotometricInterpretation::CIELogL => match self.samples {
|
||||
1 if matches!(self.bits_per_sample, 32)
|
||||
&& matches!(self.sample_format, SampleFormat::IEEEFP) =>
|
||||
@@ -660,7 +677,11 @@ impl Image {
|
||||
),
|
||||
)),
|
||||
PhotometricInterpretation::RGBPalette => Ok(ColorType::Palette(self.bits_per_sample)),
|
||||
PhotometricInterpretation::TransparencyMask => Err(TiffError::UnsupportedError(
|
||||
// TransparencyMask is a single-channel mask, treat as grayscale.
|
||||
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],
|
||||
@@ -678,7 +699,8 @@ impl Image {
|
||||
dimensions: (u32, u32),
|
||||
samples: u16,
|
||||
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] fill_order: u16,
|
||||
) -> TiffResult<Box<dyn Read + 'r>> {
|
||||
engines: &mut CompressionEngines,
|
||||
) -> TiffResult<Box<dyn ReadSamples + 'r>> {
|
||||
Ok(match compression_method {
|
||||
CompressionMethod::None => Box::new(reader),
|
||||
CompressionMethod::SgiLog => match samples {
|
||||
@@ -709,9 +731,12 @@ impl Image {
|
||||
CompressionMethod::LZW => Box::new(super::stream::LZWReader::new(
|
||||
reader,
|
||||
usize::try_from(compressed_length)?,
|
||||
engines,
|
||||
)),
|
||||
#[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 => {
|
||||
@@ -1011,6 +1036,7 @@ impl Image {
|
||||
layout: &ReadoutLayout,
|
||||
chunk_index: u32,
|
||||
scratch: &mut Vec<u8>,
|
||||
engines: &mut CompressionEngines,
|
||||
) -> TiffResult<()> {
|
||||
let ValueReader {
|
||||
reader,
|
||||
@@ -1036,6 +1062,7 @@ impl Image {
|
||||
| ColorType::YCbCr(n)
|
||||
| ColorType::Gray(n)
|
||||
| ColorType::Palette(n)
|
||||
| ColorType::Lab(n)
|
||||
| ColorType::Multiband {
|
||||
bit_depth: n,
|
||||
num_samples: _,
|
||||
@@ -1047,6 +1074,7 @@ impl Image {
|
||||
| ColorType::YCbCr(n)
|
||||
| ColorType::Gray(n)
|
||||
| ColorType::Palette(n)
|
||||
| ColorType::Lab(n)
|
||||
| ColorType::Multiband {
|
||||
bit_depth: n,
|
||||
num_samples: _,
|
||||
@@ -1155,6 +1183,7 @@ impl Image {
|
||||
chunk_dims,
|
||||
self.samples,
|
||||
self.fill_order,
|
||||
engines,
|
||||
)?;
|
||||
|
||||
// Extended bit depths (9-15, 17-31): packed N-bit samples must be unpacked to
|
||||
@@ -1190,15 +1219,26 @@ impl Image {
|
||||
super::fix_endianness_and_predict(
|
||||
out_row,
|
||||
color_type.bit_depth(),
|
||||
samples,
|
||||
data_samples,
|
||||
ByteOrder::native(),
|
||||
predictor,
|
||||
scratch,
|
||||
);
|
||||
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
super::invert_colors(out_row, color_type, self.sample_format)?;
|
||||
}
|
||||
// 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,
|
||||
color_type,
|
||||
self.sample_format,
|
||||
)?;
|
||||
}
|
||||
} else if is_output_chunk_rows && is_all_bits {
|
||||
// Here we can read directly into the output buffer itself.
|
||||
@@ -1216,9 +1256,22 @@ impl Image {
|
||||
);
|
||||
}
|
||||
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
super::invert_colors(tile, color_type, self.sample_format)?;
|
||||
}
|
||||
// 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,
|
||||
color_type,
|
||||
self.sample_format,
|
||||
)?;
|
||||
} else if chunk_row_bytes > data_row_bytes && self.predictor == Predictor::FloatingPoint {
|
||||
// The floating point predictor shuffles the padding bytes into the encoded output, so
|
||||
// this case is handled specially when needed.
|
||||
@@ -1242,9 +1295,14 @@ impl Image {
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
super::invert_colors(row, color_type, self.sample_format)?;
|
||||
}
|
||||
// No masking here: the floating point predictor requires
|
||||
// IEEEFP samples, which are never depth-promoted.
|
||||
super::post_process_row(
|
||||
row,
|
||||
photometric_interpretation,
|
||||
color_type,
|
||||
self.sample_format,
|
||||
)?;
|
||||
}
|
||||
} else if is_all_bits {
|
||||
// We read row-by-row but each row fits in its output buffer.
|
||||
@@ -1255,11 +1313,12 @@ impl Image {
|
||||
// Two ways how we get here: we have more bytes in our chunk data than in the image
|
||||
// we are to read. Then we need to skip the rest of the data. Or we have a bigger
|
||||
// row stride than the chunk contains data, then we need to fill only the front.
|
||||
reader.read_exact(&mut row[..used])?;
|
||||
// Skip horizontal padding
|
||||
if chunk_row_bytes > data_row_bytes {
|
||||
let len = u64::try_from(chunk_row_bytes - data_row_bytes)?;
|
||||
io::copy(&mut reader.by_ref().take(len), &mut io::sink())?;
|
||||
// Skip horizontal padding
|
||||
let skip = u64::try_from(chunk_row_bytes - data_row_bytes)?;
|
||||
reader.read_padded_samples(&mut row[..used], skip)?;
|
||||
} else {
|
||||
reader.read_exact(&mut row[..used])?;
|
||||
}
|
||||
|
||||
super::fix_endianness_and_predict(
|
||||
@@ -1271,9 +1330,22 @@ impl Image {
|
||||
scratch,
|
||||
);
|
||||
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
super::invert_colors(row, color_type, self.sample_format)?;
|
||||
}
|
||||
// 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,
|
||||
color_type,
|
||||
self.sample_format,
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
// The encoded data potentially takes up more space than the output data so we must be
|
||||
@@ -1300,6 +1372,10 @@ impl Image {
|
||||
let photo_range = photometric_bit_end / 8..bits_per_pixel / 8;
|
||||
let mut encoded = vec![0u8; chunk_row_bytes];
|
||||
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
|
||||
// Clip to the chunk's data width: border chunks are stored at the full chunk
|
||||
// width, and writing their horizontal padding would spill into the following
|
||||
// output row (and predict/invert would run past the chunk's own columns).
|
||||
let row = &mut row[..data_row_bytes];
|
||||
reader.read_exact(&mut encoded)?;
|
||||
|
||||
Self::compact_photometric_bytes(&mut encoded, row, &photo_range);
|
||||
@@ -1313,12 +1389,26 @@ impl Image {
|
||||
scratch,
|
||||
);
|
||||
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
super::invert_colors(row, color_type, self.sample_format)?;
|
||||
}
|
||||
// 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,
|
||||
color_type,
|
||||
self.sample_format,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
reader.cache_or_destroy(engines);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,14 @@ impl<R: Read> Read for LogLuv24<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> super::stream::ReadSamples for LogLuv24<R> {
|
||||
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> std::io::Result<()> {
|
||||
self.read_exact(buf)?;
|
||||
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn packbits_decode<R: Read, const N: usize>(
|
||||
reader: &mut R,
|
||||
buf: &mut [[u8; N]],
|
||||
@@ -212,6 +220,14 @@ impl<R: Read> Read for LogLuv32<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> super::stream::ReadSamples for LogLuv32<R> {
|
||||
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> std::io::Result<()> {
|
||||
self.read_exact(buf)?;
|
||||
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Pack-Bits like encoding for a single 16-bit log-L sample.
|
||||
pub struct LogLuv16<R> {
|
||||
reader: Take<R>,
|
||||
@@ -278,6 +294,14 @@ impl<R: Read> Read for LogLuv16<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> super::stream::ReadSamples for LogLuv16<R> {
|
||||
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> std::io::Result<()> {
|
||||
self.read_exact(buf)?;
|
||||
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Table information from: `libtiff`, 1997 by Greg Ward Larson, SGI
|
||||
#[doc(alias = "UV_SQSIZ")]
|
||||
const UV_QUANTIZATION_WIDTH: f32 = 0.003500;
|
||||
|
||||
+143
-10
@@ -3,8 +3,8 @@ use std::io::{self, Read, Seek};
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use crate::tags::{
|
||||
ByteOrder, IfdPointer, PlanarConfiguration, Predictor, SampleFormat, Tag, TiffVariant, Type,
|
||||
ValueBuffer,
|
||||
ByteOrder, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat,
|
||||
Tag, TiffVariant, Type, ValueBuffer,
|
||||
};
|
||||
use crate::{
|
||||
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
|
||||
@@ -257,6 +257,9 @@ 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,
|
||||
compression: stream::CompressionEngines,
|
||||
}
|
||||
|
||||
/// All the information needed to read and interpret byte slices from the underlying file, i.e. to
|
||||
@@ -566,6 +569,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,
|
||||
@@ -659,6 +712,56 @@ fn invert_colors(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Re-bias CIE L*a*b* a/b channels from signed to unsigned encoding.
|
||||
///
|
||||
/// CIELab (photometric=8) stores a/b as signed integers where 0 is neutral;
|
||||
/// ICCLab (photometric=9) stores them as unsigned with the neutral shifted to
|
||||
/// the middle of the range. The two differ by exactly half the sample range, so
|
||||
/// the conversion is a wrapping add of 2^(bits-1) — i.e. flipping the sign bit —
|
||||
/// applied per a/b sample. `bit_depth` is 8 or 16 (guaranteed by `colortype`);
|
||||
/// samples are in native byte order here (post `fix_endianness_and_predict`).
|
||||
/// See libtiff's `TIFFCIELabToXYZ` for reference (tif_color.c line 45
|
||||
/// @ 04884181e8903915038be553bfab404302d98ea0).
|
||||
fn cielab_to_icclab(buf: &mut [u8], bit_depth: u8) {
|
||||
match bit_depth {
|
||||
16 => {
|
||||
for pixel in buf.as_chunks_mut::<6>().0 {
|
||||
// pixel is [L, a, b] as three native-endian u16 samples.
|
||||
let (samples, _) = pixel.as_chunks_mut::<2>();
|
||||
for s in &mut samples[1..=2] {
|
||||
*s = u16::from_ne_bytes(*s).wrapping_add(0x8000).to_ne_bytes();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for [_, a, b] in buf.as_chunks_mut::<3>().0 {
|
||||
*a = a.wrapping_add(128);
|
||||
*b = b.wrapping_add(128);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply photometric-interpretation-specific post-processing to a decoded row.
|
||||
///
|
||||
/// Some interpretations need a fixup once the raw samples are in the output
|
||||
/// buffer: `WhiteIsZero` inverts intensities, and `CIELab` re-biases the signed
|
||||
/// a/b channels into the unsigned ICCLab encoding. These are mutually exclusive,
|
||||
/// so at most one applies to any given row.
|
||||
fn post_process_row(
|
||||
buf: &mut [u8],
|
||||
photometric_interpretation: PhotometricInterpretation,
|
||||
color_type: ColorType,
|
||||
sample_format: SampleFormat,
|
||||
) -> TiffResult<()> {
|
||||
match photometric_interpretation {
|
||||
PhotometricInterpretation::WhiteIsZero => invert_colors(buf, color_type, sample_format)?,
|
||||
PhotometricInterpretation::CIELab => cielab_to_icclab(buf, color_type.bit_depth()),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fix endianness. If `byte_order` matches the host, then conversion is a no-op.
|
||||
fn fix_endianness(buf: &mut [u8], byte_order: ByteOrder, bit_depth: u8) {
|
||||
let host = ByteOrder::native();
|
||||
@@ -750,6 +853,8 @@ impl TiffHeader {
|
||||
seen_ifds: cycles::IfdCycles::new(),
|
||||
image: None,
|
||||
scratch: Vec::new(),
|
||||
lenient: false,
|
||||
compression: stream::CompressionEngines::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -832,25 +937,41 @@ 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.
|
||||
fn ensure_image_and_reader(
|
||||
&mut self,
|
||||
) -> TiffResult<(&mut Image, &mut ValueReader<R>, &mut Vec<u8>)> {
|
||||
) -> TiffResult<(
|
||||
&mut Image,
|
||||
&mut ValueReader<R>,
|
||||
&mut Vec<u8>,
|
||||
&mut stream::CompressionEngines,
|
||||
)> {
|
||||
if self.image.is_some() {
|
||||
return Ok((
|
||||
self.image.as_mut().unwrap(),
|
||||
&mut self.value_reader,
|
||||
&mut self.scratch,
|
||||
&mut self.compression,
|
||||
));
|
||||
}
|
||||
|
||||
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,
|
||||
&mut self.scratch,
|
||||
&mut self.compression,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -982,6 +1103,8 @@ 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,
|
||||
compression: core::mem::take(&mut self.compression),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1030,7 +1153,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(())
|
||||
@@ -1123,7 +1250,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(())
|
||||
@@ -1457,10 +1588,10 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
chunk_index: u32,
|
||||
layout: &image::ReadoutLayout,
|
||||
) -> TiffResult<()> {
|
||||
let (image, value_reader, scratch) = self.ensure_image_and_reader()?;
|
||||
let (image, value_reader, scratch, engines) = self.ensure_image_and_reader()?;
|
||||
let offset = image.chunk_file_range(chunk_index)?.0;
|
||||
value_reader.reader.goto_offset(offset)?;
|
||||
image.expand_chunk(value_reader, buffer, layout, chunk_index, scratch)?;
|
||||
image.expand_chunk(value_reader, buffer, layout, chunk_index, scratch, engines)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1612,7 +1743,7 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
slice: TiffCodingUnit,
|
||||
buffer: &mut [u8],
|
||||
) -> TiffResult<()> {
|
||||
let (image, value_reader, scratch) = self.ensure_image_and_reader()?;
|
||||
let (image, value_reader, scratch, engines) = self.ensure_image_and_reader()?;
|
||||
|
||||
let (width, height) = image.chunk_data_dimensions(slice.0)?;
|
||||
let readout = image.readout_for_size(width, height)?;
|
||||
@@ -1645,6 +1776,7 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
readout,
|
||||
chunk,
|
||||
scratch,
|
||||
engines,
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -1816,7 +1948,7 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
/// Returns an error if the buffer fits less than one plane. In particular, for non-planar
|
||||
/// images returns an error if the buffer does not fit the required size.
|
||||
pub fn read_image_bytes(&mut self, buffer: &mut [u8]) -> TiffResult<()> {
|
||||
let (image, value_reader, scratch) = self.ensure_image_and_reader()?;
|
||||
let (image, value_reader, scratch, engines) = self.ensure_image_and_reader()?;
|
||||
let readout = image.readout_for_image()?;
|
||||
|
||||
let ref layout @ image::PlaneLayout {
|
||||
@@ -1850,6 +1982,7 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
readout,
|
||||
chunk,
|
||||
scratch,
|
||||
engines,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
+211
-11
@@ -122,9 +122,86 @@ impl<R: Read> EndianReader<R> {
|
||||
// # READERS
|
||||
//
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct CompressionEngines {
|
||||
#[cfg(feature = "lzw")]
|
||||
lzw: Option<weezl::decode::Decoder>,
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for CompressionEngines {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut dbg = f.debug_struct("CompressionEngines");
|
||||
#[cfg(feature = "lzw")]
|
||||
dbg.field("lzw", &self.lzw.is_some().then_some("<weezl>"));
|
||||
dbg.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A variant of `io::Read` that takes one block to the end.
|
||||
///
|
||||
/// This is derived from `Read` so that any special optimization done by `std` (or any nightly crate
|
||||
/// for that matter) surely remains usable.
|
||||
///
|
||||
/// We want our decompression engines to be much more specialized than the standard library trait.
|
||||
/// Firstly there is more information available to communicate (the tiff Type, known decompressed
|
||||
/// size, …) and secondly they can potentially *reset*. This could be solved with an extension trait
|
||||
/// but since we want to avoid `read` in favor of `read_exact` (or an equivalent if we want to pass
|
||||
/// in a strided block buffer) a separate trait that's auto derived is favored.
|
||||
///
|
||||
/// FIXME: Also consider that we will want to read subsets of samples. Via IO this is done by
|
||||
/// discarding data *after* it is put into the output buffer. Yet, for some decompression that write
|
||||
/// alone is considerable work where it could simply skip bits more efficiently. So the reader would
|
||||
/// likely want to know about a sample mask.
|
||||
pub trait ReadSamples: Read {
|
||||
/// Read a row of samples and throw away some additional padding samples at the end.
|
||||
///
|
||||
/// This is automatically implemented for `Self: Read` as a copy into `io::Sink`.
|
||||
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()>;
|
||||
|
||||
/// Put this back into our cache of compression engines.
|
||||
fn cache_or_destroy(self: Box<Self>, _: &mut CompressionEngines) {}
|
||||
}
|
||||
|
||||
/// Note: the `Seek` bound ensures this does not overlap actual decompression.. This is an odd
|
||||
/// design problem born out of wanting both the underlying `Read` for uncompressed streams due to
|
||||
/// performance as well as the override to `cache_or_destroy` for all compressed streams.
|
||||
impl<R: Read + Seek> ReadSamples for R {
|
||||
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
|
||||
self.read_exact(buf)?;
|
||||
if discard > 0 {
|
||||
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Type alias for the deflate Reader
|
||||
#[cfg(feature = "deflate")]
|
||||
pub type DeflateReader<R> = flate2::read::ZlibDecoder<R>;
|
||||
pub struct DeflateReader<R> {
|
||||
inner: flate2::read::ZlibDecoder<R>,
|
||||
}
|
||||
|
||||
impl<R: Read> DeflateReader<R> {
|
||||
pub fn new(inner: R) -> Self {
|
||||
DeflateReader {
|
||||
inner: flate2::read::ZlibDecoder::new(inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for DeflateReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.inner.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> ReadSamples for DeflateReader<R> {
|
||||
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
|
||||
self.read_exact(buf)?;
|
||||
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// ## LZW Reader
|
||||
@@ -135,31 +212,103 @@ pub type DeflateReader<R> = flate2::read::ZlibDecoder<R>;
|
||||
pub struct LZWReader<R: Read> {
|
||||
reader: BufReader<Take<R>>,
|
||||
decoder: weezl::decode::Decoder,
|
||||
is_old_style: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "lzw")]
|
||||
impl<R: Read> LZWReader<R> {
|
||||
/// Wraps a reader
|
||||
pub fn new(reader: R, compressed_length: usize) -> LZWReader<R> {
|
||||
let configuration =
|
||||
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
|
||||
.with_yield_on_full_buffer(true);
|
||||
pub fn new(
|
||||
reader: R,
|
||||
compressed_length: usize,
|
||||
engine: &mut CompressionEngines,
|
||||
) -> LZWReader<R> {
|
||||
let mut buffered = BufReader::with_capacity(
|
||||
(32 * 1024).min(compressed_length),
|
||||
reader.take(u64::try_from(compressed_length).unwrap()),
|
||||
);
|
||||
|
||||
// `Compression = 5` covers both the current LZW variant and the pre-TIFF-6.0
|
||||
// "old-style" one, with no tag to distinguish them. New-style is MSB-first with
|
||||
// the "early change" code-size switch, and its stream begins with the Clear code
|
||||
// (256) -> first byte 0x80. Old-style is LSB-first *without* the early change
|
||||
// (GIF-style increment timing), and begins with 0x00 followed by a byte with bit
|
||||
// 0 set. This is the heuristic libtiff uses to select its compatibility decoder;
|
||||
// on a short/failed peek we default to the modern variant.
|
||||
let old_style = matches!(
|
||||
buffered.fill_buf(),
|
||||
Ok(head) if head.len() >= 2 && head[0] == 0x00 && head[1] & 0x01 != 0
|
||||
);
|
||||
|
||||
let decoder = if old_style {
|
||||
weezl::decode::Configuration::new(weezl::BitOrder::Lsb, 8)
|
||||
.with_yield_on_full_buffer(true)
|
||||
.build()
|
||||
} else {
|
||||
// All standard LZW decoders have the same configuration. We can thus reuse any that we
|
||||
// have previously constructed, skipping internal buffer allocation. We only need to
|
||||
// reset them to a new stream.
|
||||
if let Some(mut lzw) = engine.lzw.take() {
|
||||
lzw.reset();
|
||||
lzw
|
||||
} else {
|
||||
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
|
||||
.with_yield_on_full_buffer(true)
|
||||
.build()
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
reader: BufReader::with_capacity(
|
||||
(32 * 1024).min(compressed_length),
|
||||
reader.take(u64::try_from(compressed_length).unwrap()),
|
||||
),
|
||||
decoder: configuration.build(),
|
||||
reader: buffered,
|
||||
decoder: decoder,
|
||||
is_old_style: old_style,
|
||||
}
|
||||
}
|
||||
|
||||
fn discard_n(&mut self, mut n: u64) -> io::Result<()> {
|
||||
let mut buf = [0u8; 2048];
|
||||
|
||||
while n > 0 {
|
||||
let limit = n.min(2048) as usize;
|
||||
let result = self
|
||||
.decoder
|
||||
.decode_bytes(self.reader.fill_buf()?, &mut buf[..limit]);
|
||||
|
||||
self.reader.consume(result.consumed_in);
|
||||
n -= result.consumed_out as u64;
|
||||
|
||||
match result.status {
|
||||
Ok(weezl::LzwStatus::Ok) => {
|
||||
continue;
|
||||
}
|
||||
Ok(weezl::LzwStatus::NoProgress) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"no lzw end code found",
|
||||
));
|
||||
}
|
||||
Ok(weezl::LzwStatus::Done) => {
|
||||
if n != 0 {
|
||||
// stream is too short.
|
||||
return Err(std::io::ErrorKind::UnexpectedEof)?;
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(io::Error::new(io::ErrorKind::InvalidData, err)),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "lzw")]
|
||||
impl<R: Read> Read for LZWReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
|
||||
loop {
|
||||
let result = self.decoder.decode_bytes(self.reader.fill_buf()?, buf);
|
||||
|
||||
self.reader.consume(result.consumed_in);
|
||||
let _ = buf.split_off_mut(..result.consumed_out);
|
||||
|
||||
match result.status {
|
||||
Ok(weezl::LzwStatus::Ok) => {
|
||||
@@ -184,6 +333,24 @@ impl<R: Read> Read for LZWReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> ReadSamples for LZWReader<R> {
|
||||
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
|
||||
self.read_exact(buf)?;
|
||||
|
||||
if discard > 0 {
|
||||
self.discard_n(discard)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cache_or_destroy(self: Box<Self>, engines: &mut CompressionEngines) {
|
||||
if !self.is_old_style && engines.lzw.is_none() {
|
||||
engines.lzw = Some(self.decoder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// ## PackBits Reader
|
||||
//
|
||||
@@ -256,6 +423,14 @@ impl<R: Read> Read for PackBitsReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> ReadSamples for PackBitsReader<R> {
|
||||
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
|
||||
self.read_exact(buf)?;
|
||||
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator adapter that reverses bits in each byte for FillOrder=2 (LSB-to-MSB).
|
||||
#[cfg(feature = "fax")]
|
||||
type FaxBytes<R> =
|
||||
@@ -372,6 +547,14 @@ impl<R: Read> Read for Group4Reader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> ReadSamples for Group4Reader<R> {
|
||||
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
|
||||
self.read_exact(buf)?;
|
||||
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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")]
|
||||
@@ -510,6 +693,14 @@ impl Read for Group3Reader {
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadSamples for Group3Reader {
|
||||
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
|
||||
self.read_exact(buf)?;
|
||||
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webp")]
|
||||
pub struct WebPReader {
|
||||
inner: Cursor<Vec<u8>>,
|
||||
@@ -565,6 +756,15 @@ impl Read for WebPReader {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webp")]
|
||||
impl ReadSamples for WebPReader {
|
||||
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
|
||||
self.read_exact(buf)?;
|
||||
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
+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,
|
||||
|
||||
+330
-5
@@ -1,11 +1,11 @@
|
||||
extern crate tiff;
|
||||
|
||||
use tiff::decoder::{ifd, Decoder, DecodingSampleBuffer};
|
||||
use tiff::tags::{ByteOrder, Type};
|
||||
use tiff::tags::{ByteOrder, PhotometricInterpretation, SampleFormat, Type};
|
||||
use tiff::ColorType;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek};
|
||||
use std::io::{Cursor, Read, Seek};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const TEST_IMAGE_DIR: &str = "./tests/images/";
|
||||
@@ -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);
|
||||
@@ -473,6 +475,37 @@ fn test_tiled_rect_rgb_u8() {
|
||||
test_image_sum_u8("tiled-rect-rgb-u8.tif", ColorType::RGB(8), 62081032);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tiled_extra_samples_border_u8() {
|
||||
// Regression test for https://github.com/image-rs/image-tiff/issues/400
|
||||
//
|
||||
// A tiled image whose dimensions are not multiples of the tile size, with
|
||||
// an extra sample that is discarded on decode. The horizontal padding of
|
||||
// the right border tiles used to be written past the end of the output
|
||||
// row, spilling into the leftmost columns of the following row. The
|
||||
// fixture's tile padding is filled with 0xEE so any spill is visible.
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-extra-samples-border-u8.tif");
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder.next_image().expect("Cannot read image IFD");
|
||||
assert_eq!(decoder.dimensions().unwrap(), (40, 40));
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
||||
|
||||
let DecodingSampleBuffer::U8(data) = decoder.read_image().unwrap() else {
|
||||
panic!("Wrong bit depth")
|
||||
};
|
||||
|
||||
let mut expected = Vec::with_capacity(40 * 40 * 3);
|
||||
for y in 0..40u32 {
|
||||
for x in 0..40u32 {
|
||||
expected.push(((x * 3) % 251) as u8);
|
||||
expected.push(((y * 7) % 251) as u8);
|
||||
expected.push(((x + y) % 251) as u8);
|
||||
}
|
||||
}
|
||||
assert_eq!(data, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inner_access() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-rect-rgb-u8.tif");
|
||||
@@ -825,7 +858,7 @@ fn test_predictor_3_gray_f32() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "zstd")]
|
||||
#[cfg(any(feature = "zstd", feature = "zstd-safe-rust"))]
|
||||
fn test_zstd_compression() {
|
||||
// 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);
|
||||
@@ -975,7 +1008,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]
|
||||
@@ -1057,3 +1090,295 @@ 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);
|
||||
}
|
||||
|
||||
// ---- Old-style LZW ----
|
||||
|
||||
#[test]
|
||||
fn test_old_style_lzw() {
|
||||
// Pre-TIFF-6.0 "old-style" LZW: LSB-first bit order with no early-change size
|
||||
// switch. Auto-detected from the stream (first byte 0x00, second byte odd).
|
||||
// Same image as libtiffpic's quad-lzw.tif; the decoded pixels must match.
|
||||
test_image_sum_u8("quad-lzw-compat.tiff", ColorType::RGB(8), 21048924);
|
||||
}
|
||||
|
||||
// ---- TransparencyMask ----
|
||||
|
||||
/// Build a minimal uncompressed 2x2 TIFF in memory, in the requested byte order,
|
||||
/// with the given photometric interpretation and the pixel data as a single strip.
|
||||
/// `sample_format` is emitted as a SampleFormat tag when `Some`; when `None` the
|
||||
/// tag is omitted (the decoder then defaults to unsigned). Pixel bytes must already
|
||||
/// be in `byte_order`.
|
||||
fn build_raw_tiff(
|
||||
byte_order: ByteOrder,
|
||||
photometric: PhotometricInterpretation,
|
||||
bits_per_sample: u16,
|
||||
samples_per_pixel: u16,
|
||||
sample_format: Option<SampleFormat>,
|
||||
pixel_data: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let le = matches!(byte_order, ByteOrder::LittleEndian);
|
||||
let put16 = |buf: &mut Vec<u8>, v: u16| {
|
||||
if le {
|
||||
buf.extend_from_slice(&v.to_le_bytes());
|
||||
} else {
|
||||
buf.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
};
|
||||
let put32 = |buf: &mut Vec<u8>, v: u32| {
|
||||
if le {
|
||||
buf.extend_from_slice(&v.to_le_bytes());
|
||||
} else {
|
||||
buf.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
};
|
||||
|
||||
let width: u16 = 2;
|
||||
let height: u16 = 2;
|
||||
let num_tags: u16 = if sample_format.is_some() { 9 } else { 8 };
|
||||
let ifd_offset: u32 = 8;
|
||||
let ifd_size = 2 + (num_tags as u32) * 12 + 4;
|
||||
let strip_offset = 8 + ifd_size;
|
||||
|
||||
// IFD entries (must be ascending tag order). Tag IDs stay raw for the
|
||||
// ascending-order property; the field type uses the `Type` enum.
|
||||
let mut tags: Vec<(u16, Type, u32)> = vec![
|
||||
(256, Type::SHORT, width as u32), // ImageWidth
|
||||
(257, Type::SHORT, height as u32), // ImageLength
|
||||
(258, Type::SHORT, bits_per_sample as u32), // BitsPerSample
|
||||
(259, Type::SHORT, 1), // Compression = None
|
||||
(262, Type::SHORT, photometric.to_u16() as u32), // PhotometricInterpretation
|
||||
(273, Type::LONG, strip_offset), // StripOffsets
|
||||
(277, Type::SHORT, samples_per_pixel as u32), // SamplesPerPixel
|
||||
(279, Type::LONG, pixel_data.len() as u32), // StripByteCounts
|
||||
];
|
||||
if let Some(sf) = sample_format {
|
||||
tags.push((339, Type::SHORT, sf.to_u16() as u32)); // SampleFormat (tag 339 > 279)
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(if le { b"II" } else { b"MM" });
|
||||
put16(&mut buf, 42);
|
||||
put32(&mut buf, ifd_offset);
|
||||
put16(&mut buf, num_tags);
|
||||
|
||||
for (tag, typ, value) in tags {
|
||||
put16(&mut buf, tag);
|
||||
put16(&mut buf, typ.to_u16());
|
||||
put32(&mut buf, 1); // count
|
||||
// A SHORT value is left-justified in the 4-byte value field (value in the
|
||||
// first two bytes, then padding); a LONG fills all four.
|
||||
match typ {
|
||||
Type::SHORT => {
|
||||
put16(&mut buf, value as u16);
|
||||
put16(&mut buf, 0);
|
||||
}
|
||||
_ => put32(&mut buf, value),
|
||||
}
|
||||
}
|
||||
|
||||
put32(&mut buf, 0); // next IFD = 0
|
||||
buf.extend_from_slice(pixel_data);
|
||||
buf
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transparency_mask() {
|
||||
// 2x2 mask: values 0, 255, 128, 64
|
||||
let data = build_raw_tiff(
|
||||
ByteOrder::LittleEndian,
|
||||
PhotometricInterpretation::TransparencyMask,
|
||||
8,
|
||||
1,
|
||||
None,
|
||||
&[0, 255, 128, 64],
|
||||
);
|
||||
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(8));
|
||||
|
||||
let result = decoder.read_image().unwrap();
|
||||
match result {
|
||||
DecodingSampleBuffer::U8(pixels) => {
|
||||
// TransparencyMask is decoded as grayscale
|
||||
assert_eq!(pixels.len(), 4);
|
||||
assert_eq!(pixels[0], 0);
|
||||
assert_eq!(pixels[1], 255);
|
||||
assert_eq!(pixels[2], 128);
|
||||
assert_eq!(pixels[3], 64);
|
||||
}
|
||||
_ => panic!("Expected U8 buffer"),
|
||||
}
|
||||
}
|
||||
|
||||
// ---- CIE L*a*b* ----
|
||||
|
||||
#[test]
|
||||
fn test_cielab_decode() {
|
||||
// 2x2 CIELab image: 3 bytes per pixel (L, a, b)
|
||||
// L=128, a=0 (signed neutral), b=0 (signed neutral)
|
||||
// After re-biasing: a=128 (unsigned neutral), b=128 (unsigned neutral)
|
||||
let pixels: Vec<u8> = vec![
|
||||
128, 0, 0, // pixel 0: L=128, a=0, b=0 (neutral gray)
|
||||
255, 50, 50, // pixel 1: L=255, a=+50, b=+50
|
||||
100, 206, 206, // pixel 2: L=100, a=-50 (as i8), b=-50 (as i8)
|
||||
0, 128, 128, // pixel 3: L=0, a=-128 (as i8), b=-128 (as i8)
|
||||
];
|
||||
|
||||
let data = build_raw_tiff(
|
||||
ByteOrder::LittleEndian,
|
||||
PhotometricInterpretation::CIELab,
|
||||
8,
|
||||
3,
|
||||
None,
|
||||
&pixels,
|
||||
);
|
||||
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::Lab(8));
|
||||
|
||||
let result = decoder.read_image().unwrap();
|
||||
match result {
|
||||
DecodingSampleBuffer::U8(decoded) => {
|
||||
assert_eq!(decoded.len(), 12); // 4 pixels * 3 channels
|
||||
|
||||
// Pixel 0: L=128, a=0+128=128, b=0+128=128
|
||||
assert_eq!(decoded[0], 128);
|
||||
assert_eq!(decoded[1], 128);
|
||||
assert_eq!(decoded[2], 128);
|
||||
|
||||
// Pixel 1: L=255, a=50+128=178, b=50+128=178
|
||||
assert_eq!(decoded[3], 255);
|
||||
assert_eq!(decoded[4], 178);
|
||||
assert_eq!(decoded[5], 178);
|
||||
|
||||
// Pixel 2: L=100, a=206+128=78 (wrapping), b=206+128=78
|
||||
assert_eq!(decoded[6], 100);
|
||||
assert_eq!(decoded[7], 78);
|
||||
assert_eq!(decoded[8], 78);
|
||||
|
||||
// Pixel 3: L=0, a=128+128=0 (wrapping), b=128+128=0
|
||||
assert_eq!(decoded[9], 0);
|
||||
assert_eq!(decoded[10], 0);
|
||||
assert_eq!(decoded[11], 0);
|
||||
}
|
||||
_ => panic!("Expected U8 buffer"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cielab_decode_16() {
|
||||
// 2x2 16-bit CIELab image: 3 u16 samples per pixel (L, a, b), a/b signed.
|
||||
// Re-biasing maps signed a/b to unsigned by adding 0x8000 (wrapping):
|
||||
// 0 -> 32768, +1000 -> 33768, -1000 -> 31768, -32768 -> 0, +32767 -> 65535.
|
||||
let samples: [u16; 12] = [
|
||||
32768, 0, 0, // pixel 0: L, a=0, b=0 (neutral)
|
||||
65535, 1000, 1000, // pixel 1: a=+1000, b=+1000
|
||||
10000, 64536, 64536, // pixel 2: a=-1000, b=-1000 (as u16)
|
||||
0, 32768, 32767, // pixel 3: a=-32768, b=+32767 (as u16)
|
||||
];
|
||||
let mut pixels = Vec::new();
|
||||
for s in samples {
|
||||
pixels.extend_from_slice(&s.to_le_bytes());
|
||||
}
|
||||
|
||||
let data = build_raw_tiff(
|
||||
ByteOrder::LittleEndian,
|
||||
PhotometricInterpretation::CIELab,
|
||||
16,
|
||||
3,
|
||||
None,
|
||||
&pixels,
|
||||
);
|
||||
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::Lab(16));
|
||||
|
||||
let result = decoder.read_image().unwrap();
|
||||
match result {
|
||||
DecodingSampleBuffer::U16(decoded) => {
|
||||
assert_eq!(decoded.len(), 12); // 4 pixels * 3 channels
|
||||
|
||||
// L unchanged; a/b re-biased by +0x8000 (wrapping).
|
||||
let expected: [u16; 12] = [
|
||||
32768, 32768, 32768, // pixel 0
|
||||
65535, 33768, 33768, // pixel 1
|
||||
10000, 31768, 31768, // pixel 2
|
||||
0, 0, 65535, // pixel 3
|
||||
];
|
||||
assert_eq!(decoded, expected);
|
||||
}
|
||||
_ => panic!("Expected U16 buffer"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cielab_decode_16_big_endian() {
|
||||
// Same image and expected output as `test_cielab_decode_16`, but stored as a
|
||||
// big-endian ("MM") TIFF. 16-bit is the case where byte order matters: the
|
||||
// decoder must swap to native before the a/b re-bias, so the result is
|
||||
// identical to the little-endian case.
|
||||
let samples: [u16; 12] = [
|
||||
32768, 0, 0, // pixel 0
|
||||
65535, 1000, 1000, // pixel 1
|
||||
10000, 64536, 64536, // pixel 2
|
||||
0, 32768, 32767, // pixel 3
|
||||
];
|
||||
let mut pixels = Vec::new();
|
||||
for s in samples {
|
||||
pixels.extend_from_slice(&s.to_be_bytes());
|
||||
}
|
||||
|
||||
let data = build_raw_tiff(
|
||||
ByteOrder::BigEndian,
|
||||
PhotometricInterpretation::CIELab,
|
||||
16,
|
||||
3,
|
||||
None,
|
||||
&pixels,
|
||||
);
|
||||
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::Lab(16));
|
||||
|
||||
let result = decoder.read_image().unwrap();
|
||||
match result {
|
||||
DecodingSampleBuffer::U16(decoded) => {
|
||||
let expected: [u16; 12] = [
|
||||
32768, 32768, 32768, // pixel 0
|
||||
65535, 33768, 33768, // pixel 1
|
||||
10000, 31768, 31768, // pixel 2
|
||||
0, 0, 65535, // pixel 3
|
||||
];
|
||||
assert_eq!(decoded, expected);
|
||||
}
|
||||
_ => panic!("Expected U16 buffer"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cielab_rejects_non_uint_sample_format() {
|
||||
// CIELab treats a/b as signed via the photometric interpretation and requires
|
||||
// SampleFormat::Uint (matching the ICCLab arm). A file declaring Int must be
|
||||
// rejected rather than silently producing a signed output buffer.
|
||||
let pixels = vec![0u8; 12]; // 2x2, 3 samples, 8-bit
|
||||
let data = build_raw_tiff(
|
||||
ByteOrder::LittleEndian,
|
||||
PhotometricInterpretation::CIELab,
|
||||
8,
|
||||
3,
|
||||
Some(SampleFormat::Int),
|
||||
&pixels,
|
||||
);
|
||||
|
||||
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
|
||||
// Rejection may surface at either next_image() or colortype().
|
||||
let rejected = decoder.next_image().is_err() || decoder.colortype().is_err();
|
||||
assert!(
|
||||
rejected,
|
||||
"CIELab with SampleFormat=Int must be rejected, not decoded"
|
||||
);
|
||||
}
|
||||
@@ -190,7 +190,7 @@ const FILES: &[(&str, Option<u32>)] = &[
|
||||
("tests/libtiffpic/oxford.tif", Some(0x355d2e3e)),
|
||||
("tests/libtiffpic/pc260001.tif", Some(0xdb56b753)),
|
||||
("tests/libtiffpic/quad-jpeg.tif", Some(0xcba22475)),
|
||||
("tests/libtiffpic/quad-lzw.tif", Some(0)), // invalid LZW stream
|
||||
("tests/libtiffpic/quad-lzw.tif", Some(0x6055f9ee)), // old-style (LSB) LZW
|
||||
("tests/libtiffpic/quad-tile.tif", Some(0x6055f9ee)),
|
||||
("tests/libtiffpic/README", None),
|
||||
("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -233,3 +233,167 @@ fn test_rgb_u64_predict_roundtrip() {
|
||||
fn test_ycbcr_u8_predict_roundtrip() {
|
||||
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