Bumps the optional `zstd` dependency from `0.13` to `0.14` (pulls in
`zstd-safe` 8.0.0 / `zstd-sys` 2.1.0).
We only use `zstd::Decoder::new` for streaming decode, which is unchanged
in 0.14.0, so this is a pure version bump with no code changes.
Also worth noting: `zstd-safe` 8.0.0 fixes a dangling dictionary pointer
reachable from safe code, and `zstd-sys` 2.1.0 no longer needs `libclang`
by default. The crate stack moved from MIT to BSD-3-Clause (still
permissive, compatible with our MIT).
Promoted extended-depth grayscale (9-15 bits -> u16, 17-31 bits -> u32)
can carry bits above the declared depth after the horizontal predictor,
which reconstructs prev+delta in the promoted storage width rather than
modulo 2^BitsPerSample. This underflowed `max - v` in invert_colors (the
image-rs/image-tiff#396 panic in debug, wrong pixel in release) and left
junk in the high bits of the non-inverted output (#396 review, @197g:
"clamp the channel values in the non-inverted color case").
An n-bit predictor delta is stored in exactly n bits, i.e. modulo 2^n,
so reconstruction is defined modulo 2^n as well. Since 2^n divides the
u16/u32 storage modulus, masking each promoted sample with (1<<n)-1 once
after the predictor yields exactly the mod-2^n value -- including
legitimate wrap-arounds -- and also bounds any out-of-range sample from
malformed input. invert_colors' `max - v` is then always in range,
superseding the targeted saturating_sub from #396.
mask_samples_to_bit_depth runs in every integer decode branch of
expand_chunk; outside the extended-depth unpacking branch it is a
statically provable no-op (promoted layouts, declared < storage, occur
only there -- sub-byte depths keep byte-granular storage and reject the
horizontal predictor, standard depths have declared == storage, and the
float predictor requires IEEEFP samples that are never promoted), so the
branches cannot diverge.
Adds exact-pixel regression tests on hand-built TIFFs: 12/20-bit
wrap-around (plain and WhiteIsZero) and a 9-31-bit encode/decode
identity sweep, alongside the two fuzzer-minimized panic seeds. A
saturating clamp corrupts the wrap-around cases -- 12-bit [4095, 0, 1]
is stored as deltas [4095, 1, 1] and masks back to [4095, 0, 1], but
saturating produced [4095, 4095, 4095] -- so the tests lock the modular
semantics.
Rust 1.97's new clippy lint flags the redundant `&` on `self.offset`
in Entry's Debug impl. CI runs clippy with -D warnings in every job,
so this single pre-existing lint currently fails all PR pipelines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add expand_palette_to_rgb() for converting palette indices to RGB using the color map (1/2/4/8/16-bit indices, row-aligned sub-byte).
Accept CIE L*a*b* (photometric=8) as Lab(8) for 8-bit 3-channel images. Re-bias a/b from signed to unsigned during expand_chunk so callers handle CIELab and ICCLab the same way.
Treat TransparencyMask as grayscale instead of erroring.
Handle missing LZW end-of-information code gracefully when output was already produced.
Replace decode_image with decode_comprehensive, which exercises
per-IFD via a probe_directory helper:
- Tag iteration and value parsing for every tag
- SubIFD, EXIF, and GPS directory traversal via read_directory
- Image metadata (dimensions, colortype, color_map, chunk info)
- Layout queries (image_buffer_layout, image_chunk_buffer_layout)
- Per-chunk reads at first and last indices
- Full image decode via read_image
Three IFD traversal strategies:
- Linear iteration via next_directory (tolerates invalid
intermediate IFDs)
- Random-access via seek_to_directory (find_nth_ifd code path)
- Image-aware random-access via seek_to_image
Dictionary covers classic TIFF and BigTIFF byte order marks, and
common IFD tags in both LE and BE (image geometry, strips, tiles,
SubIFD, EXIF, GPS, ColorMap).
Strict resource limits (512 KB decode/intermediate, 64 KB IFD).
Bounded iteration (max 8 linear, max 4 seek).
`1usize << bits_per_sample` panics when bits_per_sample >= 64.
Use checked_shl to return a LimitsExceeded error instead.
Found by fuzzing decode_comprehensive with a BE TIFF containing
PhotometricInterpretation=RGBPalette and BitsPerSample=83.
Format-specific dictionary with LE/BE byte order marks, TIFF magic
number, and common IFD tag numbers as 2-byte LE and BE pairs.
Also add coverage to fuzz/.gitignore.
Fuzz testing found that crafted TIFF files with corrupt Group3 fax data
could cause a u16 addition overflow when accumulating run lengths. Use
checked_add and break on overflow (treating as corrupt data) instead of
panicking.
Change predict_f32/f16/f64 to take &mut [[u8; N]] output instead of
&mut [u8], making sample alignment a compile-time guarantee rather
than a silent runtime truncation. The caller uses as_chunks_mut at the
boundary, with a debug_assert that the remainder is empty.
This follows 197g's suggestion in #376 to move alignment validation
to the caller via typed slices, keeping the predictor functions clean
for future SIMD dispatch.
Two input validation fixes:
- vec_with_capacity now checks raw IFD value bytes against
ifd_value_size before allocating. Previously only the in-memory
Value representation was bounded (by decoding_buffer_size), so a
crafted IFD with a large count of small-typed values could allocate
based on raw byte count without limit checking. Also fix ASCII value
parsing to check ifd_value_size instead of decoding_buffer_size.
- LogLuv PackBits decoder now verifies that the expected number of
pixels was actually decoded per plane, returning UnexpectedEof if
the RLE stream ends short.