mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 14:02:16 +00:00
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:
1 parent
6e21c5b0b1
commit
3b3104981b
13 files changed
+2516
No files matched your search
Generated
+9
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 }
|
||||
@@ -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.
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
|
||||
mod roundtrip;
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user