Compare commits
261
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7d24d0f7c | ||
|
|
58e93fb8a3 | ||
|
|
e8be8656f2 | ||
|
|
e1e9f36c5a | ||
|
|
0ad653433a | ||
|
|
bf79af8de4 | ||
|
|
54dddc4943 | ||
|
|
be9b3a04b9 | ||
|
|
08f1b25fac | ||
|
|
c7b97519a0 | ||
|
|
e093e15bbe | ||
|
|
647e9b4a81 | ||
|
|
31c49ea6ef | ||
|
|
0362e6e31d | ||
|
|
0fe5b2804e | ||
|
|
a589010099 | ||
|
|
53498074a4 | ||
|
|
cb543cded7 | ||
|
|
cf8f96568e | ||
|
|
4ff88e658d | ||
|
|
14ebf376f3 | ||
|
|
37578add53 | ||
|
|
2b9476566b | ||
|
|
448a0e61d1 | ||
|
|
d872ff8751 | ||
|
|
0a878888db | ||
|
|
a0a95ef86e | ||
|
|
bbb5e5a41b | ||
|
|
f74169d4af | ||
|
|
c304a3cc13 | ||
|
|
9a9193345c | ||
|
|
22046d4272 | ||
|
|
2c5fe2c2f5 | ||
|
|
eceaa1f14d | ||
|
|
d42db25c27 | ||
|
|
c74f23784c | ||
|
|
8b0da5939a | ||
|
|
fdde3467fc | ||
|
|
3f2066bb20 | ||
|
|
2ac8be1177 | ||
|
|
c81d0a9c94 | ||
|
|
983c5baea0 | ||
|
|
abf3cc617d | ||
|
|
fb4ee9b377 | ||
|
|
45cd3c3b63 | ||
|
|
701fcc46b8 | ||
|
|
e41159ce05 | ||
|
|
03979faef8 | ||
|
|
857973cb21 | ||
|
|
cef21c47fb | ||
|
|
a0191ed1a7 | ||
|
|
99a7b8cba7 | ||
|
|
418ea182c3 | ||
|
|
529fa64e13 | ||
|
|
3d6b94b65a | ||
|
|
a51b23091e | ||
|
|
ac65bd8477 | ||
|
|
1bd6293857 | ||
|
|
87341f9075 | ||
|
|
43343f64cd | ||
|
|
be04326264 | ||
|
|
93a68f52f5 | ||
|
|
5f329840c8 | ||
|
|
1d138a5568 | ||
|
|
9a23090b6e | ||
|
|
9d409433e8 | ||
|
|
cf680424d9 | ||
|
|
d3b777c961 | ||
|
|
24b008a7c3 | ||
|
|
34bd1da99e | ||
|
|
cfff04c97d | ||
|
|
915cc20945 | ||
|
|
74b2857364 | ||
|
|
1dab89f10e | ||
|
|
45f617b9f5 | ||
|
|
170bd3697c | ||
|
|
855eb4ef23 | ||
|
|
fee650d7f6 | ||
|
|
ee8d13249e | ||
|
|
169a896851 | ||
|
|
082a35adb9 | ||
|
|
923df5f0bd | ||
|
|
c4328644b8 | ||
|
|
27bfcf5562 | ||
|
|
7d498a0284 | ||
|
|
5b21bcc75c | ||
|
|
fe305ec24a | ||
|
|
a3ab252168 | ||
|
|
bd64e438a4 | ||
|
|
fa225e820b | ||
|
|
ea373bae00 | ||
|
|
23d1867f75 | ||
|
|
234b2ba677 | ||
|
|
a63a01319c | ||
|
|
efd2a6dbd8 | ||
|
|
d38fba4268 | ||
|
|
1732481b01 | ||
|
|
355ecc8ad9 | ||
|
|
2d90179602 | ||
|
|
5a3b11314e | ||
|
|
5150e91bbb | ||
|
|
6d56d014a1 | ||
|
|
af8a8d660f | ||
|
|
b1c74495ce | ||
|
|
1f02ff8ca1 | ||
|
|
5b9dc30919 | ||
|
|
c3882b252c | ||
|
|
6fe21da979 | ||
|
|
0de1261948 | ||
|
|
6439880e3d | ||
|
|
dce1c40bd5 | ||
|
|
5d3a5242ea | ||
|
|
58543962f3 | ||
|
|
95f2d475fa | ||
|
|
f9eaf9632a | ||
|
|
61b3027f38 | ||
|
|
65662c4834 | ||
|
|
985f3822c8 | ||
|
|
2eef9ac088 | ||
|
|
f6d95249dd | ||
|
|
8517bb81db | ||
|
|
eb6275547d | ||
|
|
73f066c09d | ||
|
|
24887896ff | ||
|
|
02118620e2 | ||
|
|
0b5dc72454 | ||
|
|
34e35fb27e | ||
|
|
bec6558aa8 | ||
|
|
6cdf3d8d22 | ||
|
|
0a2b3f91ab | ||
|
|
99b3c03ce3 | ||
|
|
bec8e495a5 | ||
|
|
de7dbad18b | ||
|
|
19ac30ac3f | ||
|
|
2058b7f3ca | ||
|
|
edd260fe7e | ||
|
|
2ccbe132e0 | ||
|
|
714e470595 | ||
|
|
4adba46b9f | ||
|
|
695208cffe | ||
|
|
4acdfea783 | ||
|
|
64286c2110 | ||
|
|
12c98535f6 | ||
|
|
1556713d6e | ||
|
|
682ef0c375 | ||
|
|
bc71086a96 | ||
|
|
e67bac763f | ||
|
|
839655b975 | ||
|
|
85635c3cd2 | ||
|
|
481a2ab54a | ||
|
|
7c6c327817 | ||
|
|
9f1de91880 | ||
|
|
4a68feab5a | ||
|
|
b5afb8cca4 | ||
|
|
c8d43262e9 | ||
|
|
80ce77190e | ||
|
|
c75aa98ce2 | ||
|
|
c1087f6f08 | ||
|
|
8cc6bc657d | ||
|
|
9fcc2a3b65 | ||
|
|
666f871d30 | ||
|
|
a919b8febf | ||
|
|
85b5df7a62 | ||
|
|
756ea3e813 | ||
|
|
0eead083f6 | ||
|
|
34232d8141 | ||
|
|
cbcf6b97d4 | ||
|
|
19c811e86d | ||
|
|
50be15e356 | ||
|
|
718f2cb455 | ||
|
|
36a16db659 | ||
|
|
d70c31dcbf | ||
|
|
b589d10a27 | ||
|
|
f4378eeb80 | ||
|
|
49015c6e93 | ||
|
|
29402875cb | ||
|
|
33453289b6 | ||
|
|
ce0ca5c234 | ||
|
|
f671293bd8 | ||
|
|
48d52bed9b | ||
|
|
7707060982 | ||
|
|
c370ac4b92 | ||
|
|
b2bcbbaf23 | ||
|
|
d07da5a6cb | ||
|
|
a3c81e9830 | ||
|
|
46e51cc67e | ||
|
|
f6a762b72c | ||
|
|
23672b2490 | ||
|
|
ca74c03520 | ||
|
|
2942f9de97 | ||
|
|
29add10ca7 | ||
|
|
59d32a66a7 | ||
|
|
5a6b2ad8f3 | ||
|
|
c746653269 | ||
|
|
cc75d051af | ||
|
|
ab4fb4e959 | ||
|
|
7b1683687e | ||
|
|
2e51ee4b15 | ||
|
|
f6e2aeffe5 | ||
|
|
525bb2b9b1 | ||
|
|
54f733eaa3 | ||
|
|
9b0c9ca84c | ||
|
|
890488b16e | ||
|
|
3e530d27ea | ||
|
|
02699c9588 | ||
|
|
26b6afc787 | ||
|
|
5df003368b | ||
|
|
a582fdcc72 | ||
|
|
e62cdfbf42 | ||
|
|
f021e3e599 | ||
|
|
f5ab3b363b | ||
|
|
f33c7be21c | ||
|
|
98cd3dc2d2 | ||
|
|
6275b7a36d | ||
|
|
97e230584f | ||
|
|
43d7f66c9e | ||
|
|
a1be18b548 | ||
|
|
df4e42eecc | ||
|
|
816fb85304 | ||
|
|
7648317643 | ||
|
|
140f2788de | ||
|
|
46086e2bcf | ||
|
|
d7be2a7b67 | ||
|
|
29b71ff597 | ||
|
|
83e6d64751 | ||
|
|
dc2c67469b | ||
|
|
b5d8ee5f86 | ||
|
|
f44da4a00d | ||
|
|
ee0dbf373b | ||
|
|
aeab243a96 | ||
|
|
36f7b3b4aa | ||
|
|
3bfb43e83e | ||
|
|
d42e6be507 | ||
|
|
a19cf37587 | ||
|
|
ff52aea06e | ||
|
|
96d3a02efe | ||
|
|
e28ad564d9 | ||
|
|
de6d9cf131 | ||
|
|
6dc7a266d3 | ||
|
|
307cd8be97 | ||
|
|
0c54a18e21 | ||
|
|
90ae5b8e54 | ||
|
|
9b06e3726d | ||
|
|
895c70a1ad | ||
|
|
b0ba4a6788 | ||
|
|
4a2e76858b | ||
|
|
a67f50568c | ||
|
|
d6f6b8a01e | ||
|
|
8ec91b64f6 | ||
|
|
60f866c119 | ||
|
|
daa0e3eabf | ||
|
|
24d203c7c3 | ||
|
|
473868a7bb | ||
|
|
aefea7786f | ||
|
|
d1ad9b2664 | ||
|
|
bd1d085189 | ||
|
|
fcfcecb2df | ||
|
|
aa10d27a28 | ||
|
|
8fa175c371 | ||
|
|
2a15aa6b7e | ||
|
|
42feeba8da |
No files matched your search
+73
-19
@@ -1,40 +1,94 @@
|
||||
name: Rust CI
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.61", stable, beta, nightly]
|
||||
command: [build, test]
|
||||
rust: ["1.88.0", stable, beta, nightly]
|
||||
steps:
|
||||
- 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
|
||||
run: >
|
||||
cargo build --verbose --no-default-features --features "$FEATURES"
|
||||
run: cargo build -v
|
||||
- name: test
|
||||
run: >
|
||||
cargo test --tests --benches --no-default-features --features "$FEATURES"
|
||||
if: ${{ matrix.rust != '1.61' }}
|
||||
env:
|
||||
FEATURES: ${{ matrix.features }}
|
||||
if: ${{ matrix.rust != '1.88.0' }}
|
||||
run: cargo test -v && cargo doc -v
|
||||
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt
|
||||
- name: Run rustfmt check
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: -- --check
|
||||
run: cargo fmt -- --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
|
||||
+209
@@ -1,3 +1,212 @@
|
||||
# Version 0.12.0 (unreleased)
|
||||
|
||||
Additions:
|
||||
- Added the `TiffHeader` type. It can be used to create a `Decoder` in a state
|
||||
before the first IFD and allows opening files at an arbitrary first IFD
|
||||
offset rather than one indicated by an encoded header at the start.
|
||||
|
||||
Changes:
|
||||
- The IFD iteration methods on `Decoder` (`next_image`, `seek_to_image`, etc)
|
||||
now come with a strong error guarantee. They will not modify the current
|
||||
directory state if they fail. This includes the failure to interpret an IFD
|
||||
as a supported image in the case of `*_image` variants.
|
||||
|
||||
Compatibility:
|
||||
- Changed how tiled TIFF files are detected. The presence of a TileWidth OR
|
||||
TileHeight/TileLength tag now marks an image as tiled. Data is no retrieved
|
||||
from either StripOffsets or TileOffsets with the latter taking precedence.
|
||||
This is closer to the behavior of libtiff.
|
||||
|
||||
# Version 0.11.3
|
||||
|
||||
Additions:
|
||||
- Added support for `webp` compression (as per GDAL) under the `webp` feature flag (default: off).
|
||||
- Added `Decoder::image_ifd`, creating an `IfdDecoder` of the current IFD.
|
||||
- Added `IfdDecoder::directory`, returning the `Directory` underlying it.
|
||||
- Added `LowerHex` and `UpwerHex` implementations for `IfdPointer`.
|
||||
|
||||
Notes:
|
||||
- A new example `tiff-ls` demonstrates use of the new features.
|
||||
|
||||
# Version 0.11.2
|
||||
|
||||
Changes:
|
||||
- Planar files can now be read with `Decoder::read_image_to_buffer` if the
|
||||
limits allow for such a large buffer size. Previously only the first plane
|
||||
was read and the decoder will attempt a fallback to this behavior if the
|
||||
limits do not allow all planes to be allocated.
|
||||
|
||||
Fixes:
|
||||
- An off-by-one error in a `debug_assert` of `ValueBuffer` caused its use in
|
||||
`IfdDecoder::find_tag_buf` to panic outside release mode when the buffer
|
||||
exactly matched the required size.
|
||||
|
||||
Additions:
|
||||
- The methods `ColorType::{num_samples, bit_depth}` are now public. This
|
||||
promises that we only have homogeneous color depths in this major version.
|
||||
|
||||
# Version 0.11.1
|
||||
|
||||
Fixes:
|
||||
- The enumeration types in `tags` are now all marked with a representation of
|
||||
their underlying TIFF type (e.g. `repr(u16)`) and variants are explicitly
|
||||
assigned their corresponding values. That is you may _read_ the raw
|
||||
discriminant and interpret that as the value—except for `Unknown` variants.
|
||||
- The variants `RationalBig` and `SRationalBig` of `decoder::ifd::Value` are
|
||||
deprecated as they have no corresponding TIFF type and were not constructed
|
||||
during decoding.
|
||||
|
||||
Additions:
|
||||
- Types in `tags` now generally implement `TiffValue` and can be handed to the
|
||||
`DirectoryEncoder::write_tag` method. Unlike primitive types they do _not_
|
||||
always implement the trait for slices of themselves.
|
||||
- Added `tags::ValueBuffer`, a byte-based buffer for tags with a runtime value
|
||||
type and count.
|
||||
- Added `IfdDecoder::find_tag_{buf,bytes}` to read tag values into a
|
||||
`ValueBuffer` or byte slice respectively without interpreting them
|
||||
immediately within the Rust type system.
|
||||
- Added `DirectoryEncoder::write_{data,tag}_buf` that takes a `ValueBuffer`
|
||||
instead of a statically typed argument.
|
||||
- Added `DirectoryEncoder::write_entry{,_buf,_bytes}` that encode their
|
||||
argument into the file and return an `Entry` for later use, without
|
||||
immediately adding a tag-entry pair to the directory.
|
||||
- The `TiffValue` trait is now implemented for several builtin `tags::*` types
|
||||
or slices of them as appropriate for their use as values of their intended
|
||||
TIFF tag.
|
||||
- The `TiffValue` trait is now implemented for `[T; N]` for all types that
|
||||
implement it for slices.
|
||||
|
||||
# Version 0.11.0
|
||||
|
||||
- `Directory` now implements `FromIterator<(Tag, Value)>`.
|
||||
|
||||
Changes:
|
||||
- The decoder now interprets the `ExtraSamples` tag. The sample count must now
|
||||
more strict match the expected value with alpha channels only allowing for
|
||||
explicitly denoted unassociated or associated alpha. This effects the
|
||||
indicated color type when decoding images with additional samples indicated
|
||||
as unspecified relation. Previously, these may have been interpreted as
|
||||
alpha by the total sample count (e.g. RgbA if 4 samples under a photometric
|
||||
interpretation of RGB).
|
||||
- The decoder handles planar data, current limited to non-subsampled channels.
|
||||
The `Decoder::read_image` method return planes one-after-another depending on
|
||||
the size of the buffer that was passed.
|
||||
- `Decoder::read_image_to_buffer` now takes `&mut DecodingResult` and resizes
|
||||
it according to the required layout. Previously, a borrowed `DecodingBuffer`
|
||||
was passed which can be replaced by calling `as_bytes_mut` and
|
||||
`read_image_bytes`.
|
||||
- Several methods of tags are now `const`. Note that does not guarantee any
|
||||
particular value when calling these methods.
|
||||
|
||||
Fixes:
|
||||
- Fix a bug in the uncompressed encoder that could lead to short writes, i.e.
|
||||
data silently dropped when the underlying writer did not accept all data in a
|
||||
single write call.
|
||||
- Encoding YCbCr data now writes the `ChromaSubsampling` tag as `(1, 1)` to
|
||||
indicate no subsampling, instead of leaving it at its default of `(2, 2)`.
|
||||
- The decoder will reject subsampled YCbCr data as there is no upsampling
|
||||
routine, except for JPEG compressed images where the JPEG decoders handles
|
||||
this. Since the buffer in that case indicates a full-sized plane for all
|
||||
color samples any future support for the tag will upsample all planes (at
|
||||
least within this major version).
|
||||
|
||||
Additions:
|
||||
- Added support for the `CieLab` color type.
|
||||
- Added `DecodingResult::resize_to` to create a buffer with a matching sample
|
||||
type and dimensions.
|
||||
- Added `ByteOrder::native` to access the platform's native endianness.
|
||||
- Added `ByteOrder::convert` to change the byte-order of values in a byte
|
||||
buffer, depending on their `Type` as described at runtime.
|
||||
- Added `DirectoryOffset::new` to encode a directory whose offset is known to
|
||||
the caller but that has not been written through the encoder itself.
|
||||
- Added `Encoder::extra_samples` to encode images with more samples than their
|
||||
color's trait implementation would otherwise suggest.
|
||||
- Added `DirectoryEncoder::extend_from` to encode multiple tag entries from a
|
||||
directory whose values were written to the file by means outside the
|
||||
encoder's control.
|
||||
- Added `Decoder::read_coding_unit_bytes` to retrieve data of corresponding
|
||||
coordinates from potentially planar data, which is encoded in multiple chunks
|
||||
of the file.
|
||||
|
||||
# Version 0.10.3
|
||||
|
||||
New features:
|
||||
- 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:
|
||||
|
||||
+38
-7
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "tiff"
|
||||
version = "0.8.1"
|
||||
edition = "2018"
|
||||
version = "0.11.3"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
# note: when changed, also update test runner in `.github/workflows/rust.yml`
|
||||
rust-version = "1.61.0"
|
||||
rust-version = "1.88.0"
|
||||
|
||||
license = "MIT"
|
||||
description = "TIFF decoding and encoding library in pure Rust"
|
||||
@@ -14,16 +14,47 @@ authors = ["The image-rs Developers"]
|
||||
repository = "https://github.com/image-rs/image-tiff"
|
||||
categories = ["multimedia::images", "multimedia::encoding"]
|
||||
|
||||
exclude = ["tests/images/*", "tests/fuzz_images/*"]
|
||||
exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
|
||||
|
||||
[dependencies]
|
||||
weezl = "0.1.0"
|
||||
jpeg = { package = "jpeg-decoder", version = "0.3.0", default-features = false }
|
||||
flate2 = "1.0.20"
|
||||
half = { version = "2.4.1" }
|
||||
quick-error = "2.0.1"
|
||||
|
||||
# Rename to avoid confusion with the feature name.
|
||||
fax34 = { package = "fax", version = "0.2.6", optional = true }
|
||||
flate2 = { version = "1.0.20", optional = true }
|
||||
weezl = { version = "0.1.10", optional = true }
|
||||
zstd = { version = "0.13", optional = true }
|
||||
zune-jpeg = { version = "0.5.1", optional = true }
|
||||
image-webp = {version = "0.2.4", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.3.1"
|
||||
# We may already depend on this via flate2
|
||||
crc32fast = "1.5"
|
||||
libtest-mimic = "0.8.1"
|
||||
walkdir = "2.5"
|
||||
|
||||
[features]
|
||||
default = ["deflate", "fax", "jpeg", "lzw"]
|
||||
|
||||
# Compression algorithms
|
||||
deflate = ["dep:flate2"]
|
||||
fax = ["dep:fax34"]
|
||||
jpeg = ["dep:zune-jpeg"]
|
||||
lzw = ["dep:weezl"]
|
||||
webp = ["dep:image-webp"]
|
||||
zstd = ["dep:zstd"]
|
||||
|
||||
[[bench]]
|
||||
name = "lzw"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "encode"
|
||||
harness = false
|
||||
|
||||
[[test]]
|
||||
name = "decode_libtiffpic"
|
||||
path = "tests/decode_libtiffpic.rs"
|
||||
harness = false
|
||||
@@ -1,5 +1,5 @@
|
||||
# image-tiff
|
||||
[](https://github.com/image-rs/image-tiff/actions)
|
||||
[](https://github.com/image-rs/image-tiff/actions)
|
||||
[](https://docs.rs/tiff)
|
||||
[](https://crates.io/crates/tiff)
|
||||
|
||||
@@ -24,6 +24,7 @@ This table lists photometric interpretations and sample formats which are suppor
|
||||
| `RGBPalette` | | |
|
||||
| `Mask` | | |
|
||||
| `CMYK` | CMYK(8\|16\|32\|64) | CMYK(32\|64) |
|
||||
| `CMYKA` | CMYKA(8) | |
|
||||
| `YCbCr` | | |
|
||||
| `CIELab` | | |
|
||||
|
||||
@@ -35,6 +36,9 @@ This table lists photometric interpretations and sample formats which are suppor
|
||||
| LZW | ✓ | ✓ |
|
||||
| Deflate | ✓ | ✓ |
|
||||
| PackBits | ✓ | ✓ |
|
||||
| Fax4 | ✓ | not yet |
|
||||
| JPEG | ✓ | not yet |
|
||||
| ZSTD | ✓ | not yet |
|
||||
|
||||
|
||||
## Not yet supported
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
extern crate criterion;
|
||||
extern crate tiff;
|
||||
|
||||
use criterion::{black_box, Criterion, Throughput};
|
||||
use std::io::Cursor;
|
||||
use tiff::encoder::colortype::RGB32Float;
|
||||
use tiff::encoder::{Compression, Predictor, TiffEncoder};
|
||||
|
||||
const WIDTH: u32 = 500;
|
||||
const HEIGHT: u32 = 500;
|
||||
|
||||
fn encode_image_f32_float_predictor_deflate(data: &[f32]) {
|
||||
let mut buf = Cursor::new(Vec::with_capacity(WIDTH as usize * HEIGHT as usize * 4));
|
||||
TiffEncoder::new(&mut buf)
|
||||
.unwrap()
|
||||
.with_predictor(Predictor::FloatingPoint)
|
||||
.with_compression(Compression::Deflate(1))
|
||||
.write_image::<RGB32Float>(WIDTH, HEIGHT, black_box(data))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Generate synthetic f32 image data with a gradient pattern
|
||||
let data: Vec<f32> = (0..WIDTH as usize * HEIGHT as usize * 3)
|
||||
.map(|i| {
|
||||
let pixel = i / 3;
|
||||
let x = (pixel % WIDTH as usize) as f32 / WIDTH as f32;
|
||||
let y = (pixel / WIDTH as usize) as f32 / HEIGHT as f32;
|
||||
x * 0.5 + y * 0.5
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut c = Criterion::default().configure_from_args();
|
||||
let mut group = c.benchmark_group("tiff-encode");
|
||||
|
||||
group
|
||||
.sample_size(20)
|
||||
.throughput(Throughput::Bytes(
|
||||
(WIDTH as u64) * (HEIGHT as u64) * 3 * std::mem::size_of::<f32>() as u64,
|
||||
))
|
||||
.bench_function("f32-float-predictor-deflate-fast", |b| {
|
||||
b.iter(|| encode_image_f32_float_predictor_deflate(&data))
|
||||
});
|
||||
}
|
||||
+4
-5
@@ -8,13 +8,12 @@ use tiff::decoder::Decoder;
|
||||
|
||||
fn read_image(image: &[u8]) {
|
||||
let image = std::io::Cursor::new(image);
|
||||
let decoder = Decoder::new(black_box(image));
|
||||
let mut reader = decoder.unwrap();
|
||||
let decoder = Decoder::open(black_box(image));
|
||||
|
||||
while {
|
||||
let mut reader = decoder.unwrap();
|
||||
while reader.more_images() {
|
||||
reader.read_image().unwrap();
|
||||
reader.more_images()
|
||||
} {}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -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()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,13 @@
|
||||
//! the unsafe code guidelines).
|
||||
//!
|
||||
//! 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 half::f16;
|
||||
|
||||
macro_rules! integral_slice_as_bytes{($int:ty, $const:ident $(,$mut:ident)*) => {
|
||||
pub(crate) fn $const(slice: &[$int]) -> &[u8] {
|
||||
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!(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!(f16, f16_as_ne_bytes, f16_as_ne_mut_bytes);
|
||||
integral_slice_as_bytes!(f64, f64_as_ne_bytes, f64_as_ne_mut_bytes);
|
||||
@@ -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");
|
||||
}
|
||||
+477
-194
File diff suppressed because it is too large.
Load diff
+898
-185
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
}
|
||||
+1541
-571
File diff suppressed because it is too large.
Load diff
+213
-210
@@ -1,51 +1,51 @@
|
||||
//! 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::convert::TryFrom;
|
||||
use std::io::{self, BufRead, BufReader, Read, Seek, SeekFrom, Take};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Byte order of the TIFF file.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ByteOrder {
|
||||
/// little endian byte order
|
||||
LittleEndian,
|
||||
/// big endian byte order
|
||||
BigEndian,
|
||||
}
|
||||
pub use crate::tags::ByteOrder;
|
||||
|
||||
/// Reader that is aware of the byte order.
|
||||
pub trait EndianReader: Read {
|
||||
/// Byte order that should be adhered to
|
||||
fn byte_order(&self) -> ByteOrder;
|
||||
#[derive(Debug)]
|
||||
pub struct EndianReader<R> {
|
||||
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
|
||||
#[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];
|
||||
self.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order() {
|
||||
self.reader.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order {
|
||||
ByteOrder::LittleEndian => u16::from_le_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
|
||||
#[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];
|
||||
self.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order() {
|
||||
self.reader.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order {
|
||||
ByteOrder::LittleEndian => i16::from_le_bytes(n),
|
||||
ByteOrder::BigEndian => i16::from_be_bytes(n),
|
||||
})
|
||||
@@ -53,10 +53,10 @@ pub trait EndianReader: Read {
|
||||
|
||||
/// Reads an u32
|
||||
#[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];
|
||||
self.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order() {
|
||||
self.reader.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order {
|
||||
ByteOrder::LittleEndian => u32::from_le_bytes(n),
|
||||
ByteOrder::BigEndian => u32::from_be_bytes(n),
|
||||
})
|
||||
@@ -64,10 +64,10 @@ pub trait EndianReader: Read {
|
||||
|
||||
/// Reads an i32
|
||||
#[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];
|
||||
self.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order() {
|
||||
self.reader.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order {
|
||||
ByteOrder::LittleEndian => i32::from_le_bytes(n),
|
||||
ByteOrder::BigEndian => i32::from_be_bytes(n),
|
||||
})
|
||||
@@ -75,10 +75,10 @@ pub trait EndianReader: Read {
|
||||
|
||||
/// Reads an u64
|
||||
#[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];
|
||||
self.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order() {
|
||||
self.reader.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order {
|
||||
ByteOrder::LittleEndian => u64::from_le_bytes(n),
|
||||
ByteOrder::BigEndian => u64::from_be_bytes(n),
|
||||
})
|
||||
@@ -86,10 +86,10 @@ pub trait EndianReader: Read {
|
||||
|
||||
/// Reads an i64
|
||||
#[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];
|
||||
self.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order() {
|
||||
self.reader.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order {
|
||||
ByteOrder::LittleEndian => i64::from_le_bytes(n),
|
||||
ByteOrder::BigEndian => i64::from_be_bytes(n),
|
||||
})
|
||||
@@ -97,10 +97,10 @@ pub trait EndianReader: Read {
|
||||
|
||||
/// Reads an f32
|
||||
#[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];
|
||||
self.read_exact(&mut n)?;
|
||||
Ok(f32::from_bits(match self.byte_order() {
|
||||
self.reader.read_exact(&mut n)?;
|
||||
Ok(f32::from_bits(match self.byte_order {
|
||||
ByteOrder::LittleEndian => u32::from_le_bytes(n),
|
||||
ByteOrder::BigEndian => u32::from_be_bytes(n),
|
||||
}))
|
||||
@@ -108,49 +108,53 @@ pub trait EndianReader: Read {
|
||||
|
||||
/// Reads an f64
|
||||
#[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];
|
||||
self.read_exact(&mut n)?;
|
||||
Ok(f64::from_bits(match self.byte_order() {
|
||||
self.reader.read_exact(&mut n)?;
|
||||
Ok(f64::from_bits(match self.byte_order {
|
||||
ByteOrder::LittleEndian => u64::from_le_bytes(n),
|
||||
ByteOrder::BigEndian => u64::from_be_bytes(n),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// # READERS
|
||||
///
|
||||
|
||||
///
|
||||
/// ## Deflate Reader
|
||||
///
|
||||
//
|
||||
// # READERS
|
||||
//
|
||||
|
||||
/// Type alias for the deflate Reader
|
||||
#[cfg(feature = "deflate")]
|
||||
pub type DeflateReader<R> = flate2::read::ZlibDecoder<R>;
|
||||
|
||||
///
|
||||
/// ## LZW Reader
|
||||
///
|
||||
//
|
||||
// ## LZW Reader
|
||||
//
|
||||
|
||||
/// Reader that decompresses LZW streams
|
||||
#[cfg(feature = "lzw")]
|
||||
pub struct LZWReader<R: Read> {
|
||||
reader: BufReader<Take<R>>,
|
||||
decoder: weezl::decode::Decoder,
|
||||
}
|
||||
|
||||
#[cfg(feature = "lzw")]
|
||||
impl<R: Read> LZWReader<R> {
|
||||
/// Wraps a reader
|
||||
pub fn new(reader: R, compressed_length: usize) -> LZWReader<R> {
|
||||
let configuration =
|
||||
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
|
||||
.with_yield_on_full_buffer(true);
|
||||
Self {
|
||||
reader: BufReader::with_capacity(
|
||||
(32 * 1024).min(compressed_length),
|
||||
reader.take(u64::try_from(compressed_length).unwrap()),
|
||||
),
|
||||
decoder: weezl::decode::Decoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8),
|
||||
decoder: configuration.build(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "lzw")]
|
||||
impl<R: Read> Read for LZWReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
loop {
|
||||
@@ -166,9 +170,6 @@ impl<R: Read> Read for LZWReader<R> {
|
||||
}
|
||||
}
|
||||
Ok(weezl::LzwStatus::NoProgress) => {
|
||||
assert_eq!(result.consumed_in, 0);
|
||||
assert_eq!(result.consumed_out, 0);
|
||||
assert!(self.reader.buffer().is_empty());
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"no lzw end code found",
|
||||
@@ -183,108 +184,11 @@ impl<R: Read> Read for LZWReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// ## JPEG Reader (for "new-style" JPEG format (TIFF compression tag 7))
|
||||
///
|
||||
|
||||
pub(crate) struct JpegReader {
|
||||
jpeg_tables: Option<Arc<Vec<u8>>>,
|
||||
|
||||
buffer: io::Cursor<Vec<u8>>,
|
||||
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
impl JpegReader {
|
||||
/// Constructs new JpegReader wrapping a SmartReader.
|
||||
/// Because JPEG compression in TIFF allows to save quantization and/or huffman tables in one
|
||||
/// central location, the constructor accepts this data as `jpeg_tables` here containing either
|
||||
/// or both.
|
||||
/// These `jpeg_tables` are simply prepended to the remaining jpeg image data.
|
||||
/// Because these `jpeg_tables` start with a `SOI` (HEX: `0xFFD8`) or __start of image__ marker
|
||||
/// which is also at the beginning of the remaining JPEG image data and would
|
||||
/// confuse the JPEG renderer, one of these has to be taken off. In this case the first two
|
||||
/// bytes of the remaining JPEG data is removed because it follows `jpeg_tables`.
|
||||
/// Similary, `jpeg_tables` ends with a `EOI` (HEX: `0xFFD9`) or __end of image__ marker,
|
||||
/// this has to be removed as well (last two bytes of `jpeg_tables`).
|
||||
pub fn new<R: Read>(
|
||||
mut reader: R,
|
||||
length: u64,
|
||||
jpeg_tables: Option<Arc<Vec<u8>>>,
|
||||
) -> io::Result<JpegReader> {
|
||||
// Read jpeg image data
|
||||
let mut segment = vec![0; length as usize];
|
||||
|
||||
reader.read_exact(&mut segment[..])?;
|
||||
|
||||
match jpeg_tables {
|
||||
Some(jpeg_tables) => {
|
||||
assert!(
|
||||
jpeg_tables.len() >= 2,
|
||||
"jpeg_tables, if given, must be at least 2 bytes long. Got {:?}",
|
||||
jpeg_tables
|
||||
);
|
||||
|
||||
assert!(
|
||||
length >= 2,
|
||||
"if jpeg_tables is given, length must be at least 2 bytes long, got {}",
|
||||
length
|
||||
);
|
||||
|
||||
let mut buffer = io::Cursor::new(segment);
|
||||
// Skip the first two bytes (marker bytes)
|
||||
buffer.seek(SeekFrom::Start(2))?;
|
||||
|
||||
Ok(JpegReader {
|
||||
buffer,
|
||||
jpeg_tables: Some(jpeg_tables),
|
||||
offset: 0,
|
||||
})
|
||||
}
|
||||
None => Ok(JpegReader {
|
||||
buffer: io::Cursor::new(segment),
|
||||
jpeg_tables: None,
|
||||
offset: 0,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for JpegReader {
|
||||
// #[inline]
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let mut start = 0;
|
||||
|
||||
if let Some(jpeg_tables) = &self.jpeg_tables {
|
||||
if jpeg_tables.len() - 2 > self.offset {
|
||||
// Read (rest of) jpeg_tables to buf (without the last two bytes)
|
||||
let size_remaining = jpeg_tables.len() - self.offset - 2;
|
||||
let to_copy = size_remaining.min(buf.len());
|
||||
|
||||
buf[start..start + to_copy]
|
||||
.copy_from_slice(&jpeg_tables[self.offset..self.offset + to_copy]);
|
||||
|
||||
self.offset += to_copy;
|
||||
|
||||
if to_copy == buf.len() {
|
||||
return Ok(to_copy);
|
||||
}
|
||||
|
||||
start += to_copy;
|
||||
}
|
||||
}
|
||||
|
||||
let read = self.buffer.read(&mut buf[start..])?;
|
||||
self.offset += read;
|
||||
|
||||
Ok(read + start)
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// ## PackBits Reader
|
||||
///
|
||||
//
|
||||
// ## PackBits Reader
|
||||
//
|
||||
|
||||
/// Internal state machine for the PackBitsReader.
|
||||
enum PackBitsReaderState {
|
||||
Header,
|
||||
Literal,
|
||||
@@ -318,7 +222,7 @@ impl<R: Read> Read for PackBitsReader<R> {
|
||||
let mut header: [u8; 1] = [0];
|
||||
self.reader.read_exact(&mut header)?;
|
||||
let h = header[0] as i8;
|
||||
if h >= -127 && h <= -1 {
|
||||
if (-127..=-1).contains(&h) {
|
||||
let mut data: [u8; 1] = [0];
|
||||
self.reader.read_exact(&mut data)?;
|
||||
self.state = PackBitsReaderState::Repeat { value: data[0] };
|
||||
@@ -348,63 +252,162 @@ impl<R: Read> Read for PackBitsReader<R> {
|
||||
if self.count == 0 {
|
||||
self.state = PackBitsReaderState::Header;
|
||||
}
|
||||
return Ok(actual);
|
||||
Ok(actual)
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// ## SmartReader Reader
|
||||
///
|
||||
#[cfg(feature = "fax")]
|
||||
pub struct Group4Reader<R: Read> {
|
||||
decoder: fax34::decoder::Group4Decoder<io::Bytes<io::BufReader<io::Take<R>>>>,
|
||||
line_buf: io::Cursor<Vec<u8>>,
|
||||
height: u32,
|
||||
width: u16,
|
||||
y: u32,
|
||||
}
|
||||
|
||||
/// Reader that is aware of the byte order.
|
||||
#[derive(Debug)]
|
||||
pub struct SmartReader<R>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
#[cfg(feature = "fax")]
|
||||
impl<R: Read> Group4Reader<R> {
|
||||
pub fn new(
|
||||
dimensions: (u32, u32),
|
||||
reader: R,
|
||||
pub byte_order: ByteOrder,
|
||||
}
|
||||
compressed_length: u64,
|
||||
) -> crate::TiffResult<Self> {
|
||||
let width = u16::try_from(dimensions.0)?;
|
||||
let height = dimensions.1;
|
||||
|
||||
impl<R> SmartReader<R>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
/// Wraps a reader
|
||||
pub fn wrap(reader: R, byte_order: ByteOrder) -> SmartReader<R> {
|
||||
SmartReader { reader, byte_order }
|
||||
}
|
||||
pub fn into_inner(self) -> R {
|
||||
self.reader
|
||||
}
|
||||
}
|
||||
impl<R: Read + Seek> SmartReader<R> {
|
||||
pub fn goto_offset(&mut self, offset: u64) -> io::Result<()> {
|
||||
self.seek(io::SeekFrom::Start(offset)).map(|_| ())
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> EndianReader for SmartReader<R>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn byte_order(&self) -> ByteOrder {
|
||||
self.byte_order
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for SmartReader<R> {
|
||||
#[inline]
|
||||
#[cfg(feature = "fax")]
|
||||
impl<R: Read> Read for Group4Reader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.reader.read(buf)
|
||||
// Either we have not read any line or we are at the end of a line.
|
||||
if self.line_buf.position() as usize == self.line_buf.get_ref().len()
|
||||
&& self.y < self.height
|
||||
{
|
||||
let next = self.decoder.advance().map_err(std::io::Error::other)?;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Seek for SmartReader<R> {
|
||||
#[inline]
|
||||
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
|
||||
self.reader.seek(pos)
|
||||
self.line_buf.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webp")]
|
||||
pub struct WebPReader {
|
||||
inner: Cursor<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "webp")]
|
||||
impl WebPReader {
|
||||
pub fn new<R: Read + Seek>(
|
||||
reader: R,
|
||||
compressed_length: u64,
|
||||
samples: u16,
|
||||
) -> crate::TiffResult<Self> {
|
||||
let mut decoder =
|
||||
image_webp::WebPDecoder::new(io::BufReader::new(reader.take(compressed_length)))
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
if !(samples == 4 || (samples == 3 && !decoder.has_alpha())) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"bad sample count for WebP compressed data",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let total_bytes =
|
||||
samples as usize * decoder.dimensions().0 as usize * decoder.dimensions().1 as usize;
|
||||
let mut data = vec![0; total_bytes];
|
||||
|
||||
decoder
|
||||
.read_image(&mut data)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
// Add a fully opaque alpha channel if needed
|
||||
if samples == 4 && !decoder.has_alpha() {
|
||||
for i in (0..(total_bytes / 4)).rev() {
|
||||
data[i * 4 + 3] = 255;
|
||||
data[i * 4 + 2] = data[i * 3 + 2];
|
||||
data[i * 4 + 1] = data[i * 3 + 1];
|
||||
data[i * 4] = data[i * 3];
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
inner: Cursor::new(data),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webp")]
|
||||
impl Read for WebPReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.inner.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+76
-16
@@ -1,26 +1,85 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use crate::tags::Tag;
|
||||
use crate::{TiffError, TiffFormatError, TiffResult};
|
||||
use crate::{
|
||||
tags::{Tag, ValueBuffer},
|
||||
Directory, TiffError, TiffFormatError, TiffResult,
|
||||
};
|
||||
|
||||
use super::ifd::{Directory, Value};
|
||||
use super::stream::SmartReader;
|
||||
use super::Limits;
|
||||
use super::ifd::{Entry, Value};
|
||||
use super::ValueReader;
|
||||
|
||||
pub(crate) struct TagReader<'a, R: Read + Seek> {
|
||||
pub reader: &'a mut SmartReader<R>,
|
||||
pub ifd: &'a Directory,
|
||||
pub limits: &'a Limits,
|
||||
pub bigtiff: bool,
|
||||
pub(crate) struct TagReader<'lt, R: EntryDecoder + ?Sized> {
|
||||
pub(crate) decoder: &'lt mut R,
|
||||
pub(crate) ifd: &'lt Directory,
|
||||
}
|
||||
impl<'a, R: Read + Seek> TagReader<'a, R> {
|
||||
|
||||
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>> {
|
||||
Ok(match self.ifd.get(&tag) {
|
||||
Some(entry) => Some(entry.clone().val(self.limits, self.bigtiff, self.reader)?),
|
||||
None => None,
|
||||
})
|
||||
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),
|
||||
@@ -29,6 +88,7 @@ impl<'a, R: Read + Seek> TagReader<'a, R> {
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_tag_uint_vec<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<Option<Vec<T>>> {
|
||||
self.find_tag(tag)?
|
||||
.map(|v| v.into_u64_vec())
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,127 @@
|
||||
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.
|
||||
pub trait ColorType {
|
||||
/// The type of each sample of this colortype
|
||||
@@ -10,6 +132,12 @@ pub trait ColorType {
|
||||
const BITS_PER_SAMPLE: &'static [u16];
|
||||
/// The value of the tiff tag `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;
|
||||
@@ -18,6 +146,8 @@ impl ColorType for Gray8 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[8];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct GrayI8;
|
||||
@@ -26,6 +156,8 @@ impl ColorType for GrayI8 {
|
||||
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;
|
||||
@@ -34,6 +166,8 @@ impl ColorType for Gray16 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[16];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct GrayI16;
|
||||
@@ -42,6 +176,8 @@ impl ColorType for GrayI16 {
|
||||
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;
|
||||
@@ -50,6 +186,8 @@ impl ColorType for Gray32 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[32];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct GrayI32;
|
||||
@@ -58,6 +196,8 @@ impl ColorType for GrayI32 {
|
||||
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;
|
||||
@@ -66,6 +206,14 @@ impl ColorType for Gray32Float {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[32];
|
||||
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;
|
||||
@@ -74,6 +222,8 @@ impl ColorType for Gray64 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[64];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct GrayI64;
|
||||
@@ -82,6 +232,8 @@ impl ColorType for GrayI64 {
|
||||
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;
|
||||
@@ -90,6 +242,14 @@ impl ColorType for Gray64Float {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[64];
|
||||
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;
|
||||
@@ -98,6 +258,8 @@ impl ColorType for RGB8 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct RGB16;
|
||||
@@ -106,6 +268,8 @@ impl ColorType for RGB16 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct RGB32;
|
||||
@@ -114,6 +278,8 @@ impl ColorType for RGB32 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct RGB32Float;
|
||||
@@ -122,6 +288,14 @@ impl ColorType for RGB32Float {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!("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;
|
||||
@@ -130,6 +304,8 @@ impl ColorType for RGB64 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct RGB64Float;
|
||||
@@ -138,6 +314,14 @@ impl ColorType for RGB64Float {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!("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;
|
||||
@@ -146,6 +330,8 @@ impl ColorType for RGBA8 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct RGBA16;
|
||||
@@ -154,6 +340,8 @@ impl ColorType for RGBA16 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct RGBA32;
|
||||
@@ -162,6 +350,8 @@ impl ColorType for RGBA32 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct RGBA32Float;
|
||||
@@ -170,6 +360,14 @@ impl ColorType for RGBA32Float {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!("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;
|
||||
@@ -178,6 +376,8 @@ impl ColorType for RGBA64 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct RGBA64Float;
|
||||
@@ -186,6 +386,14 @@ impl ColorType for RGBA64Float {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!("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;
|
||||
@@ -194,6 +402,8 @@ impl ColorType for CMYK8 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct CMYK16;
|
||||
@@ -202,6 +412,8 @@ impl ColorType for CMYK16 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct CMYK32;
|
||||
@@ -210,6 +422,8 @@ impl ColorType for CMYK32 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct CMYK32Float;
|
||||
@@ -218,6 +432,14 @@ impl ColorType for CMYK32Float {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!("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;
|
||||
@@ -226,6 +448,8 @@ impl ColorType for CMYK64 {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
|
||||
|
||||
integer_horizontal_predict!();
|
||||
}
|
||||
|
||||
pub struct CMYK64Float;
|
||||
@@ -234,6 +458,14 @@ impl ColorType for CMYK64Float {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!("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;
|
||||
@@ -242,4 +474,16 @@ impl ColorType for YCbCr8 {
|
||||
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!();
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::{encoder::compression::*, tags::CompressionMethod};
|
||||
use crate::encoder::compression::*;
|
||||
use flate2::{write::ZlibEncoder, Compression as FlateCompression};
|
||||
use std::io::Write;
|
||||
|
||||
/// The Deflate algorithm used to compress image data in TIFF files.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -8,28 +7,27 @@ pub struct Deflate {
|
||||
level: FlateCompression,
|
||||
}
|
||||
|
||||
/// The level of compression used by the Deflate algorithm.
|
||||
/// It allows trading compression ratio for compression speed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[non_exhaustive]
|
||||
pub enum DeflateLevel {
|
||||
/// The fastest possible compression mode.
|
||||
Fast = 1,
|
||||
/// The conserative choice between speed and ratio.
|
||||
Balanced = 6,
|
||||
/// The best compression available with Deflate.
|
||||
Best = 9,
|
||||
}
|
||||
|
||||
impl Default for DeflateLevel {
|
||||
fn default() -> Self {
|
||||
DeflateLevel::Balanced
|
||||
}
|
||||
}
|
||||
|
||||
impl Deflate {
|
||||
/// Create a new deflate compressor with a specific level of compression.
|
||||
pub fn with_level(level: DeflateLevel) -> Self {
|
||||
///
|
||||
/// The valid levels are 1 through 9 inclusive. The default level is 6.
|
||||
///
|
||||
/// - `1` stands for light but fast compression
|
||||
/// - `6` is the recommended balanced default
|
||||
/// - `9` is maximum compression ratio at the cost of slow encoding
|
||||
pub fn with_level(level: u8) -> Self {
|
||||
Self {
|
||||
level: FlateCompression::new(level as u32),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new deflate compressor with an exact level of compression.
|
||||
///
|
||||
/// `0` stands for uncompressed
|
||||
/// `1` stands for light but fast compression
|
||||
/// `6` is the recommended balanced default
|
||||
/// `9` is maximum compression ratio at the cost of slow encoding
|
||||
pub fn exact_level(level: u8) -> Self {
|
||||
Self {
|
||||
level: FlateCompression::new(level as u32),
|
||||
}
|
||||
@@ -38,7 +36,7 @@ impl Deflate {
|
||||
|
||||
impl Default for Deflate {
|
||||
fn default() -> Self {
|
||||
Self::with_level(DeflateLevel::default())
|
||||
Self::with_level(6)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +44,7 @@ impl Compression for Deflate {
|
||||
const COMPRESSION_METHOD: CompressionMethod = CompressionMethod::Deflate;
|
||||
|
||||
fn get_algorithm(&self) -> Compressor {
|
||||
Compressor::Deflate(self.clone())
|
||||
Compressor::Deflate(*self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::{encoder::compression::*, tags::CompressionMethod};
|
||||
use std::io::Write;
|
||||
use crate::encoder::compression::*;
|
||||
use weezl::encode::Encoder as LZWEncoder;
|
||||
|
||||
/// The LZW algorithm used to compress image data in TIFF files.
|
||||
@@ -41,7 +40,7 @@ mod tests {
|
||||
|
||||
let mut compressed_data = Vec::<u8>::new();
|
||||
let mut writer = Cursor::new(&mut compressed_data);
|
||||
Lzw::default().write_to(&mut writer, TEST_DATA).unwrap();
|
||||
Lzw.write_to(&mut writer, TEST_DATA).unwrap();
|
||||
assert_eq!(EXPECTED_COMPRESSED_DATA, compressed_data.as_slice());
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
use crate::tags::CompressionMethod;
|
||||
use std::io::{self, Write};
|
||||
|
||||
#[cfg(feature = "deflate")]
|
||||
mod deflate;
|
||||
#[cfg(feature = "lzw")]
|
||||
mod lzw;
|
||||
mod packbits;
|
||||
mod uncompressed;
|
||||
|
||||
pub use self::deflate::{Deflate, DeflateLevel};
|
||||
#[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;
|
||||
|
||||
@@ -28,9 +34,12 @@ pub trait Compression: CompressionAlgorithm {
|
||||
}
|
||||
|
||||
/// 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),
|
||||
}
|
||||
@@ -38,7 +47,7 @@ pub enum Compressor {
|
||||
impl Default for Compressor {
|
||||
/// The default compression strategy does not apply any compression.
|
||||
fn default() -> Self {
|
||||
Compressor::Uncompressed(Uncompressed::default())
|
||||
Compressor::Uncompressed(Uncompressed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +55,9 @@ impl CompressionAlgorithm for Compressor {
|
||||
fn write_to<W: Write>(&mut self, writer: &mut W, bytes: &[u8]) -> Result<u64, io::Error> {
|
||||
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),
|
||||
}
|
||||
@@ -55,6 +66,5 @@ impl CompressionAlgorithm for Compressor {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub const TEST_DATA: &'static [u8] =
|
||||
b"This is a string for checking various compression algorithms.";
|
||||
pub const TEST_DATA: &[u8] = b"This is a string for checking various compression algorithms.";
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{encoder::compression::*, tags::CompressionMethod};
|
||||
use std::io::{BufWriter, Error, ErrorKind, Write};
|
||||
use crate::encoder::compression::*;
|
||||
use std::io::{BufWriter, Error, ErrorKind};
|
||||
|
||||
/// Compressor that uses the Packbits[^note] algorithm to compress bytes.
|
||||
///
|
||||
@@ -80,12 +80,11 @@ impl CompressionAlgorithm for Packbits {
|
||||
run_index = 0;
|
||||
in_run = false;
|
||||
}
|
||||
} else {
|
||||
if bytes_pending > MAX_BYTES {
|
||||
} 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(&bytes[pending_index..pending_index + MAX_BYTES as usize])?;
|
||||
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;
|
||||
@@ -98,7 +97,7 @@ impl CompressionAlgorithm for Packbits {
|
||||
// Flush differing data out of input buffer
|
||||
offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(run_index))?);
|
||||
bufwriter
|
||||
.write(&bytes[pending_index..pending_index + run_index as usize])?;
|
||||
.write_all(&bytes[pending_index..pending_index + run_index as usize])?;
|
||||
bytes_written += 1 + run_index as u64;
|
||||
}
|
||||
bytes_pending -= run_index; // Length of run
|
||||
@@ -107,7 +106,6 @@ impl CompressionAlgorithm for Packbits {
|
||||
} else {
|
||||
run_index = bytes_pending - 1; // A run could start here
|
||||
}
|
||||
}
|
||||
last_byte = curr_byte;
|
||||
}
|
||||
|
||||
@@ -119,7 +117,7 @@ impl CompressionAlgorithm for Packbits {
|
||||
} else {
|
||||
bytes_written += 1 + bytes_pending as u64;
|
||||
offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(bytes_pending))?);
|
||||
bufwriter.write(&bytes[pending_index..pending_index + bytes_pending as usize])?;
|
||||
bufwriter.write_all(&bytes[pending_index..pending_index + bytes_pending as usize])?;
|
||||
}
|
||||
|
||||
bufwriter.flush()?;
|
||||
@@ -141,24 +139,20 @@ mod tests {
|
||||
|
||||
let mut compressed_data = Vec::<u8>::new();
|
||||
let mut writer = Cursor::new(&mut compressed_data);
|
||||
Packbits::default()
|
||||
.write_to(&mut writer, &UNCOMPRESSED_DATA)
|
||||
.unwrap();
|
||||
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: &'static [u8] =
|
||||
const UNCOMPRESSED_DATA: &[u8] =
|
||||
b"This strrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrring hangs.";
|
||||
const EXPECTED_COMPRESSED_DATA: &'static [u8] = b"\x06This st\xD1r\x09ing hangs.";
|
||||
const EXPECTED_COMPRESSED_DATA: &[u8] = b"\x06This st\xD1r\x09ing hangs.";
|
||||
|
||||
let mut compressed_data = Vec::<u8>::new();
|
||||
let mut writer = Cursor::new(&mut compressed_data);
|
||||
Packbits::default()
|
||||
.write_to(&mut writer, UNCOMPRESSED_DATA)
|
||||
.unwrap();
|
||||
Packbits.write_to(&mut writer, UNCOMPRESSED_DATA).unwrap();
|
||||
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
|
||||
}
|
||||
|
||||
@@ -192,23 +186,19 @@ mod tests {
|
||||
|
||||
let mut compressed_data = Vec::<u8>::new();
|
||||
let mut writer = Cursor::new(&mut compressed_data);
|
||||
Packbits::default()
|
||||
.write_to(&mut writer, data.as_slice())
|
||||
.unwrap();
|
||||
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: &'static [u8] =
|
||||
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::default()
|
||||
.write_to(&mut writer, TEST_DATA)
|
||||
.unwrap();
|
||||
Packbits.write_to(&mut writer, TEST_DATA).unwrap();
|
||||
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::{encoder::compression::*, tags::CompressionMethod};
|
||||
use std::io::Write;
|
||||
use crate::encoder::compression::*;
|
||||
|
||||
/// The default algorithm which does not compress at all.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
@@ -15,7 +14,7 @@ impl Compression for Uncompressed {
|
||||
|
||||
impl CompressionAlgorithm for Uncompressed {
|
||||
fn write_to<W: Write>(&mut self, writer: &mut W, bytes: &[u8]) -> Result<u64, io::Error> {
|
||||
writer.write(bytes).map(|byte_count| byte_count as u64)
|
||||
writer.write_all(bytes).map(|()| bytes.len() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +28,7 @@ mod tests {
|
||||
fn test_no_compression() {
|
||||
let mut compressed_data = Vec::<u8>::new();
|
||||
let mut writer = Cursor::new(&mut compressed_data);
|
||||
Uncompressed::default()
|
||||
.write_to(&mut writer, TEST_DATA)
|
||||
.unwrap();
|
||||
Uncompressed.write_to(&mut writer, TEST_DATA).unwrap();
|
||||
assert_eq!(TEST_DATA, compressed_data);
|
||||
}
|
||||
}
|
||||
+627
-149
@@ -2,18 +2,20 @@ pub use tiff_value::*;
|
||||
|
||||
use std::{
|
||||
cmp,
|
||||
collections::BTreeMap,
|
||||
convert::{TryFrom, TryInto},
|
||||
io::{self, Seek, Write},
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
num::TryFromIntError,
|
||||
num::{NonZeroU64, TryFromIntError},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::TiffResult,
|
||||
tags::{CompressionMethod, ResolutionUnit, Tag},
|
||||
TiffError, TiffFormatError,
|
||||
decoder::ifd::Entry,
|
||||
error::{TiffResult, UsageError},
|
||||
tags::{
|
||||
ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation,
|
||||
ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
|
||||
},
|
||||
Directory, TiffError, TiffFormatError,
|
||||
};
|
||||
|
||||
pub mod colortype;
|
||||
@@ -22,15 +24,63 @@ mod tiff_value;
|
||||
mod writer;
|
||||
|
||||
use self::colortype::*;
|
||||
use self::compression::Compression as Comp;
|
||||
use self::compression::*;
|
||||
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.
|
||||
///
|
||||
/// With this type you can get a `DirectoryEncoder` or a `ImageEncoder`
|
||||
/// With this type you can get a [`DirectoryEncoder`] or a [`ImageEncoder`]
|
||||
/// to encode Tiff/BigTiff ifd directories with images.
|
||||
///
|
||||
/// See `DirectoryEncoder` and `ImageEncoder`.
|
||||
/// See [`DirectoryEncoder`] and [`ImageEncoder`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
@@ -53,6 +103,10 @@ use self::writer::*;
|
||||
pub struct TiffEncoder<W, K: TiffKind = TiffKindStandard> {
|
||||
writer: TiffWriter<W>,
|
||||
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.
|
||||
@@ -81,19 +135,60 @@ impl<W: Write + Seek> TiffEncoder<W, TiffKindBig> {
|
||||
impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
|
||||
/// Creates a new Tiff or BigTiff encoder, inferred from the return type.
|
||||
pub fn new_generic(writer: W) -> TiffResult<Self> {
|
||||
let mut encoder = TiffEncoder {
|
||||
writer: TiffWriter::new(writer),
|
||||
let mut 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,
|
||||
};
|
||||
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.
|
||||
pub fn new_directory(&mut self) -> TiffResult<DirectoryEncoder<W, K>> {
|
||||
DirectoryEncoder::new(&mut self.writer)
|
||||
#[deprecated = "`image_directory` replaced the old behavior and clarifies the intent"]
|
||||
#[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.
|
||||
@@ -101,20 +196,9 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> TiffResult<ImageEncoder<W, C, K, Uncompressed>> {
|
||||
let encoder = DirectoryEncoder::new(&mut self.writer)?;
|
||||
ImageEncoder::new(encoder, width, height)
|
||||
}
|
||||
|
||||
/// Create an [`ImageEncoder`] to encode an image one slice at a time.
|
||||
pub fn new_image_with_compression<C: ColorType, D: Compression>(
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
compression: D,
|
||||
) -> TiffResult<ImageEncoder<W, C, K, D>> {
|
||||
let encoder = DirectoryEncoder::new(&mut self.writer)?;
|
||||
ImageEncoder::with_compression(encoder, width, height, compression)
|
||||
) -> TiffResult<ImageEncoder<'_, W, C, K>> {
|
||||
let encoder = Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)?;
|
||||
ImageEncoder::new(encoder, width, height, self.compression, self.predictor)
|
||||
}
|
||||
|
||||
/// Convenience function to write an entire image from memory.
|
||||
@@ -127,26 +211,22 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
|
||||
where
|
||||
[C::Inner]: TiffValue,
|
||||
{
|
||||
let encoder = DirectoryEncoder::new(&mut self.writer)?;
|
||||
let image: ImageEncoder<W, C, K> = ImageEncoder::new(encoder, width, height)?;
|
||||
let encoder = Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)?;
|
||||
let image: ImageEncoder<W, C, K> =
|
||||
ImageEncoder::new(encoder, width, height, self.compression, self.predictor)?;
|
||||
image.write_data(data)
|
||||
}
|
||||
|
||||
/// Convenience function to write an entire image from memory with a given compression.
|
||||
pub fn write_image_with_compression<C: ColorType, D: Compression>(
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
compression: D,
|
||||
data: &[C::Inner],
|
||||
) -> TiffResult<()>
|
||||
where
|
||||
[C::Inner]: TiffValue,
|
||||
{
|
||||
let encoder = DirectoryEncoder::new(&mut self.writer)?;
|
||||
let image: ImageEncoder<W, C, K, D> =
|
||||
ImageEncoder::with_compression(encoder, width, height, compression)?;
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,119 +236,341 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
|
||||
/// Encoding can silently fail while this is dropping.
|
||||
pub struct DirectoryEncoder<'a, W: 'a + Write + Seek, K: TiffKind> {
|
||||
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
|
||||
ifd_pointer_pos: u64,
|
||||
ifd: BTreeMap<u16, DirectoryEntry<K::OffsetType>>,
|
||||
directory: Directory,
|
||||
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> {
|
||||
fn new(writer: &'a mut TiffWriter<W>) -> TiffResult<Self> {
|
||||
// the previous word is the IFD offset position
|
||||
let ifd_pointer_pos = writer.offset() - mem::size_of::<K::OffsetType>() as u64;
|
||||
/// Construct a directory writer appending to data, assuming the writer is currently positioned
|
||||
/// immediately after a previously written IFD to which to append.
|
||||
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?
|
||||
Ok(DirectoryEncoder {
|
||||
Ok(Self {
|
||||
writer,
|
||||
chained_ifd_pos,
|
||||
write_chain: chain_into,
|
||||
kind: PhantomData,
|
||||
directory: Directory::empty(),
|
||||
dropped: false,
|
||||
ifd_pointer_pos,
|
||||
ifd: BTreeMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Write a single ifd tag.
|
||||
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 writer = TiffWriter::new(&mut bytes);
|
||||
value.write(&mut writer)?;
|
||||
}
|
||||
|
||||
self.ifd.insert(
|
||||
tag.to_u16(),
|
||||
let entry = Self::write_entry_inner(
|
||||
self.writer,
|
||||
DirectoryEntry {
|
||||
data_type: <T>::FIELD_TYPE.to_u16(),
|
||||
data_type: <T>::FIELD_TYPE,
|
||||
count: value.count().try_into()?,
|
||||
data: bytes,
|
||||
data: bytes.into(),
|
||||
},
|
||||
);
|
||||
)?;
|
||||
|
||||
self.directory.extend([(tag, entry)]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_directory(&mut self) -> TiffResult<u64> {
|
||||
// Start by writing out all values
|
||||
for &mut DirectoryEntry {
|
||||
data: ref mut bytes,
|
||||
..
|
||||
} in self.ifd.values_mut()
|
||||
{
|
||||
let data_bytes = mem::size_of::<K::OffsetType>();
|
||||
/// Write a tag-value pair with prepared byte data.
|
||||
///
|
||||
/// Note that the library will _not_ attempt to verify that the data type or the count of the
|
||||
/// buffered value is permissible for the given tag. The data will be associated with the tag
|
||||
/// exactly as-is.
|
||||
///
|
||||
/// An error is returned if the byte order of the presented value does not match the byte order
|
||||
/// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)).
|
||||
pub fn write_tag_buf(&mut self, tag: Tag, value: &ValueBuffer) -> TiffResult<()> {
|
||||
let entry = self.write_entry_buf(value)?;
|
||||
self.directory.extend([(tag, entry)]);
|
||||
|
||||
if bytes.len() > data_bytes {
|
||||
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)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write some data to the tiff file, the offset of the data is returned.
|
||||
///
|
||||
/// This could be used to write tiff strips.
|
||||
/// This could be used to write tiff strips. All of the data will be written into the file as a
|
||||
/// slice of bytes. It can not be used as an entry in a directory directly, where very short
|
||||
/// values are instead represented in the offset field directly. Use [`Self::write_entry`]
|
||||
/// instead.
|
||||
pub fn write_data<T: TiffValue>(&mut self, value: T) -> TiffResult<u64> {
|
||||
let offset = self.writer.offset();
|
||||
value.write(&mut self.writer)?;
|
||||
value.write(self.writer)?;
|
||||
Ok(offset)
|
||||
}
|
||||
|
||||
/// Write some data directly to the tiff file, the offset of the data is returned.
|
||||
///
|
||||
/// All of the data will be written into the file as a slice of bytes. It can not be used as an
|
||||
/// entry in a directory directly, where very short values are instead represented in the
|
||||
/// offset field directly. Use [`Self::write_entry_buf`] instead.
|
||||
///
|
||||
/// An error is returned if the byte order of the presented value does not match the byte order
|
||||
/// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)).
|
||||
pub fn write_data_buf(&mut self, value: &ValueBuffer) -> TiffResult<u64> {
|
||||
self.check_value_byteorder(value)?;
|
||||
let offset = self.writer.offset();
|
||||
self.writer.write_bytes(value.as_bytes())?;
|
||||
Ok(offset)
|
||||
}
|
||||
|
||||
/// Write a directory value into the file.
|
||||
///
|
||||
/// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
|
||||
/// stored in the entry's offset field.
|
||||
pub fn write_entry<T: TiffValue>(&mut self, value: T) -> TiffResult<Entry> {
|
||||
Self::write_entry_inner(
|
||||
self.writer,
|
||||
DirectoryEntry {
|
||||
data_type: T::FIELD_TYPE,
|
||||
count: K::convert_offset(value.count() as u64)?,
|
||||
// FIXME: not optimal we always allocate here. But the only other method we have
|
||||
// available is `T::write(&mut TiffWriter<W>)` and we must pass that into
|
||||
// `write_entry_inner` but then have a combinatorial explosion of instantiations
|
||||
// which is absurd.
|
||||
data: value.data(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Write a directory value into the file.
|
||||
///
|
||||
/// An error is returned if the byte order of the presented value does not match the byte order
|
||||
/// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)).
|
||||
///
|
||||
/// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
|
||||
/// stored in the entry's offset field.
|
||||
pub fn write_entry_buf(&mut self, value: &ValueBuffer) -> TiffResult<Entry> {
|
||||
self.check_value_byteorder(value)?;
|
||||
|
||||
Self::write_entry_inner(
|
||||
self.writer,
|
||||
DirectoryEntry {
|
||||
data_type: value.data_type(),
|
||||
count: K::convert_offset(value.count())?,
|
||||
data: value.as_bytes().into(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Write a directory entry by its byte data.
|
||||
///
|
||||
/// The data must be pre-formatted to the byte order expected by the file.
|
||||
///
|
||||
/// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
|
||||
/// stored in the entry's offset field.
|
||||
pub fn write_entry_bytes(&mut self, ty: Type, data: &[u8]) -> TiffResult<Entry> {
|
||||
if !data.len().is_multiple_of(usize::from(ty.byte_len())) {
|
||||
return Err(TiffError::UsageError(UsageError::MismatchedEntryLength {
|
||||
ty,
|
||||
found: data.len(),
|
||||
}));
|
||||
}
|
||||
|
||||
let count = data.len() / usize::from(ty.byte_len());
|
||||
let count = u64::try_from(count)?;
|
||||
|
||||
Self::write_entry_inner(
|
||||
self.writer,
|
||||
DirectoryEntry {
|
||||
data_type: ty,
|
||||
count: K::convert_offset(count)?,
|
||||
data: data.into(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Insert previously written key-value entries.
|
||||
///
|
||||
/// It is the caller's responsibility to ensure that the data referred to by the entries is
|
||||
/// 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(())
|
||||
}
|
||||
|
||||
/// Write the IFD to the file and return the offset it is written to.
|
||||
///
|
||||
/// The offset can be used as the value, or as part of a list of values, in the entry of
|
||||
/// another directory. If you're constructing the entry directly you'll want to use an
|
||||
/// appropriate type variant for this such as [`Type::IFD`].
|
||||
pub fn finish_with_offsets(mut self) -> TiffResult<DirectoryOffset<K>> {
|
||||
self.finish_internal()
|
||||
}
|
||||
|
||||
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<()> {
|
||||
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(self.ifd_pointer_pos)?;
|
||||
K::write_offset(&mut self.writer, ifd_pointer)?;
|
||||
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(&mut self.writer, 0)?;
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Write out the ifd directory.
|
||||
pub fn finish(mut self) -> TiffResult<()> {
|
||||
self.finish_internal()
|
||||
Ok(DirectoryOffset {
|
||||
pointer: IfdPointer(ifd_pointer),
|
||||
offset,
|
||||
ifd_chain,
|
||||
kind: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,42 +615,45 @@ impl<'a, W: Write + Seek, K: TiffKind> Drop for DirectoryEncoder<'a, W, K> {
|
||||
/// # }
|
||||
/// ```
|
||||
/// You can also call write_data function wich will encode by strip and finish
|
||||
pub struct ImageEncoder<
|
||||
'a,
|
||||
W: 'a + Write + Seek,
|
||||
C: ColorType,
|
||||
K: TiffKind,
|
||||
D: Compression = Uncompressed,
|
||||
> {
|
||||
pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> {
|
||||
encoder: DirectoryEncoder<'a, W, K>,
|
||||
strip_idx: u64,
|
||||
strip_count: u64,
|
||||
row_samples: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
extra_samples: Vec<u16>,
|
||||
rows_per_strip: u64,
|
||||
strip_offsets: Vec<K::OffsetType>,
|
||||
strip_byte_count: Vec<K::OffsetType>,
|
||||
dropped: bool,
|
||||
compression: D,
|
||||
compression: Compression,
|
||||
predictor: Predictor,
|
||||
_phantom: ::std::marker::PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
|
||||
ImageEncoder<'a, W, T, K, D>
|
||||
{
|
||||
fn new(encoder: DirectoryEncoder<'a, W, K>, width: u32, height: u32) -> TiffResult<Self>
|
||||
where
|
||||
D: Default,
|
||||
{
|
||||
Self::with_compression(encoder, width, height, D::default())
|
||||
impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> {
|
||||
fn sanity_check(compression: Compression, predictor: Predictor) -> TiffResult<()> {
|
||||
match (predictor, compression, T::SAMPLE_FORMAT[0]) {
|
||||
// Horizontal predictor is not valid for floating-point types
|
||||
(Predictor::Horizontal, _, SampleFormat::IEEEFP | SampleFormat::Void) => {
|
||||
Err(TiffError::UsageError(UsageError::PredictorIncompatible))
|
||||
}
|
||||
// Floating-point predictor is only valid for IEEEFP sample format
|
||||
(Predictor::FloatingPoint, _, SampleFormat::IEEEFP) => Ok(()),
|
||||
(Predictor::FloatingPoint, _, _) => {
|
||||
Err(TiffError::UsageError(UsageError::PredictorIncompatible))
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_compression(
|
||||
fn new(
|
||||
mut encoder: DirectoryEncoder<'a, W, K>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
compression: D,
|
||||
compression: Compression,
|
||||
predictor: Predictor,
|
||||
) -> TiffResult<Self> {
|
||||
if width == 0 || height == 0 {
|
||||
return Err(TiffError::FormatError(TiffFormatError::InvalidDimensions(
|
||||
@@ -356,29 +661,35 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
|
||||
)));
|
||||
}
|
||||
|
||||
Self::sanity_check(compression, predictor)?;
|
||||
|
||||
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);
|
||||
|
||||
// Limit the strip size to prevent potential memory and security issues.
|
||||
// Also keep the multiple strip handling 'oiled'
|
||||
let rows_per_strip = {
|
||||
match D::COMPRESSION_METHOD {
|
||||
match compression.tag() {
|
||||
CompressionMethod::PackBits => 1, // Each row must be packed separately. Do not compress across row boundaries
|
||||
_ => (1_000_000 + row_bytes - 1) / row_bytes,
|
||||
_ => 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::ImageLength, height)?;
|
||||
encoder.write_tag(Tag::Compression, D::COMPRESSION_METHOD.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())?;
|
||||
|
||||
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(
|
||||
@@ -387,7 +698,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
|
||||
)?;
|
||||
encoder.write_tag(Tag::XResolution, Rational { n: 1, d: 1 })?;
|
||||
encoder.write_tag(Tag::YResolution, Rational { n: 1, d: 1 })?;
|
||||
encoder.write_tag(Tag::ResolutionUnit, ResolutionUnit::None.to_u16())?;
|
||||
encoder.write_tag(Tag::ResolutionUnit, ResolutionUnit::None)?;
|
||||
|
||||
Ok(ImageEncoder {
|
||||
encoder,
|
||||
@@ -395,16 +706,39 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
|
||||
strip_idx: 0,
|
||||
row_samples,
|
||||
rows_per_strip,
|
||||
extra_samples: vec![],
|
||||
width,
|
||||
height,
|
||||
strip_offsets: Vec::new(),
|
||||
strip_byte_count: Vec::new(),
|
||||
dropped: false,
|
||||
compression: compression,
|
||||
compression,
|
||||
predictor,
|
||||
_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.
|
||||
pub fn next_strip_sample_count(&self) -> u64 {
|
||||
if self.strip_idx >= self.strip_count {
|
||||
@@ -433,7 +767,24 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
|
||||
}
|
||||
|
||||
// Write the (possible compressed) data to the encoder.
|
||||
let offset = self.encoder.write_data(value)?;
|
||||
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)?);
|
||||
@@ -524,13 +875,46 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
|
||||
self.encoder.write_tag(Tag::RowsPerStrip, value)?;
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
.write_tag(Tag::StripOffsets, K::convert_slice(&self.strip_offsets))?;
|
||||
self.encoder.write_tag(
|
||||
@@ -549,13 +933,12 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
|
||||
|
||||
/// Write out image and ifd directory.
|
||||
pub fn finish(mut self) -> TiffResult<()> {
|
||||
self.finish_internal()
|
||||
self.finish_internal()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, W: Write + Seek, C: ColorType, K: TiffKind, D: Compression> Drop
|
||||
for ImageEncoder<'a, W, C, K, D>
|
||||
{
|
||||
impl<'a, W: Write + Seek, C: ColorType, K: TiffKind> Drop for ImageEncoder<'a, W, C, K> {
|
||||
fn drop(&mut self) {
|
||||
if !self.dropped {
|
||||
let _ = self.finish_internal();
|
||||
@@ -563,10 +946,10 @@ impl<'a, W: Write + Seek, C: ColorType, K: TiffKind, D: Compression> Drop
|
||||
}
|
||||
}
|
||||
|
||||
struct DirectoryEntry<S> {
|
||||
data_type: u16,
|
||||
struct DirectoryEntry<'data, S> {
|
||||
data_type: Type,
|
||||
count: S,
|
||||
data: Vec<u8>,
|
||||
data: std::borrow::Cow<'data, [u8]>,
|
||||
}
|
||||
|
||||
/// Trait to abstract over Tiff/BigTiff differences.
|
||||
@@ -611,6 +994,71 @@ pub trait TiffKind {
|
||||
fn convert_slice(slice: &[Self::OffsetType]) -> &Self::OffsetArrayType;
|
||||
}
|
||||
|
||||
impl<K: TiffKind> DirectoryOffset<K> {
|
||||
/// Construct the directory pointer information from its raw offset.
|
||||
///
|
||||
/// Fails in these cases:
|
||||
///
|
||||
/// - if start offset is too large to be represented with the chosen TIFF format, i.e. a value
|
||||
/// larger than [`u32::MAX`] with a standard tiff which is only valid in a BigTiff. This
|
||||
/// returns [`TiffError::IntSizeError`].
|
||||
/// - the extent of the directory would overflow the file length. This returns a
|
||||
/// [`TiffError::UsageError`].
|
||||
///
|
||||
/// The library does *not* validate the contents or offset in any way, it just performs the
|
||||
/// necessary math assuming the data is an accurate representation of existing content.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// If you have any custom way of writing a directory to a file that can not go through the
|
||||
/// usual [`TiffEncoder::extra_directory`] then you can reconstruct the offset information as
|
||||
/// long as you provide all the necessary data to the library. You can then use it as a parent
|
||||
/// directory, i.e. have the library overwrite its `next` field.
|
||||
///
|
||||
/// ```
|
||||
/// use tiff::{
|
||||
/// encoder::{TiffEncoder, DirectoryOffset},
|
||||
/// Directory,
|
||||
/// tags::IfdPointer,
|
||||
/// };
|
||||
///
|
||||
/// # let mut file = std::io::Cursor::new(Vec::new());
|
||||
/// let mut tiff = TiffEncoder::new(&mut file)?;
|
||||
///
|
||||
/// // … some custom data writes, assume we know where a directory was written
|
||||
/// # fn reconstruction_of_your_directory() -> Directory { Directory::from_iter([]) }
|
||||
/// let known_offset = IfdPointer(1024);
|
||||
/// let known_contents: Directory = reconstruction_of_your_directory();
|
||||
///
|
||||
/// let reconstructed = DirectoryOffset::new(known_offset, &known_contents)?;
|
||||
///
|
||||
/// // Now we can use it as if the directory was written by the `TiffEncoder` itself.
|
||||
/// let mut chain = tiff.extra_directory()?;
|
||||
/// chain.set_parent(&reconstructed);
|
||||
/// chain.finish_with_offsets()?;
|
||||
///
|
||||
/// # Ok::<_, tiff::TiffError>(())
|
||||
/// ```
|
||||
pub fn new(pointer: IfdPointer, dir: &Directory) -> TiffResult<Self> {
|
||||
let offset = K::convert_offset(pointer.0)?;
|
||||
let encoded = dir.encoded_len::<K>();
|
||||
let offset_field_offset = encoded - mem::size_of_val(&offset) as u64;
|
||||
|
||||
let ifd_chain = pointer
|
||||
.0
|
||||
.checked_add(offset_field_offset)
|
||||
.and_then(NonZeroU64::new)
|
||||
.ok_or(TiffError::UsageError(UsageError::ZeroIfdPointer))?;
|
||||
|
||||
Ok(DirectoryOffset {
|
||||
pointer,
|
||||
offset,
|
||||
ifd_chain,
|
||||
kind: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a standard Tiff file.
|
||||
pub struct TiffKindStandard;
|
||||
|
||||
@@ -679,3 +1127,33 @@ impl TiffKind for TiffKindBig {
|
||||
slice
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directory_offset_new_equivalent_to_writing() {
|
||||
type K = TiffKindStandard;
|
||||
|
||||
let dir = Directory::from_iter(vec![
|
||||
(
|
||||
Tag::ImageWidth,
|
||||
Entry::new(Type::LONG, 1, 100u32.to_ne_bytes()),
|
||||
),
|
||||
(
|
||||
Tag::ImageLength,
|
||||
Entry::new(Type::LONG, 1, 200u32.to_ne_bytes()),
|
||||
),
|
||||
]);
|
||||
|
||||
let data = std::io::Cursor::new(vec![]);
|
||||
let mut file = TiffEncoder::new(data).unwrap();
|
||||
|
||||
let mut as_dir_encoder = file.extra_directory().unwrap();
|
||||
as_dir_encoder.extend_from(&dir);
|
||||
let dir_extent = as_dir_encoder.finish_with_offsets().unwrap();
|
||||
|
||||
// Check we can recreate the extent had we written ourselves.
|
||||
let synth = DirectoryOffset::<K>::new(dir_extent.pointer, &dir).unwrap();
|
||||
|
||||
assert_eq!(synth.pointer, dir_extent.pointer);
|
||||
assert_eq!(synth.offset, dir_extent.offset);
|
||||
assert_eq!(synth.ifd_chain, dir_extent.ifd_chain);
|
||||
}
|
||||
+194
-30
@@ -1,21 +1,34 @@
|
||||
use std::{borrow::Cow, io::Write, slice::from_ref};
|
||||
|
||||
use crate::{bytecast, tags::Type, TiffError, TiffFormatError, TiffResult};
|
||||
use crate::{bytecast, tags, tags::Type, TiffError, TiffFormatError, TiffResult};
|
||||
|
||||
use super::writer::TiffWriter;
|
||||
|
||||
/// Trait for types that can be encoded in a tiff file
|
||||
#[diagnostic::on_unimplemented(
|
||||
message = "the type `{Self}` does not implement `TiffValue`",
|
||||
note = "the trait is implemented for primitive types (`u8`, `i16`, `f32`, etc.)",
|
||||
note = "the trait is implemented for shared references to values",
|
||||
note = "the trait is implemented for slices, pass them by reference (e.g. `&[u8]`)",
|
||||
note = "the trait is implemented for arrays if it is implemented for a slice",
|
||||
note = "values in a `Vec` or `Box` should be dereferenced, e.g. `&vec[..]`"
|
||||
)]
|
||||
pub trait TiffValue {
|
||||
const BYTE_LEN: u8;
|
||||
const FIELD_TYPE: Type;
|
||||
|
||||
// FIXME: If we want this to mean 'count' in the sense of `Entry` it should return `u32` or
|
||||
// `u64`, probably the latter with `convert_offset` taking care of the check against `u32::MAX`
|
||||
// for classic tiff.
|
||||
fn count(&self) -> usize;
|
||||
|
||||
fn bytes(&self) -> usize {
|
||||
self.count() * usize::from(Self::BYTE_LEN)
|
||||
}
|
||||
|
||||
/// 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]>;
|
||||
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.
|
||||
@@ -34,7 +47,7 @@ impl TiffValue for [u8] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(self)
|
||||
}
|
||||
}
|
||||
@@ -47,7 +60,7 @@ impl TiffValue for [i8] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i8_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -60,7 +73,7 @@ impl TiffValue for [u16] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u16_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -73,7 +86,7 @@ impl TiffValue for [i16] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i16_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -86,7 +99,7 @@ impl TiffValue for [u32] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u32_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -99,7 +112,7 @@ impl TiffValue for [i32] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i32_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -112,7 +125,7 @@ impl TiffValue for [u64] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u64_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -125,7 +138,7 @@ impl TiffValue for [i64] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i64_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -138,7 +151,7 @@ impl TiffValue for [f32] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
// We write using native endian so this should be safe
|
||||
Cow::Borrowed(bytecast::f32_as_ne_bytes(self))
|
||||
}
|
||||
@@ -152,7 +165,7 @@ impl TiffValue for [f64] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
// We write using native endian so this should be safe
|
||||
Cow::Borrowed(bytecast::f64_as_ne_bytes(self))
|
||||
}
|
||||
@@ -171,7 +184,7 @@ impl TiffValue for u8 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(from_ref(self))
|
||||
}
|
||||
}
|
||||
@@ -189,7 +202,7 @@ impl TiffValue for i8 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i8_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -207,7 +220,7 @@ impl TiffValue for u16 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u16_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -225,7 +238,7 @@ impl TiffValue for i16 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i16_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -243,7 +256,7 @@ impl TiffValue for u32 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -261,7 +274,7 @@ impl TiffValue for i32 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i32_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -279,7 +292,7 @@ impl TiffValue for u64 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -297,7 +310,7 @@ impl TiffValue for i64 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i64_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -315,7 +328,7 @@ impl TiffValue for f32 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::f32_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -333,7 +346,7 @@ impl TiffValue for f64 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::f64_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -351,7 +364,7 @@ impl TiffValue for Ifd {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(&self.0)))
|
||||
}
|
||||
}
|
||||
@@ -369,7 +382,7 @@ impl TiffValue for Ifd8 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(&self.0)))
|
||||
}
|
||||
}
|
||||
@@ -388,7 +401,7 @@ impl TiffValue for Rational {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
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));
|
||||
@@ -411,7 +424,7 @@ impl TiffValue for SRational {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
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));
|
||||
@@ -438,7 +451,7 @@ impl TiffValue for str {
|
||||
}
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Owned({
|
||||
if self.is_ascii() && !self.bytes().any(|b| b == 0) {
|
||||
let bytes: &[u8] = self.as_bytes();
|
||||
@@ -450,7 +463,137 @@ impl TiffValue for str {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: TiffValue + ?Sized> TiffValue for &'a T {
|
||||
// FIXME: held up on reading the discriminant for all but the unknown variant.
|
||||
//
|
||||
// It needs to have `repr(u16)` to have a defined layout and then we may read the tag from its
|
||||
// value representation by casting a u16 pointer to a pointer to the enum type.
|
||||
// unsafe { *<*const _>::from(self).cast::<u16>() }
|
||||
// But that is quite unsafe and needs careful review to maintain the safety requirements. Maybe we
|
||||
// could get `bytemuck` to add a trait for this (it has `TransparentWrapper`) with a derive for
|
||||
// enum types that have an explicit repr.
|
||||
//
|
||||
// This would allow returning borrowed data in more cases. For all types without an unknown, in all
|
||||
// cases including as slices, and for all types with and unknown variant when we have a single
|
||||
// value (either returning a borrowed discriminant or a borrowed value within `Unknown`).
|
||||
impl TiffValue for tags::CompressionMethod {
|
||||
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
|
||||
const FIELD_TYPE: Type = Type::SHORT;
|
||||
|
||||
fn count(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
let bytes = self.to_u16().to_ne_bytes();
|
||||
Cow::Owned(bytes.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl TiffValue for tags::PhotometricInterpretation {
|
||||
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
|
||||
const FIELD_TYPE: Type = Type::SHORT;
|
||||
|
||||
fn count(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
self.to_u16().to_ne_bytes().to_vec().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TiffValue for tags::PlanarConfiguration {
|
||||
const BYTE_LEN: u8 = 2;
|
||||
const FIELD_TYPE: Type = Type::SHORT;
|
||||
|
||||
fn count(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
self.to_u16().to_ne_bytes().to_vec().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TiffValue for tags::Predictor {
|
||||
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
|
||||
const FIELD_TYPE: Type = Type::SHORT;
|
||||
|
||||
fn count(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
self.to_u16().to_ne_bytes().to_vec().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TiffValue for tags::ResolutionUnit {
|
||||
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
|
||||
const FIELD_TYPE: Type = Type::SHORT;
|
||||
|
||||
fn count(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
self.to_u16().to_ne_bytes().to_vec().into()
|
||||
}
|
||||
}
|
||||
|
||||
/// This is implemented for slices as the `SampleFormat` tag takes a count of `N`.
|
||||
///
|
||||
/// Use `core::slice::from_ref` if you really need to write a single element.
|
||||
///
|
||||
/// See: <https://web.archive.org/web/20191120220815/https://www.awaresystems.be/imaging/tiff/tifftags/sampleformat.html>
|
||||
impl TiffValue for [tags::SampleFormat] {
|
||||
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
|
||||
const FIELD_TYPE: Type = Type::SHORT;
|
||||
|
||||
fn count(&self) -> usize {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(self.len() * 2);
|
||||
for x in self {
|
||||
buf.extend_from_slice(&x.to_u16().to_ne_bytes());
|
||||
}
|
||||
Cow::Owned(buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// This is implemented for slices as the `ExtraSamples` tag takes a count of `N`.
|
||||
///
|
||||
/// Use `core::slice::from_ref` if you really need to write a single element.
|
||||
///
|
||||
/// See: <https://web.archive.org/web/20191120220815/https://www.awaresystems.be/imaging/tiff/tifftags/extrasamples.html>
|
||||
impl TiffValue for [tags::ExtraSamples] {
|
||||
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
|
||||
const FIELD_TYPE: Type = Type::SHORT;
|
||||
|
||||
fn count(&self) -> usize {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(self.len() * 2);
|
||||
for x in self {
|
||||
buf.extend_from_slice(&x.to_u16().to_ne_bytes());
|
||||
}
|
||||
Cow::Owned(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// If you pass `&Vec<_>` then you'd get at first sight the complaint:
|
||||
//
|
||||
// `Vec<T>` does not implement `TiffValue`
|
||||
//
|
||||
// and a list of implementations of `TiffValue` that are quite unrelated. That is not very helpful.
|
||||
// We do not *want* `Vec` to implement the trait as an owning type, instead you should pass a
|
||||
// reference to a slice. The error message is further customized in the diagnostics of the trait.
|
||||
#[diagnostic::do_not_recommend]
|
||||
impl<T: TiffValue + ?Sized> TiffValue for &'_ T {
|
||||
const BYTE_LEN: u8 = T::BYTE_LEN;
|
||||
const FIELD_TYPE: Type = T::FIELD_TYPE;
|
||||
|
||||
@@ -462,11 +605,32 @@ impl<'a, T: TiffValue + ?Sized> TiffValue for &'a T {
|
||||
(*self).write(writer)
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
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] {
|
||||
@@ -484,7 +648,7 @@ macro_rules! impl_tiff_value_for_contiguous_sequence {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(Self::BYTE_LEN as usize * self.len());
|
||||
for x in self {
|
||||
buf.extend_from_slice(&x.data());
|
||||
|
||||
+69
-2
@@ -69,6 +69,10 @@ impl<W: Write> TiffWriter<W> {
|
||||
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
|
||||
}
|
||||
@@ -168,7 +172,7 @@ impl<W: Write> TiffWriter<W> {
|
||||
}
|
||||
|
||||
pub fn pad_word_boundary(&mut self) -> Result<(), io::Error> {
|
||||
if self.offset % 4 != 0 {
|
||||
if !self.offset.is_multiple_of(4) {
|
||||
let padding = [0, 0, 0];
|
||||
let padd_len = 4 - (self.offset % 4);
|
||||
self.writer.write_all(&padding[..padd_len as usize])?;
|
||||
@@ -182,7 +186,70 @@ impl<W: Write> TiffWriter<W> {
|
||||
impl<W: Seek> TiffWriter<W> {
|
||||
pub fn goto_offset(&mut self, offset: u64) -> Result<(), io::Error> {
|
||||
self.offset = offset;
|
||||
self.writer.seek(SeekFrom::Start(offset as u64))?;
|
||||
self.writer.seek(SeekFrom::Start(offset))?;
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
+153
-265
@@ -1,284 +1,223 @@
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
use std::io;
|
||||
use std::str;
|
||||
use std::string;
|
||||
use std::sync::Arc;
|
||||
|
||||
use jpeg::UnsupportedFeature;
|
||||
use quick_error::quick_error;
|
||||
|
||||
use crate::decoder::{ifd::Value, ChunkType};
|
||||
use crate::decoder::ChunkType;
|
||||
use crate::tags::Type;
|
||||
use crate::tags::{
|
||||
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag,
|
||||
};
|
||||
use crate::ColorType;
|
||||
|
||||
use crate::weezl::LzwError;
|
||||
|
||||
quick_error! {
|
||||
/// Tiff error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum TiffError {
|
||||
/// The Image is not formatted properly.
|
||||
FormatError(TiffFormatError),
|
||||
|
||||
FormatError(err: TiffFormatError) {
|
||||
display("format error: {err}")
|
||||
from()
|
||||
source(err)
|
||||
}
|
||||
/// The Decoder does not support features required by the image.
|
||||
UnsupportedError(TiffUnsupportedError),
|
||||
|
||||
UnsupportedError(err: TiffUnsupportedError) {
|
||||
display("unsupported error: {err}")
|
||||
from()
|
||||
source(err)
|
||||
}
|
||||
/// An I/O Error occurred while decoding the image.
|
||||
IoError(io::Error),
|
||||
|
||||
IoError(err: io::Error) {
|
||||
display("{err}")
|
||||
from()
|
||||
source(err)
|
||||
}
|
||||
/// The Limits of the Decoder is exceeded.
|
||||
LimitsExceeded,
|
||||
|
||||
/// An integer conversion to or from a platform size failed, either due to
|
||||
/// limits of the platform size or limits of the format.
|
||||
IntSizeError,
|
||||
|
||||
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(UsageError),
|
||||
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.
|
||||
///
|
||||
/// The list of variants may grow to incorporate errors of future features. Matching against this
|
||||
/// exhaustively is not covered by interface stability guarantees.
|
||||
/// 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,
|
||||
TiffSignatureInvalid,
|
||||
ImageFileDirectoryNotFound,
|
||||
InconsistentSizesEncountered,
|
||||
UnexpectedCompressedData {
|
||||
actual_bytes: usize,
|
||||
required_bytes: usize,
|
||||
},
|
||||
InconsistentStripSamples {
|
||||
actual_samples: usize,
|
||||
required_samples: usize,
|
||||
},
|
||||
InvalidDimensions(u32, u32),
|
||||
InvalidTag,
|
||||
InvalidTagValueType(Tag),
|
||||
RequiredTagNotFound(Tag),
|
||||
UnknownPredictor(u16),
|
||||
ByteExpected(Value),
|
||||
UnsignedIntegerExpected(Value),
|
||||
SignedIntegerExpected(Value),
|
||||
Format(String),
|
||||
RequiredTagEmpty(Tag),
|
||||
StripTileTagConflict,
|
||||
CycleInOffsets,
|
||||
JpegDecoder(JpegDecoderError),
|
||||
TiffSignatureNotFound {
|
||||
display("TIFF signature not found")
|
||||
}
|
||||
|
||||
impl fmt::Display for TiffFormatError {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
use self::TiffFormatError::*;
|
||||
match *self {
|
||||
TiffSignatureNotFound => write!(fmt, "TIFF signature not found."),
|
||||
TiffSignatureInvalid => write!(fmt, "TIFF signature invalid."),
|
||||
ImageFileDirectoryNotFound => write!(fmt, "Image file directory not found."),
|
||||
InconsistentSizesEncountered => write!(fmt, "Inconsistent sizes encountered."),
|
||||
UnexpectedCompressedData {
|
||||
actual_bytes,
|
||||
required_bytes,
|
||||
} => {
|
||||
write!(
|
||||
fmt,
|
||||
"Decompression returned different amount of bytes than expected: got {}, expected {}.",
|
||||
actual_bytes, required_bytes
|
||||
)
|
||||
TiffSignatureInvalid {
|
||||
display("TIFF signature invalid")
|
||||
}
|
||||
InconsistentStripSamples {
|
||||
actual_samples,
|
||||
required_samples,
|
||||
} => {
|
||||
write!(
|
||||
fmt,
|
||||
"Inconsistent elements in strip: got {}, expected {}.",
|
||||
actual_samples, required_samples
|
||||
)
|
||||
ImageFileDirectoryNotFound {
|
||||
display("image file directory not found")
|
||||
}
|
||||
InvalidDimensions(width, height) => write!(fmt, "Invalid dimensions: {}x{}.", width, height),
|
||||
InvalidTag => write!(fmt, "Image contains invalid tag."),
|
||||
InvalidTagValueType(ref tag) => {
|
||||
write!(fmt, "Tag `{:?}` did not have the expected value type.", tag)
|
||||
InconsistentSizesEncountered {
|
||||
display("inconsistent sizes encountered")
|
||||
}
|
||||
RequiredTagNotFound(ref tag) => write!(fmt, "Required tag `{:?}` not found.", tag),
|
||||
UnknownPredictor(ref predictor) => {
|
||||
write!(fmt, "Unknown predictor “{}” encountered", predictor)
|
||||
IncorrectChunkCount {expected: u32, found: u32} {
|
||||
display("incorrect number of chunk offsets encoded for the tile count of the image, expected {expected} but found {found}")
|
||||
}
|
||||
ByteExpected(ref val) => write!(fmt, "Expected byte, {:?} found.", val),
|
||||
UnsignedIntegerExpected(ref val) => {
|
||||
write!(fmt, "Expected unsigned integer, {:?} found.", val)
|
||||
InvalidDimensions(width: u32, height: u32) {
|
||||
display("invalid dimensions: {width}x{height}")
|
||||
}
|
||||
SignedIntegerExpected(ref val) => {
|
||||
write!(fmt, "Expected signed integer, {:?} found.", val)
|
||||
InvalidTag {
|
||||
display("image contains invalid tag")
|
||||
}
|
||||
Format(ref val) => write!(fmt, "Invalid format: {:?}.", val),
|
||||
RequiredTagEmpty(ref val) => write!(fmt, "Required tag {:?} was empty.", val),
|
||||
StripTileTagConflict => write!(fmt, "File should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found."),
|
||||
CycleInOffsets => write!(fmt, "File contained a cycle in the list of IFDs"),
|
||||
JpegDecoder(ref error) => write!(fmt, "{}", error),
|
||||
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.
|
||||
///
|
||||
/// 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
|
||||
/// time and will then get deprecated before being removed.
|
||||
///
|
||||
/// The list of variants may grow. Matching against this exhaustively is not covered by interface
|
||||
/// stability guarantees.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum TiffUnsupportedError {
|
||||
FloatingPointPredictor(ColorType),
|
||||
HorizontalPredictor(ColorType),
|
||||
InterpretationWithBits(PhotometricInterpretation, Vec<u8>),
|
||||
UnknownInterpretation,
|
||||
UnknownCompressionMethod,
|
||||
UnsupportedCompressionMethod(CompressionMethod),
|
||||
UnsupportedSampleDepth(u8),
|
||||
UnsupportedSampleFormat(Vec<SampleFormat>),
|
||||
UnsupportedColorType(ColorType),
|
||||
UnsupportedBitsPerChannel(u8),
|
||||
UnsupportedPlanarConfig(Option<PlanarConfiguration>),
|
||||
UnsupportedDataType,
|
||||
UnsupportedInterpretation(PhotometricInterpretation),
|
||||
UnsupportedJpegFeature(UnsupportedFeature),
|
||||
FloatingPointPredictor(color_type: ColorType) {
|
||||
display("floating point predictor for {color_type:?} is unsupported")
|
||||
}
|
||||
|
||||
impl fmt::Display for TiffUnsupportedError {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
use self::TiffUnsupportedError::*;
|
||||
match *self {
|
||||
FloatingPointPredictor(color_type) => write!(
|
||||
fmt,
|
||||
"Floating point predictor for {:?} is unsupported.",
|
||||
color_type
|
||||
),
|
||||
HorizontalPredictor(color_type) => write!(
|
||||
fmt,
|
||||
"Horizontal predictor for {:?} is unsupported.",
|
||||
color_type
|
||||
),
|
||||
InterpretationWithBits(ref photometric_interpretation, ref bits_per_sample) => write!(
|
||||
fmt,
|
||||
"{:?} with {:?} bits per sample is unsupported",
|
||||
photometric_interpretation, bits_per_sample
|
||||
),
|
||||
UnknownInterpretation => write!(
|
||||
fmt,
|
||||
"The image is using an unknown photometric interpretation."
|
||||
),
|
||||
UnknownCompressionMethod => write!(fmt, "Unknown compression method."),
|
||||
UnsupportedCompressionMethod(method) => {
|
||||
write!(fmt, "Compression method {:?} is unsupported", method)
|
||||
HorizontalPredictor(color_type: ColorType) {
|
||||
display("horizontal predictor for {color_type:?} is unsupported")
|
||||
}
|
||||
UnsupportedSampleDepth(samples) => {
|
||||
write!(fmt, "{} samples per pixel is unsupported.", samples)
|
||||
InconsistentBitsPerSample(bits_per_sample: Vec<u8>) {
|
||||
display("inconsistent bits per sample: {bits_per_sample:?}")
|
||||
}
|
||||
UnsupportedSampleFormat(ref formats) => {
|
||||
write!(fmt, "Sample format {:?} is unsupported.", formats)
|
||||
InterpretationWithBits(interpretation: PhotometricInterpretation, bits_per_sample: Vec<u8>) {
|
||||
display("Photometric interpretation {interpretation:?} with bits per sample {bits_per_sample:?} is unsupported")
|
||||
}
|
||||
UnsupportedColorType(color_type) => {
|
||||
write!(fmt, "Color type {:?} is unsupported", color_type)
|
||||
UnknownInterpretation {
|
||||
display("unknown photometric interpretation")
|
||||
}
|
||||
UnsupportedBitsPerChannel(bits) => {
|
||||
write!(fmt, "{} bits per channel not supported", bits)
|
||||
UnknownCompressionMethod {
|
||||
display("unknown compression method")
|
||||
}
|
||||
UnsupportedPlanarConfig(config) => {
|
||||
write!(fmt, "Unsupported planar configuration “{:?}”.", config)
|
||||
UnsupportedCompressionMethod(method: CompressionMethod) {
|
||||
display("compression method {method:?} is unsupported")
|
||||
}
|
||||
UnsupportedDataType => write!(fmt, "Unsupported data type."),
|
||||
UnsupportedInterpretation(interpretation) => {
|
||||
write!(
|
||||
fmt,
|
||||
"Unsupported photometric interpretation \"{:?}\".",
|
||||
interpretation
|
||||
)
|
||||
UnsupportedSampleDepth(depth: u8) {
|
||||
display("{depth} samples per pixel is unsupported")
|
||||
}
|
||||
UnsupportedJpegFeature(ref unsupported_feature) => {
|
||||
write!(fmt, "Unsupported JPEG feature {:?}", unsupported_feature)
|
||||
UnsupportedSampleFormat(sample_format: Vec<SampleFormat>) {
|
||||
display("sample format {sample_format:?} is unsupported")
|
||||
}
|
||||
UnsupportedColorType(color_type: ColorType) {
|
||||
display("color type {color_type:?} is unsupported")
|
||||
}
|
||||
UnsupportedBitsPerChannel(bits_per_channel: u8) {
|
||||
display("{bits_per_channel} bits per channel not supported")
|
||||
}
|
||||
UnsupportedPlanarConfig(planar: Option<PlanarConfiguration>) {
|
||||
display("unsupported planar configuration “{planar:?}”")
|
||||
}
|
||||
UnsupportedDataType {
|
||||
display("unsupported data type.")
|
||||
}
|
||||
UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
|
||||
display("unsupported photometric interpretation \"{interpretation:?}\"")
|
||||
}
|
||||
ChromaSubsampling {
|
||||
display("chroma subsampling of YCbCr color is unsupported")
|
||||
}
|
||||
MisalignedTileBoundaries {
|
||||
display("tile rows are not aligned to byte boundaries")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quick_error! {
|
||||
/// 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)]
|
||||
#[non_exhaustive]
|
||||
pub enum UsageError {
|
||||
InvalidChunkType(ChunkType, ChunkType),
|
||||
InvalidChunkIndex(u32),
|
||||
InvalidChunkType(expected: ChunkType, actual: ChunkType) {
|
||||
display("requested operation is only valid for images with chunk encoding of type {expected:?} but got {actual:?}")
|
||||
}
|
||||
|
||||
impl fmt::Display for UsageError {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use self::UsageError::*;
|
||||
match *self {
|
||||
InvalidChunkType(expected, actual) => {
|
||||
write!(
|
||||
fmt,
|
||||
"Requested operation is only valid for images with chunk encoding of type: {:?}, got {:?}.",
|
||||
expected, actual
|
||||
)
|
||||
InvalidChunkIndex(index: u32) {
|
||||
display("invalid chunk index ({index}) requested")
|
||||
}
|
||||
InvalidChunkIndex(index) => write!(fmt, "Image chunk index ({}) requested.", index),
|
||||
InvalidPlaneIndex(index: u16) {
|
||||
display("invalid plane index ({index}) requested")
|
||||
}
|
||||
InvalidCodingUnit(index: u32, have: u32) {
|
||||
display("out of bounds coding unit ({index}) requested, only {have} available")
|
||||
}
|
||||
PredictorCompressionMismatch {
|
||||
display("requested predictor is not compatible with the requested compression")
|
||||
}
|
||||
|
||||
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),
|
||||
PredictorIncompatible {
|
||||
display("the requested predictor is not compatible with the image's format")
|
||||
}
|
||||
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}")
|
||||
}
|
||||
|
||||
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",
|
||||
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")
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&dyn Error> {
|
||||
match *self {
|
||||
TiffError::IoError(ref e) => Some(e),
|
||||
_ => None,
|
||||
ReconfiguredAfterImageWrite {
|
||||
display("attempted to reconfigure the encoder after image writing has started")
|
||||
}
|
||||
ByteOrderMismatch {
|
||||
display("attempted to use data with a byte order that does not match the required one")
|
||||
}
|
||||
MismatchedEntryLength {ty: Type, found: usize } {
|
||||
display("the length of data is not a multiple of the size of the type {ty:?}, found {found}")
|
||||
}
|
||||
|
||||
impl From<io::Error> for TiffError {
|
||||
fn from(err: io::Error) -> TiffError {
|
||||
TiffError::IoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,74 +233,23 @@ impl From<string::FromUtf8Error> for TiffError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TiffFormatError> for TiffError {
|
||||
fn from(err: TiffFormatError) -> TiffError {
|
||||
TiffError::FormatError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TiffUnsupportedError> for TiffError {
|
||||
fn from(err: TiffUnsupportedError) -> TiffError {
|
||||
TiffError::UnsupportedError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UsageError> for TiffError {
|
||||
fn from(err: UsageError) -> TiffError {
|
||||
TiffError::UsageError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::num::TryFromIntError> for TiffError {
|
||||
fn from(_err: std::num::TryFromIntError) -> TiffError {
|
||||
TiffError::IntSizeError
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LzwError> for TiffError {
|
||||
fn from(err: LzwError) -> TiffError {
|
||||
match err {
|
||||
LzwError::InvalidCode => TiffError::FormatError(TiffFormatError::Format(String::from(
|
||||
"LZW compressed data corrupted",
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JpegDecoderError {
|
||||
inner: Arc<jpeg::Error>,
|
||||
}
|
||||
|
||||
impl JpegDecoderError {
|
||||
fn new(error: jpeg::Error) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for JpegDecoderError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.inner, &other.inner)
|
||||
#[cfg(feature = "lzw")]
|
||||
impl From<weezl::LzwError> for TiffError {
|
||||
fn from(err: weezl::LzwError) -> TiffError {
|
||||
TiffError::FormatError(TiffFormatError::CompressedDataCorrupt(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for JpegDecoderError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.inner.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JpegDecoderError> for TiffError {
|
||||
fn from(error: JpegDecoderError) -> Self {
|
||||
TiffError::FormatError(TiffFormatError::JpegDecoder(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jpeg::Error> for TiffError {
|
||||
fn from(error: jpeg::Error) -> Self {
|
||||
JpegDecoderError::new(error).into()
|
||||
#[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()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+45
-3
@@ -6,19 +6,19 @@
|
||||
//! # Related Links
|
||||
//! * <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;
|
||||
pub mod decoder;
|
||||
mod directory;
|
||||
pub mod encoder;
|
||||
mod error;
|
||||
pub mod tags;
|
||||
|
||||
pub use self::directory::Directory;
|
||||
pub use self::error::{TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError};
|
||||
|
||||
/// An enumeration over supported color types and their bit depths
|
||||
#[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum ColorType {
|
||||
/// Pixel is grayscale
|
||||
Gray(u8),
|
||||
@@ -38,6 +38,48 @@ pub enum ColorType {
|
||||
/// Pixel is CMYK
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
+383
-17
@@ -1,8 +1,11 @@
|
||||
use crate::encoder::TiffValue;
|
||||
use core::fmt;
|
||||
|
||||
macro_rules! tags {
|
||||
{
|
||||
// Permit arbitrary meta items, which include documentation.
|
||||
$( #[$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.
|
||||
$($(#[$ident_attr:meta])* $tag:ident = $val:expr,)*
|
||||
}
|
||||
@@ -10,17 +13,18 @@ macro_rules! tags {
|
||||
$( #[$enum_attr] )*
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||
#[non_exhaustive]
|
||||
#[repr($ty)]
|
||||
pub enum $name {
|
||||
$($(#[$ident_attr])* $tag,)*
|
||||
$($(#[$ident_attr])* $tag = $val,)*
|
||||
$(
|
||||
#[doc = $unknown_doc]
|
||||
#[$unknown_meta]
|
||||
Unknown($ty),
|
||||
)*
|
||||
}
|
||||
|
||||
impl $name {
|
||||
#[inline(always)]
|
||||
fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
|
||||
const fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
|
||||
match n {
|
||||
$( $val => Ok($name::$tag), )*
|
||||
n => Err(n),
|
||||
@@ -28,10 +32,10 @@ macro_rules! tags {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn __to_inner_type(&self) -> $ty {
|
||||
const fn __to_inner_type(&self) -> $ty {
|
||||
match *self {
|
||||
$( $name::$tag => $val, )*
|
||||
$( $name::Unknown(n) => { $unknown_doc; n }, )*
|
||||
$( $name::Unknown($unknown_doc) => { $unknown_doc }, )*
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,23 +43,28 @@ macro_rules! tags {
|
||||
tags!($name, $ty, $($unknown_doc)*);
|
||||
};
|
||||
// For u16 tags, provide direct inherent primitive conversion methods.
|
||||
($name:tt, u16, $($unknown_doc:literal)*) => {
|
||||
($name:tt, u16, $($unknown_doc:ident)*) => {
|
||||
impl $name {
|
||||
#[inline(always)]
|
||||
pub fn from_u16(val: u16) -> Option<Self> {
|
||||
Self::__from_inner_type(val).ok()
|
||||
pub const fn from_u16(val: u16) -> Option<Self> {
|
||||
match Self::__from_inner_type(val) {
|
||||
Ok(v) => Some(v),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
#[inline(always)]
|
||||
pub fn from_u16_exhaustive(val: u16) -> Self {
|
||||
$unknown_doc;
|
||||
Self::__from_inner_type(val).unwrap_or_else(|_| $name::Unknown(val))
|
||||
pub const fn from_u16_exhaustive($unknown_doc: u16) -> Self {
|
||||
match Self::__from_inner_type($unknown_doc) {
|
||||
Ok(v) => v,
|
||||
Err(_) => $name::Unknown($unknown_doc),
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
#[inline(always)]
|
||||
pub fn to_u16(&self) -> u16 {
|
||||
pub const fn to_u16(&self) -> u16 {
|
||||
Self::__to_inner_type(self)
|
||||
}
|
||||
}
|
||||
@@ -68,7 +77,10 @@ macro_rules! tags {
|
||||
// Note: These tags appear in the order they are mentioned in the TIFF reference
|
||||
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:
|
||||
Artist = 315,
|
||||
// grayscale images PhotometricInterpretation 1 or 3
|
||||
@@ -78,7 +90,6 @@ pub enum Tag(u16) unknown("A private or extension tag") {
|
||||
// palette-color images (PhotometricInterpretation 3)
|
||||
ColorMap = 320, // TODO add support
|
||||
Compression = 259, // TODO add support for 2 and 32773
|
||||
Copyright = 33_432,
|
||||
DateTime = 306,
|
||||
ExtraSamples = 338, // TODO add support
|
||||
FillOrder = 266, // TODO add support
|
||||
@@ -114,23 +125,63 @@ pub enum Tag(u16) unknown("A private or extension tag") {
|
||||
TileLength = 323,
|
||||
TileOffsets = 324,
|
||||
TileByteCounts = 325,
|
||||
SubIfd = 330,
|
||||
// Data Sample Format
|
||||
SampleFormat = 339,
|
||||
SMinSampleValue = 340, // TODO add support
|
||||
SMaxSampleValue = 341, // TODO add support
|
||||
// JPEG
|
||||
JPEGTables = 347,
|
||||
// Subsampling
|
||||
#[doc(alias = "YCbCrSubsampling")]
|
||||
ChromaSubsampling = 530, // TODO add support
|
||||
#[doc(alias = "YCbCrPositioning")]
|
||||
ChromaPositioning = 531, // TODO add support
|
||||
// GeoTIFF
|
||||
ModelPixelScaleTag = 33550, // (SoftDesk)
|
||||
ModelTransformationTag = 34264, // (JPL Carto Group)
|
||||
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)
|
||||
GeoDoubleParamsTag = 34736, // (SPOT)
|
||||
GeoAsciiParamsTag = 34737, // (SPOT)
|
||||
ExifVersion = 0x9000,
|
||||
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! {
|
||||
/// The type of an IFD entry (a 2 byte field).
|
||||
pub enum Type(u16) {
|
||||
@@ -169,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! {
|
||||
/// See [TIFF compression tags](https://www.awaresystems.be/imaging/tiff/tifftags/compression.html)
|
||||
/// for reference.
|
||||
pub enum CompressionMethod(u16) {
|
||||
pub enum CompressionMethod(u16) unknown(
|
||||
/// A custom compression method
|
||||
unknown
|
||||
) {
|
||||
None = 1,
|
||||
Huffman = 2,
|
||||
Fax3 = 3,
|
||||
@@ -184,6 +277,20 @@ pub enum CompressionMethod(u16) {
|
||||
Deflate = 8,
|
||||
OldDeflate = 0x80B2,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +303,41 @@ pub enum PhotometricInterpretation(u16) {
|
||||
TransparencyMask = 4,
|
||||
CMYK = 5,
|
||||
YCbCr = 6,
|
||||
/// Biased Lab with all coefficients stored as unsigned integers.
|
||||
CIELab = 8,
|
||||
IccLab = 9,
|
||||
ItuLab = 10,
|
||||
/// Logarithmic L* color space,
|
||||
///
|
||||
/// see: <https://web.archive.org/web/20250121120358/https://www.anyhere.com/gward/pixformat/tiffluv.html>
|
||||
/// References: <https://doi.org/10.2352/CIC.1998.6.1.art00046>
|
||||
///
|
||||
/// The `uv` coordinates are based on:
|
||||
///
|
||||
/// ```text
|
||||
/// (6*u - 16*v + 12) * (-2x + 12y + 3) = 36
|
||||
///
|
||||
/// u' = 4x / ( -2x + 12y + 3 )
|
||||
/// v' = 9y / ( -2x + 12y + 3 )
|
||||
/// x = 9u' / (6u' - 16v' + 12)
|
||||
/// y = 4v' / (6u' - 16v' + 12)
|
||||
/// ```
|
||||
///
|
||||
/// Since `(-2x + 12y + 3) <= 15` it holds `(6u' - 16v' + 12) >= 36/15` for all XYZ colors
|
||||
/// which makes the back transform to `xyY` well-defined. Also `v' <= 9/15 = 0.6` and also
|
||||
/// by practical means `u' <= 2/3`.
|
||||
///
|
||||
/// The compression format that is supposed to be used with this constrains them as well. It
|
||||
/// encodes each component as a 16-bit unsigned integer scaled and clamped to a quantization
|
||||
/// using 256 out of 410 levels or an index into a table based on a quantization to 177 levels
|
||||
/// each `0.0035` (~1/286) wide and 163 levels of `v` (same quantization width).
|
||||
///
|
||||
/// > Larson, G.W., ``Overcoming Gamut and Dynamic Range Limitations in Digital Images,'' Proceedings of the Sixth Color Imaging Conference, November 1998.
|
||||
CIELogL = 32844,
|
||||
/// Logarithmic L*u'v' color space,
|
||||
///
|
||||
/// See references under [`Self::CIELogL`].
|
||||
CIELogLuv = 32845,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,8 +350,16 @@ pub enum PlanarConfiguration(u16) {
|
||||
|
||||
tags! {
|
||||
pub enum Predictor(u16) {
|
||||
/// No changes were made to the data
|
||||
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,
|
||||
/// Floating-point predictor for IEEE floating-point sample types.
|
||||
///
|
||||
/// This predictor byte-shuffles the data and applies byte-level horizontal differencing.
|
||||
FloatingPoint = 3,
|
||||
}
|
||||
}
|
||||
@@ -225,10 +374,227 @@ pub enum ResolutionUnit(u16) {
|
||||
}
|
||||
|
||||
tags! {
|
||||
pub enum SampleFormat(u16) unknown("An unknown extension sample format") {
|
||||
pub enum SampleFormat(u16) unknown(
|
||||
/// An unknown extension sample format
|
||||
unknown
|
||||
) {
|
||||
Uint = 1,
|
||||
Int = 2,
|
||||
IEEEFP = 3,
|
||||
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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -15,7 +15,9 @@ fn test_big_tiff() {
|
||||
for filename in filenames.iter() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder.next_image().expect("Cannot read image");
|
||||
|
||||
assert_eq!(
|
||||
decoder.dimensions().expect("Cannot get dimensions"),
|
||||
(64, 64)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+325
-34
@@ -4,6 +4,7 @@ use tiff::decoder::{ifd, Decoder, DecodingResult};
|
||||
use tiff::ColorType;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek};
|
||||
use std::path::PathBuf;
|
||||
|
||||
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) {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
|
||||
decoder.next_image().expect("Cannot read image IFD");
|
||||
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
||||
let img_res = decoder.read_image().unwrap();
|
||||
|
||||
@@ -43,14 +46,16 @@ test_image_sum!(test_image_sum_f64, F64, f64);
|
||||
fn test_image_color_type_unsupported(file: &str, expected_type: ColorType) {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder.next_image().expect("Cannot read image IFD");
|
||||
|
||||
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
||||
assert!(match decoder.read_image() {
|
||||
assert!(matches!(
|
||||
decoder.read_image(),
|
||||
Err(tiff::TiffError::UnsupportedError(
|
||||
tiff::TiffUnsupportedError::UnsupportedColorType(_),
|
||||
)) => true,
|
||||
_ => false,
|
||||
});
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -165,14 +170,15 @@ fn test_string_tags() {
|
||||
for filename in filenames.iter() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder.next_image().expect("Cannot read image IFD");
|
||||
let software = decoder.get_tag(tiff::tags::Tag::Software).unwrap();
|
||||
match software {
|
||||
ifd::Value::Ascii(s) => assert_eq!(
|
||||
&s,
|
||||
"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 mut decoder = Decoder::new(file).unwrap();
|
||||
let mut decoder = Decoder::open(file).unwrap();
|
||||
decoder.next_image().expect("Cannot read image IFD");
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
||||
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
||||
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
|
||||
@@ -216,6 +223,11 @@ fn issue_69() {
|
||||
//assert!(img_res.is_ok());
|
||||
//}
|
||||
|
||||
#[test]
|
||||
fn test_tiled_gray_i1() {
|
||||
test_image_sum_u8("tiled-gray-i1.tif", ColorType::Gray(1), 30531);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tiled_rgb_u8() {
|
||||
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]
|
||||
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]
|
||||
fn test_tiled_jpeg_rgb_u8() {
|
||||
test_image_sum_u8("tiled-jpeg-rgb-u8.tif", ColorType::RGB(8), 93031606);
|
||||
@@ -268,7 +329,8 @@ fn test_tiled_incremental() {
|
||||
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder.next_image().expect("Cannot read image IFD");
|
||||
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
||||
|
||||
let tiles = decoder.tile_count().unwrap();
|
||||
@@ -285,6 +347,52 @@ 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};
|
||||
@@ -297,11 +405,12 @@ fn test_div_zero() {
|
||||
178, 178, 178,
|
||||
];
|
||||
|
||||
let err = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
|
||||
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
|
||||
let err = decoder.next_image().unwrap_err();
|
||||
|
||||
match err {
|
||||
TiffError::FormatError(TiffFormatError::StripTileTagConflict) => {}
|
||||
unexpected => panic!("Unexpected error {}", unexpected),
|
||||
unexpected => panic!("Unexpected error {unexpected}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,11 +424,12 @@ fn test_too_many_value_bytes() {
|
||||
0, 89, 89, 89, 89, 89, 89, 89, 89, 96, 1, 20, 89, 89, 89, 89, 18,
|
||||
];
|
||||
|
||||
let error = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
|
||||
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
|
||||
let error = decoder.next_image().unwrap_err();
|
||||
|
||||
match error {
|
||||
tiff::TiffError::LimitsExceeded => {}
|
||||
unexpected => panic!("Unexpected error {}", unexpected),
|
||||
unexpected => panic!("Unexpected error {unexpected}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,9 +443,8 @@ fn fuzzer_testcase5() {
|
||||
178, 178, 178,
|
||||
];
|
||||
|
||||
let mut decoder = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap();
|
||||
|
||||
let _ = decoder.read_image().unwrap_err();
|
||||
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
|
||||
let _ = decoder.next_image().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -348,11 +457,12 @@ fn fuzzer_testcase1() {
|
||||
178,
|
||||
];
|
||||
|
||||
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
|
||||
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
|
||||
let _ = decoder.next_image().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stripped_image_overflow() {
|
||||
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,
|
||||
@@ -361,14 +471,8 @@ fn test_stripped_image_overflow() {
|
||||
178, 178,
|
||||
];
|
||||
|
||||
let mut decoder = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap();
|
||||
|
||||
let err = decoder.read_image().unwrap_err();
|
||||
|
||||
match err {
|
||||
tiff::TiffError::LimitsExceeded => {}
|
||||
unexpected => panic!("Unexpected error {}", unexpected),
|
||||
}
|
||||
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
|
||||
let _ = decoder.next_image().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -380,7 +484,8 @@ fn oom() {
|
||||
0, 0, 0, 40, 0, 0, 0, 23, 1, 4, 0, 1, 0, 0, 0, 178, 48, 178, 178, 178, 178, 162, 178,
|
||||
];
|
||||
|
||||
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
|
||||
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
|
||||
let _ = decoder.next_image().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -392,7 +497,8 @@ fn fuzzer_testcase4() {
|
||||
0, 0, 0, 40, 0, 0, 0, 23, 1, 4, 0, 1, 0, 0, 0, 48, 178, 178, 178, 0, 1, 0, 13, 13,
|
||||
];
|
||||
|
||||
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
|
||||
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
|
||||
let _ = decoder.next_image().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -408,7 +514,8 @@ fn fuzzer_testcase2() {
|
||||
73,
|
||||
];
|
||||
|
||||
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
|
||||
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
|
||||
let _ = decoder.next_image().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -424,7 +531,8 @@ fn invalid_jpeg_tag_2() {
|
||||
0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 36, 73, 73, 0, 42, 36, 36, 36, 36, 0, 0, 8, 0,
|
||||
];
|
||||
|
||||
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
|
||||
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
|
||||
let _ = decoder.next_image().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -437,7 +545,8 @@ fn fuzzer_testcase3() {
|
||||
255, 255,
|
||||
];
|
||||
|
||||
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
|
||||
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
|
||||
let _ = decoder.next_image().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -455,11 +564,12 @@ fn timeout() {
|
||||
0, 0, 73, 73, 42, 0, 8, 0, 0, 0, 0, 0, 32,
|
||||
];
|
||||
|
||||
let error = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
|
||||
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
|
||||
let error = decoder.next_image().unwrap_err();
|
||||
|
||||
match error {
|
||||
TiffError::FormatError(TiffFormatError::CycleInOffsets) => {}
|
||||
e => panic!("Unexpected error {:?}", e),
|
||||
e => panic!("Unexpected error {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,3 +587,184 @@ fn test_predictor_3_rgb_f32() {
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{fs::File, path};
|
||||
|
||||
use tiff::decoder::{Decoder, DecodingResult};
|
||||
|
||||
use libtest_mimic::{Arguments, Failed, Trial};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
const REFERENCE_DIR: &str = "tests/libtiffpic";
|
||||
|
||||
// `find test/libtiffpic -type f | sort`
|
||||
const FILES: &[(&str, bool)] = &[
|
||||
("tests/libtiffpic/caspian.tif", true),
|
||||
("tests/libtiffpic/cramps.tif", true),
|
||||
("tests/libtiffpic/cramps-tile.tif", true),
|
||||
("tests/libtiffpic/CVS/Entries", false),
|
||||
("tests/libtiffpic/CVS/Repository", false),
|
||||
("tests/libtiffpic/CVS/Root", false),
|
||||
("tests/libtiffpic/depth/CVS/Entries", false),
|
||||
("tests/libtiffpic/depth/CVS/Repository", false),
|
||||
("tests/libtiffpic/depth/CVS/Root", false),
|
||||
("tests/libtiffpic/depth/flower-minisblack-02.tif", true),
|
||||
("tests/libtiffpic/depth/flower-minisblack-04.tif", true),
|
||||
("tests/libtiffpic/depth/flower-minisblack-06.tif", true),
|
||||
("tests/libtiffpic/depth/flower-minisblack-08.tif", true),
|
||||
("tests/libtiffpic/depth/flower-minisblack-10.tif", true),
|
||||
("tests/libtiffpic/depth/flower-minisblack-12.tif", true),
|
||||
("tests/libtiffpic/depth/flower-minisblack-14.tif", true),
|
||||
("tests/libtiffpic/depth/flower-minisblack-16.tif", true),
|
||||
("tests/libtiffpic/depth/flower-minisblack-24.tif", true),
|
||||
("tests/libtiffpic/depth/flower-minisblack-32.tif", true),
|
||||
("tests/libtiffpic/depth/flower-palette-02.tif", true),
|
||||
("tests/libtiffpic/depth/flower-palette-04.tif", true),
|
||||
("tests/libtiffpic/depth/flower-palette-08.tif", true),
|
||||
("tests/libtiffpic/depth/flower-palette-16.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-contig-02.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-contig-04.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-contig-08.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-contig-10.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-contig-12.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-contig-14.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-contig-16.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-contig-24.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-contig-32.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-planar-02.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-planar-04.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-planar-08.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-planar-10.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-planar-12.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-planar-14.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-planar-16.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-planar-24.tif", true),
|
||||
("tests/libtiffpic/depth/flower-rgb-planar-32.tif", true),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-separated-contig-08.tif",
|
||||
true,
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-separated-contig-16.tif",
|
||||
true,
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-separated-planar-08.tif",
|
||||
true,
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-separated-planar-16.tif",
|
||||
true,
|
||||
),
|
||||
("tests/libtiffpic/depth/genimages", false),
|
||||
("tests/libtiffpic/depth/README.txt", false),
|
||||
("tests/libtiffpic/depth/summary.txt", false),
|
||||
("tests/libtiffpic/dscf0013.tif", true),
|
||||
("tests/libtiffpic/fax2d.tif", true),
|
||||
// No support for direct CCITT Group 3 1D format
|
||||
("tests/libtiffpic/g3test.g3", false),
|
||||
("tests/libtiffpic/g3test.tif", true),
|
||||
("tests/libtiffpic/jello.tif", true),
|
||||
("tests/libtiffpic/jim___ah.tif", true),
|
||||
("tests/libtiffpic/jim___cg.tif", true),
|
||||
("tests/libtiffpic/jim___dg.tif", true),
|
||||
("tests/libtiffpic/jim___gg.tif", true),
|
||||
("tests/libtiffpic/ladoga.tif", true),
|
||||
("tests/libtiffpic/off_l16.tif", true),
|
||||
("tests/libtiffpic/off_luv24.tif", true),
|
||||
("tests/libtiffpic/off_luv32.tif", true),
|
||||
("tests/libtiffpic/oxford.tif", true),
|
||||
("tests/libtiffpic/pc260001.tif", true),
|
||||
("tests/libtiffpic/quad-jpeg.tif", true),
|
||||
("tests/libtiffpic/quad-lzw.tif", true),
|
||||
("tests/libtiffpic/quad-tile.tif", true),
|
||||
("tests/libtiffpic/README", false),
|
||||
("tests/libtiffpic/smallliz.tif", true),
|
||||
("tests/libtiffpic/strike.tif", true),
|
||||
("tests/libtiffpic/text.tif", true),
|
||||
("tests/libtiffpic/ycbcr-cat.tif", true),
|
||||
("tests/libtiffpic/zackthecat.tif", true),
|
||||
];
|
||||
|
||||
fn main() {
|
||||
let mut candidates: HashMap<_, _> = FILES.iter().copied().collect();
|
||||
let mut trials = Vec::new();
|
||||
|
||||
for entry in WalkDir::new(REFERENCE_DIR) {
|
||||
let entry = entry.unwrap();
|
||||
|
||||
if !entry.file_type().is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(path) = entry.path().to_str() else {
|
||||
continue;
|
||||
};
|
||||
let path = path.replace(std::path::MAIN_SEPARATOR, "/"); // always use UNIX path separators
|
||||
|
||||
// Mark this path as consumed in either case by removing it.
|
||||
let Some(should_decode) = candidates.remove(path.as_str()) else {
|
||||
panic!("File {} not in reference list", path);
|
||||
};
|
||||
|
||||
// Check if we should turn it into a test case.
|
||||
if !should_decode {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = path.to_string();
|
||||
trials.push(Trial::test(
|
||||
path.to_string(),
|
||||
move || -> Result<(), Failed> {
|
||||
decode_libtiffpic(&path)
|
||||
.map_err(|e| Failed::from(format_args!("Decoding failed for {}: {}", path, e)))
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if !candidates.is_empty() {
|
||||
for (path, _) in candidates {
|
||||
eprintln!("Reference file {} not found in directory walk", path);
|
||||
}
|
||||
|
||||
panic!("Some reference files were not found");
|
||||
}
|
||||
|
||||
let args = Arguments::from_args();
|
||||
libtest_mimic::run(&args, trials).exit();
|
||||
}
|
||||
|
||||
fn decode_libtiffpic(path: &str) -> Result<(), tiff::TiffError> {
|
||||
let path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path);
|
||||
|
||||
let img_file = File::open(path)?;
|
||||
let mut decoder = Decoder::open(img_file)?;
|
||||
|
||||
let mut buffer = DecodingResult::U8(vec![]);
|
||||
while decoder.more_images() {
|
||||
decoder.next_image()?;
|
||||
let _layout = decoder.read_image_to_buffer(&mut buffer);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+335
-19
@@ -1,7 +1,7 @@
|
||||
extern crate tiff;
|
||||
|
||||
use tiff::decoder::{ifd, Decoder, DecodingResult};
|
||||
use tiff::encoder::{colortype, Ifd, Ifd8, SRational, TiffEncoder};
|
||||
use tiff::encoder::{colortype, Ifd, Ifd8, Predictor, SRational, TiffEncoder};
|
||||
use tiff::tags::Tag;
|
||||
use tiff::ColorType;
|
||||
|
||||
@@ -33,14 +33,60 @@ fn encode_decode() {
|
||||
}
|
||||
{
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut decoder = Decoder::new(&mut file).unwrap();
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
||||
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
||||
|
||||
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!(
|
||||
decoder.get_tag(Tag::Artist).unwrap(),
|
||||
ifd::Value::Ascii("Image-tiff".into())
|
||||
);
|
||||
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
|
||||
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let tiff::decoder::DecodingResult::U8(img_res) = buffer {
|
||||
assert_eq!(image_data, img_res);
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
@@ -72,14 +118,59 @@ fn encode_decode_big() {
|
||||
}
|
||||
{
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut decoder = Decoder::new(&mut file).unwrap();
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
||||
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
||||
|
||||
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!(
|
||||
decoder.get_tag(Tag::Artist).unwrap(),
|
||||
ifd::Value::Ascii("Image-tiff".into())
|
||||
);
|
||||
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let DecodingResult::U8(img_res) = buffer {
|
||||
assert_eq!(image_data, img_res);
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
@@ -120,8 +211,10 @@ fn test_encode_ifd() {
|
||||
|
||||
// Rewind the cursor for reading
|
||||
data.set_position(0);
|
||||
|
||||
{
|
||||
let mut decoder = Decoder::new(&mut data).unwrap();
|
||||
let mut decoder = Decoder::open(&mut data).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.assert_tag_u32(65000), 42);
|
||||
assert_eq!(decoder.assert_tag_u32_vec(65000), [42]);
|
||||
@@ -153,16 +246,22 @@ const TEST_IMAGE_DIR: &str = "./tests/images/";
|
||||
|
||||
macro_rules! test_roundtrip {
|
||||
($name:ident, $buffer:ident, $buffer_ty:ty) => {
|
||||
#[track_caller]
|
||||
fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
|
||||
file: &str,
|
||||
expected_type: ColorType,
|
||||
) {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
||||
|
||||
let image_data = match decoder.read_image().unwrap() {
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
|
||||
let image_data = match buffer {
|
||||
DecodingResult::$buffer(res) => res,
|
||||
_ => panic!("Wrong data type"),
|
||||
};
|
||||
@@ -176,9 +275,12 @@ macro_rules! test_roundtrip {
|
||||
}
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
{
|
||||
let mut decoder = Decoder::new(&mut file).unwrap();
|
||||
if let DecodingResult::$buffer(img_res) = decoder.read_image().unwrap() {
|
||||
assert_eq!(image_data, img_res);
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let DecodingResult::$buffer(img_res) = buffer {
|
||||
assert_eq!(*image_data, img_res);
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
}
|
||||
@@ -188,7 +290,9 @@ macro_rules! test_roundtrip {
|
||||
}
|
||||
|
||||
test_roundtrip!(test_u8_roundtrip, U8, u8);
|
||||
test_roundtrip!(test_i8_roundtrip, I8, i8);
|
||||
test_roundtrip!(test_u16_roundtrip, U16, u16);
|
||||
test_roundtrip!(test_i16_roundtrip, I16, i16);
|
||||
test_roundtrip!(test_u32_roundtrip, U32, u32);
|
||||
test_roundtrip!(test_u64_roundtrip, U64, u64);
|
||||
test_roundtrip!(test_f32_roundtrip, F32, f32);
|
||||
@@ -199,6 +303,11 @@ fn test_gray_u8_roundtrip() {
|
||||
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]
|
||||
fn test_rgb_u8_roundtrip() {
|
||||
test_u8_roundtrip::<colortype::RGB8>("rgb-3c-8b.tiff", ColorType::RGB(8));
|
||||
@@ -209,11 +318,21 @@ fn test_cmyk_u8_roundtrip() {
|
||||
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]
|
||||
fn test_gray_u16_roundtrip() {
|
||||
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]
|
||||
fn test_rgb_u16_roundtrip() {
|
||||
test_u16_roundtrip::<colortype::RGB16>("rgb-3c-16b.tiff", ColorType::RGB(16));
|
||||
@@ -269,6 +388,144 @@ fn test_ycbcr_u8_roundtrip() {
|
||||
test_u8_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
|
||||
}
|
||||
|
||||
/// Test that encoding with the floating-point predictor works correctly
|
||||
/// and can be decoded back to the original data.
|
||||
#[test]
|
||||
fn test_gray_f32_floating_point_predictor() {
|
||||
// Generate test data: a gradient of f32 values
|
||||
let width = 32u32;
|
||||
let height = 32u32;
|
||||
let mut image_data: Vec<f32> = Vec::with_capacity((width * height) as usize);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
// Create a gradient pattern
|
||||
image_data.push((x as f32 + y as f32 * 0.1) / 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
let mut file = Cursor::new(Vec::new());
|
||||
{
|
||||
let mut tiff = TiffEncoder::new(&mut file)
|
||||
.unwrap()
|
||||
.with_predictor(Predictor::FloatingPoint);
|
||||
tiff.write_image::<colortype::Gray32Float>(width, height, &image_data)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
{
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(32));
|
||||
|
||||
let mut buffer = DecodingResult::F32(vec![]);
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let DecodingResult::F32(img_res) = buffer {
|
||||
assert_eq!(image_data.len(), img_res.len());
|
||||
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
|
||||
assert_eq!(
|
||||
*orig, *decoded,
|
||||
"Mismatch at value: orig={}, decoded={}",
|
||||
orig, decoded
|
||||
);
|
||||
}
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test floating-point predictor with RGB f32 data
|
||||
#[test]
|
||||
fn test_rgb_f32_floating_point_predictor() {
|
||||
let width = 16u32;
|
||||
let height = 16u32;
|
||||
let mut image_data: Vec<f32> = Vec::with_capacity((width * height * 3) as usize);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
// RGB pattern
|
||||
image_data.push(x as f32 / width as f32); // R
|
||||
image_data.push(y as f32 / height as f32); // G
|
||||
image_data.push((x + y) as f32 / (width + height) as f32); // B
|
||||
}
|
||||
}
|
||||
|
||||
let mut file = Cursor::new(Vec::new());
|
||||
{
|
||||
let mut tiff = TiffEncoder::new(&mut file)
|
||||
.unwrap()
|
||||
.with_predictor(Predictor::FloatingPoint);
|
||||
tiff.write_image::<colortype::RGB32Float>(width, height, &image_data)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
{
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(32));
|
||||
|
||||
let mut buffer = DecodingResult::F32(vec![]);
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let DecodingResult::F32(img_res) = buffer {
|
||||
assert_eq!(image_data.len(), img_res.len());
|
||||
for (i, (orig, decoded)) in image_data.iter().zip(img_res.iter()).enumerate() {
|
||||
assert_eq!(
|
||||
*orig, *decoded,
|
||||
"Mismatch at index {}: orig={}, decoded={}",
|
||||
i, orig, decoded
|
||||
);
|
||||
}
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test floating-point predictor with f64 data
|
||||
#[test]
|
||||
fn test_gray_f64_floating_point_predictor() {
|
||||
let width = 16u32;
|
||||
let height = 16u32;
|
||||
let mut image_data: Vec<f64> = Vec::with_capacity((width * height) as usize);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
image_data.push((x as f64 + y as f64 * 0.1) / 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
let mut file = Cursor::new(Vec::new());
|
||||
{
|
||||
let mut tiff = TiffEncoder::new(&mut file)
|
||||
.unwrap()
|
||||
.with_predictor(Predictor::FloatingPoint);
|
||||
tiff.write_image::<colortype::Gray64Float>(width, height, &image_data)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
{
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(64));
|
||||
|
||||
let mut buffer = DecodingResult::F64(vec![]);
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let DecodingResult::F64(img_res) = buffer {
|
||||
assert_eq!(image_data.len(), img_res.len());
|
||||
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
|
||||
assert_eq!(
|
||||
*orig, *decoded,
|
||||
"Mismatch: orig={}, decoded={}",
|
||||
orig, decoded
|
||||
);
|
||||
}
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait AssertDecode {
|
||||
fn assert_tag_u32(&mut self, tag: u16) -> u32;
|
||||
fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32>;
|
||||
@@ -346,7 +603,8 @@ fn test_multiple_byte() {
|
||||
|
||||
data.set_position(0);
|
||||
{
|
||||
let mut decoder = Decoder::new(&mut data).unwrap();
|
||||
let mut decoder = Decoder::open(&mut data).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.assert_tag_u32_vec(65000), [1]);
|
||||
assert_eq!(decoder.assert_tag_u32_vec(65001), [1, 2]);
|
||||
@@ -427,7 +685,8 @@ fn test_signed() {
|
||||
//Rewind the cursor for reading
|
||||
data.set_position(0);
|
||||
{
|
||||
let mut decoder = Decoder::new(&mut data).unwrap();
|
||||
let mut decoder = Decoder::open(&mut data).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.assert_tag_i32(65000), -1);
|
||||
assert_eq!(decoder.assert_tag_i32_vec(65001), [-1]);
|
||||
@@ -479,13 +738,14 @@ fn test_multipage_image() {
|
||||
img_file.seek(SeekFrom::Start(0)).unwrap();
|
||||
|
||||
{
|
||||
let mut img_decoder = Decoder::new(&mut img_file).unwrap();
|
||||
let mut decoder = Decoder::open(&mut img_file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
// check the dimensions of the image in the first page
|
||||
assert_eq!(img_decoder.dimensions().unwrap(), (2, 2));
|
||||
img_decoder.next_image().unwrap();
|
||||
assert_eq!(decoder.dimensions().unwrap(), (2, 2));
|
||||
decoder.next_image().unwrap();
|
||||
// check the dimensions of the image in the second page
|
||||
assert_eq!(img_decoder.dimensions().unwrap(), (3, 3));
|
||||
assert_eq!(decoder.dimensions().unwrap(), (3, 3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,7 +774,8 @@ fn test_rows_per_strip() {
|
||||
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
{
|
||||
let mut decoder = Decoder::new(&mut file).unwrap();
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
assert_eq!(decoder.get_tag_u64(Tag::RowsPerStrip).unwrap(), 2);
|
||||
assert_eq!(decoder.strip_count().unwrap(), 50);
|
||||
|
||||
@@ -522,8 +783,63 @@ fn test_rows_per_strip() {
|
||||
let img2 = [i; 2 * 100];
|
||||
match decoder.read_chunk(i as u32).unwrap() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ use tiff::{
|
||||
decoder::{Decoder, DecodingResult},
|
||||
encoder::{
|
||||
colortype::{self, ColorType},
|
||||
compression::*,
|
||||
TiffEncoder, TiffValue,
|
||||
Compression, TiffEncoder, TiffValue,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -18,15 +17,12 @@ trait TestImage<const NUM_CHANNELS: usize>: From<Vec<<Self::Color as 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<C: Compression, W: Write + Seek>(
|
||||
&self,
|
||||
encoder: &mut TiffEncoder<W>,
|
||||
compression: C,
|
||||
) where
|
||||
fn compress<W: Write + Seek>(&self, encoder: &mut TiffEncoder<W>)
|
||||
where
|
||||
[<Self::Color as ColorType>::Inner]: TiffValue,
|
||||
{
|
||||
let image = encoder
|
||||
.new_image_with_compression::<Self::Color, C>(Self::WIDTH, Self::HEIGHT, compression)
|
||||
.new_image::<Self::Color>(Self::WIDTH, Self::HEIGHT)
|
||||
.unwrap();
|
||||
image.write_data(self.reference_data()).unwrap();
|
||||
}
|
||||
@@ -94,7 +90,7 @@ impl TestImage<1> for TestImageGrayscale {
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_decode_with_compression<C: Compression + Clone>(compression: C) {
|
||||
fn encode_decode_with_compression(compression: Compression) {
|
||||
let mut data = Cursor::new(Vec::new());
|
||||
|
||||
let image_rgb = TestImageColor::generate();
|
||||
@@ -103,21 +99,24 @@ fn encode_decode_with_compression<C: Compression + Clone>(compression: C) {
|
||||
// Encode tiff with compression
|
||||
{
|
||||
// Create a multipage image with 2 images
|
||||
let mut encoder = TiffEncoder::new(&mut data).unwrap();
|
||||
image_rgb.compress(&mut encoder, compression.clone());
|
||||
image_grayscale.compress(&mut encoder, compression);
|
||||
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::new(data).unwrap();
|
||||
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),
|
||||
unexpected => panic!("Descoding RGB failed: {unexpected:?}"),
|
||||
},
|
||||
image_rgb.reference_data()
|
||||
);
|
||||
@@ -127,7 +126,7 @@ fn encode_decode_with_compression<C: Compression + Clone>(compression: C) {
|
||||
assert_eq!(
|
||||
match decoder.read_image() {
|
||||
Ok(DecodingResult::U8(image_data)) => image_data,
|
||||
unexpected => panic!("Decoding grayscale failed: {:?}", unexpected),
|
||||
unexpected => panic!("Decoding grayscale failed: {unexpected:?}"),
|
||||
},
|
||||
image_grayscale.reference_data()
|
||||
);
|
||||
@@ -136,22 +135,24 @@ fn encode_decode_with_compression<C: Compression + Clone>(compression: C) {
|
||||
|
||||
#[test]
|
||||
fn encode_decode_without_compression() {
|
||||
encode_decode_with_compression(Uncompressed::default());
|
||||
encode_decode_with_compression(Compression::Uncompressed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "lzw")]
|
||||
fn encode_decode_with_lzw() {
|
||||
encode_decode_with_compression(Lzw::default());
|
||||
encode_decode_with_compression(Compression::Lzw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "deflate")]
|
||||
fn encode_decode_with_deflate() {
|
||||
encode_decode_with_compression(Deflate::with_level(DeflateLevel::Fast));
|
||||
encode_decode_with_compression(Deflate::with_level(DeflateLevel::Balanced));
|
||||
encode_decode_with_compression(Deflate::with_level(DeflateLevel::Best));
|
||||
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(Packbits::default());
|
||||
encode_decode_with_compression(Compression::Packbits);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
+2
-1
@@ -13,7 +13,8 @@ fn test_directory<F: Fn(File) -> bool>(path: &str, f: F) {
|
||||
}
|
||||
|
||||
fn decode_tiff(file: File) -> TiffResult<()> {
|
||||
let mut decoder = Decoder::new(file)?;
|
||||
let mut decoder = Decoder::open(file)?;
|
||||
decoder.next_image()?;
|
||||
decoder.read_image()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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.
@@ -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/
|
||||
@@ -0,0 +1 @@
|
||||
libtiffpic
|
||||
@@ -0,0 +1 @@
|
||||
:pserver:dron@cvs.maptools.org:/cvs/maptools/cvsroot
|
||||
@@ -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.
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
libtiffpic/depth
|
||||
@@ -0,0 +1 @@
|
||||
:pserver:dron@cvs.maptools.org:/cvs/maptools/cvsroot
|
||||
@@ -0,0 +1,9 @@
|
||||
These sample TIFF image files are prepared by Bob Friesenhahn
|
||||
<bfriesen@simple.dallas.tx.us> using a development version of
|
||||
GraphicsMagick 1.2.
|
||||
|
||||
See the file summary.txt for a description of the images.
|
||||
|
||||
These files are hereby placed in the public domain.
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user