Compare commits
190
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6e8ed772d | ||
|
|
9b452e93b7 | ||
|
|
96d37bb087 | ||
|
|
12f16dbfdd | ||
|
|
a8abdd2484 | ||
|
|
934fde185a | ||
|
|
3302936b08 | ||
|
|
a5b9a57e09 | ||
|
|
e27f6fe00d | ||
|
|
46b19cdacf | ||
|
|
c27fd94e1d | ||
|
|
f28f5fc830 | ||
|
|
3436a2222b | ||
|
|
31919c7bfc | ||
|
|
d0c9198016 | ||
|
|
8f83f0fa82 | ||
|
|
f3f9ff1244 | ||
|
|
a7dda6f4b1 | ||
|
|
35c14f1e3c | ||
|
|
a9cf5e17b9 | ||
|
|
6276defac6 | ||
|
|
dee0c2fb11 | ||
|
|
11133cb1e3 | ||
|
|
8ad05b85db | ||
|
|
4a9e1d152b | ||
|
|
1c6001ea5d | ||
|
|
c0043c1a84 | ||
|
|
c7a1176381 | ||
|
|
a658c09f09 | ||
|
|
0309349e1b | ||
|
|
279851a9a9 | ||
|
|
22b4c39254 | ||
|
|
f11a1c5393 | ||
|
|
c4285aea4a | ||
|
|
a1426ca802 | ||
|
|
66e6567acc | ||
|
|
5947d3b72e | ||
|
|
0fdf1e0502 | ||
|
|
9924ea2721 | ||
|
|
30e04f3381 | ||
|
|
dfd0191403 | ||
|
|
cf9eee64ad | ||
|
|
a59db23341 | ||
|
|
e8103509fa | ||
|
|
26b18c5184 | ||
|
|
b0712855b1 | ||
|
|
ea6e2a8d80 | ||
|
|
032421634f | ||
|
|
d760bd3442 | ||
|
|
a70e05e51e | ||
|
|
dc25c4eaea | ||
|
|
d7961a2fcf | ||
|
|
2166fd591a | ||
|
|
7380611a7b | ||
|
|
149492ca6e | ||
|
|
e4c3c1fcfd | ||
|
|
6a3282e9f2 | ||
|
|
fa8a911f79 | ||
|
|
dc94cd4436 | ||
|
|
a42b16ab72 | ||
|
|
8246b55e8f | ||
|
|
399469f512 | ||
|
|
740c5cdeea | ||
|
|
86ac5b601e | ||
|
|
97d357d446 | ||
|
|
61867b46f9 | ||
|
|
0f1f075abb | ||
|
|
5576417abe | ||
|
|
52b9cc7a19 | ||
|
|
006f0b0fa4 | ||
|
|
5b8a2832c3 | ||
|
|
f93a6380e6 | ||
|
|
8a1b65fe68 | ||
|
|
a2996ab34e | ||
|
|
f890d22657 | ||
|
|
6b04bff6b2 | ||
|
|
017425b200 | ||
|
|
57d238fd9c | ||
|
|
bd0dccef3a | ||
|
|
db1dbd3699 | ||
|
|
7491436306 | ||
|
|
8e315c6a00 | ||
|
|
860052a8f4 | ||
|
|
1c37855dd9 | ||
|
|
5696b0c78b | ||
|
|
4d1daf5c19 | ||
|
|
be58026125 | ||
|
|
488da716d7 | ||
|
|
6c8f8abc71 | ||
|
|
ba7a290ff7 | ||
|
|
85215e81e3 | ||
|
|
617dc6eed8 | ||
|
|
83409cbdf3 | ||
|
|
26301fc3f1 | ||
|
|
83dbb750df | ||
|
|
2d6d92e81d | ||
|
|
3434d0220b | ||
|
|
fcdb86ca7f | ||
|
|
6fb99107cd | ||
|
|
19dcd09e09 | ||
|
|
43cfc5bc3a | ||
|
|
c3977c4513 | ||
|
|
a4cbf7bbaf | ||
|
|
26c32431fa | ||
|
|
5090003bda | ||
|
|
fb1afc81e9 | ||
|
|
29e43d2314 | ||
|
|
4ff374990e | ||
|
|
3d3c16e26f | ||
|
|
8a5c2da511 | ||
|
|
3b11e2f577 | ||
|
|
c6f6ff4c95 | ||
|
|
d7d24d0f7c | ||
|
|
58e93fb8a3 | ||
|
|
595e67e5fb | ||
|
|
812bc970ba | ||
|
|
82b646298d | ||
|
|
22ff2b5c50 | ||
|
|
3a6492f251 | ||
|
|
a03eeccfe5 | ||
|
|
287fcb9309 | ||
|
|
ba97bad411 | ||
|
|
c4ab46ad2c | ||
|
|
eaf9cc1d6c | ||
|
|
34191fa9ca | ||
|
|
b0bd33dbe4 | ||
|
|
2ca2427c75 | ||
|
|
e8be8656f2 | ||
|
|
ea949032a2 | ||
|
|
87007d85a0 | ||
|
|
2119bff354 | ||
|
|
c62a69ec9d | ||
|
|
79a791b49a | ||
|
|
e1e9f36c5a | ||
|
|
19caae4f53 | ||
|
|
0ad653433a | ||
|
|
bf79af8de4 | ||
|
|
54dddc4943 | ||
|
|
be9b3a04b9 | ||
|
|
08f1b25fac | ||
|
|
c7b97519a0 | ||
|
|
e093e15bbe | ||
|
|
647e9b4a81 | ||
|
|
31c49ea6ef | ||
|
|
0362e6e31d | ||
|
|
0fe5b2804e | ||
|
|
61dbcc1392 | ||
|
|
d9d4f0a905 | ||
|
|
a589010099 | ||
|
|
53498074a4 | ||
|
|
cb543cded7 | ||
|
|
cf8f96568e | ||
|
|
4ff88e658d | ||
|
|
14ebf376f3 | ||
|
|
37578add53 | ||
|
|
2b9476566b | ||
|
|
448a0e61d1 | ||
|
|
d872ff8751 | ||
|
|
0a878888db | ||
|
|
a0a95ef86e | ||
|
|
bbb5e5a41b | ||
|
|
f74169d4af | ||
|
|
c304a3cc13 | ||
|
|
9a9193345c | ||
|
|
22046d4272 | ||
|
|
2c5fe2c2f5 | ||
|
|
eceaa1f14d | ||
|
|
d42db25c27 | ||
|
|
c74f23784c | ||
|
|
8b0da5939a | ||
|
|
fdde3467fc | ||
|
|
3f2066bb20 | ||
|
|
2ac8be1177 | ||
|
|
c81d0a9c94 | ||
|
|
983c5baea0 | ||
|
|
abf3cc617d | ||
|
|
fb4ee9b377 | ||
|
|
45cd3c3b63 | ||
|
|
701fcc46b8 | ||
|
|
e41159ce05 | ||
|
|
03979faef8 | ||
|
|
857973cb21 | ||
|
|
cef21c47fb | ||
|
|
a0191ed1a7 | ||
|
|
99a7b8cba7 | ||
|
|
418ea182c3 | ||
|
|
529fa64e13 | ||
|
|
3d6b94b65a | ||
|
|
a51b23091e | ||
|
|
ac65bd8477 |
No files matched your search
@@ -0,0 +1,85 @@
|
||||
name: Fuzz
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
duration:
|
||||
description: 'Seconds per fuzz target'
|
||||
required: false
|
||||
default: '300'
|
||||
|
||||
concurrency:
|
||||
group: fuzz-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
fuzz:
|
||||
name: Fuzz (${{ matrix.target }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- decode_comprehensive
|
||||
- decode_image
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: fuzz-${{ matrix.target }}
|
||||
cache-directories: fuzz/target
|
||||
|
||||
- name: Install cargo-fuzz
|
||||
run: cargo install cargo-fuzz --locked
|
||||
|
||||
- name: Determine duration
|
||||
id: duration
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
echo "seconds=300" >> "$GITHUB_OUTPUT"
|
||||
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "seconds=${{ github.event.inputs.duration || '300' }}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "seconds=60" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Seed corpus
|
||||
run: |
|
||||
mkdir -p fuzz/corpus/${{ matrix.target }}
|
||||
cp fuzz/regression/* fuzz/corpus/${{ matrix.target }}/ 2>/dev/null || true
|
||||
cp fuzz/seed_corpus/${{ matrix.target }}/* fuzz/corpus/${{ matrix.target }}/ 2>/dev/null || true
|
||||
|
||||
- name: Fuzz ${{ matrix.target }}
|
||||
run: |
|
||||
cargo +nightly fuzz run ${{ matrix.target }} \
|
||||
-- -max_total_time=${{ steps.duration.outputs.seconds }} \
|
||||
-dict=fuzz/tiff.dict \
|
||||
-rss_limit_mb=2048
|
||||
|
||||
- name: Upload crashes
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fuzz-crashes-${{ matrix.target }}
|
||||
path: fuzz/artifacts/
|
||||
retention-days: 30
|
||||
|
||||
regression:
|
||||
name: Fuzz regression
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Build
|
||||
run: cargo build
|
||||
- name: Regression seeds
|
||||
run: |
|
||||
if [ -d fuzz/regression ] && [ "$(ls fuzz/regression/ 2>/dev/null | wc -l)" -gt 0 ]; then
|
||||
cargo test --test fuzz_regression 2>/dev/null || echo "No regression test found — seeds exist but no test harness"
|
||||
fi
|
||||
+36
-16
@@ -9,14 +9,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.85.0", stable, beta, nightly]
|
||||
rust: ["1.88.0", stable, beta, nightly]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
if: ${{ matrix.rust == '1.85.0' }}
|
||||
if: ${{ matrix.rust == '1.88.0' }}
|
||||
- name: Generate Cargo.lock with minimal-version dependencies
|
||||
if: ${{ matrix.rust == '1.85.0' }}
|
||||
if: ${{ matrix.rust == '1.88.0' }}
|
||||
run: cargo -Zminimal-versions generate-lockfile
|
||||
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
@@ -29,16 +29,32 @@ jobs:
|
||||
- name: build
|
||||
run: cargo build -v
|
||||
- name: test
|
||||
if: ${{ matrix.rust != '1.85.0' }}
|
||||
if: ${{ matrix.rust != '1.88.0' }}
|
||||
run: cargo test -v && cargo doc -v
|
||||
|
||||
test-platforms:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, macos-26-intel, windows-latest, windows-11-arm]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache Cargo Dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: test
|
||||
run: cargo test -v
|
||||
|
||||
features:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
feature: ["", deflate, jpeg, lzw, zstd, default, "default,zstd"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
@@ -59,25 +75,29 @@ jobs:
|
||||
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
|
||||
- uses: actions/checkout@v6
|
||||
- uses: taiki-e/install-action@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 }}
|
||||
|
||||
msrv:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- name: Check MSRV
|
||||
run: cargo hack check --rust-version
|
||||
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
@@ -87,8 +107,8 @@ jobs:
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- run: cargo clippy --all-features # TODO: enable "-- -D warnings" once all outstanding clippy lints are fixed
|
||||
- run: cargo clippy --all-features --all-targets -- -D warnings
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
tests/benches/corpus/
|
||||
.idea
|
||||
.agentbridge
|
||||
+54
@@ -1,3 +1,57 @@
|
||||
# Version 0.12.0 (unreleased)
|
||||
|
||||
Fixes:
|
||||
- Decoding a tiled image whose width is not a multiple of the tile width no
|
||||
longer corrupts the columns below the right border tiles when extra samples
|
||||
are discarded (e.g. RGB with an unspecified extra sample). The horizontal
|
||||
tile padding was written past the end of the output row, spilling into the
|
||||
leftmost columns of the following row.
|
||||
|
||||
Additions:
|
||||
- Added the `TiffHeader` type. It can be used to create a `Decoder` in a state
|
||||
before the first IFD and allows opening files at an arbitrary first IFD
|
||||
offset rather than one indicated by an encoded header at the start.
|
||||
|
||||
Changes:
|
||||
- The IFD iteration methods on `Decoder` (`next_image`, `seek_to_image`, etc)
|
||||
now come with a strong error guarantee. They will not modify the current
|
||||
directory state if they fail. This includes the failure to interpret an IFD
|
||||
as a supported image in the case of `*_image` variants.
|
||||
|
||||
Compatibility:
|
||||
- Changed how tiled TIFF files are detected. The presence of a TileWidth OR
|
||||
TileHeight/TileLength tag now marks an image as tiled. Data is no retrieved
|
||||
from either StripOffsets or TileOffsets with the latter taking precedence.
|
||||
This is closer to the behavior of libtiff.
|
||||
|
||||
# Version 0.11.3
|
||||
|
||||
Additions:
|
||||
- Added support for `webp` compression (as per GDAL) under the `webp` feature flag (default: off).
|
||||
- Added `Decoder::image_ifd`, creating an `IfdDecoder` of the current IFD.
|
||||
- Added `IfdDecoder::directory`, returning the `Directory` underlying it.
|
||||
- Added `LowerHex` and `UpwerHex` implementations for `IfdPointer`.
|
||||
|
||||
Notes:
|
||||
- A new example `tiff-ls` demonstrates use of the new features.
|
||||
|
||||
# Version 0.11.2
|
||||
|
||||
Changes:
|
||||
- Planar files can now be read with `Decoder::read_image_to_buffer` if the
|
||||
limits allow for such a large buffer size. Previously only the first plane
|
||||
was read and the decoder will attempt a fallback to this behavior if the
|
||||
limits do not allow all planes to be allocated.
|
||||
|
||||
Fixes:
|
||||
- An off-by-one error in a `debug_assert` of `ValueBuffer` caused its use in
|
||||
`IfdDecoder::find_tag_buf` to panic outside release mode when the buffer
|
||||
exactly matched the required size.
|
||||
|
||||
Additions:
|
||||
- The methods `ColorType::{num_samples, bit_depth}` are now public. This
|
||||
promises that we only have homogeneous color depths in this major version.
|
||||
|
||||
# Version 0.11.1
|
||||
|
||||
Fixes:
|
||||
|
||||
+30
-6
@@ -1,11 +1,10 @@
|
||||
[package]
|
||||
name = "tiff"
|
||||
version = "0.11.1"
|
||||
name = "ndbioimage-tiff"
|
||||
version = "0.11.3"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
# note: when changed, also update test runner in `.github/workflows/rust.yml`
|
||||
rust-version = "1.85.0"
|
||||
rust-version = "1.88.0"
|
||||
|
||||
license = "MIT"
|
||||
description = "TIFF decoding and encoding library in pure Rust"
|
||||
@@ -16,6 +15,9 @@ categories = ["multimedia::images", "multimedia::encoding"]
|
||||
|
||||
exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
|
||||
|
||||
[lib]
|
||||
name = "tiff"
|
||||
|
||||
[dependencies]
|
||||
half = { version = "2.4.1" }
|
||||
quick-error = "2.0.1"
|
||||
@@ -23,14 +25,18 @@ quick-error = "2.0.1"
|
||||
# Rename to avoid confusion with the feature name.
|
||||
fax34 = { package = "fax", version = "0.2.6", optional = true }
|
||||
flate2 = { version = "1.0.20", optional = true }
|
||||
weezl = { version = "0.1.10", optional = true }
|
||||
zstd = { version = "0.13", optional = true }
|
||||
weezl = { version = "0.2.1", optional = true }
|
||||
zstd = { version = "0.14", optional = true }
|
||||
zrip-decode = { version = "0.8.2", features = ["paranoid"], optional = true }
|
||||
zune-jpeg = { version = "0.5.1", optional = true }
|
||||
image-webp = {version = "0.2.4", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.3.1"
|
||||
# We may already depend on this via flate2
|
||||
crc32fast = "1.5"
|
||||
libtest-mimic = "0.8.1"
|
||||
walkdir = "2.5"
|
||||
|
||||
[features]
|
||||
default = ["deflate", "fax", "jpeg", "lzw"]
|
||||
@@ -40,8 +46,26 @@ deflate = ["dep:flate2"]
|
||||
fax = ["dep:fax34"]
|
||||
jpeg = ["dep:zune-jpeg"]
|
||||
lzw = ["dep:weezl"]
|
||||
webp = ["dep:image-webp"]
|
||||
zstd = ["dep:zstd"]
|
||||
# Fully safe Rust but somewhat slower Zstd decoding.
|
||||
# When both zstd and zstd-safe-rust are enabled, zstd takes precedence.
|
||||
zstd-safe-rust = ["dep:zrip-decode"]
|
||||
|
||||
[[bench]]
|
||||
name = "lzw"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "encode"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "decode"
|
||||
harness = false
|
||||
|
||||
|
||||
[[test]]
|
||||
name = "decode_libtiffpic"
|
||||
path = "tests/decode_libtiffpic.rs"
|
||||
harness = false
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
TIFF decoding and encoding library in pure Rust
|
||||
|
||||
Forked from https://github.com/image-rs/image-tiff. Changes: read images containing RGBPalette frames, ignoring colormaps.
|
||||
|
||||
## Supported
|
||||
|
||||
### Features
|
||||
@@ -55,4 +57,4 @@ This crate uses [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) in order t
|
||||
|
||||
After installing it with `cargo install cargo-fuzz` on a nightly rustc, the
|
||||
fuzzing harness can be run with recommended settings using
|
||||
`cargo fuzz run decode_image -snone -- -timeout=5`.
|
||||
`cargo fuzz run decode_comprehensive -snone -- -dict=fuzz/tiff.dict -timeout=5`.
|
||||
@@ -0,0 +1,461 @@
|
||||
extern crate criterion;
|
||||
extern crate tiff;
|
||||
|
||||
use criterion::{black_box, Criterion, Throughput};
|
||||
use std::io::Cursor;
|
||||
use tiff::decoder::Decoder;
|
||||
use tiff::encoder::colortype::{Gray16, Gray8, RGB32Float, RGB16, RGB8, RGBA8};
|
||||
use tiff::encoder::{Compression, Predictor, TiffEncoder};
|
||||
|
||||
const WIDTH: u32 = 2048;
|
||||
const HEIGHT: u32 = 2048;
|
||||
|
||||
// === Corpus paths (real photo-based TIFFs) ===
|
||||
// Set TIFF_BENCH_DIR / TIFF_GEOTIFF_DIR env vars to a directory containing the corpus files.
|
||||
// If the files are not found, the corresponding benchmarks are silently skipped.
|
||||
// The corpus can be generated with vips/imagemagick from any large photo; see the benchmark
|
||||
// names for the expected filenames and compression settings.
|
||||
const CORPUS_DIR: &str = "tests/benches/corpus";
|
||||
|
||||
fn bench_dir() -> String {
|
||||
std::env::var("TIFF_BENCH_DIR").unwrap_or_else(|_| CORPUS_DIR.to_string())
|
||||
}
|
||||
|
||||
fn geotiff_dir() -> String {
|
||||
std::env::var("TIFF_GEOTIFF_DIR").unwrap_or_else(|_| CORPUS_DIR.to_string())
|
||||
}
|
||||
|
||||
/// Load a file from the corpus directory, returning None if not found.
|
||||
fn load_corpus(dir: &str, name: &str) -> Option<Vec<u8>> {
|
||||
let path = format!("{}/{}", dir, name);
|
||||
std::fs::read(&path).ok()
|
||||
}
|
||||
|
||||
fn decode_tiff_bytes(data: &[u8]) {
|
||||
let cursor = Cursor::new(data);
|
||||
let mut decoder = Decoder::open(black_box(cursor)).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
decoder.read_image().unwrap();
|
||||
}
|
||||
|
||||
// === Synthetic image generators ===
|
||||
|
||||
fn generate_rgb8(w: u32, h: u32) -> Vec<u8> {
|
||||
(0..w * h * 3)
|
||||
.map(|i| ((i * 7 + i / w * 3) & 0xFF) as u8)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_rgba8(w: u32, h: u32) -> Vec<u8> {
|
||||
(0..w * h * 4)
|
||||
.map(|i| ((i * 11 + i / w * 5) & 0xFF) as u8)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_gray8(w: u32, h: u32) -> Vec<u8> {
|
||||
(0..w * h)
|
||||
.map(|i| ((i * 7 + i / w * 3) & 0xFF) as u8)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_rgb16(w: u32, h: u32) -> Vec<u16> {
|
||||
(0..w * h * 3)
|
||||
.map(|i| ((i * 257 + i / w * 131) & 0xFFFF) as u16)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_gray16(w: u32, h: u32) -> Vec<u16> {
|
||||
(0..w * h)
|
||||
.map(|i| ((i * 257 + i / w * 131) & 0xFFFF) as u16)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_rgb_f32(w: u32, h: u32) -> Vec<f32> {
|
||||
let mut data = Vec::with_capacity((w * h * 3) as usize);
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let fx = x as f32 / w as f32;
|
||||
let fy = y as f32 / h as f32;
|
||||
data.push(fx * 0.5 + fy * 0.3);
|
||||
data.push(fy * 0.7 + fx * 0.2);
|
||||
data.push((fx + fy) * 0.25);
|
||||
}
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
// === Synthetic encoders (strip-only, encoder doesn't support tiles) ===
|
||||
|
||||
fn encode<C: tiff::encoder::colortype::ColorType>(
|
||||
data: &[C::Inner],
|
||||
w: u32,
|
||||
h: u32,
|
||||
compression: Compression,
|
||||
predictor: Predictor,
|
||||
) -> Vec<u8>
|
||||
where
|
||||
[C::Inner]: tiff::encoder::TiffValue,
|
||||
{
|
||||
let mut buf = Cursor::new(Vec::new());
|
||||
TiffEncoder::new(&mut buf)
|
||||
.unwrap()
|
||||
.with_predictor(predictor)
|
||||
.with_compression(compression)
|
||||
.write_image::<C>(w, h, data)
|
||||
.unwrap();
|
||||
buf.into_inner()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut c = Criterion::default().configure_from_args();
|
||||
let bd = bench_dir();
|
||||
let gd = geotiff_dir();
|
||||
|
||||
// ========================================================================
|
||||
// PART 1: Real photo corpus (5616x3744, ~21MP)
|
||||
// ========================================================================
|
||||
// Raw pixel sizes for throughput calculation:
|
||||
// RGB8: 5616*3744*3 = 63,078,912
|
||||
// RGB16: 5616*3744*6 = 126,157,824
|
||||
// F32x3: 5616*3744*12 = 252,315,648
|
||||
// Gray8: 5616*3744*1 = 21,026,304
|
||||
// GrayF32: 5616*3744*4 = 84,105,216
|
||||
// Palette8: 5616*3744*1 = 21,026,304 (output)
|
||||
|
||||
struct CorpusBench {
|
||||
file: &'static str,
|
||||
name: &'static str,
|
||||
raw_bytes: u64,
|
||||
}
|
||||
|
||||
let photo_benches = [
|
||||
// Strip-based
|
||||
CorpusBench {
|
||||
file: "photo-rgb8-none.tif",
|
||||
name: "strip-rgb8-none",
|
||||
raw_bytes: 63_078_912,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "photo-rgb8-packbits.tif",
|
||||
name: "strip-rgb8-packbits",
|
||||
raw_bytes: 63_078_912,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "photo-rgb8-lzw.tif",
|
||||
name: "strip-rgb8-lzw",
|
||||
raw_bytes: 63_078_912,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "photo-rgb8-lzw-hpred.tif",
|
||||
name: "strip-rgb8-lzw-hpred",
|
||||
raw_bytes: 63_078_912,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "photo-rgb8-deflate-hpred.tif",
|
||||
name: "strip-rgb8-deflate-hpred",
|
||||
raw_bytes: 63_078_912,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "photo-rgb16-deflate-hpred.tif",
|
||||
name: "strip-rgb16-deflate-hpred",
|
||||
raw_bytes: 126_157_824,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "photo-gray8-lzw.tif",
|
||||
name: "strip-gray8-lzw",
|
||||
raw_bytes: 21_026_304,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "photo-gray8-deflate-hpred.tif",
|
||||
name: "strip-gray8-deflate-hpred",
|
||||
raw_bytes: 21_026_304,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "photo-rgb-f32-deflate-fpred.tif",
|
||||
name: "strip-rgb-f32-deflate-fpred",
|
||||
raw_bytes: 252_315_648,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "photo-gray-f32-deflate-fpred.tif",
|
||||
name: "strip-gray-f32-deflate-fpred",
|
||||
raw_bytes: 84_105_216,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "photo-palette8-lzw.tif",
|
||||
name: "strip-palette8-lzw",
|
||||
raw_bytes: 21_026_304,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "scan-bilevel-fax4.tif",
|
||||
name: "strip-bilevel-fax4",
|
||||
raw_bytes: 5616 * 3744 / 8,
|
||||
},
|
||||
// Tiled
|
||||
CorpusBench {
|
||||
file: "photo-rgb8-lzw-hpred-tiled256.tif",
|
||||
name: "tiled256-rgb8-lzw-hpred",
|
||||
raw_bytes: 63_078_912,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "photo-rgb8-deflate-hpred-tiled256.tif",
|
||||
name: "tiled256-rgb8-deflate-hpred",
|
||||
raw_bytes: 63_078_912,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "photo-rgb8-jpeg-tiled256.tif",
|
||||
name: "tiled256-rgb8-jpeg",
|
||||
raw_bytes: 63_078_912,
|
||||
},
|
||||
CorpusBench {
|
||||
file: "photo-gray-f32-deflate-fpred-tiled512.tif",
|
||||
name: "tiled512-gray-f32-deflate-fpred",
|
||||
raw_bytes: 84_105_216,
|
||||
},
|
||||
];
|
||||
|
||||
{
|
||||
let mut group = c.benchmark_group("photo-21mp");
|
||||
group.sample_size(10);
|
||||
|
||||
for bench in &photo_benches {
|
||||
if let Some(data) = load_corpus(&bd, bench.file) {
|
||||
group.throughput(Throughput::Bytes(bench.raw_bytes));
|
||||
let data_ref = &data;
|
||||
group.bench_function(bench.name, |b| b.iter(|| decode_tiff_bytes(data_ref)));
|
||||
}
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PART 2: GeoTIFF (real tiled DEM data)
|
||||
// ========================================================================
|
||||
|
||||
let geotiff_benches = [
|
||||
// Copernicus DEM: 3600x3600, tiled 1024x1024, Gray f32, deflate + fpredict
|
||||
CorpusBench {
|
||||
file: "Copernicus_DSM_COG_10_N40_00_W075_00_DEM.tif",
|
||||
name: "copernicus-dem-f32-deflate-fpred-1024tile",
|
||||
raw_bytes: 3600 * 3600 * 4,
|
||||
},
|
||||
// USGS DEM: 3612x3612, tiled 512x512, Gray f32, LZW + fpredict
|
||||
CorpusBench {
|
||||
file: "USGS_1_n40w075.tif",
|
||||
name: "usgs-dem-f32-lzw-fpred-512tile",
|
||||
raw_bytes: 3612 * 3612 * 4,
|
||||
},
|
||||
];
|
||||
|
||||
{
|
||||
let mut group = c.benchmark_group("geotiff");
|
||||
group.sample_size(10);
|
||||
|
||||
for bench in &geotiff_benches {
|
||||
if let Some(data) = load_corpus(&gd, bench.file) {
|
||||
group.throughput(Throughput::Bytes(bench.raw_bytes));
|
||||
let data_ref = &data;
|
||||
group.bench_function(bench.name, |b| b.iter(|| decode_tiff_bytes(data_ref)));
|
||||
}
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PART 3: Synthetic (2048x2048, controlled comparisons)
|
||||
// ========================================================================
|
||||
|
||||
let rgb8 = generate_rgb8(WIDTH, HEIGHT);
|
||||
let rgba8 = generate_rgba8(WIDTH, HEIGHT);
|
||||
let gray8 = generate_gray8(WIDTH, HEIGHT);
|
||||
let rgb16 = generate_rgb16(WIDTH, HEIGHT);
|
||||
let gray16 = generate_gray16(WIDTH, HEIGHT);
|
||||
let rgb_f32 = generate_rgb_f32(WIDTH, HEIGHT);
|
||||
|
||||
let raw_rgb8 = (WIDTH * HEIGHT * 3) as u64;
|
||||
let raw_rgba8 = (WIDTH * HEIGHT * 4) as u64;
|
||||
let raw_gray8 = (WIDTH * HEIGHT) as u64;
|
||||
let raw_rgb16 = (WIDTH * HEIGHT * 6) as u64;
|
||||
let raw_gray16 = (WIDTH * HEIGHT * 2) as u64;
|
||||
let raw_f32x3 = (WIDTH * HEIGHT * 12) as u64;
|
||||
|
||||
// --- Uncompressed baseline ---
|
||||
{
|
||||
let mut group = c.benchmark_group("synth-uncompressed");
|
||||
group.sample_size(10);
|
||||
|
||||
let t = encode::<RGB8>(
|
||||
&rgb8,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Uncompressed,
|
||||
Predictor::None,
|
||||
);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_rgb8))
|
||||
.bench_function("rgb8", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
let t = encode::<RGBA8>(
|
||||
&rgba8,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Uncompressed,
|
||||
Predictor::None,
|
||||
);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_rgba8))
|
||||
.bench_function("rgba8", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
let t = encode::<Gray8>(
|
||||
&gray8,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Uncompressed,
|
||||
Predictor::None,
|
||||
);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_gray8))
|
||||
.bench_function("gray8", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
let t = encode::<RGB16>(
|
||||
&rgb16,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Uncompressed,
|
||||
Predictor::None,
|
||||
);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_rgb16))
|
||||
.bench_function("rgb16", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
let t = encode::<Gray16>(
|
||||
&gray16,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Uncompressed,
|
||||
Predictor::None,
|
||||
);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_gray16))
|
||||
.bench_function("gray16", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// --- Predictor overhead isolation (deflate-1, with vs without predictor) ---
|
||||
{
|
||||
let mut group = c.benchmark_group("synth-predictor");
|
||||
group.sample_size(10);
|
||||
|
||||
let t = encode::<RGB8>(
|
||||
&rgb8,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Deflate(1),
|
||||
Predictor::None,
|
||||
);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_rgb8))
|
||||
.bench_function("rgb8-deflate-none", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
let t = encode::<RGB8>(
|
||||
&rgb8,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Deflate(1),
|
||||
Predictor::Horizontal,
|
||||
);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_rgb8))
|
||||
.bench_function("rgb8-deflate-hpred", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
let t = encode::<RGB16>(
|
||||
&rgb16,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Deflate(1),
|
||||
Predictor::None,
|
||||
);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_rgb16))
|
||||
.bench_function("rgb16-deflate-none", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
let t = encode::<RGB16>(
|
||||
&rgb16,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Deflate(1),
|
||||
Predictor::Horizontal,
|
||||
);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_rgb16))
|
||||
.bench_function("rgb16-deflate-hpred", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
let t = encode::<RGB32Float>(
|
||||
&rgb_f32,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Deflate(1),
|
||||
Predictor::None,
|
||||
);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_f32x3))
|
||||
.bench_function("rgb-f32-deflate-none", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
let t = encode::<RGB32Float>(
|
||||
&rgb_f32,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Deflate(1),
|
||||
Predictor::FloatingPoint,
|
||||
);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_f32x3))
|
||||
.bench_function("rgb-f32-deflate-fpred", |b| {
|
||||
b.iter(|| decode_tiff_bytes(&t))
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// --- LZW vs Deflate comparison ---
|
||||
{
|
||||
let mut group = c.benchmark_group("synth-compression");
|
||||
group.sample_size(10);
|
||||
|
||||
let t = encode::<RGB8>(
|
||||
&rgb8,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Deflate(1),
|
||||
Predictor::Horizontal,
|
||||
);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_rgb8))
|
||||
.bench_function("rgb8-deflate-hpred", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
let t = encode::<RGB8>(
|
||||
&rgb8,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Lzw,
|
||||
Predictor::Horizontal,
|
||||
);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_rgb8))
|
||||
.bench_function("rgb8-lzw-hpred", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
let t = encode::<RGB8>(&rgb8, WIDTH, HEIGHT, Compression::Lzw, Predictor::None);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_rgb8))
|
||||
.bench_function("rgb8-lzw-none", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
let t = encode::<RGB8>(&rgb8, WIDTH, HEIGHT, Compression::Packbits, Predictor::None);
|
||||
group
|
||||
.throughput(Throughput::Bytes(raw_rgb8))
|
||||
.bench_function("rgb8-packbits", |b| b.iter(|| decode_tiff_bytes(&t)));
|
||||
|
||||
group.finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
extern crate criterion;
|
||||
extern crate tiff;
|
||||
|
||||
use criterion::{black_box, Criterion, Throughput};
|
||||
use std::io::Cursor;
|
||||
use tiff::encoder::colortype::RGB32Float;
|
||||
use tiff::encoder::{Compression, Predictor, TiffEncoder};
|
||||
|
||||
const WIDTH: u32 = 500;
|
||||
const HEIGHT: u32 = 500;
|
||||
|
||||
fn encode_image_f32_float_predictor_deflate(data: &[f32]) {
|
||||
let mut buf = Cursor::new(Vec::with_capacity(WIDTH as usize * HEIGHT as usize * 4));
|
||||
TiffEncoder::new(&mut buf)
|
||||
.unwrap()
|
||||
.with_predictor(Predictor::FloatingPoint)
|
||||
.with_compression(Compression::Deflate(1))
|
||||
.write_image::<RGB32Float>(WIDTH, HEIGHT, black_box(data))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Generate synthetic f32 image data with a gradient pattern
|
||||
let data: Vec<f32> = (0..WIDTH as usize * HEIGHT as usize * 3)
|
||||
.map(|i| {
|
||||
let pixel = i / 3;
|
||||
let x = (pixel % WIDTH as usize) as f32 / WIDTH as f32;
|
||||
let y = (pixel / WIDTH as usize) as f32 / HEIGHT as f32;
|
||||
x * 0.5 + y * 0.5
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut c = Criterion::default().configure_from_args();
|
||||
let mut group = c.benchmark_group("tiff-encode");
|
||||
|
||||
group
|
||||
.sample_size(20)
|
||||
.throughput(Throughput::Bytes(
|
||||
(WIDTH as u64) * (HEIGHT as u64) * 3 * std::mem::size_of::<f32>() as u64,
|
||||
))
|
||||
.bench_function("f32-float-predictor-deflate-fast", |b| {
|
||||
b.iter(|| encode_image_f32_float_predictor_deflate(&data))
|
||||
});
|
||||
}
|
||||
+4
-5
@@ -8,13 +8,12 @@ use tiff::decoder::Decoder;
|
||||
|
||||
fn read_image(image: &[u8]) {
|
||||
let image = std::io::Cursor::new(image);
|
||||
let decoder = Decoder::new(black_box(image));
|
||||
let decoder = Decoder::open(black_box(image));
|
||||
let mut reader = decoder.unwrap();
|
||||
|
||||
while {
|
||||
while reader.more_images() {
|
||||
reader.next_directory().unwrap();
|
||||
reader.read_image().unwrap();
|
||||
reader.more_images()
|
||||
} {}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
+152
-20
@@ -1,29 +1,31 @@
|
||||
use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingBuffer, DecodingResult};
|
||||
use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingSampleBuffer, DecodingSampleSlice};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Some(image) = std::env::args_os().nth(1) else {
|
||||
eprintln!("Usage: decode FILE");
|
||||
eprintln!("Usage: decode FILE [--lenient]");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let lenient = std::env::args().nth(2).as_deref() == Some("--lenient");
|
||||
|
||||
let file = std::fs::File::open(image)?;
|
||||
let io = std::io::BufReader::new(file);
|
||||
let mut reader = Decoder::new(io)?;
|
||||
let mut reader = Decoder::open(io)?.with_lenient(lenient);
|
||||
|
||||
let mut data = DecodingResult::I8(vec![]);
|
||||
let mut data = DecodingSampleBuffer::I8(vec![]);
|
||||
|
||||
for i in 0u32.. {
|
||||
if !reader.more_images() {
|
||||
break;
|
||||
}
|
||||
|
||||
reader.next_directory()?;
|
||||
|
||||
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(())
|
||||
@@ -31,12 +33,38 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
fn debug_planes(
|
||||
index: u32,
|
||||
data: &mut DecodingResult,
|
||||
data: &mut DecodingSampleBuffer,
|
||||
layout: &BufferLayoutPreference,
|
||||
colortype: &tiff::ColorType,
|
||||
(width, height): (u32, u32),
|
||||
) {
|
||||
match data {
|
||||
DecodingSampleBuffer::F16(v) => {
|
||||
let samples = v
|
||||
.iter()
|
||||
.map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16)
|
||||
.collect::<Vec<_>>();
|
||||
*data = DecodingSampleBuffer::U16(samples);
|
||||
}
|
||||
DecodingSampleBuffer::F32(v) => {
|
||||
let samples = v
|
||||
.iter()
|
||||
.map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16)
|
||||
.collect::<Vec<_>>();
|
||||
*data = DecodingSampleBuffer::U16(samples);
|
||||
}
|
||||
DecodingSampleBuffer::F64(v) => {
|
||||
let samples = v
|
||||
.iter()
|
||||
.map(|&f| (f * f64::from(u16::MAX)) as u16)
|
||||
.collect::<Vec<_>>();
|
||||
*data = DecodingSampleBuffer::U16(samples);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let (depth, mut tupltype) = match colortype {
|
||||
// Note: we will expand, so this is in fact not BLACKANDWHITE
|
||||
tiff::ColorType::Gray(_) => (1, "GRAYSCALE"),
|
||||
tiff::ColorType::RGB(_) => (3, "RGB"),
|
||||
tiff::ColorType::RGBA(_) => (4, "RGB_ALPHA"),
|
||||
@@ -49,30 +77,81 @@ fn debug_planes(
|
||||
}
|
||||
};
|
||||
|
||||
// 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) {
|
||||
DecodingBuffer::U8(_) => u64::from(u8::MAX),
|
||||
DecodingBuffer::U16(_) => u64::from(u16::MAX),
|
||||
DecodingBuffer::U32(_) => u64::from(u32::MAX),
|
||||
DecodingBuffer::U64(_) => u64::MAX,
|
||||
// `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.
|
||||
DecodingSampleSlice::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 = DecodingSampleBuffer::U8(fallback_buffer);
|
||||
u64::from(u8::MAX)
|
||||
}
|
||||
2 => {
|
||||
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
||||
expand_2bit(buf, swidth, &mut fallback_buffer);
|
||||
*data = DecodingSampleBuffer::U8(fallback_buffer);
|
||||
u64::from(u8::MAX)
|
||||
}
|
||||
1 => {
|
||||
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
||||
expand_1bit(buf, swidth, &mut fallback_buffer);
|
||||
*data = DecodingSampleBuffer::U8(fallback_buffer);
|
||||
u64::from(u8::MAX)
|
||||
}
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Unsupported bit depth for U8 buffer: {}",
|
||||
colortype.bit_depth()
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
DecodingSampleSlice::U16(_) => u64::from(u16::MAX),
|
||||
DecodingSampleSlice::U32(_) => u64::from(u32::MAX),
|
||||
DecodingSampleSlice::U64(_) => u64::MAX,
|
||||
// Eh, this is a good effort I guess.
|
||||
DecodingBuffer::I8(items) => {
|
||||
DecodingSampleSlice::I8(items) => {
|
||||
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
||||
u64::from(i8::MAX as u8)
|
||||
}
|
||||
DecodingBuffer::I16(items) => {
|
||||
DecodingSampleSlice::I16(items) => {
|
||||
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
||||
u64::from(i16::MAX as u16)
|
||||
}
|
||||
DecodingBuffer::I32(items) => {
|
||||
DecodingSampleSlice::I32(items) => {
|
||||
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
||||
u64::from(i32::MAX as u32)
|
||||
}
|
||||
DecodingBuffer::I64(items) => {
|
||||
DecodingSampleSlice::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,
|
||||
DecodingSampleSlice::F16(_) | DecodingSampleSlice::F32(_) | DecodingSampleSlice::F64(_) => {
|
||||
return
|
||||
}
|
||||
};
|
||||
|
||||
let byte_len = if layout.planes > 1 {
|
||||
@@ -101,3 +180,56 @@ fn debug_planes(
|
||||
.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,130 @@
|
||||
//! Profiling helper: encodes and decodes several TIFF configurations.
|
||||
//! Run under callgrind: `valgrind --tool=callgrind target/release/examples/profile_decode`
|
||||
use std::io::Cursor;
|
||||
use tiff::decoder::Decoder;
|
||||
use tiff::encoder::colortype::{Gray8, RGB32Float, RGB16, RGB8};
|
||||
use tiff::encoder::{Compression, Predictor, TiffEncoder};
|
||||
|
||||
const WIDTH: u32 = 2048;
|
||||
const HEIGHT: u32 = 2048;
|
||||
const ITERS: usize = 3;
|
||||
|
||||
fn generate_rgb8(w: u32, h: u32) -> Vec<u8> {
|
||||
(0..w * h * 3).map(|i| (i % 256) as u8).collect()
|
||||
}
|
||||
|
||||
fn generate_rgb16(w: u32, h: u32) -> Vec<u16> {
|
||||
(0..w * h * 3).map(|i| (i % 65536) as u16).collect()
|
||||
}
|
||||
|
||||
fn generate_rgb_f32(w: u32, h: u32) -> Vec<f32> {
|
||||
(0..w * h * 3)
|
||||
.map(|i| (i as f32) / (w * h * 3) as f32)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_gray8(w: u32, h: u32) -> Vec<u8> {
|
||||
(0..w * h).map(|i| (i % 256) as u8).collect()
|
||||
}
|
||||
|
||||
fn encode<C: tiff::encoder::colortype::ColorType>(
|
||||
data: &[C::Inner],
|
||||
w: u32,
|
||||
h: u32,
|
||||
compression: Compression,
|
||||
predictor: Predictor,
|
||||
) -> Vec<u8>
|
||||
where
|
||||
[C::Inner]: tiff::encoder::TiffValue,
|
||||
{
|
||||
let mut buf = Cursor::new(Vec::new());
|
||||
TiffEncoder::new(&mut buf)
|
||||
.unwrap()
|
||||
.with_predictor(predictor)
|
||||
.with_compression(compression)
|
||||
.write_image::<C>(w, h, data)
|
||||
.unwrap();
|
||||
buf.into_inner()
|
||||
}
|
||||
|
||||
fn decode(data: &[u8]) {
|
||||
let cursor = Cursor::new(data);
|
||||
let mut decoder = Decoder::open(cursor).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
let _ = decoder.read_image().unwrap();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let rgb8 = generate_rgb8(WIDTH, HEIGHT);
|
||||
let rgb16 = generate_rgb16(WIDTH, HEIGHT);
|
||||
let rgb_f32 = generate_rgb_f32(WIDTH, HEIGHT);
|
||||
let gray8 = generate_gray8(WIDTH, HEIGHT);
|
||||
|
||||
let cases: Vec<(&str, Vec<u8>)> = vec![
|
||||
(
|
||||
"deflate-rgb8-hpredict",
|
||||
encode::<RGB8>(
|
||||
&rgb8,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Deflate(1),
|
||||
Predictor::Horizontal,
|
||||
),
|
||||
),
|
||||
(
|
||||
"deflate-gray8-hpredict",
|
||||
encode::<Gray8>(
|
||||
&gray8,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Deflate(1),
|
||||
Predictor::Horizontal,
|
||||
),
|
||||
),
|
||||
(
|
||||
"deflate-rgb16-hpredict",
|
||||
encode::<RGB16>(
|
||||
&rgb16,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Deflate(1),
|
||||
Predictor::Horizontal,
|
||||
),
|
||||
),
|
||||
(
|
||||
"deflate-rgb-f32-fpredict",
|
||||
encode::<RGB32Float>(
|
||||
&rgb_f32,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Deflate(1),
|
||||
Predictor::FloatingPoint,
|
||||
),
|
||||
),
|
||||
(
|
||||
"lzw-rgb8-no-pred",
|
||||
encode::<RGB8>(&rgb8, WIDTH, HEIGHT, Compression::Lzw, Predictor::None),
|
||||
),
|
||||
(
|
||||
"lzw-rgb8-hpredict",
|
||||
encode::<RGB8>(
|
||||
&rgb8,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
Compression::Lzw,
|
||||
Predictor::Horizontal,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
for (name, tiff_data) in &cases {
|
||||
eprintln!(
|
||||
"=== Profiling: {} ({} bytes encoded) ===",
|
||||
name,
|
||||
tiff_data.len()
|
||||
);
|
||||
for _ in 0..ITERS {
|
||||
decode(tiff_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use tiff::decoder::Decoder;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Some(image) = std::env::args_os().nth(1) else {
|
||||
eprintln!("Usage: decode FILE");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let file = std::fs::File::open(image)?;
|
||||
let io = std::io::BufReader::new(file);
|
||||
let mut reader = Decoder::open(io)?;
|
||||
|
||||
while reader.more_images() {
|
||||
reader.next_directory()?;
|
||||
ls_dir(&mut reader);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ls_dir<R: std::io::Read + std::io::Seek>(reader: &mut Decoder<R>) {
|
||||
if let Some(ifd) = reader.ifd_pointer() {
|
||||
println!("Directory at {ifd:x}");
|
||||
}
|
||||
|
||||
println!("Name\tHex\tType\tCount");
|
||||
let ifd = reader.current_ifd();
|
||||
|
||||
for (tag, entry) in ifd.directory().iter() {
|
||||
let name: &'static str = match tag {
|
||||
tiff::tags::Tag::Artist => "Artist",
|
||||
tiff::tags::Tag::BitsPerSample => "BitsPerSample",
|
||||
tiff::tags::Tag::CellLength => "CellLength",
|
||||
tiff::tags::Tag::CellWidth => "CellWidth",
|
||||
tiff::tags::Tag::ColorMap => "ColorMap",
|
||||
tiff::tags::Tag::Compression => "Compression",
|
||||
tiff::tags::Tag::DateTime => "DateTime",
|
||||
tiff::tags::Tag::ExtraSamples => "ExtraSamples",
|
||||
tiff::tags::Tag::FillOrder => "FillOrder",
|
||||
tiff::tags::Tag::FreeByteCounts => "FreeByteCounts",
|
||||
tiff::tags::Tag::FreeOffsets => "FreeOffsets",
|
||||
tiff::tags::Tag::GrayResponseCurve => "GrayResponseCurve",
|
||||
tiff::tags::Tag::GrayResponseUnit => "GrayResponseUnit",
|
||||
tiff::tags::Tag::HostComputer => "HostComputer",
|
||||
tiff::tags::Tag::ImageDescription => "ImageDescription",
|
||||
tiff::tags::Tag::ImageLength => "ImageLength",
|
||||
tiff::tags::Tag::ImageWidth => "ImageWidth",
|
||||
tiff::tags::Tag::Make => "Make",
|
||||
tiff::tags::Tag::MaxSampleValue => "MaxSampleValue",
|
||||
tiff::tags::Tag::MinSampleValue => "MinSampleValue",
|
||||
tiff::tags::Tag::Model => "Model",
|
||||
tiff::tags::Tag::NewSubfileType => "NewSubfileType",
|
||||
tiff::tags::Tag::Orientation => "Orientation",
|
||||
tiff::tags::Tag::PhotometricInterpretation => "PhotometricInterpretation",
|
||||
tiff::tags::Tag::PlanarConfiguration => "PlanarConfiguration",
|
||||
tiff::tags::Tag::ResolutionUnit => "ResolutionUnit",
|
||||
tiff::tags::Tag::RowsPerStrip => "RowsPerStrip",
|
||||
tiff::tags::Tag::SamplesPerPixel => "SamplesPerPixel",
|
||||
tiff::tags::Tag::Software => "Software",
|
||||
tiff::tags::Tag::StripByteCounts => "StripByteCounts",
|
||||
tiff::tags::Tag::StripOffsets => "StripOffsets",
|
||||
tiff::tags::Tag::SubfileType => "SubfileType",
|
||||
tiff::tags::Tag::Threshholding => "Threshholding",
|
||||
tiff::tags::Tag::XResolution => "XResolution",
|
||||
tiff::tags::Tag::YResolution => "YResolution",
|
||||
tiff::tags::Tag::Predictor => "Predictor",
|
||||
tiff::tags::Tag::TileWidth => "TileWidth",
|
||||
tiff::tags::Tag::TileLength => "TileLength",
|
||||
tiff::tags::Tag::TileOffsets => "TileOffsets",
|
||||
tiff::tags::Tag::TileByteCounts => "TileByteCounts",
|
||||
tiff::tags::Tag::SubIfd => "SubIfd",
|
||||
tiff::tags::Tag::SampleFormat => "SampleFormat",
|
||||
tiff::tags::Tag::SMinSampleValue => "SMinSampleValue",
|
||||
tiff::tags::Tag::SMaxSampleValue => "SMaxSampleValue",
|
||||
tiff::tags::Tag::JPEGTables => "JPEGTables",
|
||||
tiff::tags::Tag::ChromaSubsampling => "ChromaSubsampling",
|
||||
tiff::tags::Tag::ChromaPositioning => "ChromaPositioning",
|
||||
tiff::tags::Tag::ModelPixelScaleTag => "ModelPixelScaleTag",
|
||||
tiff::tags::Tag::ModelTransformationTag => "ModelTransformationTag",
|
||||
tiff::tags::Tag::ModelTiepointTag => "ModelTiepointTag",
|
||||
tiff::tags::Tag::Copyright => "Copyright",
|
||||
tiff::tags::Tag::ExifDirectory => "ExifDirectory",
|
||||
tiff::tags::Tag::GpsDirectory => "GpsDirectory",
|
||||
tiff::tags::Tag::IccProfile => "IccProfile",
|
||||
tiff::tags::Tag::GeoKeyDirectoryTag => "GeoKeyDirectoryTag",
|
||||
tiff::tags::Tag::GeoDoubleParamsTag => "GeoDoubleParamsTag",
|
||||
tiff::tags::Tag::GeoAsciiParamsTag => "GeoAsciiParamsTag",
|
||||
tiff::tags::Tag::ExifVersion => "ExifVersion",
|
||||
tiff::tags::Tag::GdalNodata => "GdalNodata",
|
||||
_ => "<unknown>",
|
||||
};
|
||||
|
||||
let ty: Cow<'static, str> = match entry.field_type() {
|
||||
tiff::tags::Type::BYTE => "u8".into(),
|
||||
tiff::tags::Type::ASCII => "ascii".into(),
|
||||
tiff::tags::Type::SHORT => "u16".into(),
|
||||
tiff::tags::Type::LONG => "u32".into(),
|
||||
tiff::tags::Type::RATIONAL => "r32".into(),
|
||||
tiff::tags::Type::SBYTE => "i8".into(),
|
||||
tiff::tags::Type::UNDEFINED => "byte".into(),
|
||||
tiff::tags::Type::SSHORT => "s16".into(),
|
||||
tiff::tags::Type::SLONG => "s32".into(),
|
||||
tiff::tags::Type::SRATIONAL => "sr32".into(),
|
||||
tiff::tags::Type::FLOAT => "f32".into(),
|
||||
tiff::tags::Type::DOUBLE => "f64".into(),
|
||||
tiff::tags::Type::IFD => "ifd32".into(),
|
||||
tiff::tags::Type::LONG8 => "u64".into(),
|
||||
tiff::tags::Type::SLONG8 => "i64".into(),
|
||||
tiff::tags::Type::IFD8 => "ifd64".into(),
|
||||
other => format!("{:x}", other.to_u16()).into(),
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"{name:16}\t{tag:4x}\t{ty}\t{count}",
|
||||
tag = tag.to_u16(),
|
||||
count = entry.count(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -19,7 +19,7 @@ path = ".."
|
||||
members = ["."]
|
||||
|
||||
[[bin]]
|
||||
name = "decode_image"
|
||||
path = "fuzz_targets/decode_image.rs"
|
||||
name = "decode_comprehensive"
|
||||
path = "fuzz_targets/decode_comprehensive.rs"
|
||||
test = false
|
||||
doc = false
|
||||
@@ -0,0 +1,96 @@
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use std::io::{Cursor, Read, Seek};
|
||||
use tiff::decoder::{Decoder, Limits};
|
||||
use tiff::tags::Tag;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
// Standalone header parse (separate code path from Decoder::open)
|
||||
let _ = tiff::decoder::TiffHeader::parse(Cursor::new(data));
|
||||
|
||||
let mut decoder = match Decoder::open(Cursor::new(data)) {
|
||||
Ok(d) => d,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let mut limits = Limits::default();
|
||||
limits.decoding_buffer_size = 512 * 1024;
|
||||
limits.ifd_value_size = 64 * 1024;
|
||||
limits.intermediate_buffer_size = 512 * 1024;
|
||||
decoder = decoder.with_limits(limits);
|
||||
|
||||
// Probe first IFD (opened but image not yet parsed)
|
||||
probe_directory(&mut decoder);
|
||||
|
||||
// Linear IFD iteration via next_directory — tolerates invalid
|
||||
// intermediate IFDs that aren't decodable as images
|
||||
let mut ifd_count = 1u32;
|
||||
while decoder.more_images() && ifd_count < 8 {
|
||||
if decoder.next_directory().is_err() {
|
||||
break;
|
||||
}
|
||||
probe_directory(&mut decoder);
|
||||
ifd_count += 1;
|
||||
}
|
||||
|
||||
// Random-access traversal via seek (different code path: find_nth_ifd
|
||||
// re-walks the IFD chain from the start each time)
|
||||
for i in 0..ifd_count.min(4) as usize {
|
||||
if decoder.seek_to_directory(i).is_ok() {
|
||||
probe_directory(&mut decoder);
|
||||
}
|
||||
// seek_to_image eagerly parses the directory as an Image
|
||||
let _ = decoder.seek_to_image(i);
|
||||
}
|
||||
});
|
||||
|
||||
fn probe_directory<R: Read + Seek>(decoder: &mut Decoder<R>) {
|
||||
// Tag iteration — exercises value parsing for every tag in the IFD
|
||||
for result in decoder.current_ifd().tag_iter() {
|
||||
let _ = result;
|
||||
}
|
||||
|
||||
// SubIFD / EXIF / GPS directory traversal — exercises read_directory
|
||||
// with potentially adversarial offsets
|
||||
if let Ok(Some(val)) = decoder.current_ifd().find_tag(Tag::SubIfd) {
|
||||
if let Ok(ptrs) = val.into_ifd_vec() {
|
||||
for ptr in ptrs.into_iter().take(4) {
|
||||
let _ = decoder.read_directory(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
for tag in [Tag::ExifDirectory, Tag::GpsDirectory] {
|
||||
if let Ok(Some(val)) = decoder.current_ifd().find_tag(tag) {
|
||||
if let Ok(ptr) = val.into_ifd_pointer() {
|
||||
let _ = decoder.read_directory(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Image-level probing — dimensions() triggers ensure_image,
|
||||
// which lazily parses the directory as an Image
|
||||
if decoder.dimensions().is_ok() {
|
||||
let _ = decoder.colortype();
|
||||
let _ = decoder.color_map();
|
||||
let _ = decoder.get_chunk_type();
|
||||
let _ = decoder.chunk_dimensions();
|
||||
let _ = decoder.strip_count();
|
||||
let _ = decoder.tile_count();
|
||||
let _ = decoder.image_buffer_layout();
|
||||
let _ = decoder.image_chunk_buffer_layout(0);
|
||||
|
||||
// Chunk reads at boundaries — first and last exercise
|
||||
// different offset calculations
|
||||
let _ = decoder.read_chunk(0);
|
||||
let chunk_count = decoder
|
||||
.strip_count()
|
||||
.or_else(|_| decoder.tile_count())
|
||||
.unwrap_or(0);
|
||||
if chunk_count > 1 {
|
||||
let _ = decoder.read_chunk(chunk_count - 1);
|
||||
}
|
||||
|
||||
// Full image decode
|
||||
let _ = decoder.read_image();
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let mut decoder = if let Ok(d) = tiff::decoder::Decoder::new(std::io::Cursor::new(data)) {
|
||||
d
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut limits = tiff::decoder::Limits::default();
|
||||
limits.decoding_buffer_size = 1_000_000;
|
||||
limits.ifd_value_size = 1_000_000;
|
||||
limits.intermediate_buffer_size = 1_000_000;
|
||||
|
||||
decoder = decoder.with_limits(limits);
|
||||
|
||||
loop {
|
||||
if let Err(_) = decoder.read_image() {
|
||||
break;
|
||||
}
|
||||
|
||||
if !decoder.more_images() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Err(_) = decoder.next_image() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
# === File headers ===
|
||||
|
||||
# Classic TIFF (LE, BE)
|
||||
"\x49\x49\x2A\x00"
|
||||
"\x4D\x4D\x00\x2A"
|
||||
# BigTIFF (LE, BE) — version 43, 8-byte offsets, different parsing paths
|
||||
"\x49\x49\x2B\x00\x08\x00\x00\x00"
|
||||
"\x4D\x4D\x00\x2B\x00\x08\x00\x00"
|
||||
|
||||
# === IFD field type codes (2-byte LE) ===
|
||||
# Decoder switches on these; wrong types exercise error paths
|
||||
|
||||
"\x01\x00"
|
||||
"\x02\x00"
|
||||
"\x03\x00"
|
||||
"\x04\x00"
|
||||
"\x05\x00"
|
||||
"\x06\x00"
|
||||
"\x07\x00"
|
||||
"\x08\x00"
|
||||
"\x09\x00"
|
||||
"\x0A\x00"
|
||||
"\x0B\x00"
|
||||
"\x0C\x00"
|
||||
"\x0D\x00"
|
||||
"\x10\x00"
|
||||
"\x11\x00"
|
||||
"\x12\x00"
|
||||
|
||||
# === IFD tag numbers (2-byte LE) ===
|
||||
|
||||
# 256=ImageWidth 257=ImageLength 258=BitsPerSample
|
||||
"\x00\x01"
|
||||
"\x01\x01"
|
||||
"\x02\x01"
|
||||
# 259=Compression 262=PhotometricInterpretation
|
||||
"\x03\x01"
|
||||
"\x06\x01"
|
||||
# 273=StripOffsets 274=Orientation
|
||||
"\x11\x01"
|
||||
"\x12\x01"
|
||||
# 277=SamplesPerPixel 278=RowsPerStrip 279=StripByteCounts
|
||||
"\x15\x01"
|
||||
"\x16\x01"
|
||||
"\x17\x01"
|
||||
# 282=XResolution 283=YResolution 284=PlanarConfiguration
|
||||
"\x1A\x01"
|
||||
"\x1B\x01"
|
||||
"\x1C\x01"
|
||||
# 317=Predictor 320=ColorMap
|
||||
"\x3D\x01"
|
||||
"\x40\x01"
|
||||
# 322=TileWidth 323=TileLength 324=TileOffsets 325=TileByteCounts
|
||||
"\x42\x01"
|
||||
"\x43\x01"
|
||||
"\x44\x01"
|
||||
"\x45\x01"
|
||||
# 330=SubIfd 338=ExtraSamples 339=SampleFormat
|
||||
"\x4A\x01"
|
||||
"\x52\x01"
|
||||
"\x53\x01"
|
||||
# 347=JPEGTables 530=ChromaSubsampling
|
||||
"\x5B\x01"
|
||||
"\x12\x02"
|
||||
# 34665=ExifDirectory 34675=IccProfile 34853=GpsDirectory
|
||||
"\x69\x87"
|
||||
"\x73\x87"
|
||||
"\x25\x88"
|
||||
|
||||
# === IFD tag numbers (2-byte BE) ===
|
||||
|
||||
"\x01\x00"
|
||||
"\x01\x01"
|
||||
"\x01\x02"
|
||||
"\x01\x03"
|
||||
"\x01\x06"
|
||||
"\x01\x11"
|
||||
"\x01\x12"
|
||||
"\x01\x15"
|
||||
"\x01\x16"
|
||||
"\x01\x17"
|
||||
"\x01\x1A"
|
||||
"\x01\x1B"
|
||||
"\x01\x1C"
|
||||
"\x01\x3D"
|
||||
"\x01\x40"
|
||||
"\x01\x42"
|
||||
"\x01\x43"
|
||||
"\x01\x44"
|
||||
"\x01\x45"
|
||||
"\x01\x4A"
|
||||
"\x01\x52"
|
||||
"\x01\x53"
|
||||
"\x01\x5B"
|
||||
"\x02\x12"
|
||||
"\x87\x69"
|
||||
"\x87\x73"
|
||||
"\x88\x25"
|
||||
|
||||
# === Compression codes (4-byte LE inline SHORT values) ===
|
||||
# These are the values stored in the IFD entry for tag 259
|
||||
|
||||
# 1=None 2=Huffman 3=Fax3 4=Fax4 5=LZW
|
||||
"\x01\x00\x00\x00"
|
||||
"\x02\x00\x00\x00"
|
||||
"\x03\x00\x00\x00"
|
||||
"\x04\x00\x00\x00"
|
||||
"\x05\x00\x00\x00"
|
||||
# 6=OldJPEG 7=ModernJPEG 8=Deflate
|
||||
"\x06\x00\x00\x00"
|
||||
"\x07\x00\x00\x00"
|
||||
"\x08\x00\x00\x00"
|
||||
# 0x8005=PackBits 0x80B2=OldDeflate
|
||||
"\x05\x80\x00\x00"
|
||||
"\xB2\x80\x00\x00"
|
||||
|
||||
# === Useful small values (4-byte LE) ===
|
||||
# BitsPerSample=16, =32 (8 already covered as Deflate compression above)
|
||||
|
||||
"\x10\x00\x00\x00"
|
||||
"\x20\x00\x00\x00"
|
||||
|
||||
# === Minimal IFD entries (12-byte LE: tag + type SHORT + count 1 + value) ===
|
||||
# These let the fuzzer splice complete valid entries into an IFD
|
||||
|
||||
# ImageWidth=1
|
||||
"\x00\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
|
||||
# ImageLength=1
|
||||
"\x01\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
|
||||
# BitsPerSample=8
|
||||
"\x02\x01\x03\x00\x01\x00\x00\x00\x08\x00\x00\x00"
|
||||
# Compression=None
|
||||
"\x03\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
|
||||
# PhotometricInterpretation=BlackIsZero
|
||||
"\x06\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
|
||||
# SamplesPerPixel=1
|
||||
"\x15\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
|
||||
# RowsPerStrip=1
|
||||
"\x16\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
|
||||
@@ -0,0 +1,410 @@
|
||||
use half::f16;
|
||||
use std::alloc::{Layout, LayoutError};
|
||||
|
||||
use crate::{
|
||||
bytecast,
|
||||
decoder::{BufferLayoutPreference, Limits},
|
||||
error::{TiffError, TiffResult, TiffUnsupportedError},
|
||||
tags::{ByteOrder, EndianBytes, SampleFormat},
|
||||
};
|
||||
|
||||
/// Result of a decoding process
|
||||
#[derive(Debug)]
|
||||
pub enum DecodingSampleBuffer {
|
||||
/// A vector of unsigned bytes
|
||||
U8(Vec<u8>),
|
||||
/// A vector of unsigned words
|
||||
U16(Vec<u16>),
|
||||
/// A vector of 32 bit unsigned ints
|
||||
U32(Vec<u32>),
|
||||
/// A vector of 64 bit unsigned ints
|
||||
U64(Vec<u64>),
|
||||
/// A vector of 16 bit IEEE floats (held in u16)
|
||||
F16(Vec<f16>),
|
||||
/// A vector of 32 bit IEEE floats
|
||||
F32(Vec<f32>),
|
||||
/// A vector of 64 bit IEEE floats
|
||||
F64(Vec<f64>),
|
||||
/// A vector of 8 bit signed ints
|
||||
I8(Vec<i8>),
|
||||
/// A vector of 16 bit signed ints
|
||||
I16(Vec<i16>),
|
||||
/// A vector of 32 bit signed ints
|
||||
I32(Vec<i32>),
|
||||
/// A vector of 64 bit signed ints
|
||||
I64(Vec<i64>),
|
||||
}
|
||||
|
||||
impl DecodingSampleBuffer {
|
||||
/// Reallocate the buffer to decode all planes of the indicated layout.
|
||||
pub fn resize_to(
|
||||
&mut self,
|
||||
buffer: &BufferLayoutPreference,
|
||||
limits: &Limits,
|
||||
) -> Result<(), TiffError> {
|
||||
let sample_type = buffer.sample_type.ok_or(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::UnknownInterpretation,
|
||||
))?;
|
||||
|
||||
let extent = sample_type.extent_for_bytes(buffer.complete_len);
|
||||
self.resize_to_extent(extent, limits)
|
||||
}
|
||||
|
||||
pub(crate) fn resize_to_extent(
|
||||
&mut self,
|
||||
extent: DecodingExtent,
|
||||
limits: &Limits,
|
||||
) -> Result<(), TiffError> {
|
||||
// FIXME: we *can* reuse the allocation sometimes.
|
||||
*self = extent.to_result_buffer(limits)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new<T: Default + Copy>(
|
||||
size: usize,
|
||||
limits: &Limits,
|
||||
from_fn: fn(Vec<T>) -> Self,
|
||||
) -> TiffResult<DecodingSampleBuffer> {
|
||||
if size > limits.decoding_buffer_size / core::mem::size_of::<T>() {
|
||||
Err(TiffError::LimitsExceeded)
|
||||
} else {
|
||||
Ok(from_fn(vec![T::default(); size]))
|
||||
}
|
||||
}
|
||||
|
||||
fn new_u8(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
|
||||
Self::new(size, limits, DecodingSampleBuffer::U8)
|
||||
}
|
||||
|
||||
fn new_u16(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
|
||||
Self::new(size, limits, DecodingSampleBuffer::U16)
|
||||
}
|
||||
|
||||
fn new_u32(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
|
||||
Self::new(size, limits, DecodingSampleBuffer::U32)
|
||||
}
|
||||
|
||||
fn new_u64(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
|
||||
Self::new(size, limits, DecodingSampleBuffer::U64)
|
||||
}
|
||||
|
||||
fn new_f32(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
|
||||
Self::new(size, limits, DecodingSampleBuffer::F32)
|
||||
}
|
||||
|
||||
fn new_f64(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
|
||||
Self::new(size, limits, DecodingSampleBuffer::F64)
|
||||
}
|
||||
|
||||
fn new_f16(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
|
||||
Self::new(size, limits, DecodingSampleBuffer::F16)
|
||||
}
|
||||
|
||||
fn new_i8(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
|
||||
Self::new(size, limits, DecodingSampleBuffer::I8)
|
||||
}
|
||||
|
||||
fn new_i16(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
|
||||
Self::new(size, limits, DecodingSampleBuffer::I16)
|
||||
}
|
||||
|
||||
fn new_i32(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
|
||||
Self::new(size, limits, DecodingSampleBuffer::I32)
|
||||
}
|
||||
|
||||
fn new_i64(size: usize, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
|
||||
Self::new(size, limits, DecodingSampleBuffer::I64)
|
||||
}
|
||||
|
||||
/// Get a view of this buffer starting from the nth _sample_ of the current type.
|
||||
pub fn as_buffer(&mut self, start: usize) -> DecodingSampleSlice<'_> {
|
||||
match *self {
|
||||
DecodingSampleBuffer::U8(ref mut buf) => DecodingSampleSlice::U8(&mut buf[start..]),
|
||||
DecodingSampleBuffer::U16(ref mut buf) => DecodingSampleSlice::U16(&mut buf[start..]),
|
||||
DecodingSampleBuffer::U32(ref mut buf) => DecodingSampleSlice::U32(&mut buf[start..]),
|
||||
DecodingSampleBuffer::U64(ref mut buf) => DecodingSampleSlice::U64(&mut buf[start..]),
|
||||
DecodingSampleBuffer::F16(ref mut buf) => DecodingSampleSlice::F16(&mut buf[start..]),
|
||||
DecodingSampleBuffer::F32(ref mut buf) => DecodingSampleSlice::F32(&mut buf[start..]),
|
||||
DecodingSampleBuffer::F64(ref mut buf) => DecodingSampleSlice::F64(&mut buf[start..]),
|
||||
DecodingSampleBuffer::I8(ref mut buf) => DecodingSampleSlice::I8(&mut buf[start..]),
|
||||
DecodingSampleBuffer::I16(ref mut buf) => DecodingSampleSlice::I16(&mut buf[start..]),
|
||||
DecodingSampleBuffer::I32(ref mut buf) => DecodingSampleSlice::I32(&mut buf[start..]),
|
||||
DecodingSampleBuffer::I64(ref mut buf) => DecodingSampleSlice::I64(&mut buf[start..]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sample_type(&self) -> DecodingSampleType {
|
||||
match *self {
|
||||
DecodingSampleBuffer::U8(_) => DecodingSampleType::U8,
|
||||
DecodingSampleBuffer::U16(_) => DecodingSampleType::U16,
|
||||
DecodingSampleBuffer::U32(_) => DecodingSampleType::U32,
|
||||
DecodingSampleBuffer::U64(_) => DecodingSampleType::U64,
|
||||
DecodingSampleBuffer::F16(_) => DecodingSampleType::F16,
|
||||
DecodingSampleBuffer::F32(_) => DecodingSampleType::F32,
|
||||
DecodingSampleBuffer::F64(_) => DecodingSampleType::F64,
|
||||
DecodingSampleBuffer::I8(_) => DecodingSampleType::I8,
|
||||
DecodingSampleBuffer::I16(_) => DecodingSampleType::I16,
|
||||
DecodingSampleBuffer::I32(_) => DecodingSampleType::I32,
|
||||
DecodingSampleBuffer::I64(_) => DecodingSampleType::I64,
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the endianess of buffered data.
|
||||
///
|
||||
/// This utility may be used to prepare a buffer for consumption by methods taking raw bytes or
|
||||
/// after filling the buffer with raw bytes.
|
||||
///
|
||||
/// After this, interpreting the buffer on the host may no longer produce any reasonable
|
||||
/// results. For instance, floats in the wrong byte order may produce a `NaN` with varying
|
||||
/// payloads. Despite unexpected result however, this operation is always sound.
|
||||
pub fn convert_endian(&mut self, from: ByteOrder, to: ByteOrder) {
|
||||
self.as_buffer(0).convert_endian(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
// A buffer for image decoding
|
||||
pub enum DecodingSampleSlice<'a> {
|
||||
/// A slice of unsigned bytes
|
||||
U8(&'a mut [u8]),
|
||||
/// A slice of unsigned words
|
||||
U16(&'a mut [u16]),
|
||||
/// A slice of 32 bit unsigned ints
|
||||
U32(&'a mut [u32]),
|
||||
/// A slice of 64 bit unsigned ints
|
||||
U64(&'a mut [u64]),
|
||||
/// A slice of 16 bit IEEE floats
|
||||
F16(&'a mut [f16]),
|
||||
/// A slice of 32 bit IEEE floats
|
||||
F32(&'a mut [f32]),
|
||||
/// A slice of 64 bit IEEE floats
|
||||
F64(&'a mut [f64]),
|
||||
/// A slice of 8 bits signed ints
|
||||
I8(&'a mut [i8]),
|
||||
/// A slice of 16 bits signed ints
|
||||
I16(&'a mut [i16]),
|
||||
/// A slice of 32 bits signed ints
|
||||
I32(&'a mut [i32]),
|
||||
/// A slice of 64 bits signed ints
|
||||
I64(&'a mut [i64]),
|
||||
}
|
||||
|
||||
impl<'a> DecodingSampleSlice<'a> {
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
match self {
|
||||
DecodingSampleSlice::U8(buf) => buf,
|
||||
DecodingSampleSlice::I8(buf) => bytecast::i8_as_ne_bytes(buf),
|
||||
DecodingSampleSlice::U16(buf) => bytecast::u16_as_ne_bytes(buf),
|
||||
DecodingSampleSlice::I16(buf) => bytecast::i16_as_ne_bytes(buf),
|
||||
DecodingSampleSlice::U32(buf) => bytecast::u32_as_ne_bytes(buf),
|
||||
DecodingSampleSlice::I32(buf) => bytecast::i32_as_ne_bytes(buf),
|
||||
DecodingSampleSlice::U64(buf) => bytecast::u64_as_ne_bytes(buf),
|
||||
DecodingSampleSlice::I64(buf) => bytecast::i64_as_ne_bytes(buf),
|
||||
DecodingSampleSlice::F16(buf) => bytecast::f16_as_ne_bytes(buf),
|
||||
DecodingSampleSlice::F32(buf) => bytecast::f32_as_ne_bytes(buf),
|
||||
DecodingSampleSlice::F64(buf) => bytecast::f64_as_ne_bytes(buf),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
|
||||
match self {
|
||||
DecodingSampleSlice::U8(buf) => buf,
|
||||
DecodingSampleSlice::I8(buf) => bytecast::i8_as_ne_mut_bytes(buf),
|
||||
DecodingSampleSlice::U16(buf) => bytecast::u16_as_ne_mut_bytes(buf),
|
||||
DecodingSampleSlice::I16(buf) => bytecast::i16_as_ne_mut_bytes(buf),
|
||||
DecodingSampleSlice::U32(buf) => bytecast::u32_as_ne_mut_bytes(buf),
|
||||
DecodingSampleSlice::I32(buf) => bytecast::i32_as_ne_mut_bytes(buf),
|
||||
DecodingSampleSlice::U64(buf) => bytecast::u64_as_ne_mut_bytes(buf),
|
||||
DecodingSampleSlice::I64(buf) => bytecast::i64_as_ne_mut_bytes(buf),
|
||||
DecodingSampleSlice::F16(buf) => bytecast::f16_as_ne_mut_bytes(buf),
|
||||
DecodingSampleSlice::F32(buf) => bytecast::f32_as_ne_mut_bytes(buf),
|
||||
DecodingSampleSlice::F64(buf) => bytecast::f64_as_ne_mut_bytes(buf),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reborrow(&mut self) -> DecodingSampleSlice<'_> {
|
||||
match self {
|
||||
DecodingSampleSlice::U8(items) => DecodingSampleSlice::U8(items),
|
||||
DecodingSampleSlice::I8(items) => DecodingSampleSlice::I8(items),
|
||||
DecodingSampleSlice::U16(items) => DecodingSampleSlice::U16(items),
|
||||
DecodingSampleSlice::I16(items) => DecodingSampleSlice::I16(items),
|
||||
DecodingSampleSlice::U32(items) => DecodingSampleSlice::U32(items),
|
||||
DecodingSampleSlice::I32(items) => DecodingSampleSlice::I32(items),
|
||||
DecodingSampleSlice::U64(items) => DecodingSampleSlice::U64(items),
|
||||
DecodingSampleSlice::I64(items) => DecodingSampleSlice::I64(items),
|
||||
DecodingSampleSlice::F16(items) => DecodingSampleSlice::F16(items),
|
||||
DecodingSampleSlice::F32(items) => DecodingSampleSlice::F32(items),
|
||||
DecodingSampleSlice::F64(items) => DecodingSampleSlice::F64(items),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sample_type(&self) -> DecodingSampleType {
|
||||
match *self {
|
||||
DecodingSampleSlice::U8(_) => DecodingSampleType::U8,
|
||||
DecodingSampleSlice::U16(_) => DecodingSampleType::U16,
|
||||
DecodingSampleSlice::U32(_) => DecodingSampleType::U32,
|
||||
DecodingSampleSlice::U64(_) => DecodingSampleType::U64,
|
||||
DecodingSampleSlice::F16(_) => DecodingSampleType::F16,
|
||||
DecodingSampleSlice::F32(_) => DecodingSampleType::F32,
|
||||
DecodingSampleSlice::F64(_) => DecodingSampleType::F64,
|
||||
DecodingSampleSlice::I8(_) => DecodingSampleType::I8,
|
||||
DecodingSampleSlice::I16(_) => DecodingSampleType::I16,
|
||||
DecodingSampleSlice::I32(_) => DecodingSampleType::I32,
|
||||
DecodingSampleSlice::I64(_) => DecodingSampleType::I64,
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the endianess of referenced data.
|
||||
///
|
||||
/// This utility may be used to prepare a buffer for consumption by methods taking raw bytes or
|
||||
/// after filling the buffer with raw bytes.
|
||||
///
|
||||
/// After this, interpreting the buffer on the host may no longer produce any reasonable
|
||||
/// results. For instance, floats in the wrong byte order may produce a `NaN` with varying
|
||||
/// payloads. Despite unexpected result however, this operation is always sound.
|
||||
pub fn convert_endian(&mut self, from: ByteOrder, to: ByteOrder) {
|
||||
let byte_unit = self.sample_type().endian_bytes();
|
||||
from.convert_endian_bytes(byte_unit, self.as_bytes_mut(), to);
|
||||
}
|
||||
|
||||
pub fn byte_len(&self) -> usize {
|
||||
self.as_bytes().len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum DecodingSampleType {
|
||||
U8,
|
||||
U16,
|
||||
U32,
|
||||
U64,
|
||||
F16,
|
||||
F32,
|
||||
F64,
|
||||
I8,
|
||||
I16,
|
||||
I32,
|
||||
I64,
|
||||
}
|
||||
|
||||
impl DecodingSampleType {
|
||||
pub fn sample_format(self) -> SampleFormat {
|
||||
match self {
|
||||
DecodingSampleType::U8 => SampleFormat::Uint,
|
||||
DecodingSampleType::U16 => SampleFormat::Uint,
|
||||
DecodingSampleType::U32 => SampleFormat::Uint,
|
||||
DecodingSampleType::U64 => SampleFormat::Uint,
|
||||
DecodingSampleType::F16 => SampleFormat::IEEEFP,
|
||||
DecodingSampleType::F32 => SampleFormat::IEEEFP,
|
||||
DecodingSampleType::F64 => SampleFormat::IEEEFP,
|
||||
DecodingSampleType::I8 => SampleFormat::Int,
|
||||
DecodingSampleType::I16 => SampleFormat::Int,
|
||||
DecodingSampleType::I32 => SampleFormat::Int,
|
||||
DecodingSampleType::I64 => SampleFormat::Int,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn extent_for_bytes(self, bytes: usize) -> DecodingExtent {
|
||||
match self {
|
||||
DecodingSampleType::U8 => DecodingExtent::U8(bytes),
|
||||
DecodingSampleType::U16 => DecodingExtent::U16(bytes.div_ceil(2)),
|
||||
DecodingSampleType::U32 => DecodingExtent::U32(bytes.div_ceil(4)),
|
||||
DecodingSampleType::U64 => DecodingExtent::U64(bytes.div_ceil(8)),
|
||||
DecodingSampleType::I8 => DecodingExtent::I8(bytes),
|
||||
DecodingSampleType::I16 => DecodingExtent::I16(bytes.div_ceil(2)),
|
||||
DecodingSampleType::I32 => DecodingExtent::I32(bytes.div_ceil(4)),
|
||||
DecodingSampleType::I64 => DecodingExtent::I64(bytes.div_ceil(8)),
|
||||
DecodingSampleType::F16 => DecodingExtent::F16(bytes.div_ceil(2)),
|
||||
DecodingSampleType::F32 => DecodingExtent::F32(bytes.div_ceil(4)),
|
||||
DecodingSampleType::F64 => DecodingExtent::F64(bytes.div_ceil(8)),
|
||||
}
|
||||
}
|
||||
|
||||
fn endian_bytes(self) -> EndianBytes {
|
||||
use EndianBytes::*;
|
||||
|
||||
match self {
|
||||
DecodingSampleType::U8 => One,
|
||||
DecodingSampleType::U16 => Two,
|
||||
DecodingSampleType::U32 => Four,
|
||||
DecodingSampleType::U64 => Eight,
|
||||
DecodingSampleType::F16 => Two,
|
||||
DecodingSampleType::F32 => Four,
|
||||
DecodingSampleType::F64 => Eight,
|
||||
DecodingSampleType::I8 => One,
|
||||
DecodingSampleType::I16 => Two,
|
||||
DecodingSampleType::I32 => Four,
|
||||
DecodingSampleType::I64 => Eight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The count and matching discriminant for a `DecodingBuffer`.
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum DecodingExtent {
|
||||
U8(usize),
|
||||
U16(usize),
|
||||
U32(usize),
|
||||
U64(usize),
|
||||
F16(usize),
|
||||
F32(usize),
|
||||
F64(usize),
|
||||
I8(usize),
|
||||
I16(usize),
|
||||
I32(usize),
|
||||
I64(usize),
|
||||
}
|
||||
|
||||
impl DecodingExtent {
|
||||
pub(crate) fn to_result_buffer(&self, limits: &Limits) -> TiffResult<DecodingSampleBuffer> {
|
||||
match *self {
|
||||
DecodingExtent::U8(count) => DecodingSampleBuffer::new_u8(count, limits),
|
||||
DecodingExtent::U16(count) => DecodingSampleBuffer::new_u16(count, limits),
|
||||
DecodingExtent::U32(count) => DecodingSampleBuffer::new_u32(count, limits),
|
||||
DecodingExtent::U64(count) => DecodingSampleBuffer::new_u64(count, limits),
|
||||
DecodingExtent::F16(count) => DecodingSampleBuffer::new_f16(count, limits),
|
||||
DecodingExtent::F32(count) => DecodingSampleBuffer::new_f32(count, limits),
|
||||
DecodingExtent::F64(count) => DecodingSampleBuffer::new_f64(count, limits),
|
||||
DecodingExtent::I8(count) => DecodingSampleBuffer::new_i8(count, limits),
|
||||
DecodingExtent::I16(count) => DecodingSampleBuffer::new_i16(count, limits),
|
||||
DecodingExtent::I32(count) => DecodingSampleBuffer::new_i32(count, limits),
|
||||
DecodingExtent::I64(count) => DecodingSampleBuffer::new_i64(count, limits),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn preferred_layout(self) -> TiffResult<Layout> {
|
||||
fn overflow(_: LayoutError) -> TiffError {
|
||||
TiffError::LimitsExceeded
|
||||
}
|
||||
|
||||
match self {
|
||||
DecodingExtent::U8(count) => Layout::array::<u8>(count),
|
||||
DecodingExtent::U16(count) => Layout::array::<u16>(count),
|
||||
DecodingExtent::U32(count) => Layout::array::<u32>(count),
|
||||
DecodingExtent::U64(count) => Layout::array::<u64>(count),
|
||||
DecodingExtent::F16(count) => Layout::array::<f16>(count),
|
||||
DecodingExtent::F32(count) => Layout::array::<f32>(count),
|
||||
DecodingExtent::F64(count) => Layout::array::<f64>(count),
|
||||
DecodingExtent::I8(count) => Layout::array::<i8>(count),
|
||||
DecodingExtent::I16(count) => Layout::array::<i16>(count),
|
||||
DecodingExtent::I32(count) => Layout::array::<i32>(count),
|
||||
DecodingExtent::I64(count) => Layout::array::<i64>(count),
|
||||
}
|
||||
.map_err(overflow)
|
||||
}
|
||||
|
||||
pub(crate) fn sample_type(&self) -> DecodingSampleType {
|
||||
match *self {
|
||||
DecodingExtent::U8(_) => DecodingSampleType::U8,
|
||||
DecodingExtent::U16(_) => DecodingSampleType::U16,
|
||||
DecodingExtent::U32(_) => DecodingSampleType::U32,
|
||||
DecodingExtent::U64(_) => DecodingSampleType::U64,
|
||||
DecodingExtent::F16(_) => DecodingSampleType::F16,
|
||||
DecodingExtent::F32(_) => DecodingSampleType::F32,
|
||||
DecodingExtent::F64(_) => DecodingSampleType::F64,
|
||||
DecodingExtent::I8(_) => DecodingSampleType::I8,
|
||||
DecodingExtent::I16(_) => DecodingSampleType::I16,
|
||||
DecodingExtent::I32(_) => DecodingSampleType::I32,
|
||||
DecodingExtent::I64(_) => DecodingSampleType::I64,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use std::collections::HashMap;
|
||||
///
|
||||
/// 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)]
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct IfdCycles {
|
||||
/// The root of each component union.
|
||||
component_union: HashMap<ComponentId, ComponentId>,
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
use std::io::{self, Cursor, Read, Seek};
|
||||
|
||||
use crate::{
|
||||
decoder::{ifd::Entry, stream::EndianReader, ValueReader},
|
||||
tags::{ByteOrder, IfdPointer, Type},
|
||||
TiffResult,
|
||||
};
|
||||
|
||||
pub struct EntryBytesReader<'lt, R> {
|
||||
inner: &'lt mut EndianReader<R>,
|
||||
variant: Variant,
|
||||
}
|
||||
|
||||
const OFFSET_BUFFER_LEN: usize = 8;
|
||||
|
||||
enum Variant {
|
||||
Immediate {
|
||||
/// An in-value reader if the offset field contains the data. Note that we rotate it to the
|
||||
/// back of this buffer, not the front, so that `Cursor` already contains the right end.
|
||||
/// We do not let you seek back beyond our start.
|
||||
data: Cursor<[u8; OFFSET_BUFFER_LEN]>,
|
||||
value_bytes: u8,
|
||||
},
|
||||
InFile {
|
||||
// the length relative to the base so we can seek relative without querying the position.
|
||||
value_bytes: u64,
|
||||
remainder: u64,
|
||||
base: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'lt, R> EntryBytesReader<'lt, R> {
|
||||
pub(super) fn from_entry(inner: &'lt mut ValueReader<R>, entry: Entry) -> TiffResult<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let offset: [_; OFFSET_BUFFER_LEN] = *entry.offset_raw();
|
||||
let limit = if inner.bigtiff { 8 } else { 4 };
|
||||
assert!(limit as usize <= OFFSET_BUFFER_LEN);
|
||||
|
||||
let value_bytes = entry.field_type().value_bytes(entry.count())?;
|
||||
|
||||
if value_bytes <= limit {
|
||||
// We move the data to the back of the array whereas it is in the front right now.
|
||||
let mut value = offset;
|
||||
value.rotate_left(value_bytes as usize);
|
||||
let mut cursor = Cursor::new(value);
|
||||
cursor.set_position(OFFSET_BUFFER_LEN as u64 - value_bytes);
|
||||
|
||||
Ok(EntryBytesReader {
|
||||
inner: &mut inner.reader,
|
||||
variant: Variant::Immediate {
|
||||
data: cursor,
|
||||
value_bytes: value_bytes as u8,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// remainder > limit >= 0 so this is an unreachable panic.
|
||||
let pointer = if inner.bigtiff {
|
||||
let mut offset = offset;
|
||||
|
||||
inner
|
||||
.reader
|
||||
.byte_order
|
||||
.convert(Type::IFD8, &mut offset[..], ByteOrder::native());
|
||||
|
||||
IfdPointer(u64::from_ne_bytes(offset))
|
||||
} else {
|
||||
let mut offset = *offset.first_chunk::<4>().unwrap();
|
||||
|
||||
inner
|
||||
.reader
|
||||
.byte_order
|
||||
.convert(Type::IFD, &mut offset[..], ByteOrder::native());
|
||||
|
||||
let offset = u32::from_ne_bytes(offset);
|
||||
IfdPointer(u64::from(offset))
|
||||
};
|
||||
|
||||
inner.reader.goto_offset(pointer.0)?;
|
||||
|
||||
Ok(EntryBytesReader {
|
||||
inner: &mut inner.reader,
|
||||
variant: Variant::InFile {
|
||||
value_bytes,
|
||||
remainder: value_bytes,
|
||||
base: pointer.0,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for EntryBytesReader<'_, R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match &mut self.variant {
|
||||
Variant::Immediate { data, .. } => data.read(buf),
|
||||
Variant::InFile { remainder, .. } => {
|
||||
let bound = usize::try_from(*remainder).unwrap_or(usize::MAX);
|
||||
let avail = buf.len().min(bound);
|
||||
let read = self.inner.inner().read(&mut buf[..avail])?;
|
||||
|
||||
// On a well-behaved `Read`: read <= avail <= bound <=(mathematical) remainder
|
||||
let consume = read as u64;
|
||||
*remainder -= consume;
|
||||
Ok(read)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
|
||||
match &mut self.variant {
|
||||
Variant::Immediate { data, .. } => data.read_exact(buf),
|
||||
Variant::InFile { remainder, .. } => {
|
||||
let bound = usize::try_from(*remainder).unwrap_or(usize::MAX);
|
||||
|
||||
// Fail early if we would be sure to over-read.
|
||||
if buf.len() > bound {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"Tiff value shorter than the requested buffer size",
|
||||
));
|
||||
}
|
||||
|
||||
// since buf.len() <= bound <=(mathematical) remainder
|
||||
let consume = buf.len() as u64;
|
||||
self.inner.inner().read_exact(buf)?;
|
||||
*remainder -= consume;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Seek for EntryBytesReader<'_, R> {
|
||||
fn seek(&mut self, offset: io::SeekFrom) -> io::Result<u64> {
|
||||
let (value_bytes, remainder) = match &self.variant {
|
||||
Variant::Immediate { data, value_bytes } => {
|
||||
let remainder = data.get_ref().len() as u64 - data.position();
|
||||
(*value_bytes as u64, remainder)
|
||||
}
|
||||
&Variant::InFile {
|
||||
value_bytes,
|
||||
remainder,
|
||||
base: _,
|
||||
} => (value_bytes, remainder),
|
||||
};
|
||||
|
||||
// Normalize to an offset from the start
|
||||
let offset_from_start = match offset {
|
||||
io::SeekFrom::Start(n) => Some(n),
|
||||
io::SeekFrom::Current(n) => {
|
||||
let front = value_bytes - remainder;
|
||||
front.checked_add_signed(n)
|
||||
}
|
||||
io::SeekFrom::End(n) => value_bytes.checked_add_signed(n),
|
||||
};
|
||||
|
||||
let Some((offset, remainder)) = offset_from_start.and_then(|n| {
|
||||
let remainder = value_bytes.checked_sub(n)?;
|
||||
Some((n, remainder))
|
||||
}) else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"seek out of bounds of entry value",
|
||||
));
|
||||
};
|
||||
|
||||
match &mut self.variant {
|
||||
Variant::Immediate { data, .. } => {
|
||||
let start = OFFSET_BUFFER_LEN as u64 - value_bytes;
|
||||
data.set_position(start + offset);
|
||||
Ok(offset)
|
||||
}
|
||||
&mut Variant::InFile { base, .. } => {
|
||||
// We checked this against overflow on construction. Ignore the offset we get.
|
||||
self.inner
|
||||
.inner()
|
||||
.seek(io::SeekFrom::Start(base + offset))?;
|
||||
|
||||
self.variant = Variant::InFile {
|
||||
value_bytes,
|
||||
remainder,
|
||||
base,
|
||||
};
|
||||
|
||||
Ok(offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stream_position(&mut self) -> io::Result<u64> {
|
||||
Ok(match &self.variant {
|
||||
Variant::Immediate { data, value_bytes } => {
|
||||
// Recall the value is at the end of the buffer.
|
||||
let from_end = OFFSET_BUFFER_LEN as u64 - data.position();
|
||||
u64::from(*value_bytes) - from_end
|
||||
}
|
||||
// Note: we might think about using the inner `stream_position` here. However that
|
||||
// exposes something interesting: If the internal stream is modified concurrently from
|
||||
// another instance (e.g. a shared `File`) then we may lose track of it anyways.
|
||||
// Especially the `Read` implementation is not bounded on `Seek` so it could not
|
||||
// acquire the position. And it should not since that is just a different form of race
|
||||
// where we risk the position being modified by a different thread just as well.
|
||||
//
|
||||
// So in the interest of least surprise we just assume that we can track the position
|
||||
// accurately here and that the inner reader uses for instance `read_at` consistently.
|
||||
// We own the value (or mutable reference) it after all.
|
||||
//
|
||||
// Also this one MUST be compatible with `seek(SeekFrom::Current(0))` as per trait
|
||||
// defintion—so at least keep those consistent if changing anything.
|
||||
&Variant::InFile {
|
||||
remainder,
|
||||
value_bytes,
|
||||
..
|
||||
} => value_bytes - remainder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{decoder::Decoder, encoder, tags};
|
||||
use std::io::{Read as _, Seek as _, SeekFrom};
|
||||
|
||||
#[test]
|
||||
fn can_seek_tag_bytes() -> Result<(), crate::error::TiffError> {
|
||||
let file = std::fs::File::open(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/images/int8_rgb.tif"
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let mut decoder = Decoder::open(file).unwrap();
|
||||
decoder.next_directory().unwrap();
|
||||
|
||||
let mut buffer = crate::tags::ValueBuffer::default();
|
||||
|
||||
decoder
|
||||
.current_ifd()
|
||||
.find_tag_buf(tags::Tag::StripByteCounts, &mut buffer)
|
||||
.expect("some strip byte count is present");
|
||||
|
||||
let bytes_of_value = buffer.as_bytes().len() as u64;
|
||||
|
||||
let mut reader = decoder
|
||||
.read_tag(tags::Tag::StripByteCounts)?
|
||||
.expect("some strip byte count is present");
|
||||
|
||||
let mut data = vec![];
|
||||
reader.read_to_end(&mut data)?;
|
||||
|
||||
// `ValueBuffer` also stores its data in the file endianess until converted.
|
||||
assert_eq!(reader.stream_position()?, bytes_of_value);
|
||||
assert_eq!(buffer.as_bytes(), data);
|
||||
|
||||
reader.seek(SeekFrom::Start(0))?;
|
||||
assert_eq!(reader.stream_position()?, 0);
|
||||
reader.read_to_end(&mut data)?;
|
||||
assert_eq!(reader.stream_position()?, bytes_of_value);
|
||||
|
||||
let (first, second) = data.split_at(bytes_of_value as usize);
|
||||
assert_eq!(first, second);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::seek_from_current)] // We want to test that functionality.
|
||||
fn seek_from_end() -> Result<(), crate::error::TiffError> {
|
||||
const ARTIST: &str = "99 little bugs in the code";
|
||||
|
||||
let data = {
|
||||
let mut data = std::io::Cursor::new(vec![]);
|
||||
let mut encoder = encoder::TiffEncoder::new(&mut data)?;
|
||||
|
||||
let mut dir = encoder.image_directory()?;
|
||||
dir.write_tag(tags::Tag::Artist, ARTIST)?;
|
||||
dir.finish()?;
|
||||
|
||||
data.into_inner()
|
||||
};
|
||||
|
||||
let file = std::io::Cursor::new(data);
|
||||
let mut decoder = Decoder::open(file).unwrap();
|
||||
decoder.next_directory().unwrap();
|
||||
|
||||
let mut reader = decoder
|
||||
.read_tag(tags::Tag::Artist)?
|
||||
.expect("the artist is present?");
|
||||
|
||||
let mut value = vec![];
|
||||
reader.read_to_end(&mut value)?;
|
||||
|
||||
let (&nul, value) = value.split_last().unwrap();
|
||||
assert_eq!(value, ARTIST.as_bytes());
|
||||
assert_eq!(nul, b'\0');
|
||||
|
||||
assert_eq!(reader.seek(SeekFrom::End(0))?, ARTIST.len() as u64 + 1);
|
||||
assert!(reader.seek(SeekFrom::End(1)).is_err());
|
||||
assert_eq!(reader.stream_position()?, ARTIST.len() as u64 + 1);
|
||||
|
||||
// Check relative seeks by comparing against the "code"
|
||||
let code_offset = reader.seek(SeekFrom::End(-5))?;
|
||||
let mut is_code = [0u8; 4];
|
||||
reader.read_exact(&mut is_code)?;
|
||||
assert_eq!(is_code, *value.last_chunk::<4>().unwrap());
|
||||
|
||||
assert_eq!(reader.seek(SeekFrom::Start(0))?, 0);
|
||||
assert_eq!(reader.stream_position()?, 0);
|
||||
assert_eq!(reader.seek(SeekFrom::Current(0))?, 0);
|
||||
assert_eq!(reader.stream_position()?, 0);
|
||||
assert!(reader.seek(SeekFrom::Current(-1)).is_err());
|
||||
assert_eq!(reader.stream_position()?, 0);
|
||||
|
||||
reader.seek(SeekFrom::Start(code_offset))?;
|
||||
reader.read_exact(&mut is_code)?;
|
||||
assert_eq!(reader.stream_position()?, ARTIST.len() as u64);
|
||||
assert_eq!(is_code, *value.last_chunk::<4>().unwrap());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check that the reader from an `offset` field also works.
|
||||
#[test]
|
||||
fn read_from_offset() -> Result<(), crate::error::TiffError> {
|
||||
let data = {
|
||||
let mut data = std::io::Cursor::new(vec![]);
|
||||
let mut encoder = encoder::TiffEncoder::new(&mut data)?;
|
||||
|
||||
let mut dir = encoder.image_directory()?;
|
||||
dir.write_tag(
|
||||
tags::Tag::PlanarConfiguration,
|
||||
tags::PlanarConfiguration::Planar,
|
||||
)?;
|
||||
dir.finish()?;
|
||||
|
||||
data.into_inner()
|
||||
};
|
||||
|
||||
let file = std::io::Cursor::new(data);
|
||||
let mut decoder = Decoder::open(file).unwrap();
|
||||
decoder.next_directory().unwrap();
|
||||
let bo = decoder.byte_order();
|
||||
|
||||
let mut reader = decoder
|
||||
.read_tag(tags::Tag::PlanarConfiguration)?
|
||||
.expect("some artist is present");
|
||||
|
||||
let mut buf = [0; 2];
|
||||
reader.read_exact(&mut buf)?;
|
||||
|
||||
bo.convert(tags::Type::SHORT, &mut buf, tags::ByteOrder::BigEndian);
|
||||
assert_eq!(buf, [0, 2]);
|
||||
|
||||
// Check relative seeks, this item is only two bytes long.
|
||||
assert!(reader.seek(SeekFrom::End(-3)).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+71
-41
@@ -363,7 +363,7 @@ impl ::std::fmt::Debug for Entry {
|
||||
fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
|
||||
fmt.write_str(&format!(
|
||||
"Entry {{ type_: {:?}, count: {:?}, offset: {:?} }}",
|
||||
self.type_, self.count, &self.offset
|
||||
self.type_, self.count, self.offset
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -393,7 +393,7 @@ impl Entry {
|
||||
self.count
|
||||
}
|
||||
|
||||
pub(crate) fn offset(&self) -> &[u8] {
|
||||
pub(crate) fn offset_raw(&self) -> &[u8; 8] {
|
||||
&self.offset
|
||||
}
|
||||
|
||||
@@ -583,81 +583,93 @@ impl Entry {
|
||||
|
||||
match self.type_ {
|
||||
Type::BYTE | Type::UNDEFINED => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, 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)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, 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)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, 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()))),
|
||||
.as_chunks::<2>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|&ch| Short(u16::from_ne_bytes(ch))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::SSHORT => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, 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()))),
|
||||
.as_chunks::<2>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|&ch| SignedShort(i16::from_ne_bytes(ch))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::LONG => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, 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()))),
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|&ch| Unsigned(u32::from_ne_bytes(ch))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::SLONG => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, 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()))),
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|&ch| Signed(i32::from_ne_bytes(ch))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::FLOAT => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, 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()))),
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|&ch| Float(f32::from_ne_bytes(ch))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::DOUBLE => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, 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()))),
|
||||
.as_chunks::<8>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|&ch| Double(f64::from_ne_bytes(ch))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::RATIONAL => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.chunks_exact(8).map(|ch| {
|
||||
v.extend(bytes.as_chunks::<8>().0.iter().map(|ch| {
|
||||
Rational(
|
||||
u32::from_ne_bytes(ch[..4].try_into().unwrap()),
|
||||
u32::from_ne_bytes(ch[4..].try_into().unwrap()),
|
||||
@@ -666,9 +678,9 @@ impl Entry {
|
||||
})
|
||||
}
|
||||
Type::SRATIONAL => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.chunks_exact(8).map(|ch| {
|
||||
v.extend(bytes.as_chunks::<8>().0.iter().map(|ch| {
|
||||
SRational(
|
||||
i32::from_ne_bytes(ch[..4].try_into().unwrap()),
|
||||
i32::from_ne_bytes(ch[4..].try_into().unwrap()),
|
||||
@@ -677,50 +689,58 @@ impl Entry {
|
||||
})
|
||||
}
|
||||
Type::LONG8 => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, 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()))),
|
||||
.as_chunks::<8>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|&ch| UnsignedBig(u64::from_ne_bytes(ch))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::SLONG8 => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, 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()))),
|
||||
.as_chunks::<8>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|&ch| SignedBig(i64::from_ne_bytes(ch))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::IFD => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, 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()))),
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|&ch| Ifd(u32::from_ne_bytes(ch))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::IFD8 => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, 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()))),
|
||||
.as_chunks::<8>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|&ch| IfdBig(u64::from_ne_bytes(ch))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::ASCII => {
|
||||
let n = usize::try_from(self.count)?;
|
||||
|
||||
if n > limits.decoding_buffer_size {
|
||||
return Err(dbg!(TiffError::LimitsExceeded));
|
||||
if n > limits.ifd_value_size {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
|
||||
let mut out = vec![0; n];
|
||||
@@ -846,10 +866,20 @@ impl Entry {
|
||||
|
||||
fn vec_with_capacity(
|
||||
value_count: u64,
|
||||
type_: Type,
|
||||
limits: &super::Limits,
|
||||
) -> Result<Vec<Value>, TiffError> {
|
||||
let value_count = usize::try_from(value_count)?;
|
||||
// Check raw file bytes against ifd_value_size.
|
||||
if !type_
|
||||
.value_bytes(value_count)
|
||||
.and_then(|b| usize::try_from(b).map_err(|_| TiffError::LimitsExceeded))
|
||||
.is_ok_and(|raw_len| raw_len <= limits.ifd_value_size)
|
||||
{
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
|
||||
// Check in-memory Value representation against decoding_buffer_size.
|
||||
let value_count = usize::try_from(value_count)?;
|
||||
if value_count > limits.decoding_buffer_size / mem::size_of::<Value>() {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
|
||||
+632
-141
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,545 @@
|
||||
use std::io::{Read, Take};
|
||||
|
||||
use crate::TiffError;
|
||||
|
||||
pub struct LogLuv24<R> {
|
||||
reader: Take<R>,
|
||||
/// The required buffer size. We only decompressed all samples at once.
|
||||
chunk_sz: usize,
|
||||
}
|
||||
|
||||
impl<R> LogLuv24<R> {
|
||||
pub fn new(reader: R, n: u64, npixels: u32) -> Result<Self, TiffError>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
let chunk_sz = usize::try_from(
|
||||
u64::from(npixels)
|
||||
.checked_mul(4 * 3)
|
||||
.ok_or(TiffError::LimitsExceeded)?,
|
||||
)?;
|
||||
|
||||
let reader = reader.take(n);
|
||||
Ok(LogLuv24 { reader, chunk_sz })
|
||||
}
|
||||
|
||||
fn expand_buf(buf: &mut [u8]) {
|
||||
let data_len = buf.len() / 4;
|
||||
|
||||
for i in (0..data_len / 3).rev() {
|
||||
let ch = buf.as_chunks_mut::<3>().0[i];
|
||||
let [a, b, c] = ch.map(u32::from);
|
||||
|
||||
let raw = a << 16 | b << 8 | c;
|
||||
let l = (raw >> 14) & 0x3ff;
|
||||
let uv = decode_quantized_uv(raw & 0x3fff);
|
||||
|
||||
let by = dequantize_24_luma(l);
|
||||
let (u, v) = dequantize_24(uv);
|
||||
|
||||
let s = 1. / (6. * u - 16. * v + 12.);
|
||||
let x = 9. * u * s;
|
||||
let y = 4. * v * s;
|
||||
|
||||
let bx = x / y * by;
|
||||
let bz = (1. - x - y) / y * by;
|
||||
|
||||
let [r, g, b] = xyz_to_rgb([bx, by, bz]);
|
||||
|
||||
let into = &mut buf.as_chunks_mut::<12>().0[i];
|
||||
into[0..4].copy_from_slice(&f32::to_ne_bytes(r));
|
||||
into[4..8].copy_from_slice(&f32::to_ne_bytes(g));
|
||||
into[8..12].copy_from_slice(&f32::to_ne_bytes(b));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for LogLuv24<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
self.read_exact(buf)?;
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
// FIXME: this is the interface we have for compressed chunks but it is not optimal for what we
|
||||
// want. Note `fill_buf` is not super helpful since it does not guarantee any size of the
|
||||
// filled buffer but we need 24-bits at a time. Two code paths is even more awkward.
|
||||
fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
|
||||
if !buf.len().is_multiple_of(self.chunk_sz) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"buffer size {0} must match the chunk size for compression method SGILog24 {1}",
|
||||
buf.len(),
|
||||
self.chunk_sz,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let data_len = buf.len() / 4;
|
||||
self.reader.read_exact(&mut buf[..data_len])?;
|
||||
Self::expand_buf(buf);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn packbits_decode<R: Read, const N: usize>(
|
||||
reader: &mut R,
|
||||
buf: &mut [[u8; N]],
|
||||
) -> std::io::Result<()> {
|
||||
let expected = buf.len();
|
||||
for offset in (0..N).rev() {
|
||||
let mut i = 0;
|
||||
let mut code = [0u8; 1];
|
||||
|
||||
while let Some(pixels) = buf.get_mut(i..) {
|
||||
if pixels.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let pixels = pixels.iter_mut();
|
||||
reader.read_exact(&mut code)?;
|
||||
let [repeat] = code;
|
||||
|
||||
if repeat >= 128 {
|
||||
reader.read_exact(&mut code)?;
|
||||
let [value] = code;
|
||||
let rc = (repeat - 128) + 2;
|
||||
|
||||
for (_, px) in (0..rc).zip(pixels) {
|
||||
px[offset] = value;
|
||||
}
|
||||
|
||||
i += usize::from(rc);
|
||||
} else {
|
||||
for (_, px) in (0..repeat).zip(pixels) {
|
||||
reader.read_exact(&mut code)?;
|
||||
let [value] = code;
|
||||
px[offset] = value;
|
||||
}
|
||||
|
||||
i += usize::from(repeat);
|
||||
}
|
||||
}
|
||||
|
||||
if i < expected {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::UnexpectedEof,
|
||||
"PackBits RLE decoded fewer pixels than expected",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct LogLuv32<R> {
|
||||
reader: Take<R>,
|
||||
/// The required buffer size. We only decompressed all samples at once.
|
||||
chunk_sz: usize,
|
||||
}
|
||||
|
||||
impl<R: Read> LogLuv32<R> {
|
||||
pub fn new(reader: R, n: u64, npixels: u32) -> Result<Self, TiffError> {
|
||||
let chunk_sz = usize::try_from(
|
||||
u64::from(npixels)
|
||||
.checked_mul(4 * 3)
|
||||
.ok_or(TiffError::LimitsExceeded)?,
|
||||
)?;
|
||||
|
||||
let reader = reader.take(n);
|
||||
Ok(LogLuv32 { reader, chunk_sz })
|
||||
}
|
||||
|
||||
fn decode_rle_row(&mut self, row: &mut [u8]) -> std::io::Result<()>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
// 32-bit per pixel format.
|
||||
let n_pixels = self.chunk_sz / 12;
|
||||
// buffer for reading (using only 32-bit per pixel)
|
||||
let raw = &mut row.as_chunks_mut::<4>().0[..n_pixels];
|
||||
packbits_decode::<_, 4>(&mut self.reader, raw)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expand_row(row: &mut [u8]) {
|
||||
let uvscale = 1.0 / 410.0;
|
||||
let n_pixels = row.len() / 12;
|
||||
|
||||
// Work backwards, the 32-bit compressed raw data is at the front of the row buffer and
|
||||
// this avoids overwriting data before we read it.
|
||||
for i in (0..n_pixels).rev() {
|
||||
let raw = row.as_chunks_mut::<4>().0[i];
|
||||
|
||||
let l = u16::from(raw[0]) << 8 | u16::from(raw[1]);
|
||||
let by = log16_to_y(l);
|
||||
|
||||
let u = uvscale * (f32::from(raw[2]) + 0.5);
|
||||
let v = uvscale * (f32::from(raw[3]) + 0.5);
|
||||
|
||||
let s = 1. / (6. * u - 16. * v + 12.);
|
||||
let x = 9. * u * s;
|
||||
let y = 4. * v * s;
|
||||
|
||||
let bx = x / y * by;
|
||||
let bz = (1. - x - y) / y * by;
|
||||
|
||||
let [r, g, b] = xyz_to_rgb([bx, by, bz]);
|
||||
let into = &mut row.as_chunks_mut::<12>().0[i];
|
||||
into[0..4].copy_from_slice(&f32::to_ne_bytes(r));
|
||||
into[4..8].copy_from_slice(&f32::to_ne_bytes(g));
|
||||
into[8..12].copy_from_slice(&f32::to_ne_bytes(b));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for LogLuv32<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
self.read_exact(buf)?;
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
// FIXME: See `LogLuv24::read_exact`
|
||||
fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
|
||||
for row in buf.chunks_exact_mut(self.chunk_sz) {
|
||||
self.decode_rle_row(row)?;
|
||||
Self::expand_row(row);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Pack-Bits like encoding for a single 16-bit log-L sample.
|
||||
pub struct LogLuv16<R> {
|
||||
reader: Take<R>,
|
||||
/// The required buffer size. We only decompressed all samples at once.
|
||||
chunk_sz: usize,
|
||||
}
|
||||
|
||||
impl<R: Read> LogLuv16<R> {
|
||||
pub fn new(reader: R, n: u64, npixels: u32) -> Result<Self, TiffError> {
|
||||
let chunk_sz = usize::try_from(
|
||||
u64::from(npixels)
|
||||
.checked_mul(4)
|
||||
.ok_or(TiffError::LimitsExceeded)?,
|
||||
)?;
|
||||
|
||||
let reader = reader.take(n);
|
||||
Ok(LogLuv16 { reader, chunk_sz })
|
||||
}
|
||||
|
||||
fn decode_rle_row(&mut self, row: &mut [u8]) -> std::io::Result<()>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
// 32-bit per pixel format.
|
||||
let n_pixels = self.chunk_sz / 4;
|
||||
// buffer for reading (16-bit per pixel)
|
||||
let raw = &mut row.as_chunks_mut::<2>().0[..n_pixels];
|
||||
packbits_decode::<_, 2>(&mut self.reader, raw)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expand_row(row: &mut [u8]) {
|
||||
let n_pixels = row.len() / 4;
|
||||
|
||||
// Work backwards, the 32-bit compressed raw data is at the front of the row buffer and
|
||||
// this avoids overwriting data before we read it.
|
||||
for i in (0..n_pixels).rev() {
|
||||
let raw = row.as_chunks_mut::<2>().0[i];
|
||||
|
||||
let l = u16::from(raw[0]) << 8 | u16::from(raw[1]);
|
||||
let by = log16_to_y(l);
|
||||
|
||||
let into = &mut row.as_chunks_mut::<4>().0[i];
|
||||
into[0..4].copy_from_slice(&f32::to_ne_bytes(by));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for LogLuv16<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
self.read_exact(buf)?;
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
// FIXME: See `LogLuv24::read_exact`
|
||||
fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
|
||||
for row in buf.chunks_exact_mut(self.chunk_sz) {
|
||||
self.decode_rle_row(row)?;
|
||||
Self::expand_row(row);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Table information from: `libtiff`, 1997 by Greg Ward Larson, SGI
|
||||
#[doc(alias = "UV_SQSIZ")]
|
||||
const UV_QUANTIZATION_WIDTH: f32 = 0.003500;
|
||||
|
||||
// The `v` value corresonding to the bottom most row (quantized v' = 0).
|
||||
const UV_VSTART: f32 = 0.016940;
|
||||
|
||||
/// Number of distinct 'u' rows, each at a different v level. Each is quantized individually with
|
||||
/// the first quantization level starting at its `ustart` and `UV_QUANTIZATION_WIDTH` wide.
|
||||
const UV_NVS: usize = 163;
|
||||
|
||||
/// Contrary to the original C code we calculate the dependent values (cumulative sum).
|
||||
struct UvRow {
|
||||
ustart: f32,
|
||||
num_us: u16,
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
const UV_ROWS: [UvRow; UV_NVS] = [
|
||||
UvRow { ustart: 0.247663, num_us: 4, },
|
||||
UvRow { ustart: 0.243779, num_us: 6, },
|
||||
UvRow { ustart: 0.241684, num_us: 7, },
|
||||
UvRow { ustart: 0.237874, num_us: 9, },
|
||||
UvRow { ustart: 0.235906, num_us: 10, },
|
||||
UvRow { ustart: 0.232153, num_us: 12, },
|
||||
UvRow { ustart: 0.228352, num_us: 14, },
|
||||
UvRow { ustart: 0.226259, num_us: 15, },
|
||||
UvRow { ustart: 0.222371, num_us: 17, },
|
||||
UvRow { ustart: 0.220410, num_us: 18, },
|
||||
UvRow { ustart: 0.214710, num_us: 21, },
|
||||
UvRow { ustart: 0.212714, num_us: 22, },
|
||||
UvRow { ustart: 0.210721, num_us: 23, },
|
||||
UvRow { ustart: 0.204976, num_us: 26, },
|
||||
UvRow { ustart: 0.202986, num_us: 27, },
|
||||
UvRow { ustart: 0.199245, num_us: 29, },
|
||||
UvRow { ustart: 0.195525, num_us: 31, },
|
||||
UvRow { ustart: 0.193560, num_us: 32, },
|
||||
UvRow { ustart: 0.189878, num_us: 34, },
|
||||
UvRow { ustart: 0.186216, num_us: 36, },
|
||||
UvRow { ustart: 0.186216, num_us: 36, },
|
||||
UvRow { ustart: 0.182592, num_us: 38, },
|
||||
UvRow { ustart: 0.179003, num_us: 40, },
|
||||
UvRow { ustart: 0.175466, num_us: 42, },
|
||||
UvRow { ustart: 0.172001, num_us: 44, },
|
||||
UvRow { ustart: 0.172001, num_us: 44, },
|
||||
UvRow { ustart: 0.168612, num_us: 46, },
|
||||
UvRow { ustart: 0.168612, num_us: 46, },
|
||||
UvRow { ustart: 0.163575, num_us: 49, },
|
||||
UvRow { ustart: 0.158642, num_us: 52, },
|
||||
UvRow { ustart: 0.158642, num_us: 52, },
|
||||
UvRow { ustart: 0.158642, num_us: 52, },
|
||||
UvRow { ustart: 0.153815, num_us: 55, },
|
||||
UvRow { ustart: 0.153815, num_us: 55, },
|
||||
UvRow { ustart: 0.149097, num_us: 58, },
|
||||
UvRow { ustart: 0.149097, num_us: 58, },
|
||||
UvRow { ustart: 0.142746, num_us: 62, },
|
||||
UvRow { ustart: 0.142746, num_us: 62, },
|
||||
UvRow { ustart: 0.142746, num_us: 62, },
|
||||
UvRow { ustart: 0.138270, num_us: 65, },
|
||||
UvRow { ustart: 0.138270, num_us: 65, },
|
||||
UvRow { ustart: 0.138270, num_us: 65, },
|
||||
UvRow { ustart: 0.132166, num_us: 69, },
|
||||
UvRow { ustart: 0.132166, num_us: 69, },
|
||||
UvRow { ustart: 0.126204, num_us: 73, },
|
||||
UvRow { ustart: 0.126204, num_us: 73, },
|
||||
UvRow { ustart: 0.126204, num_us: 73, },
|
||||
UvRow { ustart: 0.120381, num_us: 77, },
|
||||
UvRow { ustart: 0.120381, num_us: 77, },
|
||||
UvRow { ustart: 0.120381, num_us: 77, },
|
||||
UvRow { ustart: 0.120381, num_us: 77, },
|
||||
UvRow { ustart: 0.112962, num_us: 82, },
|
||||
UvRow { ustart: 0.112962, num_us: 82, },
|
||||
UvRow { ustart: 0.112962, num_us: 82, },
|
||||
UvRow { ustart: 0.107450, num_us: 86, },
|
||||
UvRow { ustart: 0.107450, num_us: 86, },
|
||||
UvRow { ustart: 0.107450, num_us: 86, },
|
||||
UvRow { ustart: 0.107450, num_us: 86, },
|
||||
UvRow { ustart: 0.100343, num_us: 91, },
|
||||
UvRow { ustart: 0.100343, num_us: 91, },
|
||||
UvRow { ustart: 0.100343, num_us: 91, },
|
||||
UvRow { ustart: 0.095126, num_us: 95, },
|
||||
UvRow { ustart: 0.095126, num_us: 95, },
|
||||
UvRow { ustart: 0.095126, num_us: 95, },
|
||||
UvRow { ustart: 0.095126, num_us: 95, },
|
||||
UvRow { ustart: 0.088276, num_us: 100, },
|
||||
UvRow { ustart: 0.088276, num_us: 100, },
|
||||
UvRow { ustart: 0.088276, num_us: 100, },
|
||||
UvRow { ustart: 0.088276, num_us: 100, },
|
||||
UvRow { ustart: 0.081523, num_us: 105, },
|
||||
UvRow { ustart: 0.081523, num_us: 105, },
|
||||
UvRow { ustart: 0.081523, num_us: 105, },
|
||||
UvRow { ustart: 0.081523, num_us: 105, },
|
||||
UvRow { ustart: 0.074861, num_us: 110, },
|
||||
UvRow { ustart: 0.074861, num_us: 110, },
|
||||
UvRow { ustart: 0.074861, num_us: 110, },
|
||||
UvRow { ustart: 0.074861, num_us: 110, },
|
||||
UvRow { ustart: 0.068290, num_us: 115, },
|
||||
UvRow { ustart: 0.068290, num_us: 115, },
|
||||
UvRow { ustart: 0.068290, num_us: 115, },
|
||||
UvRow { ustart: 0.068290, num_us: 115, },
|
||||
UvRow { ustart: 0.063573, num_us: 119, },
|
||||
UvRow { ustart: 0.063573, num_us: 119, },
|
||||
UvRow { ustart: 0.063573, num_us: 119, },
|
||||
UvRow { ustart: 0.063573, num_us: 119, },
|
||||
UvRow { ustart: 0.057219, num_us: 124, },
|
||||
UvRow { ustart: 0.057219, num_us: 124, },
|
||||
UvRow { ustart: 0.057219, num_us: 124, },
|
||||
UvRow { ustart: 0.057219, num_us: 124, },
|
||||
UvRow { ustart: 0.050985, num_us: 129, },
|
||||
UvRow { ustart: 0.050985, num_us: 129, },
|
||||
UvRow { ustart: 0.050985, num_us: 129, },
|
||||
UvRow { ustart: 0.050985, num_us: 129, },
|
||||
UvRow { ustart: 0.050985, num_us: 129, },
|
||||
UvRow { ustart: 0.044859, num_us: 134, },
|
||||
UvRow { ustart: 0.044859, num_us: 134, },
|
||||
UvRow { ustart: 0.044859, num_us: 134, },
|
||||
UvRow { ustart: 0.044859, num_us: 134, },
|
||||
UvRow { ustart: 0.040571, num_us: 138, },
|
||||
UvRow { ustart: 0.040571, num_us: 138, },
|
||||
UvRow { ustart: 0.040571, num_us: 138, },
|
||||
UvRow { ustart: 0.040571, num_us: 138, },
|
||||
UvRow { ustart: 0.036339, num_us: 142, },
|
||||
UvRow { ustart: 0.036339, num_us: 142, },
|
||||
UvRow { ustart: 0.036339, num_us: 142, },
|
||||
UvRow { ustart: 0.036339, num_us: 142, },
|
||||
UvRow { ustart: 0.032139, num_us: 146, },
|
||||
UvRow { ustart: 0.032139, num_us: 146, },
|
||||
UvRow { ustart: 0.032139, num_us: 146, },
|
||||
UvRow { ustart: 0.032139, num_us: 146, },
|
||||
UvRow { ustart: 0.027947, num_us: 150, },
|
||||
UvRow { ustart: 0.027947, num_us: 150, },
|
||||
UvRow { ustart: 0.027947, num_us: 150, },
|
||||
UvRow { ustart: 0.023739, num_us: 154, },
|
||||
UvRow { ustart: 0.023739, num_us: 154, },
|
||||
UvRow { ustart: 0.023739, num_us: 154, },
|
||||
UvRow { ustart: 0.023739, num_us: 154, },
|
||||
UvRow { ustart: 0.019504, num_us: 158, },
|
||||
UvRow { ustart: 0.019504, num_us: 158, },
|
||||
UvRow { ustart: 0.019504, num_us: 158, },
|
||||
UvRow { ustart: 0.016976, num_us: 161, },
|
||||
UvRow { ustart: 0.016976, num_us: 161, },
|
||||
UvRow { ustart: 0.016976, num_us: 161, },
|
||||
UvRow { ustart: 0.016976, num_us: 161, },
|
||||
UvRow { ustart: 0.012639, num_us: 165, },
|
||||
UvRow { ustart: 0.012639, num_us: 165, },
|
||||
UvRow { ustart: 0.012639, num_us: 165, },
|
||||
UvRow { ustart: 0.009991, num_us: 168, },
|
||||
UvRow { ustart: 0.009991, num_us: 168, },
|
||||
UvRow { ustart: 0.009991, num_us: 168, },
|
||||
UvRow { ustart: 0.009016, num_us: 170, },
|
||||
UvRow { ustart: 0.009016, num_us: 170, },
|
||||
UvRow { ustart: 0.009016, num_us: 170, },
|
||||
UvRow { ustart: 0.006217, num_us: 173, },
|
||||
UvRow { ustart: 0.006217, num_us: 173, },
|
||||
UvRow { ustart: 0.005097, num_us: 175, },
|
||||
UvRow { ustart: 0.005097, num_us: 175, },
|
||||
UvRow { ustart: 0.005097, num_us: 175, },
|
||||
UvRow { ustart: 0.003909, num_us: 177, },
|
||||
UvRow { ustart: 0.003909, num_us: 177, },
|
||||
UvRow { ustart: 0.002340, num_us: 177, },
|
||||
UvRow { ustart: 0.002389, num_us: 170, },
|
||||
UvRow { ustart: 0.001068, num_us: 164, },
|
||||
UvRow { ustart: 0.001653, num_us: 157, },
|
||||
UvRow { ustart: 0.000717, num_us: 150, },
|
||||
UvRow { ustart: 0.001614, num_us: 143, },
|
||||
UvRow { ustart: 0.000270, num_us: 136, },
|
||||
UvRow { ustart: 0.000484, num_us: 129, },
|
||||
UvRow { ustart: 0.001103, num_us: 123, },
|
||||
UvRow { ustart: 0.001242, num_us: 115, },
|
||||
UvRow { ustart: 0.001188, num_us: 109, },
|
||||
UvRow { ustart: 0.001011, num_us: 103, },
|
||||
UvRow { ustart: 0.000709, num_us: 97, },
|
||||
UvRow { ustart: 0.000301, num_us: 89, },
|
||||
UvRow { ustart: 0.002416, num_us: 82, },
|
||||
UvRow { ustart: 0.003251, num_us: 76, },
|
||||
UvRow { ustart: 0.003246, num_us: 69, },
|
||||
UvRow { ustart: 0.004141, num_us: 62, },
|
||||
UvRow { ustart: 0.005963, num_us: 55, },
|
||||
UvRow { ustart: 0.008839, num_us: 47, },
|
||||
UvRow { ustart: 0.010490, num_us: 40, },
|
||||
UvRow { ustart: 0.016994, num_us: 31, },
|
||||
UvRow { ustart: 0.023659, num_us: 21, },
|
||||
];
|
||||
|
||||
const UV_CUMULATIVE: [u16; UV_NVS] = {
|
||||
let mut arr = [0u16; UV_NVS];
|
||||
let mut sum = 0;
|
||||
let mut i = 0;
|
||||
while i < UV_NVS {
|
||||
arr[i] = sum;
|
||||
sum += UV_ROWS[i].num_us;
|
||||
i += 1;
|
||||
}
|
||||
arr
|
||||
};
|
||||
|
||||
const UV_NDIVS: u16 = UV_CUMULATIVE[UV_NVS - 1] + UV_ROWS[UV_NVS - 1].num_us;
|
||||
|
||||
fn decode_quantized_uv(uv: u32) -> (u8, u8) {
|
||||
if uv >= UV_NDIVS as u32 {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// Find the start of the row, or the index after that.
|
||||
let v_index = match UV_CUMULATIVE.binary_search(&(uv as u16)) {
|
||||
Ok(n) => n,
|
||||
Err(n) => n - 1,
|
||||
};
|
||||
|
||||
let u_index = uv as u16 - UV_CUMULATIVE[v_index];
|
||||
(u_index as u8, v_index as u8)
|
||||
}
|
||||
|
||||
fn dequantize_24_luma(l: u32) -> f32 {
|
||||
if l == 0 {
|
||||
0.0
|
||||
} else {
|
||||
let le = (l & 0x3ff) as f32;
|
||||
let exponent = ((le / 64.0).floor() as i32) - 24;
|
||||
let mantissa = le - (exponent as f32 + 24.0) * 64.0;
|
||||
(mantissa + 64.0) / 64.0 * 2f32.powi(exponent)
|
||||
}
|
||||
}
|
||||
|
||||
fn dequantize_24((u, v): (u8, u8)) -> (f32, f32) {
|
||||
let v_f = UV_VSTART + (v as f32) * UV_QUANTIZATION_WIDTH;
|
||||
let u_row = &UV_ROWS[v as usize];
|
||||
let u_f = u_row.ustart + (u as f32) * UV_QUANTIZATION_WIDTH;
|
||||
(u_f, v_f)
|
||||
}
|
||||
|
||||
fn log16_to_y(l: u16) -> f32 {
|
||||
if l == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let le = f32::from(l & 0x7fff);
|
||||
// Y = exp(M_LN2 / 256. * (Le + .5) - M_LN2 * 64.);
|
||||
let y = (std::f32::consts::LN_2 / 256.0 * (le + 0.5) - std::f32::consts::LN_2 * 64.0).exp();
|
||||
|
||||
if l & 0x8000 != 0 {
|
||||
-y
|
||||
} else {
|
||||
y
|
||||
}
|
||||
}
|
||||
|
||||
fn xyz_to_rgb([l, u, v]: [f32; 3]) -> [f32; 3] {
|
||||
// XYZ to sRGB primaries at D65
|
||||
let r = 3.2404542 * l + -1.5371385 * u + -0.4985314 * v;
|
||||
let g = -0.969266 * l + 1.8760108 * u + 0.0415560 * v;
|
||||
let b = 0.0556434 * l + -0.2040259 * u + 1.0572252 * v;
|
||||
[r, g, b]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_quantized_uv() {
|
||||
// Test some known values from the original C code.
|
||||
assert_eq!(decode_quantized_uv(0), (0, 0));
|
||||
assert_eq!(decode_quantized_uv(3), (3, 0));
|
||||
assert_eq!(decode_quantized_uv(4), (0, 1));
|
||||
assert_eq!(decode_quantized_uv(9), (5, 1));
|
||||
assert_eq!(decode_quantized_uv(16288), (20, 162));
|
||||
assert_eq!(decode_quantized_uv(16289), (0, 0)); // Out of range
|
||||
}
|
||||
+1164
-592
File diff suppressed because it is too large.
Load diff
+240
-11
@@ -1,4 +1,6 @@
|
||||
//! All IO functionality needed for TIFF decoding
|
||||
#[cfg(feature = "webp")]
|
||||
use std::io::Cursor;
|
||||
use std::io::{self, BufRead, BufReader, Read, Seek, Take};
|
||||
|
||||
pub use crate::tags::ByteOrder;
|
||||
@@ -139,14 +141,32 @@ 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 =
|
||||
let mut buffered = BufReader::with_capacity(
|
||||
(32 * 1024).min(compressed_length),
|
||||
reader.take(u64::try_from(compressed_length).unwrap()),
|
||||
);
|
||||
|
||||
// `Compression = 5` covers both the current LZW variant and the pre-TIFF-6.0
|
||||
// "old-style" one, with no tag to distinguish them. New-style is MSB-first with
|
||||
// the "early change" code-size switch, and its stream begins with the Clear code
|
||||
// (256) -> first byte 0x80. Old-style is LSB-first *without* the early change
|
||||
// (GIF-style increment timing), and begins with 0x00 followed by a byte with bit
|
||||
// 0 set. This is the heuristic libtiff uses to select its compatibility decoder;
|
||||
// on a short/failed peek we default to the modern variant.
|
||||
let old_style = matches!(
|
||||
buffered.fill_buf(),
|
||||
Ok(head) if head.len() >= 2 && head[0] == 0x00 && head[1] & 0x01 != 0
|
||||
);
|
||||
|
||||
let configuration = if old_style {
|
||||
weezl::decode::Configuration::new(weezl::BitOrder::Lsb, 8)
|
||||
} else {
|
||||
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
|
||||
.with_yield_on_full_buffer(true);
|
||||
}
|
||||
.with_yield_on_full_buffer(true);
|
||||
|
||||
Self {
|
||||
reader: BufReader::with_capacity(
|
||||
(32 * 1024).min(compressed_length),
|
||||
reader.take(u64::try_from(compressed_length).unwrap()),
|
||||
),
|
||||
reader: buffered,
|
||||
decoder: configuration.build(),
|
||||
}
|
||||
}
|
||||
@@ -254,13 +274,28 @@ impl<R: Read> Read for PackBitsReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator adapter that reverses bits in each byte for FillOrder=2 (LSB-to-MSB).
|
||||
#[cfg(feature = "fax")]
|
||||
type FaxBytes<R> =
|
||||
core::iter::Map<io::Bytes<io::BufReader<io::Take<R>>>, fn(io::Result<u8>) -> io::Result<u8>>;
|
||||
|
||||
#[cfg(feature = "fax")]
|
||||
fn fax_byte_iter<R: Read>(reader: R, len: u64, fill_order: u16) -> FaxBytes<R> {
|
||||
let map_fn: fn(io::Result<u8>) -> io::Result<u8> = if fill_order == 2 {
|
||||
|r| r.map(u8::reverse_bits)
|
||||
} else {
|
||||
|r| r
|
||||
};
|
||||
io::BufReader::new(reader.take(len)).bytes().map(map_fn)
|
||||
}
|
||||
|
||||
#[cfg(feature = "fax")]
|
||||
pub struct Group4Reader<R: Read> {
|
||||
decoder: fax34::decoder::Group4Decoder<io::Bytes<io::BufReader<io::Take<R>>>>,
|
||||
decoder: fax34::decoder::Group4Decoder<FaxBytes<R>>,
|
||||
line_buf: io::Cursor<Vec<u8>>,
|
||||
height: u16,
|
||||
height: u32,
|
||||
width: u16,
|
||||
y: u16,
|
||||
y: u32,
|
||||
}
|
||||
|
||||
#[cfg(feature = "fax")]
|
||||
@@ -269,13 +304,14 @@ impl<R: Read> Group4Reader<R> {
|
||||
dimensions: (u32, u32),
|
||||
reader: R,
|
||||
compressed_length: u64,
|
||||
fill_order: u16,
|
||||
) -> crate::TiffResult<Self> {
|
||||
let width = u16::try_from(dimensions.0)?;
|
||||
let height = u16::try_from(dimensions.1)?;
|
||||
let height = dimensions.1;
|
||||
|
||||
Ok(Self {
|
||||
decoder: fax34::decoder::Group4Decoder::new(
|
||||
io::BufReader::new(reader.take(compressed_length)).bytes(),
|
||||
fax_byte_iter(reader, compressed_length, fill_order),
|
||||
width,
|
||||
)?,
|
||||
line_buf: io::Cursor::new(Vec::with_capacity(width.into())),
|
||||
@@ -354,6 +390,199 @@ impl<R: Read> Read for Group4Reader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Owning byte reader for Group 3 decoding: the compressed data is consumed
|
||||
/// into an iterator so the reader doesn't borrow the buffer.
|
||||
#[cfg(feature = "fax")]
|
||||
type OwnedBitReader = fax34::ByteReader<
|
||||
core::iter::Map<std::vec::IntoIter<u8>, fn(u8) -> Result<u8, std::convert::Infallible>>,
|
||||
>;
|
||||
|
||||
/// Decodes CCITT Group 3 (T.4) 1D compressed data line by line.
|
||||
///
|
||||
/// The fax crate's `Group3Decoder` has a bug where `new()` doesn't consume the
|
||||
/// initial EOL marker, causing its lookup tables to destructively consume bits
|
||||
/// from the EOL when trying to decode run-length codes. We work around this by
|
||||
/// using the crate's lower-level `white::decode` / `black::decode` functions
|
||||
/// and handling EOL markers ourselves.
|
||||
#[cfg(feature = "fax")]
|
||||
pub struct Group3Reader {
|
||||
reader: OwnedBitReader,
|
||||
line_buf: io::Cursor<Vec<u8>>,
|
||||
height: u32,
|
||||
width: u16,
|
||||
y: u32,
|
||||
}
|
||||
|
||||
#[cfg(feature = "fax")]
|
||||
impl Group3Reader {
|
||||
pub fn new<R: Read>(
|
||||
dimensions: (u32, u32),
|
||||
reader: R,
|
||||
compressed_length: u64,
|
||||
fill_order: u16,
|
||||
) -> crate::TiffResult<Self> {
|
||||
let width = u16::try_from(dimensions.0)?;
|
||||
let height = dimensions.1;
|
||||
|
||||
// Buffer all compressed data and apply FillOrder bit reversal
|
||||
let mut compressed = vec![0u8; compressed_length as usize];
|
||||
reader.take(compressed_length).read_exact(&mut compressed)?;
|
||||
if fill_order == 2 {
|
||||
for b in &mut compressed {
|
||||
*b = b.reverse_bits();
|
||||
}
|
||||
}
|
||||
|
||||
// Create an owning bit reader (Vec consumed by into_iter, no borrow issues)
|
||||
let map_fn: fn(u8) -> Result<u8, std::convert::Infallible> = Ok;
|
||||
let mut bit_reader = fax34::ByteReader::new(compressed.into_iter().map(map_fn)).unwrap();
|
||||
|
||||
// Skip fill bits + initial EOL marker
|
||||
Self::skip_eol(&mut bit_reader);
|
||||
|
||||
Ok(Self {
|
||||
reader: bit_reader,
|
||||
line_buf: io::Cursor::new(Vec::with_capacity(width.into())),
|
||||
width,
|
||||
height,
|
||||
y: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Skip zero fill bits followed by the terminating '1' bit of an EOL code.
|
||||
fn skip_eol(reader: &mut impl fax34::BitReader) {
|
||||
while reader.peek(1) == Some(0) {
|
||||
let _ = reader.consume(1);
|
||||
}
|
||||
if reader.peek(1) == Some(1) {
|
||||
let _ = reader.consume(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode a full run-length value (terminal + optional makeup codes).
|
||||
fn with_markup<R: fax34::BitReader>(
|
||||
decoder: fn(&mut R) -> Option<u16>,
|
||||
reader: &mut R,
|
||||
) -> Option<u16> {
|
||||
let mut sum = 0u16;
|
||||
while let Some(n) = decoder(reader) {
|
||||
sum = sum.checked_add(n)?;
|
||||
if n < 64 {
|
||||
return Some(sum);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "fax")]
|
||||
impl Read for Group3Reader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if self.line_buf.position() as usize == self.line_buf.get_ref().len()
|
||||
&& self.y < self.height
|
||||
{
|
||||
// Decode one line of alternating white/black run-lengths
|
||||
let mut transitions = Vec::new();
|
||||
let mut a0 = 0u16;
|
||||
let mut is_white = true;
|
||||
loop {
|
||||
let run = if is_white {
|
||||
Self::with_markup(fax34::maps::white::decode, &mut self.reader)
|
||||
} else {
|
||||
Self::with_markup(fax34::maps::black::decode, &mut self.reader)
|
||||
};
|
||||
match run {
|
||||
Some(len) => {
|
||||
a0 = match a0.checked_add(len) {
|
||||
Some(v) => v,
|
||||
None => break, // overflow = corrupt data
|
||||
};
|
||||
transitions.push(a0);
|
||||
is_white = !is_white;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
self.y += 1;
|
||||
|
||||
// Convert transitions to packed bilevel output
|
||||
let bytes_per_line = usize::from(self.width).div_ceil(8);
|
||||
let buffer = self.line_buf.get_mut();
|
||||
buffer.clear();
|
||||
buffer.resize(bytes_per_line, 0u8);
|
||||
|
||||
let pels = fax34::decoder::pels(&transitions, self.width);
|
||||
for (i, c) in pels.enumerate() {
|
||||
if c == fax34::Color::Black {
|
||||
buffer[i / 8] |= 1 << (7 - (i % 8));
|
||||
}
|
||||
}
|
||||
self.line_buf.set_position(0);
|
||||
|
||||
// Skip fill bits + line-ending EOL
|
||||
Self::skip_eol(&mut self.reader);
|
||||
}
|
||||
|
||||
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::*;
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ use crate::{
|
||||
/// [`Decoder::read_directory_tags`](crate::decoder::Decoder::read_directory_tags) to read the
|
||||
/// values associated with tags from an underlying file.
|
||||
#[doc(alias = "IFD")]
|
||||
#[derive(Clone)]
|
||||
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
|
||||
@@ -170,7 +171,7 @@ mod tests {
|
||||
dir.get(Tag::Unknown(1))
|
||||
.expect("tag 1 should be present after this chain")
|
||||
.count(),
|
||||
u16::MAX.into()
|
||||
u64::from(u16::MAX)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+144
-8
@@ -1,5 +1,97 @@
|
||||
use crate::tags::{PhotometricInterpretation, SampleFormat};
|
||||
|
||||
/// Apply floating-point predictor encoding to a row of floating-point samples.
|
||||
///
|
||||
/// The floating-point predictor works by:
|
||||
/// 1. Converting each value to big-endian bytes
|
||||
/// 2. Shuffling bytes so all first bytes are together, all second bytes, etc.
|
||||
/// 3. Applying byte-level horizontal difference (wrapping_sub) across the entire buffer
|
||||
///
|
||||
/// The decoder reverses this by:
|
||||
/// 1. Applying cumulative sum (wrapping_add) on the entire byte stream
|
||||
/// 2. De-shuffling bytes back into float values
|
||||
macro_rules! fp_predict {
|
||||
($name:ident, $float_type:ty) => {
|
||||
fn $name(row: &[$float_type], samples: usize, result: &mut Vec<u8>) {
|
||||
const BYTE_SIZE: usize = core::mem::size_of::<$float_type>();
|
||||
let num_values = row.len();
|
||||
let byte_count = num_values * BYTE_SIZE;
|
||||
let start_len = result.len();
|
||||
|
||||
// Allocate zeroed space so we can write to arbitrary positions
|
||||
result.resize(start_len + byte_count, 0);
|
||||
let output = &mut result[start_len..];
|
||||
|
||||
// Step 1 & 2: Convert to big-endian and scatter bytes
|
||||
for (i, &value) in row.iter().enumerate() {
|
||||
let bytes = value.to_be_bytes();
|
||||
for b in 0..BYTE_SIZE {
|
||||
output[b * num_values + i] = bytes[b];
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Apply byte-level horizontal difference across the entire buffer.
|
||||
// The prediction spans across quarter boundaries, just like the decoder's cumulative sum.
|
||||
//
|
||||
// We process elements in reverse in batches of 16 to enable autovectorization.
|
||||
// Reverse order ensures that when we compute output[i] - output[i-samples],
|
||||
// the value at output[i-samples] hasn't been overwritten yet (critical when
|
||||
// samples < 16, which is the common case: 1, 3, 4, 5).
|
||||
//
|
||||
// 8 bits * 16 = 128 bits, the width of a typical SIMD register.
|
||||
// Even if the target has no vector instructions, this still benefits from
|
||||
// instruction-level parallelism, since unlike in decoding, all subtractions
|
||||
// within a chunk are truly independent of each other.
|
||||
let output = &mut result[start_len..byte_count + start_len];
|
||||
let diff_len = byte_count - samples;
|
||||
let remainder_len = diff_len % 16;
|
||||
// Process full 16-byte chunks in reverse.
|
||||
// We iterate from the end so that predecessor values (at lower indices)
|
||||
// are still in their original state when we read them.
|
||||
let mut chunk_end = byte_count;
|
||||
while chunk_end >= samples + remainder_len + 16 {
|
||||
let chunk_start = chunk_end - 16;
|
||||
let prev_start = chunk_start - samples;
|
||||
|
||||
// Read all values in the chunk and their predecessors first.
|
||||
// This makes all subtractions below independent, enabling vectorization.
|
||||
let curr: [u8; 16] = output[chunk_start..chunk_end].try_into().unwrap();
|
||||
let prev: [u8; 16] = output[prev_start..prev_start + 16].try_into().unwrap();
|
||||
|
||||
let chunk = &mut output[chunk_start..chunk_end];
|
||||
chunk[0] = curr[0].wrapping_sub(prev[0]);
|
||||
chunk[1] = curr[1].wrapping_sub(prev[1]);
|
||||
chunk[2] = curr[2].wrapping_sub(prev[2]);
|
||||
chunk[3] = curr[3].wrapping_sub(prev[3]);
|
||||
chunk[4] = curr[4].wrapping_sub(prev[4]);
|
||||
chunk[5] = curr[5].wrapping_sub(prev[5]);
|
||||
chunk[6] = curr[6].wrapping_sub(prev[6]);
|
||||
chunk[7] = curr[7].wrapping_sub(prev[7]);
|
||||
chunk[8] = curr[8].wrapping_sub(prev[8]);
|
||||
chunk[9] = curr[9].wrapping_sub(prev[9]);
|
||||
chunk[10] = curr[10].wrapping_sub(prev[10]);
|
||||
chunk[11] = curr[11].wrapping_sub(prev[11]);
|
||||
chunk[12] = curr[12].wrapping_sub(prev[12]);
|
||||
chunk[13] = curr[13].wrapping_sub(prev[13]);
|
||||
chunk[14] = curr[14].wrapping_sub(prev[14]);
|
||||
chunk[15] = curr[15].wrapping_sub(prev[15]);
|
||||
|
||||
chunk_end = chunk_start;
|
||||
}
|
||||
// Handle the leading remainder that doesn't fill a complete chunk of 16.
|
||||
// This must be done last (after the reverse chunk loop) because these are
|
||||
// the lowest indices, and the chunks above may read predecessor values
|
||||
// from this range.
|
||||
for i in (samples..samples + remainder_len).rev() {
|
||||
output[i] = output[i].wrapping_sub(output[i - samples]);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fp_predict!(fp_predict_f32, f32);
|
||||
fp_predict!(fp_predict_f64, f64);
|
||||
|
||||
macro_rules! integer_horizontal_predict {
|
||||
() => {
|
||||
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>) {
|
||||
@@ -23,6 +115,10 @@ macro_rules! integer_horizontal_predict {
|
||||
.map(|(prev, current)| current.wrapping_sub(*prev)),
|
||||
);
|
||||
}
|
||||
|
||||
fn floating_point_predict(_: &[Self::Inner], _: &mut Vec<u8>) {
|
||||
unreachable!("floating-point predictor is only valid for floating-point sample types")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,6 +134,10 @@ pub trait ColorType {
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat];
|
||||
|
||||
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>);
|
||||
|
||||
/// Apply floating-point predictor encoding to a row of samples.
|
||||
/// This is only implemented for floating-point types; integer types will panic.
|
||||
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>);
|
||||
}
|
||||
|
||||
pub struct Gray8;
|
||||
@@ -108,7 +208,11 @@ impl ColorType for Gray32Float {
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!()
|
||||
unreachable!("horizontal predictor is not valid for floating-point sample types")
|
||||
}
|
||||
|
||||
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
|
||||
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +244,11 @@ impl ColorType for Gray64Float {
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!()
|
||||
unreachable!("horizontal predictor is not valid for floating-point sample types")
|
||||
}
|
||||
|
||||
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
|
||||
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,8 +288,13 @@ impl ColorType for RGB32Float {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!()
|
||||
unreachable!("horizontal predictor is not valid for floating-point sample types")
|
||||
}
|
||||
|
||||
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
|
||||
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,8 +314,13 @@ impl ColorType for RGB64Float {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!()
|
||||
unreachable!("horizontal predictor is not valid for floating-point sample types")
|
||||
}
|
||||
|
||||
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
|
||||
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,8 +360,13 @@ impl ColorType for RGBA32Float {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!()
|
||||
unreachable!("horizontal predictor is not valid for floating-point sample types")
|
||||
}
|
||||
|
||||
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
|
||||
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,8 +386,13 @@ impl ColorType for RGBA64Float {
|
||||
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
|
||||
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!()
|
||||
unreachable!("horizontal predictor is not valid for floating-point sample types")
|
||||
}
|
||||
|
||||
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
|
||||
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +434,11 @@ impl ColorType for CMYK32Float {
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!()
|
||||
unreachable!("horizontal predictor is not valid for floating-point sample types")
|
||||
}
|
||||
|
||||
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
|
||||
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +460,11 @@ impl ColorType for CMYK64Float {
|
||||
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
|
||||
|
||||
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
|
||||
unreachable!()
|
||||
unreachable!("horizontal predictor is not valid for floating-point sample types")
|
||||
}
|
||||
|
||||
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
|
||||
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,24 +7,15 @@ pub struct Deflate {
|
||||
level: FlateCompression,
|
||||
}
|
||||
|
||||
/// The level of compression used by the Deflate algorithm.
|
||||
/// It allows trading compression ratio for compression speed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Default)]
|
||||
pub enum DeflateLevel {
|
||||
/// The fastest possible compression mode.
|
||||
Fast = 1,
|
||||
/// The conserative choice between speed and ratio.
|
||||
#[default]
|
||||
Balanced = 6,
|
||||
/// The best compression available with Deflate.
|
||||
Best = 9,
|
||||
}
|
||||
|
||||
impl Deflate {
|
||||
/// Create a new deflate compressor with a specific level of compression.
|
||||
pub fn with_level(level: DeflateLevel) -> Self {
|
||||
///
|
||||
/// The valid levels are 1 through 9 inclusive. The default level is 6.
|
||||
///
|
||||
/// - `1` stands for light but fast compression
|
||||
/// - `6` is the recommended balanced default
|
||||
/// - `9` is maximum compression ratio at the cost of slow encoding
|
||||
pub fn with_level(level: u8) -> Self {
|
||||
Self {
|
||||
level: FlateCompression::new(level as u32),
|
||||
}
|
||||
@@ -33,7 +24,7 @@ impl Deflate {
|
||||
|
||||
impl Default for Deflate {
|
||||
fn default() -> Self {
|
||||
Self::with_level(DeflateLevel::default())
|
||||
Self::with_level(6)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ mod packbits;
|
||||
mod uncompressed;
|
||||
|
||||
#[cfg(feature = "deflate")]
|
||||
pub use self::deflate::{Deflate, DeflateLevel};
|
||||
pub use self::deflate::Deflate;
|
||||
|
||||
#[cfg(feature = "lzw")]
|
||||
pub use self::lzw::Lzw;
|
||||
|
||||
+49
-254
@@ -2,25 +2,24 @@ pub use tiff_value::*;
|
||||
|
||||
use std::{
|
||||
cmp,
|
||||
io::{self, Read, Seek, Write},
|
||||
io::{self, Seek, Write},
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
num::{NonZeroU64, TryFromIntError},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
decoder::{ifd::Entry, Decoder},
|
||||
decoder::ifd::Entry,
|
||||
error::{TiffResult, UsageError},
|
||||
tags::{
|
||||
ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation,
|
||||
PlanarConfiguration, ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
|
||||
ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
|
||||
},
|
||||
Directory, TiffError, TiffFormatError,
|
||||
};
|
||||
|
||||
pub mod colortype;
|
||||
pub mod compression;
|
||||
mod tag_filter;
|
||||
mod tiff_value;
|
||||
mod writer;
|
||||
|
||||
@@ -34,23 +33,31 @@ use self::writer::*;
|
||||
/// Image data can be very unpredictable, and thus very hard to compress. Predictors are simple
|
||||
/// passes ran over the image data to prepare it for compression. This is mostly used for LZW
|
||||
/// compression, where using [Predictor::Horizontal] we see a 35% improvement in compression
|
||||
/// ratio over the unpredicted compression !
|
||||
/// ratio over the unpredicted compression!
|
||||
///
|
||||
/// [Predictor::FloatingPoint] is currently not supported.
|
||||
/// [Predictor::Horizontal] is for integer sample types.
|
||||
/// [Predictor::FloatingPoint] is for floating-point sample types (f32, f64).
|
||||
pub type Predictor = crate::tags::Predictor;
|
||||
#[cfg(feature = "deflate")]
|
||||
pub type DeflateLevel = compression::DeflateLevel;
|
||||
|
||||
pub use tag_filter::TagFilter;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Default)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub enum Compression {
|
||||
/// No compression whatsoever. Fastest but produces large files.
|
||||
#[default]
|
||||
Uncompressed,
|
||||
/// Legacy [LZW](https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch) algorithm. Not recommended, use DEFLATE instead.
|
||||
#[cfg(feature = "lzw")]
|
||||
Lzw,
|
||||
/// [DEFLATE](https://en.wikipedia.org/wiki/DEFLATE) algorithm with the specified compression level.
|
||||
///
|
||||
/// The valid levels are 1 through 9 inclusive. The default level is 6.
|
||||
///
|
||||
/// - `1` stands for light but fast compression
|
||||
/// - `6` is the recommended balanced default
|
||||
/// - `9` is maximum compression ratio at the cost of slow encoding
|
||||
#[cfg(feature = "deflate")]
|
||||
Deflate(DeflateLevel),
|
||||
Deflate(u8),
|
||||
/// [PackBits](https://en.wikipedia.org/wiki/PackBits) compression, a variant of RLE compression scheme. Fast, primitive compression with a poor compression ratio.
|
||||
Packbits,
|
||||
}
|
||||
|
||||
@@ -156,7 +163,15 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
|
||||
/// Set the predictor to use
|
||||
///
|
||||
/// A predictor is used to simplify the file before writing it. This is very
|
||||
/// useful when writing a file compressed using LZW as it can improve efficiency
|
||||
/// useful when writing a file compressed using LZW or DEFLATE as it can improve compression ratio
|
||||
///
|
||||
/// The floating-point predictor is not universally supported. While most software can handle it,
|
||||
/// Safari, Mac OS Preview and ImageJ Java library cannot. Don't use a predictor for float data
|
||||
/// for maximum compatibility at the cost of compression ratio.
|
||||
///
|
||||
/// The chosen predictor should match the data type being encoded.
|
||||
/// Encoding floating-point data with `Predictor::Horizontal` is technically possible,
|
||||
/// but rarely beneficial, and some decoders may reject such files.
|
||||
pub fn with_predictor(mut self, predictor: Predictor) -> Self {
|
||||
self.predictor = predictor;
|
||||
|
||||
@@ -406,7 +421,7 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
||||
/// 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 {
|
||||
if !data.len().is_multiple_of(usize::from(ty.byte_len())) {
|
||||
return Err(TiffError::UsageError(UsageError::MismatchedEntryLength {
|
||||
ty,
|
||||
found: data.len(),
|
||||
@@ -437,148 +452,6 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
||||
self.directory.extend(entries);
|
||||
}
|
||||
|
||||
/// Copy metadata from the IFD that a decoder refers to.
|
||||
///
|
||||
/// You generally want to use [`ImageEncoder::write_metadata_copy_from`] which automatically
|
||||
/// fills out the [`EncodeMetadataApplicability`] as applicable to the chosen color space and
|
||||
/// other sample configuration.
|
||||
pub fn write_metadata_copy_from<R: Read + Seek>(
|
||||
&mut self,
|
||||
decoder: &mut Decoder<R>,
|
||||
options: EncodeMetadataOptions<'_>,
|
||||
kind: EncodeMetadataApplicability,
|
||||
) -> TiffResult<EncodeMetadataReport> {
|
||||
let mut report = EncodeMetadataReport {
|
||||
copied: 0,
|
||||
filtered: 0,
|
||||
};
|
||||
|
||||
enum Role {
|
||||
Ifd,
|
||||
ExifPrivate,
|
||||
GpsPrivate,
|
||||
}
|
||||
|
||||
let ptr = decoder.ifd_pointer().unwrap();
|
||||
let mut stack = vec![(ptr, Role::Ifd, usize::MAX)];
|
||||
let mut buf = ValueBuffer::empty(Type::UNDEFINED);
|
||||
|
||||
// Directories which we will later write in reverse-pre-order.
|
||||
let mut pre_order_dirs: Vec<(Directory, usize, Tag)> = vec![];
|
||||
|
||||
while let Some((ifd, role, parent)) = stack.pop() {
|
||||
let dir = decoder.read_directory(ifd)?;
|
||||
let mut created_dir = Directory::empty();
|
||||
|
||||
let filter = match role {
|
||||
Role::Ifd => TagFilter::filter_primary,
|
||||
Role::ExifPrivate => TagFilter::filter_exif_private,
|
||||
Role::GpsPrivate => TagFilter::filter_gps_private,
|
||||
};
|
||||
|
||||
let mut ifd_reader = decoder.read_directory_tags(&dir);
|
||||
|
||||
for (tag, entry) in dir.iter() {
|
||||
let level = filter(options.tag_filter, tag.to_u16());
|
||||
match options
|
||||
.tag_filter
|
||||
.should_keep_for_image_writer(level, &kind)
|
||||
{
|
||||
tag_filter::Choice::Ok => {}
|
||||
tag_filter::Choice::Descend(subifd_kind) => {
|
||||
// Only single pointers can be directories to descend to, avoid an overly
|
||||
// large read by pulling that value out of the file.
|
||||
if entry.count() != 1 {
|
||||
report.filtered = report.filtered.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ifd_reader.find_tag_buf(tag, &mut buf)?.is_none() {
|
||||
// Should not happen (we're iterating the dict already), but be robust.
|
||||
report.filtered = report.filtered.saturating_add(1);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(ifd) = buf.as_ifd_pointer() else {
|
||||
report.filtered = report.filtered.saturating_add(1);
|
||||
continue;
|
||||
};
|
||||
|
||||
let role = match subifd_kind {
|
||||
tag_filter::SubifdKind::ExifPrivate => Role::ExifPrivate,
|
||||
tag_filter::SubifdKind::Gps => Role::GpsPrivate,
|
||||
tag_filter::SubifdKind::Interoperability => {
|
||||
// Only applicable to compressed images.
|
||||
report.filtered = report.filtered.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let this_as_parent = pre_order_dirs.len();
|
||||
stack.push((ifd, role, this_as_parent));
|
||||
}
|
||||
tag_filter::Choice::Discard => continue,
|
||||
tag_filter::Choice::Filtered
|
||||
// Not supported yet.
|
||||
| tag_filter::Choice::Special(tag_filter::Special::JpegThumbnail) => {
|
||||
// Not to be copied.
|
||||
report.filtered = report.filtered.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
tag_filter::Choice::Special(tag_filter::Special::CompressedExclusive) => {
|
||||
// We're not writing an App segment for a compressed file, are we?
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ifd_reader.find_tag_buf(tag, &mut buf)?.is_none() {
|
||||
report.filtered = report.filtered.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Re-encode the value to the target file.
|
||||
buf.set_byte_order(ByteOrder::native());
|
||||
let entry = self.write_entry_buf(&buf)?;
|
||||
created_dir.extend([(tag, entry)]);
|
||||
report.copied = report.copied.saturating_add(1);
|
||||
}
|
||||
|
||||
let tag_in_parent = match role {
|
||||
// should be unreachable? For now. But thumbnails would use this so we best be a
|
||||
// little cautious here. Also thumbnails are weird, they do have image data but we
|
||||
// would not copy that data itself with these methods. Just eh.
|
||||
//
|
||||
// Anyways the tree root will use this tag as a dummy.
|
||||
Role::Ifd => Tag::SubIfd,
|
||||
Role::ExifPrivate => Tag::ExifDirectory,
|
||||
Role::GpsPrivate => Tag::GpsDirectory,
|
||||
};
|
||||
|
||||
pre_order_dirs.push((created_dir, parent, tag_in_parent));
|
||||
}
|
||||
|
||||
// Now post-order traverse the directory tree.
|
||||
while let Some((directory, parent, tag)) = pre_order_dirs.pop() {
|
||||
// The root is the items for the IFD itself.
|
||||
if pre_order_dirs.is_empty() {
|
||||
// We do not enter ourselves into a parent directory.
|
||||
let _ = (parent, tag);
|
||||
|
||||
self.extend_from(&directory);
|
||||
break;
|
||||
}
|
||||
|
||||
let offset = Self::write_directory_inner(self.writer, &directory)?;
|
||||
let offset = K::convert_offset(offset)?;
|
||||
let entry = self.write_entry(offset)?;
|
||||
|
||||
let (parent_dir, _, _) = &mut pre_order_dirs[parent];
|
||||
parent_dir.extend([(tag, entry)]);
|
||||
}
|
||||
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
/// Define the parent directory.
|
||||
///
|
||||
/// Each directory has an offset based link to its successor, forming a linked list in the
|
||||
@@ -605,21 +478,18 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
||||
}
|
||||
|
||||
fn write_directory(&mut self) -> TiffResult<u64> {
|
||||
Self::write_directory_inner(self.writer, &self.directory)
|
||||
}
|
||||
|
||||
fn write_directory_inner(writer: &mut TiffWriter<W>, directory: &Directory) -> TiffResult<u64> {
|
||||
// Start by turning all buffered unwritten values into entries.
|
||||
let offset = writer.offset();
|
||||
K::write_entry_count(writer, directory.len())?;
|
||||
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 directory.iter() {
|
||||
writer.write_u16(tag.to_u16())?;
|
||||
writer.write_u16(entry.field_type().to_u16())?;
|
||||
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(writer)?;
|
||||
writer.write_bytes(&entry.offset()[..offset_bytes])?;
|
||||
count.write(self.writer)?;
|
||||
self.writer
|
||||
.write_bytes(&entry.offset_raw()[..offset_bytes])?;
|
||||
}
|
||||
|
||||
Ok(offset)
|
||||
@@ -731,72 +601,6 @@ impl<'a, W: Write + Seek, K: TiffKind> Drop for DirectoryEncoder<'a, W, K> {
|
||||
}
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct EncodeMetadataOptions<'lt> {
|
||||
/// Several metadata kinds contain additional tags in further, linked directories that are only
|
||||
/// referenced with an `Ifd`/`Ifd8` tag. These directories may have their own semantics, e.g. a
|
||||
/// different set of tags or even new value types.
|
||||
///
|
||||
/// Since we know that the whole directory contains only metadata it would be possible to copy
|
||||
/// all tags contained in it, however tags that can not be semantically validated might contain
|
||||
/// data that is only valid within the original image or must be transformed during the copy.
|
||||
/// For instance, JPEG thumbnails are embedded with separate offset and length fields (`0x0201`
|
||||
/// and `0x202`). (This has caused the author mental harm, please do not consult me about it).
|
||||
///
|
||||
/// Setting this flag will skip such unknown tags instead of attempting a potentially incorrect
|
||||
/// copy. However the set of copied metadata is then dependent on the `tiff` library.
|
||||
pub tag_filter: &'lt TagFilter,
|
||||
// TODO:
|
||||
// Some tags are not allowed to be recorded or can be semantically validated. The library can
|
||||
// skip these tags while copying metadata. For instance, `YCbCrCoefficients` _may_ be
|
||||
// recorded for images in `YCbCr` photometric interpretation but _must not_ be recorded for
|
||||
// images in any other interpretation (colorspace conversion should have been done during the
|
||||
// transformation).
|
||||
// pub validate_tags: enum ValidateTags {
|
||||
// None,
|
||||
// Skip,
|
||||
// Error,
|
||||
// Strict /* Error but also do not allow anything that can not be validated */
|
||||
// },
|
||||
}
|
||||
|
||||
impl Default for EncodeMetadataOptions<'_> {
|
||||
fn default() -> Self {
|
||||
EncodeMetadataOptions {
|
||||
tag_filter: TagFilter::only_known_tags(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describe the kind of image for which metadata is recorded.
|
||||
///
|
||||
/// Metadata that is not applicable to the color space or the layout may be skipped. For instance,
|
||||
/// subsampling basis position is only relevant on YCbCr / Chroma color photometric interpretation
|
||||
/// where subsampling is defined.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct EncodeMetadataApplicability {
|
||||
/// The photometric interpretation of the image data.
|
||||
///
|
||||
/// If `None` then metadata tags are not filtered, otherwise only tags applicable to the given
|
||||
/// photometric interpretation are recorded.
|
||||
pub photometric: Option<PhotometricInterpretation>,
|
||||
/// The planar configuration of the image data.
|
||||
///
|
||||
/// If `None` then metadata tags are not filtered, otherwise only tags applicable to the given
|
||||
/// configuration are recorded.
|
||||
pub planar: Option<PlanarConfiguration>,
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
pub struct EncodeMetadataReport {
|
||||
/// Number of tags transferred (recursively).
|
||||
pub copied: u64,
|
||||
/// Number of tags encountered that were skipped due to the provided filter.
|
||||
pub filtered: u64,
|
||||
}
|
||||
|
||||
/// Type to encode images strip by strip.
|
||||
///
|
||||
/// You should call `finish` on this when you are finished with it.
|
||||
@@ -850,11 +654,14 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> {
|
||||
impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> {
|
||||
fn sanity_check(compression: Compression, predictor: Predictor) -> TiffResult<()> {
|
||||
match (predictor, compression, T::SAMPLE_FORMAT[0]) {
|
||||
// Horizontal predictor is not valid for floating-point types
|
||||
(Predictor::Horizontal, _, SampleFormat::IEEEFP | SampleFormat::Void) => {
|
||||
Err(TiffError::UsageError(UsageError::PredictorIncompatible))
|
||||
}
|
||||
// Floating-point predictor is only valid for IEEEFP sample format
|
||||
(Predictor::FloatingPoint, _, SampleFormat::IEEEFP) => Ok(()),
|
||||
(Predictor::FloatingPoint, _, _) => {
|
||||
Err(TiffError::UsageError(UsageError::PredictorUnavailable))
|
||||
Err(TiffError::UsageError(UsageError::PredictorIncompatible))
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
@@ -988,7 +795,13 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
}
|
||||
self.encoder.write_data(row_result.as_slice())?
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
Predictor::FloatingPoint => {
|
||||
let mut row_result = Vec::with_capacity(std::mem::size_of_val(value));
|
||||
for row in value.chunks_exact(self.row_samples as usize) {
|
||||
T::floating_point_predict(row, &mut row_result);
|
||||
}
|
||||
self.encoder.write_data(row_result.as_slice())?
|
||||
}
|
||||
};
|
||||
|
||||
let byte_count = self.encoder.last_written() as usize;
|
||||
@@ -1037,30 +850,12 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy metadata from the IFD that a decoder refers to.
|
||||
pub fn write_metadata_copy_from<R: Read + Seek>(
|
||||
&mut self,
|
||||
decoder: &mut Decoder<R>,
|
||||
options: EncodeMetadataOptions<'_>,
|
||||
) -> TiffResult<EncodeMetadataReport> {
|
||||
self.encoder.write_metadata_copy_from(
|
||||
decoder,
|
||||
options,
|
||||
EncodeMetadataApplicability {
|
||||
photometric: Some(<T>::TIFF_VALUE),
|
||||
planar: Some(PlanarConfiguration::Chunky),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Set image resolution
|
||||
pub fn resolution(&mut self, unit: ResolutionUnit, value: Rational) {
|
||||
self.encoder
|
||||
.write_tag(Tag::ResolutionUnit, unit.to_u16())
|
||||
.unwrap();
|
||||
self.encoder
|
||||
.write_tag(Tag::XResolution, value.clone())
|
||||
.unwrap();
|
||||
self.encoder.write_tag(Tag::XResolution, value).unwrap();
|
||||
self.encoder.write_tag(Tag::YResolution, value).unwrap();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
use crate::tags::{PhotometricInterpretation, PlanarConfiguration};
|
||||
|
||||
/// A filter of tags copied in
|
||||
/// [`DirectoryEncoder::write_metadata_from`][`super::DirectoryEncoder::write_metadata_from`].
|
||||
pub struct TagFilter {
|
||||
filter_level: FilterLevel,
|
||||
}
|
||||
|
||||
impl TagFilter {
|
||||
/// Return a filter that allows all tags from metadata subdirectories.
|
||||
pub fn allow_all_in_subdirectories() -> &'static TagFilter {
|
||||
static INSTANCE: TagFilter = TagFilter {
|
||||
filter_level: FilterLevel::All,
|
||||
};
|
||||
&INSTANCE
|
||||
}
|
||||
|
||||
/// Return a filter that has an explicit allow list of tags, for each of a known list of
|
||||
/// metadata subdirectories.
|
||||
pub fn only_known_tags() -> &'static TagFilter {
|
||||
static INSTANCE: TagFilter = TagFilter {
|
||||
filter_level: FilterLevel::Known,
|
||||
};
|
||||
&INSTANCE
|
||||
}
|
||||
|
||||
/// FIXME: take a configuration by which to filter out complete metadata kinds.
|
||||
/// FIXME: when we iterate over a whole directory of tags, this iteration is more expensive
|
||||
/// than necessary. Tags within a directory should be ordered. We can thus perform a binary set
|
||||
/// join which is more efficient than checking the individual filter on each. Does not matter
|
||||
/// much in most cases though.
|
||||
pub(crate) fn filter_primary(&self, tag: u16) -> Level {
|
||||
let as_exif = self.filter_primary_exif(tag);
|
||||
|
||||
if !matches!(as_exif, Level::Unknown) {
|
||||
return as_exif;
|
||||
}
|
||||
|
||||
let as_gps = self.filter_primary_gps(tag);
|
||||
|
||||
if !matches!(as_gps, Level::Unknown) {
|
||||
return as_gps;
|
||||
}
|
||||
|
||||
let as_icc = self.filter_icc(tag);
|
||||
|
||||
if !matches!(as_icc, Level::Unknown) {
|
||||
return as_icc;
|
||||
}
|
||||
|
||||
Level::Unknown
|
||||
}
|
||||
|
||||
/// As per EXIF V3.0 tags, Primary Image (0th IFD).
|
||||
pub(crate) fn filter_primary_exif(&self, tag: u16) -> Level {
|
||||
match tag {
|
||||
// width, height, etc
|
||||
0x0100..=0x0103 => Level::SampleLayout,
|
||||
// photometric interpretation
|
||||
0x0106 => Level::SampleLayout,
|
||||
// data tied to the camera and customized description, potentially PI.
|
||||
0x010f..=0x0110 => Level::Value,
|
||||
// strip offsets
|
||||
0x0111 => Level::SampleLayout,
|
||||
// orientation
|
||||
0x0112 => Level::Value,
|
||||
// strip byte counts, samples
|
||||
0x0115..=0x0117 => Level::SampleLayout,
|
||||
// pixel dimensions (ratio). Should be generic enough.
|
||||
0x011a..=0x011b => Level::Value,
|
||||
// planar configuration
|
||||
0x011c => Level::Photometric(Photometric::Planar),
|
||||
// units for 0x011a,0x011b
|
||||
0x0128 => Level::Value,
|
||||
// transfer function
|
||||
0x012d => Level::Photometric(Photometric::All),
|
||||
// file data time, software pipeline. I'm wary of privacy implications.
|
||||
0x0131..=0x0132 => Level::Value,
|
||||
// artist, clearly PI unless you explicitly wish it to be attributed.
|
||||
0x013b => Level::Value,
|
||||
// Whitepoint, Chromaticities
|
||||
0x013e..=0x013f => Level::Photometric(Photometric::All),
|
||||
// The table lists these as Not allowed for anything but we may have a choice here for
|
||||
// older models of images.
|
||||
0x0201..=0x0202 => Level::Special(Special::JpegThumbnail),
|
||||
0x0211..=0x0213 => Level::Photometric(Photometric::YCbCr),
|
||||
0x0214 => Level::Photometric(Photometric::All),
|
||||
// Copyright holder (not same as artist).
|
||||
0x8298 => Level::Value,
|
||||
0x8769 => Level::Ifd(SubifdKind::ExifPrivate),
|
||||
_ => Level::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn filter_primary_gps(&self, tag: u16) -> Level {
|
||||
match tag {
|
||||
0x8825 => Level::Ifd(SubifdKind::Gps),
|
||||
_ => Level::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn filter_icc(&self, tag: u16) -> Level {
|
||||
match tag {
|
||||
0x8ff3 => Level::Value,
|
||||
_ => Level::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn filter_exif_private(&self, tag: u16) -> Level {
|
||||
match tag {
|
||||
// Maybe this is not personal identifying but some equipment may be very specifically
|
||||
// known through these. And it's not critical to displaying it correctly.
|
||||
0x829a => Level::Value,
|
||||
0x829d => Level::Value,
|
||||
0x8822 => Level::Value,
|
||||
0x8824 => Level::Value,
|
||||
0x8827 => Level::Value,
|
||||
// Opto-electric coefficients
|
||||
0x8828 => Level::Value,
|
||||
0x8830..=0x8835 => Level::Value,
|
||||
0x9000 => Level::Value,
|
||||
0x9003..=0x9004 => Level::Value,
|
||||
0x9010..=0x9012 => Level::Value,
|
||||
// Not allowed for uncompressed data!
|
||||
0x9101..=0x9102 => Level::Special(Special::CompressedExclusive),
|
||||
0x9201..=0x9209 => Level::Value,
|
||||
0x920a => Level::Value,
|
||||
0x9214 => Level::Value,
|
||||
0x927c => Level::Value,
|
||||
0x9286 => Level::Value,
|
||||
0x9290..=0x9292 => Level::Value,
|
||||
0x9400..=0x9405 => Level::Value,
|
||||
0xa000 => Level::Value,
|
||||
0xa001 => Level::Photometric(Photometric::All),
|
||||
0xa002..=0xa003 => Level::Special(Special::CompressedExclusive),
|
||||
0xa004 => Level::Value,
|
||||
0xa005 => Level::Ifd(SubifdKind::Interoperability),
|
||||
0xa20b..=0xa20c => Level::Value,
|
||||
0xa20e..=0xa210 => Level::Value,
|
||||
0xa214..=0xa215 => Level::Value,
|
||||
0xa217 => Level::Value,
|
||||
0xa300..=0xa302 => Level::Value,
|
||||
0xa401..=0xa40c => Level::Value,
|
||||
0xa420 => Level::Value,
|
||||
0xa430..=0xa43c => Level::Value,
|
||||
0xa460..=0xa462 => Level::Value,
|
||||
0xa500 => Level::Photometric(Photometric::All),
|
||||
_ => Level::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn filter_gps_private(&self, tag: u16) -> Level {
|
||||
match tag {
|
||||
0x0000..=0x001f => Level::Value,
|
||||
_ => Level::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn should_keep_for_image_writer(
|
||||
&self,
|
||||
level: Level,
|
||||
target: &super::EncodeMetadataApplicability,
|
||||
) -> Choice {
|
||||
match level {
|
||||
Level::Unknown if matches!(self.filter_level, FilterLevel::All) => Choice::Filtered,
|
||||
// Sample Layout is controlled by the writer (dimensions, color type, …). Do not copy!
|
||||
Level::SampleLayout => Choice::Discard,
|
||||
Level::Ifd(subifd_kind) => Choice::Descend(subifd_kind),
|
||||
Level::Photometric(photometric) => {
|
||||
match (photometric, target.planar, target.photometric) {
|
||||
(Photometric::All, _, _) => Choice::Ok,
|
||||
(Photometric::Planar, Some(PlanarConfiguration::Planar) | None, _) => {
|
||||
Choice::Ok
|
||||
}
|
||||
(Photometric::YCbCr, _, Some(PhotometricInterpretation::YCbCr) | None) => {
|
||||
Choice::Ok
|
||||
}
|
||||
_ => Choice::Discard,
|
||||
}
|
||||
}
|
||||
Level::Value => Choice::Ok,
|
||||
Level::Special(special) => Choice::Special(special),
|
||||
_ => Choice::Discard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum Choice {
|
||||
Ok,
|
||||
Descend(SubifdKind),
|
||||
/// Tag is inapplicable to be copied.
|
||||
Discard,
|
||||
/// Tag was chosen not to be copied.
|
||||
Filtered,
|
||||
Special(Special),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum FilterLevel {
|
||||
All,
|
||||
Known,
|
||||
}
|
||||
|
||||
/// The level of support for a value.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Level {
|
||||
Unknown,
|
||||
/// Value describes the image data layout, which we will not want to write into the encoder's
|
||||
/// directory where the image data is written later.
|
||||
///
|
||||
/// These are treated as privacy preserving.
|
||||
SampleLayout,
|
||||
/// Value is an IFD and must be copied by making a subdirectory.
|
||||
///
|
||||
/// These are treated as privacy preserving, independent of the tags within the subdirectory
|
||||
/// which may not be. (For instance the presence of an empty directory is fine but we will omit
|
||||
/// a GPS anyways).
|
||||
Ifd(SubifdKind),
|
||||
/// Value is related to photometric interpretation and should not be copied when the target
|
||||
/// does not match the expected interpretation.
|
||||
///
|
||||
/// These are *generally* treated as privacy preserving.
|
||||
Photometric(Photometric),
|
||||
/// This is a value we know of.
|
||||
/// FIXME: we do not differentiate by personal identifiable information and otherwise which is
|
||||
/// quite important for some variations of copying metadata.
|
||||
Value,
|
||||
/// We recognize the tag but it can not be copied by itself through normal means.
|
||||
///
|
||||
/// Privacy treatment depends on the tag.
|
||||
Special(Special),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Photometric {
|
||||
All,
|
||||
/// Do not copy for non-planar data.
|
||||
Planar,
|
||||
/// Do not copy for non-YCbCr data.
|
||||
YCbCr,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum SubifdKind {
|
||||
ExifPrivate,
|
||||
Gps,
|
||||
Interoperability,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Special {
|
||||
/// Thumbnail which we regard as PI since we do not deep-clean the JPEG structure.
|
||||
JpegThumbnail,
|
||||
/// May only occur in compressed (JPEG APP segments) data.
|
||||
CompressedExclusive,
|
||||
}
|
||||
@@ -665,22 +665,22 @@ impl_tiff_value_for_contiguous_sequence!(Rational; 8; Type::RATIONAL);
|
||||
impl_tiff_value_for_contiguous_sequence!(SRational; 8; Type::SRATIONAL);
|
||||
|
||||
/// Type to represent tiff values of type `IFD`
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Ifd(pub u32);
|
||||
|
||||
/// Type to represent tiff values of type `IFD8`
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Ifd8(pub u64);
|
||||
|
||||
/// Type to represent tiff values of type `RATIONAL`
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Rational {
|
||||
pub n: u32,
|
||||
pub d: u32,
|
||||
}
|
||||
|
||||
/// Type to represent tiff values of type `SRATIONAL`
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct SRational {
|
||||
pub n: i32,
|
||||
pub d: i32,
|
||||
|
||||
@@ -172,7 +172,7 @@ impl<W: Write> TiffWriter<W> {
|
||||
}
|
||||
|
||||
pub fn pad_word_boundary(&mut self) -> Result<(), io::Error> {
|
||||
if self.offset % 4 != 0 {
|
||||
if !self.offset.is_multiple_of(4) {
|
||||
let padding = [0, 0, 0];
|
||||
let padd_len = 4 - (self.offset % 4);
|
||||
self.writer.write_all(&padding[..padd_len as usize])?;
|
||||
@@ -221,7 +221,7 @@ fn encoder_on_short_writes() {
|
||||
};
|
||||
|
||||
let imgdata = vec![42u8; 100 * 100];
|
||||
let mut data = crate::decoder::DecodingResult::U8(vec![]);
|
||||
let mut data = crate::decoder::DecodingSampleBuffer::U8(vec![]);
|
||||
|
||||
for comp in [
|
||||
super::Compression::Uncompressed,
|
||||
@@ -241,11 +241,13 @@ fn encoder_on_short_writes() {
|
||||
}
|
||||
|
||||
write.inner.set_position(0);
|
||||
let mut tiff_reader = crate::decoder::Decoder::new(&mut write.inner).unwrap();
|
||||
|
||||
let mut tiff_reader = crate::decoder::Decoder::open(&mut write.inner).unwrap();
|
||||
tiff_reader.next_image().unwrap();
|
||||
let _layout = tiff_reader.read_image_to_buffer(&mut data).unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&data, crate::decoder::DecodingResult::U8(v) if *v == imgdata),
|
||||
matches!(&data, crate::decoder::DecodingSampleBuffer::U8(v) if *v == imgdata),
|
||||
"{data:?} @ {}",
|
||||
data.as_buffer(0).byte_len()
|
||||
);
|
||||
|
||||
@@ -70,6 +70,9 @@ quick_error! {
|
||||
InconsistentSizesEncountered {
|
||||
display("inconsistent sizes encountered")
|
||||
}
|
||||
IncorrectChunkCount {expected: u32, found: u32} {
|
||||
display("incorrect number of chunk offsets encoded for the tile count of the image, expected {expected} but found {found}")
|
||||
}
|
||||
InvalidDimensions(width: u32, height: u32) {
|
||||
display("invalid dimensions: {width}x{height}")
|
||||
}
|
||||
|
||||
+4
-2
@@ -51,7 +51,8 @@ pub enum ColorType {
|
||||
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)
|
||||
@@ -66,7 +67,8 @@ impl ColorType {
|
||||
}
|
||||
}
|
||||
|
||||
fn num_samples(&self) -> u16 {
|
||||
/// 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,
|
||||
|
||||
+134
-66
@@ -1,4 +1,5 @@
|
||||
use crate::encoder::TiffValue;
|
||||
use core::fmt;
|
||||
|
||||
macro_rules! tags {
|
||||
{
|
||||
@@ -169,6 +170,18 @@ pub enum Tag(u16) unknown(
|
||||
// practice it just returns garbage and the validity does not matter greatly to us).
|
||||
pub struct IfdPointer(pub u64);
|
||||
|
||||
impl fmt::LowerHex for IfdPointer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt::LowerHex::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::UpperHex for IfdPointer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt::UpperHex::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
tags! {
|
||||
/// The type of an IFD entry (a 2 byte field).
|
||||
pub enum Type(u16) {
|
||||
@@ -267,11 +280,25 @@ pub enum CompressionMethod(u16) unknown(
|
||||
|
||||
// Self-assigned by libtiff
|
||||
ZSTD = 0xC350,
|
||||
|
||||
// Self-assigned by libtiff
|
||||
WebP = 0xC351,
|
||||
|
||||
// Self-assigned by libtiff/SGI
|
||||
// A packbit like scheme on specific 32-bit samples (16 sint luma, 8 sint u,v). Only really
|
||||
// compatible with PhotometricInterpretation CIELogLuv.
|
||||
SgiLog = 34676,
|
||||
// A packbit like scheme on specific 24-bit samples (10 packed bits luma, 14-bit uv index).
|
||||
// Only really compatible with PhotometricInterpretation CIELogLuv.
|
||||
SgiLog24 = 34677,
|
||||
}
|
||||
}
|
||||
|
||||
tags! {
|
||||
pub enum PhotometricInterpretation(u16) {
|
||||
pub enum PhotometricInterpretation(u16) unknown(
|
||||
/// A private or extension tag
|
||||
unknown
|
||||
) {
|
||||
WhiteIsZero = 0,
|
||||
BlackIsZero = 1,
|
||||
RGB = 2,
|
||||
@@ -279,9 +306,41 @@ pub enum PhotometricInterpretation(u16) {
|
||||
TransparencyMask = 4,
|
||||
CMYK = 5,
|
||||
YCbCr = 6,
|
||||
/// Biased Lab with all coefficients stored as unsigned integers.
|
||||
CIELab = 8,
|
||||
IccLab = 9,
|
||||
ItuLab = 10,
|
||||
/// Logarithmic L* color space,
|
||||
///
|
||||
/// see: <https://web.archive.org/web/20250121120358/https://www.anyhere.com/gward/pixformat/tiffluv.html>
|
||||
/// References: <https://doi.org/10.2352/CIC.1998.6.1.art00046>
|
||||
///
|
||||
/// The `uv` coordinates are based on:
|
||||
///
|
||||
/// ```text
|
||||
/// (6*u - 16*v + 12) * (-2x + 12y + 3) = 36
|
||||
///
|
||||
/// u' = 4x / ( -2x + 12y + 3 )
|
||||
/// v' = 9y / ( -2x + 12y + 3 )
|
||||
/// x = 9u' / (6u' - 16v' + 12)
|
||||
/// y = 4v' / (6u' - 16v' + 12)
|
||||
/// ```
|
||||
///
|
||||
/// Since `(-2x + 12y + 3) <= 15` it holds `(6u' - 16v' + 12) >= 36/15` for all XYZ colors
|
||||
/// which makes the back transform to `xyY` well-defined. Also `v' <= 9/15 = 0.6` and also
|
||||
/// by practical means `u' <= 2/3`.
|
||||
///
|
||||
/// The compression format that is supposed to be used with this constrains them as well. It
|
||||
/// encodes each component as a 16-bit unsigned integer scaled and clamped to a quantization
|
||||
/// using 256 out of 410 levels or an index into a table based on a quantization to 177 levels
|
||||
/// each `0.0035` (~1/286) wide and 163 levels of `v` (same quantization width).
|
||||
///
|
||||
/// > Larson, G.W., ``Overcoming Gamut and Dynamic Range Limitations in Digital Images,'' Proceedings of the Sixth Color Imaging Conference, November 1998.
|
||||
CIELogL = 32844,
|
||||
/// Logarithmic L*u'v' color space,
|
||||
///
|
||||
/// See references under [`Self::CIELogL`].
|
||||
CIELogLuv = 32845,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +360,9 @@ pub enum Predictor(u16) {
|
||||
/// This means that instead of having in order `[r1, g1. b1, r2, g2 ...]` you will find
|
||||
/// `[r1, g1, b1, r2-r1, g2-g1, b2-b1, r3-r2, g3-g2, ...]`
|
||||
Horizontal = 2,
|
||||
/// Not currently supported
|
||||
/// Floating-point predictor for IEEE floating-point sample types.
|
||||
///
|
||||
/// This predictor byte-shuffles the data and applies byte-level horizontal differencing.
|
||||
FloatingPoint = 3,
|
||||
}
|
||||
}
|
||||
@@ -368,7 +429,24 @@ impl ValueBuffer {
|
||||
}
|
||||
|
||||
/// Create a value with native byte order from in-memory data.
|
||||
pub fn from_value<T: TiffValue>(value: &T) -> Self {
|
||||
///
|
||||
/// This is equivalent to calling [`Self::set_value`] on a newly created buffer.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Create a buffer representing a particular tag's value:
|
||||
///
|
||||
/// ```
|
||||
/// use tiff::tags::{ByteOrder, CompressionMethod, ValueBuffer};
|
||||
///
|
||||
/// // A buffer with initial content, for instance a tag's value.
|
||||
/// let buffer = ValueBuffer::from_value(&CompressionMethod::ModernJPEG);
|
||||
/// assert_eq!(buffer.as_bytes().len(), 2);
|
||||
///
|
||||
/// // Note that depends on host endianess which of these holds:
|
||||
/// assert!(buffer.as_bytes() == [0, 7] || buffer.as_bytes() == [7, 0]);
|
||||
/// ```
|
||||
pub fn from_value<T: TiffValue + ?Sized>(value: &T) -> Self {
|
||||
ValueBuffer {
|
||||
bytes: value.data().into_owned(),
|
||||
ty: <T as TiffValue>::FIELD_TYPE,
|
||||
@@ -377,6 +455,42 @@ impl ValueBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the buffer to the defined value.
|
||||
///
|
||||
/// The previous data is erased. This converts the value's representation to the byte order of
|
||||
/// the buffer in the process (which is a no-op if the buffer uses host byte order).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use tiff::tags::{ByteOrder, CompressionMethod, ValueBuffer};
|
||||
///
|
||||
/// // A buffer with initial content, for instance a tag's value.
|
||||
/// let mut buffer = ValueBuffer::from_value(&CompressionMethod::JPEG);
|
||||
/// assert_eq!(buffer.as_bytes().len(), 2);
|
||||
///
|
||||
/// // Ensure a host-independent byte order for the following representation.
|
||||
/// buffer.set_byte_order(ByteOrder::LittleEndian);
|
||||
/// // slices and arrays of primitives are a possible `TiffValue`
|
||||
/// buffer.set_value(&[0, 1, 42u16]);
|
||||
///
|
||||
/// // The little endian representation of these numbers (2 bytes each)
|
||||
/// assert_eq!(buffer.as_bytes(), [0, 0, 1, 0, 42, 0]);
|
||||
/// ```
|
||||
pub fn set_value<T: TiffValue + ?Sized>(&mut self, value: &T) {
|
||||
self.bytes.clear();
|
||||
|
||||
match value.data() {
|
||||
std::borrow::Cow::Borrowed(slice) => self.bytes.extend_from_slice(slice),
|
||||
std::borrow::Cow::Owned(ref mut data) => self.bytes.append(data),
|
||||
}
|
||||
|
||||
self.ty = <T as TiffValue>::FIELD_TYPE;
|
||||
self.count = value.count() as u64;
|
||||
|
||||
ByteOrder::native().convert(self.ty, &mut self.bytes, self.byte_order);
|
||||
}
|
||||
|
||||
pub fn byte_order(&self) -> ByteOrder {
|
||||
self.byte_order
|
||||
}
|
||||
@@ -407,32 +521,6 @@ impl ValueBuffer {
|
||||
&mut self.bytes[..len]
|
||||
}
|
||||
|
||||
/// See [`Value::into_ifd_pointer`](crate::decoder::ifd::Value::into_ifd_pointer).
|
||||
pub(crate) fn as_ifd_pointer(&self) -> Option<IfdPointer> {
|
||||
if self.count != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
match self.ty {
|
||||
// See [`ifd::Value::into_ifd_pointer`] there is enough legacy that uses `LONG` to
|
||||
// refer to an offset into the file with the meaning of a directory pointer. That is
|
||||
// rather annoying but legacy and not a big deal.
|
||||
Type::IFD | Type::LONG => {
|
||||
let mut buf = [0u8; 4];
|
||||
buf.copy_from_slice(&self.bytes[..4]);
|
||||
ByteOrder::native().convert(Type::IFD, &mut buf, self.byte_order);
|
||||
Some(IfdPointer(u32::from_ne_bytes(buf).into()))
|
||||
}
|
||||
Type::IFD8 => {
|
||||
let mut buf = [0u8; 8];
|
||||
buf.copy_from_slice(&self.bytes[..8]);
|
||||
ByteOrder::native().convert(Type::IFD8, &mut buf, self.byte_order);
|
||||
Some(IfdPointer(u64::from_ne_bytes(buf)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the byte order of the value representation.
|
||||
pub fn set_byte_order(&mut self, byte_order: ByteOrder) {
|
||||
let len = self.assumed_len_from_count();
|
||||
@@ -482,6 +570,13 @@ impl ValueBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty value buffer with `Byte` type. This type choice is arbitrary.
|
||||
impl Default for ValueBuffer {
|
||||
fn default() -> Self {
|
||||
ValueBuffer::empty(Type::BYTE)
|
||||
}
|
||||
}
|
||||
|
||||
/// Byte order of the TIFF file.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ByteOrder {
|
||||
@@ -522,26 +617,22 @@ impl ByteOrder {
|
||||
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();
|
||||
for chunk in buffer.as_chunks_mut::<2>().0 {
|
||||
*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();
|
||||
for chunk in buffer.as_chunks_mut::<4>().0 {
|
||||
*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();
|
||||
for chunk in buffer.as_chunks_mut::<8>().0 {
|
||||
*chunk = u64::from_be_bytes(*chunk).to_le_bytes();
|
||||
}
|
||||
}
|
||||
@@ -558,34 +649,11 @@ pub(crate) enum EndianBytes {
|
||||
Eight,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_ifd_u32() {
|
||||
let mut value = ValueBuffer::from_value(&crate::encoder::Ifd(0x42));
|
||||
|
||||
for bo in [ByteOrder::BigEndian, ByteOrder::LittleEndian] {
|
||||
value.set_byte_order(bo);
|
||||
|
||||
let ptr = value
|
||||
.as_ifd_pointer()
|
||||
.expect("Should be convertible to IfdPointer");
|
||||
|
||||
// Should be in host byte order again.
|
||||
assert_eq!(ptr.0, 0x42);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_ifd_long8() {
|
||||
let mut value = ValueBuffer::from_value(&crate::encoder::Ifd8(0x42));
|
||||
|
||||
for bo in [ByteOrder::BigEndian, ByteOrder::LittleEndian] {
|
||||
value.set_byte_order(bo);
|
||||
|
||||
let ptr = value
|
||||
.as_ifd_pointer()
|
||||
.expect("Should be convertible to IfdPointer");
|
||||
|
||||
// Should be in host byte order again.
|
||||
assert_eq!(ptr.0, 0x42);
|
||||
}
|
||||
/// Defines how directories are laid out in the file.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TiffVariant {
|
||||
/// TIFF-V6.0 with 32-bit offsets and counts.
|
||||
Standard,
|
||||
/// Extended TIFF with 64-bit offsets and counts and long directories.
|
||||
BigTiff,
|
||||
}
|
||||
@@ -2,3 +2,11 @@ subsubifd.tif:
|
||||
url: https://data.kitware.com/api/v1/file/hashsum/sha512/372ca32735c8a8fdbe2286dabead9e779da63f10ba81eda84625e5273f76d74ca1a47a978f67e9c00c12f7f72009c7b2c07a641e643bb0c463812f4ae7f15d6e/download
|
||||
credit: https://github.com/DigitalSlideArchive/tifftools/commit/4d00c70dbce828262f86c1431f7c66b1748965ac
|
||||
license: CC0
|
||||
usda/*.tif
|
||||
url: https://registry.opendata.aws/naip/index.html
|
||||
credit: NAIP on AWS was accessed on 2026-01-28 from https://registry.opendata.aws/naip.
|
||||
license: Public Domain
|
||||
libtiffpic/
|
||||
url: https://download.osgeo.org/libtiff/
|
||||
credit: libtiff
|
||||
license: CC0
|
||||
@@ -15,7 +15,9 @@ fn test_big_tiff() {
|
||||
for filename in filenames.iter() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder.next_image().expect("Cannot read image");
|
||||
|
||||
assert_eq!(
|
||||
decoder.dimensions().expect("Cannot get dimensions"),
|
||||
(64, 64)
|
||||
@@ -24,21 +26,20 @@ fn test_big_tiff() {
|
||||
decoder.colortype().expect("Cannot get colortype"),
|
||||
ColorType::RGB(8)
|
||||
);
|
||||
|
||||
let mut ifd = decoder.current_ifd();
|
||||
assert_eq!(
|
||||
decoder
|
||||
.get_tag_u64(Tag::StripOffsets)
|
||||
ifd.get_tag_u64(Tag::StripOffsets)
|
||||
.expect("Cannot get StripOffsets"),
|
||||
16
|
||||
);
|
||||
assert_eq!(
|
||||
decoder
|
||||
.get_tag_u64(Tag::RowsPerStrip)
|
||||
ifd.get_tag_u64(Tag::RowsPerStrip)
|
||||
.expect("Cannot get RowsPerStrip"),
|
||||
64
|
||||
);
|
||||
assert_eq!(
|
||||
decoder
|
||||
.get_tag_u64(Tag::StripByteCounts)
|
||||
ifd.get_tag_u64(Tag::StripByteCounts)
|
||||
.expect("Cannot get StripByteCounts"),
|
||||
12288
|
||||
)
|
||||
|
||||
+26
-11
@@ -1,6 +1,6 @@
|
||||
extern crate tiff;
|
||||
|
||||
use tiff::decoder::{Decoder, DecodingResult};
|
||||
use tiff::decoder::{Decoder, DecodingSampleBuffer};
|
||||
use tiff::ColorType;
|
||||
|
||||
use std::fs::File;
|
||||
@@ -16,7 +16,9 @@ fn test_white_ieee_fp16() {
|
||||
for filename in filenames.iter() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder.next_image().expect("Cannot read image");
|
||||
|
||||
assert_eq!(
|
||||
decoder.dimensions().expect("Cannot get dimensions"),
|
||||
(256, 256)
|
||||
@@ -25,7 +27,8 @@ fn test_white_ieee_fp16() {
|
||||
decoder.colortype().expect("Cannot get colortype"),
|
||||
ColorType::Gray(16)
|
||||
);
|
||||
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
|
||||
|
||||
if let DecodingSampleBuffer::F16(img) = decoder.read_image().unwrap() {
|
||||
for p in img {
|
||||
assert!(p == half::f16::from_f32_const(1.0));
|
||||
}
|
||||
@@ -43,7 +46,9 @@ fn test_one_black_pixel_ieee_fp16() {
|
||||
for filename in filenames.iter() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder.next_image().expect("Cannot read image");
|
||||
|
||||
assert_eq!(
|
||||
decoder.dimensions().expect("Cannot get dimensions"),
|
||||
(256, 256)
|
||||
@@ -52,7 +57,8 @@ fn test_one_black_pixel_ieee_fp16() {
|
||||
decoder.colortype().expect("Cannot get colortype"),
|
||||
ColorType::Gray(16)
|
||||
);
|
||||
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
|
||||
|
||||
if let DecodingSampleBuffer::F16(img) = decoder.read_image().unwrap() {
|
||||
for (i, p) in img.iter().enumerate() {
|
||||
if i == 0 {
|
||||
assert!(p < &half::f16::from_f32_const(0.001));
|
||||
@@ -74,7 +80,9 @@ fn test_pattern_horizontal_differencing_ieee_fp16() {
|
||||
for filename in filenames.iter() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder.next_image().expect("Cannot read image");
|
||||
|
||||
assert_eq!(
|
||||
decoder.dimensions().expect("Cannot get dimensions"),
|
||||
(256, 256)
|
||||
@@ -83,7 +91,8 @@ fn test_pattern_horizontal_differencing_ieee_fp16() {
|
||||
decoder.colortype().expect("Cannot get colortype"),
|
||||
ColorType::Gray(16)
|
||||
);
|
||||
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
|
||||
|
||||
if let DecodingSampleBuffer::F16(img) = decoder.read_image().unwrap() {
|
||||
// 0, 2, 5, 8, 12, 16, 255 are black
|
||||
let black = [0, 2, 5, 8, 12, 16, 255];
|
||||
for (i, p) in img.iter().enumerate() {
|
||||
@@ -107,7 +116,9 @@ fn test_pattern_predictor_ieee_fp16() {
|
||||
for filename in filenames.iter() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder.next_image().expect("Cannot read image");
|
||||
|
||||
assert_eq!(
|
||||
decoder.dimensions().expect("Cannot get dimensions"),
|
||||
(256, 256)
|
||||
@@ -116,7 +127,8 @@ fn test_pattern_predictor_ieee_fp16() {
|
||||
decoder.colortype().expect("Cannot get colortype"),
|
||||
ColorType::Gray(16)
|
||||
);
|
||||
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
|
||||
|
||||
if let DecodingSampleBuffer::F16(img) = decoder.read_image().unwrap() {
|
||||
// 0, 2, 5, 8, 12, 16, 255 are black
|
||||
let black = [0, 2, 5, 8, 12, 16, 255];
|
||||
for (i, p) in img.iter().enumerate() {
|
||||
@@ -175,7 +187,9 @@ fn test_predictor_ieee_fp16() {
|
||||
for filename in filenames.iter() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder.next_image().expect("Cannot read image");
|
||||
|
||||
assert_eq!(
|
||||
decoder.dimensions().expect("Cannot get dimensions"),
|
||||
(16, 16)
|
||||
@@ -184,7 +198,8 @@ fn test_predictor_ieee_fp16() {
|
||||
decoder.colortype().expect("Cannot get colortype"),
|
||||
ColorType::Gray(16)
|
||||
);
|
||||
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
|
||||
|
||||
if let DecodingSampleBuffer::F16(img) = decoder.read_image().unwrap() {
|
||||
for (exp, found) in std::iter::zip(pnm_values.iter(), img.iter()) {
|
||||
assert!((exp - found.to_f32()).abs() < 0.0001);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
extern crate tiff;
|
||||
|
||||
use tiff::decoder::{Decoder, DecodingResult};
|
||||
use tiff::decoder::{Decoder, DecodingSampleBuffer};
|
||||
use tiff::tags::Tag;
|
||||
use tiff::ColorType;
|
||||
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const TEST_IMAGE_DIR: &str = "./tests/images";
|
||||
const TEST_IMAGE_DIR: &str = "./tests/geotiff";
|
||||
|
||||
#[test]
|
||||
fn test_geo_tiff() {
|
||||
@@ -15,8 +15,9 @@ fn test_geo_tiff() {
|
||||
for filename in filenames.iter() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder = decoder.with_limits(tiff::decoder::Limits::unlimited());
|
||||
decoder.next_image().expect("Cannot read image");
|
||||
|
||||
assert_eq!(
|
||||
decoder.dimensions().expect("Cannot get dimensions"),
|
||||
@@ -29,35 +30,51 @@ fn test_geo_tiff() {
|
||||
num_samples: 5
|
||||
}
|
||||
);
|
||||
|
||||
let mut ifd = decoder.current_ifd();
|
||||
assert_eq!(
|
||||
decoder
|
||||
.get_tag_u64(Tag::StripOffsets)
|
||||
ifd.get_tag_u64(Tag::StripOffsets)
|
||||
.expect("Cannot get StripOffsets"),
|
||||
418
|
||||
);
|
||||
assert_eq!(
|
||||
decoder
|
||||
.get_tag_u64(Tag::RowsPerStrip)
|
||||
ifd.get_tag_u64(Tag::RowsPerStrip)
|
||||
.expect("Cannot get RowsPerStrip"),
|
||||
10
|
||||
);
|
||||
assert_eq!(
|
||||
decoder
|
||||
.get_tag_u64(Tag::StripByteCounts)
|
||||
ifd.get_tag_u64(Tag::StripByteCounts)
|
||||
.expect("Cannot get StripByteCounts"),
|
||||
1000
|
||||
);
|
||||
assert_eq!(
|
||||
decoder
|
||||
.get_tag(Tag::ModelPixelScaleTag)
|
||||
ifd.get_tag(Tag::ModelPixelScaleTag)
|
||||
.expect("Cannot get pixel scale")
|
||||
.into_f64_vec()
|
||||
.expect("Cannot get pixel scale"),
|
||||
vec![60.0, 60.0, 0.0]
|
||||
);
|
||||
let DecodingResult::I16(data) = decoder.read_image().unwrap() else {
|
||||
let DecodingSampleBuffer::I16(data) = decoder.read_image().unwrap() else {
|
||||
panic!("Cannot read band data")
|
||||
};
|
||||
assert_eq!(data.len(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webp")]
|
||||
#[test]
|
||||
fn test_webp_tiff() {
|
||||
let filenames = ["usda_naip_256_webp_z3.tif"];
|
||||
let mut buffer = DecodingSampleBuffer::U8(vec![]);
|
||||
|
||||
for filename in filenames.iter() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
|
||||
while decoder.more_images() {
|
||||
decoder.next_image().unwrap();
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
+745
-42
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,364 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{fs::File, path};
|
||||
|
||||
use tiff::decoder::{Decoder, DecodingSampleBuffer};
|
||||
use tiff::tags::{ByteOrder, Type};
|
||||
use tiff::ColorType;
|
||||
|
||||
use libtest_mimic::{Arguments, Failed, Trial};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
const REFERENCE_DIR: &str = "tests/libtiffpic";
|
||||
|
||||
// `find test/libtiffpic -type f | sort`
|
||||
//
|
||||
// Each entry maps a file path to its expected behavior:
|
||||
// None = not a TIFF file, skip entirely
|
||||
// Some(0) = TIFF with unsupported features, decode-only smoke test
|
||||
// Some(nonzero) = TIFF with full CRC32 hash verification of decoded output
|
||||
const FILES: &[(&str, Option<u32>)] = &[
|
||||
("tests/libtiffpic/caspian.tif", Some(0x4ea8d4e4)),
|
||||
("tests/libtiffpic/cramps.tif", Some(0xdfa2fba4)),
|
||||
("tests/libtiffpic/cramps-tile.tif", Some(0xdfa2fba4)),
|
||||
("tests/libtiffpic/CVS/Entries", None),
|
||||
("tests/libtiffpic/CVS/Repository", None),
|
||||
("tests/libtiffpic/CVS/Root", None),
|
||||
("tests/libtiffpic/depth/CVS/Entries", None),
|
||||
("tests/libtiffpic/depth/CVS/Repository", None),
|
||||
("tests/libtiffpic/depth/CVS/Root", None),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-minisblack-02.tif",
|
||||
Some(0x1c90f515),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-minisblack-04.tif",
|
||||
Some(0x0f4813cb),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-minisblack-06.tif",
|
||||
Some(0xf0c0a901),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-minisblack-08.tif",
|
||||
Some(0xe797b15b),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-minisblack-10.tif",
|
||||
Some(0xdee442ec),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-minisblack-12.tif",
|
||||
Some(0x4810c4e9),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-minisblack-14.tif",
|
||||
Some(0x7e03cf03),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-minisblack-16.tif",
|
||||
Some(0x8e0add32),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-minisblack-24.tif",
|
||||
Some(0x5cf29313),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-minisblack-32.tif",
|
||||
Some(0x80025eee),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-palette-02.tif",
|
||||
Some(0xaeb21cff),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-palette-04.tif",
|
||||
Some(0x339d428b),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-palette-08.tif",
|
||||
Some(0xb6b203e9),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-palette-16.tif",
|
||||
Some(0x4e02308f),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-contig-02.tif",
|
||||
Some(0x8f16cf35),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-contig-04.tif",
|
||||
Some(0x95e97f94),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-contig-08.tif",
|
||||
Some(0x98134e7c),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-contig-10.tif",
|
||||
Some(0xa2ad8f4d),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-contig-12.tif",
|
||||
Some(0xfad187fc),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-contig-14.tif",
|
||||
Some(0x93fdc170),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-contig-16.tif",
|
||||
Some(0x25e1066b),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-contig-24.tif",
|
||||
Some(0x25cdf743),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-contig-32.tif",
|
||||
Some(0x195dde9c),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-planar-02.tif",
|
||||
Some(0xcc0281ca),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-planar-04.tif",
|
||||
Some(0x867134b2),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-planar-08.tif",
|
||||
Some(0x2341af83),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-planar-10.tif",
|
||||
Some(0x79211047),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-planar-12.tif",
|
||||
Some(0x14a0ff72),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-planar-14.tif",
|
||||
Some(0xc077389d),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-planar-16.tif",
|
||||
Some(0xd0849282),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-planar-24.tif",
|
||||
Some(0x433d333b),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-rgb-planar-32.tif",
|
||||
Some(0xd0780b23),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-separated-contig-08.tif",
|
||||
Some(0x97fb03a6),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-separated-contig-16.tif",
|
||||
Some(0xb6ee0135),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-separated-planar-08.tif",
|
||||
Some(0x502e93fa),
|
||||
),
|
||||
(
|
||||
"tests/libtiffpic/depth/flower-separated-planar-16.tif",
|
||||
Some(0x50e580d9),
|
||||
),
|
||||
("tests/libtiffpic/depth/genimages", None),
|
||||
("tests/libtiffpic/depth/README.txt", None),
|
||||
("tests/libtiffpic/depth/summary.txt", None),
|
||||
("tests/libtiffpic/dscf0013.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
||||
("tests/libtiffpic/fax2d.tif", Some(0xc2f30c6f)),
|
||||
// No support for direct CCITT Group 3 1D format
|
||||
("tests/libtiffpic/g3test.g3", None),
|
||||
("tests/libtiffpic/g3test.tif", Some(0x304b606b)),
|
||||
("tests/libtiffpic/jello.tif", Some(0xb03dbda9)),
|
||||
("tests/libtiffpic/jim___ah.tif", Some(0x81a27b7f)),
|
||||
("tests/libtiffpic/jim___cg.tif", Some(0xfd31747c)),
|
||||
("tests/libtiffpic/jim___dg.tif", Some(0x8c6a5e8f)),
|
||||
("tests/libtiffpic/jim___gg.tif", Some(0x8c6a5e8f)),
|
||||
("tests/libtiffpic/ladoga.tif", Some(0x9b118511)),
|
||||
("tests/libtiffpic/off_l16.tif", Some(0)), // LogLuv decoder output varies across platforms
|
||||
("tests/libtiffpic/off_luv24.tif", Some(0x2bee71a2)),
|
||||
("tests/libtiffpic/off_luv32.tif", Some(0)), // LogLuv decoder output varies across platforms
|
||||
("tests/libtiffpic/oxford.tif", Some(0x355d2e3e)),
|
||||
("tests/libtiffpic/pc260001.tif", Some(0xdb56b753)),
|
||||
("tests/libtiffpic/quad-jpeg.tif", Some(0xcba22475)),
|
||||
("tests/libtiffpic/quad-lzw.tif", Some(0x6055f9ee)), // old-style (LSB) LZW
|
||||
("tests/libtiffpic/quad-tile.tif", Some(0x6055f9ee)),
|
||||
("tests/libtiffpic/README", None),
|
||||
("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
||||
("tests/libtiffpic/strike.tif", Some(0x016b0f6a)),
|
||||
("tests/libtiffpic/text.tif", Some(0)), // unsupported compression
|
||||
("tests/libtiffpic/ycbcr-cat.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
||||
("tests/libtiffpic/zackthecat.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
||||
];
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.iter().any(|a| a == "--print-hashes") {
|
||||
print_reference_hashes();
|
||||
return;
|
||||
}
|
||||
|
||||
let mut candidates: HashMap<_, _> = FILES.iter().copied().collect();
|
||||
let mut trials = Vec::new();
|
||||
|
||||
for entry in WalkDir::new(REFERENCE_DIR) {
|
||||
let entry = entry.unwrap();
|
||||
|
||||
if !entry.file_type().is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(path) = entry.path().to_str() else {
|
||||
continue;
|
||||
};
|
||||
let path = path.replace(std::path::MAIN_SEPARATOR, "/"); // always use UNIX path separators
|
||||
|
||||
// Mark this path as consumed in either case by removing it.
|
||||
let Some(expected_hash) = candidates.remove(path.as_str()) else {
|
||||
panic!("File {} not in reference list", path);
|
||||
};
|
||||
|
||||
// None = non-image file, skip
|
||||
let Some(expected_hash) = expected_hash else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let path = path.to_string();
|
||||
trials.push(Trial::test(
|
||||
path.to_string(),
|
||||
move || -> Result<(), Failed> { decode_and_verify(&path, expected_hash) },
|
||||
));
|
||||
}
|
||||
|
||||
if !candidates.is_empty() {
|
||||
for (path, _) in candidates {
|
||||
eprintln!("Reference file {} not found in directory walk", path);
|
||||
}
|
||||
|
||||
panic!("Some reference files were not found");
|
||||
}
|
||||
|
||||
let args = Arguments::from_args();
|
||||
libtest_mimic::run(&args, trials).exit();
|
||||
}
|
||||
|
||||
/// Map DecodingResult variant to a TIFF Type for byte-order normalization.
|
||||
fn sample_type_of(result: &DecodingSampleBuffer) -> Type {
|
||||
match result {
|
||||
DecodingSampleBuffer::U8(_) => Type::BYTE,
|
||||
DecodingSampleBuffer::I8(_) => Type::SBYTE,
|
||||
DecodingSampleBuffer::U16(_) => Type::SHORT,
|
||||
DecodingSampleBuffer::I16(_) => Type::SSHORT,
|
||||
DecodingSampleBuffer::U32(_) => Type::LONG,
|
||||
DecodingSampleBuffer::I32(_) => Type::SLONG,
|
||||
DecodingSampleBuffer::F32(_) => Type::FLOAT,
|
||||
DecodingSampleBuffer::U64(_) => Type::LONG8,
|
||||
DecodingSampleBuffer::I64(_) => Type::SLONG8,
|
||||
DecodingSampleBuffer::F64(_) => Type::DOUBLE,
|
||||
DecodingSampleBuffer::F16(_) => Type::SHORT, // f16 is 2 bytes like SHORT
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash color type into the hasher (discriminant + fields).
|
||||
fn hash_color_type(hasher: &mut crc32fast::Hasher, color_type: ColorType) {
|
||||
match color_type {
|
||||
ColorType::Gray(n) => hasher.update(&[0, n]),
|
||||
ColorType::RGB(n) => hasher.update(&[1, n]),
|
||||
ColorType::Palette(n) => hasher.update(&[2, n]),
|
||||
ColorType::GrayA(n) => hasher.update(&[3, n]),
|
||||
ColorType::RGBA(n) => hasher.update(&[4, n]),
|
||||
ColorType::CMYK(n) => hasher.update(&[5, n]),
|
||||
ColorType::CMYKA(n) => hasher.update(&[6, n]),
|
||||
ColorType::YCbCr(n) => hasher.update(&[7, n]),
|
||||
ColorType::Lab(n) => hasher.update(&[8, n]),
|
||||
ColorType::Multiband {
|
||||
bit_depth,
|
||||
num_samples,
|
||||
} => {
|
||||
hasher.update(&[9, bit_depth]);
|
||||
hasher.update(&num_samples.to_le_bytes());
|
||||
}
|
||||
other => panic!("compute_tiff_hash: unhandled ColorType {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute a CRC32 hash of the full decoded TIFF output using read_image_to_buffer.
|
||||
///
|
||||
/// Hashes per page: page index, dimensions, color type, TIFF sample type tag,
|
||||
/// and decoded sample data normalized to little-endian byte order.
|
||||
fn compute_hash(path: &std::path::Path) -> Result<u32, tiff::TiffError> {
|
||||
let img_file = File::open(path)?;
|
||||
let mut decoder = Decoder::open(img_file)?;
|
||||
let mut hasher = crc32fast::Hasher::new();
|
||||
let mut buffer = DecodingSampleBuffer::U8(vec![]);
|
||||
|
||||
let mut page = 0u32;
|
||||
while decoder.more_images() {
|
||||
decoder.next_image()?;
|
||||
let (width, height) = decoder.dimensions()?;
|
||||
let color_type = decoder.colortype()?;
|
||||
let _layout = decoder.read_image_to_buffer(&mut buffer)?;
|
||||
|
||||
hasher.update(&page.to_le_bytes());
|
||||
hasher.update(&width.to_le_bytes());
|
||||
hasher.update(&height.to_le_bytes());
|
||||
hash_color_type(&mut hasher, color_type);
|
||||
|
||||
let sample_type = sample_type_of(&buffer);
|
||||
hasher.update(&sample_type.to_u16().to_le_bytes());
|
||||
|
||||
let mut buf = buffer.as_buffer(0);
|
||||
let bytes = buf.as_bytes_mut();
|
||||
ByteOrder::native().convert(sample_type, bytes, ByteOrder::LittleEndian);
|
||||
hasher.update(bytes);
|
||||
|
||||
page += 1;
|
||||
}
|
||||
|
||||
hasher.update(&page.to_le_bytes());
|
||||
Ok(hasher.finalize())
|
||||
}
|
||||
|
||||
fn decode_and_verify(path: &str, expected_hash: u32) -> Result<(), Failed> {
|
||||
let full_path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path);
|
||||
|
||||
if expected_hash == 0 {
|
||||
// Smoke test: try to decode, tolerate errors (unsupported formats)
|
||||
let _ = compute_hash(&full_path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let actual_hash = compute_hash(&full_path)
|
||||
.map_err(|e| Failed::from(format!("Decoding failed for {path}: {e}")))?;
|
||||
|
||||
if actual_hash != expected_hash {
|
||||
return Err(Failed::from(format!(
|
||||
"{path}: expected hash 0x{expected_hash:08x} but got 0x{actual_hash:08x}"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Print reference hashes for all decodable files (use --print-hashes).
|
||||
fn print_reference_hashes() {
|
||||
for &(path, expected) in FILES {
|
||||
if expected.is_none() {
|
||||
continue;
|
||||
}
|
||||
let full_path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path);
|
||||
match compute_hash(&full_path) {
|
||||
Ok(hash) => eprintln!("(\"{path}\", Some(0x{hash:08x})),"),
|
||||
Err(e) => eprintln!("// {path}: FAILED: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
-3
@@ -1,4 +1,4 @@
|
||||
use tiff::decoder::{Decoder, TiffCodingUnit};
|
||||
use tiff::decoder::{Decoder, DecodingSampleBuffer, Limits, TiffCodingUnit};
|
||||
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
@@ -10,8 +10,9 @@ fn read_all_planes() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
|
||||
let file = File::open(path).unwrap();
|
||||
let io = std::io::BufReader::new(file);
|
||||
let mut tif = Decoder::open(io).unwrap();
|
||||
|
||||
let mut tif = Decoder::new(io).unwrap();
|
||||
tif.next_image().unwrap();
|
||||
let layout = tif.image_buffer_layout().unwrap();
|
||||
assert_eq!(layout.planes, 3);
|
||||
let plane_stride = layout.plane_stride.map_or(0, |x| x.get());
|
||||
@@ -36,8 +37,9 @@ fn read_first_coding_unit() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
|
||||
let file = File::open(path).unwrap();
|
||||
let io = std::io::BufReader::new(file);
|
||||
let mut tif = Decoder::open(io).unwrap();
|
||||
|
||||
let mut tif = Decoder::new(io).unwrap();
|
||||
tif.next_image().unwrap();
|
||||
let layout = tif.image_coding_unit_layout(TiffCodingUnit(0)).unwrap();
|
||||
|
||||
assert_eq!(layout.planes, 3);
|
||||
@@ -74,3 +76,51 @@ fn read_first_coding_unit() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = DecodingSampleBuffer::U8(vec![]);
|
||||
|
||||
// Make room for 1 plane.
|
||||
let mut tif = Decoder::open(io).unwrap();
|
||||
tif = tif.with_limits({
|
||||
limits.decoding_buffer_size = 374 * 499;
|
||||
limits.clone()
|
||||
});
|
||||
|
||||
tif.next_image().unwrap();
|
||||
tif.read_image_to_buffer(&mut buffer).unwrap();
|
||||
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499);
|
||||
|
||||
// Make room for 2 planes.
|
||||
tif = tif.with_limits({
|
||||
limits.decoding_buffer_size = 374 * 499 * 2;
|
||||
limits.clone()
|
||||
});
|
||||
|
||||
tif.read_image_to_buffer(&mut buffer).unwrap();
|
||||
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499);
|
||||
|
||||
// Make room for 3 planes.
|
||||
tif = tif.with_limits({
|
||||
limits.decoding_buffer_size = 374 * 499 * 3;
|
||||
limits.clone()
|
||||
});
|
||||
|
||||
tif.read_image_to_buffer(&mut buffer).unwrap();
|
||||
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499 * 3);
|
||||
|
||||
// Make room for 4 planes?
|
||||
tif = tif.with_limits({
|
||||
limits.decoding_buffer_size = 374 * 499 * 4;
|
||||
limits.clone()
|
||||
});
|
||||
|
||||
tif.read_image_to_buffer(&mut buffer).unwrap();
|
||||
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499 * 3);
|
||||
}
|
||||
+216
-35
@@ -1,7 +1,7 @@
|
||||
extern crate tiff;
|
||||
|
||||
use tiff::decoder::{ifd, Decoder, DecodingResult};
|
||||
use tiff::encoder::{colortype, Ifd, Ifd8, SRational, TiffEncoder};
|
||||
use tiff::decoder::{ifd, Decoder, DecodingSampleBuffer};
|
||||
use tiff::encoder::{colortype, Ifd, Ifd8, Predictor, SRational, TiffEncoder};
|
||||
use tiff::tags::Tag;
|
||||
use tiff::ColorType;
|
||||
|
||||
@@ -33,11 +33,14 @@ fn encode_decode() {
|
||||
}
|
||||
{
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut decoder = Decoder::new(&mut file).unwrap();
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
||||
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
||||
|
||||
let mut all_tags = decoder
|
||||
.current_ifd()
|
||||
.tag_iter()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<_>>();
|
||||
@@ -78,13 +81,13 @@ fn encode_decode() {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
decoder.get_tag(Tag::Artist).unwrap(),
|
||||
decoder.current_ifd().get_tag(Tag::Artist).unwrap(),
|
||||
ifd::Value::Ascii("Image-tiff".into())
|
||||
);
|
||||
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
let mut buffer = tiff::decoder::DecodingSampleBuffer::U8(vec![]);
|
||||
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let tiff::decoder::DecodingResult::U8(img_res) = buffer {
|
||||
if let tiff::decoder::DecodingSampleBuffer::U8(img_res) = buffer {
|
||||
assert_eq!(image_data, img_res);
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
@@ -116,11 +119,14 @@ fn encode_decode_big() {
|
||||
}
|
||||
{
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut decoder = Decoder::new(&mut file).unwrap();
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
||||
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
||||
|
||||
let mut all_tags = decoder
|
||||
.current_ifd()
|
||||
.tag_iter()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<_>>();
|
||||
@@ -161,12 +167,12 @@ fn encode_decode_big() {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
decoder.get_tag(Tag::Artist).unwrap(),
|
||||
decoder.current_ifd().get_tag(Tag::Artist).unwrap(),
|
||||
ifd::Value::Ascii("Image-tiff".into())
|
||||
);
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
let mut buffer = tiff::decoder::DecodingSampleBuffer::U8(vec![]);
|
||||
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let DecodingResult::U8(img_res) = buffer {
|
||||
if let DecodingSampleBuffer::U8(img_res) = buffer {
|
||||
assert_eq!(image_data, img_res);
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
@@ -207,8 +213,10 @@ fn test_encode_ifd() {
|
||||
|
||||
// Rewind the cursor for reading
|
||||
data.set_position(0);
|
||||
|
||||
{
|
||||
let mut decoder = Decoder::new(&mut data).unwrap();
|
||||
let mut decoder = Decoder::open(&mut data).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.assert_tag_u32(65000), 42);
|
||||
assert_eq!(decoder.assert_tag_u32_vec(65000), [42]);
|
||||
@@ -247,14 +255,16 @@ macro_rules! test_roundtrip {
|
||||
) {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
||||
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
let mut buffer = tiff::decoder::DecodingSampleBuffer::U8(vec![]);
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
|
||||
let image_data = match buffer {
|
||||
DecodingResult::$buffer(res) => res,
|
||||
DecodingSampleBuffer::$buffer(res) => res,
|
||||
_ => panic!("Wrong data type"),
|
||||
};
|
||||
|
||||
@@ -267,10 +277,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();
|
||||
let mut buffer = tiff::decoder::DecodingSampleBuffer::U8(vec![]);
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let DecodingResult::$buffer(img_res) = buffer {
|
||||
if let DecodingSampleBuffer::$buffer(img_res) = buffer {
|
||||
assert_eq!(*image_data, img_res);
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
@@ -379,6 +390,144 @@ fn test_ycbcr_u8_roundtrip() {
|
||||
test_u8_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
|
||||
}
|
||||
|
||||
/// Test that encoding with the floating-point predictor works correctly
|
||||
/// and can be decoded back to the original data.
|
||||
#[test]
|
||||
fn test_gray_f32_floating_point_predictor() {
|
||||
// Generate test data: a gradient of f32 values
|
||||
let width = 32u32;
|
||||
let height = 32u32;
|
||||
let mut image_data: Vec<f32> = Vec::with_capacity((width * height) as usize);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
// Create a gradient pattern
|
||||
image_data.push((x as f32 + y as f32 * 0.1) / 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
let mut file = Cursor::new(Vec::new());
|
||||
{
|
||||
let mut tiff = TiffEncoder::new(&mut file)
|
||||
.unwrap()
|
||||
.with_predictor(Predictor::FloatingPoint);
|
||||
tiff.write_image::<colortype::Gray32Float>(width, height, &image_data)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
{
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(32));
|
||||
|
||||
let mut buffer = DecodingSampleBuffer::F32(vec![]);
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let DecodingSampleBuffer::F32(img_res) = buffer {
|
||||
assert_eq!(image_data.len(), img_res.len());
|
||||
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
|
||||
assert_eq!(
|
||||
*orig, *decoded,
|
||||
"Mismatch at value: orig={}, decoded={}",
|
||||
orig, decoded
|
||||
);
|
||||
}
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test floating-point predictor with RGB f32 data
|
||||
#[test]
|
||||
fn test_rgb_f32_floating_point_predictor() {
|
||||
let width = 16u32;
|
||||
let height = 16u32;
|
||||
let mut image_data: Vec<f32> = Vec::with_capacity((width * height * 3) as usize);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
// RGB pattern
|
||||
image_data.push(x as f32 / width as f32); // R
|
||||
image_data.push(y as f32 / height as f32); // G
|
||||
image_data.push((x + y) as f32 / (width + height) as f32); // B
|
||||
}
|
||||
}
|
||||
|
||||
let mut file = Cursor::new(Vec::new());
|
||||
{
|
||||
let mut tiff = TiffEncoder::new(&mut file)
|
||||
.unwrap()
|
||||
.with_predictor(Predictor::FloatingPoint);
|
||||
tiff.write_image::<colortype::RGB32Float>(width, height, &image_data)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
{
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(32));
|
||||
|
||||
let mut buffer = DecodingSampleBuffer::F32(vec![]);
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let DecodingSampleBuffer::F32(img_res) = buffer {
|
||||
assert_eq!(image_data.len(), img_res.len());
|
||||
for (i, (orig, decoded)) in image_data.iter().zip(img_res.iter()).enumerate() {
|
||||
assert_eq!(
|
||||
*orig, *decoded,
|
||||
"Mismatch at index {}: orig={}, decoded={}",
|
||||
i, orig, decoded
|
||||
);
|
||||
}
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test floating-point predictor with f64 data
|
||||
#[test]
|
||||
fn test_gray_f64_floating_point_predictor() {
|
||||
let width = 16u32;
|
||||
let height = 16u32;
|
||||
let mut image_data: Vec<f64> = Vec::with_capacity((width * height) as usize);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
image_data.push((x as f64 + y as f64 * 0.1) / 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
let mut file = Cursor::new(Vec::new());
|
||||
{
|
||||
let mut tiff = TiffEncoder::new(&mut file)
|
||||
.unwrap()
|
||||
.with_predictor(Predictor::FloatingPoint);
|
||||
tiff.write_image::<colortype::Gray64Float>(width, height, &image_data)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
{
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(64));
|
||||
|
||||
let mut buffer = DecodingSampleBuffer::F64(vec![]);
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let DecodingSampleBuffer::F64(img_res) = buffer {
|
||||
assert_eq!(image_data.len(), img_res.len());
|
||||
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
|
||||
assert_eq!(
|
||||
*orig, *decoded,
|
||||
"Mismatch: orig={}, decoded={}",
|
||||
orig, decoded
|
||||
);
|
||||
}
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait AssertDecode {
|
||||
fn assert_tag_u32(&mut self, tag: u16) -> u32;
|
||||
fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32>;
|
||||
@@ -392,37 +541,57 @@ trait AssertDecode {
|
||||
|
||||
impl<R: std::io::Read + std::io::Seek> AssertDecode for Decoder<R> {
|
||||
fn assert_tag_u32(&mut self, tag: u16) -> u32 {
|
||||
self.get_tag(Tag::Unknown(tag)).unwrap().into_u32().unwrap()
|
||||
self.current_ifd()
|
||||
.get_tag(Tag::Unknown(tag))
|
||||
.unwrap()
|
||||
.into_u32()
|
||||
.unwrap()
|
||||
}
|
||||
fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32> {
|
||||
self.get_tag(Tag::Unknown(tag))
|
||||
self.current_ifd()
|
||||
.get_tag(Tag::Unknown(tag))
|
||||
.unwrap()
|
||||
.into_u32_vec()
|
||||
.unwrap()
|
||||
}
|
||||
fn assert_tag_i32(&mut self, tag: u16) -> i32 {
|
||||
self.get_tag(Tag::Unknown(tag)).unwrap().into_i32().unwrap()
|
||||
self.current_ifd()
|
||||
.get_tag(Tag::Unknown(tag))
|
||||
.unwrap()
|
||||
.into_i32()
|
||||
.unwrap()
|
||||
}
|
||||
fn assert_tag_i32_vec(&mut self, tag: u16) -> Vec<i32> {
|
||||
self.get_tag(Tag::Unknown(tag))
|
||||
self.current_ifd()
|
||||
.get_tag(Tag::Unknown(tag))
|
||||
.unwrap()
|
||||
.into_i32_vec()
|
||||
.unwrap()
|
||||
}
|
||||
fn assert_tag_u64(&mut self, tag: u16) -> u64 {
|
||||
self.get_tag(Tag::Unknown(tag)).unwrap().into_u64().unwrap()
|
||||
self.current_ifd()
|
||||
.get_tag(Tag::Unknown(tag))
|
||||
.unwrap()
|
||||
.into_u64()
|
||||
.unwrap()
|
||||
}
|
||||
fn assert_tag_u64_vec(&mut self, tag: u16) -> Vec<u64> {
|
||||
self.get_tag(Tag::Unknown(tag))
|
||||
self.current_ifd()
|
||||
.get_tag(Tag::Unknown(tag))
|
||||
.unwrap()
|
||||
.into_u64_vec()
|
||||
.unwrap()
|
||||
}
|
||||
fn assert_tag_i64(&mut self, tag: u16) -> i64 {
|
||||
self.get_tag(Tag::Unknown(tag)).unwrap().into_i64().unwrap()
|
||||
self.current_ifd()
|
||||
.get_tag(Tag::Unknown(tag))
|
||||
.unwrap()
|
||||
.into_i64()
|
||||
.unwrap()
|
||||
}
|
||||
fn assert_tag_i64_vec(&mut self, tag: u16) -> Vec<i64> {
|
||||
self.get_tag(Tag::Unknown(tag))
|
||||
self.current_ifd()
|
||||
.get_tag(Tag::Unknown(tag))
|
||||
.unwrap()
|
||||
.into_i64_vec()
|
||||
.unwrap()
|
||||
@@ -456,7 +625,8 @@ fn test_multiple_byte() {
|
||||
|
||||
data.set_position(0);
|
||||
{
|
||||
let mut decoder = Decoder::new(&mut data).unwrap();
|
||||
let mut decoder = Decoder::open(&mut data).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.assert_tag_u32_vec(65000), [1]);
|
||||
assert_eq!(decoder.assert_tag_u32_vec(65001), [1, 2]);
|
||||
@@ -537,7 +707,8 @@ fn test_signed() {
|
||||
//Rewind the cursor for reading
|
||||
data.set_position(0);
|
||||
{
|
||||
let mut decoder = Decoder::new(&mut data).unwrap();
|
||||
let mut decoder = Decoder::open(&mut data).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
assert_eq!(decoder.assert_tag_i32(65000), -1);
|
||||
assert_eq!(decoder.assert_tag_i32_vec(65001), [-1]);
|
||||
@@ -589,13 +760,14 @@ fn test_multipage_image() {
|
||||
img_file.seek(SeekFrom::Start(0)).unwrap();
|
||||
|
||||
{
|
||||
let mut img_decoder = Decoder::new(&mut img_file).unwrap();
|
||||
let mut decoder = Decoder::open(&mut img_file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
// check the dimensions of the image in the first page
|
||||
assert_eq!(img_decoder.dimensions().unwrap(), (2, 2));
|
||||
img_decoder.next_image().unwrap();
|
||||
assert_eq!(decoder.dimensions().unwrap(), (2, 2));
|
||||
decoder.next_image().unwrap();
|
||||
// check the dimensions of the image in the second page
|
||||
assert_eq!(img_decoder.dimensions().unwrap(), (3, 3));
|
||||
assert_eq!(decoder.dimensions().unwrap(), (3, 3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,14 +796,21 @@ fn test_rows_per_strip() {
|
||||
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
{
|
||||
let mut decoder = Decoder::new(&mut file).unwrap();
|
||||
assert_eq!(decoder.get_tag_u64(Tag::RowsPerStrip).unwrap(), 2);
|
||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
assert_eq!(
|
||||
decoder
|
||||
.current_ifd()
|
||||
.get_tag_u64(Tag::RowsPerStrip)
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
assert_eq!(decoder.strip_count().unwrap(), 50);
|
||||
|
||||
for i in 0..50 {
|
||||
let img2 = [i; 2 * 100];
|
||||
match decoder.read_chunk(i as u32).unwrap() {
|
||||
DecodingResult::U8(data) => assert_eq!(&img2[..], &data[..]),
|
||||
DecodingSampleBuffer::U8(data) => assert_eq!(&img2[..], &data[..]),
|
||||
other => panic!("Incorrect strip type {other:?}"),
|
||||
}
|
||||
}
|
||||
@@ -664,9 +843,10 @@ fn test_auxiliary_directory() {
|
||||
}
|
||||
|
||||
data.set_position(0);
|
||||
let mut decoder = Decoder::new(&mut data).unwrap();
|
||||
let mut decoder = Decoder::open(&mut data).unwrap();
|
||||
|
||||
{
|
||||
decoder.next_image().unwrap();
|
||||
assert_eq!(decoder.dimensions().unwrap(), (1, 1));
|
||||
let _ = decoder.read_image().unwrap();
|
||||
}
|
||||
@@ -677,6 +857,7 @@ fn test_auxiliary_directory() {
|
||||
let _ = decoder.read_image().unwrap();
|
||||
|
||||
let exif_location = decoder
|
||||
.current_ifd()
|
||||
.get_tag(Tag::ExifDirectory)
|
||||
.expect("second directory missing Exif")
|
||||
.into_ifd_pointer()
|
||||
|
||||
@@ -2,10 +2,9 @@ extern crate tiff;
|
||||
|
||||
use std::io::{Cursor, Seek, Write};
|
||||
use tiff::{
|
||||
decoder::{Decoder, DecodingResult},
|
||||
decoder::{Decoder, DecodingSampleBuffer},
|
||||
encoder::{
|
||||
colortype::{self, ColorType},
|
||||
compression::*,
|
||||
Compression, TiffEncoder, TiffValue,
|
||||
},
|
||||
};
|
||||
@@ -110,12 +109,13 @@ fn encode_decode_with_compression(compression: Compression) {
|
||||
// Decode tiff
|
||||
data.set_position(0);
|
||||
{
|
||||
let mut decoder = Decoder::new(data).unwrap();
|
||||
let mut decoder = Decoder::open(data).unwrap();
|
||||
decoder.next_image().unwrap();
|
||||
|
||||
// Check the RGB image
|
||||
assert_eq!(
|
||||
match decoder.read_image() {
|
||||
Ok(DecodingResult::U16(image_data)) => image_data,
|
||||
Ok(DecodingSampleBuffer::U16(image_data)) => image_data,
|
||||
unexpected => panic!("Descoding RGB failed: {unexpected:?}"),
|
||||
},
|
||||
image_rgb.reference_data()
|
||||
@@ -125,7 +125,7 @@ fn encode_decode_with_compression(compression: Compression) {
|
||||
decoder.next_image().unwrap();
|
||||
assert_eq!(
|
||||
match decoder.read_image() {
|
||||
Ok(DecodingResult::U8(image_data)) => image_data,
|
||||
Ok(DecodingSampleBuffer::U8(image_data)) => image_data,
|
||||
unexpected => panic!("Decoding grayscale failed: {unexpected:?}"),
|
||||
},
|
||||
image_grayscale.reference_data()
|
||||
@@ -147,9 +147,9 @@ fn encode_decode_with_lzw() {
|
||||
#[test]
|
||||
#[cfg(feature = "deflate")]
|
||||
fn encode_decode_with_deflate() {
|
||||
encode_decode_with_compression(Compression::Deflate(DeflateLevel::Fast));
|
||||
encode_decode_with_compression(Compression::Deflate(DeflateLevel::Balanced));
|
||||
encode_decode_with_compression(Compression::Deflate(DeflateLevel::Best));
|
||||
encode_decode_with_compression(Compression::Deflate(1));
|
||||
encode_decode_with_compression(Compression::Deflate(6));
|
||||
encode_decode_with_compression(Compression::Deflate(9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
-1
@@ -13,7 +13,8 @@ fn test_directory<F: Fn(File) -> bool>(path: &str, f: F) {
|
||||
}
|
||||
|
||||
fn decode_tiff(file: File) -> TiffResult<()> {
|
||||
let mut decoder = Decoder::new(file)?;
|
||||
let mut decoder = Decoder::open(file)?;
|
||||
decoder.next_image()?;
|
||||
decoder.read_image()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 214 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user