Compare commits

...
Author SHA1 Message Date
A. Molzer 32dda6e11f Add integration test for exif copy 2026-01-15 20:21:54 +01:00
A. Molzer 74191f92bb Change EXIF Applicability's None to passthrough
This allows it to address the case of copying all EXIF data into a
separate file. And as soon as our decoder can handle files without any
image data (missing any stripoffsets, etc.) in ways that do not return
errors we can hopefully do the opposite and merge the data again.
2026-01-15 20:21:54 +01:00
A. Molzer f057b5f47d Reduce tag_filter to a minimum, implement copy 2026-01-15 20:21:52 +01:00
A. Molzer b3ced063ef Write tag filter based on tag kind/'Level' 2026-01-14 17:40:40 +01:00
A. Molzer 76721c6893 Add conversion of ValueBuffer to IfdPointer
This mirrors what the other value type provides, for now private and
only for IfdPointer which we need for iterating an existing directory.
2026-01-14 17:40:40 +01:00
A. Molzer 348bbda906 Sketch a TagFilter option for copying metadata 2026-01-13 23:46:39 +01:00
Aurelia Molzer 1bd6293857 Merge pull request #329 from image-rs/fixup-value-buffer-debug-condition
Fix debug condition of ValueBuffer
2026-01-13 14:53:12 +00:00
A. Molzer 87341f9075 Fix debug condition of ValueBuffer
Off-by-one, this was meant to check a minimum length that includes
equality but instead tested a valid index.
2026-01-13 15:37:22 +01:00
Aurelia Molzer 43343f64cd Merge pull request #328 from image-rs/reduce-package-size
Remove benchmark images from distribution
2026-01-13 09:12:59 +00:00
Aurelia Molzer be04326264 Remove benchmark images from distribution 2026-01-13 10:06:59 +01:00
Aurelia Molzer 93a68f52f5 Merge pull request #327 from image-rs/release-0.11.1
Metadata for release 0.11.1
2026-01-13 09:04:30 +00:00
A. Molzer 5f329840c8 Metadata for release 0.11.1 2026-01-12 00:43:29 +01:00
Aurelia Molzer 1d138a5568 Merge pull request #326 from image-rs/encode-values-from-buffer
Encode values from buffer
2026-01-11 22:09:29 +00:00
A. Molzer 9a23090b6e Provide write_entry_bytes
This complements the entry API of the encoder with the byte slice based
method. Note we don't provide it for writing raw data, here a
`std::io::Write` would be more appropriate and we should not attempt to
make it `write_all`, nor compress. You'd get all the benefits of the
other API through `[u8]: TiffValue` since that interface is otherwise
type agnostic and `u8` specifically is endian agnostic.
2026-01-11 22:59:34 +01:00
A. Molzer 9d409433e8 Rename write_tag_value to write_tag_buf
We have not released this yet, let's align these names before it is too
late. The decoder adopted the `_buf` suffix and I find it more
convincing than the more generic `_value` (that also confusingly shares
it with `TiffValue`).
2026-01-11 22:29:08 +01:00
Aurelia Molzer cf680424d9 Merge pull request #325 from image-rs/decode-values-into-buffer
Decode tag values into buffer
2026-01-11 19:19:41 +00:00
A. Molzer d3b777c961 Testing access through alternate tag reads 2026-01-11 17:55:15 +01:00
A. Molzer 24b008a7c3 Read tag values into byte slice 2026-01-11 17:20:04 +01:00
A. Molzer 34bd1da99e Read raw values into ValueBuffer 2026-01-11 17:20:04 +01:00
Aurelia Molzer cfff04c97d Merge pull request #324 from image-rs/encoder-tiff-value
A byte-valued endian-formatted tiff value buffer
2026-01-10 02:47:17 +00:00
A. Molzer 915cc20945 Revert "Construct tags::ValueBuffer from ifd::Value"
This reverts commit 45f617b9f5
2026-01-10 01:27:43 +01:00
A. Molzer 74b2857364 Construct ValueBuffer from TiffValue 2026-01-10 01:27:43 +01:00
A. Molzer 1dab89f10e Generalize write for pre-encoded tags 2026-01-10 01:27:25 +01:00
A. Molzer 45f617b9f5 Construct tags::ValueBuffer from ifd::Value
Provides interaction between decoder and encoder values. Not sure about
this commit yet, the implementation is a bit odd.
2026-01-09 14:49:02 +01:00
A. Molzer 170bd3697c Provide tags in their byte encoding 2026-01-09 14:42:22 +01:00
Aurelia Molzer 855eb4ef23 Merge pull request #323 from image-rs/tiff-value-for-tags
Utils for working with tags
2026-01-09 08:11:24 +00:00
A. Molzer fee650d7f6 Ensure repr and values of tags! variants 2026-01-08 19:53:47 +01:00
A. Molzer ee8d13249e Utils for working with tags
Implement the encoder's `TiffValue` trait for the tags commonly defined
and represented as separate types in our own code. We don't tie the
tag for an entry of these to the value type (yet). This lets us keep the
trait exactly as-is and we avoid unnecessary churn. We can always add a
separate trait on top.
2026-01-08 19:53:40 +01:00
Aurelia Molzer 169a896851 Merge pull request #322 from image-rs/deprecate-big-rational-types
Deprecate unofficial value types
2026-01-08 17:16:20 +00:00
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
Aurelia Molzer cbcf6b97d4 Bump version number 0.10.1 2025-08-25 23:21:34 +02:00
Sergey "Shnatsel" Davidoff 19c811e86d populate changelog for 0.10.1 2025-08-25 15:20:04 +01:00
Aurelia Molzer 50be15e356 Merge pull request #290 from jakubadamw/nightly-compilation-warnings
Address compilation warnings in the Nightly channel of Rust
2025-08-14 16:50:22 +00:00
Jacob Adam 718f2cb455 Address compilation warnings in the Nightly channel of Rust. 2025-08-14 17:45:21 +01:00
Aurelia Molzer 36a16db659 Merge pull request #285 from image-rs/read-into-buffer
Read into byte buffer
2025-07-25 19:16:09 +00:00
A. Molzer d70c31dcbf Add a Bug note about planar configuration 2025-07-25 21:13:41 +02:00
Aurelia Molzer b589d10a27 Merge pull request #289 from aotimme/decoder-inner
Add inner() method to decoder to provide access to raw reader
2025-07-22 20:18:56 +02:00
Alden Timme f4378eeb80 Also split test_planar_rgb_u8 test 2025-07-22 08:48:57 -07:00
Alden Timme 49015c6e93 Move into separate test 2025-07-22 08:42:39 -07:00
Alden Timme 29402875cb Add inner() method to decoder to provide access to raw reader 2025-07-21 20:47:47 -07:00
Aurelia Molzer 33453289b6 Merge pull request #287 from image-rs/icc-tag
Add ICC (color profile) to the set of known tags
2025-07-19 18:17:56 +02:00
A. Molzer ce0ca5c234 Add ICC (color profile) to the set of known tags 2025-07-19 18:12:54 +02:00
A. Molzer f671293bd8 Fix byte-crc test for big endian platforms 2025-07-19 18:08:02 +02:00
A. Molzer 48d52bed9b Return buffer layout as a struct
Prepares extensibility for the eventual case of supporting multi-planar
image layouts that are to be written into a multi-planar buffer as
indicated by the caller in some way (possibly not via read_*_bytes but a
method taking a wrapper around the output buffer instead). Through the
struct we can later add an indicator that can be used by the caller to
decide between methods of fetching the pixel data as appropriate.
2025-07-19 17:46:37 +02:00
Aurelia Molzer 7707060982 Merge pull request #286 from image-rs/clippy
Resolve all the clippy lints and add it to CI
2025-07-19 17:39:26 +02:00
A. Molzer c370ac4b92 Take row stride input in chunk bytes 2025-07-19 00:12:43 +02:00
A. Molzer b2bcbbaf23 Enable clippy for the merge workflow 2025-07-14 19:56:06 +02:00
A. Molzer d07da5a6cb Resolve clippy lints in tests 2025-07-14 19:56:06 +02:00
A. Molzer a3c81e9830 Add Directory::is_empty for completeness 2025-07-14 19:56:06 +02:00
A. Molzer 46e51cc67e Make documentation adhere clippy, more natural 2025-07-14 19:56:06 +02:00
A. Molzer f6a762b72c Upgrade usage of format! and Default 2025-07-14 19:56:06 +02:00
A. Molzer 23672b2490 Switch to div_ceil for rounding implementation 2025-07-14 19:25:12 +02:00
A. Molzer ca74c03520 Fix straightforward cast/borrow lints 2025-07-14 19:25:12 +02:00
A. Molzer 2942f9de97 Mark f16 bytecast as expected unused 2025-07-14 19:25:12 +02:00
A. Molzer 29add10ca7 Fix manual non_exhaustive pattern 2025-07-14 18:24:01 +02:00
A. Molzer 59d32a66a7 Add test coverage for to byte decoding 2025-07-14 18:23:30 +02:00
A. Molzer 5a6b2ad8f3 Complementary methods reading into borrowed buffer 2025-07-14 18:23:30 +02:00
A. Molzer c746653269 Add Decoder::read_image_bytes, borrowed output 2025-07-14 18:23:30 +02:00
Aurelia Molzer cc75d051af Merge pull request #283 from image-rs/write-directory
Write extra directories
2025-07-14 00:01:39 +02:00
A. Molzer ab4fb4e959 Test and polish encoding extra directories 2025-07-13 23:28:24 +02:00
A. Molzer 7b1683687e Split Encoder::write_directory
There are two kinds of directories: those that are image directories
directly in the TIFF file's main sequence, and directories that follow
the structure but represent auxiliary data. The method will link the
directory into the main sequence but those images have additional
requirement on their IFDs which is not clear to the caller. We make that
explicit and also provide a way to write auxiliary directories.
2025-07-13 22:59:53 +02:00
Aurelia Molzer 2e51ee4b15 Merge pull request #282 from image-rs/write-directory
Refactor encoding of directories
2025-07-13 22:28:05 +02:00
A. Molzer f6e2aeffe5 Encode values immediately
This currently avoids buffering the collection, but not buffering them
individually where we still format it into a byte vector.

The main drive here is making the DirectoryEncoder less dependent on the
values. That is, it should be concerned with the Entry representation of
values only. A few decoders expect all values to strictly follow the
image directory and the data layout of such files should be controllable
by the user. This could be achieved by encoding dummy entries which are
later corrected once the actual item is known by seeking back and
writing the directory anew.

By no longer assuming values to be available on finishing a directory we
also prepare an interface that encodes an existing directory.
2025-07-13 03:06:40 +02:00
A. Molzer 525bb2b9b1 Simplify encoder writing of dictionary 2025-07-13 02:22:17 +02:00
A. Molzer 54f733eaa3 Allow non-chained directories in writer
This is just the implementation details. In the DirectoryEncoder we did
not differentiate between different directories that should go into the
main image chain and other directories. This is now possible by storing
the prior offset as an explicit Option<NonZeroU64>.

Additionally, the whole encoder did not allow any intermediate data
between its last directory and new directories on the chain. The offset
of the relevant `next` field was not stored explicitly but derived from
the current stream position at the moment of starting the directory.
This would not allow any padding data or data written outside the
returned `DirectoryEncoder` object.

This relaxed both requirements by turning the offsets into attributes.
2025-07-13 02:22:17 +02:00
Aurelia Molzer 9b0c9ca84c Merge pull request #281 from image-rs/write-directory
Adjust Directory for ease of writing
2025-07-13 01:16:39 +02:00
A. Molzer 890488b16e Add Entry::field_type
The directory API offers a direct access to entries. It is useful for
downstream to have the ability of inspecting fields before consuming
them.
2025-07-13 01:11:59 +02:00
A. Molzer 3e530d27ea Directory compatibility with encoding and BigTiff 2025-07-13 01:11:59 +02:00
Aurelia Molzer 02699c9588 Merge pull request #278 from image-rs/read-directory
Expose methods related to reading directory
2025-07-12 17:23:34 +02:00
A. Molzer 26b6afc787 Document Directory related symbols 2025-07-12 15:34:46 +02:00
Aurelia Molzer 5df003368b Merge pull request #280 from image-rs/status-badge
Let status badge reflect main workflows
2025-07-11 15:17:48 +02:00
A. Molzer a582fdcc72 Let status badge reflect main workflows 2025-07-11 15:14:37 +02:00
A. Molzer e62cdfbf42 Merge TagReader and exposed IfdReader internals
Having the value reader itself take care of the bigtiff and limit
semantics is a little cleaner, with the TagReader being responsible
for the interface of grabbing specific types of IFD values.
2025-07-11 15:09:31 +02:00
A. Molzer f021e3e599 Rename ifd value adapter to read_directory_tags 2025-07-11 14:12:36 +02:00
A. Molzer f5ab3b363b Expose interacting with secondary IFDs 2025-07-11 14:12:36 +02:00
A. Molzer f33c7be21c Add the SubIfd tag 2025-07-11 14:12:36 +02:00
A. Molzer 98cd3dc2d2 Refactor directory reading
This allows us to recombine the reader and other file data in a Decoder
with any IFD, whether that of an image or an extension IFD as indicated
by an IFD pointer such as Exif and GPS.
2025-07-11 14:12:34 +02:00
Aurelia Molzer 6275b7a36d Merge pull request #279 from image-rs/seeking-multiple-times
Seeking multiple times
2025-07-11 13:18:41 +02:00
A. Molzer 97e230584f Test ordering directory IFD 2025-07-10 23:57:25 +02:00
A. Molzer 43d7f66c9e Refactor cycle and IFD tracking
The previous strategy could not keep track of all cycles such as
reflexive ones at the very start. Additionally, it would fail to be able
to iterate after seeking since it was overly cautious and verified the
next IFD position before allowing the read of a current IFD.

The new implementation is furthermore proof against having multiple
independent chains of IFD which may occur when iterating SubIFDs for
thumbnails. (Note that we do not verify the tree-structure, only a
freedom from cycles).
2025-07-10 23:52:07 +02:00
A. Molzer a1be18b548 Add subsubifds test file 2025-07-10 23:48:20 +02:00
Aurelia Molzer df4e42eecc Add a dedicated Directory type (#277) 2025-07-07 19:59:37 -07:00
Vladimir Kuznetsov 816fb85304 Use libtiff compatible weezl configuration (#276) 2025-06-29 10:24:28 -07:00
Jonathan Behrens 7648317643 Stop doing color conversion when decoding JPEG compressed images (#274) 2025-06-22 16:19:36 -07:00
Jonathan Behrens 140f2788de Update CI workflow (#275) 2025-06-22 10:37:30 -07:00
38 changed files with 5098 additions and 884 deletions

No files matched your search

+35 -6
View File
@@ -1,22 +1,22 @@
name: Rust CI name: Rust CI
on: on:
push: push:
branches: [ master ] branches: [ main ]
pull_request: pull_request:
branches: [ master ] branches: [ main ]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
rust: ["1.74.0", stable, beta, nightly] rust: ["1.85.0", stable, beta, nightly]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@nightly - 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 - 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 run: cargo -Zminimal-versions generate-lockfile
- uses: dtolnay/rust-toolchain@v1 - uses: dtolnay/rust-toolchain@v1
@@ -29,7 +29,7 @@ jobs:
- name: build - name: build
run: cargo build -v run: cargo build -v
- name: test - name: test
if: ${{ matrix.rust != '1.74.0' }} if: ${{ matrix.rust != '1.85.0' }}
run: cargo test -v && cargo doc -v run: cargo test -v && cargo doc -v
features: features:
@@ -45,6 +45,35 @@ jobs:
- name: Cargo check - name: Cargo check
run: cargo check --features "${{ matrix.feature }}" --no-default-features run: cargo check --features "${{ matrix.feature }}" --no-default-features
- name: Cargo clippy
if: ${{ matrix.feature != '' }}
run: cargo clippy --no-deps --all-features --all-targets -- -D warnings
other_archs:
# github actions does not support 32-bit or big endian systems directly, but
# it does support QEMU. so we install qemu, then build and run the tests on
# emulated systems. NOTE: you can also use this approach to test locally.
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch: [powerpc-unknown-linux-gnu, i686-unknown-linux-gnu]
steps:
- uses: actions/checkout@v4
- name: Install or use cached cross-rs/cross
uses: baptiste0928/cargo-install@v2
with:
crate: cross
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
key: ${{ matrix.arch }}
- name: Start Docker (required for cross-rs)
run: sudo systemctl start docker
- name: Cross-Run Tests using QEMU
run: cross test --target ${{ matrix.arch }}
rustfmt: rustfmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
+120
View File
@@ -1,3 +1,123 @@
# Version 0.11.1
Fixes:
- The enumeration types in `tags` are now all marked with a representation of
their underlying TIFF type (e.g. `repr(u16)`) and variants are explicitly
assigned their corresponding values. That is you may _read_ the raw
discriminant and interpret that as the value—except for `Unknown` variants.
- The variants `RationalBig` and `SRationalBig` of `decoder::ifd::Value` are
deprecated as they have no corresponding TIFF type and were not constructed
during decoding.
Additions:
- Types in `tags` now generally implement `TiffValue` and can be handed to the
`DirectoryEncoder::write_tag` method. Unlike primitive types they do _not_
always implement the trait for slices of themselves.
- Added `tags::ValueBuffer`, a byte-based buffer for tags with a runtime value
type and count.
- Added `IfdDecoder::find_tag_{buf,bytes}` to read tag values into a
`ValueBuffer` or byte slice respectively without interpreting them
immediately within the Rust type system.
- Added `DirectoryEncoder::write_{data,tag}_buf` that takes a `ValueBuffer`
instead of a statically typed argument.
- Added `DirectoryEncoder::write_entry{,_buf,_bytes}` that encode their
argument into the file and return an `Entry` for later use, without
immediately adding a tag-entry pair to the directory.
- The `TiffValue` trait is now implemented for several builtin `tags::*` types
or slices of them as appropriate for their use as values of their intended
TIFF tag.
- The `TiffValue` trait is now implemented for `[T; N]` for all types that
implement it for slices.
# 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:
- Added a dedicated `Directory` type (#277)
- Expose methods related to `Directory` publicly (#278)
- Implement writing directories (#281, #283)
- Add ICC (color profile) to the set of known tags (#287)
- Added `Decoder.inner()` to access the underlying reader (#289)
- Added methods for reading into a pre-allocated byte buffer (#285)
Changes:
- No longer perform color conversion when decoding embedded JPEG. This fixes decoding for JPEG images with alpha channel. (#274)
- More lenient LZW decoding to match libtiff (#231)
Fixes:
- Fixed seeking multiple times in a row (#279)
# Version 0.10.0 # Version 0.10.0
New features: New features:
+11 -6
View File
@@ -1,11 +1,11 @@
[package] [package]
name = "tiff" name = "tiff"
version = "0.10.0" version = "0.11.1"
edition = "2021" edition = "2021"
resolver = "2" resolver = "2"
# note: when changed, also update test runner in `.github/workflows/rust.yml` # note: when changed, also update test runner in `.github/workflows/rust.yml`
rust-version = "1.74.0" rust-version = "1.85.0"
license = "MIT" license = "MIT"
description = "TIFF decoding and encoding library in pure Rust" description = "TIFF decoding and encoding library in pure Rust"
@@ -14,25 +14,30 @@ authors = ["The image-rs Developers"]
repository = "https://github.com/image-rs/image-tiff" repository = "https://github.com/image-rs/image-tiff"
categories = ["multimedia::images", "multimedia::encoding"] categories = ["multimedia::images", "multimedia::encoding"]
exclude = ["tests/images/*", "tests/fuzz_images/*"] exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
[dependencies] [dependencies]
half = { version = "2.4.1" } half = { version = "2.4.1" }
quick-error = "2.0.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 } flate2 = { version = "1.0.20", optional = true }
weezl = { version = "0.1.0", optional = true } weezl = { version = "0.1.10", optional = true }
zstd = { version = "0.13", 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] [dev-dependencies]
criterion = "0.3.1" criterion = "0.3.1"
# We may already depend on this via flate2
crc32fast = "1.5"
[features] [features]
default = ["deflate", "jpeg", "lzw"] default = ["deflate", "fax", "jpeg", "lzw"]
# Compression algorithms # Compression algorithms
deflate = ["dep:flate2"] deflate = ["dep:flate2"]
fax = ["dep:fax34"]
jpeg = ["dep:zune-jpeg"] jpeg = ["dep:zune-jpeg"]
lzw = ["dep:weezl"] lzw = ["dep:weezl"]
zstd = ["dep:zstd"] zstd = ["dep:zstd"]
+2 -1
View File
@@ -1,5 +1,5 @@
# image-tiff # image-tiff
[![Build Status](https://github.com/image-rs/image-tiff/workflows/Rust%20CI/badge.svg)](https://github.com/image-rs/image-tiff/actions) [![Build Status](https://github.com/image-rs/image-tiff/actions/workflows/rust.yml/badge.svg?branch=main)](https://github.com/image-rs/image-tiff/actions)
[![Documentation](https://docs.rs/tiff/badge.svg)](https://docs.rs/tiff) [![Documentation](https://docs.rs/tiff/badge.svg)](https://docs.rs/tiff)
[![Further crate info](https://img.shields.io/crates/v/tiff.svg)](https://crates.io/crates/tiff) [![Further crate info](https://img.shields.io/crates/v/tiff.svg)](https://crates.io/crates/tiff)
@@ -36,6 +36,7 @@ This table lists photometric interpretations and sample formats which are suppor
| LZW | ✓ | ✓ | | LZW | ✓ | ✓ |
| Deflate | ✓ | ✓ | | Deflate | ✓ | ✓ |
| PackBits | ✓ | ✓ | | PackBits | ✓ | ✓ |
| Fax4 | ✓ | not yet |
| JPEG | ✓ | not yet | | JPEG | ✓ | not yet |
| ZSTD | ✓ | 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");
}
}
+3
View File
@@ -10,6 +10,9 @@
//! the unsafe code guidelines). //! the unsafe code guidelines).
//! //!
//! TODO: Would like to use std-lib here. //! TODO: Would like to use std-lib here.
// Until we implement predictors for f16, we don't need f16_as_ne_bytes. (Due to the macro we do
// not apply this directly to the functions). And rust-version is not new enough for `expect`.
#![allow(dead_code)]
use std::{mem, slice}; use std::{mem, slice};
use half::f16; use half::f16;
+169
View File
@@ -0,0 +1,169 @@
use crate::{tags::IfdPointer, TiffError, TiffFormatError};
use std::collections::HashMap;
/// The IFD structure of a TIFF file should be limited to a forest of entries, and a tree when we
/// only consider having a primary IFD and its children. There is up to one primary child of a node
/// that is the `next` entry in an IFD itself. Since we offer iteration over this chain we want to
/// detect when a new edge would introduce a cycle, such that callers can be guaranteed to
/// terminate iteration at some point even in malicious images.
///
/// However, we are not necessarily visiting the IFDs in their topological order.
///
/// Since we only consider one child, we run union find to assign each pointer to a known chain.
/// Then, when we insert a new edge we check if they belong to the same component.
#[derive(Default, Debug)]
pub struct IfdCycles {
/// The root of each component union.
component_union: HashMap<ComponentId, ComponentId>,
links: HashMap<IfdPointer, u64>,
chains: HashMap<IfdPointer, ComponentId>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct ComponentId(u64);
impl IfdCycles {
pub fn new() -> Self {
IfdCycles::default()
}
pub fn insert_next(
&mut self,
from: IfdPointer,
to: Option<IfdPointer>,
) -> Result<bool, TiffError> {
let to_offset = to.map_or(0, |p| p.0);
// For some reason we got
match self.links.get(&from) {
Some(existing) if *existing == to_offset => return Ok(false),
// We got here twice with two different reads of the same IFD. That is .. unusual and
// we interpret it as a cycle.
Some(_) => return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets)),
None => self.links.insert(from, to_offset),
};
self.ensure_node(from);
if let Some(to) = to {
self.ensure_node(to);
let parent = self.nominal_component(from);
let child = self.nominal_component(to);
if parent == child {
return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets));
}
self.component_union.insert(child, parent);
}
Ok(true)
}
fn ensure_node(&mut self, ifd: IfdPointer) {
self.chains.entry(ifd).or_insert_with(|| {
let id = ComponentId(self.component_union.len() as u64);
self.component_union.insert(id, id);
id
});
}
fn nominal_component(&mut self, node: IfdPointer) -> ComponentId {
let id = self.chains[&node];
let nomimal = {
let mut iter = id;
loop {
let parent = self.component_union[&iter];
if parent == iter {
break parent;
}
iter = parent;
}
};
if nomimal != id {
// Compress.
let mut iter = id;
loop {
let parent = self.component_union[&iter];
if parent == iter {
break;
}
self.component_union.insert(iter, nomimal);
iter = parent;
}
}
nomimal
}
}
#[test]
fn cycles_are_detected() {
let mut cycles = IfdCycles::new();
cycles
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x800)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x800), Some(IfdPointer(0x20)))
.expect_err("cycle must be detected");
}
#[test]
fn reflective_cycle() {
let mut cycles = IfdCycles::new();
cycles
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x20)))
.expect_err("self-referential cycle must be detected");
}
#[test]
fn late_cycle() {
let mut cycles = IfdCycles::new();
cycles
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x40)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x60), Some(IfdPointer(0x80)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x80), Some(IfdPointer(0x20)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x40), Some(IfdPointer(0x60)))
.expect_err("non-existing link is valid");
}
#[test]
fn odd_cycle() {
let mut cycles = IfdCycles::new();
cycles
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x40)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x60), Some(IfdPointer(0x80)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x80), Some(IfdPointer(0x20)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x40), Some(IfdPointer(0x80)))
.expect_err("non-existing link is valid");
}
+414 -132
View File
@@ -6,12 +6,12 @@ use std::mem;
use std::str; use std::str;
use super::stream::{ByteOrder, EndianReader}; use super::stream::{ByteOrder, EndianReader};
use crate::tags::{Tag, Type}; use crate::tags::{IfdPointer, Tag, Type, ValueBuffer};
use crate::{TiffError, TiffFormatError, TiffResult}; use crate::{TiffError, TiffFormatError, TiffResult};
use self::Value::{ use self::Value::{
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, RationalBig, SRational, SRationalBig, Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, SRational, Short, Signed, SignedBig,
Short, Signed, SignedBig, SignedByte, SignedShort, Unsigned, UnsignedBig, SignedByte, SignedShort, Unsigned, UnsignedBig,
}; };
#[allow(unused_qualifications)] #[allow(unused_qualifications)]
@@ -30,8 +30,16 @@ pub enum Value {
Double(f64), Double(f64),
List(Vec<Value>), List(Vec<Value>),
Rational(u32, u32), Rational(u32, u32),
#[deprecated(
note = "Not implemented in BigTIFF with a standard tag value",
since = "0.11.1"
)]
RationalBig(u64, u64), RationalBig(u64, u64),
SRational(i32, i32), SRational(i32, i32),
#[deprecated(
note = "Not implemented in BigTIFF with a standard tag value",
since = "0.11.1"
)]
SRationalBig(i64, i64), SRationalBig(i64, i64),
Ascii(String), Ascii(String),
Ifd(u32), Ifd(u32),
@@ -130,6 +138,25 @@ impl Value {
} }
} }
/// Turn this value into an `IfdPointer`.
///
/// Notice that this does not take an argument, a 64-bit IFD is always allowed. If the
/// difference is crucial and you do not want to be permissive you're expected to filter this
/// out before.
///
/// For compatibility the smaller sized tags should always be allowed i.e. you might use a
/// non-bigtiff's directory and its tag types and move it straight to a bigtiff. For instance
/// the SubIFD tag is defined as `LONG or IFD`:
///
/// <https://web.archive.org/web/20181105221012/https://www.awaresystems.be/imaging/tiff/tifftags/subifds.html>
pub fn into_ifd_pointer(self) -> TiffResult<IfdPointer> {
match self {
Unsigned(val) | Ifd(val) => Ok(IfdPointer(val.into())),
IfdBig(val) => Ok(IfdPointer(val)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
pub fn into_string(self) -> TiffResult<String> { pub fn into_string(self) -> TiffResult<String> {
match self { match self {
Ascii(val) => Ok(val), Ascii(val) => Ok(val),
@@ -151,7 +178,8 @@ impl Value {
Unsigned(val) => Ok(vec![val]), Unsigned(val) => Ok(vec![val]),
UnsignedBig(val) => Ok(vec![u32::try_from(val)?]), UnsignedBig(val) => Ok(vec![u32::try_from(val)?]),
Rational(numerator, denominator) => Ok(vec![numerator, denominator]), 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)?]) Ok(vec![u32::try_from(numerator)?, u32::try_from(denominator)?])
} }
Ifd(val) => Ok(vec![val]), Ifd(val) => Ok(vec![val]),
@@ -201,7 +229,8 @@ impl Value {
new_vec.push(numerator); new_vec.push(numerator);
new_vec.push(denominator); 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(numerator)?);
new_vec.push(i32::try_from(denominator)?); new_vec.push(i32::try_from(denominator)?);
} }
@@ -215,7 +244,8 @@ impl Value {
Signed(val) => Ok(vec![val]), Signed(val) => Ok(vec![val]),
SignedBig(val) => Ok(vec![i32::try_from(val)?]), SignedBig(val) => Ok(vec![i32::try_from(val)?]),
SRational(numerator, denominator) => Ok(vec![numerator, denominator]), 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)?]) Ok(vec![i32::try_from(numerator)?, i32::try_from(denominator)?])
} }
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)), _ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
@@ -264,7 +294,8 @@ impl Value {
Unsigned(val) => Ok(vec![val.into()]), Unsigned(val) => Ok(vec![val.into()]),
UnsignedBig(val) => Ok(vec![val]), UnsignedBig(val) => Ok(vec![val]),
Rational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]), 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()]), Ifd(val) => Ok(vec![val.into()]),
IfdBig(val) => Ok(vec![val]), IfdBig(val) => Ok(vec![val]),
Ascii(val) => Ok(val.chars().map(u32::from).map(u64::from).collect()), Ascii(val) => Ok(val.chars().map(u32::from).map(u64::from).collect()),
@@ -282,7 +313,8 @@ impl Value {
new_vec.push(numerator.into()); new_vec.push(numerator.into());
new_vec.push(denominator.into()); new_vec.push(denominator.into());
} }
SRationalBig(numerator, denominator) => { #[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => {
new_vec.push(numerator); new_vec.push(numerator);
new_vec.push(denominator); new_vec.push(denominator);
} }
@@ -296,12 +328,30 @@ impl Value {
Signed(val) => Ok(vec![val.into()]), Signed(val) => Ok(vec![val.into()]),
SignedBig(val) => Ok(vec![val]), SignedBig(val) => Ok(vec![val]),
SRational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]), 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)), _ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
} }
} }
pub fn into_ifd_vec(self) -> TiffResult<Vec<IfdPointer>> {
let vec = match self {
Unsigned(val) | Ifd(val) => return Ok(vec![IfdPointer(val.into())]),
IfdBig(val) => return Ok(vec![IfdPointer(val)]),
List(vec) => vec,
_ => return Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
};
vec.into_iter().map(Self::into_ifd_pointer).collect()
}
} }
/// 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)] #[derive(Clone)]
pub struct Entry { pub struct Entry {
type_: Type, type_: Type,
@@ -319,12 +369,14 @@ impl ::std::fmt::Debug for Entry {
} }
impl 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 { pub fn new(type_: Type, count: u32, offset: [u8; 4]) -> Entry {
let mut offset = offset.to_vec(); let mut entry_off = [0u8; 8];
offset.append(&mut vec![0; 4]); entry_off[..4].copy_from_slice(&offset);
Entry::new_u64(type_, count.into(), offset[..].try_into().unwrap()) 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 { pub fn new_u64(type_: Type, count: u64, offset: [u8; 8]) -> Entry {
Entry { Entry {
type_, type_,
@@ -333,8 +385,23 @@ impl Entry {
} }
} }
pub fn field_type(&self) -> Type {
self.type_
}
pub fn count(&self) -> u64 {
self.count
}
pub(crate) fn offset(&self) -> &[u8] {
&self.offset
}
/// Returns a mem_reader for the offset/value field /// 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) EndianReader::new(io::Cursor::new(self.offset.to_vec()), byte_order)
} }
@@ -350,43 +417,25 @@ impl Entry {
} }
let bo = reader.byte_order; let bo = reader.byte_order;
let value_bytes = self.type_.value_bytes(self.count)?;
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);
}
};
// Case 2: there is one value. // Case 2: there is one value.
if self.count == 1 { if self.count == 1 {
// 2a: the value is 5-8 bytes and we're in BigTiff mode. // 2a: the value is 5-8 bytes and we're in BigTiff mode.
if bigtiff && value_bytes > 4 && value_bytes <= 8 { if bigtiff && value_bytes > 4 && value_bytes <= 8 {
return Ok(match self.type_ { return Ok(match self.type_ {
Type::LONG8 => UnsignedBig(self.r(bo).read_u64()?), Type::LONG8 => UnsignedBig(self.offset_field_reader(bo).read_u64()?),
Type::SLONG8 => SignedBig(self.r(bo).read_i64()?), Type::SLONG8 => SignedBig(self.offset_field_reader(bo).read_i64()?),
Type::DOUBLE => Double(self.r(bo).read_f64()?), Type::DOUBLE => Double(self.offset_field_reader(bo).read_f64()?),
Type::RATIONAL => { Type::RATIONAL => {
let mut r = self.r(bo); let mut r = self.offset_field_reader(bo);
Rational(r.read_u32()?, r.read_u32()?) Rational(r.read_u32()?, r.read_u32()?)
} }
Type::SRATIONAL => { Type::SRATIONAL => {
let mut r = self.r(bo); let mut r = self.offset_field_reader(bo);
SRational(r.read_i32()?, r.read_i32()?) 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::BYTE
| Type::SBYTE | Type::SBYTE
| Type::ASCII | Type::ASCII
@@ -403,13 +452,13 @@ impl Entry {
// 2b: the value is at most 4 bytes or doesn't fit in the offset field. // 2b: the value is at most 4 bytes or doesn't fit in the offset field.
return Ok(match self.type_ { return Ok(match self.type_ {
Type::BYTE => Byte(self.offset[0]), Type::BYTE => Byte(self.offset[0]),
Type::SBYTE => SignedByte(i8::from(self.offset[0] as i8)), Type::SBYTE => SignedByte(self.offset[0] as i8),
Type::UNDEFINED => Byte(self.offset[0]), Type::UNDEFINED => Byte(self.offset[0]),
Type::SHORT => Short(self.r(bo).read_u16()?), Type::SHORT => Short(self.offset_field_reader(bo).read_u16()?),
Type::SSHORT => SignedShort(self.r(bo).read_i16()?), Type::SSHORT => SignedShort(self.offset_field_reader(bo).read_i16()?),
Type::LONG => Unsigned(self.r(bo).read_u32()?), Type::LONG => Unsigned(self.offset_field_reader(bo).read_u32()?),
Type::SLONG => Signed(self.r(bo).read_i32()?), Type::SLONG => Signed(self.offset_field_reader(bo).read_i32()?),
Type::FLOAT => Float(self.r(bo).read_f32()?), Type::FLOAT => Float(self.offset_field_reader(bo).read_f32()?),
Type::ASCII => { Type::ASCII => {
if self.offset[0] == 0 { if self.offset[0] == 0 {
Ascii("".to_string()) Ascii("".to_string())
@@ -418,28 +467,28 @@ impl Entry {
} }
} }
Type::LONG8 => { 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()?) UnsignedBig(reader.read_u64()?)
} }
Type::SLONG8 => { 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()?) SignedBig(reader.read_i64()?)
} }
Type::DOUBLE => { 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()?) Double(reader.read_f64()?)
} }
Type::RATIONAL => { 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()?) Rational(reader.read_u32()?, reader.read_u32()?)
} }
Type::SRATIONAL => { 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()?) 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 => { 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()?) IfdBig(reader.read_u64()?)
} }
}); });
@@ -470,7 +519,7 @@ impl Entry {
)); ));
} }
Type::SHORT => { Type::SHORT => {
let mut r = self.r(bo); let mut r = self.offset_field_reader(bo);
let mut v = Vec::new(); let mut v = Vec::new();
for _ in 0..self.count { for _ in 0..self.count {
v.push(Short(r.read_u16()?)); v.push(Short(r.read_u16()?));
@@ -478,7 +527,7 @@ impl Entry {
return Ok(List(v)); return Ok(List(v));
} }
Type::SSHORT => { Type::SSHORT => {
let mut r = self.r(bo); let mut r = self.offset_field_reader(bo);
let mut v = Vec::new(); let mut v = Vec::new();
for _ in 0..self.count { for _ in 0..self.count {
v.push(SignedShort(r.read_i16()?)); v.push(SignedShort(r.read_i16()?));
@@ -486,7 +535,7 @@ impl Entry {
return Ok(List(v)); return Ok(List(v));
} }
Type::LONG => { Type::LONG => {
let mut r = self.r(bo); let mut r = self.offset_field_reader(bo);
let mut v = Vec::new(); let mut v = Vec::new();
for _ in 0..self.count { for _ in 0..self.count {
v.push(Unsigned(r.read_u32()?)); v.push(Unsigned(r.read_u32()?));
@@ -494,7 +543,7 @@ impl Entry {
return Ok(List(v)); return Ok(List(v));
} }
Type::SLONG => { Type::SLONG => {
let mut r = self.r(bo); let mut r = self.offset_field_reader(bo);
let mut v = Vec::new(); let mut v = Vec::new();
for _ in 0..self.count { for _ in 0..self.count {
v.push(Signed(r.read_i32()?)); v.push(Signed(r.read_i32()?));
@@ -502,7 +551,7 @@ impl Entry {
return Ok(List(v)); return Ok(List(v));
} }
Type::FLOAT => { Type::FLOAT => {
let mut r = self.r(bo); let mut r = self.offset_field_reader(bo);
let mut v = Vec::new(); let mut v = Vec::new();
for _ in 0..self.count { for _ in 0..self.count {
v.push(Float(r.read_f32()?)); v.push(Float(r.read_f32()?));
@@ -510,7 +559,7 @@ impl Entry {
return Ok(List(v)); return Ok(List(v));
} }
Type::IFD => { Type::IFD => {
let mut r = self.r(bo); let mut r = self.offset_field_reader(bo);
let mut v = Vec::new(); let mut v = Vec::new();
for _ in 0..self.count { for _ in 0..self.count {
v.push(Ifd(r.read_u32()?)); v.push(Ifd(r.read_u32()?));
@@ -529,74 +578,149 @@ impl Entry {
} }
// Case 4: there is more than one value, and it doesn't fit in the offset field. // Case 4: there is more than one value, and it doesn't fit in the offset field.
let mut v;
self.set_reader_offset_relative(bigtiff, reader, 0)?;
match self.type_ { match self.type_ {
// TODO check if this could give wrong results Type::BYTE | Type::UNDEFINED => {
// at a different endianess of file/computer. v = Self::vec_with_capacity(self.count, limits)?;
Type::BYTE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| { self.decode_values(self.count, self.type_, reader, |bytes| {
let mut buf = [0; 1]; v.extend(bytes.iter().copied().map(Byte))
reader.inner().read_exact(&mut buf)?; })
Ok(UnsignedBig(u64::from(buf[0]))) }
}), Type::SBYTE => {
Type::SBYTE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| { v = Self::vec_with_capacity(self.count, limits)?;
Ok(SignedBig(i64::from(reader.read_i8()?))) self.decode_values(self.count, self.type_, reader, |bytes| {
}), v.extend(bytes.iter().copied().map(|v| SignedByte(v as i8)))
Type::SHORT => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| { })
Ok(Short(reader.read_u16()?)) }
}), Type::SHORT => {
Type::SSHORT => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| { v = Self::vec_with_capacity(self.count, limits)?;
Ok(SignedShort(reader.read_i16()?)) self.decode_values(self.count, self.type_, reader, |bytes| {
}), v.extend(
Type::LONG => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| { bytes
Ok(Unsigned(reader.read_u32()?)) .chunks_exact(2)
}), .map(|ch| Short(u16::from_ne_bytes(ch.try_into().unwrap()))),
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| { Type::SSHORT => {
Ok(Float(reader.read_f32()?)) v = Self::vec_with_capacity(self.count, limits)?;
}), self.decode_values(self.count, self.type_, reader, |bytes| {
Type::DOUBLE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| { v.extend(
Ok(Double(reader.read_f64()?)) 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 => { Type::RATIONAL => {
self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| { v = Self::vec_with_capacity(self.count, limits)?;
Ok(Rational(reader.read_u32()?, reader.read_u32()?)) 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 => { Type::SRATIONAL => {
self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| { v = Self::vec_with_capacity(self.count, limits)?;
Ok(SRational(reader.read_i32()?, reader.read_i32()?)) 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| { Type::LONG8 => {
Ok(UnsignedBig(reader.read_u64()?)) v = Self::vec_with_capacity(self.count, limits)?;
}), self.decode_values(self.count, self.type_, reader, |bytes| {
Type::SLONG8 => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| { v.extend(
Ok(SignedBig(reader.read_i64()?)) bytes
}), .chunks_exact(8)
Type::IFD => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| { .map(|ch| UnsignedBig(u64::from_ne_bytes(ch.try_into().unwrap()))),
Ok(Ifd(reader.read_u32()?)) )
}), })
Type::IFD8 => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| { }
Ok(IfdBig(reader.read_u64()?)) Type::SLONG8 => {
}), v = Self::vec_with_capacity(self.count, limits)?;
Type::UNDEFINED => { self.decode_values(self.count, self.type_, reader, |bytes| {
self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| { v.extend(
let mut buf = [0; 1]; bytes
reader.inner().read_exact(&mut buf)?; .chunks_exact(8)
Ok(Byte(buf[0])) .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 => { Type::ASCII => {
let n = usize::try_from(self.count)?; let n = usize::try_from(self.count)?;
if n > limits.decoding_buffer_size {
return Err(TiffError::LimitsExceeded);
}
if bigtiff { if n > limits.decoding_buffer_size {
reader.goto_offset(self.r(bo).read_u64()?)? return Err(dbg!(TiffError::LimitsExceeded));
} else {
reader.goto_offset(self.r(bo).read_u32()?.into())?
} }
let mut out = vec![0; n]; let mut out = vec![0; n];
@@ -605,43 +729,199 @@ impl Entry {
if let Some(first) = out.iter().position(|&b| b == 0) { if let Some(first) = out.iter().position(|&b| b == 0) {
out.truncate(first); out.truncate(first);
} }
Ok(Ascii(String::from_utf8(out)?))
return Ok(Ascii(String::from_utf8(out)?));
} }
} }?;
Ok(List(v))
} }
#[inline] pub(crate) fn buffered_value<R: Read + Seek>(
fn decode_offset<R, F>(
&self, &self,
value_count: u64, buf: &mut ValueBuffer,
bo: ByteOrder,
bigtiff: bool,
limits: &super::Limits, limits: &super::Limits,
bigtiff: bool,
reader: &mut EndianReader<R>, reader: &mut EndianReader<R>,
decode_fn: F, ) -> TiffResult<()> {
) -> TiffResult<Value> if self.count == 0 {
where buf.assume_type(self.type_, 0, reader.byte_order);
R: Read + Seek, return Ok(());
F: Fn(&mut EndianReader<R>) -> TiffResult<Value>, }
{
let value_bytes = self.buffer_with_capacity(buf, limits)?;
// Case 1: the value fits in the offset field.
if value_bytes <= 4 || bigtiff && value_bytes <= 8 {
let src = &self.offset[..value_bytes];
buf.raw_bytes_mut()[..value_bytes].copy_from_slice(src);
buf.assume_type(self.type_, self.count, reader.byte_order);
return Ok(());
}
// Case 2: the value is stored in the reader at an offset.
self.set_reader_offset_relative(bigtiff, reader, 0)?;
// In case of an error we set the type and endianess.
buf.assume_type(self.type_, 0, reader.byte_order);
let target = &mut buf.raw_bytes_mut()[..value_bytes];
// FIXME: if the read fails we have already grown to full size, which is not great.
reader.inner().read_exact(target)?;
buf.assume_type(self.type_, self.count, reader.byte_order);
Ok(())
}
pub(crate) fn raw_value_at<R: Read + Seek>(
&self,
buf: &mut [u8],
bigtiff: bool,
reader: &mut EndianReader<R>,
at: u64,
) -> TiffResult<usize> {
if self.count == 0 {
return Ok(0);
}
// We have no limits to handle, we do not allocate.
let value_bytes = self.type_.value_bytes(self.count)?;
// No bytes to fill into the buffer.
if at >= value_bytes {
return Ok(0);
}
// Case 1: the value fits in the offset field.
if value_bytes <= 4 || bigtiff && value_bytes <= 8 {
// `at < value_bytes` and `value_bytes <= 8` so casting is mathematical
let src = &self.offset[..value_bytes as usize][at as usize..];
let len = src.len().min(buf.len());
buf[..len].copy_from_slice(&src[..len]);
return Ok(value_bytes as usize);
}
// Case 2: the value is stored in the reader at an offset. We will find the offset
// encoded in the entry, apply the relative start position and seek there.
self.set_reader_offset_relative(bigtiff, reader, at)?;
let remainder = value_bytes - at;
let len = usize::try_from(remainder)
.unwrap_or(usize::MAX)
.min(buf.len());
let target = &mut buf[..len];
reader.inner().read_exact(target)?;
// Design note: in a previous draft we would consume the rest of the bytes of this value
// here (into a stack buffer if need be) to verify the stream itself. But in the end we
// have `Seek` so we better verify this by seeking over the rest of the bytes, finding if
// the stream continues that far. Even that is maybe bad if we wanted to provide a
// async-adaptor that `WouldBlock` errors to fill back a read window then the seek is
// poison to that, too.
// So a really simple choice: The caller is responsible for handling the fact that this did
// not verify the whole value. Attempt a 1-byte read at the end of the value instead?
Ok(len)
}
// Returns `Ok(bytes)` if our value's bytes through type and count fit into `usize` and are
// within the limits. Extends the buffer to that many bytes.
fn buffer_with_capacity(
&self,
buf: &mut ValueBuffer,
limits: &super::Limits,
) -> TiffResult<usize> {
let bytes = self.type_.value_bytes(self.count())?;
let allowed_length = usize::try_from(bytes)
.ok()
.filter(|&n| n <= limits.decoding_buffer_size)
.ok_or(TiffError::LimitsExceeded)?;
buf.prepare_length(allowed_length);
Ok(allowed_length)
}
fn vec_with_capacity(
value_count: u64,
limits: &super::Limits,
) -> Result<Vec<Value>, TiffError> {
let value_count = usize::try_from(value_count)?; let value_count = usize::try_from(value_count)?;
if value_count > limits.decoding_buffer_size / mem::size_of::<Value>() { if value_count > limits.decoding_buffer_size / mem::size_of::<Value>() {
return Err(TiffError::LimitsExceeded); return Err(TiffError::LimitsExceeded);
} }
let mut v = Vec::with_capacity(value_count); Ok(Vec::with_capacity(value_count))
}
/// Seek to an offset within a value stored in the offset defined by this entry.
fn set_reader_offset_relative<R>(
&self,
bigtiff: bool,
reader: &mut EndianReader<R>,
at: u64,
) -> TiffResult<()>
where
R: Read + Seek,
{
let bo = reader.byte_order;
let offset = if bigtiff { let offset = if bigtiff {
self.r(bo).read_u64()? self.offset_field_reader(bo).read_u64()?
} else { } else {
self.r(bo).read_u32()?.into() self.offset_field_reader(bo).read_u32()?.into()
}; };
// FIXME: `at` should be within `self.type_.value_bytes(self.count)` and that itself should
// be within the bounds of the stream. But we do not check this eagerly so this below will
// fail sometimes differently for exotic streams, depending on the method by which we read
// (at once or through multiple raw into-byte-slice reads).
let offset = offset.checked_add(at).ok_or(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
))?;
reader.goto_offset(offset)?; reader.goto_offset(offset)?;
for _ in 0..value_count { Ok(())
v.push(decode_fn(reader)?) }
#[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(())
} }
} }
@@ -668,4 +948,6 @@ fn offset_to_sbytes(n: usize, entry: &Entry) -> TiffResult<Value> {
} }
/// Type representing an Image File Directory /// Type representing an Image File Directory
#[doc(hidden)]
#[deprecated = "Use struct `tiff::Directory` instead which contains all fields relevant to an Image File Directory, including the offset to the next directory"]
pub type Directory = HashMap<Tag, Entry>; pub type Directory = HashMap<Tag, Entry>;
+542 -106
View File
@@ -1,12 +1,16 @@
use super::ifd::{Directory, Value}; use super::ifd::Value;
use super::stream::{ByteOrder, PackBitsReader}; use super::stream::PackBitsReader;
use super::tag_reader::TagReader; use super::tag_reader::TagReader;
use super::{predict_f16, predict_f32, predict_f64, Limits}; use super::ChunkType;
use super::{stream::EndianReader, ChunkType}; use super::{predict_f16, predict_f32, predict_f64, ValueReader};
use crate::tags::{ use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag, CompressionMethod, ExtraSamples, PhotometricInterpretation, PlanarConfiguration, Predictor,
SampleFormat, Tag,
}; };
use crate::{ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError}; use crate::{
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
};
use std::io::{self, Cursor, Read, Seek}; use std::io::{self, Cursor, Read, Seek};
use std::sync::Arc; use std::sync::Arc;
@@ -27,10 +31,10 @@ pub(crate) struct TileAttributes {
impl TileAttributes { impl TileAttributes {
pub fn tiles_across(&self) -> usize { pub fn tiles_across(&self) -> usize {
(self.image_width + self.tile_width - 1) / self.tile_width self.image_width.div_ceil(self.tile_width)
} }
pub fn tiles_down(&self) -> usize { pub fn tiles_down(&self) -> usize {
(self.image_height + self.tile_length - 1) / self.tile_length self.image_height.div_ceil(self.tile_length)
} }
fn padding_right(&self) -> usize { fn padding_right(&self) -> usize {
(self.tile_width - self.image_width % self.tile_width) % self.tile_width (self.tile_width - self.image_width % self.tile_width) % self.tile_width
@@ -65,6 +69,11 @@ pub(crate) struct Image {
pub height: u32, pub height: u32,
pub bits_per_sample: u8, pub bits_per_sample: u8,
pub samples: u16, 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 sample_format: SampleFormat,
pub photometric_interpretation: PhotometricInterpretation, pub photometric_interpretation: PhotometricInterpretation,
pub compression_method: CompressionMethod, pub compression_method: CompressionMethod,
@@ -76,21 +85,66 @@ pub(crate) struct Image {
pub tile_attributes: Option<TileAttributes>, pub tile_attributes: Option<TileAttributes>,
pub chunk_offsets: Vec<u64>, pub chunk_offsets: Vec<u64>,
pub chunk_bytes: Vec<u64>, pub chunk_bytes: Vec<u64>,
pub chroma_subsampling: (u16, u16),
}
/// 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 { impl Image {
pub fn from_reader<R: Read + Seek>( pub fn from_reader<R: Read + Seek>(
reader: &mut EndianReader<R>, decoder: &mut ValueReader<R>,
ifd: Directory, ifd: Directory,
limits: &Limits,
bigtiff: bool,
) -> TiffResult<Image> { ) -> TiffResult<Image> {
let mut tag_reader = TagReader { let mut tag_reader = TagReader { decoder, ifd: &ifd };
reader,
limits,
ifd: &ifd,
bigtiff,
};
let width = tag_reader.require_tag(Tag::ImageWidth)?.into_u32()?; let width = tag_reader.require_tag(Tag::ImageWidth)?.into_u32()?;
let height = tag_reader.require_tag(Tag::ImageLength)?.into_u32()?; let height = tag_reader.require_tag(Tag::ImageLength)?.into_u32()?;
@@ -115,7 +169,7 @@ impl Image {
}; };
let jpeg_tables = if compression_method == CompressionMethod::ModernJPEG let jpeg_tables = if compression_method == CompressionMethod::ModernJPEG
&& ifd.contains_key(&Tag::JPEGTables) && ifd.contains(Tag::JPEGTables)
{ {
let vec = tag_reader let vec = tag_reader
.find_tag(Tag::JPEGTables)? .find_tag(Tag::JPEGTables)?
@@ -137,10 +191,30 @@ impl Image {
.map(Value::into_u16) .map(Value::into_u16)
.transpose()? .transpose()?
.unwrap_or(1); .unwrap_or(1);
if samples == 0 { if samples == 0 {
return Err(TiffFormatError::SamplesPerPixelIsZero.into()); 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)? { let sample_format = match tag_reader.find_tag_uint_vec(Tag::SampleFormat)? {
Some(vals) => { Some(vals) => {
let sample_format: Vec<_> = vals let sample_format: Vec<_> = vals
@@ -148,12 +222,15 @@ impl Image {
.map(SampleFormat::from_u16_exhaustive) .map(SampleFormat::from_u16_exhaustive)
.collect(); .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. // 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()); return Err(TiffUnsupportedError::UnsupportedSampleFormat(sample_format).into());
} }
format
sample_format[0]
} }
None => SampleFormat::Uint, None => SampleFormat::Uint,
}; };
@@ -164,7 +241,7 @@ impl Image {
// Technically bits_per_sample.len() should be *equal* to samples, but libtiff also allows // 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. // 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( return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered, TiffFormatError::InconsistentSizesEncountered,
)); ));
@@ -199,6 +276,25 @@ impl Image {
.transpose()? .transpose()?
.unwrap_or(PlanarConfiguration::Chunky); .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 { let planes = match planar_config {
PlanarConfiguration::Chunky => 1, PlanarConfiguration::Chunky => 1,
PlanarConfiguration::Planar => samples, PlanarConfiguration::Planar => samples,
@@ -210,10 +306,10 @@ impl Image {
let strip_decoder; let strip_decoder;
let tile_attributes; let tile_attributes;
match ( match (
ifd.contains_key(&Tag::StripByteCounts), ifd.contains(Tag::StripByteCounts),
ifd.contains_key(&Tag::StripOffsets), ifd.contains(Tag::StripOffsets),
ifd.contains_key(&Tag::TileByteCounts), ifd.contains(Tag::TileByteCounts),
ifd.contains_key(&Tag::TileOffsets), ifd.contains(Tag::TileOffsets),
) { ) {
(true, true, false, false) => { (true, true, false, false) => {
chunk_type = ChunkType::Strip; chunk_type = ChunkType::Strip;
@@ -297,6 +393,8 @@ impl Image {
height, height,
bits_per_sample: bits_per_sample[0], bits_per_sample: bits_per_sample[0],
samples, samples,
extra_samples,
photometric_samples,
sample_format, sample_format,
photometric_interpretation, photometric_interpretation,
compression_method, compression_method,
@@ -308,19 +406,24 @@ impl Image {
tile_attributes, tile_attributes,
chunk_offsets, chunk_offsets,
chunk_bytes, chunk_bytes,
chroma_subsampling,
}) })
} }
pub(crate) fn colortype(&self) -> TiffResult<ColorType> { 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 { match self.photometric_interpretation {
PhotometricInterpretation::RGB => match self.samples { PhotometricInterpretation::RGB => match self.photometric_samples {
3 => Ok(ColorType::RGB(self.bits_per_sample)), 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)), 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( _ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits( TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation, self.photometric_interpretation,
@@ -328,8 +431,12 @@ impl Image {
), ),
)), )),
}, },
PhotometricInterpretation::CMYK => match self.samples { PhotometricInterpretation::CMYK => match self.photometric_samples {
4 => Ok(ColorType::CMYK(self.bits_per_sample)), 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)), 5 => Ok(ColorType::CMYKA(self.bits_per_sample)),
_ => Err(TiffError::UnsupportedError( _ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits( TiffUnsupportedError::InterpretationWithBits(
@@ -338,7 +445,7 @@ impl Image {
), ),
)), )),
}, },
PhotometricInterpretation::YCbCr => match self.samples { PhotometricInterpretation::YCbCr => match self.photometric_samples {
3 => Ok(ColorType::YCbCr(self.bits_per_sample)), 3 => Ok(ColorType::YCbCr(self.bits_per_sample)),
_ => Err(TiffError::UnsupportedError( _ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits( TiffUnsupportedError::InterpretationWithBits(
@@ -347,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 => { 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 { match self.samples {
1 => Ok(ColorType::Gray(self.bits_per_sample)), 1 => Ok(ColorType::Gray(self.bits_per_sample)),
_ => Ok(ColorType::Multiband { _ => Ok(ColorType::Multiband {
@@ -356,24 +469,70 @@ impl Image {
}), }),
} }
} }
// TODO: this is bad we should not fail at this point // ```
PhotometricInterpretation::RGBPalette // struct IccLab /* Interpretation 9* {
| PhotometricInterpretation::TransparencyMask // pub L: u8, // SampleFormat::Uint
| PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError( // 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( TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation, PhotometricInterpretation::CIELab,
vec![self.bits_per_sample; self.samples as usize], 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],
),
))
}
} }
} }
fn create_reader<'r, R: 'r + Read>( fn create_reader<'r, R: 'r + Read>(
reader: R, reader: R,
#[allow(unused_variables)] photometric_interpretation: PhotometricInterpretation,
compression_method: CompressionMethod, compression_method: CompressionMethod,
compressed_length: u64, 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>> { ) -> TiffResult<Box<dyn Read + 'r>> {
Ok(match compression_method { Ok(match compression_method {
CompressionMethod::None => Box::new(reader), CompressionMethod::None => Box::new(reader),
@@ -425,38 +584,29 @@ impl Image {
let mut jpeg_data = Vec::new(); let mut jpeg_data = Vec::new();
jpeg_reader.read_to_end(&mut jpeg_data)?; 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(); let mut options: zune_core::options::DecoderOptions = Default::default();
match photometric_interpretation {
PhotometricInterpretation::RGB => { // Disable color conversion by setting the output colorspace to the input
options = // colorspace.
options.jpeg_set_out_colorspace(zune_core::colorspace::ColorSpace::RGB); decoder.decode_headers()?;
} if let Some(colorspace) = decoder.input_colorspace() {
PhotometricInterpretation::CMYK => { options = options.jpeg_set_out_colorspace(colorspace);
options = options
.jpeg_set_out_colorspace(zune_core::colorspace::ColorSpace::CMYK);
}
PhotometricInterpretation::YCbCr => {
options = options
.jpeg_set_out_colorspace(zune_core::colorspace::ColorSpace::YCbCr);
}
PhotometricInterpretation::WhiteIsZero
| PhotometricInterpretation::BlackIsZero
| PhotometricInterpretation::TransparencyMask => {}
PhotometricInterpretation::RGBPalette | PhotometricInterpretation::CIELab => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedInterpretation(
photometric_interpretation,
),
));
}
} }
decoder.set_options(options); decoder.set_options(options);
let data = decoder.decode()?; let data = decoder.decode()?;
Box::new(Cursor::new(data)) Box::new(Cursor::new(data))
} }
#[cfg(feature = "fax")]
CompressionMethod::Fax4 => Box::new(super::stream::Group4Reader::new(
dimensions,
reader,
compressed_length,
)?),
method => { method => {
return Err(TiffError::UnsupportedError( return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedCompressionMethod(method), TiffUnsupportedError::UnsupportedCompressionMethod(method),
@@ -472,18 +622,25 @@ impl Image {
/// Example with `bits_per_sample = [8, 8, 8]` and `PhotometricInterpretation::RGB`: /// Example with `bits_per_sample = [8, 8, 8]` and `PhotometricInterpretation::RGB`:
/// * `PlanarConfiguration::Chunky` -> 3 (RGBRGBRGB...) /// * `PlanarConfiguration::Chunky` -> 3 (RGBRGBRGB...)
/// * `PlanarConfiguration::Planar` -> 1 (RRR...) (GGG...) (BBB...) /// * `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 { 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, PlanarConfiguration::Planar => 1,
} }
} }
/// Number of strips per pixel. /// 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 { match self.planar_config {
PlanarConfiguration::Chunky => 1, PlanarConfiguration::Chunky => 1,
PlanarConfiguration::Planar => self.samples.into(), PlanarConfiguration::Planar => self.samples,
} }
} }
@@ -521,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)> { pub(crate) fn chunk_data_dimensions(&self, chunk_index: u32) -> TiffResult<(u32, u32)> {
let dims = self.chunk_dimensions()?; let dims = self.chunk_dimensions()?;
match self.chunk_type { match self.chunk_type {
ChunkType::Strip => { ChunkType::Strip => {
let strip_attrs = self.strip_decoder.as_ref().unwrap(); let rows_per_strip = dims.1;
let strips_per_band = let strips_per_band = self.height.div_ceil(rows_per_strip);
self.height.saturating_sub(1) / strip_attrs.rows_per_strip + 1;
let strip_height_without_padding = (chunk_index % strips_per_band) let strip_height_without_padding = (chunk_index % strips_per_band)
.checked_mul(dims.1) .checked_mul(dims.1)
.and_then(|x| self.height.checked_sub(x)) .and_then(|x| self.height.checked_sub(x))
@@ -555,15 +805,22 @@ impl Image {
pub(crate) fn expand_chunk( pub(crate) fn expand_chunk(
&self, &self,
reader: impl Read, reader: &mut ValueReader<impl Read>,
buf: &mut [u8], buf: &mut [u8],
output_row_stride: usize, layout: &ReadoutLayout,
byte_order: ByteOrder,
chunk_index: u32, chunk_index: u32,
limits: &Limits,
) -> TiffResult<()> { ) -> TiffResult<()> {
let ValueReader {
reader,
bigtiff: _,
limits,
} = reader;
let byte_order = reader.byte_order;
// Validate that the color type is supported. // Validate that the color type is supported.
let color_type = self.colortype()?; let color_type = layout.color;
match color_type { match color_type {
ColorType::RGB(n) ColorType::RGB(n)
| ColorType::RGBA(n) | ColorType::RGBA(n)
@@ -625,6 +882,7 @@ impl Image {
.ok_or(TiffError::FormatError( .ok_or(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered, TiffFormatError::InconsistentSizesEncountered,
))?; ))?;
if *compressed_bytes > limits.intermediate_buffer_size as u64 { if *compressed_bytes > limits.intermediate_buffer_size as u64 {
return Err(TiffError::LimitsExceeded); return Err(TiffError::LimitsExceeded);
} }
@@ -632,38 +890,43 @@ impl Image {
let compression_method = self.compression_method; let compression_method = self.compression_method;
let photometric_interpretation = self.photometric_interpretation; let photometric_interpretation = self.photometric_interpretation;
let predictor = self.predictor; 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 chunk_dims = self.chunk_dimensions()?;
let data_dims = self.chunk_data_dimensions(chunk_index)?; let data_dims = self.chunk_data_dimensions(chunk_index)?;
let chunk_row_bits = (u64::from(chunk_dims.0) * u64::from(self.bits_per_sample)) let chunk_row_bytes: usize = layout.tiff_row_bytes;
.checked_mul(samples as u64) let data_row_bytes: usize = layout.chunk_row_bytes(data_dims.0)?;
.ok_or(TiffError::LimitsExceeded)?;
let chunk_row_bytes: usize = ((chunk_row_bits + 7) / 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 + 7) / 8).try_into()?;
// TODO: Should these return errors instead? // TODO: Should these return errors instead?
assert!(output_row_stride >= data_row_bytes); assert!(layout.minimum_row_stride >= data_row_bytes);
assert!(buf.len() >= output_row_stride * (data_dims.1 as usize - 1) + 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( let mut reader = Self::create_reader(
reader, reader.inner(),
photometric_interpretation,
compression_method, compression_method,
*compressed_bytes, *compressed_bytes,
self.jpeg_tables.as_deref().map(|a| &**a), self.jpeg_tables.as_deref().map(|a| &**a),
chunk_dims,
)?; )?;
if output_row_stride == chunk_row_bytes as usize { 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]; let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize];
reader.read_exact(tile)?; reader.read_exact(tile)?;
for row in tile.chunks_mut(chunk_row_bytes as usize) { for row in tile.chunks_mut(chunk_row_bytes) {
super::fix_endianness_and_predict( super::fix_endianness_and_predict(
row, row,
color_type.bit_depth(), color_type.bit_depth(),
@@ -672,14 +935,15 @@ impl Image {
predictor, predictor,
); );
} }
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero { 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 { } else if chunk_row_bytes > data_row_bytes && self.predictor == Predictor::FloatingPoint {
// The floating point predictor shuffles the padding bytes into the encoded output, so // The floating point predictor shuffles the padding bytes into the encoded output, so
// this case is handled specially when needed. // this case is handled specially when needed.
let mut encoded = vec![0u8; chunk_row_bytes]; 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)?; reader.read_exact(&mut encoded)?;
let row = &mut row[..data_row_bytes]; let row = &mut row[..data_row_bytes];
@@ -690,18 +954,19 @@ impl Image {
_ => unreachable!(), _ => unreachable!(),
} }
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero { if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(row, color_type, self.sample_format); super::invert_colors(row, color_type, self.sample_format)?;
} }
} }
} else { } else if is_all_bits {
for (i, row) in buf // We read row-by-row but each row fits in its output buffer.
.chunks_mut(output_row_stride) for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
.take(data_dims.1 as usize)
.enumerate()
{
let row = &mut row[..data_row_bytes]; 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 // Skip horizontal padding
if chunk_row_bytes > data_row_bytes { if chunk_row_bytes > data_row_bytes {
let len = u64::try_from(chunk_row_bytes - data_row_bytes)?; let len = u64::try_from(chunk_row_bytes - data_row_bytes)?;
@@ -715,12 +980,183 @@ impl Image {
byte_order, byte_order,
predictor, predictor,
); );
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero { 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(()) 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)
}
} }
+1184 -401
View File
File diff suppressed because it is too large. Load diff
+121 -39
View File
@@ -1,15 +1,7 @@
//! All IO functionality needed for TIFF decoding //! All IO functionality needed for TIFF decoding
use std::io::{self, BufRead, BufReader, Read, Seek, Take}; use std::io::{self, BufRead, BufReader, Read, Seek, Take};
/// Byte order of the TIFF file. pub use crate::tags::ByteOrder;
#[derive(Clone, Copy, Debug)]
pub enum ByteOrder {
/// little endian byte order
LittleEndian,
/// big endian byte order
BigEndian,
}
/// Reader that is aware of the byte order. /// Reader that is aware of the byte order.
#[derive(Debug)] #[derive(Debug)]
@@ -18,7 +10,7 @@ pub struct EndianReader<R> {
pub(crate) byte_order: ByteOrder, pub(crate) byte_order: ByteOrder,
} }
impl<R: Read + Seek> EndianReader<R> { impl<R: Read> EndianReader<R> {
pub fn new(reader: R, byte_order: ByteOrder) -> Self { pub fn new(reader: R, byte_order: ByteOrder) -> Self {
Self { reader, byte_order } Self { reader, byte_order }
} }
@@ -27,7 +19,10 @@ impl<R: Read + Seek> EndianReader<R> {
&mut self.reader &mut self.reader
} }
pub fn goto_offset(&mut self, offset: u64) -> io::Result<()> { pub fn goto_offset(&mut self, offset: u64) -> io::Result<()>
where
R: Seek,
{
self.reader.seek(io::SeekFrom::Start(offset))?; self.reader.seek(io::SeekFrom::Start(offset))?;
Ok(()) Ok(())
} }
@@ -43,17 +38,6 @@ impl<R: Read + Seek> 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 /// Reads an i16
#[inline(always)] #[inline(always)]
pub fn read_i16(&mut self) -> Result<i16, io::Error> { pub fn read_i16(&mut self) -> Result<i16, io::Error> {
@@ -132,20 +116,17 @@ impl<R: Read + Seek> EndianReader<R> {
} }
} }
/// //
/// # READERS // # READERS
/// //
///
/// ## Deflate Reader
///
/// Type alias for the deflate Reader
#[cfg(feature = "deflate")] #[cfg(feature = "deflate")]
pub type DeflateReader<R> = flate2::read::ZlibDecoder<R>; pub type DeflateReader<R> = flate2::read::ZlibDecoder<R>;
/// //
/// ## LZW Reader // ## LZW Reader
/// //
/// Reader that decompresses LZW streams /// Reader that decompresses LZW streams
#[cfg(feature = "lzw")] #[cfg(feature = "lzw")]
@@ -158,12 +139,15 @@ pub struct LZWReader<R: Read> {
impl<R: Read> LZWReader<R> { impl<R: Read> LZWReader<R> {
/// Wraps a reader /// Wraps a reader
pub fn new(reader: R, compressed_length: usize) -> LZWReader<R> { pub fn new(reader: R, compressed_length: usize) -> LZWReader<R> {
let configuration =
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
.with_yield_on_full_buffer(true);
Self { Self {
reader: BufReader::with_capacity( reader: BufReader::with_capacity(
(32 * 1024).min(compressed_length), (32 * 1024).min(compressed_length),
reader.take(u64::try_from(compressed_length).unwrap()), reader.take(u64::try_from(compressed_length).unwrap()),
), ),
decoder: weezl::decode::Decoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8), decoder: configuration.build(),
} }
} }
} }
@@ -184,9 +168,6 @@ impl<R: Read> Read for LZWReader<R> {
} }
} }
Ok(weezl::LzwStatus::NoProgress) => { 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( return Err(io::Error::new(
io::ErrorKind::UnexpectedEof, io::ErrorKind::UnexpectedEof,
"no lzw end code found", "no lzw end code found",
@@ -201,10 +182,11 @@ impl<R: Read> Read for LZWReader<R> {
} }
} }
/// //
/// ## PackBits Reader // ## PackBits Reader
/// //
/// Internal state machine for the PackBitsReader.
enum PackBitsReaderState { enum PackBitsReaderState {
Header, Header,
Literal, Literal,
@@ -272,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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
+77 -16
View File
@@ -1,25 +1,85 @@
use std::io::{Read, Seek}; use std::io::{Read, Seek};
use crate::tags::Tag; use crate::{
use crate::{TiffError, TiffFormatError, TiffResult}; tags::{Tag, ValueBuffer},
Directory, TiffError, TiffFormatError, TiffResult,
};
use super::ifd::{Directory, Value}; use super::ifd::{Entry, Value};
use super::stream::EndianReader; use super::ValueReader;
use super::Limits;
pub(crate) struct TagReader<'a, R: Read + Seek> { pub(crate) struct TagReader<'lt, R: EntryDecoder + ?Sized> {
pub reader: &'a mut EndianReader<R>, pub(crate) decoder: &'lt mut R,
pub ifd: &'a Directory, pub(crate) ifd: &'lt Directory,
pub limits: &'a Limits,
pub bigtiff: bool,
} }
impl<'a, R: Read + Seek> TagReader<'a, R> {
pub(crate) fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<Value>> { pub(crate) trait EntryDecoder {
Ok(match self.ifd.get(&tag) { /// Turn an entry into a value by fetching the required bytes in the stream.
Some(entry) => Some(entry.clone().val(self.limits, self.bigtiff, self.reader)?), fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value>;
None => None, /// Fill a `ValueBuffer` by fetching the required bytes.
}) fn entry_buf(&mut self, entry: &Entry, _: &mut ValueBuffer) -> TiffResult<()>;
/// Fill the prefix of the buffer and return the actual bytes. Start interpreting the inner
/// value at `offset`.
fn entry_raw(&mut self, entry: &Entry, _: &mut [u8], offset: u64) -> TiffResult<usize>;
}
impl<R: Seek + Read> EntryDecoder for ValueReader<R> {
fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value> {
entry.val(&self.limits, self.bigtiff, &mut self.reader)
} }
fn entry_buf(&mut self, entry: &Entry, buf: &mut ValueBuffer) -> TiffResult<()> {
entry.buffered_value(buf, &self.limits, self.bigtiff, &mut self.reader)
}
fn entry_raw(&mut self, entry: &Entry, buf: &mut [u8], offset: u64) -> TiffResult<usize> {
entry.raw_value_at(buf, self.bigtiff, &mut self.reader, offset)
}
}
impl<'a, R: EntryDecoder + ?Sized> TagReader<'a, R> {
pub(crate) fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<Value>> {
let entry = match self.ifd.get(tag) {
None => return Ok(None),
Some(entry) => entry.clone(),
};
self.decoder.entry_val(&entry).map(Some)
}
/// Find a tag and fill the provided buffer with its raw data.
///
/// Returns the entry of the tag if found. This signals that the buffer was overwritten.
/// Otherwise the buffer is preserved as-is.
pub(crate) fn find_tag_buf(
&mut self,
tag: Tag,
buf: &mut ValueBuffer,
) -> TiffResult<Option<Entry>> {
let entry = match self.ifd.get(tag) {
None => return Ok(None),
Some(entry) => entry.clone(),
};
self.decoder.entry_buf(&entry, buf)?;
Ok(Some(entry))
}
pub(crate) fn find_tag_raw(
&mut self,
tag: Tag,
buf: &mut [u8],
at: u64,
) -> TiffResult<Option<usize>> {
let entry = match self.ifd.get(tag) {
None => return Ok(None),
Some(entry) => entry.clone(),
};
let len = self.decoder.entry_raw(&entry, buf, at)?;
Ok(Some(len))
}
pub(crate) fn require_tag(&mut self, tag: Tag) -> TiffResult<Value> { pub(crate) fn require_tag(&mut self, tag: Tag) -> TiffResult<Value> {
match self.find_tag(tag)? { match self.find_tag(tag)? {
Some(val) => Ok(val), Some(val) => Ok(val),
@@ -28,6 +88,7 @@ impl<'a, R: Read + Seek> TagReader<'a, R> {
)), )),
} }
} }
pub fn find_tag_uint_vec<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<Option<Vec<T>>> { pub fn find_tag_uint_vec<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<Option<Vec<T>>> {
self.find_tag(tag)? self.find_tag(tag)?
.map(|v| v.into_u64_vec()) .map(|v| v.into_u64_vec())
+196
View File
@@ -0,0 +1,196 @@
use core::fmt;
use std::{collections::BTreeMap, num::NonZeroU64};
use crate::{
decoder::ifd::Entry,
tags::{IfdPointer, Tag},
};
/// An Image File Directory (IFD).
///
/// A directory is a map of [`Tag`]s to [`Value`](crate::decoder::ifd::Value)s. The values are
/// stored anywhere in the file, with the directory containing the offsets and length of the
/// associated values for each tag present in the directory.
///
/// A directory can be created with with
/// [`Decoder::read_directory`](crate::decoder::Decoder::read_directory) or as an empty directory
/// to be extended with entries. A directory may be used with
/// [`Decoder::read_directory_tags`](crate::decoder::Decoder::read_directory_tags) to read the
/// values associated with tags from an underlying file.
#[doc(alias = "IFD")]
pub struct Directory {
/// There are at most `u16::MAX` entries in any single directory, the count is stored as a
/// 2-byte value. The order in the file is implied to be ascending by tag value (the decoder
/// does not mind unordered entries).
pub(crate) entries: BTreeMap<u16, Entry>,
pub(crate) next_ifd: Option<NonZeroU64>,
}
impl Directory {
/// Create a directory in an initial state without entries. Note that an empty directory can
/// not be encoded in a file, it must contain at least one entry.
pub fn empty() -> Self {
Directory {
entries: BTreeMap::new(),
next_ifd: None,
}
}
/// Retrieve the value associated with a tag.
pub fn get(&self, tag: Tag) -> Option<&Entry> {
self.entries.get(&tag.to_u16())
}
/// Check if the directory contains a specified tag.
pub fn contains(&self, tag: Tag) -> bool {
self.entries.contains_key(&tag.to_u16())
}
/// Iterate over all known and unknown tags in this directory.
pub fn iter(&self) -> impl Iterator<Item = (Tag, &Entry)> + '_ {
self.entries
.iter()
.map(|(k, v)| (Tag::from_u16_exhaustive(*k), v))
}
/// Insert additional entries into the directory.
///
/// Note that a directory can contain at most `u16::MAX` values. There may be one entry that
/// does not fit into the directory. This entry is silently ignored (please check [`Self::len`]
/// to detect the condition). Providing a tag multiple times or a tag that already exists
/// within this directory overwrites the entry.
pub fn extend(&mut self, iter: impl IntoIterator<Item = (Tag, Entry)>) {
// Code size conscious extension, avoid monomorphic extensions with the assumption of these
// not being performance sensitive in practice. (Maybe we have a polymorphic interface for
// the crate usage in the future.
self.extend_inner(iter.into_iter().by_ref())
}
/// Get the number of entries.
pub fn len(&self) -> usize {
// The keys are `u16`. Since IFDs are required to have at least one entry this would have
// been a trivial thing to do in the specification by storing it minus one but alas. In
// BigTIFF the count is stored as 8-bit anyways.
self.entries.len()
}
/// Check if there are any entries in this directory. Note that an empty directory can not be
/// encoded in the file, it must contain at least one entry.
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// Get the pointer to the next IFD, if it was defined.
pub fn next(&self) -> Option<IfdPointer> {
self.next_ifd.map(|n| IfdPointer(n.get()))
}
pub fn set_next(&mut self, next: Option<IfdPointer>) {
self.next_ifd = next.and_then(|n| NonZeroU64::new(n.0));
}
fn extend_inner(&mut self, iter: &mut dyn Iterator<Item = (Tag, Entry)>) {
for (tag, entry) in iter {
// If the tag is already present, it will be overwritten.
let map_entry = self.entries.entry(tag.to_u16());
match map_entry {
std::collections::btree_map::Entry::Vacant(vacant_entry) => {
vacant_entry.insert(entry);
}
std::collections::btree_map::Entry::Occupied(mut occupied_entry) => {
occupied_entry.insert(entry);
}
}
}
}
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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Directory")
.field(
"entries",
&self.entries.iter().map(|(k, v)| (Tag::from_u16(*k), v)),
)
.field("next_ifd", &self.next_ifd)
.finish()
}
}
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;
use crate::{decoder::ifd::Entry, tags::Tag};
#[test]
fn directory_multiple_entries() {
let mut dir = Directory::empty();
assert_eq!(dir.len(), 0);
dir.extend((0..=u16::MAX).map(|i| {
let tag = Tag::Unknown(1);
let entry = Entry::new_u64(crate::tags::Type::BYTE, i.into(), [0; 8]);
(tag, entry)
}));
assert_eq!(dir.len(), 1, "Only one tag was ever modified");
assert_eq!(
dir.get(Tag::Unknown(1))
.expect("tag 1 should be present after this chain")
.count(),
u16::MAX.into()
);
}
#[test]
fn iteration_order() {
let mut dir = Directory::empty();
assert_eq!(dir.len(), 0);
let fake_entry = Entry::new_u64(crate::tags::Type::BYTE, 0, [0; 8]);
dir.extend((0..32).map(|i| {
let tag = Tag::Unknown(i);
let entry = fake_entry.clone();
(tag, entry)
}));
let iter_order: Vec<u16> = dir.iter().map(|(tag, _e)| tag.to_u16()).collect();
assert_eq!(
iter_order,
(0..32).collect::<Vec<_>>(),
"Tags must be in ascending order according to the specification"
);
}
}
+2 -6
View File
@@ -11,21 +11,17 @@ pub struct Deflate {
/// It allows trading compression ratio for compression speed. /// It allows trading compression ratio for compression speed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive] #[non_exhaustive]
#[derive(Default)]
pub enum DeflateLevel { pub enum DeflateLevel {
/// The fastest possible compression mode. /// The fastest possible compression mode.
Fast = 1, Fast = 1,
/// The conserative choice between speed and ratio. /// The conserative choice between speed and ratio.
#[default]
Balanced = 6, Balanced = 6,
/// The best compression available with Deflate. /// The best compression available with Deflate.
Best = 9, Best = 9,
} }
impl Default for DeflateLevel {
fn default() -> Self {
DeflateLevel::Balanced
}
}
impl Deflate { impl Deflate {
/// Create a new deflate compressor with a specific level of compression. /// Create a new deflate compressor with a specific level of compression.
pub fn with_level(level: DeflateLevel) -> Self { pub fn with_level(level: DeflateLevel) -> Self {
+1 -1
View File
@@ -14,7 +14,7 @@ impl Compression for Uncompressed {
impl CompressionAlgorithm for Uncompressed { impl CompressionAlgorithm for Uncompressed {
fn write_to<W: Write>(&mut self, writer: &mut W, bytes: &[u8]) -> Result<u64, io::Error> { 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)
} }
} }
+744 -102
View File
File diff suppressed because it is too large. Load diff
+256
View File
@@ -0,0 +1,256 @@
use crate::tags::{PhotometricInterpretation, PlanarConfiguration};
/// A filter of tags copied in
/// [`DirectoryEncoder::write_metadata_from`][`super::DirectoryEncoder::write_metadata_from`].
pub struct TagFilter {
filter_level: FilterLevel,
}
impl TagFilter {
/// Return a filter that allows all tags from metadata subdirectories.
pub fn allow_all_in_subdirectories() -> &'static TagFilter {
static INSTANCE: TagFilter = TagFilter {
filter_level: FilterLevel::All,
};
&INSTANCE
}
/// Return a filter that has an explicit allow list of tags, for each of a known list of
/// metadata subdirectories.
pub fn only_known_tags() -> &'static TagFilter {
static INSTANCE: TagFilter = TagFilter {
filter_level: FilterLevel::Known,
};
&INSTANCE
}
/// FIXME: take a configuration by which to filter out complete metadata kinds.
/// FIXME: when we iterate over a whole directory of tags, this iteration is more expensive
/// than necessary. Tags within a directory should be ordered. We can thus perform a binary set
/// join which is more efficient than checking the individual filter on each. Does not matter
/// much in most cases though.
pub(crate) fn filter_primary(&self, tag: u16) -> Level {
let as_exif = self.filter_primary_exif(tag);
if !matches!(as_exif, Level::Unknown) {
return as_exif;
}
let as_gps = self.filter_primary_gps(tag);
if !matches!(as_gps, Level::Unknown) {
return as_gps;
}
let as_icc = self.filter_icc(tag);
if !matches!(as_icc, Level::Unknown) {
return as_icc;
}
Level::Unknown
}
/// As per EXIF V3.0 tags, Primary Image (0th IFD).
pub(crate) fn filter_primary_exif(&self, tag: u16) -> Level {
match tag {
// width, height, etc
0x0100..=0x0103 => Level::SampleLayout,
// photometric interpretation
0x0106 => Level::SampleLayout,
// data tied to the camera and customized description, potentially PI.
0x010f..=0x0110 => Level::Value,
// strip offsets
0x0111 => Level::SampleLayout,
// orientation
0x0112 => Level::Value,
// strip byte counts, samples
0x0115..=0x0117 => Level::SampleLayout,
// pixel dimensions (ratio). Should be generic enough.
0x011a..=0x011b => Level::Value,
// planar configuration
0x011c => Level::Photometric(Photometric::Planar),
// units for 0x011a,0x011b
0x0128 => Level::Value,
// transfer function
0x012d => Level::Photometric(Photometric::All),
// file data time, software pipeline. I'm wary of privacy implications.
0x0131..=0x0132 => Level::Value,
// artist, clearly PI unless you explicitly wish it to be attributed.
0x013b => Level::Value,
// Whitepoint, Chromaticities
0x013e..=0x013f => Level::Photometric(Photometric::All),
// The table lists these as Not allowed for anything but we may have a choice here for
// older models of images.
0x0201..=0x0202 => Level::Special(Special::JpegThumbnail),
0x0211..=0x0213 => Level::Photometric(Photometric::YCbCr),
0x0214 => Level::Photometric(Photometric::All),
// Copyright holder (not same as artist).
0x8298 => Level::Value,
0x8769 => Level::Ifd(SubifdKind::ExifPrivate),
_ => Level::Unknown,
}
}
pub(crate) fn filter_primary_gps(&self, tag: u16) -> Level {
match tag {
0x8825 => Level::Ifd(SubifdKind::Gps),
_ => Level::Unknown,
}
}
pub(crate) fn filter_icc(&self, tag: u16) -> Level {
match tag {
0x8ff3 => Level::Value,
_ => Level::Unknown,
}
}
pub(crate) fn filter_exif_private(&self, tag: u16) -> Level {
match tag {
// Maybe this is not personal identifying but some equipment may be very specifically
// known through these. And it's not critical to displaying it correctly.
0x829a => Level::Value,
0x829d => Level::Value,
0x8822 => Level::Value,
0x8824 => Level::Value,
0x8827 => Level::Value,
// Opto-electric coefficients
0x8828 => Level::Value,
0x8830..=0x8835 => Level::Value,
0x9000 => Level::Value,
0x9003..=0x9004 => Level::Value,
0x9010..=0x9012 => Level::Value,
// Not allowed for uncompressed data!
0x9101..=0x9102 => Level::Special(Special::CompressedExclusive),
0x9201..=0x9209 => Level::Value,
0x920a => Level::Value,
0x9214 => Level::Value,
0x927c => Level::Value,
0x9286 => Level::Value,
0x9290..=0x9292 => Level::Value,
0x9400..=0x9405 => Level::Value,
0xa000 => Level::Value,
0xa001 => Level::Photometric(Photometric::All),
0xa002..=0xa003 => Level::Special(Special::CompressedExclusive),
0xa004 => Level::Value,
0xa005 => Level::Ifd(SubifdKind::Interoperability),
0xa20b..=0xa20c => Level::Value,
0xa20e..=0xa210 => Level::Value,
0xa214..=0xa215 => Level::Value,
0xa217 => Level::Value,
0xa300..=0xa302 => Level::Value,
0xa401..=0xa40c => Level::Value,
0xa420 => Level::Value,
0xa430..=0xa43c => Level::Value,
0xa460..=0xa462 => Level::Value,
0xa500 => Level::Photometric(Photometric::All),
_ => Level::Unknown,
}
}
pub(crate) fn filter_gps_private(&self, tag: u16) -> Level {
match tag {
0x0000..=0x001f => Level::Value,
_ => Level::Unknown,
}
}
pub(crate) fn should_keep_for_image_writer(
&self,
level: Level,
target: &super::EncodeMetadataApplicability,
) -> Choice {
match level {
Level::Unknown if matches!(self.filter_level, FilterLevel::All) => Choice::Filtered,
// Sample Layout is controlled by the writer (dimensions, color type, …). Do not copy!
Level::SampleLayout => Choice::Discard,
Level::Ifd(subifd_kind) => Choice::Descend(subifd_kind),
Level::Photometric(photometric) => {
match (photometric, target.planar, target.photometric) {
(Photometric::All, _, _) => Choice::Ok,
(Photometric::Planar, Some(PlanarConfiguration::Planar) | None, _) => {
Choice::Ok
}
(Photometric::YCbCr, _, Some(PhotometricInterpretation::YCbCr) | None) => {
Choice::Ok
}
_ => Choice::Discard,
}
}
Level::Value => Choice::Ok,
Level::Special(special) => Choice::Special(special),
_ => Choice::Discard,
}
}
}
pub(crate) enum Choice {
Ok,
Descend(SubifdKind),
/// Tag is inapplicable to be copied.
Discard,
/// Tag was chosen not to be copied.
Filtered,
Special(Special),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FilterLevel {
All,
Known,
}
/// The level of support for a value.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Level {
Unknown,
/// Value describes the image data layout, which we will not want to write into the encoder's
/// directory where the image data is written later.
///
/// These are treated as privacy preserving.
SampleLayout,
/// Value is an IFD and must be copied by making a subdirectory.
///
/// These are treated as privacy preserving, independent of the tags within the subdirectory
/// which may not be. (For instance the presence of an empty directory is fine but we will omit
/// a GPS anyways).
Ifd(SubifdKind),
/// Value is related to photometric interpretation and should not be copied when the target
/// does not match the expected interpretation.
///
/// These are *generally* treated as privacy preserving.
Photometric(Photometric),
/// This is a value we know of.
/// FIXME: we do not differentiate by personal identifiable information and otherwise which is
/// quite important for some variations of copying metadata.
Value,
/// We recognize the tag but it can not be copied by itself through normal means.
///
/// Privacy treatment depends on the tag.
Special(Special),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Photometric {
All,
/// Do not copy for non-planar data.
Planar,
/// Do not copy for non-YCbCr data.
YCbCr,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SubifdKind {
ExifPrivate,
Gps,
Interoperability,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Special {
/// Thumbnail which we regard as PI since we do not deep-clean the JPEG structure.
JpegThumbnail,
/// May only occur in compressed (JPEG APP segments) data.
CompressedExclusive,
}
+194 -30
View File
@@ -1,21 +1,34 @@
use std::{borrow::Cow, io::Write, slice::from_ref}; use std::{borrow::Cow, io::Write, slice::from_ref};
use crate::{bytecast, tags::Type, TiffError, TiffFormatError, TiffResult}; use crate::{bytecast, tags, tags::Type, TiffError, TiffFormatError, TiffResult};
use super::writer::TiffWriter; use super::writer::TiffWriter;
/// Trait for types that can be encoded in a tiff file /// 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 { pub trait TiffValue {
const BYTE_LEN: u8; const BYTE_LEN: u8;
const FIELD_TYPE: Type; const FIELD_TYPE: Type;
// FIXME: If we want this to mean 'count' in the sense of `Entry` it should return `u32` or
// `u64`, probably the latter with `convert_offset` taking care of the check against `u32::MAX`
// for classic tiff.
fn count(&self) -> usize; fn count(&self) -> usize;
fn bytes(&self) -> usize { fn bytes(&self) -> usize {
self.count() * usize::from(Self::BYTE_LEN) self.count() * usize::from(Self::BYTE_LEN)
} }
/// Access this value as an contiguous sequence of bytes. /// Access this value as an contiguous sequence of bytes.
/// If their is no trivial representation, allocate it on the heap. /// If their is no trivial representation, allocate it on the heap.
fn data(&self) -> Cow<[u8]>; fn data(&self) -> Cow<'_, [u8]>;
/// Write this value to a TiffWriter. /// Write this value to a TiffWriter.
/// While the default implementation will work in all cases, it may require unnecessary allocations. /// While the default implementation will work in all cases, it may require unnecessary allocations.
@@ -34,7 +47,7 @@ impl TiffValue for [u8] {
self.len() self.len()
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(self) Cow::Borrowed(self)
} }
} }
@@ -47,7 +60,7 @@ impl TiffValue for [i8] {
self.len() self.len()
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i8_as_ne_bytes(self)) Cow::Borrowed(bytecast::i8_as_ne_bytes(self))
} }
} }
@@ -60,7 +73,7 @@ impl TiffValue for [u16] {
self.len() self.len()
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u16_as_ne_bytes(self)) Cow::Borrowed(bytecast::u16_as_ne_bytes(self))
} }
} }
@@ -73,7 +86,7 @@ impl TiffValue for [i16] {
self.len() self.len()
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i16_as_ne_bytes(self)) Cow::Borrowed(bytecast::i16_as_ne_bytes(self))
} }
} }
@@ -86,7 +99,7 @@ impl TiffValue for [u32] {
self.len() self.len()
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u32_as_ne_bytes(self)) Cow::Borrowed(bytecast::u32_as_ne_bytes(self))
} }
} }
@@ -99,7 +112,7 @@ impl TiffValue for [i32] {
self.len() self.len()
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i32_as_ne_bytes(self)) Cow::Borrowed(bytecast::i32_as_ne_bytes(self))
} }
} }
@@ -112,7 +125,7 @@ impl TiffValue for [u64] {
self.len() self.len()
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u64_as_ne_bytes(self)) Cow::Borrowed(bytecast::u64_as_ne_bytes(self))
} }
} }
@@ -125,7 +138,7 @@ impl TiffValue for [i64] {
self.len() self.len()
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i64_as_ne_bytes(self)) Cow::Borrowed(bytecast::i64_as_ne_bytes(self))
} }
} }
@@ -138,7 +151,7 @@ impl TiffValue for [f32] {
self.len() self.len()
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
// We write using native endian so this should be safe // We write using native endian so this should be safe
Cow::Borrowed(bytecast::f32_as_ne_bytes(self)) Cow::Borrowed(bytecast::f32_as_ne_bytes(self))
} }
@@ -152,7 +165,7 @@ impl TiffValue for [f64] {
self.len() self.len()
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
// We write using native endian so this should be safe // We write using native endian so this should be safe
Cow::Borrowed(bytecast::f64_as_ne_bytes(self)) Cow::Borrowed(bytecast::f64_as_ne_bytes(self))
} }
@@ -171,7 +184,7 @@ impl TiffValue for u8 {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(from_ref(self)) Cow::Borrowed(from_ref(self))
} }
} }
@@ -189,7 +202,7 @@ impl TiffValue for i8 {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i8_as_ne_bytes(from_ref(self))) Cow::Borrowed(bytecast::i8_as_ne_bytes(from_ref(self)))
} }
} }
@@ -207,7 +220,7 @@ impl TiffValue for u16 {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u16_as_ne_bytes(from_ref(self))) Cow::Borrowed(bytecast::u16_as_ne_bytes(from_ref(self)))
} }
} }
@@ -225,7 +238,7 @@ impl TiffValue for i16 {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i16_as_ne_bytes(from_ref(self))) Cow::Borrowed(bytecast::i16_as_ne_bytes(from_ref(self)))
} }
} }
@@ -243,7 +256,7 @@ impl TiffValue for u32 {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(self))) Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(self)))
} }
} }
@@ -261,7 +274,7 @@ impl TiffValue for i32 {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i32_as_ne_bytes(from_ref(self))) Cow::Borrowed(bytecast::i32_as_ne_bytes(from_ref(self)))
} }
} }
@@ -279,7 +292,7 @@ impl TiffValue for u64 {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(self))) Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(self)))
} }
} }
@@ -297,7 +310,7 @@ impl TiffValue for i64 {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i64_as_ne_bytes(from_ref(self))) Cow::Borrowed(bytecast::i64_as_ne_bytes(from_ref(self)))
} }
} }
@@ -315,7 +328,7 @@ impl TiffValue for f32 {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::f32_as_ne_bytes(from_ref(self))) Cow::Borrowed(bytecast::f32_as_ne_bytes(from_ref(self)))
} }
} }
@@ -333,7 +346,7 @@ impl TiffValue for f64 {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::f64_as_ne_bytes(from_ref(self))) Cow::Borrowed(bytecast::f64_as_ne_bytes(from_ref(self)))
} }
} }
@@ -351,7 +364,7 @@ impl TiffValue for Ifd {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(&self.0))) Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(&self.0)))
} }
} }
@@ -369,7 +382,7 @@ impl TiffValue for Ifd8 {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(&self.0))) Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(&self.0)))
} }
} }
@@ -388,7 +401,7 @@ impl TiffValue for Rational {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Owned({ Cow::Owned({
let first_dword = bytecast::u32_as_ne_bytes(from_ref(&self.n)); let first_dword = bytecast::u32_as_ne_bytes(from_ref(&self.n));
let second_dword = bytecast::u32_as_ne_bytes(from_ref(&self.d)); let second_dword = bytecast::u32_as_ne_bytes(from_ref(&self.d));
@@ -411,7 +424,7 @@ impl TiffValue for SRational {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Owned({ Cow::Owned({
let first_dword = bytecast::i32_as_ne_bytes(from_ref(&self.n)); let first_dword = bytecast::i32_as_ne_bytes(from_ref(&self.n));
let second_dword = bytecast::i32_as_ne_bytes(from_ref(&self.d)); let second_dword = bytecast::i32_as_ne_bytes(from_ref(&self.d));
@@ -438,7 +451,7 @@ impl TiffValue for str {
} }
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
Cow::Owned({ Cow::Owned({
if self.is_ascii() && !self.bytes().any(|b| b == 0) { if self.is_ascii() && !self.bytes().any(|b| b == 0) {
let bytes: &[u8] = self.as_bytes(); let bytes: &[u8] = self.as_bytes();
@@ -450,7 +463,137 @@ impl TiffValue for str {
} }
} }
impl<'a, T: TiffValue + ?Sized> TiffValue for &'a T { // FIXME: held up on reading the discriminant for all but the unknown variant.
//
// It needs to have `repr(u16)` to have a defined layout and then we may read the tag from its
// value representation by casting a u16 pointer to a pointer to the enum type.
// unsafe { *<*const _>::from(self).cast::<u16>() }
// But that is quite unsafe and needs careful review to maintain the safety requirements. Maybe we
// could get `bytemuck` to add a trait for this (it has `TransparentWrapper`) with a derive for
// enum types that have an explicit repr.
//
// This would allow returning borrowed data in more cases. For all types without an unknown, in all
// cases including as slices, and for all types with and unknown variant when we have a single
// value (either returning a borrowed discriminant or a borrowed value within `Unknown`).
impl TiffValue for tags::CompressionMethod {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
let bytes = self.to_u16().to_ne_bytes();
Cow::Owned(bytes.to_vec())
}
}
impl TiffValue for tags::PhotometricInterpretation {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
self.to_u16().to_ne_bytes().to_vec().into()
}
}
impl TiffValue for tags::PlanarConfiguration {
const BYTE_LEN: u8 = 2;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
self.to_u16().to_ne_bytes().to_vec().into()
}
}
impl TiffValue for tags::Predictor {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
self.to_u16().to_ne_bytes().to_vec().into()
}
}
impl TiffValue for tags::ResolutionUnit {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
self.to_u16().to_ne_bytes().to_vec().into()
}
}
/// This is implemented for slices as the `SampleFormat` tag takes a count of `N`.
///
/// Use `core::slice::from_ref` if you really need to write a single element.
///
/// See: <https://web.archive.org/web/20191120220815/https://www.awaresystems.be/imaging/tiff/tifftags/sampleformat.html>
impl TiffValue for [tags::SampleFormat] {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
self.len()
}
fn data(&self) -> Cow<'_, [u8]> {
let mut buf: Vec<u8> = Vec::with_capacity(self.len() * 2);
for x in self {
buf.extend_from_slice(&x.to_u16().to_ne_bytes());
}
Cow::Owned(buf)
}
}
/// This is implemented for slices as the `ExtraSamples` tag takes a count of `N`.
///
/// Use `core::slice::from_ref` if you really need to write a single element.
///
/// See: <https://web.archive.org/web/20191120220815/https://www.awaresystems.be/imaging/tiff/tifftags/extrasamples.html>
impl TiffValue for [tags::ExtraSamples] {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
self.len()
}
fn data(&self) -> Cow<'_, [u8]> {
let mut buf: Vec<u8> = Vec::with_capacity(self.len() * 2);
for x in self {
buf.extend_from_slice(&x.to_u16().to_ne_bytes());
}
Cow::Owned(buf)
}
}
// 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 BYTE_LEN: u8 = T::BYTE_LEN;
const FIELD_TYPE: Type = T::FIELD_TYPE; const FIELD_TYPE: Type = T::FIELD_TYPE;
@@ -462,11 +605,32 @@ impl<'a, T: TiffValue + ?Sized> TiffValue for &'a T {
(*self).write(writer) (*self).write(writer)
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
T::data(self) T::data(self)
} }
} }
/// 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 { macro_rules! impl_tiff_value_for_contiguous_sequence {
($inner_type:ty; $bytes:expr; $field_type:expr) => { ($inner_type:ty; $bytes:expr; $field_type:expr) => {
impl $crate::encoder::TiffValue for [$inner_type] { impl $crate::encoder::TiffValue for [$inner_type] {
@@ -484,7 +648,7 @@ macro_rules! impl_tiff_value_for_contiguous_sequence {
Ok(()) Ok(())
} }
fn data(&self) -> Cow<[u8]> { fn data(&self) -> Cow<'_, [u8]> {
let mut buf: Vec<u8> = Vec::with_capacity(Self::BYTE_LEN as usize * self.len()); let mut buf: Vec<u8> = Vec::with_capacity(Self::BYTE_LEN as usize * self.len());
for x in self { for x in self {
buf.extend_from_slice(&x.data()); buf.extend_from_slice(&x.data());
+65
View File
@@ -69,6 +69,10 @@ impl<W: Write> TiffWriter<W> {
self.offset self.offset
} }
pub(crate) fn previous_ifd_pointer<K: super::TiffKind>(&self) -> u64 {
self.offset() - core::mem::size_of::<K::OffsetType>() as u64
}
pub fn last_written(&self) -> u64 { pub fn last_written(&self) -> u64 {
self.byte_count self.byte_count
} }
@@ -186,3 +190,64 @@ impl<W: Seek> TiffWriter<W> {
Ok(()) 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()
);
}
}
+31
View File
@@ -5,6 +5,7 @@ use std::string;
use quick_error::quick_error; use quick_error::quick_error;
use crate::decoder::ChunkType; use crate::decoder::ChunkType;
use crate::tags::Type;
use crate::tags::{ use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag, CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag,
}; };
@@ -90,6 +91,9 @@ quick_error! {
InvalidTypeForTag { InvalidTypeForTag {
display("tag has invalid type") display("tag has invalid type")
} }
InvalidCountForTag(tag: Tag, len: usize) {
display("tag `{tag:?}` with incorrect number of elements ({len}) encountered")
}
StripTileTagConflict { StripTileTagConflict {
display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found") display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found")
} }
@@ -156,6 +160,9 @@ quick_error! {
UnsupportedInterpretation(interpretation: PhotometricInterpretation) { UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
display("unsupported photometric interpretation \"{interpretation:?}\"") display("unsupported photometric interpretation \"{interpretation:?}\"")
} }
ChromaSubsampling {
display("chroma subsampling of YCbCr color is unsupported")
}
MisalignedTileBoundaries { MisalignedTileBoundaries {
display("tile rows are not aligned to byte boundaries") display("tile rows are not aligned to byte boundaries")
} }
@@ -175,6 +182,12 @@ quick_error! {
InvalidChunkIndex(index: u32) { InvalidChunkIndex(index: u32) {
display("invalid chunk index ({index}) requested") 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 { PredictorCompressionMismatch {
display("requested predictor is not compatible with the requested compression") display("requested predictor is not compatible with the requested compression")
} }
@@ -184,6 +197,24 @@ quick_error! {
PredictorUnavailable { PredictorUnavailable {
display("the requested predictor is not available") display("the requested predictor is not available")
} }
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")
}
ByteOrderMismatch {
display("attempted to use data with a byte order that does not match the required one")
}
MismatchedEntryLength {ty: Type, found: usize } {
display("the length of data is not a multiple of the size of the type {ty:?}, found {found}")
}
} }
} }
+21
View File
@@ -8,10 +8,12 @@
mod bytecast; mod bytecast;
pub mod decoder; pub mod decoder;
mod directory;
pub mod encoder; pub mod encoder;
mod error; mod error;
pub mod tags; pub mod tags;
pub use self::directory::Directory;
pub use self::error::{TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError}; pub use self::error::{TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError};
/// An enumeration over supported color types and their bit depths /// An enumeration over supported color types and their bit depths
@@ -42,6 +44,9 @@ pub enum ColorType {
/// Pixel is YCbCr /// Pixel is YCbCr
YCbCr(u8), YCbCr(u8),
/// Pixel is CIE L*a*b* (ICC LAb)
Lab(u8),
/// Pixel has multiple bands/channels /// Pixel has multiple bands/channels
Multiband { bit_depth: u8, num_samples: u16 }, Multiband { bit_depth: u8, num_samples: u16 },
} }
@@ -56,7 +61,23 @@ impl ColorType {
| ColorType::CMYK(b) | ColorType::CMYK(b)
| ColorType::CMYKA(b) | ColorType::CMYKA(b)
| ColorType::YCbCr(b) | ColorType::YCbCr(b)
| ColorType::Lab(b)
| ColorType::Multiband { bit_depth: b, .. } => 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,
}
}
} }
+365 -17
View File
@@ -1,8 +1,10 @@
use crate::encoder::TiffValue;
macro_rules! tags { macro_rules! tags {
{ {
// Permit arbitrary meta items, which include documentation. // Permit arbitrary meta items, which include documentation.
$( #[$enum_attr:meta] )* $( #[$enum_attr:meta] )*
$vis:vis enum $name:ident($ty:tt) $(unknown($unknown_doc:literal))* { $vis:vis enum $name:ident($ty:tt) $(unknown(#[$unknown_meta:meta] $unknown_doc:ident))* {
// Each of the `Name = Val,` permitting documentation. // Each of the `Name = Val,` permitting documentation.
$($(#[$ident_attr:meta])* $tag:ident = $val:expr,)* $($(#[$ident_attr:meta])* $tag:ident = $val:expr,)*
} }
@@ -10,17 +12,18 @@ macro_rules! tags {
$( #[$enum_attr] )* $( #[$enum_attr] )*
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
#[non_exhaustive] #[non_exhaustive]
#[repr($ty)]
pub enum $name { pub enum $name {
$($(#[$ident_attr])* $tag,)* $($(#[$ident_attr])* $tag = $val,)*
$( $(
#[doc = $unknown_doc] #[$unknown_meta]
Unknown($ty), Unknown($ty),
)* )*
} }
impl $name { impl $name {
#[inline(always)] #[inline(always)]
fn __from_inner_type(n: $ty) -> Result<Self, $ty> { const fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
match n { match n {
$( $val => Ok($name::$tag), )* $( $val => Ok($name::$tag), )*
n => Err(n), n => Err(n),
@@ -28,10 +31,10 @@ macro_rules! tags {
} }
#[inline(always)] #[inline(always)]
fn __to_inner_type(&self) -> $ty { const fn __to_inner_type(&self) -> $ty {
match *self { match *self {
$( $name::$tag => $val, )* $( $name::$tag => $val, )*
$( $name::Unknown(n) => { $unknown_doc; n }, )* $( $name::Unknown($unknown_doc) => { $unknown_doc }, )*
} }
} }
} }
@@ -39,23 +42,28 @@ macro_rules! tags {
tags!($name, $ty, $($unknown_doc)*); tags!($name, $ty, $($unknown_doc)*);
}; };
// For u16 tags, provide direct inherent primitive conversion methods. // For u16 tags, provide direct inherent primitive conversion methods.
($name:tt, u16, $($unknown_doc:literal)*) => { ($name:tt, u16, $($unknown_doc:ident)*) => {
impl $name { impl $name {
#[inline(always)] #[inline(always)]
pub fn from_u16(val: u16) -> Option<Self> { pub const fn from_u16(val: u16) -> Option<Self> {
Self::__from_inner_type(val).ok() match Self::__from_inner_type(val) {
Ok(v) => Some(v),
Err(_) => None,
}
} }
$( $(
#[inline(always)] #[inline(always)]
pub fn from_u16_exhaustive(val: u16) -> Self { pub const fn from_u16_exhaustive($unknown_doc: u16) -> Self {
$unknown_doc; match Self::__from_inner_type($unknown_doc) {
Self::__from_inner_type(val).unwrap_or_else(|_| $name::Unknown(val)) Ok(v) => v,
Err(_) => $name::Unknown($unknown_doc),
}
} }
)* )*
#[inline(always)] #[inline(always)]
pub fn to_u16(&self) -> u16 { pub const fn to_u16(&self) -> u16 {
Self::__to_inner_type(self) Self::__to_inner_type(self)
} }
} }
@@ -68,7 +76,10 @@ macro_rules! tags {
// Note: These tags appear in the order they are mentioned in the TIFF reference // Note: These tags appear in the order they are mentioned in the TIFF reference
tags! { tags! {
/// TIFF tags /// TIFF tags
pub enum Tag(u16) unknown("A private or extension tag") { pub enum Tag(u16) unknown(
/// A private or extension tag
unknown
) {
// Baseline tags: // Baseline tags:
Artist = 315, Artist = 315,
// grayscale images PhotometricInterpretation 1 or 3 // grayscale images PhotometricInterpretation 1 or 3
@@ -78,7 +89,6 @@ pub enum Tag(u16) unknown("A private or extension tag") {
// palette-color images (PhotometricInterpretation 3) // palette-color images (PhotometricInterpretation 3)
ColorMap = 320, // TODO add support ColorMap = 320, // TODO add support
Compression = 259, // TODO add support for 2 and 32773 Compression = 259, // TODO add support for 2 and 32773
Copyright = 33_432,
DateTime = 306, DateTime = 306,
ExtraSamples = 338, // TODO add support ExtraSamples = 338, // TODO add support
FillOrder = 266, // TODO add support FillOrder = 266, // TODO add support
@@ -114,23 +124,51 @@ pub enum Tag(u16) unknown("A private or extension tag") {
TileLength = 323, TileLength = 323,
TileOffsets = 324, TileOffsets = 324,
TileByteCounts = 325, TileByteCounts = 325,
SubIfd = 330,
// Data Sample Format // Data Sample Format
SampleFormat = 339, SampleFormat = 339,
SMinSampleValue = 340, // TODO add support SMinSampleValue = 340, // TODO add support
SMaxSampleValue = 341, // TODO add support SMaxSampleValue = 341, // TODO add support
// JPEG // JPEG
JPEGTables = 347, JPEGTables = 347,
// Subsampling
#[doc(alias = "YCbCrSubsampling")]
ChromaSubsampling = 530, // TODO add support
#[doc(alias = "YCbCrPositioning")]
ChromaPositioning = 531, // TODO add support
// GeoTIFF // GeoTIFF
ModelPixelScaleTag = 33550, // (SoftDesk) ModelPixelScaleTag = 33550, // (SoftDesk)
ModelTransformationTag = 34264, // (JPL Carto Group) ModelTransformationTag = 34264, // (JPL Carto Group)
ModelTiepointTag = 33922, // (Intergraph) ModelTiepointTag = 33922, // (Intergraph)
// <https://web.archive.org/web/20131111073619/http://www.exif.org/Exif2-1.PDF>
// *Do note its typo in the Decimal id*
Copyright = 33_432,
// <https://web.archive.org/web/20131111073619/http://www.exif.org/Exif2-1.PDF>
ExifDirectory = 0x8769,
// <https://web.archive.org/web/20131111073619/http://www.exif.org/Exif2-1.PDF>
GpsDirectory = 0x8825,
// <https://www.color.org/technotes/ICC-Technote-ProfileEmbedding.pdf>
IccProfile = 34675,
GeoKeyDirectoryTag = 34735, // (SPOT) GeoKeyDirectoryTag = 34735, // (SPOT)
GeoDoubleParamsTag = 34736, // (SPOT) GeoDoubleParamsTag = 34736, // (SPOT)
GeoAsciiParamsTag = 34737, // (SPOT) GeoAsciiParamsTag = 34737, // (SPOT)
ExifVersion = 0x9000,
GdalNodata = 42113, // Contains areas with missing data GdalNodata = 42113, // Contains areas with missing data
} }
} }
/// Identifies the offset of an IFD.
///
/// This is represented as a 64-bit integer but only BigTIFF can utilize the bits. It is encoded
/// as 32-bit unsigned value ([`Type::LONG`]) in regular TIFF files and as 64-bit unsigned value
/// ([`Type::IFD8`]) in BigTIFF files.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
// We could be using `NonZeroU64` here but I find that this complicates semantics. This type
// represents the integer value stored as a value in a tag. The semantics of treating `0` as an end
// marker are imposed by the IFD. (It's unclear if Pointer tags such as Exif would allow `0` but in
// practice it just returns garbage and the validity does not matter greatly to us).
pub struct IfdPointer(pub u64);
tags! { tags! {
/// The type of an IFD entry (a 2 byte field). /// The type of an IFD entry (a 2 byte field).
pub enum Type(u16) { pub enum Type(u16) {
@@ -169,10 +207,52 @@ 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! { tags! {
/// See [TIFF compression tags](https://www.awaresystems.be/imaging/tiff/tifftags/compression.html) /// See [TIFF compression tags](https://www.awaresystems.be/imaging/tiff/tifftags/compression.html)
/// for reference. /// for reference.
pub enum CompressionMethod(u16) unknown("A custom compression method") { pub enum CompressionMethod(u16) unknown(
/// A custom compression method
unknown
) {
None = 1, None = 1,
Huffman = 2, Huffman = 2,
Fax3 = 3, Fax3 = 3,
@@ -200,6 +280,8 @@ pub enum PhotometricInterpretation(u16) {
CMYK = 5, CMYK = 5,
YCbCr = 6, YCbCr = 6,
CIELab = 8, CIELab = 8,
IccLab = 9,
ItuLab = 10,
} }
} }
@@ -234,10 +316,276 @@ pub enum ResolutionUnit(u16) {
} }
tags! { tags! {
pub enum SampleFormat(u16) unknown("An unknown extension sample format") { pub enum SampleFormat(u16) unknown(
/// An unknown extension sample format
unknown
) {
Uint = 1, Uint = 1,
Int = 2, Int = 2,
IEEEFP = 3, IEEEFP = 3,
Void = 4, 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,
}
}
/// A value represented as in-memory bytes with flexible byteorder.
pub struct ValueBuffer {
/// The raw bytes of the value.
bytes: Vec<u8>,
/// The type of the value.
ty: Type,
/// The number of items, as `bytes` may be oversized while holding bytes that are initialized
/// but not used by any value.
count: u64,
/// The byte order of the value.
byte_order: ByteOrder,
}
impl ValueBuffer {
/// A value with a count of zero.
///
/// The byte order is set to the native byte order of the platform.
pub fn empty(ty: Type) -> Self {
ValueBuffer {
bytes: vec![],
ty,
count: 0,
byte_order: ByteOrder::native(),
}
}
/// Create a value with native byte order from in-memory data.
pub fn from_value<T: TiffValue>(value: &T) -> Self {
ValueBuffer {
bytes: value.data().into_owned(),
ty: <T as TiffValue>::FIELD_TYPE,
count: value.count() as u64,
byte_order: ByteOrder::native(),
}
}
pub fn byte_order(&self) -> ByteOrder {
self.byte_order
}
pub fn data_type(&self) -> Type {
self.ty
}
/// The count of items in the value.
pub fn count(&self) -> u64 {
debug_assert!({
self.ty
.value_bytes(self.count)
.is_ok_and(|n| n <= self.bytes.len() as u64)
});
self.count
}
/// View the underlying raw bytes of this value.
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.assumed_len_from_count()]
}
/// View the underlying mutable raw bytes of this value.
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
let len = self.assumed_len_from_count();
&mut self.bytes[..len]
}
/// See [`Value::into_ifd_pointer`](crate::decoder::ifd::Value::into_ifd_pointer).
pub(crate) fn as_ifd_pointer(&self) -> Option<IfdPointer> {
if self.count != 1 {
return None;
}
match self.ty {
// See [`ifd::Value::into_ifd_pointer`] there is enough legacy that uses `LONG` to
// refer to an offset into the file with the meaning of a directory pointer. That is
// rather annoying but legacy and not a big deal.
Type::IFD | Type::LONG => {
let mut buf = [0u8; 4];
buf.copy_from_slice(&self.bytes[..4]);
ByteOrder::native().convert(Type::IFD, &mut buf, self.byte_order);
Some(IfdPointer(u32::from_ne_bytes(buf).into()))
}
Type::IFD8 => {
let mut buf = [0u8; 8];
buf.copy_from_slice(&self.bytes[..8]);
ByteOrder::native().convert(Type::IFD8, &mut buf, self.byte_order);
Some(IfdPointer(u64::from_ne_bytes(buf)))
}
_ => None,
}
}
/// Change the byte order of the value representation.
pub fn set_byte_order(&mut self, byte_order: ByteOrder) {
let len = self.assumed_len_from_count();
self.byte_order
.convert(self.ty, &mut self.bytes[..len], byte_order);
self.byte_order = byte_order;
}
/// Prepare the internal for a value `to_len` bytes long.
///
/// Shrinks the allocation if it is far too large or extends it if it is too small. In either
/// case ensures that at least `to_len` bytes are initialized for [`Self::raw_bytes_mut`].
pub(crate) fn prepare_length(&mut self, to_len: usize) {
if to_len > self.bytes.len() {
self.bytes.resize(to_len, 0);
}
if self.bytes.len() < to_len / 2 {
self.bytes.truncate(to_len);
self.bytes.shrink_to_fit();
}
}
/// Internal method to change the type and count while re-interpreting the byte buffer.
///
/// Should only be called after writing bytes to the internal buffer prepared with
/// `Self::prepare_length`.
pub(crate) fn assume_type(&mut self, ty: Type, count: u64, bo: ByteOrder) {
debug_assert!({
ty.value_bytes(count)
.is_ok_and(|n| n <= self.bytes.len() as u64)
});
self.byte_order = bo;
self.ty = ty;
self.count = count;
}
pub(crate) fn raw_bytes_mut(&mut self) -> &mut [u8] {
&mut self.bytes
}
fn assumed_len_from_count(&self) -> usize {
usize::from(self.ty.byte_len()) * self.count as usize
}
}
/// 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,
}
#[test]
fn convert_ifd_u32() {
let mut value = ValueBuffer::from_value(&crate::encoder::Ifd(0x42));
for bo in [ByteOrder::BigEndian, ByteOrder::LittleEndian] {
value.set_byte_order(bo);
let ptr = value
.as_ifd_pointer()
.expect("Should be convertible to IfdPointer");
// Should be in host byte order again.
assert_eq!(ptr.0, 0x42);
}
}
#[test]
fn convert_ifd_long8() {
let mut value = ValueBuffer::from_value(&crate::encoder::Ifd8(0x42));
for bo in [ByteOrder::BigEndian, ByteOrder::LittleEndian] {
value.set_byte_order(bo);
let ptr = value
.as_ifd_pointer()
.expect("Should be convertible to IfdPointer");
// Should be in host byte order again.
assert_eq!(ptr.0, 0x42);
}
}
+4
View File
@@ -0,0 +1,4 @@
subsubifd.tif:
url: https://data.kitware.com/api/v1/file/hashsum/sha512/372ca32735c8a8fdbe2286dabead9e779da63f10ba81eda84625e5273f76d74ca1a47a978f67e9c00c12f7f72009c7b2c07a641e643bb0c463812f4ae7f15d6e/download
credit: https://github.com/DigitalSlideArchive/tifftools/commit/4d00c70dbce828262f86c1431f7c66b1748965ac
license: CC0
+208 -10
View File
@@ -4,6 +4,7 @@ use tiff::decoder::{ifd, Decoder, DecodingResult};
use tiff::ColorType; use tiff::ColorType;
use std::fs::File; use std::fs::File;
use std::io::{Read, Seek};
use std::path::PathBuf; use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images/"; const TEST_IMAGE_DIR: &str = "./tests/images/";
@@ -45,12 +46,12 @@ fn test_image_color_type_unsupported(file: &str, expected_type: ColorType) {
let img_file = File::open(path).expect("Cannot find test image!"); let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(decoder.colortype().unwrap(), expected_type); assert_eq!(decoder.colortype().unwrap(), expected_type);
assert!(match decoder.read_image() { assert!(matches!(
decoder.read_image(),
Err(tiff::TiffError::UnsupportedError( Err(tiff::TiffError::UnsupportedError(
tiff::TiffUnsupportedError::UnsupportedColorType(_), tiff::TiffUnsupportedError::UnsupportedColorType(_),
)) => true, ))
_ => false, ));
});
} }
#[test] #[test]
@@ -172,7 +173,7 @@ fn test_string_tags() {
&s, &s,
"GraphicsMagick 1.2 unreleased Q16 http://www.GraphicsMagick.org/" "GraphicsMagick 1.2 unreleased Q16 http://www.GraphicsMagick.org/"
), ),
_ => assert!(false), other => unreachable!("Expected Ascii tag from file, got {other:?}"),
}; };
} }
} }
@@ -231,6 +232,54 @@ fn test_tiled_rect_rgb_u8() {
test_image_sum_u8("tiled-rect-rgb-u8.tif", ColorType::RGB(8), 62081032); test_image_sum_u8("tiled-rect-rgb-u8.tif", ColorType::RGB(8), 62081032);
} }
#[test]
fn test_inner_access() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-rect-rgb-u8.tif");
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
let c = decoder
.get_tag(tiff::tags::Tag::Compression)
.unwrap()
.into_u16()
.unwrap();
assert_eq!(c, tiff::tags::CompressionMethod::None.to_u16());
// Because the image is uncompressed, reading the first tile directly with the inner reader
// should yield the same result as reading it with the decoder's read_chunk method.
let first_offset = decoder
.get_tag_u32_vec(tiff::tags::Tag::TileOffsets)
.unwrap()
.into_iter()
.next()
.unwrap();
let first_byte_count = decoder
.get_tag_u32_vec(tiff::tags::Tag::TileByteCounts)
.unwrap()
.into_iter()
.next()
.unwrap();
decoder
.inner()
.seek(std::io::SeekFrom::Start(first_offset as u64))
.expect("Cannot seek to tile offset");
let mut buf = vec![0; first_byte_count as usize];
decoder
.inner()
.read_exact(&mut buf)
.expect("Cannot read tile data");
let raw_sum: u64 = buf.into_iter().map(<u64>::from).sum();
match decoder.read_chunk(0).unwrap() {
DecodingResult::U8(chunk) => {
let sum: u64 = chunk.into_iter().map(<u64>::from).sum();
assert_eq!(sum, raw_sum);
}
_ => panic!("Wrong bit depth"),
}
}
/* #[test] /* #[test]
fn test_tiled_jpeg_rgb_u8() { fn test_tiled_jpeg_rgb_u8() {
test_image_sum_u8("tiled-jpeg-rgb-u8.tif", ColorType::RGB(8), 93031606); test_image_sum_u8("tiled-jpeg-rgb-u8.tif", ColorType::RGB(8), 93031606);
@@ -292,6 +341,11 @@ fn test_tiled_incremental() {
#[test] #[test]
fn test_planar_rgb_u8() { fn test_planar_rgb_u8() {
test_image_sum_u8("planar-rgb-u8.tif", ColorType::RGB(8), 15417630);
}
#[test]
fn test_read_planar_bands() {
// gdal_translate tiled-rgb-u8.tif planar-rgb-u8.tif -co INTERLEAVE=BAND -co COMPRESS=LZW -co PROFILE=BASELINE // gdal_translate tiled-rgb-u8.tif planar-rgb-u8.tif -co INTERLEAVE=BAND -co COMPRESS=LZW -co PROFILE=BASELINE
let file = "planar-rgb-u8.tif"; let file = "planar-rgb-u8.tif";
let expected_type = ColorType::RGB(8); let expected_type = ColorType::RGB(8);
@@ -328,8 +382,6 @@ fn test_planar_rgb_u8() {
} }
_ => panic!("Wrong bit depth"), _ => panic!("Wrong bit depth"),
} }
test_image_sum_u8(file, ColorType::RGB(8), 15417630);
} }
#[test] #[test]
@@ -348,7 +400,7 @@ fn test_div_zero() {
match err { match err {
TiffError::FormatError(TiffFormatError::StripTileTagConflict) => {} TiffError::FormatError(TiffFormatError::StripTileTagConflict) => {}
unexpected => panic!("Unexpected error {}", unexpected), unexpected => panic!("Unexpected error {unexpected}"),
} }
} }
@@ -366,7 +418,7 @@ fn test_too_many_value_bytes() {
match error { match error {
tiff::TiffError::LimitsExceeded => {} tiff::TiffError::LimitsExceeded => {}
unexpected => panic!("Unexpected error {}", unexpected), unexpected => panic!("Unexpected error {unexpected}"),
} }
} }
@@ -497,7 +549,7 @@ fn timeout() {
match error { match error {
TiffError::FormatError(TiffFormatError::CycleInOffsets) => {} TiffError::FormatError(TiffFormatError::CycleInOffsets) => {}
e => panic!("Unexpected error {:?}", e), e => panic!("Unexpected error {e:?}"),
} }
} }
@@ -522,3 +574,149 @@ fn test_zstd_compression() {
// gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif // gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif
test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396); test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396);
} }
fn test_image_bytes(
file: &str,
expected: &[(ColorType, u32)],
// Represent all samples as little endian, ensuring check sums on all platforms.
normalize_byte_order: fn(&mut [u8]),
) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
let mut buffer = vec![0u8; 0];
for (idx, &(expected_type, expected_crc)) in expected.iter().enumerate() {
if idx > 0 {
decoder
.next_image()
.expect("mismatched number of directories");
}
assert_eq!(decoder.colortype().unwrap(), expected_type);
let layout = decoder
.image_buffer_layout()
.expect("Cannot describe buffer layout");
buffer.resize(layout.len, 0u8);
decoder.read_image_bytes(&mut buffer).unwrap();
normalize_byte_order(&mut buffer);
// Well almost a crc..
assert_eq!(crc32fast::hash(&buffer), expected_crc);
}
}
fn byte_order_u16(bytes: &mut [u8]) {
for chunk in bytes.chunks_exact_mut(2) {
let n = u16::from_ne_bytes(chunk.try_into().unwrap());
chunk.copy_from_slice(&n.to_le_bytes());
}
}
fn byte_order_u32(bytes: &mut [u8]) {
for chunk in bytes.chunks_exact_mut(4) {
let n = u32::from_ne_bytes(chunk.try_into().unwrap());
chunk.copy_from_slice(&n.to_le_bytes());
}
}
fn byte_order_u64(bytes: &mut [u8]) {
for chunk in bytes.chunks_exact_mut(8) {
let n = u64::from_ne_bytes(chunk.try_into().unwrap());
chunk.copy_from_slice(&n.to_le_bytes());
}
}
#[test]
fn bytes_cmyk_3c_8b() {
test_image_bytes(
"cmyk-3c-8b.tiff",
&[(ColorType::CMYK(8), 2935032230)],
|_| (),
);
}
#[test]
fn bytes_gray_16b() {
test_image_bytes(
"minisblack-1c-16b.tiff",
&[(ColorType::Gray(16), 3524597615)],
byte_order_u16,
);
}
#[test]
fn bytes_gray_32b() {
test_image_bytes(
"gradient-1c-32b.tiff",
&[(ColorType::Gray(32), 2257036530)],
byte_order_u32,
);
}
#[test]
fn bytes_gray_u64() {
test_image_bytes(
"gradient-1c-64b.tiff",
&[(ColorType::Gray(64), 2156547113)],
byte_order_u64,
);
}
#[test]
fn bytes_gray_f32() {
test_image_bytes(
"gradient-1c-32b-float.tiff",
&[(ColorType::Gray(32), 3232356730)],
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);
}
}
}
+73 -8
View File
@@ -57,7 +57,7 @@ fn encode_decode() {
), ),
(Tag::Compression, ifd::Value::Short(1)), (Tag::Compression, ifd::Value::Short(1)),
(Tag::PhotometricInterpretation, ifd::Value::Short(2)), (Tag::PhotometricInterpretation, ifd::Value::Short(2)),
(Tag::StripOffsets, ifd::Value::Unsigned(8)), (Tag::StripOffsets, ifd::Value::Unsigned(35)),
(Tag::SamplesPerPixel, ifd::Value::Short(3)), (Tag::SamplesPerPixel, ifd::Value::Short(3)),
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)), (Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
(Tag::StripByteCounts, ifd::Value::Unsigned(30000)), (Tag::StripByteCounts, ifd::Value::Unsigned(30000)),
@@ -81,7 +81,10 @@ fn encode_decode() {
decoder.get_tag(Tag::Artist).unwrap(), decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into()) 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); assert_eq!(image_data, img_res);
} else { } else {
panic!("Wrong data type"); panic!("Wrong data type");
@@ -137,7 +140,7 @@ fn encode_decode_big() {
), ),
(Tag::Compression, ifd::Value::Short(1)), (Tag::Compression, ifd::Value::Short(1)),
(Tag::PhotometricInterpretation, ifd::Value::Short(2)), (Tag::PhotometricInterpretation, ifd::Value::Short(2)),
(Tag::StripOffsets, ifd::Value::UnsignedBig(16)), (Tag::StripOffsets, ifd::Value::UnsignedBig(27)),
(Tag::SamplesPerPixel, ifd::Value::Short(3)), (Tag::SamplesPerPixel, ifd::Value::Short(3)),
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)), (Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
(Tag::StripByteCounts, ifd::Value::UnsignedBig(30000)), (Tag::StripByteCounts, ifd::Value::UnsignedBig(30000)),
@@ -161,7 +164,9 @@ fn encode_decode_big() {
decoder.get_tag(Tag::Artist).unwrap(), decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into()) 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); assert_eq!(image_data, img_res);
} else { } else {
panic!("Wrong data type"); panic!("Wrong data type");
@@ -235,6 +240,7 @@ const TEST_IMAGE_DIR: &str = "./tests/images/";
macro_rules! test_roundtrip { macro_rules! test_roundtrip {
($name:ident, $buffer:ident, $buffer_ty:ty) => { ($name:ident, $buffer:ident, $buffer_ty:ty) => {
#[track_caller]
fn $name<C: colortype::ColorType<Inner = $buffer_ty>>( fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
file: &str, file: &str,
expected_type: ColorType, expected_type: ColorType,
@@ -244,7 +250,10 @@ macro_rules! test_roundtrip {
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(decoder.colortype().unwrap(), expected_type); 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, DecodingResult::$buffer(res) => res,
_ => panic!("Wrong data type"), _ => panic!("Wrong data type"),
}; };
@@ -258,9 +267,11 @@ macro_rules! test_roundtrip {
} }
file.seek(SeekFrom::Start(0)).unwrap(); file.seek(SeekFrom::Start(0)).unwrap();
{ {
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
let mut decoder = Decoder::new(&mut file).unwrap(); let mut decoder = Decoder::new(&mut file).unwrap();
if let DecodingResult::$buffer(img_res) = decoder.read_image().unwrap() { decoder.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(image_data, img_res); if let DecodingResult::$buffer(img_res) = buffer {
assert_eq!(*image_data, img_res);
} else { } else {
panic!("Wrong data type"); panic!("Wrong data type");
} }
@@ -621,8 +632,62 @@ fn test_rows_per_strip() {
let img2 = [i; 2 * 100]; let img2 = [i; 2 * 100];
match decoder.read_chunk(i as u32).unwrap() { match decoder.read_chunk(i as u32).unwrap() {
DecodingResult::U8(data) => assert_eq!(&img2[..], &data[..]), DecodingResult::U8(data) => assert_eq!(&img2[..], &data[..]),
other => panic!("Incorrect strip type {:?}", other), other => panic!("Incorrect strip type {other:?}"),
} }
} }
} }
} }
#[test]
fn test_auxiliary_directory() {
const ARTIST: &str = "Auxiliary-tiff";
let mut data = Cursor::new(Vec::new());
let mut tiff = TiffEncoder::new(&mut data).unwrap();
{
let mut image_encoder = tiff.new_image::<colortype::Gray8>(1, 1).unwrap();
image_encoder.write_strip(&[1]).unwrap();
}
let exif = {
let mut extra = tiff.extra_directory().unwrap();
extra.write_tag(Tag::Artist, ARTIST).unwrap();
extra.finish_with_offsets().unwrap()
};
{
let mut image_encoder = tiff.new_image::<colortype::Gray8>(2, 1).unwrap();
image_encoder.write_strip(&[1, 2]).unwrap();
let encoder = image_encoder.encoder();
encoder.write_tag(Tag::ExifDirectory, exif.offset).unwrap();
}
data.set_position(0);
let mut decoder = Decoder::new(&mut data).unwrap();
{
assert_eq!(decoder.dimensions().unwrap(), (1, 1));
let _ = decoder.read_image().unwrap();
}
{
decoder.next_image().unwrap();
assert_eq!(decoder.dimensions().unwrap(), (2, 1));
let _ = decoder.read_image().unwrap();
let exif_location = decoder
.get_tag(Tag::ExifDirectory)
.expect("second directory missing Exif")
.into_ifd_pointer()
.expect("exif directory is not an IFD pointer");
let exif = decoder.read_directory(exif_location).unwrap();
let artist = decoder
.read_directory_tags(&exif)
.get_tag_ascii_string(Tag::Artist)
.expect("EXIF directory missing artist tag");
assert_eq!(artist, ARTIST);
}
}
+2 -2
View File
@@ -116,7 +116,7 @@ fn encode_decode_with_compression(compression: Compression) {
assert_eq!( assert_eq!(
match decoder.read_image() { match decoder.read_image() {
Ok(DecodingResult::U16(image_data)) => image_data, Ok(DecodingResult::U16(image_data)) => image_data,
unexpected => panic!("Descoding RGB failed: {:?}", unexpected), unexpected => panic!("Descoding RGB failed: {unexpected:?}"),
}, },
image_rgb.reference_data() image_rgb.reference_data()
); );
@@ -126,7 +126,7 @@ fn encode_decode_with_compression(compression: Compression) {
assert_eq!( assert_eq!(
match decoder.read_image() { match decoder.read_image() {
Ok(DecodingResult::U8(image_data)) => image_data, Ok(DecodingResult::U8(image_data)) => image_data,
unexpected => panic!("Decoding grayscale failed: {:?}", unexpected), unexpected => panic!("Decoding grayscale failed: {unexpected:?}"),
}, },
image_grayscale.reference_data() image_grayscale.reference_data()
); );
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
use tiff::{
decoder::Decoder,
encoder::{colortype::Gray8, EncodeMetadataOptions, TiffEncoder},
tags::Tag,
};
#[test]
fn copies_exif_data() {
let src = std::path::PathBuf::from(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/images/predictor-3-gray-f32.tif"
));
let src = std::fs::File::open(src).unwrap();
let mut src = Decoder::new(src).unwrap();
let mut data = std::io::Cursor::new(vec![]);
{
let mut dst = TiffEncoder::new(&mut data).unwrap();
let mut img = dst.new_image::<Gray8>(1, 1).unwrap();
let report = img
.write_metadata_copy_from(&mut src, EncodeMetadataOptions::default())
.unwrap();
assert_eq!(report.copied, 8);
assert_eq!(report.filtered, 0, "There are no tags we do not recognize");
img.write_data(&[0x80]).unwrap();
}
data.set_position(0);
{
let mut roundtrip = Decoder::new(data).unwrap();
assert!(roundtrip.find_tag(Tag::DateTime).unwrap().is_some());
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
extern crate tiff; extern crate tiff;
use tiff::decoder::{Decoder, DecodingResult}; use tiff::decoder::{Decoder, DecodingResult};
use tiff::encoder::{colortype, Compression, Predictor, TiffEncoder}; use tiff::encoder::{colortype, Predictor, TiffEncoder};
use tiff::ColorType; use tiff::ColorType;
use std::fs::File; use std::fs::File;
+41
View File
@@ -0,0 +1,41 @@
use tiff::decoder::Decoder;
// This file has the following IFD structure:
//
// ```
// IFD -> IFD -> IFD
// |-> IFD
// |-> IFD -> IFD
// | \-> IFD -> IFD
// \-> IFD
// ```
//
// where the main chain of IFD has 3 images, the second of which has several sub-IFDs and some have
// more nested IFDs.
const TEST_IMAGE_SUBIFD: &str = "./tests/subsubifds.tif";
use std::fs::File;
#[test]
fn decode_seek_chain() {
let file = File::open(TEST_IMAGE_SUBIFD).expect("Cannot open test image");
let mut decoder = Decoder::new(file).expect("Invalid format to create decoder");
// Remember the offsets in the first iteration.
let offset0 = decoder.ifd_pointer().expect("First IFD pointer not found");
decoder.next_image().unwrap();
let offset1 = decoder.ifd_pointer().expect("Second IFD pointer not found");
decoder.next_image().unwrap();
let offset2 = decoder.ifd_pointer().expect("Third IFD pointer not found");
assert!(!decoder.more_images());
// Ensure we can seek, to do this again.
decoder
.seek_to_image(0)
.expect("Failed to seek to first image");
assert_eq!(Some(offset0), decoder.ifd_pointer());
decoder.next_image().unwrap();
assert_eq!(Some(offset1), decoder.ifd_pointer());
decoder.next_image().unwrap();
assert_eq!(Some(offset2), decoder.ifd_pointer());
assert!(!decoder.more_images());
}
Binary file not shown.