Compare commits

...
Author SHA1 Message Date
A. Molzer d7d24d0f7c Changelog entry for tile treatment 2026-03-08 03:00:15 +01:00
A. Molzer 58e93fb8a3 Align tile detection to libtiff 2026-03-08 03:00:15 +01:00
Aurelia Molzer e8be8656f2 Merge pull request #353 from Shnatsel/non-exhaustive-compression
Make Compression enum non-exhaustive
2026-03-06 10:45:28 +00:00
Sergey "Shnatsel" Davidoff e1e9f36c5a Make Compression enum non-exhaustive 2026-03-06 09:49:10 +00:00
Aurelia Molzer 0ad653433a Merge pull request #352 from image-rs/iterate-over-raw-tiff-or-exif
Open a Tiff without interpreting the first directory
2026-03-06 08:34:19 +00:00
A. Molzer bf79af8de4 Add Decoder::open, deprecate new 2026-03-06 09:31:23 +01:00
Aurelia Molzer 54dddc4943 Merge pull request #347 from image-rs/iterate-over-raw-tiff-or-exif
Enable iteration over raw, malformed tiff or exif
2026-03-06 07:31:13 +00:00
A. Molzer be9b3a04b9 Expand documentation with TiffHeader 2026-03-06 06:05:32 +01:00
A. Molzer 08f1b25fac Drafting release notes 2026-03-06 05:35:45 +01:00
A. Molzer c7b97519a0 Error guarantee on decoder IFD iteration 2026-03-06 04:42:02 +01:00
A. Molzer e093e15bbe Remove redundant next_ifd attribute 2026-03-06 03:44:08 +01:00
A. Molzer 647e9b4a81 Add TiffHeader to open a file without seeking 2026-03-06 02:59:45 +01:00
Aurelia Molzer 31c49ea6ef Merge pull request #351 from Shnatsel/semver-breaking-deflate-level
Implement DEFLATE compression level control properly, with a semver break
2026-03-05 20:12:27 +00:00
Sergey "Shnatsel" Davidoff 0362e6e31d Implement DEFLATE compression level control properly, with a semver break 2026-03-05 17:59:18 +00:00
Sergey "Shnatsel" Davidoff 0fe5b2804e Merge pull request #348 from Shnatsel/deflate-level
Expose precise DEFLATE level controls
2026-03-05 17:23:59 +00:00
Sergey "Shnatsel" Davidoff a589010099 Fix typo 2026-03-05 12:22:37 +00:00
Sergey "Shnatsel" Davidoff 53498074a4 Merge impl blocks 2026-03-05 12:22:25 +00:00
Sergey "Shnatsel" Davidoff cb543cded7 Expose precise DEFLATE level controls 2026-03-05 11:58:18 +00:00
A. Molzer cf8f96568e Expose Decoder::seek_to_directory 2026-03-04 08:59:41 +01:00
A. Molzer 4ff88e658d Rewrite Decoder::seek_to_image 2026-03-04 05:12:31 +01:00
A. Molzer 14ebf376f3 Move Directory from image into decoder proper
This prepares allowing to iterate over a TIFF file with invalid images
while `Image` maintains invariants. For instance we precalculate the
amount of buffer space and verify that the count of strips makes sense
in the constructor—so we do not have to redo that for every other entry
point.
2026-03-04 03:37:42 +01:00
Aurelia Molzer 37578add53 Merge pull request #344 from RunDevelopment/fix-libtiffpic-test-for-windows
Fix libtiffpic test for Windows
2026-03-03 20:42:20 +00:00
Aurelia Molzer 2b9476566b Merge pull request #346 from RunDevelopment/invert-color-as-chunks
More optimal code gen for `invert_colors`
2026-03-03 20:30:20 +00:00
Aurelia Molzer 448a0e61d1 Merge pull request #345 from mpospelova/increase-group4reader-height-size
Change Group4Reader height dimension from u16 to u32
2026-03-03 20:23:52 +00:00
RunDevelopment d872ff8751 Unify grayscale integer code paths 2026-03-03 14:54:32 +01:00
RunDevelopment 0a878888db More optimal code gen for invert_colors 2026-03-03 14:36:16 +01:00
Maria Pospelova a0a95ef86e Fix formatting 2026-03-03 13:26:09 +00:00
RunDevelopment bbb5e5a41b Use std::path::MAIN_SEPARATOR 2026-03-03 14:13:00 +01:00
RunDevelopment f74169d4af Fix libtiffpic test for Windows 2026-03-03 14:11:24 +01:00
Maria Pospelova c304a3cc13 Upgrade Group4Reader dimensions from u16 to u32 2026-03-03 10:50:27 +00:00
Sergey "Shnatsel" Davidoff 9a9193345c Merge pull request #343 from Shnatsel/float-predictor-opt
Optimize floating-point predictor encoding
2026-02-27 01:14:36 +00:00
Sergey "Shnatsel" DavidoffandClaude 22046d4272 Simplify byte shuffle to direct scatter writes
Replace chunked scratch-buffer approach with direct indexed writes
into the pre-allocated output. Simpler and faster in practice.

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

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

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

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

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

This complements the existing decoding support for Predictor 3.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 23:05:35 +00:00
Aurelia Molzer 2ac8be1177 Merge pull request #337 from image-rs/cieluv
The whole libtiffpic test suite and SgiLuv compression
2026-02-20 06:47:57 +00:00
A. Molzer c81d0a9c94 Skip endianess swap for SGI compression 2026-02-14 18:04:07 +01:00
A. Molzer 983c5baea0 Introduce LogL(uv) encodings 2026-02-14 18:04:07 +01:00
A. Molzer abf3cc617d Bump MSRV to 1.88 2026-02-14 18:04:07 +01:00
A. Molzer fb4ee9b377 Convert float images in decode example 2026-02-14 18:04:07 +01:00
A. Molzer 45cd3c3b63 Improve unexpected tile count error
Do we even want to error early here? This isn't exactly fatal to reading
the image it is a only nuisance. It becomes crucial only when we are at
the point of reading all the tiles themselves.
2026-02-14 06:55:29 +01:00
A. Molzer 701fcc46b8 Add libtiffpic test suite
Two tests are disabled, both related to tiles, since they do not follow
the fully released spec themselves but incidentally decode with libtiff
anyways. The two images (`cramps-tile.tif`, `quad-tile.tif`) are tiled
layout which libtiff detects by the presence of TileWidth/TileHeight
tags. We would detect that by chunks encoded via TileOffsets and
TileByteLengths tags. However the images encoded their chunk data into
the StripOffsets tag; additionally they contain a RowsPerStrip value
anyways.

Tests disabled as this is to be resolved in a separate issue (#338) with
a decision on treatment of this situation (strictness setting or copying
libtiff, also error late for mismatched lengths).
2026-02-14 06:55:29 +01:00
Aurelia Molzer e41159ce05 Merge pull request #340 from image-rs/release-0.11.3
Metadata for 0.11.3 release
2026-02-10 04:17:11 +00:00
A. Molzer 03979faef8 Metadata for 0.11.3 release 2026-02-07 08:38:03 +01:00
Aurelia Molzer 857973cb21 Merge pull request #339 from image-rs/file-listing-and-utils
File listing utils and example
2026-02-07 07:33:24 +00:00
A. Molzer cef21c47fb Add example listing a files directories 2026-02-07 08:22:34 +01:00
Jonathan BehrensandA. Molzer a0191ed1a7 Support decoding WebP compressed TIFFs (#336)
Co-authored-by: A. Molzer <5550310+197g@users.noreply.github.com>
2026-01-29 10:53:00 -08:00
Aurelia Molzer 99a7b8cba7 Merge pull request #334 from image-rs/release-0.11.2
Release notes for 0.11.2
2026-01-25 02:36:26 +00:00
A. Molzer 418ea182c3 Release notes for 0.11.2 2026-01-25 00:33:33 +01:00
Aurelia Molzer 529fa64e13 Merge pull request #333 from image-rs/read-image-all-planes
Make `read_image_to_buffer` read out all planes
2026-01-24 23:28:07 +00:00
A. Molzer 3d6b94b65a Make read_image_to_buffer read out all planes 2026-01-24 21:24:55 +01:00
Aurelia Molzer a51b23091e Merge pull request #332 from image-rs/public-example-for-pam
Public ColorType::{num_samples, bit_depth}
2026-01-24 04:26:31 +00:00
A. Molzer ac65bd8477 Public ColorType::{num_samples, bit_depth}
This makes the decode example capable of writing correct PAM images in
many more cases, such as bilevel encoded (Fax3) masks.
2026-01-24 05:24:19 +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
112 changed files with 4741 additions and 697 deletions

No files matched your search

+4 -4
View File
@@ -9,14 +9,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
rust: ["1.74.0", stable, beta, nightly]
rust: ["1.88.0", stable, beta, nightly]
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@nightly
if: ${{ matrix.rust == '1.74.0' }}
if: ${{ matrix.rust == '1.88.0' }}
- name: Generate Cargo.lock with minimal-version dependencies
if: ${{ matrix.rust == '1.74.0' }}
if: ${{ matrix.rust == '1.88.0' }}
run: cargo -Zminimal-versions generate-lockfile
- uses: dtolnay/rust-toolchain@v1
@@ -29,7 +29,7 @@ jobs:
- name: build
run: cargo build -v
- name: test
if: ${{ matrix.rust != '1.74.0' }}
if: ${{ matrix.rust != '1.88.0' }}
run: cargo test -v && cargo doc -v
features:
+127 -1
View File
@@ -1,7 +1,133 @@
# Version 0.10.4 (unreleased)
# Version 0.12.0 (unreleased)
Additions:
- Added the `TiffHeader` type. It can be used to create a `Decoder` in a state
before the first IFD and allows opening files at an arbitrary first IFD
offset rather than one indicated by an encoded header at the start.
Changes:
- The IFD iteration methods on `Decoder` (`next_image`, `seek_to_image`, etc)
now come with a strong error guarantee. They will not modify the current
directory state if they fail. This includes the failure to interpret an IFD
as a supported image in the case of `*_image` variants.
Compatibility:
- Changed how tiled TIFF files are detected. The presence of a TileWidth OR
TileHeight/TileLength tag now marks an image as tiled. Data is no retrieved
from either StripOffsets or TileOffsets with the latter taking precedence.
This is closer to the behavior of libtiff.
# Version 0.11.3
Additions:
- Added support for `webp` compression (as per GDAL) under the `webp` feature flag (default: off).
- Added `Decoder::image_ifd`, creating an `IfdDecoder` of the current IFD.
- Added `IfdDecoder::directory`, returning the `Directory` underlying it.
- Added `LowerHex` and `UpwerHex` implementations for `IfdPointer`.
Notes:
- A new example `tiff-ls` demonstrates use of the new features.
# Version 0.11.2
Changes:
- Planar files can now be read with `Decoder::read_image_to_buffer` if the
limits allow for such a large buffer size. Previously only the first plane
was read and the decoder will attempt a fallback to this behavior if the
limits do not allow all planes to be allocated.
Fixes:
- An off-by-one error in a `debug_assert` of `ValueBuffer` caused its use in
`IfdDecoder::find_tag_buf` to panic outside release mode when the buffer
exactly matched the required size.
Additions:
- The methods `ColorType::{num_samples, bit_depth}` are now public. This
promises that we only have homogeneous color depths in this major version.
# 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:
+16 -3
View File
@@ -1,11 +1,11 @@
[package]
name = "tiff"
version = "0.10.3"
version = "0.11.3"
edition = "2021"
resolver = "2"
# note: when changed, also update test runner in `.github/workflows/rust.yml`
rust-version = "1.74.0"
rust-version = "1.88.0"
license = "MIT"
description = "TIFF decoding and encoding library in pure Rust"
@@ -14,7 +14,7 @@ authors = ["The image-rs Developers"]
repository = "https://github.com/image-rs/image-tiff"
categories = ["multimedia::images", "multimedia::encoding"]
exclude = ["tests/images/*", "tests/fuzz_images/*"]
exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
[dependencies]
half = { version = "2.4.1" }
@@ -26,11 +26,14 @@ 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 }
image-webp = {version = "0.2.4", optional = true }
[dev-dependencies]
criterion = "0.3.1"
# We may already depend on this via flate2
crc32fast = "1.5"
libtest-mimic = "0.8.1"
walkdir = "2.5"
[features]
default = ["deflate", "fax", "jpeg", "lzw"]
@@ -40,8 +43,18 @@ deflate = ["dep:flate2"]
fax = ["dep:fax34"]
jpeg = ["dep:zune-jpeg"]
lzw = ["dep:weezl"]
webp = ["dep:image-webp"]
zstd = ["dep:zstd"]
[[bench]]
name = "lzw"
harness = false
[[bench]]
name = "encode"
harness = false
[[test]]
name = "decode_libtiffpic"
path = "tests/decode_libtiffpic.rs"
harness = false
+44
View File
@@ -0,0 +1,44 @@
extern crate criterion;
extern crate tiff;
use criterion::{black_box, Criterion, Throughput};
use std::io::Cursor;
use tiff::encoder::colortype::RGB32Float;
use tiff::encoder::{Compression, Predictor, TiffEncoder};
const WIDTH: u32 = 500;
const HEIGHT: u32 = 500;
fn encode_image_f32_float_predictor_deflate(data: &[f32]) {
let mut buf = Cursor::new(Vec::with_capacity(WIDTH as usize * HEIGHT as usize * 4));
TiffEncoder::new(&mut buf)
.unwrap()
.with_predictor(Predictor::FloatingPoint)
.with_compression(Compression::Deflate(1))
.write_image::<RGB32Float>(WIDTH, HEIGHT, black_box(data))
.unwrap();
}
fn main() {
// Generate synthetic f32 image data with a gradient pattern
let data: Vec<f32> = (0..WIDTH as usize * HEIGHT as usize * 3)
.map(|i| {
let pixel = i / 3;
let x = (pixel % WIDTH as usize) as f32 / WIDTH as f32;
let y = (pixel / WIDTH as usize) as f32 / HEIGHT as f32;
x * 0.5 + y * 0.5
})
.collect();
let mut c = Criterion::default().configure_from_args();
let mut group = c.benchmark_group("tiff-encode");
group
.sample_size(20)
.throughput(Throughput::Bytes(
(WIDTH as u64) * (HEIGHT as u64) * 3 * std::mem::size_of::<f32>() as u64,
))
.bench_function("f32-float-predictor-deflate-fast", |b| {
b.iter(|| encode_image_f32_float_predictor_deflate(&data))
});
}
+4 -5
View File
@@ -8,13 +8,12 @@ use tiff::decoder::Decoder;
fn read_image(image: &[u8]) {
let image = std::io::Cursor::new(image);
let decoder = Decoder::new(black_box(image));
let mut reader = decoder.unwrap();
let decoder = Decoder::open(black_box(image));
while {
let mut reader = decoder.unwrap();
while reader.more_images() {
reader.read_image().unwrap();
reader.more_images()
} {}
}
}
fn main() {
+231
View File
@@ -0,0 +1,231 @@
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::open(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),
) {
match data {
DecodingResult::F16(v) => {
let samples = v
.iter()
.map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16)
.collect::<Vec<_>>();
*data = DecodingResult::U16(samples);
}
DecodingResult::F32(v) => {
let samples = v
.iter()
.map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16)
.collect::<Vec<_>>();
*data = DecodingResult::U16(samples);
}
DecodingResult::F64(v) => {
let samples = v
.iter()
.map(|&f| (f * f64::from(u16::MAX)) as u16)
.collect::<Vec<_>>();
*data = DecodingResult::U16(samples);
}
_ => {}
}
let (depth, mut tupltype) = match colortype {
// Note: we will expand, so this is in fact not BLACKANDWHITE
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;
}
};
// Number of samples in a line and number of sample lines in the image, depending on whether
// they are planar or not. This determines the layout of the image buffer when expanding bit
// packed samples to a full byte (we support power-of-two sample sizes, positive and negative,
// but only those).
let (swidth, sheight) = if layout.planes > 1 {
(
u64::from(width) * u64::from(colortype.num_samples()),
u64::from(height),
)
} else {
(
u64::from(width),
u64::from(height) * u64::from(colortype.num_samples()),
)
};
let mut fallback_buffer;
let maxval = match data.as_buffer(0) {
// `U8` is also our representation of bit-packed color. PAM uses the smallest number of
// *bytes* needed to represent a sample but we must also adhere to maxval. We could explode
// them here (e.g. represent 1-bit as `{0, 1}`) but we'd also set `maxval` accordingly and
// this requires tupltype to be `BLACKANDWHITE` so introducing a bunch of complexity. If
// you want to do that in your real code, go ahead.
DecodingBuffer::U8(buf) => match colortype.bit_depth() {
8 => u64::from(u8::MAX),
4 => {
fallback_buffer = vec![0; (swidth * sheight) as usize];
expand_4bit(buf, swidth, &mut fallback_buffer);
*data = DecodingResult::U8(fallback_buffer);
u64::from(u8::MAX)
}
2 => {
fallback_buffer = vec![0; (swidth * sheight) as usize];
expand_2bit(buf, swidth, &mut fallback_buffer);
*data = DecodingResult::U8(fallback_buffer);
u64::from(u8::MAX)
}
1 => {
fallback_buffer = vec![0; (swidth * sheight) as usize];
expand_1bit(buf, swidth, &mut fallback_buffer);
*data = DecodingResult::U8(fallback_buffer);
u64::from(u8::MAX)
}
_ => {
eprintln!(
"Unsupported bit depth for U8 buffer: {}",
colortype.bit_depth()
);
return;
}
},
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");
}
}
/// Put exactly the ith `SCALE`-bit chunk of `val` into the least significant bits.
#[inline(always)]
fn bits_extract<const SCALE: u8>(val: u8, i: u8) -> u8 {
// Shift left to discard high bits, then right to bring desired bits down. This way also
// conveniently avoids all cases of overflow.
(val << (i * SCALE)) >> (8 - SCALE)
}
fn expand_1bit(from: &[u8], samples_per_padded: u64, into: &mut [u8]) {
let stride = samples_per_padded.div_ceil(8);
let from_row = from.chunks_exact(stride as usize);
let into_row = into.chunks_exact_mut(samples_per_padded as usize);
for (from_row, into_row) in from_row.zip(into_row) {
for (into, &from) in into_row.chunks_mut(8).zip(from_row) {
let data =
core::array::from_fn::<u8, 8, _>(|i| 0xff * bits_extract::<1>(from, i as u8));
into.copy_from_slice(&data[..into.len()]);
}
}
}
fn expand_2bit(from: &[u8], samples_per_padded: u64, into: &mut [u8]) {
let stride = samples_per_padded.div_ceil(4);
let from_row = from.chunks_exact(stride as usize);
let into_row = into.chunks_exact_mut(samples_per_padded as usize);
for (from_row, into_row) in from_row.zip(into_row) {
for (into, &from) in into_row.chunks_mut(4).zip(from_row) {
let data =
core::array::from_fn::<u8, 4, _>(|i| 0xaa * bits_extract::<2>(from, i as u8));
into.copy_from_slice(&data[..into.len()]);
}
}
}
fn expand_4bit(from: &[u8], samples_per_padded: u64, into: &mut [u8]) {
let stride = samples_per_padded.div_ceil(2);
let from_row = from.chunks_exact(stride as usize);
let into_row = into.chunks_exact_mut(samples_per_padded as usize);
for (from_row, into_row) in from_row.zip(into_row) {
for (into, &from) in into_row.chunks_mut(2).zip(from_row) {
let data =
core::array::from_fn::<u8, 2, _>(|i| 0x88 * bits_extract::<4>(from, i as u8));
into.copy_from_slice(&data[..into.len()]);
}
}
}
+121
View File
@@ -0,0 +1,121 @@
use std::borrow::Cow;
use tiff::decoder::Decoder;
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::open(io)?;
while reader.more_images() {
reader.next_directory()?;
ls_dir(&mut reader);
}
Ok(())
}
fn ls_dir<R: std::io::Read + std::io::Seek>(reader: &mut Decoder<R>) {
if let Some(ifd) = reader.ifd_pointer() {
println!("Directory at {ifd:x}");
}
println!("Name\tHex\tType\tCount");
let ifd = reader.current_ifd();
for (tag, entry) in ifd.directory().iter() {
let name: &'static str = match tag {
tiff::tags::Tag::Artist => "Artist",
tiff::tags::Tag::BitsPerSample => "BitsPerSample",
tiff::tags::Tag::CellLength => "CellLength",
tiff::tags::Tag::CellWidth => "CellWidth",
tiff::tags::Tag::ColorMap => "ColorMap",
tiff::tags::Tag::Compression => "Compression",
tiff::tags::Tag::DateTime => "DateTime",
tiff::tags::Tag::ExtraSamples => "ExtraSamples",
tiff::tags::Tag::FillOrder => "FillOrder",
tiff::tags::Tag::FreeByteCounts => "FreeByteCounts",
tiff::tags::Tag::FreeOffsets => "FreeOffsets",
tiff::tags::Tag::GrayResponseCurve => "GrayResponseCurve",
tiff::tags::Tag::GrayResponseUnit => "GrayResponseUnit",
tiff::tags::Tag::HostComputer => "HostComputer",
tiff::tags::Tag::ImageDescription => "ImageDescription",
tiff::tags::Tag::ImageLength => "ImageLength",
tiff::tags::Tag::ImageWidth => "ImageWidth",
tiff::tags::Tag::Make => "Make",
tiff::tags::Tag::MaxSampleValue => "MaxSampleValue",
tiff::tags::Tag::MinSampleValue => "MinSampleValue",
tiff::tags::Tag::Model => "Model",
tiff::tags::Tag::NewSubfileType => "NewSubfileType",
tiff::tags::Tag::Orientation => "Orientation",
tiff::tags::Tag::PhotometricInterpretation => "PhotometricInterpretation",
tiff::tags::Tag::PlanarConfiguration => "PlanarConfiguration",
tiff::tags::Tag::ResolutionUnit => "ResolutionUnit",
tiff::tags::Tag::RowsPerStrip => "RowsPerStrip",
tiff::tags::Tag::SamplesPerPixel => "SamplesPerPixel",
tiff::tags::Tag::Software => "Software",
tiff::tags::Tag::StripByteCounts => "StripByteCounts",
tiff::tags::Tag::StripOffsets => "StripOffsets",
tiff::tags::Tag::SubfileType => "SubfileType",
tiff::tags::Tag::Threshholding => "Threshholding",
tiff::tags::Tag::XResolution => "XResolution",
tiff::tags::Tag::YResolution => "YResolution",
tiff::tags::Tag::Predictor => "Predictor",
tiff::tags::Tag::TileWidth => "TileWidth",
tiff::tags::Tag::TileLength => "TileLength",
tiff::tags::Tag::TileOffsets => "TileOffsets",
tiff::tags::Tag::TileByteCounts => "TileByteCounts",
tiff::tags::Tag::SubIfd => "SubIfd",
tiff::tags::Tag::SampleFormat => "SampleFormat",
tiff::tags::Tag::SMinSampleValue => "SMinSampleValue",
tiff::tags::Tag::SMaxSampleValue => "SMaxSampleValue",
tiff::tags::Tag::JPEGTables => "JPEGTables",
tiff::tags::Tag::ChromaSubsampling => "ChromaSubsampling",
tiff::tags::Tag::ChromaPositioning => "ChromaPositioning",
tiff::tags::Tag::ModelPixelScaleTag => "ModelPixelScaleTag",
tiff::tags::Tag::ModelTransformationTag => "ModelTransformationTag",
tiff::tags::Tag::ModelTiepointTag => "ModelTiepointTag",
tiff::tags::Tag::Copyright => "Copyright",
tiff::tags::Tag::ExifDirectory => "ExifDirectory",
tiff::tags::Tag::GpsDirectory => "GpsDirectory",
tiff::tags::Tag::IccProfile => "IccProfile",
tiff::tags::Tag::GeoKeyDirectoryTag => "GeoKeyDirectoryTag",
tiff::tags::Tag::GeoDoubleParamsTag => "GeoDoubleParamsTag",
tiff::tags::Tag::GeoAsciiParamsTag => "GeoAsciiParamsTag",
tiff::tags::Tag::ExifVersion => "ExifVersion",
tiff::tags::Tag::GdalNodata => "GdalNodata",
_ => "<unknown>",
};
let ty: Cow<'static, str> = match entry.field_type() {
tiff::tags::Type::BYTE => "u8".into(),
tiff::tags::Type::ASCII => "ascii".into(),
tiff::tags::Type::SHORT => "u16".into(),
tiff::tags::Type::LONG => "u32".into(),
tiff::tags::Type::RATIONAL => "r32".into(),
tiff::tags::Type::SBYTE => "i8".into(),
tiff::tags::Type::UNDEFINED => "byte".into(),
tiff::tags::Type::SSHORT => "s16".into(),
tiff::tags::Type::SLONG => "s32".into(),
tiff::tags::Type::SRATIONAL => "sr32".into(),
tiff::tags::Type::FLOAT => "f32".into(),
tiff::tags::Type::DOUBLE => "f64".into(),
tiff::tags::Type::IFD => "ifd32".into(),
tiff::tags::Type::LONG8 => "u64".into(),
tiff::tags::Type::SLONG8 => "i64".into(),
tiff::tags::Type::IFD8 => "ifd64".into(),
other => format!("{:x}", other.to_u16()).into(),
};
eprintln!(
"{name:16}\t{tag:4x}\t{ty}\t{count}",
tag = tag.to_u16(),
count = entry.count(),
);
}
}
+144 -12
View File
@@ -6,12 +6,12 @@ use std::mem;
use std::str;
use super::stream::{ByteOrder, EndianReader};
use crate::tags::{IfdPointer, Tag, Type};
use crate::tags::{IfdPointer, Tag, Type, ValueBuffer};
use crate::{TiffError, TiffFormatError, TiffResult};
use self::Value::{
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, RationalBig, SRational, SRationalBig,
Short, Signed, SignedBig, SignedByte, SignedShort, Unsigned, UnsignedBig,
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, SRational, Short, Signed, SignedBig,
SignedByte, SignedShort, Unsigned, UnsignedBig,
};
#[allow(unused_qualifications)]
@@ -30,8 +30,16 @@ pub enum Value {
Double(f64),
List(Vec<Value>),
Rational(u32, u32),
#[deprecated(
note = "Not implemented in BigTIFF with a standard tag value",
since = "0.11.1"
)]
RationalBig(u64, u64),
SRational(i32, i32),
#[deprecated(
note = "Not implemented in BigTIFF with a standard tag value",
since = "0.11.1"
)]
SRationalBig(i64, i64),
Ascii(String),
Ifd(u32),
@@ -170,7 +178,8 @@ impl Value {
Unsigned(val) => Ok(vec![val]),
UnsignedBig(val) => Ok(vec![u32::try_from(val)?]),
Rational(numerator, denominator) => Ok(vec![numerator, denominator]),
RationalBig(numerator, denominator) => {
#[expect(deprecated)]
Value::RationalBig(numerator, denominator) => {
Ok(vec![u32::try_from(numerator)?, u32::try_from(denominator)?])
}
Ifd(val) => Ok(vec![val]),
@@ -220,7 +229,8 @@ impl Value {
new_vec.push(numerator);
new_vec.push(denominator);
}
SRationalBig(numerator, denominator) => {
#[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => {
new_vec.push(i32::try_from(numerator)?);
new_vec.push(i32::try_from(denominator)?);
}
@@ -234,7 +244,8 @@ impl Value {
Signed(val) => Ok(vec![val]),
SignedBig(val) => Ok(vec![i32::try_from(val)?]),
SRational(numerator, denominator) => Ok(vec![numerator, denominator]),
SRationalBig(numerator, denominator) => {
#[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => {
Ok(vec![i32::try_from(numerator)?, i32::try_from(denominator)?])
}
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
@@ -283,7 +294,8 @@ impl Value {
Unsigned(val) => Ok(vec![val.into()]),
UnsignedBig(val) => Ok(vec![val]),
Rational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
#[expect(deprecated)]
Value::RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
Ifd(val) => Ok(vec![val.into()]),
IfdBig(val) => Ok(vec![val]),
Ascii(val) => Ok(val.chars().map(u32::from).map(u64::from).collect()),
@@ -301,7 +313,8 @@ impl Value {
new_vec.push(numerator.into());
new_vec.push(denominator.into());
}
SRationalBig(numerator, denominator) => {
#[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => {
new_vec.push(numerator);
new_vec.push(denominator);
}
@@ -315,7 +328,8 @@ impl Value {
Signed(val) => Ok(vec![val.into()]),
SignedBig(val) => Ok(vec![val]),
SRational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
#[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
@@ -565,7 +579,7 @@ impl Entry {
// Case 4: there is more than one value, and it doesn't fit in the offset field.
let mut v;
self.set_reader_offset(bo, bigtiff, reader)?;
self.set_reader_offset_relative(bigtiff, reader, 0)?;
match self.type_ {
Type::BYTE | Type::UNDEFINED => {
@@ -723,6 +737,113 @@ impl Entry {
Ok(List(v))
}
pub(crate) fn buffered_value<R: Read + Seek>(
&self,
buf: &mut ValueBuffer,
limits: &super::Limits,
bigtiff: bool,
reader: &mut EndianReader<R>,
) -> TiffResult<()> {
if self.count == 0 {
buf.assume_type(self.type_, 0, reader.byte_order);
return Ok(());
}
let value_bytes = self.buffer_with_capacity(buf, limits)?;
// Case 1: the value fits in the offset field.
if value_bytes <= 4 || bigtiff && value_bytes <= 8 {
let src = &self.offset[..value_bytes];
buf.raw_bytes_mut()[..value_bytes].copy_from_slice(src);
buf.assume_type(self.type_, self.count, reader.byte_order);
return Ok(());
}
// Case 2: the value is stored in the reader at an offset.
self.set_reader_offset_relative(bigtiff, reader, 0)?;
// In case of an error we set the type and endianess.
buf.assume_type(self.type_, 0, reader.byte_order);
let target = &mut buf.raw_bytes_mut()[..value_bytes];
// FIXME: if the read fails we have already grown to full size, which is not great.
reader.inner().read_exact(target)?;
buf.assume_type(self.type_, self.count, reader.byte_order);
Ok(())
}
pub(crate) fn raw_value_at<R: Read + Seek>(
&self,
buf: &mut [u8],
bigtiff: bool,
reader: &mut EndianReader<R>,
at: u64,
) -> TiffResult<usize> {
if self.count == 0 {
return Ok(0);
}
// We have no limits to handle, we do not allocate.
let value_bytes = self.type_.value_bytes(self.count)?;
// No bytes to fill into the buffer.
if at >= value_bytes {
return Ok(0);
}
// Case 1: the value fits in the offset field.
if value_bytes <= 4 || bigtiff && value_bytes <= 8 {
// `at < value_bytes` and `value_bytes <= 8` so casting is mathematical
let src = &self.offset[..value_bytes as usize][at as usize..];
let len = src.len().min(buf.len());
buf[..len].copy_from_slice(&src[..len]);
return Ok(value_bytes as usize);
}
// Case 2: the value is stored in the reader at an offset. We will find the offset
// encoded in the entry, apply the relative start position and seek there.
self.set_reader_offset_relative(bigtiff, reader, at)?;
let remainder = value_bytes - at;
let len = usize::try_from(remainder)
.unwrap_or(usize::MAX)
.min(buf.len());
let target = &mut buf[..len];
reader.inner().read_exact(target)?;
// Design note: in a previous draft we would consume the rest of the bytes of this value
// here (into a stack buffer if need be) to verify the stream itself. But in the end we
// have `Seek` so we better verify this by seeking over the rest of the bytes, finding if
// the stream continues that far. Even that is maybe bad if we wanted to provide a
// async-adaptor that `WouldBlock` errors to fill back a read window then the seek is
// poison to that, too.
// So a really simple choice: The caller is responsible for handling the fact that this did
// not verify the whole value. Attempt a 1-byte read at the end of the value instead?
Ok(len)
}
// Returns `Ok(bytes)` if our value's bytes through type and count fit into `usize` and are
// within the limits. Extends the buffer to that many bytes.
fn buffer_with_capacity(
&self,
buf: &mut ValueBuffer,
limits: &super::Limits,
) -> TiffResult<usize> {
let bytes = self.type_.value_bytes(self.count())?;
let allowed_length = usize::try_from(bytes)
.ok()
.filter(|&n| n <= limits.decoding_buffer_size)
.ok_or(TiffError::LimitsExceeded)?;
buf.prepare_length(allowed_length);
Ok(allowed_length)
}
fn vec_with_capacity(
value_count: u64,
limits: &super::Limits,
@@ -736,21 +857,32 @@ impl Entry {
Ok(Vec::with_capacity(value_count))
}
fn set_reader_offset<R>(
/// Seek to an offset within a value stored in the offset defined by this entry.
fn set_reader_offset_relative<R>(
&self,
bo: ByteOrder,
bigtiff: bool,
reader: &mut EndianReader<R>,
at: u64,
) -> TiffResult<()>
where
R: Read + Seek,
{
let bo = reader.byte_order;
let offset = if bigtiff {
self.offset_field_reader(bo).read_u64()?
} else {
self.offset_field_reader(bo).read_u32()?.into()
};
// FIXME: `at` should be within `self.type_.value_bytes(self.count)` and that itself should
// be within the bounds of the stream. But we do not check this eagerly so this below will
// fail sometimes differently for exotic streams, depending on the method by which we read
// (at once or through multiple raw into-byte-slice reads).
let offset = offset.checked_add(at).ok_or(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
))?;
reader.goto_offset(offset)?;
Ok(())
+722 -148
View File
File diff suppressed because it is too large. Load diff
+541
View File
@@ -0,0 +1,541 @@
use std::io::{Read, Take};
use crate::TiffError;
pub struct LogLuv24<R> {
reader: Take<R>,
/// The required buffer size. We only decompressed all samples at once.
chunk_sz: usize,
}
impl<R> LogLuv24<R> {
pub fn new(reader: R, n: u64, npixels: u32) -> Result<Self, TiffError>
where
R: Read,
{
let chunk_sz = usize::try_from(
u64::from(npixels)
.checked_mul(4 * 3)
.ok_or(TiffError::LimitsExceeded)?,
)?;
let reader = reader.take(n);
Ok(LogLuv24 { reader, chunk_sz })
}
fn expand_buf(buf: &mut [u8]) {
let data_len = buf.len() / 4;
for i in (0..data_len / 3).rev() {
let ch = buf.as_chunks_mut::<3>().0[i];
let [a, b, c] = ch.map(u32::from);
let raw = a << 16 | b << 8 | c;
let l = (raw >> 14) & 0x3ff;
let uv = decode_quantized_uv(raw & 0x3fff);
let by = dequantize_24_luma(l);
let (u, v) = dequantize_24(uv);
let s = 1. / (6. * u - 16. * v + 12.);
let x = 9. * u * s;
let y = 4. * v * s;
let bx = x / y * by;
let bz = (1. - x - y) / y * by;
let [r, g, b] = xyz_to_rgb([bx, by, bz]);
let into = &mut buf.as_chunks_mut::<12>().0[i];
into[0..4].copy_from_slice(&f32::to_ne_bytes(r));
into[4..8].copy_from_slice(&f32::to_ne_bytes(g));
into[8..12].copy_from_slice(&f32::to_ne_bytes(b));
}
}
}
impl<R: Read> Read for LogLuv24<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.read_exact(buf)?;
Ok(buf.len())
}
// FIXME: this is the interface we have for compressed chunks but it is not optimal for what we
// want. Note `fill_buf` is not super helpful since it does not guarantee any size of the
// filled buffer but we need 24-bits at a time. Two code paths is even more awkward.
fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
if !buf.len().is_multiple_of(self.chunk_sz) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"buffer size {0} must match the chunk size for compression method SGILog24 {1}",
buf.len(),
self.chunk_sz,
),
));
}
let data_len = buf.len() / 4;
self.reader.read_exact(&mut buf[..data_len])?;
Self::expand_buf(buf);
Ok(())
}
}
fn packbits_decode<R: Read, const N: usize>(
reader: &mut R,
buf: &mut [[u8; N]],
) -> std::io::Result<()> {
for offset in (0..N).rev() {
let mut i = 0;
let mut code = [0u8; 1];
loop {
let Some(pixels) = buf.get_mut(i..) else {
break;
};
if pixels.is_empty() {
break;
}
let pixels = pixels.iter_mut();
reader.read_exact(&mut code)?;
let [repeat] = code;
if repeat >= 128 {
reader.read_exact(&mut code)?;
let [value] = code;
let rc = (repeat - 128) + 2;
for (_, px) in (0..rc).zip(pixels) {
px[offset] = value;
}
i += usize::from(rc);
} else {
for (_, px) in (0..repeat).zip(pixels) {
reader.read_exact(&mut code)?;
let [value] = code;
px[offset] = value;
}
i += usize::from(repeat);
}
}
}
Ok(())
}
pub struct LogLuv32<R> {
reader: Take<R>,
/// The required buffer size. We only decompressed all samples at once.
chunk_sz: usize,
}
impl<R: Read> LogLuv32<R> {
pub fn new(reader: R, n: u64, npixels: u32) -> Result<Self, TiffError> {
let chunk_sz = usize::try_from(
u64::from(npixels)
.checked_mul(4 * 3)
.ok_or(TiffError::LimitsExceeded)?,
)?;
let reader = reader.take(n);
Ok(LogLuv32 { reader, chunk_sz })
}
fn decode_rle_row(&mut self, row: &mut [u8]) -> std::io::Result<()>
where
R: Read,
{
// 32-bit per pixel format.
let n_pixels = self.chunk_sz / 12;
// buffer for reading (using only 32-bit per pixel)
let raw = &mut row.as_chunks_mut::<4>().0[..n_pixels];
packbits_decode::<_, 4>(&mut self.reader, raw)?;
Ok(())
}
fn expand_row(row: &mut [u8]) {
let uvscale = 1.0 / 410.0;
let n_pixels = row.len() / 12;
// Work backwards, the 32-bit compressed raw data is at the front of the row buffer and
// this avoids overwriting data before we read it.
for i in (0..n_pixels).rev() {
let raw = row.as_chunks_mut::<4>().0[i];
let l = u16::from(raw[0]) << 8 | u16::from(raw[1]);
let by = log16_to_y(l);
let u = uvscale * (f32::from(raw[2]) + 0.5);
let v = uvscale * (f32::from(raw[3]) + 0.5);
let s = 1. / (6. * u - 16. * v + 12.);
let x = 9. * u * s;
let y = 4. * v * s;
let bx = x / y * by;
let bz = (1. - x - y) / y * by;
let [r, g, b] = xyz_to_rgb([bx, by, bz]);
let into = &mut row.as_chunks_mut::<12>().0[i];
into[0..4].copy_from_slice(&f32::to_ne_bytes(r));
into[4..8].copy_from_slice(&f32::to_ne_bytes(g));
into[8..12].copy_from_slice(&f32::to_ne_bytes(b));
}
}
}
impl<R: Read> Read for LogLuv32<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.read_exact(buf)?;
Ok(buf.len())
}
// FIXME: See `LogLuv24::read_exact`
fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
for row in buf.chunks_exact_mut(self.chunk_sz) {
self.decode_rle_row(row)?;
Self::expand_row(row);
}
Ok(())
}
}
/// Pack-Bits like encoding for a single 16-bit log-L sample.
pub struct LogLuv16<R> {
reader: Take<R>,
/// The required buffer size. We only decompressed all samples at once.
chunk_sz: usize,
}
impl<R: Read> LogLuv16<R> {
pub fn new(reader: R, n: u64, npixels: u32) -> Result<Self, TiffError> {
let chunk_sz = usize::try_from(
u64::from(npixels)
.checked_mul(4)
.ok_or(TiffError::LimitsExceeded)?,
)?;
let reader = reader.take(n);
Ok(LogLuv16 { reader, chunk_sz })
}
fn decode_rle_row(&mut self, row: &mut [u8]) -> std::io::Result<()>
where
R: Read,
{
// 32-bit per pixel format.
let n_pixels = self.chunk_sz / 4;
// buffer for reading (16-bit per pixel)
let raw = &mut row.as_chunks_mut::<2>().0[..n_pixels];
packbits_decode::<_, 2>(&mut self.reader, raw)?;
Ok(())
}
fn expand_row(row: &mut [u8]) {
let n_pixels = row.len() / 4;
// Work backwards, the 32-bit compressed raw data is at the front of the row buffer and
// this avoids overwriting data before we read it.
for i in (0..n_pixels).rev() {
let raw = row.as_chunks_mut::<2>().0[i];
let l = u16::from(raw[0]) << 8 | u16::from(raw[1]);
let by = log16_to_y(l);
let into = &mut row.as_chunks_mut::<4>().0[i];
into[0..4].copy_from_slice(&f32::to_ne_bytes(by));
}
}
}
impl<R: Read> Read for LogLuv16<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.read_exact(buf)?;
Ok(buf.len())
}
// FIXME: See `LogLuv24::read_exact`
fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
for row in buf.chunks_exact_mut(self.chunk_sz) {
self.decode_rle_row(row)?;
Self::expand_row(row);
}
Ok(())
}
}
// Table information from: `libtiff`, 1997 by Greg Ward Larson, SGI
#[doc(alias = "UV_SQSIZ")]
const UV_QUANTIZATION_WIDTH: f32 = 0.003500;
// The `v` value corresonding to the bottom most row (quantized v' = 0).
const UV_VSTART: f32 = 0.016940;
/// Number of distinct 'u' rows, each at a different v level. Each is quantized individually with
/// the first quantization level starting at its `ustart` and `UV_QUANTIZATION_WIDTH` wide.
const UV_NVS: usize = 163;
/// Contrary to the original C code we calculate the dependent values (cumulative sum).
struct UvRow {
ustart: f32,
num_us: u16,
}
#[rustfmt::skip]
const UV_ROWS: [UvRow; UV_NVS] = [
UvRow { ustart: 0.247663, num_us: 4, },
UvRow { ustart: 0.243779, num_us: 6, },
UvRow { ustart: 0.241684, num_us: 7, },
UvRow { ustart: 0.237874, num_us: 9, },
UvRow { ustart: 0.235906, num_us: 10, },
UvRow { ustart: 0.232153, num_us: 12, },
UvRow { ustart: 0.228352, num_us: 14, },
UvRow { ustart: 0.226259, num_us: 15, },
UvRow { ustart: 0.222371, num_us: 17, },
UvRow { ustart: 0.220410, num_us: 18, },
UvRow { ustart: 0.214710, num_us: 21, },
UvRow { ustart: 0.212714, num_us: 22, },
UvRow { ustart: 0.210721, num_us: 23, },
UvRow { ustart: 0.204976, num_us: 26, },
UvRow { ustart: 0.202986, num_us: 27, },
UvRow { ustart: 0.199245, num_us: 29, },
UvRow { ustart: 0.195525, num_us: 31, },
UvRow { ustart: 0.193560, num_us: 32, },
UvRow { ustart: 0.189878, num_us: 34, },
UvRow { ustart: 0.186216, num_us: 36, },
UvRow { ustart: 0.186216, num_us: 36, },
UvRow { ustart: 0.182592, num_us: 38, },
UvRow { ustart: 0.179003, num_us: 40, },
UvRow { ustart: 0.175466, num_us: 42, },
UvRow { ustart: 0.172001, num_us: 44, },
UvRow { ustart: 0.172001, num_us: 44, },
UvRow { ustart: 0.168612, num_us: 46, },
UvRow { ustart: 0.168612, num_us: 46, },
UvRow { ustart: 0.163575, num_us: 49, },
UvRow { ustart: 0.158642, num_us: 52, },
UvRow { ustart: 0.158642, num_us: 52, },
UvRow { ustart: 0.158642, num_us: 52, },
UvRow { ustart: 0.153815, num_us: 55, },
UvRow { ustart: 0.153815, num_us: 55, },
UvRow { ustart: 0.149097, num_us: 58, },
UvRow { ustart: 0.149097, num_us: 58, },
UvRow { ustart: 0.142746, num_us: 62, },
UvRow { ustart: 0.142746, num_us: 62, },
UvRow { ustart: 0.142746, num_us: 62, },
UvRow { ustart: 0.138270, num_us: 65, },
UvRow { ustart: 0.138270, num_us: 65, },
UvRow { ustart: 0.138270, num_us: 65, },
UvRow { ustart: 0.132166, num_us: 69, },
UvRow { ustart: 0.132166, num_us: 69, },
UvRow { ustart: 0.126204, num_us: 73, },
UvRow { ustart: 0.126204, num_us: 73, },
UvRow { ustart: 0.126204, num_us: 73, },
UvRow { ustart: 0.120381, num_us: 77, },
UvRow { ustart: 0.120381, num_us: 77, },
UvRow { ustart: 0.120381, num_us: 77, },
UvRow { ustart: 0.120381, num_us: 77, },
UvRow { ustart: 0.112962, num_us: 82, },
UvRow { ustart: 0.112962, num_us: 82, },
UvRow { ustart: 0.112962, num_us: 82, },
UvRow { ustart: 0.107450, num_us: 86, },
UvRow { ustart: 0.107450, num_us: 86, },
UvRow { ustart: 0.107450, num_us: 86, },
UvRow { ustart: 0.107450, num_us: 86, },
UvRow { ustart: 0.100343, num_us: 91, },
UvRow { ustart: 0.100343, num_us: 91, },
UvRow { ustart: 0.100343, num_us: 91, },
UvRow { ustart: 0.095126, num_us: 95, },
UvRow { ustart: 0.095126, num_us: 95, },
UvRow { ustart: 0.095126, num_us: 95, },
UvRow { ustart: 0.095126, num_us: 95, },
UvRow { ustart: 0.088276, num_us: 100, },
UvRow { ustart: 0.088276, num_us: 100, },
UvRow { ustart: 0.088276, num_us: 100, },
UvRow { ustart: 0.088276, num_us: 100, },
UvRow { ustart: 0.081523, num_us: 105, },
UvRow { ustart: 0.081523, num_us: 105, },
UvRow { ustart: 0.081523, num_us: 105, },
UvRow { ustart: 0.081523, num_us: 105, },
UvRow { ustart: 0.074861, num_us: 110, },
UvRow { ustart: 0.074861, num_us: 110, },
UvRow { ustart: 0.074861, num_us: 110, },
UvRow { ustart: 0.074861, num_us: 110, },
UvRow { ustart: 0.068290, num_us: 115, },
UvRow { ustart: 0.068290, num_us: 115, },
UvRow { ustart: 0.068290, num_us: 115, },
UvRow { ustart: 0.068290, num_us: 115, },
UvRow { ustart: 0.063573, num_us: 119, },
UvRow { ustart: 0.063573, num_us: 119, },
UvRow { ustart: 0.063573, num_us: 119, },
UvRow { ustart: 0.063573, num_us: 119, },
UvRow { ustart: 0.057219, num_us: 124, },
UvRow { ustart: 0.057219, num_us: 124, },
UvRow { ustart: 0.057219, num_us: 124, },
UvRow { ustart: 0.057219, num_us: 124, },
UvRow { ustart: 0.050985, num_us: 129, },
UvRow { ustart: 0.050985, num_us: 129, },
UvRow { ustart: 0.050985, num_us: 129, },
UvRow { ustart: 0.050985, num_us: 129, },
UvRow { ustart: 0.050985, num_us: 129, },
UvRow { ustart: 0.044859, num_us: 134, },
UvRow { ustart: 0.044859, num_us: 134, },
UvRow { ustart: 0.044859, num_us: 134, },
UvRow { ustart: 0.044859, num_us: 134, },
UvRow { ustart: 0.040571, num_us: 138, },
UvRow { ustart: 0.040571, num_us: 138, },
UvRow { ustart: 0.040571, num_us: 138, },
UvRow { ustart: 0.040571, num_us: 138, },
UvRow { ustart: 0.036339, num_us: 142, },
UvRow { ustart: 0.036339, num_us: 142, },
UvRow { ustart: 0.036339, num_us: 142, },
UvRow { ustart: 0.036339, num_us: 142, },
UvRow { ustart: 0.032139, num_us: 146, },
UvRow { ustart: 0.032139, num_us: 146, },
UvRow { ustart: 0.032139, num_us: 146, },
UvRow { ustart: 0.032139, num_us: 146, },
UvRow { ustart: 0.027947, num_us: 150, },
UvRow { ustart: 0.027947, num_us: 150, },
UvRow { ustart: 0.027947, num_us: 150, },
UvRow { ustart: 0.023739, num_us: 154, },
UvRow { ustart: 0.023739, num_us: 154, },
UvRow { ustart: 0.023739, num_us: 154, },
UvRow { ustart: 0.023739, num_us: 154, },
UvRow { ustart: 0.019504, num_us: 158, },
UvRow { ustart: 0.019504, num_us: 158, },
UvRow { ustart: 0.019504, num_us: 158, },
UvRow { ustart: 0.016976, num_us: 161, },
UvRow { ustart: 0.016976, num_us: 161, },
UvRow { ustart: 0.016976, num_us: 161, },
UvRow { ustart: 0.016976, num_us: 161, },
UvRow { ustart: 0.012639, num_us: 165, },
UvRow { ustart: 0.012639, num_us: 165, },
UvRow { ustart: 0.012639, num_us: 165, },
UvRow { ustart: 0.009991, num_us: 168, },
UvRow { ustart: 0.009991, num_us: 168, },
UvRow { ustart: 0.009991, num_us: 168, },
UvRow { ustart: 0.009016, num_us: 170, },
UvRow { ustart: 0.009016, num_us: 170, },
UvRow { ustart: 0.009016, num_us: 170, },
UvRow { ustart: 0.006217, num_us: 173, },
UvRow { ustart: 0.006217, num_us: 173, },
UvRow { ustart: 0.005097, num_us: 175, },
UvRow { ustart: 0.005097, num_us: 175, },
UvRow { ustart: 0.005097, num_us: 175, },
UvRow { ustart: 0.003909, num_us: 177, },
UvRow { ustart: 0.003909, num_us: 177, },
UvRow { ustart: 0.002340, num_us: 177, },
UvRow { ustart: 0.002389, num_us: 170, },
UvRow { ustart: 0.001068, num_us: 164, },
UvRow { ustart: 0.001653, num_us: 157, },
UvRow { ustart: 0.000717, num_us: 150, },
UvRow { ustart: 0.001614, num_us: 143, },
UvRow { ustart: 0.000270, num_us: 136, },
UvRow { ustart: 0.000484, num_us: 129, },
UvRow { ustart: 0.001103, num_us: 123, },
UvRow { ustart: 0.001242, num_us: 115, },
UvRow { ustart: 0.001188, num_us: 109, },
UvRow { ustart: 0.001011, num_us: 103, },
UvRow { ustart: 0.000709, num_us: 97, },
UvRow { ustart: 0.000301, num_us: 89, },
UvRow { ustart: 0.002416, num_us: 82, },
UvRow { ustart: 0.003251, num_us: 76, },
UvRow { ustart: 0.003246, num_us: 69, },
UvRow { ustart: 0.004141, num_us: 62, },
UvRow { ustart: 0.005963, num_us: 55, },
UvRow { ustart: 0.008839, num_us: 47, },
UvRow { ustart: 0.010490, num_us: 40, },
UvRow { ustart: 0.016994, num_us: 31, },
UvRow { ustart: 0.023659, num_us: 21, },
];
const UV_CUMULATIVE: [u16; UV_NVS] = {
let mut arr = [0u16; UV_NVS];
let mut sum = 0;
let mut i = 0;
while i < UV_NVS {
arr[i] = sum;
sum += UV_ROWS[i].num_us;
i += 1;
}
arr
};
const UV_NDIVS: u16 = UV_CUMULATIVE[UV_NVS - 1] + UV_ROWS[UV_NVS - 1].num_us;
fn decode_quantized_uv(uv: u32) -> (u8, u8) {
if uv >= UV_NDIVS as u32 {
return (0, 0);
}
// Find the start of the row, or the index after that.
let v_index = match UV_CUMULATIVE.binary_search(&(uv as u16)) {
Ok(n) => n,
Err(n) => n - 1,
};
let u_index = uv as u16 - UV_CUMULATIVE[v_index];
(u_index as u8, v_index as u8)
}
fn dequantize_24_luma(l: u32) -> f32 {
if l == 0 {
0.0
} else {
let le = (l & 0x3ff) as f32;
let exponent = ((le / 64.0).floor() as i32) - 24;
let mantissa = le - (exponent as f32 + 24.0) * 64.0;
(mantissa + 64.0) / 64.0 * 2f32.powi(exponent)
}
}
fn dequantize_24((u, v): (u8, u8)) -> (f32, f32) {
let v_f = UV_VSTART + (v as f32) * UV_QUANTIZATION_WIDTH;
let u_row = &UV_ROWS[v as usize];
let u_f = u_row.ustart + (u as f32) * UV_QUANTIZATION_WIDTH;
(u_f, v_f)
}
fn log16_to_y(l: u16) -> f32 {
if l == 0 {
return 0.0;
}
let le = f32::from(l & 0x7fff);
// Y = exp(M_LN2 / 256. * (Le + .5) - M_LN2 * 64.);
let y = (std::f32::consts::LN_2 / 256.0 * (le + 0.5) - std::f32::consts::LN_2 * 64.0).exp();
if l & 0x8000 != 0 {
-y
} else {
y
}
}
fn xyz_to_rgb([l, u, v]: [f32; 3]) -> [f32; 3] {
// XYZ to sRGB primaries at D65
let r = 3.2404542 * l + -1.5371385 * u + -0.4985314 * v;
let g = -0.969266 * l + 1.8760108 * u + 0.0415560 * v;
let b = 0.0556434 * l + -0.2040259 * u + 1.0572252 * v;
[r, g, b]
}
#[test]
fn test_decode_quantized_uv() {
// Test some known values from the original C code.
assert_eq!(decode_quantized_uv(0), (0, 0));
assert_eq!(decode_quantized_uv(3), (3, 0));
assert_eq!(decode_quantized_uv(4), (0, 1));
assert_eq!(decode_quantized_uv(9), (5, 1));
assert_eq!(decode_quantized_uv(16288), (20, 162));
assert_eq!(decode_quantized_uv(16289), (0, 0)); // Out of range
}
+925 -400
View File
File diff suppressed because it is too large. Load diff
+60 -3
View File
@@ -1,4 +1,6 @@
//! All IO functionality needed for TIFF decoding
#[cfg(feature = "webp")]
use std::io::Cursor;
use std::io::{self, BufRead, BufReader, Read, Seek, Take};
pub use crate::tags::ByteOrder;
@@ -258,9 +260,9 @@ impl<R: Read> Read for PackBitsReader<R> {
pub struct Group4Reader<R: Read> {
decoder: fax34::decoder::Group4Decoder<io::Bytes<io::BufReader<io::Take<R>>>>,
line_buf: io::Cursor<Vec<u8>>,
height: u16,
height: u32,
width: u16,
y: u16,
y: u32,
}
#[cfg(feature = "fax")]
@@ -271,7 +273,7 @@ impl<R: Read> Group4Reader<R> {
compressed_length: u64,
) -> crate::TiffResult<Self> {
let width = u16::try_from(dimensions.0)?;
let height = u16::try_from(dimensions.1)?;
let height = dimensions.1;
Ok(Self {
decoder: fax34::decoder::Group4Decoder::new(
@@ -354,6 +356,61 @@ impl<R: Read> Read for Group4Reader<R> {
}
}
#[cfg(feature = "webp")]
pub struct WebPReader {
inner: Cursor<Vec<u8>>,
}
#[cfg(feature = "webp")]
impl WebPReader {
pub fn new<R: Read + Seek>(
reader: R,
compressed_length: u64,
samples: u16,
) -> crate::TiffResult<Self> {
let mut decoder =
image_webp::WebPDecoder::new(io::BufReader::new(reader.take(compressed_length)))
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
if !(samples == 4 || (samples == 3 && !decoder.has_alpha())) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"bad sample count for WebP compressed data",
)
.into());
}
let total_bytes =
samples as usize * decoder.dimensions().0 as usize * decoder.dimensions().1 as usize;
let mut data = vec![0; total_bytes];
decoder
.read_image(&mut data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
// Add a fully opaque alpha channel if needed
if samples == 4 && !decoder.has_alpha() {
for i in (0..(total_bytes / 4)).rev() {
data[i * 4 + 3] = 255;
data[i * 4 + 2] = data[i * 3 + 2];
data[i * 4 + 1] = data[i * 3 + 1];
data[i * 4] = data[i * 3];
}
}
Ok(Self {
inner: Cursor::new(data),
})
}
}
#[cfg(feature = "webp")]
impl Read for WebPReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
}
#[cfg(test)]
mod test {
use super::*;
+50 -2
View File
@@ -1,7 +1,9 @@
use std::io::{Read, Seek};
use crate::{tags::Tag, Directory};
use crate::{TiffError, TiffFormatError, TiffResult};
use crate::{
tags::{Tag, ValueBuffer},
Directory, TiffError, TiffFormatError, TiffResult,
};
use super::ifd::{Entry, Value};
use super::ValueReader;
@@ -14,12 +16,25 @@ pub(crate) struct TagReader<'lt, R: EntryDecoder + ?Sized> {
pub(crate) trait EntryDecoder {
/// Turn an entry into a value by fetching the required bytes in the stream.
fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value>;
/// 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> {
@@ -32,6 +47,39 @@ impl<'a, R: EntryDecoder + ?Sized> TagReader<'a, R> {
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> {
match self.find_tag(tag)? {
Some(val) => Ok(val),
+144 -8
View File
@@ -1,5 +1,97 @@
use crate::tags::{PhotometricInterpretation, SampleFormat};
/// Apply floating-point predictor encoding to a row of floating-point samples.
///
/// The floating-point predictor works by:
/// 1. Converting each value to big-endian bytes
/// 2. Shuffling bytes so all first bytes are together, all second bytes, etc.
/// 3. Applying byte-level horizontal difference (wrapping_sub) across the entire buffer
///
/// The decoder reverses this by:
/// 1. Applying cumulative sum (wrapping_add) on the entire byte stream
/// 2. De-shuffling bytes back into float values
macro_rules! fp_predict {
($name:ident, $float_type:ty) => {
fn $name(row: &[$float_type], samples: usize, result: &mut Vec<u8>) {
const BYTE_SIZE: usize = core::mem::size_of::<$float_type>();
let num_values = row.len();
let byte_count = num_values * BYTE_SIZE;
let start_len = result.len();
// Allocate zeroed space so we can write to arbitrary positions
result.resize(start_len + byte_count, 0);
let output = &mut result[start_len..];
// Step 1 & 2: Convert to big-endian and scatter bytes
for (i, &value) in row.iter().enumerate() {
let bytes = value.to_be_bytes();
for b in 0..BYTE_SIZE {
output[b * num_values + i] = bytes[b];
}
}
// Step 3: Apply byte-level horizontal difference across the entire buffer.
// The prediction spans across quarter boundaries, just like the decoder's cumulative sum.
//
// We process elements in reverse in batches of 16 to enable autovectorization.
// Reverse order ensures that when we compute output[i] - output[i-samples],
// the value at output[i-samples] hasn't been overwritten yet (critical when
// samples < 16, which is the common case: 1, 3, 4, 5).
//
// 8 bits * 16 = 128 bits, the width of a typical SIMD register.
// Even if the target has no vector instructions, this still benefits from
// instruction-level parallelism, since unlike in decoding, all subtractions
// within a chunk are truly independent of each other.
let output = &mut result[start_len..byte_count + start_len];
let diff_len = byte_count - samples;
let remainder_len = diff_len % 16;
// Process full 16-byte chunks in reverse.
// We iterate from the end so that predecessor values (at lower indices)
// are still in their original state when we read them.
let mut chunk_end = byte_count;
while chunk_end >= samples + remainder_len + 16 {
let chunk_start = chunk_end - 16;
let prev_start = chunk_start - samples;
// Read all values in the chunk and their predecessors first.
// This makes all subtractions below independent, enabling vectorization.
let curr: [u8; 16] = output[chunk_start..chunk_end].try_into().unwrap();
let prev: [u8; 16] = output[prev_start..prev_start + 16].try_into().unwrap();
let chunk = &mut output[chunk_start..chunk_end];
chunk[0] = curr[0].wrapping_sub(prev[0]);
chunk[1] = curr[1].wrapping_sub(prev[1]);
chunk[2] = curr[2].wrapping_sub(prev[2]);
chunk[3] = curr[3].wrapping_sub(prev[3]);
chunk[4] = curr[4].wrapping_sub(prev[4]);
chunk[5] = curr[5].wrapping_sub(prev[5]);
chunk[6] = curr[6].wrapping_sub(prev[6]);
chunk[7] = curr[7].wrapping_sub(prev[7]);
chunk[8] = curr[8].wrapping_sub(prev[8]);
chunk[9] = curr[9].wrapping_sub(prev[9]);
chunk[10] = curr[10].wrapping_sub(prev[10]);
chunk[11] = curr[11].wrapping_sub(prev[11]);
chunk[12] = curr[12].wrapping_sub(prev[12]);
chunk[13] = curr[13].wrapping_sub(prev[13]);
chunk[14] = curr[14].wrapping_sub(prev[14]);
chunk[15] = curr[15].wrapping_sub(prev[15]);
chunk_end = chunk_start;
}
// Handle the leading remainder that doesn't fill a complete chunk of 16.
// This must be done last (after the reverse chunk loop) because these are
// the lowest indices, and the chunks above may read predecessor values
// from this range.
for i in (samples..samples + remainder_len).rev() {
output[i] = output[i].wrapping_sub(output[i - samples]);
}
}
};
}
fp_predict!(fp_predict_f32, f32);
fp_predict!(fp_predict_f64, f64);
macro_rules! integer_horizontal_predict {
() => {
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>) {
@@ -23,6 +115,10 @@ macro_rules! integer_horizontal_predict {
.map(|(prev, current)| current.wrapping_sub(*prev)),
);
}
fn floating_point_predict(_: &[Self::Inner], _: &mut Vec<u8>) {
unreachable!("floating-point predictor is only valid for floating-point sample types")
}
};
}
@@ -38,6 +134,10 @@ pub trait ColorType {
const SAMPLE_FORMAT: &'static [SampleFormat];
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>);
/// Apply floating-point predictor encoding to a row of samples.
/// This is only implemented for floating-point types; integer types will panic.
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>);
}
pub struct Gray8;
@@ -108,7 +208,11 @@ impl ColorType for Gray32Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
}
}
@@ -140,7 +244,11 @@ impl ColorType for Gray64Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
}
}
@@ -180,8 +288,13 @@ impl ColorType for RGB32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
}
}
@@ -201,8 +314,13 @@ impl ColorType for RGB64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
}
}
@@ -242,8 +360,13 @@ impl ColorType for RGBA32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
}
}
@@ -263,8 +386,13 @@ impl ColorType for RGBA64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
}
}
@@ -306,7 +434,11 @@ impl ColorType for CMYK32Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
}
}
@@ -328,7 +460,11 @@ impl ColorType for CMYK64Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
}
}
+20 -17
View File
@@ -7,24 +7,27 @@ pub struct Deflate {
level: FlateCompression,
}
/// The level of compression used by the Deflate algorithm.
/// It allows trading compression ratio for compression speed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
#[derive(Default)]
pub enum DeflateLevel {
/// The fastest possible compression mode.
Fast = 1,
/// The conserative choice between speed and ratio.
#[default]
Balanced = 6,
/// The best compression available with Deflate.
Best = 9,
}
impl Deflate {
/// Create a new deflate compressor with a specific level of compression.
pub fn with_level(level: DeflateLevel) -> Self {
///
/// The valid levels are 1 through 9 inclusive. The default level is 6.
///
/// - `1` stands for light but fast compression
/// - `6` is the recommended balanced default
/// - `9` is maximum compression ratio at the cost of slow encoding
pub fn with_level(level: u8) -> Self {
Self {
level: FlateCompression::new(level as u32),
}
}
/// Create a new deflate compressor with an exact level of compression.
///
/// `0` stands for uncompressed
/// `1` stands for light but fast compression
/// `6` is the recommended balanced default
/// `9` is maximum compression ratio at the cost of slow encoding
pub fn exact_level(level: u8) -> Self {
Self {
level: FlateCompression::new(level as u32),
}
@@ -33,7 +36,7 @@ impl Deflate {
impl Default for Deflate {
fn default() -> Self {
Self::with_level(DeflateLevel::default())
Self::with_level(6)
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ mod packbits;
mod uncompressed;
#[cfg(feature = "deflate")]
pub use self::deflate::{Deflate, DeflateLevel};
pub use self::deflate::Deflate;
#[cfg(feature = "lzw")]
pub use self::lzw::Lzw;
+205 -21
View File
@@ -11,7 +11,10 @@ use std::{
use crate::{
decoder::ifd::Entry,
error::{TiffResult, UsageError},
tags::{CompressionMethod, IfdPointer, ResolutionUnit, SampleFormat, Tag, Type},
tags::{
ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation,
ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
},
Directory, TiffError, TiffFormatError,
};
@@ -30,13 +33,13 @@ use self::writer::*;
/// Image data can be very unpredictable, and thus very hard to compress. Predictors are simple
/// passes ran over the image data to prepare it for compression. This is mostly used for LZW
/// compression, where using [Predictor::Horizontal] we see a 35% improvement in compression
/// ratio over the unpredicted compression !
/// ratio over the unpredicted compression!
///
/// [Predictor::FloatingPoint] is currently not supported.
/// [Predictor::Horizontal] is for integer sample types.
/// [Predictor::FloatingPoint] is for floating-point sample types (f32, f64).
pub type Predictor = crate::tags::Predictor;
#[cfg(feature = "deflate")]
pub type DeflateLevel = compression::DeflateLevel;
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Default)]
pub enum Compression {
#[default]
@@ -44,7 +47,7 @@ pub enum Compression {
#[cfg(feature = "lzw")]
Lzw,
#[cfg(feature = "deflate")]
Deflate(DeflateLevel),
Deflate(u8),
Packbits,
}
@@ -298,12 +301,12 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
value.write(&mut writer)?;
}
let entry = Self::write_value(
let entry = Self::write_entry_inner(
self.writer,
&DirectoryEntry {
DirectoryEntry {
data_type: <T>::FIELD_TYPE,
count: value.count().try_into()?,
data: bytes,
data: bytes.into(),
},
)?;
@@ -312,15 +315,114 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
Ok(())
}
/// Write a tag-value pair with prepared byte data.
///
/// Note that the library will _not_ attempt to verify that the data type or the count of the
/// buffered value is permissible for the given tag. The data will be associated with the tag
/// exactly as-is.
///
/// An error is returned if the byte order of the presented value does not match the byte order
/// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)).
pub fn write_tag_buf(&mut self, tag: Tag, value: &ValueBuffer) -> TiffResult<()> {
let entry = self.write_entry_buf(value)?;
self.directory.extend([(tag, entry)]);
Ok(())
}
/// Write some data to the tiff file, the offset of the data is returned.
///
/// This could be used to write tiff strips.
/// This could be used to write tiff strips. All of the data will be written into the file as a
/// slice of bytes. It can not be used as an entry in a directory directly, where very short
/// values are instead represented in the offset field directly. Use [`Self::write_entry`]
/// instead.
pub fn write_data<T: TiffValue>(&mut self, value: T) -> TiffResult<u64> {
let offset = self.writer.offset();
value.write(self.writer)?;
Ok(offset)
}
/// Write some data directly to the tiff file, the offset of the data is returned.
///
/// All of the data will be written into the file as a slice of bytes. It can not be used as an
/// entry in a directory directly, where very short values are instead represented in the
/// offset field directly. Use [`Self::write_entry_buf`] instead.
///
/// An error is returned if the byte order of the presented value does not match the byte order
/// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)).
pub fn write_data_buf(&mut self, value: &ValueBuffer) -> TiffResult<u64> {
self.check_value_byteorder(value)?;
let offset = self.writer.offset();
self.writer.write_bytes(value.as_bytes())?;
Ok(offset)
}
/// Write a directory value into the file.
///
/// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
/// stored in the entry's offset field.
pub fn write_entry<T: TiffValue>(&mut self, value: T) -> TiffResult<Entry> {
Self::write_entry_inner(
self.writer,
DirectoryEntry {
data_type: T::FIELD_TYPE,
count: K::convert_offset(value.count() as u64)?,
// FIXME: not optimal we always allocate here. But the only other method we have
// available is `T::write(&mut TiffWriter<W>)` and we must pass that into
// `write_entry_inner` but then have a combinatorial explosion of instantiations
// which is absurd.
data: value.data(),
},
)
}
/// Write a directory value into the file.
///
/// An error is returned if the byte order of the presented value does not match the byte order
/// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)).
///
/// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
/// stored in the entry's offset field.
pub fn write_entry_buf(&mut self, value: &ValueBuffer) -> TiffResult<Entry> {
self.check_value_byteorder(value)?;
Self::write_entry_inner(
self.writer,
DirectoryEntry {
data_type: value.data_type(),
count: K::convert_offset(value.count())?,
data: value.as_bytes().into(),
},
)
}
/// Write a directory entry by its byte data.
///
/// The data must be pre-formatted to the byte order expected by the file.
///
/// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
/// stored in the entry's offset field.
pub fn write_entry_bytes(&mut self, ty: Type, data: &[u8]) -> TiffResult<Entry> {
if !data.len().is_multiple_of(usize::from(ty.byte_len())) {
return Err(TiffError::UsageError(UsageError::MismatchedEntryLength {
ty,
found: data.len(),
}));
}
let count = data.len() / usize::from(ty.byte_len());
let count = u64::try_from(count)?;
Self::write_entry_inner(
self.writer,
DirectoryEntry {
data_type: ty,
count: K::convert_offset(count)?,
data: data.into(),
},
)
}
/// Insert previously written key-value entries.
///
/// It is the caller's responsibility to ensure that the data referred to by the entries is
@@ -374,17 +476,21 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
Ok(offset)
}
fn write_value(
// Does it really make sense that this would be a (potentially) compressible write? For actual
// directory values their size must match the data written so there's going to be a silent
// problem if that actually occurs.
fn write_entry_inner(
writer: &mut TiffWriter<W>,
value: &DirectoryEntry<K::OffsetType>,
value: DirectoryEntry<K::OffsetType>,
) -> TiffResult<Entry> {
let &DirectoryEntry {
let DirectoryEntry {
data: ref bytes,
ref count,
data_type,
} = value;
let in_entry_bytes = mem::size_of::<K::OffsetType>();
let mut offset_bytes = [0; 8];
if bytes.len() > in_entry_bytes {
@@ -403,6 +509,7 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
// and some oversight, we can not clone the `count: K::OffsetType` and thus not convert it.
// Instead, write it to a buffer...
let mut count_bytes = [0; 8];
debug_assert!(in_entry_bytes == 4 || in_entry_bytes == 8);
// Nominally Cow but we only expect `Cow::Borrowed`.
count_bytes[..count.bytes()].copy_from_slice(&count.data());
@@ -416,6 +523,15 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
})
}
fn check_value_byteorder(&self, value: &ValueBuffer) -> TiffResult<()> {
// FIXME: we should enable writing files with any chosen byte order.
if value.byte_order() != ByteOrder::native() {
Err(TiffError::UsageError(UsageError::ByteOrderMismatch))
} else {
Ok(())
}
}
/// Provides the number of bytes written by the underlying TiffWriter during the last call.
fn last_written(&self) -> u64 {
self.writer.last_written()
@@ -506,6 +622,7 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> {
row_samples: u64,
width: u32,
height: u32,
extra_samples: Vec<u16>,
rows_per_strip: u64,
strip_offsets: Vec<K::OffsetType>,
strip_byte_count: Vec<K::OffsetType>,
@@ -518,11 +635,14 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> {
impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> {
fn sanity_check(compression: Compression, predictor: Predictor) -> TiffResult<()> {
match (predictor, compression, T::SAMPLE_FORMAT[0]) {
// Horizontal predictor is not valid for floating-point types
(Predictor::Horizontal, _, SampleFormat::IEEEFP | SampleFormat::Void) => {
Err(TiffError::UsageError(UsageError::PredictorIncompatible))
}
// Floating-point predictor is only valid for IEEEFP sample format
(Predictor::FloatingPoint, _, SampleFormat::IEEEFP) => Ok(()),
(Predictor::FloatingPoint, _, _) => {
Err(TiffError::UsageError(UsageError::PredictorUnavailable))
Err(TiffError::UsageError(UsageError::PredictorIncompatible))
}
_ => Ok(()),
}
@@ -562,11 +682,14 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
encoder.write_tag(Tag::Compression, compression.tag().to_u16())?;
encoder.write_tag(Tag::Predictor, predictor.to_u16())?;
encoder.write_tag(Tag::BitsPerSample, <T>::BITS_PER_SAMPLE)?;
let sample_format: Vec<_> = <T>::SAMPLE_FORMAT.iter().map(|s| s.to_u16()).collect();
encoder.write_tag(Tag::SampleFormat, &sample_format[..])?;
encoder.write_tag(Tag::PhotometricInterpretation, <T>::TIFF_VALUE.to_u16())?;
if matches!(<T>::TIFF_VALUE, PhotometricInterpretation::YCbCr) {
// The default for this tag is 2,2 for subsampling but we do not support such a
// transformation. Instead all samples must be provided.
encoder.write_tag(Tag::ChromaSubsampling, &[1u16, 1u16][..])?;
}
encoder.write_tag(Tag::RowsPerStrip, u32::try_from(rows_per_strip)?)?;
encoder.write_tag(
@@ -575,7 +698,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
)?;
encoder.write_tag(Tag::XResolution, Rational { n: 1, d: 1 })?;
encoder.write_tag(Tag::YResolution, Rational { n: 1, d: 1 })?;
encoder.write_tag(Tag::ResolutionUnit, ResolutionUnit::None.to_u16())?;
encoder.write_tag(Tag::ResolutionUnit, ResolutionUnit::None)?;
Ok(ImageEncoder {
encoder,
@@ -583,6 +706,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
strip_idx: 0,
row_samples,
rows_per_strip,
extra_samples: vec![],
width,
height,
strip_offsets: Vec::new(),
@@ -594,6 +718,27 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
})
}
pub fn extra_samples(&mut self, extra: &[ExtraSamples]) -> Result<(), TiffError> {
if self.strip_idx != 0 {
return Err(TiffError::UsageError(
UsageError::ReconfiguredAfterImageWrite,
));
}
let samples = self.extra_samples.len() + extra.len() + <T>::BITS_PER_SAMPLE.len();
let row_samples = u64::from(self.width) * u64::try_from(samples)?;
self.extra_samples
.extend(extra.iter().map(ExtraSamples::to_u16));
self.encoder
.write_tag(Tag::ExtraSamples, &self.extra_samples[..])?;
self.row_samples = row_samples;
Ok(())
}
/// Number of samples the next strip should have.
pub fn next_strip_sample_count(&self) -> u64 {
if self.strip_idx >= self.strip_count {
@@ -631,7 +776,13 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
}
self.encoder.write_data(row_result.as_slice())?
}
_ => unimplemented!(),
Predictor::FloatingPoint => {
let mut row_result = Vec::with_capacity(std::mem::size_of_val(value));
for row in value.chunks_exact(self.row_samples as usize) {
T::floating_point_predict(row, &mut row_result);
}
self.encoder.write_data(row_result.as_slice())?
}
};
let byte_count = self.encoder.last_written() as usize;
@@ -731,6 +882,39 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
}
fn finish_internal(&mut self) -> TiffResult<DirectoryOffset<K>> {
if self.extra_samples.is_empty() {
self.encoder
.write_tag(Tag::BitsPerSample, <T>::BITS_PER_SAMPLE)?;
} else {
let mut sample_format: Vec<_> = <T>::BITS_PER_SAMPLE.to_vec();
let replicated =
core::iter::repeat_n(<T>::BITS_PER_SAMPLE[0], self.extra_samples.len());
sample_format.extend(replicated);
self.encoder
.write_tag(Tag::BitsPerSample, &sample_format[..])?;
self.encoder.write_tag(
Tag::SamplesPerPixel,
u16::try_from(<T>::BITS_PER_SAMPLE.len() + self.extra_samples.len())?,
)?;
}
let mut sample_format: Vec<_> = <T>::SAMPLE_FORMAT.iter().map(|s| s.to_u16()).collect();
let extra_format = sample_format
.first()
.copied()
// Frankly should not occur, we have no sample format without at least one entry. We
// would need an interface upgrade to handle this however. Either decide to support
// heterogeneous sample formats or a way to provide fallback that is used for purely
// extra samples and not provided via a slice used for samples themselves.
.unwrap_or(SampleFormat::Void.to_u16());
sample_format.extend(core::iter::repeat_n(extra_format, self.extra_samples.len()));
self.encoder
.write_tag(Tag::SampleFormat, &sample_format[..])?;
self.encoder
.write_tag(Tag::StripOffsets, K::convert_slice(&self.strip_offsets))?;
self.encoder.write_tag(
@@ -762,10 +946,10 @@ impl<'a, W: Write + Seek, C: ColorType, K: TiffKind> Drop for ImageEncoder<'a, W
}
}
struct DirectoryEntry<S> {
struct DirectoryEntry<'data, S> {
data_type: Type,
count: S,
data: Vec<u8>,
data: std::borrow::Cow<'data, [u8]>,
}
/// Trait to abstract over Tiff/BigTiff differences.
+165 -1
View File
@@ -1,14 +1,27 @@
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;
/// Trait for types that can be encoded in a tiff file
#[diagnostic::on_unimplemented(
message = "the type `{Self}` does not implement `TiffValue`",
note = "the trait is implemented for primitive types (`u8`, `i16`, `f32`, etc.)",
note = "the trait is implemented for shared references to values",
note = "the trait is implemented for slices, pass them by reference (e.g. `&[u8]`)",
note = "the trait is implemented for arrays if it is implemented for a slice",
note = "values in a `Vec` or `Box` should be dereferenced, e.g. `&vec[..]`"
)]
pub trait TiffValue {
const BYTE_LEN: u8;
const FIELD_TYPE: Type;
// 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 bytes(&self) -> usize {
self.count() * usize::from(Self::BYTE_LEN)
}
@@ -450,6 +463,136 @@ impl TiffValue for str {
}
}
// 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 FIELD_TYPE: Type = T::FIELD_TYPE;
@@ -467,6 +610,27 @@ impl<T: TiffValue + ?Sized> TiffValue for &'_ T {
}
}
/// An array is treated like a slice of the same length.
impl<T, const N: usize> TiffValue for [T; N]
where
[T]: TiffValue,
{
const BYTE_LEN: u8 = <[T]>::BYTE_LEN;
const FIELD_TYPE: Type = <[T]>::FIELD_TYPE;
fn count(&self) -> usize {
self.as_slice().count()
}
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> {
self.as_slice().write(writer)
}
fn data(&self) -> Cow<'_, [u8]> {
self.as_slice().data()
}
}
macro_rules! impl_tiff_value_for_contiguous_sequence {
($inner_type:ty; $bytes:expr; $field_type:expr) => {
impl $crate::encoder::TiffValue for [$inner_type] {
+12 -4
View File
@@ -172,7 +172,7 @@ impl<W: Write> TiffWriter<W> {
}
pub fn pad_word_boundary(&mut self) -> Result<(), io::Error> {
if self.offset % 4 != 0 {
if !self.offset.is_multiple_of(4) {
let padding = [0, 0, 0];
let padd_len = 4 - (self.offset % 4);
self.writer.write_all(&padding[..padd_len as usize])?;
@@ -221,6 +221,8 @@ fn encoder_on_short_writes() {
};
let imgdata = vec![42u8; 100 * 100];
let mut data = crate::decoder::DecodingResult::U8(vec![]);
for comp in [
super::Compression::Uncompressed,
super::Compression::Packbits,
@@ -239,9 +241,15 @@ fn encoder_on_short_writes() {
}
write.inner.set_position(0);
let mut tiff_reader = crate::decoder::Decoder::new(&mut write.inner).unwrap();
let data = tiff_reader.read_image().unwrap();
assert!(matches!(data, crate::decoder::DecodingResult::U8(v) if v == imgdata));
let mut tiff_reader = crate::decoder::Decoder::open(&mut write.inner).unwrap();
tiff_reader.next_image().unwrap();
let _layout = tiff_reader.read_image_to_buffer(&mut data).unwrap();
assert!(
matches!(&data, crate::decoder::DecodingResult::U8(v) if *v == imgdata),
"{data:?} @ {}",
data.as_buffer(0).byte_len()
);
}
}
+28
View File
@@ -5,6 +5,7 @@ use std::string;
use quick_error::quick_error;
use crate::decoder::ChunkType;
use crate::tags::Type;
use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag,
};
@@ -69,6 +70,9 @@ quick_error! {
InconsistentSizesEncountered {
display("inconsistent sizes encountered")
}
IncorrectChunkCount {expected: u32, found: u32} {
display("incorrect number of chunk offsets encoded for the tile count of the image, expected {expected} but found {found}")
}
InvalidDimensions(width: u32, height: u32) {
display("invalid dimensions: {width}x{height}")
}
@@ -90,6 +94,9 @@ quick_error! {
InvalidTypeForTag {
display("tag has invalid type")
}
InvalidCountForTag(tag: Tag, len: usize) {
display("tag `{tag:?}` with incorrect number of elements ({len}) encountered")
}
StripTileTagConflict {
display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found")
}
@@ -156,6 +163,9 @@ quick_error! {
UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
display("unsupported photometric interpretation \"{interpretation:?}\"")
}
ChromaSubsampling {
display("chroma subsampling of YCbCr color is unsupported")
}
MisalignedTileBoundaries {
display("tile rows are not aligned to byte boundaries")
}
@@ -175,6 +185,12 @@ quick_error! {
InvalidChunkIndex(index: u32) {
display("invalid chunk index ({index}) requested")
}
InvalidPlaneIndex(index: u16) {
display("invalid plane index ({index}) requested")
}
InvalidCodingUnit(index: u32, have: u32) {
display("out of bounds coding unit ({index}) requested, only {have} available")
}
PredictorCompressionMismatch {
display("requested predictor is not compatible with the requested compression")
}
@@ -187,9 +203,21 @@ quick_error! {
InsufficientOutputBufferSize { needed: usize, provided: usize } {
display("the borrowed output buffer is not large enough for the decoded data, needed {needed} but have {provided}")
}
InsufficientOutputRowStride { needed: usize, requested: usize } {
display("the provided output row stride would alias rows of decoded data, needed {needed} but have {requested}")
}
ZeroIfdPointer {
display("the offset 0 can not point to a valid IFD")
}
ReconfiguredAfterImageWrite {
display("attempted to reconfigure the encoder after image writing has started")
}
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}")
}
}
}
+22 -1
View File
@@ -44,11 +44,15 @@ pub enum ColorType {
/// Pixel is YCbCr
YCbCr(u8),
/// Pixel is CIE L*a*b* (ICC LAb)
Lab(u8),
/// Pixel has multiple bands/channels
Multiband { bit_depth: u8, num_samples: u16 },
}
impl ColorType {
fn bit_depth(&self) -> u8 {
/// The number of bits used to store each sample.
pub fn bit_depth(&self) -> u8 {
match *self {
ColorType::Gray(b)
| ColorType::RGB(b)
@@ -58,7 +62,24 @@ impl ColorType {
| ColorType::CMYK(b)
| ColorType::CMYKA(b)
| ColorType::YCbCr(b)
| ColorType::Lab(b)
| ColorType::Multiband { bit_depth: b, .. } => b,
}
}
/// The number of samples in each representation of a single color.
pub 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,
}
}
}
+221 -9
View File
@@ -1,3 +1,6 @@
use crate::encoder::TiffValue;
use core::fmt;
macro_rules! tags {
{
// Permit arbitrary meta items, which include documentation.
@@ -10,8 +13,9 @@ macro_rules! tags {
$( #[$enum_attr] )*
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
#[non_exhaustive]
#[repr($ty)]
pub enum $name {
$($(#[$ident_attr])* $tag,)*
$($(#[$ident_attr])* $tag = $val,)*
$(
#[$unknown_meta]
Unknown($ty),
@@ -20,7 +24,7 @@ macro_rules! tags {
impl $name {
#[inline(always)]
fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
const fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
match n {
$( $val => Ok($name::$tag), )*
n => Err(n),
@@ -28,7 +32,7 @@ macro_rules! tags {
}
#[inline(always)]
fn __to_inner_type(&self) -> $ty {
const fn __to_inner_type(&self) -> $ty {
match *self {
$( $name::$tag => $val, )*
$( $name::Unknown($unknown_doc) => { $unknown_doc }, )*
@@ -42,19 +46,25 @@ macro_rules! tags {
($name:tt, u16, $($unknown_doc:ident)*) => {
impl $name {
#[inline(always)]
pub fn from_u16(val: u16) -> Option<Self> {
Self::__from_inner_type(val).ok()
pub const fn from_u16(val: u16) -> Option<Self> {
match Self::__from_inner_type(val) {
Ok(v) => Some(v),
Err(_) => None,
}
}
$(
#[inline(always)]
pub fn from_u16_exhaustive($unknown_doc: u16) -> Self {
Self::__from_inner_type($unknown_doc).unwrap_or_else(|_| $name::Unknown($unknown_doc))
pub const fn from_u16_exhaustive($unknown_doc: u16) -> Self {
match Self::__from_inner_type($unknown_doc) {
Ok(v) => v,
Err(_) => $name::Unknown($unknown_doc),
}
}
)*
#[inline(always)]
pub fn to_u16(&self) -> u16 {
pub const fn to_u16(&self) -> u16 {
Self::__to_inner_type(self)
}
}
@@ -122,6 +132,11 @@ pub enum Tag(u16) unknown(
SMaxSampleValue = 341, // TODO add support
// JPEG
JPEGTables = 347,
// Subsampling
#[doc(alias = "YCbCrSubsampling")]
ChromaSubsampling = 530, // TODO add support
#[doc(alias = "YCbCrPositioning")]
ChromaPositioning = 531, // TODO add support
// GeoTIFF
ModelPixelScaleTag = 33550, // (SoftDesk)
ModelTransformationTag = 34264, // (JPL Carto Group)
@@ -155,6 +170,18 @@ pub enum Tag(u16) unknown(
// practice it just returns garbage and the validity does not matter greatly to us).
pub struct IfdPointer(pub u64);
impl fmt::LowerHex for IfdPointer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::LowerHex::fmt(&self.0, f)
}
}
impl core::fmt::UpperHex for IfdPointer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::UpperHex::fmt(&self.0, f)
}
}
tags! {
/// The type of an IFD entry (a 2 byte field).
pub enum Type(u16) {
@@ -253,6 +280,17 @@ pub enum CompressionMethod(u16) unknown(
// Self-assigned by libtiff
ZSTD = 0xC350,
// Self-assigned by libtiff
WebP = 0xC351,
// Self-assigned by libtiff/SGI
// A packbit like scheme on specific 32-bit samples (16 sint luma, 8 sint u,v). Only really
// compatible with PhotometricInterpretation CIELogLuv.
SgiLog = 34676,
// A packbit like scheme on specific 24-bit samples (10 packed bits luma, 14-bit uv index).
// Only really compatible with PhotometricInterpretation CIELogLuv.
SgiLog24 = 34677,
}
}
@@ -265,7 +303,41 @@ pub enum PhotometricInterpretation(u16) {
TransparencyMask = 4,
CMYK = 5,
YCbCr = 6,
/// Biased Lab with all coefficients stored as unsigned integers.
CIELab = 8,
IccLab = 9,
ItuLab = 10,
/// Logarithmic L* color space,
///
/// see: <https://web.archive.org/web/20250121120358/https://www.anyhere.com/gward/pixformat/tiffluv.html>
/// References: <https://doi.org/10.2352/CIC.1998.6.1.art00046>
///
/// The `uv` coordinates are based on:
///
/// ```text
/// (6*u - 16*v + 12) * (-2x + 12y + 3) = 36
///
/// u' = 4x / ( -2x + 12y + 3 )
/// v' = 9y / ( -2x + 12y + 3 )
/// x = 9u' / (6u' - 16v' + 12)
/// y = 4v' / (6u' - 16v' + 12)
/// ```
///
/// Since `(-2x + 12y + 3) <= 15` it holds `(6u' - 16v' + 12) >= 36/15` for all XYZ colors
/// which makes the back transform to `xyY` well-defined. Also `v' <= 9/15 = 0.6` and also
/// by practical means `u' <= 2/3`.
///
/// The compression format that is supposed to be used with this constrains them as well. It
/// encodes each component as a 16-bit unsigned integer scaled and clamped to a quantization
/// using 256 out of 410 levels or an index into a table based on a quantization to 177 levels
/// each `0.0035` (~1/286) wide and 163 levels of `v` (same quantization width).
///
/// > Larson, G.W., ``Overcoming Gamut and Dynamic Range Limitations in Digital Images,'' Proceedings of the Sixth Color Imaging Conference, November 1998.
CIELogL = 32844,
/// Logarithmic L*u'v' color space,
///
/// See references under [`Self::CIELogL`].
CIELogLuv = 32845,
}
}
@@ -285,7 +357,9 @@ pub enum Predictor(u16) {
/// 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,
/// Not currently supported
/// Floating-point predictor for IEEE floating-point sample types.
///
/// This predictor byte-shuffles the data and applies byte-level horizontal differencing.
FloatingPoint = 3,
}
}
@@ -311,6 +385,135 @@ pub enum SampleFormat(u16) unknown(
}
}
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]
}
/// 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 {
@@ -386,3 +589,12 @@ pub(crate) enum EndianBytes {
Four,
Eight,
}
/// Defines how directories are laid out in the file.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TiffVariant {
/// TIFF-V6.0 with 32-bit offsets and counts.
Standard,
/// Extended TIFF with 64-bit offsets and counts and long directories.
BigTiff,
}
+8
View File
@@ -2,3 +2,11 @@ subsubifd.tif:
url: https://data.kitware.com/api/v1/file/hashsum/sha512/372ca32735c8a8fdbe2286dabead9e779da63f10ba81eda84625e5273f76d74ca1a47a978f67e9c00c12f7f72009c7b2c07a641e643bb0c463812f4ae7f15d6e/download
credit: https://github.com/DigitalSlideArchive/tifftools/commit/4d00c70dbce828262f86c1431f7c66b1748965ac
license: CC0
usda/*.tif
url: https://registry.opendata.aws/naip/index.html
credit: NAIP on AWS was accessed on 2026-01-28 from https://registry.opendata.aws/naip.
license: Public Domain
libtiffpic/
url: https://download.osgeo.org/libtiff/
credit: libtiff
license: CC0
+3 -1
View File
@@ -15,7 +15,9 @@ fn test_big_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");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(64, 64)
+20 -5
View File
@@ -16,7 +16,9 @@ fn test_white_ieee_fp16() {
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");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(256, 256)
@@ -25,6 +27,7 @@ fn test_white_ieee_fp16() {
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));
@@ -43,7 +46,9 @@ fn test_one_black_pixel_ieee_fp16() {
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");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(256, 256)
@@ -52,6 +57,7 @@ fn test_one_black_pixel_ieee_fp16() {
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 {
@@ -74,7 +80,9 @@ fn test_pattern_horizontal_differencing_ieee_fp16() {
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");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(256, 256)
@@ -83,6 +91,7 @@ fn test_pattern_horizontal_differencing_ieee_fp16() {
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];
@@ -107,7 +116,9 @@ fn test_pattern_predictor_ieee_fp16() {
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");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(256, 256)
@@ -116,6 +127,7 @@ fn test_pattern_predictor_ieee_fp16() {
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];
@@ -175,7 +187,9 @@ fn test_predictor_ieee_fp16() {
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");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(16, 16)
@@ -184,6 +198,7 @@ fn test_predictor_ieee_fp16() {
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);
+21 -2
View File
@@ -7,7 +7,7 @@ use tiff::ColorType;
use std::fs::File;
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images";
const TEST_IMAGE_DIR: &str = "./tests/geotiff";
#[test]
fn test_geo_tiff() {
@@ -15,8 +15,9 @@ fn test_geo_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");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder = decoder.with_limits(tiff::decoder::Limits::unlimited());
decoder.next_image().expect("Cannot read image");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
@@ -61,3 +62,21 @@ fn test_geo_tiff() {
assert_eq!(data.len(), 500);
}
}
#[cfg(feature = "webp")]
#[test]
fn test_webp_tiff() {
let filenames = ["usda_naip_256_webp_z3.tif"];
let mut buffer = DecodingResult::U8(vec![]);
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::open(img_file).expect("Cannot create decoder");
while decoder.more_images() {
decoder.next_image().unwrap();
decoder.read_image_to_buffer(&mut buffer).unwrap();
}
}
}
+103 -19
View File
@@ -14,7 +14,9 @@ macro_rules! test_image_sum {
fn $name(file: &str, expected_type: ColorType, expected_sum: $sum_ty) {
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 decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let img_res = decoder.read_image().unwrap();
@@ -44,7 +46,9 @@ test_image_sum!(test_image_sum_f64, F64, f64);
fn test_image_color_type_unsupported(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");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.colortype().unwrap(), expected_type);
assert!(matches!(
decoder.read_image(),
@@ -166,7 +170,8 @@ fn test_string_tags() {
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");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
let software = decoder.get_tag(tiff::tags::Tag::Software).unwrap();
match software {
ifd::Value::Ascii(s) => assert_eq!(
@@ -190,7 +195,8 @@ fn test_decode_data() {
}
}
let file = File::open("./tests/decodedata-rgb-3c-8b.tiff").unwrap();
let mut decoder = Decoder::new(file).unwrap();
let mut decoder = Decoder::open(file).unwrap();
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
@@ -236,7 +242,8 @@ fn test_tiled_rect_rgb_u8() {
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");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
let c = decoder
@@ -322,7 +329,8 @@ fn test_tiled_incremental() {
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 decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let tiles = decoder.tile_count().unwrap();
@@ -352,7 +360,8 @@ fn test_read_planar_bands() {
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 decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let chunks = decoder.strip_count().unwrap();
@@ -396,7 +405,8 @@ fn test_div_zero() {
178, 178, 178,
];
let err = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let err = decoder.next_image().unwrap_err();
match err {
TiffError::FormatError(TiffFormatError::StripTileTagConflict) => {}
@@ -414,7 +424,8 @@ fn test_too_many_value_bytes() {
0, 89, 89, 89, 89, 89, 89, 89, 89, 96, 1, 20, 89, 89, 89, 89, 18,
];
let error = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let error = decoder.next_image().unwrap_err();
match error {
tiff::TiffError::LimitsExceeded => {}
@@ -432,7 +443,8 @@ fn fuzzer_testcase5() {
178, 178, 178,
];
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
@@ -445,7 +457,8 @@ fn fuzzer_testcase1() {
178,
];
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
@@ -458,7 +471,8 @@ fn fuzzer_testcase6() {
178, 178,
];
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
@@ -470,7 +484,8 @@ fn oom() {
0, 0, 0, 40, 0, 0, 0, 23, 1, 4, 0, 1, 0, 0, 0, 178, 48, 178, 178, 178, 178, 162, 178,
];
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
@@ -482,7 +497,8 @@ fn fuzzer_testcase4() {
0, 0, 0, 40, 0, 0, 0, 23, 1, 4, 0, 1, 0, 0, 0, 48, 178, 178, 178, 0, 1, 0, 13, 13,
];
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
@@ -498,7 +514,8 @@ fn fuzzer_testcase2() {
73,
];
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
@@ -514,7 +531,8 @@ fn invalid_jpeg_tag_2() {
0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 36, 73, 73, 0, 42, 36, 36, 36, 36, 0, 0, 8, 0,
];
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
@@ -527,7 +545,8 @@ fn fuzzer_testcase3() {
255, 255,
];
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
@@ -545,7 +564,8 @@ fn timeout() {
0, 0, 73, 73, 42, 0, 8, 0, 0, 0, 0, 0, 32,
];
let error = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let error = decoder.next_image().unwrap_err();
match error {
TiffError::FormatError(TiffFormatError::CycleInOffsets) => {}
@@ -583,7 +603,8 @@ fn test_image_bytes(
) {
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 decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().unwrap();
let mut buffer = vec![0u8; 0];
for (idx, &(expected_type, expected_crc)) in expected.iter().enumerate() {
@@ -684,3 +705,66 @@ fn test_fax4() {
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);
}
#[test]
#[cfg(feature = "fax")]
fn test_decode_huge_g4() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("big_g4.tif");
let img_file = File::open(path).expect("failed to open large G4 test image");
let mut decoder = Decoder::open(img_file).expect("failed to create decoder");
decoder.next_image().expect("failed to read image IFD");
let (width, height) = decoder.dimensions().expect("failed to get dimensions");
assert!(
height > u16::MAX as u32,
"Test image height {} is not large enough to test u16 overflow",
height
);
let result = decoder
.read_image()
.expect("failed to decode huge G4 image");
if let tiff::decoder::DecodingResult::U8(data) = result {
assert_eq!(data.len(), (width as usize * height as usize));
} else {
panic!("Unexpected decoding result type");
}
}
+161
View File
@@ -0,0 +1,161 @@
use std::collections::HashMap;
use std::{fs::File, path};
use tiff::decoder::{Decoder, DecodingResult};
use libtest_mimic::{Arguments, Failed, Trial};
use walkdir::WalkDir;
const REFERENCE_DIR: &str = "tests/libtiffpic";
// `find test/libtiffpic -type f | sort`
const FILES: &[(&str, bool)] = &[
("tests/libtiffpic/caspian.tif", true),
("tests/libtiffpic/cramps.tif", true),
("tests/libtiffpic/cramps-tile.tif", true),
("tests/libtiffpic/CVS/Entries", false),
("tests/libtiffpic/CVS/Repository", false),
("tests/libtiffpic/CVS/Root", false),
("tests/libtiffpic/depth/CVS/Entries", false),
("tests/libtiffpic/depth/CVS/Repository", false),
("tests/libtiffpic/depth/CVS/Root", false),
("tests/libtiffpic/depth/flower-minisblack-02.tif", true),
("tests/libtiffpic/depth/flower-minisblack-04.tif", true),
("tests/libtiffpic/depth/flower-minisblack-06.tif", true),
("tests/libtiffpic/depth/flower-minisblack-08.tif", true),
("tests/libtiffpic/depth/flower-minisblack-10.tif", true),
("tests/libtiffpic/depth/flower-minisblack-12.tif", true),
("tests/libtiffpic/depth/flower-minisblack-14.tif", true),
("tests/libtiffpic/depth/flower-minisblack-16.tif", true),
("tests/libtiffpic/depth/flower-minisblack-24.tif", true),
("tests/libtiffpic/depth/flower-minisblack-32.tif", true),
("tests/libtiffpic/depth/flower-palette-02.tif", true),
("tests/libtiffpic/depth/flower-palette-04.tif", true),
("tests/libtiffpic/depth/flower-palette-08.tif", true),
("tests/libtiffpic/depth/flower-palette-16.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-02.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-04.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-08.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-10.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-12.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-14.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-16.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-24.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-32.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-02.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-04.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-08.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-10.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-12.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-14.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-16.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-24.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-32.tif", true),
(
"tests/libtiffpic/depth/flower-separated-contig-08.tif",
true,
),
(
"tests/libtiffpic/depth/flower-separated-contig-16.tif",
true,
),
(
"tests/libtiffpic/depth/flower-separated-planar-08.tif",
true,
),
(
"tests/libtiffpic/depth/flower-separated-planar-16.tif",
true,
),
("tests/libtiffpic/depth/genimages", false),
("tests/libtiffpic/depth/README.txt", false),
("tests/libtiffpic/depth/summary.txt", false),
("tests/libtiffpic/dscf0013.tif", true),
("tests/libtiffpic/fax2d.tif", true),
// No support for direct CCITT Group 3 1D format
("tests/libtiffpic/g3test.g3", false),
("tests/libtiffpic/g3test.tif", true),
("tests/libtiffpic/jello.tif", true),
("tests/libtiffpic/jim___ah.tif", true),
("tests/libtiffpic/jim___cg.tif", true),
("tests/libtiffpic/jim___dg.tif", true),
("tests/libtiffpic/jim___gg.tif", true),
("tests/libtiffpic/ladoga.tif", true),
("tests/libtiffpic/off_l16.tif", true),
("tests/libtiffpic/off_luv24.tif", true),
("tests/libtiffpic/off_luv32.tif", true),
("tests/libtiffpic/oxford.tif", true),
("tests/libtiffpic/pc260001.tif", true),
("tests/libtiffpic/quad-jpeg.tif", true),
("tests/libtiffpic/quad-lzw.tif", true),
("tests/libtiffpic/quad-tile.tif", true),
("tests/libtiffpic/README", false),
("tests/libtiffpic/smallliz.tif", true),
("tests/libtiffpic/strike.tif", true),
("tests/libtiffpic/text.tif", true),
("tests/libtiffpic/ycbcr-cat.tif", true),
("tests/libtiffpic/zackthecat.tif", true),
];
fn main() {
let mut candidates: HashMap<_, _> = FILES.iter().copied().collect();
let mut trials = Vec::new();
for entry in WalkDir::new(REFERENCE_DIR) {
let entry = entry.unwrap();
if !entry.file_type().is_file() {
continue;
}
let Some(path) = entry.path().to_str() else {
continue;
};
let path = path.replace(std::path::MAIN_SEPARATOR, "/"); // always use UNIX path separators
// Mark this path as consumed in either case by removing it.
let Some(should_decode) = candidates.remove(path.as_str()) else {
panic!("File {} not in reference list", path);
};
// Check if we should turn it into a test case.
if !should_decode {
continue;
}
let path = path.to_string();
trials.push(Trial::test(
path.to_string(),
move || -> Result<(), Failed> {
decode_libtiffpic(&path)
.map_err(|e| Failed::from(format_args!("Decoding failed for {}: {}", path, e)))
},
));
}
if !candidates.is_empty() {
for (path, _) in candidates {
eprintln!("Reference file {} not found in directory walk", path);
}
panic!("Some reference files were not found");
}
let args = Arguments::from_args();
libtest_mimic::run(&args, trials).exit();
}
fn decode_libtiffpic(path: &str) -> Result<(), tiff::TiffError> {
let path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path);
let img_file = File::open(path)?;
let mut decoder = Decoder::open(img_file)?;
let mut buffer = DecodingResult::U8(vec![]);
while decoder.more_images() {
decoder.next_image()?;
let _layout = decoder.read_image_to_buffer(&mut buffer);
}
Ok(())
}
+126
View File
@@ -0,0 +1,126 @@
use tiff::decoder::{Decoder, DecodingResult, Limits, 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::open(io).unwrap();
tif.next_image().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::open(io).unwrap();
tif.next_image().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);
}
}
}
#[test]
fn read_planes_by_limits() {
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 limits = Limits::default();
let mut buffer = DecodingResult::U8(vec![]);
// Make room for 1 plane.
let mut tif = Decoder::open(io).unwrap();
tif = tif.with_limits({
limits.decoding_buffer_size = 374 * 499;
limits.clone()
});
tif.next_image().unwrap();
tif.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499);
// Make room for 2 planes.
tif = tif.with_limits({
limits.decoding_buffer_size = 374 * 499 * 2;
limits.clone()
});
tif.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499);
// Make room for 3 planes.
tif = tif.with_limits({
limits.decoding_buffer_size = 374 * 499 * 3;
limits.clone()
});
tif.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499 * 3);
// Make room for 4 planes?
tif = tif.with_limits({
limits.decoding_buffer_size = 374 * 499 * 4;
limits.clone()
});
tif.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499 * 3);
}
+183 -20
View File
@@ -1,7 +1,7 @@
extern crate tiff;
use tiff::decoder::{ifd, Decoder, DecodingResult};
use tiff::encoder::{colortype, Ifd, Ifd8, SRational, TiffEncoder};
use tiff::encoder::{colortype, Ifd, Ifd8, Predictor, SRational, TiffEncoder};
use tiff::tags::Tag;
use tiff::ColorType;
@@ -33,7 +33,9 @@ fn encode_decode() {
}
{
file.seek(SeekFrom::Start(0)).unwrap();
let mut decoder = Decoder::new(&mut file).unwrap();
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
@@ -57,7 +59,7 @@ fn encode_decode() {
),
(Tag::Compression, ifd::Value::Short(1)),
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
(Tag::StripOffsets, ifd::Value::Unsigned(47)),
(Tag::StripOffsets, ifd::Value::Unsigned(35)),
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
(Tag::StripByteCounts, ifd::Value::Unsigned(30000)),
@@ -81,7 +83,10 @@ fn encode_decode() {
decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into())
);
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
if let tiff::decoder::DecodingResult::U8(img_res) = buffer {
assert_eq!(image_data, img_res);
} else {
panic!("Wrong data type");
@@ -113,7 +118,9 @@ fn encode_decode_big() {
}
{
file.seek(SeekFrom::Start(0)).unwrap();
let mut decoder = Decoder::new(&mut file).unwrap();
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
@@ -161,7 +168,9 @@ fn encode_decode_big() {
decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into())
);
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::U8(img_res) = buffer {
assert_eq!(image_data, img_res);
} else {
panic!("Wrong data type");
@@ -202,8 +211,10 @@ fn test_encode_ifd() {
// Rewind the cursor for reading
data.set_position(0);
{
let mut decoder = Decoder::new(&mut data).unwrap();
let mut decoder = Decoder::open(&mut data).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.assert_tag_u32(65000), 42);
assert_eq!(decoder.assert_tag_u32_vec(65000), [42]);
@@ -235,16 +246,22 @@ const TEST_IMAGE_DIR: &str = "./tests/images/";
macro_rules! test_roundtrip {
($name:ident, $buffer:ident, $buffer_ty:ty) => {
#[track_caller]
fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
file: &str,
expected_type: ColorType,
) {
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 decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), expected_type);
let image_data = match decoder.read_image().unwrap() {
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap();
let image_data = match buffer {
DecodingResult::$buffer(res) => res,
_ => panic!("Wrong data type"),
};
@@ -258,9 +275,12 @@ macro_rules! test_roundtrip {
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::new(&mut file).unwrap();
if let DecodingResult::$buffer(img_res) = decoder.read_image().unwrap() {
assert_eq!(image_data, img_res);
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::$buffer(img_res) = buffer {
assert_eq!(*image_data, img_res);
} else {
panic!("Wrong data type");
}
@@ -368,6 +388,144 @@ fn test_ycbcr_u8_roundtrip() {
test_u8_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
}
/// Test that encoding with the floating-point predictor works correctly
/// and can be decoded back to the original data.
#[test]
fn test_gray_f32_floating_point_predictor() {
// Generate test data: a gradient of f32 values
let width = 32u32;
let height = 32u32;
let mut image_data: Vec<f32> = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
// Create a gradient pattern
image_data.push((x as f32 + y as f32 * 0.1) / 100.0);
}
}
let mut file = Cursor::new(Vec::new());
{
let mut tiff = TiffEncoder::new(&mut file)
.unwrap()
.with_predictor(Predictor::FloatingPoint);
tiff.write_image::<colortype::Gray32Float>(width, height, &image_data)
.unwrap();
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(32));
let mut buffer = DecodingResult::F32(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::F32(img_res) = buffer {
assert_eq!(image_data.len(), img_res.len());
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
assert_eq!(
*orig, *decoded,
"Mismatch at value: orig={}, decoded={}",
orig, decoded
);
}
} else {
panic!("Wrong data type");
}
}
}
/// Test floating-point predictor with RGB f32 data
#[test]
fn test_rgb_f32_floating_point_predictor() {
let width = 16u32;
let height = 16u32;
let mut image_data: Vec<f32> = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
// RGB pattern
image_data.push(x as f32 / width as f32); // R
image_data.push(y as f32 / height as f32); // G
image_data.push((x + y) as f32 / (width + height) as f32); // B
}
}
let mut file = Cursor::new(Vec::new());
{
let mut tiff = TiffEncoder::new(&mut file)
.unwrap()
.with_predictor(Predictor::FloatingPoint);
tiff.write_image::<colortype::RGB32Float>(width, height, &image_data)
.unwrap();
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(32));
let mut buffer = DecodingResult::F32(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::F32(img_res) = buffer {
assert_eq!(image_data.len(), img_res.len());
for (i, (orig, decoded)) in image_data.iter().zip(img_res.iter()).enumerate() {
assert_eq!(
*orig, *decoded,
"Mismatch at index {}: orig={}, decoded={}",
i, orig, decoded
);
}
} else {
panic!("Wrong data type");
}
}
}
/// Test floating-point predictor with f64 data
#[test]
fn test_gray_f64_floating_point_predictor() {
let width = 16u32;
let height = 16u32;
let mut image_data: Vec<f64> = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
image_data.push((x as f64 + y as f64 * 0.1) / 100.0);
}
}
let mut file = Cursor::new(Vec::new());
{
let mut tiff = TiffEncoder::new(&mut file)
.unwrap()
.with_predictor(Predictor::FloatingPoint);
tiff.write_image::<colortype::Gray64Float>(width, height, &image_data)
.unwrap();
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(64));
let mut buffer = DecodingResult::F64(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::F64(img_res) = buffer {
assert_eq!(image_data.len(), img_res.len());
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
assert_eq!(
*orig, *decoded,
"Mismatch: orig={}, decoded={}",
orig, decoded
);
}
} else {
panic!("Wrong data type");
}
}
}
trait AssertDecode {
fn assert_tag_u32(&mut self, tag: u16) -> u32;
fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32>;
@@ -445,7 +603,8 @@ fn test_multiple_byte() {
data.set_position(0);
{
let mut decoder = Decoder::new(&mut data).unwrap();
let mut decoder = Decoder::open(&mut data).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.assert_tag_u32_vec(65000), [1]);
assert_eq!(decoder.assert_tag_u32_vec(65001), [1, 2]);
@@ -526,7 +685,8 @@ fn test_signed() {
//Rewind the cursor for reading
data.set_position(0);
{
let mut decoder = Decoder::new(&mut data).unwrap();
let mut decoder = Decoder::open(&mut data).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.assert_tag_i32(65000), -1);
assert_eq!(decoder.assert_tag_i32_vec(65001), [-1]);
@@ -578,13 +738,14 @@ fn test_multipage_image() {
img_file.seek(SeekFrom::Start(0)).unwrap();
{
let mut img_decoder = Decoder::new(&mut img_file).unwrap();
let mut decoder = Decoder::open(&mut img_file).unwrap();
decoder.next_image().unwrap();
// check the dimensions of the image in the first page
assert_eq!(img_decoder.dimensions().unwrap(), (2, 2));
img_decoder.next_image().unwrap();
assert_eq!(decoder.dimensions().unwrap(), (2, 2));
decoder.next_image().unwrap();
// check the dimensions of the image in the second page
assert_eq!(img_decoder.dimensions().unwrap(), (3, 3));
assert_eq!(decoder.dimensions().unwrap(), (3, 3));
}
}
@@ -613,7 +774,8 @@ fn test_rows_per_strip() {
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::new(&mut file).unwrap();
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.get_tag_u64(Tag::RowsPerStrip).unwrap(), 2);
assert_eq!(decoder.strip_count().unwrap(), 50);
@@ -653,9 +815,10 @@ fn test_auxiliary_directory() {
}
data.set_position(0);
let mut decoder = Decoder::new(&mut data).unwrap();
let mut decoder = Decoder::open(&mut data).unwrap();
{
decoder.next_image().unwrap();
assert_eq!(decoder.dimensions().unwrap(), (1, 1));
let _ = decoder.read_image().unwrap();
}
+5 -5
View File
@@ -5,7 +5,6 @@ use tiff::{
decoder::{Decoder, DecodingResult},
encoder::{
colortype::{self, ColorType},
compression::*,
Compression, TiffEncoder, TiffValue,
},
};
@@ -110,7 +109,8 @@ fn encode_decode_with_compression(compression: Compression) {
// Decode tiff
data.set_position(0);
{
let mut decoder = Decoder::new(data).unwrap();
let mut decoder = Decoder::open(data).unwrap();
decoder.next_image().unwrap();
// Check the RGB image
assert_eq!(
@@ -147,9 +147,9 @@ fn encode_decode_with_lzw() {
#[test]
#[cfg(feature = "deflate")]
fn encode_decode_with_deflate() {
encode_decode_with_compression(Compression::Deflate(DeflateLevel::Fast));
encode_decode_with_compression(Compression::Deflate(DeflateLevel::Balanced));
encode_decode_with_compression(Compression::Deflate(DeflateLevel::Best));
encode_decode_with_compression(Compression::Deflate(1));
encode_decode_with_compression(Compression::Deflate(6));
encode_decode_with_compression(Compression::Deflate(9));
}
#[test]
+2 -1
View File
@@ -13,7 +13,8 @@ fn test_directory<F: Fn(File) -> bool>(path: &str, f: F) {
}
fn decode_tiff(file: File) -> TiffResult<()> {
let mut decoder = Decoder::new(file)?;
let mut decoder = Decoder::open(file)?;
decoder.next_image()?;
decoder.read_image()?;
Ok(())
}
File renamed without changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+28
View File
@@ -0,0 +1,28 @@
/cramps-tile.tif/1.1/Sat Nov 23 05:17:31 2002/-kb/
/cramps.tif/1.1/Sat Nov 23 05:17:31 2002/-kb/
/fax2d.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/g3test.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/jello.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/jim___ah.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/jim___cg.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/jim___dg.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/jim___gg.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/off_l16.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/off_luv24.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/off_luv32.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/oxford.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/quad-jpeg.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/quad-lzw.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/quad-tile.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/smallliz.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/strike.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/text.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/ycbcr-cat.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
D/depth////
/README/1.2/Thu Dec 29 22:06:14 2005//
/caspian.tif/1.1/Thu Dec 29 22:06:43 2005/-kb/
/dscf0013.tif/1.1/Thu Dec 29 22:07:23 2005/-kb/
/g3test.g3/1.1/Thu Dec 29 22:07:27 2005/-kb/
/ladoga.tif/1.1/Thu Dec 29 22:07:29 2005/-kb/
/pc260001.tif/1.1/Mon Dec 26 14:54:00 2005/-kb/
/zackthecat.tif/1.1/Thu Dec 29 22:08:43 2005/-kb/
+1
View File
@@ -0,0 +1 @@
libtiffpic
+1
View File
@@ -0,0 +1 @@
:pserver:dron@cvs.maptools.org:/cvs/maptools/cvsroot
+101
View File
@@ -0,0 +1,101 @@
The following image files are included for testing. Note that a wide
variety of images can be generated using the tools provided with the
library (e.g. tiffcp, tiff2bw, tiffmedian, etc.); see below for some
hints on this.
PlanarConfiguration = 1 (packed data)
-------------------------------------
caspian.tif 279x220 64-bit floating point (deflate) Caspian Sea from space
cramps.tif 800x607 8-bit b&w (packbits) "cramps poster"
cramps-tile.tif 256x256 tiled version of cramps.tif (no compression)
dscf0013.tif 640x480 YCbCr digital camera image which lacks Reference
Black/White values. Contains EXIF SubIFD. No compression.
fax2d.tif 1728x1082 1-bit b&w (G3/2D) facsimile
g3test.g3 raw Group 3 encoded fax file
g3test.tif TIFF equivalent of g3test.g3 created by fax2tiff
jello.tif 256x192 8-bit RGB (lzw palette) Paul Heckbert "jello"
ladoga.tif 158x118 16-bit unsigned, single band, deflate
off_l16.tif 333x225 8-bit CIE LogL (SGILog) office from Greg Larson
off_luv24.tif 333x225 8-bit CIE LogLuv (SGILog24) office from " "
off_luv32.tif 333x225 8-bit CIE LogLuv (SGILog) office from " "
pc260001.tif 640x480 8-bit RGB digital camera image. Contains EXIF SubIFD.
No compression.
quad-jpeg.tif 512x384 8-bit YCbCr (jpeg) version of quad-lzw.tif
quad-lzw.tif 512x384 8-bit RGB (lzw) "quadric surfaces"
quad-tile.tif 512x384 tiled version of quad-lzw.tif (lzw)
strike.tif 256x200 8-bit RGBA (lzw) "bowling pins" from Pixar
text.tif 1512x359 4-bit b&w (thunderscan) am-express credit card
ycbcr-cat.tif 250x325 8-bit YCbCr (lzw) "kitty" created by rgb2ycbcr
smallliz.tif 160x160 8-bit YCbCr (OLD jpeg) lizard from HP**
zackthecat.tif 234x213 8-bit YCbCr (OLD jpeg) tiled "ZackTheCat" from NeXT**
** These images are in a deprecated format and are included only for testing
backwards compatibility. JPEG-encoded TIFF images use a different scheme
described in TIFF Technical Note #2 and implemented by libtiff.
PlanarConfiguration = 2 (separated samples)
-------------------------------------------
oxford.tif 601x81 8-bit RGB (lzw) screendump off oxford
The other images are from Hewlett Packard and exemplify the use of the
HalftoneHints tag (in their words):
The images are all the same subject, and should all appear the same
after rendering. Each of the images is slightly different as outlined
by the following table:
FileName ToneRange HalftoneHints
jim___cg.tif A Y
jim___dg.tif B N
jim___gg.tif B Y
The images with tone range A are 0-100% linear reflectance images,
in other words, fixed full gamut tone range. The images with tone
range B are what we call "normalized", meaning that the image has
been tone adjusted such the Highlight and Shadow are at gray levels
1 & 2**BitsPerSample-2.
Each of the three grayscale images should be rendered to look like
the halftone in pics/jim___ah.tif when printed on a Canon SX engine.
Generated Images
----------------
The file g3test.tif was created from g3test.g3 with the command:
fax2tiff -o t.tif -1 -p -M -R 98 g3test.g3
Tiled images can be created with tiffcp; e.g.
tiffcp -t -c none cramps.tif cramps-tile.tif
(but the tiled images included here were actually generated by the
Silicon Graphics Image Library).
The file quad-jpeg.tif was created from quad-lzw.tif with the command:
tiffcp -c jpeg quad-lzw.tif quad-jpeg.tif
To generate a tiled image with separated samples the following might
be used:
tiffcp -t -p separate quad-lzw.tif t.tif
[ Beware that tiffcp does not handle every format TIFF image. In particular,
when conversion between tiled and strip'd images is reliable only when
BitsPerSample is 8. ]
To generate a G4-encoded image with a single strip the following would work:
tiffcp -c g4 -r 1082 fax2d.tif t.tif
Contributing New Images
-----------------------
If you have images that are significantly different from those included
in this package and that do NOT HAVE A COPYRIGHT; feel free to send them
to me at sam@engr.sgi.com. I prefer to receive a URL to a location from
which I can retrieve the image by public FTP. However if that is not
possible you can send it to me in a mail message uuencode'd. Please do
not send large images by mail. Also, please do not send me an image
unless it is notably unique; e.g. not easily generated using the tools
I provide.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+40
View File
@@ -0,0 +1,40 @@
/README.txt/1.1/Sun Jan 30 23:03:00 2005//
/genimages/1.1/Mon Jan 31 02:49:00 2005//
/summary.txt/1.1/Mon Jan 31 02:49:00 2005//
/flower-minisblack-24.tif/1.2/Sun Nov 13 17:25:25 2005/-kb/
/flower-rgb-contig-24.tif/1.2/Sun Nov 13 17:22:46 2005/-kb/
/flower-rgb-planar-24.tif/1.2/Sun Nov 13 17:20:24 2005/-kb/
/flower-minisblack-02.tif/1.1/Thu Dec 29 22:08:43 2005/-kb/
/flower-minisblack-04.tif/1.1/Thu Dec 29 22:08:43 2005/-kb/
/flower-minisblack-06.tif/1.1/Thu Dec 29 22:08:43 2005/-kb/
/flower-minisblack-08.tif/1.1/Thu Dec 29 22:08:44 2005/-kb/
/flower-minisblack-10.tif/1.1/Thu Dec 29 22:08:44 2005/-kb/
/flower-minisblack-12.tif/1.1/Thu Dec 29 22:08:45 2005/-kb/
/flower-minisblack-14.tif/1.1/Thu Dec 29 22:08:45 2005/-kb/
/flower-minisblack-16.tif/1.1/Thu Dec 29 22:08:46 2005/-kb/
/flower-minisblack-32.tif/1.1/Thu Dec 29 22:08:47 2005/-kb/
/flower-palette-02.tif/1.1/Thu Dec 29 22:08:47 2005/-kb/
/flower-palette-04.tif/1.1/Thu Dec 29 22:08:47 2005/-kb/
/flower-palette-08.tif/1.1/Thu Dec 29 22:08:47 2005/-kb/
/flower-palette-16.tif/1.1/Thu Dec 29 22:08:49 2005/-kb/
/flower-rgb-contig-02.tif/1.1/Thu Dec 29 22:08:49 2005/-kb/
/flower-rgb-contig-04.tif/1.1/Thu Dec 29 22:08:49 2005/-kb/
/flower-rgb-contig-08.tif/1.1/Thu Dec 29 22:08:50 2005/-kb/
/flower-rgb-contig-10.tif/1.1/Thu Dec 29 22:08:51 2005/-kb/
/flower-rgb-contig-12.tif/1.1/Thu Dec 29 22:08:52 2005/-kb/
/flower-rgb-contig-14.tif/1.1/Thu Dec 29 22:08:53 2005/-kb/
/flower-rgb-contig-16.tif/1.1/Thu Dec 29 22:08:55 2005/-kb/
/flower-rgb-contig-32.tif/1.1/Thu Dec 29 22:08:58 2005/-kb/
/flower-rgb-planar-02.tif/1.1/Thu Dec 29 22:08:58 2005/-kb/
/flower-rgb-planar-04.tif/1.1/Thu Dec 29 22:08:58 2005/-kb/
/flower-rgb-planar-08.tif/1.1/Thu Dec 29 22:08:59 2005/-kb/
/flower-rgb-planar-10.tif/1.1/Thu Dec 29 22:09:00 2005/-kb/
/flower-rgb-planar-12.tif/1.1/Thu Dec 29 22:09:02 2005/-kb/
/flower-rgb-planar-14.tif/1.1/Thu Dec 29 22:09:02 2005/-kb/
/flower-rgb-planar-16.tif/1.1/Thu Dec 29 22:09:04 2005/-kb/
/flower-rgb-planar-32.tif/1.1/Thu Dec 29 22:09:08 2005/-kb/
/flower-separated-contig-08.tif/1.1/Thu Dec 29 22:09:10 2005/-kb/
/flower-separated-contig-16.tif/1.1/Thu Dec 29 22:09:12 2005/-kb/
/flower-separated-planar-08.tif/1.1/Thu Dec 29 22:09:12 2005/-kb/
/flower-separated-planar-16.tif/1.1/Thu Dec 29 22:09:14 2005/-kb/
D
+1
View File
@@ -0,0 +1 @@
libtiffpic/depth
+1
View File
@@ -0,0 +1 @@
:pserver:dron@cvs.maptools.org:/cvs/maptools/cvsroot
+9
View File
@@ -0,0 +1,9 @@
These sample TIFF image files are prepared by Bob Friesenhahn
<bfriesen@simple.dallas.tx.us> using a development version of
GraphicsMagick 1.2.
See the file summary.txt for a description of the images.
These files are hereby placed in the public domain.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/ksh
GM=/scratch/bfriesen/build/GraphicsMagick-32-static/utilities/gm
GM_STD_OPT="-compress none"
MASTER_IMAGE_ORIGINAL='/home/bfriesen/images/david/112_1201-flowers.jpg'
MASTER_IMAGE_GRAY=gray.miff
MASTER_IMAGE_RGB=rgb.miff
MASTER_IMAGE_CMYK=cmyk.miff
COLUMNS=73
ROWS=43
GRAY_DEPTHS='2 4 6 8 10 12 14 16 24 32'
PSEUDO_DEPTHS='2 4 8 16'
TRUE_COLORS='2 4 8 10 12 14 16 24 32'
CMYK_COLORS='8 16'
SUMMARY=summary.txt
echo > $SUMMARY
$GM convert $MASTER_IMAGE_ORIGINAL +profile '*' -depth 32 -type Grayscale -density 72x72 -geometry "${COLUMNS}x${ROWS}" $MASTER_IMAGE_GRAY
$GM convert $MASTER_IMAGE_ORIGINAL +profile '*' -depth 32 -density 72x72 -geometry "${COLUMNS}x${ROWS}" $MASTER_IMAGE_RGB
$GM convert $MASTER_IMAGE_ORIGINAL +profile '*' -colorspace cmyk -depth 32 -density 72x72 -geometry "${COLUMNS}x${ROWS}" $MASTER_IMAGE_CMYK
# Gray images
for depth in $GRAY_DEPTHS
do
outname=`printf "flower-minisblack-%02d.tif" $depth`
echo "$outname\t${COLUMNS}x${ROWS} $depth-bit minisblack gray image" >> $SUMMARY
$GM convert -verbose $MASTER_IMAGE_GRAY $GM_STD_OPT -type Grayscale -define tiff:bits-per-sample=$depth $outname
done
# Colormapped images
for depth in $PSEUDO_DEPTHS
do
colors=`echo "2^${depth}" | bc`
outname=`printf "flower-palette-%02d.tif" $depth`
echo "$outname\t${COLUMNS}x${ROWS} ${colors}-entry colormapped image" >> $SUMMARY
$GM convert -verbose $MASTER_IMAGE_RGB $GM_STD_OPT -colors $colors $outname
done
# Truecolor images
for depth in $TRUE_COLORS
do
outname=`printf "flower-rgb-contig-%02d.tif" $depth`
echo "$outname\t${COLUMNS}x${ROWS} $depth-bit contiguous RGB image" >> $SUMMARY
$GM convert -verbose $MASTER_IMAGE_RGB $GM_STD_OPT -type TrueColor -define tiff:bits-per-sample=$depth $outname
done
# Truecolor planar images
for depth in $TRUE_COLORS
do
outname=`printf "flower-rgb-planar-%02d.tif" $depth`
echo "$outname\t${COLUMNS}x${ROWS} $depth-bit seperated RGB image" >> $SUMMARY
$GM convert -verbose $MASTER_IMAGE_RGB $GM_STD_OPT -type TrueColor -interlace plane -define tiff:bits-per-sample=$depth $outname
done
# CMYK images
for depth in $CMYK_COLORS
do
outname=`printf "flower-separated-contig-%02d.tif" $depth`
echo "$outname\t${COLUMNS}x${ROWS} $depth-bit contiguous CMYK image" >> $SUMMARY
$GM convert -verbose $MASTER_IMAGE_RGB $GM_STD_OPT -type ColorSeparation -define tiff:bits-per-sample=$depth $outname
done
# CMYK planar images
for depth in $CMYK_COLORS
do
outname=`printf "flower-separated-planar-%02d.tif" $depth`
echo "$outname\t${COLUMNS}x${ROWS} $depth-bit separated CMYK image" >> $SUMMARY
$GM convert -verbose $MASTER_IMAGE_RGB $GM_STD_OPT -type ColorSeparation -interlace plane -define tiff:bits-per-sample=$depth $outname
done
rm -f $MASTER_IMAGE_GRAY $MASTER_IMAGE_RGB $MASTER_IMAGE_CMYK
+37
View File
@@ -0,0 +1,37 @@
flower-minisblack-02.tif 73x43 2-bit minisblack gray image
flower-minisblack-04.tif 73x43 4-bit minisblack gray image
flower-minisblack-06.tif 73x43 6-bit minisblack gray image
flower-minisblack-08.tif 73x43 8-bit minisblack gray image
flower-minisblack-10.tif 73x43 10-bit minisblack gray image
flower-minisblack-12.tif 73x43 12-bit minisblack gray image
flower-minisblack-14.tif 73x43 14-bit minisblack gray image
flower-minisblack-16.tif 73x43 16-bit minisblack gray image
flower-minisblack-24.tif 73x43 24-bit minisblack gray image
flower-minisblack-32.tif 73x43 32-bit minisblack gray image
flower-palette-02.tif 73x43 4-entry colormapped image
flower-palette-04.tif 73x43 16-entry colormapped image
flower-palette-08.tif 73x43 256-entry colormapped image
flower-palette-16.tif 73x43 65536-entry colormapped image
flower-rgb-contig-02.tif 73x43 2-bit contiguous RGB image
flower-rgb-contig-04.tif 73x43 4-bit contiguous RGB image
flower-rgb-contig-08.tif 73x43 8-bit contiguous RGB image
flower-rgb-contig-10.tif 73x43 10-bit contiguous RGB image
flower-rgb-contig-12.tif 73x43 12-bit contiguous RGB image
flower-rgb-contig-14.tif 73x43 14-bit contiguous RGB image
flower-rgb-contig-16.tif 73x43 16-bit contiguous RGB image
flower-rgb-contig-24.tif 73x43 24-bit contiguous RGB image
flower-rgb-contig-32.tif 73x43 32-bit contiguous RGB image
flower-rgb-planar-02.tif 73x43 2-bit seperated RGB image
flower-rgb-planar-04.tif 73x43 4-bit seperated RGB image
flower-rgb-planar-08.tif 73x43 8-bit seperated RGB image
flower-rgb-planar-10.tif 73x43 10-bit seperated RGB image
flower-rgb-planar-12.tif 73x43 12-bit seperated RGB image
flower-rgb-planar-14.tif 73x43 14-bit seperated RGB image
flower-rgb-planar-16.tif 73x43 16-bit seperated RGB image
flower-rgb-planar-24.tif 73x43 24-bit seperated RGB image
flower-rgb-planar-32.tif 73x43 32-bit seperated RGB image
flower-separated-contig-08.tif 73x43 8-bit contiguous CMYK image
flower-separated-contig-16.tif 73x43 16-bit contiguous CMYK image
flower-separated-planar-08.tif 73x43 8-bit separated CMYK image
flower-separated-planar-16.tif 73x43 16-bit separated CMYK image
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 112 files, more files were not shown because too many files have changed in this diff. Show more