Add craft-tiff: TIFF and BigTIFF IFD reader and writer, from LightCraft (#795)

Moved from lightcraft-tiff (lightcraft b188295e) unchanged apart from names
and docs. The first step of shared RAW support (#783, #587): LightCraft's raw
and metadata crates read through it, and PhotoCraft's and FilmCraft's own
TIFF parsing can follow.

Part of #778.
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-10 20:24:43 -05:00
1 parent 6e21c5b0b1
commit 3b3104981b
13 files changed
+2516

No files matched your search

Generated
+9
View File
@@ -1160,6 +1160,15 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "craft-tiff"
version = "0.1.0"
dependencies = [
"proptest",
"serde",
"thiserror 2.0.21",
]
[[package]]
name = "craft-ui"
version = "0.1.0"
+1
View File
@@ -50,6 +50,7 @@ craft-proof = { path = "crates/proof" }
craft-quit = { path = "crates/quit" }
craft-settings = { path = "crates/settings" }
craft-text = { path = "crates/text" }
craft-tiff = { path = "crates/tiff" }
craft-ui = { path = "crates/ui" }
craft-update = { path = "crates/update" }
craft-vectorcraft-format = { path = "crates/vectorcraft-format" }
+4
View File
@@ -75,6 +75,10 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
lookup with fallback (`pt-br` → `pt` → English), plurals, language names, the system locale and
a pseudo locale. The catalogs stay a fully optional build input; apps keep `tl!` and their
language setting.
- [`craft-tiff`](crates/tiff/README.md): TIFF and BigTIFF IFD reader and writer, from LightCraft:
every field type, the IFD chain, SubIFDs and Exif/GPS/Interop pointer IFDs, maker-note IFDs
(Canon, Nikon, Sony, Fujifilm, Olympus, Panasonic, Pentax), strip and tile layout. Bounded and
panic-free on hostile input. Pixel decompression stays with the callers.
- [`craft-jpeg`](crates/jpeg/README.md): clean-room decoder for 12-bit and arithmetic-coded
JPEG (T.81 SOF0/SOF1 at 12 bits, SOF9, SOF10), from PhotoCraft, and a frame probe for routing.
Metadata stays in each app.
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "craft-tiff"
description = "TIFF and BigTIFF IFD reader and writer: every field type, the Exif/GPS/Interop pointer IFDs, maker-note IFDs, strip and tile layout."
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
serde = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
proptest = { workspace = true }
+46
View File
@@ -0,0 +1,46 @@
# craft-tiff
The TIFF / IFD reader and writer underneath raw decoding (DNG, CR2, NEF, ARW…) and Exif. Moved from
LightCraft's `lightcraft-tiff` as the first step of shared RAW support (craft-libs#783, #587). It depends
on no other Craft crate.
## Features
- Classic TIFF and BigTIFF, little- and big-endian (`II`/`MM`). Also accepts the TIFF-like magic numbers of
some raw formats (ORF `IIRO`/`IIRS`, RW2 `IIU`) when `ParseOptions::accept_raw_magic` is set (default).
- Every field type: BYTE, ASCII, SHORT, LONG, RATIONAL, SBYTE, UNDEFINED, SSHORT, SLONG, SRATIONAL, FLOAT,
DOUBLE, IFD, LONG8, SLONG8, IFD8.
- IFD chain + `SubIFDs` (330) + Exif (34665) / GPS (34853) / Interoperability (40965) pointer IFDs, resolved
into a tree (`Ifd::sub_ifds`, `exif`, `gps`, `interop`). `Entry::offset` records the absolute position of
each value (needed for maker notes and embedded blobs).
- `parse_ifd_at(data, offset, order, base, bigtiff, opts)` parses an IFD whose offsets are relative to an
arbitrary base; `makernote::parse_makernote` knows the header / byte-order / offset-base conventions of
Canon, Nikon (types 1 and 3), Sony, Fujifilm, Olympus (old and new), Panasonic and Pentax notes.
- `image::ImageInfo` — width/height, bits, samples, compression, photometric, planar config, predictor,
sample format and the strip or tile chunk list (`chunks()`, `chunk_bytes()`); missing byte counts are
inferred.
- `directory_entries`, `entry_span`, `ParseOptions::budget_for` and `decode_value` are the reader's own rules
(directory layout and entry limit, which entries it reads, its value budget, value decoding), for callers that
must select entries exactly as `parse_ifd_at` would without decoding the whole directory (e.g. a raw decoder's
Sony `SR2Private` lookup).
- `TiffWriter` writes IFD chains with strips or tiles, `SubIFDs` and pointer IFDs, in either byte order,
classic or BigTIFF; `writer::rational` / `srational` approximate floats as rationals.
## Robustness
The reader never panics on malformed input (proptests feed random and mutated files): offsets are
bounds-checked; out-of-range or unknown-type entries are skipped; IFD loops are detected; IFD count,
nesting depth and entries per IFD are limited; at most 65,536 directory entry slots
(`ParseOptions::max_entries_total`) are accepted across the main IFD chain and all child IFDs, even when tables
overlap at distinct offsets, and an IFD past that bound ends the chain or is skipped like any other bad IFD after
the first; a next-IFD pointer is followed only when every declared entry was read, with the counts compared as
64-bit numbers (a BigTIFF declaring 4,294,967,297 entries but holding one can't pass for a whole directory on a
32-bit target); the total bytes of decoded values are capped at
`8 × input + 1 MiB` so a file cannot make us allocate far more than its own size.
## Sources
Public specifications only: TIFF 6.0 (Adobe, 1992), the TIFF 6.0 supplements (IFD type, SubIFDs), the
BigTIFF format description (AWare Systems), Adobe DNG Specification 1.7 (tag numbers), CIPA DC-008 Exif 2.32,
and the ExifTool tag-name documentation (maker-note header conventions, prose only). No third-party code
was consulted.
+248
View File
@@ -0,0 +1,248 @@
//! Image layout of an IFD: dimensions, sample format and the strip / tile chunks holding the pixels.
use crate::{Ifd, Result, TiffError, tags};
use serde::{Deserialize, Serialize};
/// How pixel data is split into chunks.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Layout {
Strips { rows_per_strip: u32 },
Tiles { tile_width: u32, tile_height: u32 },
}
/// Pixel layout description of an IFD (TIFF 6.0 §3–§8, §15).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ImageInfo {
pub width: u32,
pub height: u32,
pub bits_per_sample: Vec<u16>,
pub samples_per_pixel: u16,
pub compression: u16,
pub photometric: u16,
/// 1 = chunky (interleaved), 2 = planar.
pub planar: u16,
pub predictor: u16,
/// 1 = unsigned int, 2 = signed int, 3 = IEEE float.
pub sample_format: u16,
pub new_subfile_type: u32,
pub layout: Layout,
pub offsets: Vec<u64>,
pub byte_counts: Vec<u64>,
}
/// One strip or tile.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Chunk {
pub index: usize,
/// Top-left pixel of the chunk.
pub x: u32,
pub y: u32,
/// Nominal chunk size (tiles may extend past the image; strips are clipped to the image height).
pub width: u32,
pub height: u32,
/// Plane index for planar configuration 2 (else 0).
pub plane: u16,
pub offset: u64,
pub len: u64,
}
impl ImageInfo {
pub fn from_ifd(ifd: &Ifd) -> Result<ImageInfo> {
let width = ifd.u32(tags::IMAGE_WIDTH).ok_or(TiffError::MissingTag(tags::IMAGE_WIDTH))?;
let height = ifd.u32(tags::IMAGE_LENGTH).ok_or(TiffError::MissingTag(tags::IMAGE_LENGTH))?;
if width == 0 || height == 0 {
return Err(TiffError::Invalid("zero image dimension".into()));
}
let samples_per_pixel = ifd.u16(tags::SAMPLES_PER_PIXEL).unwrap_or(1).max(1);
let bits_per_sample: Vec<u16> =
ifd.u64s(tags::BITS_PER_SAMPLE).map(|v| v.into_iter().map(|b| b.min(u16::MAX as u64) as u16).collect()).unwrap_or_else(|| vec![1]);
let bits_per_sample = if bits_per_sample.is_empty() { vec![1] } else { bits_per_sample };
let planar = ifd.u16(tags::PLANAR_CONFIGURATION).unwrap_or(1);
let (layout, offsets, byte_counts) = if let Some(tw) = ifd.u32(tags::TILE_WIDTH) {
let th = ifd.u32(tags::TILE_LENGTH).ok_or(TiffError::MissingTag(tags::TILE_LENGTH))?;
if tw == 0 || th == 0 {
return Err(TiffError::Invalid("zero tile size".into()));
}
let offs = ifd.u64s(tags::TILE_OFFSETS).or_else(|| ifd.u64s(tags::STRIP_OFFSETS)).ok_or(TiffError::MissingTag(tags::TILE_OFFSETS))?;
let cnts = ifd
.u64s(tags::TILE_BYTE_COUNTS)
.or_else(|| ifd.u64s(tags::STRIP_BYTE_COUNTS))
.ok_or(TiffError::MissingTag(tags::TILE_BYTE_COUNTS))?;
(Layout::Tiles { tile_width: tw, tile_height: th }, offs, cnts)
} else {
let offs = ifd.u64s(tags::STRIP_OFFSETS).ok_or(TiffError::MissingTag(tags::STRIP_OFFSETS))?;
let cnts = ifd.u64s(tags::STRIP_BYTE_COUNTS).unwrap_or_default();
let rps = ifd.u32(tags::ROWS_PER_STRIP).unwrap_or(height).clamp(1, height);
(Layout::Strips { rows_per_strip: rps }, offs, cnts)
};
Ok(ImageInfo {
width,
height,
bits_per_sample,
samples_per_pixel,
compression: ifd.u16(tags::COMPRESSION).unwrap_or(1),
photometric: ifd.u16(tags::PHOTOMETRIC).unwrap_or(1),
planar,
predictor: ifd.u16(tags::PREDICTOR).unwrap_or(1),
sample_format: ifd.u16(tags::SAMPLE_FORMAT).unwrap_or(1),
new_subfile_type: ifd.u32(tags::NEW_SUBFILE_TYPE).unwrap_or(0),
layout,
offsets,
byte_counts,
})
}
/// Bits of the first sample.
pub fn bits(&self) -> u16 {
self.bits_per_sample[0]
}
/// Whether this IFD is a reduced-resolution image (NewSubfileType bit 0).
pub fn is_reduced(&self) -> bool {
self.new_subfile_type & 1 != 0
}
/// Number of chunks across and down per plane.
pub fn grid(&self) -> (u32, u32) {
match self.layout {
Layout::Strips { rows_per_strip } => (1, self.height.div_ceil(rows_per_strip)),
Layout::Tiles { tile_width, tile_height } => (self.width.div_ceil(tile_width), self.height.div_ceil(tile_height)),
}
}
/// Nominal chunk dimensions.
pub fn chunk_size(&self) -> (u32, u32) {
match self.layout {
Layout::Strips { rows_per_strip } => (self.width, rows_per_strip),
Layout::Tiles { tile_width, tile_height } => (tile_width, tile_height),
}
}
/// The chunk count the image declares (offsets present, cut to the layout's plane capacity) with the
/// geometry `chunks` needs to place each one. No descriptors are built, so a pathological strip array
/// costs it a length check.
fn chunk_grid(&self) -> (usize, u32, usize) {
let (across, down) = self.grid();
let per_plane = across as usize * down as usize;
let planes = if self.planar == 2 { self.samples_per_pixel as usize } else { 1 };
(self.offsets.len().min(per_plane.saturating_mul(planes)), across, per_plane)
}
fn declared_chunk_count(&self) -> usize {
self.chunk_grid().0
}
/// The one chunk when the image has exactly one, else `None` — and no descriptor vector beyond it. Preview
/// extraction only serves single-chunk images, so it asks here before serving a hostile multi-strip layout.
pub fn single_chunk(&self, data_len: u64) -> Option<Chunk> {
if self.declared_chunk_count() != 1 {
return None;
}
self.chunks(data_len).into_iter().next()
}
/// All chunks that have an offset. Missing byte counts are inferred as "to the next offset / end of data"
/// using `data_len`.
pub fn chunks(&self, data_len: u64) -> Vec<Chunk> {
let (n, across, per_plane) = self.chunk_grid();
let (cw, ch) = self.chunk_size();
let mut out = Vec::with_capacity(n);
// the distinct offsets in order, for chunks without a byte count (sorted once, not scanned per chunk)
let mut sorted: Vec<u64> = Vec::new();
if (0..n).any(|i| self.byte_counts.get(i).is_none_or(|&l| l == 0)) {
sorted = self.offsets.clone();
sorted.sort_unstable();
sorted.dedup();
}
for i in 0..n {
let plane = i / per_plane.max(1);
let j = i % per_plane.max(1);
let (cx, cy) = (j as u32 % across, j as u32 / across);
let x = cx * cw;
let y = cy * ch;
let height = match self.layout {
Layout::Strips { .. } => ch.min(self.height - y),
Layout::Tiles { .. } => ch,
};
let offset = self.offsets[i];
let len = match self.byte_counts.get(i) {
Some(&l) if l > 0 => l,
_ => {
// infer: up to the next larger offset, else end of data
let next = sorted.get(sorted.partition_point(|&o| o <= offset)).copied().unwrap_or(data_len);
next.saturating_sub(offset)
}
};
out.push(Chunk { index: i, x, y, width: cw, height, plane: plane as u16, offset, len });
}
out
}
}
/// The bytes of a chunk, clipped to the available data (truncated files yield a shorter slice).
/// Returns `None` only when the chunk starts past the end of `data`.
pub fn chunk_bytes<'a>(data: &'a [u8], c: &Chunk) -> Option<&'a [u8]> {
let start = usize::try_from(c.offset).ok()?;
if start >= data.len() {
return None;
}
let end = (c.offset.saturating_add(c.len)).min(data.len() as u64) as usize;
Some(&data[start..end])
}
#[cfg(test)]
mod tests {
use super::*;
fn strips(offsets: Vec<u64>, byte_counts: Vec<u64>) -> ImageInfo {
ImageInfo {
width: 1,
height: offsets.len() as u32,
bits_per_sample: vec![8],
samples_per_pixel: 1,
compression: 1,
photometric: 1,
planar: 1,
predictor: 1,
sample_format: 1,
new_subfile_type: 0,
layout: Layout::Strips { rows_per_strip: 1 },
offsets,
byte_counts,
}
}
/// One offset is one chunk, and `single_chunk` refuses to build descriptors when the image declares more.
#[test]
fn single_chunk_is_the_only_chunk() {
let one = strips(vec![50], vec![10]);
assert_eq!(one.single_chunk(100).map(|c| (c.index, c.offset, c.len)), one.chunks(100).first().map(|c| (c.index, c.offset, c.len)));
// the inferred length matches too, and never falls back past the data
let inferred = strips(vec![70], vec![]);
assert_eq!(inferred.single_chunk(100).map(|c| c.len), Some(30));
assert!(strips(vec![50, 10], vec![10, 5]).single_chunk(100).is_none(), "a multi-chunk image has no single chunk");
assert!(strips(Vec::new(), Vec::new()).single_chunk(100).is_none());
}
#[test]
fn missing_byte_counts_run_to_the_next_larger_offset() {
let info = strips(vec![50, 10, 30, 10, 70], vec![0, 0, 5]);
let lens: Vec<u64> = info.chunks(100).iter().map(|c| c.len).collect();
assert_eq!(lens, vec![20, 20, 5, 20, 30]);
}
/// A million strips without byte counts (offsets in descending order) are measured in one sort, not one
/// scan of all offsets per strip.
#[test]
fn many_strips_without_byte_counts_are_fast() {
let n = 1_000_000u64;
let info = strips((0..n).rev().map(|i| 1000 + i * 2).collect(), Vec::new());
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let chunks = info.chunks(1000 + n * 2);
let _ = tx.send(chunks.iter().map(|c| c.len).collect::<Vec<u64>>());
});
let lens = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("chunks() took longer than 20 s");
assert!(lens.iter().all(|&l| l == 2));
}
}
+468
View File
@@ -0,0 +1,468 @@
//! TIFF / IFD reading and writing for the Craft apps (moved from LightCraft's `lightcraft-tiff`).
//!
//! - [`Tiff::parse`] reads classic TIFF and BigTIFF in both byte orders, following the IFD chain,
//! `SubIFDs`, and the Exif / GPS / Interoperability IFD pointers. All field types of TIFF 6.0,
//! the TIFF 6.0 supplement (`IFD`) and BigTIFF (`LONG8`, `SLONG8`, `IFD8`) are decoded.
//! - [`parse_ifd_at`] parses a single IFD with an arbitrary offset base — the building block for
//! maker-note IFDs ([`makernote`]).
//! - [`image::ImageInfo`] describes the pixel layout of an IFD (strips or tiles, planar config,
//! compression, predictor) and enumerates its [`image::Chunk`]s.
//! - [`writer::TiffWriter`] writes IFD trees (with strips/tiles, `SubIFDs` and pointer IFDs) in
//! either byte order, classic or BigTIFF.
//!
//! The reader never panics on malformed input: every offset is bounds-checked, IFD loops are
//! detected, nesting depth / IFD count are limited, at most 65,536 directory entry slots are read
//! across the main chain and child IFDs, and decoded value bytes are capped relative to the input
//! size (so overlapping tables cannot multiply entry-vector allocations without bound).
//!
//! Sources: TIFF 6.0 specification (Adobe, 1992), TIFF Technical Note / supplements, the BigTIFF
//! design notes (AWare Systems), Adobe DNG specification 1.7 (tag numbers), CIPA DC-008 (Exif 2.32).
#![forbid(unsafe_code)]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
pub mod image;
pub mod makernote;
mod reader;
pub mod tags;
pub mod writer;
pub use reader::{DirectoryEntries, EntrySpan, ParseOptions, decode as decode_value, directory_entries, entry_span, parse_ifd_at};
pub use writer::{IfdBuilder, ImageData, TiffWriter};
use serde::{Deserialize, Serialize};
/// Errors from TIFF parsing / writing.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TiffError {
#[error("not a TIFF stream (bad header)")]
NotTiff,
#[error("data truncated at offset {0}")]
Truncated(u64),
#[error("invalid TIFF structure: {0}")]
Invalid(String),
#[error("resource limit exceeded: {0}")]
Limit(&'static str),
#[error("missing required tag {0}")]
MissingTag(u16),
}
pub type Result<T> = std::result::Result<T, TiffError>;
/// Byte order of a TIFF stream.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum ByteOrder {
/// `II` — Intel, little-endian.
#[default]
Little,
/// `MM` — Motorola, big-endian.
Big,
}
impl ByteOrder {
#[inline]
pub fn u16(self, b: [u8; 2]) -> u16 {
match self {
ByteOrder::Little => u16::from_le_bytes(b),
ByteOrder::Big => u16::from_be_bytes(b),
}
}
#[inline]
pub fn u32(self, b: [u8; 4]) -> u32 {
match self {
ByteOrder::Little => u32::from_le_bytes(b),
ByteOrder::Big => u32::from_be_bytes(b),
}
}
#[inline]
pub fn u64(self, b: [u8; 8]) -> u64 {
match self {
ByteOrder::Little => u64::from_le_bytes(b),
ByteOrder::Big => u64::from_be_bytes(b),
}
}
/// Read a `u16` at `off`, or `None` if out of bounds.
#[inline]
pub fn read_u16(self, data: &[u8], off: usize) -> Option<u16> {
data.get(off..off.checked_add(2)?).map(|s| self.u16([s[0], s[1]]))
}
#[inline]
pub fn read_u32(self, data: &[u8], off: usize) -> Option<u32> {
data.get(off..off.checked_add(4)?).map(|s| self.u32([s[0], s[1], s[2], s[3]]))
}
#[inline]
pub fn read_u64(self, data: &[u8], off: usize) -> Option<u64> {
data.get(off..off.checked_add(8)?).map(|s| self.u64([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]]))
}
pub fn put_u16(self, out: &mut Vec<u8>, v: u16) {
out.extend_from_slice(&match self {
ByteOrder::Little => v.to_le_bytes(),
ByteOrder::Big => v.to_be_bytes(),
});
}
pub fn put_u32(self, out: &mut Vec<u8>, v: u32) {
out.extend_from_slice(&match self {
ByteOrder::Little => v.to_le_bytes(),
ByteOrder::Big => v.to_be_bytes(),
});
}
pub fn put_u64(self, out: &mut Vec<u8>, v: u64) {
out.extend_from_slice(&match self {
ByteOrder::Little => v.to_le_bytes(),
ByteOrder::Big => v.to_be_bytes(),
});
}
}
/// TIFF field types (TIFF 6.0 §2 + supplements + BigTIFF).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u16)]
pub enum FieldType {
Byte = 1,
Ascii = 2,
Short = 3,
Long = 4,
Rational = 5,
SByte = 6,
Undefined = 7,
SShort = 8,
SLong = 9,
SRational = 10,
Float = 11,
Double = 12,
Ifd = 13,
Long8 = 16,
SLong8 = 17,
Ifd8 = 18,
}
impl FieldType {
pub fn from_u16(v: u16) -> Option<FieldType> {
use FieldType::*;
Some(match v {
1 => Byte,
2 => Ascii,
3 => Short,
4 => Long,
5 => Rational,
6 => SByte,
7 => Undefined,
8 => SShort,
9 => SLong,
10 => SRational,
11 => Float,
12 => Double,
13 => Ifd,
16 => Long8,
17 => SLong8,
18 => Ifd8,
_ => return None,
})
}
/// Size in bytes of one value.
pub fn size(self) -> usize {
use FieldType::*;
match self {
Byte | Ascii | SByte | Undefined => 1,
Short | SShort => 2,
Long | SLong | Float | Ifd => 4,
Rational | SRational | Double | Long8 | SLong8 | Ifd8 => 8,
}
}
}
/// A decoded field value. Arrays are stored in their native type.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Value {
Byte(Vec<u8>),
/// ASCII text (bytes up to the first NUL, decoded lossily as UTF-8).
Ascii(String),
Short(Vec<u16>),
Long(Vec<u32>),
Rational(Vec<(u32, u32)>),
SByte(Vec<i8>),
Undefined(Vec<u8>),
SShort(Vec<i16>),
SLong(Vec<i32>),
SRational(Vec<(i32, i32)>),
Float(Vec<f32>),
Double(Vec<f64>),
Ifd(Vec<u32>),
Long8(Vec<u64>),
SLong8(Vec<i64>),
Ifd8(Vec<u64>),
}
impl Value {
pub fn field_type(&self) -> FieldType {
match self {
Value::Byte(_) => FieldType::Byte,
Value::Ascii(_) => FieldType::Ascii,
Value::Short(_) => FieldType::Short,
Value::Long(_) => FieldType::Long,
Value::Rational(_) => FieldType::Rational,
Value::SByte(_) => FieldType::SByte,
Value::Undefined(_) => FieldType::Undefined,
Value::SShort(_) => FieldType::SShort,
Value::SLong(_) => FieldType::SLong,
Value::SRational(_) => FieldType::SRational,
Value::Float(_) => FieldType::Float,
Value::Double(_) => FieldType::Double,
Value::Ifd(_) => FieldType::Ifd,
Value::Long8(_) => FieldType::Long8,
Value::SLong8(_) => FieldType::SLong8,
Value::Ifd8(_) => FieldType::Ifd8,
}
}
/// Number of values (for ASCII: bytes including the terminating NUL).
pub fn count(&self) -> usize {
match self {
Value::Byte(v) | Value::Undefined(v) => v.len(),
Value::Ascii(s) => s.len() + 1,
Value::Short(v) => v.len(),
Value::Long(v) | Value::Ifd(v) => v.len(),
Value::Rational(v) => v.len(),
Value::SByte(v) => v.len(),
Value::SShort(v) => v.len(),
Value::SLong(v) => v.len(),
Value::SRational(v) => v.len(),
Value::Float(v) => v.len(),
Value::Double(v) => v.len(),
Value::Long8(v) | Value::Ifd8(v) => v.len(),
Value::SLong8(v) => v.len(),
}
}
/// Value `i` as `f64` (rationals divided; zero denominators give `None`). ASCII has no numbers.
pub fn get_f64(&self, i: usize) -> Option<f64> {
Some(match self {
Value::Byte(v) | Value::Undefined(v) => *v.get(i)? as f64,
Value::Ascii(_) => return None,
Value::Short(v) => *v.get(i)? as f64,
Value::Long(v) | Value::Ifd(v) => *v.get(i)? as f64,
Value::Rational(v) => {
let (n, d) = *v.get(i)?;
if d == 0 {
return None;
}
n as f64 / d as f64
}
Value::SByte(v) => *v.get(i)? as f64,
Value::SShort(v) => *v.get(i)? as f64,
Value::SLong(v) => *v.get(i)? as f64,
Value::SRational(v) => {
let (n, d) = *v.get(i)?;
if d == 0 {
return None;
}
n as f64 / d as f64
}
Value::Float(v) => *v.get(i)? as f64,
Value::Double(v) => *v.get(i)?,
Value::Long8(v) | Value::Ifd8(v) => *v.get(i)? as f64,
Value::SLong8(v) => *v.get(i)? as f64,
})
}
/// Value `i` as an unsigned integer (integer types only; negative values give `None`).
pub fn get_u64(&self, i: usize) -> Option<u64> {
match self {
Value::Byte(v) | Value::Undefined(v) => v.get(i).map(|&x| x as u64),
Value::Short(v) => v.get(i).map(|&x| x as u64),
Value::Long(v) | Value::Ifd(v) => v.get(i).map(|&x| x as u64),
Value::Long8(v) | Value::Ifd8(v) => v.get(i).copied(),
Value::SByte(v) => v.get(i).and_then(|&x| u64::try_from(x).ok()),
Value::SShort(v) => v.get(i).and_then(|&x| u64::try_from(x).ok()),
Value::SLong(v) => v.get(i).and_then(|&x| u64::try_from(x).ok()),
Value::SLong8(v) => v.get(i).and_then(|&x| u64::try_from(x).ok()),
_ => None,
}
}
/// Value `i` as a signed integer (integer types only).
pub fn get_i64(&self, i: usize) -> Option<i64> {
match self {
Value::SByte(v) => v.get(i).map(|&x| x as i64),
Value::SShort(v) => v.get(i).map(|&x| x as i64),
Value::SLong(v) => v.get(i).map(|&x| x as i64),
Value::SLong8(v) => v.get(i).copied(),
_ => self.get_u64(i).and_then(|x| i64::try_from(x).ok()),
}
}
/// All values as `f64` (missing/invalid entries skipped for rationals with zero denominators → 0).
pub fn to_f64_vec(&self) -> Vec<f64> {
(0..self.len_values()).map(|i| self.get_f64(i).unwrap_or(0.0)).collect()
}
/// All values as `u64` (integer types only; otherwise empty).
pub fn to_u64_vec(&self) -> Vec<u64> {
(0..self.len_values()).map_while(|i| self.get_u64(i)).collect()
}
fn len_values(&self) -> usize {
match self {
Value::Ascii(_) => 0,
_ => self.count(),
}
}
/// Text for ASCII values (trimmed of trailing NULs / spaces); `Byte`/`Undefined` are decoded as UTF-8 (lossy)
/// up to the first NUL, which covers XMP packets and UCS-less Exif strings.
pub fn as_str(&self) -> Option<String> {
match self {
Value::Ascii(s) => Some(s.trim_end_matches([' ', '\0']).to_string()),
Value::Byte(b) | Value::Undefined(b) => {
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
Some(String::from_utf8_lossy(&b[..end]).trim_end().to_string())
}
_ => None,
}
}
/// Raw bytes for `Byte` / `Undefined` / `SByte` / `Ascii`.
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
Value::Byte(b) | Value::Undefined(b) => Some(b),
Value::Ascii(s) => Some(s.as_bytes()),
_ => None,
}
}
}
/// One IFD entry.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Entry {
pub tag: u16,
pub value: Value,
/// Absolute position in the parsed buffer of the value bytes (inline or out-of-line).
pub offset: u64,
}
impl Entry {
pub fn field_type(&self) -> FieldType {
self.value.field_type()
}
pub fn count(&self) -> usize {
self.value.count()
}
}
/// An Image File Directory with its child IFDs already resolved.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Ifd {
/// Absolute offset of the IFD in the buffer.
pub offset: u64,
/// Entries sorted by tag (duplicate tags: the first occurrence wins).
pub entries: Vec<Entry>,
/// IFDs referenced by `SubIFDs` (330).
pub sub_ifds: Vec<Ifd>,
/// Exif private IFD (34665).
pub exif: Option<Box<Ifd>>,
/// GPS IFD (34853).
pub gps: Option<Box<Ifd>>,
/// Interoperability IFD (40965).
pub interop: Option<Box<Ifd>>,
}
impl Ifd {
pub fn get(&self, tag: u16) -> Option<&Entry> {
self.entries.binary_search_by_key(&tag, |e| e.tag).ok().map(|i| &self.entries[i])
}
pub fn contains(&self, tag: u16) -> bool {
self.get(tag).is_some()
}
pub fn value(&self, tag: u16) -> Option<&Value> {
self.get(tag).map(|e| &e.value)
}
/// First value as `u32` (integer types, saturating from wider types).
pub fn u32(&self, tag: u16) -> Option<u32> {
self.value(tag)?.get_u64(0).map(|v| v.min(u32::MAX as u64) as u32)
}
pub fn u64(&self, tag: u16) -> Option<u64> {
self.value(tag)?.get_u64(0)
}
pub fn u16(&self, tag: u16) -> Option<u16> {
self.value(tag)?.get_u64(0).map(|v| v.min(u16::MAX as u64) as u16)
}
pub fn i64(&self, tag: u16) -> Option<i64> {
self.value(tag)?.get_i64(0)
}
pub fn f64(&self, tag: u16) -> Option<f64> {
self.value(tag)?.get_f64(0)
}
pub fn f64s(&self, tag: u16) -> Option<Vec<f64>> {
self.value(tag).map(|v| v.to_f64_vec())
}
pub fn u64s(&self, tag: u16) -> Option<Vec<u64>> {
self.value(tag).map(|v| v.to_u64_vec())
}
/// Text value (ASCII, or UTF-8 bytes).
pub fn string(&self, tag: u16) -> Option<String> {
self.value(tag)?.as_str().filter(|s| !s.is_empty())
}
pub fn bytes(&self, tag: u16) -> Option<&[u8]> {
self.value(tag)?.as_bytes()
}
/// This IFD and all descendants (sub-IFDs, Exif, GPS, Interop), depth-first, pre-order.
pub fn walk(&self) -> Vec<&Ifd> {
let mut out = vec![self];
for s in &self.sub_ifds {
out.extend(s.walk());
}
for c in [&self.exif, &self.gps, &self.interop].into_iter().flatten() {
out.extend(c.walk());
}
out
}
/// Image layout of this IFD, if it describes an image.
pub fn image(&self) -> Result<image::ImageInfo> {
image::ImageInfo::from_ifd(self)
}
}
/// A parsed TIFF stream (IFD chain with resolved children).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Tiff {
pub order: ByteOrder,
pub bigtiff: bool,
/// The main IFD chain (IFD0, IFD1, …).
pub ifds: Vec<Ifd>,
}
impl Tiff {
/// Parse a TIFF stream (the header must be at offset 0 of `data`).
pub fn parse(data: &[u8]) -> Result<Tiff> {
reader::parse(data, &ParseOptions::default())
}
pub fn parse_with(data: &[u8], opts: &ParseOptions) -> Result<Tiff> {
reader::parse(data, opts)
}
/// Header check only: returns the byte order and BigTIFF flag.
pub fn sniff(data: &[u8]) -> Option<(ByteOrder, bool)> {
reader::header(data).ok().map(|(o, b, _)| (o, b))
}
/// Every IFD (main chain and all children), depth-first.
pub fn all_ifds(&self) -> Vec<&Ifd> {
self.ifds.iter().flat_map(|i| i.walk()).collect()
}
/// The Exif IFD attached to IFD0 (or any IFD).
pub fn exif(&self) -> Option<&Ifd> {
self.all_ifds().into_iter().find_map(|i| i.exif.as_deref())
}
pub fn gps(&self) -> Option<&Ifd> {
self.all_ifds().into_iter().find_map(|i| i.gps.as_deref())
}
/// First entry for `tag` in IFD0, then the Exif IFD, then any IFD.
pub fn find(&self, tag: u16) -> Option<&Entry> {
if let Some(e) = self.ifds.first().and_then(|i| i.get(tag)) {
return Some(e);
}
if let Some(e) = self.exif().and_then(|i| i.get(tag)) {
return Some(e);
}
self.all_ifds().into_iter().find_map(|i| i.get(tag))
}
}
+235
View File
@@ -0,0 +1,235 @@
//! Maker-note IFD helpers.
//!
//! Most vendors store their maker note (Exif tag 37500) as a TIFF IFD, but each uses its own header,
//! byte order and *offset base* (what stored value offsets are relative to). The header layouts here
//! follow the public descriptions in the ExifTool tag documentation (`MakerNotes` table) and the
//! vendors' own published notes; no third-party code was consulted.
use crate::{ByteOrder, Ifd, ParseOptions, parse_ifd_at};
use serde::{Deserialize, Serialize};
/// Maker-note dialects we know how to locate.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum MakerNoteKind {
/// Plain IFD at the start of the note, offsets relative to the enclosing TIFF header (Canon, many others).
Plain,
/// `Nikon\0` + version + embedded TIFF header at +10; offsets relative to that header.
NikonV3,
/// `Nikon\0\x01\0`: IFD at +8, offsets relative to the enclosing TIFF header.
NikonV1,
/// `SONY DSC `/`SONY CAM `/`SONY MOBILE` etc.: 12-byte header, offsets relative to the TIFF header.
Sony,
/// `FUJIFILM` + LE u32 IFD offset; little-endian, offsets relative to the note start.
Fujifilm,
/// `OLYMPUS\0` + byte order + version: IFD at +12, offsets relative to the note start.
OlympusNew,
/// `OLYMP\0` / `EPSON\0` etc.: IFD at +8, offsets relative to the TIFF header.
OlympusOld,
/// `Panasonic\0\0\0`: IFD at +12, offsets relative to the TIFF header.
Panasonic,
/// `AOC\0` + byte order: IFD at +6, offsets relative to the TIFF header.
PentaxAoc,
/// `PENTAX \0` + byte order: IFD at +10, offsets relative to the note start.
Pentax,
}
/// A parsed maker-note IFD.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MakerNote {
pub kind: MakerNoteKind,
pub order: ByteOrder,
/// Absolute offset that stored value offsets are relative to.
pub base: u64,
pub ifd: Ifd,
}
/// Locate and parse the maker note stored at absolute `offset` (length `len`) inside `data`, the buffer whose
/// start is the enclosing TIFF header. `order` is the enclosing stream's byte order. `make` (the Exif Make) is
/// used only as a hint for header-less notes. Returns `None` for unknown or unparsable notes.
pub fn parse_makernote(data: &[u8], offset: u64, len: u64, order: ByteOrder, make: &str) -> Option<MakerNote> {
let start = usize::try_from(offset).ok()?;
let end = start.checked_add(usize::try_from(len).ok()?)?.min(data.len());
let note = data.get(start..end)?;
let order_at = |i: usize| match note.get(i..i + 2) {
Some(b"II") => Some(ByteOrder::Little),
Some(b"MM") => Some(ByteOrder::Big),
_ => None,
};
// A fixed value budget: a maker note is at most tens of kilobytes of values in any honest file, and
// computing the budget from the buffer's length multiplies the file length (a hostile entry in a
// sparse header-read buffer, where the buffer holds the file's logical size but only a few
// kilobytes are loaded, would decode allocations no actual data backs).
let opts = ParseOptions { max_ifds: 64, max_depth: 3, value_budget: Some(1 << 20), ..Default::default() };
let try_parse = |kind: MakerNoteKind, ifd_at: u64, base: u64, ord: ByteOrder| -> Option<MakerNote> {
let (ifd, _) = parse_ifd_at(data, ifd_at, ord, base, false, &opts).ok()?;
(!ifd.entries.is_empty()).then_some(MakerNote { kind, order: ord, base, ifd })
};
if note.starts_with(b"Nikon\0") {
if note.get(6) == Some(&1) {
return try_parse(MakerNoteKind::NikonV1, offset + 8, 0, order);
}
let ord = order_at(10)?;
let ifd_rel = ord.read_u32(note, 14)? as u64;
let base = offset + 10;
return try_parse(MakerNoteKind::NikonV3, base + ifd_rel, base, ord);
}
if note.starts_with(b"FUJIFILM") || note.starts_with(b"GENERALE") {
let rel = ByteOrder::Little.read_u32(note, 8)? as u64;
return try_parse(MakerNoteKind::Fujifilm, offset + rel, offset, ByteOrder::Little);
}
if note.starts_with(b"OLYMPUS\0") || note.starts_with(b"OM SYSTEM\0") {
let (ord_at, ifd_at) = if note.starts_with(b"OM SYSTEM\0") { (12, 16) } else { (8, 12) };
let ord = order_at(ord_at).unwrap_or(order);
return try_parse(MakerNoteKind::OlympusNew, offset + ifd_at, offset, ord);
}
for p in [b"OLYMP\0", b"EPSON\0", b"MINOL\0", b"CAMER\0", b"SANYO\0"] {
if note.starts_with(p) {
return try_parse(MakerNoteKind::OlympusOld, offset + 8, 0, order);
}
}
if note.starts_with(b"Panasonic\0") {
return try_parse(MakerNoteKind::Panasonic, offset + 12, 0, order);
}
if note.starts_with(b"AOC\0") {
let ord = order_at(4).unwrap_or(order);
return try_parse(MakerNoteKind::PentaxAoc, offset + 6, 0, ord);
}
if note.starts_with(b"PENTAX \0") {
let ord = order_at(8).unwrap_or(order);
return try_parse(MakerNoteKind::Pentax, offset + 10, offset, ord);
}
if (note.starts_with(b"SONY") || note.starts_with(b"VHAB \0"))
&& let Some(m) = try_parse(MakerNoteKind::Sony, offset + 12, 0, order)
{
return Some(m);
}
let _ = make;
try_parse(MakerNoteKind::Plain, offset, 0, order)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{IfdBuilder, TiffWriter, Value, tags};
/// Build a TIFF whose Exif IFD carries `note` as the maker note, return (bytes, note offset, len).
fn with_note(order: ByteOrder, note: Vec<u8>) -> (Vec<u8>, u64, u64) {
let mut exif = IfdBuilder::new();
exif.set(tags::MAKER_NOTE, Value::Undefined(note));
let mut ifd0 = IfdBuilder::new();
ifd0.set(tags::MAKE, Value::Ascii("Test".into()));
ifd0.set_child(tags::EXIF_IFD, exif);
let bytes = TiffWriter { order, bigtiff: false }.write(&[ifd0]).unwrap();
let t = crate::Tiff::parse(&bytes).unwrap();
let e = t.exif().unwrap().get(tags::MAKER_NOTE).unwrap();
let (off, len) = (e.offset, e.count() as u64);
(bytes, off, len)
}
/// An IFD with one SHORT entry (tag 1, value 7) and one out-of-line LONG[2] entry at `rel` (relative to base).
fn tiny_ifd(order: ByteOrder, rel_values: u32) -> Vec<u8> {
let mut v = Vec::new();
order.put_u16(&mut v, 2);
order.put_u16(&mut v, 1);
order.put_u16(&mut v, 3);
order.put_u32(&mut v, 1);
order.put_u16(&mut v, 7);
order.put_u16(&mut v, 0);
order.put_u16(&mut v, 2);
order.put_u16(&mut v, 4);
order.put_u32(&mut v, 2);
order.put_u32(&mut v, rel_values);
order.put_u32(&mut v, 0);
v
}
#[test]
fn nikon_v3_uses_embedded_header_base() {
for order in [ByteOrder::Little, ByteOrder::Big] {
let mut note = b"Nikon\0\x02\x10\0\0".to_vec();
let hdr_at = note.len();
note.extend_from_slice(if order == ByteOrder::Little { b"II" } else { b"MM" });
order.put_u16(&mut note, 42);
order.put_u32(&mut note, 8);
// IFD at +8 (relative to embedded header), values after IFD: 8 + 2 + 24 + 4 = 38
note.extend(tiny_ifd(order, 38));
order.put_u32(&mut note, 111);
order.put_u32(&mut note, 222);
let (bytes, off, len) = with_note(order, note);
let m = parse_makernote(&bytes, off, len, order, "NIKON CORPORATION").unwrap();
assert_eq!(m.kind, MakerNoteKind::NikonV3);
assert_eq!(m.base, off + hdr_at as u64);
assert_eq!(m.ifd.u32(1), Some(7));
assert_eq!(m.ifd.u64s(2).unwrap(), vec![111, 222]);
}
}
#[test]
fn fujifilm_offsets_relative_to_note() {
let mut note = b"FUJIFILM".to_vec();
ByteOrder::Little.put_u32(&mut note, 12);
// IFD at 12 (relative to note), values at 12 + 30 = 42
note.extend(tiny_ifd(ByteOrder::Little, 42));
ByteOrder::Little.put_u32(&mut note, 5);
ByteOrder::Little.put_u32(&mut note, 6);
// enclosing file is big-endian: Fuji notes are always little-endian
let (bytes, off, len) = with_note(ByteOrder::Big, note);
let m = parse_makernote(&bytes, off, len, ByteOrder::Big, "FUJIFILM").unwrap();
assert_eq!(m.kind, MakerNoteKind::Fujifilm);
assert_eq!(m.order, ByteOrder::Little);
assert_eq!(m.ifd.u64s(2).unwrap(), vec![5, 6]);
}
#[test]
fn plain_canon_style_note() {
// Plain IFD whose out-of-line offset is relative to the TIFF header: we need the absolute
// position, which we only know after writing. Write once to learn the offset, then patch.
let order = ByteOrder::Little;
let mut note = tiny_ifd(order, 0);
note.extend_from_slice(&[0; 8]);
let (mut bytes, off, len) = with_note(order, note);
let values_abs = off as u32 + 30;
bytes[off as usize + 22..off as usize + 26].copy_from_slice(&values_abs.to_le_bytes());
bytes[off as usize + 30..off as usize + 34].copy_from_slice(&9u32.to_le_bytes());
let m = parse_makernote(&bytes, off, len, order, "Canon").unwrap();
assert_eq!(m.kind, MakerNoteKind::Plain);
assert_eq!(m.ifd.u64s(2).unwrap(), vec![9, 0]);
}
/// A maker note's value budget is fixed, not computed from the length of the buffer it sits in:
/// an entry claiming more than the note budget (here 2 MB) is dropped, a fitting one is read. In a
/// sparse header read the buffer holds the file's logical size while only kilobytes were loaded,
/// so a length-scaled budget would let an unrelated entry decode megabytes of zeros (see the focus
/// reader, try_read_exif_with).
#[test]
fn a_note_entry_past_the_fixed_value_budget_is_dropped() {
let order = ByteOrder::Little;
let mut note = b"SONY DSC \0\0\0".to_vec();
// the Sony IFD directly after the 12-byte header, one fitting BYTE entry and a decoy
// BYTE[2 MB] entry pointing into the file's own (zero-filled) body
order.put_u16(&mut note, 2);
order.put_u16(&mut note, 0x201b);
order.put_u16(&mut note, 1);
order.put_u32(&mut note, 1);
note.extend([3, 0, 0, 0]);
order.put_u16(&mut note, 0x9900);
order.put_u16(&mut note, 1);
order.put_u32(&mut note, 2 << 20);
order.put_u32(&mut note, 200);
order.put_u32(&mut note, 0);
let mut bytes = note.clone();
bytes.resize(200 + (2 << 20), 0); // the decoy's value range is inside the real bytes
let m = parse_makernote(&bytes, 0, note.len() as u64, order, "SONY").unwrap();
assert_eq!(m.kind, MakerNoteKind::Sony);
assert!(m.ifd.get(0x201b).is_some(), "the fitting entry is read");
assert!(m.ifd.get(0x9900).is_none(), "the decoy is dropped by the value budget");
}
#[test]
fn garbage_note_is_none() {
let (bytes, off, len) = with_note(ByteOrder::Little, vec![0xff; 40]);
assert!(parse_makernote(&bytes, off, len, ByteOrder::Little, "X").is_none());
assert!(parse_makernote(&bytes, 1 << 40, 10, ByteOrder::Little, "X").is_none());
assert!(parse_makernote(&bytes, off, u64::MAX, ByteOrder::Little, "X").is_none());
}
}
+536
View File
@@ -0,0 +1,536 @@
//! Bounds-checked IFD parsing.
use crate::{ByteOrder, Entry, FieldType, Ifd, Result, Tiff, TiffError, Value, tags};
use std::collections::HashSet;
/// Limits applied while parsing. The defaults are generous for real files and safe for hostile ones.
#[derive(Clone, Debug)]
pub struct ParseOptions {
/// Maximum number of IFDs visited in total (chain + children).
pub max_ifds: usize,
/// Maximum nesting depth of child IFDs.
pub max_depth: usize,
/// Maximum number of entries read from one IFD.
pub max_entries: usize,
/// Maximum total number of entries read across all IFDs (chain + children).
///
/// Hostile files can reuse the same bytes for up to `max_ifds` tables of `max_entries`
/// entries each (overlapping IFD tables via SubIFD pointers shifted by one entry), which can
/// allocate millions of `Entry`s from a small input (~100 KB → ~400 MB). This hard cap bounds
/// that amplification regardless of entry size. 65 536 is generous for real files — a 50 MB
/// scanned TIFF with thousands of entries across chain + children stays well under it — yet
/// stops a small file from allocating hundreds of megabytes of `Entry`s.
pub max_entries_total: usize,
/// Maximum total bytes of decoded values; `None` = `8 × input length + 1 MiB`.
pub value_budget: Option<u64>,
/// Follow `SubIFDs` / Exif / GPS / Interop pointers.
pub follow_children: bool,
/// Accept the TIFF-like magic numbers used by some raw formats (ORF `IIRO`/`IIRS`, RW2 `IIU`).
pub accept_raw_magic: bool,
}
impl Default for ParseOptions {
fn default() -> Self {
Self {
max_ifds: 1024,
max_depth: 10,
max_entries: 8192,
max_entries_total: 65536,
value_budget: None,
follow_children: true,
accept_raw_magic: true,
}
}
}
impl ParseOptions {
/// The value budget for `data_len` bytes of input: [`Self::value_budget`], else `8 × data_len + 1 MiB`.
pub fn budget_for(&self, data_len: usize) -> u64 {
self.value_budget.unwrap_or((data_len as u64).saturating_mul(8).saturating_add(1 << 20))
}
}
/// Where a directory's entries are and how many of them the reader reads, as [`directory_entries`] found them.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DirectoryEntries {
/// Position of the first entry.
table: usize,
/// Entries read: the declared count, cut to the entries the data holds and to the `max_entries` limit.
read: usize,
/// The declared entry count.
declared: u64,
/// Bytes per entry: 12, or 20 in BigTIFF.
entry_size: usize,
}
impl DirectoryEntries {
/// Positions of the entries read, in directory order. Checked arithmetic: the iteration ends rather than
/// overflow, whatever the layout.
pub fn positions(&self) -> impl Iterator<Item = usize> + use<> {
let (table, size) = (self.table, self.entry_size);
(0..self.read).map_while(move |i| i.checked_mul(size)?.checked_add(table))
}
/// The number of entries read.
pub fn read(&self) -> usize {
self.read
}
/// The entry count the directory declares.
pub fn declared(&self) -> u64 {
self.declared
}
}
/// The directory at `offset` as the reader walks it: its entry table and how many entries it reads. The same errors
/// as [`parse_ifd_at`] for a directory it can't read: past the data, empty, or without room for one entry.
pub fn directory_entries(data: &[u8], offset: u64, order: ByteOrder, bigtiff: bool, max_entries: usize) -> Result<DirectoryEntries> {
let pos = usize::try_from(offset).map_err(|_| TiffError::Truncated(offset))?;
let (n, head, entry_size) = if bigtiff {
(order.read_u64(data, pos).ok_or(TiffError::Truncated(offset))?, 8usize, 20usize)
} else {
(order.read_u16(data, pos).ok_or(TiffError::Truncated(offset))? as u64, 2, 12)
};
let table = pos + head;
let fit = (data.len().saturating_sub(table) / entry_size) as u64;
let read = n.min(fit).min(max_entries as u64) as usize;
if n == 0 {
return Err(TiffError::Invalid(format!("empty IFD at {offset}")));
}
if read == 0 {
return Err(TiffError::Truncated(offset));
}
Ok(DirectoryEntries { table, read, declared: n, entry_size })
}
/// A directory entry the reader can read: its tag, type and count, and where its values are.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EntrySpan {
pub tag: u16,
pub field_type: FieldType,
pub count: u64,
/// Position of the values in the data: the entry's own value field when they fit it, else `base` plus the offset
/// stored there.
pub start: u64,
/// `count × size` bytes.
pub len: u64,
}
/// The entry at `pos`, when the reader reads it: a known field type, `count × size` without overflow, and values
/// inside `data`. Nothing is decoded; [`parse_ifd_at`] also drops an entry whose values exceed what is left of its
/// value budget ([`ParseOptions::budget_for`]), which this leaves to the caller.
pub fn entry_span(data: &[u8], pos: usize, order: ByteOrder, bigtiff: bool, base: u64) -> Option<EntrySpan> {
let tag = order.read_u16(data, pos)?;
let field_type = FieldType::from_u16(order.read_u16(data, pos.checked_add(2)?)?)?;
let (count, field, inline) = if bigtiff {
(order.read_u64(data, pos.checked_add(4)?)?, pos.checked_add(12)?, 8u64)
} else {
(order.read_u32(data, pos.checked_add(4)?)? as u64, pos.checked_add(8)?, 4)
};
let len = count.checked_mul(field_type.size() as u64)?;
let start = if len <= inline {
field as u64
} else {
let rel = if bigtiff { order.read_u64(data, field)? } else { order.read_u32(data, field)? as u64 };
base.checked_add(rel)?
};
let end = start.checked_add(len)?;
(end <= data.len() as u64).then_some(EntrySpan { tag, field_type, count, start, len })
}
/// Whether a directory declaring `declared` entries was read whole (`read` entries), so that its next-IFD pointer
/// follows them. The counts compare as 64-bit numbers: narrowing the declared count to `usize` instead would, on a
/// 32-bit target, let a BigTIFF declaring 4,294,967,297 entries but holding one pass for a whole directory.
fn reads_every_entry(declared: u64, read: usize) -> bool {
u64::try_from(read).is_ok_and(|read| read == declared)
}
/// Parse the header: (byte order, bigtiff, first IFD offset).
pub(crate) fn header(data: &[u8]) -> Result<(ByteOrder, bool, u64)> {
header_with(data, true)
}
fn header_with(data: &[u8], raw_magic: bool) -> Result<(ByteOrder, bool, u64)> {
let order = match data.get(0..2) {
Some(b"II") => ByteOrder::Little,
Some(b"MM") => ByteOrder::Big,
_ => return Err(TiffError::NotTiff),
};
let magic = order.read_u16(data, 2).ok_or(TiffError::NotTiff)?;
match magic {
42 => Ok((order, false, order.read_u32(data, 4).ok_or(TiffError::Truncated(4))? as u64)),
43 => {
let bytesize = order.read_u16(data, 4).ok_or(TiffError::Truncated(4))?;
let zero = order.read_u16(data, 6).ok_or(TiffError::Truncated(6))?;
if bytesize != 8 || zero != 0 {
return Err(TiffError::NotTiff);
}
Ok((order, true, order.read_u64(data, 8).ok_or(TiffError::Truncated(8))?))
}
0x4f52 | 0x5352 | 0x0055 if raw_magic => Ok((order, false, order.read_u32(data, 4).ok_or(TiffError::Truncated(4))? as u64)),
_ => Err(TiffError::NotTiff),
}
}
pub(crate) fn parse(data: &[u8], opts: &ParseOptions) -> Result<Tiff> {
let (order, big, first) = header_with(data, opts.accept_raw_magic)?;
let mut ctx = Ctx::new(data, order, big, opts);
let mut ifds = Vec::new();
let mut off = first;
while off != 0 {
match ctx.ifd(off, 0, 0) {
Ok((ifd, next)) => {
ifds.push(ifd);
off = next;
}
Err(e) => {
if ifds.is_empty() {
return Err(e);
}
break;
}
}
}
if ifds.is_empty() {
return Err(TiffError::Invalid("no IFDs".into()));
}
Ok(Tiff { order, bigtiff: big, ifds })
}
/// Parse one IFD at absolute `offset` whose value offsets (and child IFD pointers) are relative to
/// `base` (absolute position = `base + stored offset`). Returns the IFD and the absolute offset of the
/// next IFD (0 when there is none). Used for maker notes, which often use their own offset base.
pub fn parse_ifd_at(data: &[u8], offset: u64, order: ByteOrder, base: u64, bigtiff: bool, opts: &ParseOptions) -> Result<(Ifd, u64)> {
let mut ctx = Ctx::new(data, order, bigtiff, opts);
ctx.ifd(offset, base, 0)
}
struct Ctx<'a> {
data: &'a [u8],
order: ByteOrder,
big: bool,
opts: &'a ParseOptions,
visited: HashSet<u64>,
budget: u64,
entries_total: usize,
}
impl<'a> Ctx<'a> {
fn new(data: &'a [u8], order: ByteOrder, big: bool, opts: &'a ParseOptions) -> Self {
let budget = opts.budget_for(data.len());
Self { data, order, big, opts, visited: HashSet::new(), budget, entries_total: 0 }
}
fn read_off(&self, pos: usize) -> Option<u64> {
if self.big { self.order.read_u64(self.data, pos) } else { self.order.read_u32(self.data, pos).map(|v| v as u64) }
}
/// Returns (ifd, absolute next offset or 0).
fn ifd(&mut self, offset: u64, base: u64, depth: usize) -> Result<(Ifd, u64)> {
if depth > self.opts.max_depth {
return Err(TiffError::Limit("IFD nesting depth"));
}
if self.visited.len() >= self.opts.max_ifds {
return Err(TiffError::Limit("IFD count"));
}
if !self.visited.insert(offset) {
return Err(TiffError::Invalid(format!("IFD loop at {offset}")));
}
let dir = directory_entries(self.data, offset, self.order, self.big, self.opts.max_entries)?;
// Bound total entries across the whole parse so overlapping IFD tables (SubIFD pointers
// shifted by one entry) cannot allocate millions of `Entry`s from a small input.
self.entries_total = self.entries_total.saturating_add(dir.read);
if self.entries_total > self.opts.max_entries_total {
return Err(TiffError::Limit("entry count"));
}
let mut entries = Vec::with_capacity(dir.read);
for p in dir.positions() {
if let Some(e) = self.entry(p, base) {
entries.push(e);
}
}
entries.sort_by_key(|e| e.tag);
entries.dedup_by_key(|e| e.tag);
// the next-IFD pointer follows the last entry, read only when every declared entry was (checked: a BigTIFF
// count can be anything up to u64::MAX)
let next = if reads_every_entry(dir.declared, dir.read) {
match dir.read.checked_mul(dir.entry_size).and_then(|n| n.checked_add(dir.table)).and_then(|p| self.read_off(p)) {
Some(0) | None => 0,
Some(v) => base.checked_add(v).unwrap_or(0),
}
} else {
0
};
let mut ifd = Ifd { offset, entries, ..Default::default() };
if self.opts.follow_children {
self.children(&mut ifd, base, depth);
}
Ok((ifd, next))
}
fn children(&mut self, ifd: &mut Ifd, base: u64, depth: usize) {
if let Some(v) = ifd.value(tags::SUB_IFDS) {
let offs = v.to_u64_vec();
for o in offs {
if let Some(abs) = base.checked_add(o)
&& let Ok((sub, _)) = self.ifd(abs, base, depth + 1)
{
ifd.sub_ifds.push(sub);
}
}
}
let child = |tag: u16, ctx: &mut Self| -> Option<Box<Ifd>> {
let o = ifd.value(tag)?.get_u64(0)?;
let abs = base.checked_add(o)?;
ctx.ifd(abs, base, depth + 1).ok().map(|(i, _)| Box::new(i))
};
let exif = child(tags::EXIF_IFD, self);
let gps = child(tags::GPS_IFD, self);
let interop = child(tags::INTEROP_IFD, self);
ifd.exif = exif;
ifd.gps = gps;
ifd.interop = interop;
}
fn entry(&mut self, p: usize, base: u64) -> Option<Entry> {
let span = entry_span(self.data, p, self.order, self.big, base)?;
if span.len > self.budget {
return None;
}
self.budget -= span.len;
let bytes = self.data.get(usize::try_from(span.start).ok()?..usize::try_from(span.start + span.len).ok()?)?;
Some(Entry { tag: span.tag, value: decode(self.order, span.field_type, bytes), offset: span.start })
}
}
/// Decode `bytes` (exactly `count × size` long, or the first values of an entry) as values of type `typ`.
pub fn decode(o: ByteOrder, typ: FieldType, b: &[u8]) -> Value {
fn arr<const N: usize>(c: &[u8]) -> [u8; N] {
let mut a = [0u8; N];
a.copy_from_slice(c);
a
}
match typ {
FieldType::Byte => Value::Byte(b.to_vec()),
FieldType::Undefined => Value::Undefined(b.to_vec()),
FieldType::Ascii => {
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
Value::Ascii(String::from_utf8_lossy(&b[..end]).into_owned())
}
FieldType::SByte => Value::SByte(b.iter().map(|&x| x as i8).collect()),
FieldType::Short => Value::Short(b.as_chunks::<2>().0.iter().map(|c| o.u16(arr(c))).collect()),
FieldType::SShort => Value::SShort(b.as_chunks::<2>().0.iter().map(|c| o.u16(arr(c)) as i16).collect()),
FieldType::Long => Value::Long(b.as_chunks::<4>().0.iter().map(|c| o.u32(arr(c))).collect()),
FieldType::Ifd => Value::Ifd(b.as_chunks::<4>().0.iter().map(|c| o.u32(arr(c))).collect()),
FieldType::SLong => Value::SLong(b.as_chunks::<4>().0.iter().map(|c| o.u32(arr(c)) as i32).collect()),
FieldType::Float => Value::Float(b.as_chunks::<4>().0.iter().map(|c| f32::from_bits(o.u32(arr(c)))).collect()),
FieldType::Rational => Value::Rational(b.as_chunks::<8>().0.iter().map(|c| (o.u32(arr(&c[..4])), o.u32(arr(&c[4..])))).collect()),
FieldType::SRational => {
Value::SRational(b.as_chunks::<8>().0.iter().map(|c| (o.u32(arr(&c[..4])) as i32, o.u32(arr(&c[4..])) as i32)).collect())
}
FieldType::Double => Value::Double(b.as_chunks::<8>().0.iter().map(|c| f64::from_bits(o.u64(arr(c)))).collect()),
FieldType::Long8 => Value::Long8(b.as_chunks::<8>().0.iter().map(|c| o.u64(arr(c))).collect()),
FieldType::Ifd8 => Value::Ifd8(b.as_chunks::<8>().0.iter().map(|c| o.u64(arr(c))).collect()),
FieldType::SLong8 => Value::SLong8(b.as_chunks::<8>().0.iter().map(|c| o.u64(arr(c)) as i64).collect()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Value;
use crate::tags as t;
use crate::writer::{IfdBuilder, TiffWriter};
/// A value budget the caller sets bounds what is decoded: an entry claiming more value bytes than
/// the budget allows is dropped, entries that fit are read (the sparse header-read path, where the
/// buffer holds the file's logical size but only a few kilobytes were ever loaded, sizes its
/// budget from what was fetched, not from whatever length the file claims).
#[test]
fn the_value_budget_drops_over_budget_entries_and_keeps_the_rest() {
let mut ifd0 = IfdBuilder::new();
ifd0.set(t::MAKE, Value::Ascii("Keep".into()));
ifd0.set(0x9000, Value::Byte(vec![0; 2 << 20]));
let file = TiffWriter { order: ByteOrder::Little, bigtiff: false }.write(&[ifd0]).unwrap();
let opts = ParseOptions { value_budget: Some(1 << 20), ..Default::default() };
let tiff = Tiff::parse_with(&file, &opts).unwrap();
assert_eq!(tiff.ifds[0].string(t::MAKE), Some("Keep".to_string()), "a fitting entry is read");
assert!(tiff.ifds[0].get(0x9000).is_none(), "the over-budget entry is dropped");
}
/// A BigTIFF whose first IFD, at 80, declares `declared` entries but holds one, followed by a next-IFD pointer
/// `next`; a valid one-entry IFD sits at 16.
fn bigtiff_chain(order: ByteOrder, declared: u64, next: u64) -> Vec<u8> {
let ifd = |d: &mut Vec<u8>, count: u64, tag: u16, next: u64| {
order.put_u64(d, count);
order.put_u16(d, tag);
order.put_u16(d, 3);
order.put_u64(d, 1);
order.put_u16(d, 8);
d.extend([0; 6]);
order.put_u64(d, next);
};
let mut d = if order == ByteOrder::Little { b"II".to_vec() } else { b"MM".to_vec() };
order.put_u16(&mut d, 43);
order.put_u16(&mut d, 8);
order.put_u16(&mut d, 0);
order.put_u64(&mut d, 80);
ifd(&mut d, 1, 0x0101, 0);
d.resize(80, 0);
ifd(&mut d, declared, 0x0100, next);
d
}
#[test]
fn only_a_directory_read_whole_has_a_next_pointer() {
// the comparison itself, on every target: a declared count past u32::MAX never matches a small read count
for (declared, read, whole) in [
(0, 0, true),
(1, 1, true),
(8192, 8192, true),
(8193, 8192, false),
(1, 0, false),
((1 << 32) + 1, 1, false),
((2 << 32) + 1, 1, false),
((1 << 63) + 1, 1, false),
(u64::MAX, 1, false),
] {
assert_eq!(reads_every_entry(declared, read), whole, "{declared} declared, {read} read");
}
}
#[test]
fn a_bigtiff_count_past_u32_max_never_follows_the_next_pointer() {
// the whole chain: on 32-bit targets (wasm32) the old cast truncated these counts to 1 and followed the pointer
for order in [ByteOrder::Little, ByteOrder::Big] {
for declared in [(1u64 << 32) + 1, (2u64 << 32) + 1, (1u64 << 63) + 1] {
let d = bigtiff_chain(order, declared, 16);
let tiff = Tiff::parse(&d).unwrap();
assert_eq!(tiff.ifds.len(), 1, "{order:?} {declared}");
assert_eq!(tiff.ifds[0].u64(0x0100), Some(8));
assert_eq!(parse_ifd_at(&d, 80, order, 0, true, &ParseOptions::default()).unwrap().1, 0, "{order:?} {declared}");
}
// control: the same file declaring its one entry follows the pointer to the IFD at 16
let whole = Tiff::parse(&bigtiff_chain(order, 1, 16)).unwrap();
assert_eq!(whole.ifds.len(), 2, "{order:?}");
assert_eq!(whole.ifds[1].u64(0x0101), Some(8));
}
}
#[test]
fn directory_positions_never_overflow() {
// a hand-built layout the reader never produces: the iterator stops instead of overflowing
let hostile = DirectoryEntries { table: usize::MAX, read: 2, declared: 2, entry_size: 12 };
assert_eq!(hostile.positions().collect::<Vec<_>>(), vec![usize::MAX]);
assert_eq!(hostile.positions().nth(1), None);
let wide = DirectoryEntries { table: usize::MAX - 30, read: 4, declared: 4, entry_size: 12 };
assert_eq!(wide.positions().collect::<Vec<_>>(), vec![usize::MAX - 30, usize::MAX - 18, usize::MAX - 6]);
// a directory read from data lists its entries
let mut data = Vec::new();
ByteOrder::Little.put_u16(&mut data, 2);
data.extend([0; 24]);
let dir = directory_entries(&data, 0, ByteOrder::Little, false, 8192).unwrap();
assert_eq!(dir.positions().collect::<Vec<_>>(), vec![2, 14]);
}
#[test]
fn a_bigtiff_directory_declaring_u64_max_entries_parses_without_overflow() {
// 44 bytes: BigTIFF header (first IFD at 16), an entry count of u64::MAX, one 20-byte entry
let mut data = b"II\x2b\x00\x08\x00\x00\x00".to_vec();
data.extend(16u64.to_le_bytes());
data.extend(u64::MAX.to_le_bytes());
data.extend(0x0100u16.to_le_bytes());
data.extend(3u16.to_le_bytes());
data.extend(1u64.to_le_bytes());
data.extend([8, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(data.len(), 44);
let tiff = Tiff::parse(&data).unwrap();
assert_eq!(tiff.ifds.len(), 1);
assert_eq!(tiff.ifds[0].u64(0x0100), Some(8));
}
#[test]
fn overlapping_ifd_tables_hit_entry_cap_instead_of_amplifying() {
// classic TIFF; IFDs at 8, 20, 32, ... each declaring 8192 entries whose tables overlap
// (next table starts one entry after the previous table's start). Without the cap, a single
// `Tiff::parse` would run `Vec::with_capacity(8192)` per IFD inside one `Ctx`, amplifying a
// small file into millions of `Entry`s; the per-`Ctx` `max_entries_total` cap rejects the
// 9th IFD before any allocation.
let mut data = vec![0u8; 200_000];
data[0..2].copy_from_slice(b"II");
data[2..4].copy_from_slice(&42u16.to_le_bytes());
data[4..8].copy_from_slice(&8u32.to_le_bytes()); // first IFD at 8
for k in 0..9u64 {
let ifd_off = (8 + 12 * k) as usize;
data[ifd_off..ifd_off + 2].copy_from_slice(&8192u16.to_le_bytes()); // count
let table = ifd_off + 2;
let ptr = table + 8192 * 12; // next-IFD pointer lives right after the entries
let next = if k + 1 < 9 { (8 + 12 * (k + 1)) as u32 } else { 0 };
data[ptr..ptr + 4].copy_from_slice(&next.to_le_bytes());
}
let tiff = Tiff::parse(&data).unwrap();
assert_eq!(tiff.ifds.len(), 8, "8 IFDs parsed, 9th rejected by max_entries_total before allocation");
}
#[test]
fn a_legitimate_multi_ifd_file_still_parses_under_the_entry_cap() {
// classic TIFF: IFD0 @8 (3 entries: value + Exif + GPS child pointers) -> IFD1 @58 chain,
// plus Exif child @60 and GPS child @90. Total entries = 6, far under max_entries_total.
let mut data = vec![0u8; 128];
data[0..2].copy_from_slice(b"II");
data[2..4].copy_from_slice(&42u16.to_le_bytes());
data[4..8].copy_from_slice(&8u32.to_le_bytes()); // first IFD at 8
// IFD0 @8: count 3, table @10, next-IFD @46 -> IFD1 @58
data[8..10].copy_from_slice(&3u16.to_le_bytes());
// entry0 @10: tag 0x0100, SHORT, count 1, value 8 (inline @18..22)
data[10..12].copy_from_slice(&0x0100u16.to_le_bytes());
data[12..14].copy_from_slice(&3u16.to_le_bytes());
data[14..18].copy_from_slice(&1u32.to_le_bytes());
data[18..22].copy_from_slice(&8u32.to_le_bytes());
// entry1 @22: tag 0x8769 ExifIFD, LONG, count 1, value 60 (inline @30..34)
data[22..24].copy_from_slice(&0x8769u16.to_le_bytes());
data[24..26].copy_from_slice(&4u16.to_le_bytes());
data[26..30].copy_from_slice(&1u32.to_le_bytes());
data[30..34].copy_from_slice(&60u32.to_le_bytes());
// entry2 @34: tag 0x8825 GPSIFD, LONG, count 1, value 90 (inline @42..46)
data[34..36].copy_from_slice(&0x8825u16.to_le_bytes());
data[36..38].copy_from_slice(&4u16.to_le_bytes());
data[38..42].copy_from_slice(&1u32.to_le_bytes());
data[42..46].copy_from_slice(&90u32.to_le_bytes());
data[46..50].copy_from_slice(&58u32.to_le_bytes()); // next IFD
// IFD1 @58: count 1, table @60, next 0
data[58..60].copy_from_slice(&1u16.to_le_bytes());
data[60..62].copy_from_slice(&0x0100u16.to_le_bytes());
data[62..64].copy_from_slice(&3u16.to_le_bytes());
data[64..68].copy_from_slice(&1u32.to_le_bytes());
data[68..72].copy_from_slice(&8u32.to_le_bytes());
data[72..76].copy_from_slice(&0u32.to_le_bytes());
// Exif child IFD @60: count 1, entry tag 0x9000 value 1, next 0
data[60..62].copy_from_slice(&1u16.to_le_bytes());
data[62..64].copy_from_slice(&0x9000u16.to_le_bytes());
data[64..66].copy_from_slice(&3u16.to_le_bytes());
data[66..70].copy_from_slice(&1u32.to_le_bytes());
data[70..74].copy_from_slice(&1u32.to_le_bytes());
data[74..78].copy_from_slice(&0u32.to_le_bytes());
// GPS child IFD @90: count 1, entry tag 0 value 1, next 0
data[90..92].copy_from_slice(&1u16.to_le_bytes());
data[92..94].copy_from_slice(&0u16.to_le_bytes());
data[94..96].copy_from_slice(&3u16.to_le_bytes());
data[96..100].copy_from_slice(&1u32.to_le_bytes());
data[100..104].copy_from_slice(&1u32.to_le_bytes());
data[104..108].copy_from_slice(&0u32.to_le_bytes());
let tiff = Tiff::parse(&data).unwrap();
assert_eq!(tiff.ifds.len(), 2, "chain IFDs parse fully");
assert_eq!(tiff.ifds[0].u64(0x0100), Some(8));
let exif = tiff.ifds[0].exif.as_ref().expect("Exif child present");
assert_eq!(exif.u64(0x9000), Some(1));
let gps = tiff.ifds[0].gps.as_ref().expect("GPS child present");
assert_eq!(gps.u64(0x0000), Some(1));
}
}
+226
View File
@@ -0,0 +1,226 @@
//! Tag numbers (TIFF 6.0, TIFF/EP, Exif 2.32, GPS, DNG 1.7). Public specification constants.
// --- TIFF baseline / extensions
pub const NEW_SUBFILE_TYPE: u16 = 254;
pub const SUBFILE_TYPE: u16 = 255;
pub const IMAGE_WIDTH: u16 = 256;
pub const IMAGE_LENGTH: u16 = 257;
pub const BITS_PER_SAMPLE: u16 = 258;
pub const COMPRESSION: u16 = 259;
pub const PHOTOMETRIC: u16 = 262;
pub const FILL_ORDER: u16 = 266;
pub const DOCUMENT_NAME: u16 = 269;
pub const IMAGE_DESCRIPTION: u16 = 270;
pub const MAKE: u16 = 271;
pub const MODEL: u16 = 272;
pub const STRIP_OFFSETS: u16 = 273;
pub const ORIENTATION: u16 = 274;
pub const SAMPLES_PER_PIXEL: u16 = 277;
pub const ROWS_PER_STRIP: u16 = 278;
pub const STRIP_BYTE_COUNTS: u16 = 279;
pub const X_RESOLUTION: u16 = 282;
pub const Y_RESOLUTION: u16 = 283;
pub const PLANAR_CONFIGURATION: u16 = 284;
pub const RESOLUTION_UNIT: u16 = 296;
pub const SOFTWARE: u16 = 305;
pub const DATE_TIME: u16 = 306;
pub const ARTIST: u16 = 315;
pub const PREDICTOR: u16 = 317;
pub const TILE_WIDTH: u16 = 322;
pub const TILE_LENGTH: u16 = 323;
pub const TILE_OFFSETS: u16 = 324;
pub const TILE_BYTE_COUNTS: u16 = 325;
pub const SUB_IFDS: u16 = 330;
pub const EXTRA_SAMPLES: u16 = 338;
pub const SAMPLE_FORMAT: u16 = 339;
pub const JPEG_INTERCHANGE_FORMAT: u16 = 513;
pub const JPEG_INTERCHANGE_FORMAT_LENGTH: u16 = 514;
pub const YCBCR_SUBSAMPLING: u16 = 530;
pub const XMP: u16 = 700;
pub const RATING: u16 = 18246;
pub const RATING_PERCENT: u16 = 18249;
pub const CFA_REPEAT_PATTERN_DIM: u16 = 33421;
pub const CFA_PATTERN_EP: u16 = 33422;
pub const COPYRIGHT: u16 = 33432;
pub const EXPOSURE_TIME_EP: u16 = 33434;
pub const IPTC_NAA: u16 = 33723;
pub const EXIF_IFD: u16 = 34665;
pub const ICC_PROFILE: u16 = 34675;
pub const GPS_IFD: u16 = 34853;
pub const INTEROP_IFD: u16 = 40965;
// --- Exif private IFD
pub const EXPOSURE_TIME: u16 = 33434;
pub const F_NUMBER: u16 = 33437;
pub const EXPOSURE_PROGRAM: u16 = 34850;
pub const ISO_SPEED: u16 = 34855;
pub const SENSITIVITY_TYPE: u16 = 34864;
pub const RECOMMENDED_EXPOSURE_INDEX: u16 = 34866;
pub const ISO_SPEED_RATINGS_EXT: u16 = 34867;
pub const EXIF_VERSION: u16 = 36864;
pub const DATE_TIME_ORIGINAL: u16 = 36867;
pub const DATE_TIME_DIGITIZED: u16 = 36868;
pub const OFFSET_TIME: u16 = 36880;
pub const OFFSET_TIME_ORIGINAL: u16 = 36881;
pub const OFFSET_TIME_DIGITIZED: u16 = 36882;
pub const SHUTTER_SPEED_VALUE: u16 = 37377;
pub const APERTURE_VALUE: u16 = 37378;
pub const EXPOSURE_BIAS: u16 = 37380;
pub const MAX_APERTURE: u16 = 37381;
pub const METERING_MODE: u16 = 37383;
pub const LIGHT_SOURCE: u16 = 37384;
pub const FLASH: u16 = 37385;
pub const FOCAL_LENGTH: u16 = 37386;
pub const MAKER_NOTE: u16 = 37500;
pub const USER_COMMENT: u16 = 37510;
pub const SUBSEC_TIME: u16 = 37520;
pub const SUBSEC_TIME_ORIGINAL: u16 = 37521;
pub const SUBSEC_TIME_DIGITIZED: u16 = 37522;
pub const PIXEL_X_DIMENSION: u16 = 40962;
pub const PIXEL_Y_DIMENSION: u16 = 40963;
pub const FOCAL_PLANE_X_RESOLUTION: u16 = 41486;
pub const EXPOSURE_MODE: u16 = 41986;
pub const WHITE_BALANCE: u16 = 41987;
pub const FOCAL_LENGTH_35MM: u16 = 41989;
pub const SCENE_CAPTURE_TYPE: u16 = 41990;
pub const BODY_SERIAL_NUMBER: u16 = 42033;
pub const LENS_SPECIFICATION: u16 = 42034;
pub const LENS_MAKE: u16 = 42035;
pub const LENS_MODEL: u16 = 42036;
pub const LENS_SERIAL_NUMBER: u16 = 42037;
// --- GPS IFD
pub const GPS_VERSION_ID: u16 = 0;
pub const GPS_LATITUDE_REF: u16 = 1;
pub const GPS_LATITUDE: u16 = 2;
pub const GPS_LONGITUDE_REF: u16 = 3;
pub const GPS_LONGITUDE: u16 = 4;
pub const GPS_ALTITUDE_REF: u16 = 5;
pub const GPS_ALTITUDE: u16 = 6;
pub const GPS_TIME_STAMP: u16 = 7;
pub const GPS_IMG_DIRECTION_REF: u16 = 16;
pub const GPS_IMG_DIRECTION: u16 = 17;
pub const GPS_DATE_STAMP: u16 = 29;
// --- DNG
pub const DNG_VERSION: u16 = 50706;
pub const DNG_BACKWARD_VERSION: u16 = 50707;
pub const UNIQUE_CAMERA_MODEL: u16 = 50708;
pub const LOCALIZED_CAMERA_MODEL: u16 = 50709;
pub const CFA_PLANE_COLOR: u16 = 50710;
pub const CFA_LAYOUT: u16 = 50711;
pub const LINEARIZATION_TABLE: u16 = 50712;
pub const BLACK_LEVEL_REPEAT_DIM: u16 = 50713;
pub const BLACK_LEVEL: u16 = 50714;
pub const BLACK_LEVEL_DELTA_H: u16 = 50715;
pub const BLACK_LEVEL_DELTA_V: u16 = 50716;
pub const WHITE_LEVEL: u16 = 50717;
pub const DEFAULT_SCALE: u16 = 50718;
pub const DEFAULT_CROP_ORIGIN: u16 = 50719;
pub const DEFAULT_CROP_SIZE: u16 = 50720;
pub const COLOR_MATRIX_1: u16 = 50721;
pub const COLOR_MATRIX_2: u16 = 50722;
pub const CAMERA_CALIBRATION_1: u16 = 50723;
pub const CAMERA_CALIBRATION_2: u16 = 50724;
pub const REDUCTION_MATRIX_1: u16 = 50725;
pub const REDUCTION_MATRIX_2: u16 = 50726;
pub const ANALOG_BALANCE: u16 = 50727;
pub const AS_SHOT_NEUTRAL: u16 = 50728;
pub const AS_SHOT_WHITE_XY: u16 = 50729;
pub const BASELINE_EXPOSURE: u16 = 50730;
pub const BASELINE_NOISE: u16 = 50731;
pub const BASELINE_SHARPNESS: u16 = 50732;
pub const BAYER_GREEN_SPLIT: u16 = 50733;
pub const LINEAR_RESPONSE_LIMIT: u16 = 50734;
pub const CAMERA_SERIAL_NUMBER: u16 = 50735;
pub const LENS_INFO: u16 = 50736;
pub const CHROMA_BLUR_RADIUS: u16 = 50737;
pub const ANTI_ALIAS_STRENGTH: u16 = 50738;
pub const SHADOW_SCALE: u16 = 50739;
pub const DNG_PRIVATE_DATA: u16 = 50740;
pub const MAKER_NOTE_SAFETY: u16 = 50741;
pub const CALIBRATION_ILLUMINANT_1: u16 = 50778;
pub const CALIBRATION_ILLUMINANT_2: u16 = 50779;
pub const BEST_QUALITY_SCALE: u16 = 50780;
pub const RAW_DATA_UNIQUE_ID: u16 = 50781;
pub const ORIGINAL_RAW_FILE_NAME: u16 = 50827;
pub const ACTIVE_AREA: u16 = 50829;
pub const MASKED_AREAS: u16 = 50830;
pub const AS_SHOT_ICC_PROFILE: u16 = 50831;
pub const COLORIMETRIC_REFERENCE: u16 = 50879;
pub const CAMERA_CALIBRATION_SIGNATURE: u16 = 50931;
pub const PROFILE_CALIBRATION_SIGNATURE: u16 = 50932;
pub const AS_SHOT_PROFILE_NAME: u16 = 50934;
pub const PROFILE_NAME: u16 = 50936;
pub const PROFILE_HUE_SAT_MAP_DIMS: u16 = 50937;
pub const PROFILE_HUE_SAT_MAP_DATA_1: u16 = 50938;
pub const PROFILE_HUE_SAT_MAP_DATA_2: u16 = 50939;
pub const PROFILE_TONE_CURVE: u16 = 50940;
pub const PROFILE_EMBED_POLICY: u16 = 50941;
pub const PROFILE_LOOK_TABLE_DIMS: u16 = 50981;
pub const PROFILE_LOOK_TABLE_DATA: u16 = 50982;
pub const PROFILE_HUE_SAT_MAP_ENCODING: u16 = 51107;
pub const PROFILE_LOOK_TABLE_ENCODING: u16 = 51108;
pub const BASELINE_EXPOSURE_OFFSET: u16 = 51109;
pub const DEFAULT_BLACK_RENDER: u16 = 51110;
pub const FORWARD_MATRIX_1: u16 = 50964;
pub const FORWARD_MATRIX_2: u16 = 50965;
pub const PREVIEW_COLOR_SPACE: u16 = 50970;
pub const CURRENT_PREVIEW_DATE_TIME: u16 = 50971;
pub const OPCODE_LIST_1: u16 = 51008;
pub const OPCODE_LIST_2: u16 = 51009;
pub const OPCODE_LIST_3: u16 = 51022;
pub const NOISE_PROFILE: u16 = 51041;
pub const DEFAULT_USER_CROP: u16 = 51125;
pub const PROFILE_GAIN_TABLE_MAP: u16 = 52525;
/// DNG 1.7 row interleave: the stored raster holds this many interleaved row groups (each group is
/// every `n`-th row of the image), so it must be un-interleaved before the data is a mosaic.
pub const ROW_INTERLEAVE_FACTOR: u16 = 50975;
/// DNG 1.7 column interleave, the column-wise counterpart of [`ROW_INTERLEAVE_FACTOR`].
pub const COLUMN_INTERLEAVE_FACTOR: u16 = 52547;
pub const CALIBRATION_ILLUMINANT_3: u16 = 52529;
pub const CAMERA_CALIBRATION_3: u16 = 52530;
pub const COLOR_MATRIX_3: u16 = 52531;
pub const FORWARD_MATRIX_3: u16 = 52532;
pub const ILLUMINANT_DATA_1: u16 = 52533;
pub const ILLUMINANT_DATA_2: u16 = 52534;
pub const ILLUMINANT_DATA_3: u16 = 52535;
// DNG 1.6 semantic masks (spec chapter 4, "Semantic Masks")
pub const SEMANTIC_NAME: u16 = 52526;
pub const SEMANTIC_INSTANCE_ID: u16 = 52528;
pub const MASK_SUB_AREA: u16 = 52536;
/// NewSubFileType of a semantic mask IFD.
pub const SUBFILE_SEMANTIC_MASK: u32 = 0x10004;
pub const PROFILE_GAIN_TABLE_MAP_2: u16 = 52544;
// --- Compression codes (TIFF 6.0, DNG, and widely used registrations)
pub mod compression {
pub const NONE: u16 = 1;
pub const LZW: u16 = 5;
pub const OLD_JPEG: u16 = 6;
pub const JPEG: u16 = 7;
pub const ADOBE_DEFLATE: u16 = 8;
pub const DEFLATE: u16 = 32946;
pub const PACKBITS: u16 = 32773;
/// DNG lossy JPEG (baseline DCT, 8-bit).
pub const LOSSY_JPEG: u16 = 34892;
pub const JPEG_XL: u16 = 52546;
/// SMPTE ST 2073 (VC-5) as written by GoPro's GPR files (the value measured in them).
pub const VC5: u16 = 9;
/// Nikon NEF Huffman compression.
pub const NIKON: u16 = 34713;
/// Sony ARW compressed (the value used by ARW files).
pub const SONY_ARW: u16 = 32767;
}
// --- PhotometricInterpretation
pub mod photometric {
pub const WHITE_IS_ZERO: u16 = 0;
pub const BLACK_IS_ZERO: u16 = 1;
pub const RGB: u16 = 2;
pub const YCBCR: u16 = 6;
pub const CFA: u16 = 32803;
pub const LINEAR_RAW: u16 = 34892;
/// DNG 1.6 semantic mask.
pub const MASK: u16 = 52527;
}
+293
View File
@@ -0,0 +1,293 @@
//! TIFF writer: IFD trees with strips / tiles, `SubIFDs` and pointer IFDs (Exif, GPS, Interop, …).
//!
//! Layout: header, then for each IFD of the main chain its image chunks, child IFDs, out-of-line values
//! and finally the IFD itself (all word-aligned). Children are written before their parent so every
//! pointer is known when the parent's entries are emitted; only the chain's next-IFD pointers are patched.
use crate::{ByteOrder, Result, TiffError, Value, tags};
use std::collections::BTreeMap;
/// Pixel data attached to an IFD. The writer fills in the offset / byte-count tags.
#[derive(Clone, Debug, PartialEq)]
pub enum ImageData {
/// Strips (each already compressed as the IFD's `Compression` says). Sets `RowsPerStrip`.
Strips { rows_per_strip: u32, strips: Vec<Vec<u8>> },
/// Tiles in row-major tile order. Sets `TileWidth` / `TileLength`.
Tiles { tile_width: u32, tile_height: u32, tiles: Vec<Vec<u8>> },
}
/// An IFD to be written.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct IfdBuilder {
pub entries: BTreeMap<u16, Value>,
/// Written and referenced through the `SubIFDs` (330) tag.
pub sub_ifds: Vec<IfdBuilder>,
/// Pointer IFDs keyed by the pointer tag (e.g. [`tags::EXIF_IFD`], [`tags::GPS_IFD`], [`tags::INTEROP_IFD`]).
pub children: BTreeMap<u16, IfdBuilder>,
pub image: Option<ImageData>,
}
impl IfdBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, tag: u16, value: Value) -> &mut Self {
self.entries.insert(tag, value);
self
}
/// Builder-style [`IfdBuilder::set`].
pub fn with(mut self, tag: u16, value: Value) -> Self {
self.entries.insert(tag, value);
self
}
pub fn set_child(&mut self, tag: u16, child: IfdBuilder) -> &mut Self {
self.children.insert(tag, child);
self
}
pub fn add_sub_ifd(&mut self, sub: IfdBuilder) -> &mut Self {
self.sub_ifds.push(sub);
self
}
pub fn set_image(&mut self, image: ImageData) -> &mut Self {
self.image = Some(image);
self
}
}
/// Serialises IFD chains.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TiffWriter {
pub order: ByteOrder,
pub bigtiff: bool,
}
impl TiffWriter {
pub fn new(order: ByteOrder, bigtiff: bool) -> Self {
Self { order, bigtiff }
}
/// Write `chain` (IFD0, IFD1, …) as a complete TIFF stream.
pub fn write(&self, chain: &[IfdBuilder]) -> Result<Vec<u8>> {
if chain.is_empty() {
return Err(TiffError::Invalid("no IFDs to write".into()));
}
let o = self.order;
let mut buf = Vec::new();
buf.extend_from_slice(match o {
ByteOrder::Little => b"II",
ByteOrder::Big => b"MM",
});
let mut patch = if self.bigtiff {
o.put_u16(&mut buf, 43);
o.put_u16(&mut buf, 8);
o.put_u16(&mut buf, 0);
let p = buf.len();
o.put_u64(&mut buf, 0);
p
} else {
o.put_u16(&mut buf, 42);
let p = buf.len();
o.put_u32(&mut buf, 0);
p
};
for ifd in chain {
let (off, next_pos) = self.write_ifd(&mut buf, ifd, 0)?;
self.patch_offset(&mut buf, patch, off)?;
patch = next_pos;
}
Ok(buf)
}
fn patch_offset(&self, buf: &mut [u8], pos: usize, v: u64) -> Result<()> {
let mut tmp = Vec::with_capacity(8);
if self.bigtiff {
self.order.put_u64(&mut tmp, v);
} else {
self.order.put_u32(&mut tmp, u32::try_from(v).map_err(|_| TiffError::Limit("classic TIFF larger than 4 GiB"))?);
}
buf[pos..pos + tmp.len()].copy_from_slice(&tmp);
Ok(())
}
fn offset_value(&self, v: Vec<u64>) -> Result<Value> {
if self.bigtiff {
Ok(Value::Long8(v))
} else {
Ok(Value::Long(
v.into_iter().map(|x| u32::try_from(x).map_err(|_| TiffError::Limit("classic TIFF larger than 4 GiB"))).collect::<Result<_>>()?,
))
}
}
/// Writes the IFD (and everything it references); returns (IFD offset, position of its next-IFD pointer).
fn write_ifd(&self, buf: &mut Vec<u8>, ifd: &IfdBuilder, depth: usize) -> Result<(u64, usize)> {
if depth > 16 {
return Err(TiffError::Limit("IFD nesting depth"));
}
let o = self.order;
let mut entries = ifd.entries.clone();
if let Some(img) = &ifd.image {
let chunks = match img {
ImageData::Strips { rows_per_strip, strips } => {
entries.insert(tags::ROWS_PER_STRIP, Value::Long(vec![*rows_per_strip]));
strips
}
ImageData::Tiles { tile_width, tile_height, tiles } => {
entries.insert(tags::TILE_WIDTH, Value::Long(vec![*tile_width]));
entries.insert(tags::TILE_LENGTH, Value::Long(vec![*tile_height]));
tiles
}
};
let mut offs = Vec::with_capacity(chunks.len());
let mut counts = Vec::with_capacity(chunks.len());
for c in chunks {
align(buf);
offs.push(buf.len() as u64);
counts.push(c.len() as u64);
buf.extend_from_slice(c);
}
let (ot, ct) = match img {
ImageData::Strips { .. } => (tags::STRIP_OFFSETS, tags::STRIP_BYTE_COUNTS),
ImageData::Tiles { .. } => (tags::TILE_OFFSETS, tags::TILE_BYTE_COUNTS),
};
entries.insert(ot, self.offset_value(offs)?);
entries.insert(ct, self.offset_value(counts)?);
}
if !ifd.sub_ifds.is_empty() {
let mut offs = Vec::new();
for s in &ifd.sub_ifds {
offs.push(self.write_ifd(buf, s, depth + 1)?.0);
}
entries.insert(tags::SUB_IFDS, self.offset_value(offs)?);
}
for (&tag, child) in &ifd.children {
let off = self.write_ifd(buf, child, depth + 1)?.0;
entries.insert(tag, self.offset_value(vec![off])?);
}
let inline = if self.bigtiff { 8 } else { 4 };
// out-of-line values
let mut encoded: Vec<(u16, &Value, Vec<u8>, Option<u64>)> = Vec::with_capacity(entries.len());
for (&tag, v) in &entries {
let bytes = encode(o, v);
let pos = if bytes.len() > inline {
align(buf);
let p = buf.len() as u64;
buf.extend_from_slice(&bytes);
Some(p)
} else {
None
};
encoded.push((tag, v, bytes, pos));
}
align(buf);
let ifd_off = buf.len() as u64;
if !self.bigtiff && ifd_off > u32::MAX as u64 {
return Err(TiffError::Limit("classic TIFF larger than 4 GiB"));
}
if self.bigtiff {
o.put_u64(buf, encoded.len() as u64);
} else {
o.put_u16(buf, u16::try_from(encoded.len()).map_err(|_| TiffError::Limit("more than 65535 entries"))?);
}
for (tag, v, bytes, pos) in &encoded {
o.put_u16(buf, *tag);
o.put_u16(buf, v.field_type() as u16);
if self.bigtiff {
o.put_u64(buf, v.count() as u64);
} else {
o.put_u32(buf, u32::try_from(v.count()).map_err(|_| TiffError::Limit("value count"))?);
}
match pos {
Some(p) => {
if self.bigtiff {
o.put_u64(buf, *p);
} else {
o.put_u32(buf, u32::try_from(*p).map_err(|_| TiffError::Limit("classic TIFF larger than 4 GiB"))?);
}
}
None => {
buf.extend_from_slice(bytes);
buf.extend(std::iter::repeat_n(0u8, inline - bytes.len()));
}
}
}
let next_pos = buf.len();
if self.bigtiff {
o.put_u64(buf, 0);
} else {
o.put_u32(buf, 0);
}
Ok((ifd_off, next_pos))
}
}
fn align(buf: &mut Vec<u8>) {
if buf.len() % 2 == 1 {
buf.push(0);
}
}
/// Serialise a value's bytes in `o` order.
pub fn encode(o: ByteOrder, v: &Value) -> Vec<u8> {
let mut b = Vec::new();
match v {
Value::Byte(x) | Value::Undefined(x) => b.extend_from_slice(x),
Value::Ascii(s) => {
b.extend_from_slice(s.as_bytes());
b.push(0);
}
Value::SByte(x) => b.extend(x.iter().map(|&c| c as u8)),
Value::Short(x) => x.iter().for_each(|&c| o.put_u16(&mut b, c)),
Value::SShort(x) => x.iter().for_each(|&c| o.put_u16(&mut b, c as u16)),
Value::Long(x) | Value::Ifd(x) => x.iter().for_each(|&c| o.put_u32(&mut b, c)),
Value::SLong(x) => x.iter().for_each(|&c| o.put_u32(&mut b, c as u32)),
Value::Float(x) => x.iter().for_each(|&c| o.put_u32(&mut b, c.to_bits())),
Value::Rational(x) => x.iter().for_each(|&(n, d)| {
o.put_u32(&mut b, n);
o.put_u32(&mut b, d);
}),
Value::SRational(x) => x.iter().for_each(|&(n, d)| {
o.put_u32(&mut b, n as u32);
o.put_u32(&mut b, d as u32);
}),
Value::Double(x) => x.iter().for_each(|&c| o.put_u64(&mut b, c.to_bits())),
Value::Long8(x) | Value::Ifd8(x) => x.iter().for_each(|&c| o.put_u64(&mut b, c)),
Value::SLong8(x) => x.iter().for_each(|&c| o.put_u64(&mut b, c as u64)),
}
b
}
/// Convenience: approximate `v` by an unsigned rational with denominator up to 1e6 (for writing Exif rationals).
pub fn rational(v: f64) -> (u32, u32) {
if !v.is_finite() || v <= 0.0 {
return (0, 1);
}
for d in [1u32, 10, 100, 1000, 10_000, 100_000, 1_000_000] {
let n = v * d as f64;
if n > u32::MAX as f64 {
return ((v * (d / 10).max(1) as f64).round().min(u32::MAX as f64) as u32, (d / 10).max(1));
}
if (n - n.round()).abs() < 1e-9 * d as f64 || d == 1_000_000 {
return (n.round() as u32, d);
}
}
(0, 1)
}
/// Signed variant of [`rational`].
pub fn srational(v: f64) -> (i32, i32) {
if !v.is_finite() {
return (0, 1);
}
for d in [1i32, 10, 100, 1000, 10_000, 100_000, 1_000_000] {
let n = v * d as f64;
if n.abs() > i32::MAX as f64 {
let d = (d / 10).max(1);
return ((v * d as f64).round().clamp(i32::MIN as f64, i32::MAX as f64) as i32, d);
}
if (n - n.round()).abs() < 1e-9 * d as f64 || d == 1_000_000 {
return (n.round() as i32, d);
}
}
(0, 1)
}
+3
View File
@@ -0,0 +1,3 @@
//! The integration tests, one module per file, linked into one test binary.
mod roundtrip;
+429
View File
@@ -0,0 +1,429 @@
use craft_tiff::image::{Layout, chunk_bytes};
use craft_tiff::writer::{rational, srational};
use craft_tiff::{ByteOrder, FieldType, IfdBuilder, ImageData, ParseOptions, Tiff, TiffError, TiffWriter, Value, parse_ifd_at, tags};
use proptest::prelude::*;
const ORDERS: [ByteOrder; 2] = [ByteOrder::Little, ByteOrder::Big];
fn all_values() -> Vec<(u16, Value)> {
vec![
(1000, Value::Byte(vec![1, 2, 3, 255])),
(1001, Value::Ascii("hello tiff".into())),
(1002, Value::Short(vec![1, 65535, 3])),
(1003, Value::Long(vec![7])),
(1004, Value::Rational(vec![(1, 3), (22, 7)])),
(1005, Value::SByte(vec![-1, 0, 127, -128])),
(1006, Value::Undefined(vec![9; 17])),
(1007, Value::SShort(vec![-32768, 32767])),
(1008, Value::SLong(vec![-5, i32::MAX])),
(1009, Value::SRational(vec![(-1, 3), (5, -2)])),
(1010, Value::Float(vec![1.5, -0.25, f32::MAX])),
(1011, Value::Double(vec![std::f64::consts::PI])),
(1012, Value::Ifd(vec![0])),
(1013, Value::Short(vec![42])),
(1014, Value::Ascii(String::new())),
]
}
fn big_values() -> Vec<(u16, Value)> {
vec![(1020, Value::Long8(vec![1 << 40, 3])), (1021, Value::SLong8(vec![-(1 << 50)])), (1022, Value::Ifd8(vec![0]))]
}
#[test]
fn all_field_types_roundtrip_every_variant() {
for order in ORDERS {
for big in [false, true] {
let mut ifd = IfdBuilder::new();
for (t, v) in all_values() {
ifd.set(t, v);
}
if big {
for (t, v) in big_values() {
ifd.set(t, v);
}
}
let bytes = TiffWriter::new(order, big).write(&[ifd]).unwrap();
let t = Tiff::parse(&bytes).unwrap();
assert_eq!(t.order, order);
assert_eq!(t.bigtiff, big);
assert_eq!(Tiff::sniff(&bytes), Some((order, big)));
let ifd0 = &t.ifds[0];
for (tag, v) in all_values().into_iter().chain(if big { big_values() } else { vec![] }) {
assert_eq!(ifd0.value(tag), Some(&v), "tag {tag} order {order:?} big {big}");
}
}
}
}
#[test]
fn accessors() {
let mut ifd = IfdBuilder::new();
ifd.set(1, Value::Rational(vec![(28, 10), (0, 0)]));
ifd.set(2, Value::Short(vec![5, 6]));
ifd.set(3, Value::SLong(vec![-3]));
ifd.set(4, Value::Ascii("Canon ".into()));
ifd.set(5, Value::Byte(b"<x:xmpmeta/>\0junk".to_vec()));
let bytes = TiffWriter::default().write(&[ifd]).unwrap();
let t = Tiff::parse(&bytes).unwrap();
let i = &t.ifds[0];
assert_eq!(i.f64(1), Some(2.8));
assert_eq!(i.f64s(1).unwrap(), vec![2.8, 0.0]);
assert_eq!(i.u32(2), Some(5));
assert_eq!(i.u64s(2).unwrap(), vec![5, 6]);
assert_eq!(i.u32(3), None);
assert_eq!(i.i64(3), Some(-3));
assert_eq!(i.string(4).as_deref(), Some("Canon"));
assert_eq!(i.string(5).as_deref(), Some("<x:xmpmeta/>"));
assert_eq!(i.get(4).unwrap().field_type(), FieldType::Ascii);
assert!(i.u32(999).is_none());
assert_eq!(t.find(2).map(|e| e.tag), Some(2));
}
#[test]
fn chain_subifds_and_pointer_ifds() {
for order in ORDERS {
for big in [false, true] {
let mut interop = IfdBuilder::new();
interop.set(1, Value::Ascii("R98".into()));
let mut exif = IfdBuilder::new();
exif.set(tags::F_NUMBER, Value::Rational(vec![(56, 10)]));
exif.set_child(tags::INTEROP_IFD, interop);
let mut gps = IfdBuilder::new();
gps.set(tags::GPS_LATITUDE_REF, Value::Ascii("N".into()));
let mut ifd0 = IfdBuilder::new();
ifd0.set(tags::MAKE, Value::Ascii("Maker".into()));
ifd0.set_child(tags::EXIF_IFD, exif);
ifd0.set_child(tags::GPS_IFD, gps);
let mut raw = IfdBuilder::new();
raw.set(tags::IMAGE_WIDTH, Value::Long(vec![4]));
let mut nested = IfdBuilder::new();
nested.set(tags::IMAGE_WIDTH, Value::Long(vec![2]));
raw.add_sub_ifd(nested);
ifd0.add_sub_ifd(raw);
ifd0.add_sub_ifd(IfdBuilder::new().with(tags::IMAGE_WIDTH, Value::Short(vec![3])));
let ifd1 = IfdBuilder::new().with(tags::IMAGE_WIDTH, Value::Short(vec![160]));
let ifd2 = IfdBuilder::new().with(tags::IMAGE_WIDTH, Value::Short(vec![80]));
let bytes = TiffWriter::new(order, big).write(&[ifd0, ifd1, ifd2]).unwrap();
let t = Tiff::parse(&bytes).unwrap();
assert_eq!(t.ifds.len(), 3);
assert_eq!(t.ifds[1].u32(tags::IMAGE_WIDTH), Some(160));
assert_eq!(t.ifds[2].u32(tags::IMAGE_WIDTH), Some(80));
let i0 = &t.ifds[0];
assert_eq!(i0.sub_ifds.len(), 2);
assert_eq!(i0.sub_ifds[0].u32(tags::IMAGE_WIDTH), Some(4));
assert_eq!(i0.sub_ifds[0].sub_ifds[0].u32(tags::IMAGE_WIDTH), Some(2));
assert_eq!(i0.sub_ifds[1].u32(tags::IMAGE_WIDTH), Some(3));
let exif = t.exif().unwrap();
assert_eq!(exif.f64(tags::F_NUMBER), Some(5.6));
assert_eq!(exif.interop.as_ref().unwrap().string(1).as_deref(), Some("R98"));
assert_eq!(t.gps().unwrap().string(tags::GPS_LATITUDE_REF).as_deref(), Some("N"));
// IFD0, sub0, sub0.sub, sub1, exif, interop, gps, ifd1, ifd2
assert_eq!(t.all_ifds().len(), 9);
}
}
}
fn gradient(w: usize, h: usize) -> Vec<u8> {
(0..w * h).map(|i| (i * 7 % 251) as u8).collect()
}
#[test]
fn strips_layout_and_bytes() {
let (w, h) = (10usize, 7usize);
let px = gradient(w, h);
let rps = 3;
let strips: Vec<Vec<u8>> = px.chunks(w * rps).map(|c| c.to_vec()).collect();
let mut ifd = IfdBuilder::new();
ifd.set(tags::IMAGE_WIDTH, Value::Long(vec![w as u32]));
ifd.set(tags::IMAGE_LENGTH, Value::Long(vec![h as u32]));
ifd.set(tags::BITS_PER_SAMPLE, Value::Short(vec![8]));
ifd.set(tags::COMPRESSION, Value::Short(vec![1]));
ifd.set_image(ImageData::Strips { rows_per_strip: rps as u32, strips });
for order in ORDERS {
for big in [false, true] {
let bytes = TiffWriter::new(order, big).write(std::slice::from_ref(&ifd)).unwrap();
let t = Tiff::parse(&bytes).unwrap();
let info = t.ifds[0].image().unwrap();
assert_eq!(info.layout, Layout::Strips { rows_per_strip: 3 });
assert_eq!(info.grid(), (1, 3));
let chunks = info.chunks(bytes.len() as u64);
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[2].height, 1);
let mut out = Vec::new();
for c in &chunks {
out.extend_from_slice(chunk_bytes(&bytes, c).unwrap());
}
assert_eq!(out, px);
}
}
}
#[test]
fn tiles_layout_and_bytes() {
let (w, h, tw, th) = (20usize, 13usize, 16usize, 8usize);
let px = gradient(w, h);
let (ta, td) = (w.div_ceil(tw), h.div_ceil(th));
let mut tiles = Vec::new();
for ty in 0..td {
for tx in 0..ta {
let mut t = vec![0u8; tw * th];
for y in 0..th {
for x in 0..tw {
let (sx, sy) = (tx * tw + x, ty * th + y);
if sx < w && sy < h {
t[y * tw + x] = px[sy * w + sx];
}
}
}
tiles.push(t);
}
}
let mut ifd = IfdBuilder::new();
ifd.set(tags::IMAGE_WIDTH, Value::Short(vec![w as u16]));
ifd.set(tags::IMAGE_LENGTH, Value::Short(vec![h as u16]));
ifd.set_image(ImageData::Tiles { tile_width: tw as u32, tile_height: th as u32, tiles });
let bytes = TiffWriter::new(ByteOrder::Big, false).write(&[ifd]).unwrap();
let t = Tiff::parse(&bytes).unwrap();
let info = t.ifds[0].image().unwrap();
assert_eq!(info.grid(), (2, 2));
let chunks = info.chunks(bytes.len() as u64);
assert_eq!(chunks.len(), 4);
let mut out = vec![0u8; w * h];
for c in &chunks {
let b = chunk_bytes(&bytes, c).unwrap();
for y in 0..c.height as usize {
for x in 0..c.width as usize {
let (sx, sy) = (c.x as usize + x, c.y as usize + y);
if sx < w && sy < h {
out[sy * w + sx] = b[y * tw + x];
}
}
}
}
assert_eq!(out, px);
}
#[test]
fn missing_byte_counts_are_inferred() {
let mut ifd = IfdBuilder::new();
ifd.set(tags::IMAGE_WIDTH, Value::Long(vec![4]));
ifd.set(tags::IMAGE_LENGTH, Value::Long(vec![2]));
ifd.set_image(ImageData::Strips { rows_per_strip: 1, strips: vec![vec![1; 4], vec![2; 4]] });
let bytes = TiffWriter::default().write(&[ifd]).unwrap();
let t = Tiff::parse(&bytes).unwrap();
let mut info = t.ifds[0].image().unwrap();
info.byte_counts.clear();
let c = info.chunks(bytes.len() as u64);
assert_eq!(c[0].len, 4);
assert_eq!(&chunk_bytes(&bytes, &c[0]).unwrap()[..4], &[1; 4]);
assert!(c[1].len >= 4);
}
#[test]
fn bad_headers() {
assert_eq!(Tiff::parse(b""), Err(TiffError::NotTiff));
assert_eq!(Tiff::parse(b"XX*\0\0\0\0\0"), Err(TiffError::NotTiff));
assert_eq!(Tiff::parse(b"II\x2b\0\x04\0\0\0"), Err(TiffError::NotTiff));
assert!(Tiff::parse(b"II*\0").is_err());
assert!(Tiff::parse(b"II*\0\x08\0\0\0").is_err());
assert!(Tiff::parse(b"II*\0\x08\0\0\0\0\0").is_err());
assert!(Tiff::sniff(b"MM\0*\0\0\0\x08").is_some());
}
#[test]
fn ifd_loops_are_detected() {
// IFD0 at 8 whose next pointer points back to itself, and whose SubIFDs point to itself too.
let mut b = b"II*\0\x08\0\0\0".to_vec();
b.extend_from_slice(&1u16.to_le_bytes());
b.extend_from_slice(&330u16.to_le_bytes());
b.extend_from_slice(&4u16.to_le_bytes());
b.extend_from_slice(&1u32.to_le_bytes());
b.extend_from_slice(&8u32.to_le_bytes());
b.extend_from_slice(&8u32.to_le_bytes());
let t = Tiff::parse(&b).unwrap();
assert_eq!(t.ifds.len(), 1);
assert!(t.ifds[0].sub_ifds.is_empty());
}
#[test]
fn out_of_range_entries_are_skipped() {
let mut ifd = IfdBuilder::new();
ifd.set(1, Value::Long(vec![1, 2, 3]));
ifd.set(2, Value::Short(vec![5]));
let mut bytes = TiffWriter::default().write(&[ifd]).unwrap();
let t = Tiff::parse(&bytes).unwrap();
let e = t.ifds[0].get(1).unwrap().clone();
// point tag 1's value offset past EOF
let ifd_off = t.ifds[0].offset as usize;
let field = ifd_off + 2 + 8;
assert_eq!(u32::from_le_bytes(bytes[field..field + 4].try_into().unwrap()) as u64, e.offset);
bytes[field..field + 4].copy_from_slice(&0xffff_fff0u32.to_le_bytes());
let t = Tiff::parse(&bytes).unwrap();
assert!(t.ifds[0].get(1).is_none());
assert_eq!(t.ifds[0].u32(2), Some(5));
}
#[test]
fn value_budget_limits_allocation() {
// 50 entries all pointing at the same 1000-byte blob.
let mut ifd = IfdBuilder::new();
ifd.set(1, Value::Undefined(vec![7; 1000]));
let bytes = TiffWriter::default().write(&[ifd]).unwrap();
let t = Tiff::parse(&bytes).unwrap();
let blob = t.ifds[0].get(1).unwrap().offset as u32;
let mut b = b"II*\0".to_vec();
b.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
let mut file = bytes.clone();
file[..8].copy_from_slice(&b);
file.extend_from_slice(&50u16.to_le_bytes());
for i in 0..50u16 {
file.extend_from_slice(&i.to_le_bytes());
file.extend_from_slice(&7u16.to_le_bytes());
file.extend_from_slice(&1000u32.to_le_bytes());
file.extend_from_slice(&blob.to_le_bytes());
}
file.extend_from_slice(&0u32.to_le_bytes());
let opts = ParseOptions { value_budget: Some(10_000), ..Default::default() };
let t = Tiff::parse_with(&file, &opts).unwrap();
assert_eq!(t.ifds[0].entries.len(), 10);
let t = Tiff::parse(&file).unwrap();
assert_eq!(t.ifds[0].entries.len(), 50);
}
#[test]
fn parse_ifd_with_base() {
// an IFD blob embedded at 100 whose offsets are relative to 100
let mut blob = Vec::new();
let o = ByteOrder::Big;
o.put_u16(&mut blob, 1);
o.put_u16(&mut blob, 5);
o.put_u16(&mut blob, 4);
o.put_u32(&mut blob, 3);
o.put_u32(&mut blob, 18);
o.put_u32(&mut blob, 0);
for v in [10u32, 20, 30] {
o.put_u32(&mut blob, v);
}
let mut data = vec![0u8; 100];
data.extend(blob);
let (ifd, next) = parse_ifd_at(&data, 100, o, 100, false, &ParseOptions::default()).unwrap();
assert_eq!(next, 0);
assert_eq!(ifd.u64s(5).unwrap(), vec![10, 20, 30]);
assert_eq!(ifd.get(5).unwrap().offset, 118);
}
#[test]
fn rationals() {
assert_eq!(rational(2.8), (28, 10));
assert_eq!(rational(0.004), (4, 1000));
assert_eq!(rational(-1.0), (0, 1));
assert_eq!(srational(-0.333), (-333, 1000));
assert_eq!(srational(7.0), (7, 1));
let (n, d) = rational(1.0 / 3.0);
assert!((n as f64 / d as f64 - 1.0 / 3.0).abs() < 1e-6);
}
fn sample_file() -> Vec<u8> {
let mut exif = IfdBuilder::new();
exif.set(tags::F_NUMBER, Value::Rational(vec![(4, 1)]));
exif.set(tags::MAKER_NOTE, Value::Undefined(b"Nikon\0\x02\x10\0\0II*\0\x08\0\0\0\x01\0\x01\0\x03\0\x01\0\0\0\x05\0\0\0\0\0\0\0".to_vec()));
let mut ifd0 = IfdBuilder::new();
ifd0.set(tags::IMAGE_WIDTH, Value::Long(vec![8]));
ifd0.set(tags::IMAGE_LENGTH, Value::Long(vec![4]));
ifd0.set(tags::MAKE, Value::Ascii("NIKON".into()));
ifd0.set_child(tags::EXIF_IFD, exif);
ifd0.set_image(ImageData::Strips { rows_per_strip: 2, strips: vec![vec![1; 16], vec![2; 16]] });
ifd0.add_sub_ifd(IfdBuilder::new().with(tags::IMAGE_WIDTH, Value::Long(vec![2])));
TiffWriter::default().write(&[ifd0, IfdBuilder::new().with(1, Value::Short(vec![1]))]).unwrap()
}
fn exercise(t: &Tiff, data: &[u8]) {
for ifd in t.all_ifds() {
if let Ok(info) = ifd.image() {
for c in info.chunks(data.len() as u64).iter().take(64) {
let _ = chunk_bytes(data, c);
}
}
for e in &ifd.entries {
let _ = e.value.to_f64_vec();
let _ = e.value.as_str();
}
}
if let Some(e) = t.exif().and_then(|x| x.get(tags::MAKER_NOTE)) {
let _ = craft_tiff::makernote::parse_makernote(data, e.offset, e.count() as u64, t.order, "NIKON");
}
}
proptest! {
#![proptest_config(ProptestConfig { cases: 2000, .. ProptestConfig::default() })]
#[test]
fn random_bytes_never_panic(mut data in proptest::collection::vec(any::<u8>(), 0..512), hdr in 0usize..4) {
let heads: [&[u8]; 4] = [b"II*\0", b"MM\0*", b"II+\0\x08\0\0\0", b"MM\0+\0\x08\0\0"];
let h = heads[hdr];
if data.len() >= h.len() {
data[..h.len()].copy_from_slice(h);
}
if let Ok(t) = Tiff::parse(&data) {
exercise(&t, &data);
}
}
#[test]
fn mutated_files_never_panic(flips in proptest::collection::vec((any::<usize>(), any::<u8>()), 1..16), cut in any::<usize>()) {
let mut data = sample_file();
for (i, v) in flips {
let n = data.len();
data[i % n] = v;
}
let keep = if cut % 4 == 0 { cut % data.len() } else { data.len() };
let data = &data[..keep];
if let Ok(t) = Tiff::parse(data) {
exercise(&t, data);
}
}
#[test]
fn random_short_arrays_roundtrip(v in proptest::collection::vec(any::<u16>(), 1..300), big in any::<bool>(), le in any::<bool>()) {
let order = if le { ByteOrder::Little } else { ByteOrder::Big };
let ifd = IfdBuilder::new().with(500, Value::Short(v.clone())).with(501, Value::Double(v.iter().map(|&x| x as f64 / 3.0).collect()));
let bytes = TiffWriter::new(order, big).write(&[ifd]).unwrap();
let t = Tiff::parse(&bytes).unwrap();
prop_assert_eq!(t.ifds[0].value(500), Some(&Value::Short(v.clone())));
prop_assert_eq!(t.ifds[0].f64s(501).unwrap().len(), v.len());
}
}
/// A BigTIFF whose IFD says it has an enormous number of entries, with one readable, computed where the next-IFD
/// offset would be from the advertised count: an add overflow, a panic in builds with overflow checks (issue
/// #1516). The readable entry is kept and there is no next IFD; complete chains still link.
#[test]
fn bigtiff_with_an_enormous_entry_count_does_not_panic() {
for order in ORDERS {
for count in [u64::MAX, 1 << 62, 2] {
// little-endian bytes, reversed for big-endian files
let e = |le: &[u8]| -> Vec<u8> { if order == ByteOrder::Little { le.to_vec() } else { le.iter().rev().copied().collect() } };
let mut f = if order == ByteOrder::Little { b"II".to_vec() } else { b"MM".to_vec() };
for part in [e(&43u16.to_le_bytes()), e(&8u16.to_le_bytes()), e(&0u16.to_le_bytes()), e(&16u64.to_le_bytes()), e(&count.to_le_bytes())] {
f.extend(part);
}
// one entry: ImageWidth, LONG, 1 value (inline in the 8-byte field)
for part in [e(&tags::IMAGE_WIDTH.to_le_bytes()), e(&4u16.to_le_bytes()), e(&1u64.to_le_bytes()), e(&640u32.to_le_bytes())] {
f.extend(part);
}
f.extend([0; 4]);
let (ifd, next) = parse_ifd_at(&f, 16, order, 0, true, &ParseOptions::default()).unwrap();
assert_eq!(ifd.value(tags::IMAGE_WIDTH), Some(&Value::Long(vec![640])), "{order:?} {count}");
assert_eq!(next, 0, "{order:?} {count}");
let t = Tiff::parse(&f).unwrap();
assert_eq!(t.ifds.len(), 1);
}
// a complete chain of two IFDs still links
let mut a = IfdBuilder::new();
a.set(1000, Value::Short(vec![1]));
let mut b = IfdBuilder::new();
b.set(1000, Value::Short(vec![2]));
let t = Tiff::parse(&TiffWriter::new(order, true).write(&[a, b]).unwrap()).unwrap();
assert_eq!(t.ifds.len(), 2, "{order:?}");
}
}