M3.1: lightcraft-tiff reader/writer

Classic + BigTIFF, both byte orders, all field types, SubIFDs and Exif/GPS/
Interop pointer IFDs, strips/tiles layout, maker-note offset-base helpers,
IFD writer; hardened against malformed input (proptests). Placeholder
manifests for lightcraft-meta and lightcraft-raw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-09-30 11:00:48 -04:00
parent 3f1ffeb690
commit 44d97a4104
14 changed files with 2069 additions and 0 deletions

37
Cargo.lock generated
View File

@ -140,6 +140,10 @@ dependencies = [
"serde",
]
[[package]]
name = "lightcraft-meta"
version = "0.1.0"
[[package]]
name = "lightcraft-raster"
version = "0.1.0"
@ -151,6 +155,19 @@ dependencies = [
"serde",
]
[[package]]
name = "lightcraft-raw"
version = "0.1.0"
[[package]]
name = "lightcraft-tiff"
version = "0.1.0"
dependencies = [
"proptest",
"serde",
"thiserror",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@ -409,6 +426,26 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "thiserror"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
]
[[package]]
name = "unarray"
version = "0.1.4"

11
crates/meta/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "lightcraft-meta"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "LightCraft metadata: EXIF, XMP, IPTC (L1)"
[lints]
workspace = true
[dependencies]

1
crates/meta/src/lib.rs Normal file
View File

@ -0,0 +1 @@
#![forbid(unsafe_code)]

11
crates/raw/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "lightcraft-raw"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "LightCraft RAW decoding, demosaicing and DNG colour model (L1)"
[lints]
workspace = true
[dependencies]

1
crates/raw/src/lib.rs Normal file
View File

@ -0,0 +1 @@
#![forbid(unsafe_code)]

16
crates/tiff/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "lightcraft-tiff"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "LightCraft TIFF/IFD reader and writer (L0)"
[lints]
workspace = true
[dependencies]
serde = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
proptest = { workspace = true }

36
crates/tiff/README.md Normal file
View File

@ -0,0 +1,36 @@
# lightcraft-tiff (L0)
TIFF / IFD reader and writer. Used by `lightcraft-raw` (DNG, CR2, NEF, ARW…), `lightcraft-meta` (Exif) and
later the TIFF/DNG exporters.
## 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.
- `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; 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.

164
crates/tiff/src/image.rs Normal file
View File

@ -0,0 +1,164 @@
//! 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),
}
}
/// 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 (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 };
let (cw, ch) = self.chunk_size();
let n = self.offsets.len().min(per_plane.saturating_mul(planes));
let mut out = Vec::with_capacity(n);
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 = self.offsets.iter().copied().filter(|&o| o > offset).min().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])
}

466
crates/tiff/src/lib.rs Normal file
View File

@ -0,0 +1,466 @@
//! TIFF / IFD reading and writing for LightCraft.
//!
//! - [`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 and the total amount of decoded value bytes is
//! capped relative to the input size (so a hostile file cannot make us allocate unboundedly).
//!
//! 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)]
pub mod image;
pub mod makernote;
mod reader;
pub mod tags;
pub mod writer;
pub use reader::{ParseOptions, 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))
}
}

View File

@ -0,0 +1,202 @@
//! 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,
};
let opts = ParseOptions { max_ifds: 64, max_depth: 3, ..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]);
}
#[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());
}
}

236
crates/tiff/src/reader.rs Normal file
View File

@ -0,0 +1,236 @@
//! 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 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, value_budget: None, follow_children: true, accept_raw_magic: true }
}
}
/// 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,
}
impl<'a> Ctx<'a> {
fn new(data: &'a [u8], order: ByteOrder, big: bool, opts: &'a ParseOptions) -> Self {
let budget = opts.value_budget.unwrap_or((data.len() as u64).saturating_mul(8).saturating_add(1 << 20));
Self { data, order, big, opts, visited: HashSet::new(), budget }
}
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 pos = usize::try_from(offset).map_err(|_| TiffError::Truncated(offset))?;
let (n, head, entry_size, inline) = if self.big {
(self.order.read_u64(self.data, pos).ok_or(TiffError::Truncated(offset))?, 8usize, 20usize, 8usize)
} else {
(self.order.read_u16(self.data, pos).ok_or(TiffError::Truncated(offset))? as u64, 2, 12, 4)
};
let table = pos + head;
let fit = (self.data.len().saturating_sub(table) / entry_size) as u64;
let n_read = n.min(fit).min(self.opts.max_entries as u64) as usize;
if n == 0 {
return Err(TiffError::Invalid(format!("empty IFD at {offset}")));
}
if n_read == 0 {
return Err(TiffError::Truncated(offset));
}
let mut entries = Vec::with_capacity(n_read);
for i in 0..n_read {
let p = table + i * entry_size;
if let Some(e) = self.entry(p, base, inline) {
entries.push(e);
}
}
entries.sort_by_key(|e| e.tag);
entries.dedup_by_key(|e| e.tag);
let next_pos = table + (n as usize).saturating_mul(entry_size);
let next = if n as usize == n_read {
match self.read_off(next_pos) {
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, inline: usize) -> Option<Entry> {
let o = self.order;
let tag = o.read_u16(self.data, p)?;
let typ = FieldType::from_u16(o.read_u16(self.data, p + 2)?)?;
let (count, field) = if self.big { (o.read_u64(self.data, p + 4)?, p + 12) } else { (o.read_u32(self.data, p + 4)? as u64, p + 8) };
let total = count.checked_mul(typ.size() as u64)?;
let start = if total <= inline as u64 {
field as u64
} else {
let rel = self.read_off(field)?;
base.checked_add(rel)?
};
let end = start.checked_add(total)?;
if end > self.data.len() as u64 {
return None;
}
if total > self.budget {
return None;
}
self.budget -= total;
let bytes = &self.data[start as usize..end as usize];
Some(Entry { tag, value: decode(o, typ, bytes), offset: start })
}
}
/// Decode `bytes` (exactly `count × size` long) as values of type `typ`.
pub(crate) 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.chunks_exact(2).map(|c| o.u16(arr(c))).collect()),
FieldType::SShort => Value::SShort(b.chunks_exact(2).map(|c| o.u16(arr(c)) as i16).collect()),
FieldType::Long => Value::Long(b.chunks_exact(4).map(|c| o.u32(arr(c))).collect()),
FieldType::Ifd => Value::Ifd(b.chunks_exact(4).map(|c| o.u32(arr(c))).collect()),
FieldType::SLong => Value::SLong(b.chunks_exact(4).map(|c| o.u32(arr(c)) as i32).collect()),
FieldType::Float => Value::Float(b.chunks_exact(4).map(|c| f32::from_bits(o.u32(arr(c)))).collect()),
FieldType::Rational => Value::Rational(b.chunks_exact(8).map(|c| (o.u32(arr(&c[..4])), o.u32(arr(&c[4..])))).collect()),
FieldType::SRational => Value::SRational(b.chunks_exact(8).map(|c| (o.u32(arr(&c[..4])) as i32, o.u32(arr(&c[4..])) as i32)).collect()),
FieldType::Double => Value::Double(b.chunks_exact(8).map(|c| f64::from_bits(o.u64(arr(c)))).collect()),
FieldType::Long8 => Value::Long8(b.chunks_exact(8).map(|c| o.u64(arr(c))).collect()),
FieldType::Ifd8 => Value::Ifd8(b.chunks_exact(8).map(|c| o.u64(arr(c))).collect()),
FieldType::SLong8 => Value::SLong8(b.chunks_exact(8).map(|c| o.u64(arr(c)) as i64).collect()),
}
}

200
crates/tiff/src/tags.rs Normal file
View File

@ -0,0 +1,200 @@
//! 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 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 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;
// --- 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;
/// 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;
}

293
crates/tiff/src/writer.rs Normal file
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)
}

View File

@ -0,0 +1,395 @@
use lightcraft_tiff::image::{Layout, chunk_bytes};
use lightcraft_tiff::writer::{rational, srational};
use lightcraft_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 _ = lightcraft_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());
}
}