Compare commits
93
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1426ca802 | ||
|
|
66e6567acc | ||
|
|
5947d3b72e | ||
|
|
0fdf1e0502 | ||
|
|
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 | ||
|
|
595e67e5fb | ||
|
|
812bc970ba | ||
|
|
82b646298d | ||
|
|
22ff2b5c50 | ||
|
|
3a6492f251 | ||
|
|
a03eeccfe5 | ||
|
|
287fcb9309 | ||
|
|
ba97bad411 | ||
|
|
c4ab46ad2c | ||
|
|
eaf9cc1d6c | ||
|
|
34191fa9ca | ||
|
|
b0bd33dbe4 | ||
|
|
2ca2427c75 | ||
|
|
ea949032a2 | ||
|
|
87007d85a0 | ||
|
|
2119bff354 | ||
|
|
c62a69ec9d | ||
|
|
79a791b49a |
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
|
||||||
+32
-12
@@ -11,7 +11,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
rust: ["1.88.0", stable, beta, nightly]
|
rust: ["1.88.0", stable, beta, nightly]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@nightly
|
- uses: dtolnay/rust-toolchain@nightly
|
||||||
if: ${{ matrix.rust == '1.88.0' }}
|
if: ${{ matrix.rust == '1.88.0' }}
|
||||||
@@ -32,13 +32,29 @@ jobs:
|
|||||||
if: ${{ matrix.rust != '1.88.0' }}
|
if: ${{ matrix.rust != '1.88.0' }}
|
||||||
run: cargo test -v && cargo doc -v
|
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:
|
features:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
feature: ["", deflate, jpeg, lzw, zstd, default, "default,zstd"]
|
feature: ["", deflate, jpeg, lzw, zstd, default, "default,zstd"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v6
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
@@ -59,25 +75,29 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
arch: [powerpc-unknown-linux-gnu, i686-unknown-linux-gnu]
|
arch: [powerpc-unknown-linux-gnu, i686-unknown-linux-gnu]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Install or use cached cross-rs/cross
|
- uses: taiki-e/install-action@cross
|
||||||
uses: baptiste0928/cargo-install@v2
|
|
||||||
with:
|
|
||||||
crate: cross
|
|
||||||
- name: Cache Cargo Dependencies
|
- name: Cache Cargo Dependencies
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
cache-on-failure: true
|
cache-on-failure: true
|
||||||
key: ${{ matrix.arch }}
|
key: ${{ matrix.arch }}
|
||||||
- name: Start Docker (required for cross-rs)
|
|
||||||
run: sudo systemctl start docker
|
|
||||||
- name: Cross-Run Tests using QEMU
|
- name: Cross-Run Tests using QEMU
|
||||||
run: cross test --target ${{ matrix.arch }}
|
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:
|
rustfmt:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v6
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
@@ -87,8 +107,8 @@ jobs:
|
|||||||
clippy:
|
clippy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v6
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
components: clippy
|
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
|
||||||
@@ -2,3 +2,4 @@
|
|||||||
/target/
|
/target/
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
tests/benches/corpus/
|
||||||
+6
-1
@@ -23,7 +23,7 @@ quick-error = "2.0.1"
|
|||||||
# Rename to avoid confusion with the feature name.
|
# Rename to avoid confusion with the feature name.
|
||||||
fax34 = { package = "fax", version = "0.2.6", optional = true }
|
fax34 = { package = "fax", version = "0.2.6", optional = true }
|
||||||
flate2 = { version = "1.0.20", optional = true }
|
flate2 = { version = "1.0.20", optional = true }
|
||||||
weezl = { version = "0.1.10", optional = true }
|
weezl = { version = "0.2.1", optional = true }
|
||||||
zstd = { version = "0.13", optional = true }
|
zstd = { version = "0.13", optional = true }
|
||||||
zune-jpeg = { version = "0.5.1", optional = true }
|
zune-jpeg = { version = "0.5.1", optional = true }
|
||||||
image-webp = {version = "0.2.4", optional = true }
|
image-webp = {version = "0.2.4", optional = true }
|
||||||
@@ -54,6 +54,11 @@ harness = false
|
|||||||
name = "encode"
|
name = "encode"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "decode"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "decode_libtiffpic"
|
name = "decode_libtiffpic"
|
||||||
path = "tests/decode_libtiffpic.rs"
|
path = "tests/decode_libtiffpic.rs"
|
||||||
|
|||||||
@@ -55,4 +55,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
|
After installing it with `cargo install cargo-fuzz` on a nightly rustc, the
|
||||||
fuzzing harness can be run with recommended settings using
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -9,9 +9,9 @@ use tiff::decoder::Decoder;
|
|||||||
fn read_image(image: &[u8]) {
|
fn read_image(image: &[u8]) {
|
||||||
let image = std::io::Cursor::new(image);
|
let image = std::io::Cursor::new(image);
|
||||||
let decoder = Decoder::open(black_box(image));
|
let decoder = Decoder::open(black_box(image));
|
||||||
|
|
||||||
let mut reader = decoder.unwrap();
|
let mut reader = decoder.unwrap();
|
||||||
while reader.more_images() {
|
while reader.more_images() {
|
||||||
|
reader.next_directory().unwrap();
|
||||||
reader.read_image().unwrap();
|
reader.read_image().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-27
@@ -1,4 +1,4 @@
|
|||||||
use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingBuffer, DecodingResult};
|
use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingSampleBuffer, DecodingSampleSlice};
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let Some(image) = std::env::args_os().nth(1) else {
|
let Some(image) = std::env::args_os().nth(1) else {
|
||||||
@@ -10,20 +10,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let io = std::io::BufReader::new(file);
|
let io = std::io::BufReader::new(file);
|
||||||
let mut reader = Decoder::open(io)?;
|
let mut reader = Decoder::open(io)?;
|
||||||
|
|
||||||
let mut data = DecodingResult::I8(vec![]);
|
let mut data = DecodingSampleBuffer::I8(vec![]);
|
||||||
|
|
||||||
for i in 0u32.. {
|
for i in 0u32.. {
|
||||||
|
if !reader.more_images() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.next_directory()?;
|
||||||
|
|
||||||
let colortype = reader.colortype()?;
|
let colortype = reader.colortype()?;
|
||||||
let dimensions = reader.dimensions()?;
|
let dimensions = reader.dimensions()?;
|
||||||
let layout = reader.read_image_to_buffer(&mut data)?;
|
let layout = reader.read_image_to_buffer(&mut data)?;
|
||||||
|
|
||||||
debug_planes(i, &mut data, &layout, &colortype, dimensions);
|
debug_planes(i, &mut data, &layout, &colortype, dimensions);
|
||||||
|
|
||||||
if !reader.more_images() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.next_image()?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -31,32 +31,32 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
fn debug_planes(
|
fn debug_planes(
|
||||||
index: u32,
|
index: u32,
|
||||||
data: &mut DecodingResult,
|
data: &mut DecodingSampleBuffer,
|
||||||
layout: &BufferLayoutPreference,
|
layout: &BufferLayoutPreference,
|
||||||
colortype: &tiff::ColorType,
|
colortype: &tiff::ColorType,
|
||||||
(width, height): (u32, u32),
|
(width, height): (u32, u32),
|
||||||
) {
|
) {
|
||||||
match data {
|
match data {
|
||||||
DecodingResult::F16(v) => {
|
DecodingSampleBuffer::F16(v) => {
|
||||||
let samples = v
|
let samples = v
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16)
|
.map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
*data = DecodingResult::U16(samples);
|
*data = DecodingSampleBuffer::U16(samples);
|
||||||
}
|
}
|
||||||
DecodingResult::F32(v) => {
|
DecodingSampleBuffer::F32(v) => {
|
||||||
let samples = v
|
let samples = v
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16)
|
.map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
*data = DecodingResult::U16(samples);
|
*data = DecodingSampleBuffer::U16(samples);
|
||||||
}
|
}
|
||||||
DecodingResult::F64(v) => {
|
DecodingSampleBuffer::F64(v) => {
|
||||||
let samples = v
|
let samples = v
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&f| (f * f64::from(u16::MAX)) as u16)
|
.map(|&f| (f * f64::from(u16::MAX)) as u16)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
*data = DecodingResult::U16(samples);
|
*data = DecodingSampleBuffer::U16(samples);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -98,24 +98,24 @@ fn debug_planes(
|
|||||||
// them here (e.g. represent 1-bit as `{0, 1}`) but we'd also set `maxval` accordingly and
|
// 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
|
// this requires tupltype to be `BLACKANDWHITE` so introducing a bunch of complexity. If
|
||||||
// you want to do that in your real code, go ahead.
|
// you want to do that in your real code, go ahead.
|
||||||
DecodingBuffer::U8(buf) => match colortype.bit_depth() {
|
DecodingSampleSlice::U8(buf) => match colortype.bit_depth() {
|
||||||
8 => u64::from(u8::MAX),
|
8 => u64::from(u8::MAX),
|
||||||
4 => {
|
4 => {
|
||||||
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
||||||
expand_4bit(buf, swidth, &mut fallback_buffer);
|
expand_4bit(buf, swidth, &mut fallback_buffer);
|
||||||
*data = DecodingResult::U8(fallback_buffer);
|
*data = DecodingSampleBuffer::U8(fallback_buffer);
|
||||||
u64::from(u8::MAX)
|
u64::from(u8::MAX)
|
||||||
}
|
}
|
||||||
2 => {
|
2 => {
|
||||||
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
||||||
expand_2bit(buf, swidth, &mut fallback_buffer);
|
expand_2bit(buf, swidth, &mut fallback_buffer);
|
||||||
*data = DecodingResult::U8(fallback_buffer);
|
*data = DecodingSampleBuffer::U8(fallback_buffer);
|
||||||
u64::from(u8::MAX)
|
u64::from(u8::MAX)
|
||||||
}
|
}
|
||||||
1 => {
|
1 => {
|
||||||
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
||||||
expand_1bit(buf, swidth, &mut fallback_buffer);
|
expand_1bit(buf, swidth, &mut fallback_buffer);
|
||||||
*data = DecodingResult::U8(fallback_buffer);
|
*data = DecodingSampleBuffer::U8(fallback_buffer);
|
||||||
u64::from(u8::MAX)
|
u64::from(u8::MAX)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -126,28 +126,30 @@ fn debug_planes(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DecodingBuffer::U16(_) => u64::from(u16::MAX),
|
DecodingSampleSlice::U16(_) => u64::from(u16::MAX),
|
||||||
DecodingBuffer::U32(_) => u64::from(u32::MAX),
|
DecodingSampleSlice::U32(_) => u64::from(u32::MAX),
|
||||||
DecodingBuffer::U64(_) => u64::MAX,
|
DecodingSampleSlice::U64(_) => u64::MAX,
|
||||||
// Eh, this is a good effort I guess.
|
// Eh, this is a good effort I guess.
|
||||||
DecodingBuffer::I8(items) => {
|
DecodingSampleSlice::I8(items) => {
|
||||||
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
||||||
u64::from(i8::MAX as u8)
|
u64::from(i8::MAX as u8)
|
||||||
}
|
}
|
||||||
DecodingBuffer::I16(items) => {
|
DecodingSampleSlice::I16(items) => {
|
||||||
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
||||||
u64::from(i16::MAX as u16)
|
u64::from(i16::MAX as u16)
|
||||||
}
|
}
|
||||||
DecodingBuffer::I32(items) => {
|
DecodingSampleSlice::I32(items) => {
|
||||||
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
||||||
u64::from(i32::MAX as u32)
|
u64::from(i32::MAX as u32)
|
||||||
}
|
}
|
||||||
DecodingBuffer::I64(items) => {
|
DecodingSampleSlice::I64(items) => {
|
||||||
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
||||||
i64::MAX as u64
|
i64::MAX as u64
|
||||||
}
|
}
|
||||||
// Not supported by PAM.
|
// 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 {
|
let byte_len = if layout.planes > 1 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -19,7 +19,7 @@ path = ".."
|
|||||||
members = ["."]
|
members = ["."]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "decode_image"
|
name = "decode_comprehensive"
|
||||||
path = "fuzz_targets/decode_image.rs"
|
path = "fuzz_targets/decode_comprehensive.rs"
|
||||||
test = false
|
test = false
|
||||||
doc = 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.
|
/// 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.
|
/// 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 {
|
pub struct IfdCycles {
|
||||||
/// The root of each component union.
|
/// The root of each component union.
|
||||||
component_union: HashMap<ComponentId, ComponentId>,
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
-18
@@ -393,7 +393,7 @@ impl Entry {
|
|||||||
self.count
|
self.count
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn offset(&self) -> &[u8] {
|
pub(crate) fn offset_raw(&self) -> &[u8; 8] {
|
||||||
&self.offset
|
&self.offset
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,19 +583,19 @@ impl Entry {
|
|||||||
|
|
||||||
match self.type_ {
|
match self.type_ {
|
||||||
Type::BYTE | Type::UNDEFINED => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(bytes.iter().copied().map(Byte))
|
v.extend(bytes.iter().copied().map(Byte))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::SBYTE => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(bytes.iter().copied().map(|v| SignedByte(v as i8)))
|
v.extend(bytes.iter().copied().map(|v| SignedByte(v as i8)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::SHORT => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -605,7 +605,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::SSHORT => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -615,7 +615,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::LONG => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -625,7 +625,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::SLONG => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -635,7 +635,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::FLOAT => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -645,7 +645,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::DOUBLE => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -655,7 +655,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::RATIONAL => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(bytes.chunks_exact(8).map(|ch| {
|
v.extend(bytes.chunks_exact(8).map(|ch| {
|
||||||
Rational(
|
Rational(
|
||||||
@@ -666,7 +666,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::SRATIONAL => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(bytes.chunks_exact(8).map(|ch| {
|
v.extend(bytes.chunks_exact(8).map(|ch| {
|
||||||
SRational(
|
SRational(
|
||||||
@@ -677,7 +677,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::LONG8 => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -687,7 +687,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::SLONG8 => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -697,7 +697,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::IFD => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -707,7 +707,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::IFD8 => {
|
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| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -719,8 +719,8 @@ impl Entry {
|
|||||||
Type::ASCII => {
|
Type::ASCII => {
|
||||||
let n = usize::try_from(self.count)?;
|
let n = usize::try_from(self.count)?;
|
||||||
|
|
||||||
if n > limits.decoding_buffer_size {
|
if n > limits.ifd_value_size {
|
||||||
return Err(dbg!(TiffError::LimitsExceeded));
|
return Err(TiffError::LimitsExceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut out = vec![0; n];
|
let mut out = vec![0; n];
|
||||||
@@ -846,10 +846,20 @@ impl Entry {
|
|||||||
|
|
||||||
fn vec_with_capacity(
|
fn vec_with_capacity(
|
||||||
value_count: u64,
|
value_count: u64,
|
||||||
|
type_: Type,
|
||||||
limits: &super::Limits,
|
limits: &super::Limits,
|
||||||
) -> Result<Vec<Value>, TiffError> {
|
) -> 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>() {
|
if value_count > limits.decoding_buffer_size / mem::size_of::<Value>() {
|
||||||
return Err(TiffError::LimitsExceeded);
|
return Err(TiffError::LimitsExceeded);
|
||||||
}
|
}
|
||||||
|
|||||||
+289
-44
@@ -85,6 +85,11 @@ pub(crate) struct Image {
|
|||||||
pub chunk_offsets: Vec<u64>,
|
pub chunk_offsets: Vec<u64>,
|
||||||
pub chunk_bytes: Vec<u64>,
|
pub chunk_bytes: Vec<u64>,
|
||||||
pub chroma_subsampling: (u16, u16),
|
pub chroma_subsampling: (u16, u16),
|
||||||
|
/// FillOrder: 1 = MSB-to-LSB (default), 2 = LSB-to-MSB (common in fax images)
|
||||||
|
pub fill_order: u16,
|
||||||
|
/// The color map (palette) for `PhotometricInterpretation::RGBPalette`.
|
||||||
|
/// Contains `3 * 2^BitsPerSample` u16 entries: R values, then G values, then B values.
|
||||||
|
pub color_map: Option<Vec<u16>>,
|
||||||
/// Will decompression yield host-endian samples?
|
/// Will decompression yield host-endian samples?
|
||||||
///
|
///
|
||||||
/// Standard compression algorithms run on pure bytes, taking the sample slice as it would be
|
/// Standard compression algorithms run on pure bytes, taking the sample slice as it would be
|
||||||
@@ -132,6 +137,10 @@ pub(crate) struct ReadoutLayout {
|
|||||||
|
|
||||||
/// Bits per sample in the encoded data.
|
/// Bits per sample in the encoded data.
|
||||||
pub tiff_bits_per_sample: u8,
|
pub tiff_bits_per_sample: u8,
|
||||||
|
/// Bits per sample in the output buffer, promoted to a standard width.
|
||||||
|
/// For sub-byte depths (1-8): same as tiff_bits_per_sample.
|
||||||
|
/// For 9-16: 16. For 17-32: 32. For 33-64: 64.
|
||||||
|
pub output_bits_per_sample: u8,
|
||||||
/// Number of samples in the encoded data.
|
/// Number of samples in the encoded data.
|
||||||
pub tiff_samples: u16,
|
pub tiff_samples: u16,
|
||||||
/// Dimensions of the underlying rectangular chunks (tile or strips).
|
/// Dimensions of the underlying rectangular chunks (tile or strips).
|
||||||
@@ -146,30 +155,6 @@ pub(crate) struct ReadoutLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
pub(crate) fn no_image() -> Image {
|
|
||||||
Image {
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
bits_per_sample: 1,
|
|
||||||
samples: 1,
|
|
||||||
extra_samples: vec![],
|
|
||||||
photometric_samples: 1,
|
|
||||||
sample_format: SampleFormat::Uint,
|
|
||||||
photometric_interpretation: PhotometricInterpretation::BlackIsZero,
|
|
||||||
compression_method: CompressionMethod::None,
|
|
||||||
jpeg_tables: None,
|
|
||||||
predictor: Predictor::None,
|
|
||||||
chunk_type: ChunkType::Strip,
|
|
||||||
planar_config: PlanarConfiguration::Chunky,
|
|
||||||
strip_decoder: None,
|
|
||||||
tile_attributes: None,
|
|
||||||
chunk_offsets: Vec::new(),
|
|
||||||
chunk_bytes: Vec::new(),
|
|
||||||
chroma_subsampling: (2, 2),
|
|
||||||
decompression_to_host_endian: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_reader<R: Read + Seek>(
|
pub fn from_reader<R: Read + Seek>(
|
||||||
decoder: &mut ValueReader<R>,
|
decoder: &mut ValueReader<R>,
|
||||||
ifd: &Directory,
|
ifd: &Directory,
|
||||||
@@ -325,6 +310,12 @@ impl Image {
|
|||||||
(2, 2)
|
(2, 2)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let fill_order = tag_reader
|
||||||
|
.find_tag(Tag::FillOrder)?
|
||||||
|
.map(Value::into_u16)
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(1);
|
||||||
|
|
||||||
let planes = match planar_config {
|
let planes = match planar_config {
|
||||||
PlanarConfiguration::Chunky => 1,
|
PlanarConfiguration::Chunky => 1,
|
||||||
PlanarConfiguration::Planar => samples,
|
PlanarConfiguration::Planar => samples,
|
||||||
@@ -458,6 +449,32 @@ impl Image {
|
|||||||
bits_per_sample = 32;
|
bits_per_sample = 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read ColorMap for palette images. The tag contains 3 * 2^BitsPerSample u16 entries.
|
||||||
|
let color_map = if photometric_interpretation == PhotometricInterpretation::RGBPalette {
|
||||||
|
match tag_reader.find_tag(Tag::ColorMap)? {
|
||||||
|
Some(v) => {
|
||||||
|
let map = v.into_u16_vec()?;
|
||||||
|
let expected_len = 1usize
|
||||||
|
.checked_shl(bits_per_sample as u32)
|
||||||
|
.and_then(|n| n.checked_mul(3))
|
||||||
|
.ok_or(TiffError::LimitsExceeded)?;
|
||||||
|
if map.len() != expected_len {
|
||||||
|
return Err(TiffError::FormatError(
|
||||||
|
TiffFormatError::InconsistentSizesEncountered,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some(map)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(TiffError::FormatError(
|
||||||
|
TiffFormatError::RequiredTagNotFound(Tag::ColorMap),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Image {
|
Ok(Image {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -477,6 +494,8 @@ impl Image {
|
|||||||
chunk_offsets,
|
chunk_offsets,
|
||||||
chunk_bytes,
|
chunk_bytes,
|
||||||
chroma_subsampling,
|
chroma_subsampling,
|
||||||
|
fill_order,
|
||||||
|
color_map,
|
||||||
decompression_to_host_endian: matches!(
|
decompression_to_host_endian: matches!(
|
||||||
compression_method,
|
compression_method,
|
||||||
CompressionMethod::SgiLog | CompressionMethod::SgiLog24
|
CompressionMethod::SgiLog | CompressionMethod::SgiLog24
|
||||||
@@ -484,6 +503,17 @@ impl Image {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn check_chunk_type(&self, expected: ChunkType) -> TiffResult<()> {
|
||||||
|
if expected != self.chunk_type {
|
||||||
|
return Err(TiffError::UsageError(UsageError::InvalidChunkType(
|
||||||
|
expected,
|
||||||
|
self.chunk_type,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
|
pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
|
||||||
let is_alpha_extra_samples = matches!(
|
let is_alpha_extra_samples = matches!(
|
||||||
self.extra_samples.as_slice(),
|
self.extra_samples.as_slice(),
|
||||||
@@ -629,14 +659,13 @@ impl Image {
|
|||||||
vec![self.bits_per_sample; self.samples as usize],
|
vec![self.bits_per_sample; self.samples as usize],
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
PhotometricInterpretation::RGBPalette | PhotometricInterpretation::TransparencyMask => {
|
PhotometricInterpretation::RGBPalette => Ok(ColorType::Palette(self.bits_per_sample)),
|
||||||
Err(TiffError::UnsupportedError(
|
PhotometricInterpretation::TransparencyMask => Err(TiffError::UnsupportedError(
|
||||||
TiffUnsupportedError::InterpretationWithBits(
|
TiffUnsupportedError::InterpretationWithBits(
|
||||||
self.photometric_interpretation,
|
self.photometric_interpretation,
|
||||||
vec![self.bits_per_sample; self.samples as usize],
|
vec![self.bits_per_sample; self.samples as usize],
|
||||||
),
|
),
|
||||||
))
|
)),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,6 +677,7 @@ impl Image {
|
|||||||
#[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>,
|
#[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>,
|
||||||
dimensions: (u32, u32),
|
dimensions: (u32, u32),
|
||||||
samples: u16,
|
samples: u16,
|
||||||
|
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] fill_order: u16,
|
||||||
) -> TiffResult<Box<dyn Read + 'r>> {
|
) -> TiffResult<Box<dyn Read + 'r>> {
|
||||||
Ok(match compression_method {
|
Ok(match compression_method {
|
||||||
CompressionMethod::None => Box::new(reader),
|
CompressionMethod::None => Box::new(reader),
|
||||||
@@ -741,10 +771,18 @@ impl Image {
|
|||||||
Box::new(Cursor::new(data))
|
Box::new(Cursor::new(data))
|
||||||
}
|
}
|
||||||
#[cfg(feature = "fax")]
|
#[cfg(feature = "fax")]
|
||||||
|
CompressionMethod::Fax3 => Box::new(super::stream::Group3Reader::new(
|
||||||
|
dimensions,
|
||||||
|
reader,
|
||||||
|
compressed_length,
|
||||||
|
fill_order,
|
||||||
|
)?),
|
||||||
|
#[cfg(feature = "fax")]
|
||||||
CompressionMethod::Fax4 => Box::new(super::stream::Group4Reader::new(
|
CompressionMethod::Fax4 => Box::new(super::stream::Group4Reader::new(
|
||||||
dimensions,
|
dimensions,
|
||||||
reader,
|
reader,
|
||||||
compressed_length,
|
compressed_length,
|
||||||
|
fill_order,
|
||||||
)?),
|
)?),
|
||||||
#[cfg(feature = "webp")]
|
#[cfg(feature = "webp")]
|
||||||
CompressionMethod::WebP => Box::new(super::stream::WebPReader::new(
|
CompressionMethod::WebP => Box::new(super::stream::WebPReader::new(
|
||||||
@@ -843,24 +881,40 @@ impl Image {
|
|||||||
|
|
||||||
let data_dimensions = (width, height);
|
let data_dimensions = (width, height);
|
||||||
|
|
||||||
|
// Output bits per sample: sub-byte depths stay packed, supra-byte non-standard
|
||||||
|
// depths are promoted to the next standard width (matching sample_type() mapping).
|
||||||
|
let output_bits_per_sample: u8 = match tiff_bits_per_sample {
|
||||||
|
0..=8 => tiff_bits_per_sample,
|
||||||
|
9..=16 => 16,
|
||||||
|
17..=32 => 32,
|
||||||
|
_ => 64,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Packed row size for reading from the file (uses tiff_bits_per_sample).
|
||||||
let tiff_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample))
|
let tiff_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample))
|
||||||
.checked_mul(u64::from(tiff_samples))
|
.checked_mul(u64::from(tiff_samples))
|
||||||
.ok_or(TiffError::LimitsExceeded)?;
|
.ok_or(TiffError::LimitsExceeded)?;
|
||||||
let tiff_row_bytes: usize = tiff_row_bits.div_ceil(8).try_into()?;
|
let tiff_row_bytes: usize = tiff_row_bits.div_ceil(8).try_into()?;
|
||||||
|
|
||||||
let chunk_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample))
|
// Output buffer sizes use output_bits_per_sample (the promoted width).
|
||||||
.checked_mul(u64::from(data_samples))
|
let chunk_row_bits = (u64::from(tiff_chunk_dimensions.0)
|
||||||
.ok_or(TiffError::LimitsExceeded)?;
|
* u64::from(output_bits_per_sample))
|
||||||
|
.checked_mul(u64::from(data_samples))
|
||||||
|
.ok_or(TiffError::LimitsExceeded)?;
|
||||||
let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?;
|
let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?;
|
||||||
|
|
||||||
let data_row_bits = (u64::from(data_dimensions.0) * u64::from(tiff_bits_per_sample))
|
let data_row_bits = (u64::from(data_dimensions.0) * u64::from(output_bits_per_sample))
|
||||||
.checked_mul(u64::from(data_samples))
|
.checked_mul(u64::from(data_samples))
|
||||||
.ok_or(TiffError::LimitsExceeded)?;
|
.ok_or(TiffError::LimitsExceeded)?;
|
||||||
let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?;
|
let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?;
|
||||||
|
|
||||||
|
// Clamp the chunk height to the image height: a chunk never contributes more
|
||||||
|
// rows than the image contains. Without this, RowsPerStrip = u32::MAX overflows
|
||||||
|
// usize on 32-bit platforms.
|
||||||
|
let clamped_chunk_height = tiff_chunk_dimensions.1.min(data_dimensions.1);
|
||||||
let chunk_col_stride: usize = data_row_bits
|
let chunk_col_stride: usize = data_row_bits
|
||||||
.div_ceil(8)
|
.div_ceil(8)
|
||||||
.checked_mul(u64::from(tiff_chunk_dimensions.1))
|
.checked_mul(u64::from(clamped_chunk_height))
|
||||||
.ok_or(TiffError::LimitsExceeded)?
|
.ok_or(TiffError::LimitsExceeded)?
|
||||||
.try_into()?;
|
.try_into()?;
|
||||||
|
|
||||||
@@ -909,6 +963,7 @@ impl Image {
|
|||||||
chunk_col_stride,
|
chunk_col_stride,
|
||||||
plane_stride,
|
plane_stride,
|
||||||
tiff_bits_per_sample,
|
tiff_bits_per_sample,
|
||||||
|
output_bits_per_sample,
|
||||||
tiff_samples,
|
tiff_samples,
|
||||||
tiff_chunk_dimensions,
|
tiff_chunk_dimensions,
|
||||||
tiff_row_bytes,
|
tiff_row_bytes,
|
||||||
@@ -955,6 +1010,7 @@ impl Image {
|
|||||||
buf: &mut [u8],
|
buf: &mut [u8],
|
||||||
layout: &ReadoutLayout,
|
layout: &ReadoutLayout,
|
||||||
chunk_index: u32,
|
chunk_index: u32,
|
||||||
|
scratch: &mut Vec<u8>,
|
||||||
) -> TiffResult<()> {
|
) -> TiffResult<()> {
|
||||||
let ValueReader {
|
let ValueReader {
|
||||||
reader,
|
reader,
|
||||||
@@ -979,11 +1035,18 @@ impl Image {
|
|||||||
| ColorType::CMYKA(n)
|
| ColorType::CMYKA(n)
|
||||||
| ColorType::YCbCr(n)
|
| ColorType::YCbCr(n)
|
||||||
| ColorType::Gray(n)
|
| ColorType::Gray(n)
|
||||||
|
| ColorType::Palette(n)
|
||||||
| ColorType::Multiband {
|
| ColorType::Multiband {
|
||||||
bit_depth: n,
|
bit_depth: n,
|
||||||
num_samples: _,
|
num_samples: _,
|
||||||
} if n == 8 || n == 16 || n == 32 || n == 64 => {}
|
} if n == 8 || n == 16 || n == 32 || n == 64 => {}
|
||||||
ColorType::Gray(n)
|
ColorType::RGB(n)
|
||||||
|
| ColorType::RGBA(n)
|
||||||
|
| ColorType::CMYK(n)
|
||||||
|
| ColorType::CMYKA(n)
|
||||||
|
| ColorType::YCbCr(n)
|
||||||
|
| ColorType::Gray(n)
|
||||||
|
| ColorType::Palette(n)
|
||||||
| ColorType::Multiband {
|
| ColorType::Multiband {
|
||||||
bit_depth: n,
|
bit_depth: n,
|
||||||
num_samples: _,
|
num_samples: _,
|
||||||
@@ -1000,6 +1063,26 @@ impl Image {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Extended bit depths (9-15, 17-31): packed N-bit samples unpacked to standard width.
|
||||||
|
// Horizontal predictor is supported (operates on unpacked standard-width samples).
|
||||||
|
// FloatingPoint predictor is not meaningful for non-standard integer depths.
|
||||||
|
ColorType::RGB(n)
|
||||||
|
| ColorType::RGBA(n)
|
||||||
|
| ColorType::CMYK(n)
|
||||||
|
| ColorType::CMYKA(n)
|
||||||
|
| ColorType::YCbCr(n)
|
||||||
|
| ColorType::Gray(n)
|
||||||
|
| ColorType::Multiband {
|
||||||
|
bit_depth: n,
|
||||||
|
num_samples: _,
|
||||||
|
} if n <= 32 => match self.predictor {
|
||||||
|
Predictor::None | Predictor::Horizontal => {}
|
||||||
|
Predictor::FloatingPoint => {
|
||||||
|
return Err(TiffError::UnsupportedError(
|
||||||
|
TiffUnsupportedError::FloatingPointPredictor(color_type),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
type_ => {
|
type_ => {
|
||||||
return Err(TiffError::UnsupportedError(
|
return Err(TiffError::UnsupportedError(
|
||||||
TiffUnsupportedError::UnsupportedColorType(type_),
|
TiffUnsupportedError::UnsupportedColorType(type_),
|
||||||
@@ -1071,9 +1154,53 @@ impl Image {
|
|||||||
self.jpeg_tables.as_deref().map(|a| &**a),
|
self.jpeg_tables.as_deref().map(|a| &**a),
|
||||||
chunk_dims,
|
chunk_dims,
|
||||||
self.samples,
|
self.samples,
|
||||||
|
self.fill_order,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if is_output_chunk_rows && is_all_bits {
|
// Extended bit depths (9-15, 17-31): packed N-bit samples must be unpacked to
|
||||||
|
// standard-width output samples before endianness fix and predictor.
|
||||||
|
let needs_unpacking = layout.tiff_bits_per_sample != layout.output_bits_per_sample
|
||||||
|
&& layout.tiff_bits_per_sample > 8;
|
||||||
|
|
||||||
|
if needs_unpacking {
|
||||||
|
if !is_all_bits {
|
||||||
|
// Packed extended depths with extra samples to discard is not yet supported.
|
||||||
|
return Err(TiffError::UnsupportedError(
|
||||||
|
TiffUnsupportedError::InterpretationWithBits(
|
||||||
|
self.photometric_interpretation,
|
||||||
|
vec![self.bits_per_sample; self.samples as usize],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let tiff_bps = layout.tiff_bits_per_sample;
|
||||||
|
let out_bps = layout.output_bits_per_sample;
|
||||||
|
let samples_per_data_row = data_dims.0 as usize * samples as usize;
|
||||||
|
|
||||||
|
let mut packed = vec![0u8; chunk_row_bytes];
|
||||||
|
|
||||||
|
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
|
||||||
|
reader.read_exact(&mut packed)?;
|
||||||
|
|
||||||
|
let out_row = &mut row[..data_row_bytes];
|
||||||
|
unpack_bits(&packed, out_row, tiff_bps, samples_per_data_row, out_bps);
|
||||||
|
|
||||||
|
// Unpacked values are in native byte order, so pass native to skip
|
||||||
|
// endianness conversion while still applying the predictor.
|
||||||
|
super::fix_endianness_and_predict(
|
||||||
|
out_row,
|
||||||
|
color_type.bit_depth(),
|
||||||
|
samples,
|
||||||
|
ByteOrder::native(),
|
||||||
|
predictor,
|
||||||
|
scratch,
|
||||||
|
);
|
||||||
|
|
||||||
|
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||||
|
super::invert_colors(out_row, color_type, self.sample_format)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if is_output_chunk_rows && is_all_bits {
|
||||||
// Here we can read directly into the output buffer itself.
|
// Here we can read directly into the output buffer itself.
|
||||||
let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize];
|
let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize];
|
||||||
reader.read_exact(tile)?;
|
reader.read_exact(tile)?;
|
||||||
@@ -1085,6 +1212,7 @@ impl Image {
|
|||||||
samples,
|
samples,
|
||||||
decompressed_byte_order,
|
decompressed_byte_order,
|
||||||
predictor,
|
predictor,
|
||||||
|
scratch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1100,9 +1228,18 @@ impl Image {
|
|||||||
|
|
||||||
let row = &mut row[..data_row_bytes];
|
let row = &mut row[..data_row_bytes];
|
||||||
match color_type.bit_depth() {
|
match color_type.bit_depth() {
|
||||||
16 => predict_f16(&mut encoded, row, samples),
|
16 => {
|
||||||
32 => predict_f32(&mut encoded, row, samples),
|
let (out, _) = row.as_chunks_mut::<2>();
|
||||||
64 => predict_f64(&mut encoded, row, samples),
|
predict_f16(&mut encoded, out, samples);
|
||||||
|
}
|
||||||
|
32 => {
|
||||||
|
let (out, _) = row.as_chunks_mut::<4>();
|
||||||
|
predict_f32(&mut encoded, out, samples);
|
||||||
|
}
|
||||||
|
64 => {
|
||||||
|
let (out, _) = row.as_chunks_mut::<8>();
|
||||||
|
predict_f64(&mut encoded, out, samples);
|
||||||
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||||
@@ -1131,6 +1268,7 @@ impl Image {
|
|||||||
samples,
|
samples,
|
||||||
decompressed_byte_order,
|
decompressed_byte_order,
|
||||||
predictor,
|
predictor,
|
||||||
|
scratch,
|
||||||
);
|
);
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||||
@@ -1172,6 +1310,7 @@ impl Image {
|
|||||||
samples,
|
samples,
|
||||||
decompressed_byte_order,
|
decompressed_byte_order,
|
||||||
predictor,
|
predictor,
|
||||||
|
scratch,
|
||||||
);
|
);
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||||
@@ -1198,6 +1337,108 @@ impl Image {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unpack N-bit packed samples (MSB-first) into standard-width output samples in native byte
|
||||||
|
/// order. Packed bits are read from `src` and expanded samples are written to `dst`.
|
||||||
|
///
|
||||||
|
/// `bits_per_sample` is the packed depth (e.g. 10, 12, 14).
|
||||||
|
/// `num_samples` is the number of samples to extract.
|
||||||
|
/// `output_width` is the output sample width in bits (16 or 32).
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if `dst` is too small for `num_samples` output samples.
|
||||||
|
fn unpack_bits(
|
||||||
|
src: &[u8],
|
||||||
|
dst: &mut [u8],
|
||||||
|
bits_per_sample: u8,
|
||||||
|
num_samples: usize,
|
||||||
|
output_width: u8,
|
||||||
|
) {
|
||||||
|
// Dispatch to const-generic specializations so the compiler can unroll the
|
||||||
|
// byte-gathering loop and precompute shift/mask constants.
|
||||||
|
macro_rules! dispatch {
|
||||||
|
($($bps:literal),+) => {
|
||||||
|
match bits_per_sample {
|
||||||
|
$( $bps => unpack_bits_const::<$bps>(src, dst, num_samples, output_width), )+
|
||||||
|
_ => unpack_bits_const::<0>(src, dst, num_samples, output_width),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Cover every bit depth from 9..=32 that could reach this path, plus 0 as fallback.
|
||||||
|
dispatch!(
|
||||||
|
9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
|
||||||
|
32
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inner loop with const BPS so the compiler can optimize the bit extraction.
|
||||||
|
/// BPS=0 is the fallback for unexpected bit depths (uses runtime value).
|
||||||
|
#[inline(always)]
|
||||||
|
fn unpack_bits_const<const BPS: u8>(
|
||||||
|
src: &[u8],
|
||||||
|
dst: &mut [u8],
|
||||||
|
num_samples: usize,
|
||||||
|
output_width: u8,
|
||||||
|
) {
|
||||||
|
debug_assert!(output_width == 16 || output_width == 32);
|
||||||
|
|
||||||
|
match output_width {
|
||||||
|
16 => {
|
||||||
|
debug_assert!(dst.len() >= num_samples * 2);
|
||||||
|
let mut bit_offset: usize = 0;
|
||||||
|
for i in 0..num_samples {
|
||||||
|
let val = if BPS > 0 {
|
||||||
|
extract_bits_msb_const::<BPS>(src, bit_offset)
|
||||||
|
} else {
|
||||||
|
extract_bits_msb_const::<16>(src, bit_offset) // unreachable fallback
|
||||||
|
};
|
||||||
|
dst[i * 2..i * 2 + 2].copy_from_slice(&(val as u16).to_ne_bytes());
|
||||||
|
bit_offset += if BPS > 0 { BPS as usize } else { 16 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
32 => {
|
||||||
|
debug_assert!(dst.len() >= num_samples * 4);
|
||||||
|
let mut bit_offset: usize = 0;
|
||||||
|
for i in 0..num_samples {
|
||||||
|
let val = if BPS > 0 {
|
||||||
|
extract_bits_msb_const::<BPS>(src, bit_offset)
|
||||||
|
} else {
|
||||||
|
extract_bits_msb_const::<32>(src, bit_offset) // unreachable fallback
|
||||||
|
};
|
||||||
|
dst[i * 4..i * 4 + 4].copy_from_slice(&val.to_ne_bytes());
|
||||||
|
bit_offset += if BPS > 0 { BPS as usize } else { 32 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract a value of `BPS` bits starting at `bit_offset` from an MSB-first packed byte stream.
|
||||||
|
/// Uses a single wide read + shift/mask for branchless extraction.
|
||||||
|
#[inline(always)]
|
||||||
|
fn extract_bits_msb_const<const BPS: u8>(src: &[u8], bit_offset: usize) -> u32 {
|
||||||
|
let byte_idx = bit_offset / 8;
|
||||||
|
let bit_idx = bit_offset % 8;
|
||||||
|
let mask: u64 = (1u64 << BPS) - 1;
|
||||||
|
|
||||||
|
// Read a u64 from as many bytes as available, big-endian, without going out of bounds.
|
||||||
|
// For BPS <= 32 (+ up to 7 bits offset = 39 bits max), we need at most 5 bytes,
|
||||||
|
// but reading 8 at once is faster and the branch is only taken near row ends.
|
||||||
|
let remaining = src.len() - byte_idx;
|
||||||
|
let acc = if remaining >= 8 {
|
||||||
|
u64::from_be_bytes(src[byte_idx..byte_idx + 8].try_into().unwrap())
|
||||||
|
} else {
|
||||||
|
// Near the end of the buffer — read available bytes into the high bits of a u64.
|
||||||
|
let mut buf = [0u8; 8];
|
||||||
|
buf[..remaining].copy_from_slice(&src[byte_idx..]);
|
||||||
|
u64::from_be_bytes(buf)
|
||||||
|
};
|
||||||
|
|
||||||
|
// The value starts at bit_idx from the MSB of our u64. Shift it to the LSB and mask.
|
||||||
|
let shift = 64 - bit_idx - BPS as usize;
|
||||||
|
((acc >> shift) & mask) as u32
|
||||||
|
}
|
||||||
|
|
||||||
impl ReadoutLayout {
|
impl ReadoutLayout {
|
||||||
pub(crate) fn samples_per_out_texel(&self) -> u16 {
|
pub(crate) fn samples_per_out_texel(&self) -> u16 {
|
||||||
match self.planar_config {
|
match self.planar_config {
|
||||||
@@ -1210,7 +1451,7 @@ impl ReadoutLayout {
|
|||||||
// pixel data.
|
// pixel data.
|
||||||
pub(crate) fn chunk_row_bytes(&self, width: u32) -> TiffResult<usize> {
|
pub(crate) fn chunk_row_bytes(&self, width: u32) -> TiffResult<usize> {
|
||||||
let data_samples = self.samples_per_out_texel();
|
let data_samples = self.samples_per_out_texel();
|
||||||
let data_row_bits = (u64::from(width) * u64::from(self.tiff_bits_per_sample))
|
let data_row_bits = (u64::from(width) * u64::from(self.output_bits_per_sample))
|
||||||
.checked_mul(u64::from(data_samples))
|
.checked_mul(u64::from(data_samples))
|
||||||
.ok_or(TiffError::LimitsExceeded)?;
|
.ok_or(TiffError::LimitsExceeded)?;
|
||||||
Ok(data_row_bits.div_ceil(8).try_into()?)
|
Ok(data_row_bits.div_ceil(8).try_into()?)
|
||||||
@@ -1228,8 +1469,12 @@ impl ReadoutLayout {
|
|||||||
|
|
||||||
let data_row_bytes = u64::try_from(row_stride)?;
|
let data_row_bytes = u64::try_from(row_stride)?;
|
||||||
|
|
||||||
|
// Use the original image height (from plane_stride / row_stride) to clamp
|
||||||
|
// the chunk height, matching the clamping in readout_for_size().
|
||||||
|
let image_height = self.plane_stride.checked_div(self.row_stride).unwrap_or(0) as u32;
|
||||||
|
let clamped_chunk_height = self.tiff_chunk_dimensions.1.min(image_height);
|
||||||
let chunk_col_stride = data_row_bytes
|
let chunk_col_stride = data_row_bytes
|
||||||
.checked_mul(u64::from(self.tiff_chunk_dimensions.1))
|
.checked_mul(u64::from(clamped_chunk_height))
|
||||||
.ok_or(TiffError::LimitsExceeded)?
|
.ok_or(TiffError::LimitsExceeded)?
|
||||||
.try_into()?;
|
.try_into()?;
|
||||||
|
|
||||||
|
|||||||
@@ -87,15 +87,12 @@ fn packbits_decode<R: Read, const N: usize>(
|
|||||||
reader: &mut R,
|
reader: &mut R,
|
||||||
buf: &mut [[u8; N]],
|
buf: &mut [[u8; N]],
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
|
let expected = buf.len();
|
||||||
for offset in (0..N).rev() {
|
for offset in (0..N).rev() {
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
let mut code = [0u8; 1];
|
let mut code = [0u8; 1];
|
||||||
|
|
||||||
loop {
|
while let Some(pixels) = buf.get_mut(i..) {
|
||||||
let Some(pixels) = buf.get_mut(i..) else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
if pixels.is_empty() {
|
if pixels.is_empty() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -124,6 +121,13 @@ fn packbits_decode<R: Read, const N: usize>(
|
|||||||
i += usize::from(repeat);
|
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(())
|
Ok(())
|
||||||
|
|||||||
+726
-487
File diff suppressed because it is too large.
Load diff
+156
-2
@@ -256,9 +256,24 @@ 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")]
|
#[cfg(feature = "fax")]
|
||||||
pub struct Group4Reader<R: Read> {
|
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>>,
|
line_buf: io::Cursor<Vec<u8>>,
|
||||||
height: u32,
|
height: u32,
|
||||||
width: u16,
|
width: u16,
|
||||||
@@ -271,13 +286,14 @@ impl<R: Read> Group4Reader<R> {
|
|||||||
dimensions: (u32, u32),
|
dimensions: (u32, u32),
|
||||||
reader: R,
|
reader: R,
|
||||||
compressed_length: u64,
|
compressed_length: u64,
|
||||||
|
fill_order: u16,
|
||||||
) -> crate::TiffResult<Self> {
|
) -> crate::TiffResult<Self> {
|
||||||
let width = u16::try_from(dimensions.0)?;
|
let width = u16::try_from(dimensions.0)?;
|
||||||
let height = dimensions.1;
|
let height = dimensions.1;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
decoder: fax34::decoder::Group4Decoder::new(
|
decoder: fax34::decoder::Group4Decoder::new(
|
||||||
io::BufReader::new(reader.take(compressed_length)).bytes(),
|
fax_byte_iter(reader, compressed_length, fill_order),
|
||||||
width,
|
width,
|
||||||
)?,
|
)?,
|
||||||
line_buf: io::Cursor::new(Vec::with_capacity(width.into())),
|
line_buf: io::Cursor::new(Vec::with_capacity(width.into())),
|
||||||
@@ -356,6 +372,144 @@ 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")]
|
#[cfg(feature = "webp")]
|
||||||
pub struct WebPReader {
|
pub struct WebPReader {
|
||||||
inner: Cursor<Vec<u8>>,
|
inner: Cursor<Vec<u8>>,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use crate::{
|
|||||||
/// [`Decoder::read_directory_tags`](crate::decoder::Decoder::read_directory_tags) to read the
|
/// [`Decoder::read_directory_tags`](crate::decoder::Decoder::read_directory_tags) to read the
|
||||||
/// values associated with tags from an underlying file.
|
/// values associated with tags from an underlying file.
|
||||||
#[doc(alias = "IFD")]
|
#[doc(alias = "IFD")]
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Directory {
|
pub struct Directory {
|
||||||
/// There are at most `u16::MAX` entries in any single directory, the count is stored as a
|
/// 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
|
/// 2-byte value. The order in the file is implied to be ascending by tag value (the decoder
|
||||||
|
|||||||
@@ -20,18 +20,6 @@ impl Deflate {
|
|||||||
level: FlateCompression::new(level as u32),
|
level: FlateCompression::new(level as u32),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new deflate compressor with an exact level of compression.
|
|
||||||
///
|
|
||||||
/// `0` stands for uncompressed
|
|
||||||
/// `1` stands for light but fast compression
|
|
||||||
/// `6` is the recommended balanced default
|
|
||||||
/// `9` is maximum compression ratio at the cost of slow encoding
|
|
||||||
pub fn exact_level(level: u8) -> Self {
|
|
||||||
Self {
|
|
||||||
level: FlateCompression::new(level as u32),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Deflate {
|
impl Default for Deflate {
|
||||||
|
|||||||
+23
-6
@@ -40,14 +40,24 @@ use self::writer::*;
|
|||||||
pub type Predictor = crate::tags::Predictor;
|
pub type Predictor = crate::tags::Predictor;
|
||||||
|
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Clone, Copy, PartialEq, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
pub enum Compression {
|
pub enum Compression {
|
||||||
|
/// No compression whatsoever. Fastest but produces large files.
|
||||||
#[default]
|
#[default]
|
||||||
Uncompressed,
|
Uncompressed,
|
||||||
|
/// Legacy [LZW](https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch) algorithm. Not recommended, use DEFLATE instead.
|
||||||
#[cfg(feature = "lzw")]
|
#[cfg(feature = "lzw")]
|
||||||
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")]
|
#[cfg(feature = "deflate")]
|
||||||
Deflate(u8),
|
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,
|
Packbits,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +163,15 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
|
|||||||
/// Set the predictor to use
|
/// Set the predictor to use
|
||||||
///
|
///
|
||||||
/// A predictor is used to simplify the file before writing it. This is very
|
/// 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 {
|
pub fn with_predictor(mut self, predictor: Predictor) -> Self {
|
||||||
self.predictor = predictor;
|
self.predictor = predictor;
|
||||||
|
|
||||||
@@ -470,7 +488,8 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
|||||||
self.writer.write_u16(entry.field_type().to_u16())?;
|
self.writer.write_u16(entry.field_type().to_u16())?;
|
||||||
let count = K::convert_offset(entry.count())?;
|
let count = K::convert_offset(entry.count())?;
|
||||||
count.write(self.writer)?;
|
count.write(self.writer)?;
|
||||||
self.writer.write_bytes(&entry.offset()[..offset_bytes])?;
|
self.writer
|
||||||
|
.write_bytes(&entry.offset_raw()[..offset_bytes])?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(offset)
|
Ok(offset)
|
||||||
@@ -836,9 +855,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
|||||||
self.encoder
|
self.encoder
|
||||||
.write_tag(Tag::ResolutionUnit, unit.to_u16())
|
.write_tag(Tag::ResolutionUnit, unit.to_u16())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
self.encoder
|
self.encoder.write_tag(Tag::XResolution, value).unwrap();
|
||||||
.write_tag(Tag::XResolution, value.clone())
|
|
||||||
.unwrap();
|
|
||||||
self.encoder.write_tag(Tag::YResolution, value).unwrap();
|
self.encoder.write_tag(Tag::YResolution, value).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -665,22 +665,22 @@ impl_tiff_value_for_contiguous_sequence!(Rational; 8; Type::RATIONAL);
|
|||||||
impl_tiff_value_for_contiguous_sequence!(SRational; 8; Type::SRATIONAL);
|
impl_tiff_value_for_contiguous_sequence!(SRational; 8; Type::SRATIONAL);
|
||||||
|
|
||||||
/// Type to represent tiff values of type `IFD`
|
/// Type to represent tiff values of type `IFD`
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
pub struct Ifd(pub u32);
|
pub struct Ifd(pub u32);
|
||||||
|
|
||||||
/// Type to represent tiff values of type `IFD8`
|
/// Type to represent tiff values of type `IFD8`
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
pub struct Ifd8(pub u64);
|
pub struct Ifd8(pub u64);
|
||||||
|
|
||||||
/// Type to represent tiff values of type `RATIONAL`
|
/// Type to represent tiff values of type `RATIONAL`
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct Rational {
|
pub struct Rational {
|
||||||
pub n: u32,
|
pub n: u32,
|
||||||
pub d: u32,
|
pub d: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type to represent tiff values of type `SRATIONAL`
|
/// Type to represent tiff values of type `SRATIONAL`
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct SRational {
|
pub struct SRational {
|
||||||
pub n: i32,
|
pub n: i32,
|
||||||
pub d: i32,
|
pub d: i32,
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ fn encoder_on_short_writes() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let imgdata = vec![42u8; 100 * 100];
|
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 [
|
for comp in [
|
||||||
super::Compression::Uncompressed,
|
super::Compression::Uncompressed,
|
||||||
@@ -247,7 +247,7 @@ fn encoder_on_short_writes() {
|
|||||||
let _layout = tiff_reader.read_image_to_buffer(&mut data).unwrap();
|
let _layout = tiff_reader.read_image_to_buffer(&mut data).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&data, crate::decoder::DecodingResult::U8(v) if *v == imgdata),
|
matches!(&data, crate::decoder::DecodingSampleBuffer::U8(v) if *v == imgdata),
|
||||||
"{data:?} @ {}",
|
"{data:?} @ {}",
|
||||||
data.as_buffer(0).byte_len()
|
data.as_buffer(0).byte_len()
|
||||||
);
|
);
|
||||||
|
|||||||
+61
-1
@@ -426,7 +426,24 @@ impl ValueBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a value with native byte order from in-memory data.
|
/// 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 {
|
ValueBuffer {
|
||||||
bytes: value.data().into_owned(),
|
bytes: value.data().into_owned(),
|
||||||
ty: <T as TiffValue>::FIELD_TYPE,
|
ty: <T as TiffValue>::FIELD_TYPE,
|
||||||
@@ -435,6 +452,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 {
|
pub fn byte_order(&self) -> ByteOrder {
|
||||||
self.byte_order
|
self.byte_order
|
||||||
}
|
}
|
||||||
@@ -514,6 +567,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.
|
/// Byte order of the TIFF file.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum ByteOrder {
|
pub enum ByteOrder {
|
||||||
|
|||||||
@@ -26,21 +26,20 @@ fn test_big_tiff() {
|
|||||||
decoder.colortype().expect("Cannot get colortype"),
|
decoder.colortype().expect("Cannot get colortype"),
|
||||||
ColorType::RGB(8)
|
ColorType::RGB(8)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut ifd = decoder.current_ifd();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decoder
|
ifd.get_tag_u64(Tag::StripOffsets)
|
||||||
.get_tag_u64(Tag::StripOffsets)
|
|
||||||
.expect("Cannot get StripOffsets"),
|
.expect("Cannot get StripOffsets"),
|
||||||
16
|
16
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decoder
|
ifd.get_tag_u64(Tag::RowsPerStrip)
|
||||||
.get_tag_u64(Tag::RowsPerStrip)
|
|
||||||
.expect("Cannot get RowsPerStrip"),
|
.expect("Cannot get RowsPerStrip"),
|
||||||
64
|
64
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decoder
|
ifd.get_tag_u64(Tag::StripByteCounts)
|
||||||
.get_tag_u64(Tag::StripByteCounts)
|
|
||||||
.expect("Cannot get StripByteCounts"),
|
.expect("Cannot get StripByteCounts"),
|
||||||
12288
|
12288
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
extern crate tiff;
|
extern crate tiff;
|
||||||
|
|
||||||
use tiff::decoder::{Decoder, DecodingResult};
|
use tiff::decoder::{Decoder, DecodingSampleBuffer};
|
||||||
use tiff::ColorType;
|
use tiff::ColorType;
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
@@ -28,7 +28,7 @@ fn test_white_ieee_fp16() {
|
|||||||
ColorType::Gray(16)
|
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 {
|
for p in img {
|
||||||
assert!(p == half::f16::from_f32_const(1.0));
|
assert!(p == half::f16::from_f32_const(1.0));
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ fn test_one_black_pixel_ieee_fp16() {
|
|||||||
ColorType::Gray(16)
|
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() {
|
for (i, p) in img.iter().enumerate() {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
assert!(p < &half::f16::from_f32_const(0.001));
|
assert!(p < &half::f16::from_f32_const(0.001));
|
||||||
@@ -92,7 +92,7 @@ fn test_pattern_horizontal_differencing_ieee_fp16() {
|
|||||||
ColorType::Gray(16)
|
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
|
// 0, 2, 5, 8, 12, 16, 255 are black
|
||||||
let black = [0, 2, 5, 8, 12, 16, 255];
|
let black = [0, 2, 5, 8, 12, 16, 255];
|
||||||
for (i, p) in img.iter().enumerate() {
|
for (i, p) in img.iter().enumerate() {
|
||||||
@@ -128,7 +128,7 @@ fn test_pattern_predictor_ieee_fp16() {
|
|||||||
ColorType::Gray(16)
|
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
|
// 0, 2, 5, 8, 12, 16, 255 are black
|
||||||
let black = [0, 2, 5, 8, 12, 16, 255];
|
let black = [0, 2, 5, 8, 12, 16, 255];
|
||||||
for (i, p) in img.iter().enumerate() {
|
for (i, p) in img.iter().enumerate() {
|
||||||
@@ -199,7 +199,7 @@ fn test_predictor_ieee_fp16() {
|
|||||||
ColorType::Gray(16)
|
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()) {
|
for (exp, found) in std::iter::zip(pnm_values.iter(), img.iter()) {
|
||||||
assert!((exp - found.to_f32()).abs() < 0.0001);
|
assert!((exp - found.to_f32()).abs() < 0.0001);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
extern crate tiff;
|
extern crate tiff;
|
||||||
|
|
||||||
use tiff::decoder::{Decoder, DecodingResult};
|
use tiff::decoder::{Decoder, DecodingSampleBuffer};
|
||||||
use tiff::tags::Tag;
|
use tiff::tags::Tag;
|
||||||
use tiff::ColorType;
|
use tiff::ColorType;
|
||||||
|
|
||||||
@@ -30,33 +30,31 @@ fn test_geo_tiff() {
|
|||||||
num_samples: 5
|
num_samples: 5
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut ifd = decoder.current_ifd();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decoder
|
ifd.get_tag_u64(Tag::StripOffsets)
|
||||||
.get_tag_u64(Tag::StripOffsets)
|
|
||||||
.expect("Cannot get StripOffsets"),
|
.expect("Cannot get StripOffsets"),
|
||||||
418
|
418
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decoder
|
ifd.get_tag_u64(Tag::RowsPerStrip)
|
||||||
.get_tag_u64(Tag::RowsPerStrip)
|
|
||||||
.expect("Cannot get RowsPerStrip"),
|
.expect("Cannot get RowsPerStrip"),
|
||||||
10
|
10
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decoder
|
ifd.get_tag_u64(Tag::StripByteCounts)
|
||||||
.get_tag_u64(Tag::StripByteCounts)
|
|
||||||
.expect("Cannot get StripByteCounts"),
|
.expect("Cannot get StripByteCounts"),
|
||||||
1000
|
1000
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decoder
|
ifd.get_tag(Tag::ModelPixelScaleTag)
|
||||||
.get_tag(Tag::ModelPixelScaleTag)
|
|
||||||
.expect("Cannot get pixel scale")
|
.expect("Cannot get pixel scale")
|
||||||
.into_f64_vec()
|
.into_f64_vec()
|
||||||
.expect("Cannot get pixel scale"),
|
.expect("Cannot get pixel scale"),
|
||||||
vec![60.0, 60.0, 0.0]
|
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")
|
panic!("Cannot read band data")
|
||||||
};
|
};
|
||||||
assert_eq!(data.len(), 500);
|
assert_eq!(data.len(), 500);
|
||||||
@@ -67,7 +65,7 @@ fn test_geo_tiff() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_webp_tiff() {
|
fn test_webp_tiff() {
|
||||||
let filenames = ["usda_naip_256_webp_z3.tif"];
|
let filenames = ["usda_naip_256_webp_z3.tif"];
|
||||||
let mut buffer = DecodingResult::U8(vec![]);
|
let mut buffer = DecodingSampleBuffer::U8(vec![]);
|
||||||
|
|
||||||
for filename in filenames.iter() {
|
for filename in filenames.iter() {
|
||||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
||||||
|
|||||||
+304
-15
@@ -1,6 +1,7 @@
|
|||||||
extern crate tiff;
|
extern crate tiff;
|
||||||
|
|
||||||
use tiff::decoder::{ifd, Decoder, DecodingResult};
|
use tiff::decoder::{ifd, Decoder, DecodingSampleBuffer};
|
||||||
|
use tiff::tags::{ByteOrder, Type};
|
||||||
use tiff::ColorType;
|
use tiff::ColorType;
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
@@ -8,6 +9,7 @@ use std::io::{Read, Seek};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
const TEST_IMAGE_DIR: &str = "./tests/images/";
|
const TEST_IMAGE_DIR: &str = "./tests/images/";
|
||||||
|
const SEQ_IMAGE_DIR: &str = "./tests/images/seq/";
|
||||||
|
|
||||||
macro_rules! test_image_sum {
|
macro_rules! test_image_sum {
|
||||||
($name:ident, $buffer:ident, $sum_ty:ty) => {
|
($name:ident, $buffer:ident, $sum_ty:ty) => {
|
||||||
@@ -21,7 +23,7 @@ macro_rules! test_image_sum {
|
|||||||
let img_res = decoder.read_image().unwrap();
|
let img_res = decoder.read_image().unwrap();
|
||||||
|
|
||||||
match img_res {
|
match img_res {
|
||||||
DecodingResult::$buffer(res) => {
|
DecodingSampleBuffer::$buffer(res) => {
|
||||||
let sum: $sum_ty = res.into_iter().map(<$sum_ty>::from).sum();
|
let sum: $sum_ty = res.into_iter().map(<$sum_ty>::from).sum();
|
||||||
assert_eq!(sum, expected_sum);
|
assert_eq!(sum, expected_sum);
|
||||||
}
|
}
|
||||||
@@ -40,9 +42,198 @@ test_image_sum!(test_image_sum_u64, U64, u64);
|
|||||||
test_image_sum!(test_image_sum_f32, F32, f32);
|
test_image_sum!(test_image_sum_f32, F32, f32);
|
||||||
test_image_sum!(test_image_sum_f64, F64, f64);
|
test_image_sum!(test_image_sum_f64, F64, f64);
|
||||||
|
|
||||||
|
/// Compute a CRC32 hash of the full decoded TIFF output including metadata.
|
||||||
|
///
|
||||||
|
/// Hashes: page count, and for each page: dimensions, color type, and decoded
|
||||||
|
/// sample data (in little-endian byte order for multi-byte types). This catches
|
||||||
|
/// wrong dimensions, wrong color type, byte-order errors, bit-alignment errors,
|
||||||
|
/// stride/padding errors, and pixel value errors.
|
||||||
|
fn compute_tiff_hash<R: Read + Seek>(reader: R) -> u32 {
|
||||||
|
let mut decoder = Decoder::open(reader).expect("Cannot create decoder");
|
||||||
|
let mut hasher = crc32fast::Hasher::new();
|
||||||
|
|
||||||
|
let mut page = 0u32;
|
||||||
|
loop {
|
||||||
|
decoder.next_image().expect("Cannot read image IFD");
|
||||||
|
let (width, height) = decoder.dimensions().unwrap();
|
||||||
|
let color_type = decoder.colortype().unwrap();
|
||||||
|
let mut result = decoder.read_image().unwrap();
|
||||||
|
|
||||||
|
// Hash page index + dimensions
|
||||||
|
hasher.update(&page.to_le_bytes());
|
||||||
|
hasher.update(&width.to_le_bytes());
|
||||||
|
hasher.update(&height.to_le_bytes());
|
||||||
|
|
||||||
|
// Hash color type (discriminant + fields)
|
||||||
|
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:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash decoded data: TIFF type tag (u16 repr) + LE sample bytes.
|
||||||
|
// Map each variant to the corresponding TIFF Type for byte-order normalization.
|
||||||
|
let sample_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
|
||||||
|
};
|
||||||
|
hasher.update(&sample_type.to_u16().to_le_bytes());
|
||||||
|
|
||||||
|
let mut buf = result.as_buffer(0);
|
||||||
|
let bytes = buf.as_bytes_mut();
|
||||||
|
ByteOrder::native().convert(sample_type, bytes, ByteOrder::LittleEndian);
|
||||||
|
hasher.update(bytes);
|
||||||
|
|
||||||
|
page += 1;
|
||||||
|
if !decoder.more_images() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash total page count at the end
|
||||||
|
hasher.update(&page.to_le_bytes());
|
||||||
|
hasher.finalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify all TIFF files in tests/images/seq/ against their filename-embedded CRC32 hash.
|
||||||
|
///
|
||||||
|
/// Each file is named `<description>-<8hex>.tiff` where the last segment before `.tiff`
|
||||||
|
/// is the expected CRC32 hash of (dimensions, color type, decoded data) for all pages.
|
||||||
|
#[test]
|
||||||
|
fn verify_all_seq_images() {
|
||||||
|
let seq_dir = std::path::Path::new(SEQ_IMAGE_DIR);
|
||||||
|
if !seq_dir.is_dir() {
|
||||||
|
panic!("seq image directory not found: {}", seq_dir.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut files: Vec<_> = std::fs::read_dir(seq_dir)
|
||||||
|
.expect("Cannot read seq image directory")
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.filter(|e| {
|
||||||
|
e.path()
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext == "tiff" || ext == "tif")
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
files.sort_by_key(|e| e.file_name());
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!files.is_empty(),
|
||||||
|
"No .tiff files found in {}",
|
||||||
|
seq_dir.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut failures = Vec::new();
|
||||||
|
for entry in &files {
|
||||||
|
let path = entry.path();
|
||||||
|
let stem = path.file_stem().unwrap().to_str().unwrap();
|
||||||
|
|
||||||
|
// Extract expected hash: last '-'-delimited segment
|
||||||
|
let expected_hash = stem
|
||||||
|
.rsplit('-')
|
||||||
|
.next()
|
||||||
|
.expect("filename must contain a '-' before the hash");
|
||||||
|
|
||||||
|
// Validate it looks like an 8-char hex string
|
||||||
|
if expected_hash.len() != 8 || !expected_hash.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||||
|
failures.push(format!(
|
||||||
|
"{}: filename does not end with 8-char hex hash (got '{}')",
|
||||||
|
stem, expected_hash
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = File::open(&path).expect("Cannot open test image");
|
||||||
|
let actual_hash = format!("{:08x}", compute_tiff_hash(file));
|
||||||
|
|
||||||
|
if actual_hash != expected_hash {
|
||||||
|
failures.push(format!(
|
||||||
|
"{}: expected hash {} but got {}",
|
||||||
|
stem, expected_hash, actual_hash
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !failures.is_empty() {
|
||||||
|
panic!(
|
||||||
|
"{} of {} seq images failed hash verification:\n {}",
|
||||||
|
failures.len(),
|
||||||
|
files.len(),
|
||||||
|
failures.join("\n ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("Verified {} seq images", files.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper: print the correct hash-suffixed filename for every *.tiff in tests/images/seq/.
|
||||||
|
/// Run with: cargo test print_seq_hashes -- --ignored --nocapture
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn print_seq_hashes() {
|
||||||
|
let dir = std::path::Path::new(SEQ_IMAGE_DIR);
|
||||||
|
let mut files: Vec<_> = std::fs::read_dir(dir)
|
||||||
|
.expect("Cannot read image directory")
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.filter(|e| {
|
||||||
|
e.file_name()
|
||||||
|
.to_str()
|
||||||
|
.is_some_and(|n| n.starts_with("seq-") && n.ends_with(".tiff"))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
files.sort_by_key(|e| e.file_name());
|
||||||
|
|
||||||
|
for entry in &files {
|
||||||
|
let path = entry.path();
|
||||||
|
let name = path.file_name().unwrap().to_str().unwrap();
|
||||||
|
let file = File::open(&path).expect("Cannot open test image");
|
||||||
|
let hash = format!("{:08x}", compute_tiff_hash(file));
|
||||||
|
// Strip the old hash (last -XXXXXXXX before .tiff) if present
|
||||||
|
let stem = path.file_stem().unwrap().to_str().unwrap();
|
||||||
|
let base = match stem.rsplit_once('-') {
|
||||||
|
Some((prefix, suffix))
|
||||||
|
if suffix.len() == 8 && suffix.chars().all(|c| c.is_ascii_hexdigit()) =>
|
||||||
|
{
|
||||||
|
prefix
|
||||||
|
}
|
||||||
|
_ => stem,
|
||||||
|
};
|
||||||
|
let new_name = format!("{base}-{hash}.tiff");
|
||||||
|
if new_name != name {
|
||||||
|
eprintln!("RENAME: {name} -> {new_name}");
|
||||||
|
} else {
|
||||||
|
eprintln!("OK: {name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Tests that a decoder can be constructed for an image and the color type
|
/// Tests that a decoder can be constructed for an image and the color type
|
||||||
/// read from the IFD and is of the appropriate type, but the type is
|
/// read from the IFD and is of the appropriate type, but the type is
|
||||||
/// unsupported.
|
/// unsupported.
|
||||||
|
#[allow(dead_code)]
|
||||||
fn test_image_color_type_unsupported(file: &str, expected_type: ColorType) {
|
fn test_image_color_type_unsupported(file: &str, expected_type: ColorType) {
|
||||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
|
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
|
||||||
let img_file = File::open(path).expect("Cannot find test image!");
|
let img_file = File::open(path).expect("Cannot find test image!");
|
||||||
@@ -73,6 +264,47 @@ fn test_cmyk_f32() {
|
|||||||
test_image_sum_f32("cmyk-3c-32b-float.tiff", ColorType::CMYK(32), 496.0405);
|
test_image_sum_f32("cmyk-3c-32b-float.tiff", ColorType::CMYK(32), 496.0405);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_palette_u1() {
|
||||||
|
test_image_sum_u8("palette-1c-1b.tiff", ColorType::Palette(1), 379313);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_palette_u4() {
|
||||||
|
test_image_sum_u8("palette-1c-4b.tiff", ColorType::Palette(4), 1480490);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_palette_u8() {
|
||||||
|
test_image_sum_u8("palette-1c-8b.tiff", ColorType::Palette(8), 2876948);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_palette_color_map() {
|
||||||
|
let path = PathBuf::from(TEST_IMAGE_DIR).join("palette-1c-8b.tiff");
|
||||||
|
let img_file = File::open(path).expect("Cannot find test image!");
|
||||||
|
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||||
|
decoder.next_image().expect("Cannot read image IFD");
|
||||||
|
|
||||||
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Palette(8));
|
||||||
|
|
||||||
|
let color_map = decoder
|
||||||
|
.color_map()
|
||||||
|
.expect("Palette image should have a color map");
|
||||||
|
// 8-bit palette: 3 * 256 = 768 entries
|
||||||
|
assert_eq!(color_map.len(), 768);
|
||||||
|
|
||||||
|
// Each section (R, G, B) has 256 entries
|
||||||
|
let r = &color_map[..256];
|
||||||
|
let g = &color_map[256..512];
|
||||||
|
let b = &color_map[512..768];
|
||||||
|
|
||||||
|
// Verify all values are valid u16 (non-zero palette, at least some variation)
|
||||||
|
assert!(r.iter().any(|&v| v > 0));
|
||||||
|
assert!(g.iter().any(|&v| v > 0));
|
||||||
|
assert!(b.iter().any(|&v| v > 0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gray_u8() {
|
fn test_gray_u8() {
|
||||||
test_image_sum_u8("minisblack-1c-8b.tiff", ColorType::Gray(8), 2840893);
|
test_image_sum_u8("minisblack-1c-8b.tiff", ColorType::Gray(8), 2840893);
|
||||||
@@ -80,7 +312,7 @@ fn test_gray_u8() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gray_u12() {
|
fn test_gray_u12() {
|
||||||
test_image_color_type_unsupported("12bit.cropped.tiff", ColorType::Gray(12));
|
test_image_sum_u16("12bit.cropped.tiff", ColorType::Gray(12), 10973);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -119,7 +351,7 @@ fn test_rgb_u8() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rgb_u12() {
|
fn test_rgb_u12() {
|
||||||
test_image_color_type_unsupported("12bit.cropped.rgb.tiff", ColorType::RGB(12));
|
test_image_sum_u16("12bit.cropped.rgb.tiff", ColorType::RGB(12), 32919);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -172,7 +404,10 @@ fn test_string_tags() {
|
|||||||
let img_file = File::open(path).expect("Cannot find test image!");
|
let img_file = File::open(path).expect("Cannot find test image!");
|
||||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||||
decoder.next_image().expect("Cannot read image IFD");
|
decoder.next_image().expect("Cannot read image IFD");
|
||||||
let software = decoder.get_tag(tiff::tags::Tag::Software).unwrap();
|
let software = decoder
|
||||||
|
.current_ifd()
|
||||||
|
.get_tag(tiff::tags::Tag::Software)
|
||||||
|
.unwrap();
|
||||||
match software {
|
match software {
|
||||||
ifd::Value::Ascii(s) => assert_eq!(
|
ifd::Value::Ascii(s) => assert_eq!(
|
||||||
&s,
|
&s,
|
||||||
@@ -199,7 +434,7 @@ fn test_decode_data() {
|
|||||||
decoder.next_image().expect("Cannot read image IFD");
|
decoder.next_image().expect("Cannot read image IFD");
|
||||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
||||||
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
||||||
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
|
if let DecodingSampleBuffer::U8(img_res) = decoder.read_image().unwrap() {
|
||||||
assert_eq!(image_data, img_res);
|
assert_eq!(image_data, img_res);
|
||||||
} else {
|
} else {
|
||||||
panic!("Wrong data type");
|
panic!("Wrong data type");
|
||||||
@@ -246,7 +481,8 @@ fn test_inner_access() {
|
|||||||
decoder.next_image().expect("Cannot read image IFD");
|
decoder.next_image().expect("Cannot read image IFD");
|
||||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
||||||
|
|
||||||
let c = decoder
|
let mut ifd = decoder.current_ifd();
|
||||||
|
let c = ifd
|
||||||
.get_tag(tiff::tags::Tag::Compression)
|
.get_tag(tiff::tags::Tag::Compression)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_u16()
|
.into_u16()
|
||||||
@@ -255,13 +491,13 @@ fn test_inner_access() {
|
|||||||
|
|
||||||
// Because the image is uncompressed, reading the first tile directly with the inner reader
|
// Because the image is uncompressed, reading the first tile directly with the inner reader
|
||||||
// should yield the same result as reading it with the decoder's read_chunk method.
|
// should yield the same result as reading it with the decoder's read_chunk method.
|
||||||
let first_offset = decoder
|
let first_offset = ifd
|
||||||
.get_tag_u32_vec(tiff::tags::Tag::TileOffsets)
|
.get_tag_u32_vec(tiff::tags::Tag::TileOffsets)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()
|
.next()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let first_byte_count = decoder
|
let first_byte_count = ifd
|
||||||
.get_tag_u32_vec(tiff::tags::Tag::TileByteCounts)
|
.get_tag_u32_vec(tiff::tags::Tag::TileByteCounts)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -279,7 +515,7 @@ fn test_inner_access() {
|
|||||||
let raw_sum: u64 = buf.into_iter().map(<u64>::from).sum();
|
let raw_sum: u64 = buf.into_iter().map(<u64>::from).sum();
|
||||||
|
|
||||||
match decoder.read_chunk(0).unwrap() {
|
match decoder.read_chunk(0).unwrap() {
|
||||||
DecodingResult::U8(chunk) => {
|
DecodingSampleBuffer::U8(chunk) => {
|
||||||
let sum: u64 = chunk.into_iter().map(<u64>::from).sum();
|
let sum: u64 = chunk.into_iter().map(<u64>::from).sum();
|
||||||
assert_eq!(sum, raw_sum);
|
assert_eq!(sum, raw_sum);
|
||||||
}
|
}
|
||||||
@@ -338,7 +574,7 @@ fn test_tiled_incremental() {
|
|||||||
|
|
||||||
for tile in 0..tiles {
|
for tile in 0..tiles {
|
||||||
match decoder.read_chunk(tile).unwrap() {
|
match decoder.read_chunk(tile).unwrap() {
|
||||||
DecodingResult::U8(res) => {
|
DecodingSampleBuffer::U8(res) => {
|
||||||
let sum: u64 = res.into_iter().map(<u64>::from).sum();
|
let sum: u64 = res.into_iter().map(<u64>::from).sum();
|
||||||
assert_eq!(sum, sums[tile as usize]);
|
assert_eq!(sum, sums[tile as usize]);
|
||||||
}
|
}
|
||||||
@@ -372,21 +608,21 @@ fn test_read_planar_bands() {
|
|||||||
|
|
||||||
// 1st band (red)
|
// 1st band (red)
|
||||||
match decoder.read_chunk(0).unwrap() {
|
match decoder.read_chunk(0).unwrap() {
|
||||||
DecodingResult::U8(chunk) => {
|
DecodingSampleBuffer::U8(chunk) => {
|
||||||
assert_eq!(chunk[0], 73);
|
assert_eq!(chunk[0], 73);
|
||||||
}
|
}
|
||||||
_ => panic!("Wrong bit depth"),
|
_ => panic!("Wrong bit depth"),
|
||||||
}
|
}
|
||||||
// 2nd band (green)
|
// 2nd band (green)
|
||||||
match decoder.read_chunk(chunks / 3).unwrap() {
|
match decoder.read_chunk(chunks / 3).unwrap() {
|
||||||
DecodingResult::U8(chunk) => {
|
DecodingSampleBuffer::U8(chunk) => {
|
||||||
assert_eq!(chunk[0], 51);
|
assert_eq!(chunk[0], 51);
|
||||||
}
|
}
|
||||||
_ => panic!("Wrong bit depth"),
|
_ => panic!("Wrong bit depth"),
|
||||||
}
|
}
|
||||||
// 3rd band (blue)
|
// 3rd band (blue)
|
||||||
match decoder.read_chunk(chunks / 3 * 2).unwrap() {
|
match decoder.read_chunk(chunks / 3 * 2).unwrap() {
|
||||||
DecodingResult::U8(chunk) => {
|
DecodingSampleBuffer::U8(chunk) => {
|
||||||
assert_eq!(chunk[0], 30);
|
assert_eq!(chunk[0], 30);
|
||||||
}
|
}
|
||||||
_ => panic!("Wrong bit depth"),
|
_ => panic!("Wrong bit depth"),
|
||||||
@@ -762,9 +998,62 @@ fn test_decode_huge_g4() {
|
|||||||
.read_image()
|
.read_image()
|
||||||
.expect("failed to decode huge G4 image");
|
.expect("failed to decode huge G4 image");
|
||||||
|
|
||||||
if let tiff::decoder::DecodingResult::U8(data) = result {
|
if let tiff::decoder::DecodingSampleBuffer::U8(data) = result {
|
||||||
assert_eq!(data.len(), (width as usize * height as usize));
|
assert_eq!(data.len(), (width as usize * height as usize));
|
||||||
} else {
|
} else {
|
||||||
panic!("Unexpected decoding result type");
|
panic!("Unexpected decoding result type");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- WhiteIsZero tests across bit depths ----
|
||||||
|
|
||||||
|
// WhiteIsZero + extended bit depth (12-bit gray, synthetic 4x2 image)
|
||||||
|
#[test]
|
||||||
|
fn test_gray_u12_white_is_zero() {
|
||||||
|
// Values: [0, 1000, 2000, 4095, 100, 500, 3000, 3500]
|
||||||
|
// After WhiteIsZero inversion: [4095, 3095, 2095, 0, 3995, 3595, 1095, 595]
|
||||||
|
test_image_sum_u16("miniswhite-1c-12b.tiff", ColorType::Gray(12), 18565);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WhiteIsZero + non-standard sub-byte depths (3-bit and 6-bit, synthetic 8x2 images)
|
||||||
|
// These verify per-sample inversion for packed depths where samples cross byte boundaries.
|
||||||
|
#[test]
|
||||||
|
fn test_gray_u3_white_is_zero() {
|
||||||
|
test_image_sum_u8("miniswhite-1c-3b.tiff", ColorType::Gray(3), 765);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gray_u6_white_is_zero() {
|
||||||
|
// Values: [0, 10, 20, 30, 40, 50, 60, 63] × 2 rows
|
||||||
|
// After inversion: [63, 53, 43, 33, 23, 13, 3, 0] × 2 rows
|
||||||
|
test_image_sum_u8("miniswhite-1c-6b.tiff", ColorType::Gray(6), 2124);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Non-standard sub-byte depths (BlackIsZero) ----
|
||||||
|
// Verify that odd bit depths (3, 5, 7) decode without error.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gray_u3() {
|
||||||
|
// 8x2 image, values [0..7, 7..0], packed to 6 bytes
|
||||||
|
test_image_sum_u8("minisblack-1c-3b.tiff", ColorType::Gray(3), 765);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gray_u5() {
|
||||||
|
// 8x2 image, packed to 10 bytes
|
||||||
|
test_image_sum_u8("minisblack-1c-5b.tiff", ColorType::Gray(5), 1275);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gray_u7() {
|
||||||
|
// 8x2 image, packed to 14 bytes
|
||||||
|
test_image_sum_u8("minisblack-1c-7b.tiff", ColorType::Gray(7), 1711);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Horizontal predictor + extended bit depth ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gray_u12_hpredict() {
|
||||||
|
// Delta-encoded 12-bit values: [100, 200, 300, 400, 1000, 2000, 3000, 4000]
|
||||||
|
test_image_sum_u16("hpredict-1c-12b.tiff", ColorType::Gray(12), 11000);
|
||||||
|
}
|
||||||
+289
-86
@@ -1,7 +1,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::{fs::File, path};
|
use std::{fs::File, path};
|
||||||
|
|
||||||
use tiff::decoder::{Decoder, DecodingResult};
|
use tiff::decoder::{Decoder, DecodingSampleBuffer};
|
||||||
|
use tiff::tags::{ByteOrder, Type};
|
||||||
|
use tiff::ColorType;
|
||||||
|
|
||||||
use libtest_mimic::{Arguments, Failed, Trial};
|
use libtest_mimic::{Arguments, Failed, Trial};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
@@ -9,95 +11,202 @@ use walkdir::WalkDir;
|
|||||||
const REFERENCE_DIR: &str = "tests/libtiffpic";
|
const REFERENCE_DIR: &str = "tests/libtiffpic";
|
||||||
|
|
||||||
// `find test/libtiffpic -type f | sort`
|
// `find test/libtiffpic -type f | sort`
|
||||||
const FILES: &[(&str, bool)] = &[
|
//
|
||||||
("tests/libtiffpic/caspian.tif", true),
|
// Each entry maps a file path to its expected behavior:
|
||||||
("tests/libtiffpic/cramps.tif", true),
|
// None = not a TIFF file, skip entirely
|
||||||
("tests/libtiffpic/cramps-tile.tif", true),
|
// Some(0) = TIFF with unsupported features, decode-only smoke test
|
||||||
("tests/libtiffpic/CVS/Entries", false),
|
// Some(nonzero) = TIFF with full CRC32 hash verification of decoded output
|
||||||
("tests/libtiffpic/CVS/Repository", false),
|
const FILES: &[(&str, Option<u32>)] = &[
|
||||||
("tests/libtiffpic/CVS/Root", false),
|
("tests/libtiffpic/caspian.tif", Some(0x4ea8d4e4)),
|
||||||
("tests/libtiffpic/depth/CVS/Entries", false),
|
("tests/libtiffpic/cramps.tif", Some(0xdfa2fba4)),
|
||||||
("tests/libtiffpic/depth/CVS/Repository", false),
|
("tests/libtiffpic/cramps-tile.tif", Some(0xdfa2fba4)),
|
||||||
("tests/libtiffpic/depth/CVS/Root", false),
|
("tests/libtiffpic/CVS/Entries", None),
|
||||||
("tests/libtiffpic/depth/flower-minisblack-02.tif", true),
|
("tests/libtiffpic/CVS/Repository", None),
|
||||||
("tests/libtiffpic/depth/flower-minisblack-04.tif", true),
|
("tests/libtiffpic/CVS/Root", None),
|
||||||
("tests/libtiffpic/depth/flower-minisblack-06.tif", true),
|
("tests/libtiffpic/depth/CVS/Entries", None),
|
||||||
("tests/libtiffpic/depth/flower-minisblack-08.tif", true),
|
("tests/libtiffpic/depth/CVS/Repository", None),
|
||||||
("tests/libtiffpic/depth/flower-minisblack-10.tif", true),
|
("tests/libtiffpic/depth/CVS/Root", None),
|
||||||
("tests/libtiffpic/depth/flower-minisblack-12.tif", true),
|
(
|
||||||
("tests/libtiffpic/depth/flower-minisblack-14.tif", true),
|
"tests/libtiffpic/depth/flower-minisblack-02.tif",
|
||||||
("tests/libtiffpic/depth/flower-minisblack-16.tif", true),
|
Some(0x1c90f515),
|
||||||
("tests/libtiffpic/depth/flower-minisblack-24.tif", true),
|
),
|
||||||
("tests/libtiffpic/depth/flower-minisblack-32.tif", true),
|
(
|
||||||
("tests/libtiffpic/depth/flower-palette-02.tif", true),
|
"tests/libtiffpic/depth/flower-minisblack-04.tif",
|
||||||
("tests/libtiffpic/depth/flower-palette-04.tif", true),
|
Some(0x0f4813cb),
|
||||||
("tests/libtiffpic/depth/flower-palette-08.tif", true),
|
),
|
||||||
("tests/libtiffpic/depth/flower-palette-16.tif", true),
|
(
|
||||||
("tests/libtiffpic/depth/flower-rgb-contig-02.tif", true),
|
"tests/libtiffpic/depth/flower-minisblack-06.tif",
|
||||||
("tests/libtiffpic/depth/flower-rgb-contig-04.tif", true),
|
Some(0xf0c0a901),
|
||||||
("tests/libtiffpic/depth/flower-rgb-contig-08.tif", true),
|
),
|
||||||
("tests/libtiffpic/depth/flower-rgb-contig-10.tif", true),
|
(
|
||||||
("tests/libtiffpic/depth/flower-rgb-contig-12.tif", true),
|
"tests/libtiffpic/depth/flower-minisblack-08.tif",
|
||||||
("tests/libtiffpic/depth/flower-rgb-contig-14.tif", true),
|
Some(0xe797b15b),
|
||||||
("tests/libtiffpic/depth/flower-rgb-contig-16.tif", true),
|
),
|
||||||
("tests/libtiffpic/depth/flower-rgb-contig-24.tif", true),
|
(
|
||||||
("tests/libtiffpic/depth/flower-rgb-contig-32.tif", true),
|
"tests/libtiffpic/depth/flower-minisblack-10.tif",
|
||||||
("tests/libtiffpic/depth/flower-rgb-planar-02.tif", true),
|
Some(0xdee442ec),
|
||||||
("tests/libtiffpic/depth/flower-rgb-planar-04.tif", true),
|
),
|
||||||
("tests/libtiffpic/depth/flower-rgb-planar-08.tif", true),
|
(
|
||||||
("tests/libtiffpic/depth/flower-rgb-planar-10.tif", true),
|
"tests/libtiffpic/depth/flower-minisblack-12.tif",
|
||||||
("tests/libtiffpic/depth/flower-rgb-planar-12.tif", true),
|
Some(0x4810c4e9),
|
||||||
("tests/libtiffpic/depth/flower-rgb-planar-14.tif", true),
|
),
|
||||||
("tests/libtiffpic/depth/flower-rgb-planar-16.tif", true),
|
(
|
||||||
("tests/libtiffpic/depth/flower-rgb-planar-24.tif", true),
|
"tests/libtiffpic/depth/flower-minisblack-14.tif",
|
||||||
("tests/libtiffpic/depth/flower-rgb-planar-32.tif", true),
|
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",
|
"tests/libtiffpic/depth/flower-separated-contig-08.tif",
|
||||||
true,
|
Some(0x97fb03a6),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"tests/libtiffpic/depth/flower-separated-contig-16.tif",
|
"tests/libtiffpic/depth/flower-separated-contig-16.tif",
|
||||||
true,
|
Some(0xb6ee0135),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"tests/libtiffpic/depth/flower-separated-planar-08.tif",
|
"tests/libtiffpic/depth/flower-separated-planar-08.tif",
|
||||||
true,
|
Some(0x502e93fa),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"tests/libtiffpic/depth/flower-separated-planar-16.tif",
|
"tests/libtiffpic/depth/flower-separated-planar-16.tif",
|
||||||
true,
|
Some(0x50e580d9),
|
||||||
),
|
),
|
||||||
("tests/libtiffpic/depth/genimages", false),
|
("tests/libtiffpic/depth/genimages", None),
|
||||||
("tests/libtiffpic/depth/README.txt", false),
|
("tests/libtiffpic/depth/README.txt", None),
|
||||||
("tests/libtiffpic/depth/summary.txt", false),
|
("tests/libtiffpic/depth/summary.txt", None),
|
||||||
("tests/libtiffpic/dscf0013.tif", true),
|
("tests/libtiffpic/dscf0013.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
||||||
("tests/libtiffpic/fax2d.tif", true),
|
("tests/libtiffpic/fax2d.tif", Some(0xc2f30c6f)),
|
||||||
// No support for direct CCITT Group 3 1D format
|
// No support for direct CCITT Group 3 1D format
|
||||||
("tests/libtiffpic/g3test.g3", false),
|
("tests/libtiffpic/g3test.g3", None),
|
||||||
("tests/libtiffpic/g3test.tif", true),
|
("tests/libtiffpic/g3test.tif", Some(0x304b606b)),
|
||||||
("tests/libtiffpic/jello.tif", true),
|
("tests/libtiffpic/jello.tif", Some(0xb03dbda9)),
|
||||||
("tests/libtiffpic/jim___ah.tif", true),
|
("tests/libtiffpic/jim___ah.tif", Some(0x81a27b7f)),
|
||||||
("tests/libtiffpic/jim___cg.tif", true),
|
("tests/libtiffpic/jim___cg.tif", Some(0xfd31747c)),
|
||||||
("tests/libtiffpic/jim___dg.tif", true),
|
("tests/libtiffpic/jim___dg.tif", Some(0x8c6a5e8f)),
|
||||||
("tests/libtiffpic/jim___gg.tif", true),
|
("tests/libtiffpic/jim___gg.tif", Some(0x8c6a5e8f)),
|
||||||
("tests/libtiffpic/ladoga.tif", true),
|
("tests/libtiffpic/ladoga.tif", Some(0x9b118511)),
|
||||||
("tests/libtiffpic/off_l16.tif", true),
|
("tests/libtiffpic/off_l16.tif", Some(0)), // LogLuv decoder output varies across platforms
|
||||||
("tests/libtiffpic/off_luv24.tif", true),
|
("tests/libtiffpic/off_luv24.tif", Some(0x2bee71a2)),
|
||||||
("tests/libtiffpic/off_luv32.tif", true),
|
("tests/libtiffpic/off_luv32.tif", Some(0)), // LogLuv decoder output varies across platforms
|
||||||
("tests/libtiffpic/oxford.tif", true),
|
("tests/libtiffpic/oxford.tif", Some(0x355d2e3e)),
|
||||||
("tests/libtiffpic/pc260001.tif", true),
|
("tests/libtiffpic/pc260001.tif", Some(0xdb56b753)),
|
||||||
("tests/libtiffpic/quad-jpeg.tif", true),
|
("tests/libtiffpic/quad-jpeg.tif", Some(0xcba22475)),
|
||||||
("tests/libtiffpic/quad-lzw.tif", true),
|
("tests/libtiffpic/quad-lzw.tif", Some(0)), // invalid LZW stream
|
||||||
("tests/libtiffpic/quad-tile.tif", true),
|
("tests/libtiffpic/quad-tile.tif", Some(0x6055f9ee)),
|
||||||
("tests/libtiffpic/README", false),
|
("tests/libtiffpic/README", None),
|
||||||
("tests/libtiffpic/smallliz.tif", true),
|
("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
||||||
("tests/libtiffpic/strike.tif", true),
|
("tests/libtiffpic/strike.tif", Some(0x016b0f6a)),
|
||||||
("tests/libtiffpic/text.tif", true),
|
("tests/libtiffpic/text.tif", Some(0)), // unsupported compression
|
||||||
("tests/libtiffpic/ycbcr-cat.tif", true),
|
("tests/libtiffpic/ycbcr-cat.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
||||||
("tests/libtiffpic/zackthecat.tif", true),
|
("tests/libtiffpic/zackthecat.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
||||||
];
|
];
|
||||||
|
|
||||||
fn main() {
|
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 candidates: HashMap<_, _> = FILES.iter().copied().collect();
|
||||||
let mut trials = Vec::new();
|
let mut trials = Vec::new();
|
||||||
|
|
||||||
@@ -114,22 +223,19 @@ fn main() {
|
|||||||
let path = path.replace(std::path::MAIN_SEPARATOR, "/"); // always use UNIX path separators
|
let path = path.replace(std::path::MAIN_SEPARATOR, "/"); // always use UNIX path separators
|
||||||
|
|
||||||
// Mark this path as consumed in either case by removing it.
|
// Mark this path as consumed in either case by removing it.
|
||||||
let Some(should_decode) = candidates.remove(path.as_str()) else {
|
let Some(expected_hash) = candidates.remove(path.as_str()) else {
|
||||||
panic!("File {} not in reference list", path);
|
panic!("File {} not in reference list", path);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if we should turn it into a test case.
|
// None = non-image file, skip
|
||||||
if !should_decode {
|
let Some(expected_hash) = expected_hash else {
|
||||||
continue;
|
continue;
|
||||||
}
|
};
|
||||||
|
|
||||||
let path = path.to_string();
|
let path = path.to_string();
|
||||||
trials.push(Trial::test(
|
trials.push(Trial::test(
|
||||||
path.to_string(),
|
path.to_string(),
|
||||||
move || -> Result<(), Failed> {
|
move || -> Result<(), Failed> { decode_and_verify(&path, expected_hash) },
|
||||||
decode_libtiffpic(&path)
|
|
||||||
.map_err(|e| Failed::from(format_args!("Decoding failed for {}: {}", path, e)))
|
|
||||||
},
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,17 +251,114 @@ fn main() {
|
|||||||
libtest_mimic::run(&args, trials).exit();
|
libtest_mimic::run(&args, trials).exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_libtiffpic(path: &str) -> Result<(), tiff::TiffError> {
|
/// Map DecodingResult variant to a TIFF Type for byte-order normalization.
|
||||||
let path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path);
|
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 img_file = File::open(path)?;
|
||||||
let mut decoder = Decoder::open(img_file)?;
|
let mut decoder = Decoder::open(img_file)?;
|
||||||
|
let mut hasher = crc32fast::Hasher::new();
|
||||||
|
let mut buffer = DecodingSampleBuffer::U8(vec![]);
|
||||||
|
|
||||||
let mut buffer = DecodingResult::U8(vec![]);
|
let mut page = 0u32;
|
||||||
while decoder.more_images() {
|
while decoder.more_images() {
|
||||||
decoder.next_image()?;
|
decoder.next_image()?;
|
||||||
let _layout = decoder.read_image_to_buffer(&mut buffer);
|
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(())
|
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}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use tiff::decoder::{Decoder, DecodingResult, Limits, TiffCodingUnit};
|
use tiff::decoder::{Decoder, DecodingSampleBuffer, Limits, TiffCodingUnit};
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -84,7 +84,7 @@ fn read_planes_by_limits() {
|
|||||||
let io = std::io::BufReader::new(file);
|
let io = std::io::BufReader::new(file);
|
||||||
|
|
||||||
let mut limits = Limits::default();
|
let mut limits = Limits::default();
|
||||||
let mut buffer = DecodingResult::U8(vec![]);
|
let mut buffer = DecodingSampleBuffer::U8(vec![]);
|
||||||
|
|
||||||
// Make room for 1 plane.
|
// Make room for 1 plane.
|
||||||
let mut tif = Decoder::open(io).unwrap();
|
let mut tif = Decoder::open(io).unwrap();
|
||||||
|
|||||||
+56
-27
@@ -1,6 +1,6 @@
|
|||||||
extern crate tiff;
|
extern crate tiff;
|
||||||
|
|
||||||
use tiff::decoder::{ifd, Decoder, DecodingResult};
|
use tiff::decoder::{ifd, Decoder, DecodingSampleBuffer};
|
||||||
use tiff::encoder::{colortype, Ifd, Ifd8, Predictor, SRational, TiffEncoder};
|
use tiff::encoder::{colortype, Ifd, Ifd8, Predictor, SRational, TiffEncoder};
|
||||||
use tiff::tags::Tag;
|
use tiff::tags::Tag;
|
||||||
use tiff::ColorType;
|
use tiff::ColorType;
|
||||||
@@ -40,6 +40,7 @@ fn encode_decode() {
|
|||||||
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
||||||
|
|
||||||
let mut all_tags = decoder
|
let mut all_tags = decoder
|
||||||
|
.current_ifd()
|
||||||
.tag_iter()
|
.tag_iter()
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -80,13 +81,13 @@ fn encode_decode() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decoder.get_tag(Tag::Artist).unwrap(),
|
decoder.current_ifd().get_tag(Tag::Artist).unwrap(),
|
||||||
ifd::Value::Ascii("Image-tiff".into())
|
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();
|
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);
|
assert_eq!(image_data, img_res);
|
||||||
} else {
|
} else {
|
||||||
panic!("Wrong data type");
|
panic!("Wrong data type");
|
||||||
@@ -125,6 +126,7 @@ fn encode_decode_big() {
|
|||||||
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
||||||
|
|
||||||
let mut all_tags = decoder
|
let mut all_tags = decoder
|
||||||
|
.current_ifd()
|
||||||
.tag_iter()
|
.tag_iter()
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -165,12 +167,12 @@ fn encode_decode_big() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decoder.get_tag(Tag::Artist).unwrap(),
|
decoder.current_ifd().get_tag(Tag::Artist).unwrap(),
|
||||||
ifd::Value::Ascii("Image-tiff".into())
|
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();
|
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);
|
assert_eq!(image_data, img_res);
|
||||||
} else {
|
} else {
|
||||||
panic!("Wrong data type");
|
panic!("Wrong data type");
|
||||||
@@ -258,11 +260,11 @@ macro_rules! test_roundtrip {
|
|||||||
|
|
||||||
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
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();
|
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||||
|
|
||||||
let image_data = match buffer {
|
let image_data = match buffer {
|
||||||
DecodingResult::$buffer(res) => res,
|
DecodingSampleBuffer::$buffer(res) => res,
|
||||||
_ => panic!("Wrong data type"),
|
_ => panic!("Wrong data type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -275,11 +277,11 @@ macro_rules! test_roundtrip {
|
|||||||
}
|
}
|
||||||
file.seek(SeekFrom::Start(0)).unwrap();
|
file.seek(SeekFrom::Start(0)).unwrap();
|
||||||
{
|
{
|
||||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
let mut buffer = tiff::decoder::DecodingSampleBuffer::U8(vec![]);
|
||||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||||
decoder.next_image().unwrap();
|
decoder.next_image().unwrap();
|
||||||
decoder.read_image_to_buffer(&mut buffer).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);
|
assert_eq!(*image_data, img_res);
|
||||||
} else {
|
} else {
|
||||||
panic!("Wrong data type");
|
panic!("Wrong data type");
|
||||||
@@ -418,9 +420,9 @@ fn test_gray_f32_floating_point_predictor() {
|
|||||||
decoder.next_image().unwrap();
|
decoder.next_image().unwrap();
|
||||||
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(32));
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(32));
|
||||||
|
|
||||||
let mut buffer = DecodingResult::F32(vec![]);
|
let mut buffer = DecodingSampleBuffer::F32(vec![]);
|
||||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||||
if let DecodingResult::F32(img_res) = buffer {
|
if let DecodingSampleBuffer::F32(img_res) = buffer {
|
||||||
assert_eq!(image_data.len(), img_res.len());
|
assert_eq!(image_data.len(), img_res.len());
|
||||||
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
|
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -465,9 +467,9 @@ fn test_rgb_f32_floating_point_predictor() {
|
|||||||
decoder.next_image().unwrap();
|
decoder.next_image().unwrap();
|
||||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(32));
|
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(32));
|
||||||
|
|
||||||
let mut buffer = DecodingResult::F32(vec![]);
|
let mut buffer = DecodingSampleBuffer::F32(vec![]);
|
||||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||||
if let DecodingResult::F32(img_res) = buffer {
|
if let DecodingSampleBuffer::F32(img_res) = buffer {
|
||||||
assert_eq!(image_data.len(), img_res.len());
|
assert_eq!(image_data.len(), img_res.len());
|
||||||
for (i, (orig, decoded)) in image_data.iter().zip(img_res.iter()).enumerate() {
|
for (i, (orig, decoded)) in image_data.iter().zip(img_res.iter()).enumerate() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -509,9 +511,9 @@ fn test_gray_f64_floating_point_predictor() {
|
|||||||
decoder.next_image().unwrap();
|
decoder.next_image().unwrap();
|
||||||
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(64));
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(64));
|
||||||
|
|
||||||
let mut buffer = DecodingResult::F64(vec![]);
|
let mut buffer = DecodingSampleBuffer::F64(vec![]);
|
||||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||||
if let DecodingResult::F64(img_res) = buffer {
|
if let DecodingSampleBuffer::F64(img_res) = buffer {
|
||||||
assert_eq!(image_data.len(), img_res.len());
|
assert_eq!(image_data.len(), img_res.len());
|
||||||
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
|
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -539,37 +541,57 @@ trait AssertDecode {
|
|||||||
|
|
||||||
impl<R: std::io::Read + std::io::Seek> AssertDecode for Decoder<R> {
|
impl<R: std::io::Read + std::io::Seek> AssertDecode for Decoder<R> {
|
||||||
fn assert_tag_u32(&mut self, tag: u16) -> u32 {
|
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> {
|
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()
|
.unwrap()
|
||||||
.into_u32_vec()
|
.into_u32_vec()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
fn assert_tag_i32(&mut self, tag: u16) -> i32 {
|
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> {
|
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()
|
.unwrap()
|
||||||
.into_i32_vec()
|
.into_i32_vec()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
fn assert_tag_u64(&mut self, tag: u16) -> u64 {
|
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> {
|
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()
|
.unwrap()
|
||||||
.into_u64_vec()
|
.into_u64_vec()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
fn assert_tag_i64(&mut self, tag: u16) -> i64 {
|
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> {
|
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()
|
.unwrap()
|
||||||
.into_i64_vec()
|
.into_i64_vec()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -776,13 +798,19 @@ fn test_rows_per_strip() {
|
|||||||
{
|
{
|
||||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||||
decoder.next_image().unwrap();
|
decoder.next_image().unwrap();
|
||||||
assert_eq!(decoder.get_tag_u64(Tag::RowsPerStrip).unwrap(), 2);
|
assert_eq!(
|
||||||
|
decoder
|
||||||
|
.current_ifd()
|
||||||
|
.get_tag_u64(Tag::RowsPerStrip)
|
||||||
|
.unwrap(),
|
||||||
|
2
|
||||||
|
);
|
||||||
assert_eq!(decoder.strip_count().unwrap(), 50);
|
assert_eq!(decoder.strip_count().unwrap(), 50);
|
||||||
|
|
||||||
for i in 0..50 {
|
for i in 0..50 {
|
||||||
let img2 = [i; 2 * 100];
|
let img2 = [i; 2 * 100];
|
||||||
match decoder.read_chunk(i as u32).unwrap() {
|
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:?}"),
|
other => panic!("Incorrect strip type {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -829,6 +857,7 @@ fn test_auxiliary_directory() {
|
|||||||
let _ = decoder.read_image().unwrap();
|
let _ = decoder.read_image().unwrap();
|
||||||
|
|
||||||
let exif_location = decoder
|
let exif_location = decoder
|
||||||
|
.current_ifd()
|
||||||
.get_tag(Tag::ExifDirectory)
|
.get_tag(Tag::ExifDirectory)
|
||||||
.expect("second directory missing Exif")
|
.expect("second directory missing Exif")
|
||||||
.into_ifd_pointer()
|
.into_ifd_pointer()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ extern crate tiff;
|
|||||||
|
|
||||||
use std::io::{Cursor, Seek, Write};
|
use std::io::{Cursor, Seek, Write};
|
||||||
use tiff::{
|
use tiff::{
|
||||||
decoder::{Decoder, DecodingResult},
|
decoder::{Decoder, DecodingSampleBuffer},
|
||||||
encoder::{
|
encoder::{
|
||||||
colortype::{self, ColorType},
|
colortype::{self, ColorType},
|
||||||
Compression, TiffEncoder, TiffValue,
|
Compression, TiffEncoder, TiffValue,
|
||||||
@@ -115,7 +115,7 @@ fn encode_decode_with_compression(compression: Compression) {
|
|||||||
// Check the RGB image
|
// Check the RGB image
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
match decoder.read_image() {
|
match decoder.read_image() {
|
||||||
Ok(DecodingResult::U16(image_data)) => image_data,
|
Ok(DecodingSampleBuffer::U16(image_data)) => image_data,
|
||||||
unexpected => panic!("Descoding RGB failed: {unexpected:?}"),
|
unexpected => panic!("Descoding RGB failed: {unexpected:?}"),
|
||||||
},
|
},
|
||||||
image_rgb.reference_data()
|
image_rgb.reference_data()
|
||||||
@@ -125,7 +125,7 @@ fn encode_decode_with_compression(compression: Compression) {
|
|||||||
decoder.next_image().unwrap();
|
decoder.next_image().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
match decoder.read_image() {
|
match decoder.read_image() {
|
||||||
Ok(DecodingResult::U8(image_data)) => image_data,
|
Ok(DecodingSampleBuffer::U8(image_data)) => image_data,
|
||||||
unexpected => panic!("Decoding grayscale failed: {unexpected:?}"),
|
unexpected => panic!("Decoding grayscale failed: {unexpected:?}"),
|
||||||
},
|
},
|
||||||
image_grayscale.reference_data()
|
image_grayscale.reference_data()
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large.
Load diff
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.
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 123 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user