Compare commits

..
Author SHA1 Message Date
A. Molzer 082a35adb9 Deprecate unofficial value types
Seems they were introduced with BigTiff but since the enumeration of the
decoder had no obligation of introducing discriminant values the list
was not cross-referenced to libtiff. Hence, some rational types exist
with no basis in decoding.
2026-01-08 14:25:09 +01:00
Aurelia Molzer 923df5f0bd Merge pull request #321 from image-rs/forward-tiff-value-for-vec-and-array
Add diagnostic helps for TiffValue proxy implementations
2026-01-06 20:05:30 +00:00
A. Molzer c4328644b8 Fixup clippy from MSRV 1.85 bump 2026-01-06 17:30:51 +01:00
A. Molzer 27bfcf5562 Bump MSRV to 1.85
This gives us the diagnostic items. As a side effect we will have
`expect` for lints from 1.81 to apply in later PRs. It's time I think.
2026-01-06 17:27:51 +01:00
A. Molzer 7d498a0284 Add TiffValue proxy implementations
We have a fundamental choice here: We could add more proxy
implementations for `Vec` and `Box` to follow `&'_ T`, for some types
that indirections is clear. Vec and arrays are also both very natural
ways of expressing a repeated value. However, passing an allocated value
to the interface forces us to drop it inside. We might be able to
utilize the allocation for an in-place byteswap in a future increment
but not with the current design.

So, overall, I think that inherently `Drop`-y types should be avoided in
these implementations. Instead, we can use the amazing diagnostic tools
in the compiler to just help the users navigate the type system. We can
still provide the impl for arrays.

For context, see: https://users.rust-lang.org/t/what-type-is-u16/137395

Now they see:

	    |
	 38 |         .write_tag(tiff::tags::Tag::GeoKeyDirectoryTag, &geokey);
	    |                                                          ^^^^^^ the trait `TiffValue` is not implemented for `&Vec<u16>`
	    |
	    = note: the trait is implemented for primitive types (`u8`, `i16`, `f32`, etc.)
	    = note: the trait is implemented for shared references to values
	    = note: the trait is implemented for slices, pass them by reference (e.g. `&[u8]`)
	    = note: the trait is implemented for arrays if it is implemented for a slice
	    = note: values in a `Vec` or `Box` should be dereferenced, e.g. `&vec[..]`
	    = help: the following other types implement trait `TiffValue`:

Previously the compiler would both strip the `&` in its first message
(due to our forwarding impl) and not list any way of getting a
reference-to-slice.
2026-01-06 17:26:18 +01:00
Aurelia Molzer 5b21bcc75c Merge pull request #320 from image-rs/release-0.11
Release notes for 0.11
2026-01-05 22:53:18 +00:00
A. Molzer fe305ec24a Bump version to 0.11 2026-01-05 22:49:00 +01:00
Aurelia Molzer a3ab252168 Merge pull request #319 from image-rs/ycbcr-subsampling
Recognize Ycbcr/Chroma subsampling tags
2026-01-05 20:57:17 +00:00
A. Molzer bd64e438a4 Add note about subsampling support to decoder 2026-01-05 21:46:59 +01:00
A. Molzer fa225e820b Exempt JPEG compression from upsampling restriction
I didn't realize we support this in a slightly weird manner where the
tag is completely ignored and zune-jpeg performs the upsampling as
indicated in the JPEG stream. This is a recurring problem with the
compression scheme and tags in TIFF. In the end, decoder can only handle
the data as indicated by the JPEG steam and while the specification
requires the tags to be consistent with the underlying data, sometimes
they are not. The useful fallback is of course to ignore the tag.

Ignoring the tag of course buts us in a weird spot for zero-copy
decoding since the buffer indication to the caller will be incorrect.
For now, maybe the best way of treating subsampling is to upsample all
the cases when we add support.
2026-01-05 21:38:32 +01:00
A. Molzer ea373bae00 Fix YCbCr encoding not having subsampling 2026-01-05 21:14:40 +01:00
A. Molzer 23d1867f75 Recognize YCbCr/Chomra subsampling tags
In particular we avoid mistaking the absence of this tag for an absence
of subsampling. The specification instead requires it to mean half the
samples in both directions—a mode that we do not support yet. Instead of
returning erroneous samples or an error in the data stream (which would
appear too short) we return an error when asked to layout the read
buffer for these images.
2026-01-05 21:14:40 +01:00
Aurelia Molzer 234b2ba677 Merge pull request #318 from image-rs/release-0.11
Fix a couple of nits
2026-01-05 17:50:30 +00:00
Dario Bartussek a63a01319c Made Tag::from_u16, from_u16_exhaustive and to_u16 const
(cherry picked from commit 5c3c25ec2d152a672b0b1307efc69ccfcb0bea29)
2026-01-05 18:34:36 +01:00
A. Molzer efd2a6dbd8 Nit: documentation links to wrong item 2026-01-05 18:31:57 +01:00
Aurelia Molzer d38fba4268 Merge pull request #317 from image-rs/cielab
Add ICC Lab interpretation
2025-12-30 05:55:33 +00:00
A. Molzer 1732481b01 Add ICC Lab interpretation
The CIE Lab interpretation uses heterogeneous samples, and the ITU Lab
encoding requires the Tag Decode (433, 0x01B1) which has default values
depending on bit depth and convering values into floating points, probably.

See also Photometric Interpretation: <https://web.archive.org/web/20050309053132/http://www.awaresystems.be/imaging/tiff/tifftags/photometricinterpretation.html>
See also Tag Decode: <https://web.archive.org/web/20050309072856/http://www.awaresystems.be/imaging/tiff/tifftags/decode.html>
2025-12-30 06:51:20 +01:00
Aurelia Molzer 355ecc8ad9 Merge pull request #313 from image-rs/decode-planar-data
Implement support for planar data
2025-12-29 19:05:39 +00:00
A. Molzer 2d90179602 Soften deprecation of Decoder::read_image 2025-12-29 19:49:09 +01:00
A. Molzer 5a3b11314e Rewrite _to_buffer variants
We previously handed it a wrapper that was supposedly type-safe but
really internally cast the buffer to bytes. That was rather pointless as
the `_bytes` method could always be used instead. We replace the
semantics by a utility which allocates an appropriate buffer with the
internal limits, wrapping the exact sequence identified previously in
the example as a pattern.
2025-12-28 22:23:59 +01:00
A. Molzer 5150e91bbb Document and deprecate read_image 2025-12-28 22:02:43 +01:00
A. Molzer 6d56d014a1 Sample types in layouts, resize DecoderResult
By adding the type interpretation to `BufferLayoutPreference` (simply
computed from the same fields that are trivial to fetch in
`ReadoutLayout` with the rest) we can allocate an appropriate
`DecoderResult` without any `Decoder` instance. We still want a Limits
instance though. This lets a caller read _typed_ data from the `_bytes`
methods which perform less allocations. It also lets them read multiple
planes in a more convenient interface. See, `read_image` will keep its
planar treatment bug for a little bit so you don't accidentally break
during an upgrade.

This also implies less code is being monomorphized, just a bonus.
2025-12-28 21:06:59 +01:00
A. Molzer af8a8d660f Write decoded planar data in example 2025-12-28 18:48:41 +01:00
A. Molzer b1c74495ce Allow extraneous extra samples beyond alpha 2025-12-28 15:09:42 +01:00
A. Molzer 1f02ff8ca1 Add method to retrieve planes of a tile
This is well-defined even if we add sub-sampling support later on. We
can adjust the layout type accordingly with factors per each plane. Note
that the layout, in theory, does not depend on the index of the unit if
we describe all planes for a multi-planar layout. The specification
requires that the subsampling factors divide the tile sizes. However we
have to handle non-planar images here and this leaves us open to other
potential parameters.
2025-12-28 15:06:10 +01:00
A. Molzer 5b9dc30919 Remove badly defined layout method 2025-12-10 21:59:03 +01:00
A. Molzer c3882b252c Cleanup documentation 2025-11-27 23:56:57 +01:00
A. Molzer 6fe21da979 Simplify layout proxy methods 2025-11-27 23:56:57 +01:00
A. Molzer 0de1261948 Simplify responsibility of testing buffer size 2025-11-27 23:56:56 +01:00
A. Molzer 6439880e3d Simplify layout structs 2025-11-27 23:56:13 +01:00
A. Molzer dce1c40bd5 Move layout responsibility into ReadoutLayout 2025-11-27 23:56:08 +01:00
A. Molzer 5d3a5242ea Add plane selection to allocating result_extent 2025-11-26 19:18:27 +01:00
A. Molzer 58543962f3 Add example that exercises decoding (no output) 2025-11-23 19:54:21 +01:00
A. Molzer 95f2d475fa Consolidate layout information type
We currently have one type in decoder/image.rs and another in
decoder/mod.rs which each contributes part of the computation for laying
out the output buffer. Confusingly though they rely on shared
implementation details such as determining the number of channels that
are written and their bit depth. We should have one shared method and
resulting struct instead.
2025-11-23 19:51:26 +01:00
A. Molzer f9eaf9632a Expand PreferredLayout for planar information 2025-11-17 21:11:28 +01:00
A. Molzer 61b3027f38 Test reading planes of a whole image 2025-11-13 23:38:15 +01:00
A. Molzer 65662c4834 Expand requested plane when reading chunk 2025-11-12 23:42:49 +01:00
Aurelia Molzer 985f3822c8 Merge pull request #311 from image-rs/decode-and-encode-extra-samples
Decode and encode extra samples
2025-11-12 10:49:06 +00:00
A. Molzer 2eef9ac088 Encoding of extra samples, roundtrip test 2025-11-12 01:41:38 +01:00
Aurelia Molzer f6d95249dd Merge pull request #310 from image-rs/directory-improvements
Add some utilities around Directory
2025-11-12 00:24:57 +00:00
A. Molzer 8517bb81db Decode with ExtraSamples
These samples are normal bits in the encoded (e.g. compressed) stream of
each tile or row of the image. However, when considering the photometric
interpretation of an image these samples should be discarded. They are
only relevant for multiband.

For the most part we previously rejected these images due to a mismatch
between sample count and the color as indicated by the interpretation.
Only, for alpha we never really verified if the extra channel is indeed
alpha or something unrelated. We don't have a perfect solution as
GeoTiff motivates us having a MultiBand pseudo-color output
configuration that has no related photometric interpretation and borrows
the usual gray tone instead. We can only really distinguish this from a
gray-alpha image through looking at the values of the ExtraSamples
vector. Then again we do not yet support GrayAlpha anyways.

As a simplification for the discarding process we only support full-byte
extra samples and require all samples to be the same bit-depth, not only
those which we return. This is blessed by Tiff's recommendations for
baseline decoding as well as libtiff.
2025-11-12 01:23:12 +01:00
A. Molzer eb6275547d Build Directory FromIterator
This enables a test for the encoder's DirectoryOffset but it seems to
fit nicely with the current design, too.
2025-11-12 01:21:53 +01:00
A. Molzer 73f066c09d Expose DirectoryOffset::new 2025-11-12 01:21:52 +01:00
Aurelia Molzer 24887896ff Merge pull request #309 from image-rs/short-writes-uncompressed
Short writes uncompressed
2025-11-08 15:52:36 +00:00
A. Molzer 02118620e2 Verify handling of a blocked writer 2025-11-08 11:52:58 +01:00
A. Molzer 0b5dc72454 Fix: uncompressed encoder doing short writes 2025-11-08 11:01:52 +01:00
Aurelia Molzer 34e35fb27e Merge pull request #307 from image-rs/public-byte-order
Provide a `tags::ByteOrder` public API
2025-11-05 03:40:13 +00:00
Aurelia Molzer bec6558aa8 Merge pull request #306 from 1c3t3a/fix-panic-on-invalid-lzw
Don't panic if LZW doesn't find an LZW end code
2025-11-04 22:22:12 +00:00
A. Molzer 6cdf3d8d22 Naming pass for byte order conversion 2025-11-04 23:17:05 +01:00
A. Molzer 0a2b3f91ab Remove unused method in EndianReader 2025-11-04 12:55:29 +01:00
A. Molzer 99b3c03ce3 Use bulk byte order conversion for tag reads 2025-11-04 12:54:53 +01:00
Aurelia Molzer bec8e495a5 Merge pull request #308 from image-rs/vectorized-predict
const-specialize predict by sample counts
2025-11-04 10:37:21 +00:00
A. Molzer de7dbad18b const-specialize predict by sample counts
The idea is to allow codegen to exploit specific known constants. It
seems that LLVM by itself will not generate split loop blocks by an
input argument that is a loop constant. Whereas we know, from PNG, that
some constants yield much better code. In this case

    coef[i] = coef[i] + coef[i - n]

This has a destructive dependency chain for `n = 1`, should never get an
argument of 0 and is almost embarrassingly parallel in SIMD if n >= 8
where we can increase the amount of data loaded at once for each
independent loop iteration.

By splitting the loop we make the compiler apply independent
optimization passes to each of case, then have a fallback for things we
did not cover with vectorized possibilities.
2025-11-04 10:23:51 +01:00
A. Molzer 19ac30ac3f Share ByteOrder in tags, publicly 2025-11-03 20:29:48 +01:00
A. Molzer 2058b7f3ca Stand-alone size computation of Type 2025-11-03 20:29:48 +01:00
Bastian Kersting edd260fe7e Don't panic if LZW doesn't find an LZW end code
Currently, when getting the LZWStatus::NoProgress from the underlying
weezl library, tiff checks that no input bytes were read and no
output bytes were written. Although this is according to what the
weezl library mentions in their doc-comment, it's not what the real
result always looks like.

Our fuzzer found input that triggered exactly this case, where the LZW
reader is able to write some output byte before hitting the NoProgress
status.

This change removes the assert, as the resulting action is anyways to return
an error that can be handled by the caller.
2025-10-31 06:17:35 +00:00
Aurelia Molzer 2ccbe132e0 Merge pull request #305 from Mrmaxmeier/fix-malformed-sample-format
Fix decoder panic with malformed (empty) sample format tag
2025-10-27 15:01:59 +00:00
Your Name 714e470595 Fix decoder panic with malformed (empty) sample format tag
I've included a minimal reproducer in
tests/fuzz_images/oor_panic/sample-format-empty-vec.bin
2025-10-27 15:56:37 +01:00
Jacob Adam W. 4adba46b9f Update zune-jpeg to 0.5 (#304) 2025-10-25 13:53:04 -07:00
Aurelia Molzer 695208cffe Merge pull request #298 from image-rs/release-0.10.3
Add metadata and changelog for 0.10.3
2025-09-01 00:33:39 +00:00
Aurelia Molzer 4acdfea783 Add metadata and changelog for 0.10.3 2025-09-01 02:30:40 +02:00
Aurelia Molzer 64286c2110 Merge pull request #294 from image-rs/fax-default-feature
Make fax a default feature
2025-09-01 00:15:14 +00:00
Aurelia Molzer 12c98535f6 Merge pull request #297 from image-rs/fax4-photometric-interpretation
Verify WhiteIsZero invert, add Gray(1) color type
2025-09-01 00:14:14 +00:00
Aurelia Molzer 1556713d6e Simplify match in invert 2025-09-01 02:12:10 +02:00
Aurelia Molzer 682ef0c375 Comment on fax3/4 RLE treatment in tiff 2025-09-01 01:59:42 +02:00
Aurelia Molzer bc71086a96 Verify WhiteIsZero invert, add Gray(1) color type
This was found through the fax4 decoding that is always bilevel.

No longer silently ignore an unknown color type when a photometric
interpretation requires an inversion. DICOM requires that WhiteIsZero
and BlackIsZero are only used for samples-per-pixel=1 but we do not
strictly enforce that early. Previously for the result Multiband color
we just ignored the tag which is also not right.
2025-09-01 00:25:48 +02:00
Sergey "Shnatsel" Davidoff e67bac763f Merge pull request #295 from image-rs/fax-supported-readme
Document Fax compression as supported
2025-08-31 20:38:38 +01:00
Sergey "Shnatsel" Davidoff 839655b975 Clarify it's Fax4 only, fax3 is not supported 2025-08-31 20:36:17 +01:00
Sergey "Shnatsel" Davidoff 85635c3cd2 Document Fax compression as supported 2025-08-31 19:18:43 +01:00
Sergey "Shnatsel" Davidoff 481a2ab54a Make fax a default feature
Fax was commonly requested, so let's make it Just Work when tiff crate is used through `image` where it's difficult to fiddle with tiff crate features.
2025-08-31 15:28:09 +01:00
Aurelia Molzer 7c6c327817 Merge pull request #229 from stephenjudkins/sdj/fax4-support
CCITT group 4 (Fax4) decoding support
2025-08-31 11:58:05 +00:00
Aurelia Molzer 9f1de91880 Make an explicit 'fax' feature 2025-08-31 01:32:32 +02:00
Aurelia Molzer 4a68feab5a Apply clippy lints 2025-08-31 01:26:05 +02:00
Aurelia Molzer b5afb8cca4 Drop bitvec dependency 2025-08-31 01:25:58 +02:00
Stephen Judkins c8d43262e9 Simplify decoder
Reuse the allocation of bits for every row though
2025-08-31 01:25:51 +02:00
Stephen Judkins 80ce77190e Refactor Group4 reader into stream module 2025-08-31 01:25:43 +02:00
Stephen Judkins c75aa98ce2 bit-addressable output (doesn't work yet) 2025-08-31 01:25:42 +02:00
Stephen Judkins c1087f6f08 Reader instance for group 4-decoded images, that buffer one line at a time 2025-08-31 01:24:21 +02:00
Aurelia Molzer 8cc6bc657d Merge pull request #293 from 1c3t3a/fix-byte-metadata-array
Fix type mismatch for reading Lists of Bytes
2025-08-30 21:33:48 +00:00
Stephen Judkins 9fcc2a3b65 review changes: pass in width/height, unwrap reader bytes 2025-08-30 23:24:30 +02:00
Stephen Judkins 666f871d30 CCITT group 4 (Fax4) decoding support 2025-08-30 23:20:41 +02:00
Bastian Kersting a919b8febf Fix type mismatch for reading Lists of Bytes
At the moment when writing Byte strings into tiff metadata sections (like
for XMP metadata) and try to read it with this crate, you will find your
characters as a Value::UnsignedBig, holding the char value with padding.

This seems wrong and we should instead return actual Value::Bytes.
2025-08-29 12:06:03 +00:00
Aurelia Molzer 85b5df7a62 Merge pull request #292 from image-rs/changelog-0.10.1
Changelog 0.10.2
2025-08-26 16:03:34 +00:00
Aurelia Molzer 756ea3e813 Add metadata for 0.10.2 2025-08-26 11:24:04 +02:00
Aurelia Molzer 0eead083f6 Fix missing method on Decoder 2025-08-26 11:19:35 +02:00
Aurelia Molzer 34232d8141 Merge pull request #291 from image-rs/changelog-0.10.1
Populate changelog for 0.10.1
2025-08-25 21:42:34 +00:00
27 changed files with 2277 additions and 557 deletions

No files matched your search

+4 -4
View File
@@ -9,14 +9,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
rust: ["1.74.0", stable, beta, nightly]
rust: ["1.85.0", stable, beta, nightly]
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@nightly
if: ${{ matrix.rust == '1.74.0' }}
if: ${{ matrix.rust == '1.85.0' }}
- name: Generate Cargo.lock with minimal-version dependencies
if: ${{ matrix.rust == '1.74.0' }}
if: ${{ matrix.rust == '1.85.0' }}
run: cargo -Zminimal-versions generate-lockfile
- uses: dtolnay/rust-toolchain@v1
@@ -29,7 +29,7 @@ jobs:
- name: build
run: cargo build -v
- name: test
if: ${{ matrix.rust != '1.74.0' }}
if: ${{ matrix.rust != '1.85.0' }}
run: cargo test -v && cargo doc -v
features:
+72
View File
@@ -1,3 +1,75 @@
# Version 0.11.0
- `Directory` now implements `FromIterator<(Tag, Value)>`.
Changes:
- The decoder now interprets the `ExtraSamples` tag. The sample count must now
more strict match the expected value with alpha channels only allowing for
explicitly denoted unassociated or associated alpha. This effects the
indicated color type when decoding images with additional samples indicated
as unspecified relation. Previously, these may have been interpreted as
alpha by the total sample count (e.g. RgbA if 4 samples under a photometric
interpretation of RGB).
- The decoder handles planar data, current limited to non-subsampled channels.
The `Decoder::read_image` method return planes one-after-another depending on
the size of the buffer that was passed.
- `Decoder::read_image_to_buffer` now takes `&mut DecodingResult` and resizes
it according to the required layout. Previously, a borrowed `DecodingBuffer`
was passed which can be replaced by calling `as_bytes_mut` and
`read_image_bytes`.
- Several methods of tags are now `const`. Note that does not guarantee any
particular value when calling these methods.
Fixes:
- Fix a bug in the uncompressed encoder that could lead to short writes, i.e.
data silently dropped when the underlying writer did not accept all data in a
single write call.
- Encoding YCbCr data now writes the `ChromaSubsampling` tag as `(1, 1)` to
indicate no subsampling, instead of leaving it at its default of `(2, 2)`.
- The decoder will reject subsampled YCbCr data as there is no upsampling
routine, except for JPEG compressed images where the JPEG decoders handles
this. Since the buffer in that case indicates a full-sized plane for all
color samples any future support for the tag will upsample all planes (at
least within this major version).
Additions:
- Added support for the `CieLab` color type.
- Added `DecodingResult::resize_to` to create a buffer with a matching sample
type and dimensions.
- Added `ByteOrder::native` to access the platform's native endianness.
- Added `ByteOrder::convert` to change the byte-order of values in a byte
buffer, depending on their `Type` as described at runtime.
- Added `DirectoryOffset::new` to encode a directory whose offset is known to
the caller but that has not been written through the encoder itself.
- Added `Encoder::extra_samples` to encode images with more samples than their
color's trait implementation would otherwise suggest.
- Added `DirectoryEncoder::extend_from` to encode multiple tag entries from a
directory whose values were written to the file by means outside the
encoder's control.
- Added `Decoder::read_coding_unit_bytes` to retrieve data of corresponding
coordinates from potentially planar data, which is encoded in multiple chunks
of the file.
# Version 0.10.3
New features:
- Group 4 `fax` is now recognized as a compression type and decoded.
- A crate feature `fax` controls related dependencies, on by default.
Changes:
- The presence of PhotometricInterpretation::WhiteIsZero now returns an error
if the color type can not be inverted by the library where they were
previously silently ignored. Please report any missing color types.
Fixes:
- The PhotometricInterpretation::WhiteIsZero is now also applied to 1-bit
images, as well as 2-bit and 4-bit sample types where previously the presence
of this tag was ignored.
# Version 0.10.2
Republished 0.10.1, fixing an accidentally removed method (`Decoder::find_tag_unsigned_vec`).
# Version 0.10.1
New features:
+7 -4
View File
@@ -1,11 +1,11 @@
[package]
name = "tiff"
version = "0.10.1"
version = "0.11.0"
edition = "2021"
resolver = "2"
# note: when changed, also update test runner in `.github/workflows/rust.yml`
rust-version = "1.74.0"
rust-version = "1.85.0"
license = "MIT"
description = "TIFF decoding and encoding library in pure Rust"
@@ -20,10 +20,12 @@ exclude = ["tests/images/*", "tests/fuzz_images/*"]
half = { version = "2.4.1" }
quick-error = "2.0.1"
# Rename to avoid confusion with the feature name.
fax34 = { package = "fax", version = "0.2.6", optional = true }
flate2 = { version = "1.0.20", optional = true }
weezl = { version = "0.1.10", optional = true }
zstd = { version = "0.13", optional = true }
zune-jpeg = { version = "0.4.17", optional = true }
zune-jpeg = { version = "0.5.1", optional = true }
[dev-dependencies]
criterion = "0.3.1"
@@ -31,10 +33,11 @@ criterion = "0.3.1"
crc32fast = "1.5"
[features]
default = ["deflate", "jpeg", "lzw"]
default = ["deflate", "fax", "jpeg", "lzw"]
# Compression algorithms
deflate = ["dep:flate2"]
fax = ["dep:fax34"]
jpeg = ["dep:zune-jpeg"]
lzw = ["dep:weezl"]
zstd = ["dep:zstd"]
+1
View File
@@ -36,6 +36,7 @@ This table lists photometric interpretations and sample formats which are suppor
| LZW | ✓ | ✓ |
| Deflate | ✓ | ✓ |
| PackBits | ✓ | ✓ |
| Fax4 | ✓ | not yet |
| JPEG | ✓ | not yet |
| ZSTD | ✓ | not yet |
+103
View File
@@ -0,0 +1,103 @@
use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingBuffer, DecodingResult};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let Some(image) = std::env::args_os().nth(1) else {
eprintln!("Usage: decode FILE");
return Ok(());
};
let file = std::fs::File::open(image)?;
let io = std::io::BufReader::new(file);
let mut reader = Decoder::new(io)?;
let mut data = DecodingResult::I8(vec![]);
for i in 0u32.. {
let colortype = reader.colortype()?;
let dimensions = reader.dimensions()?;
let layout = reader.read_image_to_buffer(&mut data)?;
debug_planes(i, &mut data, &layout, &colortype, dimensions);
if !reader.more_images() {
break;
}
reader.next_image()?
}
Ok(())
}
fn debug_planes(
index: u32,
data: &mut DecodingResult,
layout: &BufferLayoutPreference,
colortype: &tiff::ColorType,
(width, height): (u32, u32),
) {
let (depth, mut tupltype) = match colortype {
tiff::ColorType::Gray(_) => (1, "GRAYSCALE"),
tiff::ColorType::RGB(_) => (3, "RGB"),
tiff::ColorType::RGBA(_) => (4, "RGB_ALPHA"),
tiff::ColorType::Palette(_) => (1, "PALETTE"),
tiff::ColorType::CMYK(_) => (4, "CMYK"),
tiff::ColorType::Multiband { num_samples: 2, .. } => (2, "GRAYSCALE_ALPHA"),
_ => {
eprintln!("Unsupported color type for PAM output: {:?}", colortype);
return;
}
};
let maxval = match data.as_buffer(0) {
DecodingBuffer::U8(_) => u64::from(u8::MAX),
DecodingBuffer::U16(_) => u64::from(u16::MAX),
DecodingBuffer::U32(_) => u64::from(u32::MAX),
DecodingBuffer::U64(_) => u64::MAX,
// Eh, this is a good effort I guess.
DecodingBuffer::I8(items) => {
items.iter_mut().for_each(|v| *v = 0.max(*v));
u64::from(i8::MAX as u8)
}
DecodingBuffer::I16(items) => {
items.iter_mut().for_each(|v| *v = 0.max(*v));
u64::from(i16::MAX as u16)
}
DecodingBuffer::I32(items) => {
items.iter_mut().for_each(|v| *v = 0.max(*v));
u64::from(i32::MAX as u32)
}
DecodingBuffer::I64(items) => {
items.iter_mut().for_each(|v| *v = 0.max(*v));
i64::MAX as u64
}
// Not supported by PAM.
DecodingBuffer::F16(_) | DecodingBuffer::F32(_) | DecodingBuffer::F64(_) => return,
};
let byte_len = if layout.planes > 1 {
tupltype = "GRAYSCALE";
layout.plane_stride.unwrap().get()
} else {
data.as_buffer(0).byte_len()
};
// Write out this image's planes to PAM files.
let header = format!("P7\nWIDTH {width}\nHEIGHT {height}\nDEPTH {depth}\nMAXVAL {maxval}\nTUPLTYPE {tupltype}\nENDHDR\n");
for (plane, plane_data) in data
.as_buffer(0)
.as_bytes_mut()
.chunks_exact(byte_len)
.enumerate()
{
let filename = format!("image_{index:03}_plane_{plane:03}.pam");
let mut file = std::fs::File::create(filename).expect("Failed to create output file");
use std::io::Write;
file.write_all(header.as_bytes())
.expect("Failed to write PAM header");
file.write_all(plane_data)
.expect("Failed to write PAM data");
}
}
+248 -128
View File
@@ -10,8 +10,8 @@ use crate::tags::{IfdPointer, Tag, Type};
use crate::{TiffError, TiffFormatError, TiffResult};
use self::Value::{
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, RationalBig, SRational, SRationalBig,
Short, Signed, SignedBig, SignedByte, SignedShort, Unsigned, UnsignedBig,
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, SRational, Short, Signed, SignedBig,
SignedByte, SignedShort, Unsigned, UnsignedBig,
};
#[allow(unused_qualifications)]
@@ -30,8 +30,16 @@ pub enum Value {
Double(f64),
List(Vec<Value>),
Rational(u32, u32),
#[deprecated(
note = "Not implemented in BigTIFF with a standard tag value",
since = "0.11.1"
)]
RationalBig(u64, u64),
SRational(i32, i32),
#[deprecated(
note = "Not implemented in BigTIFF with a standard tag value",
since = "0.11.1"
)]
SRationalBig(i64, i64),
Ascii(String),
Ifd(u32),
@@ -170,7 +178,8 @@ impl Value {
Unsigned(val) => Ok(vec![val]),
UnsignedBig(val) => Ok(vec![u32::try_from(val)?]),
Rational(numerator, denominator) => Ok(vec![numerator, denominator]),
RationalBig(numerator, denominator) => {
#[expect(deprecated)]
Value::RationalBig(numerator, denominator) => {
Ok(vec![u32::try_from(numerator)?, u32::try_from(denominator)?])
}
Ifd(val) => Ok(vec![val]),
@@ -220,7 +229,8 @@ impl Value {
new_vec.push(numerator);
new_vec.push(denominator);
}
SRationalBig(numerator, denominator) => {
#[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => {
new_vec.push(i32::try_from(numerator)?);
new_vec.push(i32::try_from(denominator)?);
}
@@ -234,7 +244,8 @@ impl Value {
Signed(val) => Ok(vec![val]),
SignedBig(val) => Ok(vec![i32::try_from(val)?]),
SRational(numerator, denominator) => Ok(vec![numerator, denominator]),
SRationalBig(numerator, denominator) => {
#[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => {
Ok(vec![i32::try_from(numerator)?, i32::try_from(denominator)?])
}
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
@@ -283,7 +294,8 @@ impl Value {
Unsigned(val) => Ok(vec![val.into()]),
UnsignedBig(val) => Ok(vec![val]),
Rational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
#[expect(deprecated)]
Value::RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
Ifd(val) => Ok(vec![val.into()]),
IfdBig(val) => Ok(vec![val]),
Ascii(val) => Ok(val.chars().map(u32::from).map(u64::from).collect()),
@@ -301,7 +313,8 @@ impl Value {
new_vec.push(numerator.into());
new_vec.push(denominator.into());
}
SRationalBig(numerator, denominator) => {
#[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => {
new_vec.push(numerator);
new_vec.push(denominator);
}
@@ -315,7 +328,8 @@ impl Value {
Signed(val) => Ok(vec![val.into()]),
SignedBig(val) => Ok(vec![val]),
SRational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
#[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
@@ -332,6 +346,12 @@ impl Value {
}
}
/// A combination of type, count, and offset.
///
/// In a TIFF the data offset portion of an entry is used for inline data in case the length of the
/// encoded value does not exceed the size of the offset field. Since the size of the offset field
/// depends on the file kind (4 bytes for standard TIFF, 8 bytes for BigTIFF) the interpretation of
/// this struct is only complete in combination with file metadata.
#[derive(Clone)]
pub struct Entry {
type_: Type,
@@ -349,12 +369,14 @@ impl ::std::fmt::Debug for Entry {
}
impl Entry {
/// Create a new entry fit to be added to a standard TIFF IFD.
pub fn new(type_: Type, count: u32, offset: [u8; 4]) -> Entry {
let mut entry_off = [0u8; 8];
entry_off[..4].copy_from_slice(&offset);
Entry::new_u64(type_, count.into(), entry_off)
}
/// Create a new entry with data for a Big TIFF IFD.
pub fn new_u64(type_: Type, count: u64, offset: [u8; 8]) -> Entry {
Entry {
type_,
@@ -376,7 +398,10 @@ impl Entry {
}
/// Returns a mem_reader for the offset/value field
fn r(&self, byte_order: ByteOrder) -> EndianReader<io::Cursor<Vec<u8>>> {
pub(crate) fn offset_field_reader(
&self,
byte_order: ByteOrder,
) -> EndianReader<io::Cursor<Vec<u8>>> {
EndianReader::new(io::Cursor::new(self.offset.to_vec()), byte_order)
}
@@ -392,43 +417,25 @@ impl Entry {
}
let bo = reader.byte_order;
let tag_size = match self.type_ {
Type::BYTE | Type::SBYTE | Type::ASCII | Type::UNDEFINED => 1,
Type::SHORT | Type::SSHORT => 2,
Type::LONG | Type::SLONG | Type::FLOAT | Type::IFD => 4,
Type::LONG8
| Type::SLONG8
| Type::DOUBLE
| Type::RATIONAL
| Type::SRATIONAL
| Type::IFD8 => 8,
};
let value_bytes = match self.count.checked_mul(tag_size) {
Some(n) => n,
None => {
return Err(TiffError::LimitsExceeded);
}
};
let value_bytes = self.type_.value_bytes(self.count)?;
// Case 2: there is one value.
if self.count == 1 {
// 2a: the value is 5-8 bytes and we're in BigTiff mode.
if bigtiff && value_bytes > 4 && value_bytes <= 8 {
return Ok(match self.type_ {
Type::LONG8 => UnsignedBig(self.r(bo).read_u64()?),
Type::SLONG8 => SignedBig(self.r(bo).read_i64()?),
Type::DOUBLE => Double(self.r(bo).read_f64()?),
Type::LONG8 => UnsignedBig(self.offset_field_reader(bo).read_u64()?),
Type::SLONG8 => SignedBig(self.offset_field_reader(bo).read_i64()?),
Type::DOUBLE => Double(self.offset_field_reader(bo).read_f64()?),
Type::RATIONAL => {
let mut r = self.r(bo);
let mut r = self.offset_field_reader(bo);
Rational(r.read_u32()?, r.read_u32()?)
}
Type::SRATIONAL => {
let mut r = self.r(bo);
let mut r = self.offset_field_reader(bo);
SRational(r.read_i32()?, r.read_i32()?)
}
Type::IFD8 => IfdBig(self.r(bo).read_u64()?),
Type::IFD8 => IfdBig(self.offset_field_reader(bo).read_u64()?),
Type::BYTE
| Type::SBYTE
| Type::ASCII
@@ -447,11 +454,11 @@ impl Entry {
Type::BYTE => Byte(self.offset[0]),
Type::SBYTE => SignedByte(self.offset[0] as i8),
Type::UNDEFINED => Byte(self.offset[0]),
Type::SHORT => Short(self.r(bo).read_u16()?),
Type::SSHORT => SignedShort(self.r(bo).read_i16()?),
Type::LONG => Unsigned(self.r(bo).read_u32()?),
Type::SLONG => Signed(self.r(bo).read_i32()?),
Type::FLOAT => Float(self.r(bo).read_f32()?),
Type::SHORT => Short(self.offset_field_reader(bo).read_u16()?),
Type::SSHORT => SignedShort(self.offset_field_reader(bo).read_i16()?),
Type::LONG => Unsigned(self.offset_field_reader(bo).read_u32()?),
Type::SLONG => Signed(self.offset_field_reader(bo).read_i32()?),
Type::FLOAT => Float(self.offset_field_reader(bo).read_f32()?),
Type::ASCII => {
if self.offset[0] == 0 {
Ascii("".to_string())
@@ -460,28 +467,28 @@ impl Entry {
}
}
Type::LONG8 => {
reader.goto_offset(self.r(bo).read_u32()?.into())?;
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
UnsignedBig(reader.read_u64()?)
}
Type::SLONG8 => {
reader.goto_offset(self.r(bo).read_u32()?.into())?;
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
SignedBig(reader.read_i64()?)
}
Type::DOUBLE => {
reader.goto_offset(self.r(bo).read_u32()?.into())?;
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
Double(reader.read_f64()?)
}
Type::RATIONAL => {
reader.goto_offset(self.r(bo).read_u32()?.into())?;
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
Rational(reader.read_u32()?, reader.read_u32()?)
}
Type::SRATIONAL => {
reader.goto_offset(self.r(bo).read_u32()?.into())?;
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
SRational(reader.read_i32()?, reader.read_i32()?)
}
Type::IFD => Ifd(self.r(bo).read_u32()?),
Type::IFD => Ifd(self.offset_field_reader(bo).read_u32()?),
Type::IFD8 => {
reader.goto_offset(self.r(bo).read_u32()?.into())?;
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
IfdBig(reader.read_u64()?)
}
});
@@ -512,7 +519,7 @@ impl Entry {
));
}
Type::SHORT => {
let mut r = self.r(bo);
let mut r = self.offset_field_reader(bo);
let mut v = Vec::new();
for _ in 0..self.count {
v.push(Short(r.read_u16()?));
@@ -520,7 +527,7 @@ impl Entry {
return Ok(List(v));
}
Type::SSHORT => {
let mut r = self.r(bo);
let mut r = self.offset_field_reader(bo);
let mut v = Vec::new();
for _ in 0..self.count {
v.push(SignedShort(r.read_i16()?));
@@ -528,7 +535,7 @@ impl Entry {
return Ok(List(v));
}
Type::LONG => {
let mut r = self.r(bo);
let mut r = self.offset_field_reader(bo);
let mut v = Vec::new();
for _ in 0..self.count {
v.push(Unsigned(r.read_u32()?));
@@ -536,7 +543,7 @@ impl Entry {
return Ok(List(v));
}
Type::SLONG => {
let mut r = self.r(bo);
let mut r = self.offset_field_reader(bo);
let mut v = Vec::new();
for _ in 0..self.count {
v.push(Signed(r.read_i32()?));
@@ -544,7 +551,7 @@ impl Entry {
return Ok(List(v));
}
Type::FLOAT => {
let mut r = self.r(bo);
let mut r = self.offset_field_reader(bo);
let mut v = Vec::new();
for _ in 0..self.count {
v.push(Float(r.read_f32()?));
@@ -552,7 +559,7 @@ impl Entry {
return Ok(List(v));
}
Type::IFD => {
let mut r = self.r(bo);
let mut r = self.offset_field_reader(bo);
let mut v = Vec::new();
for _ in 0..self.count {
v.push(Ifd(r.read_u32()?));
@@ -571,74 +578,149 @@ impl Entry {
}
// Case 4: there is more than one value, and it doesn't fit in the offset field.
let mut v;
self.set_reader_offset(bo, bigtiff, reader)?;
match self.type_ {
// TODO check if this could give wrong results
// at a different endianess of file/computer.
Type::BYTE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
let mut buf = [0; 1];
reader.inner().read_exact(&mut buf)?;
Ok(UnsignedBig(u64::from(buf[0])))
}),
Type::SBYTE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(SignedBig(i64::from(reader.read_i8()?)))
}),
Type::SHORT => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(Short(reader.read_u16()?))
}),
Type::SSHORT => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(SignedShort(reader.read_i16()?))
}),
Type::LONG => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(Unsigned(reader.read_u32()?))
}),
Type::SLONG => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(Signed(reader.read_i32()?))
}),
Type::FLOAT => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(Float(reader.read_f32()?))
}),
Type::DOUBLE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(Double(reader.read_f64()?))
}),
Type::BYTE | Type::UNDEFINED => {
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(bytes.iter().copied().map(Byte))
})
}
Type::SBYTE => {
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(bytes.iter().copied().map(|v| SignedByte(v as i8)))
})
}
Type::SHORT => {
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(
bytes
.chunks_exact(2)
.map(|ch| Short(u16::from_ne_bytes(ch.try_into().unwrap()))),
)
})
}
Type::SSHORT => {
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(
bytes
.chunks_exact(2)
.map(|ch| SignedShort(i16::from_ne_bytes(ch.try_into().unwrap()))),
)
})
}
Type::LONG => {
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(
bytes
.chunks_exact(4)
.map(|ch| Unsigned(u32::from_ne_bytes(ch.try_into().unwrap()))),
)
})
}
Type::SLONG => {
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(
bytes
.chunks_exact(4)
.map(|ch| Signed(i32::from_ne_bytes(ch.try_into().unwrap()))),
)
})
}
Type::FLOAT => {
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(
bytes
.chunks_exact(4)
.map(|ch| Float(f32::from_ne_bytes(ch.try_into().unwrap()))),
)
})
}
Type::DOUBLE => {
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(
bytes
.chunks_exact(8)
.map(|ch| Double(f64::from_ne_bytes(ch.try_into().unwrap()))),
)
})
}
Type::RATIONAL => {
self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(Rational(reader.read_u32()?, reader.read_u32()?))
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(bytes.chunks_exact(8).map(|ch| {
Rational(
u32::from_ne_bytes(ch[..4].try_into().unwrap()),
u32::from_ne_bytes(ch[4..].try_into().unwrap()),
)
}))
})
}
Type::SRATIONAL => {
self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(SRational(reader.read_i32()?, reader.read_i32()?))
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(bytes.chunks_exact(8).map(|ch| {
SRational(
i32::from_ne_bytes(ch[..4].try_into().unwrap()),
i32::from_ne_bytes(ch[4..].try_into().unwrap()),
)
}))
})
}
Type::LONG8 => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(UnsignedBig(reader.read_u64()?))
}),
Type::SLONG8 => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(SignedBig(reader.read_i64()?))
}),
Type::IFD => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(Ifd(reader.read_u32()?))
}),
Type::IFD8 => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(IfdBig(reader.read_u64()?))
}),
Type::UNDEFINED => {
self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
let mut buf = [0; 1];
reader.inner().read_exact(&mut buf)?;
Ok(Byte(buf[0]))
Type::LONG8 => {
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(
bytes
.chunks_exact(8)
.map(|ch| UnsignedBig(u64::from_ne_bytes(ch.try_into().unwrap()))),
)
})
}
Type::SLONG8 => {
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(
bytes
.chunks_exact(8)
.map(|ch| SignedBig(i64::from_ne_bytes(ch.try_into().unwrap()))),
)
})
}
Type::IFD => {
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(
bytes
.chunks_exact(4)
.map(|ch| Ifd(u32::from_ne_bytes(ch.try_into().unwrap()))),
)
})
}
Type::IFD8 => {
v = Self::vec_with_capacity(self.count, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(
bytes
.chunks_exact(8)
.map(|ch| IfdBig(u64::from_ne_bytes(ch.try_into().unwrap()))),
)
})
}
Type::ASCII => {
let n = usize::try_from(self.count)?;
if n > limits.decoding_buffer_size {
return Err(TiffError::LimitsExceeded);
}
if bigtiff {
reader.goto_offset(self.r(bo).read_u64()?)?
} else {
reader.goto_offset(self.r(bo).read_u32()?.into())?
if n > limits.decoding_buffer_size {
return Err(dbg!(TiffError::LimitsExceeded));
}
let mut out = vec![0; n];
@@ -647,43 +729,81 @@ impl Entry {
if let Some(first) = out.iter().position(|&b| b == 0) {
out.truncate(first);
}
Ok(Ascii(String::from_utf8(out)?))
return Ok(Ascii(String::from_utf8(out)?));
}
}
}?;
Ok(List(v))
}
#[inline]
fn decode_offset<R, F>(
&self,
fn vec_with_capacity(
value_count: u64,
bo: ByteOrder,
bigtiff: bool,
limits: &super::Limits,
reader: &mut EndianReader<R>,
decode_fn: F,
) -> TiffResult<Value>
where
R: Read + Seek,
F: Fn(&mut EndianReader<R>) -> TiffResult<Value>,
{
) -> Result<Vec<Value>, TiffError> {
let value_count = usize::try_from(value_count)?;
if value_count > limits.decoding_buffer_size / mem::size_of::<Value>() {
return Err(TiffError::LimitsExceeded);
}
let mut v = Vec::with_capacity(value_count);
Ok(Vec::with_capacity(value_count))
}
fn set_reader_offset<R>(
&self,
bo: ByteOrder,
bigtiff: bool,
reader: &mut EndianReader<R>,
) -> TiffResult<()>
where
R: Read + Seek,
{
let offset = if bigtiff {
self.r(bo).read_u64()?
self.offset_field_reader(bo).read_u64()?
} else {
self.r(bo).read_u32()?.into()
self.offset_field_reader(bo).read_u32()?.into()
};
reader.goto_offset(offset)?;
for _ in 0..value_count {
v.push(decode_fn(reader)?)
Ok(())
}
#[inline]
fn decode_values<R, F>(
&self,
value_count: u64,
type_: Type,
reader: &mut EndianReader<R>,
mut collect: F,
) -> TiffResult<()>
where
R: Read + Seek,
F: FnMut(&[u8]),
{
let mut total_bytes = type_.value_bytes(value_count)?;
let mut buffer = [0u8; 512];
let buf_unit = usize::from(type_.byte_len());
let mul_of_ty = buffer.len() / buf_unit * buf_unit;
let cls = type_.endian_bytes();
let native = ByteOrder::native();
while total_bytes > 0 {
// `now <= mul_of_ty < 512` so casting is mathematical
let now = total_bytes.min(mul_of_ty as u64);
total_bytes -= now;
let buffer = &mut buffer[..now as usize];
reader.inner().read_exact(buffer)?;
reader.byte_order.convert_endian_bytes(cls, buffer, native);
collect(buffer);
}
Ok(List(v))
Ok(())
}
}
+510 -71
View File
@@ -4,13 +4,14 @@ use super::tag_reader::TagReader;
use super::ChunkType;
use super::{predict_f16, predict_f32, predict_f64, ValueReader};
use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag,
CompressionMethod, ExtraSamples, PhotometricInterpretation, PlanarConfiguration, Predictor,
SampleFormat, Tag,
};
use crate::{
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
};
use std::io::{self, Cursor, Read, Seek};
use std::num::NonZeroUsize;
use std::sync::Arc;
#[derive(Debug)]
@@ -68,6 +69,11 @@ pub(crate) struct Image {
pub height: u32,
pub bits_per_sample: u8,
pub samples: u16,
/// The `ExtraSamples`, defaulting to empty if not given.
pub extra_samples: Vec<ExtraSamples>,
/// Number of samples that belong to the photometric interpretation, samples except
/// `ExtraSamples` (338, 0x0152) tag.
pub photometric_samples: u16,
pub sample_format: SampleFormat,
pub photometric_interpretation: PhotometricInterpretation,
pub compression_method: CompressionMethod,
@@ -79,6 +85,58 @@ pub(crate) struct Image {
pub tile_attributes: Option<TileAttributes>,
pub chunk_offsets: Vec<u64>,
pub chunk_bytes: Vec<u64>,
pub chroma_subsampling: (u16, u16),
}
/// Describes how to read a tile-aligned portion of the image.
#[derive(Clone)]
pub(crate) struct ReadoutLayout {
/// The planar configuration, which applies to both the underlying image and the output buffer.
/// This may be relaxed if we find a clean enough way to provide it.
pub planar_config: PlanarConfiguration,
/// The sample interpretation (interpret with planar_config).
///
/// FIXME: we should not require this here. The ability to turn out the raw bytes from the
/// sample arrays is very different from turning out interpretable color. Firstly we can always
/// readout `Multiband` but currently only use that ColorType in special circumstances (it must
/// not overlap cases where actually want to use a ColorType).
///
/// And then we have CIE Lab, which uses a tuple of `(u8, i8, i8)`, that is still filterable
/// but still not represented by any of our `DecoderResult` variants. Other color variants
/// depend on extra tags (YCbCrCoefficients/0x0211) and we don't have a good side channel to
/// tag the output with all that TIFF specific information, so arguably we should process and
/// apply those to the data so it becomes a self-contained representation.
///
/// This should be computed at a higher level, in `Decoder`, instead.
pub color: ColorType,
/// The number of bytes from one row to another.
pub minimum_row_stride: usize,
/// The format of samples (assumed uniform for now, same with depth of `ColorType`).
pub sample_format: SampleFormat,
/// Number of bytes to advance in output per row.
pub row_stride: usize,
/// Number of bytes to advance in output per chunk in width.
pub chunk_row_stride: usize,
/// Number of bytes to advance in output per chunk in height.
pub chunk_col_stride: usize,
/// Number of bytes in output from one plane to another.
pub plane_stride: usize,
/// Bits per sample in the encoded data.
pub tiff_bits_per_sample: u8,
/// Number of samples in the encoded data.
pub tiff_samples: u16,
/// Dimensions of the underlying rectangular chunks (tile or strips).
pub tiff_chunk_dimensions: (u32, u32),
/// Number of bytes in the underlying data with all samples per row of chunks.
pub tiff_row_bytes: usize,
/// Chunks until wrapping to the next row of chunks.
pub chunks_across: u32,
/// Chunks to advance to get to the next plane of chunks.
pub chunks_per_plane: u32,
}
impl Image {
@@ -133,10 +191,30 @@ impl Image {
.map(Value::into_u16)
.transpose()?
.unwrap_or(1);
if samples == 0 {
return Err(TiffFormatError::SamplesPerPixelIsZero.into());
}
let extra_samples = match tag_reader.find_tag(Tag::ExtraSamples)? {
Some(n) => n.into_u16_vec()?,
None => vec![],
};
let extra_samples = extra_samples
.into_iter()
.map(|x| ExtraSamples::from_u16(x).unwrap_or(ExtraSamples::Unspecified))
.collect::<Vec<_>>();
let photometric_samples = match usize::from(samples).checked_sub(extra_samples.len()) {
None => {
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
));
}
Some(n) => n as u16,
};
let sample_format = match tag_reader.find_tag_uint_vec(Tag::SampleFormat)? {
Some(vals) => {
let sample_format: Vec<_> = vals
@@ -144,12 +222,15 @@ impl Image {
.map(SampleFormat::from_u16_exhaustive)
.collect();
let Some(format) = sample_format.first().copied() else {
// Reject empty sample formats
return Err(TiffFormatError::InvalidTagValueType(Tag::SampleFormat).into());
};
// TODO: for now, only homogenous formats across samples are supported.
if !sample_format.windows(2).all(|s| s[0] == s[1]) {
if !sample_format.iter().all(|&s| s == format) {
return Err(TiffUnsupportedError::UnsupportedSampleFormat(sample_format).into());
}
sample_format[0]
format
}
None => SampleFormat::Uint,
};
@@ -160,7 +241,7 @@ impl Image {
// Technically bits_per_sample.len() should be *equal* to samples, but libtiff also allows
// it to be a single value that applies to all samples.
if bits_per_sample.len() != samples.into() && bits_per_sample.len() != 1 {
if bits_per_sample.len() != usize::from(samples) && bits_per_sample.len() != 1 {
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
));
@@ -195,6 +276,25 @@ impl Image {
.transpose()?
.unwrap_or(PlanarConfiguration::Chunky);
let ycbcr_subsampling = tag_reader.find_tag_uint_vec::<u16>(Tag::ChromaSubsampling)?;
let chroma_subsampling = if let Some(subsamples) = &ycbcr_subsampling {
let [a, b] = subsamples.as_slice() else {
return Err(TiffError::FormatError(TiffFormatError::InvalidCountForTag(
Tag::ChromaSubsampling,
subsamples.len(),
)));
};
// ImageWidth and ImageLength are constrained to be integer multiples of
// YCbCrSubsampleHoriz and YCbCrSubsampleVert respectively. TileWidth and TileLength
// have the same constraints. RowsPerStrip must be an integer multiple of
// YCbCrSubsampleVert.
(*a, *b)
} else {
(2, 2)
};
let planes = match planar_config {
PlanarConfiguration::Chunky => 1,
PlanarConfiguration::Planar => samples,
@@ -293,6 +393,8 @@ impl Image {
height,
bits_per_sample: bits_per_sample[0],
samples,
extra_samples,
photometric_samples,
sample_format,
photometric_interpretation,
compression_method,
@@ -304,19 +406,24 @@ impl Image {
tile_attributes,
chunk_offsets,
chunk_bytes,
chroma_subsampling,
})
}
pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
let is_alpha_extra_samples = matches!(
self.extra_samples.as_slice(),
[ExtraSamples::AssociatedAlpha, ..] | [ExtraSamples::UnassociatedAlpha, ..]
);
match self.photometric_interpretation {
PhotometricInterpretation::RGB => match self.samples {
3 => Ok(ColorType::RGB(self.bits_per_sample)),
PhotometricInterpretation::RGB => match self.photometric_samples {
3 => Ok(if is_alpha_extra_samples {
ColorType::RGBA(self.bits_per_sample)
} else {
ColorType::RGB(self.bits_per_sample)
}),
4 => Ok(ColorType::RGBA(self.bits_per_sample)),
// FIXME: We should _ignore_ other components. In particular:
// > Beware of extra components. Some TIFF files may have more components per pixel
// than you think. A Baseline TIFF reader must skip over them gracefully,using the
// values of the SamplesPerPixel and BitsPerSample fields.
// > -- TIFF 6.0 Specification, Section 7, Additional Baseline requirements.
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
@@ -324,8 +431,12 @@ impl Image {
),
)),
},
PhotometricInterpretation::CMYK => match self.samples {
4 => Ok(ColorType::CMYK(self.bits_per_sample)),
PhotometricInterpretation::CMYK => match self.photometric_samples {
4 => Ok(if is_alpha_extra_samples {
ColorType::CMYKA(self.bits_per_sample)
} else {
ColorType::CMYK(self.bits_per_sample)
}),
5 => Ok(ColorType::CMYKA(self.bits_per_sample)),
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
@@ -334,7 +445,7 @@ impl Image {
),
)),
},
PhotometricInterpretation::YCbCr => match self.samples {
PhotometricInterpretation::YCbCr => match self.photometric_samples {
3 => Ok(ColorType::YCbCr(self.bits_per_sample)),
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
@@ -343,7 +454,13 @@ impl Image {
),
)),
},
// TODO: treatment of WhiteIsZero is not quite consistent with `invert_colors` that is
// later called when that interpretation is read. That function does not support
// Multiband as a color type and will error. It's unclear how to resolve that exactly.
PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero => {
// Note: compatibility with previous implementation requires us to return extra
// samples as `Multiband`. For gray images however the better choice would be
// returning a `Gray` color, i.e. matching on `photometric_samples` instead.
match self.samples {
1 => Ok(ColorType::Gray(self.bits_per_sample)),
_ => Ok(ColorType::Multiband {
@@ -352,39 +469,70 @@ impl Image {
}),
}
}
// TODO: this is bad we should not fail at this point
PhotometricInterpretation::RGBPalette
| PhotometricInterpretation::TransparencyMask
| PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
// ```
// struct IccLab /* Interpretation 9* {
// pub L: u8, // SampleFormat::Uint
// pub a: u8, // SampleFormat::Uint, defined as TiffLab::a + 128
// pub b: u8, // SampleFormat::Uint, defined as TiffLab::b + 128
// }
// ```
PhotometricInterpretation::IccLab => match self.photometric_samples {
3 if matches!(self.sample_format, SampleFormat::Uint) => {
Ok(ColorType::Lab(self.bits_per_sample))
}
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
vec![self.bits_per_sample; self.samples as usize],
),
)),
},
// Unsupported due to inherently heterogeneous sample types. This is represented as:
// ```
// struct TiffLab /* Interpretation 8* {
// pub L: u8, // SampleFormat::Uint
// pub a: i8, // SampleFormat::Int
// pub b: i8, // SampleFormat::Int
// }
// ```
PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
PhotometricInterpretation::CIELab,
vec![self.bits_per_sample; self.samples as usize],
),
)),
// Unsupported due to extra unfiltering and conversion steps. We need to find the
// Decode tag (SRATIONAL; 2 * SamplesPerPixel) and apply the following conversion:
//
// L* = Decode[0] + Lsample x (Decode[1] - Decode[0]) / (2^n -1)
// …
//
// So we'll have a larger depth in the output and either worry about reducing fractions
// or turn everything into floats. That's a lot of decisions.
PhotometricInterpretation::ItuLab => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
PhotometricInterpretation::CIELab,
vec![self.bits_per_sample; self.samples as usize],
),
)),
PhotometricInterpretation::RGBPalette | PhotometricInterpretation::TransparencyMask => {
Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
vec![self.bits_per_sample; self.samples as usize],
),
))
}
}
}
pub(crate) fn minimum_row_stride(&self, dims: (u32, u32)) -> Option<NonZeroUsize> {
let (width, height) = dims;
let row_stride = u64::from(width)
.saturating_mul(self.samples_per_pixel() as u64)
.saturating_mul(self.bits_per_sample as u64)
.div_ceil(8);
// Note: row stride should be smaller than the len if we have an actual buffer. If there
// are no pixels in the buffer (height _or_ width is 0) then the stride is not well defined
// and we return `None`.
(height > 0)
.then_some(row_stride as usize)
.and_then(NonZeroUsize::new)
}
fn create_reader<'r, R: 'r + Read>(
reader: R,
compression_method: CompressionMethod,
compressed_length: u64,
#[allow(unused_variables)] jpeg_tables: Option<&[u8]>,
// FIXME: these should be `expect` attributes or we choose another way of passing them.
#[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>,
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] dimensions: (u32, u32),
) -> TiffResult<Box<dyn Read + 'r>> {
Ok(match compression_method {
CompressionMethod::None => Box::new(reader),
@@ -436,13 +584,14 @@ impl Image {
let mut jpeg_data = Vec::new();
jpeg_reader.read_to_end(&mut jpeg_data)?;
let mut decoder = zune_jpeg::JpegDecoder::new(jpeg_data);
let mut decoder =
zune_jpeg::JpegDecoder::new(zune_core::bytestream::ZCursor::new(jpeg_data));
let mut options: zune_core::options::DecoderOptions = Default::default();
// Disable color conversion by setting the output colorspace to the input
// colorspace.
decoder.decode_headers()?;
if let Some(colorspace) = decoder.get_input_colorspace() {
if let Some(colorspace) = decoder.input_colorspace() {
options = options.jpeg_set_out_colorspace(colorspace);
}
@@ -452,6 +601,12 @@ impl Image {
Box::new(Cursor::new(data))
}
#[cfg(feature = "fax")]
CompressionMethod::Fax4 => Box::new(super::stream::Group4Reader::new(
dimensions,
reader,
compressed_length,
)?),
method => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedCompressionMethod(method),
@@ -467,18 +622,25 @@ impl Image {
/// Example with `bits_per_sample = [8, 8, 8]` and `PhotometricInterpretation::RGB`:
/// * `PlanarConfiguration::Chunky` -> 3 (RGBRGBRGB...)
/// * `PlanarConfiguration::Planar` -> 1 (RRR...) (GGG...) (BBB...)
pub(crate) fn samples_per_pixel(&self) -> usize {
pub(crate) fn samples_per_pixel(&self) -> u16 {
match self.planar_config {
PlanarConfiguration::Chunky => self.samples.into(),
PlanarConfiguration::Chunky => self.samples,
PlanarConfiguration::Planar => 1,
}
}
pub(crate) fn samples_per_out_texel(&self, color: ColorType) -> u16 {
match self.planar_config {
PlanarConfiguration::Chunky => color.num_samples(),
PlanarConfiguration::Planar => 1,
}
}
/// Number of strips per pixel.
pub(crate) fn strips_per_pixel(&self) -> usize {
pub(crate) fn strips_per_pixel(&self) -> u16 {
match self.planar_config {
PlanarConfiguration::Chunky => 1,
PlanarConfiguration::Planar => self.samples.into(),
PlanarConfiguration::Planar => self.samples,
}
}
@@ -516,14 +678,107 @@ impl Image {
}
}
pub(crate) fn readout_for_image(&self) -> TiffResult<ReadoutLayout> {
let Image { width, height, .. } = *self;
self.readout_for_size(width, height)
}
/// Get the layout for reading out a tile-aligned portion of the image.
///
/// The provided width and height should be less than or equal to the image dimensions.
pub(crate) fn readout_for_size(&self, width: u32, height: u32) -> TiffResult<ReadoutLayout> {
let color = self.colortype()?;
let tiff_samples = self.samples_per_pixel();
let tiff_bits_per_sample = self.bits_per_sample;
let data_samples = self.samples_per_out_texel(color);
let tiff_chunk_dimensions = self.chunk_dimensions()?;
let strips_per_pixel = self.strips_per_pixel();
let data_dimensions = (width, height);
let tiff_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample))
.checked_mul(u64::from(tiff_samples))
.ok_or(TiffError::LimitsExceeded)?;
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))
.checked_mul(u64::from(data_samples))
.ok_or(TiffError::LimitsExceeded)?;
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))
.checked_mul(u64::from(data_samples))
.ok_or(TiffError::LimitsExceeded)?;
let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?;
let chunk_col_stride: usize = data_row_bits
.div_ceil(8)
.checked_mul(u64::from(tiff_chunk_dimensions.1))
.ok_or(TiffError::LimitsExceeded)?
.try_into()?;
let plane_stride: usize = data_row_bits
.div_ceil(8)
.checked_mul(u64::from(data_dimensions.1))
.ok_or(TiffError::LimitsExceeded)?
.try_into()?;
let minimum_row_stride = data_row_bytes;
let chunks_across: u32 = data_dimensions.0.div_ceil(tiff_chunk_dimensions.0);
let chunks_per_plane = (self.chunk_offsets.len() as u32) / u32::from(strips_per_pixel);
// We would not get an offset in byte units, sorry, no bit interleaving in the output.
if chunks_across > 1 && chunk_row_bits % 8 != 0 {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::MisalignedTileBoundaries,
));
}
// Only this color type interprets the tag, which is defined with a default of (2, 2)
if matches!(color, ColorType::YCbCr(_)) && self.chroma_subsampling != (1, 1) {
// The JPEG library does upsampling for us and defines its buffers correctly
// (presumably). All other compression schemes are not supported..
//
// NOTE: as explained in <fa225e820b96bef35f01bf4685654beeb4a8df0c> we may be better
// off supporting this tag by consistently upsampling, not by adjusting the buffer
// size. At least as a default this makes more sense and is much more permissive in
// case the compression stream disagrees with the tags (we would not have enough / or
// the wrong buffer layout if we only asked for subsampled planes in a planar layout).
if !matches!(self.compression_method, CompressionMethod::ModernJPEG) {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::ChromaSubsampling,
));
}
}
Ok(ReadoutLayout {
planar_config: self.planar_config,
color,
minimum_row_stride,
sample_format: self.sample_format,
row_stride: data_row_bytes,
chunk_row_stride: chunk_row_bytes,
chunk_col_stride,
plane_stride,
tiff_bits_per_sample,
tiff_samples,
tiff_chunk_dimensions,
tiff_row_bytes,
chunks_across,
chunks_per_plane,
})
}
pub(crate) fn chunk_data_dimensions(&self, chunk_index: u32) -> TiffResult<(u32, u32)> {
let dims = self.chunk_dimensions()?;
match self.chunk_type {
ChunkType::Strip => {
let strip_attrs = self.strip_decoder.as_ref().unwrap();
let strips_per_band =
self.height.saturating_sub(1) / strip_attrs.rows_per_strip + 1;
let rows_per_strip = dims.1;
let strips_per_band = self.height.div_ceil(rows_per_strip);
let strip_height_without_padding = (chunk_index % strips_per_band)
.checked_mul(dims.1)
.and_then(|x| self.height.checked_sub(x))
@@ -552,7 +807,7 @@ impl Image {
&self,
reader: &mut ValueReader<impl Read>,
buf: &mut [u8],
output_row_stride: usize,
layout: &ReadoutLayout,
chunk_index: u32,
) -> TiffResult<()> {
let ValueReader {
@@ -562,10 +817,10 @@ impl Image {
} = reader;
let byte_order = reader.byte_order;
let reader = reader.inner();
// Validate that the color type is supported.
let color_type = self.colortype()?;
let color_type = layout.color;
match color_type {
ColorType::RGB(n)
| ColorType::RGBA(n)
@@ -627,6 +882,7 @@ impl Image {
.ok_or(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
))?;
if *compressed_bytes > limits.intermediate_buffer_size as u64 {
return Err(TiffError::LimitsExceeded);
}
@@ -634,33 +890,39 @@ impl Image {
let compression_method = self.compression_method;
let photometric_interpretation = self.photometric_interpretation;
let predictor = self.predictor;
let samples = self.samples_per_pixel();
let samples = layout.tiff_samples;
let data_samples = layout.samples_per_out_texel();
// We have two dimensions: the 2d rectangle of encoded data and the 2d rectangle this
// takes up in the output. Each has an associated count of bits per pixel. The first
// dimension, i.e. a ''row'', is the number of pixels that are encoded with bit packing
// while the second is the byte-padded array of each so encoded slices.
//
// During decoding we map the relevant bits from one to the other.
let chunk_dims = self.chunk_dimensions()?;
let data_dims = self.chunk_data_dimensions(chunk_index)?;
let chunk_row_bits = (u64::from(chunk_dims.0) * u64::from(self.bits_per_sample))
.checked_mul(samples as u64)
.ok_or(TiffError::LimitsExceeded)?;
let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?;
let data_row_bits = (u64::from(data_dims.0) * u64::from(self.bits_per_sample))
.checked_mul(samples as u64)
.ok_or(TiffError::LimitsExceeded)?;
let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?;
let chunk_row_bytes: usize = layout.tiff_row_bytes;
let data_row_bytes: usize = layout.chunk_row_bytes(data_dims.0)?;
// TODO: Should these return errors instead?
assert!(output_row_stride >= data_row_bytes);
assert!(buf.len() >= output_row_stride * (data_dims.1 as usize - 1) + data_row_bytes);
assert!(layout.minimum_row_stride >= data_row_bytes);
assert!(buf.len() >= layout.row_stride * (data_dims.1 as usize - 1) + data_row_bytes);
let is_all_bits = samples == data_samples;
let is_output_chunk_rows = layout.row_stride == chunk_row_bytes;
let mut reader = Self::create_reader(
reader,
reader.inner(),
compression_method,
*compressed_bytes,
self.jpeg_tables.as_deref().map(|a| &**a),
chunk_dims,
)?;
if output_row_stride == chunk_row_bytes {
if is_output_chunk_rows && is_all_bits {
// Here we can read directly into the output buffer itself.
let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize];
reader.read_exact(tile)?;
@@ -673,14 +935,15 @@ impl Image {
predictor,
);
}
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(tile, color_type, self.sample_format);
super::invert_colors(tile, color_type, self.sample_format)?;
}
} else if chunk_row_bytes > data_row_bytes && self.predictor == Predictor::FloatingPoint {
// The floating point predictor shuffles the padding bytes into the encoded output, so
// this case is handled specially when needed.
let mut encoded = vec![0u8; chunk_row_bytes];
for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) {
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
reader.read_exact(&mut encoded)?;
let row = &mut row[..data_row_bytes];
@@ -691,14 +954,19 @@ impl Image {
_ => unreachable!(),
}
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(row, color_type, self.sample_format);
super::invert_colors(row, color_type, self.sample_format)?;
}
}
} else {
for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) {
} else if is_all_bits {
// We read row-by-row but each row fits in its output buffer.
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
let row = &mut row[..data_row_bytes];
reader.read_exact(row)?;
let used = data_row_bytes.min(chunk_row_bytes);
// Two ways how we get here: we have more bytes in our chunk data than in the image
// we are to read. Then we need to skip the rest of the data. Or we have a bigger
// row stride than the chunk contains data, then we need to fill only the front.
reader.read_exact(&mut row[..used])?;
// Skip horizontal padding
if chunk_row_bytes > data_row_bytes {
let len = u64::try_from(chunk_row_bytes - data_row_bytes)?;
@@ -712,12 +980,183 @@ impl Image {
byte_order,
predictor,
);
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(row, color_type, self.sample_format);
super::invert_colors(row, color_type, self.sample_format)?;
}
}
} else {
// The encoded data potentially takes up more space than the output data so we must be
// prepared to discard some of it. That decision is bit-by-bit.
let bits_per_pixel = u32::from(self.bits_per_sample) * u32::from(self.samples);
// Assumes the photometric samples are always the start.. This is slightly problematic.
// To expand spport we should instead have different methods of transforming the read
// buffer data, not only the `compact_photometric_bytes` method below and then choose
// from the right one with supplied parameters. Then we can also bit-for-bit copy with
// a selection for better performance.
let photometric_bit_end = u32::from(self.bits_per_sample) * data_samples as u32;
debug_assert!(bits_per_pixel >= photometric_bit_end);
if bits_per_pixel % 8 != 0 || photometric_bit_end % 8 != 0 {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
vec![self.bits_per_sample; self.samples as usize],
),
));
}
let photo_range = photometric_bit_end / 8..bits_per_pixel / 8;
let mut encoded = vec![0u8; chunk_row_bytes];
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
reader.read_exact(&mut encoded)?;
Self::compact_photometric_bytes(&mut encoded, row, &photo_range);
super::fix_endianness_and_predict(
row,
color_type.bit_depth(),
samples,
byte_order,
predictor,
);
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(row, color_type, self.sample_format)?;
}
}
}
Ok(())
}
/// Turn a contiguous buffer of a whole number of raw sample arrays into a whole number of
/// photometric sample arrays by removing the extra samples in-between.
fn compact_photometric_bytes(
raw: &mut [u8],
row: &mut [u8],
photo_range: &std::ops::Range<u32>,
) {
raw.chunks_exact_mut(photo_range.end as usize)
.zip(row.chunks_exact_mut(photo_range.start as usize))
.for_each(|(src, dst)| {
dst.copy_from_slice(&src[..photo_range.start as usize]);
});
}
}
impl ReadoutLayout {
pub(crate) fn samples_per_out_texel(&self) -> u16 {
match self.planar_config {
PlanarConfiguration::Chunky => self.color.num_samples(),
PlanarConfiguration::Planar => 1,
}
}
// For a concrete chunk, which may be a partial border chunk, the byte length of one row of its
// pixel data.
pub(crate) fn chunk_row_bytes(&self, width: u32) -> TiffResult<usize> {
let data_samples = self.samples_per_out_texel();
let data_row_bits = (u64::from(width) * u64::from(self.tiff_bits_per_sample))
.checked_mul(u64::from(data_samples))
.ok_or(TiffError::LimitsExceeded)?;
Ok(data_row_bits.div_ceil(8).try_into()?)
}
pub(crate) fn set_row_stride(&mut self, row_stride: usize) -> Result<(), TiffError> {
if row_stride < self.minimum_row_stride {
return Err(TiffError::UsageError(
UsageError::InsufficientOutputRowStride {
needed: self.minimum_row_stride,
requested: row_stride,
},
));
}
let data_row_bytes = u64::try_from(row_stride)?;
let chunk_col_stride = data_row_bytes
.checked_mul(u64::from(self.tiff_chunk_dimensions.1))
.ok_or(TiffError::LimitsExceeded)?
.try_into()?;
let height = self.plane_stride.checked_div(self.row_stride);
let plane_stride = height
.and_then(|h| data_row_bytes.checked_mul(h as u64))
// If height was zero, or the previous stride was zero, there are no bytes in a plane
.unwrap_or(0)
.try_into()?;
self.row_stride = row_stride;
self.chunk_col_stride = chunk_col_stride;
self.plane_stride = plane_stride;
Ok(())
}
/// Reduce this down to the layout of the output planes.
pub(crate) fn to_plane_layout(&self) -> Result<PlaneLayout, TiffError> {
let num_planes = self.color.num_samples() / self.samples_per_out_texel();
// Using the standard range iterator as checked_add on steroids.
//
// Note: for supporting subsampling, adjust as required.
let mut offset = (0..=usize::MAX).step_by(self.plane_stride);
let plane_offsets = offset.by_ref().take(usize::from(num_planes)).collect();
// Get the past-the-end of the last plane.
//
// This also verifies the `take` above was not short.
let Some(total_bytes) = offset.next() else {
return Err(TiffError::LimitsExceeded);
};
Ok(PlaneLayout {
plane_offsets,
total_bytes,
readout: self.clone(),
})
}
}
/// A `ReadoutLayout` with pre-calculated plane information.
pub(crate) struct PlaneLayout {
/// The underlying readout layout.
pub readout: ReadoutLayout,
/// Buffer offset from one plane of output to the next.
pub plane_offsets: Vec<usize>,
/// Total number of bytes for all planes in given order.
pub total_bytes: usize,
}
impl PlaneLayout {
/// Return the number of planes to extract into the provided buffer.
pub(crate) fn used_planes(&self, buffer: &[impl Sized]) -> TiffResult<u16> {
self.readout.assert_min_layout(buffer)?;
let buffer_len = core::mem::size_of_val(buffer);
// Note: with differently sized planes this is dependent on the plane.
let last_plane_start = buffer_len.checked_sub(self.readout.plane_stride);
// Find how many planes fit into the output buffer.
let used_plane_offsets = self
.plane_offsets
.iter()
.enumerate()
// Find the first plane that would not fit completely at its offset.
.skip_while(|(_, &offset)| last_plane_start >= Some(offset))
.nth(0)
// If all planes fit, use all of them.
.map_or(self.plane_offsets.len(), |(idx, _)| idx);
debug_assert!(
used_plane_offsets <= usize::from(u16::MAX),
"Planes limited by number of samples, which is encoded as u16"
);
Ok(used_plane_offsets as u16)
}
}
+583 -297
View File
File diff suppressed because it is too large. Load diff
+101 -23
View File
@@ -1,15 +1,7 @@
//! All IO functionality needed for TIFF decoding
use std::io::{self, BufRead, BufReader, Read, Seek, Take};
/// Byte order of the TIFF file.
#[derive(Clone, Copy, Debug)]
pub enum ByteOrder {
/// little endian byte order
LittleEndian,
/// big endian byte order
BigEndian,
}
pub use crate::tags::ByteOrder;
/// Reader that is aware of the byte order.
#[derive(Debug)]
@@ -46,17 +38,6 @@ impl<R: Read> EndianReader<R> {
})
}
/// Reads an i8
#[inline(always)]
pub fn read_i8(&mut self) -> Result<i8, io::Error> {
let mut n = [0u8; 1];
self.reader.read_exact(&mut n)?;
Ok(match self.byte_order {
ByteOrder::LittleEndian => i8::from_le_bytes(n),
ByteOrder::BigEndian => i8::from_be_bytes(n),
})
}
/// Reads an i16
#[inline(always)]
pub fn read_i16(&mut self) -> Result<i16, io::Error> {
@@ -187,9 +168,6 @@ impl<R: Read> Read for LZWReader<R> {
}
}
Ok(weezl::LzwStatus::NoProgress) => {
assert_eq!(result.consumed_in, 0);
assert_eq!(result.consumed_out, 0);
assert!(self.reader.buffer().is_empty());
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"no lzw end code found",
@@ -276,6 +254,106 @@ impl<R: Read> Read for PackBitsReader<R> {
}
}
#[cfg(feature = "fax")]
pub struct Group4Reader<R: Read> {
decoder: fax34::decoder::Group4Decoder<io::Bytes<io::BufReader<io::Take<R>>>>,
line_buf: io::Cursor<Vec<u8>>,
height: u16,
width: u16,
y: u16,
}
#[cfg(feature = "fax")]
impl<R: Read> Group4Reader<R> {
pub fn new(
dimensions: (u32, u32),
reader: R,
compressed_length: u64,
) -> crate::TiffResult<Self> {
let width = u16::try_from(dimensions.0)?;
let height = u16::try_from(dimensions.1)?;
Ok(Self {
decoder: fax34::decoder::Group4Decoder::new(
io::BufReader::new(reader.take(compressed_length)).bytes(),
width,
)?,
line_buf: io::Cursor::new(Vec::with_capacity(width.into())),
width,
height,
y: 0,
})
}
}
#[cfg(feature = "fax")]
impl<R: Read> Read for Group4Reader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
// Either we have not read any line or we are at the end of a line.
if self.line_buf.position() as usize == self.line_buf.get_ref().len()
&& self.y < self.height
{
let next = self.decoder.advance().map_err(std::io::Error::other)?;
match next {
fax34::decoder::DecodeStatus::End => (),
fax34::decoder::DecodeStatus::Incomplete => {
self.y += 1;
// We known `transitions` yields exactly `self.width` items (per doc).
// FIXME: performance. We do not need an individual pixel iterator, filling
// memory especially for long runs can be much quicker. The `transitions` are
// the positions at which each run-length ends, i.e. the prefix sum of run
// lengths. Runs in fax4 start with white.
let transitions = fax34::decoder::pels(self.decoder.transition(), self.width);
let buffer = self.line_buf.get_mut();
buffer.resize(usize::from(self.width).div_ceil(8), 0u8);
let target = &mut buffer[..];
// Note: it may seem strange to treat black as 0b1 and white as 0b0 despite all
// our streams by default decoding as-if PhotometricInterpretation::BlackIsMin.
// This is however consistent with libtiff. It seems that fax4's "White"
// differs from what libtiff thinks of as "White". For content, a line of data
// is in runlength encoding of white-black-white-black always starting with
// white. In libtiff, the loop always does both colors in one go and the
// structure is:
//
// ```
// for (; runs < erun; runs += 2)
// // white run
// do { *lp++ = 0L; } while (…)
// // black run
// do { *lp++ = -1L; } while (…)
// ```
//
// So indeed the Fax4::White run is implemented by filling with zeros.
let mut bits = transitions.map(|c| match c {
fax34::Color::Black => true,
fax34::Color::White => false,
});
// Assemble bits in MSB as per our library representation for buffer.
for byte in target {
let mut val = 0;
for (idx, bit) in bits.by_ref().take(8).enumerate() {
val |= u8::from(bit) << (7 - idx % 8);
}
*byte = val;
}
self.line_buf.set_position(0);
}
}
}
self.line_buf.read(buf)
}
}
#[cfg(test)]
mod test {
use super::*;
+30
View File
@@ -104,6 +104,28 @@ impl Directory {
}
}
}
pub(crate) fn encoded_len<K: crate::encoder::TiffKind>(&self) -> u64 {
let (len_count, len_offset);
match std::mem::size_of::<K::OffsetType>() {
offset @ 4 => {
// non-BigTiff uses a `u16` here.
len_count = 2;
len_offset = offset as u64;
}
offset => {
let offset = offset as u64;
// assuming BigTIFF
len_count = offset;
len_offset = offset;
}
}
// Each entry is 12 bytes in classic TIFF, 20 bytes in BigTIFF.
let entry_bytes = 4 + 2 * len_offset;
len_count + entry_bytes * self.entries.len() as u64 + len_offset
}
}
impl fmt::Debug for Directory {
@@ -118,6 +140,14 @@ impl fmt::Debug for Directory {
}
}
impl core::iter::FromIterator<(Tag, Entry)> for Directory {
fn from_iter<T: IntoIterator<Item = (Tag, Entry)>>(iter: T) -> Self {
let mut dir = Directory::empty();
dir.extend(iter);
dir
}
}
#[cfg(test)]
mod tests {
use super::Directory;
+1 -1
View File
@@ -14,7 +14,7 @@ impl Compression for Uncompressed {
impl CompressionAlgorithm for Uncompressed {
fn write_to<W: Write>(&mut self, writer: &mut W, bytes: &[u8]) -> Result<u64, io::Error> {
writer.write(bytes).map(|byte_count| byte_count as u64)
writer.write_all(bytes).map(|()| bytes.len() as u64)
}
}
+192 -16
View File
@@ -11,7 +11,10 @@ use std::{
use crate::{
decoder::ifd::Entry,
error::{TiffResult, UsageError},
tags::{CompressionMethod, IfdPointer, ResolutionUnit, SampleFormat, Tag, Type},
tags::{
CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation, ResolutionUnit,
SampleFormat, Tag, Type,
},
Directory, TiffError, TiffFormatError,
};
@@ -37,8 +40,9 @@ pub type Predictor = crate::tags::Predictor;
#[cfg(feature = "deflate")]
pub type DeflateLevel = compression::DeflateLevel;
#[derive(Clone, Copy, PartialEq)]
#[derive(Clone, Copy, PartialEq, Default)]
pub enum Compression {
#[default]
Uncompressed,
#[cfg(feature = "lzw")]
Lzw,
@@ -47,12 +51,6 @@ pub enum Compression {
Packbits,
}
impl Default for Compression {
fn default() -> Self {
Self::Uncompressed
}
}
impl Compression {
fn tag(&self) -> CompressionMethod {
match self {
@@ -79,10 +77,10 @@ impl Compression {
/// Encoder for Tiff and BigTiff files.
///
/// With this type you can get a `DirectoryEncoder` or a `ImageEncoder`
/// With this type you can get a [`DirectoryEncoder`] or a [`ImageEncoder`]
/// to encode Tiff/BigTiff ifd directories with images.
///
/// See `DirectoryEncoder` and `ImageEncoder`.
/// See [`DirectoryEncoder`] and [`ImageEncoder`].
///
/// # Examples
/// ```
@@ -176,7 +174,7 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)
}
/// Create a [`DirectoryEncoder`] to encode an ifd directory.
/// Create a [`DirectoryEncoder`] to encode an ifd (directory).
///
/// The caller is responsible for ensuring that the directory is a valid image in the main TIFF
/// IFD sequence. To encode additional directories that are not linked into the sequence, use
@@ -185,7 +183,7 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)
}
/// Create a [`DirectoryEncoder`] to encode an ifd directory.
/// Create a [`DirectoryEncoder`] to encode a non-image directory.
///
/// The directory is not linked into the sequence of directories. For instance, encode Exif
/// directories or SubIfd directories with this method.
@@ -249,6 +247,12 @@ pub struct DirectoryEncoder<'a, W: 'a + Write + Seek, K: TiffKind> {
}
/// The offset of an encoded directory in the file.
///
/// Both the `offset` and `pointer` field are a complete description of the start offset of the
/// dictionary which is used to point to it. The struct also describes the extent covered by the
/// encoding of the directory. The constructors [`new`][DirectoryOffset::new] allow the checked
/// conversion. Note that even though the offsets are public do not expect their information to
/// propagate when modified.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DirectoryOffset<K: TiffKind> {
/// The start of the directory as a Tiff value.
@@ -260,7 +264,9 @@ pub struct DirectoryOffset<K: TiffKind> {
pub offset: K::OffsetType,
/// The start of the directory as a pure offset.
pub pointer: IfdPointer,
/// The offset of its sequence link field, in our private representation.
/// The offset of its sequence link field, in our private representation. Before exposing this
/// make sure that we don't want any invariants (e.g. pointer being smaller than this). The
/// type should be fine.
ifd_chain: NonZeroU64,
/// The kind of Tiff file the offset is for.
kind: PhantomData<K>,
@@ -318,6 +324,17 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
Ok(offset)
}
/// Insert previously written key-value entries.
///
/// It is the caller's responsibility to ensure that the data referred to by the entries is
/// actually (or will be) written to the file. Note that small values are necessarily inline in
/// the entry, the size limit depends on the Tiff kind. So do not confuse a directory from a
/// BigTiff in a standard tiff or the other way around.
pub fn extend_from(&mut self, dir: &Directory) {
let entries = dir.iter().map(|(tag, val)| (tag, val.clone()));
self.directory.extend(entries);
}
/// Define the parent directory.
///
/// Each directory has an offset based link to its successor, forming a linked list in the
@@ -334,6 +351,11 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
Ok(())
}
/// Write the IFD to the file and return the offset it is written to.
///
/// The offset can be used as the value, or as part of a list of values, in the entry of
/// another directory. If you're constructing the entry directly you'll want to use an
/// appropriate type variant for this such as [`Type::IFD`].
pub fn finish_with_offsets(mut self) -> TiffResult<DirectoryOffset<K>> {
self.finish_internal()
}
@@ -487,6 +509,7 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> {
row_samples: u64,
width: u32,
height: u32,
extra_samples: Vec<u16>,
rows_per_strip: u64,
strip_offsets: Vec<K::OffsetType>,
strip_byte_count: Vec<K::OffsetType>,
@@ -543,11 +566,14 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
encoder.write_tag(Tag::Compression, compression.tag().to_u16())?;
encoder.write_tag(Tag::Predictor, predictor.to_u16())?;
encoder.write_tag(Tag::BitsPerSample, <T>::BITS_PER_SAMPLE)?;
let sample_format: Vec<_> = <T>::SAMPLE_FORMAT.iter().map(|s| s.to_u16()).collect();
encoder.write_tag(Tag::SampleFormat, &sample_format[..])?;
encoder.write_tag(Tag::PhotometricInterpretation, <T>::TIFF_VALUE.to_u16())?;
if matches!(<T>::TIFF_VALUE, PhotometricInterpretation::YCbCr) {
// The default for this tag is 2,2 for subsampling but we do not support such a
// transformation. Instead all samples must be provided.
encoder.write_tag(Tag::ChromaSubsampling, &[1u16, 1u16][..])?;
}
encoder.write_tag(Tag::RowsPerStrip, u32::try_from(rows_per_strip)?)?;
encoder.write_tag(
@@ -564,6 +590,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
strip_idx: 0,
row_samples,
rows_per_strip,
extra_samples: vec![],
width,
height,
strip_offsets: Vec::new(),
@@ -575,6 +602,27 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
})
}
pub fn extra_samples(&mut self, extra: &[ExtraSamples]) -> Result<(), TiffError> {
if self.strip_idx != 0 {
return Err(TiffError::UsageError(
UsageError::ReconfiguredAfterImageWrite,
));
}
let samples = self.extra_samples.len() + extra.len() + <T>::BITS_PER_SAMPLE.len();
let row_samples = u64::from(self.width) * u64::try_from(samples)?;
self.extra_samples
.extend(extra.iter().map(ExtraSamples::to_u16));
self.encoder
.write_tag(Tag::ExtraSamples, &self.extra_samples[..])?;
self.row_samples = row_samples;
Ok(())
}
/// Number of samples the next strip should have.
pub fn next_strip_sample_count(&self) -> u64 {
if self.strip_idx >= self.strip_count {
@@ -712,6 +760,39 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
}
fn finish_internal(&mut self) -> TiffResult<DirectoryOffset<K>> {
if self.extra_samples.is_empty() {
self.encoder
.write_tag(Tag::BitsPerSample, <T>::BITS_PER_SAMPLE)?;
} else {
let mut sample_format: Vec<_> = <T>::BITS_PER_SAMPLE.to_vec();
let replicated =
core::iter::repeat_n(<T>::BITS_PER_SAMPLE[0], self.extra_samples.len());
sample_format.extend(replicated);
self.encoder
.write_tag(Tag::BitsPerSample, &sample_format[..])?;
self.encoder.write_tag(
Tag::SamplesPerPixel,
u16::try_from(<T>::BITS_PER_SAMPLE.len() + self.extra_samples.len())?,
)?;
}
let mut sample_format: Vec<_> = <T>::SAMPLE_FORMAT.iter().map(|s| s.to_u16()).collect();
let extra_format = sample_format
.first()
.copied()
// Frankly should not occur, we have no sample format without at least one entry. We
// would need an interface upgrade to handle this however. Either decide to support
// heterogeneous sample formats or a way to provide fallback that is used for purely
// extra samples and not provided via a slice used for samples themselves.
.unwrap_or(SampleFormat::Void.to_u16());
sample_format.extend(core::iter::repeat_n(extra_format, self.extra_samples.len()));
self.encoder
.write_tag(Tag::SampleFormat, &sample_format[..])?;
self.encoder
.write_tag(Tag::StripOffsets, K::convert_slice(&self.strip_offsets))?;
self.encoder.write_tag(
@@ -791,6 +872,71 @@ pub trait TiffKind {
fn convert_slice(slice: &[Self::OffsetType]) -> &Self::OffsetArrayType;
}
impl<K: TiffKind> DirectoryOffset<K> {
/// Construct the directory pointer information from its raw offset.
///
/// Fails in these cases:
///
/// - if start offset is too large to be represented with the chosen TIFF format, i.e. a value
/// larger than [`u32::MAX`] with a standard tiff which is only valid in a BigTiff. This
/// returns [`TiffError::IntSizeError`].
/// - the extent of the directory would overflow the file length. This returns a
/// [`TiffError::UsageError`].
///
/// The library does *not* validate the contents or offset in any way, it just performs the
/// necessary math assuming the data is an accurate representation of existing content.
///
/// # Examples
///
/// If you have any custom way of writing a directory to a file that can not go through the
/// usual [`TiffEncoder::extra_directory`] then you can reconstruct the offset information as
/// long as you provide all the necessary data to the library. You can then use it as a parent
/// directory, i.e. have the library overwrite its `next` field.
///
/// ```
/// use tiff::{
/// encoder::{TiffEncoder, DirectoryOffset},
/// Directory,
/// tags::IfdPointer,
/// };
///
/// # let mut file = std::io::Cursor::new(Vec::new());
/// let mut tiff = TiffEncoder::new(&mut file)?;
///
/// // … some custom data writes, assume we know where a directory was written
/// # fn reconstruction_of_your_directory() -> Directory { Directory::from_iter([]) }
/// let known_offset = IfdPointer(1024);
/// let known_contents: Directory = reconstruction_of_your_directory();
///
/// let reconstructed = DirectoryOffset::new(known_offset, &known_contents)?;
///
/// // Now we can use it as if the directory was written by the `TiffEncoder` itself.
/// let mut chain = tiff.extra_directory()?;
/// chain.set_parent(&reconstructed);
/// chain.finish_with_offsets()?;
///
/// # Ok::<_, tiff::TiffError>(())
/// ```
pub fn new(pointer: IfdPointer, dir: &Directory) -> TiffResult<Self> {
let offset = K::convert_offset(pointer.0)?;
let encoded = dir.encoded_len::<K>();
let offset_field_offset = encoded - mem::size_of_val(&offset) as u64;
let ifd_chain = pointer
.0
.checked_add(offset_field_offset)
.and_then(NonZeroU64::new)
.ok_or(TiffError::UsageError(UsageError::ZeroIfdPointer))?;
Ok(DirectoryOffset {
pointer,
offset,
ifd_chain,
kind: PhantomData,
})
}
}
/// Create a standard Tiff file.
pub struct TiffKindStandard;
@@ -859,3 +1005,33 @@ impl TiffKind for TiffKindBig {
slice
}
}
#[test]
fn directory_offset_new_equivalent_to_writing() {
type K = TiffKindStandard;
let dir = Directory::from_iter(vec![
(
Tag::ImageWidth,
Entry::new(Type::LONG, 1, 100u32.to_ne_bytes()),
),
(
Tag::ImageLength,
Entry::new(Type::LONG, 1, 200u32.to_ne_bytes()),
),
]);
let data = std::io::Cursor::new(vec![]);
let mut file = TiffEncoder::new(data).unwrap();
let mut as_dir_encoder = file.extra_directory().unwrap();
as_dir_encoder.extend_from(&dir);
let dir_extent = as_dir_encoder.finish_with_offsets().unwrap();
// Check we can recreate the extent had we written ourselves.
let synth = DirectoryOffset::<K>::new(dir_extent.pointer, &dir).unwrap();
assert_eq!(synth.pointer, dir_extent.pointer);
assert_eq!(synth.offset, dir_extent.offset);
assert_eq!(synth.ifd_chain, dir_extent.ifd_chain);
}
+37
View File
@@ -5,6 +5,14 @@ use crate::{bytecast, tags::Type, TiffError, TiffFormatError, TiffResult};
use super::writer::TiffWriter;
/// Trait for types that can be encoded in a tiff file
#[diagnostic::on_unimplemented(
message = "the type `{Self}` does not implement `TiffValue`",
note = "the trait is implemented for primitive types (`u8`, `i16`, `f32`, etc.)",
note = "the trait is implemented for shared references to values",
note = "the trait is implemented for slices, pass them by reference (e.g. `&[u8]`)",
note = "the trait is implemented for arrays if it is implemented for a slice",
note = "values in a `Vec` or `Box` should be dereferenced, e.g. `&vec[..]`"
)]
pub trait TiffValue {
const BYTE_LEN: u8;
const FIELD_TYPE: Type;
@@ -450,6 +458,14 @@ impl TiffValue for str {
}
}
// If you pass `&Vec<_>` then you'd get at first sight the complaint:
//
// `Vec<T>` does not implement `TiffValue`
//
// and a list of implementations of `TiffValue` that are quite unrelated. That is not very helpful.
// We do not *want* `Vec` to implement the trait as an owning type, instead you should pass a
// reference to a slice. The error message is further customized in the diagnostics of the trait.
#[diagnostic::do_not_recommend]
impl<T: TiffValue + ?Sized> TiffValue for &'_ T {
const BYTE_LEN: u8 = T::BYTE_LEN;
const FIELD_TYPE: Type = T::FIELD_TYPE;
@@ -467,6 +483,27 @@ impl<T: TiffValue + ?Sized> TiffValue for &'_ T {
}
}
/// An array is treated like a slice of the same length.
impl<T, const N: usize> TiffValue for [T; N]
where
[T]: TiffValue,
{
const BYTE_LEN: u8 = <[T]>::BYTE_LEN;
const FIELD_TYPE: Type = <[T]>::FIELD_TYPE;
fn count(&self) -> usize {
self.as_slice().count()
}
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> {
self.as_slice().write(writer)
}
fn data(&self) -> Cow<'_, [u8]> {
self.as_slice().data()
}
}
macro_rules! impl_tiff_value_for_contiguous_sequence {
($inner_type:ty; $bytes:expr; $field_type:expr) => {
impl $crate::encoder::TiffValue for [$inner_type] {
+61
View File
@@ -190,3 +190,64 @@ impl<W: Seek> TiffWriter<W> {
Ok(())
}
}
#[test]
fn encoder_on_short_writes() {
struct ShortWriter<T> {
inner: T,
}
impl<T: std::io::Write> std::io::Write for ShortWriter<T> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let lower_bound = buf.len().min(1);
let lower = buf.len().saturating_sub(1).max(lower_bound);
self.inner.write(&buf[..lower])
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
impl<T: std::io::Seek> std::io::Seek for ShortWriter<T> {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.inner.seek(pos)
}
}
let mut write = ShortWriter {
inner: std::io::Cursor::new(vec![0u8; 0]),
};
let imgdata = vec![42u8; 100 * 100];
let mut data = crate::decoder::DecodingResult::U8(vec![]);
for comp in [
super::Compression::Uncompressed,
super::Compression::Packbits,
] {
write.inner.set_position(0);
write.inner.get_mut().clear();
{
let mut tiff_writer = super::TiffEncoder::new(&mut write)
.unwrap()
.with_compression(comp);
tiff_writer
.write_image::<super::Gray8>(100, 100, &imgdata)
.unwrap();
}
write.inner.set_position(0);
let mut tiff_reader = crate::decoder::Decoder::new(&mut write.inner).unwrap();
let _layout = tiff_reader.read_image_to_buffer(&mut data).unwrap();
assert!(
matches!(&data, crate::decoder::DecodingResult::U8(v) if *v == imgdata),
"{data:?} @ {}",
data.as_buffer(0).byte_len()
);
}
}
+21
View File
@@ -90,6 +90,9 @@ quick_error! {
InvalidTypeForTag {
display("tag has invalid type")
}
InvalidCountForTag(tag: Tag, len: usize) {
display("tag `{tag:?}` with incorrect number of elements ({len}) encountered")
}
StripTileTagConflict {
display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found")
}
@@ -156,6 +159,9 @@ quick_error! {
UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
display("unsupported photometric interpretation \"{interpretation:?}\"")
}
ChromaSubsampling {
display("chroma subsampling of YCbCr color is unsupported")
}
MisalignedTileBoundaries {
display("tile rows are not aligned to byte boundaries")
}
@@ -175,6 +181,12 @@ quick_error! {
InvalidChunkIndex(index: u32) {
display("invalid chunk index ({index}) requested")
}
InvalidPlaneIndex(index: u16) {
display("invalid plane index ({index}) requested")
}
InvalidCodingUnit(index: u32, have: u32) {
display("out of bounds coding unit ({index}) requested, only {have} available")
}
PredictorCompressionMismatch {
display("requested predictor is not compatible with the requested compression")
}
@@ -187,6 +199,15 @@ quick_error! {
InsufficientOutputBufferSize { needed: usize, provided: usize } {
display("the borrowed output buffer is not large enough for the decoded data, needed {needed} but have {provided}")
}
InsufficientOutputRowStride { needed: usize, requested: usize } {
display("the provided output row stride would alias rows of decoded data, needed {needed} but have {requested}")
}
ZeroIfdPointer {
display("the offset 0 can not point to a valid IFD")
}
ReconfiguredAfterImageWrite {
display("attempted to reconfigure the encoder after image writing has started")
}
}
}
+19
View File
@@ -44,6 +44,9 @@ pub enum ColorType {
/// Pixel is YCbCr
YCbCr(u8),
/// Pixel is CIE L*a*b* (ICC LAb)
Lab(u8),
/// Pixel has multiple bands/channels
Multiband { bit_depth: u8, num_samples: u16 },
}
@@ -58,7 +61,23 @@ impl ColorType {
| ColorType::CMYK(b)
| ColorType::CMYKA(b)
| ColorType::YCbCr(b)
| ColorType::Lab(b)
| ColorType::Multiband { bit_depth: b, .. } => b,
}
}
fn num_samples(&self) -> u16 {
match *self {
ColorType::Gray(_) => 1,
ColorType::RGB(_) => 3,
ColorType::Palette(_) => 1,
ColorType::GrayA(_) => 2,
ColorType::RGBA(_) => 4,
ColorType::CMYK(_) => 4,
ColorType::CMYKA(_) => 5,
ColorType::YCbCr(_) => 3,
ColorType::Lab(_) => 3,
ColorType::Multiband { num_samples, .. } => num_samples,
}
}
}
+146 -7
View File
@@ -20,7 +20,7 @@ macro_rules! tags {
impl $name {
#[inline(always)]
fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
const fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
match n {
$( $val => Ok($name::$tag), )*
n => Err(n),
@@ -28,7 +28,7 @@ macro_rules! tags {
}
#[inline(always)]
fn __to_inner_type(&self) -> $ty {
const fn __to_inner_type(&self) -> $ty {
match *self {
$( $name::$tag => $val, )*
$( $name::Unknown($unknown_doc) => { $unknown_doc }, )*
@@ -42,19 +42,25 @@ macro_rules! tags {
($name:tt, u16, $($unknown_doc:ident)*) => {
impl $name {
#[inline(always)]
pub fn from_u16(val: u16) -> Option<Self> {
Self::__from_inner_type(val).ok()
pub const fn from_u16(val: u16) -> Option<Self> {
match Self::__from_inner_type(val) {
Ok(v) => Some(v),
Err(_) => None,
}
}
$(
#[inline(always)]
pub fn from_u16_exhaustive($unknown_doc: u16) -> Self {
Self::__from_inner_type($unknown_doc).unwrap_or_else(|_| $name::Unknown($unknown_doc))
pub const fn from_u16_exhaustive($unknown_doc: u16) -> Self {
match Self::__from_inner_type($unknown_doc) {
Ok(v) => v,
Err(_) => $name::Unknown($unknown_doc),
}
}
)*
#[inline(always)]
pub fn to_u16(&self) -> u16 {
pub const fn to_u16(&self) -> u16 {
Self::__to_inner_type(self)
}
}
@@ -122,6 +128,11 @@ pub enum Tag(u16) unknown(
SMaxSampleValue = 341, // TODO add support
// JPEG
JPEGTables = 347,
// Subsampling
#[doc(alias = "YCbCrSubsampling")]
ChromaSubsampling = 530, // TODO add support
#[doc(alias = "YCbCrPositioning")]
ChromaPositioning = 531, // TODO add support
// GeoTIFF
ModelPixelScaleTag = 33550, // (SoftDesk)
ModelTransformationTag = 34264, // (JPL Carto Group)
@@ -193,6 +204,45 @@ pub enum Type(u16) {
}
}
impl Type {
pub(crate) fn byte_len(&self) -> u8 {
match *self {
Type::BYTE | Type::SBYTE | Type::ASCII | Type::UNDEFINED => 1,
Type::SHORT | Type::SSHORT => 2,
Type::LONG | Type::SLONG | Type::FLOAT | Type::IFD => 4,
Type::LONG8
| Type::SLONG8
| Type::DOUBLE
| Type::RATIONAL
| Type::SRATIONAL
| Type::IFD8 => 8,
}
}
pub(crate) fn value_bytes(&self, count: u64) -> Result<u64, crate::error::TiffError> {
let tag_size = u64::from(self.byte_len());
match count.checked_mul(tag_size) {
Some(n) => Ok(n),
None => Err(crate::error::TiffError::LimitsExceeded),
}
}
pub(crate) fn endian_bytes(self) -> EndianBytes {
match self {
Type::BYTE | Type::SBYTE | Type::ASCII | Type::UNDEFINED => EndianBytes::One,
Type::SHORT | Type::SSHORT => EndianBytes::Two,
Type::LONG
| Type::SLONG
| Type::FLOAT
| Type::IFD
| Type::RATIONAL
| Type::SRATIONAL => EndianBytes::Four,
Type::LONG8 | Type::SLONG8 | Type::DOUBLE | Type::IFD8 => EndianBytes::Eight,
}
}
}
tags! {
/// See [TIFF compression tags](https://www.awaresystems.be/imaging/tiff/tifftags/compression.html)
/// for reference.
@@ -227,6 +277,8 @@ pub enum PhotometricInterpretation(u16) {
CMYK = 5,
YCbCr = 6,
CIELab = 8,
IccLab = 9,
ItuLab = 10,
}
}
@@ -271,3 +323,90 @@ pub enum SampleFormat(u16) unknown(
Void = 4,
}
}
tags! {
pub enum ExtraSamples(u16) {
/// There is no specified association between the sample and the image.
Unspecified = 0,
/// The sample is associated alpha, i.e. pre-multiplied color.
AssociatedAlpha = 1,
/// The sample is unassociated alpha such as a mask. There might be more than one such sample.
UnassociatedAlpha = 2,
}
}
/// Byte order of the TIFF file.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ByteOrder {
/// little endian byte order
LittleEndian,
/// big endian byte order
BigEndian,
}
impl ByteOrder {
/// Get the byte order representing the running target.
///
/// The infallibility of this method represents the fact that only little and big endian
/// systems are supported by the library. No mixed endian and no other weird stuff. (Note: as
/// of Rust 1.90 this is a tautology as Rust itself only has those two kinds).
pub const fn native() -> Self {
match () {
#[cfg(target_endian = "little")]
() => ByteOrder::LittleEndian,
#[cfg(target_endian = "big")]
() => ByteOrder::BigEndian,
#[cfg(not(any(target_endian = "big", target_endian = "little")))]
() => compile_error!("Unsupported target"),
}
}
/// Given a typed buffer, convert its contents to the specified byte order in-place.
///
/// The buffer is assumed to represent an array of the given type. If the length of the buffer
/// is not divisible into an integer number of values, the behavior for the remaining bytes it
/// not specified.
pub fn convert(self, ty: Type, buffer: &mut [u8], to: ByteOrder) {
self.convert_endian_bytes(ty.endian_bytes(), buffer, to)
}
pub(crate) fn convert_endian_bytes(self, cls: EndianBytes, buffer: &mut [u8], to: ByteOrder) {
if self == to {
return;
}
// FIXME: at MSRV 1.89 or higher use `slice::as_chunks_mut`.
match cls {
EndianBytes::One => {
// No change needed
}
EndianBytes::Two => {
for chunk in buffer.chunks_exact_mut(2) {
let chunk: &mut [u8; 2] = chunk.try_into().unwrap();
*chunk = u16::from_be_bytes(*chunk).to_le_bytes();
}
}
EndianBytes::Four => {
for chunk in buffer.chunks_exact_mut(4) {
let chunk: &mut [u8; 4] = chunk.try_into().unwrap();
*chunk = u32::from_be_bytes(*chunk).to_le_bytes();
}
}
EndianBytes::Eight => {
for chunk in buffer.chunks_exact_mut(8) {
let chunk: &mut [u8; 8] = chunk.try_into().unwrap();
*chunk = u64::from_be_bytes(*chunk).to_le_bytes();
}
}
}
}
}
/// The size of individual byte-order corrected elements.
#[derive(Clone, Copy)]
pub(crate) enum EndianBytes {
One,
Two,
Four,
Eight,
}
+48
View File
@@ -672,3 +672,51 @@ fn bytes_gray_f32() {
byte_order_u32,
);
}
#[test]
#[cfg(feature = "fax")]
fn test_fax4() {
test_image_sum_u8("fax4.tiff", ColorType::Gray(1), 7802706);
}
#[test]
#[cfg(feature = "fax")]
fn test_fax4_white_is_min() {
test_image_sum_u8("imagemagick_group4.tiff", ColorType::Gray(1), 3742820);
}
#[test]
fn write_extra_bits_image() {
use tiff::{encoder, tags::ExtraSamples};
let path = PathBuf::from(TEST_IMAGE_DIR).join("extra_bits_rgb_8b.tiff");
let img_file = File::create(path).expect("Cannot find test image!");
let mut encoder = encoder::TiffEncoder::new(img_file).expect("Cannot create encoder");
let mut img = encoder.new_image::<encoder::colortype::RGB8>(8, 8).unwrap();
img.extra_samples(&[ExtraSamples::Unspecified]).unwrap();
// all pixels are supposed to be 0, the extra is 1
let data: [u8; 256] = core::array::from_fn(|i| (i as u8 % 4).saturating_sub(2));
img.write_data(&data)
.expect("correct amount of data to write");
}
#[test]
fn extra_bits_gray() {
test_image_sum_u8(
"extra_bits_gray_8b.tiff",
ColorType::Multiband {
bit_depth: 8,
num_samples: 2,
},
64,
);
}
#[test]
fn extra_bits_rgb() {
test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGB(8), 0);
}
+76
View File
@@ -0,0 +1,76 @@
use tiff::decoder::{Decoder, TiffCodingUnit};
use std::fs::File;
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images/";
#[test]
fn read_all_planes() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
let file = File::open(path).unwrap();
let io = std::io::BufReader::new(file);
let mut tif = Decoder::new(io).unwrap();
let layout = tif.image_buffer_layout().unwrap();
assert_eq!(layout.planes, 3);
let plane_stride = layout.plane_stride.map_or(0, |x| x.get());
eprintln!("{:?}", layout);
let mut buffer = vec![0u8; layout.complete_len];
tif.read_image_bytes(&mut buffer).unwrap();
// Mainly you can see: these are different and non-zero. Otherwise these magic constants just
// depend on the file itself that is being used.
const PLANE_EXCPECTED_SUMS: [u32; 3] = [15417630, 13007788, 11103530];
for (plane, expected) in (0..layout.planes).zip(PLANE_EXCPECTED_SUMS) {
let plane_data = &buffer[plane_stride * plane..][..layout.len];
let sum: u32 = plane_data.iter().copied().map(u32::from).sum();
assert_eq!(sum, expected);
}
}
#[test]
fn read_first_coding_unit() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
let file = File::open(path).unwrap();
let io = std::io::BufReader::new(file);
let mut tif = Decoder::new(io).unwrap();
let layout = tif.image_coding_unit_layout(TiffCodingUnit(0)).unwrap();
assert_eq!(layout.planes, 3);
let plane_stride = layout.plane_stride.map_or(0, |x| x.get());
eprintln!("{:?}", layout);
let mut buffer = vec![0u8; layout.complete_len];
struct Expected {
plane_sums: [u32; 3],
unit: TiffCodingUnit,
}
// Mainly you can see: these are different and non-zero. Otherwise these magic constants just
// depend on the file itself that is being used. Also this varies by what unit we are reading.
const PLANE_EXCPECTED_SUMS: [Expected; 2] = [
Expected {
plane_sums: [856853, 767686, 688532],
unit: TiffCodingUnit(0),
},
Expected {
plane_sums: [883679, 797329, 711086],
unit: TiffCodingUnit(1),
},
];
for expected in PLANE_EXCPECTED_SUMS {
tif.read_coding_unit_bytes(expected.unit, &mut buffer)
.unwrap();
for (plane, expected) in (0..layout.planes).zip(expected.plane_sums) {
let plane_data = &buffer[plane_stride * plane..][..layout.len];
let sum: u32 = plane_data.iter().copied().map(u32::from).sum();
assert_eq!(sum, expected);
}
}
}
+17 -6
View File
@@ -57,7 +57,7 @@ fn encode_decode() {
),
(Tag::Compression, ifd::Value::Short(1)),
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
(Tag::StripOffsets, ifd::Value::Unsigned(47)),
(Tag::StripOffsets, ifd::Value::Unsigned(35)),
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
(Tag::StripByteCounts, ifd::Value::Unsigned(30000)),
@@ -81,7 +81,10 @@ fn encode_decode() {
decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into())
);
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
if let tiff::decoder::DecodingResult::U8(img_res) = buffer {
assert_eq!(image_data, img_res);
} else {
panic!("Wrong data type");
@@ -161,7 +164,9 @@ fn encode_decode_big() {
decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into())
);
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::U8(img_res) = buffer {
assert_eq!(image_data, img_res);
} else {
panic!("Wrong data type");
@@ -235,6 +240,7 @@ const TEST_IMAGE_DIR: &str = "./tests/images/";
macro_rules! test_roundtrip {
($name:ident, $buffer:ident, $buffer_ty:ty) => {
#[track_caller]
fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
file: &str,
expected_type: ColorType,
@@ -244,7 +250,10 @@ macro_rules! test_roundtrip {
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let image_data = match decoder.read_image().unwrap() {
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap();
let image_data = match buffer {
DecodingResult::$buffer(res) => res,
_ => panic!("Wrong data type"),
};
@@ -258,9 +267,11 @@ macro_rules! test_roundtrip {
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
let mut decoder = Decoder::new(&mut file).unwrap();
if let DecodingResult::$buffer(img_res) = decoder.read_image().unwrap() {
assert_eq!(image_data, img_res);
decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::$buffer(img_res) = buffer {
assert_eq!(*image_data, img_res);
} else {
panic!("Wrong data type");
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.