Compare commits

..
Author SHA1 Message Date
A. Molzer 812bc970ba Document Decoder::open behavior 2026-03-06 21:09:32 +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
A. Molzer eb6275547d Build Directory FromIterator
This enables a test for the encoder's DirectoryOffset but it seems to
fit nicely with the current design, too.
2025-11-12 01:21:53 +01:00
A. Molzer 73f066c09d Expose DirectoryOffset::new 2025-11-12 01:21:52 +01:00
Aurelia Molzer 24887896ff Merge pull request #309 from image-rs/short-writes-uncompressed
Short writes uncompressed
2025-11-08 15:52:36 +00:00
A. Molzer 02118620e2 Verify handling of a blocked writer 2025-11-08 11:52:58 +01:00
A. Molzer 0b5dc72454 Fix: uncompressed encoder doing short writes 2025-11-08 11:01:52 +01:00
Aurelia Molzer 34e35fb27e Merge pull request #307 from image-rs/public-byte-order
Provide a `tags::ByteOrder` public API
2025-11-05 03:40:13 +00:00
Aurelia Molzer bec6558aa8 Merge pull request #306 from 1c3t3a/fix-panic-on-invalid-lzw
Don't panic if LZW doesn't find an LZW end code
2025-11-04 22:22:12 +00:00
A. Molzer 6cdf3d8d22 Naming pass for byte order conversion 2025-11-04 23:17:05 +01:00
A. Molzer 0a2b3f91ab Remove unused method in EndianReader 2025-11-04 12:55:29 +01:00
A. Molzer 99b3c03ce3 Use bulk byte order conversion for tag reads 2025-11-04 12:54:53 +01:00
Aurelia Molzer bec8e495a5 Merge pull request #308 from image-rs/vectorized-predict
const-specialize predict by sample counts
2025-11-04 10:37:21 +00:00
A. Molzer de7dbad18b const-specialize predict by sample counts
The idea is to allow codegen to exploit specific known constants. It
seems that LLVM by itself will not generate split loop blocks by an
input argument that is a loop constant. Whereas we know, from PNG, that
some constants yield much better code. In this case

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

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

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

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

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

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

This seems wrong and we should instead return actual Value::Bytes.
2025-08-29 12:06:03 +00:00
Aurelia Molzer 85b5df7a62 Merge pull request #292 from image-rs/changelog-0.10.1
Changelog 0.10.2
2025-08-26 16:03:34 +00:00
Aurelia Molzer 756ea3e813 Add metadata for 0.10.2 2025-08-26 11:24:04 +02:00
Aurelia Molzer 0eead083f6 Fix missing method on Decoder 2025-08-26 11:19:35 +02:00
Aurelia Molzer 34232d8141 Merge pull request #291 from image-rs/changelog-0.10.1
Populate changelog for 0.10.1
2025-08-25 21:42:34 +00:00
Aurelia Molzer cbcf6b97d4 Bump version number 0.10.1 2025-08-25 23:21:34 +02:00
Sergey "Shnatsel" Davidoff 19c811e86d populate changelog for 0.10.1 2025-08-25 15:20:04 +01:00
Aurelia Molzer 50be15e356 Merge pull request #290 from jakubadamw/nightly-compilation-warnings
Address compilation warnings in the Nightly channel of Rust
2025-08-14 16:50:22 +00:00
Jacob Adam 718f2cb455 Address compilation warnings in the Nightly channel of Rust. 2025-08-14 17:45:21 +01:00
Aurelia Molzer 36a16db659 Merge pull request #285 from image-rs/read-into-buffer
Read into byte buffer
2025-07-25 19:16:09 +00:00
A. Molzer d70c31dcbf Add a Bug note about planar configuration 2025-07-25 21:13:41 +02:00
Aurelia Molzer b589d10a27 Merge pull request #289 from aotimme/decoder-inner
Add inner() method to decoder to provide access to raw reader
2025-07-22 20:18:56 +02:00
Alden Timme f4378eeb80 Also split test_planar_rgb_u8 test 2025-07-22 08:48:57 -07:00
Alden Timme 49015c6e93 Move into separate test 2025-07-22 08:42:39 -07:00
Alden Timme 29402875cb Add inner() method to decoder to provide access to raw reader 2025-07-21 20:47:47 -07:00
Aurelia Molzer 33453289b6 Merge pull request #287 from image-rs/icc-tag
Add ICC (color profile) to the set of known tags
2025-07-19 18:17:56 +02:00
A. Molzer ce0ca5c234 Add ICC (color profile) to the set of known tags 2025-07-19 18:12:54 +02:00
A. Molzer f671293bd8 Fix byte-crc test for big endian platforms 2025-07-19 18:08:02 +02:00
A. Molzer 48d52bed9b Return buffer layout as a struct
Prepares extensibility for the eventual case of supporting multi-planar
image layouts that are to be written into a multi-planar buffer as
indicated by the caller in some way (possibly not via read_*_bytes but a
method taking a wrapper around the output buffer instead). Through the
struct we can later add an indicator that can be used by the caller to
decide between methods of fetching the pixel data as appropriate.
2025-07-19 17:46:37 +02:00
Aurelia Molzer 7707060982 Merge pull request #286 from image-rs/clippy
Resolve all the clippy lints and add it to CI
2025-07-19 17:39:26 +02:00
A. Molzer c370ac4b92 Take row stride input in chunk bytes 2025-07-19 00:12:43 +02:00
A. Molzer b2bcbbaf23 Enable clippy for the merge workflow 2025-07-14 19:56:06 +02:00
A. Molzer d07da5a6cb Resolve clippy lints in tests 2025-07-14 19:56:06 +02:00
A. Molzer a3c81e9830 Add Directory::is_empty for completeness 2025-07-14 19:56:06 +02:00
A. Molzer 46e51cc67e Make documentation adhere clippy, more natural 2025-07-14 19:56:06 +02:00
A. Molzer f6a762b72c Upgrade usage of format! and Default 2025-07-14 19:56:06 +02:00
A. Molzer 23672b2490 Switch to div_ceil for rounding implementation 2025-07-14 19:25:12 +02:00
A. Molzer ca74c03520 Fix straightforward cast/borrow lints 2025-07-14 19:25:12 +02:00
A. Molzer 2942f9de97 Mark f16 bytecast as expected unused 2025-07-14 19:25:12 +02:00
A. Molzer 29add10ca7 Fix manual non_exhaustive pattern 2025-07-14 18:24:01 +02:00
A. Molzer 59d32a66a7 Add test coverage for to byte decoding 2025-07-14 18:23:30 +02:00
A. Molzer 5a6b2ad8f3 Complementary methods reading into borrowed buffer 2025-07-14 18:23:30 +02:00
A. Molzer c746653269 Add Decoder::read_image_bytes, borrowed output 2025-07-14 18:23:30 +02:00
Aurelia Molzer cc75d051af Merge pull request #283 from image-rs/write-directory
Write extra directories
2025-07-14 00:01:39 +02:00
A. Molzer ab4fb4e959 Test and polish encoding extra directories 2025-07-13 23:28:24 +02:00
A. Molzer 7b1683687e Split Encoder::write_directory
There are two kinds of directories: those that are image directories
directly in the TIFF file's main sequence, and directories that follow
the structure but represent auxiliary data. The method will link the
directory into the main sequence but those images have additional
requirement on their IFDs which is not clear to the caller. We make that
explicit and also provide a way to write auxiliary directories.
2025-07-13 22:59:53 +02:00
Aurelia Molzer 2e51ee4b15 Merge pull request #282 from image-rs/write-directory
Refactor encoding of directories
2025-07-13 22:28:05 +02:00
A. Molzer f6e2aeffe5 Encode values immediately
This currently avoids buffering the collection, but not buffering them
individually where we still format it into a byte vector.

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

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

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

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

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

* Also run clippy and verify msrv

* Fix string mismatch

* Have clippy only check for serious errors
2023-08-20 13:44:35 -07:00
Jonathan Behrens d6f6b8a01e Merge pull request #213 from fintelia/fix-clippy
A bunch of clippy fixes
2023-08-20 13:39:27 -07:00
Jonathan Behrens 8ec91b64f6 Cargo fmt 2023-08-20 13:33:35 -07:00
Jonathan Behrens 60f866c119 A bunch of clippy fixes 2023-08-20 13:32:08 -07:00
Jonathan Behrens daa0e3eabf Merge pull request #206 from fintelia/simplify-jpeg
Simplify and avoid allocation in jpeg decompression
2023-06-14 14:04:01 -07:00
Jonathan Behrens 24d203c7c3 Simplify and avoid allocation in jpeg decompression 2023-06-11 12:54:30 -07:00
Jonathan Behrens 473868a7bb Merge pull request #199 from pka/planar-config
Basic support for planar configuration
2023-06-10 12:46:20 -07:00
Andreas Molzer aefea7786f Merge pull request #203 from AlanRace/custom-compression
Don't panic when parsing metadata when custom compression is used
2023-03-08 09:33:03 +01:00
Alan Race d1ad9b2664 Don't panic when parsing metadata when custom compression is used 2023-03-07 09:15:24 +01:00
Andreas Molzer bd1d085189 Merge pull request #201 from image-rs/release-0.8.2
Add changelog and metadata for 0.9.0
2023-02-06 23:58:59 +01:00
Andreas Molzer fcfcecb2df Add changelog and metadata for 0.8.2 2023-02-06 23:37:12 +01:00
Andreas Molzer aa10d27a28 Merge pull request #200 from anfedotoff/master
Fix capacity overflow in JpegReader
2023-02-06 21:22:13 +01:00
fedotoff 8fa175c371 Fix capacity overflow in JpegReader 2023-02-05 17:53:37 +03:00
Pirmin Kalberer 2a15aa6b7e Support also tiled planar images 2023-01-17 11:57:54 +01:00
Pirmin Kalberer 42feeba8da Add basic support for planar configuration 2023-01-12 23:34:31 +01:00
Jonathan Behrens 0da9770916 Merge pull request #190 from kurtkuehnert/YCbCr_support
Added support for the YCbCr PhotometricInterpretation
2023-01-06 23:54:16 -08:00
Kurt Kühnert c30ea911a2 fixed ci 2023-01-05 10:00:10 +01:00
Kurt Kühnert da07c0f4f5 added round trip test
fixed ci
added colortype
2023-01-05 09:34:54 +01:00
Jonathan Behrens 289fab2fcd Merge pull request #197 from pka/fix-padding
Fix padding when image size is multiple of chunk size
2022-12-16 15:49:58 -08:00
Pirmin Kalberer 21252d9d89 Fix padding when image size is multiple of chunk size 2022-12-16 23:37:38 +01:00
Andreas Molzer ffaccf2b52 Merge pull request #196 from image-rs/release-0.8.1
Changelog and metadata for 0.8.1
2022-12-10 16:57:28 +01:00
Andreas Molzer 4dea7c4c81 Changelog and metadata for 0.8.1 2022-12-10 08:25:57 +01:00
Andreas Molzer aa42fcb1bb Merge pull request #193 from image-rs/issue-192-desync-packbits
Fix read in packbits decoder
2022-12-03 18:49:51 +01:00
Andreas Molzer 1f7b25ef55 Fix read in packbits decoder
This assumed that a `read` would succeed for the full length, thus
interpreting in a subsequent call some data bits as a header.

For instance, consider 77 bytes of literal were left and a buffer of
1024 bytes was provided. Then the `read` call might return 60 which was
ignored and assumed to be 77. Thus the next 17 bytes of literal data
were instead interpreted as a header due to setting `self.count` to 0.
2022-12-03 18:42:08 +01:00
Andreas Molzer 6c65e66440 Merge pull request #170 from AlanRace/jpeg-unwrap
Removed `unwrap` and set colour transform with Jpeg compressed tiffs
2022-11-22 16:17:40 +01:00
Alan Race a733ff13d9 Changed to using private constructor rather than From<> JpegDecoderError 2022-11-22 15:53:42 +01:00
Alan Race b1cd459330 Updated error handling 2022-11-22 15:44:10 +01:00
Alan Race 4f1c88e35f Added unsupported error for interpretations not supported by JPEG 2022-11-22 14:38:27 +01:00
Alan Race 7307e96782 Removed unwrap and set colour transform with Jpeg compressed tiffs 2022-11-22 14:31:33 +01:00
Kurt Kühnert a08604b015 added support for the YCbCr PhotometricInterpretation 2022-11-16 17:33:39 +01:00
HeroicKatora 9333d99c92 Merge pull request #187 from image-rs/release-0.8.0
Changelog for 0.8
2022-11-10 21:02:40 +01:00
Andreas Molzer 65bc24d5d9 Add deprecation removals to changelog 2022-11-10 17:49:32 +01:00
Andreas Molzer 7fc75f194a Changelog for 0.8 2022-11-10 17:28:12 +01:00
HeroicKatora 7679be19c1 Merge pull request #189 from image-rs/non-exhaustive-enumeration
Non exhaustive enumerations
2022-11-10 17:27:31 +01:00
Andreas Molzer 0e9f8e3b5b Mark more tag enums as non_exhaustive 2022-11-10 12:45:04 +01:00
Andreas Molzer c900129f2c Change to non_exhaustive attributes 2022-11-10 12:45:04 +01:00
HeroicKatora 61234c0005 Merge pull request #188 from fintelia/decoder-deprecated
Remove deprecated methods on Decoder
2022-11-05 20:22:38 +01:00
Jonathan Behrens 1416af1018 Remove deprecated methods on Decoder 2022-11-05 10:15:31 -07:00
HeroicKatora 87d652ae5e Merge pull request #186 from micahsnyder/bump-jpeg-decoder-dependency
Bump jpeg-decoder version pin to 0.3.0
2022-11-03 08:28:43 +01:00
Micah Snyder b5a78fbec4 CI: Correct the MSRV from 1.48 to 1.61 2022-11-02 15:00:29 -07:00
Micah Snyder 30640d50ff Bump jpeg-decoder version pin to 0.3.0
The jpeg-decoder project has a new version with a number of improvements
plus a fix for a possible panic.
Semantic versioning considers 0.2.4 to be incompatible with 0.3.X because
of the leading zero, so this change bumps the pin to 0.3.0.
2022-11-02 14:25:18 -07:00
HeroicKatora b87770225f Merge pull request #185 from image-rs/release-0.7.4
Changelog for 0.7.4
2022-10-29 12:44:25 +02:00
Andreas Molzer d38b65eeb2 Changelog for 0.7.4 2022-10-28 22:58:18 +02:00
HeroicKatora ee27553cf6 Merge pull request #180 from luksab/patch-1
Update link to the TIFF specification
2022-10-14 20:14:07 +02:00
Lukas Sabatschus 5a809a728e Update link to the TIFF specification
The old link is unavailable since mid-2017, so we link to archive.org
2022-10-14 13:53:29 +02:00
HeroicKatora b0b99fc536 Merge pull request #173 from micahsnyder/issue-168-handle-jpeg-decoder-errors
Handle possible JPEG decoder errors
2022-09-06 21:17:04 +02:00
Jonathan Behrens 3bce92781c Merge pull request #176 from fintelia/zero-size
Return an error for zero-size images
2022-09-05 13:10:30 -07:00
Jonathan Behrens 907bbee576 cargo fmt 2022-09-05 12:54:21 -07:00
Jonathan Behrens fc0dafcde2 Change error kind 2022-09-05 12:45:22 -07:00
Jonathan Behrens 945a02d1e1 Format 2022-09-05 12:44:42 -07:00
Jonathan Behrens ef3294d1fe Return an error for zero-size images 2022-09-05 12:44:42 -07:00
HeroicKatora 0311402999 Merge pull request #177 from fintelia/fix-warnings
Fix some warnings
2022-09-05 12:45:04 +02:00
Jonathan Behrens 361578df00 Fix some warnings 2022-09-04 12:35:15 -07:00
Jonathan Behrens bd23428f52 Validate that images contain the proper number of strips (#174) 2022-09-04 12:00:34 -07:00
Micah Snyder d32adf8ef4 Handle possible JPEG decoder errors
The JPEG decoder may fail and panic the TIFF decoder. This commit
converts the JPEG `Format(String)` error to a TIFF `Format(String)`
error.

Fixes #168
2022-09-02 15:02:23 -07:00
Jonathan Behrens 2ec1ab7489 Add changelog and metadata for 0.7.3 (#167) 2022-07-24 10:31:37 -07:00
Jonathan Behrens 80e7c46151 Merge pull request #163 from AlanRace/access-tile
Add ability to select image directory and tile using index
2022-07-23 15:44:45 -07:00
Jonathan Behrens 5003803c97 cargo fmt 2022-07-23 15:27:02 -07:00
Jonathan Behrens 7c4a6fae08 Update src/decoder/mod.rs 2022-07-18 19:36:35 -07:00
Jonathan Behrens 723ef6431a Update src/decoder/mod.rs 2022-07-18 19:36:28 -07:00
Alan Race 802949f6e1 Removed ChunkInfo and added additional functions on decoder 2022-07-18 17:15:44 +02:00
Alan Race 5cadbffebf Changed to use u32 for chunk_index 2022-07-11 08:56:35 +02:00
Alan Race e1f4161e0b Removed unused functions and make InvalidChunkIndex a UsageError 2022-07-08 11:08:30 +02:00
Alan Race 8c0c13f022 Added read_chunk_at and read_chunk_to_buffer functions 2022-07-07 15:25:01 +02:00
Alan Race d5e3f4aa8a Merge remote-tracking branch 'image-tiff/master' into access-tile 2022-07-07 15:06:56 +02:00
Jonathan Behrens 3c42a6e1f4 Merge pull request #165 from fintelia/improve-predictor
Floating point predictor
2022-06-27 07:36:45 -07:00
Jonathan Behrens 5e7702a874 Suggested edits 2022-06-26 20:45:21 -07:00
Jonathan Behrens 8e5c5b44c5 Support floating point predictor (predictor 3)
Based on work by @TomasKralCZ in #135
2022-06-22 20:14:25 -07:00
Jonathan Behrens 8a6e93f66e Merge fix_endianness and rev_hpredict methods 2022-06-22 20:13:55 -07:00
Alan Race 6e398ce7e3 Remove ChunkInfo from mod.rs that was added through merge 2022-05-09 09:58:51 +02:00
Alan Race 8bd02ac389 Merge remote-tracking branch 'origin/access-tile' into access-tile 2022-05-09 09:57:24 +02:00
Alan Race 0447d81b07 Updated to use new unified chunk handling 2022-05-09 09:56:46 +02:00
Alan Race 54664d553d Added comments for read_tile, read_strip and read_*_at 2022-05-09 09:17:39 +02:00
Alan Race 6bec36ab20 Reverted read_strip and read_tile to previous call signature
Added comments to ChunkInfo
2022-05-09 09:17:39 +02:00
Alan Race e57ca1a926 Added functionality for loading selected strip 2022-05-09 09:17:27 +02:00
Alan Race ca4da33dd7 Mirrored return value of read_tile to read_strip 2022-05-09 09:16:35 +02:00
Alan Race 6d4c4a49a1 read_tile now returns ChunkInfo 2022-05-09 09:16:13 +02:00
Alan Race d8520bde1f Now using the list of offsets to know how many IFDs we have seen 2022-05-09 09:14:56 +02:00
Alan Race 56c20c2c20 Added ability to select which image (IFD) to load 2022-05-09 09:14:56 +02:00
Alan Race 6efa89c249 Added ability to select which tile to load (based on index)
Also reduced repeated code when calculating padding etc.
2022-05-09 09:14:04 +02:00
HeroicKatora 68d677d1b3 Merge pull request #164 from fintelia/unify-strip-tile
Unify strip and tile decoding
2022-05-05 18:35:53 +02:00
Alan Race 72ca636625 Added comments for read_tile, read_strip and read_*_at 2022-05-03 09:27:23 +02:00
Alan Race 1f501b5acb Reverted read_strip and read_tile to previous call signature
Added comments to ChunkInfo
2022-05-03 09:21:23 +02:00
Jonathan Behrens 697d2877a1 Cargo fmt 2022-05-02 20:37:37 -07:00
Jonathan Behrens 805a1e5cef Rename chunk_byte_range method 2022-05-02 19:47:41 -07:00
Jonathan Behrens dc8311fb99 Merge read_tiled_image and read_stripped_image 2022-05-02 19:46:28 -07:00
Jonathan Behrens f25530adec Unify expand_strip and expand_tile 2022-05-02 19:37:00 -07:00
Alan Race dd8c9e4f2b Added functionality for loading selected strip 2022-05-02 15:59:48 +02:00
Alan Race 8f34748d01 Mirrored return value of read_tile to read_strip 2022-05-02 15:39:14 +02:00
Alan Race 4ce3c2f41f read_tile now returns ChunkInfo 2022-05-02 15:35:48 +02:00
Alan Race f809d7d1bb Fixed compilation issue after merging 2022-05-02 08:47:05 +02:00
Alan Race 902f945375 Merge branch 'tiled-jpeg' into access-tile 2022-05-02 08:41:29 +02:00
Jonathan Behrens d437c2708b Merge pull request #162 from AlanRace/tiled-jpeg
Added ability to decompress tiled jpeg files
2022-05-01 22:31:14 -07:00
Alan Race a133056287 Removed passing of PhotometricInterpretation to create_reader
as this is now handled for JPEG globally
2022-04-30 14:55:52 +02:00
Alan Race ce00bb5a28 Removed old code 2022-04-30 14:35:33 +02:00
Alan Race 623a3d957d Avoid cloning jpeg_tables and instead pass Arc<Vec<u8>> 2022-04-30 14:33:12 +02:00
Alan Race 9e6f9771d3 Updated create_reader to only require &mut SmartReader and not &mut Self 2022-04-28 08:54:32 +02:00
Alan Race b196d19238 Now using the list of offsets to know how many IFDs we have seen 2022-04-27 17:50:34 +02:00
Alan Race 436cfaf69f Added ability to select which image (IFD) to load 2022-04-27 17:38:44 +02:00
Alan Race 8fcf49deac Added ability to select which tile to load (based on index)
Also reduced repeated code when calculating padding etc.
2022-04-27 16:46:48 +02:00
Alan Race 2cf33d634d Clean up read_image 2022-04-27 15:00:22 +02:00
Alan Race eeb79ee07a Reduce number of clones when processing jpeg data 2022-04-27 14:46:52 +02:00
Alan Race 592405268e Added ability to decompress tiled jpeg files 2022-04-27 14:05:41 +02:00
HeroicKatora 9f2508e37e Merge pull request #160 from fintelia/stream-pack-bits
Make PackBitsReader incrementally decompress output
2022-04-16 22:22:28 +02:00
Jonathan Behrens 4f46bacfda Fixes to PackBitsReader 2022-04-16 16:10:09 -04:00
Jonathan Behrens b286457337 Remove dbg! print 2022-04-16 15:52:09 -04:00
Jonathan Behrens 7881291032 Make PackBitsReader incrementally decompress output 2022-04-15 12:25:24 -04:00
HeroicKatora 167088b33d Merge pull request #159 from fintelia/stream-lzw
Make LZWReader incrementally decompress input
2022-04-15 17:35:43 +02:00
Jonathan Behrens ff33343372 Fix formatting 2022-04-15 11:28:56 -04:00
Jonathan Behrens 4097bf37b1 Rename test oom_2 -> fuzzer_testcase4 2022-04-15 10:04:05 -04:00
Jonathan Behrens c910ef8534 Remove redundant limit on LZWReader output length 2022-04-15 10:02:33 -04:00
Jonathan Behrens 1063d6dd27 Have LZWReader internally use a BufReader<Take<R>> 2022-04-15 09:49:18 -04:00
Jonathan Behrens e4eac36c1c Remove now unneeded limit check 2022-04-14 21:40:19 -04:00
Jonathan Behrens 9394f6d043 Make LZWReader incrementally decompress input 2022-04-14 21:00:36 -04:00
HeroicKatora 3399f93620 Merge pull request #158 from image-rs/release-0.7.2
Add changelog and metadata for 0.7.2
2022-04-11 18:48:10 +02:00
Andreas Molzer ef7592cf82 Add changelog and metadata for 0.7.2 2022-04-11 18:22:39 +02:00
Jonathan Behrens 7cdfd154ba Merge pull request #155 from Aaron1011/update/jpeg-decoder
Update jpeg-decoder to 0.2.1
2022-02-09 00:44:56 -05:00
Aaron Hill fa0d6eb99f Update jpeg-decoder to 0.2.1 2022-02-08 19:55:40 -05:00
Jonathan Behrens a780da0b34 Merge pull request #154 from fintelia/decoder-refactor
Decoder refactor
2022-01-15 17:04:11 -05:00
Jonathan Behrens ca56836a79 Add back fuzzer generated testcases 2022-01-15 15:11:54 -05:00
Jonathan Behrens 2c7c8891f4 Remove use half-open range pattern which is unstable on rustc 1.48 2022-01-12 20:46:17 -05:00
Jonathan Behrens 01df985ef8 Fix formatting 2022-01-12 20:13:23 -05:00
Jonathan Behrens 593479e5e3 Avoid reading tags in read_jpeg 2022-01-12 19:56:08 -05:00
Jonathan Behrens bc2055243a Add consistency check between StripOffsets and StripByteCounts 2022-01-12 19:56:08 -05:00
Jonathan Behrens 7a7d4f0305 Avoid unused warning 2022-01-12 19:56:08 -05:00
Jonathan Behrens 720e6586b9 Make read_ifd and read_entry stateless 2022-01-12 19:56:08 -05:00
Jonathan Behrens 47080293db Move image loading to Image::from_reader 2022-01-12 19:56:02 -05:00
Jonathan Behrens b385a6e752 Move some decoder fields into a new Image struct 2022-01-12 19:50:06 -05:00
Jonathan Behrens 6ba5553d20 Compute tile attributes on demand 2022-01-12 19:50:06 -05:00
Jonathan Behrens a36cd6bd90 chunk_{offsets,bytes} to replace {tile,strip}_{offsets,bytes} 2022-01-12 19:50:06 -05:00
Jonathan Behrens 63e6ef8bba Add current_chunk to replace strip_index and current_tile 2022-01-12 19:50:06 -05:00
Jonathan Behrens 2d10cff97e Reading fields shouldn't need the entire Decoder 2022-01-12 19:50:06 -05:00
Jonathan Behrens 23fe194ad9 Remove read_ifd_offset_nonrepeating 2022-01-12 19:50:06 -05:00
Jonathan Behrens 2d572c0370 Remove Decoder::byte_order 2022-01-12 19:50:06 -05:00
Jonathan Behrens 4c942ca690 Read JPEGTables ahead of time 2022-01-12 19:50:06 -05:00
Jonathan Behrens a19d35233a Read predictor ahead of time 2022-01-12 19:50:06 -05:00
Jonathan Behrens dc95ce10ac Remove some extremely brittle tests 2022-01-12 19:49:55 -05:00
Jonathan Behrens 31da6737b9 More refactoring 2022-01-12 19:45:41 -05:00
Jonathan Behrens e9aa39d895 Initialize Decoder in new instead of init 2022-01-12 19:45:41 -05:00
HeroicKatora 77f15b5c0a Merge pull request #153 from mindQ-GmbH-Co-KG/compression-release
fixed deflate and packbits
2022-01-11 18:53:12 +01:00
Pretorer 6f89033fd3 moved namespace qualifier 2022-01-11 15:51:34 +01:00
Pretorer 9df18a1674 fixed deflate and packbits 2022-01-11 15:43:00 +01:00
HeroicKatora 37cfcf82aa Merge pull request #151 from mindQ-GmbH-Co-KG/compression-release
Add TIFF compression during encoding and corresponding tests
2022-01-10 22:30:19 +01:00
Pretorer 1d8cbbc6a1 add Packbits compression 2022-01-10 11:57:59 +01:00
Pretorer 9f1e3d7a89 moved compression into TiffWriter 2022-01-10 11:55:08 +01:00
Jonathan Behrens fb4c119b88 Merge pull request #152 from fintelia/refactor-decode
Refactor strip/tile decoding
2022-01-05 21:48:14 -05:00
Jonathan Behrens 3242d328d1 Reformat 2021-12-22 12:06:12 -05:00
Jonathan Behrens d6c42dd60a Some refactoring of strip/tile decoding 2021-12-22 12:05:38 -05:00
Christopher Gundler de95ac35a2 Remove "CompressionError" 2021-12-22 12:29:39 +01:00
Christopher Gundler 30bf508983 Avoid allocations 2021-12-22 11:10:13 +01:00
Christopher Gundler bc7be68d0f Simplify tests 2021-12-22 10:48:52 +01:00
Christopher Gundler 99b2aa9b3a Add TIFF compression and corresponding tests 2021-12-21 16:47:11 +01:00
HeroicKatora d7d63e648b Merge pull request #149 from image-rs/release-0.7.1
Release 0.7.1
2021-09-22 17:47:02 +02:00
Andreas Molzer b8459f413a Add fuzzing work and bugs to release notes 2021-09-22 01:20:00 +02:00
Andreas Molzer 26e760e303 Add notes on MSRV 2021-09-22 01:13:10 +02:00
Andreas Molzer a4878e9658 Release notes for 0.7.1 2021-09-22 01:05:25 +02:00
HeroicKatora 73285481cd Merge pull request #141 from 5225225/oom
Fix out of memory issues / misc other decoding bugs.
2021-09-18 15:51:33 +02:00
5225225 b065539c07 Add fuzzing instructions to readme, bump CI version to 1.48 2021-09-18 14:42:53 +01:00
5225225 1c5800daa0 Infinitely try to read off images rather than only trying 128 2021-09-18 12:14:17 +01:00
5225225 885d762def Implement cycle checking in IFD offsets 2021-09-08 18:08:10 +01:00
5225225 2c9e02d347 Merge remote-tracking branch 'origin/master' into oom 2021-09-06 17:53:16 +01:00
Jonathan Behrens b318481c55 Merge pull request #143 from Christopher22/reset_tag_values
Do not carry tag values from previous images
2021-08-15 11:43:44 -04:00
5225225 1774b362d3 x 2021-07-15 18:08:40 +01:00
Jonathan Behrens f2fec92283 Merge pull request #146 from fintelia/encode-signed-int
Support encoding signed integer formats
2021-06-20 18:39:08 -04:00
Jonathan Behrens be0619d6a1 Support encoding signed integer formats 2021-06-20 14:33:13 -04:00
Tomáš Král e92cf219a9 Fix the mandrill.tiff regression (#145)
* fix mandrill.tiff regression from upstream

* use contains_key(), added StripTileTagConflict error

* updated test_div_zero, added test_no_rows_per_strip
2021-06-17 09:22:07 -04:00
Christopher Gundler 3e5f977460 Do not carry tag values from previous images 2021-06-14 20:21:51 +02:00
5225225 f0666aebbf Fix overflow when calculating strip height for multiple images 2021-05-29 22:52:42 +01:00
5225225 65a1fbe8ae Test for short JPEG tag 2021-05-29 22:12:35 +01:00
5225225 03b6ed413b Fix out of bounds subtraction with invalid JPEG table 2021-05-29 22:12:35 +01:00
5225225 5b7814c994 Check for OOM when creating LZWReader 2021-05-29 22:12:35 +01:00
5225225 a15d619471 Fix OOM in Jpeg decoding buffer allocation 2021-05-29 22:12:35 +01:00
HeroicKatora 458a713827 Merge pull request #140 from 5225225/fuzzer
Add fuzzer with 1MB size limits decoding image
2021-05-29 23:12:04 +02:00
5225225 2f6ad07f28 Try to read multiple images in fuzzer 2021-05-29 22:00:36 +01:00
HeroicKatora 153ed48ee7 Merge pull request #136 from TomasKralCZ/padding_skip
remove padding_buffer
2021-05-29 15:13:20 +02:00
5225225 420667955a Add fuzzer with 1MB size limits decoding image 2021-05-29 13:18:35 +01:00
HeroicKatora 3bf19822a2 Merge pull request #139 from 5225225/value-bytes-overflow
Check for various arithmetic overflows
2021-05-29 13:48:03 +02:00
5225225 2bb37a4ecd Do checked multiplication for samples per strip 2021-05-29 12:37:15 +01:00
5225225 b41ac105ab Use checked multiplication when calculating buffer size 2021-05-29 12:36:59 +01:00
5225225 1e10b9803b Add check for an invalid strip count 2021-05-29 12:36:50 +01:00
5225225 2f4e491874 Check for where tag count * tag size overflows 2021-05-28 20:32:30 +01:00
HeroicKatora 285b881e27 Merge pull request #138 from 5225225/divide_by_zero
Fix division by zero issue with malformed file
2021-05-28 19:59:37 +02:00
5225225 bdfe2f55fd Check error returned from parsing invalid image 2021-05-28 15:46:55 +01:00
5225225 eda5a8ac30 Fix division by zero issue with malformed file 2021-05-28 14:44:50 +01:00
HeroicKatora bd1e37d78b Merge pull request #130 from phil-opp/release
Prepare 0.7.0 release
2021-05-25 21:03:44 +02:00
Tomáš Král ad0f75222a remove padding_buffer 2021-05-21 18:06:05 +02:00
144 changed files with 10337 additions and 2166 deletions

No files matched your search

+73 -19
View File
@@ -1,40 +1,94 @@
name: Rust CI name: Rust CI
on: on:
push: push:
branches: [ master ] branches: [ main ]
pull_request: pull_request:
branches: [ master ] branches: [ main ]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
rust: ["1.34.2", stable, beta, nightly] rust: ["1.88.0", stable, beta, nightly]
command: [build, test]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- run: rustup default ${{ matrix.rust }}
- uses: dtolnay/rust-toolchain@nightly
if: ${{ matrix.rust == '1.88.0' }}
- name: Generate Cargo.lock with minimal-version dependencies
if: ${{ matrix.rust == '1.88.0' }}
run: cargo -Zminimal-versions generate-lockfile
- uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ matrix.rust }}
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: build - name: build
run: > run: cargo build -v
cargo build --verbose --no-default-features --features "$FEATURES"
- name: test - name: test
run: > if: ${{ matrix.rust != '1.88.0' }}
cargo test --tests --benches --no-default-features --features "$FEATURES" run: cargo test -v && cargo doc -v
if: ${{ matrix.rust != '1.34.2' }}
env: features:
FEATURES: ${{ matrix.features }} runs-on: ubuntu-latest
strategy:
matrix:
feature: ["", deflate, jpeg, lzw, zstd, default, "default,zstd"]
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cargo check
run: cargo check --features "${{ matrix.feature }}" --no-default-features
- name: Cargo clippy
if: ${{ matrix.feature != '' }}
run: cargo clippy --no-deps --all-features --all-targets -- -D warnings
other_archs:
# github actions does not support 32-bit or big endian systems directly, but
# it does support QEMU. so we install qemu, then build and run the tests on
# emulated systems. NOTE: you can also use this approach to test locally.
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch: [powerpc-unknown-linux-gnu, i686-unknown-linux-gnu]
steps:
- uses: actions/checkout@v4
- name: Install or use cached cross-rs/cross
uses: baptiste0928/cargo-install@v2
with:
crate: cross
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
key: ${{ matrix.arch }}
- name: Start Docker (required for cross-rs)
run: sudo systemctl start docker
- name: Cross-Run Tests using QEMU
run: cross test --target ${{ matrix.arch }}
rustfmt: rustfmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: stable
override: true
components: rustfmt components: rustfmt
- name: Run rustfmt check - name: Run rustfmt check
uses: actions-rs/cargo@v1 run: cargo fmt -- --check
with:
command: fmt
args: -- --check
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- run: cargo clippy --all-features # TODO: enable "-- -D warnings" once all outstanding clippy lints are fixed
+302
View File
@@ -1,3 +1,305 @@
# 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.
# 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:
- Group 4 `fax` is now recognized as a compression type and decoded.
- A crate feature `fax` controls related dependencies, on by default.
Changes:
- The presence of PhotometricInterpretation::WhiteIsZero now returns an error
if the color type can not be inverted by the library where they were
previously silently ignored. Please report any missing color types.
Fixes:
- The PhotometricInterpretation::WhiteIsZero is now also applied to 1-bit
images, as well as 2-bit and 4-bit sample types where previously the presence
of this tag was ignored.
# Version 0.10.2
Republished 0.10.1, fixing an accidentally removed method (`Decoder::find_tag_unsigned_vec`).
# Version 0.10.1
New features:
- Added a dedicated `Directory` type (#277)
- Expose methods related to `Directory` publicly (#278)
- Implement writing directories (#281, #283)
- Add ICC (color profile) to the set of known tags (#287)
- Added `Decoder.inner()` to access the underlying reader (#289)
- Added methods for reading into a pre-allocated byte buffer (#285)
Changes:
- No longer perform color conversion when decoding embedded JPEG. This fixes decoding for JPEG images with alpha channel. (#274)
- More lenient LZW decoding to match libtiff (#231)
Fixes:
- Fixed seeking multiple times in a row (#279)
# Version 0.10.0
New features:
- Added `ColorType::Multiband` and `ColorType::CMYKA`.
- Added `Value::Byte` and `Value::SignedByte` to represent `u8` and `i8` tags.
- Encoding can now use horizontal predictor.
- The `Decoder::tag_iter` method allows iterating over all tags in the image.
- Half precision floating point values are now supported during decoding.
- ZStandard compression has been added as an additional compression method
during decoding, but requires the `zstd` feature flag.
Changes:
- Switched `u16` and `i16` values to be intepreted as `Value::Short` and
`Value::SignedShort` respectively.
- Changed JPEG compression to use the `zune-jpeg` crate instead of
`jpeg-decoder`.
- The `deflate`, `jpeg`, and `lzw` compression methods are now controlled by
(enabled by default) feature flags.
Fixes:
- Fixed a bug in 1 bit-per-pixel decoding.
# Version 0.9.1
New features:
- Basic support for planar configuration.
- Allow arbitrary number of samples as long as all have the same bit depth.
Fixes:
- Don't panic when parsing metadata when custom compression is used.
# Version 0.9.0
New features:
* Added support for photometric interpretation `YCbCr` and added related
`ColorType`.
Fixes:
* Decoding tiled images calculates padding correctly when image width or height
is a multiple of tile size. It could previously corrupt the last tile per row
by skipping over data.
# Version 0.8.1
Changes:
* The jpeg decoder gained to ability to utilize the Photometric Interpretation
directly instead of relying on a custom APP segment.
Fixes:
* A spurious error within the PackBits decoder lead to the incorrect results
(wrong bits or errors), based on the maximum size of reads from the
underlying reader.
* Removed a panic path in jpeg decoding, when a feature such as photometric
interpretation is not supported. An error is returned instead.
# Version 0.8.0
Changes:
* The minimum supported rust version is now indicated in `Cargo.toml`.
* The enums `TiffFormatError` and `TiffUnsupportedError` are now
marked with the `#[non_exhaustive]` attribute.
* Additionally, tag related enums `Value`, `Tags`, `Type`, `CompressionMethod`,
`PhotometricInterpretation`, `PlanarConfiguration`, `Predictor`,
`ResolutionUnit`, `SampleFormat` are also changed.
Removals:
* Removed deprecated methods of `Decoder`: `init`, `read_jpeg`,
`read_strip_to_buffer`, `read_strip`, `read_tile`. The implicit chunk (row or
tile) index order could not be easily tracked by the caller. New separate
utility interfaces may be introduced at a later point, for now callers are
obligated to choose their own.
Fixes:
* Update to `jpeg_decoder = 0.3`.
# Version 0.7.4
New features:
* Creating an encoder for invalid, zero-sized images is now rejected.
Fixes:
* Fix panic, in a case where decoding jpeg encoded images did not expect the
jpeg decoder to return an error.
* Fix panic by validating `rows_per_strip` better, fixing a division-by-zero.
# Version 0.7.3
New features:
* Allow decoder to access specific tiles by index.
* Add support for floating point predictor.
* Tiled jpeg file support.
Changes:
* Various refactoring and performance improvements.
# Version 0.7.2
New features:
* Encoding with `ImageEncoder` now takes an optional compressor argument,
allowing compressed encoding. See the methods
`TiffEncoder::{new_image,write_image}_with_compression`.
* `jpeg_decoder` has been upgraded, now supports lossless JPEG.
Changes:
* Decoding now more consistently reads and interprets the initial IFD, instead
of performing _some_ interpretation lazily. (This change prepares fully lazy
and backwards seeking.)
# Version 0.7.1
New features:
* Encoding signed integer formats is now supported.
* Extensive fuzzing with `cargo fuzz`.
Changes:
* Tile decoding should be a little faster, requires one less intermediate buffer.
* Images whose IFDs form a cycle due to their offsets will now raise an error
when the cycle would be entered (jumping back should still be supported).
Fixes:
* Fixed a regression that caused conflict between strips and tile images,
causing errors in decoding some images.
* Use checked integer arithmetic in limit calculations, fixes overflows.
* IFD Tags are now always cleared between images.
* Found by fuzzing: Several memory limit overflows; JPEG now correctly
validates offsets and a minimum size of its table; Check upper limit of strip
byte size correctly;
Notes:
Our CI has warned that this version no longer builds on `1.34.2` out of the
box. We're still committed to the MSRV on this major version yet one
dependency—`flate2`—has already bumped it in a SemVer compatible version of its
own. This is out-of-our-control (cargo's dependency resolution does not allow
us to address this in a reasonable manner).
This can be address this by pinning the version of `flate2` to `1.0.21` in your
own files. However, you should understand that this puts you in considerable
maintenance debt as you will no longer receive any updates for this dependency
and any package that _requires_ a new version of the `1.0` series would be
incompatible with this requirement (cargo might yell at you very loudly).
# Version 0.7.0 # Version 0.7.0
New features: New features:
+46 -16
View File
@@ -1,30 +1,60 @@
[package] [package]
name = "tiff" name = "tiff"
version = "0.7.0" version = "0.11.3"
authors = [ edition = "2021"
"ccgn", resolver = "2"
"bvssvni <bvssvni@gmail.com>",
"nwin", # note: when changed, also update test runner in `.github/workflows/rust.yml`
"TyOverby <ty@pre-alpha.com>", rust-version = "1.88.0"
"HeroicKatora",
"Calum",
"CensoredUsername <cens.username@gmail.com>",
"Robzz",
"birktj"]
license = "MIT" license = "MIT"
description = "TIFF decoding and encoding library in pure Rust" description = "TIFF decoding and encoding library in pure Rust"
authors = ["The image-rs Developers"]
repository = "https://github.com/image-rs/image-tiff" repository = "https://github.com/image-rs/image-tiff"
exclude = ["tests/images/*", "tests/fuzz_images/*"] categories = ["multimedia::images", "multimedia::encoding"]
edition = "2018"
exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
[dependencies] [dependencies]
weezl = "0.1.0" half = { version = "2.4.1" }
jpeg = { package = "jpeg-decoder", version = "0.1.17", default-features = false } quick-error = "2.0.1"
flate2 = "1.0.20"
# Rename to avoid confusion with the feature name.
fax34 = { package = "fax", version = "0.2.6", optional = true }
flate2 = { version = "1.0.20", optional = true }
weezl = { version = "0.1.10", optional = true }
zstd = { version = "0.13", optional = true }
zune-jpeg = { version = "0.5.1", optional = true }
image-webp = {version = "0.2.4", optional = true }
[dev-dependencies] [dev-dependencies]
criterion = "0.3.1" criterion = "0.3.1"
# We may already depend on this via flate2
crc32fast = "1.5"
libtest-mimic = "0.8.1"
walkdir = "2.5"
[features]
default = ["deflate", "fax", "jpeg", "lzw"]
# Compression algorithms
deflate = ["dep:flate2"]
fax = ["dep:fax34"]
jpeg = ["dep:zune-jpeg"]
lzw = ["dep:weezl"]
webp = ["dep:image-webp"]
zstd = ["dep:zstd"]
[[bench]] [[bench]]
name = "lzw" name = "lzw"
harness = false harness = false
[[bench]]
name = "encode"
harness = false
[[test]]
name = "decode_libtiffpic"
path = "tests/decode_libtiffpic.rs"
harness = false
+29 -17
View File
@@ -1,5 +1,5 @@
# image-tiff # image-tiff
[![Build Status](https://github.com/image-rs/image-tiff/workflows/Rust%20CI/badge.svg)](https://github.com/image-rs/image-tiff/actions) [![Build Status](https://github.com/image-rs/image-tiff/actions/workflows/rust.yml/badge.svg?branch=main)](https://github.com/image-rs/image-tiff/actions)
[![Documentation](https://docs.rs/tiff/badge.svg)](https://docs.rs/tiff) [![Documentation](https://docs.rs/tiff/badge.svg)](https://docs.rs/tiff)
[![Further crate info](https://img.shields.io/crates/v/tiff.svg)](https://crates.io/crates/tiff) [![Further crate info](https://img.shields.io/crates/v/tiff.svg)](https://crates.io/crates/tiff)
@@ -16,25 +16,29 @@ TIFF decoding and encoding library in pure Rust
### Formats ### Formats
This table lists photometric interpretations and sample formats which are supported for encoding and decoding. The entries are `ColorType` variants for which sample bit depths are supported. Only samples where all bit depths are equal are currently supported. For example, `RGB(8)` means that the bit depth [8, 8, 8] is supported and will be interpreted as an 8 bit per channel RGB color type. This table lists photometric interpretations and sample formats which are supported for encoding and decoding. The entries are `ColorType` variants for which sample bit depths are supported. Only samples where all bit depths are equal are currently supported. For example, `RGB(8)` means that the bit depth [8, 8, 8] is supported and will be interpreted as an 8 bit per channel RGB color type.
| `PhotometricInterpretation` | UINT Format | IEEEFP Format | | `PhotometricInterpretation` | UINT Format | IEEEFP Format |
|-|-|-| | --------------------------- | --------------------------------------- | ------------------------- |
| `WhiteIsZero` | Gray(8\|16\|32\|64) | Gray(32\|64) | | `WhiteIsZero` | Gray(8\|16\|32\|64) | Gray(32\|64) |
| `BlackIsZero` | Gray(8\|16\|32\|64) | Gray(32\|64) | | `BlackIsZero` | Gray(8\|16\|32\|64) | Gray(32\|64) |
| `RGB` | RGB(8\|16\|32\|64), RGBA(8\|16\|32\|64) | RGB(32\|64), RGBA(32\|64) | | `RGB` | RGB(8\|16\|32\|64), RGBA(8\|16\|32\|64) | RGB(32\|64), RGBA(32\|64) |
| `RGBPalette` | | | | `RGBPalette` | | |
| `Mask` | | | | `Mask` | | |
| `CMYK` | CMYK(8\|16\|32\|64) | CMYK(32\|64) | | `CMYK` | CMYK(8\|16\|32\|64) | CMYK(32\|64) |
| `YCbCr` | | | | `CMYKA` | CMYKA(8) | |
| `CIELab` | | | | `YCbCr` | | |
| `CIELab` | | |
### Compressions ### Compressions
| | Decoding | Encoding | | | Decoding | Encoding |
|-|-|-| | -------- | -------- | -------- |
| None | ✓ | ✓ | | None | ✓ | ✓ |
| LZW | ✓ | | | LZW | ✓ | ✓ |
| Deflate | ✓ | | | Deflate | ✓ | ✓ |
| PackBits | ✓ | | | PackBits | ✓ | ✓ |
| Fax4 | ✓ | not yet |
| JPEG | ✓ | not yet |
| ZSTD | ✓ | not yet |
## Not yet supported ## Not yet supported
@@ -44,3 +48,11 @@ Formats and interpretations not listed above or with empty entries are unsupport
- Baseline tags - Baseline tags
- `ExtraSamples` - `ExtraSamples`
- Extension tags - Extension tags
## Fuzzing
This crate uses [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) in order to test the image parser.
After installing it with `cargo install cargo-fuzz` on a nightly rustc, the
fuzzing harness can be run with recommended settings using
`cargo fuzz run decode_image -snone -- -timeout=5`.
+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))
});
}
+5 -6
View File
@@ -8,13 +8,12 @@ use tiff::decoder::Decoder;
fn read_image(image: &[u8]) { fn read_image(image: &[u8]) {
let image = std::io::Cursor::new(image); let image = std::io::Cursor::new(image);
let decoder = Decoder::new(black_box(image)); let decoder = Decoder::open(black_box(image));
let mut reader = decoder.unwrap();
while { let mut reader = decoder.unwrap();
while reader.more_images() {
reader.read_image().unwrap(); reader.read_image().unwrap();
reader.more_images() }
} {}
} }
fn main() { fn main() {
@@ -33,7 +32,7 @@ fn main() {
def.data, def.data,
|b, input| b.iter(|| read_image(input)), |b, input| b.iter(|| read_image(input)),
); );
}; }
let mut c = Criterion::default().configure_from_args(); let mut c = Criterion::default().configure_from_args();
let mut group = c.benchmark_group("tiff-lzw"); let mut group = c.benchmark_group("tiff-lzw");
+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(),
);
}
}
+3
View File
@@ -0,0 +1,3 @@
target
corpus
artifacts
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "tiff-fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
edition = "2018"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
[dependencies.tiff]
path = ".."
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[[bin]]
name = "decode_image"
path = "fuzz_targets/decode_image.rs"
test = false
doc = false
+31
View File
@@ -0,0 +1,31 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let mut decoder = if let Ok(d) = tiff::decoder::Decoder::new(std::io::Cursor::new(data)) {
d
} else {
return;
};
let mut limits = tiff::decoder::Limits::default();
limits.decoding_buffer_size = 1_000_000;
limits.ifd_value_size = 1_000_000;
limits.intermediate_buffer_size = 1_000_000;
decoder = decoder.with_limits(limits);
loop {
if let Err(_) = decoder.read_image() {
break;
}
if !decoder.more_images() {
break;
}
if let Err(_) = decoder.next_image() {
break;
}
}
});
+6
View File
@@ -10,8 +10,13 @@
//! the unsafe code guidelines). //! the unsafe code guidelines).
//! //!
//! TODO: Would like to use std-lib here. //! TODO: Would like to use std-lib here.
// Until we implement predictors for f16, we don't need f16_as_ne_bytes. (Due to the macro we do
// not apply this directly to the functions). And rust-version is not new enough for `expect`.
#![allow(dead_code)]
use std::{mem, slice}; use std::{mem, slice};
use half::f16;
macro_rules! integral_slice_as_bytes{($int:ty, $const:ident $(,$mut:ident)*) => { macro_rules! integral_slice_as_bytes{($int:ty, $const:ident $(,$mut:ident)*) => {
pub(crate) fn $const(slice: &[$int]) -> &[u8] { pub(crate) fn $const(slice: &[$int]) -> &[u8] {
assert!(mem::align_of::<$int>() <= mem::size_of::<$int>()); assert!(mem::align_of::<$int>() <= mem::size_of::<$int>());
@@ -31,4 +36,5 @@ integral_slice_as_bytes!(i32, i32_as_ne_bytes, i32_as_ne_mut_bytes);
integral_slice_as_bytes!(u64, u64_as_ne_bytes, u64_as_ne_mut_bytes); integral_slice_as_bytes!(u64, u64_as_ne_bytes, u64_as_ne_mut_bytes);
integral_slice_as_bytes!(i64, i64_as_ne_bytes, i64_as_ne_mut_bytes); integral_slice_as_bytes!(i64, i64_as_ne_bytes, i64_as_ne_mut_bytes);
integral_slice_as_bytes!(f32, f32_as_ne_bytes, f32_as_ne_mut_bytes); integral_slice_as_bytes!(f32, f32_as_ne_bytes, f32_as_ne_mut_bytes);
integral_slice_as_bytes!(f16, f16_as_ne_bytes, f16_as_ne_mut_bytes);
integral_slice_as_bytes!(f64, f64_as_ne_bytes, f64_as_ne_mut_bytes); integral_slice_as_bytes!(f64, f64_as_ne_bytes, f64_as_ne_mut_bytes);
+169
View File
@@ -0,0 +1,169 @@
use crate::{tags::IfdPointer, TiffError, TiffFormatError};
use std::collections::HashMap;
/// The IFD structure of a TIFF file should be limited to a forest of entries, and a tree when we
/// only consider having a primary IFD and its children. There is up to one primary child of a node
/// that is the `next` entry in an IFD itself. Since we offer iteration over this chain we want to
/// detect when a new edge would introduce a cycle, such that callers can be guaranteed to
/// terminate iteration at some point even in malicious images.
///
/// However, we are not necessarily visiting the IFDs in their topological order.
///
/// Since we only consider one child, we run union find to assign each pointer to a known chain.
/// Then, when we insert a new edge we check if they belong to the same component.
#[derive(Default, Debug)]
pub struct IfdCycles {
/// The root of each component union.
component_union: HashMap<ComponentId, ComponentId>,
links: HashMap<IfdPointer, u64>,
chains: HashMap<IfdPointer, ComponentId>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct ComponentId(u64);
impl IfdCycles {
pub fn new() -> Self {
IfdCycles::default()
}
pub fn insert_next(
&mut self,
from: IfdPointer,
to: Option<IfdPointer>,
) -> Result<bool, TiffError> {
let to_offset = to.map_or(0, |p| p.0);
// For some reason we got
match self.links.get(&from) {
Some(existing) if *existing == to_offset => return Ok(false),
// We got here twice with two different reads of the same IFD. That is .. unusual and
// we interpret it as a cycle.
Some(_) => return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets)),
None => self.links.insert(from, to_offset),
};
self.ensure_node(from);
if let Some(to) = to {
self.ensure_node(to);
let parent = self.nominal_component(from);
let child = self.nominal_component(to);
if parent == child {
return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets));
}
self.component_union.insert(child, parent);
}
Ok(true)
}
fn ensure_node(&mut self, ifd: IfdPointer) {
self.chains.entry(ifd).or_insert_with(|| {
let id = ComponentId(self.component_union.len() as u64);
self.component_union.insert(id, id);
id
});
}
fn nominal_component(&mut self, node: IfdPointer) -> ComponentId {
let id = self.chains[&node];
let nomimal = {
let mut iter = id;
loop {
let parent = self.component_union[&iter];
if parent == iter {
break parent;
}
iter = parent;
}
};
if nomimal != id {
// Compress.
let mut iter = id;
loop {
let parent = self.component_union[&iter];
if parent == iter {
break;
}
self.component_union.insert(iter, nomimal);
iter = parent;
}
}
nomimal
}
}
#[test]
fn cycles_are_detected() {
let mut cycles = IfdCycles::new();
cycles
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x800)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x800), Some(IfdPointer(0x20)))
.expect_err("cycle must be detected");
}
#[test]
fn reflective_cycle() {
let mut cycles = IfdCycles::new();
cycles
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x20)))
.expect_err("self-referential cycle must be detected");
}
#[test]
fn late_cycle() {
let mut cycles = IfdCycles::new();
cycles
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x40)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x60), Some(IfdPointer(0x80)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x80), Some(IfdPointer(0x20)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x40), Some(IfdPointer(0x60)))
.expect_err("non-existing link is valid");
}
#[test]
fn odd_cycle() {
let mut cycles = IfdCycles::new();
cycles
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x40)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x60), Some(IfdPointer(0x80)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x80), Some(IfdPointer(0x20)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x40), Some(IfdPointer(0x80)))
.expect_err("non-existing link is valid");
}
+505 -200
View File
File diff suppressed because it is too large. Load diff
+1313
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
}
+1662 -1136
View File
File diff suppressed because it is too large. Load diff
+291 -233
View File
@@ -1,49 +1,51 @@
//! All IO functionality needed for TIFF decoding //! All IO functionality needed for TIFF decoding
#[cfg(feature = "webp")]
use std::io::Cursor;
use std::io::{self, BufRead, BufReader, Read, Seek, Take};
use std::io::{self, Read, Seek}; pub use crate::tags::ByteOrder;
/// Byte order of the TIFF file.
#[derive(Clone, Copy, Debug)]
pub enum ByteOrder {
/// little endian byte order
LittleEndian,
/// big endian byte order
BigEndian,
}
/// Reader that is aware of the byte order. /// Reader that is aware of the byte order.
pub trait EndianReader: Read { #[derive(Debug)]
/// Byte order that should be adhered to pub struct EndianReader<R> {
fn byte_order(&self) -> ByteOrder; reader: R,
pub(crate) byte_order: ByteOrder,
}
impl<R: Read> EndianReader<R> {
pub fn new(reader: R, byte_order: ByteOrder) -> Self {
Self { reader, byte_order }
}
pub fn inner(&mut self) -> &mut R {
&mut self.reader
}
pub fn goto_offset(&mut self, offset: u64) -> io::Result<()>
where
R: Seek,
{
self.reader.seek(io::SeekFrom::Start(offset))?;
Ok(())
}
/// Reads an u16 /// Reads an u16
#[inline(always)] #[inline(always)]
fn read_u16(&mut self) -> Result<u16, io::Error> { pub fn read_u16(&mut self) -> Result<u16, io::Error> {
let mut n = [0u8; 2]; let mut n = [0u8; 2];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(match self.byte_order() { Ok(match self.byte_order {
ByteOrder::LittleEndian => u16::from_le_bytes(n), ByteOrder::LittleEndian => u16::from_le_bytes(n),
ByteOrder::BigEndian => u16::from_be_bytes(n), ByteOrder::BigEndian => u16::from_be_bytes(n),
}) })
} }
/// Reads an i8
#[inline(always)]
fn read_i8(&mut self) -> Result<i8, io::Error> {
let mut n = [0u8; 1];
self.read_exact(&mut n)?;
Ok(match self.byte_order() {
ByteOrder::LittleEndian => i8::from_le_bytes(n),
ByteOrder::BigEndian => i8::from_be_bytes(n),
})
}
/// Reads an i16 /// Reads an i16
#[inline(always)] #[inline(always)]
fn read_i16(&mut self) -> Result<i16, io::Error> { pub fn read_i16(&mut self) -> Result<i16, io::Error> {
let mut n = [0u8; 2]; let mut n = [0u8; 2];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(match self.byte_order() { Ok(match self.byte_order {
ByteOrder::LittleEndian => i16::from_le_bytes(n), ByteOrder::LittleEndian => i16::from_le_bytes(n),
ByteOrder::BigEndian => i16::from_be_bytes(n), ByteOrder::BigEndian => i16::from_be_bytes(n),
}) })
@@ -51,10 +53,10 @@ pub trait EndianReader: Read {
/// Reads an u32 /// Reads an u32
#[inline(always)] #[inline(always)]
fn read_u32(&mut self) -> Result<u32, io::Error> { pub fn read_u32(&mut self) -> Result<u32, io::Error> {
let mut n = [0u8; 4]; let mut n = [0u8; 4];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(match self.byte_order() { Ok(match self.byte_order {
ByteOrder::LittleEndian => u32::from_le_bytes(n), ByteOrder::LittleEndian => u32::from_le_bytes(n),
ByteOrder::BigEndian => u32::from_be_bytes(n), ByteOrder::BigEndian => u32::from_be_bytes(n),
}) })
@@ -62,10 +64,10 @@ pub trait EndianReader: Read {
/// Reads an i32 /// Reads an i32
#[inline(always)] #[inline(always)]
fn read_i32(&mut self) -> Result<i32, io::Error> { pub fn read_i32(&mut self) -> Result<i32, io::Error> {
let mut n = [0u8; 4]; let mut n = [0u8; 4];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(match self.byte_order() { Ok(match self.byte_order {
ByteOrder::LittleEndian => i32::from_le_bytes(n), ByteOrder::LittleEndian => i32::from_le_bytes(n),
ByteOrder::BigEndian => i32::from_be_bytes(n), ByteOrder::BigEndian => i32::from_be_bytes(n),
}) })
@@ -73,10 +75,10 @@ pub trait EndianReader: Read {
/// Reads an u64 /// Reads an u64
#[inline(always)] #[inline(always)]
fn read_u64(&mut self) -> Result<u64, io::Error> { pub fn read_u64(&mut self) -> Result<u64, io::Error> {
let mut n = [0u8; 8]; let mut n = [0u8; 8];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(match self.byte_order() { Ok(match self.byte_order {
ByteOrder::LittleEndian => u64::from_le_bytes(n), ByteOrder::LittleEndian => u64::from_le_bytes(n),
ByteOrder::BigEndian => u64::from_be_bytes(n), ByteOrder::BigEndian => u64::from_be_bytes(n),
}) })
@@ -84,10 +86,10 @@ pub trait EndianReader: Read {
/// Reads an i64 /// Reads an i64
#[inline(always)] #[inline(always)]
fn read_i64(&mut self) -> Result<i64, io::Error> { pub fn read_i64(&mut self) -> Result<i64, io::Error> {
let mut n = [0u8; 8]; let mut n = [0u8; 8];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(match self.byte_order() { Ok(match self.byte_order {
ByteOrder::LittleEndian => i64::from_le_bytes(n), ByteOrder::LittleEndian => i64::from_le_bytes(n),
ByteOrder::BigEndian => i64::from_be_bytes(n), ByteOrder::BigEndian => i64::from_be_bytes(n),
}) })
@@ -95,10 +97,10 @@ pub trait EndianReader: Read {
/// Reads an f32 /// Reads an f32
#[inline(always)] #[inline(always)]
fn read_f32(&mut self) -> Result<f32, io::Error> { pub fn read_f32(&mut self) -> Result<f32, io::Error> {
let mut n = [0u8; 4]; let mut n = [0u8; 4];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(f32::from_bits(match self.byte_order() { Ok(f32::from_bits(match self.byte_order {
ByteOrder::LittleEndian => u32::from_le_bytes(n), ByteOrder::LittleEndian => u32::from_le_bytes(n),
ByteOrder::BigEndian => u32::from_be_bytes(n), ByteOrder::BigEndian => u32::from_be_bytes(n),
})) }))
@@ -106,250 +108,306 @@ pub trait EndianReader: Read {
/// Reads an f64 /// Reads an f64
#[inline(always)] #[inline(always)]
fn read_f64(&mut self) -> Result<f64, io::Error> { pub fn read_f64(&mut self) -> Result<f64, io::Error> {
let mut n = [0u8; 8]; let mut n = [0u8; 8];
self.read_exact(&mut n)?; self.reader.read_exact(&mut n)?;
Ok(f64::from_bits(match self.byte_order() { Ok(f64::from_bits(match self.byte_order {
ByteOrder::LittleEndian => u64::from_le_bytes(n), ByteOrder::LittleEndian => u64::from_le_bytes(n),
ByteOrder::BigEndian => u64::from_be_bytes(n), ByteOrder::BigEndian => u64::from_be_bytes(n),
})) }))
} }
} }
/// //
/// # READERS // # READERS
/// //
/// /// Type alias for the deflate Reader
/// ## Deflate Reader #[cfg(feature = "deflate")]
/// pub type DeflateReader<R> = flate2::read::ZlibDecoder<R>;
pub type DeflateReader<'r, R> = flate2::read::ZlibDecoder<&'r mut SmartReader<R>>; //
// ## LZW Reader
/// //
/// ## LZW Reader
///
/// Reader that decompresses LZW streams /// Reader that decompresses LZW streams
pub struct LZWReader { #[cfg(feature = "lzw")]
buffer: io::Cursor<Vec<u8>>, pub struct LZWReader<R: Read> {
reader: BufReader<Take<R>>,
decoder: weezl::decode::Decoder,
} }
impl LZWReader { #[cfg(feature = "lzw")]
impl<R: Read> LZWReader<R> {
/// Wraps a reader /// Wraps a reader
pub fn new<R>( pub fn new(reader: R, compressed_length: usize) -> LZWReader<R> {
reader: &mut SmartReader<R>, let configuration =
compressed_length: usize, weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
max_uncompressed_length: usize, .with_yield_on_full_buffer(true);
) -> io::Result<(usize, LZWReader)> Self {
where reader: BufReader::with_capacity(
R: Read + Seek, (32 * 1024).min(compressed_length),
{ reader.take(u64::try_from(compressed_length).unwrap()),
let mut compressed = vec![0; compressed_length as usize]; ),
reader.read_exact(&mut compressed[..])?; decoder: configuration.build(),
let mut uncompressed = vec![0; max_uncompressed_length]; }
let mut decoder = weezl::decode::Decoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8); }
}
let mut bytes_read = 0; #[cfg(feature = "lzw")]
let mut bytes_written = 0; impl<R: Read> Read for LZWReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
while bytes_written < max_uncompressed_length { loop {
let result = decoder.decode_bytes( let result = self.decoder.decode_bytes(self.reader.fill_buf()?, buf);
&compressed[bytes_read..], self.reader.consume(result.consumed_in);
&mut uncompressed[bytes_written..],
);
bytes_read += result.consumed_in;
bytes_written += result.consumed_out;
match result.status { match result.status {
Ok(weezl::LzwStatus::Ok) => {} Ok(weezl::LzwStatus::Ok) => {
Ok(weezl::LzwStatus::Done) => break, if result.consumed_out == 0 {
continue;
} else {
return Ok(result.consumed_out);
}
}
Ok(weezl::LzwStatus::NoProgress) => { Ok(weezl::LzwStatus::NoProgress) => {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::UnexpectedEof, io::ErrorKind::UnexpectedEof,
"no lzw end code found", "no lzw end code found",
)) ));
}
Ok(weezl::LzwStatus::Done) => {
return Ok(result.consumed_out);
} }
Err(err) => return Err(io::Error::new(io::ErrorKind::InvalidData, err)), Err(err) => return Err(io::Error::new(io::ErrorKind::InvalidData, err)),
} }
} }
uncompressed.resize(bytes_written, 0);
Ok((
uncompressed.len(),
LZWReader {
buffer: io::Cursor::new(uncompressed),
},
))
} }
} }
impl Read for LZWReader { //
#[inline] // ## PackBits Reader
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { //
self.buffer.read(buf)
} /// Internal state machine for the PackBitsReader.
enum PackBitsReaderState {
Header,
Literal,
Repeat { value: u8 },
} }
/// /// Reader that unpacks Apple's `PackBits` format
/// ## JPEG Reader (for "new-style" JPEG format (TIFF compression tag 7)) pub struct PackBitsReader<R: Read> {
/// reader: Take<R>,
state: PackBitsReaderState,
pub(crate) struct JpegReader { count: usize,
buffer: io::Cursor<Vec<u8>>,
} }
impl JpegReader {
/// Constructs new JpegReader wrapping a SmartReader.
/// Because JPEG compression in TIFF allows to save quantization and/or huffman tables in one
/// central location, the constructor accepts this data as `jpeg_tables` here containing either
/// or both.
/// These `jpeg_tables` are simply prepended to the remaining jpeg image data.
/// Because these `jpeg_tables` start with a `SOI` (HEX: `0xFFD8`) or __start of image__ marker
/// which is also at the beginning of the remaining JPEG image data and would
/// confuse the JPEG renderer, one of these has to be taken off. In this case the first two
/// bytes of the remaining JPEG data is removed because it follows `jpeg_tables`.
/// Similary, `jpeg_tables` ends with a `EOI` (HEX: `0xFFD9`) or __end of image__ marker,
/// this has to be removed as well (last two bytes of `jpeg_tables`).
pub fn new<R>( impl<R: Read> PackBitsReader<R> {
reader: &mut SmartReader<R>, /// Wraps a reader
length: u32, pub fn new(reader: R, length: u64) -> Self {
jpeg_tables: &Option<Vec<u8>>, Self {
) -> io::Result<JpegReader> reader: reader.take(length),
where state: PackBitsReaderState::Header,
R: Read + Seek, count: 0,
{
// Read jpeg image data
let mut segment = vec![0; length as usize];
reader.read_exact(&mut segment[..])?;
match jpeg_tables {
Some(tables) => {
let mut jpeg_data = tables.clone();
let truncated_length = jpeg_data.len() - 2;
jpeg_data.truncate(truncated_length);
jpeg_data.extend_from_slice(&segment[2..]);
Ok(JpegReader {
buffer: io::Cursor::new(jpeg_data),
})
}
None => Ok(JpegReader {
buffer: io::Cursor::new(segment),
}),
} }
} }
} }
impl Read for JpegReader { impl<R: Read> Read for PackBitsReader<R> {
// #[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.buffer.read(buf) while let PackBitsReaderState::Header = self.state {
} if self.reader.limit() == 0 {
} return Ok(0);
}
/// let mut header: [u8; 1] = [0];
/// ## PackBits Reader self.reader.read_exact(&mut header)?;
///
/// Reader that unpacks Apple's `PackBits` format
pub struct PackBitsReader {
buffer: io::Cursor<Vec<u8>>,
}
impl PackBitsReader {
/// Wraps a reader
pub fn new<R: Read + Seek>(
mut reader: R,
length: usize,
) -> io::Result<(usize, PackBitsReader)> {
let mut buffer = Vec::new();
let mut header: [u8; 1] = [0];
let mut data: [u8; 1] = [0];
let mut bytes_read = 0;
while bytes_read < length {
reader.read_exact(&mut header)?;
bytes_read += 1;
let h = header[0] as i8; let h = header[0] as i8;
if h >= -127 && h <= -1 { if (-127..=-1).contains(&h) {
let new_len = buffer.len() + (1 - h as isize) as usize; let mut data: [u8; 1] = [0];
reader.read_exact(&mut data)?; self.reader.read_exact(&mut data)?;
buffer.resize(new_len, data[0]); self.state = PackBitsReaderState::Repeat { value: data[0] };
bytes_read += 1; self.count = (1 - h as isize) as usize;
} else if h >= 0 { } else if h >= 0 {
let num_vals = h as usize + 1; self.state = PackBitsReaderState::Literal;
let start = buffer.len(); self.count = h as usize + 1;
buffer.resize(start + num_vals, 0);
reader.read_exact(&mut buffer[start..])?;
bytes_read += num_vals
} else { } else {
// h = -128 is a no-op. // h = -128 is a no-op.
} }
} }
Ok(( let length = buf.len().min(self.count);
buffer.len(), let actual = match self.state {
PackBitsReader { PackBitsReaderState::Literal => self.reader.read(&mut buf[..length])?,
buffer: io::Cursor::new(buffer), PackBitsReaderState::Repeat { value } => {
}, for b in &mut buf[..length] {
)) *b = value;
}
length
}
PackBitsReaderState::Header => unreachable!(),
};
self.count -= actual;
if self.count == 0 {
self.state = PackBitsReaderState::Header;
}
Ok(actual)
} }
} }
impl Read for PackBitsReader { #[cfg(feature = "fax")]
#[inline] pub struct Group4Reader<R: Read> {
decoder: fax34::decoder::Group4Decoder<io::Bytes<io::BufReader<io::Take<R>>>>,
line_buf: io::Cursor<Vec<u8>>,
height: u32,
width: u16,
y: u32,
}
#[cfg(feature = "fax")]
impl<R: Read> Group4Reader<R> {
pub fn new(
dimensions: (u32, u32),
reader: R,
compressed_length: u64,
) -> crate::TiffResult<Self> {
let width = u16::try_from(dimensions.0)?;
let height = dimensions.1;
Ok(Self {
decoder: fax34::decoder::Group4Decoder::new(
io::BufReader::new(reader.take(compressed_length)).bytes(),
width,
)?,
line_buf: io::Cursor::new(Vec::with_capacity(width.into())),
width,
height,
y: 0,
})
}
}
#[cfg(feature = "fax")]
impl<R: Read> Read for Group4Reader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.buffer.read(buf) // Either we have not read any line or we are at the end of a line.
if self.line_buf.position() as usize == self.line_buf.get_ref().len()
&& self.y < self.height
{
let next = self.decoder.advance().map_err(std::io::Error::other)?;
match next {
fax34::decoder::DecodeStatus::End => (),
fax34::decoder::DecodeStatus::Incomplete => {
self.y += 1;
// We known `transitions` yields exactly `self.width` items (per doc).
// FIXME: performance. We do not need an individual pixel iterator, filling
// memory especially for long runs can be much quicker. The `transitions` are
// the positions at which each run-length ends, i.e. the prefix sum of run
// lengths. Runs in fax4 start with white.
let transitions = fax34::decoder::pels(self.decoder.transition(), self.width);
let buffer = self.line_buf.get_mut();
buffer.resize(usize::from(self.width).div_ceil(8), 0u8);
let target = &mut buffer[..];
// Note: it may seem strange to treat black as 0b1 and white as 0b0 despite all
// our streams by default decoding as-if PhotometricInterpretation::BlackIsMin.
// This is however consistent with libtiff. It seems that fax4's "White"
// differs from what libtiff thinks of as "White". For content, a line of data
// is in runlength encoding of white-black-white-black always starting with
// white. In libtiff, the loop always does both colors in one go and the
// structure is:
//
// ```
// for (; runs < erun; runs += 2)
// // white run
// do { *lp++ = 0L; } while (…)
// // black run
// do { *lp++ = -1L; } while (…)
// ```
//
// So indeed the Fax4::White run is implemented by filling with zeros.
let mut bits = transitions.map(|c| match c {
fax34::Color::Black => true,
fax34::Color::White => false,
});
// Assemble bits in MSB as per our library representation for buffer.
for byte in target {
let mut val = 0;
for (idx, bit) in bits.by_ref().take(8).enumerate() {
val |= u8::from(bit) << (7 - idx % 8);
}
*byte = val;
}
self.line_buf.set_position(0);
}
}
}
self.line_buf.read(buf)
} }
} }
/// #[cfg(feature = "webp")]
/// ## SmartReader Reader pub struct WebPReader {
/// inner: Cursor<Vec<u8>>,
/// Reader that is aware of the byte order.
#[derive(Debug)]
pub struct SmartReader<R>
where
R: Read,
{
reader: R,
pub byte_order: ByteOrder,
} }
impl<R> SmartReader<R> #[cfg(feature = "webp")]
where impl WebPReader {
R: Read, pub fn new<R: Read + Seek>(
{ reader: R,
/// Wraps a reader compressed_length: u64,
pub fn wrap(reader: R, byte_order: ByteOrder) -> SmartReader<R> { samples: u16,
SmartReader { reader, byte_order } ) -> 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),
})
} }
} }
impl<R> EndianReader for SmartReader<R> #[cfg(feature = "webp")]
where impl Read for WebPReader {
R: Read,
{
#[inline(always)]
fn byte_order(&self) -> ByteOrder {
self.byte_order
}
}
impl<R: Read> Read for SmartReader<R> {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader.read(buf) self.inner.read(buf)
}
}
impl<R: Read + Seek> Seek for SmartReader<R> {
#[inline]
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
self.reader.seek(pos)
} }
} }
@@ -366,7 +424,7 @@ mod test {
let encoded_len = encoded.len(); let encoded_len = encoded.len();
let buff = io::Cursor::new(encoded); let buff = io::Cursor::new(encoded);
let (_, mut decoder) = PackBitsReader::new(buff, encoded_len).unwrap(); let mut decoder = PackBitsReader::new(buff, encoded_len as u64);
let mut decoded = Vec::new(); let mut decoded = Vec::new();
decoder.read_to_end(&mut decoded).unwrap(); decoder.read_to_end(&mut decoded).unwrap();
+105
View File
@@ -0,0 +1,105 @@
use std::io::{Read, Seek};
use crate::{
tags::{Tag, ValueBuffer},
Directory, TiffError, TiffFormatError, TiffResult,
};
use super::ifd::{Entry, Value};
use super::ValueReader;
pub(crate) struct TagReader<'lt, R: EntryDecoder + ?Sized> {
pub(crate) decoder: &'lt mut R,
pub(crate) ifd: &'lt Directory,
}
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> {
pub(crate) fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<Value>> {
let entry = match self.ifd.get(tag) {
None => return Ok(None),
Some(entry) => entry.clone(),
};
self.decoder.entry_val(&entry).map(Some)
}
/// Find a tag and fill the provided buffer with its raw data.
///
/// Returns the entry of the tag if found. This signals that the buffer was overwritten.
/// Otherwise the buffer is preserved as-is.
pub(crate) fn find_tag_buf(
&mut self,
tag: Tag,
buf: &mut ValueBuffer,
) -> TiffResult<Option<Entry>> {
let entry = match self.ifd.get(tag) {
None => return Ok(None),
Some(entry) => entry.clone(),
};
self.decoder.entry_buf(&entry, buf)?;
Ok(Some(entry))
}
pub(crate) fn find_tag_raw(
&mut self,
tag: Tag,
buf: &mut [u8],
at: u64,
) -> TiffResult<Option<usize>> {
let entry = match self.ifd.get(tag) {
None => return Ok(None),
Some(entry) => entry.clone(),
};
let len = self.decoder.entry_raw(&entry, buf, at)?;
Ok(Some(len))
}
pub(crate) fn require_tag(&mut self, tag: Tag) -> TiffResult<Value> {
match self.find_tag(tag)? {
Some(val) => Ok(val),
None => Err(TiffError::FormatError(
TiffFormatError::RequiredTagNotFound(tag),
)),
}
}
pub fn find_tag_uint_vec<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<Option<Vec<T>>> {
self.find_tag(tag)?
.map(|v| v.into_u64_vec())
.transpose()?
.map(|v| {
v.into_iter()
.map(|u| {
T::try_from(u).map_err(|_| TiffFormatError::InvalidTagValueType(tag).into())
})
.collect()
})
.transpose()
}
}
+196
View File
@@ -0,0 +1,196 @@
use core::fmt;
use std::{collections::BTreeMap, num::NonZeroU64};
use crate::{
decoder::ifd::Entry,
tags::{IfdPointer, Tag},
};
/// An Image File Directory (IFD).
///
/// A directory is a map of [`Tag`]s to [`Value`](crate::decoder::ifd::Value)s. The values are
/// stored anywhere in the file, with the directory containing the offsets and length of the
/// associated values for each tag present in the directory.
///
/// A directory can be created with with
/// [`Decoder::read_directory`](crate::decoder::Decoder::read_directory) or as an empty directory
/// to be extended with entries. A directory may be used with
/// [`Decoder::read_directory_tags`](crate::decoder::Decoder::read_directory_tags) to read the
/// values associated with tags from an underlying file.
#[doc(alias = "IFD")]
pub struct Directory {
/// There are at most `u16::MAX` entries in any single directory, the count is stored as a
/// 2-byte value. The order in the file is implied to be ascending by tag value (the decoder
/// does not mind unordered entries).
pub(crate) entries: BTreeMap<u16, Entry>,
pub(crate) next_ifd: Option<NonZeroU64>,
}
impl Directory {
/// Create a directory in an initial state without entries. Note that an empty directory can
/// not be encoded in a file, it must contain at least one entry.
pub fn empty() -> Self {
Directory {
entries: BTreeMap::new(),
next_ifd: None,
}
}
/// Retrieve the value associated with a tag.
pub fn get(&self, tag: Tag) -> Option<&Entry> {
self.entries.get(&tag.to_u16())
}
/// Check if the directory contains a specified tag.
pub fn contains(&self, tag: Tag) -> bool {
self.entries.contains_key(&tag.to_u16())
}
/// Iterate over all known and unknown tags in this directory.
pub fn iter(&self) -> impl Iterator<Item = (Tag, &Entry)> + '_ {
self.entries
.iter()
.map(|(k, v)| (Tag::from_u16_exhaustive(*k), v))
}
/// Insert additional entries into the directory.
///
/// Note that a directory can contain at most `u16::MAX` values. There may be one entry that
/// does not fit into the directory. This entry is silently ignored (please check [`Self::len`]
/// to detect the condition). Providing a tag multiple times or a tag that already exists
/// within this directory overwrites the entry.
pub fn extend(&mut self, iter: impl IntoIterator<Item = (Tag, Entry)>) {
// Code size conscious extension, avoid monomorphic extensions with the assumption of these
// not being performance sensitive in practice. (Maybe we have a polymorphic interface for
// the crate usage in the future.
self.extend_inner(iter.into_iter().by_ref())
}
/// Get the number of entries.
pub fn len(&self) -> usize {
// The keys are `u16`. Since IFDs are required to have at least one entry this would have
// been a trivial thing to do in the specification by storing it minus one but alas. In
// BigTIFF the count is stored as 8-bit anyways.
self.entries.len()
}
/// Check if there are any entries in this directory. Note that an empty directory can not be
/// encoded in the file, it must contain at least one entry.
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// Get the pointer to the next IFD, if it was defined.
pub fn next(&self) -> Option<IfdPointer> {
self.next_ifd.map(|n| IfdPointer(n.get()))
}
pub fn set_next(&mut self, next: Option<IfdPointer>) {
self.next_ifd = next.and_then(|n| NonZeroU64::new(n.0));
}
fn extend_inner(&mut self, iter: &mut dyn Iterator<Item = (Tag, Entry)>) {
for (tag, entry) in iter {
// If the tag is already present, it will be overwritten.
let map_entry = self.entries.entry(tag.to_u16());
match map_entry {
std::collections::btree_map::Entry::Vacant(vacant_entry) => {
vacant_entry.insert(entry);
}
std::collections::btree_map::Entry::Occupied(mut occupied_entry) => {
occupied_entry.insert(entry);
}
}
}
}
pub(crate) fn encoded_len<K: crate::encoder::TiffKind>(&self) -> u64 {
let (len_count, len_offset);
match std::mem::size_of::<K::OffsetType>() {
offset @ 4 => {
// non-BigTiff uses a `u16` here.
len_count = 2;
len_offset = offset as u64;
}
offset => {
let offset = offset as u64;
// assuming BigTIFF
len_count = offset;
len_offset = offset;
}
}
// Each entry is 12 bytes in classic TIFF, 20 bytes in BigTIFF.
let entry_bytes = 4 + 2 * len_offset;
len_count + entry_bytes * self.entries.len() as u64 + len_offset
}
}
impl fmt::Debug for Directory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Directory")
.field(
"entries",
&self.entries.iter().map(|(k, v)| (Tag::from_u16(*k), v)),
)
.field("next_ifd", &self.next_ifd)
.finish()
}
}
impl core::iter::FromIterator<(Tag, Entry)> for Directory {
fn from_iter<T: IntoIterator<Item = (Tag, Entry)>>(iter: T) -> Self {
let mut dir = Directory::empty();
dir.extend(iter);
dir
}
}
#[cfg(test)]
mod tests {
use super::Directory;
use crate::{decoder::ifd::Entry, tags::Tag};
#[test]
fn directory_multiple_entries() {
let mut dir = Directory::empty();
assert_eq!(dir.len(), 0);
dir.extend((0..=u16::MAX).map(|i| {
let tag = Tag::Unknown(1);
let entry = Entry::new_u64(crate::tags::Type::BYTE, i.into(), [0; 8]);
(tag, entry)
}));
assert_eq!(dir.len(), 1, "Only one tag was ever modified");
assert_eq!(
dir.get(Tag::Unknown(1))
.expect("tag 1 should be present after this chain")
.count(),
u16::MAX.into()
);
}
#[test]
fn iteration_order() {
let mut dir = Directory::empty();
assert_eq!(dir.len(), 0);
let fake_entry = Entry::new_u64(crate::tags::Type::BYTE, 0, [0; 8]);
dir.extend((0..32).map(|i| {
let tag = Tag::Unknown(i);
let entry = fake_entry.clone();
(tag, entry)
}));
let iter_order: Vec<u16> = dir.iter().map(|(tag, _e)| tag.to_u16()).collect();
assert_eq!(
iter_order,
(0..32).collect::<Vec<_>>(),
"Tags must be in ascending order according to the specification"
);
}
}
+284
View File
@@ -1,5 +1,127 @@
use crate::tags::{PhotometricInterpretation, SampleFormat}; use crate::tags::{PhotometricInterpretation, SampleFormat};
/// Apply floating-point predictor encoding to a row of floating-point samples.
///
/// The floating-point predictor works by:
/// 1. Converting each value to big-endian bytes
/// 2. Shuffling bytes so all first bytes are together, all second bytes, etc.
/// 3. Applying byte-level horizontal difference (wrapping_sub) across the entire buffer
///
/// The decoder reverses this by:
/// 1. Applying cumulative sum (wrapping_add) on the entire byte stream
/// 2. De-shuffling bytes back into float values
macro_rules! fp_predict {
($name:ident, $float_type:ty) => {
fn $name(row: &[$float_type], samples: usize, result: &mut Vec<u8>) {
const BYTE_SIZE: usize = core::mem::size_of::<$float_type>();
let num_values = row.len();
let byte_count = num_values * BYTE_SIZE;
let start_len = result.len();
// Allocate zeroed space so we can write to arbitrary positions
result.resize(start_len + byte_count, 0);
let output = &mut result[start_len..];
// Step 1 & 2: Convert to big-endian and scatter bytes
for (i, &value) in row.iter().enumerate() {
let bytes = value.to_be_bytes();
for b in 0..BYTE_SIZE {
output[b * num_values + i] = bytes[b];
}
}
// Step 3: Apply byte-level horizontal difference across the entire buffer.
// The prediction spans across quarter boundaries, just like the decoder's cumulative sum.
//
// We process elements in reverse in batches of 16 to enable autovectorization.
// Reverse order ensures that when we compute output[i] - output[i-samples],
// the value at output[i-samples] hasn't been overwritten yet (critical when
// samples < 16, which is the common case: 1, 3, 4, 5).
//
// 8 bits * 16 = 128 bits, the width of a typical SIMD register.
// Even if the target has no vector instructions, this still benefits from
// instruction-level parallelism, since unlike in decoding, all subtractions
// within a chunk are truly independent of each other.
let output = &mut result[start_len..byte_count + start_len];
let diff_len = byte_count - samples;
let remainder_len = diff_len % 16;
// Process full 16-byte chunks in reverse.
// We iterate from the end so that predecessor values (at lower indices)
// are still in their original state when we read them.
let mut chunk_end = byte_count;
while chunk_end >= samples + remainder_len + 16 {
let chunk_start = chunk_end - 16;
let prev_start = chunk_start - samples;
// Read all values in the chunk and their predecessors first.
// This makes all subtractions below independent, enabling vectorization.
let curr: [u8; 16] = output[chunk_start..chunk_end].try_into().unwrap();
let prev: [u8; 16] = output[prev_start..prev_start + 16].try_into().unwrap();
let chunk = &mut output[chunk_start..chunk_end];
chunk[0] = curr[0].wrapping_sub(prev[0]);
chunk[1] = curr[1].wrapping_sub(prev[1]);
chunk[2] = curr[2].wrapping_sub(prev[2]);
chunk[3] = curr[3].wrapping_sub(prev[3]);
chunk[4] = curr[4].wrapping_sub(prev[4]);
chunk[5] = curr[5].wrapping_sub(prev[5]);
chunk[6] = curr[6].wrapping_sub(prev[6]);
chunk[7] = curr[7].wrapping_sub(prev[7]);
chunk[8] = curr[8].wrapping_sub(prev[8]);
chunk[9] = curr[9].wrapping_sub(prev[9]);
chunk[10] = curr[10].wrapping_sub(prev[10]);
chunk[11] = curr[11].wrapping_sub(prev[11]);
chunk[12] = curr[12].wrapping_sub(prev[12]);
chunk[13] = curr[13].wrapping_sub(prev[13]);
chunk[14] = curr[14].wrapping_sub(prev[14]);
chunk[15] = curr[15].wrapping_sub(prev[15]);
chunk_end = chunk_start;
}
// Handle the leading remainder that doesn't fill a complete chunk of 16.
// This must be done last (after the reverse chunk loop) because these are
// the lowest indices, and the chunks above may read predecessor values
// from this range.
for i in (samples..samples + remainder_len).rev() {
output[i] = output[i].wrapping_sub(output[i - samples]);
}
}
};
}
fp_predict!(fp_predict_f32, f32);
fp_predict!(fp_predict_f64, f64);
macro_rules! integer_horizontal_predict {
() => {
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>) {
let sample_size = Self::SAMPLE_FORMAT.len();
if row.len() < sample_size {
debug_assert!(false);
return;
}
let (start, rest) = row.split_at(sample_size);
result.extend_from_slice(start);
if result.capacity() - result.len() < rest.len() {
return;
}
result.extend(
row.into_iter()
.zip(rest)
.map(|(prev, current)| current.wrapping_sub(*prev)),
);
}
fn floating_point_predict(_: &[Self::Inner], _: &mut Vec<u8>) {
unreachable!("floating-point predictor is only valid for floating-point sample types")
}
};
}
/// Trait for different colortypes that can be encoded. /// Trait for different colortypes that can be encoded.
pub trait ColorType { pub trait ColorType {
/// The type of each sample of this colortype /// The type of each sample of this colortype
@@ -10,6 +132,12 @@ pub trait ColorType {
const BITS_PER_SAMPLE: &'static [u16]; const BITS_PER_SAMPLE: &'static [u16];
/// The value of the tiff tag `SampleFormat` /// The value of the tiff tag `SampleFormat`
const SAMPLE_FORMAT: &'static [SampleFormat]; const SAMPLE_FORMAT: &'static [SampleFormat];
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>);
/// Apply floating-point predictor encoding to a row of samples.
/// This is only implemented for floating-point types; integer types will panic.
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>);
} }
pub struct Gray8; pub struct Gray8;
@@ -18,6 +146,18 @@ impl ColorType for Gray8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[8]; const BITS_PER_SAMPLE: &'static [u16] = &[8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
integer_horizontal_predict!();
}
pub struct GrayI8;
impl ColorType for GrayI8 {
type Inner = i8;
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int];
integer_horizontal_predict!();
} }
pub struct Gray16; pub struct Gray16;
@@ -26,6 +166,18 @@ impl ColorType for Gray16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[16]; const BITS_PER_SAMPLE: &'static [u16] = &[16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
integer_horizontal_predict!();
}
pub struct GrayI16;
impl ColorType for GrayI16 {
type Inner = i16;
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int];
integer_horizontal_predict!();
} }
pub struct Gray32; pub struct Gray32;
@@ -34,6 +186,18 @@ impl ColorType for Gray32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[32]; const BITS_PER_SAMPLE: &'static [u16] = &[32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
integer_horizontal_predict!();
}
pub struct GrayI32;
impl ColorType for GrayI32 {
type Inner = i32;
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int];
integer_horizontal_predict!();
} }
pub struct Gray32Float; pub struct Gray32Float;
@@ -42,6 +206,14 @@ impl ColorType for Gray32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[32]; const BITS_PER_SAMPLE: &'static [u16] = &[32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("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)
}
} }
pub struct Gray64; pub struct Gray64;
@@ -50,6 +222,18 @@ impl ColorType for Gray64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[64]; const BITS_PER_SAMPLE: &'static [u16] = &[64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
integer_horizontal_predict!();
}
pub struct GrayI64;
impl ColorType for GrayI64 {
type Inner = i64;
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int];
integer_horizontal_predict!();
} }
pub struct Gray64Float; pub struct Gray64Float;
@@ -58,6 +242,14 @@ impl ColorType for Gray64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[64]; const BITS_PER_SAMPLE: &'static [u16] = &[64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("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)
}
} }
pub struct RGB8; pub struct RGB8;
@@ -66,6 +258,8 @@ impl ColorType for RGB8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8]; const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
} }
pub struct RGB16; pub struct RGB16;
@@ -74,6 +268,8 @@ impl ColorType for RGB16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16]; const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
} }
pub struct RGB32; pub struct RGB32;
@@ -82,6 +278,8 @@ impl ColorType for RGB32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
} }
pub struct RGB32Float; pub struct RGB32Float;
@@ -90,6 +288,14 @@ impl ColorType for RGB32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("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)
}
} }
pub struct RGB64; pub struct RGB64;
@@ -98,6 +304,8 @@ impl ColorType for RGB64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
} }
pub struct RGB64Float; pub struct RGB64Float;
@@ -106,6 +314,14 @@ impl ColorType for RGB64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("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)
}
} }
pub struct RGBA8; pub struct RGBA8;
@@ -114,6 +330,8 @@ impl ColorType for RGBA8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8]; const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct RGBA16; pub struct RGBA16;
@@ -122,6 +340,8 @@ impl ColorType for RGBA16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16]; const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct RGBA32; pub struct RGBA32;
@@ -130,6 +350,8 @@ impl ColorType for RGBA32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct RGBA32Float; pub struct RGBA32Float;
@@ -138,6 +360,14 @@ impl ColorType for RGBA32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("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)
}
} }
pub struct RGBA64; pub struct RGBA64;
@@ -146,6 +376,8 @@ impl ColorType for RGBA64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct RGBA64Float; pub struct RGBA64Float;
@@ -154,6 +386,14 @@ impl ColorType for RGBA64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("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)
}
} }
pub struct CMYK8; pub struct CMYK8;
@@ -162,6 +402,8 @@ impl ColorType for CMYK8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8]; const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct CMYK16; pub struct CMYK16;
@@ -170,6 +412,8 @@ impl ColorType for CMYK16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16]; const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct CMYK32; pub struct CMYK32;
@@ -178,6 +422,8 @@ impl ColorType for CMYK32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct CMYK32Float; pub struct CMYK32Float;
@@ -186,6 +432,14 @@ impl ColorType for CMYK32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("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)
}
} }
pub struct CMYK64; pub struct CMYK64;
@@ -194,6 +448,8 @@ impl ColorType for CMYK64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
} }
pub struct CMYK64Float; pub struct CMYK64Float;
@@ -202,4 +458,32 @@ impl ColorType for CMYK64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("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)
}
}
pub struct YCbCr8;
impl ColorType for YCbCr8 {
type Inner = u8;
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::YCbCr;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
}
pub struct CMYKA8;
impl ColorType for CMYKA8 {
type Inner = u8;
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 5];
integer_horizontal_predict!();
} }
+81
View File
@@ -0,0 +1,81 @@
use crate::encoder::compression::*;
use flate2::{write::ZlibEncoder, Compression as FlateCompression};
/// The Deflate algorithm used to compress image data in TIFF files.
#[derive(Debug, Clone, Copy)]
pub struct Deflate {
level: FlateCompression,
}
impl Deflate {
/// Create a new deflate compressor with a specific level of compression.
///
/// 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),
}
}
}
impl Default for Deflate {
fn default() -> Self {
Self::with_level(6)
}
}
impl Compression for Deflate {
const COMPRESSION_METHOD: CompressionMethod = CompressionMethod::Deflate;
fn get_algorithm(&self) -> Compressor {
Compressor::Deflate(*self)
}
}
impl CompressionAlgorithm for Deflate {
fn write_to<W: Write>(&mut self, writer: &mut W, bytes: &[u8]) -> Result<u64, io::Error> {
let mut encoder = ZlibEncoder::new(writer, self.level);
encoder.write_all(bytes)?;
encoder.try_finish()?;
Ok(encoder.total_out())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encoder::compression::tests::TEST_DATA;
use std::io::Cursor;
#[test]
fn test_deflate() {
const EXPECTED_COMPRESSED_DATA: [u8; 64] = [
0x78, 0x9C, 0x15, 0xC7, 0xD1, 0x0D, 0x80, 0x20, 0x0C, 0x04, 0xD0, 0x55, 0x6E, 0x02,
0xA7, 0x71, 0x81, 0xA6, 0x41, 0xDA, 0x28, 0xD4, 0xF4, 0xD0, 0xF9, 0x81, 0xE4, 0xFD,
0xBC, 0xD3, 0x9C, 0x58, 0x04, 0x1C, 0xE9, 0xBD, 0xE2, 0x8A, 0x84, 0x5A, 0xD1, 0x7B,
0xE7, 0x97, 0xF4, 0xF8, 0x08, 0x8D, 0xF6, 0x66, 0x21, 0x3D, 0x3A, 0xE4, 0xA9, 0x91,
0x3E, 0xAC, 0xF1, 0x98, 0xB9, 0x70, 0x17, 0x13,
];
let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data);
Deflate::default().write_to(&mut writer, TEST_DATA).unwrap();
assert_eq!(EXPECTED_COMPRESSED_DATA, compressed_data.as_slice());
}
}
+46
View File
@@ -0,0 +1,46 @@
use crate::encoder::compression::*;
use weezl::encode::Encoder as LZWEncoder;
/// The LZW algorithm used to compress image data in TIFF files.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Lzw;
impl Compression for Lzw {
const COMPRESSION_METHOD: CompressionMethod = CompressionMethod::LZW;
fn get_algorithm(&self) -> Compressor {
Compressor::Lzw(*self)
}
}
impl CompressionAlgorithm for Lzw {
fn write_to<W: Write>(&mut self, writer: &mut W, bytes: &[u8]) -> Result<u64, io::Error> {
let mut encoder = LZWEncoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8);
let result = encoder.into_stream(writer).encode_all(bytes);
let byte_count = result.bytes_written as u64;
result.status.map(|_| byte_count)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encoder::compression::tests::TEST_DATA;
use std::io::Cursor;
#[test]
fn test_lzw() {
const EXPECTED_COMPRESSED_DATA: [u8; 63] = [
0x80, 0x15, 0x0D, 0x06, 0x93, 0x98, 0x82, 0x08, 0x20, 0x30, 0x88, 0x0E, 0x67, 0x43,
0x91, 0xA4, 0xDC, 0x67, 0x10, 0x19, 0x8D, 0xE7, 0x21, 0x01, 0x8C, 0xD0, 0x65, 0x31,
0x9A, 0xE1, 0xD1, 0x03, 0xB1, 0x86, 0x1A, 0x6F, 0x3A, 0xC1, 0x4C, 0x66, 0xF3, 0x69,
0xC0, 0xE4, 0x65, 0x39, 0x9C, 0xCD, 0x26, 0xF3, 0x74, 0x20, 0xD8, 0x67, 0x89, 0x9A,
0x4E, 0x86, 0x83, 0x69, 0xCC, 0x5D, 0x01,
];
let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data);
Lzw.write_to(&mut writer, TEST_DATA).unwrap();
assert_eq!(EXPECTED_COMPRESSED_DATA, compressed_data.as_slice());
}
}
+70
View File
@@ -0,0 +1,70 @@
use crate::tags::CompressionMethod;
use std::io::{self, Write};
#[cfg(feature = "deflate")]
mod deflate;
#[cfg(feature = "lzw")]
mod lzw;
mod packbits;
mod uncompressed;
#[cfg(feature = "deflate")]
pub use self::deflate::Deflate;
#[cfg(feature = "lzw")]
pub use self::lzw::Lzw;
pub use self::packbits::Packbits;
pub use self::uncompressed::Uncompressed;
/// An algorithm used for compression
pub trait CompressionAlgorithm {
/// The algorithm writes data directly into the writer.
/// It returns the total number of bytes written.
fn write_to<W: Write>(&mut self, writer: &mut W, bytes: &[u8]) -> Result<u64, io::Error>;
}
/// An algorithm used for compression with associated enums and optional configurations.
pub trait Compression: CompressionAlgorithm {
/// The corresponding tag to the algorithm.
const COMPRESSION_METHOD: CompressionMethod;
/// Method to optain a type that can store each variant of comression algorithm.
fn get_algorithm(&self) -> Compressor;
}
/// An enum to store each compression algorithm.
#[non_exhaustive]
pub enum Compressor {
Uncompressed(Uncompressed),
#[cfg(feature = "lzw")]
Lzw(Lzw),
#[cfg(feature = "deflate")]
Deflate(Deflate),
Packbits(Packbits),
}
impl Default for Compressor {
/// The default compression strategy does not apply any compression.
fn default() -> Self {
Compressor::Uncompressed(Uncompressed)
}
}
impl CompressionAlgorithm for Compressor {
fn write_to<W: Write>(&mut self, writer: &mut W, bytes: &[u8]) -> Result<u64, io::Error> {
match self {
Compressor::Uncompressed(algorithm) => algorithm.write_to(writer, bytes),
#[cfg(feature = "lzw")]
Compressor::Lzw(algorithm) => algorithm.write_to(writer, bytes),
#[cfg(feature = "deflate")]
Compressor::Deflate(algorithm) => algorithm.write_to(writer, bytes),
Compressor::Packbits(algorithm) => algorithm.write_to(writer, bytes),
}
}
}
#[cfg(test)]
mod tests {
pub const TEST_DATA: &[u8] = b"This is a string for checking various compression algorithms.";
}
+204
View File
@@ -0,0 +1,204 @@
use crate::encoder::compression::*;
use std::io::{BufWriter, Error, ErrorKind};
/// Compressor that uses the Packbits[^note] algorithm to compress bytes.
///
/// [^note]: PackBits is often ineffective on continuous tone images,
/// including many grayscale images. In such cases, it is better
/// to leave the image uncompressed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Packbits;
impl Compression for Packbits {
const COMPRESSION_METHOD: CompressionMethod = CompressionMethod::PackBits;
fn get_algorithm(&self) -> Compressor {
Compressor::Packbits(*self)
}
}
impl CompressionAlgorithm for Packbits {
fn write_to<W: Write>(&mut self, writer: &mut W, bytes: &[u8]) -> Result<u64, io::Error> {
// Inspired by https://github.com/skirridsystems/packbits
const MIN_REPT: u8 = 3; // Minimum run to compress between differ blocks
const MAX_BYTES: u8 = 128; // Maximum number of bytes that can be encoded in a header byte
// Encoding for header byte based on number of bytes represented.
fn encode_diff(n: u8) -> u8 {
n - 1
}
fn encode_rept(n: u8) -> u8 {
let var = 256 - (n - 1) as u16;
var as u8
}
fn write_u8<W: Write>(writer: &mut W, byte: u8) -> Result<u64, Error> {
writer.write(&[byte]).map(|byte_count| byte_count as u64)
}
let mut bufwriter = BufWriter::new(writer);
let mut bytes_written = 0u64; // The number of bytes written into the writer
let mut offset: Option<u64> = None; // The index of the first byte written into the writer
let mut src_index: usize = 0; // Index of the current byte
let mut src_count = bytes.len(); //The number of bytes remaining to be compressed
let mut in_run = false; // Indication whether counting of similar bytes is performed
let mut run_index = 0u8; // Distance into pending bytes that a run starts
let mut bytes_pending = 0u8; // Bytes looked at but not yet output
let mut pending_index = 0usize; // Index of the first pending byte
let mut curr_byte: u8; // Byte currently being considered
let mut last_byte: u8; // Previous byte
// Need at least one byte to compress
if src_count == 0 {
return Err(Error::new(ErrorKind::WriteZero, "write zero"));
}
// Prime compressor with first character.
last_byte = bytes[src_index];
src_index += 1;
bytes_pending += 1;
while src_count - 1 != 0 {
src_count -= 1;
curr_byte = bytes[src_index];
src_index += 1;
bytes_pending += 1;
if in_run {
if (curr_byte != last_byte) || (bytes_pending > MAX_BYTES) {
offset.get_or_insert(write_u8(&mut bufwriter, encode_rept(bytes_pending - 1))?);
write_u8(&mut bufwriter, last_byte)?;
bytes_written += 2;
bytes_pending = 1;
pending_index = src_index - 1;
run_index = 0;
in_run = false;
}
} else if bytes_pending > MAX_BYTES {
// We have as much differing data as we can output in one chunk.
// Output MAX_BYTES leaving one byte.
offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(MAX_BYTES))?);
bufwriter.write_all(&bytes[pending_index..pending_index + MAX_BYTES as usize])?;
bytes_written += 1 + MAX_BYTES as u64;
pending_index += MAX_BYTES as usize;
bytes_pending -= MAX_BYTES;
run_index = bytes_pending - 1; // A run could start here
} else if curr_byte == last_byte {
if (bytes_pending - run_index >= MIN_REPT) || (run_index == 0) {
// This is a worthwhile run
if run_index != 0 {
// Flush differing data out of input buffer
offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(run_index))?);
bufwriter
.write_all(&bytes[pending_index..pending_index + run_index as usize])?;
bytes_written += 1 + run_index as u64;
}
bytes_pending -= run_index; // Length of run
in_run = true;
}
} else {
run_index = bytes_pending - 1; // A run could start here
}
last_byte = curr_byte;
}
// Output the remainder
if in_run {
bytes_written += 2;
offset.get_or_insert(write_u8(&mut bufwriter, encode_rept(bytes_pending))?);
write_u8(&mut bufwriter, last_byte)?;
} else {
bytes_written += 1 + bytes_pending as u64;
offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(bytes_pending))?);
bufwriter.write_all(&bytes[pending_index..pending_index + bytes_pending as usize])?;
}
bufwriter.flush()?;
Ok(bytes_written)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encoder::compression::tests::TEST_DATA;
use std::io::Cursor;
#[test]
fn test_packbits_single_byte() {
// compress single byte
const UNCOMPRESSED_DATA: [u8; 1] = [0x3F];
const EXPECTED_COMPRESSED_DATA: [u8; 2] = [0x00, 0x3F];
let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data);
Packbits.write_to(&mut writer, &UNCOMPRESSED_DATA).unwrap();
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
}
#[test]
fn test_packbits_rept() {
// compress buffer with repetitive sequence
const UNCOMPRESSED_DATA: &[u8] =
b"This strrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrring hangs.";
const EXPECTED_COMPRESSED_DATA: &[u8] = b"\x06This st\xD1r\x09ing hangs.";
let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data);
Packbits.write_to(&mut writer, UNCOMPRESSED_DATA).unwrap();
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
}
#[test]
fn test_packbits_large_rept_nonrept() {
// compress buffer with large repetitive and non-repetitive sequence
let mut data = b"This st".to_vec();
for _i in 0..158 {
data.push(b'r');
}
data.extend_from_slice(b"ing hangs.");
for i in 0..158 {
data.push(i);
}
const EXPECTED_COMPRESSED_DATA: [u8; 182] = [
0x06, 0x54, 0x68, 0x69, 0x73, 0x20, 0x73, 0x74, 0x81, 0x72, 0xE3, 0x72, 0x7F, 0x69,
0x6E, 0x67, 0x20, 0x68, 0x61, 0x6E, 0x67, 0x73, 0x2E, 0x00, 0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12,
0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E,
0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C,
0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A,
0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74,
0x75, 0x27, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, 0x80, 0x81,
0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D,
];
let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data);
Packbits.write_to(&mut writer, data.as_slice()).unwrap();
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
}
#[test]
fn test_packbits() {
// compress teststring
const EXPECTED_COMPRESSED_DATA: &[u8] =
b"\x3CThis is a string for checking various compression algorithms.";
let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data);
Packbits.write_to(&mut writer, TEST_DATA).unwrap();
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
}
}
+34
View File
@@ -0,0 +1,34 @@
use crate::encoder::compression::*;
/// The default algorithm which does not compress at all.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Uncompressed;
impl Compression for Uncompressed {
const COMPRESSION_METHOD: CompressionMethod = CompressionMethod::None;
fn get_algorithm(&self) -> Compressor {
Compressor::Uncompressed(*self)
}
}
impl CompressionAlgorithm for Uncompressed {
fn write_to<W: Write>(&mut self, writer: &mut W, bytes: &[u8]) -> Result<u64, io::Error> {
writer.write_all(bytes).map(|()| bytes.len() as u64)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encoder::compression::tests::TEST_DATA;
use std::io::Cursor;
#[test]
fn test_no_compression() {
let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data);
Uncompressed.write_to(&mut writer, TEST_DATA).unwrap();
assert_eq!(TEST_DATA, compressed_data);
}
}
+660 -102
View File
@@ -2,32 +2,85 @@ pub use tiff_value::*;
use std::{ use std::{
cmp, cmp,
collections::BTreeMap,
convert::{TryFrom, TryInto},
io::{self, Seek, Write}, io::{self, Seek, Write},
marker::PhantomData, marker::PhantomData,
mem, mem,
num::TryFromIntError, num::{NonZeroU64, TryFromIntError},
}; };
use crate::{ use crate::{
error::TiffResult, decoder::ifd::Entry,
tags::{self, ResolutionUnit, Tag}, error::{TiffResult, UsageError},
tags::{
ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation,
ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
},
Directory, TiffError, TiffFormatError,
}; };
pub mod colortype; pub mod colortype;
pub mod compression;
mod tiff_value; mod tiff_value;
mod writer; mod writer;
use self::colortype::*; use self::colortype::*;
use self::compression::Compression as Comp;
use self::compression::*;
use self::writer::*; use self::writer::*;
/// Type of prediction to prepare the image with.
///
/// 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!
///
/// [Predictor::Horizontal] is for integer sample types.
/// [Predictor::FloatingPoint] is for floating-point sample types (f32, f64).
pub type Predictor = crate::tags::Predictor;
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Default)]
pub enum Compression {
#[default]
Uncompressed,
#[cfg(feature = "lzw")]
Lzw,
#[cfg(feature = "deflate")]
Deflate(u8),
Packbits,
}
impl Compression {
fn tag(&self) -> CompressionMethod {
match self {
Compression::Uncompressed => CompressionMethod::None,
#[cfg(feature = "lzw")]
Compression::Lzw => CompressionMethod::LZW,
#[cfg(feature = "deflate")]
Compression::Deflate(_) => CompressionMethod::Deflate,
Compression::Packbits => CompressionMethod::PackBits,
}
}
fn get_algorithm(&self) -> Compressor {
match self {
Compression::Uncompressed => compression::Uncompressed {}.get_algorithm(),
#[cfg(feature = "lzw")]
Compression::Lzw => compression::Lzw {}.get_algorithm(),
#[cfg(feature = "deflate")]
Compression::Deflate(level) => compression::Deflate::with_level(*level).get_algorithm(),
Compression::Packbits => compression::Packbits {}.get_algorithm(),
}
}
}
/// Encoder for Tiff and BigTiff files. /// Encoder for Tiff and BigTiff files.
/// ///
/// With this type you can get a `DirectoryEncoder` or a `ImageEncoder` /// With this type you can get a [`DirectoryEncoder`] or a [`ImageEncoder`]
/// to encode Tiff/BigTiff ifd directories with images. /// to encode Tiff/BigTiff ifd directories with images.
/// ///
/// See `DirectoryEncoder` and `ImageEncoder`. /// See [`DirectoryEncoder`] and [`ImageEncoder`].
/// ///
/// # Examples /// # Examples
/// ``` /// ```
@@ -50,6 +103,10 @@ use self::writer::*;
pub struct TiffEncoder<W, K: TiffKind = TiffKindStandard> { pub struct TiffEncoder<W, K: TiffKind = TiffKindStandard> {
writer: TiffWriter<W>, writer: TiffWriter<W>,
kind: PhantomData<K>, kind: PhantomData<K>,
predictor: Predictor,
compression: Compression,
/// The offset of the last main image directory's `next` field.
last_ifd_chain: NonZeroU64,
} }
/// Constructor functions to create standard Tiff files. /// Constructor functions to create standard Tiff files.
@@ -78,19 +135,60 @@ impl<W: Write + Seek> TiffEncoder<W, TiffKindBig> {
impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> { impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
/// Creates a new Tiff or BigTiff encoder, inferred from the return type. /// Creates a new Tiff or BigTiff encoder, inferred from the return type.
pub fn new_generic(writer: W) -> TiffResult<Self> { pub fn new_generic(writer: W) -> TiffResult<Self> {
let mut encoder = TiffEncoder { let mut writer = TiffWriter::new(writer);
writer: TiffWriter::new(writer), K::write_header(&mut writer)?;
let last_ifd_chain = NonZeroU64::new(writer.previous_ifd_pointer::<K>())
.expect("Header is at a non-zero offset");
Ok(TiffEncoder {
writer,
kind: PhantomData, kind: PhantomData,
}; predictor: Predictor::None,
compression: Compression::Uncompressed,
last_ifd_chain,
})
}
K::write_header(&mut encoder.writer)?; /// Set the predictor to use
///
/// A predictor is used to simplify the file before writing it. This is very
/// useful when writing a file compressed using LZW as it can improve efficiency
pub fn with_predictor(mut self, predictor: Predictor) -> Self {
self.predictor = predictor;
Ok(encoder) self
}
/// Set the compression method to use
pub fn with_compression(mut self, compression: Compression) -> Self {
self.compression = compression;
self
} }
/// Create a [`DirectoryEncoder`] to encode an ifd directory. /// Create a [`DirectoryEncoder`] to encode an ifd directory.
pub fn new_directory(&mut self) -> TiffResult<DirectoryEncoder<W, K>> { #[deprecated = "`image_directory` replaced the old behavior and clarifies the intent"]
DirectoryEncoder::new(&mut self.writer) #[doc(hidden)]
pub fn new_directory(&mut self) -> TiffResult<DirectoryEncoder<'_, W, K>> {
Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)
}
/// Create a [`DirectoryEncoder`] to encode an ifd (directory).
///
/// The caller is responsible for ensuring that the directory is a valid image in the main TIFF
/// IFD sequence. To encode additional directories that are not linked into the sequence, use
/// [`Self::extra_directory`][TiffEncoder::extra_directory].
pub fn image_directory(&mut self) -> TiffResult<DirectoryEncoder<'_, W, K>> {
Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)
}
/// Create a [`DirectoryEncoder`] to encode a non-image directory.
///
/// The directory is not linked into the sequence of directories. For instance, encode Exif
/// directories or SubIfd directories with this method.
pub fn extra_directory(&mut self) -> TiffResult<DirectoryEncoder<'_, W, K>> {
Self::unchained_directory(&mut self.writer)
} }
/// Create an [`ImageEncoder`] to encode an image one slice at a time. /// Create an [`ImageEncoder`] to encode an image one slice at a time.
@@ -98,9 +196,9 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
&mut self, &mut self,
width: u32, width: u32,
height: u32, height: u32,
) -> TiffResult<ImageEncoder<W, C, K>> { ) -> TiffResult<ImageEncoder<'_, W, C, K>> {
let encoder = DirectoryEncoder::new(&mut self.writer)?; let encoder = Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)?;
ImageEncoder::new(encoder, width, height) ImageEncoder::new(encoder, width, height, self.compression, self.predictor)
} }
/// Convenience function to write an entire image from memory. /// Convenience function to write an entire image from memory.
@@ -113,10 +211,23 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
where where
[C::Inner]: TiffValue, [C::Inner]: TiffValue,
{ {
let encoder = DirectoryEncoder::new(&mut self.writer)?; let encoder = Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)?;
let image: ImageEncoder<W, C, K> = ImageEncoder::new(encoder, width, height)?; let image: ImageEncoder<W, C, K> =
ImageEncoder::new(encoder, width, height, self.compression, self.predictor)?;
image.write_data(data) image.write_data(data)
} }
fn chain_directory<'lt>(
writer: &'lt mut TiffWriter<W>,
last_ifd_chain: &'lt mut NonZeroU64,
) -> TiffResult<DirectoryEncoder<'lt, W, K>> {
let last_ifd = *last_ifd_chain;
DirectoryEncoder::new(writer, Some(last_ifd), Some(last_ifd_chain))
}
fn unchained_directory(writer: &mut TiffWriter<W>) -> TiffResult<DirectoryEncoder<'_, W, K>> {
DirectoryEncoder::new(writer, None, None)
}
} }
/// Low level interface to encode ifd directories. /// Low level interface to encode ifd directories.
@@ -125,115 +236,342 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
/// Encoding can silently fail while this is dropping. /// Encoding can silently fail while this is dropping.
pub struct DirectoryEncoder<'a, W: 'a + Write + Seek, K: TiffKind> { pub struct DirectoryEncoder<'a, W: 'a + Write + Seek, K: TiffKind> {
writer: &'a mut TiffWriter<W>, writer: &'a mut TiffWriter<W>,
dropped: bool, /// The position of the previous directory's `next` field, if any.
chained_ifd_pos: Option<NonZeroU64>,
/// An output to write the `next` field offset on completion.
write_chain: Option<&'a mut NonZeroU64>,
kind: PhantomData<K>,
// We use BTreeMap to make sure tags are written in correct order // We use BTreeMap to make sure tags are written in correct order
ifd_pointer_pos: u64, directory: Directory,
ifd: BTreeMap<u16, DirectoryEntry<K::OffsetType>>, dropped: bool,
}
/// The offset of an encoded directory in the file.
///
/// Both the `offset` and `pointer` field are a complete description of the start offset of the
/// dictionary which is used to point to it. The struct also describes the extent covered by the
/// encoding of the directory. The constructors [`new`][DirectoryOffset::new] allow the checked
/// conversion. Note that even though the offsets are public do not expect their information to
/// propagate when modified.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DirectoryOffset<K: TiffKind> {
/// The start of the directory as a Tiff value.
///
/// This is a bit of a wart in the strongly typed design of the encoder. The value _type_ must
/// itself know how to be represented in the Tiff file but that may differ based on endianess
/// as well as the offset size (`u32` or BigTIFF's `u64`). Thankfully we're allowed to
/// represent offsets with `LONG` or `IFD` in the usual case.
pub offset: K::OffsetType,
/// The start of the directory as a pure offset.
pub pointer: IfdPointer,
/// The offset of its sequence link field, in our private representation. Before exposing this
/// make sure that we don't want any invariants (e.g. pointer being smaller than this). The
/// type should be fine.
ifd_chain: NonZeroU64,
/// The kind of Tiff file the offset is for.
kind: PhantomData<K>,
} }
impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
fn new(writer: &'a mut TiffWriter<W>) -> TiffResult<Self> { /// Construct a directory writer appending to data, assuming the writer is currently positioned
// the previous word is the IFD offset position /// immediately after a previously written IFD to which to append.
let ifd_pointer_pos = writer.offset() - mem::size_of::<K::OffsetType>() as u64; fn new(
writer: &'a mut TiffWriter<W>,
chained_ifd_pos: Option<NonZeroU64>,
chain_into: Option<&'a mut NonZeroU64>,
) -> TiffResult<Self> {
writer.pad_word_boundary()?; // TODO: Do we need to adjust this for BigTiff? writer.pad_word_boundary()?; // TODO: Do we need to adjust this for BigTiff?
Ok(DirectoryEncoder { Ok(Self {
writer, writer,
chained_ifd_pos,
write_chain: chain_into,
kind: PhantomData,
directory: Directory::empty(),
dropped: false, dropped: false,
ifd_pointer_pos,
ifd: BTreeMap::new(),
}) })
} }
/// Write a single ifd tag. /// Write a single ifd tag.
pub fn write_tag<T: TiffValue>(&mut self, tag: Tag, value: T) -> TiffResult<()> { pub fn write_tag<T: TiffValue>(&mut self, tag: Tag, value: T) -> TiffResult<()> {
// Encodes the value if necessary. In the current bytes all integers are taken as native
// endian and thus transparent but this keeps the interface generic.
let mut bytes = Vec::with_capacity(value.bytes()); let mut bytes = Vec::with_capacity(value.bytes());
{ {
let mut writer = TiffWriter::new(&mut bytes); let mut writer = TiffWriter::new(&mut bytes);
value.write(&mut writer)?; value.write(&mut writer)?;
} }
self.ifd.insert( let entry = Self::write_entry_inner(
tag.to_u16(), self.writer,
DirectoryEntry { DirectoryEntry {
data_type: <T>::FIELD_TYPE.to_u16(), data_type: <T>::FIELD_TYPE,
count: value.count().try_into()?, count: value.count().try_into()?,
data: bytes, data: bytes.into(),
}, },
); )?;
self.directory.extend([(tag, entry)]);
Ok(()) Ok(())
} }
fn write_directory(&mut self) -> TiffResult<u64> { /// Write a tag-value pair with prepared byte data.
// Start by writing out all values ///
for &mut DirectoryEntry { /// Note that the library will _not_ attempt to verify that the data type or the count of the
data: ref mut bytes, /// buffered value is permissible for the given tag. The data will be associated with the tag
.. /// exactly as-is.
} in self.ifd.values_mut() ///
{ /// An error is returned if the byte order of the presented value does not match the byte order
let data_bytes = mem::size_of::<K::OffsetType>(); /// 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)]);
if bytes.len() > data_bytes { Ok(())
let offset = self.writer.offset();
self.writer.write_bytes(bytes)?;
*bytes = vec![0; data_bytes];
let mut writer = TiffWriter::new(bytes as &mut [u8]);
K::write_offset(&mut writer, offset)?;
} else {
while bytes.len() < data_bytes {
bytes.push(0);
}
}
}
let offset = self.writer.offset();
K::write_entry_count(&mut self.writer, self.ifd.len())?;
for (
tag,
&DirectoryEntry {
data_type: ref field_type,
ref count,
data: ref offset,
},
) in self.ifd.iter()
{
self.writer.write_u16(*tag)?;
self.writer.write_u16(*field_type)?;
(*count).write(&mut self.writer)?;
self.writer.write_bytes(offset)?;
}
Ok(offset)
} }
/// Write some data to the tiff file, the offset of the data is returned. /// 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> { pub fn write_data<T: TiffValue>(&mut self, value: T) -> TiffResult<u64> {
let offset = self.writer.offset(); let offset = self.writer.offset();
value.write(&mut self.writer)?; value.write(self.writer)?;
Ok(offset) Ok(offset)
} }
fn finish_internal(&mut self) -> TiffResult<()> { /// Write some data directly to the tiff file, the offset of the data is returned.
let ifd_pointer = self.write_directory()?; ///
let curr_pos = self.writer.offset(); /// 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)
}
self.writer.goto_offset(self.ifd_pointer_pos)?; /// Write a directory value into the file.
K::write_offset(&mut self.writer, ifd_pointer)?; ///
self.writer.goto_offset(curr_pos)?; /// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
K::write_offset(&mut self.writer, 0)?; /// 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(),
},
)
}
self.dropped = true; /// 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
/// actually (or will be) written to the file. Note that small values are necessarily inline in
/// the entry, the size limit depends on the Tiff kind. So do not confuse a directory from a
/// BigTiff in a standard tiff or the other way around.
pub fn extend_from(&mut self, dir: &Directory) {
let entries = dir.iter().map(|(tag, val)| (tag, val.clone()));
self.directory.extend(entries);
}
/// Define the parent directory.
///
/// Each directory has an offset based link to its successor, forming a linked list in the
/// file. The encoder writes its own offset into the parent's field when [`Self::finish`] is
/// called or it is dropped. This redefines the parent. An offset representation of the parent
/// must be acquired by finishing it with [`Self::finish_with_offsets`].
pub fn set_parent(&mut self, offset: &DirectoryOffset<K>) {
self.chained_ifd_pos = Some(offset.ifd_chain);
}
/// Write out the ifd directory itself.
pub fn finish(mut self) -> TiffResult<()> {
self.finish_internal()?;
Ok(()) Ok(())
} }
/// Write out the ifd directory. /// Write the IFD to the file and return the offset it is written to.
pub fn finish(mut self) -> TiffResult<()> { ///
/// The offset can be used as the value, or as part of a list of values, in the entry of
/// another directory. If you're constructing the entry directly you'll want to use an
/// appropriate type variant for this such as [`Type::IFD`].
pub fn finish_with_offsets(mut self) -> TiffResult<DirectoryOffset<K>> {
self.finish_internal() self.finish_internal()
} }
fn write_directory(&mut self) -> TiffResult<u64> {
// Start by turning all buffered unwritten values into entries.
let offset = self.writer.offset();
K::write_entry_count(self.writer, self.directory.len())?;
let offset_bytes = mem::size_of::<K::OffsetType>();
for (tag, entry) in self.directory.iter() {
self.writer.write_u16(tag.to_u16())?;
self.writer.write_u16(entry.field_type().to_u16())?;
let count = K::convert_offset(entry.count())?;
count.write(self.writer)?;
self.writer.write_bytes(&entry.offset()[..offset_bytes])?;
}
Ok(offset)
}
// 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>,
) -> TiffResult<Entry> {
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 {
let offset = writer.offset();
writer.write_bytes(bytes)?;
let offset = K::convert_offset(offset)?;
offset_bytes[..offset.bytes()].copy_from_slice(&offset.data());
} else {
// Note: we have indicated our native byte order in the header, hence this
// corresponds to our byte order no matter the value type.
offset_bytes[..bytes.len()].copy_from_slice(bytes);
}
// Undoing some hidden type. Offset is either u32 or u64. Due to the trait API being public
// 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());
Ok(if in_entry_bytes == 4 {
let count = u32::from_ne_bytes(count_bytes[..4].try_into().unwrap());
Entry::new(data_type, count, offset_bytes[..4].try_into().unwrap())
} else {
debug_assert_eq!(in_entry_bytes, 8);
let count = u64::from_ne_bytes(count_bytes);
Entry::new_u64(data_type, count, offset_bytes)
})
}
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()
}
fn finish_internal(&mut self) -> TiffResult<DirectoryOffset<K>> {
let ifd_pointer = self.write_directory()?;
let offset = K::convert_offset(ifd_pointer)?;
if let Some(prior) = self.chained_ifd_pos {
let curr_pos = self.writer.offset();
self.writer.goto_offset(prior.get())?;
// Note how we are not writing the `offset` type itself here as doing so would need to
// go through the `TiffValue` trait—the type is not constrained by much. But for the
// trait we need a `TiffWriter` which comes with a bunch of additional state such as
// compressor etc. that we have no need for.
K::write_offset(self.writer, ifd_pointer)?;
self.writer.goto_offset(curr_pos)?;
}
K::write_offset(self.writer, 0)?;
let ifd_chain = NonZeroU64::new(self.writer.previous_ifd_pointer::<K>())
.expect("IFD chain field is at a non-zero offset");
if let Some(prior) = self.write_chain.take() {
*prior = ifd_chain;
}
self.dropped = true;
Ok(DirectoryOffset {
pointer: IfdPointer(ifd_pointer),
offset,
ifd_chain,
kind: PhantomData,
})
}
} }
impl<'a, W: Write + Seek, K: TiffKind> Drop for DirectoryEncoder<'a, W, K> { impl<'a, W: Write + Seek, K: TiffKind> Drop for DirectoryEncoder<'a, W, K> {
@@ -284,33 +622,74 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> {
row_samples: u64, row_samples: u64,
width: u32, width: u32,
height: u32, height: u32,
extra_samples: Vec<u16>,
rows_per_strip: u64, rows_per_strip: u64,
strip_offsets: Vec<K::OffsetType>, strip_offsets: Vec<K::OffsetType>,
strip_byte_count: Vec<K::OffsetType>, strip_byte_count: Vec<K::OffsetType>,
dropped: bool, dropped: bool,
compression: Compression,
predictor: Predictor,
_phantom: ::std::marker::PhantomData<C>, _phantom: ::std::marker::PhantomData<C>,
} }
impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> { impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> {
fn new(mut encoder: DirectoryEncoder<'a, W, K>, width: u32, height: u32) -> TiffResult<Self> { 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::PredictorIncompatible))
}
_ => Ok(()),
}
}
fn new(
mut encoder: DirectoryEncoder<'a, W, K>,
width: u32,
height: u32,
compression: Compression,
predictor: Predictor,
) -> TiffResult<Self> {
if width == 0 || height == 0 {
return Err(TiffError::FormatError(TiffFormatError::InvalidDimensions(
width, height,
)));
}
Self::sanity_check(compression, predictor)?;
let row_samples = u64::from(width) * u64::try_from(<T>::BITS_PER_SAMPLE.len())?; let row_samples = u64::from(width) * u64::try_from(<T>::BITS_PER_SAMPLE.len())?;
let row_bytes = row_samples * u64::from(<T::Inner>::BYTE_LEN); let row_bytes = row_samples * u64::from(<T::Inner>::BYTE_LEN);
// Limit the strip size to prevent potential memory and security issues. // Limit the strip size to prevent potential memory and security issues.
// Also keep the multiple strip handling 'oiled' // Also keep the multiple strip handling 'oiled'
let rows_per_strip = (1_000_000 + row_bytes - 1) / row_bytes; let rows_per_strip = {
match compression.tag() {
CompressionMethod::PackBits => 1, // Each row must be packed separately. Do not compress across row boundaries
_ => 1_000_000_u64.div_ceil(row_bytes),
}
};
let strip_count = (u64::from(height) + rows_per_strip - 1) / rows_per_strip; let strip_count = u64::from(height).div_ceil(rows_per_strip);
encoder.write_tag(Tag::ImageWidth, width)?; encoder.write_tag(Tag::ImageWidth, width)?;
encoder.write_tag(Tag::ImageLength, height)?; encoder.write_tag(Tag::ImageLength, height)?;
encoder.write_tag(Tag::Compression, tags::CompressionMethod::None.to_u16())?; 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())?; 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(Tag::RowsPerStrip, u32::try_from(rows_per_strip)?)?;
encoder.write_tag( encoder.write_tag(
@@ -319,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::XResolution, Rational { n: 1, d: 1 })?;
encoder.write_tag(Tag::YResolution, 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 { Ok(ImageEncoder {
encoder, encoder,
@@ -327,15 +706,39 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
strip_idx: 0, strip_idx: 0,
row_samples, row_samples,
rows_per_strip, rows_per_strip,
extra_samples: vec![],
width, width,
height, height,
strip_offsets: Vec::new(), strip_offsets: Vec::new(),
strip_byte_count: Vec::new(), strip_byte_count: Vec::new(),
dropped: false, dropped: false,
compression,
predictor,
_phantom: ::std::marker::PhantomData, _phantom: ::std::marker::PhantomData,
}) })
} }
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. /// Number of samples the next strip should have.
pub fn next_strip_sample_count(&self) -> u64 { pub fn next_strip_sample_count(&self) -> u64 {
if self.strip_idx >= self.strip_count { if self.strip_idx >= self.strip_count {
@@ -354,7 +757,6 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
where where
[T::Inner]: TiffValue, [T::Inner]: TiffValue,
{ {
// TODO: Compression
let samples = self.next_strip_sample_count(); let samples = self.next_strip_sample_count();
if u64::try_from(value.len())? != samples { if u64::try_from(value.len())? != samples {
return Err(io::Error::new( return Err(io::Error::new(
@@ -364,9 +766,29 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
.into()); .into());
} }
let offset = self.encoder.write_data(value)?; // Write the (possible compressed) data to the encoder.
let offset = match self.predictor {
Predictor::None => self.encoder.write_data(value)?,
Predictor::Horizontal => {
let mut row_result = Vec::with_capacity(value.len());
for row in value.chunks_exact(self.row_samples as usize) {
T::horizontal_predict(row, &mut row_result);
}
self.encoder.write_data(row_result.as_slice())?
}
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;
self.strip_offsets.push(K::convert_offset(offset)?); self.strip_offsets.push(K::convert_offset(offset)?);
self.strip_byte_count.push(value.bytes().try_into()?); self.strip_byte_count.push(byte_count.try_into()?);
self.strip_idx += 1; self.strip_idx += 1;
Ok(()) Ok(())
@@ -392,12 +814,19 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
) )
.into()); .into());
} }
self.encoder
.writer
.set_compression(self.compression.get_algorithm());
let mut idx = 0; let mut idx = 0;
while self.next_strip_sample_count() > 0 { while self.next_strip_sample_count() > 0 {
let sample_count = usize::try_from(self.next_strip_sample_count())?; let sample_count = usize::try_from(self.next_strip_sample_count())?;
self.write_strip(&data[idx..idx + sample_count])?; self.write_strip(&data[idx..idx + sample_count])?;
idx += sample_count; idx += sample_count;
} }
self.encoder.writer.reset_compression();
self.finish()?; self.finish()?;
Ok(()) Ok(())
} }
@@ -446,13 +875,46 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
self.encoder.write_tag(Tag::RowsPerStrip, value)?; self.encoder.write_tag(Tag::RowsPerStrip, value)?;
let value: u64 = value as u64; let value: u64 = value as u64;
self.strip_count = (self.height as u64 + value - 1) / value; self.strip_count = (self.height as u64).div_ceil(value);
self.rows_per_strip = value; self.rows_per_strip = value;
Ok(()) Ok(())
} }
fn finish_internal(&mut self) -> TiffResult<()> { 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 self.encoder
.write_tag(Tag::StripOffsets, K::convert_slice(&self.strip_offsets))?; .write_tag(Tag::StripOffsets, K::convert_slice(&self.strip_offsets))?;
self.encoder.write_tag( self.encoder.write_tag(
@@ -471,7 +933,8 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
/// Write out image and ifd directory. /// Write out image and ifd directory.
pub fn finish(mut self) -> TiffResult<()> { pub fn finish(mut self) -> TiffResult<()> {
self.finish_internal() self.finish_internal()?;
Ok(())
} }
} }
@@ -483,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: u16, data_type: Type,
count: S, count: S,
data: Vec<u8>, data: std::borrow::Cow<'data, [u8]>,
} }
/// Trait to abstract over Tiff/BigTiff differences. /// Trait to abstract over Tiff/BigTiff differences.
@@ -531,6 +994,71 @@ pub trait TiffKind {
fn convert_slice(slice: &[Self::OffsetType]) -> &Self::OffsetArrayType; fn convert_slice(slice: &[Self::OffsetType]) -> &Self::OffsetArrayType;
} }
impl<K: TiffKind> DirectoryOffset<K> {
/// Construct the directory pointer information from its raw offset.
///
/// Fails in these cases:
///
/// - if start offset is too large to be represented with the chosen TIFF format, i.e. a value
/// larger than [`u32::MAX`] with a standard tiff which is only valid in a BigTiff. This
/// returns [`TiffError::IntSizeError`].
/// - the extent of the directory would overflow the file length. This returns a
/// [`TiffError::UsageError`].
///
/// The library does *not* validate the contents or offset in any way, it just performs the
/// necessary math assuming the data is an accurate representation of existing content.
///
/// # Examples
///
/// If you have any custom way of writing a directory to a file that can not go through the
/// usual [`TiffEncoder::extra_directory`] then you can reconstruct the offset information as
/// long as you provide all the necessary data to the library. You can then use it as a parent
/// directory, i.e. have the library overwrite its `next` field.
///
/// ```
/// use tiff::{
/// encoder::{TiffEncoder, DirectoryOffset},
/// Directory,
/// tags::IfdPointer,
/// };
///
/// # let mut file = std::io::Cursor::new(Vec::new());
/// let mut tiff = TiffEncoder::new(&mut file)?;
///
/// // … some custom data writes, assume we know where a directory was written
/// # fn reconstruction_of_your_directory() -> Directory { Directory::from_iter([]) }
/// let known_offset = IfdPointer(1024);
/// let known_contents: Directory = reconstruction_of_your_directory();
///
/// let reconstructed = DirectoryOffset::new(known_offset, &known_contents)?;
///
/// // Now we can use it as if the directory was written by the `TiffEncoder` itself.
/// let mut chain = tiff.extra_directory()?;
/// chain.set_parent(&reconstructed);
/// chain.finish_with_offsets()?;
///
/// # Ok::<_, tiff::TiffError>(())
/// ```
pub fn new(pointer: IfdPointer, dir: &Directory) -> TiffResult<Self> {
let offset = K::convert_offset(pointer.0)?;
let encoded = dir.encoded_len::<K>();
let offset_field_offset = encoded - mem::size_of_val(&offset) as u64;
let ifd_chain = pointer
.0
.checked_add(offset_field_offset)
.and_then(NonZeroU64::new)
.ok_or(TiffError::UsageError(UsageError::ZeroIfdPointer))?;
Ok(DirectoryOffset {
pointer,
offset,
ifd_chain,
kind: PhantomData,
})
}
}
/// Create a standard Tiff file. /// Create a standard Tiff file.
pub struct TiffKindStandard; pub struct TiffKindStandard;
@@ -599,3 +1127,33 @@ impl TiffKind for TiffKindBig {
slice slice
} }
} }
#[test]
fn directory_offset_new_equivalent_to_writing() {
type K = TiffKindStandard;
let dir = Directory::from_iter(vec![
(
Tag::ImageWidth,
Entry::new(Type::LONG, 1, 100u32.to_ne_bytes()),
),
(
Tag::ImageLength,
Entry::new(Type::LONG, 1, 200u32.to_ne_bytes()),
),
]);
let data = std::io::Cursor::new(vec![]);
let mut file = TiffEncoder::new(data).unwrap();
let mut as_dir_encoder = file.extra_directory().unwrap();
as_dir_encoder.extend_from(&dir);
let dir_extent = as_dir_encoder.finish_with_offsets().unwrap();
// Check we can recreate the extent had we written ourselves.
let synth = DirectoryOffset::<K>::new(dir_extent.pointer, &dir).unwrap();
assert_eq!(synth.pointer, dir_extent.pointer);
assert_eq!(synth.offset, dir_extent.offset);
assert_eq!(synth.ifd_chain, dir_extent.ifd_chain);
}
+313 -109
View File
@@ -1,18 +1,42 @@
use std::io::Write; use std::{borrow::Cow, io::Write, slice::from_ref};
use crate::{bytecast, tags::Type, TiffError, TiffFormatError, TiffResult}; use crate::{bytecast, tags, tags::Type, TiffError, TiffFormatError, TiffResult};
use super::writer::TiffWriter; use super::writer::TiffWriter;
/// Trait for types that can be encoded in a tiff file /// Trait for types that can be encoded in a tiff file
#[diagnostic::on_unimplemented(
message = "the type `{Self}` does not implement `TiffValue`",
note = "the trait is implemented for primitive types (`u8`, `i16`, `f32`, etc.)",
note = "the trait is implemented for shared references to values",
note = "the trait is implemented for slices, pass them by reference (e.g. `&[u8]`)",
note = "the trait is implemented for arrays if it is implemented for a slice",
note = "values in a `Vec` or `Box` should be dereferenced, e.g. `&vec[..]`"
)]
pub trait TiffValue { pub trait TiffValue {
const BYTE_LEN: u8; const BYTE_LEN: u8;
const FIELD_TYPE: Type; const FIELD_TYPE: Type;
// FIXME: If we want this to mean 'count' in the sense of `Entry` it should return `u32` or
// `u64`, probably the latter with `convert_offset` taking care of the check against `u32::MAX`
// for classic tiff.
fn count(&self) -> usize; fn count(&self) -> usize;
fn bytes(&self) -> usize { fn bytes(&self) -> usize {
self.count() * usize::from(Self::BYTE_LEN) self.count() * usize::from(Self::BYTE_LEN)
} }
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()>;
/// Access this value as an contiguous sequence of bytes.
/// If their is no trivial representation, allocate it on the heap.
fn data(&self) -> Cow<'_, [u8]>;
/// Write this value to a TiffWriter.
/// While the default implementation will work in all cases, it may require unnecessary allocations.
/// The written bytes of any custom implementation MUST be the same as yielded by `self.data()`.
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> {
writer.write_bytes(&self.data())?;
Ok(())
}
} }
impl TiffValue for [u8] { impl TiffValue for [u8] {
@@ -23,9 +47,8 @@ impl TiffValue for [u8] {
self.len() self.len()
} }
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> { fn data(&self) -> Cow<'_, [u8]> {
writer.write_bytes(self)?; Cow::Borrowed(self)
Ok(())
} }
} }
@@ -37,10 +60,8 @@ impl TiffValue for [i8] {
self.len() self.len()
} }
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> { fn data(&self) -> Cow<'_, [u8]> {
let slice = bytecast::i8_as_ne_bytes(self); Cow::Borrowed(bytecast::i8_as_ne_bytes(self))
writer.write_bytes(slice)?;
Ok(())
} }
} }
@@ -52,10 +73,8 @@ impl TiffValue for [u16] {
self.len() self.len()
} }
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> { fn data(&self) -> Cow<'_, [u8]> {
let slice = bytecast::u16_as_ne_bytes(self); Cow::Borrowed(bytecast::u16_as_ne_bytes(self))
writer.write_bytes(slice)?;
Ok(())
} }
} }
@@ -67,10 +86,8 @@ impl TiffValue for [i16] {
self.len() self.len()
} }
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> { fn data(&self) -> Cow<'_, [u8]> {
let slice = bytecast::i16_as_ne_bytes(self); Cow::Borrowed(bytecast::i16_as_ne_bytes(self))
writer.write_bytes(slice)?;
Ok(())
} }
} }
@@ -82,10 +99,8 @@ impl TiffValue for [u32] {
self.len() self.len()
} }
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> { fn data(&self) -> Cow<'_, [u8]> {
let slice = bytecast::u32_as_ne_bytes(self); Cow::Borrowed(bytecast::u32_as_ne_bytes(self))
writer.write_bytes(slice)?;
Ok(())
} }
} }
@@ -97,10 +112,8 @@ impl TiffValue for [i32] {
self.len() self.len()
} }
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> { fn data(&self) -> Cow<'_, [u8]> {
let slice = bytecast::i32_as_ne_bytes(self); Cow::Borrowed(bytecast::i32_as_ne_bytes(self))
writer.write_bytes(slice)?;
Ok(())
} }
} }
@@ -112,10 +125,8 @@ impl TiffValue for [u64] {
self.len() self.len()
} }
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> { fn data(&self) -> Cow<'_, [u8]> {
let slice = bytecast::u64_as_ne_bytes(self); Cow::Borrowed(bytecast::u64_as_ne_bytes(self))
writer.write_bytes(slice)?;
Ok(())
} }
} }
@@ -127,10 +138,8 @@ impl TiffValue for [i64] {
self.len() self.len()
} }
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> { fn data(&self) -> Cow<'_, [u8]> {
let slice = bytecast::i64_as_ne_bytes(self); Cow::Borrowed(bytecast::i64_as_ne_bytes(self))
writer.write_bytes(slice)?;
Ok(())
} }
} }
@@ -142,11 +151,9 @@ impl TiffValue for [f32] {
self.len() self.len()
} }
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> { fn data(&self) -> Cow<'_, [u8]> {
// We write using nativeedian so this sould be safe // We write using native endian so this should be safe
let slice = bytecast::f32_as_ne_bytes(self); Cow::Borrowed(bytecast::f32_as_ne_bytes(self))
writer.write_bytes(slice)?;
Ok(())
} }
} }
@@ -158,75 +165,9 @@ impl TiffValue for [f64] {
self.len() self.len()
} }
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> { fn data(&self) -> Cow<'_, [u8]> {
// We write using nativeedian so this sould be safe // We write using native endian so this should be safe
let slice = bytecast::f64_as_ne_bytes(self); Cow::Borrowed(bytecast::f64_as_ne_bytes(self))
writer.write_bytes(slice)?;
Ok(())
}
}
impl TiffValue for [Ifd] {
const BYTE_LEN: u8 = 4;
const FIELD_TYPE: Type = Type::IFD;
fn count(&self) -> usize {
self.len()
}
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> {
for x in self {
x.write(writer)?;
}
Ok(())
}
}
impl TiffValue for [Ifd8] {
const BYTE_LEN: u8 = 8;
const FIELD_TYPE: Type = Type::IFD8;
fn count(&self) -> usize {
self.len()
}
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> {
for x in self {
x.write(writer)?;
}
Ok(())
}
}
impl TiffValue for [Rational] {
const BYTE_LEN: u8 = 8;
const FIELD_TYPE: Type = Type::RATIONAL;
fn count(&self) -> usize {
self.len()
}
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> {
for x in self {
x.write(writer)?;
}
Ok(())
}
}
impl TiffValue for [SRational] {
const BYTE_LEN: u8 = 8;
const FIELD_TYPE: Type = Type::SRATIONAL;
fn count(&self) -> usize {
self.len()
}
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> {
for x in self {
x.write(writer)?;
}
Ok(())
} }
} }
@@ -242,6 +183,10 @@ impl TiffValue for u8 {
writer.write_u8(*self)?; writer.write_u8(*self)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(from_ref(self))
}
} }
impl TiffValue for i8 { impl TiffValue for i8 {
@@ -256,6 +201,10 @@ impl TiffValue for i8 {
writer.write_i8(*self)?; writer.write_i8(*self)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i8_as_ne_bytes(from_ref(self)))
}
} }
impl TiffValue for u16 { impl TiffValue for u16 {
@@ -270,6 +219,10 @@ impl TiffValue for u16 {
writer.write_u16(*self)?; writer.write_u16(*self)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u16_as_ne_bytes(from_ref(self)))
}
} }
impl TiffValue for i16 { impl TiffValue for i16 {
@@ -284,6 +237,10 @@ impl TiffValue for i16 {
writer.write_i16(*self)?; writer.write_i16(*self)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i16_as_ne_bytes(from_ref(self)))
}
} }
impl TiffValue for u32 { impl TiffValue for u32 {
@@ -298,6 +255,10 @@ impl TiffValue for u32 {
writer.write_u32(*self)?; writer.write_u32(*self)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(self)))
}
} }
impl TiffValue for i32 { impl TiffValue for i32 {
@@ -312,6 +273,10 @@ impl TiffValue for i32 {
writer.write_i32(*self)?; writer.write_i32(*self)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i32_as_ne_bytes(from_ref(self)))
}
} }
impl TiffValue for u64 { impl TiffValue for u64 {
@@ -326,6 +291,10 @@ impl TiffValue for u64 {
writer.write_u64(*self)?; writer.write_u64(*self)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(self)))
}
} }
impl TiffValue for i64 { impl TiffValue for i64 {
@@ -340,6 +309,10 @@ impl TiffValue for i64 {
writer.write_i64(*self)?; writer.write_i64(*self)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i64_as_ne_bytes(from_ref(self)))
}
} }
impl TiffValue for f32 { impl TiffValue for f32 {
@@ -354,6 +327,10 @@ impl TiffValue for f32 {
writer.write_f32(*self)?; writer.write_f32(*self)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::f32_as_ne_bytes(from_ref(self)))
}
} }
impl TiffValue for f64 { impl TiffValue for f64 {
@@ -368,6 +345,10 @@ impl TiffValue for f64 {
writer.write_f64(*self)?; writer.write_f64(*self)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::f64_as_ne_bytes(from_ref(self)))
}
} }
impl TiffValue for Ifd { impl TiffValue for Ifd {
@@ -382,6 +363,10 @@ impl TiffValue for Ifd {
writer.write_u32(self.0)?; writer.write_u32(self.0)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(&self.0)))
}
} }
impl TiffValue for Ifd8 { impl TiffValue for Ifd8 {
@@ -396,6 +381,10 @@ impl TiffValue for Ifd8 {
writer.write_u64(self.0)?; writer.write_u64(self.0)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(&self.0)))
}
} }
impl TiffValue for Rational { impl TiffValue for Rational {
@@ -411,6 +400,14 @@ impl TiffValue for Rational {
writer.write_u32(self.d)?; writer.write_u32(self.d)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Owned({
let first_dword = bytecast::u32_as_ne_bytes(from_ref(&self.n));
let second_dword = bytecast::u32_as_ne_bytes(from_ref(&self.d));
[first_dword, second_dword].concat()
})
}
} }
impl TiffValue for SRational { impl TiffValue for SRational {
@@ -426,6 +423,14 @@ impl TiffValue for SRational {
writer.write_i32(self.d)?; writer.write_i32(self.d)?;
Ok(()) Ok(())
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Owned({
let first_dword = bytecast::i32_as_ne_bytes(from_ref(&self.n));
let second_dword = bytecast::i32_as_ne_bytes(from_ref(&self.d));
[first_dword, second_dword].concat()
})
}
} }
impl TiffValue for str { impl TiffValue for str {
@@ -445,9 +450,150 @@ impl TiffValue for str {
Err(TiffError::FormatError(TiffFormatError::InvalidTag)) Err(TiffError::FormatError(TiffFormatError::InvalidTag))
} }
} }
fn data(&self) -> Cow<'_, [u8]> {
Cow::Owned({
if self.is_ascii() && !self.bytes().any(|b| b == 0) {
let bytes: &[u8] = self.as_bytes();
[bytes, &[0]].concat()
} else {
vec![]
}
})
}
} }
impl<'a, T: TiffValue + ?Sized> TiffValue for &'a T { // FIXME: held up on reading the discriminant for all but the unknown variant.
//
// It needs to have `repr(u16)` to have a defined layout and then we may read the tag from its
// value representation by casting a u16 pointer to a pointer to the enum type.
// unsafe { *<*const _>::from(self).cast::<u16>() }
// But that is quite unsafe and needs careful review to maintain the safety requirements. Maybe we
// could get `bytemuck` to add a trait for this (it has `TransparentWrapper`) with a derive for
// enum types that have an explicit repr.
//
// This would allow returning borrowed data in more cases. For all types without an unknown, in all
// cases including as slices, and for all types with and unknown variant when we have a single
// value (either returning a borrowed discriminant or a borrowed value within `Unknown`).
impl TiffValue for tags::CompressionMethod {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
let bytes = self.to_u16().to_ne_bytes();
Cow::Owned(bytes.to_vec())
}
}
impl TiffValue for tags::PhotometricInterpretation {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
self.to_u16().to_ne_bytes().to_vec().into()
}
}
impl TiffValue for tags::PlanarConfiguration {
const BYTE_LEN: u8 = 2;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
self.to_u16().to_ne_bytes().to_vec().into()
}
}
impl TiffValue for tags::Predictor {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
self.to_u16().to_ne_bytes().to_vec().into()
}
}
impl TiffValue for tags::ResolutionUnit {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
self.to_u16().to_ne_bytes().to_vec().into()
}
}
/// This is implemented for slices as the `SampleFormat` tag takes a count of `N`.
///
/// Use `core::slice::from_ref` if you really need to write a single element.
///
/// See: <https://web.archive.org/web/20191120220815/https://www.awaresystems.be/imaging/tiff/tifftags/sampleformat.html>
impl TiffValue for [tags::SampleFormat] {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
self.len()
}
fn data(&self) -> Cow<'_, [u8]> {
let mut buf: Vec<u8> = Vec::with_capacity(self.len() * 2);
for x in self {
buf.extend_from_slice(&x.to_u16().to_ne_bytes());
}
Cow::Owned(buf)
}
}
/// This is implemented for slices as the `ExtraSamples` tag takes a count of `N`.
///
/// Use `core::slice::from_ref` if you really need to write a single element.
///
/// See: <https://web.archive.org/web/20191120220815/https://www.awaresystems.be/imaging/tiff/tifftags/extrasamples.html>
impl TiffValue for [tags::ExtraSamples] {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
self.len()
}
fn data(&self) -> Cow<'_, [u8]> {
let mut buf: Vec<u8> = Vec::with_capacity(self.len() * 2);
for x in self {
buf.extend_from_slice(&x.to_u16().to_ne_bytes());
}
Cow::Owned(buf)
}
}
// If you pass `&Vec<_>` then you'd get at first sight the complaint:
//
// `Vec<T>` does not implement `TiffValue`
//
// and a list of implementations of `TiffValue` that are quite unrelated. That is not very helpful.
// We do not *want* `Vec` to implement the trait as an owning type, instead you should pass a
// reference to a slice. The error message is further customized in the diagnostics of the trait.
#[diagnostic::do_not_recommend]
impl<T: TiffValue + ?Sized> TiffValue for &'_ T {
const BYTE_LEN: u8 = T::BYTE_LEN; const BYTE_LEN: u8 = T::BYTE_LEN;
const FIELD_TYPE: Type = T::FIELD_TYPE; const FIELD_TYPE: Type = T::FIELD_TYPE;
@@ -458,8 +604,66 @@ impl<'a, T: TiffValue + ?Sized> TiffValue for &'a T {
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> { fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> {
(*self).write(writer) (*self).write(writer)
} }
fn data(&self) -> Cow<'_, [u8]> {
T::data(self)
}
} }
/// An array is treated like a slice of the same length.
impl<T, const N: usize> TiffValue for [T; N]
where
[T]: TiffValue,
{
const BYTE_LEN: u8 = <[T]>::BYTE_LEN;
const FIELD_TYPE: Type = <[T]>::FIELD_TYPE;
fn count(&self) -> usize {
self.as_slice().count()
}
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> {
self.as_slice().write(writer)
}
fn data(&self) -> Cow<'_, [u8]> {
self.as_slice().data()
}
}
macro_rules! impl_tiff_value_for_contiguous_sequence {
($inner_type:ty; $bytes:expr; $field_type:expr) => {
impl $crate::encoder::TiffValue for [$inner_type] {
const BYTE_LEN: u8 = $bytes;
const FIELD_TYPE: Type = $field_type;
fn count(&self) -> usize {
self.len()
}
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> {
for x in self {
x.write(writer)?;
}
Ok(())
}
fn data(&self) -> Cow<'_, [u8]> {
let mut buf: Vec<u8> = Vec::with_capacity(Self::BYTE_LEN as usize * self.len());
for x in self {
buf.extend_from_slice(&x.data());
}
Cow::Owned(buf)
}
}
};
}
impl_tiff_value_for_contiguous_sequence!(Ifd; 4; Type::IFD);
impl_tiff_value_for_contiguous_sequence!(Ifd8; 8; Type::IFD8);
impl_tiff_value_for_contiguous_sequence!(Rational; 8; Type::RATIONAL);
impl_tiff_value_for_contiguous_sequence!(SRational; 8; Type::SRATIONAL);
/// Type to represent tiff values of type `IFD` /// Type to represent tiff values of type `IFD`
#[derive(Clone)] #[derive(Clone)]
pub struct Ifd(pub u32); pub struct Ifd(pub u32);
+132 -25
View File
@@ -1,3 +1,4 @@
use crate::encoder::compression::*;
use crate::error::TiffResult; use crate::error::TiffResult;
use std::io::{self, Seek, SeekFrom, Write}; use std::io::{self, Seek, SeekFrom, Write};
@@ -42,93 +43,136 @@ pub fn write_bigtiff_header<W: Write>(writer: &mut TiffWriter<W>) -> TiffResult<
pub struct TiffWriter<W> { pub struct TiffWriter<W> {
writer: W, writer: W,
offset: u64, offset: u64,
byte_count: u64,
compressor: Compressor,
} }
impl<W: Write> TiffWriter<W> { impl<W: Write> TiffWriter<W> {
pub fn new(writer: W) -> Self { pub fn new(writer: W) -> Self {
Self { writer, offset: 0 } Self {
writer,
offset: 0,
byte_count: 0,
compressor: Compressor::default(),
}
}
pub fn set_compression(&mut self, compressor: Compressor) {
self.compressor = compressor;
}
pub fn reset_compression(&mut self) {
self.compressor = Compressor::default();
} }
pub fn offset(&self) -> u64 { pub fn offset(&self) -> u64 {
self.offset self.offset
} }
pub(crate) fn previous_ifd_pointer<K: super::TiffKind>(&self) -> u64 {
self.offset() - core::mem::size_of::<K::OffsetType>() as u64
}
pub fn last_written(&self) -> u64 {
self.byte_count
}
pub fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), io::Error> { pub fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), io::Error> {
self.writer.write_all(bytes)?; self.byte_count = self.compressor.write_to(&mut self.writer, bytes)?;
self.offset += bytes.len() as u64; self.offset += self.byte_count;
Ok(()) Ok(())
} }
pub fn write_u8(&mut self, n: u8) -> Result<(), io::Error> { pub fn write_u8(&mut self, n: u8) -> Result<(), io::Error> {
self.writer.write_all(&n.to_ne_bytes())?; self.byte_count = self
self.offset += 1; .compressor
.write_to(&mut self.writer, &n.to_ne_bytes())?;
self.offset += self.byte_count;
Ok(()) Ok(())
} }
pub fn write_i8(&mut self, n: i8) -> Result<(), io::Error> { pub fn write_i8(&mut self, n: i8) -> Result<(), io::Error> {
self.writer.write_all(&n.to_ne_bytes())?; self.byte_count = self
self.offset += 1; .compressor
.write_to(&mut self.writer, &n.to_ne_bytes())?;
self.offset += self.byte_count;
Ok(()) Ok(())
} }
pub fn write_u16(&mut self, n: u16) -> Result<(), io::Error> { pub fn write_u16(&mut self, n: u16) -> Result<(), io::Error> {
self.writer.write_all(&n.to_ne_bytes())?; self.byte_count = self
self.offset += 2; .compressor
.write_to(&mut self.writer, &n.to_ne_bytes())?;
self.offset += self.byte_count;
Ok(()) Ok(())
} }
pub fn write_i16(&mut self, n: i16) -> Result<(), io::Error> { pub fn write_i16(&mut self, n: i16) -> Result<(), io::Error> {
self.writer.write_all(&n.to_ne_bytes())?; self.byte_count = self
self.offset += 2; .compressor
.write_to(&mut self.writer, &n.to_ne_bytes())?;
self.offset += self.byte_count;
Ok(()) Ok(())
} }
pub fn write_u32(&mut self, n: u32) -> Result<(), io::Error> { pub fn write_u32(&mut self, n: u32) -> Result<(), io::Error> {
self.writer.write_all(&n.to_ne_bytes())?; self.byte_count = self
self.offset += 4; .compressor
.write_to(&mut self.writer, &n.to_ne_bytes())?;
self.offset += self.byte_count;
Ok(()) Ok(())
} }
pub fn write_i32(&mut self, n: i32) -> Result<(), io::Error> { pub fn write_i32(&mut self, n: i32) -> Result<(), io::Error> {
self.writer.write_all(&n.to_ne_bytes())?; self.byte_count = self
self.offset += 4; .compressor
.write_to(&mut self.writer, &n.to_ne_bytes())?;
self.offset += self.byte_count;
Ok(()) Ok(())
} }
pub fn write_u64(&mut self, n: u64) -> Result<(), io::Error> { pub fn write_u64(&mut self, n: u64) -> Result<(), io::Error> {
self.writer.write_all(&n.to_ne_bytes())?; self.byte_count = self
self.offset += 8; .compressor
.write_to(&mut self.writer, &n.to_ne_bytes())?;
self.offset += self.byte_count;
Ok(()) Ok(())
} }
pub fn write_i64(&mut self, n: i64) -> Result<(), io::Error> { pub fn write_i64(&mut self, n: i64) -> Result<(), io::Error> {
self.writer.write_all(&n.to_ne_bytes())?; self.byte_count = self
self.offset += 8; .compressor
.write_to(&mut self.writer, &n.to_ne_bytes())?;
self.offset += self.byte_count;
Ok(()) Ok(())
} }
pub fn write_f32(&mut self, n: f32) -> Result<(), io::Error> { pub fn write_f32(&mut self, n: f32) -> Result<(), io::Error> {
self.writer.write_all(&u32::to_ne_bytes(n.to_bits()))?; self.byte_count = self
self.offset += 4; .compressor
.write_to(&mut self.writer, &u32::to_ne_bytes(n.to_bits()))?;
self.offset += self.byte_count;
Ok(()) Ok(())
} }
pub fn write_f64(&mut self, n: f64) -> Result<(), io::Error> { pub fn write_f64(&mut self, n: f64) -> Result<(), io::Error> {
self.writer.write_all(&u64::to_ne_bytes(n.to_bits()))?; self.byte_count = self
self.offset += 8; .compressor
.write_to(&mut self.writer, &u64::to_ne_bytes(n.to_bits()))?;
self.offset += self.byte_count;
Ok(()) Ok(())
} }
pub fn pad_word_boundary(&mut self) -> Result<(), io::Error> { 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 padding = [0, 0, 0];
let padd_len = 4 - (self.offset % 4); let padd_len = 4 - (self.offset % 4);
self.writer.write_all(&padding[..padd_len as usize])?; self.writer.write_all(&padding[..padd_len as usize])?;
@@ -142,7 +186,70 @@ impl<W: Write> TiffWriter<W> {
impl<W: Seek> TiffWriter<W> { impl<W: Seek> TiffWriter<W> {
pub fn goto_offset(&mut self, offset: u64) -> Result<(), io::Error> { pub fn goto_offset(&mut self, offset: u64) -> Result<(), io::Error> {
self.offset = offset; self.offset = offset;
self.writer.seek(SeekFrom::Start(offset as u64))?; self.writer.seek(SeekFrom::Start(offset))?;
Ok(()) Ok(())
} }
} }
#[test]
fn encoder_on_short_writes() {
struct ShortWriter<T> {
inner: T,
}
impl<T: std::io::Write> std::io::Write for ShortWriter<T> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let lower_bound = buf.len().min(1);
let lower = buf.len().saturating_sub(1).max(lower_bound);
self.inner.write(&buf[..lower])
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
impl<T: std::io::Seek> std::io::Seek for ShortWriter<T> {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.inner.seek(pos)
}
}
let mut write = ShortWriter {
inner: std::io::Cursor::new(vec![0u8; 0]),
};
let imgdata = vec![42u8; 100 * 100];
let mut data = crate::decoder::DecodingResult::U8(vec![]);
for comp in [
super::Compression::Uncompressed,
super::Compression::Packbits,
] {
write.inner.set_position(0);
write.inner.get_mut().clear();
{
let mut tiff_writer = super::TiffEncoder::new(&mut write)
.unwrap()
.with_compression(comp);
tiff_writer
.write_image::<super::Gray8>(100, 100, &imgdata)
.unwrap();
}
write.inner.set_position(0);
let mut tiff_reader = crate::decoder::Decoder::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()
);
}
}
+214 -245
View File
@@ -1,256 +1,223 @@
use std::error::Error;
use std::fmt;
use std::io; use std::io;
use std::str; use std::str;
use std::string; use std::string;
use crate::decoder::{ifd::Value, ChunkType}; use quick_error::quick_error;
use crate::decoder::ChunkType;
use crate::tags::Type;
use crate::tags::{ use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag, CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag,
}; };
use crate::ColorType; use crate::ColorType;
/// Tiff error kinds. quick_error! {
#[derive(Debug)] /// Tiff error kinds.
pub enum TiffError { #[derive(Debug)]
/// The Image is not formatted properly. pub enum TiffError {
FormatError(TiffFormatError), /// The Image is not formatted properly.
FormatError(err: TiffFormatError) {
display("format error: {err}")
from()
source(err)
}
/// The Decoder does not support features required by the image.
UnsupportedError(err: TiffUnsupportedError) {
display("unsupported error: {err}")
from()
source(err)
}
/// An I/O Error occurred while decoding the image.
IoError(err: io::Error) {
display("{err}")
from()
source(err)
}
/// The Limits of the Decoder is exceeded.
LimitsExceeded {
display("decoder limits exceeded")
}
/// An integer conversion to or from a platform size failed.
IntSizeError {
display("platform or format size limits exceeded")
}
/// The image does not support the requested operation
UsageError(err: UsageError) {
display("usage error: {err}")
from()
source(err)
}
}
}
quick_error! {
/// The image is not formatted properly.
///
/// This indicates that the encoder producing the image might behave incorrectly or that the
/// input file has been corrupted.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum TiffFormatError {
TiffSignatureNotFound {
display("TIFF signature not found")
}
TiffSignatureInvalid {
display("TIFF signature invalid")
}
ImageFileDirectoryNotFound {
display("image file directory not found")
}
InconsistentSizesEncountered {
display("inconsistent sizes encountered")
}
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}")
}
InvalidTag {
display("image contains invalid tag")
}
InvalidTagValueType(tag: Tag) {
display("tag `{tag:?}` did not have the expected value type")
}
RequiredTagNotFound(tag: Tag) {
display("required tag `{tag:?}` not found")
}
UnknownPredictor(pred: u16) {
display("unknown predictor “{pred}” encountered")
}
UnknownPlanarConfiguration(cfg: u16) {
display("unknown planar configuration “{cfg}”")
}
InvalidTypeForTag {
display("tag has invalid type")
}
InvalidCountForTag(tag: Tag, len: usize) {
display("tag `{tag:?}` with incorrect number of elements ({len}) encountered")
}
StripTileTagConflict {
display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found")
}
CycleInOffsets {
display("file contained a cycle in the list of IFDs")
}
SamplesPerPixelIsZero {
display("samples per pixel is zero")
}
CompressedDataCorrupt(message: String) {
display("compressed data is corrupt: {message}")
}
}
}
quick_error! {
/// The Decoder does not support features required by the image. /// The Decoder does not support features required by the image.
UnsupportedError(TiffUnsupportedError), ///
/// This only captures known failures for which the standard either does not require support or an
/// An I/O Error occurred while decoding the image. /// implementation has been planned but not yet completed. Some variants may become unused over
IoError(io::Error), /// time and will then get deprecated before being removed.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
/// The Limits of the Decoder is exceeded. #[non_exhaustive]
LimitsExceeded, pub enum TiffUnsupportedError {
FloatingPointPredictor(color_type: ColorType) {
/// An integer conversion to or from a platform size failed, either due to display("floating point predictor for {color_type:?} is unsupported")
/// limits of the platform size or limits of the format. }
IntSizeError, HorizontalPredictor(color_type: ColorType) {
display("horizontal predictor for {color_type:?} is unsupported")
/// The image does not support the requested operation }
UsageError(UsageError), InconsistentBitsPerSample(bits_per_sample: Vec<u8>) {
} display("inconsistent bits per sample: {bits_per_sample:?}")
}
/// The image is not formatted properly. InterpretationWithBits(interpretation: PhotometricInterpretation, bits_per_sample: Vec<u8>) {
/// display("Photometric interpretation {interpretation:?} with bits per sample {bits_per_sample:?} is unsupported")
/// This indicates that the encoder producing the image might behave incorrectly or that the input }
/// file has been corrupted. UnknownInterpretation {
/// display("unknown photometric interpretation")
/// The list of variants may grow to incorporate errors of future features. Matching against this }
/// exhaustively is not covered by interface stability guarantees. UnknownCompressionMethod {
#[derive(Debug, Clone, PartialEq)] display("unknown compression method")
pub enum TiffFormatError { }
TiffSignatureNotFound, UnsupportedCompressionMethod(method: CompressionMethod) {
TiffSignatureInvalid, display("compression method {method:?} is unsupported")
ImageFileDirectoryNotFound, }
InconsistentSizesEncountered, UnsupportedSampleDepth(depth: u8) {
UnexpectedCompressedData { display("{depth} samples per pixel is unsupported")
actual_bytes: usize, }
required_bytes: usize, UnsupportedSampleFormat(sample_format: Vec<SampleFormat>) {
}, display("sample format {sample_format:?} is unsupported")
InconsistentStripSamples { }
actual_samples: usize, UnsupportedColorType(color_type: ColorType) {
required_samples: usize, display("color type {color_type:?} is unsupported")
}, }
InvalidTag, UnsupportedBitsPerChannel(bits_per_channel: u8) {
InvalidTagValueType(Tag), display("{bits_per_channel} bits per channel not supported")
RequiredTagNotFound(Tag), }
UnknownPredictor(u16), UnsupportedPlanarConfig(planar: Option<PlanarConfiguration>) {
ByteExpected(Value), display("unsupported planar configuration “{planar:?}”")
UnsignedIntegerExpected(Value), }
SignedIntegerExpected(Value), UnsupportedDataType {
Format(String), display("unsupported data type.")
RequiredTagEmpty(Tag), }
#[doc(hidden)] UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
/// Do not match against this variant. It may get removed. display("unsupported photometric interpretation \"{interpretation:?}\"")
__NonExhaustive, }
} ChromaSubsampling {
display("chroma subsampling of YCbCr color is unsupported")
impl fmt::Display for TiffFormatError { }
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { MisalignedTileBoundaries {
use self::TiffFormatError::*; display("tile rows are not aligned to byte boundaries")
match *self {
TiffSignatureNotFound => write!(fmt, "TIFF signature not found."),
TiffSignatureInvalid => write!(fmt, "TIFF signature invalid."),
ImageFileDirectoryNotFound => write!(fmt, "Image file directory not found."),
InconsistentSizesEncountered => write!(fmt, "Inconsistent sizes encountered."),
UnexpectedCompressedData {
actual_bytes,
required_bytes,
} => {
write!(
fmt,
"Decompression returned different amount of bytes than expected: got {}, expected {}.",
actual_bytes, required_bytes
)
}
InconsistentStripSamples {
actual_samples,
required_samples,
} => {
write!(
fmt,
"Inconsistent elements in strip: got {}, expected {}.",
actual_samples, required_samples
)
}
InvalidTag => write!(fmt, "Image contains invalid tag."),
InvalidTagValueType(ref tag) => {
write!(fmt, "Tag `{:?}` did not have the expected value type.", tag)
}
RequiredTagNotFound(ref tag) => write!(fmt, "Required tag `{:?}` not found.", tag),
UnknownPredictor(ref predictor) => {
write!(fmt, "Unknown predictor “{}” encountered", predictor)
}
ByteExpected(ref val) => write!(fmt, "Expected byte, {:?} found.", val),
UnsignedIntegerExpected(ref val) => {
write!(fmt, "Expected unsigned integer, {:?} found.", val)
}
SignedIntegerExpected(ref val) => {
write!(fmt, "Expected signed integer, {:?} found.", val)
}
Format(ref val) => write!(fmt, "Invalid format: {:?}.", val),
RequiredTagEmpty(ref val) => write!(fmt, "Required tag {:?} was empty.", val),
__NonExhaustive => unreachable!(),
} }
} }
} }
/// The Decoder does not support features required by the image. quick_error! {
/// /// User attempted to use the Decoder in a way that is incompatible with a specific image.
/// This only captures known failures for which the standard either does not require support or an ///
/// implementation has been planned but not yet completed. Some variants may become unused over /// For example: attempting to read a tile from a stripped image.
/// time and will then get deprecated before being removed. #[derive(Debug)]
/// #[non_exhaustive]
/// The list of variants may grow. Matching against this exhaustively is not covered by interface pub enum UsageError {
/// stability guarantees. InvalidChunkType(expected: ChunkType, actual: ChunkType) {
#[derive(Debug, Clone, PartialEq, Eq, Hash)] display("requested operation is only valid for images with chunk encoding of type {expected:?} but got {actual:?}")
pub enum TiffUnsupportedError {
HorizontalPredictor(ColorType),
InterpretationWithBits(PhotometricInterpretation, Vec<u8>),
UnknownInterpretation,
UnknownCompressionMethod,
UnsupportedCompressionMethod(CompressionMethod),
UnsupportedSampleDepth(u8),
UnsupportedSampleFormat(Vec<SampleFormat>),
UnsupportedColorType(ColorType),
UnsupportedBitsPerChannel(u8),
UnsupportedPlanarConfig(Option<PlanarConfiguration>),
UnsupportedDataType,
#[doc(hidden)]
/// Do not match against this variant. It may get removed.
__NonExhaustive,
}
impl fmt::Display for TiffUnsupportedError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use self::TiffUnsupportedError::*;
match *self {
HorizontalPredictor(color_type) => write!(
fmt,
"Horizontal predictor for {:?} is unsupported.",
color_type
),
InterpretationWithBits(ref photometric_interpretation, ref bits_per_sample) => write!(
fmt,
"{:?} with {:?} bits per sample is unsupported",
photometric_interpretation, bits_per_sample
),
UnknownInterpretation => write!(
fmt,
"The image is using an unknown photometric interpretation."
),
UnknownCompressionMethod => write!(fmt, "Unknown compression method."),
UnsupportedCompressionMethod(method) => {
write!(fmt, "Compression method {:?} is unsupported", method)
}
UnsupportedSampleDepth(samples) => {
write!(fmt, "{} samples per pixel is unsupported.", samples)
}
UnsupportedSampleFormat(ref formats) => {
write!(fmt, "Sample format {:?} is unsupported.", formats)
}
UnsupportedColorType(color_type) => {
write!(fmt, "Color type {:?} is unsupported", color_type)
}
UnsupportedBitsPerChannel(bits) => {
write!(fmt, "{} bits per channel not supported", bits)
}
UnsupportedPlanarConfig(config) => {
write!(fmt, "Unsupported planar configuration “{:?}”.", config)
}
UnsupportedDataType => write!(fmt, "Unsupported data type."),
__NonExhaustive => unreachable!(),
} }
} InvalidChunkIndex(index: u32) {
} display("invalid chunk index ({index}) requested")
/// User attempted to use the Decoder in a way that is incompatible with a specific image.
///
/// For example: attempting to read a tile from a stripped image.
#[derive(Debug)]
pub enum UsageError {
InvalidChunkType(ChunkType, ChunkType),
}
impl fmt::Display for UsageError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::UsageError::*;
match *self {
InvalidChunkType(expected, actual) => {
write!(
fmt,
"Requested operation is only valid for images with chunk encoding of type: {:?}, got {:?}.",
expected, actual
)
}
} }
} InvalidPlaneIndex(index: u16) {
} display("invalid plane index ({index}) requested")
impl fmt::Display for TiffError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
TiffError::FormatError(ref e) => write!(fmt, "Format error: {}", e),
TiffError::UnsupportedError(ref f) => write!(
fmt,
"The Decoder does not support the \
image format `{}`",
f
),
TiffError::IoError(ref e) => e.fmt(fmt),
TiffError::LimitsExceeded => write!(fmt, "The Decoder limits are exceeded"),
TiffError::IntSizeError => write!(fmt, "Platform or format size limits exceeded"),
TiffError::UsageError(ref e) => write!(fmt, "Usage error: {}", e),
} }
} InvalidCodingUnit(index: u32, have: u32) {
} display("out of bounds coding unit ({index}) requested, only {have} available")
impl Error for TiffError {
fn description(&self) -> &str {
match *self {
TiffError::FormatError(..) => "Format error",
TiffError::UnsupportedError(..) => "Unsupported error",
TiffError::IoError(..) => "IO error",
TiffError::LimitsExceeded => "Decoder limits exceeded",
TiffError::IntSizeError => "Platform or format size limits exceeded",
TiffError::UsageError(..) => "Invalid usage",
} }
} PredictorCompressionMismatch {
display("requested predictor is not compatible with the requested compression")
fn cause(&self) -> Option<&dyn Error> { }
match *self { PredictorIncompatible {
TiffError::IoError(ref e) => Some(e), display("the requested predictor is not compatible with the image's format")
_ => None, }
PredictorUnavailable {
display("the requested predictor is not available")
}
InsufficientOutputBufferSize { needed: usize, provided: usize } {
display("the borrowed output buffer is not large enough for the decoded data, needed {needed} but have {provided}")
}
InsufficientOutputRowStride { needed: usize, requested: usize } {
display("the provided output row stride would alias rows of decoded data, needed {needed} but have {requested}")
}
ZeroIfdPointer {
display("the offset 0 can not point to a valid IFD")
}
ReconfiguredAfterImageWrite {
display("attempted to reconfigure the encoder after image writing has started")
}
ByteOrderMismatch {
display("attempted to use data with a byte order that does not match the required one")
}
MismatchedEntryLength {ty: Type, found: usize } {
display("the length of data is not a multiple of the size of the type {ty:?}, found {found}")
} }
}
}
impl From<io::Error> for TiffError {
fn from(err: io::Error) -> TiffError {
TiffError::IoError(err)
} }
} }
@@ -266,23 +233,25 @@ impl From<string::FromUtf8Error> for TiffError {
} }
} }
impl From<TiffFormatError> for TiffError {
fn from(err: TiffFormatError) -> TiffError {
TiffError::FormatError(err)
}
}
impl From<TiffUnsupportedError> for TiffError {
fn from(err: TiffUnsupportedError) -> TiffError {
TiffError::UnsupportedError(err)
}
}
impl From<std::num::TryFromIntError> for TiffError { impl From<std::num::TryFromIntError> for TiffError {
fn from(_err: std::num::TryFromIntError) -> TiffError { fn from(_err: std::num::TryFromIntError) -> TiffError {
TiffError::IntSizeError TiffError::IntSizeError
} }
} }
#[cfg(feature = "lzw")]
impl From<weezl::LzwError> for TiffError {
fn from(err: weezl::LzwError) -> TiffError {
TiffError::FormatError(TiffFormatError::CompressedDataCorrupt(err.to_string()))
}
}
#[cfg(feature = "jpeg")]
impl From<zune_jpeg::errors::DecodeErrors> for TiffError {
fn from(err: zune_jpeg::errors::DecodeErrors) -> Self {
TiffError::FormatError(TiffFormatError::CompressedDataCorrupt(err.to_string()))
}
}
/// Result of an image decoding/encoding process /// Result of an image decoding/encoding process
pub type TiffResult<T> = Result<T, TiffError>; pub type TiffResult<T> = Result<T, TiffError>;
+49 -4
View File
@@ -4,21 +4,21 @@
//! lossless and lossy compression. //! lossless and lossy compression.
//! //!
//! # Related Links //! # Related Links
//! * <https://www.adobe.io/open/standards/TIFF.html> - The TIFF specification //! * <https://web.archive.org/web/20210108073850/https://www.adobe.io/open/standards/TIFF.html> - The TIFF specification
extern crate jpeg;
extern crate weezl;
mod bytecast; mod bytecast;
pub mod decoder; pub mod decoder;
mod directory;
pub mod encoder; pub mod encoder;
mod error; mod error;
pub mod tags; pub mod tags;
pub use self::directory::Directory;
pub use self::error::{TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError}; pub use self::error::{TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError};
/// An enumeration over supported color types and their bit depths /// An enumeration over supported color types and their bit depths
#[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)] #[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)]
#[non_exhaustive]
pub enum ColorType { pub enum ColorType {
/// Pixel is grayscale /// Pixel is grayscale
Gray(u8), Gray(u8),
@@ -37,4 +37,49 @@ pub enum ColorType {
/// Pixel is CMYK /// Pixel is CMYK
CMYK(u8), CMYK(u8),
/// Pixel is CMYK with an alpha channel
CMYKA(u8),
/// Pixel is YCbCr
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 {
/// The number of bits used to store each sample.
pub fn bit_depth(&self) -> u8 {
match *self {
ColorType::Gray(b)
| ColorType::RGB(b)
| ColorType::Palette(b)
| ColorType::GrayA(b)
| ColorType::RGBA(b)
| ColorType::CMYK(b)
| ColorType::CMYKA(b)
| ColorType::YCbCr(b)
| ColorType::Lab(b)
| ColorType::Multiband { bit_depth: b, .. } => b,
}
}
/// 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,
}
}
} }
+385 -21
View File
@@ -1,28 +1,30 @@
use crate::encoder::TiffValue;
use core::fmt;
macro_rules! tags { macro_rules! tags {
{ {
// Permit arbitrary meta items, which include documentation. // Permit arbitrary meta items, which include documentation.
$( #[$enum_attr:meta] )* $( #[$enum_attr:meta] )*
$vis:vis enum $name:ident($ty:tt) $(unknown($unknown_doc:literal))* { $vis:vis enum $name:ident($ty:tt) $(unknown(#[$unknown_meta:meta] $unknown_doc:ident))* {
// Each of the `Name = Val,` permitting documentation. // Each of the `Name = Val,` permitting documentation.
$($(#[$ident_attr:meta])* $tag:ident = $val:expr,)* $($(#[$ident_attr:meta])* $tag:ident = $val:expr,)*
} }
} => { } => {
$( #[$enum_attr] )* $( #[$enum_attr] )*
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
#[non_exhaustive]
#[repr($ty)]
pub enum $name { pub enum $name {
$($(#[$ident_attr])* $tag,)* $($(#[$ident_attr])* $tag = $val,)*
// FIXME: switch to non_exhaustive once stabilized and compiler requirement new enough
#[doc(hidden)]
__NonExhaustive,
$( $(
#[doc = $unknown_doc] #[$unknown_meta]
Unknown($ty), Unknown($ty),
)* )*
} }
impl $name { impl $name {
#[inline(always)] #[inline(always)]
fn __from_inner_type(n: $ty) -> Result<Self, $ty> { const fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
match n { match n {
$( $val => Ok($name::$tag), )* $( $val => Ok($name::$tag), )*
n => Err(n), n => Err(n),
@@ -30,11 +32,10 @@ macro_rules! tags {
} }
#[inline(always)] #[inline(always)]
fn __to_inner_type(&self) -> $ty { const fn __to_inner_type(&self) -> $ty {
match *self { match *self {
$( $name::$tag => $val, )* $( $name::$tag => $val, )*
$( $name::Unknown(n) => { $unknown_doc; n }, )* $( $name::Unknown($unknown_doc) => { $unknown_doc }, )*
$name::__NonExhaustive => unreachable!(),
} }
} }
} }
@@ -42,23 +43,28 @@ macro_rules! tags {
tags!($name, $ty, $($unknown_doc)*); tags!($name, $ty, $($unknown_doc)*);
}; };
// For u16 tags, provide direct inherent primitive conversion methods. // For u16 tags, provide direct inherent primitive conversion methods.
($name:tt, u16, $($unknown_doc:literal)*) => { ($name:tt, u16, $($unknown_doc:ident)*) => {
impl $name { impl $name {
#[inline(always)] #[inline(always)]
pub fn from_u16(val: u16) -> Option<Self> { pub const fn from_u16(val: u16) -> Option<Self> {
Self::__from_inner_type(val).ok() match Self::__from_inner_type(val) {
Ok(v) => Some(v),
Err(_) => None,
}
} }
$( $(
#[inline(always)] #[inline(always)]
pub fn from_u16_exhaustive(val: u16) -> Self { pub const fn from_u16_exhaustive($unknown_doc: u16) -> Self {
$unknown_doc; match Self::__from_inner_type($unknown_doc) {
Self::__from_inner_type(val).unwrap_or_else(|_| $name::Unknown(val)) Ok(v) => v,
Err(_) => $name::Unknown($unknown_doc),
}
} }
)* )*
#[inline(always)] #[inline(always)]
pub fn to_u16(&self) -> u16 { pub const fn to_u16(&self) -> u16 {
Self::__to_inner_type(self) Self::__to_inner_type(self)
} }
} }
@@ -71,7 +77,10 @@ macro_rules! tags {
// Note: These tags appear in the order they are mentioned in the TIFF reference // Note: These tags appear in the order they are mentioned in the TIFF reference
tags! { tags! {
/// TIFF tags /// TIFF tags
pub enum Tag(u16) unknown("A private or extension tag") { pub enum Tag(u16) unknown(
/// A private or extension tag
unknown
) {
// Baseline tags: // Baseline tags:
Artist = 315, Artist = 315,
// grayscale images PhotometricInterpretation 1 or 3 // grayscale images PhotometricInterpretation 1 or 3
@@ -81,7 +90,6 @@ pub enum Tag(u16) unknown("A private or extension tag") {
// palette-color images (PhotometricInterpretation 3) // palette-color images (PhotometricInterpretation 3)
ColorMap = 320, // TODO add support ColorMap = 320, // TODO add support
Compression = 259, // TODO add support for 2 and 32773 Compression = 259, // TODO add support for 2 and 32773
Copyright = 33_432,
DateTime = 306, DateTime = 306,
ExtraSamples = 338, // TODO add support ExtraSamples = 338, // TODO add support
FillOrder = 266, // TODO add support FillOrder = 266, // TODO add support
@@ -117,23 +125,63 @@ pub enum Tag(u16) unknown("A private or extension tag") {
TileLength = 323, TileLength = 323,
TileOffsets = 324, TileOffsets = 324,
TileByteCounts = 325, TileByteCounts = 325,
SubIfd = 330,
// Data Sample Format // Data Sample Format
SampleFormat = 339, SampleFormat = 339,
SMinSampleValue = 340, // TODO add support SMinSampleValue = 340, // TODO add support
SMaxSampleValue = 341, // TODO add support SMaxSampleValue = 341, // TODO add support
// JPEG // JPEG
JPEGTables = 347, JPEGTables = 347,
// Subsampling
#[doc(alias = "YCbCrSubsampling")]
ChromaSubsampling = 530, // TODO add support
#[doc(alias = "YCbCrPositioning")]
ChromaPositioning = 531, // TODO add support
// GeoTIFF // GeoTIFF
ModelPixelScaleTag = 33550, // (SoftDesk) ModelPixelScaleTag = 33550, // (SoftDesk)
ModelTransformationTag = 34264, // (JPL Carto Group) ModelTransformationTag = 34264, // (JPL Carto Group)
ModelTiepointTag = 33922, // (Intergraph) ModelTiepointTag = 33922, // (Intergraph)
// <https://web.archive.org/web/20131111073619/http://www.exif.org/Exif2-1.PDF>
// *Do note its typo in the Decimal id*
Copyright = 33_432,
// <https://web.archive.org/web/20131111073619/http://www.exif.org/Exif2-1.PDF>
ExifDirectory = 0x8769,
// <https://web.archive.org/web/20131111073619/http://www.exif.org/Exif2-1.PDF>
GpsDirectory = 0x8825,
// <https://www.color.org/technotes/ICC-Technote-ProfileEmbedding.pdf>
IccProfile = 34675,
GeoKeyDirectoryTag = 34735, // (SPOT) GeoKeyDirectoryTag = 34735, // (SPOT)
GeoDoubleParamsTag = 34736, // (SPOT) GeoDoubleParamsTag = 34736, // (SPOT)
GeoAsciiParamsTag = 34737, // (SPOT) GeoAsciiParamsTag = 34737, // (SPOT)
ExifVersion = 0x9000,
GdalNodata = 42113, // Contains areas with missing data GdalNodata = 42113, // Contains areas with missing data
} }
} }
/// Identifies the offset of an IFD.
///
/// This is represented as a 64-bit integer but only BigTIFF can utilize the bits. It is encoded
/// as 32-bit unsigned value ([`Type::LONG`]) in regular TIFF files and as 64-bit unsigned value
/// ([`Type::IFD8`]) in BigTIFF files.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
// We could be using `NonZeroU64` here but I find that this complicates semantics. This type
// represents the integer value stored as a value in a tag. The semantics of treating `0` as an end
// marker are imposed by the IFD. (It's unclear if Pointer tags such as Exif would allow `0` but in
// practice it just returns garbage and the validity does not matter greatly to us).
pub struct IfdPointer(pub u64);
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! { tags! {
/// The type of an IFD entry (a 2 byte field). /// The type of an IFD entry (a 2 byte field).
pub enum Type(u16) { pub enum Type(u16) {
@@ -172,10 +220,52 @@ pub enum Type(u16) {
} }
} }
impl Type {
pub(crate) fn byte_len(&self) -> u8 {
match *self {
Type::BYTE | Type::SBYTE | Type::ASCII | Type::UNDEFINED => 1,
Type::SHORT | Type::SSHORT => 2,
Type::LONG | Type::SLONG | Type::FLOAT | Type::IFD => 4,
Type::LONG8
| Type::SLONG8
| Type::DOUBLE
| Type::RATIONAL
| Type::SRATIONAL
| Type::IFD8 => 8,
}
}
pub(crate) fn value_bytes(&self, count: u64) -> Result<u64, crate::error::TiffError> {
let tag_size = u64::from(self.byte_len());
match count.checked_mul(tag_size) {
Some(n) => Ok(n),
None => Err(crate::error::TiffError::LimitsExceeded),
}
}
pub(crate) fn endian_bytes(self) -> EndianBytes {
match self {
Type::BYTE | Type::SBYTE | Type::ASCII | Type::UNDEFINED => EndianBytes::One,
Type::SHORT | Type::SSHORT => EndianBytes::Two,
Type::LONG
| Type::SLONG
| Type::FLOAT
| Type::IFD
| Type::RATIONAL
| Type::SRATIONAL => EndianBytes::Four,
Type::LONG8 | Type::SLONG8 | Type::DOUBLE | Type::IFD8 => EndianBytes::Eight,
}
}
}
tags! { tags! {
/// See [TIFF compression tags](https://www.awaresystems.be/imaging/tiff/tifftags/compression.html) /// See [TIFF compression tags](https://www.awaresystems.be/imaging/tiff/tifftags/compression.html)
/// for reference. /// for reference.
pub enum CompressionMethod(u16) { pub enum CompressionMethod(u16) unknown(
/// A custom compression method
unknown
) {
None = 1, None = 1,
Huffman = 2, Huffman = 2,
Fax3 = 3, Fax3 = 3,
@@ -187,6 +277,20 @@ pub enum CompressionMethod(u16) {
Deflate = 8, Deflate = 8,
OldDeflate = 0x80B2, OldDeflate = 0x80B2,
PackBits = 0x8005, PackBits = 0x8005,
// 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,
} }
} }
@@ -199,7 +303,41 @@ pub enum PhotometricInterpretation(u16) {
TransparencyMask = 4, TransparencyMask = 4,
CMYK = 5, CMYK = 5,
YCbCr = 6, YCbCr = 6,
/// Biased Lab with all coefficients stored as unsigned integers.
CIELab = 8, 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,
} }
} }
@@ -212,8 +350,17 @@ pub enum PlanarConfiguration(u16) {
tags! { tags! {
pub enum Predictor(u16) { pub enum Predictor(u16) {
/// No changes were made to the data
None = 1, None = 1,
/// The images' rows were processed to contain the difference of each pixel from the previous one.
///
/// This means that instead of having in order `[r1, g1. b1, r2, g2 ...]` you will find
/// `[r1, g1, b1, r2-r1, g2-g1, b2-b1, r3-r2, g3-g2, ...]`
Horizontal = 2, Horizontal = 2,
/// Floating-point predictor for IEEE floating-point sample types.
///
/// This predictor byte-shuffles the data and applies byte-level horizontal differencing.
FloatingPoint = 3,
} }
} }
@@ -227,10 +374,227 @@ pub enum ResolutionUnit(u16) {
} }
tags! { tags! {
pub enum SampleFormat(u16) unknown("An unknown extension sample format") { pub enum SampleFormat(u16) unknown(
/// An unknown extension sample format
unknown
) {
Uint = 1, Uint = 1,
Int = 2, Int = 2,
IEEEFP = 3, IEEEFP = 3,
Void = 4, Void = 4,
} }
} }
tags! {
pub enum ExtraSamples(u16) {
/// There is no specified association between the sample and the image.
Unspecified = 0,
/// The sample is associated alpha, i.e. pre-multiplied color.
AssociatedAlpha = 1,
/// The sample is unassociated alpha such as a mask. There might be more than one such sample.
UnassociatedAlpha = 2,
}
}
/// A value represented as in-memory bytes with flexible byteorder.
pub struct ValueBuffer {
/// The raw bytes of the value.
bytes: Vec<u8>,
/// The type of the value.
ty: Type,
/// The number of items, as `bytes` may be oversized while holding bytes that are initialized
/// but not used by any value.
count: u64,
/// The byte order of the value.
byte_order: ByteOrder,
}
impl ValueBuffer {
/// A value with a count of zero.
///
/// The byte order is set to the native byte order of the platform.
pub fn empty(ty: Type) -> Self {
ValueBuffer {
bytes: vec![],
ty,
count: 0,
byte_order: ByteOrder::native(),
}
}
/// Create a value with native byte order from in-memory data.
pub fn from_value<T: TiffValue>(value: &T) -> Self {
ValueBuffer {
bytes: value.data().into_owned(),
ty: <T as TiffValue>::FIELD_TYPE,
count: value.count() as u64,
byte_order: ByteOrder::native(),
}
}
pub fn byte_order(&self) -> ByteOrder {
self.byte_order
}
pub fn data_type(&self) -> Type {
self.ty
}
/// The count of items in the value.
pub fn count(&self) -> u64 {
debug_assert!({
self.ty
.value_bytes(self.count)
.is_ok_and(|n| n <= self.bytes.len() as u64)
});
self.count
}
/// View the underlying raw bytes of this value.
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.assumed_len_from_count()]
}
/// View the underlying mutable raw bytes of this value.
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
let len = self.assumed_len_from_count();
&mut self.bytes[..len]
}
/// Change the byte order of the value representation.
pub fn set_byte_order(&mut self, byte_order: ByteOrder) {
let len = self.assumed_len_from_count();
self.byte_order
.convert(self.ty, &mut self.bytes[..len], byte_order);
self.byte_order = byte_order;
}
/// Prepare the internal for a value `to_len` bytes long.
///
/// Shrinks the allocation if it is far too large or extends it if it is too small. In either
/// case ensures that at least `to_len` bytes are initialized for [`Self::raw_bytes_mut`].
pub(crate) fn prepare_length(&mut self, to_len: usize) {
if to_len > self.bytes.len() {
self.bytes.resize(to_len, 0);
}
if self.bytes.len() < to_len / 2 {
self.bytes.truncate(to_len);
self.bytes.shrink_to_fit();
}
}
/// Internal method to change the type and count while re-interpreting the byte buffer.
///
/// Should only be called after writing bytes to the internal buffer prepared with
/// `Self::prepare_length`.
pub(crate) fn assume_type(&mut self, ty: Type, count: u64, bo: ByteOrder) {
debug_assert!({
ty.value_bytes(count)
.is_ok_and(|n| n <= self.bytes.len() as u64)
});
self.byte_order = bo;
self.ty = ty;
self.count = count;
}
pub(crate) fn raw_bytes_mut(&mut self) -> &mut [u8] {
&mut self.bytes
}
fn assumed_len_from_count(&self) -> usize {
usize::from(self.ty.byte_len()) * self.count as usize
}
}
/// Byte order of the TIFF file.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ByteOrder {
/// little endian byte order
LittleEndian,
/// big endian byte order
BigEndian,
}
impl ByteOrder {
/// Get the byte order representing the running target.
///
/// The infallibility of this method represents the fact that only little and big endian
/// systems are supported by the library. No mixed endian and no other weird stuff. (Note: as
/// of Rust 1.90 this is a tautology as Rust itself only has those two kinds).
pub const fn native() -> Self {
match () {
#[cfg(target_endian = "little")]
() => ByteOrder::LittleEndian,
#[cfg(target_endian = "big")]
() => ByteOrder::BigEndian,
#[cfg(not(any(target_endian = "big", target_endian = "little")))]
() => compile_error!("Unsupported target"),
}
}
/// Given a typed buffer, convert its contents to the specified byte order in-place.
///
/// The buffer is assumed to represent an array of the given type. If the length of the buffer
/// is not divisible into an integer number of values, the behavior for the remaining bytes it
/// not specified.
pub fn convert(self, ty: Type, buffer: &mut [u8], to: ByteOrder) {
self.convert_endian_bytes(ty.endian_bytes(), buffer, to)
}
pub(crate) fn convert_endian_bytes(self, cls: EndianBytes, buffer: &mut [u8], to: ByteOrder) {
if self == to {
return;
}
// FIXME: at MSRV 1.89 or higher use `slice::as_chunks_mut`.
match cls {
EndianBytes::One => {
// No change needed
}
EndianBytes::Two => {
for chunk in buffer.chunks_exact_mut(2) {
let chunk: &mut [u8; 2] = chunk.try_into().unwrap();
*chunk = u16::from_be_bytes(*chunk).to_le_bytes();
}
}
EndianBytes::Four => {
for chunk in buffer.chunks_exact_mut(4) {
let chunk: &mut [u8; 4] = chunk.try_into().unwrap();
*chunk = u32::from_be_bytes(*chunk).to_le_bytes();
}
}
EndianBytes::Eight => {
for chunk in buffer.chunks_exact_mut(8) {
let chunk: &mut [u8; 8] = chunk.try_into().unwrap();
*chunk = u64::from_be_bytes(*chunk).to_le_bytes();
}
}
}
}
}
/// The size of individual byte-order corrected elements.
#[derive(Clone, Copy)]
pub(crate) enum EndianBytes {
One,
Two,
Four,
Eight,
}
/// 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,
}
+12
View File
@@ -0,0 +1,12 @@
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() { for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename); let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
let img_file = File::open(path).expect("Cannot find test image!"); let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!( assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"), decoder.dimensions().expect("Cannot get dimensions"),
(64, 64) (64, 64)
+210
View File
@@ -0,0 +1,210 @@
extern crate tiff;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::ColorType;
use std::fs::File;
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images/";
/// Test a basic all white image
#[test]
fn test_white_ieee_fp16() {
let filenames = ["white-fp16.tiff"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(256, 256)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
for p in img {
assert!(p == half::f16::from_f32_const(1.0));
}
} else {
panic!("Wrong data type");
}
}
}
/// Test a single black pixel, to make sure scaling is ok
#[test]
fn test_one_black_pixel_ieee_fp16() {
let filenames = ["single-black-fp16.tiff"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(256, 256)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
for (i, p) in img.iter().enumerate() {
if i == 0 {
assert!(p < &half::f16::from_f32_const(0.001));
} else {
assert!(p == &half::f16::from_f32_const(1.0));
}
}
} else {
panic!("Wrong data type");
}
}
}
/// Test white with horizontal differencing predictor
#[test]
fn test_pattern_horizontal_differencing_ieee_fp16() {
let filenames = ["white-fp16-pred2.tiff"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(256, 256)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
// 0, 2, 5, 8, 12, 16, 255 are black
let black = [0, 2, 5, 8, 12, 16, 255];
for (i, p) in img.iter().enumerate() {
if black.contains(&i) {
assert!(p < &half::f16::from_f32_const(0.001));
} else {
assert!(p == &half::f16::from_f32_const(1.0));
}
}
} else {
panic!("Wrong data type");
}
}
}
/// Test white with floating point predictor
#[test]
fn test_pattern_predictor_ieee_fp16() {
let filenames = ["white-fp16-pred3.tiff"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(256, 256)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
// 0, 2, 5, 8, 12, 16, 255 are black
let black = [0, 2, 5, 8, 12, 16, 255];
for (i, p) in img.iter().enumerate() {
if black.contains(&i) {
assert!(p < &half::f16::from_f32_const(0.001));
} else {
assert!(p == &half::f16::from_f32_const(1.0));
}
}
} else {
panic!("Wrong data type");
}
}
}
/// Test several random images
/// we'rell compare against a pnm file, that scales from 0 (for 0.0) to 65767 (for 1.0)
#[test]
fn test_predictor_ieee_fp16() {
// first parse pnm, skip the first 4 \n
let pnm_path = PathBuf::from(TEST_IMAGE_DIR).join("random-fp16.pgm");
let pnm_bytes = std::fs::read(pnm_path).expect("Failed to read expected PNM file");
// PGM looks like this:
// ---
// P5
// #Created with GIMP
// 16 16
// 65535
// ... <big-endian bytes>
// ---
// get index of 4th \n
let byte_start = pnm_bytes
.iter()
.enumerate()
.filter(|(_, &v)| v == b'\n')
.map(|(i, _)| i)
.nth(3)
.expect("Must be 4 \\n's");
let pnm_values: Vec<f32> = pnm_bytes[(byte_start + 1)..]
.chunks(2)
.map(|slice| {
let bts = [slice[0], slice[1]];
(u16::from_be_bytes(bts) as f32) / (u16::MAX as f32)
})
.collect();
assert!(pnm_values.len() == 256);
let filenames = [
"random-fp16-pred2.tiff",
"random-fp16-pred3.tiff",
"random-fp16.tiff",
];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(16, 16)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
for (exp, found) in std::iter::zip(pnm_values.iter(), img.iter()) {
assert!((exp - found.to_f32()).abs() < 0.0001);
}
} else {
panic!("Wrong data type");
}
}
}
+82
View File
@@ -0,0 +1,82 @@
extern crate tiff;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::tags::Tag;
use tiff::ColorType;
use std::fs::File;
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/geotiff";
#[test]
fn test_geo_tiff() {
let filenames = ["geo-5b.tif"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::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"),
(10, 10)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Multiband {
bit_depth: 16,
num_samples: 5
}
);
assert_eq!(
decoder
.get_tag_u64(Tag::StripOffsets)
.expect("Cannot get StripOffsets"),
418
);
assert_eq!(
decoder
.get_tag_u64(Tag::RowsPerStrip)
.expect("Cannot get RowsPerStrip"),
10
);
assert_eq!(
decoder
.get_tag_u64(Tag::StripByteCounts)
.expect("Cannot get StripByteCounts"),
1000
);
assert_eq!(
decoder
.get_tag(Tag::ModelPixelScaleTag)
.expect("Cannot get pixel scale")
.into_f64_vec()
.expect("Cannot get pixel scale"),
vec![60.0, 60.0, 0.0]
);
let DecodingResult::I16(data) = decoder.read_image().unwrap() else {
panic!("Cannot read band data")
};
assert_eq!(data.len(), 500);
}
}
#[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();
}
}
}
+495 -11
View File
@@ -4,6 +4,7 @@ use tiff::decoder::{ifd, Decoder, DecodingResult};
use tiff::ColorType; use tiff::ColorType;
use std::fs::File; use std::fs::File;
use std::io::{Read, Seek};
use std::path::PathBuf; use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images/"; const TEST_IMAGE_DIR: &str = "./tests/images/";
@@ -13,7 +14,9 @@ macro_rules! test_image_sum {
fn $name(file: &str, expected_type: ColorType, expected_sum: $sum_ty) { fn $name(file: &str, expected_type: ColorType, expected_sum: $sum_ty) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file); let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!"); let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.colortype().unwrap(), expected_type); assert_eq!(decoder.colortype().unwrap(), expected_type);
let img_res = decoder.read_image().unwrap(); let img_res = decoder.read_image().unwrap();
@@ -43,14 +46,16 @@ test_image_sum!(test_image_sum_f64, F64, f64);
fn test_image_color_type_unsupported(file: &str, expected_type: ColorType) { fn test_image_color_type_unsupported(file: &str, expected_type: ColorType) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file); let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!"); let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.colortype().unwrap(), expected_type); assert_eq!(decoder.colortype().unwrap(), expected_type);
assert!(match decoder.read_image() { assert!(matches!(
decoder.read_image(),
Err(tiff::TiffError::UnsupportedError( Err(tiff::TiffError::UnsupportedError(
tiff::TiffUnsupportedError::UnsupportedColorType(_), tiff::TiffUnsupportedError::UnsupportedColorType(_),
)) => true, ))
_ => false, ));
});
} }
#[test] #[test]
@@ -165,14 +170,15 @@ fn test_string_tags() {
for filename in filenames.iter() { for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename); let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
let img_file = File::open(path).expect("Cannot find test image!"); let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
let software = decoder.get_tag(tiff::tags::Tag::Software).unwrap(); let software = decoder.get_tag(tiff::tags::Tag::Software).unwrap();
match software { match software {
ifd::Value::Ascii(s) => assert_eq!( ifd::Value::Ascii(s) => assert_eq!(
&s, &s,
"GraphicsMagick 1.2 unreleased Q16 http://www.GraphicsMagick.org/" "GraphicsMagick 1.2 unreleased Q16 http://www.GraphicsMagick.org/"
), ),
_ => assert!(false), other => unreachable!("Expected Ascii tag from file, got {other:?}"),
}; };
} }
} }
@@ -189,7 +195,8 @@ fn test_decode_data() {
} }
} }
let file = File::open("./tests/decodedata-rgb-3c-8b.tiff").unwrap(); 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.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100)); assert_eq!(decoder.dimensions().unwrap(), (100, 100));
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() { if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
@@ -216,6 +223,11 @@ fn issue_69() {
//assert!(img_res.is_ok()); //assert!(img_res.is_ok());
//} //}
#[test]
fn test_tiled_gray_i1() {
test_image_sum_u8("tiled-gray-i1.tif", ColorType::Gray(1), 30531);
}
#[test] #[test]
fn test_tiled_rgb_u8() { fn test_tiled_rgb_u8() {
test_image_sum_u8("tiled-rgb-u8.tif", ColorType::RGB(8), 39528948); test_image_sum_u8("tiled-rgb-u8.tif", ColorType::RGB(8), 39528948);
@@ -226,6 +238,55 @@ fn test_tiled_rect_rgb_u8() {
test_image_sum_u8("tiled-rect-rgb-u8.tif", ColorType::RGB(8), 62081032); test_image_sum_u8("tiled-rect-rgb-u8.tif", ColorType::RGB(8), 62081032);
} }
#[test]
fn test_inner_access() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-rect-rgb-u8.tif");
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::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
.get_tag(tiff::tags::Tag::Compression)
.unwrap()
.into_u16()
.unwrap();
assert_eq!(c, tiff::tags::CompressionMethod::None.to_u16());
// Because the image is uncompressed, reading the first tile directly with the inner reader
// should yield the same result as reading it with the decoder's read_chunk method.
let first_offset = decoder
.get_tag_u32_vec(tiff::tags::Tag::TileOffsets)
.unwrap()
.into_iter()
.next()
.unwrap();
let first_byte_count = decoder
.get_tag_u32_vec(tiff::tags::Tag::TileByteCounts)
.unwrap()
.into_iter()
.next()
.unwrap();
decoder
.inner()
.seek(std::io::SeekFrom::Start(first_offset as u64))
.expect("Cannot seek to tile offset");
let mut buf = vec![0; first_byte_count as usize];
decoder
.inner()
.read_exact(&mut buf)
.expect("Cannot read tile data");
let raw_sum: u64 = buf.into_iter().map(<u64>::from).sum();
match decoder.read_chunk(0).unwrap() {
DecodingResult::U8(chunk) => {
let sum: u64 = chunk.into_iter().map(<u64>::from).sum();
assert_eq!(sum, raw_sum);
}
_ => panic!("Wrong bit depth"),
}
}
/* #[test] /* #[test]
fn test_tiled_jpeg_rgb_u8() { fn test_tiled_jpeg_rgb_u8() {
test_image_sum_u8("tiled-jpeg-rgb-u8.tif", ColorType::RGB(8), 93031606); test_image_sum_u8("tiled-jpeg-rgb-u8.tif", ColorType::RGB(8), 93031606);
@@ -268,14 +329,15 @@ fn test_tiled_incremental() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file); let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!"); let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.colortype().unwrap(), expected_type); assert_eq!(decoder.colortype().unwrap(), expected_type);
let tiles = decoder.tile_count().unwrap(); let tiles = decoder.tile_count().unwrap();
assert_eq!(tiles as usize, sums.len()); assert_eq!(tiles as usize, sums.len());
for tile in 0..tiles { for tile in 0..tiles {
match decoder.read_tile().unwrap() { match decoder.read_chunk(tile).unwrap() {
DecodingResult::U8(res) => { DecodingResult::U8(res) => {
let sum: u64 = res.into_iter().map(<u64>::from).sum(); let sum: u64 = res.into_iter().map(<u64>::from).sum();
assert_eq!(sum, sums[tile as usize]); assert_eq!(sum, sums[tile as usize]);
@@ -284,3 +346,425 @@ fn test_tiled_incremental() {
} }
} }
} }
#[test]
fn test_planar_rgb_u8() {
test_image_sum_u8("planar-rgb-u8.tif", ColorType::RGB(8), 15417630);
}
#[test]
fn test_read_planar_bands() {
// gdal_translate tiled-rgb-u8.tif planar-rgb-u8.tif -co INTERLEAVE=BAND -co COMPRESS=LZW -co PROFILE=BASELINE
let file = "planar-rgb-u8.tif";
let expected_type = ColorType::RGB(8);
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::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();
assert_eq!(chunks as usize, 72);
// convert -quiet planar-rgb-u8.tif[0] -crop 1x1+0+0 txt:
// 0,0: (73,51,30) #49331E srgb(73,51,30)
// 1st band (red)
match decoder.read_chunk(0).unwrap() {
DecodingResult::U8(chunk) => {
assert_eq!(chunk[0], 73);
}
_ => panic!("Wrong bit depth"),
}
// 2nd band (green)
match decoder.read_chunk(chunks / 3).unwrap() {
DecodingResult::U8(chunk) => {
assert_eq!(chunk[0], 51);
}
_ => panic!("Wrong bit depth"),
}
// 3rd band (blue)
match decoder.read_chunk(chunks / 3 * 2).unwrap() {
DecodingResult::U8(chunk) => {
assert_eq!(chunk[0], 30);
}
_ => panic!("Wrong bit depth"),
}
}
#[test]
fn test_div_zero() {
use tiff::{TiffError, TiffFormatError};
let image = [
73, 73, 42, 0, 8, 0, 0, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 40, 1, 0, 0,
0, 158, 0, 0, 251, 3, 1, 3, 0, 1, 0, 0, 0, 1, 0, 0, 39, 6, 1, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0,
17, 1, 4, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 3, 0, 1, 0, 0, 0, 158, 0, 0, 251, 67, 1, 3, 0,
1, 0, 0, 0, 40, 0, 0, 0, 66, 1, 4, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 178, 178, 178, 178,
178, 178, 178,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let err = decoder.next_image().unwrap_err();
match err {
TiffError::FormatError(TiffFormatError::StripTileTagConflict) => {}
unexpected => panic!("Unexpected error {unexpected}"),
}
}
#[test]
fn test_too_many_value_bytes() {
let image = [
73, 73, 43, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 8, 0, 0, 0,
23, 0, 12, 0, 0, 65, 4, 0, 1, 6, 0, 0, 1, 16, 0, 1, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0,
0, 0, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0, 59, 73, 84, 186, 202, 83, 240, 66, 1, 53, 22, 56, 47,
0, 0, 0, 0, 0, 0, 1, 222, 4, 0, 58, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 4, 0, 0, 100, 0,
0, 89, 89, 89, 89, 89, 89, 89, 89, 96, 1, 20, 89, 89, 89, 89, 18,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let error = decoder.next_image().unwrap_err();
match error {
tiff::TiffError::LimitsExceeded => {}
unexpected => panic!("Unexpected error {unexpected}"),
}
}
#[test]
fn fuzzer_testcase5() {
let image = [
73, 73, 42, 0, 8, 0, 0, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 100, 0, 0, 0, 1, 1, 4, 0, 1, 0, 0,
0, 158, 0, 0, 251, 3, 1, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0, 6, 1, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0,
17, 1, 4, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 1, 3, 0, 0, 0, 0, 0, 246, 16, 0, 0, 22, 1, 4, 0, 1,
0, 0, 0, 40, 0, 251, 255, 23, 1, 4, 0, 1, 0, 0, 0, 48, 178, 178, 178, 178, 178, 178, 178,
178, 178, 178,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
fn fuzzer_testcase1() {
let image = [
73, 73, 42, 0, 8, 0, 0, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 99, 255, 255, 254, 1, 1, 4, 0, 1,
0, 0, 0, 158, 0, 0, 251, 3, 1, 3, 255, 254, 255, 255, 0, 1, 0, 0, 0, 6, 1, 3, 0, 1, 0, 0,
0, 0, 0, 0, 0, 17, 1, 4, 0, 9, 0, 0, 0, 0, 0, 0, 0, 2, 1, 3, 0, 2, 0, 0, 0, 63, 0, 0, 0,
22, 1, 4, 0, 1, 0, 0, 0, 44, 0, 0, 0, 23, 1, 4, 0, 0, 0, 0, 0, 0, 0, 2, 1, 3, 1, 0, 178,
178,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
fn fuzzer_testcase6() {
let image = [
73, 73, 42, 0, 8, 0, 0, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 100, 0, 0, 148, 1, 1, 4, 0, 1, 0,
0, 0, 158, 0, 0, 251, 3, 1, 3, 255, 254, 255, 255, 0, 1, 0, 0, 0, 6, 1, 3, 0, 1, 0, 0, 0,
0, 0, 0, 0, 17, 1, 4, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 1, 3, 0, 2, 0, 0, 0, 63, 0, 0, 0, 22,
1, 4, 0, 1, 0, 0, 0, 44, 0, 248, 255, 23, 1, 4, 0, 1, 0, 0, 0, 178, 178, 178, 0, 1, 178,
178, 178,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
fn oom() {
let image = [
73, 73, 42, 0, 8, 0, 0, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 40, 1, 0, 0,
0, 158, 0, 0, 251, 3, 1, 3, 0, 1, 0, 0, 0, 7, 0, 0, 0, 6, 1, 3, 0, 1, 0, 0, 0, 2, 0, 0, 0,
17, 1, 4, 0, 1, 0, 0, 0, 3, 77, 0, 0, 1, 1, 3, 0, 1, 0, 0, 0, 3, 128, 0, 0, 22, 1, 4, 0, 1,
0, 0, 0, 40, 0, 0, 0, 23, 1, 4, 0, 1, 0, 0, 0, 178, 48, 178, 178, 178, 178, 162, 178,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
fn fuzzer_testcase4() {
let image = [
73, 73, 42, 0, 8, 0, 0, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 40, 1, 0, 0,
0, 158, 0, 0, 251, 3, 1, 3, 0, 1, 0, 0, 0, 5, 0, 0, 0, 6, 1, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0,
17, 1, 4, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 3, 0, 1, 0, 0, 0, 3, 128, 0, 0, 22, 1, 4, 0, 1,
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 mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
fn fuzzer_testcase2() {
let image = [
73, 73, 42, 0, 8, 0, 0, 0, 15, 0, 0, 254, 44, 1, 0, 0, 0, 0, 0, 32, 0, 0, 0, 1, 4, 0, 1, 0,
0, 0, 0, 1, 0, 0, 91, 1, 1, 0, 0, 0, 0, 0, 242, 4, 0, 0, 0, 22, 0, 56, 77, 0, 77, 1, 0, 0,
73, 42, 0, 1, 4, 0, 1, 0, 0, 0, 4, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 158, 0, 0, 251, 3, 1,
3, 0, 1, 0, 0, 0, 7, 0, 0, 0, 6, 1, 3, 0, 1, 0, 0, 0, 2, 0, 0, 0, 17, 1, 4, 0, 1, 0, 0, 0,
0, 0, 0, 0, 1, 1, 3, 0, 1, 0, 0, 0, 0, 0, 0, 4, 61, 1, 18, 0, 1, 0, 0, 0, 202, 0, 0, 0, 17,
1, 100, 0, 129, 0, 0, 0, 0, 0, 0, 0, 232, 254, 252, 255, 254, 255, 255, 255, 1, 29, 0, 0,
22, 1, 3, 0, 1, 0, 0, 0, 16, 0, 0, 0, 23, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 123, 73, 254, 0,
73,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
fn invalid_jpeg_tag_2() {
let image = [
73, 73, 42, 0, 8, 0, 0, 0, 16, 0, 254, 0, 4, 0, 1, 0, 0, 0, 0, 0, 0, 242, 0, 1, 4, 0, 1, 0,
0, 0, 0, 129, 16, 0, 1, 1, 4, 0, 1, 0, 0, 0, 214, 0, 0, 248, 253, 1, 3, 0, 1, 0, 0, 0, 64,
0, 0, 0, 3, 1, 3, 0, 1, 0, 0, 0, 7, 0, 0, 0, 6, 1, 3, 0, 1, 0, 0, 0, 1, 0, 0, 64, 14, 1, 0,
2, 0, 0, 148, 0, 206, 0, 0, 0, 17, 1, 4, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
1, 0, 0, 0, 22, 1, 4, 0, 17, 0, 0, 201, 1, 0, 0, 0, 23, 1, 2, 0, 20, 0, 0, 0, 194, 0, 0, 0,
91, 1, 7, 0, 5, 0, 0, 0, 64, 0, 0, 0, 237, 254, 65, 255, 255, 255, 255, 255, 1, 0, 0, 0,
22, 1, 4, 0, 1, 0, 0, 0, 42, 0, 0, 0, 23, 1, 255, 255, 255, 255, 255, 36, 36, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 36, 73, 73, 0, 42, 36, 36, 36, 36, 0, 0, 8, 0,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
fn fuzzer_testcase3() {
let image = [
73, 73, 42, 0, 8, 0, 0, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 2, 0, 0, 0, 61, 1, 9, 0, 46, 22,
128, 0, 0, 0, 0, 1, 6, 1, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 17, 1, 4, 0, 27, 0, 0, 0, 0, 0, 0,
0, 1, 1, 3, 0, 1, 0, 0, 0, 17, 1, 0, 231, 22, 1, 1, 0, 1, 0, 0, 0, 130, 0, 0, 0, 23, 1, 4,
0, 14, 0, 0, 0, 0, 0, 0, 0, 133, 133, 133, 77, 77, 77, 0, 0, 22, 128, 0, 255, 255, 255,
255, 255,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
}
#[test]
fn timeout() {
use tiff::{TiffError, TiffFormatError};
let image = [
73, 73, 42, 0, 8, 0, 0, 0, 16, 0, 254, 0, 4, 0, 1, 68, 0, 0, 0, 2, 0, 32, 254, 252, 0, 109,
0, 129, 0, 0, 0, 32, 0, 58, 0, 1, 4, 0, 1, 0, 6, 0, 0, 0, 8, 0, 0, 1, 73, 73, 42, 0, 8, 0,
0, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 21, 0, 0, 0, 61, 1, 255, 128, 9, 0, 0, 8, 0, 1, 113, 2,
3, 1, 3, 0, 1, 0, 0, 0, 5, 0, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 112, 0, 0, 36, 0, 0,
0, 112, 56, 200, 0, 5, 0, 0, 64, 0, 0, 1, 0, 4, 0, 0, 0, 2, 0, 6, 1, 3, 0, 1, 0, 0, 0, 0,
0, 0, 4, 17, 1, 1, 0, 93, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 3, 6, 0, 231, 22, 1,
1, 0, 1, 0, 0, 0, 2, 64, 118, 36, 23, 1, 1, 0, 43, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 4, 0, 8,
0, 0, 73, 73, 42, 0, 8, 0, 0, 0, 0, 0, 32,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let error = decoder.next_image().unwrap_err();
match error {
TiffError::FormatError(TiffFormatError::CycleInOffsets) => {}
e => panic!("Unexpected error {e:?}"),
}
}
#[test]
fn test_no_rows_per_strip() {
test_image_sum_u8("no_rows_per_strip.tiff", ColorType::RGB(8), 99448840);
}
#[test]
fn test_predictor_3_rgb_f32() {
test_image_sum_f32("predictor-3-rgb-f32.tif", ColorType::RGB(32), 54004.33);
}
#[test]
fn test_predictor_3_gray_f32() {
test_image_sum_f32("predictor-3-gray-f32.tif", ColorType::Gray(32), 20008.275);
}
#[test]
#[cfg(feature = "zstd")]
fn test_zstd_compression() {
// gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif
test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396);
}
fn test_image_bytes(
file: &str,
expected: &[(ColorType, u32)],
// Represent all samples as little endian, ensuring check sums on all platforms.
normalize_byte_order: fn(&mut [u8]),
) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::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() {
if idx > 0 {
decoder
.next_image()
.expect("mismatched number of directories");
}
assert_eq!(decoder.colortype().unwrap(), expected_type);
let layout = decoder
.image_buffer_layout()
.expect("Cannot describe buffer layout");
buffer.resize(layout.len, 0u8);
decoder.read_image_bytes(&mut buffer).unwrap();
normalize_byte_order(&mut buffer);
// Well almost a crc..
assert_eq!(crc32fast::hash(&buffer), expected_crc);
}
}
fn byte_order_u16(bytes: &mut [u8]) {
for chunk in bytes.chunks_exact_mut(2) {
let n = u16::from_ne_bytes(chunk.try_into().unwrap());
chunk.copy_from_slice(&n.to_le_bytes());
}
}
fn byte_order_u32(bytes: &mut [u8]) {
for chunk in bytes.chunks_exact_mut(4) {
let n = u32::from_ne_bytes(chunk.try_into().unwrap());
chunk.copy_from_slice(&n.to_le_bytes());
}
}
fn byte_order_u64(bytes: &mut [u8]) {
for chunk in bytes.chunks_exact_mut(8) {
let n = u64::from_ne_bytes(chunk.try_into().unwrap());
chunk.copy_from_slice(&n.to_le_bytes());
}
}
#[test]
fn bytes_cmyk_3c_8b() {
test_image_bytes(
"cmyk-3c-8b.tiff",
&[(ColorType::CMYK(8), 2935032230)],
|_| (),
);
}
#[test]
fn bytes_gray_16b() {
test_image_bytes(
"minisblack-1c-16b.tiff",
&[(ColorType::Gray(16), 3524597615)],
byte_order_u16,
);
}
#[test]
fn bytes_gray_32b() {
test_image_bytes(
"gradient-1c-32b.tiff",
&[(ColorType::Gray(32), 2257036530)],
byte_order_u32,
);
}
#[test]
fn bytes_gray_u64() {
test_image_bytes(
"gradient-1c-64b.tiff",
&[(ColorType::Gray(64), 2156547113)],
byte_order_u64,
);
}
#[test]
fn bytes_gray_f32() {
test_image_bytes(
"gradient-1c-32b-float.tiff",
&[(ColorType::Gray(32), 3232356730)],
byte_order_u32,
);
}
#[test]
#[cfg(feature = "fax")]
fn test_fax4() {
test_image_sum_u8("fax4.tiff", ColorType::Gray(1), 7802706);
}
#[test]
#[cfg(feature = "fax")]
fn test_fax4_white_is_min() {
test_image_sum_u8("imagemagick_group4.tiff", ColorType::Gray(1), 3742820);
}
#[test]
fn write_extra_bits_image() {
use tiff::{encoder, tags::ExtraSamples};
let path = PathBuf::from(TEST_IMAGE_DIR).join("extra_bits_rgb_8b.tiff");
let img_file = File::create(path).expect("Cannot find test image!");
let mut encoder = encoder::TiffEncoder::new(img_file).expect("Cannot create encoder");
let mut img = encoder.new_image::<encoder::colortype::RGB8>(8, 8).unwrap();
img.extra_samples(&[ExtraSamples::Unspecified]).unwrap();
// all pixels are supposed to be 0, the extra is 1
let data: [u8; 256] = core::array::from_fn(|i| (i as u8 % 4).saturating_sub(2));
img.write_data(&data)
.expect("correct amount of data to write");
}
#[test]
fn extra_bits_gray() {
test_image_sum_u8(
"extra_bits_gray_8b.tiff",
ColorType::Multiband {
bit_depth: 8,
num_samples: 2,
},
64,
);
}
#[test]
fn extra_bits_rgb() {
test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGB(8), 0);
}
#[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");
}
}
+165
View File
@@ -0,0 +1,165 @@
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),
// FIXME: See <https://github.com/image-rs/image-tiff/issues/338>, this has tiles as
// StripOffsets and has a RowsPerStrip.
("tests/libtiffpic/cramps-tile.tif", false),
("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),
// FIXME: See <https://github.com/image-rs/image-tiff/issues/338>, this has tiles as
// StripOffsets and has a RowsPerStrip.
("tests/libtiffpic/quad-tile.tif", false),
("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);
}
+344 -20
View File
@@ -1,7 +1,7 @@
extern crate tiff; extern crate tiff;
use tiff::decoder::{ifd, Decoder, DecodingResult}; 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::tags::Tag;
use tiff::ColorType; use tiff::ColorType;
@@ -33,14 +33,60 @@ fn encode_decode() {
} }
{ {
file.seek(SeekFrom::Start(0)).unwrap(); file.seek(SeekFrom::Start(0)).unwrap();
let mut decoder = Decoder::new(&mut file).unwrap(); let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8)); assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100)); assert_eq!(decoder.dimensions().unwrap(), (100, 100));
let mut all_tags = decoder
.tag_iter()
.filter_map(Result::ok)
.collect::<Vec<_>>();
all_tags.sort_by_key(|(t, _)| t.to_u16());
assert_eq!(
all_tags,
vec![
(Tag::ImageWidth, ifd::Value::Unsigned(100)),
(Tag::ImageLength, ifd::Value::Unsigned(100)),
(
Tag::BitsPerSample,
ifd::Value::List(vec![
ifd::Value::Short(8),
ifd::Value::Short(8),
ifd::Value::Short(8)
])
),
(Tag::Compression, ifd::Value::Short(1)),
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
(Tag::StripOffsets, ifd::Value::Unsigned(35)),
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
(Tag::StripByteCounts, ifd::Value::Unsigned(30000)),
(Tag::XResolution, ifd::Value::Rational(1, 1)),
(Tag::YResolution, ifd::Value::Rational(1, 1)),
(Tag::ResolutionUnit, ifd::Value::Short(1)),
(Tag::Artist, ifd::Value::Ascii("Image-tiff".into())),
(Tag::Predictor, ifd::Value::Short(1)),
(
Tag::SampleFormat,
ifd::Value::List(vec![
ifd::Value::Short(1),
ifd::Value::Short(1),
ifd::Value::Short(1)
])
),
]
);
assert_eq!( assert_eq!(
decoder.get_tag(Tag::Artist).unwrap(), decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into()) ifd::Value::Ascii("Image-tiff".into())
); );
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
if let tiff::decoder::DecodingResult::U8(img_res) = buffer {
assert_eq!(image_data, img_res); assert_eq!(image_data, img_res);
} else { } else {
panic!("Wrong data type"); panic!("Wrong data type");
@@ -72,14 +118,59 @@ fn encode_decode_big() {
} }
{ {
file.seek(SeekFrom::Start(0)).unwrap(); file.seek(SeekFrom::Start(0)).unwrap();
let mut decoder = Decoder::new(&mut file).unwrap(); let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8)); assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100)); assert_eq!(decoder.dimensions().unwrap(), (100, 100));
let mut all_tags = decoder
.tag_iter()
.filter_map(Result::ok)
.collect::<Vec<_>>();
all_tags.sort_by_key(|(t, _)| t.to_u16());
assert_eq!(
all_tags,
vec![
(Tag::ImageWidth, ifd::Value::Unsigned(100)),
(Tag::ImageLength, ifd::Value::Unsigned(100)),
(
Tag::BitsPerSample,
ifd::Value::List(vec![
ifd::Value::Short(8),
ifd::Value::Short(8),
ifd::Value::Short(8)
])
),
(Tag::Compression, ifd::Value::Short(1)),
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
(Tag::StripOffsets, ifd::Value::UnsignedBig(27)),
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
(Tag::StripByteCounts, ifd::Value::UnsignedBig(30000)),
(Tag::XResolution, ifd::Value::Rational(1, 1)),
(Tag::YResolution, ifd::Value::Rational(1, 1)),
(Tag::ResolutionUnit, ifd::Value::Short(1)),
(Tag::Artist, ifd::Value::Ascii("Image-tiff".into())),
(Tag::Predictor, ifd::Value::Short(1)),
(
Tag::SampleFormat,
ifd::Value::List(vec![
ifd::Value::Short(1),
ifd::Value::Short(1),
ifd::Value::Short(1)
])
),
]
);
assert_eq!( assert_eq!(
decoder.get_tag(Tag::Artist).unwrap(), decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into()) ifd::Value::Ascii("Image-tiff".into())
); );
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() { let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::U8(img_res) = buffer {
assert_eq!(image_data, img_res); assert_eq!(image_data, img_res);
} else { } else {
panic!("Wrong data type"); panic!("Wrong data type");
@@ -94,6 +185,7 @@ fn test_encode_ifd() {
{ {
let mut tiff = TiffEncoder::new(&mut data).unwrap(); let mut tiff = TiffEncoder::new(&mut data).unwrap();
let mut image_encoder = tiff.new_image::<colortype::Gray8>(1, 1).unwrap(); let mut image_encoder = tiff.new_image::<colortype::Gray8>(1, 1).unwrap();
image_encoder.write_strip(&[1]).unwrap();
let encoder = image_encoder.encoder(); let encoder = image_encoder.encoder();
// Use the "reusable" tags section as per the TIFF6 spec // Use the "reusable" tags section as per the TIFF6 spec
@@ -119,8 +211,10 @@ fn test_encode_ifd() {
// Rewind the cursor for reading // Rewind the cursor for reading
data.set_position(0); data.set_position(0);
{ {
let mut decoder = Decoder::new(&mut data).unwrap(); let mut decoder = Decoder::open(&mut data).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.assert_tag_u32(65000), 42); assert_eq!(decoder.assert_tag_u32(65000), 42);
assert_eq!(decoder.assert_tag_u32_vec(65000), [42]); assert_eq!(decoder.assert_tag_u32_vec(65000), [42]);
@@ -152,16 +246,22 @@ const TEST_IMAGE_DIR: &str = "./tests/images/";
macro_rules! test_roundtrip { macro_rules! test_roundtrip {
($name:ident, $buffer:ident, $buffer_ty:ty) => { ($name:ident, $buffer:ident, $buffer_ty:ty) => {
#[track_caller]
fn $name<C: colortype::ColorType<Inner = $buffer_ty>>( fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
file: &str, file: &str,
expected_type: ColorType, expected_type: ColorType,
) { ) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file); let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!"); let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), expected_type); assert_eq!(decoder.colortype().unwrap(), expected_type);
let image_data = match decoder.read_image().unwrap() { let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap();
let image_data = match buffer {
DecodingResult::$buffer(res) => res, DecodingResult::$buffer(res) => res,
_ => panic!("Wrong data type"), _ => panic!("Wrong data type"),
}; };
@@ -175,9 +275,12 @@ macro_rules! test_roundtrip {
} }
file.seek(SeekFrom::Start(0)).unwrap(); file.seek(SeekFrom::Start(0)).unwrap();
{ {
let mut decoder = Decoder::new(&mut file).unwrap(); let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
if let DecodingResult::$buffer(img_res) = decoder.read_image().unwrap() { let mut decoder = Decoder::open(&mut file).unwrap();
assert_eq!(image_data, img_res); 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 { } else {
panic!("Wrong data type"); panic!("Wrong data type");
} }
@@ -187,7 +290,9 @@ macro_rules! test_roundtrip {
} }
test_roundtrip!(test_u8_roundtrip, U8, u8); test_roundtrip!(test_u8_roundtrip, U8, u8);
test_roundtrip!(test_i8_roundtrip, I8, i8);
test_roundtrip!(test_u16_roundtrip, U16, u16); test_roundtrip!(test_u16_roundtrip, U16, u16);
test_roundtrip!(test_i16_roundtrip, I16, i16);
test_roundtrip!(test_u32_roundtrip, U32, u32); test_roundtrip!(test_u32_roundtrip, U32, u32);
test_roundtrip!(test_u64_roundtrip, U64, u64); test_roundtrip!(test_u64_roundtrip, U64, u64);
test_roundtrip!(test_f32_roundtrip, F32, f32); test_roundtrip!(test_f32_roundtrip, F32, f32);
@@ -198,6 +303,11 @@ fn test_gray_u8_roundtrip() {
test_u8_roundtrip::<colortype::Gray8>("minisblack-1c-8b.tiff", ColorType::Gray(8)); test_u8_roundtrip::<colortype::Gray8>("minisblack-1c-8b.tiff", ColorType::Gray(8));
} }
#[test]
fn test_gray_i8_roundtrip() {
test_i8_roundtrip::<colortype::GrayI8>("minisblack-1c-i8b.tiff", ColorType::Gray(8));
}
#[test] #[test]
fn test_rgb_u8_roundtrip() { fn test_rgb_u8_roundtrip() {
test_u8_roundtrip::<colortype::RGB8>("rgb-3c-8b.tiff", ColorType::RGB(8)); test_u8_roundtrip::<colortype::RGB8>("rgb-3c-8b.tiff", ColorType::RGB(8));
@@ -208,11 +318,21 @@ fn test_cmyk_u8_roundtrip() {
test_u8_roundtrip::<colortype::CMYK8>("cmyk-3c-8b.tiff", ColorType::CMYK(8)); test_u8_roundtrip::<colortype::CMYK8>("cmyk-3c-8b.tiff", ColorType::CMYK(8));
} }
#[test]
fn test_cmyka_u8_roundtrip() {
test_u8_roundtrip::<colortype::CMYKA8>("cmyk-4c-8b.tiff", ColorType::CMYKA(8));
}
#[test] #[test]
fn test_gray_u16_roundtrip() { fn test_gray_u16_roundtrip() {
test_u16_roundtrip::<colortype::Gray16>("minisblack-1c-16b.tiff", ColorType::Gray(16)); test_u16_roundtrip::<colortype::Gray16>("minisblack-1c-16b.tiff", ColorType::Gray(16));
} }
#[test]
fn test_gray_i16_roundtrip() {
test_i16_roundtrip::<colortype::GrayI16>("minisblack-1c-i16b.tiff", ColorType::Gray(16));
}
#[test] #[test]
fn test_rgb_u16_roundtrip() { fn test_rgb_u16_roundtrip() {
test_u16_roundtrip::<colortype::RGB16>("rgb-3c-16b.tiff", ColorType::RGB(16)); test_u16_roundtrip::<colortype::RGB16>("rgb-3c-16b.tiff", ColorType::RGB(16));
@@ -263,6 +383,149 @@ fn test_gray_f64_roundtrip() {
test_f64_roundtrip::<colortype::Gray64Float>("gradient-1c-64b-float.tiff", ColorType::Gray(64)); test_f64_roundtrip::<colortype::Gray64Float>("gradient-1c-64b-float.tiff", ColorType::Gray(64));
} }
#[test]
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 { trait AssertDecode {
fn assert_tag_u32(&mut self, tag: u16) -> u32; fn assert_tag_u32(&mut self, tag: u16) -> u32;
fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32>; fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32>;
@@ -320,6 +583,7 @@ fn test_multiple_byte() {
{ {
let mut tiff = TiffEncoder::new(&mut data).unwrap(); let mut tiff = TiffEncoder::new(&mut data).unwrap();
let mut image_encoder = tiff.new_image::<colortype::Gray8>(1, 1).unwrap(); let mut image_encoder = tiff.new_image::<colortype::Gray8>(1, 1).unwrap();
image_encoder.write_strip(&[1]).unwrap();
let encoder = image_encoder.encoder(); let encoder = image_encoder.encoder();
encoder.write_tag(Tag::Unknown(65000), &[1_u8][..]).unwrap(); encoder.write_tag(Tag::Unknown(65000), &[1_u8][..]).unwrap();
@@ -339,7 +603,8 @@ fn test_multiple_byte() {
data.set_position(0); data.set_position(0);
{ {
let mut decoder = Decoder::new(&mut data).unwrap(); let mut decoder = Decoder::open(&mut data).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.assert_tag_u32_vec(65000), [1]); assert_eq!(decoder.assert_tag_u32_vec(65000), [1]);
assert_eq!(decoder.assert_tag_u32_vec(65001), [1, 2]); assert_eq!(decoder.assert_tag_u32_vec(65001), [1, 2]);
@@ -360,6 +625,7 @@ fn test_signed() {
{ {
let mut tiff = TiffEncoder::new(&mut data).unwrap(); let mut tiff = TiffEncoder::new(&mut data).unwrap();
let mut image_encoder = tiff.new_image::<colortype::Gray8>(1, 1).unwrap(); let mut image_encoder = tiff.new_image::<colortype::Gray8>(1, 1).unwrap();
image_encoder.write_strip(&[1]).unwrap();
let encoder = image_encoder.encoder(); let encoder = image_encoder.encoder();
//Use the "reusable" tags section as per the TIFF6 spec //Use the "reusable" tags section as per the TIFF6 spec
@@ -419,7 +685,8 @@ fn test_signed() {
//Rewind the cursor for reading //Rewind the cursor for reading
data.set_position(0); data.set_position(0);
{ {
let mut decoder = Decoder::new(&mut data).unwrap(); let mut decoder = Decoder::open(&mut data).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.assert_tag_i32(65000), -1); assert_eq!(decoder.assert_tag_i32(65000), -1);
assert_eq!(decoder.assert_tag_i32_vec(65001), [-1]); assert_eq!(decoder.assert_tag_i32_vec(65001), [-1]);
@@ -471,13 +738,14 @@ fn test_multipage_image() {
img_file.seek(SeekFrom::Start(0)).unwrap(); img_file.seek(SeekFrom::Start(0)).unwrap();
{ {
let mut img_decoder = Decoder::new(&mut img_file).unwrap(); let mut decoder = Decoder::open(&mut img_file).unwrap();
decoder.next_image().unwrap();
// check the dimensions of the image in the first page // check the dimensions of the image in the first page
assert_eq!(img_decoder.dimensions().unwrap(), (2, 2)); assert_eq!(decoder.dimensions().unwrap(), (2, 2));
img_decoder.next_image().unwrap(); decoder.next_image().unwrap();
// check the dimensions of the image in the second page // check the dimensions of the image in the second page
assert_eq!(img_decoder.dimensions().unwrap(), (3, 3)); assert_eq!(decoder.dimensions().unwrap(), (3, 3));
} }
} }
@@ -506,16 +774,72 @@ fn test_rows_per_strip() {
file.seek(SeekFrom::Start(0)).unwrap(); file.seek(SeekFrom::Start(0)).unwrap();
{ {
let mut decoder = Decoder::new(&mut file).unwrap(); let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.get_tag_u64(Tag::RowsPerStrip).unwrap(), 2); assert_eq!(decoder.get_tag_u64(Tag::RowsPerStrip).unwrap(), 2);
assert_eq!(decoder.strip_count().unwrap(), 50); assert_eq!(decoder.strip_count().unwrap(), 50);
for i in 0..50 { for i in 0..50 {
let img2 = [i; 2 * 100]; let img2 = [i; 2 * 100];
match decoder.read_strip().unwrap() { match decoder.read_chunk(i as u32).unwrap() {
DecodingResult::U8(data) => assert_eq!(&img2[..], &data[..]), DecodingResult::U8(data) => assert_eq!(&img2[..], &data[..]),
other => panic!("Incorrect strip type {:?}", other), other => panic!("Incorrect strip type {other:?}"),
} }
} }
} }
} }
#[test]
fn test_auxiliary_directory() {
const ARTIST: &str = "Auxiliary-tiff";
let mut data = Cursor::new(Vec::new());
let mut tiff = TiffEncoder::new(&mut data).unwrap();
{
let mut image_encoder = tiff.new_image::<colortype::Gray8>(1, 1).unwrap();
image_encoder.write_strip(&[1]).unwrap();
}
let exif = {
let mut extra = tiff.extra_directory().unwrap();
extra.write_tag(Tag::Artist, ARTIST).unwrap();
extra.finish_with_offsets().unwrap()
};
{
let mut image_encoder = tiff.new_image::<colortype::Gray8>(2, 1).unwrap();
image_encoder.write_strip(&[1, 2]).unwrap();
let encoder = image_encoder.encoder();
encoder.write_tag(Tag::ExifDirectory, exif.offset).unwrap();
}
data.set_position(0);
let mut decoder = Decoder::open(&mut data).unwrap();
{
decoder.next_image().unwrap();
assert_eq!(decoder.dimensions().unwrap(), (1, 1));
let _ = decoder.read_image().unwrap();
}
{
decoder.next_image().unwrap();
assert_eq!(decoder.dimensions().unwrap(), (2, 1));
let _ = decoder.read_image().unwrap();
let exif_location = decoder
.get_tag(Tag::ExifDirectory)
.expect("second directory missing Exif")
.into_ifd_pointer()
.expect("exif directory is not an IFD pointer");
let exif = decoder.read_directory(exif_location).unwrap();
let artist = decoder
.read_directory_tags(&exif)
.get_tag_ascii_string(Tag::Artist)
.expect("EXIF directory missing artist tag");
assert_eq!(artist, ARTIST);
}
}
+158
View File
@@ -0,0 +1,158 @@
extern crate tiff;
use std::io::{Cursor, Seek, Write};
use tiff::{
decoder::{Decoder, DecodingResult},
encoder::{
colortype::{self, ColorType},
Compression, TiffEncoder, TiffValue,
},
};
trait TestImage<const NUM_CHANNELS: usize>: From<Vec<<Self::Color as ColorType>::Inner>> {
const WIDTH: u32;
const HEIGHT: u32;
type Color: ColorType;
fn reference_data(&self) -> &[<Self::Color as ColorType>::Inner];
fn generate_pixel(x: u32, y: u32) -> [<Self::Color as ColorType>::Inner; NUM_CHANNELS];
fn compress<W: Write + Seek>(&self, encoder: &mut TiffEncoder<W>)
where
[<Self::Color as ColorType>::Inner]: TiffValue,
{
let image = encoder
.new_image::<Self::Color>(Self::WIDTH, Self::HEIGHT)
.unwrap();
image.write_data(self.reference_data()).unwrap();
}
fn generate() -> Self {
assert_eq!(
Self::Color::BITS_PER_SAMPLE.len(),
NUM_CHANNELS,
"Incompatible color type"
);
let mut data = Vec::with_capacity((Self::WIDTH * Self::HEIGHT) as usize * NUM_CHANNELS);
for x in 0..Self::WIDTH {
for y in 0..Self::HEIGHT {
data.extend(IntoIterator::into_iter(Self::generate_pixel(x, y)));
}
}
Self::from(data)
}
}
struct TestImageColor(Vec<u16>);
impl From<Vec<u16>> for TestImageColor {
fn from(value: Vec<u16>) -> Self {
Self(value)
}
}
impl TestImage<3> for TestImageColor {
const WIDTH: u32 = 1;
const HEIGHT: u32 = 7;
type Color = colortype::RGB16;
fn reference_data(&self) -> &[u16] {
&self.0
}
fn generate_pixel(x: u32, y: u32) -> [<Self::Color as ColorType>::Inner; 3] {
let val = (x + y) % <Self::Color as ColorType>::Inner::MAX as u32;
[val as <Self::Color as ColorType>::Inner; 3]
}
}
struct TestImageGrayscale(Vec<u8>);
impl From<Vec<u8>> for TestImageGrayscale {
fn from(value: Vec<u8>) -> Self {
Self(value)
}
}
impl TestImage<1> for TestImageGrayscale {
const WIDTH: u32 = 21;
const HEIGHT: u32 = 10;
type Color = colortype::Gray8;
fn reference_data(&self) -> &[u8] {
&self.0
}
fn generate_pixel(x: u32, y: u32) -> [<Self::Color as ColorType>::Inner; 1] {
let val = (x + y) % <Self::Color as ColorType>::Inner::MAX as u32;
[val as <Self::Color as ColorType>::Inner]
}
}
fn encode_decode_with_compression(compression: Compression) {
let mut data = Cursor::new(Vec::new());
let image_rgb = TestImageColor::generate();
let image_grayscale = TestImageGrayscale::generate();
// Encode tiff with compression
{
// Create a multipage image with 2 images
let mut encoder = TiffEncoder::new(&mut data)
.unwrap()
.with_compression(compression);
image_rgb.compress(&mut encoder);
image_grayscale.compress(&mut encoder);
}
// Decode tiff
data.set_position(0);
{
let mut decoder = Decoder::open(data).unwrap();
decoder.next_image().unwrap();
// Check the RGB image
assert_eq!(
match decoder.read_image() {
Ok(DecodingResult::U16(image_data)) => image_data,
unexpected => panic!("Descoding RGB failed: {unexpected:?}"),
},
image_rgb.reference_data()
);
// Check the grayscale image
decoder.next_image().unwrap();
assert_eq!(
match decoder.read_image() {
Ok(DecodingResult::U8(image_data)) => image_data,
unexpected => panic!("Decoding grayscale failed: {unexpected:?}"),
},
image_grayscale.reference_data()
);
}
}
#[test]
fn encode_decode_without_compression() {
encode_decode_with_compression(Compression::Uncompressed);
}
#[test]
#[cfg(feature = "lzw")]
fn encode_decode_with_lzw() {
encode_decode_with_compression(Compression::Lzw);
}
#[test]
#[cfg(feature = "deflate")]
fn encode_decode_with_deflate() {
encode_decode_with_compression(Compression::Deflate(1));
encode_decode_with_compression(Compression::Deflate(6));
encode_decode_with_compression(Compression::Deflate(9));
}
#[test]
fn encode_decode_with_packbits() {
encode_decode_with_compression(Compression::Packbits);
}
+2 -1
View File
@@ -13,7 +13,8 @@ fn test_directory<F: Fn(File) -> bool>(path: &str, f: F) {
} }
fn decode_tiff(file: File) -> TiffResult<()> { fn decode_tiff(file: File) -> TiffResult<()> {
let mut decoder = Decoder::new(file)?; let mut decoder = Decoder::open(file)?;
decoder.next_image()?;
decoder.read_image()?; decoder.read_image()?;
Ok(()) Ok(())
} }
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.
+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.
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff. Show more