Compare commits

...
173 Commits
Author SHA1 Message Date
Wim Pomp a6e8ed772d doc typo
Fuzz / Fuzz (decode_comprehensive) (push) Failing after 1m14s
Fuzz / Fuzz (decode_image) (push) Failing after 39s
Fuzz / Fuzz regression (push) Successful in 40s
Rust CI / build (1.88.0) (push) Successful in 58s
Rust CI / build (beta) (push) Successful in 1m8s
Rust CI / build (nightly) (push) Successful in 56s
Rust CI / build (stable) (push) Successful in 58s
Rust CI / features (default,zstd) (push) Successful in 41s
Rust CI / features (jpeg) (push) Successful in 41s
Rust CI / features (lzw) (push) Successful in 41s
Rust CI / features (zstd) (push) Successful in 40s
Rust CI / other_archs (i686-unknown-linux-gnu) (push) Failing after 17s
Rust CI / clippy (push) Successful in 36s
Rust CI / test-platforms (ubuntu-latest) (push) Successful in 45s
Rust CI / features () (push) Successful in 37s
Rust CI / features (default) (push) Successful in 41s
Rust CI / features (deflate) (push) Successful in 45s
Rust CI / other_archs (powerpc-unknown-linux-gnu) (push) Failing after 11s
Rust CI / msrv (push) Successful in 1m0s
Rust CI / rustfmt (push) Successful in 29s
Rust CI / test-platforms (macos-26-intel) (push) Canceled after 0s
Rust CI / test-platforms (macos-latest) (push) Canceled after 0s
Rust CI / test-platforms (ubuntu-24.04-arm) (push) Canceled after 0s
Rust CI / test-platforms (windows-11-arm) (push) Canceled after 0s
Rust CI / test-platforms (windows-latest) (push) Canceled after 0s
2026-10-10 12:31:33 +02:00
Wim Pomp 9b452e93b7 - fix tiled image reading
Fuzz / Fuzz (decode_comprehensive) (push) Failing after 37s
Fuzz / Fuzz (decode_image) (push) Failing after 35s
Fuzz / Fuzz regression (push) Successful in 28s
Rust CI / build (1.88.0) (push) Successful in 46s
Rust CI / build (beta) (push) Successful in 38s
Rust CI / build (nightly) (push) Successful in 36s
Rust CI / build (stable) (push) Successful in 39s
Rust CI / test-platforms (ubuntu-latest) (push) Successful in 33s
Rust CI / features () (push) Successful in 29s
Rust CI / features (default) (push) Successful in 37s
Rust CI / features (default,zstd) (push) Successful in 36s
Rust CI / features (deflate) (push) Successful in 35s
Rust CI / features (jpeg) (push) Successful in 36s
Rust CI / features (lzw) (push) Successful in 35s
Rust CI / features (zstd) (push) Successful in 35s
Rust CI / other_archs (i686-unknown-linux-gnu) (push) Failing after 19s
Rust CI / other_archs (powerpc-unknown-linux-gnu) (push) Failing after 18s
Rust CI / rustfmt (push) Successful in 25s
Rust CI / msrv (push) Successful in 57s
Rust CI / clippy (push) Successful in 33s
Rust CI / test-platforms (macos-26-intel) (push) Canceled after 0s
Rust CI / test-platforms (macos-latest) (push) Canceled after 0s
Rust CI / test-platforms (ubuntu-24.04-arm) (push) Canceled after 0s
Rust CI / test-platforms (windows-11-arm) (push) Canceled after 0s
Rust CI / test-platforms (windows-latest) (push) Canceled after 0s
2026-10-03 23:07:34 +02:00
Wim Pomp 96d37bb087 - use a fork of the tiff crate to open RGBPalette tiffs
Fuzz / Fuzz (decode_comprehensive) (push) Failing after 27s
Fuzz / Fuzz (decode_image) (push) Failing after 36s
Fuzz / Fuzz regression (push) Successful in 29s
Rust CI / build (beta) (push) Successful in 40s
Rust CI / build (nightly) (push) Successful in 39s
Rust CI / features (default,zstd) (push) Successful in 35s
Rust CI / features (jpeg) (push) Successful in 35s
Rust CI / features (lzw) (push) Successful in 36s
Rust CI / features (zstd) (push) Successful in 36s
Rust CI / other_archs (i686-unknown-linux-gnu) (push) Failing after 19s
Rust CI / msrv (push) Successful in 56s
Rust CI / build (1.88.0) (push) Successful in 47s
Rust CI / build (stable) (push) Successful in 39s
Rust CI / test-platforms (ubuntu-latest) (push) Successful in 32s
Rust CI / features () (push) Successful in 31s
Rust CI / features (default) (push) Successful in 35s
Rust CI / features (deflate) (push) Successful in 37s
Rust CI / other_archs (powerpc-unknown-linux-gnu) (push) Failing after 17s
Rust CI / rustfmt (push) Successful in 25s
Rust CI / clippy (push) Successful in 33s
Rust CI / test-platforms (macos-26-intel) (push) Canceled after 0s
Rust CI / test-platforms (macos-latest) (push) Canceled after 0s
Rust CI / test-platforms (ubuntu-24.04-arm) (push) Canceled after 0s
Rust CI / test-platforms (windows-11-arm) (push) Canceled after 0s
Rust CI / test-platforms (windows-latest) (push) Canceled after 0s
2026-10-03 19:35:01 +02:00
Wim Pomp 12f16dbfdd - ignore missing colormap
Fuzz / Fuzz (decode_comprehensive) (push) Successful in 1m39s
Fuzz / Fuzz regression (push) Successful in 27s
Fuzz / Fuzz (decode_image) (push) Failing after 37s
Rust CI / build (1.88.0) (push) Successful in 47s
Rust CI / build (beta) (push) Successful in 37s
Rust CI / build (nightly) (push) Successful in 39s
Rust CI / build (stable) (push) Successful in 37s
Rust CI / test-platforms (ubuntu-latest) (push) Successful in 31s
Rust CI / features () (push) Successful in 29s
Rust CI / features (default) (push) Failing after 35s
Rust CI / features (default,zstd) (push) Canceled after 0s
Rust CI / features (deflate) (push) Canceled after 0s
Rust CI / features (jpeg) (push) Canceled after 0s
Rust CI / features (lzw) (push) Canceled after 0s
Rust CI / features (zstd) (push) Canceled after 0s
Rust CI / other_archs (i686-unknown-linux-gnu) (push) Failing after 10s
Rust CI / other_archs (powerpc-unknown-linux-gnu) (push) Failing after 10s
Rust CI / msrv (push) Successful in 47s
Rust CI / rustfmt (push) Successful in 24s
Rust CI / clippy (push) Failing after 33s
Rust CI / test-platforms (macos-26-intel) (push) Canceled after 0s
Rust CI / test-platforms (macos-latest) (push) Canceled after 0s
Rust CI / test-platforms (ubuntu-24.04-arm) (push) Canceled after 0s
Rust CI / test-platforms (windows-11-arm) (push) Canceled after 0s
Rust CI / test-platforms (windows-latest) (push) Canceled after 0s
2026-10-03 16:50:04 +02:00
Wim Pomp a8abdd2484 update README
Fuzz / Fuzz (decode_comprehensive) (push) Successful in 2m31s
Fuzz / Fuzz (decode_image) (push) Failing after 37s
Fuzz / Fuzz regression (push) Successful in 35s
Rust CI / build (1.88.0) (push) Successful in 52s
Rust CI / build (beta) (push) Successful in 47s
Rust CI / build (nightly) (push) Successful in 48s
Rust CI / test-platforms (ubuntu-latest) (push) Successful in 41s
Rust CI / features (default) (push) Failing after 35s
Rust CI / features (default,zstd) (push) Canceled after 0s
Rust CI / features (deflate) (push) Canceled after 0s
Rust CI / features (jpeg) (push) Canceled after 0s
Rust CI / features (lzw) (push) Canceled after 0s
Rust CI / features (zstd) (push) Canceled after 0s
Rust CI / build (stable) (push) Successful in 47s
Rust CI / features () (push) Successful in 29s
Rust CI / other_archs (i686-unknown-linux-gnu) (push) Failing after 16s
Rust CI / other_archs (powerpc-unknown-linux-gnu) (push) Failing after 9s
Rust CI / msrv (push) Successful in 55s
Rust CI / rustfmt (push) Successful in 24s
Rust CI / clippy (push) Failing after 32s
Rust CI / test-platforms (macos-26-intel) (push) Canceled after 0s
Rust CI / test-platforms (macos-latest) (push) Canceled after 0s
Rust CI / test-platforms (ubuntu-24.04-arm) (push) Canceled after 0s
Rust CI / test-platforms (windows-11-arm) (push) Canceled after 0s
Rust CI / test-platforms (windows-latest) (push) Canceled after 0s
2026-10-03 15:00:48 +02:00
Wim Pomp 934fde185a Default missing PhotometricInterpretation to BlackIsZero 2026-10-03 14:52:07 +02:00
Aurelia Molzer 3302936b08 Merge pull request #415 from paolobarbolini/zstd-0.14
Upgrade zstd to 0.14
2026-09-06 18:22:06 +00:00
Paolo Barbolini a5b9a57e09 Upgrade zstd to 0.14
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).
2026-09-06 16:59:40 +02:00
Aurelia Molzer e27f6fe00d Merge pull request #401 from jacovdbergh/fix-tiled-border-extra-samples
Fix: clip border tile rows when discarding extra samples
2026-08-10 18:46:22 +00:00
Aurelia Molzer 46b19cdacf Merge pull request #407 from aarshivv/feat/old-style-lzw
feat: decode old-style (pre-TIFF-6.0) LZW streams
2026-07-25 08:11:01 +00:00
aarshivv c27fd94e1d feat: decode old-style (pre-TIFF-6.0) LZW streams 2026-07-25 12:45:40 +05:30
Aurelia Molzer f28f5fc830 Merge pull request #406 from aarshivv/feat/cielab-16bit
feat: support 16-bit CIELab decoding
2026-07-24 10:55:17 +00:00
aarshivv 3436a2222b feat: support 16-bit CIELab decoding 2026-07-24 13:40:56 +05:30
Aurelia Molzer 31919c7bfc Merge pull request #386 from aarshivv/palette-cielab-transparency-lzw
support palette expansion, CIE L*a*b*, transparency mask, and lzw end…
2026-07-24 06:14:30 +00:00
aarshivv d0c9198016 Merge remote-tracking branch 'origin/main' into palette-cielab-transparency-lzw 2026-07-23 00:52:37 +05:30
aarshivv 8f83f0fa82 chore: address review feedback on CIELab/TransparencyMask PR 2026-07-23 00:39:19 +05:30
Sergey "Shnatsel" Davidoff f3f9ff1244 Merge pull request #398 from Shnatsel/safe-rust-zstd-2
Add safe Rust zstd decoding backend
2026-07-20 00:33:05 +01:00
Aurelia Molzer a7dda6f4b1 Merge pull request #399 from lilith/fix/clamp-extended-depth
decoder: reconstruct extended-depth samples modulo declared bit depth (supersedes #396)
2026-07-19 22:18:33 +00:00
Lilith River 35c14f1e3c decoder: reconstruct extended-depth samples modulo declared bit depth
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.
2026-07-18 16:04:42 -06:00
Aurelia Molzer a9cf5e17b9 Merge pull request #404 from kleinicke/fix-clippy-useless-borrow
Fix clippy 1.97 useless_borrows_in_formatting lint failing CI
2026-07-16 15:59:04 +00:00
florianandClaude Fable 5 6276defac6 Fix clippy 1.97 useless_borrows_in_formatting lint
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>
2026-07-16 16:29:05 +02:00
jacovdbergh@gmail.com dee0c2fb11 fix: clip border tile rows when discarding extra samples
Tiled images with a width that is not a multiple of the tile width
wrote the border tiles' horizontal padding into the next output row.
Fixes #400.
2026-07-08 08:16:27 +02:00
Sergey "Shnatsel" Davidoff 11133cb1e3 upgrade zrip to 0.8.x series 2026-07-06 15:15:04 +01:00
Aurelia Molzer 8ad05b85db Merge pull request #397 from soyosaki/lenient
Fix: add a lenient mode for image decoder
2026-07-03 14:36:35 +00:00
陈启 4a9e1d152b fix: resolve comments 2026-07-03 18:49:16 +08:00
陈启 1c6001ea5d fix: resolve comments 2026-07-03 17:19:48 +08:00
Sergey "Shnatsel" Davidoff c0043c1a84 Use zrip-decode crate instead of ruzstd 2026-06-27 12:47:51 +01:00
陈启 c7a1176381 fix: eliminate clippy errors 2026-06-22 14:13:53 +08:00
陈启 a658c09f09 feat: add lenient decoder example 2026-06-22 12:02:54 +08:00
陈启 0309349e1b fix: add lenient mode for tiff image decoder 2026-06-22 10:59:36 +08:00
陈启 279851a9a9 fix: correct extra bits rgb test assertion based on write extra bits
image logic
2026-06-22 10:58:42 +08:00
Aurelia Molzer 22b4c39254 Merge pull request #394 from powturns/main
Add PhotometricInterpretation unknown tag support
2026-06-02 17:50:20 +00:00
Dustin Thomson f11a1c5393 Add PhotometricInterpretation unknown tag support
Provides a mechanism to support private or extension
photometric interpretations (such as CFA)
2026-05-28 08:48:12 -06:00
Aurelia Molzer c4285aea4a Merge pull request #393 from image-rs/bench-lzw
Fix benches/lzw.rs directory iteration
2026-05-16 15:59:17 +00:00
A. Molzer a1426ca802 Fix benches/lzw.rs directory iteration 2026-05-16 02:28:36 +02:00
Aurelia Molzer 66e6567acc Merge pull request #392 from image-rs/upgrade-weezl
Upgrade weezl to 0.2
2026-05-15 22:51:46 +00:00
A. Molzer 5947d3b72e Make newer clippy happy 2026-05-16 00:46:02 +02:00
A. Molzer 0fdf1e0502 Upgrade weezl to 0.2 2026-05-16 00:40:00 +02:00
aarshivv 9924ea2721 chore: clean clippy deprecated warn in test case 2026-04-11 20:56:42 +05:30
aarshivv 30e04f3381 chore: drop palette and LZW, add tests for CIELab and TransparencyMask 2026-04-11 20:48:40 +05:30
aarshivv dfd0191403 support palette expansion, CIE L*a*b*, transparency mask, and lzw end marker tolerance
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.
2026-04-10 23:02:15 +05:30
Aurelia Molzer cf9eee64ad Merge pull request #383 from lilith/chore/fuzz-dict-and-target
chore: replace decode_image fuzz target with comprehensive decoder exerciser
2026-04-09 22:26:34 +00:00
Lilith River a59db23341 chore: replace decode_image fuzz target with comprehensive decoder exerciser
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).
2026-04-09 14:14:35 -06:00
Aurelia Molzer e8103509fa Merge pull request #385 from lilith/fix/colormap-shl-overflow
fix: prevent shift overflow panic in ColorMap size validation
2026-04-09 12:25:51 +00:00
Lilith River 26b18c5184 fix: prevent shift overflow panic in ColorMap size validation
`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.
2026-04-09 04:46:05 -06:00
Lilith River b0712855b1 ci: add nightly fuzz workflow (60s on push, 5min nightly) 2026-04-09 04:43:12 -06:00
Lilith River ea6e2a8d80 feat: add decode_comprehensive fuzz target with safe metadata probing
New fuzz target exercises TiffHeader::parse, dimensions/colortype
probing, chunk-by-chunk reading, and multi-IFD iteration. Guards
get_chunk_type/chunk_dimensions behind dimensions() success check
to avoid documented panics on non-image IFDs.
2026-04-09 04:43:12 -06:00
Lilith River 032421634f chore: add fuzz dictionary for TIFF
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.
2026-04-09 04:43:12 -06:00
Aurelia Molzer d760bd3442 Merge pull request #384 from image-rs/renovate-decode-iteration
Fix decode example/fuzz with new iteration
2026-04-08 18:49:52 +00:00
A. Molzer a70e05e51e Fix decode example/fuzz with new iteration 2026-04-08 20:43:12 +02:00
Aurelia Molzer dc25c4eaea Merge pull request #382 from lilith/fix/fax-overflow-panic
fix: prevent u16 overflow panics in Group3 fax run-length decode
2026-04-08 05:42:03 +00:00
Lilith River d7961a2fcf fix: prevent u16 overflow panic in Group3 fax run-length decode
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.
2026-04-07 17:45:59 -06:00
Aurelia Molzer 2166fd591a Merge pull request #380 from lilith/fix/ifd-limits-and-packbits
fix: enforce ifd_value_size limits and detect PackBits under-decode
2026-04-07 18:13:40 +00:00
Aurelia Molzer 7380611a7b Merge pull request #381 from lilith/refactor/typed-predictor-slices
refactor: use typed slices for float predictor output
2026-04-07 13:03:16 +00:00
Lilith River 149492ca6e refactor: use typed slices for float predictor output
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.
2026-04-07 03:03:23 -06:00
Lilith River e4c3c1fcfd fix: enforce ifd_value_size limits and detect PackBits under-decode
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.
2026-04-07 02:53:43 -06:00
Aurelia Molzer 6a3282e9f2 Merge pull request #379 from lilith/main
Optimize horizontal and float predictor decode (4-43% faster)
2026-04-06 17:25:25 +00:00
Lilith River fa8a911f79 perf: optimize horizontal and float predictor decode
Horizontal predictor (one_byte_predict, all integer TIFFs with
Predictor=2): rewrite with explicit [u8; N] accumulator so LLVM unrolls
and keeps each channel in a register. For N=3 (RGB8), this replaces a
scalar 1-byte-per-iteration loop with a 2x-unrolled 3-register loop.

Float predictor (Predictor=3, f16/f32/f64): split deinterleave into
per-format functions with plane slicing up front; f32 batches 4 samples
through a u128 wide store to eliminate per-sample bounds checks. Thread
a reusable scratch buffer through expand_chunk to eliminate per-row
heap allocation.

Benchmarked on real 21MP photos and GeoTIFF DEMs (vs upstream main):

  Strip RGB8 deflate+hpred:      253ms -> 224ms (-11%)
  Strip RGB8 LZW:                405ms -> 370ms (-9%)
  Tiled RGB8 deflate+hpred:      241ms -> 222ms (-8%)
  Strip RGB f32 deflate+fpred:   726ms -> 527ms (-27%)
  Gray f32 tiled 512:            290ms -> 166ms (-43%)
  Copernicus DEM GeoTIFF:        239ms -> 152ms (-37%)
  USGS DEM GeoTIFF:              284ms -> 198ms (-30%)
2026-04-06 08:49:00 -06:00
Lilith River dc94cd4436 bench: add comprehensive decode benchmarks
Add decode benchmark covering 16 TIFF configurations with both a real
photo corpus (if available) and synthetic in-memory images:

Strip: uncompressed, PackBits, LZW, LZW+hpred, deflate+hpred, RGB16
deflate+hpred, gray8 LZW, gray8 deflate+hpred, RGB f32 deflate+fpred,
gray f32 deflate+fpred, palette8 LZW, bilevel fax4.
Tiled: RGB8 LZW/deflate/JPEG 256, gray f32 deflate+fpred 512.

Synthetic benchmarks isolate predictor and compression overhead with
controlled 2048x2048 images. A callgrind profiling example is included
for instruction-level analysis.
2026-04-05 22:00:22 -06:00
Aurelia Molzer a42b16ab72 Merge pull request #377 from image-rs/change-reader-of-decoder
Transfer decoder to new reader
2026-04-05 08:05:41 +00:00
Aurelia Molzer 8246b55e8f Merge pull request #378 from image-rs/forgotten-dbg-statement
Cleanup dbg! left in error path
2026-04-04 21:16:36 +00:00
A. Molzer 399469f512 Cleanup dbg! left in error path 2026-04-04 23:13:07 +02:00
A. Molzer 740c5cdeea Transfer decoder to new reader 2026-04-04 15:45:31 +02:00
Aurelia Molzer 86ac5b601e Merge pull request #375 from image-rs/image-as-option
Make decoder's `Image` an option
2026-04-04 11:57:46 +00:00
A. Molzer 97d357d446 Prepare ImageDecoder split, avoid as_mut|unwrap 2026-04-04 10:20:10 +02:00
A. Molzer 61867b46f9 Make decoder's Image an option
Instead all methods with mutable self will initialize it on-demand.
There are only few methods that take `&self` and can not do that. For
those that have no error/option return we panic but both of these could
panic previously already. This should be fine by documentation alone. In
particular, the image size can be queried safely.
2026-04-04 10:20:08 +02:00
Aurelia Molzer 0f1f075abb Merge pull request #372 from image-rs/deprecate-and-hide-tag-reading
Deprecate and hide direct tag reading
2026-04-04 08:13:45 +00:00
Aurelia Molzer 5576417abe Merge pull request #374 from image-rs/value-read-provider
Add a `Read`-er for entry values
2026-03-31 16:47:14 +00:00
A. Molzer 52b9cc7a19 Rename EntryBytesReader module 2026-03-31 18:35:19 +02:00
A. Molzer 006f0b0fa4 Directly implement Seek::stream_position 2026-03-31 18:34:16 +02:00
A. Molzer 5b8a2832c3 Add design note 2026-03-31 18:34:15 +02:00
A. Molzer f93a6380e6 Extra utils for reading tags 2026-03-31 18:34:14 +02:00
A. Molzer 8a1b65fe68 Expand testing 2026-03-31 14:01:39 +02:00
A. Molzer a2996ab34e Basic test for EntryByteReader 2026-03-31 09:16:33 +02:00
A. Molzer f890d22657 Add EntryBytesReader: impl Read + Seek 2026-03-31 09:16:33 +02:00
A. Molzer 6b04bff6b2 Relax Decoder, new interface documentation
The bounds `Read + Seek` are not representational so we may remove them
from the struct itself. This also allows us to use more precise bounds
for direct access to the inner stream, especially were that does not
seek.

Groups operations according to their use case.
2026-03-31 09:05:43 +02:00
Aurelia Molzer 017425b200 Merge pull request #373 from image-rs/more-tag-utils
Add ValueBuffer::set_value and some documentation
2026-03-31 06:55:52 +00:00
A. Molzer 57d238fd9c Add ValueBuffer::set and some documentation 2026-03-31 08:47:10 +02:00
A. Molzer bd0dccef3a Deprecate Decoder::*tag methods
Funnel users towards the `IfdDecoder` instead. That type does not have
a generic over the underlying reader. It also works with a directory of
your own choosing. So the duplicates that should go are those directly
on the decoder.
2026-03-31 05:47:04 +02:00
Aurelia Molzer db1dbd3699 Merge pull request #371 from image-rs/restructure-buffer
Utils for DecodingBuffer et.al
2026-03-28 01:08:27 +00:00
A. Molzer 7491436306 Utils for DecodingBuffer et.al
Most of this is slightly gratuitous for now, we have no need internally,
but especially since the enums are non-exhaustive it will probably be
appreciated that you can interact with the buffer at a byte-level.

Additionally we make the extra promise that all sample types correspond
to some supported SampleType tag value. This prepares using the same
buffer for a runtime version of `Encoder::write_image`.
2026-03-28 01:25:07 +01:00
Aurelia Molzer 8e315c6a00 Merge pull request #370 from image-rs/restructure-buffer
Restructure decoding's Buffer
2026-03-27 19:48:46 +00:00
A. Molzer 860052a8f4 Rename decoding buffers
New names align better with conventions. In particular, Buffer refers to
an owning type.
2026-03-27 19:58:59 +01:00
Aurelia Molzer 1c37855dd9 Merge pull request #369 from lilith/feat/fax3-test-images
test: add CCITT Group 3 (Fax3) test images
2026-03-27 17:01:14 +00:00
A. Molzer 5696b0c78b Move DecodingBuffer related types to module 2026-03-27 17:57:13 +01:00
Aurelia Molzer 4d1daf5c19 Merge pull request #368 from lilith/ci/expand-platform-coverage
ci: expand platform coverage, enforce clippy, add MSRV check
2026-03-24 02:20:28 +00:00
Lilith River be58026125 fix: demote off_l16.tif to smoke test (LogLuv exp() platform variance)
log16_to_y() uses f32::exp() which is not required to be correctly
rounded by IEEE 754. Apple libm on arm64 differs by 1 ULP from glibc
on x86_64, producing different CRC32 checksums for the decoded output.

Same issue as off_luv32.tif which was already demoted in commit 617dc6e.

A deterministic LUT-based approach could fix both in a follow-up.
2026-03-23 19:50:43 -06:00
Lilith River 488da716d7 ci: expand platform coverage, enforce clippy warnings, add MSRV check
- Upgrade actions/checkout from v2/v4 to v6
- Add test-platforms job: ubuntu, ubuntu-arm, macos-arm, macos-intel, windows, windows-arm
- Add MSRV job using cargo-hack check --rust-version
- Replace baptiste0928/cargo-install with taiki-e/install-action@cross for cross-compilation
- Remove unnecessary Docker start step (cross handles container lifecycle)
- Enable clippy -D warnings with --all-targets (was disabled with TODO)
2026-03-23 18:55:24 -06:00
Lilith River 6c8f8abc71 test: add CCITT Group 3 (Fax3) test images with libtiff cross-validation
Three 16x16 bilevel fax3 test images covering the key Group 3 variants:
- seq-1c-1b-fax3: basic G3, no fill bits, MSB-first fill order
- seq-1c-1b-fax3-fillbits: G3 with T4Options fill bits (byte-aligned EOLs)
- seq-1c-1b-fax3-lsb: G3 with FillOrder=2 (LSB-first)

All use MinIsWhite photometric with inverted sequential pattern, producing
the same decoded CRC32 hash (71f6a21a) as the uncompressed 1-bit images.
Cross-validated: libtiff writes the images, both libtiff and image-tiff
read them back and agree on the decoded data.
2026-03-12 00:52:03 -06:00
Aurelia Molzer ba7a290ff7 Merge pull request #363 from lilith/feat/non-standard-bit-depths
Support non-standard bit depths (sub-byte and extended) with WhiteIsZero fixes
2026-03-12 02:03:49 +00:00
Lilith River 85215e81e3 Revert "feat: add YCbCr chroma subsampling support for non-JPEG compression"
This reverts commit 83409cbdf3.
2026-03-11 00:39:37 -06:00
Lilith River 617dc6eed8 fix: 32-bit platform failures in chunk_col_stride and LogLuv hash
Fix two CI failures on i686 and powerpc:

1. chunk_col_stride overflow: RowsPerStrip = u32::MAX causes
   data_row_bytes * chunk_height to exceed 32-bit usize. Clamp
   the chunk height to the image height before computing stride
   values, since a chunk never contributes more rows than the
   image contains. Applied to both readout_for_size() and
   set_row_stride().

2. off_luv32.tif hash mismatch: the LogLuv decoder produces
   platform-dependent output (different hashes on x86_64, i686,
   and powerpc), so demote to smoke test.
2026-03-11 00:27:06 -06:00
Lilith River 83409cbdf3 feat: add YCbCr chroma subsampling support for non-JPEG compression
Add YCbCrUpsamplingReader that converts chroma-subsampled YCbCr block
data to full-resolution 3-bytes-per-pixel output. Each block of h×v
pixels stores h*v Y samples plus one shared Cb and Cr sample; the
reader expands these into per-pixel (Y, Cb, Cr) triples.

This enables decoding YCbCr TIFFs with any supported compression
method (uncompressed, LZW, Deflate, etc.), not just JPEG. Two more
libtiffpic test images now decode with verified hashes:
- dscf0013.tif (uncompressed YCbCr 2,1 subsampling)
- ycbcr-cat.tif (LZW-compressed YCbCr 2,2 subsampling)

The remaining two YCbCr test images (smallliz.tif, zackthecat.tif)
use Old JPEG compression (type 6) which is a separate unsupported
feature.
2026-03-10 22:46:59 -06:00
Lilith River 26301fc3f1 feat: add CCITT Group 3 (Fax3) compression and FillOrder support
Add Group3Reader for decoding CCITT Group 3 (T.4) 1D compressed TIFF
images. The fax crate's Group3Decoder has a bug where new() verifies
but doesn't consume the initial EOL marker, causing its lookup tables
to destructively consume bits from the EOL code. We work around this
by using the crate's lower-level white::decode / black::decode
functions and handling EOL markers ourselves.

Also adds FillOrder tag (266) support: FillOrder=2 (LSB-to-MSB,
common in fax images) is handled by reversing bits in each byte
before decoding. This applies to both Group 3 and Group 4 readers.

Both libtiffpic fax test images (fax2d.tif, g3test.tif) now decode
correctly with verified CRC32 hashes.
2026-03-10 22:41:03 -06:00
Lilith River 83dbb750df test: add CRC32 hash verification for all libtiffpic images
Upgrade the libtiffpic test harness from decode-only smoke tests to
full output verification using CRC32 reference hashes. 50 of 61 images
now have hash-verified decoded output; the remaining 11 use unsupported
formats (YCbCr subsampling, Fax3, etc.) and remain as decode-only tests.

Uses read_image_to_buffer (the non-deprecated API) with ByteOrder::convert
for cross-platform LE normalization.

Remove the 13 individual depth tests from decode_images.rs — they tested
the same libtiffpic flower images that are now hash-verified here.
2026-03-10 17:33:12 -06:00
Lilith River 2d6d92e81d refactor: simplify hash computation and remove unused benchmark
Address PR review feedback:

- Remove benches/decode.rs which referenced non-existent gen_bench_tiff.c
- Simplify compute_tiff_hash: replace 66-line match block with
  as_buffer().as_bytes() + ByteOrder::convert for LE normalization
- Use TIFF Type repr(u16) as hash discriminant instead of ad-hoc u8
- Rename all 74 seq test images to match updated hashes
2026-03-10 14:43:07 -06:00
Lilith River 3434d0220b perf: const-generic bit unpacking with wide u64 reads
Specialize extract_bits_msb and unpack_bits via const generics so the
compiler knows BPS at compile time. Replace the byte-by-byte accumulation
loop with a single u64::from_be_bytes read + shift + mask.

Benchmarks (1024x1024 gray, uncompressed):
  10-bit: 1.32ms → 886µs  (1.49x faster)
  12-bit: 1.34ms → 871µs  (1.54x faster)
  14-bit: 1.49ms → 963µs  (1.55x faster)
  24-bit: 1.84ms → 776µs  (2.37x faster)
2026-03-10 08:08:09 -06:00
Lilith River fcdb86ca7f bench: add decode benchmark for extended bit depths
Measures decode throughput for 1024x1024 gray images at various bit
depths (3-24 bit). Requires pre-generated images in /tmp/bench-tiff/
(see stderr instructions for generation).
2026-03-10 08:02:46 -06:00
Lilith River 6fb99107cd fix: cargo fmt 2026-03-10 06:44:58 -06:00
Lilith River 19dcd09e09 fix: panic on unknown ColorType in hash, fix rfind in hash helper
- compute_tiff_hash now panics on unhandled ColorType variants instead
  of silently using a fallback discriminant
- print_seq_hashes correctly identifies existing 8-char hex hash suffix
  vs non-hash filename segments
2026-03-10 06:02:19 -06:00
Lilith River 43cfc5bc3a test: self-verifying seq images with CRC32 hash in filenames
Move 74 seq-*.tiff test images to tests/images/seq/ and embed a CRC32
hash of the full decoded output (dimensions, color type, sample data)
as the last filename segment. Replace 75 individual test functions with
one generic verify_all_seq_images test that decodes each file and
validates its hash against the filename.

This catches byte-order, bit-alignment, stride, padding, and value
errors that sum-based tests would miss (e.g., swapped pixels preserve
the sum but change the hash). Adding new test images only requires
dropping a correctly-named file in seq/ — no test code changes needed.
2026-03-10 06:02:19 -06:00
Lilith River c3977c4513 test: expand test suite to 74 images with libtiff reference decoder
Add 19 new libtiff-generated test images and a reference decoder
(verify_seq_tiff.c) that independently verifies all checksums using
libtiff's own read path, giving three-way agreement:
  libtiff writer → libtiff reader → image-tiff reader

New coverage:
- Standard sub-byte: 1-bit, 2-bit, 4-bit grayscale (BIZ + WIZ)
- Multi-strip: RowsPerStrip=4 (gray 8/16, RGB 8)
- Tiled + compressed: LZW and Deflate tiles
- Float predictor: f32/f64 + Deflate + predictor=3
- Unassociated alpha: RGBA 8-bit with EXTRASAMPLE_UNASSALPHA
- Sub-byte RGB: 5-bit and 7-bit contiguous
- Signed RGB: i8 and i16 (3-channel, decoded as Multiband)
- Float f64 RGB
- Tiled BigTIFF

verify_seq_tiff.c: reads each TIFF with libtiff, computes matching
checksums (handling packed sub-byte, unpacked extended, float, signed),
and verifies 68/68 cases pass.

Total: 74 test images (66.8 KB), 317 tests all passing.
2026-03-10 06:02:19 -06:00
Lilith River a4cbf7bbaf test: add 28 format-variant test images covering compression, tiled, BigTIFF, float, signed, CMYK, palette, RGBA, multi-page, and big-endian
Expand gen_seq_tiff.c with writers for standard-depth formats that exercise
different decoder code paths. All 55 seq-* images are 16x16, libtiff-generated,
and cross-validated against tifffile.

New coverage:
- Compression: LZW, Deflate, PackBits (gray 8/16-bit, RGB 8-bit)
- LZW + horizontal predictor (8-bit gray)
- Tiled layout: gray 8/16-bit, RGB 8-bit (16x16 tiles)
- BigTIFF: gray 8-bit, RGB 16-bit
- Float: gray f32/f64, RGB f32
- Signed integer: i8, i16, i32
- CMYK: 8-bit, 16-bit
- Palette/indexed: 4-bit, 8-bit (with known colormap)
- RGBA with associated alpha: 8-bit, 16-bit
- Multi-page: 3-page gray 8-bit (all pages verified)
- Big-endian byte order: gray 8/16-bit, RGB 8-bit

Total: 298 tests, all passing.
2026-03-10 06:02:19 -06:00
Lilith River 26c32431fa test: regenerate seq-* test images using libtiff C API
Replace Python-generated test TIFFs with libtiff-generated ones to
eliminate circular validation. The C program (gen_seq_tiff.c) uses
libtiff's TIFFWriteScanline with MSB-first packed bit streams.

Cross-validated: tifffile independently confirms all 27 images match
the original Python-generated versions byte-for-byte. tiffinfo validates
all IFD structures. All 27 image-tiff decode tests pass unchanged.
2026-03-10 06:02:19 -06:00
Lilith River 5090003bda test: add 27 sequential-value test images for bit-error detection
Generated by gen_seq_images.py using manual TIFF writer with packed
bit streams matching TIFF spec. Each 16x16 image has cycling 0..max
values so any bit-level decode error changes the checksum.

Test matrix:
- Sub-byte BlackIsZero: 3/5/7-bit
- Sub-byte WhiteIsZero: 3/5/6/7-bit
- Extended BlackIsZero: 10/12/14/24-bit
- Extended WhiteIsZero: 10/12/14/24-bit
- Extended HPredict: 10/12/14/24-bit
- RGB contiguous: 10/12/14/24-bit
- RGB planar: 10/12/14/24-bit (first plane only via read_image)
2026-03-10 06:02:19 -06:00
Lilith River fb1afc81e9 fix: handle WhiteIsZero for non-standard sub-byte depths (3/5/6/7-bit)
For sub-byte depths where samples cross byte boundaries (3, 5, 6, 7-bit),
byte-wise NOT corrupts samples. Added per-sample inversion that extracts
each N-bit sample from the packed stream, computes (max - value), and
writes it back.

Tests cover:
- 3/5/7-bit BlackIsZero (verify basic decoding)
- 3-bit and 6-bit WhiteIsZero (verify per-sample inversion)
2026-03-10 06:02:19 -06:00
Lilith River 29e43d2314 fix: handle WhiteIsZero for extended bit depths, add comprehensive tests
Fix invert_colors to support extended bit depths (9-15 promoted to u16,
17-31 promoted to u32). Byte-wise NOT doesn't work for promoted samples
because it flips unused upper bits; use (max - value) instead.

Add regression tests covering the extended depth matrix:
- Gray 10/14/24-bit (contiguous, BlackIsZero)
- RGB 10/14/24-bit (contiguous)
- RGB 10/12/14/24-bit (planar)
- Gray 12-bit with WhiteIsZero (synthetic 4x2 image)
- Gray 12-bit with horizontal predictor (synthetic 4x2 image)
2026-03-10 06:02:19 -06:00
Lilith River 4ff374990e feat: accept sub-byte bit depths for all color types
Previously, sub-byte bit depths (1-7 bits per sample) were only
accepted for Gray and Multiband color types. This extends the
validation to accept all color types (RGB, RGBA, CMYK, CMYKA, YCbCr)
at sub-byte depths.

The data format is unchanged: sub-byte samples remain packed MSB-first
in the output buffer, exactly as for grayscale. Predictors are still
rejected for sub-byte data.

Tested via libtiffpic flower-rgb-contig-02/04 and flower-rgb-planar-02/04.
2026-03-10 06:02:19 -06:00
Lilith River 3d3c16e26f feat: support packed extended bit depths (9-15, 17-31 bits per sample)
TIFF files can store samples at non-standard bit depths (e.g. 10, 12,
14, 24 bits) packed tightly in a continuous bit stream. Previously
these were rejected as unsupported color types.

This adds:
- MSB-first bit extraction from packed byte streams (unpack_bits,
  extract_bits_msb) to expand N-bit samples to standard-width output
  (9-16 bit → u16, 17-32 bit → u32)
- Output buffer sizing using the promoted sample width instead of the
  packed bit depth (output_bits_per_sample field in ReadoutLayout)
- A new code path in expand_chunk that reads packed rows, unpacks to
  standard-width samples, then applies predictor and color inversion
- Horizontal predictor support for extended depths

Tested with 12-bit grayscale and RGB images (existing test suite),
plus all libtiffpic depth test images (10/12/14/24-bit in gray, RGB
contig, and RGB planar configurations).
2026-03-10 06:01:48 -06:00
Aurelia Molzer 8a5c2da511 Merge pull request #361 from lilith/feat/palette-support
Support palette (ColorMap) image decoding
2026-03-10 04:15:52 +00:00
Lilith River 3b11e2f577 feat: support palette (ColorMap) image decoding
Read the ColorMap tag (320) for RGBPalette images and return raw
palette indices as the decoded data with ColorType::Palette(n).

Changes:
- Read and validate ColorMap tag during IFD parsing (3 * 2^bps entries)
- Return ColorType::Palette from colortype() for RGBPalette images
- Accept Palette in expand_chunk validation (standard + sub-byte depths)
- Expose color_map() method on Decoder for palette lookup
- Add tests for 1-bit, 4-bit, 8-bit palette images with checksums
- Add test verifying ColorMap accessibility and structure

The decoder returns raw palette indices; consumers apply the lookup
using the color map returned by Decoder::color_map().
2026-03-09 21:03:30 -06:00
Aurelia Molzer c6f6ff4c95 Merge pull request #359 from image-rs/tile-offset-compatibility
Tile offset compatibility
2026-03-08 02:29:15 +00:00
A. Molzer d7d24d0f7c Changelog entry for tile treatment 2026-03-08 03:00:15 +01:00
A. Molzer 58e93fb8a3 Align tile detection to libtiff 2026-03-08 03:00:15 +01:00
Aurelia Molzer 595e67e5fb Merge pull request #358 from image-rs/open-behavior-documentation
Document `Decoder::open` behavior
2026-03-06 20:15:29 +00:00
A. Molzer 812bc970ba Document Decoder::open behavior 2026-03-06 21:09:32 +01:00
Aurelia Molzer 82b646298d Merge pull request #356 from Shnatsel/better-docs
Improve documentation on compression and predictor
2026-03-06 19:12:56 +00:00
Sergey "Shnatsel" Davidoff 22ff2b5c50 Merge pull request #355 from Shnatsel/drop-exact-level
Drop obsolete Deflate::exact_level()
2026-03-06 18:49:06 +00:00
Sergey "Shnatsel" Davidoff 3a6492f251 More docs on the predictor 2026-03-06 15:46:37 +00:00
Sergey "Shnatsel" Davidoff a03eeccfe5 Document Compression enum variants 2026-03-06 14:47:44 +00:00
Sergey "Shnatsel" Davidoff 287fcb9309 Drop obsolete Deflate::exact_level() 2026-03-06 14:40:37 +00:00
Sergey "Shnatsel" Davidoff ba97bad411 Merge branch 'main' into better-docs 2026-03-06 14:39:37 +00:00
Aurelia Molzer c4ab46ad2c Merge pull request #354 from Shnatsel/derive-debug
Derive useful traits on public types
2026-03-06 11:45:45 +00:00
Sergey "Shnatsel" Davidoff eaf9cc1d6c Merge branch 'main' into better-docs 2026-03-06 11:02:09 +00:00
Sergey "Shnatsel" Davidoff 34191fa9ca Merge branch 'main' into derive-debug 2026-03-06 10:55:44 +00:00
Sergey "Shnatsel" Davidoff b0bd33dbe4 Remove equality from rational types; can be implemented later via cross-multiplication 2026-03-06 10:54:37 +00:00
Sergey "Shnatsel" Davidoff 2ca2427c75 cargo fmt 2026-03-06 10:51:23 +00:00
Aurelia Molzer e8be8656f2 Merge pull request #353 from Shnatsel/non-exhaustive-compression
Make Compression enum non-exhaustive
2026-03-06 10:45:28 +00:00
Sergey "Shnatsel" Davidoff ea949032a2 cargo clippy --fix 2026-03-06 10:43:54 +00:00
Sergey "Shnatsel" Davidoff 87007d85a0 Derive Debug on Compression enum 2026-03-06 10:41:20 +00:00
Sergey "Shnatsel" Davidoff 2119bff354 More derives on TIFF value structs 2026-03-06 10:36:12 +00:00
Sergey "Shnatsel" Davidoff c62a69ec9d Expand predictor docs 2026-03-06 10:07:06 +00:00
Sergey "Shnatsel" Davidoff 79a791b49a Clarify predictor docs 2026-03-06 10:02:24 +00:00
Sergey "Shnatsel" Davidoff e1e9f36c5a Make Compression enum non-exhaustive 2026-03-06 09:49:10 +00:00
Sergey "Shnatsel" Davidoff 19caae4f53 Rename safe-rust-zstd to zstd-safe-rust 2026-03-06 09:36:09 +00:00
Aurelia Molzer 0ad653433a Merge pull request #352 from image-rs/iterate-over-raw-tiff-or-exif
Open a Tiff without interpreting the first directory
2026-03-06 08:34:19 +00:00
A. Molzer bf79af8de4 Add Decoder::open, deprecate new 2026-03-06 09:31:23 +01:00
Aurelia Molzer 54dddc4943 Merge pull request #347 from image-rs/iterate-over-raw-tiff-or-exif
Enable iteration over raw, malformed tiff or exif
2026-03-06 07:31:13 +00:00
A. Molzer be9b3a04b9 Expand documentation with TiffHeader 2026-03-06 06:05:32 +01:00
A. Molzer 08f1b25fac Drafting release notes 2026-03-06 05:35:45 +01:00
A. Molzer c7b97519a0 Error guarantee on decoder IFD iteration 2026-03-06 04:42:02 +01:00
A. Molzer e093e15bbe Remove redundant next_ifd attribute 2026-03-06 03:44:08 +01:00
A. Molzer 647e9b4a81 Add TiffHeader to open a file without seeking 2026-03-06 02:59:45 +01:00
Aurelia Molzer 31c49ea6ef Merge pull request #351 from Shnatsel/semver-breaking-deflate-level
Implement DEFLATE compression level control properly, with a semver break
2026-03-05 20:12:27 +00:00
Sergey "Shnatsel" Davidoff 0362e6e31d Implement DEFLATE compression level control properly, with a semver break 2026-03-05 17:59:18 +00:00
Sergey "Shnatsel" Davidoff 0fe5b2804e Merge pull request #348 from Shnatsel/deflate-level
Expose precise DEFLATE level controls
2026-03-05 17:23:59 +00:00
Sergey "Shnatsel" Davidoff 61dbcc1392 Add error handling 2026-03-05 13:15:33 +00:00
Sergey "Shnatsel" Davidoff d9d4f0a905 Add safe Rust Zstd decoding option 2026-03-05 13:10:07 +00:00
Sergey "Shnatsel" Davidoff a589010099 Fix typo 2026-03-05 12:22:37 +00:00
Sergey "Shnatsel" Davidoff 53498074a4 Merge impl blocks 2026-03-05 12:22:25 +00:00
Sergey "Shnatsel" Davidoff cb543cded7 Expose precise DEFLATE level controls 2026-03-05 11:58:18 +00:00
A. Molzer cf8f96568e Expose Decoder::seek_to_directory 2026-03-04 08:59:41 +01:00
A. Molzer 4ff88e658d Rewrite Decoder::seek_to_image 2026-03-04 05:12:31 +01:00
A. Molzer 14ebf376f3 Move Directory from image into decoder proper
This prepares allowing to iterate over a TIFF file with invalid images
while `Image` maintains invariants. For instance we precalculate the
amount of buffer space and verify that the count of strips makes sense
in the constructor—so we do not have to redo that for every other entry
point.
2026-03-04 03:37:42 +01:00
Aurelia Molzer 37578add53 Merge pull request #344 from RunDevelopment/fix-libtiffpic-test-for-windows
Fix libtiffpic test for Windows
2026-03-03 20:42:20 +00:00
Aurelia Molzer 2b9476566b Merge pull request #346 from RunDevelopment/invert-color-as-chunks
More optimal code gen for `invert_colors`
2026-03-03 20:30:20 +00:00
Aurelia Molzer 448a0e61d1 Merge pull request #345 from mpospelova/increase-group4reader-height-size
Change Group4Reader height dimension from u16 to u32
2026-03-03 20:23:52 +00:00
RunDevelopment d872ff8751 Unify grayscale integer code paths 2026-03-03 14:54:32 +01:00
RunDevelopment 0a878888db More optimal code gen for invert_colors 2026-03-03 14:36:16 +01:00
Maria Pospelova a0a95ef86e Fix formatting 2026-03-03 13:26:09 +00:00
RunDevelopment bbb5e5a41b Use std::path::MAIN_SEPARATOR 2026-03-03 14:13:00 +01:00
RunDevelopment f74169d4af Fix libtiffpic test for Windows 2026-03-03 14:11:24 +01:00
Maria Pospelova c304a3cc13 Upgrade Group4Reader dimensions from u16 to u32 2026-03-03 10:50:27 +00:00
Sergey "Shnatsel" Davidoff 9a9193345c Merge pull request #343 from Shnatsel/float-predictor-opt
Optimize floating-point predictor encoding
2026-02-27 01:14:36 +00:00
Sergey "Shnatsel" DavidoffandClaude 22046d4272 Simplify byte shuffle to direct scatter writes
Replace chunked scratch-buffer approach with direct indexed writes
into the pre-allocated output. Simpler and faster in practice.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 01:02:41 +00:00
Sergey "Shnatsel" DavidoffandClaude 2c5fe2c2f5 Use chunked writes for byte shuffle in fp_predict
Replace single-byte pushes with chunk-based processing: pre-allocate
zeroed output, shuffle 4 values at a time into a stack buffer, then
copy_from_slice each byte lane into the output.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 00:47:13 +00:00
Sergey "Shnatsel" DavidoffandClaude eceaa1f14d Add encoding benchmark for RGB32Float with float predictor and DEFLATE
Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 00:27:40 +00:00
Aurelia Molzer d42db25c27 Merge pull request #342 from Shnatsel/float-predictor-encoding
Implement floating-point predictor (Predictor 3) encoding
2026-02-26 20:30:08 +00:00
Sergey "Shnatsel" DavidoffandClaude c74f23784c Vectorize byte-level horizontal differencing in fp_predict
Process the difference step in reverse 16-byte chunks, reading both
curr and prev into local arrays before writing. This makes all 16
subtractions independent, enabling autovectorization (confirmed with
SSE2: movdqu + psubb).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-26 15:45:21 +00:00
Sergey "Shnatsel" DavidoffandClaude 8b0da5939a Deduplicate fp_predict_f32 and fp_predict_f64 with a declarative macro
Replace the two near-identical floating-point predictor encoding
functions with a single fp_predict! macro parameterized on the float
type, using core::mem::size_of to derive the byte size.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-26 15:29:29 +00:00
Sergey "Shnatsel" Davidoff fdde3467fc Apply clippy lint 2026-02-26 15:00:21 +00:00
Sergey "Shnatsel" DavidoffandClaude Opus 4.5 3f2066bb20 Implement floating-point predictor (Predictor 3) encoding
Add support for encoding TIFF images with the floating-point predictor,
which improves compression ratios for floating-point image data.

The floating-point predictor works by:
1. Converting floats to big-endian bytes
2. Shuffling bytes so all first bytes are together, all second bytes, etc.
3. Applying byte-level horizontal differencing across the buffer

This complements the existing decoding support for Predictor 3.

Supported types: Gray32Float, Gray64Float, RGB32Float, RGB64Float,
RGBA32Float, RGBA64Float, CMYK32Float, CMYK64Float.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 23:05:35 +00:00
Aurelia Molzer 2ac8be1177 Merge pull request #337 from image-rs/cieluv
The whole libtiffpic test suite and SgiLuv compression
2026-02-20 06:47:57 +00:00
135 changed files with 7330 additions and 1095 deletions

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
name: Fuzz
on:
push:
branches: [main]
schedule:
- cron: '0 3 * * *'
workflow_dispatch:
inputs:
duration:
description: 'Seconds per fuzz target'
required: false
default: '300'
concurrency:
group: fuzz-${{ github.ref }}
cancel-in-progress: true
jobs:
fuzz:
name: Fuzz (${{ matrix.target }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- decode_comprehensive
- decode_image
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
with:
key: fuzz-${{ matrix.target }}
cache-directories: fuzz/target
- name: Install cargo-fuzz
run: cargo install cargo-fuzz --locked
- name: Determine duration
id: duration
run: |
if [ "${{ github.event_name }}" = "schedule" ]; then
echo "seconds=300" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "seconds=${{ github.event.inputs.duration || '300' }}" >> "$GITHUB_OUTPUT"
else
echo "seconds=60" >> "$GITHUB_OUTPUT"
fi
- name: Seed corpus
run: |
mkdir -p fuzz/corpus/${{ matrix.target }}
cp fuzz/regression/* fuzz/corpus/${{ matrix.target }}/ 2>/dev/null || true
cp fuzz/seed_corpus/${{ matrix.target }}/* fuzz/corpus/${{ matrix.target }}/ 2>/dev/null || true
- name: Fuzz ${{ matrix.target }}
run: |
cargo +nightly fuzz run ${{ matrix.target }} \
-- -max_total_time=${{ steps.duration.outputs.seconds }} \
-dict=fuzz/tiff.dict \
-rss_limit_mb=2048
- name: Upload crashes
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crashes-${{ matrix.target }}
path: fuzz/artifacts/
retention-days: 30
regression:
name: Fuzz regression
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build
- name: Regression seeds
run: |
if [ -d fuzz/regression ] && [ "$(ls fuzz/regression/ 2>/dev/null | wc -l)" -gt 0 ]; then
cargo test --test fuzz_regression 2>/dev/null || echo "No regression test found — seeds exist but no test harness"
fi
+32 -12
View File
@@ -11,7 +11,7 @@ jobs:
matrix: matrix:
rust: ["1.88.0", stable, beta, nightly] rust: ["1.88.0", stable, beta, nightly]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@nightly - uses: dtolnay/rust-toolchain@nightly
if: ${{ matrix.rust == '1.88.0' }} if: ${{ matrix.rust == '1.88.0' }}
@@ -32,13 +32,29 @@ jobs:
if: ${{ matrix.rust != '1.88.0' }} if: ${{ matrix.rust != '1.88.0' }}
run: cargo test -v && cargo doc -v run: cargo test -v && cargo doc -v
test-platforms:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, macos-26-intel, windows-latest, windows-11-arm]
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: test
run: cargo test -v
features: features:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
feature: ["", deflate, jpeg, lzw, zstd, default, "default,zstd"] feature: ["", deflate, jpeg, lzw, zstd, default, "default,zstd"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
components: clippy components: clippy
@@ -59,25 +75,29 @@ jobs:
matrix: matrix:
arch: [powerpc-unknown-linux-gnu, i686-unknown-linux-gnu] arch: [powerpc-unknown-linux-gnu, i686-unknown-linux-gnu]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Install or use cached cross-rs/cross - uses: taiki-e/install-action@cross
uses: baptiste0928/cargo-install@v2
with:
crate: cross
- name: Cache Cargo Dependencies - name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
cache-on-failure: true cache-on-failure: true
key: ${{ matrix.arch }} key: ${{ matrix.arch }}
- name: Start Docker (required for cross-rs)
run: sudo systemctl start docker
- name: Cross-Run Tests using QEMU - name: Cross-Run Tests using QEMU
run: cross test --target ${{ matrix.arch }} run: cross test --target ${{ matrix.arch }}
msrv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-hack
- name: Check MSRV
run: cargo hack check --rust-version
rustfmt: rustfmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
components: rustfmt components: rustfmt
@@ -87,8 +107,8 @@ jobs:
clippy: clippy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
components: clippy components: clippy
- run: cargo clippy --all-features # TODO: enable "-- -D warnings" once all outstanding clippy lints are fixed - run: cargo clippy --all-features --all-targets -- -D warnings
+3 -1
View File
@@ -1,4 +1,6 @@
/target/ /target/
**/*.rs.bk **/*.rs.bk
Cargo.lock Cargo.lock
tests/benches/corpus/
.idea
.agentbridge
+26
View File
@@ -1,3 +1,29 @@
# 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
offset rather than one indicated by an encoded header at the start.
Changes:
- The IFD iteration methods on `Decoder` (`next_image`, `seek_to_image`, etc)
now come with a strong error guarantee. They will not modify the current
directory state if they fail. This includes the failure to interpret an IFD
as a supported image in the case of `*_image` variants.
Compatibility:
- Changed how tiled TIFF files are detected. The presence of a TileWidth OR
TileHeight/TileLength tag now marks an image as tiled. Data is no retrieved
from either StripOffsets or TileOffsets with the latter taking precedence.
This is closer to the behavior of libtiff.
# Version 0.11.3 # Version 0.11.3
Additions: Additions:
+19 -4
View File
@@ -1,8 +1,7 @@
[package] [package]
name = "tiff" name = "ndbioimage-tiff"
version = "0.11.3" version = "0.11.3"
edition = "2021" edition = "2021"
resolver = "2"
# note: when changed, also update test runner in `.github/workflows/rust.yml` # note: when changed, also update test runner in `.github/workflows/rust.yml`
rust-version = "1.88.0" rust-version = "1.88.0"
@@ -16,6 +15,9 @@ categories = ["multimedia::images", "multimedia::encoding"]
exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"] exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
[lib]
name = "tiff"
[dependencies] [dependencies]
half = { version = "2.4.1" } half = { version = "2.4.1" }
quick-error = "2.0.1" quick-error = "2.0.1"
@@ -23,8 +25,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.14", 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,11 +48,23 @@ 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"
harness = false harness = false
[[bench]]
name = "encode"
harness = false
[[bench]]
name = "decode"
harness = false
[[test]] [[test]]
name = "decode_libtiffpic" name = "decode_libtiffpic"
path = "tests/decode_libtiffpic.rs" path = "tests/decode_libtiffpic.rs"
+3 -1
View File
@@ -5,6 +5,8 @@
TIFF decoding and encoding library in pure Rust TIFF decoding and encoding library in pure Rust
Forked from https://github.com/image-rs/image-tiff. Changes: read images containing RGBPalette frames, ignoring colormaps.
## Supported ## Supported
### Features ### Features
@@ -55,4 +57,4 @@ This crate uses [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) in order t
After installing it with `cargo install cargo-fuzz` on a nightly rustc, the After installing it with `cargo install cargo-fuzz` on a nightly rustc, the
fuzzing harness can be run with recommended settings using fuzzing harness can be run with recommended settings using
`cargo fuzz run decode_image -snone -- -timeout=5`. `cargo fuzz run decode_comprehensive -snone -- -dict=fuzz/tiff.dict -timeout=5`.
+461
View File
@@ -0,0 +1,461 @@
extern crate criterion;
extern crate tiff;
use criterion::{black_box, Criterion, Throughput};
use std::io::Cursor;
use tiff::decoder::Decoder;
use tiff::encoder::colortype::{Gray16, Gray8, RGB32Float, RGB16, RGB8, RGBA8};
use tiff::encoder::{Compression, Predictor, TiffEncoder};
const WIDTH: u32 = 2048;
const HEIGHT: u32 = 2048;
// === Corpus paths (real photo-based TIFFs) ===
// Set TIFF_BENCH_DIR / TIFF_GEOTIFF_DIR env vars to a directory containing the corpus files.
// If the files are not found, the corresponding benchmarks are silently skipped.
// The corpus can be generated with vips/imagemagick from any large photo; see the benchmark
// names for the expected filenames and compression settings.
const CORPUS_DIR: &str = "tests/benches/corpus";
fn bench_dir() -> String {
std::env::var("TIFF_BENCH_DIR").unwrap_or_else(|_| CORPUS_DIR.to_string())
}
fn geotiff_dir() -> String {
std::env::var("TIFF_GEOTIFF_DIR").unwrap_or_else(|_| CORPUS_DIR.to_string())
}
/// Load a file from the corpus directory, returning None if not found.
fn load_corpus(dir: &str, name: &str) -> Option<Vec<u8>> {
let path = format!("{}/{}", dir, name);
std::fs::read(&path).ok()
}
fn decode_tiff_bytes(data: &[u8]) {
let cursor = Cursor::new(data);
let mut decoder = Decoder::open(black_box(cursor)).unwrap();
decoder.next_image().unwrap();
decoder.read_image().unwrap();
}
// === Synthetic image generators ===
fn generate_rgb8(w: u32, h: u32) -> Vec<u8> {
(0..w * h * 3)
.map(|i| ((i * 7 + i / w * 3) & 0xFF) as u8)
.collect()
}
fn generate_rgba8(w: u32, h: u32) -> Vec<u8> {
(0..w * h * 4)
.map(|i| ((i * 11 + i / w * 5) & 0xFF) as u8)
.collect()
}
fn generate_gray8(w: u32, h: u32) -> Vec<u8> {
(0..w * h)
.map(|i| ((i * 7 + i / w * 3) & 0xFF) as u8)
.collect()
}
fn generate_rgb16(w: u32, h: u32) -> Vec<u16> {
(0..w * h * 3)
.map(|i| ((i * 257 + i / w * 131) & 0xFFFF) as u16)
.collect()
}
fn generate_gray16(w: u32, h: u32) -> Vec<u16> {
(0..w * h)
.map(|i| ((i * 257 + i / w * 131) & 0xFFFF) as u16)
.collect()
}
fn generate_rgb_f32(w: u32, h: u32) -> Vec<f32> {
let mut data = Vec::with_capacity((w * h * 3) as usize);
for y in 0..h {
for x in 0..w {
let fx = x as f32 / w as f32;
let fy = y as f32 / h as f32;
data.push(fx * 0.5 + fy * 0.3);
data.push(fy * 0.7 + fx * 0.2);
data.push((fx + fy) * 0.25);
}
}
data
}
// === Synthetic encoders (strip-only, encoder doesn't support tiles) ===
fn encode<C: tiff::encoder::colortype::ColorType>(
data: &[C::Inner],
w: u32,
h: u32,
compression: Compression,
predictor: Predictor,
) -> Vec<u8>
where
[C::Inner]: tiff::encoder::TiffValue,
{
let mut buf = Cursor::new(Vec::new());
TiffEncoder::new(&mut buf)
.unwrap()
.with_predictor(predictor)
.with_compression(compression)
.write_image::<C>(w, h, data)
.unwrap();
buf.into_inner()
}
fn main() {
let mut c = Criterion::default().configure_from_args();
let bd = bench_dir();
let gd = geotiff_dir();
// ========================================================================
// PART 1: Real photo corpus (5616x3744, ~21MP)
// ========================================================================
// Raw pixel sizes for throughput calculation:
// RGB8: 5616*3744*3 = 63,078,912
// RGB16: 5616*3744*6 = 126,157,824
// F32x3: 5616*3744*12 = 252,315,648
// Gray8: 5616*3744*1 = 21,026,304
// GrayF32: 5616*3744*4 = 84,105,216
// Palette8: 5616*3744*1 = 21,026,304 (output)
struct CorpusBench {
file: &'static str,
name: &'static str,
raw_bytes: u64,
}
let photo_benches = [
// Strip-based
CorpusBench {
file: "photo-rgb8-none.tif",
name: "strip-rgb8-none",
raw_bytes: 63_078_912,
},
CorpusBench {
file: "photo-rgb8-packbits.tif",
name: "strip-rgb8-packbits",
raw_bytes: 63_078_912,
},
CorpusBench {
file: "photo-rgb8-lzw.tif",
name: "strip-rgb8-lzw",
raw_bytes: 63_078_912,
},
CorpusBench {
file: "photo-rgb8-lzw-hpred.tif",
name: "strip-rgb8-lzw-hpred",
raw_bytes: 63_078_912,
},
CorpusBench {
file: "photo-rgb8-deflate-hpred.tif",
name: "strip-rgb8-deflate-hpred",
raw_bytes: 63_078_912,
},
CorpusBench {
file: "photo-rgb16-deflate-hpred.tif",
name: "strip-rgb16-deflate-hpred",
raw_bytes: 126_157_824,
},
CorpusBench {
file: "photo-gray8-lzw.tif",
name: "strip-gray8-lzw",
raw_bytes: 21_026_304,
},
CorpusBench {
file: "photo-gray8-deflate-hpred.tif",
name: "strip-gray8-deflate-hpred",
raw_bytes: 21_026_304,
},
CorpusBench {
file: "photo-rgb-f32-deflate-fpred.tif",
name: "strip-rgb-f32-deflate-fpred",
raw_bytes: 252_315_648,
},
CorpusBench {
file: "photo-gray-f32-deflate-fpred.tif",
name: "strip-gray-f32-deflate-fpred",
raw_bytes: 84_105_216,
},
CorpusBench {
file: "photo-palette8-lzw.tif",
name: "strip-palette8-lzw",
raw_bytes: 21_026_304,
},
CorpusBench {
file: "scan-bilevel-fax4.tif",
name: "strip-bilevel-fax4",
raw_bytes: 5616 * 3744 / 8,
},
// Tiled
CorpusBench {
file: "photo-rgb8-lzw-hpred-tiled256.tif",
name: "tiled256-rgb8-lzw-hpred",
raw_bytes: 63_078_912,
},
CorpusBench {
file: "photo-rgb8-deflate-hpred-tiled256.tif",
name: "tiled256-rgb8-deflate-hpred",
raw_bytes: 63_078_912,
},
CorpusBench {
file: "photo-rgb8-jpeg-tiled256.tif",
name: "tiled256-rgb8-jpeg",
raw_bytes: 63_078_912,
},
CorpusBench {
file: "photo-gray-f32-deflate-fpred-tiled512.tif",
name: "tiled512-gray-f32-deflate-fpred",
raw_bytes: 84_105_216,
},
];
{
let mut group = c.benchmark_group("photo-21mp");
group.sample_size(10);
for bench in &photo_benches {
if let Some(data) = load_corpus(&bd, bench.file) {
group.throughput(Throughput::Bytes(bench.raw_bytes));
let data_ref = &data;
group.bench_function(bench.name, |b| b.iter(|| decode_tiff_bytes(data_ref)));
}
}
group.finish();
}
// ========================================================================
// PART 2: GeoTIFF (real tiled DEM data)
// ========================================================================
let geotiff_benches = [
// Copernicus DEM: 3600x3600, tiled 1024x1024, Gray f32, deflate + fpredict
CorpusBench {
file: "Copernicus_DSM_COG_10_N40_00_W075_00_DEM.tif",
name: "copernicus-dem-f32-deflate-fpred-1024tile",
raw_bytes: 3600 * 3600 * 4,
},
// USGS DEM: 3612x3612, tiled 512x512, Gray f32, LZW + fpredict
CorpusBench {
file: "USGS_1_n40w075.tif",
name: "usgs-dem-f32-lzw-fpred-512tile",
raw_bytes: 3612 * 3612 * 4,
},
];
{
let mut group = c.benchmark_group("geotiff");
group.sample_size(10);
for bench in &geotiff_benches {
if let Some(data) = load_corpus(&gd, bench.file) {
group.throughput(Throughput::Bytes(bench.raw_bytes));
let data_ref = &data;
group.bench_function(bench.name, |b| b.iter(|| decode_tiff_bytes(data_ref)));
}
}
group.finish();
}
// ========================================================================
// PART 3: Synthetic (2048x2048, controlled comparisons)
// ========================================================================
let rgb8 = generate_rgb8(WIDTH, HEIGHT);
let rgba8 = generate_rgba8(WIDTH, HEIGHT);
let gray8 = generate_gray8(WIDTH, HEIGHT);
let rgb16 = generate_rgb16(WIDTH, HEIGHT);
let gray16 = generate_gray16(WIDTH, HEIGHT);
let rgb_f32 = generate_rgb_f32(WIDTH, HEIGHT);
let raw_rgb8 = (WIDTH * HEIGHT * 3) as u64;
let raw_rgba8 = (WIDTH * HEIGHT * 4) as u64;
let raw_gray8 = (WIDTH * HEIGHT) as u64;
let raw_rgb16 = (WIDTH * HEIGHT * 6) as u64;
let raw_gray16 = (WIDTH * HEIGHT * 2) as u64;
let raw_f32x3 = (WIDTH * HEIGHT * 12) as u64;
// --- Uncompressed baseline ---
{
let mut group = c.benchmark_group("synth-uncompressed");
group.sample_size(10);
let t = encode::<RGB8>(
&rgb8,
WIDTH,
HEIGHT,
Compression::Uncompressed,
Predictor::None,
);
group
.throughput(Throughput::Bytes(raw_rgb8))
.bench_function("rgb8", |b| b.iter(|| decode_tiff_bytes(&t)));
let t = encode::<RGBA8>(
&rgba8,
WIDTH,
HEIGHT,
Compression::Uncompressed,
Predictor::None,
);
group
.throughput(Throughput::Bytes(raw_rgba8))
.bench_function("rgba8", |b| b.iter(|| decode_tiff_bytes(&t)));
let t = encode::<Gray8>(
&gray8,
WIDTH,
HEIGHT,
Compression::Uncompressed,
Predictor::None,
);
group
.throughput(Throughput::Bytes(raw_gray8))
.bench_function("gray8", |b| b.iter(|| decode_tiff_bytes(&t)));
let t = encode::<RGB16>(
&rgb16,
WIDTH,
HEIGHT,
Compression::Uncompressed,
Predictor::None,
);
group
.throughput(Throughput::Bytes(raw_rgb16))
.bench_function("rgb16", |b| b.iter(|| decode_tiff_bytes(&t)));
let t = encode::<Gray16>(
&gray16,
WIDTH,
HEIGHT,
Compression::Uncompressed,
Predictor::None,
);
group
.throughput(Throughput::Bytes(raw_gray16))
.bench_function("gray16", |b| b.iter(|| decode_tiff_bytes(&t)));
group.finish();
}
// --- Predictor overhead isolation (deflate-1, with vs without predictor) ---
{
let mut group = c.benchmark_group("synth-predictor");
group.sample_size(10);
let t = encode::<RGB8>(
&rgb8,
WIDTH,
HEIGHT,
Compression::Deflate(1),
Predictor::None,
);
group
.throughput(Throughput::Bytes(raw_rgb8))
.bench_function("rgb8-deflate-none", |b| b.iter(|| decode_tiff_bytes(&t)));
let t = encode::<RGB8>(
&rgb8,
WIDTH,
HEIGHT,
Compression::Deflate(1),
Predictor::Horizontal,
);
group
.throughput(Throughput::Bytes(raw_rgb8))
.bench_function("rgb8-deflate-hpred", |b| b.iter(|| decode_tiff_bytes(&t)));
let t = encode::<RGB16>(
&rgb16,
WIDTH,
HEIGHT,
Compression::Deflate(1),
Predictor::None,
);
group
.throughput(Throughput::Bytes(raw_rgb16))
.bench_function("rgb16-deflate-none", |b| b.iter(|| decode_tiff_bytes(&t)));
let t = encode::<RGB16>(
&rgb16,
WIDTH,
HEIGHT,
Compression::Deflate(1),
Predictor::Horizontal,
);
group
.throughput(Throughput::Bytes(raw_rgb16))
.bench_function("rgb16-deflate-hpred", |b| b.iter(|| decode_tiff_bytes(&t)));
let t = encode::<RGB32Float>(
&rgb_f32,
WIDTH,
HEIGHT,
Compression::Deflate(1),
Predictor::None,
);
group
.throughput(Throughput::Bytes(raw_f32x3))
.bench_function("rgb-f32-deflate-none", |b| b.iter(|| decode_tiff_bytes(&t)));
let t = encode::<RGB32Float>(
&rgb_f32,
WIDTH,
HEIGHT,
Compression::Deflate(1),
Predictor::FloatingPoint,
);
group
.throughput(Throughput::Bytes(raw_f32x3))
.bench_function("rgb-f32-deflate-fpred", |b| {
b.iter(|| decode_tiff_bytes(&t))
});
group.finish();
}
// --- LZW vs Deflate comparison ---
{
let mut group = c.benchmark_group("synth-compression");
group.sample_size(10);
let t = encode::<RGB8>(
&rgb8,
WIDTH,
HEIGHT,
Compression::Deflate(1),
Predictor::Horizontal,
);
group
.throughput(Throughput::Bytes(raw_rgb8))
.bench_function("rgb8-deflate-hpred", |b| b.iter(|| decode_tiff_bytes(&t)));
let t = encode::<RGB8>(
&rgb8,
WIDTH,
HEIGHT,
Compression::Lzw,
Predictor::Horizontal,
);
group
.throughput(Throughput::Bytes(raw_rgb8))
.bench_function("rgb8-lzw-hpred", |b| b.iter(|| decode_tiff_bytes(&t)));
let t = encode::<RGB8>(&rgb8, WIDTH, HEIGHT, Compression::Lzw, Predictor::None);
group
.throughput(Throughput::Bytes(raw_rgb8))
.bench_function("rgb8-lzw-none", |b| b.iter(|| decode_tiff_bytes(&t)));
let t = encode::<RGB8>(&rgb8, WIDTH, HEIGHT, Compression::Packbits, Predictor::None);
group
.throughput(Throughput::Bytes(raw_rgb8))
.bench_function("rgb8-packbits", |b| b.iter(|| decode_tiff_bytes(&t)));
group.finish();
}
}
+44
View File
@@ -0,0 +1,44 @@
extern crate criterion;
extern crate tiff;
use criterion::{black_box, Criterion, Throughput};
use std::io::Cursor;
use tiff::encoder::colortype::RGB32Float;
use tiff::encoder::{Compression, Predictor, TiffEncoder};
const WIDTH: u32 = 500;
const HEIGHT: u32 = 500;
fn encode_image_f32_float_predictor_deflate(data: &[f32]) {
let mut buf = Cursor::new(Vec::with_capacity(WIDTH as usize * HEIGHT as usize * 4));
TiffEncoder::new(&mut buf)
.unwrap()
.with_predictor(Predictor::FloatingPoint)
.with_compression(Compression::Deflate(1))
.write_image::<RGB32Float>(WIDTH, HEIGHT, black_box(data))
.unwrap();
}
fn main() {
// Generate synthetic f32 image data with a gradient pattern
let data: Vec<f32> = (0..WIDTH as usize * HEIGHT as usize * 3)
.map(|i| {
let pixel = i / 3;
let x = (pixel % WIDTH as usize) as f32 / WIDTH as f32;
let y = (pixel / WIDTH as usize) as f32 / HEIGHT as f32;
x * 0.5 + y * 0.5
})
.collect();
let mut c = Criterion::default().configure_from_args();
let mut group = c.benchmark_group("tiff-encode");
group
.sample_size(20)
.throughput(Throughput::Bytes(
(WIDTH as u64) * (HEIGHT as u64) * 3 * std::mem::size_of::<f32>() as u64,
))
.bench_function("f32-float-predictor-deflate-fast", |b| {
b.iter(|| encode_image_f32_float_predictor_deflate(&data))
});
}
+4 -5
View File
@@ -8,13 +8,12 @@ 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::new(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.next_directory().unwrap();
reader.read_image().unwrap(); reader.read_image().unwrap();
reader.more_images() }
} {}
} }
fn main() { fn main() {
+33 -29
View File
@@ -1,29 +1,31 @@
use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingBuffer, DecodingResult}; use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingSampleBuffer, DecodingSampleSlice};
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::new(io)?; let mut reader = Decoder::open(io)?.with_lenient(lenient);
let mut data = DecodingResult::I8(vec![]); let mut data = DecodingSampleBuffer::I8(vec![]);
for i in 0u32.. { for i in 0u32.. {
if !reader.more_images() {
break;
}
reader.next_directory()?;
let colortype = reader.colortype()?; let colortype = reader.colortype()?;
let dimensions = reader.dimensions()?; let dimensions = reader.dimensions()?;
let layout = reader.read_image_to_buffer(&mut data)?; let layout = reader.read_image_to_buffer(&mut data)?;
debug_planes(i, &mut data, &layout, &colortype, dimensions); debug_planes(i, &mut data, &layout, &colortype, dimensions);
if !reader.more_images() {
break;
}
reader.next_image()?
} }
Ok(()) Ok(())
@@ -31,32 +33,32 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
fn debug_planes( fn debug_planes(
index: u32, index: u32,
data: &mut DecodingResult, data: &mut DecodingSampleBuffer,
layout: &BufferLayoutPreference, layout: &BufferLayoutPreference,
colortype: &tiff::ColorType, colortype: &tiff::ColorType,
(width, height): (u32, u32), (width, height): (u32, u32),
) { ) {
match data { match data {
DecodingResult::F16(v) => { DecodingSampleBuffer::F16(v) => {
let samples = v let samples = v
.iter() .iter()
.map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16) .map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
*data = DecodingResult::U16(samples); *data = DecodingSampleBuffer::U16(samples);
} }
DecodingResult::F32(v) => { DecodingSampleBuffer::F32(v) => {
let samples = v let samples = v
.iter() .iter()
.map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16) .map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
*data = DecodingResult::U16(samples); *data = DecodingSampleBuffer::U16(samples);
} }
DecodingResult::F64(v) => { DecodingSampleBuffer::F64(v) => {
let samples = v let samples = v
.iter() .iter()
.map(|&f| (f * f64::from(u16::MAX)) as u16) .map(|&f| (f * f64::from(u16::MAX)) as u16)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
*data = DecodingResult::U16(samples); *data = DecodingSampleBuffer::U16(samples);
} }
_ => {} _ => {}
} }
@@ -98,24 +100,24 @@ fn debug_planes(
// them here (e.g. represent 1-bit as `{0, 1}`) but we'd also set `maxval` accordingly and // them here (e.g. represent 1-bit as `{0, 1}`) but we'd also set `maxval` accordingly and
// this requires tupltype to be `BLACKANDWHITE` so introducing a bunch of complexity. If // this requires tupltype to be `BLACKANDWHITE` so introducing a bunch of complexity. If
// you want to do that in your real code, go ahead. // you want to do that in your real code, go ahead.
DecodingBuffer::U8(buf) => match colortype.bit_depth() { DecodingSampleSlice::U8(buf) => match colortype.bit_depth() {
8 => u64::from(u8::MAX), 8 => u64::from(u8::MAX),
4 => { 4 => {
fallback_buffer = vec![0; (swidth * sheight) as usize]; fallback_buffer = vec![0; (swidth * sheight) as usize];
expand_4bit(buf, swidth, &mut fallback_buffer); expand_4bit(buf, swidth, &mut fallback_buffer);
*data = DecodingResult::U8(fallback_buffer); *data = DecodingSampleBuffer::U8(fallback_buffer);
u64::from(u8::MAX) u64::from(u8::MAX)
} }
2 => { 2 => {
fallback_buffer = vec![0; (swidth * sheight) as usize]; fallback_buffer = vec![0; (swidth * sheight) as usize];
expand_2bit(buf, swidth, &mut fallback_buffer); expand_2bit(buf, swidth, &mut fallback_buffer);
*data = DecodingResult::U8(fallback_buffer); *data = DecodingSampleBuffer::U8(fallback_buffer);
u64::from(u8::MAX) u64::from(u8::MAX)
} }
1 => { 1 => {
fallback_buffer = vec![0; (swidth * sheight) as usize]; fallback_buffer = vec![0; (swidth * sheight) as usize];
expand_1bit(buf, swidth, &mut fallback_buffer); expand_1bit(buf, swidth, &mut fallback_buffer);
*data = DecodingResult::U8(fallback_buffer); *data = DecodingSampleBuffer::U8(fallback_buffer);
u64::from(u8::MAX) u64::from(u8::MAX)
} }
_ => { _ => {
@@ -126,28 +128,30 @@ fn debug_planes(
return; return;
} }
}, },
DecodingBuffer::U16(_) => u64::from(u16::MAX), DecodingSampleSlice::U16(_) => u64::from(u16::MAX),
DecodingBuffer::U32(_) => u64::from(u32::MAX), DecodingSampleSlice::U32(_) => u64::from(u32::MAX),
DecodingBuffer::U64(_) => u64::MAX, DecodingSampleSlice::U64(_) => u64::MAX,
// Eh, this is a good effort I guess. // Eh, this is a good effort I guess.
DecodingBuffer::I8(items) => { DecodingSampleSlice::I8(items) => {
items.iter_mut().for_each(|v| *v = 0.max(*v)); items.iter_mut().for_each(|v| *v = 0.max(*v));
u64::from(i8::MAX as u8) u64::from(i8::MAX as u8)
} }
DecodingBuffer::I16(items) => { DecodingSampleSlice::I16(items) => {
items.iter_mut().for_each(|v| *v = 0.max(*v)); items.iter_mut().for_each(|v| *v = 0.max(*v));
u64::from(i16::MAX as u16) u64::from(i16::MAX as u16)
} }
DecodingBuffer::I32(items) => { DecodingSampleSlice::I32(items) => {
items.iter_mut().for_each(|v| *v = 0.max(*v)); items.iter_mut().for_each(|v| *v = 0.max(*v));
u64::from(i32::MAX as u32) u64::from(i32::MAX as u32)
} }
DecodingBuffer::I64(items) => { DecodingSampleSlice::I64(items) => {
items.iter_mut().for_each(|v| *v = 0.max(*v)); items.iter_mut().for_each(|v| *v = 0.max(*v));
i64::MAX as u64 i64::MAX as u64
} }
// Not supported by PAM. // Not supported by PAM.
DecodingBuffer::F16(_) | DecodingBuffer::F32(_) | DecodingBuffer::F64(_) => return, DecodingSampleSlice::F16(_) | DecodingSampleSlice::F32(_) | DecodingSampleSlice::F64(_) => {
return
}
}; };
let byte_len = if layout.planes > 1 { let byte_len = if layout.planes > 1 {
+130
View File
@@ -0,0 +1,130 @@
//! Profiling helper: encodes and decodes several TIFF configurations.
//! Run under callgrind: `valgrind --tool=callgrind target/release/examples/profile_decode`
use std::io::Cursor;
use tiff::decoder::Decoder;
use tiff::encoder::colortype::{Gray8, RGB32Float, RGB16, RGB8};
use tiff::encoder::{Compression, Predictor, TiffEncoder};
const WIDTH: u32 = 2048;
const HEIGHT: u32 = 2048;
const ITERS: usize = 3;
fn generate_rgb8(w: u32, h: u32) -> Vec<u8> {
(0..w * h * 3).map(|i| (i % 256) as u8).collect()
}
fn generate_rgb16(w: u32, h: u32) -> Vec<u16> {
(0..w * h * 3).map(|i| (i % 65536) as u16).collect()
}
fn generate_rgb_f32(w: u32, h: u32) -> Vec<f32> {
(0..w * h * 3)
.map(|i| (i as f32) / (w * h * 3) as f32)
.collect()
}
fn generate_gray8(w: u32, h: u32) -> Vec<u8> {
(0..w * h).map(|i| (i % 256) as u8).collect()
}
fn encode<C: tiff::encoder::colortype::ColorType>(
data: &[C::Inner],
w: u32,
h: u32,
compression: Compression,
predictor: Predictor,
) -> Vec<u8>
where
[C::Inner]: tiff::encoder::TiffValue,
{
let mut buf = Cursor::new(Vec::new());
TiffEncoder::new(&mut buf)
.unwrap()
.with_predictor(predictor)
.with_compression(compression)
.write_image::<C>(w, h, data)
.unwrap();
buf.into_inner()
}
fn decode(data: &[u8]) {
let cursor = Cursor::new(data);
let mut decoder = Decoder::open(cursor).unwrap();
decoder.next_image().unwrap();
let _ = decoder.read_image().unwrap();
}
fn main() {
let rgb8 = generate_rgb8(WIDTH, HEIGHT);
let rgb16 = generate_rgb16(WIDTH, HEIGHT);
let rgb_f32 = generate_rgb_f32(WIDTH, HEIGHT);
let gray8 = generate_gray8(WIDTH, HEIGHT);
let cases: Vec<(&str, Vec<u8>)> = vec![
(
"deflate-rgb8-hpredict",
encode::<RGB8>(
&rgb8,
WIDTH,
HEIGHT,
Compression::Deflate(1),
Predictor::Horizontal,
),
),
(
"deflate-gray8-hpredict",
encode::<Gray8>(
&gray8,
WIDTH,
HEIGHT,
Compression::Deflate(1),
Predictor::Horizontal,
),
),
(
"deflate-rgb16-hpredict",
encode::<RGB16>(
&rgb16,
WIDTH,
HEIGHT,
Compression::Deflate(1),
Predictor::Horizontal,
),
),
(
"deflate-rgb-f32-fpredict",
encode::<RGB32Float>(
&rgb_f32,
WIDTH,
HEIGHT,
Compression::Deflate(1),
Predictor::FloatingPoint,
),
),
(
"lzw-rgb8-no-pred",
encode::<RGB8>(&rgb8, WIDTH, HEIGHT, Compression::Lzw, Predictor::None),
),
(
"lzw-rgb8-hpredict",
encode::<RGB8>(
&rgb8,
WIDTH,
HEIGHT,
Compression::Lzw,
Predictor::Horizontal,
),
),
];
for (name, tiff_data) in &cases {
eprintln!(
"=== Profiling: {} ({} bytes encoded) ===",
name,
tiff_data.len()
);
for _ in 0..ITERS {
decode(tiff_data);
}
}
}
+4 -7
View File
@@ -10,14 +10,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
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::new(io)?; let mut reader = Decoder::open(io)?;
loop { while reader.more_images() {
reader.next_directory()?;
ls_dir(&mut reader); ls_dir(&mut reader);
if !reader.more_images() {
break;
}
} }
Ok(()) Ok(())
@@ -29,7 +26,7 @@ fn ls_dir<R: std::io::Read + std::io::Seek>(reader: &mut Decoder<R>) {
} }
println!("Name\tHex\tType\tCount"); println!("Name\tHex\tType\tCount");
let ifd = reader.image_ifd(); let ifd = reader.current_ifd();
for (tag, entry) in ifd.directory().iter() { for (tag, entry) in ifd.directory().iter() {
let name: &'static str = match tag { let name: &'static str = match tag {
+2 -2
View File
@@ -19,7 +19,7 @@ path = ".."
members = ["."] members = ["."]
[[bin]] [[bin]]
name = "decode_image" name = "decode_comprehensive"
path = "fuzz_targets/decode_image.rs" path = "fuzz_targets/decode_comprehensive.rs"
test = false test = false
doc = false doc = false
+96
View File
@@ -0,0 +1,96 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use std::io::{Cursor, Read, Seek};
use tiff::decoder::{Decoder, Limits};
use tiff::tags::Tag;
fuzz_target!(|data: &[u8]| {
// Standalone header parse (separate code path from Decoder::open)
let _ = tiff::decoder::TiffHeader::parse(Cursor::new(data));
let mut decoder = match Decoder::open(Cursor::new(data)) {
Ok(d) => d,
Err(_) => return,
};
let mut limits = Limits::default();
limits.decoding_buffer_size = 512 * 1024;
limits.ifd_value_size = 64 * 1024;
limits.intermediate_buffer_size = 512 * 1024;
decoder = decoder.with_limits(limits);
// Probe first IFD (opened but image not yet parsed)
probe_directory(&mut decoder);
// Linear IFD iteration via next_directory — tolerates invalid
// intermediate IFDs that aren't decodable as images
let mut ifd_count = 1u32;
while decoder.more_images() && ifd_count < 8 {
if decoder.next_directory().is_err() {
break;
}
probe_directory(&mut decoder);
ifd_count += 1;
}
// Random-access traversal via seek (different code path: find_nth_ifd
// re-walks the IFD chain from the start each time)
for i in 0..ifd_count.min(4) as usize {
if decoder.seek_to_directory(i).is_ok() {
probe_directory(&mut decoder);
}
// seek_to_image eagerly parses the directory as an Image
let _ = decoder.seek_to_image(i);
}
});
fn probe_directory<R: Read + Seek>(decoder: &mut Decoder<R>) {
// Tag iteration — exercises value parsing for every tag in the IFD
for result in decoder.current_ifd().tag_iter() {
let _ = result;
}
// SubIFD / EXIF / GPS directory traversal — exercises read_directory
// with potentially adversarial offsets
if let Ok(Some(val)) = decoder.current_ifd().find_tag(Tag::SubIfd) {
if let Ok(ptrs) = val.into_ifd_vec() {
for ptr in ptrs.into_iter().take(4) {
let _ = decoder.read_directory(ptr);
}
}
}
for tag in [Tag::ExifDirectory, Tag::GpsDirectory] {
if let Ok(Some(val)) = decoder.current_ifd().find_tag(tag) {
if let Ok(ptr) = val.into_ifd_pointer() {
let _ = decoder.read_directory(ptr);
}
}
}
// Image-level probing — dimensions() triggers ensure_image,
// which lazily parses the directory as an Image
if decoder.dimensions().is_ok() {
let _ = decoder.colortype();
let _ = decoder.color_map();
let _ = decoder.get_chunk_type();
let _ = decoder.chunk_dimensions();
let _ = decoder.strip_count();
let _ = decoder.tile_count();
let _ = decoder.image_buffer_layout();
let _ = decoder.image_chunk_buffer_layout(0);
// Chunk reads at boundaries — first and last exercise
// different offset calculations
let _ = decoder.read_chunk(0);
let chunk_count = decoder
.strip_count()
.or_else(|_| decoder.tile_count())
.unwrap_or(0);
if chunk_count > 1 {
let _ = decoder.read_chunk(chunk_count - 1);
}
// Full image decode
let _ = decoder.read_image();
}
}
-31
View File
@@ -1,31 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let mut decoder = if let Ok(d) = tiff::decoder::Decoder::new(std::io::Cursor::new(data)) {
d
} else {
return;
};
let mut limits = tiff::decoder::Limits::default();
limits.decoding_buffer_size = 1_000_000;
limits.ifd_value_size = 1_000_000;
limits.intermediate_buffer_size = 1_000_000;
decoder = decoder.with_limits(limits);
loop {
if let Err(_) = decoder.read_image() {
break;
}
if !decoder.more_images() {
break;
}
if let Err(_) = decoder.next_image() {
break;
}
}
});
+139
View File
@@ -0,0 +1,139 @@
# === File headers ===
# Classic TIFF (LE, BE)
"\x49\x49\x2A\x00"
"\x4D\x4D\x00\x2A"
# BigTIFF (LE, BE) — version 43, 8-byte offsets, different parsing paths
"\x49\x49\x2B\x00\x08\x00\x00\x00"
"\x4D\x4D\x00\x2B\x00\x08\x00\x00"
# === IFD field type codes (2-byte LE) ===
# Decoder switches on these; wrong types exercise error paths
"\x01\x00"
"\x02\x00"
"\x03\x00"
"\x04\x00"
"\x05\x00"
"\x06\x00"
"\x07\x00"
"\x08\x00"
"\x09\x00"
"\x0A\x00"
"\x0B\x00"
"\x0C\x00"
"\x0D\x00"
"\x10\x00"
"\x11\x00"
"\x12\x00"
# === IFD tag numbers (2-byte LE) ===
# 256=ImageWidth 257=ImageLength 258=BitsPerSample
"\x00\x01"
"\x01\x01"
"\x02\x01"
# 259=Compression 262=PhotometricInterpretation
"\x03\x01"
"\x06\x01"
# 273=StripOffsets 274=Orientation
"\x11\x01"
"\x12\x01"
# 277=SamplesPerPixel 278=RowsPerStrip 279=StripByteCounts
"\x15\x01"
"\x16\x01"
"\x17\x01"
# 282=XResolution 283=YResolution 284=PlanarConfiguration
"\x1A\x01"
"\x1B\x01"
"\x1C\x01"
# 317=Predictor 320=ColorMap
"\x3D\x01"
"\x40\x01"
# 322=TileWidth 323=TileLength 324=TileOffsets 325=TileByteCounts
"\x42\x01"
"\x43\x01"
"\x44\x01"
"\x45\x01"
# 330=SubIfd 338=ExtraSamples 339=SampleFormat
"\x4A\x01"
"\x52\x01"
"\x53\x01"
# 347=JPEGTables 530=ChromaSubsampling
"\x5B\x01"
"\x12\x02"
# 34665=ExifDirectory 34675=IccProfile 34853=GpsDirectory
"\x69\x87"
"\x73\x87"
"\x25\x88"
# === IFD tag numbers (2-byte BE) ===
"\x01\x00"
"\x01\x01"
"\x01\x02"
"\x01\x03"
"\x01\x06"
"\x01\x11"
"\x01\x12"
"\x01\x15"
"\x01\x16"
"\x01\x17"
"\x01\x1A"
"\x01\x1B"
"\x01\x1C"
"\x01\x3D"
"\x01\x40"
"\x01\x42"
"\x01\x43"
"\x01\x44"
"\x01\x45"
"\x01\x4A"
"\x01\x52"
"\x01\x53"
"\x01\x5B"
"\x02\x12"
"\x87\x69"
"\x87\x73"
"\x88\x25"
# === Compression codes (4-byte LE inline SHORT values) ===
# These are the values stored in the IFD entry for tag 259
# 1=None 2=Huffman 3=Fax3 4=Fax4 5=LZW
"\x01\x00\x00\x00"
"\x02\x00\x00\x00"
"\x03\x00\x00\x00"
"\x04\x00\x00\x00"
"\x05\x00\x00\x00"
# 6=OldJPEG 7=ModernJPEG 8=Deflate
"\x06\x00\x00\x00"
"\x07\x00\x00\x00"
"\x08\x00\x00\x00"
# 0x8005=PackBits 0x80B2=OldDeflate
"\x05\x80\x00\x00"
"\xB2\x80\x00\x00"
# === Useful small values (4-byte LE) ===
# BitsPerSample=16, =32 (8 already covered as Deflate compression above)
"\x10\x00\x00\x00"
"\x20\x00\x00\x00"
# === Minimal IFD entries (12-byte LE: tag + type SHORT + count 1 + value) ===
# These let the fuzzer splice complete valid entries into an IFD
# ImageWidth=1
"\x00\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
# ImageLength=1
"\x01\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
# BitsPerSample=8
"\x02\x01\x03\x00\x01\x00\x00\x00\x08\x00\x00\x00"
# Compression=None
"\x03\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
# PhotometricInterpretation=BlackIsZero
"\x06\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
# SamplesPerPixel=1
"\x15\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
# RowsPerStrip=1
"\x16\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
+410
View File
@@ -0,0 +1,410 @@
use half::f16;
use std::alloc::{Layout, LayoutError};
use crate::{
bytecast,
decoder::{BufferLayoutPreference, Limits},
error::{TiffError, TiffResult, TiffUnsupportedError},
tags::{ByteOrder, EndianBytes, SampleFormat},
};
/// Result of a decoding process
#[derive(Debug)]
pub enum DecodingSampleBuffer {
/// A vector of unsigned bytes
U8(Vec<u8>),
/// A vector of unsigned words
U16(Vec<u16>),
/// A vector of 32 bit unsigned ints
U32(Vec<u32>),
/// A vector of 64 bit unsigned ints
U64(Vec<u64>),
/// A vector of 16 bit IEEE floats (held in u16)
F16(Vec<f16>),
/// A vector of 32 bit IEEE floats
F32(Vec<f32>),
/// A vector of 64 bit IEEE floats
F64(Vec<f64>),
/// A vector of 8 bit signed ints
I8(Vec<i8>),
/// A vector of 16 bit signed ints
I16(Vec<i16>),
/// A vector of 32 bit signed ints
I32(Vec<i32>),
/// A vector of 64 bit signed ints
I64(Vec<i64>),
}
impl DecodingSampleBuffer {
/// Reallocate the buffer to decode all planes of the indicated layout.
pub fn resize_to(
&mut self,
buffer: &BufferLayoutPreference,
limits: &Limits,
) -> Result<(), TiffError> {
let sample_type = buffer.sample_type.ok_or(TiffError::UnsupportedError(
TiffUnsupportedError::UnknownInterpretation,
))?;
let extent = sample_type.extent_for_bytes(buffer.complete_len);
self.resize_to_extent(extent, limits)
}
pub(crate) fn resize_to_extent(
&mut self,
extent: DecodingExtent,
limits: &Limits,
) -> Result<(), TiffError> {
// FIXME: we *can* reuse the allocation sometimes.
*self = extent.to_result_buffer(limits)?;
Ok(())
}
fn new<T: Default + Copy>(
size: usize,
limits: &Limits,
from_fn: fn(Vec<T>) -> Self,
) -> TiffResult<DecodingSampleBuffer> {
if size > limits.decoding_buffer_size / core::mem::size_of::<T>() {
Err(TiffError::LimitsExceeded)
} else {
Ok(from_fn(vec![T::default(); size]))
}
}
fn new_u8(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
Self::new(size, limits, DecodingSampleBuffer::U8)
}
fn new_u16(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
Self::new(size, limits, DecodingSampleBuffer::U16)
}
fn new_u32(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
Self::new(size, limits, DecodingSampleBuffer::U32)
}
fn new_u64(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
Self::new(size, limits, DecodingSampleBuffer::U64)
}
fn new_f32(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
Self::new(size, limits, DecodingSampleBuffer::F32)
}
fn new_f64(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
Self::new(size, limits, DecodingSampleBuffer::F64)
}
fn new_f16(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
Self::new(size, limits, DecodingSampleBuffer::F16)
}
fn new_i8(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
Self::new(size, limits, DecodingSampleBuffer::I8)
}
fn new_i16(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
Self::new(size, limits, DecodingSampleBuffer::I16)
}
fn new_i32(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
Self::new(size, limits, DecodingSampleBuffer::I32)
}
fn new_i64(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
Self::new(size, limits, DecodingSampleBuffer::I64)
}
/// Get a view of this buffer starting from the nth _sample_ of the current type.
pub fn as_buffer(&mut self, start: usize) -> DecodingSampleSlice<'_> {
match *self {
DecodingSampleBuffer::U8(ref mut buf) => DecodingSampleSlice::U8(&mut buf[start..]),
DecodingSampleBuffer::U16(ref mut buf) => DecodingSampleSlice::U16(&mut buf[start..]),
DecodingSampleBuffer::U32(ref mut buf) => DecodingSampleSlice::U32(&mut buf[start..]),
DecodingSampleBuffer::U64(ref mut buf) => DecodingSampleSlice::U64(&mut buf[start..]),
DecodingSampleBuffer::F16(ref mut buf) => DecodingSampleSlice::F16(&mut buf[start..]),
DecodingSampleBuffer::F32(ref mut buf) => DecodingSampleSlice::F32(&mut buf[start..]),
DecodingSampleBuffer::F64(ref mut buf) => DecodingSampleSlice::F64(&mut buf[start..]),
DecodingSampleBuffer::I8(ref mut buf) => DecodingSampleSlice::I8(&mut buf[start..]),
DecodingSampleBuffer::I16(ref mut buf) => DecodingSampleSlice::I16(&mut buf[start..]),
DecodingSampleBuffer::I32(ref mut buf) => DecodingSampleSlice::I32(&mut buf[start..]),
DecodingSampleBuffer::I64(ref mut buf) => DecodingSampleSlice::I64(&mut buf[start..]),
}
}
pub fn sample_type(&self) -> DecodingSampleType {
match *self {
DecodingSampleBuffer::U8(_) => DecodingSampleType::U8,
DecodingSampleBuffer::U16(_) => DecodingSampleType::U16,
DecodingSampleBuffer::U32(_) => DecodingSampleType::U32,
DecodingSampleBuffer::U64(_) => DecodingSampleType::U64,
DecodingSampleBuffer::F16(_) => DecodingSampleType::F16,
DecodingSampleBuffer::F32(_) => DecodingSampleType::F32,
DecodingSampleBuffer::F64(_) => DecodingSampleType::F64,
DecodingSampleBuffer::I8(_) => DecodingSampleType::I8,
DecodingSampleBuffer::I16(_) => DecodingSampleType::I16,
DecodingSampleBuffer::I32(_) => DecodingSampleType::I32,
DecodingSampleBuffer::I64(_) => DecodingSampleType::I64,
}
}
/// Change the endianess of buffered data.
///
/// This utility may be used to prepare a buffer for consumption by methods taking raw bytes or
/// after filling the buffer with raw bytes.
///
/// After this, interpreting the buffer on the host may no longer produce any reasonable
/// results. For instance, floats in the wrong byte order may produce a `NaN` with varying
/// payloads. Despite unexpected result however, this operation is always sound.
pub fn convert_endian(&mut self, from: ByteOrder, to: ByteOrder) {
self.as_buffer(0).convert_endian(from, to)
}
}
// A buffer for image decoding
pub enum DecodingSampleSlice<'a> {
/// A slice of unsigned bytes
U8(&'a mut [u8]),
/// A slice of unsigned words
U16(&'a mut [u16]),
/// A slice of 32 bit unsigned ints
U32(&'a mut [u32]),
/// A slice of 64 bit unsigned ints
U64(&'a mut [u64]),
/// A slice of 16 bit IEEE floats
F16(&'a mut [f16]),
/// A slice of 32 bit IEEE floats
F32(&'a mut [f32]),
/// A slice of 64 bit IEEE floats
F64(&'a mut [f64]),
/// A slice of 8 bits signed ints
I8(&'a mut [i8]),
/// A slice of 16 bits signed ints
I16(&'a mut [i16]),
/// A slice of 32 bits signed ints
I32(&'a mut [i32]),
/// A slice of 64 bits signed ints
I64(&'a mut [i64]),
}
impl<'a> DecodingSampleSlice<'a> {
pub fn as_bytes(&self) -> &[u8] {
match self {
DecodingSampleSlice::U8(buf) => buf,
DecodingSampleSlice::I8(buf) => bytecast::i8_as_ne_bytes(buf),
DecodingSampleSlice::U16(buf) => bytecast::u16_as_ne_bytes(buf),
DecodingSampleSlice::I16(buf) => bytecast::i16_as_ne_bytes(buf),
DecodingSampleSlice::U32(buf) => bytecast::u32_as_ne_bytes(buf),
DecodingSampleSlice::I32(buf) => bytecast::i32_as_ne_bytes(buf),
DecodingSampleSlice::U64(buf) => bytecast::u64_as_ne_bytes(buf),
DecodingSampleSlice::I64(buf) => bytecast::i64_as_ne_bytes(buf),
DecodingSampleSlice::F16(buf) => bytecast::f16_as_ne_bytes(buf),
DecodingSampleSlice::F32(buf) => bytecast::f32_as_ne_bytes(buf),
DecodingSampleSlice::F64(buf) => bytecast::f64_as_ne_bytes(buf),
}
}
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
match self {
DecodingSampleSlice::U8(buf) => buf,
DecodingSampleSlice::I8(buf) => bytecast::i8_as_ne_mut_bytes(buf),
DecodingSampleSlice::U16(buf) => bytecast::u16_as_ne_mut_bytes(buf),
DecodingSampleSlice::I16(buf) => bytecast::i16_as_ne_mut_bytes(buf),
DecodingSampleSlice::U32(buf) => bytecast::u32_as_ne_mut_bytes(buf),
DecodingSampleSlice::I32(buf) => bytecast::i32_as_ne_mut_bytes(buf),
DecodingSampleSlice::U64(buf) => bytecast::u64_as_ne_mut_bytes(buf),
DecodingSampleSlice::I64(buf) => bytecast::i64_as_ne_mut_bytes(buf),
DecodingSampleSlice::F16(buf) => bytecast::f16_as_ne_mut_bytes(buf),
DecodingSampleSlice::F32(buf) => bytecast::f32_as_ne_mut_bytes(buf),
DecodingSampleSlice::F64(buf) => bytecast::f64_as_ne_mut_bytes(buf),
}
}
pub fn reborrow(&mut self) -> DecodingSampleSlice<'_> {
match self {
DecodingSampleSlice::U8(items) => DecodingSampleSlice::U8(items),
DecodingSampleSlice::I8(items) => DecodingSampleSlice::I8(items),
DecodingSampleSlice::U16(items) => DecodingSampleSlice::U16(items),
DecodingSampleSlice::I16(items) => DecodingSampleSlice::I16(items),
DecodingSampleSlice::U32(items) => DecodingSampleSlice::U32(items),
DecodingSampleSlice::I32(items) => DecodingSampleSlice::I32(items),
DecodingSampleSlice::U64(items) => DecodingSampleSlice::U64(items),
DecodingSampleSlice::I64(items) => DecodingSampleSlice::I64(items),
DecodingSampleSlice::F16(items) => DecodingSampleSlice::F16(items),
DecodingSampleSlice::F32(items) => DecodingSampleSlice::F32(items),
DecodingSampleSlice::F64(items) => DecodingSampleSlice::F64(items),
}
}
pub fn sample_type(&self) -> DecodingSampleType {
match *self {
DecodingSampleSlice::U8(_) => DecodingSampleType::U8,
DecodingSampleSlice::U16(_) => DecodingSampleType::U16,
DecodingSampleSlice::U32(_) => DecodingSampleType::U32,
DecodingSampleSlice::U64(_) => DecodingSampleType::U64,
DecodingSampleSlice::F16(_) => DecodingSampleType::F16,
DecodingSampleSlice::F32(_) => DecodingSampleType::F32,
DecodingSampleSlice::F64(_) => DecodingSampleType::F64,
DecodingSampleSlice::I8(_) => DecodingSampleType::I8,
DecodingSampleSlice::I16(_) => DecodingSampleType::I16,
DecodingSampleSlice::I32(_) => DecodingSampleType::I32,
DecodingSampleSlice::I64(_) => DecodingSampleType::I64,
}
}
/// Change the endianess of referenced data.
///
/// This utility may be used to prepare a buffer for consumption by methods taking raw bytes or
/// after filling the buffer with raw bytes.
///
/// After this, interpreting the buffer on the host may no longer produce any reasonable
/// results. For instance, floats in the wrong byte order may produce a `NaN` with varying
/// payloads. Despite unexpected result however, this operation is always sound.
pub fn convert_endian(&mut self, from: ByteOrder, to: ByteOrder) {
let byte_unit = self.sample_type().endian_bytes();
from.convert_endian_bytes(byte_unit, self.as_bytes_mut(), to);
}
pub fn byte_len(&self) -> usize {
self.as_bytes().len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DecodingSampleType {
U8,
U16,
U32,
U64,
F16,
F32,
F64,
I8,
I16,
I32,
I64,
}
impl DecodingSampleType {
pub fn sample_format(self) -> SampleFormat {
match self {
DecodingSampleType::U8 => SampleFormat::Uint,
DecodingSampleType::U16 => SampleFormat::Uint,
DecodingSampleType::U32 => SampleFormat::Uint,
DecodingSampleType::U64 => SampleFormat::Uint,
DecodingSampleType::F16 => SampleFormat::IEEEFP,
DecodingSampleType::F32 => SampleFormat::IEEEFP,
DecodingSampleType::F64 => SampleFormat::IEEEFP,
DecodingSampleType::I8 => SampleFormat::Int,
DecodingSampleType::I16 => SampleFormat::Int,
DecodingSampleType::I32 => SampleFormat::Int,
DecodingSampleType::I64 => SampleFormat::Int,
}
}
pub(crate) fn extent_for_bytes(self, bytes: usize) -> DecodingExtent {
match self {
DecodingSampleType::U8 => DecodingExtent::U8(bytes),
DecodingSampleType::U16 => DecodingExtent::U16(bytes.div_ceil(2)),
DecodingSampleType::U32 => DecodingExtent::U32(bytes.div_ceil(4)),
DecodingSampleType::U64 => DecodingExtent::U64(bytes.div_ceil(8)),
DecodingSampleType::I8 => DecodingExtent::I8(bytes),
DecodingSampleType::I16 => DecodingExtent::I16(bytes.div_ceil(2)),
DecodingSampleType::I32 => DecodingExtent::I32(bytes.div_ceil(4)),
DecodingSampleType::I64 => DecodingExtent::I64(bytes.div_ceil(8)),
DecodingSampleType::F16 => DecodingExtent::F16(bytes.div_ceil(2)),
DecodingSampleType::F32 => DecodingExtent::F32(bytes.div_ceil(4)),
DecodingSampleType::F64 => DecodingExtent::F64(bytes.div_ceil(8)),
}
}
fn endian_bytes(self) -> EndianBytes {
use EndianBytes::*;
match self {
DecodingSampleType::U8 => One,
DecodingSampleType::U16 => Two,
DecodingSampleType::U32 => Four,
DecodingSampleType::U64 => Eight,
DecodingSampleType::F16 => Two,
DecodingSampleType::F32 => Four,
DecodingSampleType::F64 => Eight,
DecodingSampleType::I8 => One,
DecodingSampleType::I16 => Two,
DecodingSampleType::I32 => Four,
DecodingSampleType::I64 => Eight,
}
}
}
/// The count and matching discriminant for a `DecodingBuffer`.
#[derive(Clone)]
pub(crate) enum DecodingExtent {
U8(usize),
U16(usize),
U32(usize),
U64(usize),
F16(usize),
F32(usize),
F64(usize),
I8(usize),
I16(usize),
I32(usize),
I64(usize),
}
impl DecodingExtent {
pub(crate) fn to_result_buffer(&self, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
match *self {
DecodingExtent::U8(count) => DecodingSampleBuffer::new_u8(count, limits),
DecodingExtent::U16(count) => DecodingSampleBuffer::new_u16(count, limits),
DecodingExtent::U32(count) => DecodingSampleBuffer::new_u32(count, limits),
DecodingExtent::U64(count) => DecodingSampleBuffer::new_u64(count, limits),
DecodingExtent::F16(count) => DecodingSampleBuffer::new_f16(count, limits),
DecodingExtent::F32(count) => DecodingSampleBuffer::new_f32(count, limits),
DecodingExtent::F64(count) => DecodingSampleBuffer::new_f64(count, limits),
DecodingExtent::I8(count) => DecodingSampleBuffer::new_i8(count, limits),
DecodingExtent::I16(count) => DecodingSampleBuffer::new_i16(count, limits),
DecodingExtent::I32(count) => DecodingSampleBuffer::new_i32(count, limits),
DecodingExtent::I64(count) => DecodingSampleBuffer::new_i64(count, limits),
}
}
pub(crate) fn preferred_layout(self) -> TiffResult<Layout> {
fn overflow(_: LayoutError) -> TiffError {
TiffError::LimitsExceeded
}
match self {
DecodingExtent::U8(count) => Layout::array::<u8>(count),
DecodingExtent::U16(count) => Layout::array::<u16>(count),
DecodingExtent::U32(count) => Layout::array::<u32>(count),
DecodingExtent::U64(count) => Layout::array::<u64>(count),
DecodingExtent::F16(count) => Layout::array::<f16>(count),
DecodingExtent::F32(count) => Layout::array::<f32>(count),
DecodingExtent::F64(count) => Layout::array::<f64>(count),
DecodingExtent::I8(count) => Layout::array::<i8>(count),
DecodingExtent::I16(count) => Layout::array::<i16>(count),
DecodingExtent::I32(count) => Layout::array::<i32>(count),
DecodingExtent::I64(count) => Layout::array::<i64>(count),
}
.map_err(overflow)
}
pub(crate) fn sample_type(&self) -> DecodingSampleType {
match *self {
DecodingExtent::U8(_) => DecodingSampleType::U8,
DecodingExtent::U16(_) => DecodingSampleType::U16,
DecodingExtent::U32(_) => DecodingSampleType::U32,
DecodingExtent::U64(_) => DecodingSampleType::U64,
DecodingExtent::F16(_) => DecodingSampleType::F16,
DecodingExtent::F32(_) => DecodingSampleType::F32,
DecodingExtent::F64(_) => DecodingSampleType::F64,
DecodingExtent::I8(_) => DecodingSampleType::I8,
DecodingExtent::I16(_) => DecodingSampleType::I16,
DecodingExtent::I32(_) => DecodingSampleType::I32,
DecodingExtent::I64(_) => DecodingSampleType::I64,
}
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ use std::collections::HashMap;
/// ///
/// Since we only consider one child, we run union find to assign each pointer to a known chain. /// Since we only consider one child, we run union find to assign each pointer to a known chain.
/// Then, when we insert a new edge we check if they belong to the same component. /// Then, when we insert a new edge we check if they belong to the same component.
#[derive(Default, Debug)] #[derive(Clone, Default, Debug)]
pub struct IfdCycles { pub struct IfdCycles {
/// The root of each component union. /// The root of each component union.
component_union: HashMap<ComponentId, ComponentId>, component_union: HashMap<ComponentId, ComponentId>,
+362
View File
@@ -0,0 +1,362 @@
use std::io::{self, Cursor, Read, Seek};
use crate::{
decoder::{ifd::Entry, stream::EndianReader, ValueReader},
tags::{ByteOrder, IfdPointer, Type},
TiffResult,
};
pub struct EntryBytesReader<'lt, R> {
inner: &'lt mut EndianReader<R>,
variant: Variant,
}
const OFFSET_BUFFER_LEN: usize = 8;
enum Variant {
Immediate {
/// An in-value reader if the offset field contains the data. Note that we rotate it to the
/// back of this buffer, not the front, so that `Cursor` already contains the right end.
/// We do not let you seek back beyond our start.
data: Cursor<[u8; OFFSET_BUFFER_LEN]>,
value_bytes: u8,
},
InFile {
// the length relative to the base so we can seek relative without querying the position.
value_bytes: u64,
remainder: u64,
base: u64,
},
}
impl<'lt, R> EntryBytesReader<'lt, R> {
pub(super) fn from_entry(inner: &'lt mut ValueReader<R>, entry: Entry) -> TiffResult<Self>
where
R: Read + Seek,
{
let offset: [_; OFFSET_BUFFER_LEN] = *entry.offset_raw();
let limit = if inner.bigtiff { 8 } else { 4 };
assert!(limit as usize <= OFFSET_BUFFER_LEN);
let value_bytes = entry.field_type().value_bytes(entry.count())?;
if value_bytes <= limit {
// We move the data to the back of the array whereas it is in the front right now.
let mut value = offset;
value.rotate_left(value_bytes as usize);
let mut cursor = Cursor::new(value);
cursor.set_position(OFFSET_BUFFER_LEN as u64 - value_bytes);
Ok(EntryBytesReader {
inner: &mut inner.reader,
variant: Variant::Immediate {
data: cursor,
value_bytes: value_bytes as u8,
},
})
} else {
// remainder > limit >= 0 so this is an unreachable panic.
let pointer = if inner.bigtiff {
let mut offset = offset;
inner
.reader
.byte_order
.convert(Type::IFD8, &mut offset[..], ByteOrder::native());
IfdPointer(u64::from_ne_bytes(offset))
} else {
let mut offset = *offset.first_chunk::<4>().unwrap();
inner
.reader
.byte_order
.convert(Type::IFD, &mut offset[..], ByteOrder::native());
let offset = u32::from_ne_bytes(offset);
IfdPointer(u64::from(offset))
};
inner.reader.goto_offset(pointer.0)?;
Ok(EntryBytesReader {
inner: &mut inner.reader,
variant: Variant::InFile {
value_bytes,
remainder: value_bytes,
base: pointer.0,
},
})
}
}
}
impl<R: Read> Read for EntryBytesReader<'_, R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match &mut self.variant {
Variant::Immediate { data, .. } => data.read(buf),
Variant::InFile { remainder, .. } => {
let bound = usize::try_from(*remainder).unwrap_or(usize::MAX);
let avail = buf.len().min(bound);
let read = self.inner.inner().read(&mut buf[..avail])?;
// On a well-behaved `Read`: read <= avail <= bound <=(mathematical) remainder
let consume = read as u64;
*remainder -= consume;
Ok(read)
}
}
}
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
match &mut self.variant {
Variant::Immediate { data, .. } => data.read_exact(buf),
Variant::InFile { remainder, .. } => {
let bound = usize::try_from(*remainder).unwrap_or(usize::MAX);
// Fail early if we would be sure to over-read.
if buf.len() > bound {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"Tiff value shorter than the requested buffer size",
));
}
// since buf.len() <= bound <=(mathematical) remainder
let consume = buf.len() as u64;
self.inner.inner().read_exact(buf)?;
*remainder -= consume;
Ok(())
}
}
}
}
impl<R: Read + Seek> Seek for EntryBytesReader<'_, R> {
fn seek(&mut self, offset: io::SeekFrom) -> io::Result<u64> {
let (value_bytes, remainder) = match &self.variant {
Variant::Immediate { data, value_bytes } => {
let remainder = data.get_ref().len() as u64 - data.position();
(*value_bytes as u64, remainder)
}
&Variant::InFile {
value_bytes,
remainder,
base: _,
} => (value_bytes, remainder),
};
// Normalize to an offset from the start
let offset_from_start = match offset {
io::SeekFrom::Start(n) => Some(n),
io::SeekFrom::Current(n) => {
let front = value_bytes - remainder;
front.checked_add_signed(n)
}
io::SeekFrom::End(n) => value_bytes.checked_add_signed(n),
};
let Some((offset, remainder)) = offset_from_start.and_then(|n| {
let remainder = value_bytes.checked_sub(n)?;
Some((n, remainder))
}) else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"seek out of bounds of entry value",
));
};
match &mut self.variant {
Variant::Immediate { data, .. } => {
let start = OFFSET_BUFFER_LEN as u64 - value_bytes;
data.set_position(start + offset);
Ok(offset)
}
&mut Variant::InFile { base, .. } => {
// We checked this against overflow on construction. Ignore the offset we get.
self.inner
.inner()
.seek(io::SeekFrom::Start(base + offset))?;
self.variant = Variant::InFile {
value_bytes,
remainder,
base,
};
Ok(offset)
}
}
}
fn stream_position(&mut self) -> io::Result<u64> {
Ok(match &self.variant {
Variant::Immediate { data, value_bytes } => {
// Recall the value is at the end of the buffer.
let from_end = OFFSET_BUFFER_LEN as u64 - data.position();
u64::from(*value_bytes) - from_end
}
// Note: we might think about using the inner `stream_position` here. However that
// exposes something interesting: If the internal stream is modified concurrently from
// another instance (e.g. a shared `File`) then we may lose track of it anyways.
// Especially the `Read` implementation is not bounded on `Seek` so it could not
// acquire the position. And it should not since that is just a different form of race
// where we risk the position being modified by a different thread just as well.
//
// So in the interest of least surprise we just assume that we can track the position
// accurately here and that the inner reader uses for instance `read_at` consistently.
// We own the value (or mutable reference) it after all.
//
// Also this one MUST be compatible with `seek(SeekFrom::Current(0))` as per trait
// defintion—so at least keep those consistent if changing anything.
&Variant::InFile {
remainder,
value_bytes,
..
} => value_bytes - remainder,
})
}
}
#[cfg(test)]
mod tests {
use crate::{decoder::Decoder, encoder, tags};
use std::io::{Read as _, Seek as _, SeekFrom};
#[test]
fn can_seek_tag_bytes() -> Result<(), crate::error::TiffError> {
let file = std::fs::File::open(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/images/int8_rgb.tif"
))
.unwrap();
let mut decoder = Decoder::open(file).unwrap();
decoder.next_directory().unwrap();
let mut buffer = crate::tags::ValueBuffer::default();
decoder
.current_ifd()
.find_tag_buf(tags::Tag::StripByteCounts, &mut buffer)
.expect("some strip byte count is present");
let bytes_of_value = buffer.as_bytes().len() as u64;
let mut reader = decoder
.read_tag(tags::Tag::StripByteCounts)?
.expect("some strip byte count is present");
let mut data = vec![];
reader.read_to_end(&mut data)?;
// `ValueBuffer` also stores its data in the file endianess until converted.
assert_eq!(reader.stream_position()?, bytes_of_value);
assert_eq!(buffer.as_bytes(), data);
reader.seek(SeekFrom::Start(0))?;
assert_eq!(reader.stream_position()?, 0);
reader.read_to_end(&mut data)?;
assert_eq!(reader.stream_position()?, bytes_of_value);
let (first, second) = data.split_at(bytes_of_value as usize);
assert_eq!(first, second);
Ok(())
}
#[test]
#[allow(clippy::seek_from_current)] // We want to test that functionality.
fn seek_from_end() -> Result<(), crate::error::TiffError> {
const ARTIST: &str = "99 little bugs in the code";
let data = {
let mut data = std::io::Cursor::new(vec![]);
let mut encoder = encoder::TiffEncoder::new(&mut data)?;
let mut dir = encoder.image_directory()?;
dir.write_tag(tags::Tag::Artist, ARTIST)?;
dir.finish()?;
data.into_inner()
};
let file = std::io::Cursor::new(data);
let mut decoder = Decoder::open(file).unwrap();
decoder.next_directory().unwrap();
let mut reader = decoder
.read_tag(tags::Tag::Artist)?
.expect("the artist is present?");
let mut value = vec![];
reader.read_to_end(&mut value)?;
let (&nul, value) = value.split_last().unwrap();
assert_eq!(value, ARTIST.as_bytes());
assert_eq!(nul, b'\0');
assert_eq!(reader.seek(SeekFrom::End(0))?, ARTIST.len() as u64 + 1);
assert!(reader.seek(SeekFrom::End(1)).is_err());
assert_eq!(reader.stream_position()?, ARTIST.len() as u64 + 1);
// Check relative seeks by comparing against the "code"
let code_offset = reader.seek(SeekFrom::End(-5))?;
let mut is_code = [0u8; 4];
reader.read_exact(&mut is_code)?;
assert_eq!(is_code, *value.last_chunk::<4>().unwrap());
assert_eq!(reader.seek(SeekFrom::Start(0))?, 0);
assert_eq!(reader.stream_position()?, 0);
assert_eq!(reader.seek(SeekFrom::Current(0))?, 0);
assert_eq!(reader.stream_position()?, 0);
assert!(reader.seek(SeekFrom::Current(-1)).is_err());
assert_eq!(reader.stream_position()?, 0);
reader.seek(SeekFrom::Start(code_offset))?;
reader.read_exact(&mut is_code)?;
assert_eq!(reader.stream_position()?, ARTIST.len() as u64);
assert_eq!(is_code, *value.last_chunk::<4>().unwrap());
Ok(())
}
/// Check that the reader from an `offset` field also works.
#[test]
fn read_from_offset() -> Result<(), crate::error::TiffError> {
let data = {
let mut data = std::io::Cursor::new(vec![]);
let mut encoder = encoder::TiffEncoder::new(&mut data)?;
let mut dir = encoder.image_directory()?;
dir.write_tag(
tags::Tag::PlanarConfiguration,
tags::PlanarConfiguration::Planar,
)?;
dir.finish()?;
data.into_inner()
};
let file = std::io::Cursor::new(data);
let mut decoder = Decoder::open(file).unwrap();
decoder.next_directory().unwrap();
let bo = decoder.byte_order();
let mut reader = decoder
.read_tag(tags::Tag::PlanarConfiguration)?
.expect("some artist is present");
let mut buf = [0; 2];
reader.read_exact(&mut buf)?;
bo.convert(tags::Type::SHORT, &mut buf, tags::ByteOrder::BigEndian);
assert_eq!(buf, [0, 2]);
// Check relative seeks, this item is only two bytes long.
assert!(reader.seek(SeekFrom::End(-3)).is_err());
Ok(())
}
}
+71 -41
View File
@@ -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
)) ))
} }
} }
@@ -393,7 +393,7 @@ impl Entry {
self.count self.count
} }
pub(crate) fn offset(&self) -> &[u8] { pub(crate) fn offset_raw(&self) -> &[u8; 8] {
&self.offset &self.offset
} }
@@ -583,81 +583,93 @@ impl Entry {
match self.type_ { match self.type_ {
Type::BYTE | Type::UNDEFINED => { Type::BYTE | Type::UNDEFINED => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(bytes.iter().copied().map(Byte)) v.extend(bytes.iter().copied().map(Byte))
}) })
} }
Type::SBYTE => { Type::SBYTE => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(bytes.iter().copied().map(|v| SignedByte(v as i8))) v.extend(bytes.iter().copied().map(|v| SignedByte(v as i8)))
}) })
} }
Type::SHORT => { Type::SHORT => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.chunks_exact(2) .as_chunks::<2>()
.map(|ch| Short(u16::from_ne_bytes(ch.try_into().unwrap()))), .0
.iter()
.map(|&ch| Short(u16::from_ne_bytes(ch))),
) )
}) })
} }
Type::SSHORT => { Type::SSHORT => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.chunks_exact(2) .as_chunks::<2>()
.map(|ch| SignedShort(i16::from_ne_bytes(ch.try_into().unwrap()))), .0
.iter()
.map(|&ch| SignedShort(i16::from_ne_bytes(ch))),
) )
}) })
} }
Type::LONG => { Type::LONG => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.chunks_exact(4) .as_chunks::<4>()
.map(|ch| Unsigned(u32::from_ne_bytes(ch.try_into().unwrap()))), .0
.iter()
.map(|&ch| Unsigned(u32::from_ne_bytes(ch))),
) )
}) })
} }
Type::SLONG => { Type::SLONG => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.chunks_exact(4) .as_chunks::<4>()
.map(|ch| Signed(i32::from_ne_bytes(ch.try_into().unwrap()))), .0
.iter()
.map(|&ch| Signed(i32::from_ne_bytes(ch))),
) )
}) })
} }
Type::FLOAT => { Type::FLOAT => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.chunks_exact(4) .as_chunks::<4>()
.map(|ch| Float(f32::from_ne_bytes(ch.try_into().unwrap()))), .0
.iter()
.map(|&ch| Float(f32::from_ne_bytes(ch))),
) )
}) })
} }
Type::DOUBLE => { Type::DOUBLE => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.chunks_exact(8) .as_chunks::<8>()
.map(|ch| Double(f64::from_ne_bytes(ch.try_into().unwrap()))), .0
.iter()
.map(|&ch| Double(f64::from_ne_bytes(ch))),
) )
}) })
} }
Type::RATIONAL => { Type::RATIONAL => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(bytes.chunks_exact(8).map(|ch| { v.extend(bytes.as_chunks::<8>().0.iter().map(|ch| {
Rational( Rational(
u32::from_ne_bytes(ch[..4].try_into().unwrap()), u32::from_ne_bytes(ch[..4].try_into().unwrap()),
u32::from_ne_bytes(ch[4..].try_into().unwrap()), u32::from_ne_bytes(ch[4..].try_into().unwrap()),
@@ -666,9 +678,9 @@ impl Entry {
}) })
} }
Type::SRATIONAL => { Type::SRATIONAL => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(bytes.chunks_exact(8).map(|ch| { v.extend(bytes.as_chunks::<8>().0.iter().map(|ch| {
SRational( SRational(
i32::from_ne_bytes(ch[..4].try_into().unwrap()), i32::from_ne_bytes(ch[..4].try_into().unwrap()),
i32::from_ne_bytes(ch[4..].try_into().unwrap()), i32::from_ne_bytes(ch[4..].try_into().unwrap()),
@@ -677,50 +689,58 @@ impl Entry {
}) })
} }
Type::LONG8 => { Type::LONG8 => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.chunks_exact(8) .as_chunks::<8>()
.map(|ch| UnsignedBig(u64::from_ne_bytes(ch.try_into().unwrap()))), .0
.iter()
.map(|&ch| UnsignedBig(u64::from_ne_bytes(ch))),
) )
}) })
} }
Type::SLONG8 => { Type::SLONG8 => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.chunks_exact(8) .as_chunks::<8>()
.map(|ch| SignedBig(i64::from_ne_bytes(ch.try_into().unwrap()))), .0
.iter()
.map(|&ch| SignedBig(i64::from_ne_bytes(ch))),
) )
}) })
} }
Type::IFD => { Type::IFD => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.chunks_exact(4) .as_chunks::<4>()
.map(|ch| Ifd(u32::from_ne_bytes(ch.try_into().unwrap()))), .0
.iter()
.map(|&ch| Ifd(u32::from_ne_bytes(ch))),
) )
}) })
} }
Type::IFD8 => { Type::IFD8 => {
v = Self::vec_with_capacity(self.count, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.chunks_exact(8) .as_chunks::<8>()
.map(|ch| IfdBig(u64::from_ne_bytes(ch.try_into().unwrap()))), .0
.iter()
.map(|&ch| IfdBig(u64::from_ne_bytes(ch))),
) )
}) })
} }
Type::ASCII => { Type::ASCII => {
let n = usize::try_from(self.count)?; let n = usize::try_from(self.count)?;
if n > limits.decoding_buffer_size { if n > limits.ifd_value_size {
return Err(dbg!(TiffError::LimitsExceeded)); return Err(TiffError::LimitsExceeded);
} }
let mut out = vec![0; n]; let mut out = vec![0; n];
@@ -846,10 +866,20 @@ impl Entry {
fn vec_with_capacity( fn vec_with_capacity(
value_count: u64, value_count: u64,
type_: Type,
limits: &super::Limits, limits: &super::Limits,
) -> Result<Vec<Value>, TiffError> { ) -> Result<Vec<Value>, TiffError> {
let value_count = usize::try_from(value_count)?; // Check raw file bytes against ifd_value_size.
if !type_
.value_bytes(value_count)
.and_then(|b| usize::try_from(b).map_err(|_| TiffError::LimitsExceeded))
.is_ok_and(|raw_len| raw_len <= limits.ifd_value_size)
{
return Err(TiffError::LimitsExceeded);
}
// Check in-memory Value representation against decoding_buffer_size.
let value_count = usize::try_from(value_count)?;
if value_count > limits.decoding_buffer_size / mem::size_of::<Value>() { if value_count > limits.decoding_buffer_size / mem::size_of::<Value>() {
return Err(TiffError::LimitsExceeded); return Err(TiffError::LimitsExceeded);
} }
+432 -70
View File
@@ -36,6 +36,9 @@ impl TileAttributes {
pub fn tiles_down(&self) -> usize { pub fn tiles_down(&self) -> usize {
self.image_height.div_ceil(self.tile_length) self.image_height.div_ceil(self.tile_length)
} }
pub fn tiles_per_plane(&self) -> usize {
self.tiles_across() * self.tiles_down()
}
fn padding_right(&self) -> usize { fn padding_right(&self) -> usize {
(self.tile_width - self.image_width % self.tile_width) % self.tile_width (self.tile_width - self.image_width % self.tile_width) % self.tile_width
} }
@@ -64,7 +67,6 @@ impl TileAttributes {
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Image { pub(crate) struct Image {
pub ifd: Option<Directory>,
pub width: u32, pub width: u32,
pub height: u32, pub height: u32,
pub bits_per_sample: u8, pub bits_per_sample: u8,
@@ -86,6 +88,11 @@ pub(crate) struct Image {
pub chunk_offsets: Vec<u64>, pub chunk_offsets: Vec<u64>,
pub chunk_bytes: Vec<u64>, pub chunk_bytes: Vec<u64>,
pub chroma_subsampling: (u16, u16), pub chroma_subsampling: (u16, u16),
/// FillOrder: 1 = MSB-to-LSB (default), 2 = LSB-to-MSB (common in fax images)
pub fill_order: u16,
/// The color map (palette) for `PhotometricInterpretation::RGBPalette`.
/// Contains `3 * 2^BitsPerSample` u16 entries: R values, then G values, then B values.
pub color_map: Option<Vec<u16>>,
/// Will decompression yield host-endian samples? /// Will decompression yield host-endian samples?
/// ///
/// Standard compression algorithms run on pure bytes, taking the sample slice as it would be /// Standard compression algorithms run on pure bytes, taking the sample slice as it would be
@@ -93,6 +100,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.
@@ -133,6 +141,10 @@ pub(crate) struct ReadoutLayout {
/// Bits per sample in the encoded data. /// Bits per sample in the encoded data.
pub tiff_bits_per_sample: u8, pub tiff_bits_per_sample: u8,
/// Bits per sample in the output buffer, promoted to a standard width.
/// For sub-byte depths (1-8): same as tiff_bits_per_sample.
/// For 9-16: 16. For 17-32: 32. For 33-64: 64.
pub output_bits_per_sample: u8,
/// Number of samples in the encoded data. /// Number of samples in the encoded data.
pub tiff_samples: u16, pub tiff_samples: u16,
/// Dimensions of the underlying rectangular chunks (tile or strips). /// Dimensions of the underlying rectangular chunks (tile or strips).
@@ -149,9 +161,10 @@ pub(crate) struct ReadoutLayout {
impl Image { 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: &ifd }; let mut tag_reader = TagReader { decoder, ifd };
let width = tag_reader.require_tag(Tag::ImageWidth)?.into_u32()?; let width = tag_reader.require_tag(Tag::ImageWidth)?.into_u32()?;
let height = tag_reader.require_tag(Tag::ImageLength)?.into_u32()?; let height = tag_reader.require_tag(Tag::ImageLength)?.into_u32()?;
@@ -166,7 +179,7 @@ impl Image {
.map(Value::into_u16) .map(Value::into_u16)
.transpose()? .transpose()?
.and_then(PhotometricInterpretation::from_u16) .and_then(PhotometricInterpretation::from_u16)
.ok_or(TiffUnsupportedError::UnknownInterpretation)?; .unwrap_or(PhotometricInterpretation::BlackIsZero);
// Try to parse both the compression method and the number, format, and bits of the included samples. // Try to parse both the compression method and the number, format, and bits of the included samples.
// If they are not explicitly specified, those tags are reset to their default values and not carried from previous images. // If they are not explicitly specified, those tags are reset to their default values and not carried from previous images.
@@ -302,23 +315,33 @@ impl Image {
(2, 2) (2, 2)
}; };
let fill_order = tag_reader
.find_tag(Tag::FillOrder)?
.map(Value::into_u16)
.transpose()?
.unwrap_or(1);
let planes = match planar_config { let planes = match planar_config {
PlanarConfiguration::Chunky => 1, PlanarConfiguration::Chunky => 1,
PlanarConfiguration::Planar => samples, PlanarConfiguration::Planar => samples,
}; };
// Note: Not strictly according to specification. The specification says that tiled images
// must have their data encoded in `TileByteCounts` and `TileOffsets`. But `libtiff`
// processes tags one-by-one and with *either* of these it will set the flag for the image
// being tiled. Also it will just use whatever offsets are available. (I believe also using
// the first of the two tag pairs that's available—remember tags are encoded ascending).
let is_tiled = ifd.contains(Tag::TileWidth) || ifd.contains(Tag::TileLength);
let has_strips = ifd.contains(Tag::StripByteCounts) && ifd.contains(Tag::StripOffsets);
let has_tiles = ifd.contains(Tag::TileByteCounts) && ifd.contains(Tag::TileOffsets);
let chunk_type; let chunk_type;
let chunk_offsets; let chunk_offsets;
let chunk_bytes; let chunk_bytes;
let strip_decoder; let strip_decoder;
let tile_attributes; let tile_attributes;
match (
ifd.contains(Tag::StripByteCounts), if !is_tiled && has_strips {
ifd.contains(Tag::StripOffsets),
ifd.contains(Tag::TileByteCounts),
ifd.contains(Tag::TileOffsets),
) {
(true, true, false, false) => {
chunk_type = ChunkType::Strip; chunk_type = ChunkType::Strip;
chunk_offsets = tag_reader chunk_offsets = tag_reader
@@ -359,15 +382,20 @@ impl Image {
}, },
)); ));
} }
} } else if is_tiled && (has_strips || has_tiles) {
(false, false, true, true) => {
chunk_type = ChunkType::Tile; chunk_type = ChunkType::Tile;
let tile_width = let tile_width = usize::try_from(tag_reader.require_tag(Tag::TileWidth)?.into_u32()?)?;
usize::try_from(tag_reader.require_tag(Tag::TileWidth)?.into_u32()?)?;
let tile_length = let tile_length =
usize::try_from(tag_reader.require_tag(Tag::TileLength)?.into_u32()?)?; usize::try_from(tag_reader.require_tag(Tag::TileLength)?.into_u32()?)?;
// Prioritize tiles over strips, tiles are what we expect.
let (tag_offsets, tag_counts) = if has_tiles {
(Tag::TileOffsets, Tag::TileByteCounts)
} else {
(Tag::StripOffsets, Tag::StripByteCounts)
};
if tile_width == 0 { if tile_width == 0 {
return Err(TiffFormatError::InvalidTagValueType(Tag::TileWidth).into()); return Err(TiffFormatError::InvalidTagValueType(Tag::TileWidth).into());
} else if tile_length == 0 { } else if tile_length == 0 {
@@ -381,14 +409,9 @@ impl Image {
tile_width, tile_width,
tile_length, tile_length,
}); });
chunk_offsets = tag_reader
.find_tag(Tag::TileOffsets)? chunk_offsets = tag_reader.find_tag(tag_offsets)?.unwrap().into_u64_vec()?;
.unwrap() chunk_bytes = tag_reader.find_tag(tag_counts)?.unwrap().into_u64_vec()?;
.into_u64_vec()?;
chunk_bytes = tag_reader
.find_tag(Tag::TileByteCounts)?
.unwrap()
.into_u64_vec()?;
let tile = tile_attributes.as_ref().unwrap(); let tile = tile_attributes.as_ref().unwrap();
@@ -398,8 +421,7 @@ impl Image {
)); ));
} }
if chunk_offsets.len() != tile.tiles_down() * tile.tiles_across() * planes as usize if chunk_offsets.len() != tile.tiles_down() * tile.tiles_across() * planes as usize {
{
return Err(TiffError::FormatError( return Err(TiffError::FormatError(
TiffFormatError::IncorrectChunkCount { TiffFormatError::IncorrectChunkCount {
expected: (tile.tiles_down() * tile.tiles_across() * planes as usize) expected: (tile.tiles_down() * tile.tiles_across() * planes as usize)
@@ -408,12 +430,10 @@ impl Image {
}, },
)); ));
} }
} } else {
(_, _, _, _) => {
return Err(TiffError::FormatError( return Err(TiffError::FormatError(
TiffFormatError::StripTileTagConflict, TiffFormatError::StripTileTagConflict,
)) ));
}
}; };
let mut sample_format = sample_format; let mut sample_format = sample_format;
@@ -434,8 +454,33 @@ impl Image {
bits_per_sample = 32; bits_per_sample = 32;
} }
// Read ColorMap for palette images. The tag contains 3 * 2^BitsPerSample u16 entries.
let color_map = if photometric_interpretation == PhotometricInterpretation::RGBPalette {
match tag_reader.find_tag(Tag::ColorMap)? {
Some(v) => {
let map = v.into_u16_vec()?;
let expected_len = 1usize
.checked_shl(bits_per_sample as u32)
.and_then(|n| n.checked_mul(3))
.ok_or(TiffError::LimitsExceeded)?;
if map.len() != expected_len {
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
));
}
Some(map)
}
// A missing palette is not an error. The pixel values of an indexed image are just
// indices into the palette, and the palette itself is only display information, so
// the image still decodes. Callers observe the absence as `None` from
// `Decoder::color_map`.
None => None,
}
} else {
None
};
Ok(Image { Ok(Image {
ifd: Some(ifd),
width, width,
height, height,
bits_per_sample, bits_per_sample,
@@ -454,18 +499,35 @@ impl Image {
chunk_offsets, chunk_offsets,
chunk_bytes, chunk_bytes,
chroma_subsampling, chroma_subsampling,
fill_order,
color_map,
decompression_to_host_endian: matches!( decompression_to_host_endian: matches!(
compression_method, compression_method,
CompressionMethod::SgiLog | CompressionMethod::SgiLog24 CompressionMethod::SgiLog | CompressionMethod::SgiLog24
), ),
lenient,
}) })
} }
pub(crate) fn check_chunk_type(&self, expected: ChunkType) -> TiffResult<()> {
if expected != self.chunk_type {
return Err(TiffError::UsageError(UsageError::InvalidChunkType(
expected,
self.chunk_type,
)));
}
Ok(())
}
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 {
@@ -538,20 +600,30 @@ impl Image {
), ),
)), )),
}, },
// Unsupported due to inherently heterogeneous sample types. This is represented as: // 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*
// struct TiffLab /* Interpretation 8* { // (photometric=9) uses unsigned samples with the neutral at the middle of
// pub L: u8, // SampleFormat::Uint // the range. `SampleFormat` applies uniformly to all samples, so it can't
// pub a: i8, // SampleFormat::Int // describe the L=unsigned + a/b=signed mix; instead we take a/b as signed
// pub b: i8, // SampleFormat::Int // 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
PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError( // 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( TiffUnsupportedError::InterpretationWithBits(
PhotometricInterpretation::CIELab, PhotometricInterpretation::CIELab,
vec![self.bits_per_sample; self.samples as usize], vec![self.bits_per_sample; self.samples as usize],
), ),
)), )),
},
PhotometricInterpretation::CIELogL => match self.samples { PhotometricInterpretation::CIELogL => match self.samples {
1 if matches!(self.bits_per_sample, 32) 1 if matches!(self.bits_per_sample, 32)
&& matches!(self.sample_format, SampleFormat::IEEEFP) => && matches!(self.sample_format, SampleFormat::IEEEFP) =>
@@ -606,14 +678,17 @@ impl Image {
vec![self.bits_per_sample; self.samples as usize], vec![self.bits_per_sample; self.samples as usize],
), ),
)), )),
PhotometricInterpretation::RGBPalette | PhotometricInterpretation::TransparencyMask => { PhotometricInterpretation::RGBPalette => Ok(ColorType::Palette(self.bits_per_sample)),
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( TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation, self.photometric_interpretation,
vec![self.bits_per_sample; self.samples as usize], vec![self.bits_per_sample; self.samples as usize],
), ),
)) )),
}
} }
} }
@@ -625,6 +700,7 @@ impl Image {
#[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>, #[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>,
dimensions: (u32, u32), dimensions: (u32, u32),
samples: u16, samples: u16,
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] fill_order: u16,
) -> TiffResult<Box<dyn Read + 'r>> { ) -> TiffResult<Box<dyn Read + 'r>> {
Ok(match compression_method { Ok(match compression_method {
CompressionMethod::None => Box::new(reader), CompressionMethod::None => Box::new(reader),
@@ -659,6 +735,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 => {
@@ -718,10 +796,18 @@ impl Image {
Box::new(Cursor::new(data)) Box::new(Cursor::new(data))
} }
#[cfg(feature = "fax")] #[cfg(feature = "fax")]
CompressionMethod::Fax3 => Box::new(super::stream::Group3Reader::new(
dimensions,
reader,
compressed_length,
fill_order,
)?),
#[cfg(feature = "fax")]
CompressionMethod::Fax4 => Box::new(super::stream::Group4Reader::new( CompressionMethod::Fax4 => Box::new(super::stream::Group4Reader::new(
dimensions, dimensions,
reader, reader,
compressed_length, compressed_length,
fill_order,
)?), )?),
#[cfg(feature = "webp")] #[cfg(feature = "webp")]
CompressionMethod::WebP => Box::new(super::stream::WebPReader::new( CompressionMethod::WebP => Box::new(super::stream::WebPReader::new(
@@ -820,24 +906,40 @@ impl Image {
let data_dimensions = (width, height); let data_dimensions = (width, height);
// Output bits per sample: sub-byte depths stay packed, supra-byte non-standard
// depths are promoted to the next standard width (matching sample_type() mapping).
let output_bits_per_sample: u8 = match tiff_bits_per_sample {
0..=8 => tiff_bits_per_sample,
9..=16 => 16,
17..=32 => 32,
_ => 64,
};
// Packed row size for reading from the file (uses tiff_bits_per_sample).
let tiff_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample)) let tiff_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample))
.checked_mul(u64::from(tiff_samples)) .checked_mul(u64::from(tiff_samples))
.ok_or(TiffError::LimitsExceeded)?; .ok_or(TiffError::LimitsExceeded)?;
let tiff_row_bytes: usize = tiff_row_bits.div_ceil(8).try_into()?; let tiff_row_bytes: usize = tiff_row_bits.div_ceil(8).try_into()?;
let chunk_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample)) // Output buffer sizes use output_bits_per_sample (the promoted width).
let chunk_row_bits = (u64::from(tiff_chunk_dimensions.0)
* u64::from(output_bits_per_sample))
.checked_mul(u64::from(data_samples)) .checked_mul(u64::from(data_samples))
.ok_or(TiffError::LimitsExceeded)?; .ok_or(TiffError::LimitsExceeded)?;
let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?; let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?;
let data_row_bits = (u64::from(data_dimensions.0) * u64::from(tiff_bits_per_sample)) let data_row_bits = (u64::from(data_dimensions.0) * u64::from(output_bits_per_sample))
.checked_mul(u64::from(data_samples)) .checked_mul(u64::from(data_samples))
.ok_or(TiffError::LimitsExceeded)?; .ok_or(TiffError::LimitsExceeded)?;
let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?; let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?;
// Clamp the chunk height to the image height: a chunk never contributes more
// rows than the image contains. Without this, RowsPerStrip = u32::MAX overflows
// usize on 32-bit platforms.
let clamped_chunk_height = tiff_chunk_dimensions.1.min(data_dimensions.1);
let chunk_col_stride: usize = data_row_bits let chunk_col_stride: usize = data_row_bits
.div_ceil(8) .div_ceil(8)
.checked_mul(u64::from(tiff_chunk_dimensions.1)) .checked_mul(u64::from(clamped_chunk_height))
.ok_or(TiffError::LimitsExceeded)? .ok_or(TiffError::LimitsExceeded)?
.try_into()?; .try_into()?;
@@ -886,6 +988,7 @@ impl Image {
chunk_col_stride, chunk_col_stride,
plane_stride, plane_stride,
tiff_bits_per_sample, tiff_bits_per_sample,
output_bits_per_sample,
tiff_samples, tiff_samples,
tiff_chunk_dimensions, tiff_chunk_dimensions,
tiff_row_bytes, tiff_row_bytes,
@@ -916,7 +1019,15 @@ impl Image {
} }
ChunkType::Tile => { ChunkType::Tile => {
let tile_attrs = self.tile_attributes.as_ref().unwrap(); let tile_attrs = self.tile_attributes.as_ref().unwrap();
let (padding_right, padding_down) = tile_attrs.get_padding(chunk_index as usize); // The chunk index counts the tiles of all planes, but the tile grid repeats per
// plane, so only the position within the plane decides the padding.
let tiles_per_plane = tile_attrs.tiles_per_plane();
let tile = if tiles_per_plane == 0 {
0
} else {
chunk_index as usize % tiles_per_plane
};
let (padding_right, padding_down) = tile_attrs.get_padding(tile);
let tile_width = tile_attrs.tile_width - padding_right; let tile_width = tile_attrs.tile_width - padding_right;
let tile_length = tile_attrs.tile_length - padding_down; let tile_length = tile_attrs.tile_length - padding_down;
@@ -932,6 +1043,7 @@ impl Image {
buf: &mut [u8], buf: &mut [u8],
layout: &ReadoutLayout, layout: &ReadoutLayout,
chunk_index: u32, chunk_index: u32,
scratch: &mut Vec<u8>,
) -> TiffResult<()> { ) -> TiffResult<()> {
let ValueReader { let ValueReader {
reader, reader,
@@ -956,11 +1068,20 @@ impl Image {
| ColorType::CMYKA(n) | ColorType::CMYKA(n)
| ColorType::YCbCr(n) | ColorType::YCbCr(n)
| ColorType::Gray(n) | ColorType::Gray(n)
| ColorType::Palette(n)
| ColorType::Lab(n)
| ColorType::Multiband { | ColorType::Multiband {
bit_depth: n, bit_depth: n,
num_samples: _, num_samples: _,
} if n == 8 || n == 16 || n == 32 || n == 64 => {} } if n == 8 || n == 16 || n == 32 || n == 64 => {}
ColorType::Gray(n) ColorType::RGB(n)
| ColorType::RGBA(n)
| ColorType::CMYK(n)
| ColorType::CMYKA(n)
| ColorType::YCbCr(n)
| ColorType::Gray(n)
| ColorType::Palette(n)
| ColorType::Lab(n)
| ColorType::Multiband { | ColorType::Multiband {
bit_depth: n, bit_depth: n,
num_samples: _, num_samples: _,
@@ -977,6 +1098,26 @@ impl Image {
)); ));
} }
}, },
// Extended bit depths (9-15, 17-31): packed N-bit samples unpacked to standard width.
// Horizontal predictor is supported (operates on unpacked standard-width samples).
// FloatingPoint predictor is not meaningful for non-standard integer depths.
ColorType::RGB(n)
| ColorType::RGBA(n)
| ColorType::CMYK(n)
| ColorType::CMYKA(n)
| ColorType::YCbCr(n)
| ColorType::Gray(n)
| ColorType::Multiband {
bit_depth: n,
num_samples: _,
} if n <= 32 => match self.predictor {
Predictor::None | Predictor::Horizontal => {}
Predictor::FloatingPoint => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::FloatingPointPredictor(color_type),
));
}
},
type_ => { type_ => {
return Err(TiffError::UnsupportedError( return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedColorType(type_), TiffUnsupportedError::UnsupportedColorType(type_),
@@ -1048,9 +1189,64 @@ impl Image {
self.jpeg_tables.as_deref().map(|a| &**a), self.jpeg_tables.as_deref().map(|a| &**a),
chunk_dims, chunk_dims,
self.samples, self.samples,
self.fill_order,
)?; )?;
if is_output_chunk_rows && is_all_bits { // Extended bit depths (9-15, 17-31): packed N-bit samples must be unpacked to
// standard-width output samples before endianness fix and predictor.
let needs_unpacking = layout.tiff_bits_per_sample != layout.output_bits_per_sample
&& layout.tiff_bits_per_sample > 8;
if needs_unpacking {
if !is_all_bits {
// Packed extended depths with extra samples to discard is not yet supported.
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
vec![self.bits_per_sample; self.samples as usize],
),
));
}
let tiff_bps = layout.tiff_bits_per_sample;
let out_bps = layout.output_bits_per_sample;
let samples_per_data_row = data_dims.0 as usize * samples as usize;
let mut packed = vec![0u8; chunk_row_bytes];
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
reader.read_exact(&mut packed)?;
let out_row = &mut row[..data_row_bytes];
unpack_bits(&packed, out_row, tiff_bps, samples_per_data_row, out_bps);
// Unpacked values are in native byte order, so pass native to skip
// endianness conversion while still applying the predictor.
super::fix_endianness_and_predict(
out_row,
color_type.bit_depth(),
data_samples,
ByteOrder::native(),
predictor,
scratch,
);
// An n-bit predictor delta is stored modulo 2^n, but the
// reconstruction above accumulated it in the promoted u16/u32
// storage width. Reduce back modulo the declared depth so both
// the inverted and non-inverted paths see exact, in-range
// values (2^n divides the storage modulus, so one mask after
// the row is exact).
super::mask_samples_to_bit_depth(out_row, tiff_bps, out_bps, self.sample_format);
super::post_process_row(
out_row,
photometric_interpretation,
color_type,
self.sample_format,
)?;
}
} else if is_output_chunk_rows && is_all_bits {
// Here we can read directly into the output buffer itself. // Here we can read directly into the output buffer itself.
let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize]; let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize];
reader.read_exact(tile)?; reader.read_exact(tile)?;
@@ -1062,12 +1258,26 @@ impl Image {
samples, samples,
decompressed_byte_order, decompressed_byte_order,
predictor, predictor,
scratch,
); );
} }
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero { // No-op in this branch: promoted layouts (declared depth < storage
super::invert_colors(tile, color_type, self.sample_format)?; // 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 { } 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 // The floating point predictor shuffles the padding bytes into the encoded output, so
// this case is handled specially when needed. // this case is handled specially when needed.
@@ -1077,14 +1287,28 @@ impl Image {
let row = &mut row[..data_row_bytes]; let row = &mut row[..data_row_bytes];
match color_type.bit_depth() { match color_type.bit_depth() {
16 => predict_f16(&mut encoded, row, samples), 16 => {
32 => predict_f32(&mut encoded, row, samples), let (out, _) = row.as_chunks_mut::<2>();
64 => predict_f64(&mut encoded, row, samples), predict_f16(&mut encoded, out, samples);
}
32 => {
let (out, _) = row.as_chunks_mut::<4>();
predict_f32(&mut encoded, out, samples);
}
64 => {
let (out, _) = row.as_chunks_mut::<8>();
predict_f64(&mut encoded, out, samples);
}
_ => unreachable!(), _ => unreachable!(),
} }
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero { // No masking here: the floating point predictor requires
super::invert_colors(row, color_type, self.sample_format)?; // IEEEFP samples, which are never depth-promoted.
} super::post_process_row(
row,
photometric_interpretation,
color_type,
self.sample_format,
)?;
} }
} else if is_all_bits { } else if is_all_bits {
// We read row-by-row but each row fits in its output buffer. // We read row-by-row but each row fits in its output buffer.
@@ -1108,11 +1332,25 @@ impl Image {
samples, samples,
decompressed_byte_order, decompressed_byte_order,
predictor, predictor,
scratch,
); );
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero { // No-op in this branch: promoted layouts only occur in the
super::invert_colors(row, color_type, self.sample_format)?; // 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 { } else {
// The encoded data potentially takes up more space than the output data so we must be // The encoded data potentially takes up more space than the output data so we must be
@@ -1139,6 +1377,10 @@ impl Image {
let photo_range = photometric_bit_end / 8..bits_per_pixel / 8; let photo_range = photometric_bit_end / 8..bits_per_pixel / 8;
let mut encoded = vec![0u8; chunk_row_bytes]; let mut encoded = vec![0u8; chunk_row_bytes];
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) { 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)?; reader.read_exact(&mut encoded)?;
Self::compact_photometric_bytes(&mut encoded, row, &photo_range); Self::compact_photometric_bytes(&mut encoded, row, &photo_range);
@@ -1149,11 +1391,25 @@ impl Image {
samples, samples,
decompressed_byte_order, decompressed_byte_order,
predictor, predictor,
scratch,
); );
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero { // No-op in this branch: promoted layouts only occur in the
super::invert_colors(row, color_type, self.sample_format)?; // 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,
)?;
} }
} }
@@ -1175,6 +1431,108 @@ impl Image {
} }
} }
/// Unpack N-bit packed samples (MSB-first) into standard-width output samples in native byte
/// order. Packed bits are read from `src` and expanded samples are written to `dst`.
///
/// `bits_per_sample` is the packed depth (e.g. 10, 12, 14).
/// `num_samples` is the number of samples to extract.
/// `output_width` is the output sample width in bits (16 or 32).
///
/// # Panics
///
/// Panics if `dst` is too small for `num_samples` output samples.
fn unpack_bits(
src: &[u8],
dst: &mut [u8],
bits_per_sample: u8,
num_samples: usize,
output_width: u8,
) {
// Dispatch to const-generic specializations so the compiler can unroll the
// byte-gathering loop and precompute shift/mask constants.
macro_rules! dispatch {
($($bps:literal),+) => {
match bits_per_sample {
$( $bps => unpack_bits_const::<$bps>(src, dst, num_samples, output_width), )+
_ => unpack_bits_const::<0>(src, dst, num_samples, output_width),
}
};
}
// Cover every bit depth from 9..=32 that could reach this path, plus 0 as fallback.
dispatch!(
9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
32
);
}
/// Inner loop with const BPS so the compiler can optimize the bit extraction.
/// BPS=0 is the fallback for unexpected bit depths (uses runtime value).
#[inline(always)]
fn unpack_bits_const<const BPS: u8>(
src: &[u8],
dst: &mut [u8],
num_samples: usize,
output_width: u8,
) {
debug_assert!(output_width == 16 || output_width == 32);
match output_width {
16 => {
debug_assert!(dst.len() >= num_samples * 2);
let mut bit_offset: usize = 0;
for i in 0..num_samples {
let val = if BPS > 0 {
extract_bits_msb_const::<BPS>(src, bit_offset)
} else {
extract_bits_msb_const::<16>(src, bit_offset) // unreachable fallback
};
dst[i * 2..i * 2 + 2].copy_from_slice(&(val as u16).to_ne_bytes());
bit_offset += if BPS > 0 { BPS as usize } else { 16 };
}
}
32 => {
debug_assert!(dst.len() >= num_samples * 4);
let mut bit_offset: usize = 0;
for i in 0..num_samples {
let val = if BPS > 0 {
extract_bits_msb_const::<BPS>(src, bit_offset)
} else {
extract_bits_msb_const::<32>(src, bit_offset) // unreachable fallback
};
dst[i * 4..i * 4 + 4].copy_from_slice(&val.to_ne_bytes());
bit_offset += if BPS > 0 { BPS as usize } else { 32 };
}
}
_ => unreachable!(),
}
}
/// Extract a value of `BPS` bits starting at `bit_offset` from an MSB-first packed byte stream.
/// Uses a single wide read + shift/mask for branchless extraction.
#[inline(always)]
fn extract_bits_msb_const<const BPS: u8>(src: &[u8], bit_offset: usize) -> u32 {
let byte_idx = bit_offset / 8;
let bit_idx = bit_offset % 8;
let mask: u64 = (1u64 << BPS) - 1;
// Read a u64 from as many bytes as available, big-endian, without going out of bounds.
// For BPS <= 32 (+ up to 7 bits offset = 39 bits max), we need at most 5 bytes,
// but reading 8 at once is faster and the branch is only taken near row ends.
let remaining = src.len() - byte_idx;
let acc = if remaining >= 8 {
u64::from_be_bytes(src[byte_idx..byte_idx + 8].try_into().unwrap())
} else {
// Near the end of the buffer — read available bytes into the high bits of a u64.
let mut buf = [0u8; 8];
buf[..remaining].copy_from_slice(&src[byte_idx..]);
u64::from_be_bytes(buf)
};
// The value starts at bit_idx from the MSB of our u64. Shift it to the LSB and mask.
let shift = 64 - bit_idx - BPS as usize;
((acc >> shift) & mask) as u32
}
impl ReadoutLayout { impl ReadoutLayout {
pub(crate) fn samples_per_out_texel(&self) -> u16 { pub(crate) fn samples_per_out_texel(&self) -> u16 {
match self.planar_config { match self.planar_config {
@@ -1187,7 +1545,7 @@ impl ReadoutLayout {
// pixel data. // pixel data.
pub(crate) fn chunk_row_bytes(&self, width: u32) -> TiffResult<usize> { pub(crate) fn chunk_row_bytes(&self, width: u32) -> TiffResult<usize> {
let data_samples = self.samples_per_out_texel(); let data_samples = self.samples_per_out_texel();
let data_row_bits = (u64::from(width) * u64::from(self.tiff_bits_per_sample)) let data_row_bits = (u64::from(width) * u64::from(self.output_bits_per_sample))
.checked_mul(u64::from(data_samples)) .checked_mul(u64::from(data_samples))
.ok_or(TiffError::LimitsExceeded)?; .ok_or(TiffError::LimitsExceeded)?;
Ok(data_row_bits.div_ceil(8).try_into()?) Ok(data_row_bits.div_ceil(8).try_into()?)
@@ -1205,8 +1563,12 @@ impl ReadoutLayout {
let data_row_bytes = u64::try_from(row_stride)?; let data_row_bytes = u64::try_from(row_stride)?;
// Use the original image height (from plane_stride / row_stride) to clamp
// the chunk height, matching the clamping in readout_for_size().
let image_height = self.plane_stride.checked_div(self.row_stride).unwrap_or(0) as u32;
let clamped_chunk_height = self.tiff_chunk_dimensions.1.min(image_height);
let chunk_col_stride = data_row_bytes let chunk_col_stride = data_row_bytes
.checked_mul(u64::from(self.tiff_chunk_dimensions.1)) .checked_mul(u64::from(clamped_chunk_height))
.ok_or(TiffError::LimitsExceeded)? .ok_or(TiffError::LimitsExceeded)?
.try_into()?; .try_into()?;
+9 -5
View File
@@ -87,15 +87,12 @@ fn packbits_decode<R: Read, const N: usize>(
reader: &mut R, reader: &mut R,
buf: &mut [[u8; N]], buf: &mut [[u8; N]],
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let expected = buf.len();
for offset in (0..N).rev() { for offset in (0..N).rev() {
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;
} }
@@ -124,6 +121,13 @@ fn packbits_decode<R: Read, const N: usize>(
i += usize::from(repeat); i += usize::from(repeat);
} }
} }
if i < expected {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"PackBits RLE decoded fewer pixels than expected",
));
}
} }
Ok(()) Ok(())
+1117 -586
View File
File diff suppressed because it is too large. Load diff
+183 -11
View File
@@ -141,14 +141,32 @@ pub struct LZWReader<R: Read> {
impl<R: Read> LZWReader<R> { impl<R: Read> LZWReader<R> {
/// Wraps a reader /// Wraps a reader
pub fn new(reader: R, compressed_length: usize) -> LZWReader<R> { pub fn new(reader: R, compressed_length: usize) -> LZWReader<R> {
let configuration = let mut buffered = BufReader::with_capacity(
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
.with_yield_on_full_buffer(true);
Self {
reader: BufReader::with_capacity(
(32 * 1024).min(compressed_length), (32 * 1024).min(compressed_length),
reader.take(u64::try_from(compressed_length).unwrap()), 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 configuration = if old_style {
weezl::decode::Configuration::new(weezl::BitOrder::Lsb, 8)
} else {
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
}
.with_yield_on_full_buffer(true);
Self {
reader: buffered,
decoder: configuration.build(), decoder: configuration.build(),
} }
} }
@@ -256,13 +274,28 @@ impl<R: Read> Read for PackBitsReader<R> {
} }
} }
/// Iterator adapter that reverses bits in each byte for FillOrder=2 (LSB-to-MSB).
#[cfg(feature = "fax")]
type FaxBytes<R> =
core::iter::Map<io::Bytes<io::BufReader<io::Take<R>>>, fn(io::Result<u8>) -> io::Result<u8>>;
#[cfg(feature = "fax")]
fn fax_byte_iter<R: Read>(reader: R, len: u64, fill_order: u16) -> FaxBytes<R> {
let map_fn: fn(io::Result<u8>) -> io::Result<u8> = if fill_order == 2 {
|r| r.map(u8::reverse_bits)
} else {
|r| r
};
io::BufReader::new(reader.take(len)).bytes().map(map_fn)
}
#[cfg(feature = "fax")] #[cfg(feature = "fax")]
pub struct Group4Reader<R: Read> { pub struct Group4Reader<R: Read> {
decoder: fax34::decoder::Group4Decoder<io::Bytes<io::BufReader<io::Take<R>>>>, decoder: fax34::decoder::Group4Decoder<FaxBytes<R>>,
line_buf: io::Cursor<Vec<u8>>, line_buf: io::Cursor<Vec<u8>>,
height: u16, height: u32,
width: u16, width: u16,
y: u16, y: u32,
} }
#[cfg(feature = "fax")] #[cfg(feature = "fax")]
@@ -271,13 +304,14 @@ impl<R: Read> Group4Reader<R> {
dimensions: (u32, u32), dimensions: (u32, u32),
reader: R, reader: R,
compressed_length: u64, compressed_length: u64,
fill_order: u16,
) -> crate::TiffResult<Self> { ) -> crate::TiffResult<Self> {
let width = u16::try_from(dimensions.0)?; let width = u16::try_from(dimensions.0)?;
let height = u16::try_from(dimensions.1)?; let height = dimensions.1;
Ok(Self { Ok(Self {
decoder: fax34::decoder::Group4Decoder::new( decoder: fax34::decoder::Group4Decoder::new(
io::BufReader::new(reader.take(compressed_length)).bytes(), fax_byte_iter(reader, compressed_length, fill_order),
width, width,
)?, )?,
line_buf: io::Cursor::new(Vec::with_capacity(width.into())), line_buf: io::Cursor::new(Vec::with_capacity(width.into())),
@@ -356,6 +390,144 @@ impl<R: Read> Read for Group4Reader<R> {
} }
} }
/// 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")]
type OwnedBitReader = fax34::ByteReader<
core::iter::Map<std::vec::IntoIter<u8>, fn(u8) -> Result<u8, std::convert::Infallible>>,
>;
/// Decodes CCITT Group 3 (T.4) 1D compressed data line by line.
///
/// The fax crate's `Group3Decoder` has a bug where `new()` doesn't consume the
/// initial EOL marker, causing its lookup tables to destructively consume bits
/// from the EOL when trying to decode run-length codes. We work around this by
/// using the crate's lower-level `white::decode` / `black::decode` functions
/// and handling EOL markers ourselves.
#[cfg(feature = "fax")]
pub struct Group3Reader {
reader: OwnedBitReader,
line_buf: io::Cursor<Vec<u8>>,
height: u32,
width: u16,
y: u32,
}
#[cfg(feature = "fax")]
impl Group3Reader {
pub fn new<R: Read>(
dimensions: (u32, u32),
reader: R,
compressed_length: u64,
fill_order: u16,
) -> crate::TiffResult<Self> {
let width = u16::try_from(dimensions.0)?;
let height = dimensions.1;
// Buffer all compressed data and apply FillOrder bit reversal
let mut compressed = vec![0u8; compressed_length as usize];
reader.take(compressed_length).read_exact(&mut compressed)?;
if fill_order == 2 {
for b in &mut compressed {
*b = b.reverse_bits();
}
}
// Create an owning bit reader (Vec consumed by into_iter, no borrow issues)
let map_fn: fn(u8) -> Result<u8, std::convert::Infallible> = Ok;
let mut bit_reader = fax34::ByteReader::new(compressed.into_iter().map(map_fn)).unwrap();
// Skip fill bits + initial EOL marker
Self::skip_eol(&mut bit_reader);
Ok(Self {
reader: bit_reader,
line_buf: io::Cursor::new(Vec::with_capacity(width.into())),
width,
height,
y: 0,
})
}
/// Skip zero fill bits followed by the terminating '1' bit of an EOL code.
fn skip_eol(reader: &mut impl fax34::BitReader) {
while reader.peek(1) == Some(0) {
let _ = reader.consume(1);
}
if reader.peek(1) == Some(1) {
let _ = reader.consume(1);
}
}
/// Decode a full run-length value (terminal + optional makeup codes).
fn with_markup<R: fax34::BitReader>(
decoder: fn(&mut R) -> Option<u16>,
reader: &mut R,
) -> Option<u16> {
let mut sum = 0u16;
while let Some(n) = decoder(reader) {
sum = sum.checked_add(n)?;
if n < 64 {
return Some(sum);
}
}
None
}
}
#[cfg(feature = "fax")]
impl Read for Group3Reader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.line_buf.position() as usize == self.line_buf.get_ref().len()
&& self.y < self.height
{
// Decode one line of alternating white/black run-lengths
let mut transitions = Vec::new();
let mut a0 = 0u16;
let mut is_white = true;
loop {
let run = if is_white {
Self::with_markup(fax34::maps::white::decode, &mut self.reader)
} else {
Self::with_markup(fax34::maps::black::decode, &mut self.reader)
};
match run {
Some(len) => {
a0 = match a0.checked_add(len) {
Some(v) => v,
None => break, // overflow = corrupt data
};
transitions.push(a0);
is_white = !is_white;
}
None => break,
}
}
self.y += 1;
// Convert transitions to packed bilevel output
let bytes_per_line = usize::from(self.width).div_ceil(8);
let buffer = self.line_buf.get_mut();
buffer.clear();
buffer.resize(bytes_per_line, 0u8);
let pels = fax34::decoder::pels(&transitions, self.width);
for (i, c) in pels.enumerate() {
if c == fax34::Color::Black {
buffer[i / 8] |= 1 << (7 - (i % 8));
}
}
self.line_buf.set_position(0);
// Skip fill bits + line-ending EOL
Self::skip_eol(&mut self.reader);
}
self.line_buf.read(buf)
}
}
#[cfg(feature = "webp")] #[cfg(feature = "webp")]
pub struct WebPReader { pub struct WebPReader {
inner: Cursor<Vec<u8>>, inner: Cursor<Vec<u8>>,
+2 -1
View File
@@ -18,6 +18,7 @@ use crate::{
/// [`Decoder::read_directory_tags`](crate::decoder::Decoder::read_directory_tags) to read the /// [`Decoder::read_directory_tags`](crate::decoder::Decoder::read_directory_tags) to read the
/// values associated with tags from an underlying file. /// values associated with tags from an underlying file.
#[doc(alias = "IFD")] #[doc(alias = "IFD")]
#[derive(Clone)]
pub struct Directory { pub struct Directory {
/// There are at most `u16::MAX` entries in any single directory, the count is stored as a /// There are at most `u16::MAX` entries in any single directory, the count is stored as a
/// 2-byte value. The order in the file is implied to be ascending by tag value (the decoder /// 2-byte value. The order in the file is implied to be ascending by tag value (the decoder
@@ -170,7 +171,7 @@ mod tests {
dir.get(Tag::Unknown(1)) dir.get(Tag::Unknown(1))
.expect("tag 1 should be present after this chain") .expect("tag 1 should be present after this chain")
.count(), .count(),
u16::MAX.into() u64::from(u16::MAX)
); );
} }
+144 -8
View File
@@ -1,5 +1,97 @@
use crate::tags::{PhotometricInterpretation, SampleFormat}; use crate::tags::{PhotometricInterpretation, SampleFormat};
/// Apply floating-point predictor encoding to a row of floating-point samples.
///
/// The floating-point predictor works by:
/// 1. Converting each value to big-endian bytes
/// 2. Shuffling bytes so all first bytes are together, all second bytes, etc.
/// 3. Applying byte-level horizontal difference (wrapping_sub) across the entire buffer
///
/// The decoder reverses this by:
/// 1. Applying cumulative sum (wrapping_add) on the entire byte stream
/// 2. De-shuffling bytes back into float values
macro_rules! fp_predict {
($name:ident, $float_type:ty) => {
fn $name(row: &[$float_type], samples: usize, result: &mut Vec<u8>) {
const BYTE_SIZE: usize = core::mem::size_of::<$float_type>();
let num_values = row.len();
let byte_count = num_values * BYTE_SIZE;
let start_len = result.len();
// Allocate zeroed space so we can write to arbitrary positions
result.resize(start_len + byte_count, 0);
let output = &mut result[start_len..];
// Step 1 & 2: Convert to big-endian and scatter bytes
for (i, &value) in row.iter().enumerate() {
let bytes = value.to_be_bytes();
for b in 0..BYTE_SIZE {
output[b * num_values + i] = bytes[b];
}
}
// Step 3: Apply byte-level horizontal difference across the entire buffer.
// The prediction spans across quarter boundaries, just like the decoder's cumulative sum.
//
// We process elements in reverse in batches of 16 to enable autovectorization.
// Reverse order ensures that when we compute output[i] - output[i-samples],
// the value at output[i-samples] hasn't been overwritten yet (critical when
// samples < 16, which is the common case: 1, 3, 4, 5).
//
// 8 bits * 16 = 128 bits, the width of a typical SIMD register.
// Even if the target has no vector instructions, this still benefits from
// instruction-level parallelism, since unlike in decoding, all subtractions
// within a chunk are truly independent of each other.
let output = &mut result[start_len..byte_count + start_len];
let diff_len = byte_count - samples;
let remainder_len = diff_len % 16;
// Process full 16-byte chunks in reverse.
// We iterate from the end so that predecessor values (at lower indices)
// are still in their original state when we read them.
let mut chunk_end = byte_count;
while chunk_end >= samples + remainder_len + 16 {
let chunk_start = chunk_end - 16;
let prev_start = chunk_start - samples;
// Read all values in the chunk and their predecessors first.
// This makes all subtractions below independent, enabling vectorization.
let curr: [u8; 16] = output[chunk_start..chunk_end].try_into().unwrap();
let prev: [u8; 16] = output[prev_start..prev_start + 16].try_into().unwrap();
let chunk = &mut output[chunk_start..chunk_end];
chunk[0] = curr[0].wrapping_sub(prev[0]);
chunk[1] = curr[1].wrapping_sub(prev[1]);
chunk[2] = curr[2].wrapping_sub(prev[2]);
chunk[3] = curr[3].wrapping_sub(prev[3]);
chunk[4] = curr[4].wrapping_sub(prev[4]);
chunk[5] = curr[5].wrapping_sub(prev[5]);
chunk[6] = curr[6].wrapping_sub(prev[6]);
chunk[7] = curr[7].wrapping_sub(prev[7]);
chunk[8] = curr[8].wrapping_sub(prev[8]);
chunk[9] = curr[9].wrapping_sub(prev[9]);
chunk[10] = curr[10].wrapping_sub(prev[10]);
chunk[11] = curr[11].wrapping_sub(prev[11]);
chunk[12] = curr[12].wrapping_sub(prev[12]);
chunk[13] = curr[13].wrapping_sub(prev[13]);
chunk[14] = curr[14].wrapping_sub(prev[14]);
chunk[15] = curr[15].wrapping_sub(prev[15]);
chunk_end = chunk_start;
}
// Handle the leading remainder that doesn't fill a complete chunk of 16.
// This must be done last (after the reverse chunk loop) because these are
// the lowest indices, and the chunks above may read predecessor values
// from this range.
for i in (samples..samples + remainder_len).rev() {
output[i] = output[i].wrapping_sub(output[i - samples]);
}
}
};
}
fp_predict!(fp_predict_f32, f32);
fp_predict!(fp_predict_f64, f64);
macro_rules! integer_horizontal_predict { macro_rules! integer_horizontal_predict {
() => { () => {
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>) { fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>) {
@@ -23,6 +115,10 @@ macro_rules! integer_horizontal_predict {
.map(|(prev, current)| current.wrapping_sub(*prev)), .map(|(prev, current)| current.wrapping_sub(*prev)),
); );
} }
fn floating_point_predict(_: &[Self::Inner], _: &mut Vec<u8>) {
unreachable!("floating-point predictor is only valid for floating-point sample types")
}
}; };
} }
@@ -38,6 +134,10 @@ pub trait ColorType {
const SAMPLE_FORMAT: &'static [SampleFormat]; const SAMPLE_FORMAT: &'static [SampleFormat];
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>); fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>);
/// Apply floating-point predictor encoding to a row of samples.
/// This is only implemented for floating-point types; integer types will panic.
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>);
} }
pub struct Gray8; pub struct Gray8;
@@ -108,7 +208,11 @@ impl ColorType for Gray32Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
@@ -140,7 +244,11 @@ impl ColorType for Gray64Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
@@ -180,8 +288,13 @@ impl ColorType for RGB32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
@@ -201,8 +314,13 @@ impl ColorType for RGB64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
@@ -242,8 +360,13 @@ impl ColorType for RGBA32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
@@ -263,8 +386,13 @@ impl ColorType for RGBA64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
@@ -306,7 +434,11 @@ impl ColorType for CMYK32Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
@@ -328,7 +460,11 @@ impl ColorType for CMYK64Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
+8 -17
View File
@@ -7,24 +7,15 @@ pub struct Deflate {
level: FlateCompression, level: FlateCompression,
} }
/// The level of compression used by the Deflate algorithm.
/// It allows trading compression ratio for compression speed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
#[derive(Default)]
pub enum DeflateLevel {
/// The fastest possible compression mode.
Fast = 1,
/// The conserative choice between speed and ratio.
#[default]
Balanced = 6,
/// The best compression available with Deflate.
Best = 9,
}
impl Deflate { impl Deflate {
/// Create a new deflate compressor with a specific level of compression. /// Create a new deflate compressor with a specific level of compression.
pub fn with_level(level: DeflateLevel) -> Self { ///
/// The valid levels are 1 through 9 inclusive. The default level is 6.
///
/// - `1` stands for light but fast compression
/// - `6` is the recommended balanced default
/// - `9` is maximum compression ratio at the cost of slow encoding
pub fn with_level(level: u8) -> Self {
Self { Self {
level: FlateCompression::new(level as u32), level: FlateCompression::new(level as u32),
} }
@@ -33,7 +24,7 @@ impl Deflate {
impl Default for Deflate { impl Default for Deflate {
fn default() -> Self { fn default() -> Self {
Self::with_level(DeflateLevel::default()) Self::with_level(6)
} }
} }
+1 -1
View File
@@ -9,7 +9,7 @@ mod packbits;
mod uncompressed; mod uncompressed;
#[cfg(feature = "deflate")] #[cfg(feature = "deflate")]
pub use self::deflate::{Deflate, DeflateLevel}; pub use self::deflate::Deflate;
#[cfg(feature = "lzw")] #[cfg(feature = "lzw")]
pub use self::lzw::Lzw; pub use self::lzw::Lzw;
+39 -13
View File
@@ -33,21 +33,31 @@ use self::writer::*;
/// Image data can be very unpredictable, and thus very hard to compress. Predictors are simple /// Image data can be very unpredictable, and thus very hard to compress. Predictors are simple
/// passes ran over the image data to prepare it for compression. This is mostly used for LZW /// passes ran over the image data to prepare it for compression. This is mostly used for LZW
/// compression, where using [Predictor::Horizontal] we see a 35% improvement in compression /// compression, where using [Predictor::Horizontal] we see a 35% improvement in compression
/// ratio over the unpredicted compression ! /// ratio over the unpredicted compression!
/// ///
/// [Predictor::FloatingPoint] is currently not supported. /// [Predictor::Horizontal] is for integer sample types.
/// [Predictor::FloatingPoint] is for floating-point sample types (f32, f64).
pub type Predictor = crate::tags::Predictor; pub type Predictor = crate::tags::Predictor;
#[cfg(feature = "deflate")]
pub type DeflateLevel = compression::DeflateLevel;
#[derive(Clone, Copy, PartialEq, Default)] #[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Compression { pub enum Compression {
/// No compression whatsoever. Fastest but produces large files.
#[default] #[default]
Uncompressed, Uncompressed,
/// Legacy [LZW](https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch) algorithm. Not recommended, use DEFLATE instead.
#[cfg(feature = "lzw")] #[cfg(feature = "lzw")]
Lzw, Lzw,
/// [DEFLATE](https://en.wikipedia.org/wiki/DEFLATE) algorithm with the specified compression level.
///
/// The valid levels are 1 through 9 inclusive. The default level is 6.
///
/// - `1` stands for light but fast compression
/// - `6` is the recommended balanced default
/// - `9` is maximum compression ratio at the cost of slow encoding
#[cfg(feature = "deflate")] #[cfg(feature = "deflate")]
Deflate(DeflateLevel), Deflate(u8),
/// [PackBits](https://en.wikipedia.org/wiki/PackBits) compression, a variant of RLE compression scheme. Fast, primitive compression with a poor compression ratio.
Packbits, Packbits,
} }
@@ -153,7 +163,15 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
/// Set the predictor to use /// Set the predictor to use
/// ///
/// A predictor is used to simplify the file before writing it. This is very /// A predictor is used to simplify the file before writing it. This is very
/// useful when writing a file compressed using LZW as it can improve efficiency /// useful when writing a file compressed using LZW or DEFLATE as it can improve compression ratio
///
/// The floating-point predictor is not universally supported. While most software can handle it,
/// Safari, Mac OS Preview and ImageJ Java library cannot. Don't use a predictor for float data
/// for maximum compatibility at the cost of compression ratio.
///
/// The chosen predictor should match the data type being encoded.
/// Encoding floating-point data with `Predictor::Horizontal` is technically possible,
/// but rarely beneficial, and some decoders may reject such files.
pub fn with_predictor(mut self, predictor: Predictor) -> Self { pub fn with_predictor(mut self, predictor: Predictor) -> Self {
self.predictor = predictor; self.predictor = predictor;
@@ -470,7 +488,8 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
self.writer.write_u16(entry.field_type().to_u16())?; self.writer.write_u16(entry.field_type().to_u16())?;
let count = K::convert_offset(entry.count())?; let count = K::convert_offset(entry.count())?;
count.write(self.writer)?; count.write(self.writer)?;
self.writer.write_bytes(&entry.offset()[..offset_bytes])?; self.writer
.write_bytes(&entry.offset_raw()[..offset_bytes])?;
} }
Ok(offset) Ok(offset)
@@ -635,11 +654,14 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> {
impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> { impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> {
fn sanity_check(compression: Compression, predictor: Predictor) -> TiffResult<()> { fn sanity_check(compression: Compression, predictor: Predictor) -> TiffResult<()> {
match (predictor, compression, T::SAMPLE_FORMAT[0]) { match (predictor, compression, T::SAMPLE_FORMAT[0]) {
// Horizontal predictor is not valid for floating-point types
(Predictor::Horizontal, _, SampleFormat::IEEEFP | SampleFormat::Void) => { (Predictor::Horizontal, _, SampleFormat::IEEEFP | SampleFormat::Void) => {
Err(TiffError::UsageError(UsageError::PredictorIncompatible)) Err(TiffError::UsageError(UsageError::PredictorIncompatible))
} }
// Floating-point predictor is only valid for IEEEFP sample format
(Predictor::FloatingPoint, _, SampleFormat::IEEEFP) => Ok(()),
(Predictor::FloatingPoint, _, _) => { (Predictor::FloatingPoint, _, _) => {
Err(TiffError::UsageError(UsageError::PredictorUnavailable)) Err(TiffError::UsageError(UsageError::PredictorIncompatible))
} }
_ => Ok(()), _ => Ok(()),
} }
@@ -773,7 +795,13 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
} }
self.encoder.write_data(row_result.as_slice())? self.encoder.write_data(row_result.as_slice())?
} }
_ => unimplemented!(), Predictor::FloatingPoint => {
let mut row_result = Vec::with_capacity(std::mem::size_of_val(value));
for row in value.chunks_exact(self.row_samples as usize) {
T::floating_point_predict(row, &mut row_result);
}
self.encoder.write_data(row_result.as_slice())?
}
}; };
let byte_count = self.encoder.last_written() as usize; let byte_count = self.encoder.last_written() as usize;
@@ -827,9 +855,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
self.encoder self.encoder
.write_tag(Tag::ResolutionUnit, unit.to_u16()) .write_tag(Tag::ResolutionUnit, unit.to_u16())
.unwrap(); .unwrap();
self.encoder self.encoder.write_tag(Tag::XResolution, value).unwrap();
.write_tag(Tag::XResolution, value.clone())
.unwrap();
self.encoder.write_tag(Tag::YResolution, value).unwrap(); self.encoder.write_tag(Tag::YResolution, value).unwrap();
} }
+4 -4
View File
@@ -665,22 +665,22 @@ impl_tiff_value_for_contiguous_sequence!(Rational; 8; Type::RATIONAL);
impl_tiff_value_for_contiguous_sequence!(SRational; 8; Type::SRATIONAL); impl_tiff_value_for_contiguous_sequence!(SRational; 8; Type::SRATIONAL);
/// Type to represent tiff values of type `IFD` /// Type to represent tiff values of type `IFD`
#[derive(Clone)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Ifd(pub u32); pub struct Ifd(pub u32);
/// Type to represent tiff values of type `IFD8` /// Type to represent tiff values of type `IFD8`
#[derive(Clone)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Ifd8(pub u64); pub struct Ifd8(pub u64);
/// Type to represent tiff values of type `RATIONAL` /// Type to represent tiff values of type `RATIONAL`
#[derive(Clone)] #[derive(Debug, Copy, Clone)]
pub struct Rational { pub struct Rational {
pub n: u32, pub n: u32,
pub d: u32, pub d: u32,
} }
/// Type to represent tiff values of type `SRATIONAL` /// Type to represent tiff values of type `SRATIONAL`
#[derive(Clone)] #[derive(Debug, Copy, Clone)]
pub struct SRational { pub struct SRational {
pub n: i32, pub n: i32,
pub d: i32, pub d: i32,
+5 -3
View File
@@ -221,7 +221,7 @@ fn encoder_on_short_writes() {
}; };
let imgdata = vec![42u8; 100 * 100]; let imgdata = vec![42u8; 100 * 100];
let mut data = crate::decoder::DecodingResult::U8(vec![]); let mut data = crate::decoder::DecodingSampleBuffer::U8(vec![]);
for comp in [ for comp in [
super::Compression::Uncompressed, super::Compression::Uncompressed,
@@ -241,11 +241,13 @@ fn encoder_on_short_writes() {
} }
write.inner.set_position(0); write.inner.set_position(0);
let mut tiff_reader = crate::decoder::Decoder::new(&mut write.inner).unwrap();
let mut tiff_reader = crate::decoder::Decoder::open(&mut write.inner).unwrap();
tiff_reader.next_image().unwrap();
let _layout = tiff_reader.read_image_to_buffer(&mut data).unwrap(); let _layout = tiff_reader.read_image_to_buffer(&mut data).unwrap();
assert!( assert!(
matches!(&data, crate::decoder::DecodingResult::U8(v) if *v == imgdata), matches!(&data, crate::decoder::DecodingSampleBuffer::U8(v) if *v == imgdata),
"{data:?} @ {}", "{data:?} @ {}",
data.as_buffer(0).byte_len() data.as_buffer(0).byte_len()
); );
+80 -10
View File
@@ -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,
@@ -357,7 +360,9 @@ pub enum Predictor(u16) {
/// This means that instead of having in order `[r1, g1. b1, r2, g2 ...]` you will find /// This means that instead of having in order `[r1, g1. b1, r2, g2 ...]` you will find
/// `[r1, g1, b1, r2-r1, g2-g1, b2-b1, r3-r2, g3-g2, ...]` /// `[r1, g1, b1, r2-r1, g2-g1, b2-b1, r3-r2, g3-g2, ...]`
Horizontal = 2, Horizontal = 2,
/// Not currently supported /// Floating-point predictor for IEEE floating-point sample types.
///
/// This predictor byte-shuffles the data and applies byte-level horizontal differencing.
FloatingPoint = 3, FloatingPoint = 3,
} }
} }
@@ -424,7 +429,24 @@ impl ValueBuffer {
} }
/// Create a value with native byte order from in-memory data. /// Create a value with native byte order from in-memory data.
pub fn from_value<T: TiffValue>(value: &T) -> Self { ///
/// This is equivalent to calling [`Self::set_value`] on a newly created buffer.
///
/// # Example
///
/// Create a buffer representing a particular tag's value:
///
/// ```
/// use tiff::tags::{ByteOrder, CompressionMethod, ValueBuffer};
///
/// // A buffer with initial content, for instance a tag's value.
/// let buffer = ValueBuffer::from_value(&CompressionMethod::ModernJPEG);
/// assert_eq!(buffer.as_bytes().len(), 2);
///
/// // Note that depends on host endianess which of these holds:
/// assert!(buffer.as_bytes() == [0, 7] || buffer.as_bytes() == [7, 0]);
/// ```
pub fn from_value<T: TiffValue + ?Sized>(value: &T) -> Self {
ValueBuffer { ValueBuffer {
bytes: value.data().into_owned(), bytes: value.data().into_owned(),
ty: <T as TiffValue>::FIELD_TYPE, ty: <T as TiffValue>::FIELD_TYPE,
@@ -433,6 +455,42 @@ impl ValueBuffer {
} }
} }
/// Set the buffer to the defined value.
///
/// The previous data is erased. This converts the value's representation to the byte order of
/// the buffer in the process (which is a no-op if the buffer uses host byte order).
///
/// # Example
///
/// ```
/// use tiff::tags::{ByteOrder, CompressionMethod, ValueBuffer};
///
/// // A buffer with initial content, for instance a tag's value.
/// let mut buffer = ValueBuffer::from_value(&CompressionMethod::JPEG);
/// assert_eq!(buffer.as_bytes().len(), 2);
///
/// // Ensure a host-independent byte order for the following representation.
/// buffer.set_byte_order(ByteOrder::LittleEndian);
/// // slices and arrays of primitives are a possible `TiffValue`
/// buffer.set_value(&[0, 1, 42u16]);
///
/// // The little endian representation of these numbers (2 bytes each)
/// assert_eq!(buffer.as_bytes(), [0, 0, 1, 0, 42, 0]);
/// ```
pub fn set_value<T: TiffValue + ?Sized>(&mut self, value: &T) {
self.bytes.clear();
match value.data() {
std::borrow::Cow::Borrowed(slice) => self.bytes.extend_from_slice(slice),
std::borrow::Cow::Owned(ref mut data) => self.bytes.append(data),
}
self.ty = <T as TiffValue>::FIELD_TYPE;
self.count = value.count() as u64;
ByteOrder::native().convert(self.ty, &mut self.bytes, self.byte_order);
}
pub fn byte_order(&self) -> ByteOrder { pub fn byte_order(&self) -> ByteOrder {
self.byte_order self.byte_order
} }
@@ -512,6 +570,13 @@ impl ValueBuffer {
} }
} }
/// Create an empty value buffer with `Byte` type. This type choice is arbitrary.
impl Default for ValueBuffer {
fn default() -> Self {
ValueBuffer::empty(Type::BYTE)
}
}
/// Byte order of the TIFF file. /// Byte order of the TIFF file.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ByteOrder { pub enum ByteOrder {
@@ -552,26 +617,22 @@ impl ByteOrder {
return; return;
} }
// FIXME: at MSRV 1.89 or higher use `slice::as_chunks_mut`.
match cls { match cls {
EndianBytes::One => { EndianBytes::One => {
// No change needed // No change needed
} }
EndianBytes::Two => { EndianBytes::Two => {
for chunk in buffer.chunks_exact_mut(2) { for chunk in buffer.as_chunks_mut::<2>().0 {
let chunk: &mut [u8; 2] = chunk.try_into().unwrap();
*chunk = u16::from_be_bytes(*chunk).to_le_bytes(); *chunk = u16::from_be_bytes(*chunk).to_le_bytes();
} }
} }
EndianBytes::Four => { EndianBytes::Four => {
for chunk in buffer.chunks_exact_mut(4) { for chunk in buffer.as_chunks_mut::<4>().0 {
let chunk: &mut [u8; 4] = chunk.try_into().unwrap();
*chunk = u32::from_be_bytes(*chunk).to_le_bytes(); *chunk = u32::from_be_bytes(*chunk).to_le_bytes();
} }
} }
EndianBytes::Eight => { EndianBytes::Eight => {
for chunk in buffer.chunks_exact_mut(8) { for chunk in buffer.as_chunks_mut::<8>().0 {
let chunk: &mut [u8; 8] = chunk.try_into().unwrap();
*chunk = u64::from_be_bytes(*chunk).to_le_bytes(); *chunk = u64::from_be_bytes(*chunk).to_le_bytes();
} }
} }
@@ -587,3 +648,12 @@ pub(crate) enum EndianBytes {
Four, Four,
Eight, Eight,
} }
/// Defines how directories are laid out in the file.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TiffVariant {
/// TIFF-V6.0 with 32-bit offsets and counts.
Standard,
/// Extended TIFF with 64-bit offsets and counts and long directories.
BigTiff,
}
+8 -7
View File
@@ -15,7 +15,9 @@ fn test_big_tiff() {
for filename in filenames.iter() { for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename); let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!( assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"), decoder.dimensions().expect("Cannot get dimensions"),
(64, 64) (64, 64)
@@ -24,21 +26,20 @@ fn test_big_tiff() {
decoder.colortype().expect("Cannot get colortype"), decoder.colortype().expect("Cannot get colortype"),
ColorType::RGB(8) ColorType::RGB(8)
); );
let mut ifd = decoder.current_ifd();
assert_eq!( assert_eq!(
decoder ifd.get_tag_u64(Tag::StripOffsets)
.get_tag_u64(Tag::StripOffsets)
.expect("Cannot get StripOffsets"), .expect("Cannot get StripOffsets"),
16 16
); );
assert_eq!( assert_eq!(
decoder ifd.get_tag_u64(Tag::RowsPerStrip)
.get_tag_u64(Tag::RowsPerStrip)
.expect("Cannot get RowsPerStrip"), .expect("Cannot get RowsPerStrip"),
64 64
); );
assert_eq!( assert_eq!(
decoder ifd.get_tag_u64(Tag::StripByteCounts)
.get_tag_u64(Tag::StripByteCounts)
.expect("Cannot get StripByteCounts"), .expect("Cannot get StripByteCounts"),
12288 12288
) )
+26 -11
View File
@@ -1,6 +1,6 @@
extern crate tiff; extern crate tiff;
use tiff::decoder::{Decoder, DecodingResult}; use tiff::decoder::{Decoder, DecodingSampleBuffer};
use tiff::ColorType; use tiff::ColorType;
use std::fs::File; use std::fs::File;
@@ -16,7 +16,9 @@ fn test_white_ieee_fp16() {
for filename in filenames.iter() { for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename); let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!( assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"), decoder.dimensions().expect("Cannot get dimensions"),
(256, 256) (256, 256)
@@ -25,7 +27,8 @@ fn test_white_ieee_fp16() {
decoder.colortype().expect("Cannot get colortype"), decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16) ColorType::Gray(16)
); );
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
if let DecodingSampleBuffer::F16(img) = decoder.read_image().unwrap() {
for p in img { for p in img {
assert!(p == half::f16::from_f32_const(1.0)); assert!(p == half::f16::from_f32_const(1.0));
} }
@@ -43,7 +46,9 @@ fn test_one_black_pixel_ieee_fp16() {
for filename in filenames.iter() { for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename); let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!( assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"), decoder.dimensions().expect("Cannot get dimensions"),
(256, 256) (256, 256)
@@ -52,7 +57,8 @@ fn test_one_black_pixel_ieee_fp16() {
decoder.colortype().expect("Cannot get colortype"), decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16) ColorType::Gray(16)
); );
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
if let DecodingSampleBuffer::F16(img) = decoder.read_image().unwrap() {
for (i, p) in img.iter().enumerate() { for (i, p) in img.iter().enumerate() {
if i == 0 { if i == 0 {
assert!(p < &half::f16::from_f32_const(0.001)); assert!(p < &half::f16::from_f32_const(0.001));
@@ -74,7 +80,9 @@ fn test_pattern_horizontal_differencing_ieee_fp16() {
for filename in filenames.iter() { for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename); let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!( assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"), decoder.dimensions().expect("Cannot get dimensions"),
(256, 256) (256, 256)
@@ -83,7 +91,8 @@ fn test_pattern_horizontal_differencing_ieee_fp16() {
decoder.colortype().expect("Cannot get colortype"), decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16) ColorType::Gray(16)
); );
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
if let DecodingSampleBuffer::F16(img) = decoder.read_image().unwrap() {
// 0, 2, 5, 8, 12, 16, 255 are black // 0, 2, 5, 8, 12, 16, 255 are black
let black = [0, 2, 5, 8, 12, 16, 255]; let black = [0, 2, 5, 8, 12, 16, 255];
for (i, p) in img.iter().enumerate() { for (i, p) in img.iter().enumerate() {
@@ -107,7 +116,9 @@ fn test_pattern_predictor_ieee_fp16() {
for filename in filenames.iter() { for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename); let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!( assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"), decoder.dimensions().expect("Cannot get dimensions"),
(256, 256) (256, 256)
@@ -116,7 +127,8 @@ fn test_pattern_predictor_ieee_fp16() {
decoder.colortype().expect("Cannot get colortype"), decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16) ColorType::Gray(16)
); );
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
if let DecodingSampleBuffer::F16(img) = decoder.read_image().unwrap() {
// 0, 2, 5, 8, 12, 16, 255 are black // 0, 2, 5, 8, 12, 16, 255 are black
let black = [0, 2, 5, 8, 12, 16, 255]; let black = [0, 2, 5, 8, 12, 16, 255];
for (i, p) in img.iter().enumerate() { for (i, p) in img.iter().enumerate() {
@@ -175,7 +187,9 @@ fn test_predictor_ieee_fp16() {
for filename in filenames.iter() { for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename); let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!( assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"), decoder.dimensions().expect("Cannot get dimensions"),
(16, 16) (16, 16)
@@ -184,7 +198,8 @@ fn test_predictor_ieee_fp16() {
decoder.colortype().expect("Cannot get colortype"), decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16) ColorType::Gray(16)
); );
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
if let DecodingSampleBuffer::F16(img) = decoder.read_image().unwrap() {
for (exp, found) in std::iter::zip(pnm_values.iter(), img.iter()) { for (exp, found) in std::iter::zip(pnm_values.iter(), img.iter()) {
assert!((exp - found.to_f32()).abs() < 0.0001); assert!((exp - found.to_f32()).abs() < 0.0001);
} }
+14 -20
View File
@@ -1,6 +1,6 @@
extern crate tiff; extern crate tiff;
use tiff::decoder::{Decoder, DecodingResult}; use tiff::decoder::{Decoder, DecodingSampleBuffer};
use tiff::tags::Tag; use tiff::tags::Tag;
use tiff::ColorType; use tiff::ColorType;
@@ -15,8 +15,9 @@ fn test_geo_tiff() {
for filename in filenames.iter() { for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename); let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder = decoder.with_limits(tiff::decoder::Limits::unlimited()); decoder = decoder.with_limits(tiff::decoder::Limits::unlimited());
decoder.next_image().expect("Cannot read image");
assert_eq!( assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"), decoder.dimensions().expect("Cannot get dimensions"),
@@ -29,33 +30,31 @@ fn test_geo_tiff() {
num_samples: 5 num_samples: 5
} }
); );
let mut ifd = decoder.current_ifd();
assert_eq!( assert_eq!(
decoder ifd.get_tag_u64(Tag::StripOffsets)
.get_tag_u64(Tag::StripOffsets)
.expect("Cannot get StripOffsets"), .expect("Cannot get StripOffsets"),
418 418
); );
assert_eq!( assert_eq!(
decoder ifd.get_tag_u64(Tag::RowsPerStrip)
.get_tag_u64(Tag::RowsPerStrip)
.expect("Cannot get RowsPerStrip"), .expect("Cannot get RowsPerStrip"),
10 10
); );
assert_eq!( assert_eq!(
decoder ifd.get_tag_u64(Tag::StripByteCounts)
.get_tag_u64(Tag::StripByteCounts)
.expect("Cannot get StripByteCounts"), .expect("Cannot get StripByteCounts"),
1000 1000
); );
assert_eq!( assert_eq!(
decoder ifd.get_tag(Tag::ModelPixelScaleTag)
.get_tag(Tag::ModelPixelScaleTag)
.expect("Cannot get pixel scale") .expect("Cannot get pixel scale")
.into_f64_vec() .into_f64_vec()
.expect("Cannot get pixel scale"), .expect("Cannot get pixel scale"),
vec![60.0, 60.0, 0.0] vec![60.0, 60.0, 0.0]
); );
let DecodingResult::I16(data) = decoder.read_image().unwrap() else { let DecodingSampleBuffer::I16(data) = decoder.read_image().unwrap() else {
panic!("Cannot read band data") panic!("Cannot read band data")
}; };
assert_eq!(data.len(), 500); assert_eq!(data.len(), 500);
@@ -66,21 +65,16 @@ fn test_geo_tiff() {
#[test] #[test]
fn test_webp_tiff() { fn test_webp_tiff() {
let filenames = ["usda_naip_256_webp_z3.tif"]; let filenames = ["usda_naip_256_webp_z3.tif"];
let mut buffer = DecodingResult::U8(vec![]); let mut buffer = DecodingSampleBuffer::U8(vec![]);
for filename in filenames.iter() { for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename); let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
loop {
decoder.read_image_to_buffer(&mut buffer).unwrap();
if !decoder.more_images() {
break;
}
while decoder.more_images() {
decoder.next_image().unwrap(); decoder.next_image().unwrap();
decoder.read_image_to_buffer(&mut buffer).unwrap();
} }
} }
} }
+745 -42
View File
File diff suppressed because it is too large. Load diff
+290 -95
View File
@@ -1,7 +1,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::{fs::File, path}; use std::{fs::File, path};
use tiff::decoder::{Decoder, DecodingResult}; use tiff::decoder::{Decoder, DecodingSampleBuffer};
use tiff::tags::{ByteOrder, Type};
use tiff::ColorType;
use libtest_mimic::{Arguments, Failed, Trial}; use libtest_mimic::{Arguments, Failed, Trial};
use walkdir::WalkDir; use walkdir::WalkDir;
@@ -9,99 +11,202 @@ use walkdir::WalkDir;
const REFERENCE_DIR: &str = "tests/libtiffpic"; const REFERENCE_DIR: &str = "tests/libtiffpic";
// `find test/libtiffpic -type f | sort` // `find test/libtiffpic -type f | sort`
const FILES: &[(&str, bool)] = &[ //
("tests/libtiffpic/caspian.tif", true), // Each entry maps a file path to its expected behavior:
("tests/libtiffpic/cramps.tif", true), // None = not a TIFF file, skip entirely
// FIXME: See <https://github.com/image-rs/image-tiff/issues/338>, this has tiles as // Some(0) = TIFF with unsupported features, decode-only smoke test
// StripOffsets and has a RowsPerStrip. // Some(nonzero) = TIFF with full CRC32 hash verification of decoded output
("tests/libtiffpic/cramps-tile.tif", false), const FILES: &[(&str, Option<u32>)] = &[
("tests/libtiffpic/CVS/Entries", false), ("tests/libtiffpic/caspian.tif", Some(0x4ea8d4e4)),
("tests/libtiffpic/CVS/Repository", false), ("tests/libtiffpic/cramps.tif", Some(0xdfa2fba4)),
("tests/libtiffpic/CVS/Root", false), ("tests/libtiffpic/cramps-tile.tif", Some(0xdfa2fba4)),
("tests/libtiffpic/depth/CVS/Entries", false), ("tests/libtiffpic/CVS/Entries", None),
("tests/libtiffpic/depth/CVS/Repository", false), ("tests/libtiffpic/CVS/Repository", None),
("tests/libtiffpic/depth/CVS/Root", false), ("tests/libtiffpic/CVS/Root", None),
("tests/libtiffpic/depth/flower-minisblack-02.tif", true), ("tests/libtiffpic/depth/CVS/Entries", None),
("tests/libtiffpic/depth/flower-minisblack-04.tif", true), ("tests/libtiffpic/depth/CVS/Repository", None),
("tests/libtiffpic/depth/flower-minisblack-06.tif", true), ("tests/libtiffpic/depth/CVS/Root", None),
("tests/libtiffpic/depth/flower-minisblack-08.tif", true), (
("tests/libtiffpic/depth/flower-minisblack-10.tif", true), "tests/libtiffpic/depth/flower-minisblack-02.tif",
("tests/libtiffpic/depth/flower-minisblack-12.tif", true), Some(0x1c90f515),
("tests/libtiffpic/depth/flower-minisblack-14.tif", true), ),
("tests/libtiffpic/depth/flower-minisblack-16.tif", true), (
("tests/libtiffpic/depth/flower-minisblack-24.tif", true), "tests/libtiffpic/depth/flower-minisblack-04.tif",
("tests/libtiffpic/depth/flower-minisblack-32.tif", true), Some(0x0f4813cb),
("tests/libtiffpic/depth/flower-palette-02.tif", true), ),
("tests/libtiffpic/depth/flower-palette-04.tif", true), (
("tests/libtiffpic/depth/flower-palette-08.tif", true), "tests/libtiffpic/depth/flower-minisblack-06.tif",
("tests/libtiffpic/depth/flower-palette-16.tif", true), Some(0xf0c0a901),
("tests/libtiffpic/depth/flower-rgb-contig-02.tif", true), ),
("tests/libtiffpic/depth/flower-rgb-contig-04.tif", true), (
("tests/libtiffpic/depth/flower-rgb-contig-08.tif", true), "tests/libtiffpic/depth/flower-minisblack-08.tif",
("tests/libtiffpic/depth/flower-rgb-contig-10.tif", true), Some(0xe797b15b),
("tests/libtiffpic/depth/flower-rgb-contig-12.tif", true), ),
("tests/libtiffpic/depth/flower-rgb-contig-14.tif", true), (
("tests/libtiffpic/depth/flower-rgb-contig-16.tif", true), "tests/libtiffpic/depth/flower-minisblack-10.tif",
("tests/libtiffpic/depth/flower-rgb-contig-24.tif", true), Some(0xdee442ec),
("tests/libtiffpic/depth/flower-rgb-contig-32.tif", true), ),
("tests/libtiffpic/depth/flower-rgb-planar-02.tif", true), (
("tests/libtiffpic/depth/flower-rgb-planar-04.tif", true), "tests/libtiffpic/depth/flower-minisblack-12.tif",
("tests/libtiffpic/depth/flower-rgb-planar-08.tif", true), Some(0x4810c4e9),
("tests/libtiffpic/depth/flower-rgb-planar-10.tif", true), ),
("tests/libtiffpic/depth/flower-rgb-planar-12.tif", true), (
("tests/libtiffpic/depth/flower-rgb-planar-14.tif", true), "tests/libtiffpic/depth/flower-minisblack-14.tif",
("tests/libtiffpic/depth/flower-rgb-planar-16.tif", true), Some(0x7e03cf03),
("tests/libtiffpic/depth/flower-rgb-planar-24.tif", true), ),
("tests/libtiffpic/depth/flower-rgb-planar-32.tif", true), (
"tests/libtiffpic/depth/flower-minisblack-16.tif",
Some(0x8e0add32),
),
(
"tests/libtiffpic/depth/flower-minisblack-24.tif",
Some(0x5cf29313),
),
(
"tests/libtiffpic/depth/flower-minisblack-32.tif",
Some(0x80025eee),
),
(
"tests/libtiffpic/depth/flower-palette-02.tif",
Some(0xaeb21cff),
),
(
"tests/libtiffpic/depth/flower-palette-04.tif",
Some(0x339d428b),
),
(
"tests/libtiffpic/depth/flower-palette-08.tif",
Some(0xb6b203e9),
),
(
"tests/libtiffpic/depth/flower-palette-16.tif",
Some(0x4e02308f),
),
(
"tests/libtiffpic/depth/flower-rgb-contig-02.tif",
Some(0x8f16cf35),
),
(
"tests/libtiffpic/depth/flower-rgb-contig-04.tif",
Some(0x95e97f94),
),
(
"tests/libtiffpic/depth/flower-rgb-contig-08.tif",
Some(0x98134e7c),
),
(
"tests/libtiffpic/depth/flower-rgb-contig-10.tif",
Some(0xa2ad8f4d),
),
(
"tests/libtiffpic/depth/flower-rgb-contig-12.tif",
Some(0xfad187fc),
),
(
"tests/libtiffpic/depth/flower-rgb-contig-14.tif",
Some(0x93fdc170),
),
(
"tests/libtiffpic/depth/flower-rgb-contig-16.tif",
Some(0x25e1066b),
),
(
"tests/libtiffpic/depth/flower-rgb-contig-24.tif",
Some(0x25cdf743),
),
(
"tests/libtiffpic/depth/flower-rgb-contig-32.tif",
Some(0x195dde9c),
),
(
"tests/libtiffpic/depth/flower-rgb-planar-02.tif",
Some(0xcc0281ca),
),
(
"tests/libtiffpic/depth/flower-rgb-planar-04.tif",
Some(0x867134b2),
),
(
"tests/libtiffpic/depth/flower-rgb-planar-08.tif",
Some(0x2341af83),
),
(
"tests/libtiffpic/depth/flower-rgb-planar-10.tif",
Some(0x79211047),
),
(
"tests/libtiffpic/depth/flower-rgb-planar-12.tif",
Some(0x14a0ff72),
),
(
"tests/libtiffpic/depth/flower-rgb-planar-14.tif",
Some(0xc077389d),
),
(
"tests/libtiffpic/depth/flower-rgb-planar-16.tif",
Some(0xd0849282),
),
(
"tests/libtiffpic/depth/flower-rgb-planar-24.tif",
Some(0x433d333b),
),
(
"tests/libtiffpic/depth/flower-rgb-planar-32.tif",
Some(0xd0780b23),
),
( (
"tests/libtiffpic/depth/flower-separated-contig-08.tif", "tests/libtiffpic/depth/flower-separated-contig-08.tif",
true, Some(0x97fb03a6),
), ),
( (
"tests/libtiffpic/depth/flower-separated-contig-16.tif", "tests/libtiffpic/depth/flower-separated-contig-16.tif",
true, Some(0xb6ee0135),
), ),
( (
"tests/libtiffpic/depth/flower-separated-planar-08.tif", "tests/libtiffpic/depth/flower-separated-planar-08.tif",
true, Some(0x502e93fa),
), ),
( (
"tests/libtiffpic/depth/flower-separated-planar-16.tif", "tests/libtiffpic/depth/flower-separated-planar-16.tif",
true, Some(0x50e580d9),
), ),
("tests/libtiffpic/depth/genimages", false), ("tests/libtiffpic/depth/genimages", None),
("tests/libtiffpic/depth/README.txt", false), ("tests/libtiffpic/depth/README.txt", None),
("tests/libtiffpic/depth/summary.txt", false), ("tests/libtiffpic/depth/summary.txt", None),
("tests/libtiffpic/dscf0013.tif", true), ("tests/libtiffpic/dscf0013.tif", Some(0)), // unsupported YCbCr chroma subsampling
("tests/libtiffpic/fax2d.tif", true), ("tests/libtiffpic/fax2d.tif", Some(0xc2f30c6f)),
// No support for direct CCITT Group 3 1D format // No support for direct CCITT Group 3 1D format
("tests/libtiffpic/g3test.g3", false), ("tests/libtiffpic/g3test.g3", None),
("tests/libtiffpic/g3test.tif", true), ("tests/libtiffpic/g3test.tif", Some(0x304b606b)),
("tests/libtiffpic/jello.tif", true), ("tests/libtiffpic/jello.tif", Some(0xb03dbda9)),
("tests/libtiffpic/jim___ah.tif", true), ("tests/libtiffpic/jim___ah.tif", Some(0x81a27b7f)),
("tests/libtiffpic/jim___cg.tif", true), ("tests/libtiffpic/jim___cg.tif", Some(0xfd31747c)),
("tests/libtiffpic/jim___dg.tif", true), ("tests/libtiffpic/jim___dg.tif", Some(0x8c6a5e8f)),
("tests/libtiffpic/jim___gg.tif", true), ("tests/libtiffpic/jim___gg.tif", Some(0x8c6a5e8f)),
("tests/libtiffpic/ladoga.tif", true), ("tests/libtiffpic/ladoga.tif", Some(0x9b118511)),
("tests/libtiffpic/off_l16.tif", true), ("tests/libtiffpic/off_l16.tif", Some(0)), // LogLuv decoder output varies across platforms
("tests/libtiffpic/off_luv24.tif", true), ("tests/libtiffpic/off_luv24.tif", Some(0x2bee71a2)),
("tests/libtiffpic/off_luv32.tif", true), ("tests/libtiffpic/off_luv32.tif", Some(0)), // LogLuv decoder output varies across platforms
("tests/libtiffpic/oxford.tif", true), ("tests/libtiffpic/oxford.tif", Some(0x355d2e3e)),
("tests/libtiffpic/pc260001.tif", true), ("tests/libtiffpic/pc260001.tif", Some(0xdb56b753)),
("tests/libtiffpic/quad-jpeg.tif", true), ("tests/libtiffpic/quad-jpeg.tif", Some(0xcba22475)),
("tests/libtiffpic/quad-lzw.tif", true), ("tests/libtiffpic/quad-lzw.tif", Some(0x6055f9ee)), // old-style (LSB) LZW
// FIXME: See <https://github.com/image-rs/image-tiff/issues/338>, this has tiles as ("tests/libtiffpic/quad-tile.tif", Some(0x6055f9ee)),
// StripOffsets and has a RowsPerStrip. ("tests/libtiffpic/README", None),
("tests/libtiffpic/quad-tile.tif", false), ("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported YCbCr chroma subsampling
("tests/libtiffpic/README", false), ("tests/libtiffpic/strike.tif", Some(0x016b0f6a)),
("tests/libtiffpic/smallliz.tif", true), ("tests/libtiffpic/text.tif", Some(0)), // unsupported compression
("tests/libtiffpic/strike.tif", true), ("tests/libtiffpic/ycbcr-cat.tif", Some(0)), // unsupported YCbCr chroma subsampling
("tests/libtiffpic/text.tif", true), ("tests/libtiffpic/zackthecat.tif", Some(0)), // unsupported YCbCr chroma subsampling
("tests/libtiffpic/ycbcr-cat.tif", true),
("tests/libtiffpic/zackthecat.tif", true),
]; ];
fn main() { fn main() {
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|a| a == "--print-hashes") {
print_reference_hashes();
return;
}
let mut candidates: HashMap<_, _> = FILES.iter().copied().collect(); let mut candidates: HashMap<_, _> = FILES.iter().copied().collect();
let mut trials = Vec::new(); let mut trials = Vec::new();
@@ -115,24 +220,22 @@ fn main() {
let Some(path) = entry.path().to_str() else { let Some(path) = entry.path().to_str() else {
continue; continue;
}; };
let path = path.replace(std::path::MAIN_SEPARATOR, "/"); // always use UNIX path separators
// Mark this path as consumed in either case by removing it. // Mark this path as consumed in either case by removing it.
let Some(should_decode) = candidates.remove(path) else { let Some(expected_hash) = candidates.remove(path.as_str()) else {
panic!("File {} not in reference list", path); panic!("File {} not in reference list", path);
}; };
// Check if we should turn it into a test case. // None = non-image file, skip
if !should_decode { let Some(expected_hash) = expected_hash else {
continue; continue;
} };
let path = path.to_string(); let path = path.to_string();
trials.push(Trial::test( trials.push(Trial::test(
path.to_string(), path.to_string(),
move || -> Result<(), Failed> { move || -> Result<(), Failed> { decode_and_verify(&path, expected_hash) },
decode_libtiffpic(&path)
.map_err(|e| Failed::from(format_args!("Decoding failed for {}: {}", path, e)))
},
)); ));
} }
@@ -148,22 +251,114 @@ fn main() {
libtest_mimic::run(&args, trials).exit(); libtest_mimic::run(&args, trials).exit();
} }
fn decode_libtiffpic(path: &str) -> Result<(), tiff::TiffError> { /// Map DecodingResult variant to a TIFF Type for byte-order normalization.
let path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path); fn sample_type_of(result: &DecodingSampleBuffer) -> Type {
match result {
DecodingSampleBuffer::U8(_) => Type::BYTE,
DecodingSampleBuffer::I8(_) => Type::SBYTE,
DecodingSampleBuffer::U16(_) => Type::SHORT,
DecodingSampleBuffer::I16(_) => Type::SSHORT,
DecodingSampleBuffer::U32(_) => Type::LONG,
DecodingSampleBuffer::I32(_) => Type::SLONG,
DecodingSampleBuffer::F32(_) => Type::FLOAT,
DecodingSampleBuffer::U64(_) => Type::LONG8,
DecodingSampleBuffer::I64(_) => Type::SLONG8,
DecodingSampleBuffer::F64(_) => Type::DOUBLE,
DecodingSampleBuffer::F16(_) => Type::SHORT, // f16 is 2 bytes like SHORT
}
}
/// Hash color type into the hasher (discriminant + fields).
fn hash_color_type(hasher: &mut crc32fast::Hasher, color_type: ColorType) {
match color_type {
ColorType::Gray(n) => hasher.update(&[0, n]),
ColorType::RGB(n) => hasher.update(&[1, n]),
ColorType::Palette(n) => hasher.update(&[2, n]),
ColorType::GrayA(n) => hasher.update(&[3, n]),
ColorType::RGBA(n) => hasher.update(&[4, n]),
ColorType::CMYK(n) => hasher.update(&[5, n]),
ColorType::CMYKA(n) => hasher.update(&[6, n]),
ColorType::YCbCr(n) => hasher.update(&[7, n]),
ColorType::Lab(n) => hasher.update(&[8, n]),
ColorType::Multiband {
bit_depth,
num_samples,
} => {
hasher.update(&[9, bit_depth]);
hasher.update(&num_samples.to_le_bytes());
}
other => panic!("compute_tiff_hash: unhandled ColorType {other:?}"),
}
}
/// Compute a CRC32 hash of the full decoded TIFF output using read_image_to_buffer.
///
/// Hashes per page: page index, dimensions, color type, TIFF sample type tag,
/// and decoded sample data normalized to little-endian byte order.
fn compute_hash(path: &std::path::Path) -> Result<u32, tiff::TiffError> {
let img_file = File::open(path)?; let img_file = File::open(path)?;
let mut decoder = Decoder::new(img_file)?; let mut decoder = Decoder::open(img_file)?;
let mut buffer = DecodingResult::U8(vec![]); let mut hasher = crc32fast::Hasher::new();
let mut buffer = DecodingSampleBuffer::U8(vec![]);
loop { let mut page = 0u32;
let _layout = decoder.read_image_to_buffer(&mut buffer); while decoder.more_images() {
decoder.next_image()?;
let (width, height) = decoder.dimensions()?;
let color_type = decoder.colortype()?;
let _layout = decoder.read_image_to_buffer(&mut buffer)?;
if !decoder.more_images() { hasher.update(&page.to_le_bytes());
break; hasher.update(&width.to_le_bytes());
hasher.update(&height.to_le_bytes());
hash_color_type(&mut hasher, color_type);
let sample_type = sample_type_of(&buffer);
hasher.update(&sample_type.to_u16().to_le_bytes());
let mut buf = buffer.as_buffer(0);
let bytes = buf.as_bytes_mut();
ByteOrder::native().convert(sample_type, bytes, ByteOrder::LittleEndian);
hasher.update(bytes);
page += 1;
} }
decoder.next_image()?; hasher.update(&page.to_le_bytes());
Ok(hasher.finalize())
}
fn decode_and_verify(path: &str, expected_hash: u32) -> Result<(), Failed> {
let full_path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path);
if expected_hash == 0 {
// Smoke test: try to decode, tolerate errors (unsupported formats)
let _ = compute_hash(&full_path);
return Ok(());
}
let actual_hash = compute_hash(&full_path)
.map_err(|e| Failed::from(format!("Decoding failed for {path}: {e}")))?;
if actual_hash != expected_hash {
return Err(Failed::from(format!(
"{path}: expected hash 0x{expected_hash:08x} but got 0x{actual_hash:08x}"
)));
} }
Ok(()) Ok(())
} }
/// Print reference hashes for all decodable files (use --print-hashes).
fn print_reference_hashes() {
for &(path, expected) in FILES {
if expected.is_none() {
continue;
}
let full_path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path);
match compute_hash(&full_path) {
Ok(hash) => eprintln!("(\"{path}\", Some(0x{hash:08x})),"),
Err(e) => eprintln!("// {path}: FAILED: {e}"),
}
}
}
+8 -5
View File
@@ -1,4 +1,4 @@
use tiff::decoder::{Decoder, DecodingResult, Limits, TiffCodingUnit}; use tiff::decoder::{Decoder, DecodingSampleBuffer, Limits, TiffCodingUnit};
use std::fs::File; use std::fs::File;
use std::path::PathBuf; use std::path::PathBuf;
@@ -10,8 +10,9 @@ fn read_all_planes() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif"); let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
let file = File::open(path).unwrap(); let file = File::open(path).unwrap();
let io = std::io::BufReader::new(file); let io = std::io::BufReader::new(file);
let mut tif = Decoder::open(io).unwrap();
let mut tif = Decoder::new(io).unwrap(); tif.next_image().unwrap();
let layout = tif.image_buffer_layout().unwrap(); let layout = tif.image_buffer_layout().unwrap();
assert_eq!(layout.planes, 3); assert_eq!(layout.planes, 3);
let plane_stride = layout.plane_stride.map_or(0, |x| x.get()); let plane_stride = layout.plane_stride.map_or(0, |x| x.get());
@@ -36,8 +37,9 @@ fn read_first_coding_unit() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif"); let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
let file = File::open(path).unwrap(); let file = File::open(path).unwrap();
let io = std::io::BufReader::new(file); let io = std::io::BufReader::new(file);
let mut tif = Decoder::open(io).unwrap();
let mut tif = Decoder::new(io).unwrap(); tif.next_image().unwrap();
let layout = tif.image_coding_unit_layout(TiffCodingUnit(0)).unwrap(); let layout = tif.image_coding_unit_layout(TiffCodingUnit(0)).unwrap();
assert_eq!(layout.planes, 3); assert_eq!(layout.planes, 3);
@@ -82,15 +84,16 @@ fn read_planes_by_limits() {
let io = std::io::BufReader::new(file); let io = std::io::BufReader::new(file);
let mut limits = Limits::default(); let mut limits = Limits::default();
let mut buffer = DecodingResult::U8(vec![]); let mut buffer = DecodingSampleBuffer::U8(vec![]);
// Make room for 1 plane. // Make room for 1 plane.
let mut tif = Decoder::new(io).unwrap(); let mut tif = Decoder::open(io).unwrap();
tif = tif.with_limits({ tif = tif.with_limits({
limits.decoding_buffer_size = 374 * 499; limits.decoding_buffer_size = 374 * 499;
limits.clone() limits.clone()
}); });
tif.next_image().unwrap();
tif.read_image_to_buffer(&mut buffer).unwrap(); tif.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499); assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499);
+216 -35
View File
@@ -1,7 +1,7 @@
extern crate tiff; extern crate tiff;
use tiff::decoder::{ifd, Decoder, DecodingResult}; use tiff::decoder::{ifd, Decoder, DecodingSampleBuffer};
use tiff::encoder::{colortype, Ifd, Ifd8, SRational, TiffEncoder}; use tiff::encoder::{colortype, Ifd, Ifd8, Predictor, SRational, TiffEncoder};
use tiff::tags::Tag; use tiff::tags::Tag;
use tiff::ColorType; use tiff::ColorType;
@@ -33,11 +33,14 @@ fn encode_decode() {
} }
{ {
file.seek(SeekFrom::Start(0)).unwrap(); file.seek(SeekFrom::Start(0)).unwrap();
let mut decoder = Decoder::new(&mut file).unwrap(); let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8)); assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100)); assert_eq!(decoder.dimensions().unwrap(), (100, 100));
let mut all_tags = decoder let mut all_tags = decoder
.current_ifd()
.tag_iter() .tag_iter()
.filter_map(Result::ok) .filter_map(Result::ok)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -78,13 +81,13 @@ fn encode_decode() {
); );
assert_eq!( assert_eq!(
decoder.get_tag(Tag::Artist).unwrap(), decoder.current_ifd().get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into()) ifd::Value::Ascii("Image-tiff".into())
); );
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]); let mut buffer = tiff::decoder::DecodingSampleBuffer::U8(vec![]);
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap(); let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
if let tiff::decoder::DecodingResult::U8(img_res) = buffer { if let tiff::decoder::DecodingSampleBuffer::U8(img_res) = buffer {
assert_eq!(image_data, img_res); assert_eq!(image_data, img_res);
} else { } else {
panic!("Wrong data type"); panic!("Wrong data type");
@@ -116,11 +119,14 @@ fn encode_decode_big() {
} }
{ {
file.seek(SeekFrom::Start(0)).unwrap(); file.seek(SeekFrom::Start(0)).unwrap();
let mut decoder = Decoder::new(&mut file).unwrap(); let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8)); assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100)); assert_eq!(decoder.dimensions().unwrap(), (100, 100));
let mut all_tags = decoder let mut all_tags = decoder
.current_ifd()
.tag_iter() .tag_iter()
.filter_map(Result::ok) .filter_map(Result::ok)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -161,12 +167,12 @@ fn encode_decode_big() {
); );
assert_eq!( assert_eq!(
decoder.get_tag(Tag::Artist).unwrap(), decoder.current_ifd().get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into()) ifd::Value::Ascii("Image-tiff".into())
); );
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]); let mut buffer = tiff::decoder::DecodingSampleBuffer::U8(vec![]);
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap(); let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::U8(img_res) = buffer { if let DecodingSampleBuffer::U8(img_res) = buffer {
assert_eq!(image_data, img_res); assert_eq!(image_data, img_res);
} else { } else {
panic!("Wrong data type"); panic!("Wrong data type");
@@ -207,8 +213,10 @@ fn test_encode_ifd() {
// Rewind the cursor for reading // Rewind the cursor for reading
data.set_position(0); data.set_position(0);
{ {
let mut decoder = Decoder::new(&mut data).unwrap(); let mut decoder = Decoder::open(&mut data).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.assert_tag_u32(65000), 42); assert_eq!(decoder.assert_tag_u32(65000), 42);
assert_eq!(decoder.assert_tag_u32_vec(65000), [42]); assert_eq!(decoder.assert_tag_u32_vec(65000), [42]);
@@ -247,14 +255,16 @@ macro_rules! test_roundtrip {
) { ) {
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::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), expected_type); assert_eq!(decoder.colortype().unwrap(), expected_type);
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]); let mut buffer = tiff::decoder::DecodingSampleBuffer::U8(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap(); decoder.read_image_to_buffer(&mut buffer).unwrap();
let image_data = match buffer { let image_data = match buffer {
DecodingResult::$buffer(res) => res, DecodingSampleBuffer::$buffer(res) => res,
_ => panic!("Wrong data type"), _ => panic!("Wrong data type"),
}; };
@@ -267,10 +277,11 @@ macro_rules! test_roundtrip {
} }
file.seek(SeekFrom::Start(0)).unwrap(); file.seek(SeekFrom::Start(0)).unwrap();
{ {
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]); let mut buffer = tiff::decoder::DecodingSampleBuffer::U8(vec![]);
let mut decoder = Decoder::new(&mut file).unwrap(); let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
decoder.read_image_to_buffer(&mut buffer).unwrap(); decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::$buffer(img_res) = buffer { if let DecodingSampleBuffer::$buffer(img_res) = buffer {
assert_eq!(*image_data, img_res); assert_eq!(*image_data, img_res);
} else { } else {
panic!("Wrong data type"); panic!("Wrong data type");
@@ -379,6 +390,144 @@ fn test_ycbcr_u8_roundtrip() {
test_u8_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8)); test_u8_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
} }
/// Test that encoding with the floating-point predictor works correctly
/// and can be decoded back to the original data.
#[test]
fn test_gray_f32_floating_point_predictor() {
// Generate test data: a gradient of f32 values
let width = 32u32;
let height = 32u32;
let mut image_data: Vec<f32> = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
// Create a gradient pattern
image_data.push((x as f32 + y as f32 * 0.1) / 100.0);
}
}
let mut file = Cursor::new(Vec::new());
{
let mut tiff = TiffEncoder::new(&mut file)
.unwrap()
.with_predictor(Predictor::FloatingPoint);
tiff.write_image::<colortype::Gray32Float>(width, height, &image_data)
.unwrap();
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(32));
let mut buffer = DecodingSampleBuffer::F32(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingSampleBuffer::F32(img_res) = buffer {
assert_eq!(image_data.len(), img_res.len());
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
assert_eq!(
*orig, *decoded,
"Mismatch at value: orig={}, decoded={}",
orig, decoded
);
}
} else {
panic!("Wrong data type");
}
}
}
/// Test floating-point predictor with RGB f32 data
#[test]
fn test_rgb_f32_floating_point_predictor() {
let width = 16u32;
let height = 16u32;
let mut image_data: Vec<f32> = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
// RGB pattern
image_data.push(x as f32 / width as f32); // R
image_data.push(y as f32 / height as f32); // G
image_data.push((x + y) as f32 / (width + height) as f32); // B
}
}
let mut file = Cursor::new(Vec::new());
{
let mut tiff = TiffEncoder::new(&mut file)
.unwrap()
.with_predictor(Predictor::FloatingPoint);
tiff.write_image::<colortype::RGB32Float>(width, height, &image_data)
.unwrap();
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(32));
let mut buffer = DecodingSampleBuffer::F32(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingSampleBuffer::F32(img_res) = buffer {
assert_eq!(image_data.len(), img_res.len());
for (i, (orig, decoded)) in image_data.iter().zip(img_res.iter()).enumerate() {
assert_eq!(
*orig, *decoded,
"Mismatch at index {}: orig={}, decoded={}",
i, orig, decoded
);
}
} else {
panic!("Wrong data type");
}
}
}
/// Test floating-point predictor with f64 data
#[test]
fn test_gray_f64_floating_point_predictor() {
let width = 16u32;
let height = 16u32;
let mut image_data: Vec<f64> = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
image_data.push((x as f64 + y as f64 * 0.1) / 100.0);
}
}
let mut file = Cursor::new(Vec::new());
{
let mut tiff = TiffEncoder::new(&mut file)
.unwrap()
.with_predictor(Predictor::FloatingPoint);
tiff.write_image::<colortype::Gray64Float>(width, height, &image_data)
.unwrap();
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(64));
let mut buffer = DecodingSampleBuffer::F64(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingSampleBuffer::F64(img_res) = buffer {
assert_eq!(image_data.len(), img_res.len());
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
assert_eq!(
*orig, *decoded,
"Mismatch: orig={}, decoded={}",
orig, decoded
);
}
} else {
panic!("Wrong data type");
}
}
}
trait AssertDecode { trait AssertDecode {
fn assert_tag_u32(&mut self, tag: u16) -> u32; fn assert_tag_u32(&mut self, tag: u16) -> u32;
fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32>; fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32>;
@@ -392,37 +541,57 @@ trait AssertDecode {
impl<R: std::io::Read + std::io::Seek> AssertDecode for Decoder<R> { impl<R: std::io::Read + std::io::Seek> AssertDecode for Decoder<R> {
fn assert_tag_u32(&mut self, tag: u16) -> u32 { fn assert_tag_u32(&mut self, tag: u16) -> u32 {
self.get_tag(Tag::Unknown(tag)).unwrap().into_u32().unwrap() self.current_ifd()
.get_tag(Tag::Unknown(tag))
.unwrap()
.into_u32()
.unwrap()
} }
fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32> { fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32> {
self.get_tag(Tag::Unknown(tag)) self.current_ifd()
.get_tag(Tag::Unknown(tag))
.unwrap() .unwrap()
.into_u32_vec() .into_u32_vec()
.unwrap() .unwrap()
} }
fn assert_tag_i32(&mut self, tag: u16) -> i32 { fn assert_tag_i32(&mut self, tag: u16) -> i32 {
self.get_tag(Tag::Unknown(tag)).unwrap().into_i32().unwrap() self.current_ifd()
.get_tag(Tag::Unknown(tag))
.unwrap()
.into_i32()
.unwrap()
} }
fn assert_tag_i32_vec(&mut self, tag: u16) -> Vec<i32> { fn assert_tag_i32_vec(&mut self, tag: u16) -> Vec<i32> {
self.get_tag(Tag::Unknown(tag)) self.current_ifd()
.get_tag(Tag::Unknown(tag))
.unwrap() .unwrap()
.into_i32_vec() .into_i32_vec()
.unwrap() .unwrap()
} }
fn assert_tag_u64(&mut self, tag: u16) -> u64 { fn assert_tag_u64(&mut self, tag: u16) -> u64 {
self.get_tag(Tag::Unknown(tag)).unwrap().into_u64().unwrap() self.current_ifd()
.get_tag(Tag::Unknown(tag))
.unwrap()
.into_u64()
.unwrap()
} }
fn assert_tag_u64_vec(&mut self, tag: u16) -> Vec<u64> { fn assert_tag_u64_vec(&mut self, tag: u16) -> Vec<u64> {
self.get_tag(Tag::Unknown(tag)) self.current_ifd()
.get_tag(Tag::Unknown(tag))
.unwrap() .unwrap()
.into_u64_vec() .into_u64_vec()
.unwrap() .unwrap()
} }
fn assert_tag_i64(&mut self, tag: u16) -> i64 { fn assert_tag_i64(&mut self, tag: u16) -> i64 {
self.get_tag(Tag::Unknown(tag)).unwrap().into_i64().unwrap() self.current_ifd()
.get_tag(Tag::Unknown(tag))
.unwrap()
.into_i64()
.unwrap()
} }
fn assert_tag_i64_vec(&mut self, tag: u16) -> Vec<i64> { fn assert_tag_i64_vec(&mut self, tag: u16) -> Vec<i64> {
self.get_tag(Tag::Unknown(tag)) self.current_ifd()
.get_tag(Tag::Unknown(tag))
.unwrap() .unwrap()
.into_i64_vec() .into_i64_vec()
.unwrap() .unwrap()
@@ -456,7 +625,8 @@ fn test_multiple_byte() {
data.set_position(0); data.set_position(0);
{ {
let mut decoder = Decoder::new(&mut data).unwrap(); let mut decoder = Decoder::open(&mut data).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.assert_tag_u32_vec(65000), [1]); assert_eq!(decoder.assert_tag_u32_vec(65000), [1]);
assert_eq!(decoder.assert_tag_u32_vec(65001), [1, 2]); assert_eq!(decoder.assert_tag_u32_vec(65001), [1, 2]);
@@ -537,7 +707,8 @@ fn test_signed() {
//Rewind the cursor for reading //Rewind the cursor for reading
data.set_position(0); data.set_position(0);
{ {
let mut decoder = Decoder::new(&mut data).unwrap(); let mut decoder = Decoder::open(&mut data).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.assert_tag_i32(65000), -1); assert_eq!(decoder.assert_tag_i32(65000), -1);
assert_eq!(decoder.assert_tag_i32_vec(65001), [-1]); assert_eq!(decoder.assert_tag_i32_vec(65001), [-1]);
@@ -589,13 +760,14 @@ fn test_multipage_image() {
img_file.seek(SeekFrom::Start(0)).unwrap(); img_file.seek(SeekFrom::Start(0)).unwrap();
{ {
let mut img_decoder = Decoder::new(&mut img_file).unwrap(); let mut decoder = Decoder::open(&mut img_file).unwrap();
decoder.next_image().unwrap();
// check the dimensions of the image in the first page // check the dimensions of the image in the first page
assert_eq!(img_decoder.dimensions().unwrap(), (2, 2)); assert_eq!(decoder.dimensions().unwrap(), (2, 2));
img_decoder.next_image().unwrap(); decoder.next_image().unwrap();
// check the dimensions of the image in the second page // check the dimensions of the image in the second page
assert_eq!(img_decoder.dimensions().unwrap(), (3, 3)); assert_eq!(decoder.dimensions().unwrap(), (3, 3));
} }
} }
@@ -624,14 +796,21 @@ fn test_rows_per_strip() {
file.seek(SeekFrom::Start(0)).unwrap(); file.seek(SeekFrom::Start(0)).unwrap();
{ {
let mut decoder = Decoder::new(&mut file).unwrap(); let mut decoder = Decoder::open(&mut file).unwrap();
assert_eq!(decoder.get_tag_u64(Tag::RowsPerStrip).unwrap(), 2); decoder.next_image().unwrap();
assert_eq!(
decoder
.current_ifd()
.get_tag_u64(Tag::RowsPerStrip)
.unwrap(),
2
);
assert_eq!(decoder.strip_count().unwrap(), 50); assert_eq!(decoder.strip_count().unwrap(), 50);
for i in 0..50 { for i in 0..50 {
let img2 = [i; 2 * 100]; let img2 = [i; 2 * 100];
match decoder.read_chunk(i as u32).unwrap() { match decoder.read_chunk(i as u32).unwrap() {
DecodingResult::U8(data) => assert_eq!(&img2[..], &data[..]), DecodingSampleBuffer::U8(data) => assert_eq!(&img2[..], &data[..]),
other => panic!("Incorrect strip type {other:?}"), other => panic!("Incorrect strip type {other:?}"),
} }
} }
@@ -664,9 +843,10 @@ fn test_auxiliary_directory() {
} }
data.set_position(0); data.set_position(0);
let mut decoder = Decoder::new(&mut data).unwrap(); let mut decoder = Decoder::open(&mut data).unwrap();
{ {
decoder.next_image().unwrap();
assert_eq!(decoder.dimensions().unwrap(), (1, 1)); assert_eq!(decoder.dimensions().unwrap(), (1, 1));
let _ = decoder.read_image().unwrap(); let _ = decoder.read_image().unwrap();
} }
@@ -677,6 +857,7 @@ fn test_auxiliary_directory() {
let _ = decoder.read_image().unwrap(); let _ = decoder.read_image().unwrap();
let exif_location = decoder let exif_location = decoder
.current_ifd()
.get_tag(Tag::ExifDirectory) .get_tag(Tag::ExifDirectory)
.expect("second directory missing Exif") .expect("second directory missing Exif")
.into_ifd_pointer() .into_ifd_pointer()
+8 -8
View File
@@ -2,10 +2,9 @@ extern crate tiff;
use std::io::{Cursor, Seek, Write}; use std::io::{Cursor, Seek, Write};
use tiff::{ use tiff::{
decoder::{Decoder, DecodingResult}, decoder::{Decoder, DecodingSampleBuffer},
encoder::{ encoder::{
colortype::{self, ColorType}, colortype::{self, ColorType},
compression::*,
Compression, TiffEncoder, TiffValue, Compression, TiffEncoder, TiffValue,
}, },
}; };
@@ -110,12 +109,13 @@ fn encode_decode_with_compression(compression: Compression) {
// Decode tiff // Decode tiff
data.set_position(0); data.set_position(0);
{ {
let mut decoder = Decoder::new(data).unwrap(); let mut decoder = Decoder::open(data).unwrap();
decoder.next_image().unwrap();
// Check the RGB image // Check the RGB image
assert_eq!( assert_eq!(
match decoder.read_image() { match decoder.read_image() {
Ok(DecodingResult::U16(image_data)) => image_data, Ok(DecodingSampleBuffer::U16(image_data)) => image_data,
unexpected => panic!("Descoding RGB failed: {unexpected:?}"), unexpected => panic!("Descoding RGB failed: {unexpected:?}"),
}, },
image_rgb.reference_data() image_rgb.reference_data()
@@ -125,7 +125,7 @@ fn encode_decode_with_compression(compression: Compression) {
decoder.next_image().unwrap(); decoder.next_image().unwrap();
assert_eq!( assert_eq!(
match decoder.read_image() { match decoder.read_image() {
Ok(DecodingResult::U8(image_data)) => image_data, Ok(DecodingSampleBuffer::U8(image_data)) => image_data,
unexpected => panic!("Decoding grayscale failed: {unexpected:?}"), unexpected => panic!("Decoding grayscale failed: {unexpected:?}"),
}, },
image_grayscale.reference_data() image_grayscale.reference_data()
@@ -147,9 +147,9 @@ fn encode_decode_with_lzw() {
#[test] #[test]
#[cfg(feature = "deflate")] #[cfg(feature = "deflate")]
fn encode_decode_with_deflate() { fn encode_decode_with_deflate() {
encode_decode_with_compression(Compression::Deflate(DeflateLevel::Fast)); encode_decode_with_compression(Compression::Deflate(1));
encode_decode_with_compression(Compression::Deflate(DeflateLevel::Balanced)); encode_decode_with_compression(Compression::Deflate(6));
encode_decode_with_compression(Compression::Deflate(DeflateLevel::Best)); encode_decode_with_compression(Compression::Deflate(9));
} }
#[test] #[test]
+2 -1
View File
@@ -13,7 +13,8 @@ fn test_directory<F: Fn(File) -> bool>(path: &str, f: F) {
} }
fn decode_tiff(file: File) -> TiffResult<()> { fn decode_tiff(file: File) -> TiffResult<()> {
let mut decoder = Decoder::new(file)?; let mut decoder = Decoder::open(file)?;
decoder.next_image()?;
decoder.read_image()?; decoder.read_image()?;
Ok(()) Ok(())
} }
+1417
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 135 files, more files were not shown because too many files have changed in this diff. Show more