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
Jonathan Behrens 46086e2bcf Release 0.10.0 (#271) 2025-06-20 20:15:42 -07:00
Jonathan Behrens d7be2a7b67 Make the Compressor enum non-exhaustive (#272) 2025-06-15 18:31:03 -07:00
Emma 29b71ff597 add CMYKA (#208) 2025-06-15 16:38:22 -07:00
Jonathan Behrens 83e6d64751 Create feature flags for more compression algorithms (#270) 2025-06-15 15:50:14 -07:00
Jonathan Behrens dc2c67469b Switch to zune-jpeg (#268)
]
2025-06-14 21:13:16 -07:00
Jonathan Behrens b5d8ee5f86 Switch to quick_error and cleaner error messages (#269) 2025-06-09 19:10:12 -07:00
Jonathan Behrens f44da4a00d Switch from SmartReader to EndianReader (#233) 2025-06-08 18:45:10 -07:00
Wei Ji ee0dbf373b Read Byte as u8, SignedByte as i8 (#236) 2025-06-08 18:31:51 -07:00
Wei Ji aeab243a96 Read Short as u16, SignedShort as i16 (#235) 2025-06-08 18:03:08 -07:00
Wei Ji 36f7b3b4aa Bump MSRV from 1.61.0 to 1.70.0
Found using `cargo msrv find`, the `1.70.0` version is determined by `half=2.4.1` having an MSRV of 1.70.0.
2025-03-16 21:31:12 +00:00
Micah Chambers 3bfb43e83e Add FP16 support (#257) 2025-02-04 19:12:01 -08:00
Jonathan Behrens d42e6be507 Fix formatting (#263) 2025-01-30 23:29:29 -08:00
Stefan Schake a19cf37587 Support ZSTD compression method for reading (#256) 2024-12-20 12:50:34 -08:00
Gunnar Schulze ff52aea06e Add support for iterating over all tags (#255) 2024-11-25 19:34:11 -08:00
Jonathan Behrens 96d3a02efe Fix 1bpp image decoding and add a test image (#252) 2024-10-31 20:21:02 -07:00
Jean-Baptiste Skutnik e28ad564d9 Add horizontal predictor support (#240) 2024-09-23 19:45:45 +01:00
Even Rouault de6d9cf131 README.md: advertize JPEG decode support (#244) 2024-09-07 10:49:22 -07:00
Jonathan Behrens 6dc7a266d3 Make decoding internally operate on byte buffers (#230) 2024-07-22 20:13:39 -07:00
Wei Ji 307cd8be97 Add SignedByte and SignedShort enum variants (#234) 2024-07-21 20:37:25 -07:00
Wei Jiandtromper 0c54a18e21 Add ColorType::Multiband enum variant to support samples>1 per pixel (#224)
Co-authored-by: tromper <scott@hopkinsunderground.com>
2024-03-24 15:04:52 -07:00
Jonathan Behrens 90ae5b8e54 Fix warnings (#225) 2024-03-23 13:51:28 -07:00
Jonathan Behrens 9b06e3726d Sample format should not be a Vec (#226) 2024-03-23 13:51:19 -07:00
Jonathan Behrens 895c70a1ad Test minimal versions in CI (#223) 2024-02-11 12:40:11 -08:00
Jonathan Behrens b0ba4a6788 Release 0.9.1 (#220) 2024-01-13 12:01:04 -08:00
Jonathan Behrens 4a2e76858b Allow arbitrary number of samples as long as all have the same bit depth (#215) 2023-10-04 09:45:03 -07:00
Jonathan Behrens a67f50568c Update edition and CI script, and add Cargo.lock.msrv (#212)
* Add Cargo.lock.msrv, update edition, and CI script

* Also run clippy and verify msrv

* Fix string mismatch

* Have clippy only check for serious errors
2023-08-20 13:44:35 -07:00
Jonathan Behrens d6f6b8a01e Merge pull request #213 from fintelia/fix-clippy
A bunch of clippy fixes
2023-08-20 13:39:27 -07:00
Jonathan Behrens 8ec91b64f6 Cargo fmt 2023-08-20 13:33:35 -07:00
Jonathan Behrens 60f866c119 A bunch of clippy fixes 2023-08-20 13:32:08 -07:00
Jonathan Behrens daa0e3eabf Merge pull request #206 from fintelia/simplify-jpeg
Simplify and avoid allocation in jpeg decompression
2023-06-14 14:04:01 -07:00
Jonathan Behrens 24d203c7c3 Simplify and avoid allocation in jpeg decompression 2023-06-11 12:54:30 -07:00
Jonathan Behrens 473868a7bb Merge pull request #199 from pka/planar-config
Basic support for planar configuration
2023-06-10 12:46:20 -07:00
Andreas Molzer aefea7786f Merge pull request #203 from AlanRace/custom-compression
Don't panic when parsing metadata when custom compression is used
2023-03-08 09:33:03 +01:00
Alan Race d1ad9b2664 Don't panic when parsing metadata when custom compression is used 2023-03-07 09:15:24 +01:00
Andreas Molzer bd1d085189 Merge pull request #201 from image-rs/release-0.8.2
Add changelog and metadata for 0.9.0
2023-02-06 23:58:59 +01:00
Andreas Molzer aa10d27a28 Merge pull request #200 from anfedotoff/master
Fix capacity overflow in JpegReader
2023-02-06 21:22:13 +01:00
fedotoff 8fa175c371 Fix capacity overflow in JpegReader 2023-02-05 17:53:37 +03:00
Pirmin Kalberer 2a15aa6b7e Support also tiled planar images 2023-01-17 11:57:54 +01:00
Pirmin Kalberer 42feeba8da Add basic support for planar configuration 2023-01-12 23:34:31 +01:00
58 changed files with 6725 additions and 1795 deletions

No files matched your search

+73 -19
View File
@@ -1,40 +1,94 @@
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.61", stable, beta, nightly] rust: ["1.85.0", stable, beta, nightly]
command: [build, test]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- run: rustup default ${{ matrix.rust }}
- uses: dtolnay/rust-toolchain@nightly
if: ${{ matrix.rust == '1.85.0' }}
- name: Generate Cargo.lock with minimal-version dependencies
if: ${{ matrix.rust == '1.85.0' }}
run: cargo -Zminimal-versions generate-lockfile
- uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ matrix.rust }}
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: build - name: build
run: > run: cargo build -v
cargo build --verbose --no-default-features --features "$FEATURES"
- name: test - name: test
run: > if: ${{ matrix.rust != '1.85.0' }}
cargo test --tests --benches --no-default-features --features "$FEATURES" run: cargo test -v && cargo doc -v
if: ${{ matrix.rust != '1.61' }}
env: features:
FEATURES: ${{ matrix.features }} runs-on: ubuntu-latest
strategy:
matrix:
feature: ["", deflate, jpeg, lzw, zstd, default, "default,zstd"]
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cargo check
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:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: stable
override: true
components: rustfmt components: rustfmt
- name: Run rustfmt check - name: Run rustfmt check
uses: actions-rs/cargo@v1 run: cargo fmt -- --check
with:
command: fmt
args: -- --check
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- run: cargo clippy --all-features # TODO: enable "-- -D warnings" once all outstanding clippy lints are fixed
+151
View File
@@ -1,3 +1,154 @@
# 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
New features:
- Added `ColorType::Multiband` and `ColorType::CMYKA`.
- Added `Value::Byte` and `Value::SignedByte` to represent `u8` and `i8` tags.
- Encoding can now use horizontal predictor.
- The `Decoder::tag_iter` method allows iterating over all tags in the image.
- Half precision floating point values are now supported during decoding.
- ZStandard compression has been added as an additional compression method
during decoding, but requires the `zstd` feature flag.
Changes:
- Switched `u16` and `i16` values to be intepreted as `Value::Short` and
`Value::SignedShort` respectively.
- Changed JPEG compression to use the `zune-jpeg` crate instead of
`jpeg-decoder`.
- The `deflate`, `jpeg`, and `lzw` compression methods are now controlled by
(enabled by default) feature flags.
Fixes:
- Fixed a bug in 1 bit-per-pixel decoding.
# Version 0.9.1
New features:
- Basic support for planar configuration.
- Allow arbitrary number of samples as long as all have the same bit depth.
Fixes:
- Don't panic when parsing metadata when custom compression is used.
# Version 0.9.0 # Version 0.9.0
New features: New features:
+25 -7
View File
@@ -1,11 +1,11 @@
[package] [package]
name = "tiff" name = "tiff"
version = "0.9.0" version = "0.11.1"
edition = "2018" 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.61.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,15 +14,33 @@ 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]
weezl = "0.1.0" half = { version = "2.4.1" }
jpeg = { package = "jpeg-decoder", version = "0.3.0", default-features = false } quick-error = "2.0.1"
flate2 = "1.0.20"
# Rename to avoid confusion with the feature name.
fax34 = { package = "fax", version = "0.2.6", optional = true }
flate2 = { version = "1.0.20", optional = true }
weezl = { version = "0.1.10", optional = true }
zstd = { version = "0.13", optional = true }
zune-jpeg = { version = "0.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]
default = ["deflate", "fax", "jpeg", "lzw"]
# Compression algorithms
deflate = ["dep:flate2"]
fax = ["dep:fax34"]
jpeg = ["dep:zune-jpeg"]
lzw = ["dep:weezl"]
zstd = ["dep:zstd"]
[[bench]] [[bench]]
name = "lzw" name = "lzw"
+5 -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)
@@ -24,6 +24,7 @@ This table lists photometric interpretations and sample formats which are suppor
| `RGBPalette` | | | | `RGBPalette` | | |
| `Mask` | | | | `Mask` | | |
| `CMYK` | CMYK(8\|16\|32\|64) | CMYK(32\|64) | | `CMYK` | CMYK(8\|16\|32\|64) | CMYK(32\|64) |
| `CMYKA` | CMYKA(8) | |
| `YCbCr` | | | | `YCbCr` | | |
| `CIELab` | | | | `CIELab` | | |
@@ -35,6 +36,9 @@ This table lists photometric interpretations and sample formats which are suppor
| LZW | ✓ | ✓ | | LZW | ✓ | ✓ |
| Deflate | ✓ | ✓ | | Deflate | ✓ | ✓ |
| PackBits | ✓ | ✓ | | PackBits | ✓ | ✓ |
| Fax4 | ✓ | not yet |
| JPEG | ✓ | not yet |
| ZSTD | ✓ | not yet |
## Not yet supported ## Not yet supported
+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");
}
}
+6
View File
@@ -10,8 +10,13 @@
//! 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;
macro_rules! integral_slice_as_bytes{($int:ty, $const:ident $(,$mut:ident)*) => { macro_rules! integral_slice_as_bytes{($int:ty, $const:ident $(,$mut:ident)*) => {
pub(crate) fn $const(slice: &[$int]) -> &[u8] { pub(crate) fn $const(slice: &[$int]) -> &[u8] {
assert!(mem::align_of::<$int>() <= mem::size_of::<$int>()); assert!(mem::align_of::<$int>() <= mem::size_of::<$int>());
@@ -31,4 +36,5 @@ integral_slice_as_bytes!(i32, i32_as_ne_bytes, i32_as_ne_mut_bytes);
integral_slice_as_bytes!(u64, u64_as_ne_bytes, u64_as_ne_mut_bytes); integral_slice_as_bytes!(u64, u64_as_ne_bytes, u64_as_ne_mut_bytes);
integral_slice_as_bytes!(i64, i64_as_ne_bytes, i64_as_ne_mut_bytes); integral_slice_as_bytes!(i64, i64_as_ne_bytes, i64_as_ne_mut_bytes);
integral_slice_as_bytes!(f32, f32_as_ne_bytes, f32_as_ne_mut_bytes); integral_slice_as_bytes!(f32, f32_as_ne_bytes, f32_as_ne_mut_bytes);
integral_slice_as_bytes!(f16, f16_as_ne_bytes, f16_as_ne_mut_bytes);
integral_slice_as_bytes!(f64, f64_as_ne_bytes, f64_as_ne_mut_bytes); integral_slice_as_bytes!(f64, f64_as_ne_bytes, f64_as_ne_mut_bytes);
+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");
}
+476 -193
View File
File diff suppressed because it is too large. Load diff
+744 -183
View File
File diff suppressed because it is too large. Load diff
+1293 -535
View File
File diff suppressed because it is too large. Load diff
+159 -213
View File
@@ -1,51 +1,49 @@
//! 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::convert::TryFrom; pub use crate::tags::ByteOrder;
use std::io::{self, BufRead, BufReader, Read, Seek, SeekFrom, Take};
use std::sync::Arc;
/// Byte order of the TIFF file.
#[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.
pub trait EndianReader: Read { #[derive(Debug)]
/// Byte order that should be adhered to pub struct EndianReader<R> {
fn byte_order(&self) -> ByteOrder; reader: R,
pub(crate) byte_order: ByteOrder,
}
impl<R: Read> EndianReader<R> {
pub fn new(reader: R, byte_order: ByteOrder) -> Self {
Self { reader, byte_order }
}
pub fn inner(&mut self) -> &mut R {
&mut self.reader
}
pub fn goto_offset(&mut self, offset: u64) -> io::Result<()>
where
R: Seek,
{
self.reader.seek(io::SeekFrom::Start(offset))?;
Ok(())
}
/// Reads an u16 /// Reads an u16
#[inline(always)] #[inline(always)]
fn read_u16(&mut self) -> Result<u16, io::Error> { pub fn read_u16(&mut self) -> Result<u16, io::Error> {
let mut n = [0u8; 2]; let mut n = [0u8; 2];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(match self.byte_order() { Ok(match self.byte_order {
ByteOrder::LittleEndian => u16::from_le_bytes(n), ByteOrder::LittleEndian => u16::from_le_bytes(n),
ByteOrder::BigEndian => u16::from_be_bytes(n), ByteOrder::BigEndian => u16::from_be_bytes(n),
}) })
} }
/// Reads an i8
#[inline(always)]
fn read_i8(&mut self) -> Result<i8, io::Error> {
let mut n = [0u8; 1];
self.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)]
fn read_i16(&mut self) -> Result<i16, io::Error> { pub fn read_i16(&mut self) -> Result<i16, io::Error> {
let mut n = [0u8; 2]; let mut n = [0u8; 2];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(match self.byte_order() { Ok(match self.byte_order {
ByteOrder::LittleEndian => i16::from_le_bytes(n), ByteOrder::LittleEndian => i16::from_le_bytes(n),
ByteOrder::BigEndian => i16::from_be_bytes(n), ByteOrder::BigEndian => i16::from_be_bytes(n),
}) })
@@ -53,10 +51,10 @@ pub trait EndianReader: Read {
/// Reads an u32 /// Reads an u32
#[inline(always)] #[inline(always)]
fn read_u32(&mut self) -> Result<u32, io::Error> { pub fn read_u32(&mut self) -> Result<u32, io::Error> {
let mut n = [0u8; 4]; let mut n = [0u8; 4];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(match self.byte_order() { Ok(match self.byte_order {
ByteOrder::LittleEndian => u32::from_le_bytes(n), ByteOrder::LittleEndian => u32::from_le_bytes(n),
ByteOrder::BigEndian => u32::from_be_bytes(n), ByteOrder::BigEndian => u32::from_be_bytes(n),
}) })
@@ -64,10 +62,10 @@ pub trait EndianReader: Read {
/// Reads an i32 /// Reads an i32
#[inline(always)] #[inline(always)]
fn read_i32(&mut self) -> Result<i32, io::Error> { pub fn read_i32(&mut self) -> Result<i32, io::Error> {
let mut n = [0u8; 4]; let mut n = [0u8; 4];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(match self.byte_order() { Ok(match self.byte_order {
ByteOrder::LittleEndian => i32::from_le_bytes(n), ByteOrder::LittleEndian => i32::from_le_bytes(n),
ByteOrder::BigEndian => i32::from_be_bytes(n), ByteOrder::BigEndian => i32::from_be_bytes(n),
}) })
@@ -75,10 +73,10 @@ pub trait EndianReader: Read {
/// Reads an u64 /// Reads an u64
#[inline(always)] #[inline(always)]
fn read_u64(&mut self) -> Result<u64, io::Error> { pub fn read_u64(&mut self) -> Result<u64, io::Error> {
let mut n = [0u8; 8]; let mut n = [0u8; 8];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(match self.byte_order() { Ok(match self.byte_order {
ByteOrder::LittleEndian => u64::from_le_bytes(n), ByteOrder::LittleEndian => u64::from_le_bytes(n),
ByteOrder::BigEndian => u64::from_be_bytes(n), ByteOrder::BigEndian => u64::from_be_bytes(n),
}) })
@@ -86,10 +84,10 @@ pub trait EndianReader: Read {
/// Reads an i64 /// Reads an i64
#[inline(always)] #[inline(always)]
fn read_i64(&mut self) -> Result<i64, io::Error> { pub fn read_i64(&mut self) -> Result<i64, io::Error> {
let mut n = [0u8; 8]; let mut n = [0u8; 8];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(match self.byte_order() { Ok(match self.byte_order {
ByteOrder::LittleEndian => i64::from_le_bytes(n), ByteOrder::LittleEndian => i64::from_le_bytes(n),
ByteOrder::BigEndian => i64::from_be_bytes(n), ByteOrder::BigEndian => i64::from_be_bytes(n),
}) })
@@ -97,10 +95,10 @@ pub trait EndianReader: Read {
/// Reads an f32 /// Reads an f32
#[inline(always)] #[inline(always)]
fn read_f32(&mut self) -> Result<f32, io::Error> { pub fn read_f32(&mut self) -> Result<f32, io::Error> {
let mut n = [0u8; 4]; let mut n = [0u8; 4];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(f32::from_bits(match self.byte_order() { Ok(f32::from_bits(match self.byte_order {
ByteOrder::LittleEndian => u32::from_le_bytes(n), ByteOrder::LittleEndian => u32::from_le_bytes(n),
ByteOrder::BigEndian => u32::from_be_bytes(n), ByteOrder::BigEndian => u32::from_be_bytes(n),
})) }))
@@ -108,49 +106,53 @@ pub trait EndianReader: Read {
/// Reads an f64 /// Reads an f64
#[inline(always)] #[inline(always)]
fn read_f64(&mut self) -> Result<f64, io::Error> { pub fn read_f64(&mut self) -> Result<f64, io::Error> {
let mut n = [0u8; 8]; let mut n = [0u8; 8];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(f64::from_bits(match self.byte_order() { Ok(f64::from_bits(match self.byte_order {
ByteOrder::LittleEndian => u64::from_le_bytes(n), ByteOrder::LittleEndian => u64::from_le_bytes(n),
ByteOrder::BigEndian => u64::from_be_bytes(n), ByteOrder::BigEndian => u64::from_be_bytes(n),
})) }))
} }
} }
/// //
/// # READERS // # READERS
/// //
///
/// ## Deflate Reader
///
/// Type alias for the deflate Reader
#[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")]
pub struct LZWReader<R: Read> { pub struct LZWReader<R: Read> {
reader: BufReader<Take<R>>, reader: BufReader<Take<R>>,
decoder: weezl::decode::Decoder, decoder: weezl::decode::Decoder,
} }
#[cfg(feature = "lzw")]
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(),
} }
} }
} }
#[cfg(feature = "lzw")]
impl<R: Read> Read for LZWReader<R> { impl<R: Read> Read for LZWReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
loop { loop {
@@ -166,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",
@@ -183,108 +182,11 @@ impl<R: Read> Read for LZWReader<R> {
} }
} }
/// //
/// ## JPEG Reader (for "new-style" JPEG format (TIFF compression tag 7)) // ## PackBits Reader
/// //
pub(crate) struct JpegReader {
jpeg_tables: Option<Arc<Vec<u8>>>,
buffer: io::Cursor<Vec<u8>>,
offset: usize,
}
impl JpegReader {
/// Constructs new JpegReader wrapping a SmartReader.
/// Because JPEG compression in TIFF allows to save quantization and/or huffman tables in one
/// central location, the constructor accepts this data as `jpeg_tables` here containing either
/// or both.
/// These `jpeg_tables` are simply prepended to the remaining jpeg image data.
/// Because these `jpeg_tables` start with a `SOI` (HEX: `0xFFD8`) or __start of image__ marker
/// which is also at the beginning of the remaining JPEG image data and would
/// confuse the JPEG renderer, one of these has to be taken off. In this case the first two
/// bytes of the remaining JPEG data is removed because it follows `jpeg_tables`.
/// Similary, `jpeg_tables` ends with a `EOI` (HEX: `0xFFD9`) or __end of image__ marker,
/// this has to be removed as well (last two bytes of `jpeg_tables`).
pub fn new<R: Read>(
mut reader: R,
length: u64,
jpeg_tables: Option<Arc<Vec<u8>>>,
) -> io::Result<JpegReader> {
// Read jpeg image data
let mut segment = vec![0; length as usize];
reader.read_exact(&mut segment[..])?;
match jpeg_tables {
Some(jpeg_tables) => {
assert!(
jpeg_tables.len() >= 2,
"jpeg_tables, if given, must be at least 2 bytes long. Got {:?}",
jpeg_tables
);
assert!(
length >= 2,
"if jpeg_tables is given, length must be at least 2 bytes long, got {}",
length
);
let mut buffer = io::Cursor::new(segment);
// Skip the first two bytes (marker bytes)
buffer.seek(SeekFrom::Start(2))?;
Ok(JpegReader {
buffer,
jpeg_tables: Some(jpeg_tables),
offset: 0,
})
}
None => Ok(JpegReader {
buffer: io::Cursor::new(segment),
jpeg_tables: None,
offset: 0,
}),
}
}
}
impl Read for JpegReader {
// #[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let mut start = 0;
if let Some(jpeg_tables) = &self.jpeg_tables {
if jpeg_tables.len() - 2 > self.offset {
// Read (rest of) jpeg_tables to buf (without the last two bytes)
let size_remaining = jpeg_tables.len() - self.offset - 2;
let to_copy = size_remaining.min(buf.len());
buf[start..start + to_copy]
.copy_from_slice(&jpeg_tables[self.offset..self.offset + to_copy]);
self.offset += to_copy;
if to_copy == buf.len() {
return Ok(to_copy);
}
start += to_copy;
}
}
let read = self.buffer.read(&mut buf[start..])?;
self.offset += read;
Ok(read + start)
}
}
///
/// ## PackBits Reader
///
/// Internal state machine for the PackBitsReader.
enum PackBitsReaderState { enum PackBitsReaderState {
Header, Header,
Literal, Literal,
@@ -318,7 +220,7 @@ impl<R: Read> Read for PackBitsReader<R> {
let mut header: [u8; 1] = [0]; let mut header: [u8; 1] = [0];
self.reader.read_exact(&mut header)?; self.reader.read_exact(&mut header)?;
let h = header[0] as i8; let h = header[0] as i8;
if h >= -127 && h <= -1 { if (-127..=-1).contains(&h) {
let mut data: [u8; 1] = [0]; let mut data: [u8; 1] = [0];
self.reader.read_exact(&mut data)?; self.reader.read_exact(&mut data)?;
self.state = PackBitsReaderState::Repeat { value: data[0] }; self.state = PackBitsReaderState::Repeat { value: data[0] };
@@ -348,63 +250,107 @@ impl<R: Read> Read for PackBitsReader<R> {
if self.count == 0 { if self.count == 0 {
self.state = PackBitsReaderState::Header; self.state = PackBitsReaderState::Header;
} }
return Ok(actual); Ok(actual)
} }
} }
/// #[cfg(feature = "fax")]
/// ## SmartReader Reader pub struct Group4Reader<R: Read> {
/// decoder: fax34::decoder::Group4Decoder<io::Bytes<io::BufReader<io::Take<R>>>>,
line_buf: io::Cursor<Vec<u8>>,
/// Reader that is aware of the byte order. height: u16,
#[derive(Debug)] width: u16,
pub struct SmartReader<R> y: u16,
where
R: Read,
{
reader: R,
pub byte_order: ByteOrder,
} }
impl<R> SmartReader<R> #[cfg(feature = "fax")]
where impl<R: Read> Group4Reader<R> {
R: Read, pub fn new(
{ dimensions: (u32, u32),
/// Wraps a reader reader: R,
pub fn wrap(reader: R, byte_order: ByteOrder) -> SmartReader<R> { compressed_length: u64,
SmartReader { reader, byte_order } ) -> crate::TiffResult<Self> {
} let width = u16::try_from(dimensions.0)?;
pub fn into_inner(self) -> R { let height = u16::try_from(dimensions.1)?;
self.reader
} Ok(Self {
} decoder: fax34::decoder::Group4Decoder::new(
impl<R: Read + Seek> SmartReader<R> { io::BufReader::new(reader.take(compressed_length)).bytes(),
pub fn goto_offset(&mut self, offset: u64) -> io::Result<()> { width,
self.seek(io::SeekFrom::Start(offset)).map(|_| ()) )?,
line_buf: io::Cursor::new(Vec::with_capacity(width.into())),
width,
height,
y: 0,
})
} }
} }
impl<R> EndianReader for SmartReader<R> #[cfg(feature = "fax")]
where impl<R: Read> Read for Group4Reader<R> {
R: Read,
{
#[inline(always)]
fn byte_order(&self) -> ByteOrder {
self.byte_order
}
}
impl<R: Read> Read for SmartReader<R> {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader.read(buf) // 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)?;
impl<R: Read + Seek> Seek for SmartReader<R> { match next {
#[inline] fax34::decoder::DecodeStatus::End => (),
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> { fax34::decoder::DecodeStatus::Incomplete => {
self.reader.seek(pos) 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)
} }
} }
+77 -17
View File
@@ -1,26 +1,85 @@
use std::convert::TryFrom;
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::SmartReader; 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 SmartReader<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),
@@ -29,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"
);
}
}
+108
View File
@@ -1,5 +1,31 @@
use crate::tags::{PhotometricInterpretation, SampleFormat}; use crate::tags::{PhotometricInterpretation, SampleFormat};
macro_rules! integer_horizontal_predict {
() => {
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>) {
let sample_size = Self::SAMPLE_FORMAT.len();
if row.len() < sample_size {
debug_assert!(false);
return;
}
let (start, rest) = row.split_at(sample_size);
result.extend_from_slice(start);
if result.capacity() - result.len() < rest.len() {
return;
}
result.extend(
row.into_iter()
.zip(rest)
.map(|(prev, current)| current.wrapping_sub(*prev)),
);
}
};
}
/// Trait for different colortypes that can be encoded. /// Trait for different colortypes that can be encoded.
pub trait ColorType { pub trait ColorType {
/// The type of each sample of this colortype /// The type of each sample of this colortype
@@ -10,6 +36,8 @@ pub trait ColorType {
const BITS_PER_SAMPLE: &'static [u16]; const BITS_PER_SAMPLE: &'static [u16];
/// The value of the tiff tag `SampleFormat` /// The value of the tiff tag `SampleFormat`
const SAMPLE_FORMAT: &'static [SampleFormat]; const SAMPLE_FORMAT: &'static [SampleFormat];
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>);
} }
pub struct Gray8; pub struct Gray8;
@@ -18,6 +46,8 @@ impl ColorType for Gray8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[8]; const BITS_PER_SAMPLE: &'static [u16] = &[8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
integer_horizontal_predict!();
} }
pub struct GrayI8; pub struct GrayI8;
@@ -26,6 +56,8 @@ impl ColorType for GrayI8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[8]; const BITS_PER_SAMPLE: &'static [u16] = &[8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int];
integer_horizontal_predict!();
} }
pub struct Gray16; pub struct Gray16;
@@ -34,6 +66,8 @@ impl ColorType for Gray16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[16]; const BITS_PER_SAMPLE: &'static [u16] = &[16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
integer_horizontal_predict!();
} }
pub struct GrayI16; pub struct GrayI16;
@@ -42,6 +76,8 @@ impl ColorType for GrayI16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[16]; const BITS_PER_SAMPLE: &'static [u16] = &[16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int];
integer_horizontal_predict!();
} }
pub struct Gray32; pub struct Gray32;
@@ -50,6 +86,8 @@ impl ColorType for Gray32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[32]; const BITS_PER_SAMPLE: &'static [u16] = &[32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
integer_horizontal_predict!();
} }
pub struct GrayI32; pub struct GrayI32;
@@ -58,6 +96,8 @@ impl ColorType for GrayI32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[32]; const BITS_PER_SAMPLE: &'static [u16] = &[32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int];
integer_horizontal_predict!();
} }
pub struct Gray32Float; pub struct Gray32Float;
@@ -66,6 +106,10 @@ impl ColorType for Gray32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[32]; const BITS_PER_SAMPLE: &'static [u16] = &[32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
} }
pub struct Gray64; pub struct Gray64;
@@ -74,6 +118,8 @@ impl ColorType for Gray64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[64]; const BITS_PER_SAMPLE: &'static [u16] = &[64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
integer_horizontal_predict!();
} }
pub struct GrayI64; pub struct GrayI64;
@@ -82,6 +128,8 @@ impl ColorType for GrayI64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[64]; const BITS_PER_SAMPLE: &'static [u16] = &[64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int];
integer_horizontal_predict!();
} }
pub struct Gray64Float; pub struct Gray64Float;
@@ -90,6 +138,10 @@ impl ColorType for Gray64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[64]; const BITS_PER_SAMPLE: &'static [u16] = &[64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
} }
pub struct RGB8; pub struct RGB8;
@@ -98,6 +150,8 @@ impl ColorType for RGB8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8]; const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
} }
pub struct RGB16; pub struct RGB16;
@@ -106,6 +160,8 @@ impl ColorType for RGB16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16]; const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
} }
pub struct RGB32; pub struct RGB32;
@@ -114,6 +170,8 @@ impl ColorType for RGB32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
} }
pub struct RGB32Float; pub struct RGB32Float;
@@ -122,6 +180,9 @@ impl ColorType for RGB32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
} }
pub struct RGB64; pub struct RGB64;
@@ -130,6 +191,8 @@ impl ColorType for RGB64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
} }
pub struct RGB64Float; pub struct RGB64Float;
@@ -138,6 +201,9 @@ impl ColorType for RGB64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
} }
pub struct RGBA8; pub struct RGBA8;
@@ -146,6 +212,8 @@ impl ColorType for RGBA8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8]; const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct RGBA16; pub struct RGBA16;
@@ -154,6 +222,8 @@ impl ColorType for RGBA16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16]; const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct RGBA32; pub struct RGBA32;
@@ -162,6 +232,8 @@ impl ColorType for RGBA32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct RGBA32Float; pub struct RGBA32Float;
@@ -170,6 +242,9 @@ impl ColorType for RGBA32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
} }
pub struct RGBA64; pub struct RGBA64;
@@ -178,6 +253,8 @@ impl ColorType for RGBA64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct RGBA64Float; pub struct RGBA64Float;
@@ -186,6 +263,9 @@ impl ColorType for RGBA64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
} }
pub struct CMYK8; pub struct CMYK8;
@@ -194,6 +274,8 @@ impl ColorType for CMYK8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8]; const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct CMYK16; pub struct CMYK16;
@@ -202,6 +284,8 @@ impl ColorType for CMYK16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16]; const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct CMYK32; pub struct CMYK32;
@@ -210,6 +294,8 @@ impl ColorType for CMYK32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct CMYK32Float; pub struct CMYK32Float;
@@ -218,6 +304,10 @@ impl ColorType for CMYK32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
} }
pub struct CMYK64; pub struct CMYK64;
@@ -226,6 +316,8 @@ impl ColorType for CMYK64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct CMYK64Float; pub struct CMYK64Float;
@@ -234,6 +326,10 @@ impl ColorType for CMYK64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
} }
pub struct YCbCr8; pub struct YCbCr8;
@@ -242,4 +338,16 @@ impl ColorType for YCbCr8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::YCbCr; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::YCbCr;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8]; const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
}
pub struct CMYKA8;
impl ColorType for CMYKA8 {
type Inner = u8;
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 5];
integer_horizontal_predict!();
} }
+4 -9
View File
@@ -1,6 +1,5 @@
use crate::{encoder::compression::*, tags::CompressionMethod}; use crate::encoder::compression::*;
use flate2::{write::ZlibEncoder, Compression as FlateCompression}; use flate2::{write::ZlibEncoder, Compression as FlateCompression};
use std::io::Write;
/// The Deflate algorithm used to compress image data in TIFF files. /// The Deflate algorithm used to compress image data in TIFF files.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -12,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 {
@@ -46,7 +41,7 @@ impl Compression for Deflate {
const COMPRESSION_METHOD: CompressionMethod = CompressionMethod::Deflate; const COMPRESSION_METHOD: CompressionMethod = CompressionMethod::Deflate;
fn get_algorithm(&self) -> Compressor { fn get_algorithm(&self) -> Compressor {
Compressor::Deflate(self.clone()) Compressor::Deflate(*self)
} }
} }
+2 -3
View File
@@ -1,5 +1,4 @@
use crate::{encoder::compression::*, tags::CompressionMethod}; use crate::encoder::compression::*;
use std::io::Write;
use weezl::encode::Encoder as LZWEncoder; use weezl::encode::Encoder as LZWEncoder;
/// The LZW algorithm used to compress image data in TIFF files. /// The LZW algorithm used to compress image data in TIFF files.
@@ -41,7 +40,7 @@ mod tests {
let mut compressed_data = Vec::<u8>::new(); let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data); let mut writer = Cursor::new(&mut compressed_data);
Lzw::default().write_to(&mut writer, TEST_DATA).unwrap(); Lzw.write_to(&mut writer, TEST_DATA).unwrap();
assert_eq!(EXPECTED_COMPRESSED_DATA, compressed_data.as_slice()); assert_eq!(EXPECTED_COMPRESSED_DATA, compressed_data.as_slice());
} }
} }
+13 -3
View File
@@ -1,13 +1,19 @@
use crate::tags::CompressionMethod; use crate::tags::CompressionMethod;
use std::io::{self, Write}; use std::io::{self, Write};
#[cfg(feature = "deflate")]
mod deflate; mod deflate;
#[cfg(feature = "lzw")]
mod lzw; mod lzw;
mod packbits; mod packbits;
mod uncompressed; mod uncompressed;
#[cfg(feature = "deflate")]
pub use self::deflate::{Deflate, DeflateLevel}; pub use self::deflate::{Deflate, DeflateLevel};
#[cfg(feature = "lzw")]
pub use self::lzw::Lzw; pub use self::lzw::Lzw;
pub use self::packbits::Packbits; pub use self::packbits::Packbits;
pub use self::uncompressed::Uncompressed; pub use self::uncompressed::Uncompressed;
@@ -28,9 +34,12 @@ pub trait Compression: CompressionAlgorithm {
} }
/// An enum to store each compression algorithm. /// An enum to store each compression algorithm.
#[non_exhaustive]
pub enum Compressor { pub enum Compressor {
Uncompressed(Uncompressed), Uncompressed(Uncompressed),
#[cfg(feature = "lzw")]
Lzw(Lzw), Lzw(Lzw),
#[cfg(feature = "deflate")]
Deflate(Deflate), Deflate(Deflate),
Packbits(Packbits), Packbits(Packbits),
} }
@@ -38,7 +47,7 @@ pub enum Compressor {
impl Default for Compressor { impl Default for Compressor {
/// The default compression strategy does not apply any compression. /// The default compression strategy does not apply any compression.
fn default() -> Self { fn default() -> Self {
Compressor::Uncompressed(Uncompressed::default()) Compressor::Uncompressed(Uncompressed)
} }
} }
@@ -46,7 +55,9 @@ impl CompressionAlgorithm for Compressor {
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> {
match self { match self {
Compressor::Uncompressed(algorithm) => algorithm.write_to(writer, bytes), Compressor::Uncompressed(algorithm) => algorithm.write_to(writer, bytes),
#[cfg(feature = "lzw")]
Compressor::Lzw(algorithm) => algorithm.write_to(writer, bytes), Compressor::Lzw(algorithm) => algorithm.write_to(writer, bytes),
#[cfg(feature = "deflate")]
Compressor::Deflate(algorithm) => algorithm.write_to(writer, bytes), Compressor::Deflate(algorithm) => algorithm.write_to(writer, bytes),
Compressor::Packbits(algorithm) => algorithm.write_to(writer, bytes), Compressor::Packbits(algorithm) => algorithm.write_to(writer, bytes),
} }
@@ -55,6 +66,5 @@ impl CompressionAlgorithm for Compressor {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
pub const TEST_DATA: &'static [u8] = pub const TEST_DATA: &[u8] = b"This is a string for checking various compression algorithms.";
b"This is a string for checking various compression algorithms.";
} }
+32 -42
View File
@@ -1,5 +1,5 @@
use crate::{encoder::compression::*, tags::CompressionMethod}; use crate::encoder::compression::*;
use std::io::{BufWriter, Error, ErrorKind, Write}; use std::io::{BufWriter, Error, ErrorKind};
/// Compressor that uses the Packbits[^note] algorithm to compress bytes. /// Compressor that uses the Packbits[^note] algorithm to compress bytes.
/// ///
@@ -80,33 +80,31 @@ impl CompressionAlgorithm for Packbits {
run_index = 0; run_index = 0;
in_run = false; in_run = false;
} }
} else { } else if bytes_pending > MAX_BYTES {
if bytes_pending > MAX_BYTES { // We have as much differing data as we can output in one chunk.
// We have as much differing data as we can output in one chunk. // Output MAX_BYTES leaving one byte.
// Output MAX_BYTES leaving one byte. offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(MAX_BYTES))?);
offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(MAX_BYTES))?); bufwriter.write_all(&bytes[pending_index..pending_index + MAX_BYTES as usize])?;
bufwriter.write(&bytes[pending_index..pending_index + MAX_BYTES as usize])?; bytes_written += 1 + MAX_BYTES as u64;
bytes_written += 1 + MAX_BYTES as u64;
pending_index += MAX_BYTES as usize; pending_index += MAX_BYTES as usize;
bytes_pending -= MAX_BYTES; bytes_pending -= MAX_BYTES;
run_index = bytes_pending - 1; // A run could start here run_index = bytes_pending - 1; // A run could start here
} else if curr_byte == last_byte { } else if curr_byte == last_byte {
if (bytes_pending - run_index >= MIN_REPT) || (run_index == 0) { if (bytes_pending - run_index >= MIN_REPT) || (run_index == 0) {
// This is a worthwhile run // This is a worthwhile run
if run_index != 0 { if run_index != 0 {
// Flush differing data out of input buffer // Flush differing data out of input buffer
offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(run_index))?); offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(run_index))?);
bufwriter bufwriter
.write(&bytes[pending_index..pending_index + run_index as usize])?; .write_all(&bytes[pending_index..pending_index + run_index as usize])?;
bytes_written += 1 + run_index as u64; bytes_written += 1 + run_index as u64;
}
bytes_pending -= run_index; // Length of run
in_run = true;
} }
} else { bytes_pending -= run_index; // Length of run
run_index = bytes_pending - 1; // A run could start here in_run = true;
} }
} else {
run_index = bytes_pending - 1; // A run could start here
} }
last_byte = curr_byte; last_byte = curr_byte;
} }
@@ -119,7 +117,7 @@ impl CompressionAlgorithm for Packbits {
} else { } else {
bytes_written += 1 + bytes_pending as u64; bytes_written += 1 + bytes_pending as u64;
offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(bytes_pending))?); offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(bytes_pending))?);
bufwriter.write(&bytes[pending_index..pending_index + bytes_pending as usize])?; bufwriter.write_all(&bytes[pending_index..pending_index + bytes_pending as usize])?;
} }
bufwriter.flush()?; bufwriter.flush()?;
@@ -141,24 +139,20 @@ mod tests {
let mut compressed_data = Vec::<u8>::new(); let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data); let mut writer = Cursor::new(&mut compressed_data);
Packbits::default() Packbits.write_to(&mut writer, &UNCOMPRESSED_DATA).unwrap();
.write_to(&mut writer, &UNCOMPRESSED_DATA)
.unwrap();
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA); assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
} }
#[test] #[test]
fn test_packbits_rept() { fn test_packbits_rept() {
// compress buffer with repetitive sequence // compress buffer with repetitive sequence
const UNCOMPRESSED_DATA: &'static [u8] = const UNCOMPRESSED_DATA: &[u8] =
b"This strrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrring hangs."; b"This strrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrring hangs.";
const EXPECTED_COMPRESSED_DATA: &'static [u8] = b"\x06This st\xD1r\x09ing hangs."; const EXPECTED_COMPRESSED_DATA: &[u8] = b"\x06This st\xD1r\x09ing hangs.";
let mut compressed_data = Vec::<u8>::new(); let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data); let mut writer = Cursor::new(&mut compressed_data);
Packbits::default() Packbits.write_to(&mut writer, UNCOMPRESSED_DATA).unwrap();
.write_to(&mut writer, UNCOMPRESSED_DATA)
.unwrap();
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA); assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
} }
@@ -192,23 +186,19 @@ mod tests {
let mut compressed_data = Vec::<u8>::new(); let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data); let mut writer = Cursor::new(&mut compressed_data);
Packbits::default() Packbits.write_to(&mut writer, data.as_slice()).unwrap();
.write_to(&mut writer, data.as_slice())
.unwrap();
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA); assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
} }
#[test] #[test]
fn test_packbits() { fn test_packbits() {
// compress teststring // compress teststring
const EXPECTED_COMPRESSED_DATA: &'static [u8] = const EXPECTED_COMPRESSED_DATA: &[u8] =
b"\x3CThis is a string for checking various compression algorithms."; b"\x3CThis is a string for checking various compression algorithms.";
let mut compressed_data = Vec::<u8>::new(); let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data); let mut writer = Cursor::new(&mut compressed_data);
Packbits::default() Packbits.write_to(&mut writer, TEST_DATA).unwrap();
.write_to(&mut writer, TEST_DATA)
.unwrap();
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA); assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
} }
} }
+3 -6
View File
@@ -1,5 +1,4 @@
use crate::{encoder::compression::*, tags::CompressionMethod}; use crate::encoder::compression::*;
use std::io::Write;
/// The default algorithm which does not compress at all. /// The default algorithm which does not compress at all.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -15,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)
} }
} }
@@ -29,9 +28,7 @@ mod tests {
fn test_no_compression() { fn test_no_compression() {
let mut compressed_data = Vec::<u8>::new(); let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data); let mut writer = Cursor::new(&mut compressed_data);
Uncompressed::default() Uncompressed.write_to(&mut writer, TEST_DATA).unwrap();
.write_to(&mut writer, TEST_DATA)
.unwrap();
assert_eq!(TEST_DATA, compressed_data); assert_eq!(TEST_DATA, compressed_data);
} }
} }
+850 -150
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());
+66 -1
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
} }
@@ -182,7 +186,68 @@ impl<W: Write> TiffWriter<W> {
impl<W: Seek> TiffWriter<W> { impl<W: Seek> TiffWriter<W> {
pub fn goto_offset(&mut self, offset: u64) -> Result<(), io::Error> { pub fn goto_offset(&mut self, offset: u64) -> Result<(), io::Error> {
self.offset = offset; self.offset = offset;
self.writer.seek(SeekFrom::Start(offset as u64))?; self.writer.seek(SeekFrom::Start(offset))?;
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()
);
}
}
+203 -318
View File
@@ -1,284 +1,220 @@
use std::error::Error;
use std::fmt;
use std::fmt::Display;
use std::io; use std::io;
use std::str; use std::str;
use std::string; use std::string;
use std::sync::Arc;
use jpeg::UnsupportedFeature; use quick_error::quick_error;
use crate::decoder::{ifd::Value, 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,
}; };
use crate::ColorType; use crate::ColorType;
use crate::weezl::LzwError; quick_error! {
/// Tiff error kinds.
#[derive(Debug)]
pub enum TiffError {
/// The Image is not formatted properly.
FormatError(err: TiffFormatError) {
display("format error: {err}")
from()
source(err)
}
/// The Decoder does not support features required by the image.
UnsupportedError(err: TiffUnsupportedError) {
display("unsupported error: {err}")
from()
source(err)
}
/// An I/O Error occurred while decoding the image.
IoError(err: io::Error) {
display("{err}")
from()
source(err)
}
/// The Limits of the Decoder is exceeded.
LimitsExceeded {
display("decoder limits exceeded")
}
/// An integer conversion to or from a platform size failed.
IntSizeError {
display("platform or format size limits exceeded")
}
/// The image does not support the requested operation
UsageError(err: UsageError) {
display("usage error: {err}")
from()
source(err)
}
}
}
/// Tiff error kinds. quick_error! {
#[derive(Debug)] /// The image is not formatted properly.
pub enum TiffError { ///
/// The Image is not formatted properly. /// This indicates that the encoder producing the image might behave incorrectly or that the
FormatError(TiffFormatError), /// input file has been corrupted.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum TiffFormatError {
TiffSignatureNotFound {
display("TIFF signature not found")
}
TiffSignatureInvalid {
display("TIFF signature invalid")
}
ImageFileDirectoryNotFound {
display("image file directory not found")
}
InconsistentSizesEncountered {
display("inconsistent sizes encountered")
}
InvalidDimensions(width: u32, height: u32) {
display("invalid dimensions: {width}x{height}")
}
InvalidTag {
display("image contains invalid tag")
}
InvalidTagValueType(tag: Tag) {
display("tag `{tag:?}` did not have the expected value type")
}
RequiredTagNotFound(tag: Tag) {
display("required tag `{tag:?}` not found")
}
UnknownPredictor(pred: u16) {
display("unknown predictor “{pred}” encountered")
}
UnknownPlanarConfiguration(cfg: u16) {
display("unknown planar configuration “{cfg}”")
}
InvalidTypeForTag {
display("tag has invalid type")
}
InvalidCountForTag(tag: Tag, len: usize) {
display("tag `{tag:?}` with incorrect number of elements ({len}) encountered")
}
StripTileTagConflict {
display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found")
}
CycleInOffsets {
display("file contained a cycle in the list of IFDs")
}
SamplesPerPixelIsZero {
display("samples per pixel is zero")
}
CompressedDataCorrupt(message: String) {
display("compressed data is corrupt: {message}")
}
}
}
quick_error! {
/// The Decoder does not support features required by the image. /// The Decoder does not support features required by the image.
UnsupportedError(TiffUnsupportedError), ///
/// This only captures known failures for which the standard either does not require support or an
/// An I/O Error occurred while decoding the image. /// implementation has been planned but not yet completed. Some variants may become unused over
IoError(io::Error), /// time and will then get deprecated before being removed.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
/// The Limits of the Decoder is exceeded. #[non_exhaustive]
LimitsExceeded, pub enum TiffUnsupportedError {
FloatingPointPredictor(color_type: ColorType) {
/// An integer conversion to or from a platform size failed, either due to display("floating point predictor for {color_type:?} is unsupported")
/// limits of the platform size or limits of the format. }
IntSizeError, HorizontalPredictor(color_type: ColorType) {
display("horizontal predictor for {color_type:?} is unsupported")
/// The image does not support the requested operation }
UsageError(UsageError), InconsistentBitsPerSample(bits_per_sample: Vec<u8>) {
} display("inconsistent bits per sample: {bits_per_sample:?}")
}
/// The image is not formatted properly. InterpretationWithBits(interpretation: PhotometricInterpretation, bits_per_sample: Vec<u8>) {
/// display("Photometric interpretation {interpretation:?} with bits per sample {bits_per_sample:?} is unsupported")
/// This indicates that the encoder producing the image might behave incorrectly or that the input }
/// file has been corrupted. UnknownInterpretation {
/// display("unknown photometric interpretation")
/// The list of variants may grow to incorporate errors of future features. Matching against this }
/// exhaustively is not covered by interface stability guarantees. UnknownCompressionMethod {
#[derive(Debug, Clone, PartialEq)] display("unknown compression method")
#[non_exhaustive] }
pub enum TiffFormatError { UnsupportedCompressionMethod(method: CompressionMethod) {
TiffSignatureNotFound, display("compression method {method:?} is unsupported")
TiffSignatureInvalid, }
ImageFileDirectoryNotFound, UnsupportedSampleDepth(depth: u8) {
InconsistentSizesEncountered, display("{depth} samples per pixel is unsupported")
UnexpectedCompressedData { }
actual_bytes: usize, UnsupportedSampleFormat(sample_format: Vec<SampleFormat>) {
required_bytes: usize, display("sample format {sample_format:?} is unsupported")
}, }
InconsistentStripSamples { UnsupportedColorType(color_type: ColorType) {
actual_samples: usize, display("color type {color_type:?} is unsupported")
required_samples: usize, }
}, UnsupportedBitsPerChannel(bits_per_channel: u8) {
InvalidDimensions(u32, u32), display("{bits_per_channel} bits per channel not supported")
InvalidTag, }
InvalidTagValueType(Tag), UnsupportedPlanarConfig(planar: Option<PlanarConfiguration>) {
RequiredTagNotFound(Tag), display("unsupported planar configuration “{planar:?}”")
UnknownPredictor(u16), }
ByteExpected(Value), UnsupportedDataType {
UnsignedIntegerExpected(Value), display("unsupported data type.")
SignedIntegerExpected(Value), }
Format(String), UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
RequiredTagEmpty(Tag), display("unsupported photometric interpretation \"{interpretation:?}\"")
StripTileTagConflict, }
CycleInOffsets, ChromaSubsampling {
JpegDecoder(JpegDecoderError), display("chroma subsampling of YCbCr color is unsupported")
} }
MisalignedTileBoundaries {
impl fmt::Display for TiffFormatError { display("tile rows are not aligned to byte boundaries")
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use self::TiffFormatError::*;
match *self {
TiffSignatureNotFound => write!(fmt, "TIFF signature not found."),
TiffSignatureInvalid => write!(fmt, "TIFF signature invalid."),
ImageFileDirectoryNotFound => write!(fmt, "Image file directory not found."),
InconsistentSizesEncountered => write!(fmt, "Inconsistent sizes encountered."),
UnexpectedCompressedData {
actual_bytes,
required_bytes,
} => {
write!(
fmt,
"Decompression returned different amount of bytes than expected: got {}, expected {}.",
actual_bytes, required_bytes
)
}
InconsistentStripSamples {
actual_samples,
required_samples,
} => {
write!(
fmt,
"Inconsistent elements in strip: got {}, expected {}.",
actual_samples, required_samples
)
}
InvalidDimensions(width, height) => write!(fmt, "Invalid dimensions: {}x{}.", width, height),
InvalidTag => write!(fmt, "Image contains invalid tag."),
InvalidTagValueType(ref tag) => {
write!(fmt, "Tag `{:?}` did not have the expected value type.", tag)
}
RequiredTagNotFound(ref tag) => write!(fmt, "Required tag `{:?}` not found.", tag),
UnknownPredictor(ref predictor) => {
write!(fmt, "Unknown predictor “{}” encountered", predictor)
}
ByteExpected(ref val) => write!(fmt, "Expected byte, {:?} found.", val),
UnsignedIntegerExpected(ref val) => {
write!(fmt, "Expected unsigned integer, {:?} found.", val)
}
SignedIntegerExpected(ref val) => {
write!(fmt, "Expected signed integer, {:?} found.", val)
}
Format(ref val) => write!(fmt, "Invalid format: {:?}.", val),
RequiredTagEmpty(ref val) => write!(fmt, "Required tag {:?} was empty.", val),
StripTileTagConflict => write!(fmt, "File should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found."),
CycleInOffsets => write!(fmt, "File contained a cycle in the list of IFDs"),
JpegDecoder(ref error) => write!(fmt, "{}", error),
} }
} }
} }
/// The Decoder does not support features required by the image. quick_error! {
/// /// User attempted to use the Decoder in a way that is incompatible with a specific image.
/// This only captures known failures for which the standard either does not require support or an ///
/// implementation has been planned but not yet completed. Some variants may become unused over /// For example: attempting to read a tile from a stripped image.
/// time and will then get deprecated before being removed. #[derive(Debug)]
/// #[non_exhaustive]
/// The list of variants may grow. Matching against this exhaustively is not covered by interface pub enum UsageError {
/// stability guarantees. InvalidChunkType(expected: ChunkType, actual: ChunkType) {
#[derive(Debug, Clone, PartialEq, Eq, Hash)] display("requested operation is only valid for images with chunk encoding of type {expected:?} but got {actual:?}")
#[non_exhaustive]
pub enum TiffUnsupportedError {
FloatingPointPredictor(ColorType),
HorizontalPredictor(ColorType),
InterpretationWithBits(PhotometricInterpretation, Vec<u8>),
UnknownInterpretation,
UnknownCompressionMethod,
UnsupportedCompressionMethod(CompressionMethod),
UnsupportedSampleDepth(u8),
UnsupportedSampleFormat(Vec<SampleFormat>),
UnsupportedColorType(ColorType),
UnsupportedBitsPerChannel(u8),
UnsupportedPlanarConfig(Option<PlanarConfiguration>),
UnsupportedDataType,
UnsupportedInterpretation(PhotometricInterpretation),
UnsupportedJpegFeature(UnsupportedFeature),
}
impl fmt::Display for TiffUnsupportedError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use self::TiffUnsupportedError::*;
match *self {
FloatingPointPredictor(color_type) => write!(
fmt,
"Floating point predictor for {:?} is unsupported.",
color_type
),
HorizontalPredictor(color_type) => write!(
fmt,
"Horizontal predictor for {:?} is unsupported.",
color_type
),
InterpretationWithBits(ref photometric_interpretation, ref bits_per_sample) => write!(
fmt,
"{:?} with {:?} bits per sample is unsupported",
photometric_interpretation, bits_per_sample
),
UnknownInterpretation => write!(
fmt,
"The image is using an unknown photometric interpretation."
),
UnknownCompressionMethod => write!(fmt, "Unknown compression method."),
UnsupportedCompressionMethod(method) => {
write!(fmt, "Compression method {:?} is unsupported", method)
}
UnsupportedSampleDepth(samples) => {
write!(fmt, "{} samples per pixel is unsupported.", samples)
}
UnsupportedSampleFormat(ref formats) => {
write!(fmt, "Sample format {:?} is unsupported.", formats)
}
UnsupportedColorType(color_type) => {
write!(fmt, "Color type {:?} is unsupported", color_type)
}
UnsupportedBitsPerChannel(bits) => {
write!(fmt, "{} bits per channel not supported", bits)
}
UnsupportedPlanarConfig(config) => {
write!(fmt, "Unsupported planar configuration “{:?}”.", config)
}
UnsupportedDataType => write!(fmt, "Unsupported data type."),
UnsupportedInterpretation(interpretation) => {
write!(
fmt,
"Unsupported photometric interpretation \"{:?}\".",
interpretation
)
}
UnsupportedJpegFeature(ref unsupported_feature) => {
write!(fmt, "Unsupported JPEG feature {:?}", unsupported_feature)
}
} }
} InvalidChunkIndex(index: u32) {
} display("invalid chunk index ({index}) requested")
/// User attempted to use the Decoder in a way that is incompatible with a specific image.
///
/// For example: attempting to read a tile from a stripped image.
#[derive(Debug)]
pub enum UsageError {
InvalidChunkType(ChunkType, ChunkType),
InvalidChunkIndex(u32),
}
impl fmt::Display for UsageError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::UsageError::*;
match *self {
InvalidChunkType(expected, actual) => {
write!(
fmt,
"Requested operation is only valid for images with chunk encoding of type: {:?}, got {:?}.",
expected, actual
)
}
InvalidChunkIndex(index) => write!(fmt, "Image chunk index ({}) requested.", index),
} }
} InvalidPlaneIndex(index: u16) {
} display("invalid plane index ({index}) requested")
impl fmt::Display for TiffError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
TiffError::FormatError(ref e) => write!(fmt, "Format error: {}", e),
TiffError::UnsupportedError(ref f) => write!(
fmt,
"The Decoder does not support the \
image format `{}`",
f
),
TiffError::IoError(ref e) => e.fmt(fmt),
TiffError::LimitsExceeded => write!(fmt, "The Decoder limits are exceeded"),
TiffError::IntSizeError => write!(fmt, "Platform or format size limits exceeded"),
TiffError::UsageError(ref e) => write!(fmt, "Usage error: {}", e),
} }
} InvalidCodingUnit(index: u32, have: u32) {
} display("out of bounds coding unit ({index}) requested, only {have} available")
impl Error for TiffError {
fn description(&self) -> &str {
match *self {
TiffError::FormatError(..) => "Format error",
TiffError::UnsupportedError(..) => "Unsupported error",
TiffError::IoError(..) => "IO error",
TiffError::LimitsExceeded => "Decoder limits exceeded",
TiffError::IntSizeError => "Platform or format size limits exceeded",
TiffError::UsageError(..) => "Invalid usage",
} }
} PredictorCompressionMismatch {
display("requested predictor is not compatible with the requested compression")
fn cause(&self) -> Option<&dyn Error> { }
match *self { PredictorIncompatible {
TiffError::IoError(ref e) => Some(e), display("the requested predictor is not compatible with the image's format")
_ => None, }
PredictorUnavailable {
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}")
} }
}
}
impl From<io::Error> for TiffError {
fn from(err: io::Error) -> TiffError {
TiffError::IoError(err)
} }
} }
@@ -294,74 +230,23 @@ impl From<string::FromUtf8Error> for TiffError {
} }
} }
impl From<TiffFormatError> for TiffError {
fn from(err: TiffFormatError) -> TiffError {
TiffError::FormatError(err)
}
}
impl From<TiffUnsupportedError> for TiffError {
fn from(err: TiffUnsupportedError) -> TiffError {
TiffError::UnsupportedError(err)
}
}
impl From<UsageError> for TiffError {
fn from(err: UsageError) -> TiffError {
TiffError::UsageError(err)
}
}
impl From<std::num::TryFromIntError> for TiffError { impl From<std::num::TryFromIntError> for TiffError {
fn from(_err: std::num::TryFromIntError) -> TiffError { fn from(_err: std::num::TryFromIntError) -> TiffError {
TiffError::IntSizeError TiffError::IntSizeError
} }
} }
impl From<LzwError> for TiffError { #[cfg(feature = "lzw")]
fn from(err: LzwError) -> TiffError { impl From<weezl::LzwError> for TiffError {
match err { fn from(err: weezl::LzwError) -> TiffError {
LzwError::InvalidCode => TiffError::FormatError(TiffFormatError::Format(String::from( TiffError::FormatError(TiffFormatError::CompressedDataCorrupt(err.to_string()))
"LZW compressed data corrupted",
))),
}
} }
} }
#[derive(Debug, Clone)] #[cfg(feature = "jpeg")]
pub struct JpegDecoderError { impl From<zune_jpeg::errors::DecodeErrors> for TiffError {
inner: Arc<jpeg::Error>, fn from(err: zune_jpeg::errors::DecodeErrors) -> Self {
} TiffError::FormatError(TiffFormatError::CompressedDataCorrupt(err.to_string()))
impl JpegDecoderError {
fn new(error: jpeg::Error) -> Self {
Self {
inner: Arc::new(error),
}
}
}
impl PartialEq for JpegDecoderError {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.inner, &other.inner)
}
}
impl Display for JpegDecoderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.fmt(f)
}
}
impl From<JpegDecoderError> for TiffError {
fn from(error: JpegDecoderError) -> Self {
TiffError::FormatError(TiffFormatError::JpegDecoder(error))
}
}
impl From<jpeg::Error> for TiffError {
fn from(error: jpeg::Error) -> Self {
JpegDecoderError::new(error).into()
} }
} }
+43 -3
View File
@@ -6,19 +6,19 @@
//! # Related Links //! # Related Links
//! * <https://web.archive.org/web/20210108073850/https://www.adobe.io/open/standards/TIFF.html> - The TIFF specification //! * <https://web.archive.org/web/20210108073850/https://www.adobe.io/open/standards/TIFF.html> - The TIFF specification
extern crate jpeg;
extern crate weezl;
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
#[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)] #[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)]
#[non_exhaustive]
pub enum ColorType { pub enum ColorType {
/// Pixel is grayscale /// Pixel is grayscale
Gray(u8), Gray(u8),
@@ -38,6 +38,46 @@ pub enum ColorType {
/// Pixel is CMYK /// Pixel is CMYK
CMYK(u8), CMYK(u8),
/// Pixel is CMYK with an alpha channel
CMYKA(u8),
/// 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
Multiband { bit_depth: u8, num_samples: u16 },
}
impl ColorType {
fn bit_depth(&self) -> u8 {
match *self {
ColorType::Gray(b)
| ColorType::RGB(b)
| ColorType::Palette(b)
| ColorType::GrayA(b)
| ColorType::RGBA(b)
| ColorType::CMYK(b)
| ColorType::CMYKA(b)
| ColorType::YCbCr(b)
| ColorType::Lab(b)
| ColorType::Multiband { bit_depth: b, .. } => b,
}
}
fn num_samples(&self) -> u16 {
match *self {
ColorType::Gray(_) => 1,
ColorType::RGB(_) => 3,
ColorType::Palette(_) => 1,
ColorType::GrayA(_) => 2,
ColorType::RGBA(_) => 4,
ColorType::CMYK(_) => 4,
ColorType::CMYKA(_) => 5,
ColorType::YCbCr(_) => 3,
ColorType::Lab(_) => 3,
ColorType::Multiband { num_samples, .. } => num_samples,
}
}
} }
+374 -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) { pub enum CompressionMethod(u16) unknown(
/// A custom compression method
unknown
) {
None = 1, None = 1,
Huffman = 2, Huffman = 2,
Fax3 = 3, Fax3 = 3,
@@ -184,6 +264,9 @@ pub enum CompressionMethod(u16) {
Deflate = 8, Deflate = 8,
OldDeflate = 0x80B2, OldDeflate = 0x80B2,
PackBits = 0x8005, PackBits = 0x8005,
// Self-assigned by libtiff
ZSTD = 0xC350,
} }
} }
@@ -197,6 +280,8 @@ pub enum PhotometricInterpretation(u16) {
CMYK = 5, CMYK = 5,
YCbCr = 6, YCbCr = 6,
CIELab = 8, CIELab = 8,
IccLab = 9,
ItuLab = 10,
} }
} }
@@ -209,8 +294,14 @@ pub enum PlanarConfiguration(u16) {
tags! { tags! {
pub enum Predictor(u16) { pub enum Predictor(u16) {
/// No changes were made to the data
None = 1, None = 1,
/// The images' rows were processed to contain the difference of each pixel from the previous one.
///
/// This means that instead of having in order `[r1, g1. b1, r2, g2 ...]` you will find
/// `[r1, g1, b1, r2-r1, g2-g1, b2-b1, r3-r2, g3-g2, ...]`
Horizontal = 2, Horizontal = 2,
/// Not currently supported
FloatingPoint = 3, FloatingPoint = 3,
} }
} }
@@ -225,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
+195
View File
@@ -0,0 +1,195 @@
extern crate tiff;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::ColorType;
use std::fs::File;
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images/";
/// Test a basic all white image
#[test]
fn test_white_ieee_fp16() {
let filenames = ["white-fp16.tiff"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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.dimensions().expect("Cannot get dimensions"),
(256, 256)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
for p in img {
assert!(p == half::f16::from_f32_const(1.0));
}
} else {
panic!("Wrong data type");
}
}
}
/// Test a single black pixel, to make sure scaling is ok
#[test]
fn test_one_black_pixel_ieee_fp16() {
let filenames = ["single-black-fp16.tiff"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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.dimensions().expect("Cannot get dimensions"),
(256, 256)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
for (i, p) in img.iter().enumerate() {
if i == 0 {
assert!(p < &half::f16::from_f32_const(0.001));
} else {
assert!(p == &half::f16::from_f32_const(1.0));
}
}
} else {
panic!("Wrong data type");
}
}
}
/// Test white with horizontal differencing predictor
#[test]
fn test_pattern_horizontal_differencing_ieee_fp16() {
let filenames = ["white-fp16-pred2.tiff"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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.dimensions().expect("Cannot get dimensions"),
(256, 256)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
// 0, 2, 5, 8, 12, 16, 255 are black
let black = [0, 2, 5, 8, 12, 16, 255];
for (i, p) in img.iter().enumerate() {
if black.contains(&i) {
assert!(p < &half::f16::from_f32_const(0.001));
} else {
assert!(p == &half::f16::from_f32_const(1.0));
}
}
} else {
panic!("Wrong data type");
}
}
}
/// Test white with floating point predictor
#[test]
fn test_pattern_predictor_ieee_fp16() {
let filenames = ["white-fp16-pred3.tiff"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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.dimensions().expect("Cannot get dimensions"),
(256, 256)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
// 0, 2, 5, 8, 12, 16, 255 are black
let black = [0, 2, 5, 8, 12, 16, 255];
for (i, p) in img.iter().enumerate() {
if black.contains(&i) {
assert!(p < &half::f16::from_f32_const(0.001));
} else {
assert!(p == &half::f16::from_f32_const(1.0));
}
}
} else {
panic!("Wrong data type");
}
}
}
/// Test several random images
/// we'rell compare against a pnm file, that scales from 0 (for 0.0) to 65767 (for 1.0)
#[test]
fn test_predictor_ieee_fp16() {
// first parse pnm, skip the first 4 \n
let pnm_path = PathBuf::from(TEST_IMAGE_DIR).join("random-fp16.pgm");
let pnm_bytes = std::fs::read(pnm_path).expect("Failed to read expected PNM file");
// PGM looks like this:
// ---
// P5
// #Created with GIMP
// 16 16
// 65535
// ... <big-endian bytes>
// ---
// get index of 4th \n
let byte_start = pnm_bytes
.iter()
.enumerate()
.filter(|(_, &v)| v == b'\n')
.map(|(i, _)| i)
.nth(3)
.expect("Must be 4 \\n's");
let pnm_values: Vec<f32> = pnm_bytes[(byte_start + 1)..]
.chunks(2)
.map(|slice| {
let bts = [slice[0], slice[1]];
(u16::from_be_bytes(bts) as f32) / (u16::MAX as f32)
})
.collect();
assert!(pnm_values.len() == 256);
let filenames = [
"random-fp16-pred2.tiff",
"random-fp16-pred3.tiff",
"random-fp16.tiff",
];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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.dimensions().expect("Cannot get dimensions"),
(16, 16)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
for (exp, found) in std::iter::zip(pnm_values.iter(), img.iter()) {
assert!((exp - found.to_f32()).abs() < 0.0001);
}
} else {
panic!("Wrong data type");
}
}
}
+63
View File
@@ -0,0 +1,63 @@
extern crate tiff;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::tags::Tag;
use tiff::ColorType;
use std::fs::File;
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images";
#[test]
fn test_geo_tiff() {
let filenames = ["geo-5b.tif"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
decoder = decoder.with_limits(tiff::decoder::Limits::unlimited());
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(10, 10)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Multiband {
bit_depth: 16,
num_samples: 5
}
);
assert_eq!(
decoder
.get_tag_u64(Tag::StripOffsets)
.expect("Cannot get StripOffsets"),
418
);
assert_eq!(
decoder
.get_tag_u64(Tag::RowsPerStrip)
.expect("Cannot get RowsPerStrip"),
10
);
assert_eq!(
decoder
.get_tag_u64(Tag::StripByteCounts)
.expect("Cannot get StripByteCounts"),
1000
);
assert_eq!(
decoder
.get_tag(Tag::ModelPixelScaleTag)
.expect("Cannot get pixel scale")
.into_f64_vec()
.expect("Cannot get pixel scale"),
vec![60.0, 60.0, 0.0]
);
let DecodingResult::I16(data) = decoder.read_image().unwrap() else {
panic!("Cannot read band data")
};
assert_eq!(data.len(), 500);
}
}
+263 -20
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:?}"),
}; };
} }
} }
@@ -216,6 +217,11 @@ fn issue_69() {
//assert!(img_res.is_ok()); //assert!(img_res.is_ok());
//} //}
#[test]
fn test_tiled_gray_i1() {
test_image_sum_u8("tiled-gray-i1.tif", ColorType::Gray(1), 30531);
}
#[test] #[test]
fn test_tiled_rgb_u8() { fn test_tiled_rgb_u8() {
test_image_sum_u8("tiled-rgb-u8.tif", ColorType::RGB(8), 39528948); test_image_sum_u8("tiled-rgb-u8.tif", ColorType::RGB(8), 39528948);
@@ -226,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);
@@ -285,6 +339,51 @@ fn test_tiled_incremental() {
} }
} }
#[test]
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
let file = "planar-rgb-u8.tif";
let expected_type = ColorType::RGB(8);
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");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let chunks = decoder.strip_count().unwrap();
assert_eq!(chunks as usize, 72);
// convert -quiet planar-rgb-u8.tif[0] -crop 1x1+0+0 txt:
// 0,0: (73,51,30) #49331E srgb(73,51,30)
// 1st band (red)
match decoder.read_chunk(0).unwrap() {
DecodingResult::U8(chunk) => {
assert_eq!(chunk[0], 73);
}
_ => panic!("Wrong bit depth"),
}
// 2nd band (green)
match decoder.read_chunk(chunks / 3).unwrap() {
DecodingResult::U8(chunk) => {
assert_eq!(chunk[0], 51);
}
_ => panic!("Wrong bit depth"),
}
// 3rd band (blue)
match decoder.read_chunk(chunks / 3 * 2).unwrap() {
DecodingResult::U8(chunk) => {
assert_eq!(chunk[0], 30);
}
_ => panic!("Wrong bit depth"),
}
}
#[test] #[test]
fn test_div_zero() { fn test_div_zero() {
use tiff::{TiffError, TiffFormatError}; use tiff::{TiffError, TiffFormatError};
@@ -301,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}"),
} }
} }
@@ -319,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}"),
} }
} }
@@ -333,9 +432,7 @@ fn fuzzer_testcase5() {
178, 178, 178, 178, 178, 178,
]; ];
let mut decoder = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap(); let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let _ = decoder.read_image().unwrap_err();
} }
#[test] #[test]
@@ -352,7 +449,7 @@ fn fuzzer_testcase1() {
} }
#[test] #[test]
fn test_stripped_image_overflow() { fn fuzzer_testcase6() {
let image = [ let image = [
73, 73, 42, 0, 8, 0, 0, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 100, 0, 0, 148, 1, 1, 4, 0, 1, 0, 73, 73, 42, 0, 8, 0, 0, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 100, 0, 0, 148, 1, 1, 4, 0, 1, 0,
0, 0, 158, 0, 0, 251, 3, 1, 3, 255, 254, 255, 255, 0, 1, 0, 0, 0, 6, 1, 3, 0, 1, 0, 0, 0, 0, 0, 158, 0, 0, 251, 3, 1, 3, 255, 254, 255, 255, 0, 1, 0, 0, 0, 6, 1, 3, 0, 1, 0, 0, 0,
@@ -361,14 +458,7 @@ fn test_stripped_image_overflow() {
178, 178, 178, 178,
]; ];
let mut decoder = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap(); let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let err = decoder.read_image().unwrap_err();
match err {
tiff::TiffError::LimitsExceeded => {}
unexpected => panic!("Unexpected error {}", unexpected),
}
} }
#[test] #[test]
@@ -459,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:?}"),
} }
} }
@@ -477,3 +567,156 @@ fn test_predictor_3_rgb_f32() {
fn test_predictor_3_gray_f32() { fn test_predictor_3_gray_f32() {
test_image_sum_f32("predictor-3-gray-f32.tif", ColorType::Gray(32), 20008.275); test_image_sum_f32("predictor-3-gray-f32.tif", ColorType::Gray(32), 20008.275);
} }
#[test]
#[cfg(feature = "zstd")]
fn test_zstd_compression() {
// gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif
test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396);
}
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);
}
}
}
+170 -6
View File
@@ -36,11 +36,55 @@ fn encode_decode() {
let mut decoder = Decoder::new(&mut file).unwrap(); let mut decoder = Decoder::new(&mut file).unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8)); assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100)); assert_eq!(decoder.dimensions().unwrap(), (100, 100));
let mut all_tags = decoder
.tag_iter()
.filter_map(Result::ok)
.collect::<Vec<_>>();
all_tags.sort_by_key(|(t, _)| t.to_u16());
assert_eq!(
all_tags,
vec![
(Tag::ImageWidth, ifd::Value::Unsigned(100)),
(Tag::ImageLength, ifd::Value::Unsigned(100)),
(
Tag::BitsPerSample,
ifd::Value::List(vec![
ifd::Value::Short(8),
ifd::Value::Short(8),
ifd::Value::Short(8)
])
),
(Tag::Compression, ifd::Value::Short(1)),
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
(Tag::StripOffsets, ifd::Value::Unsigned(35)),
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
(Tag::StripByteCounts, ifd::Value::Unsigned(30000)),
(Tag::XResolution, ifd::Value::Rational(1, 1)),
(Tag::YResolution, ifd::Value::Rational(1, 1)),
(Tag::ResolutionUnit, ifd::Value::Short(1)),
(Tag::Artist, ifd::Value::Ascii("Image-tiff".into())),
(Tag::Predictor, ifd::Value::Short(1)),
(
Tag::SampleFormat,
ifd::Value::List(vec![
ifd::Value::Short(1),
ifd::Value::Short(1),
ifd::Value::Short(1)
])
),
]
);
assert_eq!( assert_eq!(
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");
@@ -75,11 +119,54 @@ fn encode_decode_big() {
let mut decoder = Decoder::new(&mut file).unwrap(); let mut decoder = Decoder::new(&mut file).unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8)); assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100)); assert_eq!(decoder.dimensions().unwrap(), (100, 100));
let mut all_tags = decoder
.tag_iter()
.filter_map(Result::ok)
.collect::<Vec<_>>();
all_tags.sort_by_key(|(t, _)| t.to_u16());
assert_eq!(
all_tags,
vec![
(Tag::ImageWidth, ifd::Value::Unsigned(100)),
(Tag::ImageLength, ifd::Value::Unsigned(100)),
(
Tag::BitsPerSample,
ifd::Value::List(vec![
ifd::Value::Short(8),
ifd::Value::Short(8),
ifd::Value::Short(8)
])
),
(Tag::Compression, ifd::Value::Short(1)),
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
(Tag::StripOffsets, ifd::Value::UnsignedBig(27)),
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
(Tag::StripByteCounts, ifd::Value::UnsignedBig(30000)),
(Tag::XResolution, ifd::Value::Rational(1, 1)),
(Tag::YResolution, ifd::Value::Rational(1, 1)),
(Tag::ResolutionUnit, ifd::Value::Short(1)),
(Tag::Artist, ifd::Value::Ascii("Image-tiff".into())),
(Tag::Predictor, ifd::Value::Short(1)),
(
Tag::SampleFormat,
ifd::Value::List(vec![
ifd::Value::Short(1),
ifd::Value::Short(1),
ifd::Value::Short(1)
])
),
]
);
assert_eq!( assert_eq!(
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");
@@ -153,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,
@@ -162,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"),
}; };
@@ -176,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");
} }
@@ -188,7 +281,9 @@ macro_rules! test_roundtrip {
} }
test_roundtrip!(test_u8_roundtrip, U8, u8); test_roundtrip!(test_u8_roundtrip, U8, u8);
test_roundtrip!(test_i8_roundtrip, I8, i8);
test_roundtrip!(test_u16_roundtrip, U16, u16); test_roundtrip!(test_u16_roundtrip, U16, u16);
test_roundtrip!(test_i16_roundtrip, I16, i16);
test_roundtrip!(test_u32_roundtrip, U32, u32); test_roundtrip!(test_u32_roundtrip, U32, u32);
test_roundtrip!(test_u64_roundtrip, U64, u64); test_roundtrip!(test_u64_roundtrip, U64, u64);
test_roundtrip!(test_f32_roundtrip, F32, f32); test_roundtrip!(test_f32_roundtrip, F32, f32);
@@ -199,6 +294,11 @@ fn test_gray_u8_roundtrip() {
test_u8_roundtrip::<colortype::Gray8>("minisblack-1c-8b.tiff", ColorType::Gray(8)); test_u8_roundtrip::<colortype::Gray8>("minisblack-1c-8b.tiff", ColorType::Gray(8));
} }
#[test]
fn test_gray_i8_roundtrip() {
test_i8_roundtrip::<colortype::GrayI8>("minisblack-1c-i8b.tiff", ColorType::Gray(8));
}
#[test] #[test]
fn test_rgb_u8_roundtrip() { fn test_rgb_u8_roundtrip() {
test_u8_roundtrip::<colortype::RGB8>("rgb-3c-8b.tiff", ColorType::RGB(8)); test_u8_roundtrip::<colortype::RGB8>("rgb-3c-8b.tiff", ColorType::RGB(8));
@@ -209,11 +309,21 @@ fn test_cmyk_u8_roundtrip() {
test_u8_roundtrip::<colortype::CMYK8>("cmyk-3c-8b.tiff", ColorType::CMYK(8)); test_u8_roundtrip::<colortype::CMYK8>("cmyk-3c-8b.tiff", ColorType::CMYK(8));
} }
#[test]
fn test_cmyka_u8_roundtrip() {
test_u8_roundtrip::<colortype::CMYKA8>("cmyk-4c-8b.tiff", ColorType::CMYKA(8));
}
#[test] #[test]
fn test_gray_u16_roundtrip() { fn test_gray_u16_roundtrip() {
test_u16_roundtrip::<colortype::Gray16>("minisblack-1c-16b.tiff", ColorType::Gray(16)); test_u16_roundtrip::<colortype::Gray16>("minisblack-1c-16b.tiff", ColorType::Gray(16));
} }
#[test]
fn test_gray_i16_roundtrip() {
test_i16_roundtrip::<colortype::GrayI16>("minisblack-1c-i16b.tiff", ColorType::Gray(16));
}
#[test] #[test]
fn test_rgb_u16_roundtrip() { fn test_rgb_u16_roundtrip() {
test_u16_roundtrip::<colortype::RGB16>("rgb-3c-16b.tiff", ColorType::RGB(16)); test_u16_roundtrip::<colortype::RGB16>("rgb-3c-16b.tiff", ColorType::RGB(16));
@@ -522,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);
}
}
+20 -19
View File
@@ -6,7 +6,7 @@ use tiff::{
encoder::{ encoder::{
colortype::{self, ColorType}, colortype::{self, ColorType},
compression::*, compression::*,
TiffEncoder, TiffValue, Compression, TiffEncoder, TiffValue,
}, },
}; };
@@ -18,15 +18,12 @@ trait TestImage<const NUM_CHANNELS: usize>: From<Vec<<Self::Color as ColorType>:
fn reference_data(&self) -> &[<Self::Color as ColorType>::Inner]; fn reference_data(&self) -> &[<Self::Color as ColorType>::Inner];
fn generate_pixel(x: u32, y: u32) -> [<Self::Color as ColorType>::Inner; NUM_CHANNELS]; fn generate_pixel(x: u32, y: u32) -> [<Self::Color as ColorType>::Inner; NUM_CHANNELS];
fn compress<C: Compression, W: Write + Seek>( fn compress<W: Write + Seek>(&self, encoder: &mut TiffEncoder<W>)
&self, where
encoder: &mut TiffEncoder<W>,
compression: C,
) where
[<Self::Color as ColorType>::Inner]: TiffValue, [<Self::Color as ColorType>::Inner]: TiffValue,
{ {
let image = encoder let image = encoder
.new_image_with_compression::<Self::Color, C>(Self::WIDTH, Self::HEIGHT, compression) .new_image::<Self::Color>(Self::WIDTH, Self::HEIGHT)
.unwrap(); .unwrap();
image.write_data(self.reference_data()).unwrap(); image.write_data(self.reference_data()).unwrap();
} }
@@ -94,7 +91,7 @@ impl TestImage<1> for TestImageGrayscale {
} }
} }
fn encode_decode_with_compression<C: Compression + Clone>(compression: C) { fn encode_decode_with_compression(compression: Compression) {
let mut data = Cursor::new(Vec::new()); let mut data = Cursor::new(Vec::new());
let image_rgb = TestImageColor::generate(); let image_rgb = TestImageColor::generate();
@@ -103,9 +100,11 @@ fn encode_decode_with_compression<C: Compression + Clone>(compression: C) {
// Encode tiff with compression // Encode tiff with compression
{ {
// Create a multipage image with 2 images // Create a multipage image with 2 images
let mut encoder = TiffEncoder::new(&mut data).unwrap(); let mut encoder = TiffEncoder::new(&mut data)
image_rgb.compress(&mut encoder, compression.clone()); .unwrap()
image_grayscale.compress(&mut encoder, compression); .with_compression(compression);
image_rgb.compress(&mut encoder);
image_grayscale.compress(&mut encoder);
} }
// Decode tiff // Decode tiff
@@ -117,7 +116,7 @@ fn encode_decode_with_compression<C: Compression + Clone>(compression: C) {
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()
); );
@@ -127,7 +126,7 @@ fn encode_decode_with_compression<C: Compression + Clone>(compression: C) {
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()
); );
@@ -136,22 +135,24 @@ fn encode_decode_with_compression<C: Compression + Clone>(compression: C) {
#[test] #[test]
fn encode_decode_without_compression() { fn encode_decode_without_compression() {
encode_decode_with_compression(Uncompressed::default()); encode_decode_with_compression(Compression::Uncompressed);
} }
#[test] #[test]
#[cfg(feature = "lzw")]
fn encode_decode_with_lzw() { fn encode_decode_with_lzw() {
encode_decode_with_compression(Lzw::default()); encode_decode_with_compression(Compression::Lzw);
} }
#[test] #[test]
#[cfg(feature = "deflate")]
fn encode_decode_with_deflate() { fn encode_decode_with_deflate() {
encode_decode_with_compression(Deflate::with_level(DeflateLevel::Fast)); encode_decode_with_compression(Compression::Deflate(DeflateLevel::Fast));
encode_decode_with_compression(Deflate::with_level(DeflateLevel::Balanced)); encode_decode_with_compression(Compression::Deflate(DeflateLevel::Balanced));
encode_decode_with_compression(Deflate::with_level(DeflateLevel::Best)); encode_decode_with_compression(Compression::Deflate(DeflateLevel::Best));
} }
#[test] #[test]
fn encode_decode_with_packbits() { fn encode_decode_with_packbits() {
encode_decode_with_compression(Packbits::default()); encode_decode_with_compression(Compression::Packbits);
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+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());
}
}
+231
View File
@@ -0,0 +1,231 @@
extern crate tiff;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::encoder::{colortype, Predictor, TiffEncoder};
use tiff::ColorType;
use std::fs::File;
use std::io::{Cursor, Seek, SeekFrom};
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images/";
macro_rules! test_predict {
($name:ident, $buffer:ident, $buffer_ty:ty) => {
fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
file: &str,
expected_type: ColorType,
) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(file).expect("Cannot create decoder!");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let image_data = match decoder.read_image().unwrap() {
DecodingResult::$buffer(res) => res,
_ => panic!("Wrong data type"),
};
let mut predicted = Vec::with_capacity(image_data.len());
C::horizontal_predict(&image_data, &mut predicted);
let sample_size = C::SAMPLE_FORMAT.len();
(0..sample_size).for_each(|i| {
assert_eq!(predicted[i], image_data[i]);
});
(sample_size..image_data.len()).for_each(|i| {
predicted[i] = predicted[i].wrapping_add(predicted[i - sample_size]);
assert_eq!(predicted[i], image_data[i]);
});
}
};
}
test_predict!(test_u8_predict, U8, u8);
test_predict!(test_i8_predict, I8, i8);
test_predict!(test_u16_predict, U16, u16);
test_predict!(test_i16_predict, I16, i16);
test_predict!(test_u32_predict, U32, u32);
test_predict!(test_u64_predict, U64, u64);
#[test]
fn test_gray_u8_predict() {
test_u8_predict::<colortype::Gray8>("minisblack-1c-8b.tiff", ColorType::Gray(8));
}
#[test]
fn test_gray_i8_predict() {
test_i8_predict::<colortype::GrayI8>("minisblack-1c-i8b.tiff", ColorType::Gray(8));
}
#[test]
fn test_rgb_u8_predict() {
test_u8_predict::<colortype::RGB8>("rgb-3c-8b.tiff", ColorType::RGB(8));
}
#[test]
fn test_cmyk_u8_predict() {
test_u8_predict::<colortype::CMYK8>("cmyk-3c-8b.tiff", ColorType::CMYK(8));
}
#[test]
fn test_gray_u16_predict() {
test_u16_predict::<colortype::Gray16>("minisblack-1c-16b.tiff", ColorType::Gray(16));
}
#[test]
fn test_gray_i16_predict() {
test_i16_predict::<colortype::GrayI16>("minisblack-1c-i16b.tiff", ColorType::Gray(16));
}
#[test]
fn test_rgb_u16_predict() {
test_u16_predict::<colortype::RGB16>("rgb-3c-16b.tiff", ColorType::RGB(16));
}
#[test]
fn test_cmyk_u16_predict() {
test_u16_predict::<colortype::CMYK16>("cmyk-3c-16b.tiff", ColorType::CMYK(16));
}
#[test]
fn test_gray_u32_predict() {
test_u32_predict::<colortype::Gray32>("gradient-1c-32b.tiff", ColorType::Gray(32));
}
#[test]
fn test_rgb_u32_predict() {
test_u32_predict::<colortype::RGB32>("gradient-3c-32b.tiff", ColorType::RGB(32));
}
#[test]
fn test_gray_u64_predict() {
test_u64_predict::<colortype::Gray64>("gradient-1c-64b.tiff", ColorType::Gray(64));
}
#[test]
fn test_rgb_u64_predict() {
test_u64_predict::<colortype::RGB64>("gradient-3c-64b.tiff", ColorType::RGB(64));
}
#[test]
fn test_ycbcr_u8_predict() {
test_u8_predict::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
}
macro_rules! test_predict_roundtrip {
($name:ident, $buffer:ident, $buffer_ty:ty) => {
fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
file: &str,
expected_type: ColorType,
) {
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");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let image_data = match decoder.read_image().unwrap() {
DecodingResult::$buffer(res) => res,
_ => panic!("Wrong data type"),
};
let mut file = Cursor::new(Vec::new());
{
let mut tiff = TiffEncoder::new(&mut file)
.unwrap()
.with_predictor(Predictor::Horizontal);
let (width, height) = decoder.dimensions().unwrap();
tiff.write_image::<C>(width, height, &image_data).unwrap();
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::new(&mut file).unwrap();
if let DecodingResult::$buffer(img_res) =
decoder.read_image().expect("Decoding image failed")
{
assert_eq!(image_data, img_res);
} else {
panic!("Wrong data type");
}
}
}
};
}
test_predict_roundtrip!(test_u8_predict_roundtrip, U8, u8);
test_predict_roundtrip!(test_i8_predict_roundtrip, I8, i8);
test_predict_roundtrip!(test_u16_predict_roundtrip, U16, u16);
test_predict_roundtrip!(test_i16_predict_roundtrip, I16, i16);
test_predict_roundtrip!(test_u32_predict_roundtrip, U32, u32);
test_predict_roundtrip!(test_u64_predict_roundtrip, U64, u64);
#[test]
fn test_gray_u8_predict_roundtrip() {
test_u8_predict_roundtrip::<colortype::Gray8>("minisblack-1c-8b.tiff", ColorType::Gray(8));
}
#[test]
fn test_gray_i8_predict_roundtrip() {
test_i8_predict_roundtrip::<colortype::GrayI8>("minisblack-1c-i8b.tiff", ColorType::Gray(8));
}
#[test]
fn test_rgb_u8_predict_roundtrip() {
test_u8_predict_roundtrip::<colortype::RGB8>("rgb-3c-8b.tiff", ColorType::RGB(8));
}
#[test]
fn test_cmyk_u8_predict_roundtrip() {
test_u8_predict_roundtrip::<colortype::CMYK8>("cmyk-3c-8b.tiff", ColorType::CMYK(8));
}
#[test]
fn test_gray_u16_predict_roundtrip() {
test_u16_predict_roundtrip::<colortype::Gray16>("minisblack-1c-16b.tiff", ColorType::Gray(16));
}
#[test]
fn test_gray_i16_predict_roundtrip() {
test_i16_predict_roundtrip::<colortype::GrayI16>(
"minisblack-1c-i16b.tiff",
ColorType::Gray(16),
);
}
#[test]
fn test_rgb_u16_predict_roundtrip() {
test_u16_predict_roundtrip::<colortype::RGB16>("rgb-3c-16b.tiff", ColorType::RGB(16));
}
#[test]
fn test_cmyk_u16_predict_roundtrip() {
test_u16_predict_roundtrip::<colortype::CMYK16>("cmyk-3c-16b.tiff", ColorType::CMYK(16));
}
#[test]
fn test_gray_u32_predict_roundtrip() {
test_u32_predict_roundtrip::<colortype::Gray32>("gradient-1c-32b.tiff", ColorType::Gray(32));
}
#[test]
fn test_rgb_u32_predict_roundtrip() {
test_u32_predict_roundtrip::<colortype::RGB32>("gradient-3c-32b.tiff", ColorType::RGB(32));
}
#[test]
fn test_gray_u64_predict_roundtrip() {
test_u64_predict_roundtrip::<colortype::Gray64>("gradient-1c-64b.tiff", ColorType::Gray(64));
}
#[test]
fn test_rgb_u64_predict_roundtrip() {
test_u64_predict_roundtrip::<colortype::RGB64>("gradient-3c-64b.tiff", ColorType::RGB(64));
}
#[test]
fn test_ycbcr_u8_predict_roundtrip() {
test_u8_predict_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
}
+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.