Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3340fc04e0 |
No files matched your search
@@ -1,85 +0,0 @@
|
|||||||
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,4 +2,3 @@
|
|||||||
/target/
|
/target/
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
tests/benches/corpus/
|
|
||||||
+1
-5
@@ -23,7 +23,7 @@ quick-error = "2.0.1"
|
|||||||
# Rename to avoid confusion with the feature name.
|
# Rename to avoid confusion with the feature name.
|
||||||
fax34 = { package = "fax", version = "0.2.6", optional = true }
|
fax34 = { package = "fax", version = "0.2.6", optional = true }
|
||||||
flate2 = { version = "1.0.20", optional = true }
|
flate2 = { version = "1.0.20", optional = true }
|
||||||
weezl = { version = "0.2.1", optional = true }
|
weezl = { version = "0.1.10", optional = true }
|
||||||
zstd = { version = "0.13", optional = true }
|
zstd = { version = "0.13", optional = true }
|
||||||
zune-jpeg = { version = "0.5.1", optional = true }
|
zune-jpeg = { version = "0.5.1", optional = true }
|
||||||
image-webp = {version = "0.2.4", optional = true }
|
image-webp = {version = "0.2.4", optional = true }
|
||||||
@@ -54,10 +54,6 @@ harness = false
|
|||||||
name = "encode"
|
name = "encode"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "decode"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "decode_libtiffpic"
|
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
|
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_comprehensive -snone -- -dict=fuzz/tiff.dict -timeout=5`.
|
`cargo fuzz run decode_image -snone -- -timeout=5`.
|
||||||
@@ -1,461 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+6
-6
@@ -13,17 +13,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
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(())
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
//! 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_comprehensive"
|
name = "decode_image"
|
||||||
path = "fuzz_targets/decode_comprehensive.rs"
|
path = "fuzz_targets/decode_image.rs"
|
||||||
test = false
|
test = false
|
||||||
doc = false
|
doc = false
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#![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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#![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
@@ -1,139 +0,0 @@
|
|||||||
# === 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.
|
/// Since we only consider one child, we run union find to assign each pointer to a known chain.
|
||||||
/// Then, when we insert a new edge we check if they belong to the same component.
|
/// Then, when we insert a new edge we check if they belong to the same component.
|
||||||
#[derive(Clone, Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct IfdCycles {
|
pub struct IfdCycles {
|
||||||
/// The root of each component union.
|
/// The root of each component union.
|
||||||
component_union: HashMap<ComponentId, ComponentId>,
|
component_union: HashMap<ComponentId, ComponentId>,
|
||||||
|
|||||||
@@ -1,362 +0,0 @@
|
|||||||
use std::io::{self, Cursor, Read, Seek};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
decoder::{ifd::Entry, stream::EndianReader, ValueReader},
|
|
||||||
tags::{ByteOrder, IfdPointer, Type},
|
|
||||||
TiffResult,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct EntryBytesReader<'lt, R> {
|
|
||||||
inner: &'lt mut EndianReader<R>,
|
|
||||||
variant: Variant,
|
|
||||||
}
|
|
||||||
|
|
||||||
const OFFSET_BUFFER_LEN: usize = 8;
|
|
||||||
|
|
||||||
enum Variant {
|
|
||||||
Immediate {
|
|
||||||
/// An in-value reader if the offset field contains the data. Note that we rotate it to the
|
|
||||||
/// back of this buffer, not the front, so that `Cursor` already contains the right end.
|
|
||||||
/// We do not let you seek back beyond our start.
|
|
||||||
data: Cursor<[u8; OFFSET_BUFFER_LEN]>,
|
|
||||||
value_bytes: u8,
|
|
||||||
},
|
|
||||||
InFile {
|
|
||||||
// the length relative to the base so we can seek relative without querying the position.
|
|
||||||
value_bytes: u64,
|
|
||||||
remainder: u64,
|
|
||||||
base: u64,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lt, R> EntryBytesReader<'lt, R> {
|
|
||||||
pub(super) fn from_entry(inner: &'lt mut ValueReader<R>, entry: Entry) -> TiffResult<Self>
|
|
||||||
where
|
|
||||||
R: Read + Seek,
|
|
||||||
{
|
|
||||||
let offset: [_; OFFSET_BUFFER_LEN] = *entry.offset_raw();
|
|
||||||
let limit = if inner.bigtiff { 8 } else { 4 };
|
|
||||||
assert!(limit as usize <= OFFSET_BUFFER_LEN);
|
|
||||||
|
|
||||||
let value_bytes = entry.field_type().value_bytes(entry.count())?;
|
|
||||||
|
|
||||||
if value_bytes <= limit {
|
|
||||||
// We move the data to the back of the array whereas it is in the front right now.
|
|
||||||
let mut value = offset;
|
|
||||||
value.rotate_left(value_bytes as usize);
|
|
||||||
let mut cursor = Cursor::new(value);
|
|
||||||
cursor.set_position(OFFSET_BUFFER_LEN as u64 - value_bytes);
|
|
||||||
|
|
||||||
Ok(EntryBytesReader {
|
|
||||||
inner: &mut inner.reader,
|
|
||||||
variant: Variant::Immediate {
|
|
||||||
data: cursor,
|
|
||||||
value_bytes: value_bytes as u8,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// remainder > limit >= 0 so this is an unreachable panic.
|
|
||||||
let pointer = if inner.bigtiff {
|
|
||||||
let mut offset = offset;
|
|
||||||
|
|
||||||
inner
|
|
||||||
.reader
|
|
||||||
.byte_order
|
|
||||||
.convert(Type::IFD8, &mut offset[..], ByteOrder::native());
|
|
||||||
|
|
||||||
IfdPointer(u64::from_ne_bytes(offset))
|
|
||||||
} else {
|
|
||||||
let mut offset = *offset.first_chunk::<4>().unwrap();
|
|
||||||
|
|
||||||
inner
|
|
||||||
.reader
|
|
||||||
.byte_order
|
|
||||||
.convert(Type::IFD, &mut offset[..], ByteOrder::native());
|
|
||||||
|
|
||||||
let offset = u32::from_ne_bytes(offset);
|
|
||||||
IfdPointer(u64::from(offset))
|
|
||||||
};
|
|
||||||
|
|
||||||
inner.reader.goto_offset(pointer.0)?;
|
|
||||||
|
|
||||||
Ok(EntryBytesReader {
|
|
||||||
inner: &mut inner.reader,
|
|
||||||
variant: Variant::InFile {
|
|
||||||
value_bytes,
|
|
||||||
remainder: value_bytes,
|
|
||||||
base: pointer.0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: Read> Read for EntryBytesReader<'_, R> {
|
|
||||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
||||||
match &mut self.variant {
|
|
||||||
Variant::Immediate { data, .. } => data.read(buf),
|
|
||||||
Variant::InFile { remainder, .. } => {
|
|
||||||
let bound = usize::try_from(*remainder).unwrap_or(usize::MAX);
|
|
||||||
let avail = buf.len().min(bound);
|
|
||||||
let read = self.inner.inner().read(&mut buf[..avail])?;
|
|
||||||
|
|
||||||
// On a well-behaved `Read`: read <= avail <= bound <=(mathematical) remainder
|
|
||||||
let consume = read as u64;
|
|
||||||
*remainder -= consume;
|
|
||||||
Ok(read)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
|
|
||||||
match &mut self.variant {
|
|
||||||
Variant::Immediate { data, .. } => data.read_exact(buf),
|
|
||||||
Variant::InFile { remainder, .. } => {
|
|
||||||
let bound = usize::try_from(*remainder).unwrap_or(usize::MAX);
|
|
||||||
|
|
||||||
// Fail early if we would be sure to over-read.
|
|
||||||
if buf.len() > bound {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::UnexpectedEof,
|
|
||||||
"Tiff value shorter than the requested buffer size",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// since buf.len() <= bound <=(mathematical) remainder
|
|
||||||
let consume = buf.len() as u64;
|
|
||||||
self.inner.inner().read_exact(buf)?;
|
|
||||||
*remainder -= consume;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: Read + Seek> Seek for EntryBytesReader<'_, R> {
|
|
||||||
fn seek(&mut self, offset: io::SeekFrom) -> io::Result<u64> {
|
|
||||||
let (value_bytes, remainder) = match &self.variant {
|
|
||||||
Variant::Immediate { data, value_bytes } => {
|
|
||||||
let remainder = data.get_ref().len() as u64 - data.position();
|
|
||||||
(*value_bytes as u64, remainder)
|
|
||||||
}
|
|
||||||
&Variant::InFile {
|
|
||||||
value_bytes,
|
|
||||||
remainder,
|
|
||||||
base: _,
|
|
||||||
} => (value_bytes, remainder),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Normalize to an offset from the start
|
|
||||||
let offset_from_start = match offset {
|
|
||||||
io::SeekFrom::Start(n) => Some(n),
|
|
||||||
io::SeekFrom::Current(n) => {
|
|
||||||
let front = value_bytes - remainder;
|
|
||||||
front.checked_add_signed(n)
|
|
||||||
}
|
|
||||||
io::SeekFrom::End(n) => value_bytes.checked_add_signed(n),
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some((offset, remainder)) = offset_from_start.and_then(|n| {
|
|
||||||
let remainder = value_bytes.checked_sub(n)?;
|
|
||||||
Some((n, remainder))
|
|
||||||
}) else {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"seek out of bounds of entry value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
match &mut self.variant {
|
|
||||||
Variant::Immediate { data, .. } => {
|
|
||||||
let start = OFFSET_BUFFER_LEN as u64 - value_bytes;
|
|
||||||
data.set_position(start + offset);
|
|
||||||
Ok(offset)
|
|
||||||
}
|
|
||||||
&mut Variant::InFile { base, .. } => {
|
|
||||||
// We checked this against overflow on construction. Ignore the offset we get.
|
|
||||||
self.inner
|
|
||||||
.inner()
|
|
||||||
.seek(io::SeekFrom::Start(base + offset))?;
|
|
||||||
|
|
||||||
self.variant = Variant::InFile {
|
|
||||||
value_bytes,
|
|
||||||
remainder,
|
|
||||||
base,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(offset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stream_position(&mut self) -> io::Result<u64> {
|
|
||||||
Ok(match &self.variant {
|
|
||||||
Variant::Immediate { data, value_bytes } => {
|
|
||||||
// Recall the value is at the end of the buffer.
|
|
||||||
let from_end = OFFSET_BUFFER_LEN as u64 - data.position();
|
|
||||||
u64::from(*value_bytes) - from_end
|
|
||||||
}
|
|
||||||
// Note: we might think about using the inner `stream_position` here. However that
|
|
||||||
// exposes something interesting: If the internal stream is modified concurrently from
|
|
||||||
// another instance (e.g. a shared `File`) then we may lose track of it anyways.
|
|
||||||
// Especially the `Read` implementation is not bounded on `Seek` so it could not
|
|
||||||
// acquire the position. And it should not since that is just a different form of race
|
|
||||||
// where we risk the position being modified by a different thread just as well.
|
|
||||||
//
|
|
||||||
// So in the interest of least surprise we just assume that we can track the position
|
|
||||||
// accurately here and that the inner reader uses for instance `read_at` consistently.
|
|
||||||
// We own the value (or mutable reference) it after all.
|
|
||||||
//
|
|
||||||
// Also this one MUST be compatible with `seek(SeekFrom::Current(0))` as per trait
|
|
||||||
// defintion—so at least keep those consistent if changing anything.
|
|
||||||
&Variant::InFile {
|
|
||||||
remainder,
|
|
||||||
value_bytes,
|
|
||||||
..
|
|
||||||
} => value_bytes - remainder,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::{decoder::Decoder, encoder, tags};
|
|
||||||
use std::io::{Read as _, Seek as _, SeekFrom};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn can_seek_tag_bytes() -> Result<(), crate::error::TiffError> {
|
|
||||||
let file = std::fs::File::open(concat!(
|
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
|
||||||
"/tests/images/int8_rgb.tif"
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut decoder = Decoder::open(file).unwrap();
|
|
||||||
decoder.next_directory().unwrap();
|
|
||||||
|
|
||||||
let mut buffer = crate::tags::ValueBuffer::default();
|
|
||||||
|
|
||||||
decoder
|
|
||||||
.current_ifd()
|
|
||||||
.find_tag_buf(tags::Tag::StripByteCounts, &mut buffer)
|
|
||||||
.expect("some strip byte count is present");
|
|
||||||
|
|
||||||
let bytes_of_value = buffer.as_bytes().len() as u64;
|
|
||||||
|
|
||||||
let mut reader = decoder
|
|
||||||
.read_tag(tags::Tag::StripByteCounts)?
|
|
||||||
.expect("some strip byte count is present");
|
|
||||||
|
|
||||||
let mut data = vec![];
|
|
||||||
reader.read_to_end(&mut data)?;
|
|
||||||
|
|
||||||
// `ValueBuffer` also stores its data in the file endianess until converted.
|
|
||||||
assert_eq!(reader.stream_position()?, bytes_of_value);
|
|
||||||
assert_eq!(buffer.as_bytes(), data);
|
|
||||||
|
|
||||||
reader.seek(SeekFrom::Start(0))?;
|
|
||||||
assert_eq!(reader.stream_position()?, 0);
|
|
||||||
reader.read_to_end(&mut data)?;
|
|
||||||
assert_eq!(reader.stream_position()?, bytes_of_value);
|
|
||||||
|
|
||||||
let (first, second) = data.split_at(bytes_of_value as usize);
|
|
||||||
assert_eq!(first, second);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[allow(clippy::seek_from_current)] // We want to test that functionality.
|
|
||||||
fn seek_from_end() -> Result<(), crate::error::TiffError> {
|
|
||||||
const ARTIST: &str = "99 little bugs in the code";
|
|
||||||
|
|
||||||
let data = {
|
|
||||||
let mut data = std::io::Cursor::new(vec![]);
|
|
||||||
let mut encoder = encoder::TiffEncoder::new(&mut data)?;
|
|
||||||
|
|
||||||
let mut dir = encoder.image_directory()?;
|
|
||||||
dir.write_tag(tags::Tag::Artist, ARTIST)?;
|
|
||||||
dir.finish()?;
|
|
||||||
|
|
||||||
data.into_inner()
|
|
||||||
};
|
|
||||||
|
|
||||||
let file = std::io::Cursor::new(data);
|
|
||||||
let mut decoder = Decoder::open(file).unwrap();
|
|
||||||
decoder.next_directory().unwrap();
|
|
||||||
|
|
||||||
let mut reader = decoder
|
|
||||||
.read_tag(tags::Tag::Artist)?
|
|
||||||
.expect("the artist is present?");
|
|
||||||
|
|
||||||
let mut value = vec![];
|
|
||||||
reader.read_to_end(&mut value)?;
|
|
||||||
|
|
||||||
let (&nul, value) = value.split_last().unwrap();
|
|
||||||
assert_eq!(value, ARTIST.as_bytes());
|
|
||||||
assert_eq!(nul, b'\0');
|
|
||||||
|
|
||||||
assert_eq!(reader.seek(SeekFrom::End(0))?, ARTIST.len() as u64 + 1);
|
|
||||||
assert!(reader.seek(SeekFrom::End(1)).is_err());
|
|
||||||
assert_eq!(reader.stream_position()?, ARTIST.len() as u64 + 1);
|
|
||||||
|
|
||||||
// Check relative seeks by comparing against the "code"
|
|
||||||
let code_offset = reader.seek(SeekFrom::End(-5))?;
|
|
||||||
let mut is_code = [0u8; 4];
|
|
||||||
reader.read_exact(&mut is_code)?;
|
|
||||||
assert_eq!(is_code, *value.last_chunk::<4>().unwrap());
|
|
||||||
|
|
||||||
assert_eq!(reader.seek(SeekFrom::Start(0))?, 0);
|
|
||||||
assert_eq!(reader.stream_position()?, 0);
|
|
||||||
assert_eq!(reader.seek(SeekFrom::Current(0))?, 0);
|
|
||||||
assert_eq!(reader.stream_position()?, 0);
|
|
||||||
assert!(reader.seek(SeekFrom::Current(-1)).is_err());
|
|
||||||
assert_eq!(reader.stream_position()?, 0);
|
|
||||||
|
|
||||||
reader.seek(SeekFrom::Start(code_offset))?;
|
|
||||||
reader.read_exact(&mut is_code)?;
|
|
||||||
assert_eq!(reader.stream_position()?, ARTIST.len() as u64);
|
|
||||||
assert_eq!(is_code, *value.last_chunk::<4>().unwrap());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check that the reader from an `offset` field also works.
|
|
||||||
#[test]
|
|
||||||
fn read_from_offset() -> Result<(), crate::error::TiffError> {
|
|
||||||
let data = {
|
|
||||||
let mut data = std::io::Cursor::new(vec![]);
|
|
||||||
let mut encoder = encoder::TiffEncoder::new(&mut data)?;
|
|
||||||
|
|
||||||
let mut dir = encoder.image_directory()?;
|
|
||||||
dir.write_tag(
|
|
||||||
tags::Tag::PlanarConfiguration,
|
|
||||||
tags::PlanarConfiguration::Planar,
|
|
||||||
)?;
|
|
||||||
dir.finish()?;
|
|
||||||
|
|
||||||
data.into_inner()
|
|
||||||
};
|
|
||||||
|
|
||||||
let file = std::io::Cursor::new(data);
|
|
||||||
let mut decoder = Decoder::open(file).unwrap();
|
|
||||||
decoder.next_directory().unwrap();
|
|
||||||
let bo = decoder.byte_order();
|
|
||||||
|
|
||||||
let mut reader = decoder
|
|
||||||
.read_tag(tags::Tag::PlanarConfiguration)?
|
|
||||||
.expect("some artist is present");
|
|
||||||
|
|
||||||
let mut buf = [0; 2];
|
|
||||||
reader.read_exact(&mut buf)?;
|
|
||||||
|
|
||||||
bo.convert(tags::Type::SHORT, &mut buf, tags::ByteOrder::BigEndian);
|
|
||||||
assert_eq!(buf, [0, 2]);
|
|
||||||
|
|
||||||
// Check relative seeks, this item is only two bytes long.
|
|
||||||
assert!(reader.seek(SeekFrom::End(-3)).is_err());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+18
-28
@@ -393,7 +393,7 @@ impl Entry {
|
|||||||
self.count
|
self.count
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn offset_raw(&self) -> &[u8; 8] {
|
pub(crate) fn offset(&self) -> &[u8] {
|
||||||
&self.offset
|
&self.offset
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,19 +583,19 @@ impl Entry {
|
|||||||
|
|
||||||
match self.type_ {
|
match self.type_ {
|
||||||
Type::BYTE | Type::UNDEFINED => {
|
Type::BYTE | Type::UNDEFINED => {
|
||||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, 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, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, 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, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, limits)?;
|
||||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -605,7 +605,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::SSHORT => {
|
Type::SSHORT => {
|
||||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, limits)?;
|
||||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -615,7 +615,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::LONG => {
|
Type::LONG => {
|
||||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, limits)?;
|
||||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -625,7 +625,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::SLONG => {
|
Type::SLONG => {
|
||||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, limits)?;
|
||||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -635,7 +635,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::FLOAT => {
|
Type::FLOAT => {
|
||||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, limits)?;
|
||||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -645,7 +645,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::DOUBLE => {
|
Type::DOUBLE => {
|
||||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, limits)?;
|
||||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -655,7 +655,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::RATIONAL => {
|
Type::RATIONAL => {
|
||||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, limits)?;
|
||||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(bytes.chunks_exact(8).map(|ch| {
|
v.extend(bytes.chunks_exact(8).map(|ch| {
|
||||||
Rational(
|
Rational(
|
||||||
@@ -666,7 +666,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::SRATIONAL => {
|
Type::SRATIONAL => {
|
||||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, limits)?;
|
||||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(bytes.chunks_exact(8).map(|ch| {
|
v.extend(bytes.chunks_exact(8).map(|ch| {
|
||||||
SRational(
|
SRational(
|
||||||
@@ -677,7 +677,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::LONG8 => {
|
Type::LONG8 => {
|
||||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, limits)?;
|
||||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -687,7 +687,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::SLONG8 => {
|
Type::SLONG8 => {
|
||||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, limits)?;
|
||||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -697,7 +697,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::IFD => {
|
Type::IFD => {
|
||||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, limits)?;
|
||||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -707,7 +707,7 @@ impl Entry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Type::IFD8 => {
|
Type::IFD8 => {
|
||||||
v = Self::vec_with_capacity(self.count, self.type_, limits)?;
|
v = Self::vec_with_capacity(self.count, limits)?;
|
||||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||||
v.extend(
|
v.extend(
|
||||||
bytes
|
bytes
|
||||||
@@ -719,8 +719,8 @@ impl Entry {
|
|||||||
Type::ASCII => {
|
Type::ASCII => {
|
||||||
let n = usize::try_from(self.count)?;
|
let n = usize::try_from(self.count)?;
|
||||||
|
|
||||||
if n > limits.ifd_value_size {
|
if n > limits.decoding_buffer_size {
|
||||||
return Err(TiffError::LimitsExceeded);
|
return Err(dbg!(TiffError::LimitsExceeded));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut out = vec![0; n];
|
let mut out = vec![0; n];
|
||||||
@@ -846,20 +846,10 @@ 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> {
|
||||||
// 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)?;
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-31
@@ -155,6 +155,32 @@ pub(crate) struct ReadoutLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
|
pub(crate) fn no_image() -> Image {
|
||||||
|
Image {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
bits_per_sample: 1,
|
||||||
|
samples: 1,
|
||||||
|
extra_samples: vec![],
|
||||||
|
photometric_samples: 1,
|
||||||
|
sample_format: SampleFormat::Uint,
|
||||||
|
photometric_interpretation: PhotometricInterpretation::BlackIsZero,
|
||||||
|
compression_method: CompressionMethod::None,
|
||||||
|
jpeg_tables: None,
|
||||||
|
predictor: Predictor::None,
|
||||||
|
chunk_type: ChunkType::Strip,
|
||||||
|
planar_config: PlanarConfiguration::Chunky,
|
||||||
|
strip_decoder: None,
|
||||||
|
tile_attributes: None,
|
||||||
|
chunk_offsets: Vec::new(),
|
||||||
|
chunk_bytes: Vec::new(),
|
||||||
|
chroma_subsampling: (2, 2),
|
||||||
|
fill_order: 1,
|
||||||
|
color_map: None,
|
||||||
|
decompression_to_host_endian: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn from_reader<R: Read + Seek>(
|
pub fn from_reader<R: Read + Seek>(
|
||||||
decoder: &mut ValueReader<R>,
|
decoder: &mut ValueReader<R>,
|
||||||
ifd: &Directory,
|
ifd: &Directory,
|
||||||
@@ -454,9 +480,8 @@ 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 = 1usize
|
let expected_len = 3usize
|
||||||
.checked_shl(bits_per_sample as u32)
|
.checked_mul(1usize << bits_per_sample)
|
||||||
.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(
|
||||||
@@ -503,17 +528,6 @@ impl Image {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn check_chunk_type(&self, expected: ChunkType) -> TiffResult<()> {
|
|
||||||
if expected != self.chunk_type {
|
|
||||||
return Err(TiffError::UsageError(UsageError::InvalidChunkType(
|
|
||||||
expected,
|
|
||||||
self.chunk_type,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
|
pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
|
||||||
let is_alpha_extra_samples = matches!(
|
let is_alpha_extra_samples = matches!(
|
||||||
self.extra_samples.as_slice(),
|
self.extra_samples.as_slice(),
|
||||||
@@ -1010,7 +1024,6 @@ 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,
|
||||||
@@ -1193,7 +1206,6 @@ impl Image {
|
|||||||
samples,
|
samples,
|
||||||
ByteOrder::native(),
|
ByteOrder::native(),
|
||||||
predictor,
|
predictor,
|
||||||
scratch,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||||
@@ -1212,7 +1224,6 @@ impl Image {
|
|||||||
samples,
|
samples,
|
||||||
decompressed_byte_order,
|
decompressed_byte_order,
|
||||||
predictor,
|
predictor,
|
||||||
scratch,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1228,18 +1239,9 @@ 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 => {
|
16 => predict_f16(&mut encoded, row, samples),
|
||||||
let (out, _) = row.as_chunks_mut::<2>();
|
32 => predict_f32(&mut encoded, row, samples),
|
||||||
predict_f16(&mut encoded, out, samples);
|
64 => predict_f64(&mut encoded, row, samples),
|
||||||
}
|
|
||||||
32 => {
|
|
||||||
let (out, _) = row.as_chunks_mut::<4>();
|
|
||||||
predict_f32(&mut encoded, out, samples);
|
|
||||||
}
|
|
||||||
64 => {
|
|
||||||
let (out, _) = row.as_chunks_mut::<8>();
|
|
||||||
predict_f64(&mut encoded, out, samples);
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||||
@@ -1268,7 +1270,6 @@ impl Image {
|
|||||||
samples,
|
samples,
|
||||||
decompressed_byte_order,
|
decompressed_byte_order,
|
||||||
predictor,
|
predictor,
|
||||||
scratch,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||||
@@ -1310,7 +1311,6 @@ impl Image {
|
|||||||
samples,
|
samples,
|
||||||
decompressed_byte_order,
|
decompressed_byte_order,
|
||||||
predictor,
|
predictor,
|
||||||
scratch,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||||
|
|||||||
@@ -87,12 +87,15 @@ 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];
|
||||||
|
|
||||||
while let Some(pixels) = buf.get_mut(i..) {
|
loop {
|
||||||
|
let Some(pixels) = buf.get_mut(i..) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
if pixels.is_empty() {
|
if pixels.is_empty() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -121,13 +124,6 @@ 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(())
|
||||||
|
|||||||
+183
-602
File diff suppressed because it is too large.
Load diff
@@ -448,7 +448,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 = sum.checked_add(n)?;
|
sum += n;
|
||||||
if n < 64 {
|
if n < 64 {
|
||||||
return Some(sum);
|
return Some(sum);
|
||||||
}
|
}
|
||||||
@@ -475,10 +475,7 @@ impl Read for Group3Reader {
|
|||||||
};
|
};
|
||||||
match run {
|
match run {
|
||||||
Some(len) => {
|
Some(len) => {
|
||||||
a0 = match a0.checked_add(len) {
|
a0 += len;
|
||||||
Some(v) => v,
|
|
||||||
None => break, // overflow = corrupt data
|
|
||||||
};
|
|
||||||
transitions.push(a0);
|
transitions.push(a0);
|
||||||
is_white = !is_white;
|
is_white = !is_white;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ use crate::{
|
|||||||
/// [`Decoder::read_directory_tags`](crate::decoder::Decoder::read_directory_tags) to read the
|
/// [`Decoder::read_directory_tags`](crate::decoder::Decoder::read_directory_tags) to read the
|
||||||
/// values associated with tags from an underlying file.
|
/// values associated with tags from an underlying file.
|
||||||
#[doc(alias = "IFD")]
|
#[doc(alias = "IFD")]
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Directory {
|
pub struct Directory {
|
||||||
/// There are at most `u16::MAX` entries in any single directory, the count is stored as a
|
/// There are at most `u16::MAX` entries in any single directory, the count is stored as a
|
||||||
/// 2-byte value. The order in the file is implied to be ascending by tag value (the decoder
|
/// 2-byte value. The order in the file is implied to be ascending by tag value (the decoder
|
||||||
|
|||||||
+11
-2
@@ -28,6 +28,8 @@ use self::compression::Compression as Comp;
|
|||||||
use self::compression::*;
|
use self::compression::*;
|
||||||
use self::writer::*;
|
use self::writer::*;
|
||||||
|
|
||||||
|
pub use self::writer::TiffWriter;
|
||||||
|
|
||||||
/// Type of prediction to prepare the image with.
|
/// Type of prediction to prepare the image with.
|
||||||
///
|
///
|
||||||
/// Image data can be very unpredictable, and thus very hard to compress. Predictors are simple
|
/// Image data can be very unpredictable, and thus very hard to compress. Predictors are simple
|
||||||
@@ -185,6 +187,14 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference to a writer for raw values.
|
||||||
|
///
|
||||||
|
/// This can be used to write custom data sections to the underlying writer that may or may not
|
||||||
|
/// be referenced in the IFDs of the tiff structure.
|
||||||
|
pub fn get_mut(&mut self) -> &mut TiffWriter<W> {
|
||||||
|
&mut self.writer
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a [`DirectoryEncoder`] to encode an ifd directory.
|
/// Create a [`DirectoryEncoder`] to encode an ifd directory.
|
||||||
#[deprecated = "`image_directory` replaced the old behavior and clarifies the intent"]
|
#[deprecated = "`image_directory` replaced the old behavior and clarifies the intent"]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
@@ -488,8 +498,7 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
|||||||
self.writer.write_u16(entry.field_type().to_u16())?;
|
self.writer.write_u16(entry.field_type().to_u16())?;
|
||||||
let count = K::convert_offset(entry.count())?;
|
let count = K::convert_offset(entry.count())?;
|
||||||
count.write(self.writer)?;
|
count.write(self.writer)?;
|
||||||
self.writer
|
self.writer.write_bytes(&entry.offset()[..offset_bytes])?;
|
||||||
.write_bytes(&entry.offset_raw()[..offset_bytes])?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(offset)
|
Ok(offset)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub fn write_bigtiff_header<W: Write>(writer: &mut TiffWriter<W>) -> TiffResult<
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A writer that tracks offset and can compress.
|
||||||
pub struct TiffWriter<W> {
|
pub struct TiffWriter<W> {
|
||||||
writer: W,
|
writer: W,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
@@ -61,6 +62,7 @@ impl<W: Write> TiffWriter<W> {
|
|||||||
self.compressor = compressor;
|
self.compressor = compressor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unset the compression.
|
||||||
pub fn reset_compression(&mut self) {
|
pub fn reset_compression(&mut self) {
|
||||||
self.compressor = Compressor::default();
|
self.compressor = Compressor::default();
|
||||||
}
|
}
|
||||||
@@ -73,6 +75,7 @@ impl<W: Write> TiffWriter<W> {
|
|||||||
self.offset() - core::mem::size_of::<K::OffsetType>() as u64
|
self.offset() - core::mem::size_of::<K::OffsetType>() as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the number of bytes written by the last write operation.
|
||||||
pub fn last_written(&self) -> u64 {
|
pub fn last_written(&self) -> u64 {
|
||||||
self.byte_count
|
self.byte_count
|
||||||
}
|
}
|
||||||
@@ -183,6 +186,19 @@ impl<W: Write> TiffWriter<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write uncompressed bytes to the raw underlying file.
|
||||||
|
impl<W: Write> std::io::Write for TiffWriter<W> {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
let n = self.writer.write(buf)?;
|
||||||
|
self.offset += n as u64;
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.writer.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<W: Seek> TiffWriter<W> {
|
impl<W: Seek> TiffWriter<W> {
|
||||||
pub fn goto_offset(&mut self, offset: u64) -> Result<(), io::Error> {
|
pub fn goto_offset(&mut self, offset: u64) -> Result<(), io::Error> {
|
||||||
self.offset = offset;
|
self.offset = offset;
|
||||||
|
|||||||
@@ -567,13 +567,6 @@ impl ValueBuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an empty value buffer with `Byte` type. This type choice is arbitrary.
|
|
||||||
impl Default for ValueBuffer {
|
|
||||||
fn default() -> Self {
|
|
||||||
ValueBuffer::empty(Type::BYTE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Byte order of the TIFF file.
|
/// Byte order of the TIFF file.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum ByteOrder {
|
pub enum ByteOrder {
|
||||||
|
|||||||
@@ -26,20 +26,21 @@ fn test_big_tiff() {
|
|||||||
decoder.colortype().expect("Cannot get colortype"),
|
decoder.colortype().expect("Cannot get colortype"),
|
||||||
ColorType::RGB(8)
|
ColorType::RGB(8)
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut ifd = decoder.current_ifd();
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ifd.get_tag_u64(Tag::StripOffsets)
|
decoder
|
||||||
|
.get_tag_u64(Tag::StripOffsets)
|
||||||
.expect("Cannot get StripOffsets"),
|
.expect("Cannot get StripOffsets"),
|
||||||
16
|
16
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ifd.get_tag_u64(Tag::RowsPerStrip)
|
decoder
|
||||||
|
.get_tag_u64(Tag::RowsPerStrip)
|
||||||
.expect("Cannot get RowsPerStrip"),
|
.expect("Cannot get RowsPerStrip"),
|
||||||
64
|
64
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ifd.get_tag_u64(Tag::StripByteCounts)
|
decoder
|
||||||
|
.get_tag_u64(Tag::StripByteCounts)
|
||||||
.expect("Cannot get StripByteCounts"),
|
.expect("Cannot get StripByteCounts"),
|
||||||
12288
|
12288
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,25 +30,27 @@ fn test_geo_tiff() {
|
|||||||
num_samples: 5
|
num_samples: 5
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut ifd = decoder.current_ifd();
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ifd.get_tag_u64(Tag::StripOffsets)
|
decoder
|
||||||
|
.get_tag_u64(Tag::StripOffsets)
|
||||||
.expect("Cannot get StripOffsets"),
|
.expect("Cannot get StripOffsets"),
|
||||||
418
|
418
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ifd.get_tag_u64(Tag::RowsPerStrip)
|
decoder
|
||||||
|
.get_tag_u64(Tag::RowsPerStrip)
|
||||||
.expect("Cannot get RowsPerStrip"),
|
.expect("Cannot get RowsPerStrip"),
|
||||||
10
|
10
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ifd.get_tag_u64(Tag::StripByteCounts)
|
decoder
|
||||||
|
.get_tag_u64(Tag::StripByteCounts)
|
||||||
.expect("Cannot get StripByteCounts"),
|
.expect("Cannot get StripByteCounts"),
|
||||||
1000
|
1000
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ifd.get_tag(Tag::ModelPixelScaleTag)
|
decoder
|
||||||
|
.get_tag(Tag::ModelPixelScaleTag)
|
||||||
.expect("Cannot get pixel scale")
|
.expect("Cannot get pixel scale")
|
||||||
.into_f64_vec()
|
.into_f64_vec()
|
||||||
.expect("Cannot get pixel scale"),
|
.expect("Cannot get pixel scale"),
|
||||||
|
|||||||
@@ -404,10 +404,7 @@ fn test_string_tags() {
|
|||||||
let img_file = File::open(path).expect("Cannot find test image!");
|
let img_file = File::open(path).expect("Cannot find test image!");
|
||||||
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
|
||||||
decoder.next_image().expect("Cannot read image IFD");
|
decoder.next_image().expect("Cannot read image IFD");
|
||||||
let software = decoder
|
let software = decoder.get_tag(tiff::tags::Tag::Software).unwrap();
|
||||||
.current_ifd()
|
|
||||||
.get_tag(tiff::tags::Tag::Software)
|
|
||||||
.unwrap();
|
|
||||||
match software {
|
match software {
|
||||||
ifd::Value::Ascii(s) => assert_eq!(
|
ifd::Value::Ascii(s) => assert_eq!(
|
||||||
&s,
|
&s,
|
||||||
@@ -481,8 +478,7 @@ fn test_inner_access() {
|
|||||||
decoder.next_image().expect("Cannot read image IFD");
|
decoder.next_image().expect("Cannot read image IFD");
|
||||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
||||||
|
|
||||||
let mut ifd = decoder.current_ifd();
|
let c = decoder
|
||||||
let c = ifd
|
|
||||||
.get_tag(tiff::tags::Tag::Compression)
|
.get_tag(tiff::tags::Tag::Compression)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_u16()
|
.into_u16()
|
||||||
@@ -491,13 +487,13 @@ fn test_inner_access() {
|
|||||||
|
|
||||||
// Because the image is uncompressed, reading the first tile directly with the inner reader
|
// Because the image is uncompressed, reading the first tile directly with the inner reader
|
||||||
// should yield the same result as reading it with the decoder's read_chunk method.
|
// should yield the same result as reading it with the decoder's read_chunk method.
|
||||||
let first_offset = ifd
|
let first_offset = decoder
|
||||||
.get_tag_u32_vec(tiff::tags::Tag::TileOffsets)
|
.get_tag_u32_vec(tiff::tags::Tag::TileOffsets)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()
|
.next()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let first_byte_count = ifd
|
let first_byte_count = decoder
|
||||||
.get_tag_u32_vec(tiff::tags::Tag::TileByteCounts)
|
.get_tag_u32_vec(tiff::tags::Tag::TileByteCounts)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
+11
-40
@@ -40,7 +40,6 @@ fn encode_decode() {
|
|||||||
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
||||||
|
|
||||||
let mut all_tags = decoder
|
let mut all_tags = decoder
|
||||||
.current_ifd()
|
|
||||||
.tag_iter()
|
.tag_iter()
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -81,7 +80,7 @@ fn encode_decode() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decoder.current_ifd().get_tag(Tag::Artist).unwrap(),
|
decoder.get_tag(Tag::Artist).unwrap(),
|
||||||
ifd::Value::Ascii("Image-tiff".into())
|
ifd::Value::Ascii("Image-tiff".into())
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -126,7 +125,6 @@ fn encode_decode_big() {
|
|||||||
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
|
||||||
|
|
||||||
let mut all_tags = decoder
|
let mut all_tags = decoder
|
||||||
.current_ifd()
|
|
||||||
.tag_iter()
|
.tag_iter()
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -167,7 +165,7 @@ fn encode_decode_big() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decoder.current_ifd().get_tag(Tag::Artist).unwrap(),
|
decoder.get_tag(Tag::Artist).unwrap(),
|
||||||
ifd::Value::Ascii("Image-tiff".into())
|
ifd::Value::Ascii("Image-tiff".into())
|
||||||
);
|
);
|
||||||
let mut buffer = tiff::decoder::DecodingSampleBuffer::U8(vec![]);
|
let mut buffer = tiff::decoder::DecodingSampleBuffer::U8(vec![]);
|
||||||
@@ -541,57 +539,37 @@ trait AssertDecode {
|
|||||||
|
|
||||||
impl<R: std::io::Read + std::io::Seek> AssertDecode for Decoder<R> {
|
impl<R: std::io::Read + std::io::Seek> AssertDecode for Decoder<R> {
|
||||||
fn assert_tag_u32(&mut self, tag: u16) -> u32 {
|
fn assert_tag_u32(&mut self, tag: u16) -> u32 {
|
||||||
self.current_ifd()
|
self.get_tag(Tag::Unknown(tag)).unwrap().into_u32().unwrap()
|
||||||
.get_tag(Tag::Unknown(tag))
|
|
||||||
.unwrap()
|
|
||||||
.into_u32()
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32> {
|
fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32> {
|
||||||
self.current_ifd()
|
self.get_tag(Tag::Unknown(tag))
|
||||||
.get_tag(Tag::Unknown(tag))
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_u32_vec()
|
.into_u32_vec()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
fn assert_tag_i32(&mut self, tag: u16) -> i32 {
|
fn assert_tag_i32(&mut self, tag: u16) -> i32 {
|
||||||
self.current_ifd()
|
self.get_tag(Tag::Unknown(tag)).unwrap().into_i32().unwrap()
|
||||||
.get_tag(Tag::Unknown(tag))
|
|
||||||
.unwrap()
|
|
||||||
.into_i32()
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
fn assert_tag_i32_vec(&mut self, tag: u16) -> Vec<i32> {
|
fn assert_tag_i32_vec(&mut self, tag: u16) -> Vec<i32> {
|
||||||
self.current_ifd()
|
self.get_tag(Tag::Unknown(tag))
|
||||||
.get_tag(Tag::Unknown(tag))
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_i32_vec()
|
.into_i32_vec()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
fn assert_tag_u64(&mut self, tag: u16) -> u64 {
|
fn assert_tag_u64(&mut self, tag: u16) -> u64 {
|
||||||
self.current_ifd()
|
self.get_tag(Tag::Unknown(tag)).unwrap().into_u64().unwrap()
|
||||||
.get_tag(Tag::Unknown(tag))
|
|
||||||
.unwrap()
|
|
||||||
.into_u64()
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
fn assert_tag_u64_vec(&mut self, tag: u16) -> Vec<u64> {
|
fn assert_tag_u64_vec(&mut self, tag: u16) -> Vec<u64> {
|
||||||
self.current_ifd()
|
self.get_tag(Tag::Unknown(tag))
|
||||||
.get_tag(Tag::Unknown(tag))
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_u64_vec()
|
.into_u64_vec()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
fn assert_tag_i64(&mut self, tag: u16) -> i64 {
|
fn assert_tag_i64(&mut self, tag: u16) -> i64 {
|
||||||
self.current_ifd()
|
self.get_tag(Tag::Unknown(tag)).unwrap().into_i64().unwrap()
|
||||||
.get_tag(Tag::Unknown(tag))
|
|
||||||
.unwrap()
|
|
||||||
.into_i64()
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
fn assert_tag_i64_vec(&mut self, tag: u16) -> Vec<i64> {
|
fn assert_tag_i64_vec(&mut self, tag: u16) -> Vec<i64> {
|
||||||
self.current_ifd()
|
self.get_tag(Tag::Unknown(tag))
|
||||||
.get_tag(Tag::Unknown(tag))
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_i64_vec()
|
.into_i64_vec()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -798,13 +776,7 @@ fn test_rows_per_strip() {
|
|||||||
{
|
{
|
||||||
let mut decoder = Decoder::open(&mut file).unwrap();
|
let mut decoder = Decoder::open(&mut file).unwrap();
|
||||||
decoder.next_image().unwrap();
|
decoder.next_image().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(decoder.get_tag_u64(Tag::RowsPerStrip).unwrap(), 2);
|
||||||
decoder
|
|
||||||
.current_ifd()
|
|
||||||
.get_tag_u64(Tag::RowsPerStrip)
|
|
||||||
.unwrap(),
|
|
||||||
2
|
|
||||||
);
|
|
||||||
assert_eq!(decoder.strip_count().unwrap(), 50);
|
assert_eq!(decoder.strip_count().unwrap(), 50);
|
||||||
|
|
||||||
for i in 0..50 {
|
for i in 0..50 {
|
||||||
@@ -857,7 +829,6 @@ fn test_auxiliary_directory() {
|
|||||||
let _ = decoder.read_image().unwrap();
|
let _ = decoder.read_image().unwrap();
|
||||||
|
|
||||||
let exif_location = decoder
|
let exif_location = decoder
|
||||||
.current_ifd()
|
|
||||||
.get_tag(Tag::ExifDirectory)
|
.get_tag(Tag::ExifDirectory)
|
||||||
.expect("second directory missing Exif")
|
.expect("second directory missing Exif")
|
||||||
.into_ifd_pointer()
|
.into_ifd_pointer()
|
||||||
|
|||||||
Binary file not shown.
Reference in new issue
Block a user