Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1426ca802 | ||
|
|
66e6567acc | ||
|
|
5947d3b72e | ||
|
|
0fdf1e0502 | ||
|
|
cf9eee64ad | ||
|
|
a59db23341 | ||
|
|
e8103509fa | ||
|
|
26b18c5184 | ||
|
|
b0712855b1 | ||
|
|
ea6e2a8d80 | ||
|
|
032421634f | ||
|
|
d760bd3442 | ||
|
|
a70e05e51e | ||
|
|
dc25c4eaea | ||
|
|
d7961a2fcf | ||
|
|
2166fd591a | ||
|
|
7380611a7b | ||
|
|
149492ca6e | ||
|
|
e4c3c1fcfd | ||
|
|
6a3282e9f2 | ||
|
|
fa8a911f79 | ||
|
|
dc94cd4436 | ||
|
|
a42b16ab72 | ||
|
|
8246b55e8f | ||
|
|
399469f512 | ||
|
|
740c5cdeea | ||
|
|
86ac5b601e |
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
|
||||
@@ -2,3 +2,4 @@
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
tests/benches/corpus/
|
||||
+5
-1
@@ -23,7 +23,7 @@ quick-error = "2.0.1"
|
||||
# Rename to avoid confusion with the feature name.
|
||||
fax34 = { package = "fax", version = "0.2.6", optional = true }
|
||||
flate2 = { version = "1.0.20", optional = true }
|
||||
weezl = { version = "0.1.10", optional = true }
|
||||
weezl = { version = "0.2.1", optional = true }
|
||||
zstd = { version = "0.13", optional = true }
|
||||
zune-jpeg = { version = "0.5.1", optional = true }
|
||||
image-webp = {version = "0.2.4", optional = true }
|
||||
@@ -54,6 +54,10 @@ harness = false
|
||||
name = "encode"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "decode"
|
||||
harness = false
|
||||
|
||||
|
||||
[[test]]
|
||||
name = "decode_libtiffpic"
|
||||
|
||||
@@ -55,4 +55,4 @@ This crate uses [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) in order t
|
||||
|
||||
After installing it with `cargo install cargo-fuzz` on a nightly rustc, the
|
||||
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]) {
|
||||
let image = std::io::Cursor::new(image);
|
||||
let decoder = Decoder::open(black_box(image));
|
||||
|
||||
let mut reader = decoder.unwrap();
|
||||
while reader.more_images() {
|
||||
reader.next_directory().unwrap();
|
||||
reader.read_image().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -13,17 +13,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut data = DecodingSampleBuffer::I8(vec![]);
|
||||
|
||||
for i in 0u32.. {
|
||||
if !reader.more_images() {
|
||||
break;
|
||||
}
|
||||
|
||||
reader.next_directory()?;
|
||||
|
||||
let colortype = reader.colortype()?;
|
||||
let dimensions = reader.dimensions()?;
|
||||
let layout = reader.read_image_to_buffer(&mut data)?;
|
||||
|
||||
debug_planes(i, &mut data, &layout, &colortype, dimensions);
|
||||
|
||||
if !reader.more_images() {
|
||||
break;
|
||||
}
|
||||
|
||||
reader.next_image()?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -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 = ["."]
|
||||
|
||||
[[bin]]
|
||||
name = "decode_image"
|
||||
path = "fuzz_targets/decode_image.rs"
|
||||
name = "decode_comprehensive"
|
||||
path = "fuzz_targets/decode_comprehensive.rs"
|
||||
test = false
|
||||
doc = false
|
||||
@@ -0,0 +1,96 @@
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use std::io::{Cursor, Read, Seek};
|
||||
use tiff::decoder::{Decoder, Limits};
|
||||
use tiff::tags::Tag;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
// Standalone header parse (separate code path from Decoder::open)
|
||||
let _ = tiff::decoder::TiffHeader::parse(Cursor::new(data));
|
||||
|
||||
let mut decoder = match Decoder::open(Cursor::new(data)) {
|
||||
Ok(d) => d,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let mut limits = Limits::default();
|
||||
limits.decoding_buffer_size = 512 * 1024;
|
||||
limits.ifd_value_size = 64 * 1024;
|
||||
limits.intermediate_buffer_size = 512 * 1024;
|
||||
decoder = decoder.with_limits(limits);
|
||||
|
||||
// Probe first IFD (opened but image not yet parsed)
|
||||
probe_directory(&mut decoder);
|
||||
|
||||
// Linear IFD iteration via next_directory — tolerates invalid
|
||||
// intermediate IFDs that aren't decodable as images
|
||||
let mut ifd_count = 1u32;
|
||||
while decoder.more_images() && ifd_count < 8 {
|
||||
if decoder.next_directory().is_err() {
|
||||
break;
|
||||
}
|
||||
probe_directory(&mut decoder);
|
||||
ifd_count += 1;
|
||||
}
|
||||
|
||||
// Random-access traversal via seek (different code path: find_nth_ifd
|
||||
// re-walks the IFD chain from the start each time)
|
||||
for i in 0..ifd_count.min(4) as usize {
|
||||
if decoder.seek_to_directory(i).is_ok() {
|
||||
probe_directory(&mut decoder);
|
||||
}
|
||||
// seek_to_image eagerly parses the directory as an Image
|
||||
let _ = decoder.seek_to_image(i);
|
||||
}
|
||||
});
|
||||
|
||||
fn probe_directory<R: Read + Seek>(decoder: &mut Decoder<R>) {
|
||||
// Tag iteration — exercises value parsing for every tag in the IFD
|
||||
for result in decoder.current_ifd().tag_iter() {
|
||||
let _ = result;
|
||||
}
|
||||
|
||||
// SubIFD / EXIF / GPS directory traversal — exercises read_directory
|
||||
// with potentially adversarial offsets
|
||||
if let Ok(Some(val)) = decoder.current_ifd().find_tag(Tag::SubIfd) {
|
||||
if let Ok(ptrs) = val.into_ifd_vec() {
|
||||
for ptr in ptrs.into_iter().take(4) {
|
||||
let _ = decoder.read_directory(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
for tag in [Tag::ExifDirectory, Tag::GpsDirectory] {
|
||||
if let Ok(Some(val)) = decoder.current_ifd().find_tag(tag) {
|
||||
if let Ok(ptr) = val.into_ifd_pointer() {
|
||||
let _ = decoder.read_directory(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Image-level probing — dimensions() triggers ensure_image,
|
||||
// which lazily parses the directory as an Image
|
||||
if decoder.dimensions().is_ok() {
|
||||
let _ = decoder.colortype();
|
||||
let _ = decoder.color_map();
|
||||
let _ = decoder.get_chunk_type();
|
||||
let _ = decoder.chunk_dimensions();
|
||||
let _ = decoder.strip_count();
|
||||
let _ = decoder.tile_count();
|
||||
let _ = decoder.image_buffer_layout();
|
||||
let _ = decoder.image_chunk_buffer_layout(0);
|
||||
|
||||
// Chunk reads at boundaries — first and last exercise
|
||||
// different offset calculations
|
||||
let _ = decoder.read_chunk(0);
|
||||
let chunk_count = decoder
|
||||
.strip_count()
|
||||
.or_else(|_| decoder.tile_count())
|
||||
.unwrap_or(0);
|
||||
if chunk_count > 1 {
|
||||
let _ = decoder.read_chunk(chunk_count - 1);
|
||||
}
|
||||
|
||||
// Full image decode
|
||||
let _ = decoder.read_image();
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let mut decoder = if let Ok(d) = tiff::decoder::Decoder::new(std::io::Cursor::new(data)) {
|
||||
d
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut limits = tiff::decoder::Limits::default();
|
||||
limits.decoding_buffer_size = 1_000_000;
|
||||
limits.ifd_value_size = 1_000_000;
|
||||
limits.intermediate_buffer_size = 1_000_000;
|
||||
|
||||
decoder = decoder.with_limits(limits);
|
||||
|
||||
loop {
|
||||
if let Err(_) = decoder.read_image() {
|
||||
break;
|
||||
}
|
||||
|
||||
if !decoder.more_images() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Err(_) = decoder.next_image() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
# === File headers ===
|
||||
|
||||
# Classic TIFF (LE, BE)
|
||||
"\x49\x49\x2A\x00"
|
||||
"\x4D\x4D\x00\x2A"
|
||||
# BigTIFF (LE, BE) — version 43, 8-byte offsets, different parsing paths
|
||||
"\x49\x49\x2B\x00\x08\x00\x00\x00"
|
||||
"\x4D\x4D\x00\x2B\x00\x08\x00\x00"
|
||||
|
||||
# === IFD field type codes (2-byte LE) ===
|
||||
# Decoder switches on these; wrong types exercise error paths
|
||||
|
||||
"\x01\x00"
|
||||
"\x02\x00"
|
||||
"\x03\x00"
|
||||
"\x04\x00"
|
||||
"\x05\x00"
|
||||
"\x06\x00"
|
||||
"\x07\x00"
|
||||
"\x08\x00"
|
||||
"\x09\x00"
|
||||
"\x0A\x00"
|
||||
"\x0B\x00"
|
||||
"\x0C\x00"
|
||||
"\x0D\x00"
|
||||
"\x10\x00"
|
||||
"\x11\x00"
|
||||
"\x12\x00"
|
||||
|
||||
# === IFD tag numbers (2-byte LE) ===
|
||||
|
||||
# 256=ImageWidth 257=ImageLength 258=BitsPerSample
|
||||
"\x00\x01"
|
||||
"\x01\x01"
|
||||
"\x02\x01"
|
||||
# 259=Compression 262=PhotometricInterpretation
|
||||
"\x03\x01"
|
||||
"\x06\x01"
|
||||
# 273=StripOffsets 274=Orientation
|
||||
"\x11\x01"
|
||||
"\x12\x01"
|
||||
# 277=SamplesPerPixel 278=RowsPerStrip 279=StripByteCounts
|
||||
"\x15\x01"
|
||||
"\x16\x01"
|
||||
"\x17\x01"
|
||||
# 282=XResolution 283=YResolution 284=PlanarConfiguration
|
||||
"\x1A\x01"
|
||||
"\x1B\x01"
|
||||
"\x1C\x01"
|
||||
# 317=Predictor 320=ColorMap
|
||||
"\x3D\x01"
|
||||
"\x40\x01"
|
||||
# 322=TileWidth 323=TileLength 324=TileOffsets 325=TileByteCounts
|
||||
"\x42\x01"
|
||||
"\x43\x01"
|
||||
"\x44\x01"
|
||||
"\x45\x01"
|
||||
# 330=SubIfd 338=ExtraSamples 339=SampleFormat
|
||||
"\x4A\x01"
|
||||
"\x52\x01"
|
||||
"\x53\x01"
|
||||
# 347=JPEGTables 530=ChromaSubsampling
|
||||
"\x5B\x01"
|
||||
"\x12\x02"
|
||||
# 34665=ExifDirectory 34675=IccProfile 34853=GpsDirectory
|
||||
"\x69\x87"
|
||||
"\x73\x87"
|
||||
"\x25\x88"
|
||||
|
||||
# === IFD tag numbers (2-byte BE) ===
|
||||
|
||||
"\x01\x00"
|
||||
"\x01\x01"
|
||||
"\x01\x02"
|
||||
"\x01\x03"
|
||||
"\x01\x06"
|
||||
"\x01\x11"
|
||||
"\x01\x12"
|
||||
"\x01\x15"
|
||||
"\x01\x16"
|
||||
"\x01\x17"
|
||||
"\x01\x1A"
|
||||
"\x01\x1B"
|
||||
"\x01\x1C"
|
||||
"\x01\x3D"
|
||||
"\x01\x40"
|
||||
"\x01\x42"
|
||||
"\x01\x43"
|
||||
"\x01\x44"
|
||||
"\x01\x45"
|
||||
"\x01\x4A"
|
||||
"\x01\x52"
|
||||
"\x01\x53"
|
||||
"\x01\x5B"
|
||||
"\x02\x12"
|
||||
"\x87\x69"
|
||||
"\x87\x73"
|
||||
"\x88\x25"
|
||||
|
||||
# === Compression codes (4-byte LE inline SHORT values) ===
|
||||
# These are the values stored in the IFD entry for tag 259
|
||||
|
||||
# 1=None 2=Huffman 3=Fax3 4=Fax4 5=LZW
|
||||
"\x01\x00\x00\x00"
|
||||
"\x02\x00\x00\x00"
|
||||
"\x03\x00\x00\x00"
|
||||
"\x04\x00\x00\x00"
|
||||
"\x05\x00\x00\x00"
|
||||
# 6=OldJPEG 7=ModernJPEG 8=Deflate
|
||||
"\x06\x00\x00\x00"
|
||||
"\x07\x00\x00\x00"
|
||||
"\x08\x00\x00\x00"
|
||||
# 0x8005=PackBits 0x80B2=OldDeflate
|
||||
"\x05\x80\x00\x00"
|
||||
"\xB2\x80\x00\x00"
|
||||
|
||||
# === Useful small values (4-byte LE) ===
|
||||
# BitsPerSample=16, =32 (8 already covered as Deflate compression above)
|
||||
|
||||
"\x10\x00\x00\x00"
|
||||
"\x20\x00\x00\x00"
|
||||
|
||||
# === Minimal IFD entries (12-byte LE: tag + type SHORT + count 1 + value) ===
|
||||
# These let the fuzzer splice complete valid entries into an IFD
|
||||
|
||||
# ImageWidth=1
|
||||
"\x00\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
|
||||
# ImageLength=1
|
||||
"\x01\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
|
||||
# BitsPerSample=8
|
||||
"\x02\x01\x03\x00\x01\x00\x00\x00\x08\x00\x00\x00"
|
||||
# Compression=None
|
||||
"\x03\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
|
||||
# PhotometricInterpretation=BlackIsZero
|
||||
"\x06\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
|
||||
# SamplesPerPixel=1
|
||||
"\x15\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
|
||||
# RowsPerStrip=1
|
||||
"\x16\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
|
||||
@@ -11,7 +11,7 @@ use std::collections::HashMap;
|
||||
///
|
||||
/// Since we only consider one child, we run union find to assign each pointer to a known chain.
|
||||
/// Then, when we insert a new edge we check if they belong to the same component.
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct IfdCycles {
|
||||
/// The root of each component union.
|
||||
component_union: HashMap<ComponentId, ComponentId>,
|
||||
|
||||
+27
-17
@@ -583,19 +583,19 @@ impl Entry {
|
||||
|
||||
match self.type_ {
|
||||
Type::BYTE | Type::UNDEFINED => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.iter().copied().map(Byte))
|
||||
})
|
||||
}
|
||||
Type::SBYTE => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.iter().copied().map(|v| SignedByte(v as i8)))
|
||||
})
|
||||
}
|
||||
Type::SHORT => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
@@ -605,7 +605,7 @@ impl Entry {
|
||||
})
|
||||
}
|
||||
Type::SSHORT => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
@@ -615,7 +615,7 @@ impl Entry {
|
||||
})
|
||||
}
|
||||
Type::LONG => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
@@ -625,7 +625,7 @@ impl Entry {
|
||||
})
|
||||
}
|
||||
Type::SLONG => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
@@ -635,7 +635,7 @@ impl Entry {
|
||||
})
|
||||
}
|
||||
Type::FLOAT => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
@@ -645,7 +645,7 @@ impl Entry {
|
||||
})
|
||||
}
|
||||
Type::DOUBLE => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
@@ -655,7 +655,7 @@ impl Entry {
|
||||
})
|
||||
}
|
||||
Type::RATIONAL => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.chunks_exact(8).map(|ch| {
|
||||
Rational(
|
||||
@@ -666,7 +666,7 @@ impl Entry {
|
||||
})
|
||||
}
|
||||
Type::SRATIONAL => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.chunks_exact(8).map(|ch| {
|
||||
SRational(
|
||||
@@ -677,7 +677,7 @@ impl Entry {
|
||||
})
|
||||
}
|
||||
Type::LONG8 => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
@@ -687,7 +687,7 @@ impl Entry {
|
||||
})
|
||||
}
|
||||
Type::SLONG8 => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
@@ -697,7 +697,7 @@ impl Entry {
|
||||
})
|
||||
}
|
||||
Type::IFD => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
@@ -707,7 +707,7 @@ impl Entry {
|
||||
})
|
||||
}
|
||||
Type::IFD8 => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
@@ -719,8 +719,8 @@ impl Entry {
|
||||
Type::ASCII => {
|
||||
let n = usize::try_from(self.count)?;
|
||||
|
||||
if n > limits.decoding_buffer_size {
|
||||
return Err(dbg!(TiffError::LimitsExceeded));
|
||||
if n > limits.ifd_value_size {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
|
||||
let mut out = vec![0; n];
|
||||
@@ -846,10 +846,20 @@ impl Entry {
|
||||
|
||||
fn vec_with_capacity(
|
||||
value_count: u64,
|
||||
type_: Type,
|
||||
limits: &super::Limits,
|
||||
) -> Result<Vec<Value>, TiffError> {
|
||||
let value_count = usize::try_from(value_count)?;
|
||||
// Check raw file bytes against ifd_value_size.
|
||||
if !type_
|
||||
.value_bytes(value_count)
|
||||
.and_then(|b| usize::try_from(b).map_err(|_| TiffError::LimitsExceeded))
|
||||
.is_ok_and(|raw_len| raw_len <= limits.ifd_value_size)
|
||||
{
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
|
||||
// Check in-memory Value representation against decoding_buffer_size.
|
||||
let value_count = usize::try_from(value_count)?;
|
||||
if value_count > limits.decoding_buffer_size / mem::size_of::<Value>() {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
|
||||
+20
-5
@@ -454,8 +454,9 @@ impl Image {
|
||||
match tag_reader.find_tag(Tag::ColorMap)? {
|
||||
Some(v) => {
|
||||
let map = v.into_u16_vec()?;
|
||||
let expected_len = 3usize
|
||||
.checked_mul(1usize << bits_per_sample)
|
||||
let expected_len = 1usize
|
||||
.checked_shl(bits_per_sample as u32)
|
||||
.and_then(|n| n.checked_mul(3))
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
if map.len() != expected_len {
|
||||
return Err(TiffError::FormatError(
|
||||
@@ -1009,6 +1010,7 @@ impl Image {
|
||||
buf: &mut [u8],
|
||||
layout: &ReadoutLayout,
|
||||
chunk_index: u32,
|
||||
scratch: &mut Vec<u8>,
|
||||
) -> TiffResult<()> {
|
||||
let ValueReader {
|
||||
reader,
|
||||
@@ -1191,6 +1193,7 @@ impl Image {
|
||||
samples,
|
||||
ByteOrder::native(),
|
||||
predictor,
|
||||
scratch,
|
||||
);
|
||||
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
@@ -1209,6 +1212,7 @@ impl Image {
|
||||
samples,
|
||||
decompressed_byte_order,
|
||||
predictor,
|
||||
scratch,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1224,9 +1228,18 @@ impl Image {
|
||||
|
||||
let row = &mut row[..data_row_bytes];
|
||||
match color_type.bit_depth() {
|
||||
16 => predict_f16(&mut encoded, row, samples),
|
||||
32 => predict_f32(&mut encoded, row, samples),
|
||||
64 => predict_f64(&mut encoded, row, samples),
|
||||
16 => {
|
||||
let (out, _) = row.as_chunks_mut::<2>();
|
||||
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!(),
|
||||
}
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
@@ -1255,6 +1268,7 @@ impl Image {
|
||||
samples,
|
||||
decompressed_byte_order,
|
||||
predictor,
|
||||
scratch,
|
||||
);
|
||||
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
@@ -1296,6 +1310,7 @@ impl Image {
|
||||
samples,
|
||||
decompressed_byte_order,
|
||||
predictor,
|
||||
scratch,
|
||||
);
|
||||
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
|
||||
@@ -87,15 +87,12 @@ fn packbits_decode<R: Read, const N: usize>(
|
||||
reader: &mut R,
|
||||
buf: &mut [[u8; N]],
|
||||
) -> std::io::Result<()> {
|
||||
let expected = buf.len();
|
||||
for offset in (0..N).rev() {
|
||||
let mut i = 0;
|
||||
let mut code = [0u8; 1];
|
||||
|
||||
loop {
|
||||
let Some(pixels) = buf.get_mut(i..) else {
|
||||
break;
|
||||
};
|
||||
|
||||
while let Some(pixels) = buf.get_mut(i..) {
|
||||
if pixels.is_empty() {
|
||||
break;
|
||||
}
|
||||
@@ -124,6 +121,13 @@ fn packbits_decode<R: Read, const N: usize>(
|
||||
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(())
|
||||
|
||||
+319
-52
@@ -254,6 +254,9 @@ pub struct Decoder<R> {
|
||||
/// Map from the ifd into the `ifd_offsets` ordered list.
|
||||
seen_ifds: cycles::IfdCycles,
|
||||
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>,
|
||||
}
|
||||
|
||||
/// All the information needed to read and interpret byte slices from the underlying file, i.e. to
|
||||
@@ -273,13 +276,37 @@ pub struct IfdDecoder<'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
|
||||
/// the element N positions earlier. For N >= 2, the N interleaved channels are independent streams.
|
||||
///
|
||||
/// 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]) {
|
||||
for i in N..buf.len() {
|
||||
buf[i] = buf[i].wrapping_add(buf[i - N]);
|
||||
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 mut chunks = buf[N..].chunks_exact_mut(N);
|
||||
for chunk in &mut chunks {
|
||||
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.into_remainder().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]) {
|
||||
for i in (2 * N..buf.len()).step_by(2) {
|
||||
let v = u16::from_ne_bytes(buf[i..][..2].try_into().unwrap());
|
||||
@@ -364,65 +391,145 @@ fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: u16) {
|
||||
}
|
||||
}
|
||||
|
||||
fn predict_f32(input: &mut [u8], output: &mut [u8], samples: u16) {
|
||||
let samples = usize::from(samples);
|
||||
|
||||
for i in samples..input.len() {
|
||||
input[i] = input[i].wrapping_add(input[i - samples]);
|
||||
/// Undo the floating-point predictor: byte-level prefix sum, then de-interleave byte planes back
|
||||
/// 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, 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; 2]], samples: u16) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn predict_f16(input: &mut [u8], output: &mut [u8], samples: u16) {
|
||||
let samples = usize::from(samples);
|
||||
/// Transpose byte-planes back into packed f16/f32/f64 samples with big-endian to native conversion.
|
||||
///
|
||||
/// 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() {
|
||||
input[i] = input[i].wrapping_add(input[i - samples]);
|
||||
// Batch 4 samples per iteration. Fixed-size array slicing eliminates bounds checks.
|
||||
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() {
|
||||
chunk.copy_from_slice(&u16::to_ne_bytes(u16::from_be_bytes([
|
||||
input[i],
|
||||
input[input.len() / 2 + i],
|
||||
])));
|
||||
// Remaining 0–3 samples.
|
||||
let base = out_chunks.len() * 4;
|
||||
for (j, out) in out_rest.iter_mut().enumerate() {
|
||||
*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) {
|
||||
let samples = usize::from(samples);
|
||||
|
||||
for i in samples..input.len() {
|
||||
input[i] = input[i].wrapping_add(input[i - samples]);
|
||||
fn deinterleave_f16_planes(input: &[u8], output: &mut [[u8; 2]]) {
|
||||
let n = output.len();
|
||||
if n == 0 || input.len() < n * 2 {
|
||||
return;
|
||||
}
|
||||
let (p0, p1) = input.split_at(n);
|
||||
|
||||
for (i, chunk) in output.chunks_mut(8).enumerate() {
|
||||
chunk.copy_from_slice(&u64::to_ne_bytes(u64::from_be_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],
|
||||
])));
|
||||
for (out, (b0, b1)) in output.iter_mut().zip(p0.iter().zip(p1)) {
|
||||
*out = u16::from_be_bytes([*b0, *b1]).to_ne_bytes();
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
buf: &mut [u8],
|
||||
bit_depth: u8,
|
||||
samples: u16,
|
||||
byte_order: ByteOrder,
|
||||
predictor: Predictor,
|
||||
scratch: &mut Vec<u8>,
|
||||
) {
|
||||
match predictor {
|
||||
Predictor::None => {
|
||||
@@ -433,11 +540,26 @@ fn fix_endianness_and_predict(
|
||||
rev_hpredict_nsamp(buf, bit_depth, samples);
|
||||
}
|
||||
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 {
|
||||
16 => predict_f16(&mut buffer_copy, buf, samples),
|
||||
32 => predict_f32(&mut buffer_copy, buf, samples),
|
||||
64 => predict_f64(&mut buffer_copy, buf, samples),
|
||||
16 => {
|
||||
let (output, rest) = buf.as_chunks_mut::<2>();
|
||||
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."),
|
||||
}
|
||||
}
|
||||
@@ -627,6 +749,7 @@ impl TiffHeader {
|
||||
},
|
||||
seen_ifds: cycles::IfdCycles::new(),
|
||||
image: None,
|
||||
scratch: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -712,19 +835,156 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
// 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.
|
||||
#[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() {
|
||||
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)?;
|
||||
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> {
|
||||
Ok(self.ensure_image_and_reader()?.0)
|
||||
}
|
||||
|
||||
/// Change to another reader in a new decoder, taking with it parsed data and limits.
|
||||
///
|
||||
/// The reader will keep the IFD state but resources and in particular the parsed image are
|
||||
/// transferred to the new decoder. This has two motivations. Firstly, it can not silently
|
||||
/// duplicate any limits and exhaust more memory than expected. Secondly, this is compatible
|
||||
/// with additional resources that can not be cloned at all.
|
||||
///
|
||||
/// This method assumes that the alternate reader contains the same data as the current reader
|
||||
/// (but not necessarily the same stream).
|
||||
///
|
||||
/// The limits used by `self` get replaced with default limits. To keep using the reader that
|
||||
/// is the receiver of this method you likely want to reinitialize it with
|
||||
/// [`Self::with_limits`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// This method can also be used to 'fork' the reader into multiple independent iterators over
|
||||
/// the file, provided you use access to [`Self::inner`] to clone the underlying stream. Since
|
||||
/// the first call takes the resources, keep that object separate if you want each worker to
|
||||
/// get the same decoder (each with default configured limits).
|
||||
///
|
||||
/// Here we show the functionality by opening cursors over data in-memory.
|
||||
///
|
||||
/// ```
|
||||
/// use std::io::Cursor;
|
||||
/// use tiff::{decoder::Decoder, TiffError};
|
||||
///
|
||||
/// let data: &[u8] = /* */
|
||||
/// # &include_bytes!(concat!(
|
||||
/// # env!("CARGO_MANIFEST_DIR"), "/tests/images/tiled-gray-i1.tif"
|
||||
/// # ))[..];
|
||||
/// # const N: usize = 1;
|
||||
/// let decoder = Decoder::open(Cursor::new(data))?;
|
||||
///
|
||||
/// let mut workers = vec![decoder];
|
||||
///
|
||||
/// for _ in 0..N {
|
||||
/// let decoder = workers.last_mut().unwrap();
|
||||
/// // Takes the resources with it, they are always in the last.
|
||||
/// let dup = decoder.take_with_reader(Cursor::new(data));
|
||||
/// workers.push(dup);
|
||||
/// }
|
||||
///
|
||||
/// let with_resources = workers.pop().unwrap();
|
||||
///
|
||||
/// // Now all `workers` are at the same IFD but in a bare state.
|
||||
///
|
||||
/// Ok::<_, TiffError>(())
|
||||
/// ```
|
||||
///
|
||||
/// Please note that you do **not** want to use [`File::try_clone`][`std::fs::File::open`], or
|
||||
/// any other method where multiple readers share a single position variable, in combination
|
||||
/// with concurrent use of decoders. When the independent decoders each seek the file
|
||||
/// descriptor in parallel there is a race as to what position they then read from
|
||||
/// (time-to-check-to-time-to-use). This of course applies to the original file passed to
|
||||
/// [`Decoder::open`] already but may be more tempting here. Instead, use a file reader that
|
||||
/// owns a position and uses [`read_at`] or equivalents.
|
||||
///
|
||||
/// [`read_at`]: `std::os::unix::fs::FileExt::read_at`
|
||||
///
|
||||
/// An *incorrect* use would be:
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::fs::File;
|
||||
/// use tiff::{decoder::Decoder, TiffError};
|
||||
///
|
||||
/// let file: File = /* */
|
||||
/// # File::open(concat!(
|
||||
/// # env!("CARGO_MANIFEST_DIR"), "/tests/images/tiled-gray-i1.tif"
|
||||
/// # ))?;
|
||||
/// let mut decoder = Decoder::open(&file)?;
|
||||
///
|
||||
/// let concurrent = decoder.inner().try_clone()?;
|
||||
/// // ⚠️Do NOT attempt to use both decoder and racing_decoder in parallel!
|
||||
/// // This will be defined behavior but likely cause both to fail.
|
||||
/// let racing_decoder = decoder.take_with_reader(concurrent);
|
||||
///
|
||||
/// # Ok::<_, TiffError>(())
|
||||
/// ```
|
||||
///
|
||||
/// In another use case you have a partially cached file backed by fast storage that you only
|
||||
/// access from slow disk when any operation misses the cache. In that case you may use this
|
||||
/// method to replace the cached reader with a file reader after a miss is detected, without
|
||||
/// losing any of the state collected by the decoder.
|
||||
///
|
||||
/// ```
|
||||
/// use tiff::{decoder::Decoder, TiffError};
|
||||
///
|
||||
/// let partial_file = /* */
|
||||
/// # std::io::Cursor::new(include_bytes!(concat!(
|
||||
/// # env!("CARGO_MANIFEST_DIR"), "/tests/images/tiled-gray-i1.tif"
|
||||
/// # )));
|
||||
/// let mut decoder = Decoder::open(partial_file)?;
|
||||
///
|
||||
/// # let backing_file = concat!(
|
||||
/// # env!("CARGO_MANIFEST_DIR"), "/tests/images/tiled-gray-i1.tif"
|
||||
/// # );
|
||||
///
|
||||
/// // Catch cache errors: we are out of pages we have for this cache..
|
||||
/// if let Err(TiffError::IoError(e)) = decoder.next_image() {
|
||||
/// if e.kind() == std::io::ErrorKind::OutOfMemory {
|
||||
/// let file = std::fs::File::open(backing_file)?;
|
||||
/// let mut slow_decoder = decoder.take_with_reader(file);
|
||||
/// // Repeat the op, `next_image` is atomic on error.
|
||||
/// slow_decoder.next_image()?;
|
||||
/// // …
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # Ok::<_, TiffError>(())
|
||||
/// ```
|
||||
pub fn take_with_reader<S: Read + Seek>(&mut self, reader: S) -> Decoder<S> {
|
||||
Decoder {
|
||||
value_reader: ValueReader {
|
||||
reader: EndianReader::new(reader, self.value_reader.reader.byte_order),
|
||||
bigtiff: self.value_reader.bigtiff,
|
||||
limits: core::mem::take(&mut self.value_reader.limits),
|
||||
},
|
||||
ifd_offsets: self.ifd_offsets.clone(),
|
||||
seen_ifds: self.seen_ifds.clone(),
|
||||
directory: self.directory.clone(),
|
||||
current_ifd_pointer: self.current_ifd_pointer,
|
||||
image: core::mem::take(&mut self.image),
|
||||
scratch: core::mem::take(&mut self.scratch),
|
||||
}
|
||||
}
|
||||
|
||||
/// The offset of the directory representing the current image.
|
||||
pub fn ifd_pointer(&mut self) -> Option<IfdPointer> {
|
||||
self.current_ifd_pointer
|
||||
@@ -1197,10 +1457,10 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
chunk_index: u32,
|
||||
layout: &image::ReadoutLayout,
|
||||
) -> 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;
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -1352,7 +1612,7 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
slice: TiffCodingUnit,
|
||||
buffer: &mut [u8],
|
||||
) -> 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 readout = image.readout_for_size(width, height)?;
|
||||
@@ -1379,7 +1639,13 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
let chunk = slice.0 + idx as u32 * readout.chunks_per_plane;
|
||||
let offset = image.chunk_offsets[chunk as usize];
|
||||
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(())
|
||||
@@ -1550,7 +1816,7 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
/// 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.
|
||||
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 ref layout @ image::PlaneLayout {
|
||||
@@ -1583,6 +1849,7 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
&mut buffer[plane_offset..][buffer_offset..],
|
||||
readout,
|
||||
chunk,
|
||||
scratch,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,7 +448,7 @@ impl Group3Reader {
|
||||
) -> Option<u16> {
|
||||
let mut sum = 0u16;
|
||||
while let Some(n) = decoder(reader) {
|
||||
sum += n;
|
||||
sum = sum.checked_add(n)?;
|
||||
if n < 64 {
|
||||
return Some(sum);
|
||||
}
|
||||
@@ -475,7 +475,10 @@ impl Read for Group3Reader {
|
||||
};
|
||||
match run {
|
||||
Some(len) => {
|
||||
a0 += len;
|
||||
a0 = match a0.checked_add(len) {
|
||||
Some(v) => v,
|
||||
None => break, // overflow = corrupt data
|
||||
};
|
||||
transitions.push(a0);
|
||||
is_white = !is_white;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::{
|
||||
/// [`Decoder::read_directory_tags`](crate::decoder::Decoder::read_directory_tags) to read the
|
||||
/// values associated with tags from an underlying file.
|
||||
#[doc(alias = "IFD")]
|
||||
#[derive(Clone)]
|
||||
pub struct Directory {
|
||||
/// There are at most `u16::MAX` entries in any single directory, the count is stored as a
|
||||
/// 2-byte value. The order in the file is implied to be ascending by tag value (the decoder
|
||||
|
||||
Binary file not shown.
Reference in new issue
Block a user