Compare commits
174
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -1,22 +1,22 @@
|
||||
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.74.0", stable, beta, nightly]
|
||||
rust: ["1.85.0", stable, beta, nightly]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
if: ${{ matrix.rust == '1.74.0' }}
|
||||
if: ${{ matrix.rust == '1.85.0' }}
|
||||
- name: Generate Cargo.lock with minimal-version dependencies
|
||||
if: ${{ matrix.rust == '1.74.0' }}
|
||||
if: ${{ matrix.rust == '1.85.0' }}
|
||||
run: cargo -Zminimal-versions generate-lockfile
|
||||
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: build
|
||||
run: cargo build -v
|
||||
- name: test
|
||||
if: ${{ matrix.rust != '1.74.0' }}
|
||||
if: ${{ matrix.rust != '1.85.0' }}
|
||||
run: cargo test -v && cargo doc -v
|
||||
|
||||
features:
|
||||
@@ -45,6 +45,35 @@ jobs:
|
||||
- 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:
|
||||
|
||||
+148
@@ -1,3 +1,151 @@
|
||||
# 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:
|
||||
|
||||
+13
-6
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "tiff"
|
||||
version = "0.10.0"
|
||||
version = "0.11.3"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
# note: when changed, also update test runner in `.github/workflows/rust.yml`
|
||||
rust-version = "1.74.0"
|
||||
rust-version = "1.85.0"
|
||||
|
||||
license = "MIT"
|
||||
description = "TIFF decoding and encoding library in pure Rust"
|
||||
@@ -14,27 +14,34 @@ authors = ["The image-rs Developers"]
|
||||
repository = "https://github.com/image-rs/image-tiff"
|
||||
categories = ["multimedia::images", "multimedia::encoding"]
|
||||
|
||||
exclude = ["tests/images/*", "tests/fuzz_images/*"]
|
||||
exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
|
||||
|
||||
[dependencies]
|
||||
half = { version = "2.4.1" }
|
||||
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.0", optional = true }
|
||||
weezl = { version = "0.1.10", optional = true }
|
||||
zstd = { version = "0.13", optional = true }
|
||||
zune-jpeg = { version = "0.4.17", 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"
|
||||
|
||||
[features]
|
||||
default = ["deflate", "jpeg", "lzw"]
|
||||
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]]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -36,6 +36,7 @@ This table lists photometric interpretations and sample formats which are suppor
|
||||
| LZW | ✓ | ✓ |
|
||||
| Deflate | ✓ | ✓ |
|
||||
| PackBits | ✓ | ✓ |
|
||||
| Fax4 | ✓ | not yet |
|
||||
| JPEG | ✓ | not yet |
|
||||
| ZSTD | ✓ | not yet |
|
||||
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingBuffer, DecodingResult};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Some(image) = std::env::args_os().nth(1) else {
|
||||
eprintln!("Usage: decode FILE");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let file = std::fs::File::open(image)?;
|
||||
let io = std::io::BufReader::new(file);
|
||||
let mut reader = Decoder::new(io)?;
|
||||
|
||||
let mut data = DecodingResult::I8(vec![]);
|
||||
|
||||
for i in 0u32.. {
|
||||
let colortype = reader.colortype()?;
|
||||
let dimensions = reader.dimensions()?;
|
||||
let layout = reader.read_image_to_buffer(&mut data)?;
|
||||
|
||||
debug_planes(i, &mut data, &layout, &colortype, dimensions);
|
||||
|
||||
if !reader.more_images() {
|
||||
break;
|
||||
}
|
||||
|
||||
reader.next_image()?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn debug_planes(
|
||||
index: u32,
|
||||
data: &mut DecodingResult,
|
||||
layout: &BufferLayoutPreference,
|
||||
colortype: &tiff::ColorType,
|
||||
(width, height): (u32, u32),
|
||||
) {
|
||||
let (depth, mut tupltype) = match colortype {
|
||||
// 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,124 @@
|
||||
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::new(io)?;
|
||||
|
||||
loop {
|
||||
ls_dir(&mut reader);
|
||||
|
||||
if !reader.more_images() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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.image_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,6 +10,9 @@
|
||||
//! 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;
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
use crate::{tags::IfdPointer, TiffError, TiffFormatError};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// The IFD structure of a TIFF file should be limited to a forest of entries, and a tree when we
|
||||
/// only consider having a primary IFD and its children. There is up to one primary child of a node
|
||||
/// that is the `next` entry in an IFD itself. Since we offer iteration over this chain we want to
|
||||
/// detect when a new edge would introduce a cycle, such that callers can be guaranteed to
|
||||
/// terminate iteration at some point even in malicious images.
|
||||
///
|
||||
/// However, we are not necessarily visiting the IFDs in their topological order.
|
||||
///
|
||||
/// Since we only consider one child, we run union find to assign each pointer to a known chain.
|
||||
/// Then, when we insert a new edge we check if they belong to the same component.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct IfdCycles {
|
||||
/// The root of each component union.
|
||||
component_union: HashMap<ComponentId, ComponentId>,
|
||||
links: HashMap<IfdPointer, u64>,
|
||||
chains: HashMap<IfdPointer, ComponentId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
struct ComponentId(u64);
|
||||
|
||||
impl IfdCycles {
|
||||
pub fn new() -> Self {
|
||||
IfdCycles::default()
|
||||
}
|
||||
|
||||
pub fn insert_next(
|
||||
&mut self,
|
||||
from: IfdPointer,
|
||||
to: Option<IfdPointer>,
|
||||
) -> Result<bool, TiffError> {
|
||||
let to_offset = to.map_or(0, |p| p.0);
|
||||
|
||||
// For some reason we got
|
||||
match self.links.get(&from) {
|
||||
Some(existing) if *existing == to_offset => return Ok(false),
|
||||
// We got here twice with two different reads of the same IFD. That is .. unusual and
|
||||
// we interpret it as a cycle.
|
||||
Some(_) => return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets)),
|
||||
None => self.links.insert(from, to_offset),
|
||||
};
|
||||
|
||||
self.ensure_node(from);
|
||||
|
||||
if let Some(to) = to {
|
||||
self.ensure_node(to);
|
||||
|
||||
let parent = self.nominal_component(from);
|
||||
let child = self.nominal_component(to);
|
||||
|
||||
if parent == child {
|
||||
return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets));
|
||||
}
|
||||
|
||||
self.component_union.insert(child, parent);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn ensure_node(&mut self, ifd: IfdPointer) {
|
||||
self.chains.entry(ifd).or_insert_with(|| {
|
||||
let id = ComponentId(self.component_union.len() as u64);
|
||||
self.component_union.insert(id, id);
|
||||
id
|
||||
});
|
||||
}
|
||||
|
||||
fn nominal_component(&mut self, node: IfdPointer) -> ComponentId {
|
||||
let id = self.chains[&node];
|
||||
|
||||
let nomimal = {
|
||||
let mut iter = id;
|
||||
|
||||
loop {
|
||||
let parent = self.component_union[&iter];
|
||||
|
||||
if parent == iter {
|
||||
break parent;
|
||||
}
|
||||
|
||||
iter = parent;
|
||||
}
|
||||
};
|
||||
|
||||
if nomimal != id {
|
||||
// Compress.
|
||||
let mut iter = id;
|
||||
|
||||
loop {
|
||||
let parent = self.component_union[&iter];
|
||||
|
||||
if parent == iter {
|
||||
break;
|
||||
}
|
||||
|
||||
self.component_union.insert(iter, nomimal);
|
||||
iter = parent;
|
||||
}
|
||||
}
|
||||
|
||||
nomimal
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycles_are_detected() {
|
||||
let mut cycles = IfdCycles::new();
|
||||
|
||||
cycles
|
||||
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x800)))
|
||||
.expect("non-existing link is valid");
|
||||
|
||||
cycles
|
||||
.insert_next(IfdPointer(0x800), Some(IfdPointer(0x20)))
|
||||
.expect_err("cycle must be detected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflective_cycle() {
|
||||
let mut cycles = IfdCycles::new();
|
||||
|
||||
cycles
|
||||
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x20)))
|
||||
.expect_err("self-referential cycle must be detected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_cycle() {
|
||||
let mut cycles = IfdCycles::new();
|
||||
|
||||
cycles
|
||||
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x40)))
|
||||
.expect("non-existing link is valid");
|
||||
|
||||
cycles
|
||||
.insert_next(IfdPointer(0x60), Some(IfdPointer(0x80)))
|
||||
.expect("non-existing link is valid");
|
||||
cycles
|
||||
.insert_next(IfdPointer(0x80), Some(IfdPointer(0x20)))
|
||||
.expect("non-existing link is valid");
|
||||
|
||||
cycles
|
||||
.insert_next(IfdPointer(0x40), Some(IfdPointer(0x60)))
|
||||
.expect_err("non-existing link is valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_cycle() {
|
||||
let mut cycles = IfdCycles::new();
|
||||
|
||||
cycles
|
||||
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x40)))
|
||||
.expect("non-existing link is valid");
|
||||
|
||||
cycles
|
||||
.insert_next(IfdPointer(0x60), Some(IfdPointer(0x80)))
|
||||
.expect("non-existing link is valid");
|
||||
cycles
|
||||
.insert_next(IfdPointer(0x80), Some(IfdPointer(0x20)))
|
||||
.expect("non-existing link is valid");
|
||||
|
||||
cycles
|
||||
.insert_next(IfdPointer(0x40), Some(IfdPointer(0x80)))
|
||||
.expect_err("non-existing link is valid");
|
||||
}
|
||||
+414
-132
@@ -6,12 +6,12 @@ use std::mem;
|
||||
use std::str;
|
||||
|
||||
use super::stream::{ByteOrder, EndianReader};
|
||||
use crate::tags::{Tag, Type};
|
||||
use crate::tags::{IfdPointer, Tag, Type, ValueBuffer};
|
||||
use crate::{TiffError, TiffFormatError, TiffResult};
|
||||
|
||||
use self::Value::{
|
||||
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, RationalBig, SRational, SRationalBig,
|
||||
Short, Signed, SignedBig, SignedByte, SignedShort, Unsigned, UnsignedBig,
|
||||
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, SRational, Short, Signed, SignedBig,
|
||||
SignedByte, SignedShort, Unsigned, UnsignedBig,
|
||||
};
|
||||
|
||||
#[allow(unused_qualifications)]
|
||||
@@ -30,8 +30,16 @@ pub enum Value {
|
||||
Double(f64),
|
||||
List(Vec<Value>),
|
||||
Rational(u32, u32),
|
||||
#[deprecated(
|
||||
note = "Not implemented in BigTIFF with a standard tag value",
|
||||
since = "0.11.1"
|
||||
)]
|
||||
RationalBig(u64, u64),
|
||||
SRational(i32, i32),
|
||||
#[deprecated(
|
||||
note = "Not implemented in BigTIFF with a standard tag value",
|
||||
since = "0.11.1"
|
||||
)]
|
||||
SRationalBig(i64, i64),
|
||||
Ascii(String),
|
||||
Ifd(u32),
|
||||
@@ -130,6 +138,25 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn this value into an `IfdPointer`.
|
||||
///
|
||||
/// Notice that this does not take an argument, a 64-bit IFD is always allowed. If the
|
||||
/// difference is crucial and you do not want to be permissive you're expected to filter this
|
||||
/// out before.
|
||||
///
|
||||
/// For compatibility the smaller sized tags should always be allowed i.e. you might use a
|
||||
/// non-bigtiff's directory and its tag types and move it straight to a bigtiff. For instance
|
||||
/// the SubIFD tag is defined as `LONG or IFD`:
|
||||
///
|
||||
/// <https://web.archive.org/web/20181105221012/https://www.awaresystems.be/imaging/tiff/tifftags/subifds.html>
|
||||
pub fn into_ifd_pointer(self) -> TiffResult<IfdPointer> {
|
||||
match self {
|
||||
Unsigned(val) | Ifd(val) => Ok(IfdPointer(val.into())),
|
||||
IfdBig(val) => Ok(IfdPointer(val)),
|
||||
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_string(self) -> TiffResult<String> {
|
||||
match self {
|
||||
Ascii(val) => Ok(val),
|
||||
@@ -151,7 +178,8 @@ impl Value {
|
||||
Unsigned(val) => Ok(vec![val]),
|
||||
UnsignedBig(val) => Ok(vec![u32::try_from(val)?]),
|
||||
Rational(numerator, denominator) => Ok(vec![numerator, denominator]),
|
||||
RationalBig(numerator, denominator) => {
|
||||
#[expect(deprecated)]
|
||||
Value::RationalBig(numerator, denominator) => {
|
||||
Ok(vec![u32::try_from(numerator)?, u32::try_from(denominator)?])
|
||||
}
|
||||
Ifd(val) => Ok(vec![val]),
|
||||
@@ -201,7 +229,8 @@ impl Value {
|
||||
new_vec.push(numerator);
|
||||
new_vec.push(denominator);
|
||||
}
|
||||
SRationalBig(numerator, denominator) => {
|
||||
#[expect(deprecated)]
|
||||
Value::SRationalBig(numerator, denominator) => {
|
||||
new_vec.push(i32::try_from(numerator)?);
|
||||
new_vec.push(i32::try_from(denominator)?);
|
||||
}
|
||||
@@ -215,7 +244,8 @@ impl Value {
|
||||
Signed(val) => Ok(vec![val]),
|
||||
SignedBig(val) => Ok(vec![i32::try_from(val)?]),
|
||||
SRational(numerator, denominator) => Ok(vec![numerator, denominator]),
|
||||
SRationalBig(numerator, denominator) => {
|
||||
#[expect(deprecated)]
|
||||
Value::SRationalBig(numerator, denominator) => {
|
||||
Ok(vec![i32::try_from(numerator)?, i32::try_from(denominator)?])
|
||||
}
|
||||
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
|
||||
@@ -264,7 +294,8 @@ impl Value {
|
||||
Unsigned(val) => Ok(vec![val.into()]),
|
||||
UnsignedBig(val) => Ok(vec![val]),
|
||||
Rational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
|
||||
RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
|
||||
#[expect(deprecated)]
|
||||
Value::RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
|
||||
Ifd(val) => Ok(vec![val.into()]),
|
||||
IfdBig(val) => Ok(vec![val]),
|
||||
Ascii(val) => Ok(val.chars().map(u32::from).map(u64::from).collect()),
|
||||
@@ -282,7 +313,8 @@ impl Value {
|
||||
new_vec.push(numerator.into());
|
||||
new_vec.push(denominator.into());
|
||||
}
|
||||
SRationalBig(numerator, denominator) => {
|
||||
#[expect(deprecated)]
|
||||
Value::SRationalBig(numerator, denominator) => {
|
||||
new_vec.push(numerator);
|
||||
new_vec.push(denominator);
|
||||
}
|
||||
@@ -296,12 +328,30 @@ impl Value {
|
||||
Signed(val) => Ok(vec![val.into()]),
|
||||
SignedBig(val) => Ok(vec![val]),
|
||||
SRational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
|
||||
SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
|
||||
#[expect(deprecated)]
|
||||
Value::SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
|
||||
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_ifd_vec(self) -> TiffResult<Vec<IfdPointer>> {
|
||||
let vec = match self {
|
||||
Unsigned(val) | Ifd(val) => return Ok(vec![IfdPointer(val.into())]),
|
||||
IfdBig(val) => return Ok(vec![IfdPointer(val)]),
|
||||
List(vec) => vec,
|
||||
_ => return Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
|
||||
};
|
||||
|
||||
vec.into_iter().map(Self::into_ifd_pointer).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// A combination of type, count, and offset.
|
||||
///
|
||||
/// In a TIFF the data offset portion of an entry is used for inline data in case the length of the
|
||||
/// encoded value does not exceed the size of the offset field. Since the size of the offset field
|
||||
/// depends on the file kind (4 bytes for standard TIFF, 8 bytes for BigTIFF) the interpretation of
|
||||
/// this struct is only complete in combination with file metadata.
|
||||
#[derive(Clone)]
|
||||
pub struct Entry {
|
||||
type_: Type,
|
||||
@@ -319,12 +369,14 @@ impl ::std::fmt::Debug for Entry {
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
/// Create a new entry fit to be added to a standard TIFF IFD.
|
||||
pub fn new(type_: Type, count: u32, offset: [u8; 4]) -> Entry {
|
||||
let mut offset = offset.to_vec();
|
||||
offset.append(&mut vec![0; 4]);
|
||||
Entry::new_u64(type_, count.into(), offset[..].try_into().unwrap())
|
||||
let mut entry_off = [0u8; 8];
|
||||
entry_off[..4].copy_from_slice(&offset);
|
||||
Entry::new_u64(type_, count.into(), entry_off)
|
||||
}
|
||||
|
||||
/// Create a new entry with data for a Big TIFF IFD.
|
||||
pub fn new_u64(type_: Type, count: u64, offset: [u8; 8]) -> Entry {
|
||||
Entry {
|
||||
type_,
|
||||
@@ -333,8 +385,23 @@ impl Entry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn field_type(&self) -> Type {
|
||||
self.type_
|
||||
}
|
||||
|
||||
pub fn count(&self) -> u64 {
|
||||
self.count
|
||||
}
|
||||
|
||||
pub(crate) fn offset(&self) -> &[u8] {
|
||||
&self.offset
|
||||
}
|
||||
|
||||
/// Returns a mem_reader for the offset/value field
|
||||
fn r(&self, byte_order: ByteOrder) -> EndianReader<io::Cursor<Vec<u8>>> {
|
||||
pub(crate) fn offset_field_reader(
|
||||
&self,
|
||||
byte_order: ByteOrder,
|
||||
) -> EndianReader<io::Cursor<Vec<u8>>> {
|
||||
EndianReader::new(io::Cursor::new(self.offset.to_vec()), byte_order)
|
||||
}
|
||||
|
||||
@@ -350,43 +417,25 @@ impl Entry {
|
||||
}
|
||||
|
||||
let bo = reader.byte_order;
|
||||
|
||||
let tag_size = match self.type_ {
|
||||
Type::BYTE | Type::SBYTE | Type::ASCII | Type::UNDEFINED => 1,
|
||||
Type::SHORT | Type::SSHORT => 2,
|
||||
Type::LONG | Type::SLONG | Type::FLOAT | Type::IFD => 4,
|
||||
Type::LONG8
|
||||
| Type::SLONG8
|
||||
| Type::DOUBLE
|
||||
| Type::RATIONAL
|
||||
| Type::SRATIONAL
|
||||
| Type::IFD8 => 8,
|
||||
};
|
||||
|
||||
let value_bytes = match self.count.checked_mul(tag_size) {
|
||||
Some(n) => n,
|
||||
None => {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
};
|
||||
let value_bytes = self.type_.value_bytes(self.count)?;
|
||||
|
||||
// Case 2: there is one value.
|
||||
if self.count == 1 {
|
||||
// 2a: the value is 5-8 bytes and we're in BigTiff mode.
|
||||
if bigtiff && value_bytes > 4 && value_bytes <= 8 {
|
||||
return Ok(match self.type_ {
|
||||
Type::LONG8 => UnsignedBig(self.r(bo).read_u64()?),
|
||||
Type::SLONG8 => SignedBig(self.r(bo).read_i64()?),
|
||||
Type::DOUBLE => Double(self.r(bo).read_f64()?),
|
||||
Type::LONG8 => UnsignedBig(self.offset_field_reader(bo).read_u64()?),
|
||||
Type::SLONG8 => SignedBig(self.offset_field_reader(bo).read_i64()?),
|
||||
Type::DOUBLE => Double(self.offset_field_reader(bo).read_f64()?),
|
||||
Type::RATIONAL => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
Rational(r.read_u32()?, r.read_u32()?)
|
||||
}
|
||||
Type::SRATIONAL => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
SRational(r.read_i32()?, r.read_i32()?)
|
||||
}
|
||||
Type::IFD8 => IfdBig(self.r(bo).read_u64()?),
|
||||
Type::IFD8 => IfdBig(self.offset_field_reader(bo).read_u64()?),
|
||||
Type::BYTE
|
||||
| Type::SBYTE
|
||||
| Type::ASCII
|
||||
@@ -403,13 +452,13 @@ impl Entry {
|
||||
// 2b: the value is at most 4 bytes or doesn't fit in the offset field.
|
||||
return Ok(match self.type_ {
|
||||
Type::BYTE => Byte(self.offset[0]),
|
||||
Type::SBYTE => SignedByte(i8::from(self.offset[0] as i8)),
|
||||
Type::SBYTE => SignedByte(self.offset[0] as i8),
|
||||
Type::UNDEFINED => Byte(self.offset[0]),
|
||||
Type::SHORT => Short(self.r(bo).read_u16()?),
|
||||
Type::SSHORT => SignedShort(self.r(bo).read_i16()?),
|
||||
Type::LONG => Unsigned(self.r(bo).read_u32()?),
|
||||
Type::SLONG => Signed(self.r(bo).read_i32()?),
|
||||
Type::FLOAT => Float(self.r(bo).read_f32()?),
|
||||
Type::SHORT => Short(self.offset_field_reader(bo).read_u16()?),
|
||||
Type::SSHORT => SignedShort(self.offset_field_reader(bo).read_i16()?),
|
||||
Type::LONG => Unsigned(self.offset_field_reader(bo).read_u32()?),
|
||||
Type::SLONG => Signed(self.offset_field_reader(bo).read_i32()?),
|
||||
Type::FLOAT => Float(self.offset_field_reader(bo).read_f32()?),
|
||||
Type::ASCII => {
|
||||
if self.offset[0] == 0 {
|
||||
Ascii("".to_string())
|
||||
@@ -418,28 +467,28 @@ impl Entry {
|
||||
}
|
||||
}
|
||||
Type::LONG8 => {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?;
|
||||
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
|
||||
UnsignedBig(reader.read_u64()?)
|
||||
}
|
||||
Type::SLONG8 => {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?;
|
||||
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
|
||||
SignedBig(reader.read_i64()?)
|
||||
}
|
||||
Type::DOUBLE => {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?;
|
||||
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
|
||||
Double(reader.read_f64()?)
|
||||
}
|
||||
Type::RATIONAL => {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?;
|
||||
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
|
||||
Rational(reader.read_u32()?, reader.read_u32()?)
|
||||
}
|
||||
Type::SRATIONAL => {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?;
|
||||
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
|
||||
SRational(reader.read_i32()?, reader.read_i32()?)
|
||||
}
|
||||
Type::IFD => Ifd(self.r(bo).read_u32()?),
|
||||
Type::IFD => Ifd(self.offset_field_reader(bo).read_u32()?),
|
||||
Type::IFD8 => {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?;
|
||||
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
|
||||
IfdBig(reader.read_u64()?)
|
||||
}
|
||||
});
|
||||
@@ -470,7 +519,7 @@ impl Entry {
|
||||
));
|
||||
}
|
||||
Type::SHORT => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..self.count {
|
||||
v.push(Short(r.read_u16()?));
|
||||
@@ -478,7 +527,7 @@ impl Entry {
|
||||
return Ok(List(v));
|
||||
}
|
||||
Type::SSHORT => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..self.count {
|
||||
v.push(SignedShort(r.read_i16()?));
|
||||
@@ -486,7 +535,7 @@ impl Entry {
|
||||
return Ok(List(v));
|
||||
}
|
||||
Type::LONG => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..self.count {
|
||||
v.push(Unsigned(r.read_u32()?));
|
||||
@@ -494,7 +543,7 @@ impl Entry {
|
||||
return Ok(List(v));
|
||||
}
|
||||
Type::SLONG => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..self.count {
|
||||
v.push(Signed(r.read_i32()?));
|
||||
@@ -502,7 +551,7 @@ impl Entry {
|
||||
return Ok(List(v));
|
||||
}
|
||||
Type::FLOAT => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..self.count {
|
||||
v.push(Float(r.read_f32()?));
|
||||
@@ -510,7 +559,7 @@ impl Entry {
|
||||
return Ok(List(v));
|
||||
}
|
||||
Type::IFD => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..self.count {
|
||||
v.push(Ifd(r.read_u32()?));
|
||||
@@ -529,74 +578,149 @@ impl Entry {
|
||||
}
|
||||
|
||||
// Case 4: there is more than one value, and it doesn't fit in the offset field.
|
||||
let mut v;
|
||||
self.set_reader_offset_relative(bigtiff, reader, 0)?;
|
||||
|
||||
match self.type_ {
|
||||
// TODO check if this could give wrong results
|
||||
// at a different endianess of file/computer.
|
||||
Type::BYTE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
let mut buf = [0; 1];
|
||||
reader.inner().read_exact(&mut buf)?;
|
||||
Ok(UnsignedBig(u64::from(buf[0])))
|
||||
}),
|
||||
Type::SBYTE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(SignedBig(i64::from(reader.read_i8()?)))
|
||||
}),
|
||||
Type::SHORT => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Short(reader.read_u16()?))
|
||||
}),
|
||||
Type::SSHORT => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(SignedShort(reader.read_i16()?))
|
||||
}),
|
||||
Type::LONG => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Unsigned(reader.read_u32()?))
|
||||
}),
|
||||
Type::SLONG => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Signed(reader.read_i32()?))
|
||||
}),
|
||||
Type::FLOAT => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Float(reader.read_f32()?))
|
||||
}),
|
||||
Type::DOUBLE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Double(reader.read_f64()?))
|
||||
}),
|
||||
Type::BYTE | Type::UNDEFINED => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.iter().copied().map(Byte))
|
||||
})
|
||||
}
|
||||
Type::SBYTE => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.iter().copied().map(|v| SignedByte(v as i8)))
|
||||
})
|
||||
}
|
||||
Type::SHORT => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(2)
|
||||
.map(|ch| Short(u16::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::SSHORT => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(2)
|
||||
.map(|ch| SignedShort(i16::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::LONG => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(4)
|
||||
.map(|ch| Unsigned(u32::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::SLONG => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(4)
|
||||
.map(|ch| Signed(i32::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::FLOAT => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(4)
|
||||
.map(|ch| Float(f32::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::DOUBLE => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(8)
|
||||
.map(|ch| Double(f64::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::RATIONAL => {
|
||||
self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Rational(reader.read_u32()?, reader.read_u32()?))
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.chunks_exact(8).map(|ch| {
|
||||
Rational(
|
||||
u32::from_ne_bytes(ch[..4].try_into().unwrap()),
|
||||
u32::from_ne_bytes(ch[4..].try_into().unwrap()),
|
||||
)
|
||||
}))
|
||||
})
|
||||
}
|
||||
Type::SRATIONAL => {
|
||||
self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(SRational(reader.read_i32()?, reader.read_i32()?))
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.chunks_exact(8).map(|ch| {
|
||||
SRational(
|
||||
i32::from_ne_bytes(ch[..4].try_into().unwrap()),
|
||||
i32::from_ne_bytes(ch[4..].try_into().unwrap()),
|
||||
)
|
||||
}))
|
||||
})
|
||||
}
|
||||
Type::LONG8 => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(UnsignedBig(reader.read_u64()?))
|
||||
}),
|
||||
Type::SLONG8 => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(SignedBig(reader.read_i64()?))
|
||||
}),
|
||||
Type::IFD => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Ifd(reader.read_u32()?))
|
||||
}),
|
||||
Type::IFD8 => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(IfdBig(reader.read_u64()?))
|
||||
}),
|
||||
Type::UNDEFINED => {
|
||||
self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
let mut buf = [0; 1];
|
||||
reader.inner().read_exact(&mut buf)?;
|
||||
Ok(Byte(buf[0]))
|
||||
Type::LONG8 => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(8)
|
||||
.map(|ch| UnsignedBig(u64::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::SLONG8 => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(8)
|
||||
.map(|ch| SignedBig(i64::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::IFD => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(4)
|
||||
.map(|ch| Ifd(u32::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::IFD8 => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(8)
|
||||
.map(|ch| IfdBig(u64::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::ASCII => {
|
||||
let n = usize::try_from(self.count)?;
|
||||
if n > limits.decoding_buffer_size {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
|
||||
if bigtiff {
|
||||
reader.goto_offset(self.r(bo).read_u64()?)?
|
||||
} else {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?
|
||||
if n > limits.decoding_buffer_size {
|
||||
return Err(dbg!(TiffError::LimitsExceeded));
|
||||
}
|
||||
|
||||
let mut out = vec![0; n];
|
||||
@@ -605,43 +729,199 @@ impl Entry {
|
||||
if let Some(first) = out.iter().position(|&b| b == 0) {
|
||||
out.truncate(first);
|
||||
}
|
||||
Ok(Ascii(String::from_utf8(out)?))
|
||||
}
|
||||
|
||||
return Ok(Ascii(String::from_utf8(out)?));
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(List(v))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn decode_offset<R, F>(
|
||||
pub(crate) fn buffered_value<R: Read + Seek>(
|
||||
&self,
|
||||
value_count: u64,
|
||||
bo: ByteOrder,
|
||||
bigtiff: bool,
|
||||
buf: &mut ValueBuffer,
|
||||
limits: &super::Limits,
|
||||
bigtiff: bool,
|
||||
reader: &mut EndianReader<R>,
|
||||
decode_fn: F,
|
||||
) -> TiffResult<Value>
|
||||
where
|
||||
R: Read + Seek,
|
||||
F: Fn(&mut EndianReader<R>) -> TiffResult<Value>,
|
||||
{
|
||||
) -> TiffResult<()> {
|
||||
if self.count == 0 {
|
||||
buf.assume_type(self.type_, 0, reader.byte_order);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let value_bytes = self.buffer_with_capacity(buf, limits)?;
|
||||
|
||||
// Case 1: the value fits in the offset field.
|
||||
if value_bytes <= 4 || bigtiff && value_bytes <= 8 {
|
||||
let src = &self.offset[..value_bytes];
|
||||
buf.raw_bytes_mut()[..value_bytes].copy_from_slice(src);
|
||||
buf.assume_type(self.type_, self.count, reader.byte_order);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Case 2: the value is stored in the reader at an offset.
|
||||
self.set_reader_offset_relative(bigtiff, reader, 0)?;
|
||||
|
||||
// In case of an error we set the type and endianess.
|
||||
buf.assume_type(self.type_, 0, reader.byte_order);
|
||||
let target = &mut buf.raw_bytes_mut()[..value_bytes];
|
||||
// FIXME: if the read fails we have already grown to full size, which is not great.
|
||||
reader.inner().read_exact(target)?;
|
||||
buf.assume_type(self.type_, self.count, reader.byte_order);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn raw_value_at<R: Read + Seek>(
|
||||
&self,
|
||||
buf: &mut [u8],
|
||||
bigtiff: bool,
|
||||
reader: &mut EndianReader<R>,
|
||||
at: u64,
|
||||
) -> TiffResult<usize> {
|
||||
if self.count == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// We have no limits to handle, we do not allocate.
|
||||
let value_bytes = self.type_.value_bytes(self.count)?;
|
||||
|
||||
// No bytes to fill into the buffer.
|
||||
if at >= value_bytes {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// Case 1: the value fits in the offset field.
|
||||
if value_bytes <= 4 || bigtiff && value_bytes <= 8 {
|
||||
// `at < value_bytes` and `value_bytes <= 8` so casting is mathematical
|
||||
let src = &self.offset[..value_bytes as usize][at as usize..];
|
||||
let len = src.len().min(buf.len());
|
||||
buf[..len].copy_from_slice(&src[..len]);
|
||||
return Ok(value_bytes as usize);
|
||||
}
|
||||
|
||||
// Case 2: the value is stored in the reader at an offset. We will find the offset
|
||||
// encoded in the entry, apply the relative start position and seek there.
|
||||
self.set_reader_offset_relative(bigtiff, reader, at)?;
|
||||
|
||||
let remainder = value_bytes - at;
|
||||
let len = usize::try_from(remainder)
|
||||
.unwrap_or(usize::MAX)
|
||||
.min(buf.len());
|
||||
|
||||
let target = &mut buf[..len];
|
||||
reader.inner().read_exact(target)?;
|
||||
|
||||
// Design note: in a previous draft we would consume the rest of the bytes of this value
|
||||
// here (into a stack buffer if need be) to verify the stream itself. But in the end we
|
||||
// have `Seek` so we better verify this by seeking over the rest of the bytes, finding if
|
||||
// the stream continues that far. Even that is maybe bad if we wanted to provide a
|
||||
// async-adaptor that `WouldBlock` errors to fill back a read window then the seek is
|
||||
// poison to that, too.
|
||||
|
||||
// So a really simple choice: The caller is responsible for handling the fact that this did
|
||||
// not verify the whole value. Attempt a 1-byte read at the end of the value instead?
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
// Returns `Ok(bytes)` if our value's bytes through type and count fit into `usize` and are
|
||||
// within the limits. Extends the buffer to that many bytes.
|
||||
fn buffer_with_capacity(
|
||||
&self,
|
||||
buf: &mut ValueBuffer,
|
||||
limits: &super::Limits,
|
||||
) -> TiffResult<usize> {
|
||||
let bytes = self.type_.value_bytes(self.count())?;
|
||||
|
||||
let allowed_length = usize::try_from(bytes)
|
||||
.ok()
|
||||
.filter(|&n| n <= limits.decoding_buffer_size)
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
|
||||
buf.prepare_length(allowed_length);
|
||||
|
||||
Ok(allowed_length)
|
||||
}
|
||||
|
||||
fn vec_with_capacity(
|
||||
value_count: u64,
|
||||
limits: &super::Limits,
|
||||
) -> Result<Vec<Value>, TiffError> {
|
||||
let value_count = usize::try_from(value_count)?;
|
||||
|
||||
if value_count > limits.decoding_buffer_size / mem::size_of::<Value>() {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
|
||||
let mut v = Vec::with_capacity(value_count);
|
||||
Ok(Vec::with_capacity(value_count))
|
||||
}
|
||||
|
||||
/// Seek to an offset within a value stored in the offset defined by this entry.
|
||||
fn set_reader_offset_relative<R>(
|
||||
&self,
|
||||
bigtiff: bool,
|
||||
reader: &mut EndianReader<R>,
|
||||
at: u64,
|
||||
) -> TiffResult<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let bo = reader.byte_order;
|
||||
|
||||
let offset = if bigtiff {
|
||||
self.r(bo).read_u64()?
|
||||
self.offset_field_reader(bo).read_u64()?
|
||||
} else {
|
||||
self.r(bo).read_u32()?.into()
|
||||
self.offset_field_reader(bo).read_u32()?.into()
|
||||
};
|
||||
|
||||
// FIXME: `at` should be within `self.type_.value_bytes(self.count)` and that itself should
|
||||
// be within the bounds of the stream. But we do not check this eagerly so this below will
|
||||
// fail sometimes differently for exotic streams, depending on the method by which we read
|
||||
// (at once or through multiple raw into-byte-slice reads).
|
||||
let offset = offset.checked_add(at).ok_or(TiffError::FormatError(
|
||||
TiffFormatError::InconsistentSizesEncountered,
|
||||
))?;
|
||||
|
||||
reader.goto_offset(offset)?;
|
||||
|
||||
for _ in 0..value_count {
|
||||
v.push(decode_fn(reader)?)
|
||||
Ok(())
|
||||
}
|
||||
Ok(List(v))
|
||||
|
||||
#[inline]
|
||||
fn decode_values<R, F>(
|
||||
&self,
|
||||
value_count: u64,
|
||||
type_: Type,
|
||||
reader: &mut EndianReader<R>,
|
||||
mut collect: F,
|
||||
) -> TiffResult<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
F: FnMut(&[u8]),
|
||||
{
|
||||
let mut total_bytes = type_.value_bytes(value_count)?;
|
||||
let mut buffer = [0u8; 512];
|
||||
|
||||
let buf_unit = usize::from(type_.byte_len());
|
||||
let mul_of_ty = buffer.len() / buf_unit * buf_unit;
|
||||
|
||||
let cls = type_.endian_bytes();
|
||||
let native = ByteOrder::native();
|
||||
|
||||
while total_bytes > 0 {
|
||||
// `now <= mul_of_ty < 512` so casting is mathematical
|
||||
let now = total_bytes.min(mul_of_ty as u64);
|
||||
total_bytes -= now;
|
||||
|
||||
let buffer = &mut buffer[..now as usize];
|
||||
reader.inner().read_exact(buffer)?;
|
||||
|
||||
reader.byte_order.convert_endian_bytes(cls, buffer, native);
|
||||
collect(buffer);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,4 +948,6 @@ fn offset_to_sbytes(n: usize, entry: &Entry) -> TiffResult<Value> {
|
||||
}
|
||||
|
||||
/// Type representing an Image File Directory
|
||||
#[doc(hidden)]
|
||||
#[deprecated = "Use struct `tiff::Directory` instead which contains all fields relevant to an Image File Directory, including the offset to the next directory"]
|
||||
pub type Directory = HashMap<Tag, Entry>;
|
||||
+551
-106
@@ -1,12 +1,16 @@
|
||||
use super::ifd::{Directory, Value};
|
||||
use super::stream::{ByteOrder, PackBitsReader};
|
||||
use super::ifd::Value;
|
||||
use super::stream::PackBitsReader;
|
||||
use super::tag_reader::TagReader;
|
||||
use super::{predict_f16, predict_f32, predict_f64, Limits};
|
||||
use super::{stream::EndianReader, ChunkType};
|
||||
use super::ChunkType;
|
||||
use super::{predict_f16, predict_f32, predict_f64, ValueReader};
|
||||
use crate::tags::{
|
||||
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag,
|
||||
CompressionMethod, ExtraSamples, PhotometricInterpretation, PlanarConfiguration, Predictor,
|
||||
SampleFormat, Tag,
|
||||
};
|
||||
use crate::{ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError};
|
||||
use crate::{
|
||||
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
|
||||
};
|
||||
|
||||
use std::io::{self, Cursor, Read, Seek};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -27,10 +31,10 @@ pub(crate) struct TileAttributes {
|
||||
|
||||
impl TileAttributes {
|
||||
pub fn tiles_across(&self) -> usize {
|
||||
(self.image_width + self.tile_width - 1) / self.tile_width
|
||||
self.image_width.div_ceil(self.tile_width)
|
||||
}
|
||||
pub fn tiles_down(&self) -> usize {
|
||||
(self.image_height + self.tile_length - 1) / self.tile_length
|
||||
self.image_height.div_ceil(self.tile_length)
|
||||
}
|
||||
fn padding_right(&self) -> usize {
|
||||
(self.tile_width - self.image_width % self.tile_width) % self.tile_width
|
||||
@@ -65,6 +69,11 @@ pub(crate) struct Image {
|
||||
pub height: u32,
|
||||
pub bits_per_sample: u8,
|
||||
pub samples: u16,
|
||||
/// The `ExtraSamples`, defaulting to empty if not given.
|
||||
pub extra_samples: Vec<ExtraSamples>,
|
||||
/// Number of samples that belong to the photometric interpretation, samples except
|
||||
/// `ExtraSamples` (338, 0x0152) tag.
|
||||
pub photometric_samples: u16,
|
||||
pub sample_format: SampleFormat,
|
||||
pub photometric_interpretation: PhotometricInterpretation,
|
||||
pub compression_method: CompressionMethod,
|
||||
@@ -76,21 +85,66 @@ pub(crate) struct Image {
|
||||
pub tile_attributes: Option<TileAttributes>,
|
||||
pub chunk_offsets: Vec<u64>,
|
||||
pub chunk_bytes: Vec<u64>,
|
||||
pub chroma_subsampling: (u16, u16),
|
||||
}
|
||||
|
||||
/// Describes how to read a tile-aligned portion of the image.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ReadoutLayout {
|
||||
/// The planar configuration, which applies to both the underlying image and the output buffer.
|
||||
/// This may be relaxed if we find a clean enough way to provide it.
|
||||
pub planar_config: PlanarConfiguration,
|
||||
|
||||
/// The sample interpretation (interpret with planar_config).
|
||||
///
|
||||
/// FIXME: we should not require this here. The ability to turn out the raw bytes from the
|
||||
/// sample arrays is very different from turning out interpretable color. Firstly we can always
|
||||
/// readout `Multiband` but currently only use that ColorType in special circumstances (it must
|
||||
/// not overlap cases where actually want to use a ColorType).
|
||||
///
|
||||
/// And then we have CIE Lab, which uses a tuple of `(u8, i8, i8)`, that is still filterable
|
||||
/// but still not represented by any of our `DecoderResult` variants. Other color variants
|
||||
/// depend on extra tags (YCbCrCoefficients/0x0211) and we don't have a good side channel to
|
||||
/// tag the output with all that TIFF specific information, so arguably we should process and
|
||||
/// apply those to the data so it becomes a self-contained representation.
|
||||
///
|
||||
/// This should be computed at a higher level, in `Decoder`, instead.
|
||||
pub color: ColorType,
|
||||
/// The number of bytes from one row to another.
|
||||
pub minimum_row_stride: usize,
|
||||
/// The format of samples (assumed uniform for now, same with depth of `ColorType`).
|
||||
pub sample_format: SampleFormat,
|
||||
|
||||
/// Number of bytes to advance in output per row.
|
||||
pub row_stride: usize,
|
||||
/// Number of bytes to advance in output per chunk in width.
|
||||
pub chunk_row_stride: usize,
|
||||
/// Number of bytes to advance in output per chunk in height.
|
||||
pub chunk_col_stride: usize,
|
||||
/// Number of bytes in output from one plane to another.
|
||||
pub plane_stride: usize,
|
||||
|
||||
/// Bits per sample in the encoded data.
|
||||
pub tiff_bits_per_sample: u8,
|
||||
/// Number of samples in the encoded data.
|
||||
pub tiff_samples: u16,
|
||||
/// Dimensions of the underlying rectangular chunks (tile or strips).
|
||||
pub tiff_chunk_dimensions: (u32, u32),
|
||||
/// Number of bytes in the underlying data with all samples per row of chunks.
|
||||
pub tiff_row_bytes: usize,
|
||||
|
||||
/// Chunks until wrapping to the next row of chunks.
|
||||
pub chunks_across: u32,
|
||||
/// Chunks to advance to get to the next plane of chunks.
|
||||
pub chunks_per_plane: u32,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn from_reader<R: Read + Seek>(
|
||||
reader: &mut EndianReader<R>,
|
||||
decoder: &mut ValueReader<R>,
|
||||
ifd: Directory,
|
||||
limits: &Limits,
|
||||
bigtiff: bool,
|
||||
) -> TiffResult<Image> {
|
||||
let mut tag_reader = TagReader {
|
||||
reader,
|
||||
limits,
|
||||
ifd: &ifd,
|
||||
bigtiff,
|
||||
};
|
||||
let mut tag_reader = TagReader { decoder, ifd: &ifd };
|
||||
|
||||
let width = tag_reader.require_tag(Tag::ImageWidth)?.into_u32()?;
|
||||
let height = tag_reader.require_tag(Tag::ImageLength)?.into_u32()?;
|
||||
@@ -115,7 +169,7 @@ impl Image {
|
||||
};
|
||||
|
||||
let jpeg_tables = if compression_method == CompressionMethod::ModernJPEG
|
||||
&& ifd.contains_key(&Tag::JPEGTables)
|
||||
&& ifd.contains(Tag::JPEGTables)
|
||||
{
|
||||
let vec = tag_reader
|
||||
.find_tag(Tag::JPEGTables)?
|
||||
@@ -137,10 +191,30 @@ impl Image {
|
||||
.map(Value::into_u16)
|
||||
.transpose()?
|
||||
.unwrap_or(1);
|
||||
|
||||
if samples == 0 {
|
||||
return Err(TiffFormatError::SamplesPerPixelIsZero.into());
|
||||
}
|
||||
|
||||
let extra_samples = match tag_reader.find_tag(Tag::ExtraSamples)? {
|
||||
Some(n) => n.into_u16_vec()?,
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let extra_samples = extra_samples
|
||||
.into_iter()
|
||||
.map(|x| ExtraSamples::from_u16(x).unwrap_or(ExtraSamples::Unspecified))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let photometric_samples = match usize::from(samples).checked_sub(extra_samples.len()) {
|
||||
None => {
|
||||
return Err(TiffError::FormatError(
|
||||
TiffFormatError::InconsistentSizesEncountered,
|
||||
));
|
||||
}
|
||||
Some(n) => n as u16,
|
||||
};
|
||||
|
||||
let sample_format = match tag_reader.find_tag_uint_vec(Tag::SampleFormat)? {
|
||||
Some(vals) => {
|
||||
let sample_format: Vec<_> = vals
|
||||
@@ -148,12 +222,15 @@ impl Image {
|
||||
.map(SampleFormat::from_u16_exhaustive)
|
||||
.collect();
|
||||
|
||||
let Some(format) = sample_format.first().copied() else {
|
||||
// Reject empty sample formats
|
||||
return Err(TiffFormatError::InvalidTagValueType(Tag::SampleFormat).into());
|
||||
};
|
||||
// TODO: for now, only homogenous formats across samples are supported.
|
||||
if !sample_format.windows(2).all(|s| s[0] == s[1]) {
|
||||
if !sample_format.iter().all(|&s| s == format) {
|
||||
return Err(TiffUnsupportedError::UnsupportedSampleFormat(sample_format).into());
|
||||
}
|
||||
|
||||
sample_format[0]
|
||||
format
|
||||
}
|
||||
None => SampleFormat::Uint,
|
||||
};
|
||||
@@ -164,7 +241,7 @@ impl Image {
|
||||
|
||||
// Technically bits_per_sample.len() should be *equal* to samples, but libtiff also allows
|
||||
// it to be a single value that applies to all samples.
|
||||
if bits_per_sample.len() != samples.into() && bits_per_sample.len() != 1 {
|
||||
if bits_per_sample.len() != usize::from(samples) && bits_per_sample.len() != 1 {
|
||||
return Err(TiffError::FormatError(
|
||||
TiffFormatError::InconsistentSizesEncountered,
|
||||
));
|
||||
@@ -199,6 +276,25 @@ impl Image {
|
||||
.transpose()?
|
||||
.unwrap_or(PlanarConfiguration::Chunky);
|
||||
|
||||
let ycbcr_subsampling = tag_reader.find_tag_uint_vec::<u16>(Tag::ChromaSubsampling)?;
|
||||
|
||||
let chroma_subsampling = if let Some(subsamples) = &ycbcr_subsampling {
|
||||
let [a, b] = subsamples.as_slice() else {
|
||||
return Err(TiffError::FormatError(TiffFormatError::InvalidCountForTag(
|
||||
Tag::ChromaSubsampling,
|
||||
subsamples.len(),
|
||||
)));
|
||||
};
|
||||
|
||||
// ImageWidth and ImageLength are constrained to be integer multiples of
|
||||
// YCbCrSubsampleHoriz and YCbCrSubsampleVert respectively. TileWidth and TileLength
|
||||
// have the same constraints. RowsPerStrip must be an integer multiple of
|
||||
// YCbCrSubsampleVert.
|
||||
(*a, *b)
|
||||
} else {
|
||||
(2, 2)
|
||||
};
|
||||
|
||||
let planes = match planar_config {
|
||||
PlanarConfiguration::Chunky => 1,
|
||||
PlanarConfiguration::Planar => samples,
|
||||
@@ -210,10 +306,10 @@ impl Image {
|
||||
let strip_decoder;
|
||||
let tile_attributes;
|
||||
match (
|
||||
ifd.contains_key(&Tag::StripByteCounts),
|
||||
ifd.contains_key(&Tag::StripOffsets),
|
||||
ifd.contains_key(&Tag::TileByteCounts),
|
||||
ifd.contains_key(&Tag::TileOffsets),
|
||||
ifd.contains(Tag::StripByteCounts),
|
||||
ifd.contains(Tag::StripOffsets),
|
||||
ifd.contains(Tag::TileByteCounts),
|
||||
ifd.contains(Tag::TileOffsets),
|
||||
) {
|
||||
(true, true, false, false) => {
|
||||
chunk_type = ChunkType::Strip;
|
||||
@@ -297,6 +393,8 @@ impl Image {
|
||||
height,
|
||||
bits_per_sample: bits_per_sample[0],
|
||||
samples,
|
||||
extra_samples,
|
||||
photometric_samples,
|
||||
sample_format,
|
||||
photometric_interpretation,
|
||||
compression_method,
|
||||
@@ -308,19 +406,24 @@ impl Image {
|
||||
tile_attributes,
|
||||
chunk_offsets,
|
||||
chunk_bytes,
|
||||
chroma_subsampling,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
|
||||
let is_alpha_extra_samples = matches!(
|
||||
self.extra_samples.as_slice(),
|
||||
[ExtraSamples::AssociatedAlpha, ..] | [ExtraSamples::UnassociatedAlpha, ..]
|
||||
);
|
||||
|
||||
match self.photometric_interpretation {
|
||||
PhotometricInterpretation::RGB => match self.samples {
|
||||
3 => Ok(ColorType::RGB(self.bits_per_sample)),
|
||||
PhotometricInterpretation::RGB => match self.photometric_samples {
|
||||
3 => Ok(if is_alpha_extra_samples {
|
||||
ColorType::RGBA(self.bits_per_sample)
|
||||
} else {
|
||||
ColorType::RGB(self.bits_per_sample)
|
||||
}),
|
||||
4 => Ok(ColorType::RGBA(self.bits_per_sample)),
|
||||
// FIXME: We should _ignore_ other components. In particular:
|
||||
// > Beware of extra components. Some TIFF files may have more components per pixel
|
||||
// than you think. A Baseline TIFF reader must skip over them gracefully,using the
|
||||
// values of the SamplesPerPixel and BitsPerSample fields.
|
||||
// > -- TIFF 6.0 Specification, Section 7, Additional Baseline requirements.
|
||||
_ => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
self.photometric_interpretation,
|
||||
@@ -328,8 +431,12 @@ impl Image {
|
||||
),
|
||||
)),
|
||||
},
|
||||
PhotometricInterpretation::CMYK => match self.samples {
|
||||
4 => Ok(ColorType::CMYK(self.bits_per_sample)),
|
||||
PhotometricInterpretation::CMYK => match self.photometric_samples {
|
||||
4 => Ok(if is_alpha_extra_samples {
|
||||
ColorType::CMYKA(self.bits_per_sample)
|
||||
} else {
|
||||
ColorType::CMYK(self.bits_per_sample)
|
||||
}),
|
||||
5 => Ok(ColorType::CMYKA(self.bits_per_sample)),
|
||||
_ => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
@@ -338,7 +445,7 @@ impl Image {
|
||||
),
|
||||
)),
|
||||
},
|
||||
PhotometricInterpretation::YCbCr => match self.samples {
|
||||
PhotometricInterpretation::YCbCr => match self.photometric_samples {
|
||||
3 => Ok(ColorType::YCbCr(self.bits_per_sample)),
|
||||
_ => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
@@ -347,7 +454,13 @@ impl Image {
|
||||
),
|
||||
)),
|
||||
},
|
||||
// TODO: treatment of WhiteIsZero is not quite consistent with `invert_colors` that is
|
||||
// later called when that interpretation is read. That function does not support
|
||||
// Multiband as a color type and will error. It's unclear how to resolve that exactly.
|
||||
PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero => {
|
||||
// Note: compatibility with previous implementation requires us to return extra
|
||||
// samples as `Multiband`. For gray images however the better choice would be
|
||||
// returning a `Gray` color, i.e. matching on `photometric_samples` instead.
|
||||
match self.samples {
|
||||
1 => Ok(ColorType::Gray(self.bits_per_sample)),
|
||||
_ => Ok(ColorType::Multiband {
|
||||
@@ -356,24 +469,71 @@ impl Image {
|
||||
}),
|
||||
}
|
||||
}
|
||||
// TODO: this is bad we should not fail at this point
|
||||
PhotometricInterpretation::RGBPalette
|
||||
| PhotometricInterpretation::TransparencyMask
|
||||
| PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
|
||||
// ```
|
||||
// struct IccLab /* Interpretation 9* {
|
||||
// pub L: u8, // SampleFormat::Uint
|
||||
// pub a: u8, // SampleFormat::Uint, defined as TiffLab::a + 128
|
||||
// pub b: u8, // SampleFormat::Uint, defined as TiffLab::b + 128
|
||||
// }
|
||||
// ```
|
||||
PhotometricInterpretation::IccLab => match self.photometric_samples {
|
||||
3 if matches!(self.sample_format, SampleFormat::Uint) => {
|
||||
Ok(ColorType::Lab(self.bits_per_sample))
|
||||
}
|
||||
_ => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
self.photometric_interpretation,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
)),
|
||||
},
|
||||
// Unsupported due to inherently heterogeneous sample types. This is represented as:
|
||||
// ```
|
||||
// struct TiffLab /* Interpretation 8* {
|
||||
// pub L: u8, // SampleFormat::Uint
|
||||
// pub a: i8, // SampleFormat::Int
|
||||
// pub b: i8, // SampleFormat::Int
|
||||
// }
|
||||
// ```
|
||||
PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
PhotometricInterpretation::CIELab,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
)),
|
||||
// Unsupported due to extra unfiltering and conversion steps. We need to find the
|
||||
// Decode tag (SRATIONAL; 2 * SamplesPerPixel) and apply the following conversion:
|
||||
//
|
||||
// L* = Decode[0] + Lsample x (Decode[1] - Decode[0]) / (2^n -1)
|
||||
// …
|
||||
//
|
||||
// So we'll have a larger depth in the output and either worry about reducing fractions
|
||||
// or turn everything into floats. That's a lot of decisions.
|
||||
PhotometricInterpretation::ItuLab => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
PhotometricInterpretation::CIELab,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
)),
|
||||
PhotometricInterpretation::RGBPalette | PhotometricInterpretation::TransparencyMask => {
|
||||
Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
self.photometric_interpretation,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_reader<'r, R: 'r + Read>(
|
||||
fn create_reader<'r, R: 'r + Read + Seek>(
|
||||
reader: R,
|
||||
#[allow(unused_variables)] photometric_interpretation: PhotometricInterpretation,
|
||||
compression_method: CompressionMethod,
|
||||
compressed_length: u64,
|
||||
#[allow(unused_variables)] jpeg_tables: Option<&[u8]>,
|
||||
// FIXME: these should be `expect` attributes or we choose another way of passing them.
|
||||
#[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>,
|
||||
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] dimensions: (u32, u32),
|
||||
#[cfg_attr(not(feature = "webp"), allow(unused_variables))] samples: u16,
|
||||
) -> TiffResult<Box<dyn Read + 'r>> {
|
||||
Ok(match compression_method {
|
||||
CompressionMethod::None => Box::new(reader),
|
||||
@@ -425,38 +585,36 @@ impl Image {
|
||||
let mut jpeg_data = Vec::new();
|
||||
jpeg_reader.read_to_end(&mut jpeg_data)?;
|
||||
|
||||
let mut decoder = zune_jpeg::JpegDecoder::new(jpeg_data);
|
||||
let mut decoder =
|
||||
zune_jpeg::JpegDecoder::new(zune_core::bytestream::ZCursor::new(jpeg_data));
|
||||
let mut options: zune_core::options::DecoderOptions = Default::default();
|
||||
match photometric_interpretation {
|
||||
PhotometricInterpretation::RGB => {
|
||||
options =
|
||||
options.jpeg_set_out_colorspace(zune_core::colorspace::ColorSpace::RGB);
|
||||
}
|
||||
PhotometricInterpretation::CMYK => {
|
||||
options = options
|
||||
.jpeg_set_out_colorspace(zune_core::colorspace::ColorSpace::CMYK);
|
||||
}
|
||||
PhotometricInterpretation::YCbCr => {
|
||||
options = options
|
||||
.jpeg_set_out_colorspace(zune_core::colorspace::ColorSpace::YCbCr);
|
||||
}
|
||||
PhotometricInterpretation::WhiteIsZero
|
||||
| PhotometricInterpretation::BlackIsZero
|
||||
| PhotometricInterpretation::TransparencyMask => {}
|
||||
PhotometricInterpretation::RGBPalette | PhotometricInterpretation::CIELab => {
|
||||
return Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::UnsupportedInterpretation(
|
||||
photometric_interpretation,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Disable color conversion by setting the output colorspace to the input
|
||||
// colorspace.
|
||||
decoder.decode_headers()?;
|
||||
if let Some(colorspace) = decoder.input_colorspace() {
|
||||
options = options.jpeg_set_out_colorspace(colorspace);
|
||||
}
|
||||
|
||||
decoder.set_options(options);
|
||||
|
||||
let data = decoder.decode()?;
|
||||
|
||||
Box::new(Cursor::new(data))
|
||||
}
|
||||
#[cfg(feature = "fax")]
|
||||
CompressionMethod::Fax4 => Box::new(super::stream::Group4Reader::new(
|
||||
dimensions,
|
||||
reader,
|
||||
compressed_length,
|
||||
)?),
|
||||
#[cfg(feature = "webp")]
|
||||
CompressionMethod::WebP => Box::new(super::stream::WebPReader::new(
|
||||
reader,
|
||||
compressed_length,
|
||||
samples,
|
||||
)?),
|
||||
|
||||
method => {
|
||||
return Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::UnsupportedCompressionMethod(method),
|
||||
@@ -472,18 +630,25 @@ impl Image {
|
||||
/// Example with `bits_per_sample = [8, 8, 8]` and `PhotometricInterpretation::RGB`:
|
||||
/// * `PlanarConfiguration::Chunky` -> 3 (RGBRGBRGB...)
|
||||
/// * `PlanarConfiguration::Planar` -> 1 (RRR...) (GGG...) (BBB...)
|
||||
pub(crate) fn samples_per_pixel(&self) -> usize {
|
||||
pub(crate) fn samples_per_pixel(&self) -> u16 {
|
||||
match self.planar_config {
|
||||
PlanarConfiguration::Chunky => self.samples.into(),
|
||||
PlanarConfiguration::Chunky => self.samples,
|
||||
PlanarConfiguration::Planar => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn samples_per_out_texel(&self, color: ColorType) -> u16 {
|
||||
match self.planar_config {
|
||||
PlanarConfiguration::Chunky => color.num_samples(),
|
||||
PlanarConfiguration::Planar => 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of strips per pixel.
|
||||
pub(crate) fn strips_per_pixel(&self) -> usize {
|
||||
pub(crate) fn strips_per_pixel(&self) -> u16 {
|
||||
match self.planar_config {
|
||||
PlanarConfiguration::Chunky => 1,
|
||||
PlanarConfiguration::Planar => self.samples.into(),
|
||||
PlanarConfiguration::Planar => self.samples,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,14 +686,107 @@ impl Image {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn readout_for_image(&self) -> TiffResult<ReadoutLayout> {
|
||||
let Image { width, height, .. } = *self;
|
||||
self.readout_for_size(width, height)
|
||||
}
|
||||
|
||||
/// Get the layout for reading out a tile-aligned portion of the image.
|
||||
///
|
||||
/// The provided width and height should be less than or equal to the image dimensions.
|
||||
pub(crate) fn readout_for_size(&self, width: u32, height: u32) -> TiffResult<ReadoutLayout> {
|
||||
let color = self.colortype()?;
|
||||
|
||||
let tiff_samples = self.samples_per_pixel();
|
||||
let tiff_bits_per_sample = self.bits_per_sample;
|
||||
let data_samples = self.samples_per_out_texel(color);
|
||||
let tiff_chunk_dimensions = self.chunk_dimensions()?;
|
||||
let strips_per_pixel = self.strips_per_pixel();
|
||||
|
||||
let data_dimensions = (width, height);
|
||||
|
||||
let tiff_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample))
|
||||
.checked_mul(u64::from(tiff_samples))
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
let tiff_row_bytes: usize = tiff_row_bits.div_ceil(8).try_into()?;
|
||||
|
||||
let chunk_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample))
|
||||
.checked_mul(u64::from(data_samples))
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?;
|
||||
|
||||
let data_row_bits = (u64::from(data_dimensions.0) * u64::from(tiff_bits_per_sample))
|
||||
.checked_mul(u64::from(data_samples))
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?;
|
||||
|
||||
let chunk_col_stride: usize = data_row_bits
|
||||
.div_ceil(8)
|
||||
.checked_mul(u64::from(tiff_chunk_dimensions.1))
|
||||
.ok_or(TiffError::LimitsExceeded)?
|
||||
.try_into()?;
|
||||
|
||||
let plane_stride: usize = data_row_bits
|
||||
.div_ceil(8)
|
||||
.checked_mul(u64::from(data_dimensions.1))
|
||||
.ok_or(TiffError::LimitsExceeded)?
|
||||
.try_into()?;
|
||||
|
||||
let minimum_row_stride = data_row_bytes;
|
||||
|
||||
let chunks_across: u32 = data_dimensions.0.div_ceil(tiff_chunk_dimensions.0);
|
||||
let chunks_per_plane = (self.chunk_offsets.len() as u32) / u32::from(strips_per_pixel);
|
||||
|
||||
// We would not get an offset in byte units, sorry, no bit interleaving in the output.
|
||||
if chunks_across > 1 && chunk_row_bits % 8 != 0 {
|
||||
return Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::MisalignedTileBoundaries,
|
||||
));
|
||||
}
|
||||
|
||||
// Only this color type interprets the tag, which is defined with a default of (2, 2)
|
||||
if matches!(color, ColorType::YCbCr(_)) && self.chroma_subsampling != (1, 1) {
|
||||
// The JPEG library does upsampling for us and defines its buffers correctly
|
||||
// (presumably). All other compression schemes are not supported..
|
||||
//
|
||||
// NOTE: as explained in <fa225e820b96bef35f01bf4685654beeb4a8df0c> we may be better
|
||||
// off supporting this tag by consistently upsampling, not by adjusting the buffer
|
||||
// size. At least as a default this makes more sense and is much more permissive in
|
||||
// case the compression stream disagrees with the tags (we would not have enough / or
|
||||
// the wrong buffer layout if we only asked for subsampled planes in a planar layout).
|
||||
if !matches!(self.compression_method, CompressionMethod::ModernJPEG) {
|
||||
return Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::ChromaSubsampling,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ReadoutLayout {
|
||||
planar_config: self.planar_config,
|
||||
color,
|
||||
minimum_row_stride,
|
||||
sample_format: self.sample_format,
|
||||
row_stride: data_row_bytes,
|
||||
chunk_row_stride: chunk_row_bytes,
|
||||
chunk_col_stride,
|
||||
plane_stride,
|
||||
tiff_bits_per_sample,
|
||||
tiff_samples,
|
||||
tiff_chunk_dimensions,
|
||||
tiff_row_bytes,
|
||||
chunks_across,
|
||||
chunks_per_plane,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn chunk_data_dimensions(&self, chunk_index: u32) -> TiffResult<(u32, u32)> {
|
||||
let dims = self.chunk_dimensions()?;
|
||||
|
||||
match self.chunk_type {
|
||||
ChunkType::Strip => {
|
||||
let strip_attrs = self.strip_decoder.as_ref().unwrap();
|
||||
let strips_per_band =
|
||||
self.height.saturating_sub(1) / strip_attrs.rows_per_strip + 1;
|
||||
let rows_per_strip = dims.1;
|
||||
let strips_per_band = self.height.div_ceil(rows_per_strip);
|
||||
|
||||
let strip_height_without_padding = (chunk_index % strips_per_band)
|
||||
.checked_mul(dims.1)
|
||||
.and_then(|x| self.height.checked_sub(x))
|
||||
@@ -555,15 +813,22 @@ impl Image {
|
||||
|
||||
pub(crate) fn expand_chunk(
|
||||
&self,
|
||||
reader: impl Read,
|
||||
reader: &mut ValueReader<impl Read + Seek>,
|
||||
buf: &mut [u8],
|
||||
output_row_stride: usize,
|
||||
byte_order: ByteOrder,
|
||||
layout: &ReadoutLayout,
|
||||
chunk_index: u32,
|
||||
limits: &Limits,
|
||||
) -> TiffResult<()> {
|
||||
let ValueReader {
|
||||
reader,
|
||||
bigtiff: _,
|
||||
limits,
|
||||
} = reader;
|
||||
|
||||
let byte_order = reader.byte_order;
|
||||
|
||||
// Validate that the color type is supported.
|
||||
let color_type = self.colortype()?;
|
||||
let color_type = layout.color;
|
||||
|
||||
match color_type {
|
||||
ColorType::RGB(n)
|
||||
| ColorType::RGBA(n)
|
||||
@@ -625,6 +890,7 @@ impl Image {
|
||||
.ok_or(TiffError::FormatError(
|
||||
TiffFormatError::InconsistentSizesEncountered,
|
||||
))?;
|
||||
|
||||
if *compressed_bytes > limits.intermediate_buffer_size as u64 {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
@@ -632,38 +898,44 @@ impl Image {
|
||||
let compression_method = self.compression_method;
|
||||
let photometric_interpretation = self.photometric_interpretation;
|
||||
let predictor = self.predictor;
|
||||
let samples = self.samples_per_pixel();
|
||||
|
||||
let samples = layout.tiff_samples;
|
||||
let data_samples = layout.samples_per_out_texel();
|
||||
|
||||
// We have two dimensions: the 2d rectangle of encoded data and the 2d rectangle this
|
||||
// takes up in the output. Each has an associated count of bits per pixel. The first
|
||||
// dimension, i.e. a ''row'', is the number of pixels that are encoded with bit packing
|
||||
// while the second is the byte-padded array of each so encoded slices.
|
||||
//
|
||||
// During decoding we map the relevant bits from one to the other.
|
||||
let chunk_dims = self.chunk_dimensions()?;
|
||||
let data_dims = self.chunk_data_dimensions(chunk_index)?;
|
||||
|
||||
let chunk_row_bits = (u64::from(chunk_dims.0) * u64::from(self.bits_per_sample))
|
||||
.checked_mul(samples as u64)
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
let chunk_row_bytes: usize = ((chunk_row_bits + 7) / 8).try_into()?;
|
||||
|
||||
let data_row_bits = (u64::from(data_dims.0) * u64::from(self.bits_per_sample))
|
||||
.checked_mul(samples as u64)
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
let data_row_bytes: usize = ((data_row_bits + 7) / 8).try_into()?;
|
||||
let chunk_row_bytes: usize = layout.tiff_row_bytes;
|
||||
let data_row_bytes: usize = layout.chunk_row_bytes(data_dims.0)?;
|
||||
|
||||
// TODO: Should these return errors instead?
|
||||
assert!(output_row_stride >= data_row_bytes);
|
||||
assert!(buf.len() >= output_row_stride * (data_dims.1 as usize - 1) + data_row_bytes);
|
||||
assert!(layout.minimum_row_stride >= data_row_bytes);
|
||||
assert!(buf.len() >= layout.row_stride * (data_dims.1 as usize - 1) + data_row_bytes);
|
||||
|
||||
let is_all_bits = samples == data_samples;
|
||||
let is_output_chunk_rows = layout.row_stride == chunk_row_bytes;
|
||||
|
||||
let mut reader = Self::create_reader(
|
||||
reader,
|
||||
photometric_interpretation,
|
||||
reader.inner(),
|
||||
compression_method,
|
||||
*compressed_bytes,
|
||||
self.jpeg_tables.as_deref().map(|a| &**a),
|
||||
chunk_dims,
|
||||
self.samples,
|
||||
)?;
|
||||
|
||||
if output_row_stride == chunk_row_bytes as usize {
|
||||
if is_output_chunk_rows && is_all_bits {
|
||||
// Here we can read directly into the output buffer itself.
|
||||
let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize];
|
||||
reader.read_exact(tile)?;
|
||||
|
||||
for row in tile.chunks_mut(chunk_row_bytes as usize) {
|
||||
for row in tile.chunks_mut(chunk_row_bytes) {
|
||||
super::fix_endianness_and_predict(
|
||||
row,
|
||||
color_type.bit_depth(),
|
||||
@@ -672,14 +944,15 @@ impl Image {
|
||||
predictor,
|
||||
);
|
||||
}
|
||||
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
super::invert_colors(tile, color_type, self.sample_format);
|
||||
super::invert_colors(tile, color_type, self.sample_format)?;
|
||||
}
|
||||
} else if chunk_row_bytes > data_row_bytes && self.predictor == Predictor::FloatingPoint {
|
||||
// The floating point predictor shuffles the padding bytes into the encoded output, so
|
||||
// this case is handled specially when needed.
|
||||
let mut encoded = vec![0u8; chunk_row_bytes];
|
||||
for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) {
|
||||
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
|
||||
reader.read_exact(&mut encoded)?;
|
||||
|
||||
let row = &mut row[..data_row_bytes];
|
||||
@@ -690,18 +963,19 @@ impl Image {
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
super::invert_colors(row, color_type, self.sample_format);
|
||||
super::invert_colors(row, color_type, self.sample_format)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (i, row) in buf
|
||||
.chunks_mut(output_row_stride)
|
||||
.take(data_dims.1 as usize)
|
||||
.enumerate()
|
||||
{
|
||||
} else if is_all_bits {
|
||||
// We read row-by-row but each row fits in its output buffer.
|
||||
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
|
||||
let row = &mut row[..data_row_bytes];
|
||||
reader.read_exact(row)?;
|
||||
let used = data_row_bytes.min(chunk_row_bytes);
|
||||
|
||||
// Two ways how we get here: we have more bytes in our chunk data than in the image
|
||||
// we are to read. Then we need to skip the rest of the data. Or we have a bigger
|
||||
// row stride than the chunk contains data, then we need to fill only the front.
|
||||
reader.read_exact(&mut row[..used])?;
|
||||
// Skip horizontal padding
|
||||
if chunk_row_bytes > data_row_bytes {
|
||||
let len = u64::try_from(chunk_row_bytes - data_row_bytes)?;
|
||||
@@ -715,12 +989,183 @@ impl Image {
|
||||
byte_order,
|
||||
predictor,
|
||||
);
|
||||
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
super::invert_colors(row, color_type, self.sample_format);
|
||||
super::invert_colors(row, color_type, self.sample_format)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The encoded data potentially takes up more space than the output data so we must be
|
||||
// prepared to discard some of it. That decision is bit-by-bit.
|
||||
let bits_per_pixel = u32::from(self.bits_per_sample) * u32::from(self.samples);
|
||||
// Assumes the photometric samples are always the start.. This is slightly problematic.
|
||||
// To expand spport we should instead have different methods of transforming the read
|
||||
// buffer data, not only the `compact_photometric_bytes` method below and then choose
|
||||
// from the right one with supplied parameters. Then we can also bit-for-bit copy with
|
||||
// a selection for better performance.
|
||||
let photometric_bit_end = u32::from(self.bits_per_sample) * data_samples as u32;
|
||||
|
||||
debug_assert!(bits_per_pixel >= photometric_bit_end);
|
||||
|
||||
if bits_per_pixel % 8 != 0 || photometric_bit_end % 8 != 0 {
|
||||
return Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
self.photometric_interpretation,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let photo_range = photometric_bit_end / 8..bits_per_pixel / 8;
|
||||
let mut encoded = vec![0u8; chunk_row_bytes];
|
||||
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
|
||||
reader.read_exact(&mut encoded)?;
|
||||
|
||||
Self::compact_photometric_bytes(&mut encoded, row, &photo_range);
|
||||
|
||||
super::fix_endianness_and_predict(
|
||||
row,
|
||||
color_type.bit_depth(),
|
||||
samples,
|
||||
byte_order,
|
||||
predictor,
|
||||
);
|
||||
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
super::invert_colors(row, color_type, self.sample_format)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Turn a contiguous buffer of a whole number of raw sample arrays into a whole number of
|
||||
/// photometric sample arrays by removing the extra samples in-between.
|
||||
fn compact_photometric_bytes(
|
||||
raw: &mut [u8],
|
||||
row: &mut [u8],
|
||||
photo_range: &std::ops::Range<u32>,
|
||||
) {
|
||||
raw.chunks_exact_mut(photo_range.end as usize)
|
||||
.zip(row.chunks_exact_mut(photo_range.start as usize))
|
||||
.for_each(|(src, dst)| {
|
||||
dst.copy_from_slice(&src[..photo_range.start as usize]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadoutLayout {
|
||||
pub(crate) fn samples_per_out_texel(&self) -> u16 {
|
||||
match self.planar_config {
|
||||
PlanarConfiguration::Chunky => self.color.num_samples(),
|
||||
PlanarConfiguration::Planar => 1,
|
||||
}
|
||||
}
|
||||
|
||||
// For a concrete chunk, which may be a partial border chunk, the byte length of one row of its
|
||||
// pixel data.
|
||||
pub(crate) fn chunk_row_bytes(&self, width: u32) -> TiffResult<usize> {
|
||||
let data_samples = self.samples_per_out_texel();
|
||||
let data_row_bits = (u64::from(width) * u64::from(self.tiff_bits_per_sample))
|
||||
.checked_mul(u64::from(data_samples))
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
Ok(data_row_bits.div_ceil(8).try_into()?)
|
||||
}
|
||||
|
||||
pub(crate) fn set_row_stride(&mut self, row_stride: usize) -> Result<(), TiffError> {
|
||||
if row_stride < self.minimum_row_stride {
|
||||
return Err(TiffError::UsageError(
|
||||
UsageError::InsufficientOutputRowStride {
|
||||
needed: self.minimum_row_stride,
|
||||
requested: row_stride,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let data_row_bytes = u64::try_from(row_stride)?;
|
||||
|
||||
let chunk_col_stride = data_row_bytes
|
||||
.checked_mul(u64::from(self.tiff_chunk_dimensions.1))
|
||||
.ok_or(TiffError::LimitsExceeded)?
|
||||
.try_into()?;
|
||||
|
||||
let height = self.plane_stride.checked_div(self.row_stride);
|
||||
|
||||
let plane_stride = height
|
||||
.and_then(|h| data_row_bytes.checked_mul(h as u64))
|
||||
// If height was zero, or the previous stride was zero, there are no bytes in a plane
|
||||
.unwrap_or(0)
|
||||
.try_into()?;
|
||||
|
||||
self.row_stride = row_stride;
|
||||
self.chunk_col_stride = chunk_col_stride;
|
||||
self.plane_stride = plane_stride;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reduce this down to the layout of the output planes.
|
||||
pub(crate) fn to_plane_layout(&self) -> Result<PlaneLayout, TiffError> {
|
||||
let num_planes = self.color.num_samples() / self.samples_per_out_texel();
|
||||
|
||||
// Using the standard range iterator as checked_add on steroids.
|
||||
//
|
||||
// Note: for supporting subsampling, adjust as required.
|
||||
let mut offset = (0..=usize::MAX).step_by(self.plane_stride);
|
||||
|
||||
let plane_offsets = offset.by_ref().take(usize::from(num_planes)).collect();
|
||||
|
||||
// Get the past-the-end of the last plane.
|
||||
//
|
||||
// This also verifies the `take` above was not short.
|
||||
let Some(total_bytes) = offset.next() else {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
};
|
||||
|
||||
Ok(PlaneLayout {
|
||||
plane_offsets,
|
||||
total_bytes,
|
||||
readout: self.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A `ReadoutLayout` with pre-calculated plane information.
|
||||
pub(crate) struct PlaneLayout {
|
||||
/// The underlying readout layout.
|
||||
pub readout: ReadoutLayout,
|
||||
/// Buffer offset from one plane of output to the next.
|
||||
pub plane_offsets: Vec<usize>,
|
||||
/// Total number of bytes for all planes in given order.
|
||||
pub total_bytes: usize,
|
||||
}
|
||||
|
||||
impl PlaneLayout {
|
||||
/// Return the number of planes to extract into the provided buffer.
|
||||
pub(crate) fn used_planes(&self, buffer: &[impl Sized]) -> TiffResult<u16> {
|
||||
self.readout.assert_min_layout(buffer)?;
|
||||
let buffer_len = core::mem::size_of_val(buffer);
|
||||
|
||||
// Note: with differently sized planes this is dependent on the plane.
|
||||
let last_plane_start = buffer_len.checked_sub(self.readout.plane_stride);
|
||||
|
||||
// Find how many planes fit into the output buffer.
|
||||
let used_plane_offsets = self
|
||||
.plane_offsets
|
||||
.iter()
|
||||
.enumerate()
|
||||
// Find the first plane that would not fit completely at its offset.
|
||||
.skip_while(|(_, &offset)| last_plane_start >= Some(offset))
|
||||
.nth(0)
|
||||
// If all planes fit, use all of them.
|
||||
.map_or(self.plane_offsets.len(), |(idx, _)| idx);
|
||||
|
||||
debug_assert!(
|
||||
used_plane_offsets <= usize::from(u16::MAX),
|
||||
"Planes limited by number of samples, which is encoded as u16"
|
||||
);
|
||||
|
||||
Ok(used_plane_offsets as u16)
|
||||
}
|
||||
}
|
||||
+1220
-398
File diff suppressed because it is too large.
Load diff
+178
-39
@@ -1,15 +1,9 @@
|
||||
//! All IO functionality needed for TIFF decoding
|
||||
|
||||
#[cfg(feature = "webp")]
|
||||
use std::io::Cursor;
|
||||
use std::io::{self, BufRead, BufReader, Read, Seek, Take};
|
||||
|
||||
/// 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.
|
||||
#[derive(Debug)]
|
||||
@@ -18,7 +12,7 @@ pub struct EndianReader<R> {
|
||||
pub(crate) byte_order: ByteOrder,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> EndianReader<R> {
|
||||
impl<R: Read> EndianReader<R> {
|
||||
pub fn new(reader: R, byte_order: ByteOrder) -> Self {
|
||||
Self { reader, byte_order }
|
||||
}
|
||||
@@ -27,7 +21,10 @@ impl<R: Read + Seek> EndianReader<R> {
|
||||
&mut self.reader
|
||||
}
|
||||
|
||||
pub fn goto_offset(&mut self, offset: u64) -> io::Result<()> {
|
||||
pub fn goto_offset(&mut self, offset: u64) -> io::Result<()>
|
||||
where
|
||||
R: Seek,
|
||||
{
|
||||
self.reader.seek(io::SeekFrom::Start(offset))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -43,17 +40,6 @@ impl<R: Read + Seek> EndianReader<R> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads an i8
|
||||
#[inline(always)]
|
||||
pub fn read_i8(&mut self) -> Result<i8, io::Error> {
|
||||
let mut n = [0u8; 1];
|
||||
self.reader.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order {
|
||||
ByteOrder::LittleEndian => i8::from_le_bytes(n),
|
||||
ByteOrder::BigEndian => i8::from_be_bytes(n),
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads an i16
|
||||
#[inline(always)]
|
||||
pub fn read_i16(&mut self) -> Result<i16, io::Error> {
|
||||
@@ -132,20 +118,17 @@ impl<R: Read + Seek> EndianReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// # 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")]
|
||||
@@ -158,12 +141,15 @@ pub struct LZWReader<R: Read> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,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",
|
||||
@@ -201,10 +184,11 @@ impl<R: Read> Read for LZWReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// ## PackBits Reader
|
||||
///
|
||||
//
|
||||
// ## PackBits Reader
|
||||
//
|
||||
|
||||
/// Internal state machine for the PackBitsReader.
|
||||
enum PackBitsReaderState {
|
||||
Header,
|
||||
Literal,
|
||||
@@ -272,6 +256,161 @@ impl<R: Read> Read for PackBitsReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "fax")]
|
||||
pub struct Group4Reader<R: Read> {
|
||||
decoder: fax34::decoder::Group4Decoder<io::Bytes<io::BufReader<io::Take<R>>>>,
|
||||
line_buf: io::Cursor<Vec<u8>>,
|
||||
height: u16,
|
||||
width: u16,
|
||||
y: u16,
|
||||
}
|
||||
|
||||
#[cfg(feature = "fax")]
|
||||
impl<R: Read> Group4Reader<R> {
|
||||
pub fn new(
|
||||
dimensions: (u32, u32),
|
||||
reader: R,
|
||||
compressed_length: u64,
|
||||
) -> crate::TiffResult<Self> {
|
||||
let width = u16::try_from(dimensions.0)?;
|
||||
let height = u16::try_from(dimensions.1)?;
|
||||
|
||||
Ok(Self {
|
||||
decoder: fax34::decoder::Group4Decoder::new(
|
||||
io::BufReader::new(reader.take(compressed_length)).bytes(),
|
||||
width,
|
||||
)?,
|
||||
line_buf: io::Cursor::new(Vec::with_capacity(width.into())),
|
||||
width,
|
||||
height,
|
||||
y: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "fax")]
|
||||
impl<R: Read> Read for Group4Reader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
// Either we have not read any line or we are at the end of a line.
|
||||
if self.line_buf.position() as usize == self.line_buf.get_ref().len()
|
||||
&& self.y < self.height
|
||||
{
|
||||
let next = self.decoder.advance().map_err(std::io::Error::other)?;
|
||||
|
||||
match next {
|
||||
fax34::decoder::DecodeStatus::End => (),
|
||||
fax34::decoder::DecodeStatus::Incomplete => {
|
||||
self.y += 1;
|
||||
|
||||
// We known `transitions` yields exactly `self.width` items (per doc).
|
||||
// FIXME: performance. We do not need an individual pixel iterator, filling
|
||||
// memory especially for long runs can be much quicker. The `transitions` are
|
||||
// the positions at which each run-length ends, i.e. the prefix sum of run
|
||||
// lengths. Runs in fax4 start with white.
|
||||
let transitions = fax34::decoder::pels(self.decoder.transition(), self.width);
|
||||
|
||||
let buffer = self.line_buf.get_mut();
|
||||
buffer.resize(usize::from(self.width).div_ceil(8), 0u8);
|
||||
|
||||
let target = &mut buffer[..];
|
||||
|
||||
// Note: it may seem strange to treat black as 0b1 and white as 0b0 despite all
|
||||
// our streams by default decoding as-if PhotometricInterpretation::BlackIsMin.
|
||||
// This is however consistent with libtiff. It seems that fax4's "White"
|
||||
// differs from what libtiff thinks of as "White". For content, a line of data
|
||||
// is in runlength encoding of white-black-white-black always starting with
|
||||
// white. In libtiff, the loop always does both colors in one go and the
|
||||
// structure is:
|
||||
//
|
||||
// ```
|
||||
// for (; runs < erun; runs += 2)
|
||||
// // white run
|
||||
// do { *lp++ = 0L; } while (…)
|
||||
// // black run
|
||||
// do { *lp++ = -1L; } while (…)
|
||||
// ```
|
||||
//
|
||||
// So indeed the Fax4::White run is implemented by filling with zeros.
|
||||
let mut bits = transitions.map(|c| match c {
|
||||
fax34::Color::Black => true,
|
||||
fax34::Color::White => false,
|
||||
});
|
||||
|
||||
// Assemble bits in MSB as per our library representation for buffer.
|
||||
for byte in target {
|
||||
let mut val = 0;
|
||||
|
||||
for (idx, bit) in bits.by_ref().take(8).enumerate() {
|
||||
val |= u8::from(bit) << (7 - idx % 8);
|
||||
}
|
||||
|
||||
*byte = val;
|
||||
}
|
||||
|
||||
self.line_buf.set_position(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.line_buf.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webp")]
|
||||
pub struct WebPReader {
|
||||
inner: Cursor<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "webp")]
|
||||
impl WebPReader {
|
||||
pub fn new<R: Read + Seek>(
|
||||
reader: R,
|
||||
compressed_length: u64,
|
||||
samples: u16,
|
||||
) -> crate::TiffResult<Self> {
|
||||
let mut decoder =
|
||||
image_webp::WebPDecoder::new(io::BufReader::new(reader.take(compressed_length)))
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
if !(samples == 4 || (samples == 3 && !decoder.has_alpha())) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"bad sample count for WebP compressed data",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let total_bytes =
|
||||
samples as usize * decoder.dimensions().0 as usize * decoder.dimensions().1 as usize;
|
||||
let mut data = vec![0; total_bytes];
|
||||
|
||||
decoder
|
||||
.read_image(&mut data)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
// Add a fully opaque alpha channel if needed
|
||||
if samples == 4 && !decoder.has_alpha() {
|
||||
for i in (0..(total_bytes / 4)).rev() {
|
||||
data[i * 4 + 3] = 255;
|
||||
data[i * 4 + 2] = data[i * 3 + 2];
|
||||
data[i * 4 + 1] = data[i * 3 + 1];
|
||||
data[i * 4] = data[i * 3];
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
inner: Cursor::new(data),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webp")]
|
||||
impl Read for WebPReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.inner.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
+77
-16
@@ -1,25 +1,85 @@
|
||||
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::EndianReader;
|
||||
use super::Limits;
|
||||
use super::ifd::{Entry, Value};
|
||||
use super::ValueReader;
|
||||
|
||||
pub(crate) struct TagReader<'a, R: Read + Seek> {
|
||||
pub reader: &'a mut EndianReader<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) 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,
|
||||
})
|
||||
|
||||
pub(crate) trait EntryDecoder {
|
||||
/// Turn an entry into a value by fetching the required bytes in the stream.
|
||||
fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value>;
|
||||
/// Fill a `ValueBuffer` by fetching the required bytes.
|
||||
fn entry_buf(&mut self, entry: &Entry, _: &mut ValueBuffer) -> TiffResult<()>;
|
||||
/// Fill the prefix of the buffer and return the actual bytes. Start interpreting the inner
|
||||
/// value at `offset`.
|
||||
fn entry_raw(&mut self, entry: &Entry, _: &mut [u8], offset: u64) -> TiffResult<usize>;
|
||||
}
|
||||
|
||||
impl<R: Seek + Read> EntryDecoder for ValueReader<R> {
|
||||
fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value> {
|
||||
entry.val(&self.limits, self.bigtiff, &mut self.reader)
|
||||
}
|
||||
|
||||
fn entry_buf(&mut self, entry: &Entry, buf: &mut ValueBuffer) -> TiffResult<()> {
|
||||
entry.buffered_value(buf, &self.limits, self.bigtiff, &mut self.reader)
|
||||
}
|
||||
|
||||
fn entry_raw(&mut self, entry: &Entry, buf: &mut [u8], offset: u64) -> TiffResult<usize> {
|
||||
entry.raw_value_at(buf, self.bigtiff, &mut self.reader, offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R: EntryDecoder + ?Sized> TagReader<'a, R> {
|
||||
pub(crate) fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<Value>> {
|
||||
let entry = match self.ifd.get(tag) {
|
||||
None => return Ok(None),
|
||||
Some(entry) => entry.clone(),
|
||||
};
|
||||
|
||||
self.decoder.entry_val(&entry).map(Some)
|
||||
}
|
||||
|
||||
/// Find a tag and fill the provided buffer with its raw data.
|
||||
///
|
||||
/// Returns the entry of the tag if found. This signals that the buffer was overwritten.
|
||||
/// Otherwise the buffer is preserved as-is.
|
||||
pub(crate) fn find_tag_buf(
|
||||
&mut self,
|
||||
tag: Tag,
|
||||
buf: &mut ValueBuffer,
|
||||
) -> TiffResult<Option<Entry>> {
|
||||
let entry = match self.ifd.get(tag) {
|
||||
None => return Ok(None),
|
||||
Some(entry) => entry.clone(),
|
||||
};
|
||||
|
||||
self.decoder.entry_buf(&entry, buf)?;
|
||||
Ok(Some(entry))
|
||||
}
|
||||
|
||||
pub(crate) fn find_tag_raw(
|
||||
&mut self,
|
||||
tag: Tag,
|
||||
buf: &mut [u8],
|
||||
at: u64,
|
||||
) -> TiffResult<Option<usize>> {
|
||||
let entry = match self.ifd.get(tag) {
|
||||
None => return Ok(None),
|
||||
Some(entry) => entry.clone(),
|
||||
};
|
||||
|
||||
let len = self.decoder.entry_raw(&entry, buf, at)?;
|
||||
Ok(Some(len))
|
||||
}
|
||||
|
||||
pub(crate) fn require_tag(&mut self, tag: Tag) -> TiffResult<Value> {
|
||||
match self.find_tag(tag)? {
|
||||
Some(val) => Ok(val),
|
||||
@@ -28,6 +88,7 @@ impl<'a, R: Read + Seek> TagReader<'a, R> {
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_tag_uint_vec<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<Option<Vec<T>>> {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,21 +11,17 @@ pub struct Deflate {
|
||||
/// It allows trading compression ratio for compression speed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Default)]
|
||||
pub enum DeflateLevel {
|
||||
/// The fastest possible compression mode.
|
||||
Fast = 1,
|
||||
/// The conserative choice between speed and ratio.
|
||||
#[default]
|
||||
Balanced = 6,
|
||||
/// The best compression available with Deflate.
|
||||
Best = 9,
|
||||
}
|
||||
|
||||
impl 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 {
|
||||
|
||||
@@ -14,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+511
-100
@@ -2,17 +2,20 @@ pub use tiff_value::*;
|
||||
|
||||
use std::{
|
||||
cmp,
|
||||
collections::BTreeMap,
|
||||
io::{self, Seek, Write},
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
num::TryFromIntError,
|
||||
num::{NonZeroU64, TryFromIntError},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
decoder::ifd::Entry,
|
||||
error::{TiffResult, UsageError},
|
||||
tags::{CompressionMethod, ResolutionUnit, SampleFormat, Tag},
|
||||
TiffError, TiffFormatError,
|
||||
tags::{
|
||||
ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation,
|
||||
ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
|
||||
},
|
||||
Directory, TiffError, TiffFormatError,
|
||||
};
|
||||
|
||||
pub mod colortype;
|
||||
@@ -37,8 +40,9 @@ pub type Predictor = crate::tags::Predictor;
|
||||
#[cfg(feature = "deflate")]
|
||||
pub type DeflateLevel = compression::DeflateLevel;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Default)]
|
||||
pub enum Compression {
|
||||
#[default]
|
||||
Uncompressed,
|
||||
#[cfg(feature = "lzw")]
|
||||
Lzw,
|
||||
@@ -47,12 +51,6 @@ pub enum Compression {
|
||||
Packbits,
|
||||
}
|
||||
|
||||
impl Default for Compression {
|
||||
fn default() -> Self {
|
||||
Self::Uncompressed
|
||||
}
|
||||
}
|
||||
|
||||
impl Compression {
|
||||
fn tag(&self) -> CompressionMethod {
|
||||
match self {
|
||||
@@ -79,10 +77,10 @@ impl Compression {
|
||||
|
||||
/// 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
|
||||
/// ```
|
||||
@@ -107,6 +105,8 @@ pub struct TiffEncoder<W, K: TiffKind = TiffKindStandard> {
|
||||
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.
|
||||
@@ -135,16 +135,19 @@ 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,
|
||||
};
|
||||
|
||||
K::write_header(&mut encoder.writer)?;
|
||||
|
||||
Ok(encoder)
|
||||
last_ifd_chain,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the predictor to use
|
||||
@@ -165,8 +168,27 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -174,8 +196,8 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> TiffResult<ImageEncoder<W, C, K>> {
|
||||
let encoder = DirectoryEncoder::new(&mut self.writer)?;
|
||||
) -> 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)
|
||||
}
|
||||
|
||||
@@ -189,11 +211,23 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
|
||||
where
|
||||
[C::Inner]: TiffValue,
|
||||
{
|
||||
let encoder = DirectoryEncoder::new(&mut self.writer)?;
|
||||
let encoder = Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)?;
|
||||
let image: ImageEncoder<W, C, K> =
|
||||
ImageEncoder::new(encoder, width, height, self.compression, self.predictor)?;
|
||||
image.write_data(data)
|
||||
}
|
||||
|
||||
fn chain_directory<'lt>(
|
||||
writer: &'lt mut TiffWriter<W>,
|
||||
last_ifd_chain: &'lt mut NonZeroU64,
|
||||
) -> TiffResult<DirectoryEncoder<'lt, W, K>> {
|
||||
let last_ifd = *last_ifd_chain;
|
||||
DirectoryEncoder::new(writer, Some(last_ifd), Some(last_ifd_chain))
|
||||
}
|
||||
|
||||
fn unchained_directory(writer: &mut TiffWriter<W>) -> TiffResult<DirectoryEncoder<'_, W, K>> {
|
||||
DirectoryEncoder::new(writer, None, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Low level interface to encode ifd directories.
|
||||
@@ -202,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(self.writer, self.ifd.len())?;
|
||||
for (
|
||||
tag,
|
||||
DirectoryEntry {
|
||||
data_type: field_type,
|
||||
count,
|
||||
data: offset,
|
||||
},
|
||||
) in self.ifd.iter()
|
||||
{
|
||||
self.writer.write_u16(*tag)?;
|
||||
self.writer.write_u16(*field_type)?;
|
||||
(*count).write(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(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() % usize::from(ty.byte_len()) != 0 {
|
||||
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)?;
|
||||
self.writer.goto_offset(prior.get())?;
|
||||
// Note how we are not writing the `offset` type itself here as doing so would need to
|
||||
// go through the `TiffValue` trait—the type is not constrained by much. But for the
|
||||
// trait we need a `TiffWriter` which comes with a bunch of additional state such as
|
||||
// compressor etc. that we have no need for.
|
||||
K::write_offset(self.writer, ifd_pointer)?;
|
||||
|
||||
self.writer.goto_offset(curr_pos)?;
|
||||
}
|
||||
|
||||
K::write_offset(self.writer, 0)?;
|
||||
|
||||
let ifd_chain = NonZeroU64::new(self.writer.previous_ifd_pointer::<K>())
|
||||
.expect("IFD chain field is at a non-zero offset");
|
||||
|
||||
if let Some(prior) = self.write_chain.take() {
|
||||
*prior = ifd_chain;
|
||||
}
|
||||
|
||||
self.dropped = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +622,7 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> {
|
||||
row_samples: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
extra_samples: Vec<u16>,
|
||||
rows_per_strip: u64,
|
||||
strip_offsets: Vec<K::OffsetType>,
|
||||
strip_byte_count: Vec<K::OffsetType>,
|
||||
@@ -411,22 +668,25 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
let rows_per_strip = {
|
||||
match compression.tag() {
|
||||
CompressionMethod::PackBits => 1, // Each row must be packed separately. Do not compress across row boundaries
|
||||
_ => (1_000_000 + 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, 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(
|
||||
@@ -435,7 +695,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
)?;
|
||||
encoder.write_tag(Tag::XResolution, Rational { n: 1, d: 1 })?;
|
||||
encoder.write_tag(Tag::YResolution, Rational { n: 1, d: 1 })?;
|
||||
encoder.write_tag(Tag::ResolutionUnit, ResolutionUnit::None.to_u16())?;
|
||||
encoder.write_tag(Tag::ResolutionUnit, ResolutionUnit::None)?;
|
||||
|
||||
Ok(ImageEncoder {
|
||||
encoder,
|
||||
@@ -443,6 +703,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
strip_idx: 0,
|
||||
row_samples,
|
||||
rows_per_strip,
|
||||
extra_samples: vec![],
|
||||
width,
|
||||
height,
|
||||
strip_offsets: Vec::new(),
|
||||
@@ -454,6 +715,27 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn extra_samples(&mut self, extra: &[ExtraSamples]) -> Result<(), TiffError> {
|
||||
if self.strip_idx != 0 {
|
||||
return Err(TiffError::UsageError(
|
||||
UsageError::ReconfiguredAfterImageWrite,
|
||||
));
|
||||
}
|
||||
|
||||
let samples = self.extra_samples.len() + extra.len() + <T>::BITS_PER_SAMPLE.len();
|
||||
let row_samples = u64::from(self.width) * u64::try_from(samples)?;
|
||||
|
||||
self.extra_samples
|
||||
.extend(extra.iter().map(ExtraSamples::to_u16));
|
||||
|
||||
self.encoder
|
||||
.write_tag(Tag::ExtraSamples, &self.extra_samples[..])?;
|
||||
|
||||
self.row_samples = row_samples;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Number of samples the next strip should have.
|
||||
pub fn next_strip_sample_count(&self) -> u64 {
|
||||
if self.strip_idx >= self.strip_count {
|
||||
@@ -584,13 +866,46 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
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(
|
||||
@@ -609,7 +924,8 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
|
||||
/// Write out image and ifd directory.
|
||||
pub fn finish(mut self) -> TiffResult<()> {
|
||||
self.finish_internal()
|
||||
self.finish_internal()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,10 +937,10 @@ impl<'a, W: Write + Seek, C: ColorType, K: TiffKind> Drop for ImageEncoder<'a, W
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -669,6 +985,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;
|
||||
|
||||
@@ -737,3 +1118,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,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
|
||||
}
|
||||
@@ -186,3 +190,64 @@ impl<W: Seek> TiffWriter<W> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoder_on_short_writes() {
|
||||
struct ShortWriter<T> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
impl<T: std::io::Write> std::io::Write for ShortWriter<T> {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let lower_bound = buf.len().min(1);
|
||||
let lower = buf.len().saturating_sub(1).max(lower_bound);
|
||||
|
||||
self.inner.write(&buf[..lower])
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::io::Seek> std::io::Seek for ShortWriter<T> {
|
||||
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
|
||||
self.inner.seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
let mut write = ShortWriter {
|
||||
inner: std::io::Cursor::new(vec![0u8; 0]),
|
||||
};
|
||||
|
||||
let imgdata = vec![42u8; 100 * 100];
|
||||
let mut data = crate::decoder::DecodingResult::U8(vec![]);
|
||||
|
||||
for comp in [
|
||||
super::Compression::Uncompressed,
|
||||
super::Compression::Packbits,
|
||||
] {
|
||||
write.inner.set_position(0);
|
||||
write.inner.get_mut().clear();
|
||||
|
||||
{
|
||||
let mut tiff_writer = super::TiffEncoder::new(&mut write)
|
||||
.unwrap()
|
||||
.with_compression(comp);
|
||||
|
||||
tiff_writer
|
||||
.write_image::<super::Gray8>(100, 100, &imgdata)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
write.inner.set_position(0);
|
||||
let mut tiff_reader = crate::decoder::Decoder::new(&mut write.inner).unwrap();
|
||||
let _layout = tiff_reader.read_image_to_buffer(&mut data).unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&data, crate::decoder::DecodingResult::U8(v) if *v == imgdata),
|
||||
"{data:?} @ {}",
|
||||
data.as_buffer(0).byte_len()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use std::string;
|
||||
use quick_error::quick_error;
|
||||
|
||||
use crate::decoder::ChunkType;
|
||||
use crate::tags::Type;
|
||||
use crate::tags::{
|
||||
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag,
|
||||
};
|
||||
@@ -90,6 +91,9 @@ quick_error! {
|
||||
InvalidTypeForTag {
|
||||
display("tag has invalid type")
|
||||
}
|
||||
InvalidCountForTag(tag: Tag, len: usize) {
|
||||
display("tag `{tag:?}` with incorrect number of elements ({len}) encountered")
|
||||
}
|
||||
StripTileTagConflict {
|
||||
display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found")
|
||||
}
|
||||
@@ -156,6 +160,9 @@ quick_error! {
|
||||
UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
|
||||
display("unsupported photometric interpretation \"{interpretation:?}\"")
|
||||
}
|
||||
ChromaSubsampling {
|
||||
display("chroma subsampling of YCbCr color is unsupported")
|
||||
}
|
||||
MisalignedTileBoundaries {
|
||||
display("tile rows are not aligned to byte boundaries")
|
||||
}
|
||||
@@ -175,6 +182,12 @@ quick_error! {
|
||||
InvalidChunkIndex(index: u32) {
|
||||
display("invalid chunk index ({index}) requested")
|
||||
}
|
||||
InvalidPlaneIndex(index: u16) {
|
||||
display("invalid plane index ({index}) requested")
|
||||
}
|
||||
InvalidCodingUnit(index: u32, have: u32) {
|
||||
display("out of bounds coding unit ({index}) requested, only {have} available")
|
||||
}
|
||||
PredictorCompressionMismatch {
|
||||
display("requested predictor is not compatible with the requested compression")
|
||||
}
|
||||
@@ -184,6 +197,24 @@ quick_error! {
|
||||
PredictorUnavailable {
|
||||
display("the requested predictor is not available")
|
||||
}
|
||||
InsufficientOutputBufferSize { needed: usize, provided: usize } {
|
||||
display("the borrowed output buffer is not large enough for the decoded data, needed {needed} but have {provided}")
|
||||
}
|
||||
InsufficientOutputRowStride { needed: usize, requested: usize } {
|
||||
display("the provided output row stride would alias rows of decoded data, needed {needed} but have {requested}")
|
||||
}
|
||||
ZeroIfdPointer {
|
||||
display("the offset 0 can not point to a valid IFD")
|
||||
}
|
||||
ReconfiguredAfterImageWrite {
|
||||
display("attempted to reconfigure the encoder after image writing has started")
|
||||
}
|
||||
ByteOrderMismatch {
|
||||
display("attempted to use data with a byte order that does not match the required one")
|
||||
}
|
||||
MismatchedEntryLength {ty: Type, found: usize } {
|
||||
display("the length of data is not a multiple of the size of the type {ty:?}, found {found}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-1
@@ -8,10 +8,12 @@
|
||||
|
||||
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
|
||||
@@ -42,11 +44,15 @@ pub enum ColorType {
|
||||
/// Pixel is YCbCr
|
||||
YCbCr(u8),
|
||||
|
||||
/// Pixel is CIE L*a*b* (ICC LAb)
|
||||
Lab(u8),
|
||||
|
||||
/// Pixel has multiple bands/channels
|
||||
Multiband { bit_depth: u8, num_samples: u16 },
|
||||
}
|
||||
impl ColorType {
|
||||
fn bit_depth(&self) -> u8 {
|
||||
/// The number of bits used to store each sample.
|
||||
pub fn bit_depth(&self) -> u8 {
|
||||
match *self {
|
||||
ColorType::Gray(b)
|
||||
| ColorType::RGB(b)
|
||||
@@ -56,7 +62,24 @@ impl ColorType {
|
||||
| ColorType::CMYK(b)
|
||||
| ColorType::CMYKA(b)
|
||||
| ColorType::YCbCr(b)
|
||||
| ColorType::Lab(b)
|
||||
| ColorType::Multiband { bit_depth: b, .. } => b,
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of samples in each representation of a single color.
|
||||
pub fn num_samples(&self) -> u16 {
|
||||
match *self {
|
||||
ColorType::Gray(_) => 1,
|
||||
ColorType::RGB(_) => 3,
|
||||
ColorType::Palette(_) => 1,
|
||||
ColorType::GrayA(_) => 2,
|
||||
ColorType::RGBA(_) => 4,
|
||||
ColorType::CMYK(_) => 4,
|
||||
ColorType::CMYKA(_) => 5,
|
||||
ColorType::YCbCr(_) => 3,
|
||||
ColorType::Lab(_) => 3,
|
||||
ColorType::Multiband { num_samples, .. } => num_samples,
|
||||
}
|
||||
}
|
||||
}
|
||||
+323
-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) unknown("A custom compression method") {
|
||||
pub enum CompressionMethod(u16) unknown(
|
||||
/// A custom compression method
|
||||
unknown
|
||||
) {
|
||||
None = 1,
|
||||
Huffman = 2,
|
||||
Fax3 = 3,
|
||||
@@ -187,6 +280,9 @@ pub enum CompressionMethod(u16) unknown("A custom compression method") {
|
||||
|
||||
// Self-assigned by libtiff
|
||||
ZSTD = 0xC350,
|
||||
|
||||
// Self-assigned by libtiff
|
||||
WebP = 0xC351,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +296,8 @@ pub enum PhotometricInterpretation(u16) {
|
||||
CMYK = 5,
|
||||
YCbCr = 6,
|
||||
CIELab = 8,
|
||||
IccLab = 9,
|
||||
ItuLab = 10,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,10 +332,218 @@ 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,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
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
|
||||
@@ -7,7 +7,7 @@ use tiff::ColorType;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const TEST_IMAGE_DIR: &str = "./tests/images";
|
||||
const TEST_IMAGE_DIR: &str = "./tests/geotiff";
|
||||
|
||||
#[test]
|
||||
fn test_geo_tiff() {
|
||||
@@ -61,3 +61,26 @@ fn test_geo_tiff() {
|
||||
assert_eq!(data.len(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webp")]
|
||||
#[test]
|
||||
fn test_webp_tiff() {
|
||||
let filenames = ["usda_naip_256_webp_z3.tif"];
|
||||
let mut buffer = DecodingResult::U8(vec![]);
|
||||
|
||||
for filename in filenames.iter() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
|
||||
loop {
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
|
||||
if !decoder.more_images() {
|
||||
break;
|
||||
}
|
||||
|
||||
decoder.next_image().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
+208
-10
@@ -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/";
|
||||
@@ -45,12 +46,12 @@ fn test_image_color_type_unsupported(file: &str, expected_type: ColorType) {
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
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]
|
||||
@@ -172,7 +173,7 @@ fn test_string_tags() {
|
||||
&s,
|
||||
"GraphicsMagick 1.2 unreleased Q16 http://www.GraphicsMagick.org/"
|
||||
),
|
||||
_ => assert!(false),
|
||||
other => unreachable!("Expected Ascii tag from file, got {other:?}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -231,6 +232,54 @@ fn test_tiled_rect_rgb_u8() {
|
||||
test_image_sum_u8("tiled-rect-rgb-u8.tif", ColorType::RGB(8), 62081032);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inner_access() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-rect-rgb-u8.tif");
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
||||
|
||||
let c = decoder
|
||||
.get_tag(tiff::tags::Tag::Compression)
|
||||
.unwrap()
|
||||
.into_u16()
|
||||
.unwrap();
|
||||
assert_eq!(c, tiff::tags::CompressionMethod::None.to_u16());
|
||||
|
||||
// Because the image is uncompressed, reading the first tile directly with the inner reader
|
||||
// should yield the same result as reading it with the decoder's read_chunk method.
|
||||
let first_offset = decoder
|
||||
.get_tag_u32_vec(tiff::tags::Tag::TileOffsets)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
let first_byte_count = decoder
|
||||
.get_tag_u32_vec(tiff::tags::Tag::TileByteCounts)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
decoder
|
||||
.inner()
|
||||
.seek(std::io::SeekFrom::Start(first_offset as u64))
|
||||
.expect("Cannot seek to tile offset");
|
||||
let mut buf = vec![0; first_byte_count as usize];
|
||||
decoder
|
||||
.inner()
|
||||
.read_exact(&mut buf)
|
||||
.expect("Cannot read tile data");
|
||||
let raw_sum: u64 = buf.into_iter().map(<u64>::from).sum();
|
||||
|
||||
match decoder.read_chunk(0).unwrap() {
|
||||
DecodingResult::U8(chunk) => {
|
||||
let sum: u64 = chunk.into_iter().map(<u64>::from).sum();
|
||||
assert_eq!(sum, raw_sum);
|
||||
}
|
||||
_ => panic!("Wrong bit depth"),
|
||||
}
|
||||
}
|
||||
|
||||
/* #[test]
|
||||
fn test_tiled_jpeg_rgb_u8() {
|
||||
test_image_sum_u8("tiled-jpeg-rgb-u8.tif", ColorType::RGB(8), 93031606);
|
||||
@@ -292,6 +341,11 @@ 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);
|
||||
@@ -328,8 +382,6 @@ fn test_planar_rgb_u8() {
|
||||
}
|
||||
_ => panic!("Wrong bit depth"),
|
||||
}
|
||||
|
||||
test_image_sum_u8(file, ColorType::RGB(8), 15417630);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -348,7 +400,7 @@ fn test_div_zero() {
|
||||
|
||||
match err {
|
||||
TiffError::FormatError(TiffFormatError::StripTileTagConflict) => {}
|
||||
unexpected => panic!("Unexpected error {}", unexpected),
|
||||
unexpected => panic!("Unexpected error {unexpected}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +418,7 @@ fn test_too_many_value_bytes() {
|
||||
|
||||
match error {
|
||||
tiff::TiffError::LimitsExceeded => {}
|
||||
unexpected => panic!("Unexpected error {}", unexpected),
|
||||
unexpected => panic!("Unexpected error {unexpected}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,7 +549,7 @@ fn timeout() {
|
||||
|
||||
match error {
|
||||
TiffError::FormatError(TiffFormatError::CycleInOffsets) => {}
|
||||
e => panic!("Unexpected error {:?}", e),
|
||||
e => panic!("Unexpected error {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,3 +574,149 @@ fn test_zstd_compression() {
|
||||
// gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif
|
||||
test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396);
|
||||
}
|
||||
|
||||
fn test_image_bytes(
|
||||
file: &str,
|
||||
expected: &[(ColorType, u32)],
|
||||
// Represent all samples as little endian, ensuring check sums on all platforms.
|
||||
normalize_byte_order: fn(&mut [u8]),
|
||||
) {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
|
||||
let mut buffer = vec![0u8; 0];
|
||||
for (idx, &(expected_type, expected_crc)) in expected.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
decoder
|
||||
.next_image()
|
||||
.expect("mismatched number of directories");
|
||||
}
|
||||
|
||||
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
||||
let layout = decoder
|
||||
.image_buffer_layout()
|
||||
.expect("Cannot describe buffer layout");
|
||||
|
||||
buffer.resize(layout.len, 0u8);
|
||||
decoder.read_image_bytes(&mut buffer).unwrap();
|
||||
normalize_byte_order(&mut buffer);
|
||||
|
||||
// Well almost a crc..
|
||||
assert_eq!(crc32fast::hash(&buffer), expected_crc);
|
||||
}
|
||||
}
|
||||
|
||||
fn byte_order_u16(bytes: &mut [u8]) {
|
||||
for chunk in bytes.chunks_exact_mut(2) {
|
||||
let n = u16::from_ne_bytes(chunk.try_into().unwrap());
|
||||
chunk.copy_from_slice(&n.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
fn byte_order_u32(bytes: &mut [u8]) {
|
||||
for chunk in bytes.chunks_exact_mut(4) {
|
||||
let n = u32::from_ne_bytes(chunk.try_into().unwrap());
|
||||
chunk.copy_from_slice(&n.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
fn byte_order_u64(bytes: &mut [u8]) {
|
||||
for chunk in bytes.chunks_exact_mut(8) {
|
||||
let n = u64::from_ne_bytes(chunk.try_into().unwrap());
|
||||
chunk.copy_from_slice(&n.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_cmyk_3c_8b() {
|
||||
test_image_bytes(
|
||||
"cmyk-3c-8b.tiff",
|
||||
&[(ColorType::CMYK(8), 2935032230)],
|
||||
|_| (),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_gray_16b() {
|
||||
test_image_bytes(
|
||||
"minisblack-1c-16b.tiff",
|
||||
&[(ColorType::Gray(16), 3524597615)],
|
||||
byte_order_u16,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_gray_32b() {
|
||||
test_image_bytes(
|
||||
"gradient-1c-32b.tiff",
|
||||
&[(ColorType::Gray(32), 2257036530)],
|
||||
byte_order_u32,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_gray_u64() {
|
||||
test_image_bytes(
|
||||
"gradient-1c-64b.tiff",
|
||||
&[(ColorType::Gray(64), 2156547113)],
|
||||
byte_order_u64,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_gray_f32() {
|
||||
test_image_bytes(
|
||||
"gradient-1c-32b-float.tiff",
|
||||
&[(ColorType::Gray(32), 3232356730)],
|
||||
byte_order_u32,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "fax")]
|
||||
fn test_fax4() {
|
||||
test_image_sum_u8("fax4.tiff", ColorType::Gray(1), 7802706);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "fax")]
|
||||
fn test_fax4_white_is_min() {
|
||||
test_image_sum_u8("imagemagick_group4.tiff", ColorType::Gray(1), 3742820);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_extra_bits_image() {
|
||||
use tiff::{encoder, tags::ExtraSamples};
|
||||
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join("extra_bits_rgb_8b.tiff");
|
||||
let img_file = File::create(path).expect("Cannot find test image!");
|
||||
|
||||
let mut encoder = encoder::TiffEncoder::new(img_file).expect("Cannot create encoder");
|
||||
let mut img = encoder.new_image::<encoder::colortype::RGB8>(8, 8).unwrap();
|
||||
|
||||
img.extra_samples(&[ExtraSamples::Unspecified]).unwrap();
|
||||
|
||||
// all pixels are supposed to be 0, the extra is 1
|
||||
let data: [u8; 256] = core::array::from_fn(|i| (i as u8 % 4).saturating_sub(2));
|
||||
|
||||
img.write_data(&data)
|
||||
.expect("correct amount of data to write");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_bits_gray() {
|
||||
test_image_sum_u8(
|
||||
"extra_bits_gray_8b.tiff",
|
||||
ColorType::Multiband {
|
||||
bit_depth: 8,
|
||||
num_samples: 2,
|
||||
},
|
||||
64,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_bits_rgb() {
|
||||
test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGB(8), 0);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
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::new(io).unwrap();
|
||||
let layout = tif.image_buffer_layout().unwrap();
|
||||
assert_eq!(layout.planes, 3);
|
||||
let plane_stride = layout.plane_stride.map_or(0, |x| x.get());
|
||||
|
||||
eprintln!("{:?}", layout);
|
||||
let mut buffer = vec![0u8; layout.complete_len];
|
||||
tif.read_image_bytes(&mut buffer).unwrap();
|
||||
|
||||
// Mainly you can see: these are different and non-zero. Otherwise these magic constants just
|
||||
// depend on the file itself that is being used.
|
||||
const PLANE_EXCPECTED_SUMS: [u32; 3] = [15417630, 13007788, 11103530];
|
||||
|
||||
for (plane, expected) in (0..layout.planes).zip(PLANE_EXCPECTED_SUMS) {
|
||||
let plane_data = &buffer[plane_stride * plane..][..layout.len];
|
||||
let sum: u32 = plane_data.iter().copied().map(u32::from).sum();
|
||||
assert_eq!(sum, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_first_coding_unit() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
|
||||
let file = File::open(path).unwrap();
|
||||
let io = std::io::BufReader::new(file);
|
||||
|
||||
let mut tif = Decoder::new(io).unwrap();
|
||||
let layout = tif.image_coding_unit_layout(TiffCodingUnit(0)).unwrap();
|
||||
|
||||
assert_eq!(layout.planes, 3);
|
||||
let plane_stride = layout.plane_stride.map_or(0, |x| x.get());
|
||||
eprintln!("{:?}", layout);
|
||||
let mut buffer = vec![0u8; layout.complete_len];
|
||||
|
||||
struct Expected {
|
||||
plane_sums: [u32; 3],
|
||||
unit: TiffCodingUnit,
|
||||
}
|
||||
|
||||
// Mainly you can see: these are different and non-zero. Otherwise these magic constants just
|
||||
// depend on the file itself that is being used. Also this varies by what unit we are reading.
|
||||
const PLANE_EXCPECTED_SUMS: [Expected; 2] = [
|
||||
Expected {
|
||||
plane_sums: [856853, 767686, 688532],
|
||||
unit: TiffCodingUnit(0),
|
||||
},
|
||||
Expected {
|
||||
plane_sums: [883679, 797329, 711086],
|
||||
unit: TiffCodingUnit(1),
|
||||
},
|
||||
];
|
||||
|
||||
for expected in PLANE_EXCPECTED_SUMS {
|
||||
tif.read_coding_unit_bytes(expected.unit, &mut buffer)
|
||||
.unwrap();
|
||||
|
||||
for (plane, expected) in (0..layout.planes).zip(expected.plane_sums) {
|
||||
let plane_data = &buffer[plane_stride * plane..][..layout.len];
|
||||
let sum: u32 = plane_data.iter().copied().map(u32::from).sum();
|
||||
assert_eq!(sum, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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::new(io).unwrap();
|
||||
tif = tif.with_limits({
|
||||
limits.decoding_buffer_size = 374 * 499;
|
||||
limits.clone()
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
+73
-8
@@ -57,7 +57,7 @@ fn encode_decode() {
|
||||
),
|
||||
(Tag::Compression, ifd::Value::Short(1)),
|
||||
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
|
||||
(Tag::StripOffsets, ifd::Value::Unsigned(8)),
|
||||
(Tag::StripOffsets, ifd::Value::Unsigned(35)),
|
||||
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
|
||||
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
|
||||
(Tag::StripByteCounts, ifd::Value::Unsigned(30000)),
|
||||
@@ -81,7 +81,10 @@ fn encode_decode() {
|
||||
decoder.get_tag(Tag::Artist).unwrap(),
|
||||
ifd::Value::Ascii("Image-tiff".into())
|
||||
);
|
||||
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
|
||||
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let tiff::decoder::DecodingResult::U8(img_res) = buffer {
|
||||
assert_eq!(image_data, img_res);
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
@@ -137,7 +140,7 @@ fn encode_decode_big() {
|
||||
),
|
||||
(Tag::Compression, ifd::Value::Short(1)),
|
||||
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
|
||||
(Tag::StripOffsets, ifd::Value::UnsignedBig(16)),
|
||||
(Tag::StripOffsets, ifd::Value::UnsignedBig(27)),
|
||||
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
|
||||
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
|
||||
(Tag::StripByteCounts, ifd::Value::UnsignedBig(30000)),
|
||||
@@ -161,7 +164,9 @@ fn encode_decode_big() {
|
||||
decoder.get_tag(Tag::Artist).unwrap(),
|
||||
ifd::Value::Ascii("Image-tiff".into())
|
||||
);
|
||||
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let DecodingResult::U8(img_res) = buffer {
|
||||
assert_eq!(image_data, img_res);
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
@@ -235,6 +240,7 @@ 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,
|
||||
@@ -244,7 +250,10 @@ macro_rules! test_roundtrip {
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
||||
|
||||
let image_data = match decoder.read_image().unwrap() {
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
|
||||
let image_data = match buffer {
|
||||
DecodingResult::$buffer(res) => res,
|
||||
_ => panic!("Wrong data type"),
|
||||
};
|
||||
@@ -258,9 +267,11 @@ macro_rules! test_roundtrip {
|
||||
}
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
{
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
let mut decoder = Decoder::new(&mut file).unwrap();
|
||||
if let DecodingResult::$buffer(img_res) = decoder.read_image().unwrap() {
|
||||
assert_eq!(image_data, img_res);
|
||||
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");
|
||||
}
|
||||
@@ -621,8 +632,62 @@ 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::new(&mut data).unwrap();
|
||||
|
||||
{
|
||||
assert_eq!(decoder.dimensions().unwrap(), (1, 1));
|
||||
let _ = decoder.read_image().unwrap();
|
||||
}
|
||||
|
||||
{
|
||||
decoder.next_image().unwrap();
|
||||
assert_eq!(decoder.dimensions().unwrap(), (2, 1));
|
||||
let _ = decoder.read_image().unwrap();
|
||||
|
||||
let exif_location = decoder
|
||||
.get_tag(Tag::ExifDirectory)
|
||||
.expect("second directory missing Exif")
|
||||
.into_ifd_pointer()
|
||||
.expect("exif directory is not an IFD pointer");
|
||||
|
||||
let exif = decoder.read_directory(exif_location).unwrap();
|
||||
|
||||
let artist = decoder
|
||||
.read_directory_tags(&exif)
|
||||
.get_tag_ascii_string(Tag::Artist)
|
||||
.expect("EXIF directory missing artist tag");
|
||||
assert_eq!(artist, ARTIST);
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@ fn encode_decode_with_compression(compression: Compression) {
|
||||
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()
|
||||
);
|
||||
@@ -126,7 +126,7 @@ fn encode_decode_with_compression(compression: Compression) {
|
||||
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()
|
||||
);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File renamed without changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -1,7 +1,7 @@
|
||||
extern crate tiff;
|
||||
|
||||
use tiff::decoder::{Decoder, DecodingResult};
|
||||
use tiff::encoder::{colortype, Compression, Predictor, TiffEncoder};
|
||||
use tiff::encoder::{colortype, Predictor, TiffEncoder};
|
||||
use tiff::ColorType;
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use tiff::decoder::Decoder;
|
||||
|
||||
// This file has the following IFD structure:
|
||||
//
|
||||
// ```
|
||||
// IFD -> IFD -> IFD
|
||||
// |-> IFD
|
||||
// |-> IFD -> IFD
|
||||
// | \-> IFD -> IFD
|
||||
// \-> IFD
|
||||
// ```
|
||||
//
|
||||
// where the main chain of IFD has 3 images, the second of which has several sub-IFDs and some have
|
||||
// more nested IFDs.
|
||||
const TEST_IMAGE_SUBIFD: &str = "./tests/subsubifds.tif";
|
||||
use std::fs::File;
|
||||
|
||||
#[test]
|
||||
fn decode_seek_chain() {
|
||||
let file = File::open(TEST_IMAGE_SUBIFD).expect("Cannot open test image");
|
||||
let mut decoder = Decoder::new(file).expect("Invalid format to create decoder");
|
||||
|
||||
// Remember the offsets in the first iteration.
|
||||
let offset0 = decoder.ifd_pointer().expect("First IFD pointer not found");
|
||||
decoder.next_image().unwrap();
|
||||
let offset1 = decoder.ifd_pointer().expect("Second IFD pointer not found");
|
||||
decoder.next_image().unwrap();
|
||||
let offset2 = decoder.ifd_pointer().expect("Third IFD pointer not found");
|
||||
assert!(!decoder.more_images());
|
||||
|
||||
// Ensure we can seek, to do this again.
|
||||
decoder
|
||||
.seek_to_image(0)
|
||||
.expect("Failed to seek to first image");
|
||||
assert_eq!(Some(offset0), decoder.ifd_pointer());
|
||||
decoder.next_image().unwrap();
|
||||
assert_eq!(Some(offset1), decoder.ifd_pointer());
|
||||
decoder.next_image().unwrap();
|
||||
assert_eq!(Some(offset2), decoder.ifd_pointer());
|
||||
assert!(!decoder.more_images());
|
||||
}
|
||||
Binary file not shown.
Reference in new issue
Block a user