Compare commits
65
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6e8ed772d | ||
|
|
9b452e93b7 | ||
|
|
96d37bb087 | ||
|
|
12f16dbfdd | ||
|
|
a8abdd2484 | ||
|
|
934fde185a | ||
|
|
3302936b08 | ||
|
|
a5b9a57e09 | ||
|
|
e27f6fe00d | ||
|
|
46b19cdacf | ||
|
|
c27fd94e1d | ||
|
|
f28f5fc830 | ||
|
|
3436a2222b | ||
|
|
31919c7bfc | ||
|
|
d0c9198016 | ||
|
|
8f83f0fa82 | ||
|
|
f3f9ff1244 | ||
|
|
a7dda6f4b1 | ||
|
|
35c14f1e3c | ||
|
|
a9cf5e17b9 | ||
|
|
6276defac6 | ||
|
|
dee0c2fb11 | ||
|
|
11133cb1e3 | ||
|
|
8ad05b85db | ||
|
|
4a9e1d152b | ||
|
|
1c6001ea5d | ||
|
|
c0043c1a84 | ||
|
|
c7a1176381 | ||
|
|
a658c09f09 | ||
|
|
0309349e1b | ||
|
|
279851a9a9 | ||
|
|
22b4c39254 | ||
|
|
f11a1c5393 | ||
|
|
c4285aea4a | ||
|
|
a1426ca802 | ||
|
|
66e6567acc | ||
|
|
5947d3b72e | ||
|
|
0fdf1e0502 | ||
|
|
9924ea2721 | ||
|
|
30e04f3381 | ||
|
|
dfd0191403 | ||
|
|
cf9eee64ad | ||
|
|
a59db23341 | ||
|
|
e8103509fa | ||
|
|
26b18c5184 | ||
|
|
b0712855b1 | ||
|
|
ea6e2a8d80 | ||
|
|
032421634f | ||
|
|
d760bd3442 | ||
|
|
a70e05e51e | ||
|
|
dc25c4eaea | ||
|
|
d7961a2fcf | ||
|
|
2166fd591a | ||
|
|
7380611a7b | ||
|
|
149492ca6e | ||
|
|
e4c3c1fcfd | ||
|
|
6a3282e9f2 | ||
|
|
fa8a911f79 | ||
|
|
dc94cd4436 | ||
|
|
a42b16ab72 | ||
|
|
8246b55e8f | ||
|
|
399469f512 | ||
|
|
19caae4f53 | ||
|
|
61dbcc1392 | ||
|
|
d9d4f0a905 |
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
|
||||||
+3
-1
@@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
/target/
|
/target/
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
tests/benches/corpus/
|
||||||
|
.idea
|
||||||
|
.agentbridge
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
# Version 0.12.0 (unreleased)
|
# Version 0.12.0 (unreleased)
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Decoding a tiled image whose width is not a multiple of the tile width no
|
||||||
|
longer corrupts the columns below the right border tiles when extra samples
|
||||||
|
are discarded (e.g. RGB with an unspecified extra sample). The horizontal
|
||||||
|
tile padding was written past the end of the output row, spilling into the
|
||||||
|
leftmost columns of the following row.
|
||||||
|
|
||||||
Additions:
|
Additions:
|
||||||
- Added the `TiffHeader` type. It can be used to create a `Decoder` in a state
|
- Added the `TiffHeader` type. It can be used to create a `Decoder` in a state
|
||||||
before the first IFD and allows opening files at an arbitrary first IFD
|
before the first IFD and allows opening files at an arbitrary first IFD
|
||||||
|
|||||||
+14
-4
@@ -1,8 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tiff"
|
name = "ndbioimage-tiff"
|
||||||
version = "0.11.3"
|
version = "0.11.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
|
||||||
|
|
||||||
# note: when changed, also update test runner in `.github/workflows/rust.yml`
|
# note: when changed, also update test runner in `.github/workflows/rust.yml`
|
||||||
rust-version = "1.88.0"
|
rust-version = "1.88.0"
|
||||||
@@ -16,6 +15,9 @@ categories = ["multimedia::images", "multimedia::encoding"]
|
|||||||
|
|
||||||
exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
|
exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "tiff"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
half = { version = "2.4.1" }
|
half = { version = "2.4.1" }
|
||||||
quick-error = "2.0.1"
|
quick-error = "2.0.1"
|
||||||
@@ -23,8 +25,9 @@ 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.14", optional = true }
|
||||||
|
zrip-decode = { version = "0.8.2", features = ["paranoid"], 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 }
|
||||||
|
|
||||||
@@ -45,6 +48,9 @@ jpeg = ["dep:zune-jpeg"]
|
|||||||
lzw = ["dep:weezl"]
|
lzw = ["dep:weezl"]
|
||||||
webp = ["dep:image-webp"]
|
webp = ["dep:image-webp"]
|
||||||
zstd = ["dep:zstd"]
|
zstd = ["dep:zstd"]
|
||||||
|
# Fully safe Rust but somewhat slower Zstd decoding.
|
||||||
|
# When both zstd and zstd-safe-rust are enabled, zstd takes precedence.
|
||||||
|
zstd-safe-rust = ["dep:zrip-decode"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "lzw"
|
name = "lzw"
|
||||||
@@ -54,6 +60,10 @@ harness = false
|
|||||||
name = "encode"
|
name = "encode"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "decode"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "decode_libtiffpic"
|
name = "decode_libtiffpic"
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
TIFF decoding and encoding library in pure Rust
|
TIFF decoding and encoding library in pure Rust
|
||||||
|
|
||||||
|
Forked from https://github.com/image-rs/image-tiff. Changes: read images containing RGBPalette frames, ignoring colormaps.
|
||||||
|
|
||||||
## Supported
|
## Supported
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
@@ -55,4 +57,4 @@ This crate uses [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) in order t
|
|||||||
|
|
||||||
After installing it with `cargo install cargo-fuzz` on a nightly rustc, the
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-8
@@ -2,28 +2,30 @@ use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingSampleBuffer, Decod
|
|||||||
|
|
||||||
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 {
|
||||||
eprintln!("Usage: decode FILE");
|
eprintln!("Usage: decode FILE [--lenient]");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let lenient = std::env::args().nth(2).as_deref() == Some("--lenient");
|
||||||
|
|
||||||
let file = std::fs::File::open(image)?;
|
let file = std::fs::File::open(image)?;
|
||||||
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)?.with_lenient(lenient);
|
||||||
|
|
||||||
let mut data = DecodingSampleBuffer::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(())
|
||||||
|
|||||||
@@ -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"
|
||||||
+70
-40
@@ -363,7 +363,7 @@ impl ::std::fmt::Debug for Entry {
|
|||||||
fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
|
fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
|
||||||
fmt.write_str(&format!(
|
fmt.write_str(&format!(
|
||||||
"Entry {{ type_: {:?}, count: {:?}, offset: {:?} }}",
|
"Entry {{ type_: {:?}, count: {:?}, offset: {:?} }}",
|
||||||
self.type_, self.count, &self.offset
|
self.type_, self.count, self.offset
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -583,81 +583,93 @@ 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
|
||||||
.chunks_exact(2)
|
.as_chunks::<2>()
|
||||||
.map(|ch| Short(u16::from_ne_bytes(ch.try_into().unwrap()))),
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|&ch| Short(u16::from_ne_bytes(ch))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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
|
||||||
.chunks_exact(2)
|
.as_chunks::<2>()
|
||||||
.map(|ch| SignedShort(i16::from_ne_bytes(ch.try_into().unwrap()))),
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|&ch| SignedShort(i16::from_ne_bytes(ch))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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
|
||||||
.chunks_exact(4)
|
.as_chunks::<4>()
|
||||||
.map(|ch| Unsigned(u32::from_ne_bytes(ch.try_into().unwrap()))),
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|&ch| Unsigned(u32::from_ne_bytes(ch))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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
|
||||||
.chunks_exact(4)
|
.as_chunks::<4>()
|
||||||
.map(|ch| Signed(i32::from_ne_bytes(ch.try_into().unwrap()))),
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|&ch| Signed(i32::from_ne_bytes(ch))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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
|
||||||
.chunks_exact(4)
|
.as_chunks::<4>()
|
||||||
.map(|ch| Float(f32::from_ne_bytes(ch.try_into().unwrap()))),
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|&ch| Float(f32::from_ne_bytes(ch))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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
|
||||||
.chunks_exact(8)
|
.as_chunks::<8>()
|
||||||
.map(|ch| Double(f64::from_ne_bytes(ch.try_into().unwrap()))),
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|&ch| Double(f64::from_ne_bytes(ch))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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.as_chunks::<8>().0.iter().map(|ch| {
|
||||||
Rational(
|
Rational(
|
||||||
u32::from_ne_bytes(ch[..4].try_into().unwrap()),
|
u32::from_ne_bytes(ch[..4].try_into().unwrap()),
|
||||||
u32::from_ne_bytes(ch[4..].try_into().unwrap()),
|
u32::from_ne_bytes(ch[4..].try_into().unwrap()),
|
||||||
@@ -666,9 +678,9 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::SRATIONAL => {
|
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.as_chunks::<8>().0.iter().map(|ch| {
|
||||||
SRational(
|
SRational(
|
||||||
i32::from_ne_bytes(ch[..4].try_into().unwrap()),
|
i32::from_ne_bytes(ch[..4].try_into().unwrap()),
|
||||||
i32::from_ne_bytes(ch[4..].try_into().unwrap()),
|
i32::from_ne_bytes(ch[4..].try_into().unwrap()),
|
||||||
@@ -677,50 +689,58 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::LONG8 => {
|
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
|
||||||
.chunks_exact(8)
|
.as_chunks::<8>()
|
||||||
.map(|ch| UnsignedBig(u64::from_ne_bytes(ch.try_into().unwrap()))),
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|&ch| UnsignedBig(u64::from_ne_bytes(ch))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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
|
||||||
.chunks_exact(8)
|
.as_chunks::<8>()
|
||||||
.map(|ch| SignedBig(i64::from_ne_bytes(ch.try_into().unwrap()))),
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|&ch| SignedBig(i64::from_ne_bytes(ch))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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
|
||||||
.chunks_exact(4)
|
.as_chunks::<4>()
|
||||||
.map(|ch| Ifd(u32::from_ne_bytes(ch.try_into().unwrap()))),
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|&ch| Ifd(u32::from_ne_bytes(ch))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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
|
||||||
.chunks_exact(8)
|
.as_chunks::<8>()
|
||||||
.map(|ch| IfdBig(u64::from_ne_bytes(ch.try_into().unwrap()))),
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|&ch| IfdBig(u64::from_ne_bytes(ch))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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 +866,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);
|
||||||
}
|
}
|
||||||
|
|||||||
+151
-42
@@ -36,6 +36,9 @@ impl TileAttributes {
|
|||||||
pub fn tiles_down(&self) -> usize {
|
pub fn tiles_down(&self) -> usize {
|
||||||
self.image_height.div_ceil(self.tile_length)
|
self.image_height.div_ceil(self.tile_length)
|
||||||
}
|
}
|
||||||
|
pub fn tiles_per_plane(&self) -> usize {
|
||||||
|
self.tiles_across() * self.tiles_down()
|
||||||
|
}
|
||||||
fn padding_right(&self) -> usize {
|
fn padding_right(&self) -> usize {
|
||||||
(self.tile_width - self.image_width % self.tile_width) % self.tile_width
|
(self.tile_width - self.image_width % self.tile_width) % self.tile_width
|
||||||
}
|
}
|
||||||
@@ -97,6 +100,7 @@ pub(crate) struct Image {
|
|||||||
/// run on samples directly and thus skip the outer byte order entirely, except potentially
|
/// run on samples directly and thus skip the outer byte order entirely, except potentially
|
||||||
/// they have interior byte order treatment.
|
/// they have interior byte order treatment.
|
||||||
pub decompression_to_host_endian: bool,
|
pub decompression_to_host_endian: bool,
|
||||||
|
pub lenient: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Describes how to read a tile-aligned portion of the image.
|
/// Describes how to read a tile-aligned portion of the image.
|
||||||
@@ -158,6 +162,7 @@ impl Image {
|
|||||||
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,
|
||||||
|
lenient: bool,
|
||||||
) -> TiffResult<Image> {
|
) -> TiffResult<Image> {
|
||||||
let mut tag_reader = TagReader { decoder, ifd };
|
let mut tag_reader = TagReader { decoder, ifd };
|
||||||
|
|
||||||
@@ -174,7 +179,7 @@ impl Image {
|
|||||||
.map(Value::into_u16)
|
.map(Value::into_u16)
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.and_then(PhotometricInterpretation::from_u16)
|
.and_then(PhotometricInterpretation::from_u16)
|
||||||
.ok_or(TiffUnsupportedError::UnknownInterpretation)?;
|
.unwrap_or(PhotometricInterpretation::BlackIsZero);
|
||||||
|
|
||||||
// Try to parse both the compression method and the number, format, and bits of the included samples.
|
// Try to parse both the compression method and the number, format, and bits of the included samples.
|
||||||
// If they are not explicitly specified, those tags are reset to their default values and not carried from previous images.
|
// If they are not explicitly specified, those tags are reset to their default values and not carried from previous images.
|
||||||
@@ -454,8 +459,9 @@ impl Image {
|
|||||||
match tag_reader.find_tag(Tag::ColorMap)? {
|
match tag_reader.find_tag(Tag::ColorMap)? {
|
||||||
Some(v) => {
|
Some(v) => {
|
||||||
let map = v.into_u16_vec()?;
|
let map = v.into_u16_vec()?;
|
||||||
let expected_len = 3usize
|
let expected_len = 1usize
|
||||||
.checked_mul(1usize << bits_per_sample)
|
.checked_shl(bits_per_sample as u32)
|
||||||
|
.and_then(|n| n.checked_mul(3))
|
||||||
.ok_or(TiffError::LimitsExceeded)?;
|
.ok_or(TiffError::LimitsExceeded)?;
|
||||||
if map.len() != expected_len {
|
if map.len() != expected_len {
|
||||||
return Err(TiffError::FormatError(
|
return Err(TiffError::FormatError(
|
||||||
@@ -464,11 +470,11 @@ impl Image {
|
|||||||
}
|
}
|
||||||
Some(map)
|
Some(map)
|
||||||
}
|
}
|
||||||
None => {
|
// A missing palette is not an error. The pixel values of an indexed image are just
|
||||||
return Err(TiffError::FormatError(
|
// indices into the palette, and the palette itself is only display information, so
|
||||||
TiffFormatError::RequiredTagNotFound(Tag::ColorMap),
|
// the image still decodes. Callers observe the absence as `None` from
|
||||||
));
|
// `Decoder::color_map`.
|
||||||
}
|
None => None,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -499,6 +505,7 @@ impl Image {
|
|||||||
compression_method,
|
compression_method,
|
||||||
CompressionMethod::SgiLog | CompressionMethod::SgiLog24
|
CompressionMethod::SgiLog | CompressionMethod::SgiLog24
|
||||||
),
|
),
|
||||||
|
lenient,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,10 +521,13 @@ impl Image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 = match self.extra_samples.as_slice().first() {
|
||||||
self.extra_samples.as_slice(),
|
Some(ExtraSamples::AssociatedAlpha) | Some(ExtraSamples::UnassociatedAlpha) => true,
|
||||||
[ExtraSamples::AssociatedAlpha, ..] | [ExtraSamples::UnassociatedAlpha, ..]
|
// In lenient mode, keep unspecified alpha interpretation logic compatible with libtiff
|
||||||
);
|
// See Issue #341
|
||||||
|
Some(ExtraSamples::Unspecified) if self.lenient && self.samples > 3 => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
match self.photometric_interpretation {
|
match self.photometric_interpretation {
|
||||||
PhotometricInterpretation::RGB => match self.photometric_samples {
|
PhotometricInterpretation::RGB => match self.photometric_samples {
|
||||||
@@ -590,20 +600,30 @@ impl Image {
|
|||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
// Unsupported due to inherently heterogeneous sample types. This is represented as:
|
// CIE L*a*b* (TIFF6 Section 23, pp 110-115) encodes a/b as signed integers
|
||||||
// ```
|
// (int8 at 8-bit, int16 at 16-bit) with 0 neutral, while ICC L*a*b*
|
||||||
// struct TiffLab /* Interpretation 8* {
|
// (photometric=9) uses unsigned samples with the neutral at the middle of
|
||||||
// pub L: u8, // SampleFormat::Uint
|
// the range. `SampleFormat` applies uniformly to all samples, so it can't
|
||||||
// pub a: i8, // SampleFormat::Int
|
// describe the L=unsigned + a/b=signed mix; instead we take a/b as signed
|
||||||
// pub b: i8, // SampleFormat::Int
|
// from the photometric interpretation (requiring `SampleFormat::Uint`, like
|
||||||
// }
|
// the ICCLab arm) and re-bias them to the unsigned ICCLab encoding in
|
||||||
// ```
|
// post-processing (see `cielab_to_icclab`). libtiff does the same, decoding
|
||||||
PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
|
// both depths (`putcontig8bitCIELab8` / `putcontig8bitCIELab16`, selected by
|
||||||
|
// `initCIELabConversion` in tif_getimage.c line 2216
|
||||||
|
// @ 04884181e8903915038be553bfab404302d98ea0).
|
||||||
|
PhotometricInterpretation::CIELab => match self.photometric_samples {
|
||||||
|
3 if matches!(self.bits_per_sample, 8 | 16)
|
||||||
|
&& matches!(self.sample_format, SampleFormat::Uint) =>
|
||||||
|
{
|
||||||
|
Ok(ColorType::Lab(self.bits_per_sample))
|
||||||
|
}
|
||||||
|
_ => Err(TiffError::UnsupportedError(
|
||||||
TiffUnsupportedError::InterpretationWithBits(
|
TiffUnsupportedError::InterpretationWithBits(
|
||||||
PhotometricInterpretation::CIELab,
|
PhotometricInterpretation::CIELab,
|
||||||
vec![self.bits_per_sample; self.samples as usize],
|
vec![self.bits_per_sample; self.samples as usize],
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
|
},
|
||||||
PhotometricInterpretation::CIELogL => match self.samples {
|
PhotometricInterpretation::CIELogL => match self.samples {
|
||||||
1 if matches!(self.bits_per_sample, 32)
|
1 if matches!(self.bits_per_sample, 32)
|
||||||
&& matches!(self.sample_format, SampleFormat::IEEEFP) =>
|
&& matches!(self.sample_format, SampleFormat::IEEEFP) =>
|
||||||
@@ -659,7 +679,11 @@ impl Image {
|
|||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
PhotometricInterpretation::RGBPalette => Ok(ColorType::Palette(self.bits_per_sample)),
|
PhotometricInterpretation::RGBPalette => Ok(ColorType::Palette(self.bits_per_sample)),
|
||||||
PhotometricInterpretation::TransparencyMask => Err(TiffError::UnsupportedError(
|
// TransparencyMask is a single-channel mask, treat as grayscale.
|
||||||
|
PhotometricInterpretation::TransparencyMask => {
|
||||||
|
Ok(ColorType::Gray(self.bits_per_sample))
|
||||||
|
}
|
||||||
|
PhotometricInterpretation::Unknown(_) => 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],
|
||||||
@@ -711,6 +735,8 @@ impl Image {
|
|||||||
)),
|
)),
|
||||||
#[cfg(feature = "zstd")]
|
#[cfg(feature = "zstd")]
|
||||||
CompressionMethod::ZSTD => Box::new(zstd::Decoder::new(reader)?),
|
CompressionMethod::ZSTD => Box::new(zstd::Decoder::new(reader)?),
|
||||||
|
#[cfg(all(not(feature = "zstd"), feature = "zstd-safe-rust"))]
|
||||||
|
CompressionMethod::ZSTD => Box::new(zrip_decode::streaming::FrameDecoder::new(reader)),
|
||||||
CompressionMethod::PackBits => Box::new(PackBitsReader::new(reader, compressed_length)),
|
CompressionMethod::PackBits => Box::new(PackBitsReader::new(reader, compressed_length)),
|
||||||
#[cfg(feature = "deflate")]
|
#[cfg(feature = "deflate")]
|
||||||
CompressionMethod::Deflate | CompressionMethod::OldDeflate => {
|
CompressionMethod::Deflate | CompressionMethod::OldDeflate => {
|
||||||
@@ -993,7 +1019,15 @@ impl Image {
|
|||||||
}
|
}
|
||||||
ChunkType::Tile => {
|
ChunkType::Tile => {
|
||||||
let tile_attrs = self.tile_attributes.as_ref().unwrap();
|
let tile_attrs = self.tile_attributes.as_ref().unwrap();
|
||||||
let (padding_right, padding_down) = tile_attrs.get_padding(chunk_index as usize);
|
// The chunk index counts the tiles of all planes, but the tile grid repeats per
|
||||||
|
// plane, so only the position within the plane decides the padding.
|
||||||
|
let tiles_per_plane = tile_attrs.tiles_per_plane();
|
||||||
|
let tile = if tiles_per_plane == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
chunk_index as usize % tiles_per_plane
|
||||||
|
};
|
||||||
|
let (padding_right, padding_down) = tile_attrs.get_padding(tile);
|
||||||
|
|
||||||
let tile_width = tile_attrs.tile_width - padding_right;
|
let tile_width = tile_attrs.tile_width - padding_right;
|
||||||
let tile_length = tile_attrs.tile_length - padding_down;
|
let tile_length = tile_attrs.tile_length - padding_down;
|
||||||
@@ -1009,6 +1043,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,
|
||||||
@@ -1034,6 +1069,7 @@ impl Image {
|
|||||||
| ColorType::YCbCr(n)
|
| ColorType::YCbCr(n)
|
||||||
| ColorType::Gray(n)
|
| ColorType::Gray(n)
|
||||||
| ColorType::Palette(n)
|
| ColorType::Palette(n)
|
||||||
|
| ColorType::Lab(n)
|
||||||
| ColorType::Multiband {
|
| ColorType::Multiband {
|
||||||
bit_depth: n,
|
bit_depth: n,
|
||||||
num_samples: _,
|
num_samples: _,
|
||||||
@@ -1045,6 +1081,7 @@ impl Image {
|
|||||||
| ColorType::YCbCr(n)
|
| ColorType::YCbCr(n)
|
||||||
| ColorType::Gray(n)
|
| ColorType::Gray(n)
|
||||||
| ColorType::Palette(n)
|
| ColorType::Palette(n)
|
||||||
|
| ColorType::Lab(n)
|
||||||
| ColorType::Multiband {
|
| ColorType::Multiband {
|
||||||
bit_depth: n,
|
bit_depth: n,
|
||||||
num_samples: _,
|
num_samples: _,
|
||||||
@@ -1188,14 +1225,26 @@ impl Image {
|
|||||||
super::fix_endianness_and_predict(
|
super::fix_endianness_and_predict(
|
||||||
out_row,
|
out_row,
|
||||||
color_type.bit_depth(),
|
color_type.bit_depth(),
|
||||||
samples,
|
data_samples,
|
||||||
ByteOrder::native(),
|
ByteOrder::native(),
|
||||||
predictor,
|
predictor,
|
||||||
|
scratch,
|
||||||
);
|
);
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
// An n-bit predictor delta is stored modulo 2^n, but the
|
||||||
super::invert_colors(out_row, color_type, self.sample_format)?;
|
// reconstruction above accumulated it in the promoted u16/u32
|
||||||
}
|
// storage width. Reduce back modulo the declared depth so both
|
||||||
|
// the inverted and non-inverted paths see exact, in-range
|
||||||
|
// values (2^n divides the storage modulus, so one mask after
|
||||||
|
// the row is exact).
|
||||||
|
super::mask_samples_to_bit_depth(out_row, tiff_bps, out_bps, self.sample_format);
|
||||||
|
|
||||||
|
super::post_process_row(
|
||||||
|
out_row,
|
||||||
|
photometric_interpretation,
|
||||||
|
color_type,
|
||||||
|
self.sample_format,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
} else if is_output_chunk_rows && is_all_bits {
|
} 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.
|
||||||
@@ -1209,12 +1258,26 @@ impl Image {
|
|||||||
samples,
|
samples,
|
||||||
decompressed_byte_order,
|
decompressed_byte_order,
|
||||||
predictor,
|
predictor,
|
||||||
|
scratch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
// No-op in this branch: promoted layouts (declared depth < storage
|
||||||
super::invert_colors(tile, color_type, self.sample_format)?;
|
// width) are handled exclusively by the unpacking branch above.
|
||||||
}
|
// Called in every integer branch so they cannot diverge.
|
||||||
|
super::mask_samples_to_bit_depth(
|
||||||
|
tile,
|
||||||
|
layout.tiff_bits_per_sample,
|
||||||
|
layout.output_bits_per_sample,
|
||||||
|
self.sample_format,
|
||||||
|
);
|
||||||
|
|
||||||
|
super::post_process_row(
|
||||||
|
tile,
|
||||||
|
photometric_interpretation,
|
||||||
|
color_type,
|
||||||
|
self.sample_format,
|
||||||
|
)?;
|
||||||
} else if chunk_row_bytes > data_row_bytes && self.predictor == Predictor::FloatingPoint {
|
} else if chunk_row_bytes > data_row_bytes && self.predictor == Predictor::FloatingPoint {
|
||||||
// The floating point predictor shuffles the padding bytes into the encoded output, so
|
// The floating point predictor shuffles the padding bytes into the encoded output, so
|
||||||
// this case is handled specially when needed.
|
// this case is handled specially when needed.
|
||||||
@@ -1224,14 +1287,28 @@ 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 {
|
// No masking here: the floating point predictor requires
|
||||||
super::invert_colors(row, color_type, self.sample_format)?;
|
// IEEEFP samples, which are never depth-promoted.
|
||||||
}
|
super::post_process_row(
|
||||||
|
row,
|
||||||
|
photometric_interpretation,
|
||||||
|
color_type,
|
||||||
|
self.sample_format,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
} else if is_all_bits {
|
} else if is_all_bits {
|
||||||
// We read row-by-row but each row fits in its output buffer.
|
// We read row-by-row but each row fits in its output buffer.
|
||||||
@@ -1255,11 +1332,25 @@ impl Image {
|
|||||||
samples,
|
samples,
|
||||||
decompressed_byte_order,
|
decompressed_byte_order,
|
||||||
predictor,
|
predictor,
|
||||||
|
scratch,
|
||||||
);
|
);
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
// No-op in this branch: promoted layouts only occur in the
|
||||||
super::invert_colors(row, color_type, self.sample_format)?;
|
// unpacking branch. Called so the integer branches cannot
|
||||||
}
|
// diverge.
|
||||||
|
super::mask_samples_to_bit_depth(
|
||||||
|
row,
|
||||||
|
layout.tiff_bits_per_sample,
|
||||||
|
layout.output_bits_per_sample,
|
||||||
|
self.sample_format,
|
||||||
|
);
|
||||||
|
|
||||||
|
super::post_process_row(
|
||||||
|
row,
|
||||||
|
photometric_interpretation,
|
||||||
|
color_type,
|
||||||
|
self.sample_format,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// The encoded data potentially takes up more space than the output data so we must be
|
// The encoded data potentially takes up more space than the output data so we must be
|
||||||
@@ -1286,6 +1377,10 @@ impl Image {
|
|||||||
let photo_range = photometric_bit_end / 8..bits_per_pixel / 8;
|
let photo_range = photometric_bit_end / 8..bits_per_pixel / 8;
|
||||||
let mut encoded = vec![0u8; chunk_row_bytes];
|
let mut encoded = vec![0u8; chunk_row_bytes];
|
||||||
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
|
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
|
||||||
|
// Clip to the chunk's data width: border chunks are stored at the full chunk
|
||||||
|
// width, and writing their horizontal padding would spill into the following
|
||||||
|
// output row (and predict/invert would run past the chunk's own columns).
|
||||||
|
let row = &mut row[..data_row_bytes];
|
||||||
reader.read_exact(&mut encoded)?;
|
reader.read_exact(&mut encoded)?;
|
||||||
|
|
||||||
Self::compact_photometric_bytes(&mut encoded, row, &photo_range);
|
Self::compact_photometric_bytes(&mut encoded, row, &photo_range);
|
||||||
@@ -1296,11 +1391,25 @@ impl Image {
|
|||||||
samples,
|
samples,
|
||||||
decompressed_byte_order,
|
decompressed_byte_order,
|
||||||
predictor,
|
predictor,
|
||||||
|
scratch,
|
||||||
);
|
);
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
// No-op in this branch: promoted layouts only occur in the
|
||||||
super::invert_colors(row, color_type, self.sample_format)?;
|
// unpacking branch. Called so the integer branches cannot
|
||||||
}
|
// diverge.
|
||||||
|
super::mask_samples_to_bit_depth(
|
||||||
|
row,
|
||||||
|
layout.tiff_bits_per_sample,
|
||||||
|
layout.output_bits_per_sample,
|
||||||
|
self.sample_format,
|
||||||
|
);
|
||||||
|
|
||||||
|
super::post_process_row(
|
||||||
|
row,
|
||||||
|
photometric_interpretation,
|
||||||
|
color_type,
|
||||||
|
self.sample_format,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
+321
-59
@@ -3,8 +3,8 @@ use std::io::{self, Read, Seek};
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use crate::tags::{
|
use crate::tags::{
|
||||||
ByteOrder, IfdPointer, PlanarConfiguration, Predictor, SampleFormat, Tag, TiffVariant, Type,
|
ByteOrder, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat,
|
||||||
ValueBuffer,
|
Tag, TiffVariant, Type, ValueBuffer,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
|
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
|
||||||
@@ -254,6 +254,11 @@ pub struct Decoder<R> {
|
|||||||
/// Map from the ifd into the `ifd_offsets` ordered list.
|
/// Map from the ifd into the `ifd_offsets` ordered list.
|
||||||
seen_ifds: cycles::IfdCycles,
|
seen_ifds: cycles::IfdCycles,
|
||||||
image: Option<Image>,
|
image: Option<Image>,
|
||||||
|
/// Reusable scratch buffer for the floating-point predictor. Stored here so that the
|
||||||
|
/// allocation persists across strips/tiles and images, avoiding per-row heap allocation.
|
||||||
|
scratch: Vec<u8>,
|
||||||
|
/// Leniency flag that allows libtiff-like TIFF color interpretation behavior
|
||||||
|
lenient: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All the information needed to read and interpret byte slices from the underlying file, i.e. to
|
/// All the information needed to read and interpret byte slices from the underlying file, i.e. to
|
||||||
@@ -273,13 +278,37 @@ pub struct IfdDecoder<'lt> {
|
|||||||
inner: tag_reader::TagReader<'lt, dyn tag_reader::EntryDecoder + 'lt>,
|
inner: tag_reader::TagReader<'lt, dyn tag_reader::EntryDecoder + 'lt>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: u16) {
|
/// Byte-level prefix sum with compile-time stride. Each element is the wrapping sum of itself and
|
||||||
fn one_byte_predict<const N: usize>(buf: &mut [u8]) {
|
/// the element N positions earlier. For N >= 2, the N interleaved channels are independent streams.
|
||||||
for i in N..buf.len() {
|
///
|
||||||
buf[i] = buf[i].wrapping_add(buf[i - N]);
|
/// For N=3 (RGB) and N=4 (RGBA), we use an explicit `[u8; N]` accumulator so LLVM sees each
|
||||||
|
/// channel as an independent scalar chain and can unroll effectively. Without this, LLVM emits a
|
||||||
|
/// scalar byte-at-a-time loop for N=3 because 3 doesn't map cleanly to SIMD widths.
|
||||||
|
fn one_byte_predict<const N: usize>(buf: &mut [u8]) {
|
||||||
|
if N == 0 || buf.len() <= N {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the first N bytes as the initial accumulator state.
|
||||||
|
let mut acc = [0u8; N];
|
||||||
|
acc.copy_from_slice(&buf[..N]);
|
||||||
|
|
||||||
|
// Process N bytes per iteration. Each channel's prefix sum is independent.
|
||||||
|
let chunks = buf[N..].as_chunks_mut::<N>();
|
||||||
|
for chunk in &mut chunks.0.iter_mut() {
|
||||||
|
for c in 0..N {
|
||||||
|
acc[c] = chunk[c].wrapping_add(acc[c]);
|
||||||
|
chunk[c] = acc[c];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle trailing bytes (when buf.len() - N is not a multiple of N).
|
||||||
|
for (c, byte) in chunks.1.iter_mut().enumerate() {
|
||||||
|
*byte = byte.wrapping_add(acc[c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: u16) {
|
||||||
fn two_bytes_predict<const N: usize>(buf: &mut [u8]) {
|
fn two_bytes_predict<const N: usize>(buf: &mut [u8]) {
|
||||||
for i in (2 * N..buf.len()).step_by(2) {
|
for i in (2 * N..buf.len()).step_by(2) {
|
||||||
let v = u16::from_ne_bytes(buf[i..][..2].try_into().unwrap());
|
let v = u16::from_ne_bytes(buf[i..][..2].try_into().unwrap());
|
||||||
@@ -364,65 +393,145 @@ fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: u16) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn predict_f32(input: &mut [u8], output: &mut [u8], samples: u16) {
|
/// Undo the floating-point predictor: byte-level prefix sum, then de-interleave byte planes back
|
||||||
let samples = usize::from(samples);
|
/// into native-endian samples.
|
||||||
|
///
|
||||||
|
/// The TIFF float predictor splits each N-byte sample into N separate byte planes within the row,
|
||||||
|
/// then delta-encodes each plane. Decoding reverses this: prefix-sum to undo the deltas, then
|
||||||
|
/// transpose the N planes back into packed samples.
|
||||||
|
fn predict_f32(input: &mut [u8], output: &mut [[u8; 4]], samples: u16) {
|
||||||
|
one_byte_predict_generic(input, usize::from(samples));
|
||||||
|
deinterleave_f32_planes(input, output);
|
||||||
|
}
|
||||||
|
|
||||||
for i in samples..input.len() {
|
fn predict_f16(input: &mut [u8], output: &mut [[u8; 2]], samples: u16) {
|
||||||
input[i] = input[i].wrapping_add(input[i - samples]);
|
one_byte_predict_generic(input, usize::from(samples));
|
||||||
|
deinterleave_f16_planes(input, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn predict_f64(input: &mut [u8], output: &mut [[u8; 8]], samples: u16) {
|
||||||
|
one_byte_predict_generic(input, usize::from(samples));
|
||||||
|
deinterleave_f64_planes(input, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Byte-level prefix sum: `buf[i] += buf[i - stride]` for all `i >= stride`.
|
||||||
|
///
|
||||||
|
/// Same operation as `one_byte_predict<N>` but dispatched at runtime. The most common float
|
||||||
|
/// channel counts (1, 3, 4) get dedicated loops so LLVM can see the independent channel streams
|
||||||
|
/// and vectorize each one separately.
|
||||||
|
fn one_byte_predict_generic(buf: &mut [u8], stride: usize) {
|
||||||
|
match stride {
|
||||||
|
1 => one_byte_predict::<1>(buf),
|
||||||
|
2 => one_byte_predict::<2>(buf),
|
||||||
|
3 => one_byte_predict::<3>(buf),
|
||||||
|
4 => one_byte_predict::<4>(buf),
|
||||||
|
_ => {
|
||||||
|
for i in stride..buf.len() {
|
||||||
|
buf[i] = buf[i].wrapping_add(buf[i - stride]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i, chunk) in output.chunks_mut(4).enumerate() {
|
|
||||||
chunk.copy_from_slice(&u32::to_ne_bytes(u32::from_be_bytes([
|
|
||||||
input[i],
|
|
||||||
input[input.len() / 4 + i],
|
|
||||||
input[input.len() / 4 * 2 + i],
|
|
||||||
input[input.len() / 4 * 3 + i],
|
|
||||||
])));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn predict_f16(input: &mut [u8], output: &mut [u8], samples: u16) {
|
/// Transpose byte-planes back into packed f16/f32/f64 samples with big-endian to native conversion.
|
||||||
let samples = usize::from(samples);
|
///
|
||||||
|
/// Input layout: `[plane0: n bytes][plane1: n bytes]...[planeP: n bytes]` where each plane holds
|
||||||
|
/// one byte position of each sample. Output: packed native-endian samples.
|
||||||
|
///
|
||||||
|
/// Each function splits the input into equal-sized plane slices, then gathers one byte per plane
|
||||||
|
/// per sample. Using `from_be_bytes` for endian conversion lets LLVM recognise the byte-swap idiom
|
||||||
|
/// and emit efficient code (bswap or movbe).
|
||||||
|
fn deinterleave_f32_planes(input: &[u8], output: &mut [[u8; 4]]) {
|
||||||
|
let n = output.len();
|
||||||
|
if n == 0 || input.len() < n * 4 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (p0, rest) = input.split_at(n);
|
||||||
|
let (p1, rest) = rest.split_at(n);
|
||||||
|
let (p2, p3) = rest.split_at(n);
|
||||||
|
|
||||||
for i in samples..input.len() {
|
// Batch 4 samples per iteration. Fixed-size array slicing eliminates bounds checks.
|
||||||
input[i] = input[i].wrapping_add(input[i - samples]);
|
let (out_chunks, out_rest) = output.as_chunks_mut::<4>();
|
||||||
|
let (p0_chunks, _) = p0.as_chunks::<4>();
|
||||||
|
let (p1_chunks, _) = p1.as_chunks::<4>();
|
||||||
|
let (p2_chunks, _) = p2.as_chunks::<4>();
|
||||||
|
let (p3_chunks, _) = p3.as_chunks::<4>();
|
||||||
|
|
||||||
|
for ((((out, b0), b1), b2), b3) in out_chunks
|
||||||
|
.iter_mut()
|
||||||
|
.zip(p0_chunks)
|
||||||
|
.zip(p1_chunks)
|
||||||
|
.zip(p2_chunks)
|
||||||
|
.zip(p3_chunks)
|
||||||
|
{
|
||||||
|
let s0 = u32::from_be_bytes([b0[0], b1[0], b2[0], b3[0]]).to_ne_bytes();
|
||||||
|
let s1 = u32::from_be_bytes([b0[1], b1[1], b2[1], b3[1]]).to_ne_bytes();
|
||||||
|
let s2 = u32::from_be_bytes([b0[2], b1[2], b2[2], b3[2]]).to_ne_bytes();
|
||||||
|
let s3 = u32::from_be_bytes([b0[3], b1[3], b2[3], b3[3]]).to_ne_bytes();
|
||||||
|
out[0] = s0;
|
||||||
|
out[1] = s1;
|
||||||
|
out[2] = s2;
|
||||||
|
out[3] = s3;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i, chunk) in output.chunks_mut(2).enumerate() {
|
// Remaining 0–3 samples.
|
||||||
chunk.copy_from_slice(&u16::to_ne_bytes(u16::from_be_bytes([
|
let base = out_chunks.len() * 4;
|
||||||
input[i],
|
for (j, out) in out_rest.iter_mut().enumerate() {
|
||||||
input[input.len() / 2 + i],
|
*out = u32::from_be_bytes([p0[base + j], p1[base + j], p2[base + j], p3[base + j]])
|
||||||
])));
|
.to_ne_bytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn predict_f64(input: &mut [u8], output: &mut [u8], samples: u16) {
|
fn deinterleave_f16_planes(input: &[u8], output: &mut [[u8; 2]]) {
|
||||||
let samples = usize::from(samples);
|
let n = output.len();
|
||||||
|
if n == 0 || input.len() < n * 2 {
|
||||||
for i in samples..input.len() {
|
return;
|
||||||
input[i] = input[i].wrapping_add(input[i - samples]);
|
|
||||||
}
|
}
|
||||||
|
let (p0, p1) = input.split_at(n);
|
||||||
|
|
||||||
for (i, chunk) in output.chunks_mut(8).enumerate() {
|
for (out, (b0, b1)) in output.iter_mut().zip(p0.iter().zip(p1)) {
|
||||||
chunk.copy_from_slice(&u64::to_ne_bytes(u64::from_be_bytes([
|
*out = u16::from_be_bytes([*b0, *b1]).to_ne_bytes();
|
||||||
input[i],
|
|
||||||
input[input.len() / 8 + i],
|
|
||||||
input[input.len() / 8 * 2 + i],
|
|
||||||
input[input.len() / 8 * 3 + i],
|
|
||||||
input[input.len() / 8 * 4 + i],
|
|
||||||
input[input.len() / 8 * 5 + i],
|
|
||||||
input[input.len() / 8 * 6 + i],
|
|
||||||
input[input.len() / 8 * 7 + i],
|
|
||||||
])));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deinterleave_f64_planes(input: &[u8], output: &mut [[u8; 8]]) {
|
||||||
|
let n = output.len();
|
||||||
|
if n == 0 || input.len() < n * 8 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (p0, rest) = input.split_at(n);
|
||||||
|
let (p1, rest) = rest.split_at(n);
|
||||||
|
let (p2, rest) = rest.split_at(n);
|
||||||
|
let (p3, rest) = rest.split_at(n);
|
||||||
|
let (p4, rest) = rest.split_at(n);
|
||||||
|
let (p5, rest) = rest.split_at(n);
|
||||||
|
let (p6, p7) = rest.split_at(n);
|
||||||
|
|
||||||
|
for (out, (((((((b0, b1), b2), b3), b4), b5), b6), b7)) in output.iter_mut().zip(
|
||||||
|
p0.iter()
|
||||||
|
.zip(p1)
|
||||||
|
.zip(p2)
|
||||||
|
.zip(p3)
|
||||||
|
.zip(p4)
|
||||||
|
.zip(p5)
|
||||||
|
.zip(p6)
|
||||||
|
.zip(p7),
|
||||||
|
) {
|
||||||
|
*out = u64::from_be_bytes([*b0, *b1, *b2, *b3, *b4, *b5, *b6, *b7]).to_ne_bytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fix byte order and apply the inverse predictor for one row of decoded data.
|
||||||
|
///
|
||||||
|
/// `scratch` is a reusable buffer for the floating-point predictor (which needs a working copy of
|
||||||
|
/// the input). Callers should pass the same buffer across rows to avoid per-row heap allocation.
|
||||||
fn fix_endianness_and_predict(
|
fn fix_endianness_and_predict(
|
||||||
buf: &mut [u8],
|
buf: &mut [u8],
|
||||||
bit_depth: u8,
|
bit_depth: u8,
|
||||||
samples: u16,
|
samples: u16,
|
||||||
byte_order: ByteOrder,
|
byte_order: ByteOrder,
|
||||||
predictor: Predictor,
|
predictor: Predictor,
|
||||||
|
scratch: &mut Vec<u8>,
|
||||||
) {
|
) {
|
||||||
match predictor {
|
match predictor {
|
||||||
Predictor::None => {
|
Predictor::None => {
|
||||||
@@ -433,17 +542,82 @@ fn fix_endianness_and_predict(
|
|||||||
rev_hpredict_nsamp(buf, bit_depth, samples);
|
rev_hpredict_nsamp(buf, bit_depth, samples);
|
||||||
}
|
}
|
||||||
Predictor::FloatingPoint => {
|
Predictor::FloatingPoint => {
|
||||||
let mut buffer_copy = buf.to_vec();
|
// The float predictor reads from scattered byte planes, so it needs a separate input
|
||||||
|
// buffer.
|
||||||
|
scratch.resize(buf.len(), 0);
|
||||||
|
scratch.copy_from_slice(buf);
|
||||||
match bit_depth {
|
match bit_depth {
|
||||||
16 => predict_f16(&mut buffer_copy, buf, samples),
|
16 => {
|
||||||
32 => predict_f32(&mut buffer_copy, buf, samples),
|
let (output, rest) = buf.as_chunks_mut::<2>();
|
||||||
64 => predict_f64(&mut buffer_copy, buf, samples),
|
debug_assert!(rest.is_empty(), "f16 row buffer not a multiple of 2 bytes");
|
||||||
|
predict_f16(scratch, output, samples);
|
||||||
|
}
|
||||||
|
32 => {
|
||||||
|
let (output, rest) = buf.as_chunks_mut::<4>();
|
||||||
|
debug_assert!(rest.is_empty(), "f32 row buffer not a multiple of 4 bytes");
|
||||||
|
predict_f32(scratch, output, samples);
|
||||||
|
}
|
||||||
|
64 => {
|
||||||
|
let (output, rest) = buf.as_chunks_mut::<8>();
|
||||||
|
debug_assert!(rest.is_empty(), "f64 row buffer not a multiple of 8 bytes");
|
||||||
|
predict_f64(scratch, output, samples);
|
||||||
|
}
|
||||||
_ => unreachable!("Caller should have validated arguments. Please file a bug."),
|
_ => unreachable!("Caller should have validated arguments. Please file a bug."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reduce promoted extended-depth samples (9–15 bits → `u16`, 17–31 bits →
|
||||||
|
/// `u32`) modulo their declared bit depth, i.e. mask with `(1 << n) - 1`.
|
||||||
|
///
|
||||||
|
/// An n-bit horizontal-predicted sample stores its delta in exactly n bits,
|
||||||
|
/// i.e. modulo `2^n`, so reconstruction is also defined modulo `2^n`. The
|
||||||
|
/// predictor accumulates `prev + delta` in the promoted storage width
|
||||||
|
/// (`u16`/`u32`, see `rev_hpredict_nsamp`); since `2^n` divides the storage
|
||||||
|
/// modulus, masking once after the predictor yields exactly the mod-`2^n`
|
||||||
|
/// value for every sample — including legitimate wrap-arounds (a 9-bit
|
||||||
|
/// `511, 0` pair is stored as delta `1`, and `511 + 1 = 512` masks back to
|
||||||
|
/// `0`). Masking also bounds any out-of-range sample in malformed input, so
|
||||||
|
/// all downstream consumers (`invert_colors`' `max - v` and the non-inverted
|
||||||
|
/// output path) see in-range values. For samples already in range it is a
|
||||||
|
/// no-op.
|
||||||
|
///
|
||||||
|
/// This runs in every integer decode branch of `expand_chunk` so the
|
||||||
|
/// branches cannot diverge. Outside the unpacking branch it reduces to the
|
||||||
|
/// two guards below and touches nothing: promoted layouts (`declared <
|
||||||
|
/// storage`) exist only there — standard depths have `declared == storage`,
|
||||||
|
/// and sub-byte depths keep byte-granular storage.
|
||||||
|
#[inline]
|
||||||
|
fn mask_samples_to_bit_depth(
|
||||||
|
buf: &mut [u8],
|
||||||
|
declared_bits: u8,
|
||||||
|
storage_bits: u8,
|
||||||
|
sample_format: SampleFormat,
|
||||||
|
) {
|
||||||
|
// Only unsigned samples are promoted with zero-extension; signed
|
||||||
|
// extended depths would need sign extension instead and are not
|
||||||
|
// supported by the unpacking path.
|
||||||
|
if sample_format != SampleFormat::Uint {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match storage_bits {
|
||||||
|
16 if declared_bits < 16 => {
|
||||||
|
let mask = (1u16 << declared_bits) - 1;
|
||||||
|
for chunk in buf.as_chunks_mut::<2>().0 {
|
||||||
|
*chunk = (u16::from_ne_bytes(*chunk) & mask).to_ne_bytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
32 if declared_bits < 32 => {
|
||||||
|
let mask = (1u32 << declared_bits) - 1;
|
||||||
|
for chunk in buf.as_chunks_mut::<4>().0 {
|
||||||
|
*chunk = (u32::from_ne_bytes(*chunk) & mask).to_ne_bytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn invert_colors(
|
fn invert_colors(
|
||||||
buf: &mut [u8],
|
buf: &mut [u8],
|
||||||
color_type: ColorType,
|
color_type: ColorType,
|
||||||
@@ -537,6 +711,56 @@ fn invert_colors(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Re-bias CIE L*a*b* a/b channels from signed to unsigned encoding.
|
||||||
|
///
|
||||||
|
/// CIELab (photometric=8) stores a/b as signed integers where 0 is neutral;
|
||||||
|
/// ICCLab (photometric=9) stores them as unsigned with the neutral shifted to
|
||||||
|
/// the middle of the range. The two differ by exactly half the sample range, so
|
||||||
|
/// the conversion is a wrapping add of 2^(bits-1) — i.e. flipping the sign bit —
|
||||||
|
/// applied per a/b sample. `bit_depth` is 8 or 16 (guaranteed by `colortype`);
|
||||||
|
/// samples are in native byte order here (post `fix_endianness_and_predict`).
|
||||||
|
/// See libtiff's `TIFFCIELabToXYZ` for reference (tif_color.c line 45
|
||||||
|
/// @ 04884181e8903915038be553bfab404302d98ea0).
|
||||||
|
fn cielab_to_icclab(buf: &mut [u8], bit_depth: u8) {
|
||||||
|
match bit_depth {
|
||||||
|
16 => {
|
||||||
|
for pixel in buf.as_chunks_mut::<6>().0 {
|
||||||
|
// pixel is [L, a, b] as three native-endian u16 samples.
|
||||||
|
let (samples, _) = pixel.as_chunks_mut::<2>();
|
||||||
|
for s in &mut samples[1..=2] {
|
||||||
|
*s = u16::from_ne_bytes(*s).wrapping_add(0x8000).to_ne_bytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
for [_, a, b] in buf.as_chunks_mut::<3>().0 {
|
||||||
|
*a = a.wrapping_add(128);
|
||||||
|
*b = b.wrapping_add(128);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply photometric-interpretation-specific post-processing to a decoded row.
|
||||||
|
///
|
||||||
|
/// Some interpretations need a fixup once the raw samples are in the output
|
||||||
|
/// buffer: `WhiteIsZero` inverts intensities, and `CIELab` re-biases the signed
|
||||||
|
/// a/b channels into the unsigned ICCLab encoding. These are mutually exclusive,
|
||||||
|
/// so at most one applies to any given row.
|
||||||
|
fn post_process_row(
|
||||||
|
buf: &mut [u8],
|
||||||
|
photometric_interpretation: PhotometricInterpretation,
|
||||||
|
color_type: ColorType,
|
||||||
|
sample_format: SampleFormat,
|
||||||
|
) -> TiffResult<()> {
|
||||||
|
match photometric_interpretation {
|
||||||
|
PhotometricInterpretation::WhiteIsZero => invert_colors(buf, color_type, sample_format)?,
|
||||||
|
PhotometricInterpretation::CIELab => cielab_to_icclab(buf, color_type.bit_depth()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Fix endianness. If `byte_order` matches the host, then conversion is a no-op.
|
/// Fix endianness. If `byte_order` matches the host, then conversion is a no-op.
|
||||||
fn fix_endianness(buf: &mut [u8], byte_order: ByteOrder, bit_depth: u8) {
|
fn fix_endianness(buf: &mut [u8], byte_order: ByteOrder, bit_depth: u8) {
|
||||||
let host = ByteOrder::native();
|
let host = ByteOrder::native();
|
||||||
@@ -627,6 +851,8 @@ impl TiffHeader {
|
|||||||
},
|
},
|
||||||
seen_ifds: cycles::IfdCycles::new(),
|
seen_ifds: cycles::IfdCycles::new(),
|
||||||
image: None,
|
image: None,
|
||||||
|
scratch: Vec::new(),
|
||||||
|
lenient: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -709,16 +935,35 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Use a lenient decoder.
|
||||||
|
///
|
||||||
|
/// Configuring this make the parser respect libtiff's behavior to maintain compatibility with
|
||||||
|
/// some older softwares (e.g. some old Adobe suite products).
|
||||||
|
pub fn with_lenient(mut self, lenient: bool) -> Decoder<R> {
|
||||||
|
self.lenient = lenient;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: this tuple would be a good candidate for a proxy, like IfdDecoder. That has been
|
// FIXME: this tuple would be a good candidate for a proxy, like IfdDecoder. That has been
|
||||||
// requested in some form or another in a number of issues.
|
// requested in some form or another in a number of issues.
|
||||||
#[expect(clippy::unnecessary_unwrap)] // Lifetimes would overlap due to return.
|
#[expect(clippy::unnecessary_unwrap)] // Lifetimes would overlap due to return.
|
||||||
fn ensure_image_and_reader(&mut self) -> TiffResult<(&mut Image, &mut ValueReader<R>)> {
|
fn ensure_image_and_reader(
|
||||||
|
&mut self,
|
||||||
|
) -> TiffResult<(&mut Image, &mut ValueReader<R>, &mut Vec<u8>)> {
|
||||||
if self.image.is_some() {
|
if self.image.is_some() {
|
||||||
return Ok((self.image.as_mut().unwrap(), &mut self.value_reader));
|
return Ok((
|
||||||
|
self.image.as_mut().unwrap(),
|
||||||
|
&mut self.value_reader,
|
||||||
|
&mut self.scratch,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let image = Image::from_reader(&mut self.value_reader, &self.directory)?;
|
let image = Image::from_reader(&mut self.value_reader, &self.directory, self.lenient)?;
|
||||||
Ok((self.image.insert(image), &mut self.value_reader))
|
Ok((
|
||||||
|
self.image.insert(image),
|
||||||
|
&mut self.value_reader,
|
||||||
|
&mut self.scratch,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_image(&mut self) -> TiffResult<&mut Image> {
|
fn ensure_image(&mut self) -> TiffResult<&mut Image> {
|
||||||
@@ -848,6 +1093,8 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
directory: self.directory.clone(),
|
directory: self.directory.clone(),
|
||||||
current_ifd_pointer: self.current_ifd_pointer,
|
current_ifd_pointer: self.current_ifd_pointer,
|
||||||
image: core::mem::take(&mut self.image),
|
image: core::mem::take(&mut self.image),
|
||||||
|
scratch: core::mem::take(&mut self.scratch),
|
||||||
|
lenient: self.lenient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,7 +1143,11 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
/// error is returned.
|
/// error is returned.
|
||||||
pub fn seek_to_image(&mut self, ifd_index: usize) -> TiffResult<()> {
|
pub fn seek_to_image(&mut self, ifd_index: usize) -> TiffResult<()> {
|
||||||
let (offset, directory) = self.find_nth_ifd(ifd_index)?;
|
let (offset, directory) = self.find_nth_ifd(ifd_index)?;
|
||||||
self.image = Some(Image::from_reader(&mut self.value_reader, &directory)?);
|
self.image = Some(Image::from_reader(
|
||||||
|
&mut self.value_reader,
|
||||||
|
&directory,
|
||||||
|
self.lenient,
|
||||||
|
)?);
|
||||||
self.current_ifd_pointer = Some(offset);
|
self.current_ifd_pointer = Some(offset);
|
||||||
self.directory = directory;
|
self.directory = directory;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -989,7 +1240,11 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
pub fn next_image(&mut self) -> TiffResult<()> {
|
pub fn next_image(&mut self) -> TiffResult<()> {
|
||||||
let next_ifd = self.directory.next();
|
let next_ifd = self.directory.next();
|
||||||
let (offset, directory) = self.read_chained_ifd(self.current_ifd_pointer, next_ifd)?;
|
let (offset, directory) = self.read_chained_ifd(self.current_ifd_pointer, next_ifd)?;
|
||||||
self.image = Some(Image::from_reader(&mut self.value_reader, &directory)?);
|
self.image = Some(Image::from_reader(
|
||||||
|
&mut self.value_reader,
|
||||||
|
&directory,
|
||||||
|
self.lenient,
|
||||||
|
)?);
|
||||||
self.current_ifd_pointer = Some(offset);
|
self.current_ifd_pointer = Some(offset);
|
||||||
self.directory = directory;
|
self.directory = directory;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1033,7 +1288,7 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
/// Utility to create a reader like [`Self::read_entry`], from the current IFD.
|
/// Utility to create a reader like [`Self::read_entry`], from the current IFD.
|
||||||
///
|
///
|
||||||
/// Returns an `Ok(None)` if the tag is not found in the current directory. See
|
/// Returns an `Ok(None)` if the tag is not found in the current directory. See
|
||||||
/// [`Self::read_entry´] of which this is a simple wrapper.
|
/// [`Self::read_entry`] of which this is a simple wrapper.
|
||||||
pub fn read_tag(&mut self, tag: Tag) -> TiffResult<Option<EntryBytesReader<'_, R>>> {
|
pub fn read_tag(&mut self, tag: Tag) -> TiffResult<Option<EntryBytesReader<'_, R>>> {
|
||||||
match self.current_ifd().find_entry(tag) {
|
match self.current_ifd().find_entry(tag) {
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
@@ -1323,10 +1578,10 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
chunk_index: u32,
|
chunk_index: u32,
|
||||||
layout: &image::ReadoutLayout,
|
layout: &image::ReadoutLayout,
|
||||||
) -> TiffResult<()> {
|
) -> TiffResult<()> {
|
||||||
let (image, value_reader) = self.ensure_image_and_reader()?;
|
let (image, value_reader, scratch) = self.ensure_image_and_reader()?;
|
||||||
let offset = image.chunk_file_range(chunk_index)?.0;
|
let offset = image.chunk_file_range(chunk_index)?.0;
|
||||||
value_reader.reader.goto_offset(offset)?;
|
value_reader.reader.goto_offset(offset)?;
|
||||||
image.expand_chunk(value_reader, buffer, layout, chunk_index)?;
|
image.expand_chunk(value_reader, buffer, layout, chunk_index, scratch)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1478,7 +1733,7 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
slice: TiffCodingUnit,
|
slice: TiffCodingUnit,
|
||||||
buffer: &mut [u8],
|
buffer: &mut [u8],
|
||||||
) -> TiffResult<()> {
|
) -> TiffResult<()> {
|
||||||
let (image, value_reader) = self.ensure_image_and_reader()?;
|
let (image, value_reader, scratch) = self.ensure_image_and_reader()?;
|
||||||
|
|
||||||
let (width, height) = image.chunk_data_dimensions(slice.0)?;
|
let (width, height) = image.chunk_data_dimensions(slice.0)?;
|
||||||
let readout = image.readout_for_size(width, height)?;
|
let readout = image.readout_for_size(width, height)?;
|
||||||
@@ -1505,7 +1760,13 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
let chunk = slice.0 + idx as u32 * readout.chunks_per_plane;
|
let chunk = slice.0 + idx as u32 * readout.chunks_per_plane;
|
||||||
let offset = image.chunk_offsets[chunk as usize];
|
let offset = image.chunk_offsets[chunk as usize];
|
||||||
value_reader.reader.goto_offset(offset)?;
|
value_reader.reader.goto_offset(offset)?;
|
||||||
image.expand_chunk(value_reader, &mut buffer[plane_offset..], readout, chunk)?;
|
image.expand_chunk(
|
||||||
|
value_reader,
|
||||||
|
&mut buffer[plane_offset..],
|
||||||
|
readout,
|
||||||
|
chunk,
|
||||||
|
scratch,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1676,7 +1937,7 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
/// Returns an error if the buffer fits less than one plane. In particular, for non-planar
|
/// Returns an error if the buffer fits less than one plane. In particular, for non-planar
|
||||||
/// images returns an error if the buffer does not fit the required size.
|
/// images returns an error if the buffer does not fit the required size.
|
||||||
pub fn read_image_bytes(&mut self, buffer: &mut [u8]) -> TiffResult<()> {
|
pub fn read_image_bytes(&mut self, buffer: &mut [u8]) -> TiffResult<()> {
|
||||||
let (image, value_reader) = self.ensure_image_and_reader()?;
|
let (image, value_reader, scratch) = self.ensure_image_and_reader()?;
|
||||||
let readout = image.readout_for_image()?;
|
let readout = image.readout_for_image()?;
|
||||||
|
|
||||||
let ref layout @ image::PlaneLayout {
|
let ref layout @ image::PlaneLayout {
|
||||||
@@ -1709,6 +1970,7 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
&mut buffer[plane_offset..][buffer_offset..],
|
&mut buffer[plane_offset..][buffer_offset..],
|
||||||
readout,
|
readout,
|
||||||
chunk,
|
chunk,
|
||||||
|
scratch,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-8
@@ -141,14 +141,32 @@ pub struct LZWReader<R: Read> {
|
|||||||
impl<R: Read> LZWReader<R> {
|
impl<R: Read> LZWReader<R> {
|
||||||
/// Wraps a reader
|
/// Wraps a reader
|
||||||
pub fn new(reader: R, compressed_length: usize) -> LZWReader<R> {
|
pub fn new(reader: R, compressed_length: usize) -> LZWReader<R> {
|
||||||
let configuration =
|
let mut buffered = BufReader::with_capacity(
|
||||||
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
|
|
||||||
.with_yield_on_full_buffer(true);
|
|
||||||
Self {
|
|
||||||
reader: BufReader::with_capacity(
|
|
||||||
(32 * 1024).min(compressed_length),
|
(32 * 1024).min(compressed_length),
|
||||||
reader.take(u64::try_from(compressed_length).unwrap()),
|
reader.take(u64::try_from(compressed_length).unwrap()),
|
||||||
),
|
);
|
||||||
|
|
||||||
|
// `Compression = 5` covers both the current LZW variant and the pre-TIFF-6.0
|
||||||
|
// "old-style" one, with no tag to distinguish them. New-style is MSB-first with
|
||||||
|
// the "early change" code-size switch, and its stream begins with the Clear code
|
||||||
|
// (256) -> first byte 0x80. Old-style is LSB-first *without* the early change
|
||||||
|
// (GIF-style increment timing), and begins with 0x00 followed by a byte with bit
|
||||||
|
// 0 set. This is the heuristic libtiff uses to select its compatibility decoder;
|
||||||
|
// on a short/failed peek we default to the modern variant.
|
||||||
|
let old_style = matches!(
|
||||||
|
buffered.fill_buf(),
|
||||||
|
Ok(head) if head.len() >= 2 && head[0] == 0x00 && head[1] & 0x01 != 0
|
||||||
|
);
|
||||||
|
|
||||||
|
let configuration = if old_style {
|
||||||
|
weezl::decode::Configuration::new(weezl::BitOrder::Lsb, 8)
|
||||||
|
} else {
|
||||||
|
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
|
||||||
|
}
|
||||||
|
.with_yield_on_full_buffer(true);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
reader: buffered,
|
||||||
decoder: configuration.build(),
|
decoder: configuration.build(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,7 +466,7 @@ impl Group3Reader {
|
|||||||
) -> Option<u16> {
|
) -> Option<u16> {
|
||||||
let mut sum = 0u16;
|
let mut sum = 0u16;
|
||||||
while let Some(n) = decoder(reader) {
|
while let Some(n) = decoder(reader) {
|
||||||
sum += n;
|
sum = sum.checked_add(n)?;
|
||||||
if n < 64 {
|
if n < 64 {
|
||||||
return Some(sum);
|
return Some(sum);
|
||||||
}
|
}
|
||||||
@@ -475,7 +493,10 @@ impl Read for Group3Reader {
|
|||||||
};
|
};
|
||||||
match run {
|
match run {
|
||||||
Some(len) => {
|
Some(len) => {
|
||||||
a0 += len;
|
a0 = match a0.checked_add(len) {
|
||||||
|
Some(v) => v,
|
||||||
|
None => break, // overflow = corrupt data
|
||||||
|
};
|
||||||
transitions.push(a0);
|
transitions.push(a0);
|
||||||
is_white = !is_white;
|
is_white = !is_white;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -171,7 +171,7 @@ mod tests {
|
|||||||
dir.get(Tag::Unknown(1))
|
dir.get(Tag::Unknown(1))
|
||||||
.expect("tag 1 should be present after this chain")
|
.expect("tag 1 should be present after this chain")
|
||||||
.count(),
|
.count(),
|
||||||
u16::MAX.into()
|
u64::from(u16::MAX)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-8
@@ -295,7 +295,10 @@ pub enum CompressionMethod(u16) unknown(
|
|||||||
}
|
}
|
||||||
|
|
||||||
tags! {
|
tags! {
|
||||||
pub enum PhotometricInterpretation(u16) {
|
pub enum PhotometricInterpretation(u16) unknown(
|
||||||
|
/// A private or extension tag
|
||||||
|
unknown
|
||||||
|
) {
|
||||||
WhiteIsZero = 0,
|
WhiteIsZero = 0,
|
||||||
BlackIsZero = 1,
|
BlackIsZero = 1,
|
||||||
RGB = 2,
|
RGB = 2,
|
||||||
@@ -614,26 +617,22 @@ impl ByteOrder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: at MSRV 1.89 or higher use `slice::as_chunks_mut`.
|
|
||||||
match cls {
|
match cls {
|
||||||
EndianBytes::One => {
|
EndianBytes::One => {
|
||||||
// No change needed
|
// No change needed
|
||||||
}
|
}
|
||||||
EndianBytes::Two => {
|
EndianBytes::Two => {
|
||||||
for chunk in buffer.chunks_exact_mut(2) {
|
for chunk in buffer.as_chunks_mut::<2>().0 {
|
||||||
let chunk: &mut [u8; 2] = chunk.try_into().unwrap();
|
|
||||||
*chunk = u16::from_be_bytes(*chunk).to_le_bytes();
|
*chunk = u16::from_be_bytes(*chunk).to_le_bytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EndianBytes::Four => {
|
EndianBytes::Four => {
|
||||||
for chunk in buffer.chunks_exact_mut(4) {
|
for chunk in buffer.as_chunks_mut::<4>().0 {
|
||||||
let chunk: &mut [u8; 4] = chunk.try_into().unwrap();
|
|
||||||
*chunk = u32::from_be_bytes(*chunk).to_le_bytes();
|
*chunk = u32::from_be_bytes(*chunk).to_le_bytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EndianBytes::Eight => {
|
EndianBytes::Eight => {
|
||||||
for chunk in buffer.chunks_exact_mut(8) {
|
for chunk in buffer.as_chunks_mut::<8>().0 {
|
||||||
let chunk: &mut [u8; 8] = chunk.try_into().unwrap();
|
|
||||||
*chunk = u64::from_be_bytes(*chunk).to_le_bytes();
|
*chunk = u64::from_be_bytes(*chunk).to_le_bytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+377
-11
@@ -1,11 +1,11 @@
|
|||||||
extern crate tiff;
|
extern crate tiff;
|
||||||
|
|
||||||
use tiff::decoder::{ifd, Decoder, DecodingSampleBuffer};
|
use tiff::decoder::{ifd, Decoder, DecodingSampleBuffer};
|
||||||
use tiff::tags::{ByteOrder, Type};
|
use tiff::tags::{ByteOrder, PhotometricInterpretation, SampleFormat, Type};
|
||||||
use tiff::ColorType;
|
use tiff::ColorType;
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Read, Seek};
|
use std::io::{Cursor, Read, Seek};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
const TEST_IMAGE_DIR: &str = "./tests/images/";
|
const TEST_IMAGE_DIR: &str = "./tests/images/";
|
||||||
@@ -16,7 +16,9 @@ macro_rules! test_image_sum {
|
|||||||
fn $name(file: &str, expected_type: ColorType, expected_sum: $sum_ty) {
|
fn $name(file: &str, expected_type: ColorType, expected_sum: $sum_ty) {
|
||||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
|
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
|
||||||
let img_file = File::open(path).expect("Cannot find test image!");
|
let img_file = File::open(path).expect("Cannot find test image!");
|
||||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
let mut decoder = Decoder::open(img_file)
|
||||||
|
.expect("Cannot create decoder")
|
||||||
|
.with_lenient(true);
|
||||||
|
|
||||||
decoder.next_image().expect("Cannot read image IFD");
|
decoder.next_image().expect("Cannot read image IFD");
|
||||||
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
||||||
@@ -473,6 +475,78 @@ fn test_tiled_rect_rgb_u8() {
|
|||||||
test_image_sum_u8("tiled-rect-rgb-u8.tif", ColorType::RGB(8), 62081032);
|
test_image_sum_u8("tiled-rect-rgb-u8.tif", ColorType::RGB(8), 62081032);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tiled_extra_samples_border_u8() {
|
||||||
|
// Regression test for https://github.com/image-rs/image-tiff/issues/400
|
||||||
|
//
|
||||||
|
// A tiled image whose dimensions are not multiples of the tile size, with
|
||||||
|
// an extra sample that is discarded on decode. The horizontal padding of
|
||||||
|
// the right border tiles used to be written past the end of the output
|
||||||
|
// row, spilling into the leftmost columns of the following row. The
|
||||||
|
// fixture's tile padding is filled with 0xEE so any spill is visible.
|
||||||
|
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-extra-samples-border-u8.tif");
|
||||||
|
let img_file = File::open(path).expect("Cannot find test image!");
|
||||||
|
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||||
|
decoder.next_image().expect("Cannot read image IFD");
|
||||||
|
assert_eq!(decoder.dimensions().unwrap(), (40, 40));
|
||||||
|
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
||||||
|
|
||||||
|
let DecodingSampleBuffer::U8(data) = decoder.read_image().unwrap() else {
|
||||||
|
panic!("Wrong bit depth")
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut expected = Vec::with_capacity(40 * 40 * 3);
|
||||||
|
for y in 0..40u32 {
|
||||||
|
for x in 0..40u32 {
|
||||||
|
expected.push(((x * 3) % 251) as u8);
|
||||||
|
expected.push(((y * 7) % 251) as u8);
|
||||||
|
expected.push(((x + y) % 251) as u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(data, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tiled_planar_border_u16() {
|
||||||
|
// A planar (separate) tiled image whose dimensions are not multiples of the tile size.
|
||||||
|
// The tile grid repeats for every plane, but the chunk (tile) index counts all planes, so
|
||||||
|
// the border padding of the second plane used to be looked up with the wrong tile index.
|
||||||
|
// The bottom-right tile of plane 1 then claimed a full tile height, and the decoder ran
|
||||||
|
// past the end of the output buffer.
|
||||||
|
//
|
||||||
|
// Note that `read_image` only decodes the first plane, so this has to go through
|
||||||
|
// `read_image_to_buffer` to reach the tiles that were mis-sized.
|
||||||
|
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-planar-2c-16b.tif");
|
||||||
|
let img_file = File::open(path).expect("Cannot find test image!");
|
||||||
|
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||||
|
decoder.next_image().expect("Cannot read image IFD");
|
||||||
|
assert_eq!(decoder.dimensions().unwrap(), (30, 20));
|
||||||
|
assert_eq!(
|
||||||
|
decoder.colortype().unwrap(),
|
||||||
|
ColorType::Multiband {
|
||||||
|
bit_depth: 16,
|
||||||
|
num_samples: 2
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut buffer = DecodingSampleBuffer::U16(Vec::new());
|
||||||
|
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||||
|
let DecodingSampleBuffer::U16(data) = buffer else {
|
||||||
|
panic!("Wrong bit depth")
|
||||||
|
};
|
||||||
|
assert_eq!(data.len(), 2 * 20 * 30);
|
||||||
|
|
||||||
|
let mut expected = Vec::with_capacity(2 * 20 * 30);
|
||||||
|
for plane in 0..2u16 {
|
||||||
|
for y in 0..20u16 {
|
||||||
|
for x in 0..30u16 {
|
||||||
|
expected.push(1000 * plane + 10 * y + x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(data, expected);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_inner_access() {
|
fn test_inner_access() {
|
||||||
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-rect-rgb-u8.tif");
|
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-rect-rgb-u8.tif");
|
||||||
@@ -825,7 +899,7 @@ fn test_predictor_3_gray_f32() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "zstd")]
|
#[cfg(any(feature = "zstd", feature = "zstd-safe-rust"))]
|
||||||
fn test_zstd_compression() {
|
fn test_zstd_compression() {
|
||||||
// gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif
|
// gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif
|
||||||
test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396);
|
test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396);
|
||||||
@@ -865,22 +939,22 @@ fn test_image_bytes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn byte_order_u16(bytes: &mut [u8]) {
|
fn byte_order_u16(bytes: &mut [u8]) {
|
||||||
for chunk in bytes.chunks_exact_mut(2) {
|
for chunk in bytes.as_chunks_mut::<2>().0 {
|
||||||
let n = u16::from_ne_bytes(chunk.try_into().unwrap());
|
let n = u16::from_ne_bytes(*chunk);
|
||||||
chunk.copy_from_slice(&n.to_le_bytes());
|
chunk.copy_from_slice(&n.to_le_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn byte_order_u32(bytes: &mut [u8]) {
|
fn byte_order_u32(bytes: &mut [u8]) {
|
||||||
for chunk in bytes.chunks_exact_mut(4) {
|
for chunk in bytes.as_chunks_mut::<4>().0 {
|
||||||
let n = u32::from_ne_bytes(chunk.try_into().unwrap());
|
let n = u32::from_ne_bytes(*chunk);
|
||||||
chunk.copy_from_slice(&n.to_le_bytes());
|
chunk.copy_from_slice(&n.to_le_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn byte_order_u64(bytes: &mut [u8]) {
|
fn byte_order_u64(bytes: &mut [u8]) {
|
||||||
for chunk in bytes.chunks_exact_mut(8) {
|
for chunk in bytes.as_chunks_mut::<8>().0 {
|
||||||
let n = u64::from_ne_bytes(chunk.try_into().unwrap());
|
let n = u64::from_ne_bytes(*chunk);
|
||||||
chunk.copy_from_slice(&n.to_le_bytes());
|
chunk.copy_from_slice(&n.to_le_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -975,7 +1049,7 @@ fn extra_bits_gray() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extra_bits_rgb() {
|
fn extra_bits_rgb() {
|
||||||
test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGB(8), 0);
|
test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGBA(8), 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1057,3 +1131,295 @@ fn test_gray_u12_hpredict() {
|
|||||||
// Delta-encoded 12-bit values: [100, 200, 300, 400, 1000, 2000, 3000, 4000]
|
// 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);
|
test_image_sum_u16("hpredict-1c-12b.tiff", ColorType::Gray(12), 11000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Old-style LZW ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_old_style_lzw() {
|
||||||
|
// Pre-TIFF-6.0 "old-style" LZW: LSB-first bit order with no early-change size
|
||||||
|
// switch. Auto-detected from the stream (first byte 0x00, second byte odd).
|
||||||
|
// Same image as libtiffpic's quad-lzw.tif; the decoded pixels must match.
|
||||||
|
test_image_sum_u8("quad-lzw-compat.tiff", ColorType::RGB(8), 21048924);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- TransparencyMask ----
|
||||||
|
|
||||||
|
/// Build a minimal uncompressed 2x2 TIFF in memory, in the requested byte order,
|
||||||
|
/// with the given photometric interpretation and the pixel data as a single strip.
|
||||||
|
/// `sample_format` is emitted as a SampleFormat tag when `Some`; when `None` the
|
||||||
|
/// tag is omitted (the decoder then defaults to unsigned). Pixel bytes must already
|
||||||
|
/// be in `byte_order`.
|
||||||
|
fn build_raw_tiff(
|
||||||
|
byte_order: ByteOrder,
|
||||||
|
photometric: PhotometricInterpretation,
|
||||||
|
bits_per_sample: u16,
|
||||||
|
samples_per_pixel: u16,
|
||||||
|
sample_format: Option<SampleFormat>,
|
||||||
|
pixel_data: &[u8],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let le = matches!(byte_order, ByteOrder::LittleEndian);
|
||||||
|
let put16 = |buf: &mut Vec<u8>, v: u16| {
|
||||||
|
if le {
|
||||||
|
buf.extend_from_slice(&v.to_le_bytes());
|
||||||
|
} else {
|
||||||
|
buf.extend_from_slice(&v.to_be_bytes());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let put32 = |buf: &mut Vec<u8>, v: u32| {
|
||||||
|
if le {
|
||||||
|
buf.extend_from_slice(&v.to_le_bytes());
|
||||||
|
} else {
|
||||||
|
buf.extend_from_slice(&v.to_be_bytes());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let width: u16 = 2;
|
||||||
|
let height: u16 = 2;
|
||||||
|
let num_tags: u16 = if sample_format.is_some() { 9 } else { 8 };
|
||||||
|
let ifd_offset: u32 = 8;
|
||||||
|
let ifd_size = 2 + (num_tags as u32) * 12 + 4;
|
||||||
|
let strip_offset = 8 + ifd_size;
|
||||||
|
|
||||||
|
// IFD entries (must be ascending tag order). Tag IDs stay raw for the
|
||||||
|
// ascending-order property; the field type uses the `Type` enum.
|
||||||
|
let mut tags: Vec<(u16, Type, u32)> = vec![
|
||||||
|
(256, Type::SHORT, width as u32), // ImageWidth
|
||||||
|
(257, Type::SHORT, height as u32), // ImageLength
|
||||||
|
(258, Type::SHORT, bits_per_sample as u32), // BitsPerSample
|
||||||
|
(259, Type::SHORT, 1), // Compression = None
|
||||||
|
(262, Type::SHORT, photometric.to_u16() as u32), // PhotometricInterpretation
|
||||||
|
(273, Type::LONG, strip_offset), // StripOffsets
|
||||||
|
(277, Type::SHORT, samples_per_pixel as u32), // SamplesPerPixel
|
||||||
|
(279, Type::LONG, pixel_data.len() as u32), // StripByteCounts
|
||||||
|
];
|
||||||
|
if let Some(sf) = sample_format {
|
||||||
|
tags.push((339, Type::SHORT, sf.to_u16() as u32)); // SampleFormat (tag 339 > 279)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
buf.extend_from_slice(if le { b"II" } else { b"MM" });
|
||||||
|
put16(&mut buf, 42);
|
||||||
|
put32(&mut buf, ifd_offset);
|
||||||
|
put16(&mut buf, num_tags);
|
||||||
|
|
||||||
|
for (tag, typ, value) in tags {
|
||||||
|
put16(&mut buf, tag);
|
||||||
|
put16(&mut buf, typ.to_u16());
|
||||||
|
put32(&mut buf, 1); // count
|
||||||
|
// A SHORT value is left-justified in the 4-byte value field (value in the
|
||||||
|
// first two bytes, then padding); a LONG fills all four.
|
||||||
|
match typ {
|
||||||
|
Type::SHORT => {
|
||||||
|
put16(&mut buf, value as u16);
|
||||||
|
put16(&mut buf, 0);
|
||||||
|
}
|
||||||
|
_ => put32(&mut buf, value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
put32(&mut buf, 0); // next IFD = 0
|
||||||
|
buf.extend_from_slice(pixel_data);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transparency_mask() {
|
||||||
|
// 2x2 mask: values 0, 255, 128, 64
|
||||||
|
let data = build_raw_tiff(
|
||||||
|
ByteOrder::LittleEndian,
|
||||||
|
PhotometricInterpretation::TransparencyMask,
|
||||||
|
8,
|
||||||
|
1,
|
||||||
|
None,
|
||||||
|
&[0, 255, 128, 64],
|
||||||
|
);
|
||||||
|
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(8));
|
||||||
|
|
||||||
|
let result = decoder.read_image().unwrap();
|
||||||
|
match result {
|
||||||
|
DecodingSampleBuffer::U8(pixels) => {
|
||||||
|
// TransparencyMask is decoded as grayscale
|
||||||
|
assert_eq!(pixels.len(), 4);
|
||||||
|
assert_eq!(pixels[0], 0);
|
||||||
|
assert_eq!(pixels[1], 255);
|
||||||
|
assert_eq!(pixels[2], 128);
|
||||||
|
assert_eq!(pixels[3], 64);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected U8 buffer"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CIE L*a*b* ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cielab_decode() {
|
||||||
|
// 2x2 CIELab image: 3 bytes per pixel (L, a, b)
|
||||||
|
// L=128, a=0 (signed neutral), b=0 (signed neutral)
|
||||||
|
// After re-biasing: a=128 (unsigned neutral), b=128 (unsigned neutral)
|
||||||
|
let pixels: Vec<u8> = vec![
|
||||||
|
128, 0, 0, // pixel 0: L=128, a=0, b=0 (neutral gray)
|
||||||
|
255, 50, 50, // pixel 1: L=255, a=+50, b=+50
|
||||||
|
100, 206, 206, // pixel 2: L=100, a=-50 (as i8), b=-50 (as i8)
|
||||||
|
0, 128, 128, // pixel 3: L=0, a=-128 (as i8), b=-128 (as i8)
|
||||||
|
];
|
||||||
|
|
||||||
|
let data = build_raw_tiff(
|
||||||
|
ByteOrder::LittleEndian,
|
||||||
|
PhotometricInterpretation::CIELab,
|
||||||
|
8,
|
||||||
|
3,
|
||||||
|
None,
|
||||||
|
&pixels,
|
||||||
|
);
|
||||||
|
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Lab(8));
|
||||||
|
|
||||||
|
let result = decoder.read_image().unwrap();
|
||||||
|
match result {
|
||||||
|
DecodingSampleBuffer::U8(decoded) => {
|
||||||
|
assert_eq!(decoded.len(), 12); // 4 pixels * 3 channels
|
||||||
|
|
||||||
|
// Pixel 0: L=128, a=0+128=128, b=0+128=128
|
||||||
|
assert_eq!(decoded[0], 128);
|
||||||
|
assert_eq!(decoded[1], 128);
|
||||||
|
assert_eq!(decoded[2], 128);
|
||||||
|
|
||||||
|
// Pixel 1: L=255, a=50+128=178, b=50+128=178
|
||||||
|
assert_eq!(decoded[3], 255);
|
||||||
|
assert_eq!(decoded[4], 178);
|
||||||
|
assert_eq!(decoded[5], 178);
|
||||||
|
|
||||||
|
// Pixel 2: L=100, a=206+128=78 (wrapping), b=206+128=78
|
||||||
|
assert_eq!(decoded[6], 100);
|
||||||
|
assert_eq!(decoded[7], 78);
|
||||||
|
assert_eq!(decoded[8], 78);
|
||||||
|
|
||||||
|
// Pixel 3: L=0, a=128+128=0 (wrapping), b=128+128=0
|
||||||
|
assert_eq!(decoded[9], 0);
|
||||||
|
assert_eq!(decoded[10], 0);
|
||||||
|
assert_eq!(decoded[11], 0);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected U8 buffer"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cielab_decode_16() {
|
||||||
|
// 2x2 16-bit CIELab image: 3 u16 samples per pixel (L, a, b), a/b signed.
|
||||||
|
// Re-biasing maps signed a/b to unsigned by adding 0x8000 (wrapping):
|
||||||
|
// 0 -> 32768, +1000 -> 33768, -1000 -> 31768, -32768 -> 0, +32767 -> 65535.
|
||||||
|
let samples: [u16; 12] = [
|
||||||
|
32768, 0, 0, // pixel 0: L, a=0, b=0 (neutral)
|
||||||
|
65535, 1000, 1000, // pixel 1: a=+1000, b=+1000
|
||||||
|
10000, 64536, 64536, // pixel 2: a=-1000, b=-1000 (as u16)
|
||||||
|
0, 32768, 32767, // pixel 3: a=-32768, b=+32767 (as u16)
|
||||||
|
];
|
||||||
|
let mut pixels = Vec::new();
|
||||||
|
for s in samples {
|
||||||
|
pixels.extend_from_slice(&s.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = build_raw_tiff(
|
||||||
|
ByteOrder::LittleEndian,
|
||||||
|
PhotometricInterpretation::CIELab,
|
||||||
|
16,
|
||||||
|
3,
|
||||||
|
None,
|
||||||
|
&pixels,
|
||||||
|
);
|
||||||
|
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Lab(16));
|
||||||
|
|
||||||
|
let result = decoder.read_image().unwrap();
|
||||||
|
match result {
|
||||||
|
DecodingSampleBuffer::U16(decoded) => {
|
||||||
|
assert_eq!(decoded.len(), 12); // 4 pixels * 3 channels
|
||||||
|
|
||||||
|
// L unchanged; a/b re-biased by +0x8000 (wrapping).
|
||||||
|
let expected: [u16; 12] = [
|
||||||
|
32768, 32768, 32768, // pixel 0
|
||||||
|
65535, 33768, 33768, // pixel 1
|
||||||
|
10000, 31768, 31768, // pixel 2
|
||||||
|
0, 0, 65535, // pixel 3
|
||||||
|
];
|
||||||
|
assert_eq!(decoded, expected);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected U16 buffer"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cielab_decode_16_big_endian() {
|
||||||
|
// Same image and expected output as `test_cielab_decode_16`, but stored as a
|
||||||
|
// big-endian ("MM") TIFF. 16-bit is the case where byte order matters: the
|
||||||
|
// decoder must swap to native before the a/b re-bias, so the result is
|
||||||
|
// identical to the little-endian case.
|
||||||
|
let samples: [u16; 12] = [
|
||||||
|
32768, 0, 0, // pixel 0
|
||||||
|
65535, 1000, 1000, // pixel 1
|
||||||
|
10000, 64536, 64536, // pixel 2
|
||||||
|
0, 32768, 32767, // pixel 3
|
||||||
|
];
|
||||||
|
let mut pixels = Vec::new();
|
||||||
|
for s in samples {
|
||||||
|
pixels.extend_from_slice(&s.to_be_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = build_raw_tiff(
|
||||||
|
ByteOrder::BigEndian,
|
||||||
|
PhotometricInterpretation::CIELab,
|
||||||
|
16,
|
||||||
|
3,
|
||||||
|
None,
|
||||||
|
&pixels,
|
||||||
|
);
|
||||||
|
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Lab(16));
|
||||||
|
|
||||||
|
let result = decoder.read_image().unwrap();
|
||||||
|
match result {
|
||||||
|
DecodingSampleBuffer::U16(decoded) => {
|
||||||
|
let expected: [u16; 12] = [
|
||||||
|
32768, 32768, 32768, // pixel 0
|
||||||
|
65535, 33768, 33768, // pixel 1
|
||||||
|
10000, 31768, 31768, // pixel 2
|
||||||
|
0, 0, 65535, // pixel 3
|
||||||
|
];
|
||||||
|
assert_eq!(decoded, expected);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected U16 buffer"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cielab_rejects_non_uint_sample_format() {
|
||||||
|
// CIELab treats a/b as signed via the photometric interpretation and requires
|
||||||
|
// SampleFormat::Uint (matching the ICCLab arm). A file declaring Int must be
|
||||||
|
// rejected rather than silently producing a signed output buffer.
|
||||||
|
let pixels = vec![0u8; 12]; // 2x2, 3 samples, 8-bit
|
||||||
|
let data = build_raw_tiff(
|
||||||
|
ByteOrder::LittleEndian,
|
||||||
|
PhotometricInterpretation::CIELab,
|
||||||
|
8,
|
||||||
|
3,
|
||||||
|
Some(SampleFormat::Int),
|
||||||
|
&pixels,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
|
||||||
|
// Rejection may surface at either next_image() or colortype().
|
||||||
|
let rejected = decoder.next_image().is_err() || decoder.colortype().is_err();
|
||||||
|
assert!(
|
||||||
|
rejected,
|
||||||
|
"CIELab with SampleFormat=Int must be rejected, not decoded"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -190,7 +190,7 @@ const FILES: &[(&str, Option<u32>)] = &[
|
|||||||
("tests/libtiffpic/oxford.tif", Some(0x355d2e3e)),
|
("tests/libtiffpic/oxford.tif", Some(0x355d2e3e)),
|
||||||
("tests/libtiffpic/pc260001.tif", Some(0xdb56b753)),
|
("tests/libtiffpic/pc260001.tif", Some(0xdb56b753)),
|
||||||
("tests/libtiffpic/quad-jpeg.tif", Some(0xcba22475)),
|
("tests/libtiffpic/quad-jpeg.tif", Some(0xcba22475)),
|
||||||
("tests/libtiffpic/quad-lzw.tif", Some(0)), // invalid LZW stream
|
("tests/libtiffpic/quad-lzw.tif", Some(0x6055f9ee)), // old-style (LSB) LZW
|
||||||
("tests/libtiffpic/quad-tile.tif", Some(0x6055f9ee)),
|
("tests/libtiffpic/quad-tile.tif", Some(0x6055f9ee)),
|
||||||
("tests/libtiffpic/README", None),
|
("tests/libtiffpic/README", None),
|
||||||
("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -233,3 +233,167 @@ fn test_rgb_u64_predict_roundtrip() {
|
|||||||
fn test_ycbcr_u8_predict_roundtrip() {
|
fn test_ycbcr_u8_predict_roundtrip() {
|
||||||
test_u8_predict_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
|
test_u8_predict_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Extended bit depths (9-15 promoted to u16, 17-31 promoted to u32).
|
||||||
|
//
|
||||||
|
// A horizontal-predicted n-bit sample stores its delta in exactly n bits,
|
||||||
|
// i.e. modulo 2^n, so reconstruction must also be modulo 2^n. The decoder
|
||||||
|
// accumulates in the promoted storage width and reduces back to n bits;
|
||||||
|
// these tests pin that reduction down with exact pixel values, including
|
||||||
|
// deltas that wrap modulo 2^n. The encoder does not support extended
|
||||||
|
// depths, so the TIFFs are hand-built.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Hand-build a minimal little-endian, single-strip, uncompressed grayscale
|
||||||
|
/// TIFF with `Predictor::Horizontal` and the given (possibly non-standard)
|
||||||
|
/// bit depth. `pixels` are the intended sample values; they are converted to
|
||||||
|
/// horizontal differences modulo `2^bits` and bit-packed MSB-first with each
|
||||||
|
/// row padded to a byte boundary — the only representation an n-bit field
|
||||||
|
/// can hold.
|
||||||
|
fn build_gray_hpredict_tiff(
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
bits: u8,
|
||||||
|
photometric: u16,
|
||||||
|
pixels: &[u32],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let max = ((1u64 << bits) - 1) as u32;
|
||||||
|
let row_samples = width as usize;
|
||||||
|
assert_eq!(pixels.len(), row_samples * height as usize);
|
||||||
|
assert!(pixels.iter().all(|&v| v <= max));
|
||||||
|
|
||||||
|
// Per-row horizontal differencing modulo 2^bits.
|
||||||
|
let mut deltas = Vec::with_capacity(pixels.len());
|
||||||
|
for row in pixels.chunks(row_samples) {
|
||||||
|
for (i, &v) in row.iter().enumerate() {
|
||||||
|
deltas.push(if i == 0 {
|
||||||
|
v
|
||||||
|
} else {
|
||||||
|
v.wrapping_sub(row[i - 1]) & max
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bit-pack MSB-first, each row padded to a byte boundary.
|
||||||
|
let row_bytes = (row_samples * bits as usize).div_ceil(8);
|
||||||
|
let mut data = vec![0u8; row_bytes * height as usize];
|
||||||
|
for (r, row) in deltas.chunks(row_samples).enumerate() {
|
||||||
|
for (i, &d) in row.iter().enumerate() {
|
||||||
|
let start = r * row_bytes * 8 + i * bits as usize;
|
||||||
|
for (off, b) in (start..).zip((0..bits).rev()) {
|
||||||
|
if (d >> b) & 1 == 1 {
|
||||||
|
data[off / 8] |= 0x80 >> (off % 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classic little-endian TIFF: header, strip data, then a single IFD.
|
||||||
|
let mut out = Vec::new();
|
||||||
|
out.extend_from_slice(b"II");
|
||||||
|
out.extend_from_slice(&42u16.to_le_bytes());
|
||||||
|
let data_offset = 8u32;
|
||||||
|
let ifd_offset = data_offset + data.len() as u32;
|
||||||
|
out.extend_from_slice(&ifd_offset.to_le_bytes());
|
||||||
|
out.extend_from_slice(&data);
|
||||||
|
|
||||||
|
const SHORT: u16 = 3;
|
||||||
|
const LONG: u16 = 4;
|
||||||
|
let entries: &[(u16, u16, u32)] = &[
|
||||||
|
(256, LONG, width), // ImageWidth
|
||||||
|
(257, LONG, height), // ImageLength
|
||||||
|
(258, SHORT, u32::from(bits)), // BitsPerSample
|
||||||
|
(259, SHORT, 1), // Compression: none
|
||||||
|
(262, SHORT, u32::from(photometric)), // PhotometricInterpretation
|
||||||
|
(273, LONG, data_offset), // StripOffsets
|
||||||
|
(277, SHORT, 1), // SamplesPerPixel
|
||||||
|
(278, LONG, height), // RowsPerStrip
|
||||||
|
(279, LONG, data.len() as u32), // StripByteCounts
|
||||||
|
(317, SHORT, 2), // Predictor: horizontal
|
||||||
|
(339, SHORT, 1), // SampleFormat: unsigned integer
|
||||||
|
];
|
||||||
|
out.extend_from_slice(&(entries.len() as u16).to_le_bytes());
|
||||||
|
for &(tag, ty, value) in entries {
|
||||||
|
out.extend_from_slice(&tag.to_le_bytes());
|
||||||
|
out.extend_from_slice(&ty.to_le_bytes());
|
||||||
|
out.extend_from_slice(&1u32.to_le_bytes());
|
||||||
|
// A count-1 SHORT payload occupies the low bytes of the value field.
|
||||||
|
out.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes()); // no next IFD
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_gray_u16(tiff: Vec<u8>, bits: u8) -> Vec<u16> {
|
||||||
|
let mut decoder = Decoder::open(Cursor::new(tiff)).expect("Cannot create decoder");
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(bits));
|
||||||
|
match decoder.read_image().unwrap() {
|
||||||
|
DecodingSampleBuffer::U16(res) => res,
|
||||||
|
_ => panic!("Wrong data type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_gray_u32(tiff: Vec<u8>, bits: u8) -> Vec<u32> {
|
||||||
|
let mut decoder = Decoder::open(Cursor::new(tiff)).expect("Cannot create decoder");
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(bits));
|
||||||
|
match decoder.read_image().unwrap() {
|
||||||
|
DecodingSampleBuffer::U32(res) => res,
|
||||||
|
_ => panic!("Wrong data type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gray12_hpredict_wraparound_exact() {
|
||||||
|
// Pixel row [4095, 0, 1] encodes as deltas [4095, 1, 1] (mod 2^12).
|
||||||
|
// u16 accumulation yields [4095, 4096, 4097]; reduction modulo 2^12
|
||||||
|
// must restore [4095, 0, 1]. Saturating instead would corrupt the two
|
||||||
|
// wrapped samples to 4095.
|
||||||
|
let tiff = build_gray_hpredict_tiff(3, 1, 12, 1, &[4095, 0, 1]);
|
||||||
|
assert_eq!(decode_gray_u16(tiff, 12), vec![4095, 0, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gray12_hpredict_wraparound_white_is_zero_exact() {
|
||||||
|
// Same stored samples with PhotometricInterpretation = WhiteIsZero:
|
||||||
|
// after the mod-2^12 reduction the decoder inverts as (2^12 - 1) - v.
|
||||||
|
let tiff = build_gray_hpredict_tiff(3, 1, 12, 0, &[4095, 0, 1]);
|
||||||
|
assert_eq!(decode_gray_u16(tiff, 12), vec![0, 4095, 4094]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gray20_hpredict_wraparound_exact() {
|
||||||
|
// The same wrap-around shape on the u32 storage path (17-31 bits).
|
||||||
|
let tiff = build_gray_hpredict_tiff(3, 1, 20, 1, &[0xF_FFFF, 0, 1]);
|
||||||
|
assert_eq!(decode_gray_u32(tiff, 20), vec![0xF_FFFF, 0, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extended_depth_hpredict_random_roundtrip() {
|
||||||
|
// Deterministic pseudo-random pixels across a spread of extended depths;
|
||||||
|
// encode-then-decode must be the identity for every sample, including
|
||||||
|
// every mod-2^n wrap-around the differencing produces. Three rows verify
|
||||||
|
// that prediction restarts per row.
|
||||||
|
let mut state = 0x243F_6A88_85A3_08D3u64;
|
||||||
|
let mut next = move || {
|
||||||
|
state = state
|
||||||
|
.wrapping_mul(6364136223846793005)
|
||||||
|
.wrapping_add(1442695040888963407);
|
||||||
|
(state >> 33) as u32
|
||||||
|
};
|
||||||
|
for &bits in &[9u8, 11, 12, 15] {
|
||||||
|
let max = (1u32 << bits) - 1;
|
||||||
|
let pixels: Vec<u32> = (0..16 * 3).map(|_| next() & max).collect();
|
||||||
|
let tiff = build_gray_hpredict_tiff(16, 3, bits, 1, &pixels);
|
||||||
|
let expected: Vec<u16> = pixels.iter().map(|&v| v as u16).collect();
|
||||||
|
assert_eq!(decode_gray_u16(tiff, bits), expected, "bits={bits}");
|
||||||
|
}
|
||||||
|
for &bits in &[17u8, 20, 25, 31] {
|
||||||
|
let max = ((1u64 << bits) - 1) as u32;
|
||||||
|
let pixels: Vec<u32> = (0..16 * 3).map(|_| next() & max).collect();
|
||||||
|
let tiff = build_gray_hpredict_tiff(16, 3, bits, 1, &pixels);
|
||||||
|
assert_eq!(decode_gray_u32(tiff, bits), pixels, "bits={bits}");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user