Compare commits

..
Author SHA1 Message Date
A. Molzer 3340fc04e0 Publicly export TiffWriter 2026-03-31 18:29:48 +02:00
34 changed files with 402 additions and 3050 deletions

No files matched your search

-85
View File
@@ -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
-1
View File
@@ -2,4 +2,3 @@
/target/
**/*.rs.bk
Cargo.lock
tests/benches/corpus/
-7
View File
@@ -1,12 +1,5 @@
# Version 0.12.0 (unreleased)
Fixes:
- Decoding a tiled image whose width is not a multiple of the tile width no
longer corrupts the columns below the right border tiles when extra samples
are discarded (e.g. RGB with an unspecified extra sample). The horizontal
tile padding was written past the end of the output row, spilling into the
leftmost columns of the following row.
Additions:
- Added the `TiffHeader` type. It can be used to create a `Decoder` in a state
before the first IFD and allows opening files at an arbitrary first IFD
+1 -9
View File
@@ -23,9 +23,8 @@ 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.2.1", optional = true }
weezl = { version = "0.1.10", optional = true }
zstd = { version = "0.13", optional = true }
zrip-decode = { version = "0.8.2", features = ["paranoid"], optional = true }
zune-jpeg = { version = "0.5.1", optional = true }
image-webp = {version = "0.2.4", optional = true }
@@ -46,9 +45,6 @@ jpeg = ["dep:zune-jpeg"]
lzw = ["dep:weezl"]
webp = ["dep:image-webp"]
zstd = ["dep:zstd"]
# Fully safe Rust but somewhat slower Zstd decoding.
# When both zstd and zstd-safe-rust are enabled, zstd takes precedence.
zstd-safe-rust = ["dep:zrip-decode"]
[[bench]]
name = "lzw"
@@ -58,10 +54,6 @@ harness = false
name = "encode"
harness = false
[[bench]]
name = "decode"
harness = false
[[test]]
name = "decode_libtiffpic"
+1 -1
View File
@@ -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_comprehensive -snone -- -dict=fuzz/tiff.dict -timeout=5`.
`cargo fuzz run decode_image -snone -- -timeout=5`.
-461
View File
@@ -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();
}
}
+1 -1
View File
@@ -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();
}
}
+8 -10
View File
@@ -2,30 +2,28 @@ use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingSampleBuffer, Decod
fn main() -> Result<(), Box<dyn std::error::Error>> {
let Some(image) = std::env::args_os().nth(1) else {
eprintln!("Usage: decode FILE [--lenient]");
eprintln!("Usage: decode FILE");
return Ok(());
};
let lenient = std::env::args().nth(2).as_deref() == Some("--lenient");
let file = std::fs::File::open(image)?;
let io = std::io::BufReader::new(file);
let mut reader = Decoder::open(io)?.with_lenient(lenient);
let mut reader = Decoder::open(io)?;
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(())
-130
View File
@@ -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
View File
@@ -19,7 +19,7 @@ path = ".."
members = ["."]
[[bin]]
name = "decode_comprehensive"
path = "fuzz_targets/decode_comprehensive.rs"
name = "decode_image"
path = "fuzz_targets/decode_image.rs"
test = false
doc = false
-96
View File
@@ -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();
}
}
+31
View File
@@ -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
View File
@@ -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"
+1 -1
View File
@@ -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(Clone, Default, Debug)]
#[derive(Default, Debug)]
pub struct IfdCycles {
/// The root of each component union.
component_union: HashMap<ComponentId, ComponentId>,
-362
View File
@@ -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(())
}
}
+19 -29
View File
@@ -363,7 +363,7 @@ impl ::std::fmt::Debug for Entry {
fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
fmt.write_str(&format!(
"Entry {{ type_: {:?}, count: {:?}, offset: {:?} }}",
self.type_, self.count, self.offset
self.type_, self.count, &self.offset
))
}
}
@@ -393,7 +393,7 @@ impl Entry {
self.count
}
pub(crate) fn offset_raw(&self) -> &[u8; 8] {
pub(crate) fn offset(&self) -> &[u8] {
&self.offset
}
@@ -583,19 +583,19 @@ impl Entry {
match self.type_ {
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| {
v.extend(bytes.iter().copied().map(Byte))
})
}
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| {
v.extend(bytes.iter().copied().map(|v| SignedByte(v as i8)))
})
}
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| {
v.extend(
bytes
@@ -605,7 +605,7 @@ impl Entry {
})
}
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| {
v.extend(
bytes
@@ -615,7 +615,7 @@ impl Entry {
})
}
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| {
v.extend(
bytes
@@ -625,7 +625,7 @@ impl Entry {
})
}
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| {
v.extend(
bytes
@@ -635,7 +635,7 @@ impl Entry {
})
}
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| {
v.extend(
bytes
@@ -645,7 +645,7 @@ impl Entry {
})
}
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| {
v.extend(
bytes
@@ -655,7 +655,7 @@ impl Entry {
})
}
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| {
v.extend(bytes.chunks_exact(8).map(|ch| {
Rational(
@@ -666,7 +666,7 @@ impl Entry {
})
}
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| {
v.extend(bytes.chunks_exact(8).map(|ch| {
SRational(
@@ -677,7 +677,7 @@ impl Entry {
})
}
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| {
v.extend(
bytes
@@ -687,7 +687,7 @@ impl Entry {
})
}
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| {
v.extend(
bytes
@@ -697,7 +697,7 @@ impl Entry {
})
}
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| {
v.extend(
bytes
@@ -707,7 +707,7 @@ impl Entry {
})
}
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| {
v.extend(
bytes
@@ -719,8 +719,8 @@ impl Entry {
Type::ASCII => {
let n = usize::try_from(self.count)?;
if n > limits.ifd_value_size {
return Err(TiffError::LimitsExceeded);
if n > limits.decoding_buffer_size {
return Err(dbg!(TiffError::LimitsExceeded));
}
let mut out = vec![0; n];
@@ -846,20 +846,10 @@ impl Entry {
fn vec_with_capacity(
value_count: u64,
type_: Type,
limits: &super::Limits,
) -> 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)?;
if value_count > limits.decoding_buffer_size / mem::size_of::<Value>() {
return Err(TiffError::LimitsExceeded);
}
+73 -163
View File
@@ -1,9 +1,8 @@
use super::ifd::Value;
use super::stream::{PackBitsReader, ReadSamples};
use super::stream::PackBitsReader;
use super::tag_reader::TagReader;
use super::ChunkType;
use super::{predict_f16, predict_f32, predict_f64, ValueReader};
use crate::decoder::stream::CompressionEngines;
use crate::tags::{
ByteOrder, CompressionMethod, ExtraSamples, PhotometricInterpretation, PlanarConfiguration,
Predictor, SampleFormat, Tag,
@@ -12,7 +11,7 @@ use crate::{
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
};
use std::io::{Cursor, Read, Seek};
use std::io::{self, Cursor, Read, Seek};
use std::sync::Arc;
#[derive(Debug)]
@@ -98,7 +97,6 @@ pub(crate) struct Image {
/// run on samples directly and thus skip the outer byte order entirely, except potentially
/// they have interior byte order treatment.
pub decompression_to_host_endian: bool,
pub lenient: bool,
}
/// Describes how to read a tile-aligned portion of the image.
@@ -157,10 +155,35 @@ pub(crate) struct ReadoutLayout {
}
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>(
decoder: &mut ValueReader<R>,
ifd: &Directory,
lenient: bool,
) -> TiffResult<Image> {
let mut tag_reader = TagReader { decoder, ifd };
@@ -457,9 +480,8 @@ impl Image {
match tag_reader.find_tag(Tag::ColorMap)? {
Some(v) => {
let map = v.into_u16_vec()?;
let expected_len = 1usize
.checked_shl(bits_per_sample as u32)
.and_then(|n| n.checked_mul(3))
let expected_len = 3usize
.checked_mul(1usize << bits_per_sample)
.ok_or(TiffError::LimitsExceeded)?;
if map.len() != expected_len {
return Err(TiffError::FormatError(
@@ -503,29 +525,14 @@ impl Image {
compression_method,
CompressionMethod::SgiLog | CompressionMethod::SgiLog24
),
lenient,
})
}
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> {
let is_alpha_extra_samples = match self.extra_samples.as_slice().first() {
Some(ExtraSamples::AssociatedAlpha) | Some(ExtraSamples::UnassociatedAlpha) => true,
// In lenient mode, keep unspecified alpha interpretation logic compatible with libtiff
// See Issue #341
Some(ExtraSamples::Unspecified) if self.lenient && self.samples > 3 => true,
_ => false,
};
let is_alpha_extra_samples = matches!(
self.extra_samples.as_slice(),
[ExtraSamples::AssociatedAlpha, ..] | [ExtraSamples::UnassociatedAlpha, ..]
);
match self.photometric_interpretation {
PhotometricInterpretation::RGB => match self.photometric_samples {
@@ -598,30 +605,20 @@ impl Image {
),
)),
},
// CIE L*a*b* (TIFF6 Section 23, pp 110-115) encodes a/b as signed integers
// (int8 at 8-bit, int16 at 16-bit) with 0 neutral, while ICC L*a*b*
// (photometric=9) uses unsigned samples with the neutral at the middle of
// the range. `SampleFormat` applies uniformly to all samples, so it can't
// describe the L=unsigned + a/b=signed mix; instead we take a/b as signed
// from the photometric interpretation (requiring `SampleFormat::Uint`, like
// the ICCLab arm) and re-bias them to the unsigned ICCLab encoding in
// post-processing (see `cielab_to_icclab`). libtiff does the same, decoding
// both depths (`putcontig8bitCIELab8` / `putcontig8bitCIELab16`, selected by
// `initCIELabConversion` in tif_getimage.c line 2216
// @ 04884181e8903915038be553bfab404302d98ea0).
PhotometricInterpretation::CIELab => match self.photometric_samples {
3 if matches!(self.bits_per_sample, 8 | 16)
&& matches!(self.sample_format, SampleFormat::Uint) =>
{
Ok(ColorType::Lab(self.bits_per_sample))
}
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
PhotometricInterpretation::CIELab,
vec![self.bits_per_sample; self.samples as usize],
),
)),
},
// Unsupported due to inherently heterogeneous sample types. This is represented as:
// ```
// struct TiffLab /* Interpretation 8* {
// pub L: u8, // SampleFormat::Uint
// pub a: i8, // SampleFormat::Int
// pub b: i8, // SampleFormat::Int
// }
// ```
PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
PhotometricInterpretation::CIELab,
vec![self.bits_per_sample; self.samples as usize],
),
)),
PhotometricInterpretation::CIELogL => match self.samples {
1 if matches!(self.bits_per_sample, 32)
&& matches!(self.sample_format, SampleFormat::IEEEFP) =>
@@ -677,11 +674,7 @@ impl Image {
),
)),
PhotometricInterpretation::RGBPalette => Ok(ColorType::Palette(self.bits_per_sample)),
// TransparencyMask is a single-channel mask, treat as grayscale.
PhotometricInterpretation::TransparencyMask => {
Ok(ColorType::Gray(self.bits_per_sample))
}
PhotometricInterpretation::Unknown(_) => Err(TiffError::UnsupportedError(
PhotometricInterpretation::TransparencyMask => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
vec![self.bits_per_sample; self.samples as usize],
@@ -699,8 +692,7 @@ impl Image {
dimensions: (u32, u32),
samples: u16,
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] fill_order: u16,
engines: &mut CompressionEngines,
) -> TiffResult<Box<dyn ReadSamples + 'r>> {
) -> TiffResult<Box<dyn Read + 'r>> {
Ok(match compression_method {
CompressionMethod::None => Box::new(reader),
CompressionMethod::SgiLog => match samples {
@@ -731,12 +723,9 @@ impl Image {
CompressionMethod::LZW => Box::new(super::stream::LZWReader::new(
reader,
usize::try_from(compressed_length)?,
engines,
)),
#[cfg(feature = "zstd")]
CompressionMethod::ZSTD => Box::new(zstd::Decoder::new(reader)?),
#[cfg(all(not(feature = "zstd"), feature = "zstd-safe-rust"))]
CompressionMethod::ZSTD => Box::new(zrip_decode::streaming::FrameDecoder::new(reader)),
CompressionMethod::PackBits => Box::new(PackBitsReader::new(reader, compressed_length)),
#[cfg(feature = "deflate")]
CompressionMethod::Deflate | CompressionMethod::OldDeflate => {
@@ -1035,8 +1024,6 @@ impl Image {
buf: &mut [u8],
layout: &ReadoutLayout,
chunk_index: u32,
scratch: &mut Vec<u8>,
engines: &mut CompressionEngines,
) -> TiffResult<()> {
let ValueReader {
reader,
@@ -1062,7 +1049,6 @@ impl Image {
| ColorType::YCbCr(n)
| ColorType::Gray(n)
| ColorType::Palette(n)
| ColorType::Lab(n)
| ColorType::Multiband {
bit_depth: n,
num_samples: _,
@@ -1074,7 +1060,6 @@ impl Image {
| ColorType::YCbCr(n)
| ColorType::Gray(n)
| ColorType::Palette(n)
| ColorType::Lab(n)
| ColorType::Multiband {
bit_depth: n,
num_samples: _,
@@ -1183,7 +1168,6 @@ impl Image {
chunk_dims,
self.samples,
self.fill_order,
engines,
)?;
// Extended bit depths (9-15, 17-31): packed N-bit samples must be unpacked to
@@ -1219,26 +1203,14 @@ impl Image {
super::fix_endianness_and_predict(
out_row,
color_type.bit_depth(),
data_samples,
samples,
ByteOrder::native(),
predictor,
scratch,
);
// An n-bit predictor delta is stored modulo 2^n, but the
// reconstruction above accumulated it in the promoted u16/u32
// storage width. Reduce back modulo the declared depth so both
// the inverted and non-inverted paths see exact, in-range
// values (2^n divides the storage modulus, so one mask after
// the row is exact).
super::mask_samples_to_bit_depth(out_row, tiff_bps, out_bps, self.sample_format);
super::post_process_row(
out_row,
photometric_interpretation,
color_type,
self.sample_format,
)?;
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(out_row, color_type, self.sample_format)?;
}
}
} else if is_output_chunk_rows && is_all_bits {
// Here we can read directly into the output buffer itself.
@@ -1252,26 +1224,12 @@ impl Image {
samples,
decompressed_byte_order,
predictor,
scratch,
);
}
// No-op in this branch: promoted layouts (declared depth < storage
// width) are handled exclusively by the unpacking branch above.
// Called in every integer branch so they cannot diverge.
super::mask_samples_to_bit_depth(
tile,
layout.tiff_bits_per_sample,
layout.output_bits_per_sample,
self.sample_format,
);
super::post_process_row(
tile,
photometric_interpretation,
color_type,
self.sample_format,
)?;
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(tile, color_type, self.sample_format)?;
}
} else if chunk_row_bytes > data_row_bytes && self.predictor == Predictor::FloatingPoint {
// The floating point predictor shuffles the padding bytes into the encoded output, so
// this case is handled specially when needed.
@@ -1281,28 +1239,14 @@ impl Image {
let row = &mut row[..data_row_bytes];
match color_type.bit_depth() {
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);
}
16 => predict_f16(&mut encoded, row, samples),
32 => predict_f32(&mut encoded, row, samples),
64 => predict_f64(&mut encoded, row, samples),
_ => unreachable!(),
}
// No masking here: the floating point predictor requires
// IEEEFP samples, which are never depth-promoted.
super::post_process_row(
row,
photometric_interpretation,
color_type,
self.sample_format,
)?;
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(row, color_type, self.sample_format)?;
}
}
} else if is_all_bits {
// We read row-by-row but each row fits in its output buffer.
@@ -1313,12 +1257,11 @@ impl Image {
// Two ways how we get here: we have more bytes in our chunk data than in the image
// we are to read. Then we need to skip the rest of the data. Or we have a bigger
// row stride than the chunk contains data, then we need to fill only the front.
reader.read_exact(&mut row[..used])?;
// Skip horizontal padding
if chunk_row_bytes > data_row_bytes {
// Skip horizontal padding
let skip = u64::try_from(chunk_row_bytes - data_row_bytes)?;
reader.read_padded_samples(&mut row[..used], skip)?;
} else {
reader.read_exact(&mut row[..used])?;
let len = u64::try_from(chunk_row_bytes - data_row_bytes)?;
io::copy(&mut reader.by_ref().take(len), &mut io::sink())?;
}
super::fix_endianness_and_predict(
@@ -1327,25 +1270,11 @@ impl Image {
samples,
decompressed_byte_order,
predictor,
scratch,
);
// No-op in this branch: promoted layouts only occur in the
// unpacking branch. Called so the integer branches cannot
// diverge.
super::mask_samples_to_bit_depth(
row,
layout.tiff_bits_per_sample,
layout.output_bits_per_sample,
self.sample_format,
);
super::post_process_row(
row,
photometric_interpretation,
color_type,
self.sample_format,
)?;
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(row, color_type, self.sample_format)?;
}
}
} else {
// The encoded data potentially takes up more space than the output data so we must be
@@ -1372,10 +1301,6 @@ impl Image {
let photo_range = photometric_bit_end / 8..bits_per_pixel / 8;
let mut encoded = vec![0u8; chunk_row_bytes];
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
// Clip to the chunk's data width: border chunks are stored at the full chunk
// width, and writing their horizontal padding would spill into the following
// output row (and predict/invert would run past the chunk's own columns).
let row = &mut row[..data_row_bytes];
reader.read_exact(&mut encoded)?;
Self::compact_photometric_bytes(&mut encoded, row, &photo_range);
@@ -1386,29 +1311,14 @@ impl Image {
samples,
decompressed_byte_order,
predictor,
scratch,
);
// No-op in this branch: promoted layouts only occur in the
// unpacking branch. Called so the integer branches cannot
// diverge.
super::mask_samples_to_bit_depth(
row,
layout.tiff_bits_per_sample,
layout.output_bits_per_sample,
self.sample_format,
);
super::post_process_row(
row,
photometric_interpretation,
color_type,
self.sample_format,
)?;
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(row, color_type, self.sample_format)?;
}
}
}
reader.cache_or_destroy(engines);
Ok(())
}
+5 -33
View File
@@ -83,24 +83,19 @@ impl<R: Read> Read for LogLuv24<R> {
}
}
impl<R: Read> super::stream::ReadSamples for LogLuv24<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> std::io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
fn packbits_decode<R: Read, const N: usize>(
reader: &mut R,
buf: &mut [[u8; N]],
) -> std::io::Result<()> {
let expected = buf.len();
for offset in (0..N).rev() {
let mut i = 0;
let mut code = [0u8; 1];
while let Some(pixels) = buf.get_mut(i..) {
loop {
let Some(pixels) = buf.get_mut(i..) else {
break;
};
if pixels.is_empty() {
break;
}
@@ -129,13 +124,6 @@ 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(())
@@ -220,14 +208,6 @@ impl<R: Read> Read for LogLuv32<R> {
}
}
impl<R: Read> super::stream::ReadSamples for LogLuv32<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> std::io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
/// Pack-Bits like encoding for a single 16-bit log-L sample.
pub struct LogLuv16<R> {
reader: Take<R>,
@@ -294,14 +274,6 @@ impl<R: Read> Read for LogLuv16<R> {
}
}
impl<R: Read> super::stream::ReadSamples for LogLuv16<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> std::io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
// Table information from: `libtiff`, 1997 by Greg Ward Larson, SGI
#[doc(alias = "UV_SQSIZ")]
const UV_QUANTIZATION_WIDTH: f32 = 0.003500;
+184 -736
View File
File diff suppressed because it is too large. Load diff
+13 -216
View File
@@ -122,86 +122,9 @@ impl<R: Read> EndianReader<R> {
// # READERS
//
#[derive(Default)]
pub(crate) struct CompressionEngines {
#[cfg(feature = "lzw")]
lzw: Option<weezl::decode::Decoder>,
}
impl core::fmt::Debug for CompressionEngines {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut dbg = f.debug_struct("CompressionEngines");
#[cfg(feature = "lzw")]
dbg.field("lzw", &self.lzw.is_some().then_some("<weezl>"));
dbg.finish()
}
}
/// A variant of `io::Read` that takes one block to the end.
///
/// This is derived from `Read` so that any special optimization done by `std` (or any nightly crate
/// for that matter) surely remains usable.
///
/// We want our decompression engines to be much more specialized than the standard library trait.
/// Firstly there is more information available to communicate (the tiff Type, known decompressed
/// size, …) and secondly they can potentially *reset*. This could be solved with an extension trait
/// but since we want to avoid `read` in favor of `read_exact` (or an equivalent if we want to pass
/// in a strided block buffer) a separate trait that's auto derived is favored.
///
/// FIXME: Also consider that we will want to read subsets of samples. Via IO this is done by
/// discarding data *after* it is put into the output buffer. Yet, for some decompression that write
/// alone is considerable work where it could simply skip bits more efficiently. So the reader would
/// likely want to know about a sample mask.
pub trait ReadSamples: Read {
/// Read a row of samples and throw away some additional padding samples at the end.
///
/// This is automatically implemented for `Self: Read` as a copy into `io::Sink`.
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()>;
/// Put this back into our cache of compression engines.
fn cache_or_destroy(self: Box<Self>, _: &mut CompressionEngines) {}
}
/// Note: the `Seek` bound ensures this does not overlap actual decompression.. This is an odd
/// design problem born out of wanting both the underlying `Read` for uncompressed streams due to
/// performance as well as the override to `cache_or_destroy` for all compressed streams.
impl<R: Read + Seek> ReadSamples for R {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
if discard > 0 {
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
}
Ok(())
}
}
/// Type alias for the deflate Reader
#[cfg(feature = "deflate")]
pub struct DeflateReader<R> {
inner: flate2::read::ZlibDecoder<R>,
}
impl<R: Read> DeflateReader<R> {
pub fn new(inner: R) -> Self {
DeflateReader {
inner: flate2::read::ZlibDecoder::new(inner),
}
}
}
impl<R: Read> Read for DeflateReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
}
impl<R: Read> ReadSamples for DeflateReader<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
pub type DeflateReader<R> = flate2::read::ZlibDecoder<R>;
//
// ## LZW Reader
@@ -212,103 +135,31 @@ impl<R: Read> ReadSamples for DeflateReader<R> {
pub struct LZWReader<R: Read> {
reader: BufReader<Take<R>>,
decoder: weezl::decode::Decoder,
is_old_style: bool,
}
#[cfg(feature = "lzw")]
impl<R: Read> LZWReader<R> {
/// Wraps a reader
pub fn new(
reader: R,
compressed_length: usize,
engine: &mut CompressionEngines,
) -> LZWReader<R> {
let mut buffered = BufReader::with_capacity(
(32 * 1024).min(compressed_length),
reader.take(u64::try_from(compressed_length).unwrap()),
);
// `Compression = 5` covers both the current LZW variant and the pre-TIFF-6.0
// "old-style" one, with no tag to distinguish them. New-style is MSB-first with
// the "early change" code-size switch, and its stream begins with the Clear code
// (256) -> first byte 0x80. Old-style is LSB-first *without* the early change
// (GIF-style increment timing), and begins with 0x00 followed by a byte with bit
// 0 set. This is the heuristic libtiff uses to select its compatibility decoder;
// on a short/failed peek we default to the modern variant.
let old_style = matches!(
buffered.fill_buf(),
Ok(head) if head.len() >= 2 && head[0] == 0x00 && head[1] & 0x01 != 0
);
let decoder = if old_style {
weezl::decode::Configuration::new(weezl::BitOrder::Lsb, 8)
.with_yield_on_full_buffer(true)
.build()
} else {
// All standard LZW decoders have the same configuration. We can thus reuse any that we
// have previously constructed, skipping internal buffer allocation. We only need to
// reset them to a new stream.
if let Some(mut lzw) = engine.lzw.take() {
lzw.reset();
lzw
} else {
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
.with_yield_on_full_buffer(true)
.build()
}
};
pub fn new(reader: R, compressed_length: usize) -> LZWReader<R> {
let configuration =
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
.with_yield_on_full_buffer(true);
Self {
reader: buffered,
decoder: decoder,
is_old_style: old_style,
reader: BufReader::with_capacity(
(32 * 1024).min(compressed_length),
reader.take(u64::try_from(compressed_length).unwrap()),
),
decoder: configuration.build(),
}
}
fn discard_n(&mut self, mut n: u64) -> io::Result<()> {
let mut buf = [0u8; 2048];
while n > 0 {
let limit = n.min(2048) as usize;
let result = self
.decoder
.decode_bytes(self.reader.fill_buf()?, &mut buf[..limit]);
self.reader.consume(result.consumed_in);
n -= result.consumed_out as u64;
match result.status {
Ok(weezl::LzwStatus::Ok) => {
continue;
}
Ok(weezl::LzwStatus::NoProgress) => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"no lzw end code found",
));
}
Ok(weezl::LzwStatus::Done) => {
if n != 0 {
// stream is too short.
return Err(std::io::ErrorKind::UnexpectedEof)?;
}
}
Err(err) => return Err(io::Error::new(io::ErrorKind::InvalidData, err)),
}
}
Ok(())
}
}
#[cfg(feature = "lzw")]
impl<R: Read> Read for LZWReader<R> {
fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
loop {
let result = self.decoder.decode_bytes(self.reader.fill_buf()?, buf);
self.reader.consume(result.consumed_in);
let _ = buf.split_off_mut(..result.consumed_out);
match result.status {
Ok(weezl::LzwStatus::Ok) => {
@@ -333,24 +184,6 @@ impl<R: Read> Read for LZWReader<R> {
}
}
impl<R: Read> ReadSamples for LZWReader<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
if discard > 0 {
self.discard_n(discard)?;
}
Ok(())
}
fn cache_or_destroy(self: Box<Self>, engines: &mut CompressionEngines) {
if !self.is_old_style && engines.lzw.is_none() {
engines.lzw = Some(self.decoder);
}
}
}
//
// ## PackBits Reader
//
@@ -423,14 +256,6 @@ impl<R: Read> Read for PackBitsReader<R> {
}
}
impl<R: Read> ReadSamples for PackBitsReader<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
/// Iterator adapter that reverses bits in each byte for FillOrder=2 (LSB-to-MSB).
#[cfg(feature = "fax")]
type FaxBytes<R> =
@@ -547,14 +372,6 @@ impl<R: Read> Read for Group4Reader<R> {
}
}
impl<R: Read> ReadSamples for Group4Reader<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
/// Owning byte reader for Group 3 decoding: the compressed data is consumed
/// into an iterator so the reader doesn't borrow the buffer.
#[cfg(feature = "fax")]
@@ -631,7 +448,7 @@ impl Group3Reader {
) -> Option<u16> {
let mut sum = 0u16;
while let Some(n) = decoder(reader) {
sum = sum.checked_add(n)?;
sum += n;
if n < 64 {
return Some(sum);
}
@@ -658,10 +475,7 @@ impl Read for Group3Reader {
};
match run {
Some(len) => {
a0 = match a0.checked_add(len) {
Some(v) => v,
None => break, // overflow = corrupt data
};
a0 += len;
transitions.push(a0);
is_white = !is_white;
}
@@ -693,14 +507,6 @@ impl Read for Group3Reader {
}
}
impl ReadSamples for Group3Reader {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
#[cfg(feature = "webp")]
pub struct WebPReader {
inner: Cursor<Vec<u8>>,
@@ -756,15 +562,6 @@ impl Read for WebPReader {
}
}
#[cfg(feature = "webp")]
impl ReadSamples for WebPReader {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
-1
View File
@@ -18,7 +18,6 @@ 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
+11 -2
View File
@@ -28,6 +28,8 @@ use self::compression::Compression as Comp;
use self::compression::*;
use self::writer::*;
pub use self::writer::TiffWriter;
/// Type of prediction to prepare the image with.
///
/// 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
}
/// 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.
#[deprecated = "`image_directory` replaced the old behavior and clarifies the intent"]
#[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())?;
let count = K::convert_offset(entry.count())?;
count.write(self.writer)?;
self.writer
.write_bytes(&entry.offset_raw()[..offset_bytes])?;
self.writer.write_bytes(&entry.offset()[..offset_bytes])?;
}
Ok(offset)
+16
View File
@@ -40,6 +40,7 @@ pub fn write_bigtiff_header<W: Write>(writer: &mut TiffWriter<W>) -> TiffResult<
Ok(())
}
/// A writer that tracks offset and can compress.
pub struct TiffWriter<W> {
writer: W,
offset: u64,
@@ -61,6 +62,7 @@ impl<W: Write> TiffWriter<W> {
self.compressor = compressor;
}
/// Unset the compression.
pub fn reset_compression(&mut self) {
self.compressor = Compressor::default();
}
@@ -73,6 +75,7 @@ impl<W: Write> TiffWriter<W> {
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 {
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> {
pub fn goto_offset(&mut self, offset: u64) -> Result<(), io::Error> {
self.offset = offset;
+1 -11
View File
@@ -295,10 +295,7 @@ pub enum CompressionMethod(u16) unknown(
}
tags! {
pub enum PhotometricInterpretation(u16) unknown(
/// A private or extension tag
unknown
) {
pub enum PhotometricInterpretation(u16) {
WhiteIsZero = 0,
BlackIsZero = 1,
RGB = 2,
@@ -570,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.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ByteOrder {
+6 -5
View File
@@ -26,20 +26,21 @@ fn test_big_tiff() {
decoder.colortype().expect("Cannot get colortype"),
ColorType::RGB(8)
);
let mut ifd = decoder.current_ifd();
assert_eq!(
ifd.get_tag_u64(Tag::StripOffsets)
decoder
.get_tag_u64(Tag::StripOffsets)
.expect("Cannot get StripOffsets"),
16
);
assert_eq!(
ifd.get_tag_u64(Tag::RowsPerStrip)
decoder
.get_tag_u64(Tag::RowsPerStrip)
.expect("Cannot get RowsPerStrip"),
64
);
assert_eq!(
ifd.get_tag_u64(Tag::StripByteCounts)
decoder
.get_tag_u64(Tag::StripByteCounts)
.expect("Cannot get StripByteCounts"),
12288
)
+8 -6
View File
@@ -30,25 +30,27 @@ fn test_geo_tiff() {
num_samples: 5
}
);
let mut ifd = decoder.current_ifd();
assert_eq!(
ifd.get_tag_u64(Tag::StripOffsets)
decoder
.get_tag_u64(Tag::StripOffsets)
.expect("Cannot get StripOffsets"),
418
);
assert_eq!(
ifd.get_tag_u64(Tag::RowsPerStrip)
decoder
.get_tag_u64(Tag::RowsPerStrip)
.expect("Cannot get RowsPerStrip"),
10
);
assert_eq!(
ifd.get_tag_u64(Tag::StripByteCounts)
decoder
.get_tag_u64(Tag::StripByteCounts)
.expect("Cannot get StripByteCounts"),
1000
);
assert_eq!(
ifd.get_tag(Tag::ModelPixelScaleTag)
decoder
.get_tag(Tag::ModelPixelScaleTag)
.expect("Cannot get pixel scale")
.into_f64_vec()
.expect("Cannot get pixel scale"),
+9 -338
View File
@@ -1,11 +1,11 @@
extern crate tiff;
use tiff::decoder::{ifd, Decoder, DecodingSampleBuffer};
use tiff::tags::{ByteOrder, PhotometricInterpretation, SampleFormat, Type};
use tiff::tags::{ByteOrder, Type};
use tiff::ColorType;
use std::fs::File;
use std::io::{Cursor, Read, Seek};
use std::io::{Read, Seek};
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images/";
@@ -16,9 +16,7 @@ macro_rules! test_image_sum {
fn $name(file: &str, expected_type: ColorType, expected_sum: $sum_ty) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file)
.expect("Cannot create decoder")
.with_lenient(true);
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.colortype().unwrap(), expected_type);
@@ -406,10 +404,7 @@ fn test_string_tags() {
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
let software = decoder
.current_ifd()
.get_tag(tiff::tags::Tag::Software)
.unwrap();
let software = decoder.get_tag(tiff::tags::Tag::Software).unwrap();
match software {
ifd::Value::Ascii(s) => assert_eq!(
&s,
@@ -475,37 +470,6 @@ fn test_tiled_rect_rgb_u8() {
test_image_sum_u8("tiled-rect-rgb-u8.tif", ColorType::RGB(8), 62081032);
}
#[test]
fn test_tiled_extra_samples_border_u8() {
// Regression test for https://github.com/image-rs/image-tiff/issues/400
//
// A tiled image whose dimensions are not multiples of the tile size, with
// an extra sample that is discarded on decode. The horizontal padding of
// the right border tiles used to be written past the end of the output
// row, spilling into the leftmost columns of the following row. The
// fixture's tile padding is filled with 0xEE so any spill is visible.
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-extra-samples-border-u8.tif");
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.dimensions().unwrap(), (40, 40));
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
let DecodingSampleBuffer::U8(data) = decoder.read_image().unwrap() else {
panic!("Wrong bit depth")
};
let mut expected = Vec::with_capacity(40 * 40 * 3);
for y in 0..40u32 {
for x in 0..40u32 {
expected.push(((x * 3) % 251) as u8);
expected.push(((y * 7) % 251) as u8);
expected.push(((x + y) % 251) as u8);
}
}
assert_eq!(data, expected);
}
#[test]
fn test_inner_access() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-rect-rgb-u8.tif");
@@ -514,8 +478,7 @@ fn test_inner_access() {
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
let mut ifd = decoder.current_ifd();
let c = ifd
let c = decoder
.get_tag(tiff::tags::Tag::Compression)
.unwrap()
.into_u16()
@@ -524,13 +487,13 @@ fn test_inner_access() {
// 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.
let first_offset = ifd
let first_offset = decoder
.get_tag_u32_vec(tiff::tags::Tag::TileOffsets)
.unwrap()
.into_iter()
.next()
.unwrap();
let first_byte_count = ifd
let first_byte_count = decoder
.get_tag_u32_vec(tiff::tags::Tag::TileByteCounts)
.unwrap()
.into_iter()
@@ -858,7 +821,7 @@ fn test_predictor_3_gray_f32() {
}
#[test]
#[cfg(any(feature = "zstd", feature = "zstd-safe-rust"))]
#[cfg(feature = "zstd")]
fn test_zstd_compression() {
// gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif
test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396);
@@ -1008,7 +971,7 @@ fn extra_bits_gray() {
#[test]
fn extra_bits_rgb() {
test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGBA(8), 64);
test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGB(8), 0);
}
#[test]
@@ -1090,295 +1053,3 @@ fn test_gray_u12_hpredict() {
// Delta-encoded 12-bit values: [100, 200, 300, 400, 1000, 2000, 3000, 4000]
test_image_sum_u16("hpredict-1c-12b.tiff", ColorType::Gray(12), 11000);
}
// ---- Old-style LZW ----
#[test]
fn test_old_style_lzw() {
// Pre-TIFF-6.0 "old-style" LZW: LSB-first bit order with no early-change size
// switch. Auto-detected from the stream (first byte 0x00, second byte odd).
// Same image as libtiffpic's quad-lzw.tif; the decoded pixels must match.
test_image_sum_u8("quad-lzw-compat.tiff", ColorType::RGB(8), 21048924);
}
// ---- TransparencyMask ----
/// Build a minimal uncompressed 2x2 TIFF in memory, in the requested byte order,
/// with the given photometric interpretation and the pixel data as a single strip.
/// `sample_format` is emitted as a SampleFormat tag when `Some`; when `None` the
/// tag is omitted (the decoder then defaults to unsigned). Pixel bytes must already
/// be in `byte_order`.
fn build_raw_tiff(
byte_order: ByteOrder,
photometric: PhotometricInterpretation,
bits_per_sample: u16,
samples_per_pixel: u16,
sample_format: Option<SampleFormat>,
pixel_data: &[u8],
) -> Vec<u8> {
let le = matches!(byte_order, ByteOrder::LittleEndian);
let put16 = |buf: &mut Vec<u8>, v: u16| {
if le {
buf.extend_from_slice(&v.to_le_bytes());
} else {
buf.extend_from_slice(&v.to_be_bytes());
}
};
let put32 = |buf: &mut Vec<u8>, v: u32| {
if le {
buf.extend_from_slice(&v.to_le_bytes());
} else {
buf.extend_from_slice(&v.to_be_bytes());
}
};
let width: u16 = 2;
let height: u16 = 2;
let num_tags: u16 = if sample_format.is_some() { 9 } else { 8 };
let ifd_offset: u32 = 8;
let ifd_size = 2 + (num_tags as u32) * 12 + 4;
let strip_offset = 8 + ifd_size;
// IFD entries (must be ascending tag order). Tag IDs stay raw for the
// ascending-order property; the field type uses the `Type` enum.
let mut tags: Vec<(u16, Type, u32)> = vec![
(256, Type::SHORT, width as u32), // ImageWidth
(257, Type::SHORT, height as u32), // ImageLength
(258, Type::SHORT, bits_per_sample as u32), // BitsPerSample
(259, Type::SHORT, 1), // Compression = None
(262, Type::SHORT, photometric.to_u16() as u32), // PhotometricInterpretation
(273, Type::LONG, strip_offset), // StripOffsets
(277, Type::SHORT, samples_per_pixel as u32), // SamplesPerPixel
(279, Type::LONG, pixel_data.len() as u32), // StripByteCounts
];
if let Some(sf) = sample_format {
tags.push((339, Type::SHORT, sf.to_u16() as u32)); // SampleFormat (tag 339 > 279)
}
let mut buf = Vec::new();
buf.extend_from_slice(if le { b"II" } else { b"MM" });
put16(&mut buf, 42);
put32(&mut buf, ifd_offset);
put16(&mut buf, num_tags);
for (tag, typ, value) in tags {
put16(&mut buf, tag);
put16(&mut buf, typ.to_u16());
put32(&mut buf, 1); // count
// A SHORT value is left-justified in the 4-byte value field (value in the
// first two bytes, then padding); a LONG fills all four.
match typ {
Type::SHORT => {
put16(&mut buf, value as u16);
put16(&mut buf, 0);
}
_ => put32(&mut buf, value),
}
}
put32(&mut buf, 0); // next IFD = 0
buf.extend_from_slice(pixel_data);
buf
}
#[test]
fn test_transparency_mask() {
// 2x2 mask: values 0, 255, 128, 64
let data = build_raw_tiff(
ByteOrder::LittleEndian,
PhotometricInterpretation::TransparencyMask,
8,
1,
None,
&[0, 255, 128, 64],
);
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(8));
let result = decoder.read_image().unwrap();
match result {
DecodingSampleBuffer::U8(pixels) => {
// TransparencyMask is decoded as grayscale
assert_eq!(pixels.len(), 4);
assert_eq!(pixels[0], 0);
assert_eq!(pixels[1], 255);
assert_eq!(pixels[2], 128);
assert_eq!(pixels[3], 64);
}
_ => panic!("Expected U8 buffer"),
}
}
// ---- CIE L*a*b* ----
#[test]
fn test_cielab_decode() {
// 2x2 CIELab image: 3 bytes per pixel (L, a, b)
// L=128, a=0 (signed neutral), b=0 (signed neutral)
// After re-biasing: a=128 (unsigned neutral), b=128 (unsigned neutral)
let pixels: Vec<u8> = vec![
128, 0, 0, // pixel 0: L=128, a=0, b=0 (neutral gray)
255, 50, 50, // pixel 1: L=255, a=+50, b=+50
100, 206, 206, // pixel 2: L=100, a=-50 (as i8), b=-50 (as i8)
0, 128, 128, // pixel 3: L=0, a=-128 (as i8), b=-128 (as i8)
];
let data = build_raw_tiff(
ByteOrder::LittleEndian,
PhotometricInterpretation::CIELab,
8,
3,
None,
&pixels,
);
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Lab(8));
let result = decoder.read_image().unwrap();
match result {
DecodingSampleBuffer::U8(decoded) => {
assert_eq!(decoded.len(), 12); // 4 pixels * 3 channels
// Pixel 0: L=128, a=0+128=128, b=0+128=128
assert_eq!(decoded[0], 128);
assert_eq!(decoded[1], 128);
assert_eq!(decoded[2], 128);
// Pixel 1: L=255, a=50+128=178, b=50+128=178
assert_eq!(decoded[3], 255);
assert_eq!(decoded[4], 178);
assert_eq!(decoded[5], 178);
// Pixel 2: L=100, a=206+128=78 (wrapping), b=206+128=78
assert_eq!(decoded[6], 100);
assert_eq!(decoded[7], 78);
assert_eq!(decoded[8], 78);
// Pixel 3: L=0, a=128+128=0 (wrapping), b=128+128=0
assert_eq!(decoded[9], 0);
assert_eq!(decoded[10], 0);
assert_eq!(decoded[11], 0);
}
_ => panic!("Expected U8 buffer"),
}
}
#[test]
fn test_cielab_decode_16() {
// 2x2 16-bit CIELab image: 3 u16 samples per pixel (L, a, b), a/b signed.
// Re-biasing maps signed a/b to unsigned by adding 0x8000 (wrapping):
// 0 -> 32768, +1000 -> 33768, -1000 -> 31768, -32768 -> 0, +32767 -> 65535.
let samples: [u16; 12] = [
32768, 0, 0, // pixel 0: L, a=0, b=0 (neutral)
65535, 1000, 1000, // pixel 1: a=+1000, b=+1000
10000, 64536, 64536, // pixel 2: a=-1000, b=-1000 (as u16)
0, 32768, 32767, // pixel 3: a=-32768, b=+32767 (as u16)
];
let mut pixels = Vec::new();
for s in samples {
pixels.extend_from_slice(&s.to_le_bytes());
}
let data = build_raw_tiff(
ByteOrder::LittleEndian,
PhotometricInterpretation::CIELab,
16,
3,
None,
&pixels,
);
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Lab(16));
let result = decoder.read_image().unwrap();
match result {
DecodingSampleBuffer::U16(decoded) => {
assert_eq!(decoded.len(), 12); // 4 pixels * 3 channels
// L unchanged; a/b re-biased by +0x8000 (wrapping).
let expected: [u16; 12] = [
32768, 32768, 32768, // pixel 0
65535, 33768, 33768, // pixel 1
10000, 31768, 31768, // pixel 2
0, 0, 65535, // pixel 3
];
assert_eq!(decoded, expected);
}
_ => panic!("Expected U16 buffer"),
}
}
#[test]
fn test_cielab_decode_16_big_endian() {
// Same image and expected output as `test_cielab_decode_16`, but stored as a
// big-endian ("MM") TIFF. 16-bit is the case where byte order matters: the
// decoder must swap to native before the a/b re-bias, so the result is
// identical to the little-endian case.
let samples: [u16; 12] = [
32768, 0, 0, // pixel 0
65535, 1000, 1000, // pixel 1
10000, 64536, 64536, // pixel 2
0, 32768, 32767, // pixel 3
];
let mut pixels = Vec::new();
for s in samples {
pixels.extend_from_slice(&s.to_be_bytes());
}
let data = build_raw_tiff(
ByteOrder::BigEndian,
PhotometricInterpretation::CIELab,
16,
3,
None,
&pixels,
);
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Lab(16));
let result = decoder.read_image().unwrap();
match result {
DecodingSampleBuffer::U16(decoded) => {
let expected: [u16; 12] = [
32768, 32768, 32768, // pixel 0
65535, 33768, 33768, // pixel 1
10000, 31768, 31768, // pixel 2
0, 0, 65535, // pixel 3
];
assert_eq!(decoded, expected);
}
_ => panic!("Expected U16 buffer"),
}
}
#[test]
fn test_cielab_rejects_non_uint_sample_format() {
// CIELab treats a/b as signed via the photometric interpretation and requires
// SampleFormat::Uint (matching the ICCLab arm). A file declaring Int must be
// rejected rather than silently producing a signed output buffer.
let pixels = vec![0u8; 12]; // 2x2, 3 samples, 8-bit
let data = build_raw_tiff(
ByteOrder::LittleEndian,
PhotometricInterpretation::CIELab,
8,
3,
Some(SampleFormat::Int),
&pixels,
);
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
// Rejection may surface at either next_image() or colortype().
let rejected = decoder.next_image().is_err() || decoder.colortype().is_err();
assert!(
rejected,
"CIELab with SampleFormat=Int must be rejected, not decoded"
);
}
+1 -1
View File
@@ -190,7 +190,7 @@ const FILES: &[(&str, Option<u32>)] = &[
("tests/libtiffpic/oxford.tif", Some(0x355d2e3e)),
("tests/libtiffpic/pc260001.tif", Some(0xdb56b753)),
("tests/libtiffpic/quad-jpeg.tif", Some(0xcba22475)),
("tests/libtiffpic/quad-lzw.tif", Some(0x6055f9ee)), // old-style (LSB) LZW
("tests/libtiffpic/quad-lzw.tif", Some(0)), // invalid LZW stream
("tests/libtiffpic/quad-tile.tif", Some(0x6055f9ee)),
("tests/libtiffpic/README", None),
("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported YCbCr chroma subsampling
+11 -40
View File
@@ -40,7 +40,6 @@ fn encode_decode() {
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
let mut all_tags = decoder
.current_ifd()
.tag_iter()
.filter_map(Result::ok)
.collect::<Vec<_>>();
@@ -81,7 +80,7 @@ fn encode_decode() {
);
assert_eq!(
decoder.current_ifd().get_tag(Tag::Artist).unwrap(),
decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into())
);
@@ -126,7 +125,6 @@ fn encode_decode_big() {
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
let mut all_tags = decoder
.current_ifd()
.tag_iter()
.filter_map(Result::ok)
.collect::<Vec<_>>();
@@ -167,7 +165,7 @@ fn encode_decode_big() {
);
assert_eq!(
decoder.current_ifd().get_tag(Tag::Artist).unwrap(),
decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into())
);
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> {
fn assert_tag_u32(&mut self, tag: u16) -> u32 {
self.current_ifd()
.get_tag(Tag::Unknown(tag))
.unwrap()
.into_u32()
.unwrap()
self.get_tag(Tag::Unknown(tag)).unwrap().into_u32().unwrap()
}
fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32> {
self.current_ifd()
.get_tag(Tag::Unknown(tag))
self.get_tag(Tag::Unknown(tag))
.unwrap()
.into_u32_vec()
.unwrap()
}
fn assert_tag_i32(&mut self, tag: u16) -> i32 {
self.current_ifd()
.get_tag(Tag::Unknown(tag))
.unwrap()
.into_i32()
.unwrap()
self.get_tag(Tag::Unknown(tag)).unwrap().into_i32().unwrap()
}
fn assert_tag_i32_vec(&mut self, tag: u16) -> Vec<i32> {
self.current_ifd()
.get_tag(Tag::Unknown(tag))
self.get_tag(Tag::Unknown(tag))
.unwrap()
.into_i32_vec()
.unwrap()
}
fn assert_tag_u64(&mut self, tag: u16) -> u64 {
self.current_ifd()
.get_tag(Tag::Unknown(tag))
.unwrap()
.into_u64()
.unwrap()
self.get_tag(Tag::Unknown(tag)).unwrap().into_u64().unwrap()
}
fn assert_tag_u64_vec(&mut self, tag: u16) -> Vec<u64> {
self.current_ifd()
.get_tag(Tag::Unknown(tag))
self.get_tag(Tag::Unknown(tag))
.unwrap()
.into_u64_vec()
.unwrap()
}
fn assert_tag_i64(&mut self, tag: u16) -> i64 {
self.current_ifd()
.get_tag(Tag::Unknown(tag))
.unwrap()
.into_i64()
.unwrap()
self.get_tag(Tag::Unknown(tag)).unwrap().into_i64().unwrap()
}
fn assert_tag_i64_vec(&mut self, tag: u16) -> Vec<i64> {
self.current_ifd()
.get_tag(Tag::Unknown(tag))
self.get_tag(Tag::Unknown(tag))
.unwrap()
.into_i64_vec()
.unwrap()
@@ -798,13 +776,7 @@ fn test_rows_per_strip() {
{
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(
decoder
.current_ifd()
.get_tag_u64(Tag::RowsPerStrip)
.unwrap(),
2
);
assert_eq!(decoder.get_tag_u64(Tag::RowsPerStrip).unwrap(), 2);
assert_eq!(decoder.strip_count().unwrap(), 50);
for i in 0..50 {
@@ -857,7 +829,6 @@ fn test_auxiliary_directory() {
let _ = decoder.read_image().unwrap();
let exif_location = decoder
.current_ifd()
.get_tag(Tag::ExifDirectory)
.expect("second directory missing Exif")
.into_ifd_pointer()
Binary file not shown.
-164
View File
@@ -233,167 +233,3 @@ fn test_rgb_u64_predict_roundtrip() {
fn test_ycbcr_u8_predict_roundtrip() {
test_u8_predict_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
}
// ---------------------------------------------------------------------------
// Extended bit depths (9-15 promoted to u16, 17-31 promoted to u32).
//
// A horizontal-predicted n-bit sample stores its delta in exactly n bits,
// i.e. modulo 2^n, so reconstruction must also be modulo 2^n. The decoder
// accumulates in the promoted storage width and reduces back to n bits;
// these tests pin that reduction down with exact pixel values, including
// deltas that wrap modulo 2^n. The encoder does not support extended
// depths, so the TIFFs are hand-built.
// ---------------------------------------------------------------------------
/// Hand-build a minimal little-endian, single-strip, uncompressed grayscale
/// TIFF with `Predictor::Horizontal` and the given (possibly non-standard)
/// bit depth. `pixels` are the intended sample values; they are converted to
/// horizontal differences modulo `2^bits` and bit-packed MSB-first with each
/// row padded to a byte boundary — the only representation an n-bit field
/// can hold.
fn build_gray_hpredict_tiff(
width: u32,
height: u32,
bits: u8,
photometric: u16,
pixels: &[u32],
) -> Vec<u8> {
let max = ((1u64 << bits) - 1) as u32;
let row_samples = width as usize;
assert_eq!(pixels.len(), row_samples * height as usize);
assert!(pixels.iter().all(|&v| v <= max));
// Per-row horizontal differencing modulo 2^bits.
let mut deltas = Vec::with_capacity(pixels.len());
for row in pixels.chunks(row_samples) {
for (i, &v) in row.iter().enumerate() {
deltas.push(if i == 0 {
v
} else {
v.wrapping_sub(row[i - 1]) & max
});
}
}
// Bit-pack MSB-first, each row padded to a byte boundary.
let row_bytes = (row_samples * bits as usize).div_ceil(8);
let mut data = vec![0u8; row_bytes * height as usize];
for (r, row) in deltas.chunks(row_samples).enumerate() {
for (i, &d) in row.iter().enumerate() {
let start = r * row_bytes * 8 + i * bits as usize;
for (off, b) in (start..).zip((0..bits).rev()) {
if (d >> b) & 1 == 1 {
data[off / 8] |= 0x80 >> (off % 8);
}
}
}
}
// Classic little-endian TIFF: header, strip data, then a single IFD.
let mut out = Vec::new();
out.extend_from_slice(b"II");
out.extend_from_slice(&42u16.to_le_bytes());
let data_offset = 8u32;
let ifd_offset = data_offset + data.len() as u32;
out.extend_from_slice(&ifd_offset.to_le_bytes());
out.extend_from_slice(&data);
const SHORT: u16 = 3;
const LONG: u16 = 4;
let entries: &[(u16, u16, u32)] = &[
(256, LONG, width), // ImageWidth
(257, LONG, height), // ImageLength
(258, SHORT, u32::from(bits)), // BitsPerSample
(259, SHORT, 1), // Compression: none
(262, SHORT, u32::from(photometric)), // PhotometricInterpretation
(273, LONG, data_offset), // StripOffsets
(277, SHORT, 1), // SamplesPerPixel
(278, LONG, height), // RowsPerStrip
(279, LONG, data.len() as u32), // StripByteCounts
(317, SHORT, 2), // Predictor: horizontal
(339, SHORT, 1), // SampleFormat: unsigned integer
];
out.extend_from_slice(&(entries.len() as u16).to_le_bytes());
for &(tag, ty, value) in entries {
out.extend_from_slice(&tag.to_le_bytes());
out.extend_from_slice(&ty.to_le_bytes());
out.extend_from_slice(&1u32.to_le_bytes());
// A count-1 SHORT payload occupies the low bytes of the value field.
out.extend_from_slice(&value.to_le_bytes());
}
out.extend_from_slice(&0u32.to_le_bytes()); // no next IFD
out
}
fn decode_gray_u16(tiff: Vec<u8>, bits: u8) -> Vec<u16> {
let mut decoder = Decoder::open(Cursor::new(tiff)).expect("Cannot create decoder");
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(bits));
match decoder.read_image().unwrap() {
DecodingSampleBuffer::U16(res) => res,
_ => panic!("Wrong data type"),
}
}
fn decode_gray_u32(tiff: Vec<u8>, bits: u8) -> Vec<u32> {
let mut decoder = Decoder::open(Cursor::new(tiff)).expect("Cannot create decoder");
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(bits));
match decoder.read_image().unwrap() {
DecodingSampleBuffer::U32(res) => res,
_ => panic!("Wrong data type"),
}
}
#[test]
fn test_gray12_hpredict_wraparound_exact() {
// Pixel row [4095, 0, 1] encodes as deltas [4095, 1, 1] (mod 2^12).
// u16 accumulation yields [4095, 4096, 4097]; reduction modulo 2^12
// must restore [4095, 0, 1]. Saturating instead would corrupt the two
// wrapped samples to 4095.
let tiff = build_gray_hpredict_tiff(3, 1, 12, 1, &[4095, 0, 1]);
assert_eq!(decode_gray_u16(tiff, 12), vec![4095, 0, 1]);
}
#[test]
fn test_gray12_hpredict_wraparound_white_is_zero_exact() {
// Same stored samples with PhotometricInterpretation = WhiteIsZero:
// after the mod-2^12 reduction the decoder inverts as (2^12 - 1) - v.
let tiff = build_gray_hpredict_tiff(3, 1, 12, 0, &[4095, 0, 1]);
assert_eq!(decode_gray_u16(tiff, 12), vec![0, 4095, 4094]);
}
#[test]
fn test_gray20_hpredict_wraparound_exact() {
// The same wrap-around shape on the u32 storage path (17-31 bits).
let tiff = build_gray_hpredict_tiff(3, 1, 20, 1, &[0xF_FFFF, 0, 1]);
assert_eq!(decode_gray_u32(tiff, 20), vec![0xF_FFFF, 0, 1]);
}
#[test]
fn test_extended_depth_hpredict_random_roundtrip() {
// Deterministic pseudo-random pixels across a spread of extended depths;
// encode-then-decode must be the identity for every sample, including
// every mod-2^n wrap-around the differencing produces. Three rows verify
// that prediction restarts per row.
let mut state = 0x243F_6A88_85A3_08D3u64;
let mut next = move || {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(state >> 33) as u32
};
for &bits in &[9u8, 11, 12, 15] {
let max = (1u32 << bits) - 1;
let pixels: Vec<u32> = (0..16 * 3).map(|_| next() & max).collect();
let tiff = build_gray_hpredict_tiff(16, 3, bits, 1, &pixels);
let expected: Vec<u16> = pixels.iter().map(|&v| v as u16).collect();
assert_eq!(decode_gray_u16(tiff, bits), expected, "bits={bits}");
}
for &bits in &[17u8, 20, 25, 31] {
let max = ((1u64 << bits) - 1) as u32;
let pixels: Vec<u32> = (0..16 * 3).map(|_| next() & max).collect();
let tiff = build_gray_hpredict_tiff(16, 3, bits, 1, &pixels);
assert_eq!(decode_gray_u32(tiff, bits), pixels, "bits={bits}");
}
}