M3.1: lightcraft-tiff reader/writer
Classic + BigTIFF, both byte orders, all field types, SubIFDs and Exif/GPS/ Interop pointer IFDs, strips/tiles layout, maker-note offset-base helpers, IFD writer; hardened against malformed input (proptests). Placeholder manifests for lightcraft-meta and lightcraft-raw. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3f1ffeb690
commit
44d97a4104
|
|
@ -140,6 +140,10 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-meta"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-raster"
|
||||
version = "0.1.0"
|
||||
|
|
@ -151,6 +155,19 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-raw"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-tiff"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
|
|
@ -409,6 +426,26 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unarray"
|
||||
version = "0.1.4"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "lightcraft-meta"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "LightCraft metadata: EXIF, XMP, IPTC (L1)"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -0,0 +1 @@
|
|||
#![forbid(unsafe_code)]
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "lightcraft-raw"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "LightCraft RAW decoding, demosaicing and DNG colour model (L1)"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -0,0 +1 @@
|
|||
#![forbid(unsafe_code)]
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "lightcraft-tiff"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "LightCraft TIFF/IFD reader and writer (L0)"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = { workspace = true }
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# lightcraft-tiff (L0)
|
||||
|
||||
TIFF / IFD reader and writer. Used by `lightcraft-raw` (DNG, CR2, NEF, ARW…), `lightcraft-meta` (Exif) and
|
||||
later the TIFF/DNG exporters.
|
||||
|
||||
## Features
|
||||
|
||||
- Classic TIFF and BigTIFF, little- and big-endian (`II`/`MM`). Also accepts the TIFF-like magic numbers of
|
||||
some raw formats (ORF `IIRO`/`IIRS`, RW2 `IIU`) when `ParseOptions::accept_raw_magic` is set (default).
|
||||
- Every field type: BYTE, ASCII, SHORT, LONG, RATIONAL, SBYTE, UNDEFINED, SSHORT, SLONG, SRATIONAL, FLOAT,
|
||||
DOUBLE, IFD, LONG8, SLONG8, IFD8.
|
||||
- IFD chain + `SubIFDs` (330) + Exif (34665) / GPS (34853) / Interoperability (40965) pointer IFDs, resolved
|
||||
into a tree (`Ifd::sub_ifds`, `exif`, `gps`, `interop`). `Entry::offset` records the absolute position of
|
||||
each value (needed for maker notes and embedded blobs).
|
||||
- `parse_ifd_at(data, offset, order, base, bigtiff, opts)` parses an IFD whose offsets are relative to an
|
||||
arbitrary base; `makernote::parse_makernote` knows the header / byte-order / offset-base conventions of
|
||||
Canon, Nikon (types 1 and 3), Sony, Fujifilm, Olympus (old and new), Panasonic and Pentax notes.
|
||||
- `image::ImageInfo` — width/height, bits, samples, compression, photometric, planar config, predictor,
|
||||
sample format and the strip or tile chunk list (`chunks()`, `chunk_bytes()`); missing byte counts are
|
||||
inferred.
|
||||
- `TiffWriter` writes IFD chains with strips or tiles, `SubIFDs` and pointer IFDs, in either byte order,
|
||||
classic or BigTIFF; `writer::rational` / `srational` approximate floats as rationals.
|
||||
|
||||
## Robustness
|
||||
|
||||
The reader never panics on malformed input (proptests feed random and mutated files): offsets are
|
||||
bounds-checked; out-of-range or unknown-type entries are skipped; IFD loops are detected; IFD count,
|
||||
nesting depth and entries per IFD are limited; the total bytes of decoded values are capped at
|
||||
`8 × input + 1 MiB` so a file cannot make us allocate far more than its own size.
|
||||
|
||||
## Sources
|
||||
|
||||
Public specifications only: TIFF 6.0 (Adobe, 1992), the TIFF 6.0 supplements (IFD type, SubIFDs), the
|
||||
BigTIFF format description (AWare Systems), Adobe DNG Specification 1.7 (tag numbers), CIPA DC-008 Exif 2.32,
|
||||
and the ExifTool tag-name documentation (maker-note header conventions, prose only). No third-party code
|
||||
was consulted.
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
//! Image layout of an IFD: dimensions, sample format and the strip / tile chunks holding the pixels.
|
||||
|
||||
use crate::{Ifd, Result, TiffError, tags};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// How pixel data is split into chunks.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Layout {
|
||||
Strips { rows_per_strip: u32 },
|
||||
Tiles { tile_width: u32, tile_height: u32 },
|
||||
}
|
||||
|
||||
/// Pixel layout description of an IFD (TIFF 6.0 §3–§8, §15).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ImageInfo {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub bits_per_sample: Vec<u16>,
|
||||
pub samples_per_pixel: u16,
|
||||
pub compression: u16,
|
||||
pub photometric: u16,
|
||||
/// 1 = chunky (interleaved), 2 = planar.
|
||||
pub planar: u16,
|
||||
pub predictor: u16,
|
||||
/// 1 = unsigned int, 2 = signed int, 3 = IEEE float.
|
||||
pub sample_format: u16,
|
||||
pub new_subfile_type: u32,
|
||||
pub layout: Layout,
|
||||
pub offsets: Vec<u64>,
|
||||
pub byte_counts: Vec<u64>,
|
||||
}
|
||||
|
||||
/// One strip or tile.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Chunk {
|
||||
pub index: usize,
|
||||
/// Top-left pixel of the chunk.
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
/// Nominal chunk size (tiles may extend past the image; strips are clipped to the image height).
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
/// Plane index for planar configuration 2 (else 0).
|
||||
pub plane: u16,
|
||||
pub offset: u64,
|
||||
pub len: u64,
|
||||
}
|
||||
|
||||
impl ImageInfo {
|
||||
pub fn from_ifd(ifd: &Ifd) -> Result<ImageInfo> {
|
||||
let width = ifd.u32(tags::IMAGE_WIDTH).ok_or(TiffError::MissingTag(tags::IMAGE_WIDTH))?;
|
||||
let height = ifd.u32(tags::IMAGE_LENGTH).ok_or(TiffError::MissingTag(tags::IMAGE_LENGTH))?;
|
||||
if width == 0 || height == 0 {
|
||||
return Err(TiffError::Invalid("zero image dimension".into()));
|
||||
}
|
||||
let samples_per_pixel = ifd.u16(tags::SAMPLES_PER_PIXEL).unwrap_or(1).max(1);
|
||||
let bits_per_sample: Vec<u16> =
|
||||
ifd.u64s(tags::BITS_PER_SAMPLE).map(|v| v.into_iter().map(|b| b.min(u16::MAX as u64) as u16).collect()).unwrap_or_else(|| vec![1]);
|
||||
let bits_per_sample = if bits_per_sample.is_empty() { vec![1] } else { bits_per_sample };
|
||||
let planar = ifd.u16(tags::PLANAR_CONFIGURATION).unwrap_or(1);
|
||||
let (layout, offsets, byte_counts) = if let Some(tw) = ifd.u32(tags::TILE_WIDTH) {
|
||||
let th = ifd.u32(tags::TILE_LENGTH).ok_or(TiffError::MissingTag(tags::TILE_LENGTH))?;
|
||||
if tw == 0 || th == 0 {
|
||||
return Err(TiffError::Invalid("zero tile size".into()));
|
||||
}
|
||||
let offs = ifd.u64s(tags::TILE_OFFSETS).or_else(|| ifd.u64s(tags::STRIP_OFFSETS)).ok_or(TiffError::MissingTag(tags::TILE_OFFSETS))?;
|
||||
let cnts = ifd
|
||||
.u64s(tags::TILE_BYTE_COUNTS)
|
||||
.or_else(|| ifd.u64s(tags::STRIP_BYTE_COUNTS))
|
||||
.ok_or(TiffError::MissingTag(tags::TILE_BYTE_COUNTS))?;
|
||||
(Layout::Tiles { tile_width: tw, tile_height: th }, offs, cnts)
|
||||
} else {
|
||||
let offs = ifd.u64s(tags::STRIP_OFFSETS).ok_or(TiffError::MissingTag(tags::STRIP_OFFSETS))?;
|
||||
let cnts = ifd.u64s(tags::STRIP_BYTE_COUNTS).unwrap_or_default();
|
||||
let rps = ifd.u32(tags::ROWS_PER_STRIP).unwrap_or(height).clamp(1, height);
|
||||
(Layout::Strips { rows_per_strip: rps }, offs, cnts)
|
||||
};
|
||||
Ok(ImageInfo {
|
||||
width,
|
||||
height,
|
||||
bits_per_sample,
|
||||
samples_per_pixel,
|
||||
compression: ifd.u16(tags::COMPRESSION).unwrap_or(1),
|
||||
photometric: ifd.u16(tags::PHOTOMETRIC).unwrap_or(1),
|
||||
planar,
|
||||
predictor: ifd.u16(tags::PREDICTOR).unwrap_or(1),
|
||||
sample_format: ifd.u16(tags::SAMPLE_FORMAT).unwrap_or(1),
|
||||
new_subfile_type: ifd.u32(tags::NEW_SUBFILE_TYPE).unwrap_or(0),
|
||||
layout,
|
||||
offsets,
|
||||
byte_counts,
|
||||
})
|
||||
}
|
||||
|
||||
/// Bits of the first sample.
|
||||
pub fn bits(&self) -> u16 {
|
||||
self.bits_per_sample[0]
|
||||
}
|
||||
|
||||
/// Whether this IFD is a reduced-resolution image (NewSubfileType bit 0).
|
||||
pub fn is_reduced(&self) -> bool {
|
||||
self.new_subfile_type & 1 != 0
|
||||
}
|
||||
|
||||
/// Number of chunks across and down per plane.
|
||||
pub fn grid(&self) -> (u32, u32) {
|
||||
match self.layout {
|
||||
Layout::Strips { rows_per_strip } => (1, self.height.div_ceil(rows_per_strip)),
|
||||
Layout::Tiles { tile_width, tile_height } => (self.width.div_ceil(tile_width), self.height.div_ceil(tile_height)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Nominal chunk dimensions.
|
||||
pub fn chunk_size(&self) -> (u32, u32) {
|
||||
match self.layout {
|
||||
Layout::Strips { rows_per_strip } => (self.width, rows_per_strip),
|
||||
Layout::Tiles { tile_width, tile_height } => (tile_width, tile_height),
|
||||
}
|
||||
}
|
||||
|
||||
/// All chunks that have an offset. Missing byte counts are inferred as "to the next offset / end of data"
|
||||
/// using `data_len`.
|
||||
pub fn chunks(&self, data_len: u64) -> Vec<Chunk> {
|
||||
let (across, down) = self.grid();
|
||||
let per_plane = across as usize * down as usize;
|
||||
let planes = if self.planar == 2 { self.samples_per_pixel as usize } else { 1 };
|
||||
let (cw, ch) = self.chunk_size();
|
||||
let n = self.offsets.len().min(per_plane.saturating_mul(planes));
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let plane = i / per_plane.max(1);
|
||||
let j = i % per_plane.max(1);
|
||||
let (cx, cy) = (j as u32 % across, j as u32 / across);
|
||||
let x = cx * cw;
|
||||
let y = cy * ch;
|
||||
let height = match self.layout {
|
||||
Layout::Strips { .. } => ch.min(self.height - y),
|
||||
Layout::Tiles { .. } => ch,
|
||||
};
|
||||
let offset = self.offsets[i];
|
||||
let len = match self.byte_counts.get(i) {
|
||||
Some(&l) if l > 0 => l,
|
||||
_ => {
|
||||
// infer: up to the next larger offset, else end of data
|
||||
let next = self.offsets.iter().copied().filter(|&o| o > offset).min().unwrap_or(data_len);
|
||||
next.saturating_sub(offset)
|
||||
}
|
||||
};
|
||||
out.push(Chunk { index: i, x, y, width: cw, height, plane: plane as u16, offset, len });
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// The bytes of a chunk, clipped to the available data (truncated files yield a shorter slice).
|
||||
/// Returns `None` only when the chunk starts past the end of `data`.
|
||||
pub fn chunk_bytes<'a>(data: &'a [u8], c: &Chunk) -> Option<&'a [u8]> {
|
||||
let start = usize::try_from(c.offset).ok()?;
|
||||
if start >= data.len() {
|
||||
return None;
|
||||
}
|
||||
let end = (c.offset.saturating_add(c.len)).min(data.len() as u64) as usize;
|
||||
Some(&data[start..end])
|
||||
}
|
||||
|
|
@ -0,0 +1,466 @@
|
|||
//! TIFF / IFD reading and writing for LightCraft.
|
||||
//!
|
||||
//! - [`Tiff::parse`] reads classic TIFF and BigTIFF in both byte orders, following the IFD chain,
|
||||
//! `SubIFDs`, and the Exif / GPS / Interoperability IFD pointers. All field types of TIFF 6.0,
|
||||
//! the TIFF 6.0 supplement (`IFD`) and BigTIFF (`LONG8`, `SLONG8`, `IFD8`) are decoded.
|
||||
//! - [`parse_ifd_at`] parses a single IFD with an arbitrary offset base — the building block for
|
||||
//! maker-note IFDs ([`makernote`]).
|
||||
//! - [`image::ImageInfo`] describes the pixel layout of an IFD (strips or tiles, planar config,
|
||||
//! compression, predictor) and enumerates its [`image::Chunk`]s.
|
||||
//! - [`writer::TiffWriter`] writes IFD trees (with strips/tiles, `SubIFDs` and pointer IFDs) in
|
||||
//! either byte order, classic or BigTIFF.
|
||||
//!
|
||||
//! The reader never panics on malformed input: every offset is bounds-checked, IFD loops are
|
||||
//! detected, nesting depth / IFD count are limited and the total amount of decoded value bytes is
|
||||
//! capped relative to the input size (so a hostile file cannot make us allocate unboundedly).
|
||||
//!
|
||||
//! Sources: TIFF 6.0 specification (Adobe, 1992), TIFF Technical Note / supplements, the BigTIFF
|
||||
//! design notes (AWare Systems), Adobe DNG specification 1.7 (tag numbers), CIPA DC-008 (Exif 2.32).
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod image;
|
||||
pub mod makernote;
|
||||
mod reader;
|
||||
pub mod tags;
|
||||
pub mod writer;
|
||||
|
||||
pub use reader::{ParseOptions, parse_ifd_at};
|
||||
pub use writer::{IfdBuilder, ImageData, TiffWriter};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Errors from TIFF parsing / writing.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum TiffError {
|
||||
#[error("not a TIFF stream (bad header)")]
|
||||
NotTiff,
|
||||
#[error("data truncated at offset {0}")]
|
||||
Truncated(u64),
|
||||
#[error("invalid TIFF structure: {0}")]
|
||||
Invalid(String),
|
||||
#[error("resource limit exceeded: {0}")]
|
||||
Limit(&'static str),
|
||||
#[error("missing required tag {0}")]
|
||||
MissingTag(u16),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, TiffError>;
|
||||
|
||||
/// Byte order of a TIFF stream.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
|
||||
pub enum ByteOrder {
|
||||
/// `II` — Intel, little-endian.
|
||||
#[default]
|
||||
Little,
|
||||
/// `MM` — Motorola, big-endian.
|
||||
Big,
|
||||
}
|
||||
|
||||
impl ByteOrder {
|
||||
#[inline]
|
||||
pub fn u16(self, b: [u8; 2]) -> u16 {
|
||||
match self {
|
||||
ByteOrder::Little => u16::from_le_bytes(b),
|
||||
ByteOrder::Big => u16::from_be_bytes(b),
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn u32(self, b: [u8; 4]) -> u32 {
|
||||
match self {
|
||||
ByteOrder::Little => u32::from_le_bytes(b),
|
||||
ByteOrder::Big => u32::from_be_bytes(b),
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn u64(self, b: [u8; 8]) -> u64 {
|
||||
match self {
|
||||
ByteOrder::Little => u64::from_le_bytes(b),
|
||||
ByteOrder::Big => u64::from_be_bytes(b),
|
||||
}
|
||||
}
|
||||
/// Read a `u16` at `off`, or `None` if out of bounds.
|
||||
#[inline]
|
||||
pub fn read_u16(self, data: &[u8], off: usize) -> Option<u16> {
|
||||
data.get(off..off.checked_add(2)?).map(|s| self.u16([s[0], s[1]]))
|
||||
}
|
||||
#[inline]
|
||||
pub fn read_u32(self, data: &[u8], off: usize) -> Option<u32> {
|
||||
data.get(off..off.checked_add(4)?).map(|s| self.u32([s[0], s[1], s[2], s[3]]))
|
||||
}
|
||||
#[inline]
|
||||
pub fn read_u64(self, data: &[u8], off: usize) -> Option<u64> {
|
||||
data.get(off..off.checked_add(8)?).map(|s| self.u64([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]]))
|
||||
}
|
||||
pub fn put_u16(self, out: &mut Vec<u8>, v: u16) {
|
||||
out.extend_from_slice(&match self {
|
||||
ByteOrder::Little => v.to_le_bytes(),
|
||||
ByteOrder::Big => v.to_be_bytes(),
|
||||
});
|
||||
}
|
||||
pub fn put_u32(self, out: &mut Vec<u8>, v: u32) {
|
||||
out.extend_from_slice(&match self {
|
||||
ByteOrder::Little => v.to_le_bytes(),
|
||||
ByteOrder::Big => v.to_be_bytes(),
|
||||
});
|
||||
}
|
||||
pub fn put_u64(self, out: &mut Vec<u8>, v: u64) {
|
||||
out.extend_from_slice(&match self {
|
||||
ByteOrder::Little => v.to_le_bytes(),
|
||||
ByteOrder::Big => v.to_be_bytes(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// TIFF field types (TIFF 6.0 §2 + supplements + BigTIFF).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[repr(u16)]
|
||||
pub enum FieldType {
|
||||
Byte = 1,
|
||||
Ascii = 2,
|
||||
Short = 3,
|
||||
Long = 4,
|
||||
Rational = 5,
|
||||
SByte = 6,
|
||||
Undefined = 7,
|
||||
SShort = 8,
|
||||
SLong = 9,
|
||||
SRational = 10,
|
||||
Float = 11,
|
||||
Double = 12,
|
||||
Ifd = 13,
|
||||
Long8 = 16,
|
||||
SLong8 = 17,
|
||||
Ifd8 = 18,
|
||||
}
|
||||
|
||||
impl FieldType {
|
||||
pub fn from_u16(v: u16) -> Option<FieldType> {
|
||||
use FieldType::*;
|
||||
Some(match v {
|
||||
1 => Byte,
|
||||
2 => Ascii,
|
||||
3 => Short,
|
||||
4 => Long,
|
||||
5 => Rational,
|
||||
6 => SByte,
|
||||
7 => Undefined,
|
||||
8 => SShort,
|
||||
9 => SLong,
|
||||
10 => SRational,
|
||||
11 => Float,
|
||||
12 => Double,
|
||||
13 => Ifd,
|
||||
16 => Long8,
|
||||
17 => SLong8,
|
||||
18 => Ifd8,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
/// Size in bytes of one value.
|
||||
pub fn size(self) -> usize {
|
||||
use FieldType::*;
|
||||
match self {
|
||||
Byte | Ascii | SByte | Undefined => 1,
|
||||
Short | SShort => 2,
|
||||
Long | SLong | Float | Ifd => 4,
|
||||
Rational | SRational | Double | Long8 | SLong8 | Ifd8 => 8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A decoded field value. Arrays are stored in their native type.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Value {
|
||||
Byte(Vec<u8>),
|
||||
/// ASCII text (bytes up to the first NUL, decoded lossily as UTF-8).
|
||||
Ascii(String),
|
||||
Short(Vec<u16>),
|
||||
Long(Vec<u32>),
|
||||
Rational(Vec<(u32, u32)>),
|
||||
SByte(Vec<i8>),
|
||||
Undefined(Vec<u8>),
|
||||
SShort(Vec<i16>),
|
||||
SLong(Vec<i32>),
|
||||
SRational(Vec<(i32, i32)>),
|
||||
Float(Vec<f32>),
|
||||
Double(Vec<f64>),
|
||||
Ifd(Vec<u32>),
|
||||
Long8(Vec<u64>),
|
||||
SLong8(Vec<i64>),
|
||||
Ifd8(Vec<u64>),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn field_type(&self) -> FieldType {
|
||||
match self {
|
||||
Value::Byte(_) => FieldType::Byte,
|
||||
Value::Ascii(_) => FieldType::Ascii,
|
||||
Value::Short(_) => FieldType::Short,
|
||||
Value::Long(_) => FieldType::Long,
|
||||
Value::Rational(_) => FieldType::Rational,
|
||||
Value::SByte(_) => FieldType::SByte,
|
||||
Value::Undefined(_) => FieldType::Undefined,
|
||||
Value::SShort(_) => FieldType::SShort,
|
||||
Value::SLong(_) => FieldType::SLong,
|
||||
Value::SRational(_) => FieldType::SRational,
|
||||
Value::Float(_) => FieldType::Float,
|
||||
Value::Double(_) => FieldType::Double,
|
||||
Value::Ifd(_) => FieldType::Ifd,
|
||||
Value::Long8(_) => FieldType::Long8,
|
||||
Value::SLong8(_) => FieldType::SLong8,
|
||||
Value::Ifd8(_) => FieldType::Ifd8,
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of values (for ASCII: bytes including the terminating NUL).
|
||||
pub fn count(&self) -> usize {
|
||||
match self {
|
||||
Value::Byte(v) | Value::Undefined(v) => v.len(),
|
||||
Value::Ascii(s) => s.len() + 1,
|
||||
Value::Short(v) => v.len(),
|
||||
Value::Long(v) | Value::Ifd(v) => v.len(),
|
||||
Value::Rational(v) => v.len(),
|
||||
Value::SByte(v) => v.len(),
|
||||
Value::SShort(v) => v.len(),
|
||||
Value::SLong(v) => v.len(),
|
||||
Value::SRational(v) => v.len(),
|
||||
Value::Float(v) => v.len(),
|
||||
Value::Double(v) => v.len(),
|
||||
Value::Long8(v) | Value::Ifd8(v) => v.len(),
|
||||
Value::SLong8(v) => v.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Value `i` as `f64` (rationals divided; zero denominators give `None`). ASCII has no numbers.
|
||||
pub fn get_f64(&self, i: usize) -> Option<f64> {
|
||||
Some(match self {
|
||||
Value::Byte(v) | Value::Undefined(v) => *v.get(i)? as f64,
|
||||
Value::Ascii(_) => return None,
|
||||
Value::Short(v) => *v.get(i)? as f64,
|
||||
Value::Long(v) | Value::Ifd(v) => *v.get(i)? as f64,
|
||||
Value::Rational(v) => {
|
||||
let (n, d) = *v.get(i)?;
|
||||
if d == 0 {
|
||||
return None;
|
||||
}
|
||||
n as f64 / d as f64
|
||||
}
|
||||
Value::SByte(v) => *v.get(i)? as f64,
|
||||
Value::SShort(v) => *v.get(i)? as f64,
|
||||
Value::SLong(v) => *v.get(i)? as f64,
|
||||
Value::SRational(v) => {
|
||||
let (n, d) = *v.get(i)?;
|
||||
if d == 0 {
|
||||
return None;
|
||||
}
|
||||
n as f64 / d as f64
|
||||
}
|
||||
Value::Float(v) => *v.get(i)? as f64,
|
||||
Value::Double(v) => *v.get(i)?,
|
||||
Value::Long8(v) | Value::Ifd8(v) => *v.get(i)? as f64,
|
||||
Value::SLong8(v) => *v.get(i)? as f64,
|
||||
})
|
||||
}
|
||||
|
||||
/// Value `i` as an unsigned integer (integer types only; negative values give `None`).
|
||||
pub fn get_u64(&self, i: usize) -> Option<u64> {
|
||||
match self {
|
||||
Value::Byte(v) | Value::Undefined(v) => v.get(i).map(|&x| x as u64),
|
||||
Value::Short(v) => v.get(i).map(|&x| x as u64),
|
||||
Value::Long(v) | Value::Ifd(v) => v.get(i).map(|&x| x as u64),
|
||||
Value::Long8(v) | Value::Ifd8(v) => v.get(i).copied(),
|
||||
Value::SByte(v) => v.get(i).and_then(|&x| u64::try_from(x).ok()),
|
||||
Value::SShort(v) => v.get(i).and_then(|&x| u64::try_from(x).ok()),
|
||||
Value::SLong(v) => v.get(i).and_then(|&x| u64::try_from(x).ok()),
|
||||
Value::SLong8(v) => v.get(i).and_then(|&x| u64::try_from(x).ok()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Value `i` as a signed integer (integer types only).
|
||||
pub fn get_i64(&self, i: usize) -> Option<i64> {
|
||||
match self {
|
||||
Value::SByte(v) => v.get(i).map(|&x| x as i64),
|
||||
Value::SShort(v) => v.get(i).map(|&x| x as i64),
|
||||
Value::SLong(v) => v.get(i).map(|&x| x as i64),
|
||||
Value::SLong8(v) => v.get(i).copied(),
|
||||
_ => self.get_u64(i).and_then(|x| i64::try_from(x).ok()),
|
||||
}
|
||||
}
|
||||
|
||||
/// All values as `f64` (missing/invalid entries skipped for rationals with zero denominators → 0).
|
||||
pub fn to_f64_vec(&self) -> Vec<f64> {
|
||||
(0..self.len_values()).map(|i| self.get_f64(i).unwrap_or(0.0)).collect()
|
||||
}
|
||||
|
||||
/// All values as `u64` (integer types only; otherwise empty).
|
||||
pub fn to_u64_vec(&self) -> Vec<u64> {
|
||||
(0..self.len_values()).map_while(|i| self.get_u64(i)).collect()
|
||||
}
|
||||
|
||||
fn len_values(&self) -> usize {
|
||||
match self {
|
||||
Value::Ascii(_) => 0,
|
||||
_ => self.count(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Text for ASCII values (trimmed of trailing NULs / spaces); `Byte`/`Undefined` are decoded as UTF-8 (lossy)
|
||||
/// up to the first NUL, which covers XMP packets and UCS-less Exif strings.
|
||||
pub fn as_str(&self) -> Option<String> {
|
||||
match self {
|
||||
Value::Ascii(s) => Some(s.trim_end_matches([' ', '\0']).to_string()),
|
||||
Value::Byte(b) | Value::Undefined(b) => {
|
||||
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
|
||||
Some(String::from_utf8_lossy(&b[..end]).trim_end().to_string())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw bytes for `Byte` / `Undefined` / `SByte` / `Ascii`.
|
||||
pub fn as_bytes(&self) -> Option<&[u8]> {
|
||||
match self {
|
||||
Value::Byte(b) | Value::Undefined(b) => Some(b),
|
||||
Value::Ascii(s) => Some(s.as_bytes()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One IFD entry.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Entry {
|
||||
pub tag: u16,
|
||||
pub value: Value,
|
||||
/// Absolute position in the parsed buffer of the value bytes (inline or out-of-line).
|
||||
pub offset: u64,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
pub fn field_type(&self) -> FieldType {
|
||||
self.value.field_type()
|
||||
}
|
||||
pub fn count(&self) -> usize {
|
||||
self.value.count()
|
||||
}
|
||||
}
|
||||
|
||||
/// An Image File Directory with its child IFDs already resolved.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Ifd {
|
||||
/// Absolute offset of the IFD in the buffer.
|
||||
pub offset: u64,
|
||||
/// Entries sorted by tag (duplicate tags: the first occurrence wins).
|
||||
pub entries: Vec<Entry>,
|
||||
/// IFDs referenced by `SubIFDs` (330).
|
||||
pub sub_ifds: Vec<Ifd>,
|
||||
/// Exif private IFD (34665).
|
||||
pub exif: Option<Box<Ifd>>,
|
||||
/// GPS IFD (34853).
|
||||
pub gps: Option<Box<Ifd>>,
|
||||
/// Interoperability IFD (40965).
|
||||
pub interop: Option<Box<Ifd>>,
|
||||
}
|
||||
|
||||
impl Ifd {
|
||||
pub fn get(&self, tag: u16) -> Option<&Entry> {
|
||||
self.entries.binary_search_by_key(&tag, |e| e.tag).ok().map(|i| &self.entries[i])
|
||||
}
|
||||
pub fn contains(&self, tag: u16) -> bool {
|
||||
self.get(tag).is_some()
|
||||
}
|
||||
pub fn value(&self, tag: u16) -> Option<&Value> {
|
||||
self.get(tag).map(|e| &e.value)
|
||||
}
|
||||
/// First value as `u32` (integer types, saturating from wider types).
|
||||
pub fn u32(&self, tag: u16) -> Option<u32> {
|
||||
self.value(tag)?.get_u64(0).map(|v| v.min(u32::MAX as u64) as u32)
|
||||
}
|
||||
pub fn u64(&self, tag: u16) -> Option<u64> {
|
||||
self.value(tag)?.get_u64(0)
|
||||
}
|
||||
pub fn u16(&self, tag: u16) -> Option<u16> {
|
||||
self.value(tag)?.get_u64(0).map(|v| v.min(u16::MAX as u64) as u16)
|
||||
}
|
||||
pub fn i64(&self, tag: u16) -> Option<i64> {
|
||||
self.value(tag)?.get_i64(0)
|
||||
}
|
||||
pub fn f64(&self, tag: u16) -> Option<f64> {
|
||||
self.value(tag)?.get_f64(0)
|
||||
}
|
||||
pub fn f64s(&self, tag: u16) -> Option<Vec<f64>> {
|
||||
self.value(tag).map(|v| v.to_f64_vec())
|
||||
}
|
||||
pub fn u64s(&self, tag: u16) -> Option<Vec<u64>> {
|
||||
self.value(tag).map(|v| v.to_u64_vec())
|
||||
}
|
||||
/// Text value (ASCII, or UTF-8 bytes).
|
||||
pub fn string(&self, tag: u16) -> Option<String> {
|
||||
self.value(tag)?.as_str().filter(|s| !s.is_empty())
|
||||
}
|
||||
pub fn bytes(&self, tag: u16) -> Option<&[u8]> {
|
||||
self.value(tag)?.as_bytes()
|
||||
}
|
||||
|
||||
/// This IFD and all descendants (sub-IFDs, Exif, GPS, Interop), depth-first, pre-order.
|
||||
pub fn walk(&self) -> Vec<&Ifd> {
|
||||
let mut out = vec![self];
|
||||
for s in &self.sub_ifds {
|
||||
out.extend(s.walk());
|
||||
}
|
||||
for c in [&self.exif, &self.gps, &self.interop].into_iter().flatten() {
|
||||
out.extend(c.walk());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Image layout of this IFD, if it describes an image.
|
||||
pub fn image(&self) -> Result<image::ImageInfo> {
|
||||
image::ImageInfo::from_ifd(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A parsed TIFF stream (IFD chain with resolved children).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Tiff {
|
||||
pub order: ByteOrder,
|
||||
pub bigtiff: bool,
|
||||
/// The main IFD chain (IFD0, IFD1, …).
|
||||
pub ifds: Vec<Ifd>,
|
||||
}
|
||||
|
||||
impl Tiff {
|
||||
/// Parse a TIFF stream (the header must be at offset 0 of `data`).
|
||||
pub fn parse(data: &[u8]) -> Result<Tiff> {
|
||||
reader::parse(data, &ParseOptions::default())
|
||||
}
|
||||
pub fn parse_with(data: &[u8], opts: &ParseOptions) -> Result<Tiff> {
|
||||
reader::parse(data, opts)
|
||||
}
|
||||
/// Header check only: returns the byte order and BigTIFF flag.
|
||||
pub fn sniff(data: &[u8]) -> Option<(ByteOrder, bool)> {
|
||||
reader::header(data).ok().map(|(o, b, _)| (o, b))
|
||||
}
|
||||
/// Every IFD (main chain and all children), depth-first.
|
||||
pub fn all_ifds(&self) -> Vec<&Ifd> {
|
||||
self.ifds.iter().flat_map(|i| i.walk()).collect()
|
||||
}
|
||||
/// The Exif IFD attached to IFD0 (or any IFD).
|
||||
pub fn exif(&self) -> Option<&Ifd> {
|
||||
self.all_ifds().into_iter().find_map(|i| i.exif.as_deref())
|
||||
}
|
||||
pub fn gps(&self) -> Option<&Ifd> {
|
||||
self.all_ifds().into_iter().find_map(|i| i.gps.as_deref())
|
||||
}
|
||||
/// First entry for `tag` in IFD0, then the Exif IFD, then any IFD.
|
||||
pub fn find(&self, tag: u16) -> Option<&Entry> {
|
||||
if let Some(e) = self.ifds.first().and_then(|i| i.get(tag)) {
|
||||
return Some(e);
|
||||
}
|
||||
if let Some(e) = self.exif().and_then(|i| i.get(tag)) {
|
||||
return Some(e);
|
||||
}
|
||||
self.all_ifds().into_iter().find_map(|i| i.get(tag))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
//! Maker-note IFD helpers.
|
||||
//!
|
||||
//! Most vendors store their maker note (Exif tag 37500) as a TIFF IFD, but each uses its own header,
|
||||
//! byte order and *offset base* (what stored value offsets are relative to). The header layouts here
|
||||
//! follow the public descriptions in the ExifTool tag documentation (`MakerNotes` table) and the
|
||||
//! vendors' own published notes; no third-party code was consulted.
|
||||
|
||||
use crate::{ByteOrder, Ifd, ParseOptions, parse_ifd_at};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Maker-note dialects we know how to locate.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MakerNoteKind {
|
||||
/// Plain IFD at the start of the note, offsets relative to the enclosing TIFF header (Canon, many others).
|
||||
Plain,
|
||||
/// `Nikon\0` + version + embedded TIFF header at +10; offsets relative to that header.
|
||||
NikonV3,
|
||||
/// `Nikon\0\x01\0`: IFD at +8, offsets relative to the enclosing TIFF header.
|
||||
NikonV1,
|
||||
/// `SONY DSC `/`SONY CAM `/`SONY MOBILE` etc.: 12-byte header, offsets relative to the TIFF header.
|
||||
Sony,
|
||||
/// `FUJIFILM` + LE u32 IFD offset; little-endian, offsets relative to the note start.
|
||||
Fujifilm,
|
||||
/// `OLYMPUS\0` + byte order + version: IFD at +12, offsets relative to the note start.
|
||||
OlympusNew,
|
||||
/// `OLYMP\0` / `EPSON\0` etc.: IFD at +8, offsets relative to the TIFF header.
|
||||
OlympusOld,
|
||||
/// `Panasonic\0\0\0`: IFD at +12, offsets relative to the TIFF header.
|
||||
Panasonic,
|
||||
/// `AOC\0` + byte order: IFD at +6, offsets relative to the TIFF header.
|
||||
PentaxAoc,
|
||||
/// `PENTAX \0` + byte order: IFD at +10, offsets relative to the note start.
|
||||
Pentax,
|
||||
}
|
||||
|
||||
/// A parsed maker-note IFD.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MakerNote {
|
||||
pub kind: MakerNoteKind,
|
||||
pub order: ByteOrder,
|
||||
/// Absolute offset that stored value offsets are relative to.
|
||||
pub base: u64,
|
||||
pub ifd: Ifd,
|
||||
}
|
||||
|
||||
/// Locate and parse the maker note stored at absolute `offset` (length `len`) inside `data`, the buffer whose
|
||||
/// start is the enclosing TIFF header. `order` is the enclosing stream's byte order. `make` (the Exif Make) is
|
||||
/// used only as a hint for header-less notes. Returns `None` for unknown or unparsable notes.
|
||||
pub fn parse_makernote(data: &[u8], offset: u64, len: u64, order: ByteOrder, make: &str) -> Option<MakerNote> {
|
||||
let start = usize::try_from(offset).ok()?;
|
||||
let end = start.checked_add(usize::try_from(len).ok()?)?.min(data.len());
|
||||
let note = data.get(start..end)?;
|
||||
let order_at = |i: usize| match note.get(i..i + 2) {
|
||||
Some(b"II") => Some(ByteOrder::Little),
|
||||
Some(b"MM") => Some(ByteOrder::Big),
|
||||
_ => None,
|
||||
};
|
||||
let opts = ParseOptions { max_ifds: 64, max_depth: 3, ..Default::default() };
|
||||
let try_parse = |kind: MakerNoteKind, ifd_at: u64, base: u64, ord: ByteOrder| -> Option<MakerNote> {
|
||||
let (ifd, _) = parse_ifd_at(data, ifd_at, ord, base, false, &opts).ok()?;
|
||||
(!ifd.entries.is_empty()).then_some(MakerNote { kind, order: ord, base, ifd })
|
||||
};
|
||||
if note.starts_with(b"Nikon\0") {
|
||||
if note.get(6) == Some(&1) {
|
||||
return try_parse(MakerNoteKind::NikonV1, offset + 8, 0, order);
|
||||
}
|
||||
let ord = order_at(10)?;
|
||||
let ifd_rel = ord.read_u32(note, 14)? as u64;
|
||||
let base = offset + 10;
|
||||
return try_parse(MakerNoteKind::NikonV3, base + ifd_rel, base, ord);
|
||||
}
|
||||
if note.starts_with(b"FUJIFILM") || note.starts_with(b"GENERALE") {
|
||||
let rel = ByteOrder::Little.read_u32(note, 8)? as u64;
|
||||
return try_parse(MakerNoteKind::Fujifilm, offset + rel, offset, ByteOrder::Little);
|
||||
}
|
||||
if note.starts_with(b"OLYMPUS\0") || note.starts_with(b"OM SYSTEM\0") {
|
||||
let (ord_at, ifd_at) = if note.starts_with(b"OM SYSTEM\0") { (12, 16) } else { (8, 12) };
|
||||
let ord = order_at(ord_at).unwrap_or(order);
|
||||
return try_parse(MakerNoteKind::OlympusNew, offset + ifd_at, offset, ord);
|
||||
}
|
||||
for p in [b"OLYMP\0", b"EPSON\0", b"MINOL\0", b"CAMER\0", b"SANYO\0"] {
|
||||
if note.starts_with(p) {
|
||||
return try_parse(MakerNoteKind::OlympusOld, offset + 8, 0, order);
|
||||
}
|
||||
}
|
||||
if note.starts_with(b"Panasonic\0") {
|
||||
return try_parse(MakerNoteKind::Panasonic, offset + 12, 0, order);
|
||||
}
|
||||
if note.starts_with(b"AOC\0") {
|
||||
let ord = order_at(4).unwrap_or(order);
|
||||
return try_parse(MakerNoteKind::PentaxAoc, offset + 6, 0, ord);
|
||||
}
|
||||
if note.starts_with(b"PENTAX \0") {
|
||||
let ord = order_at(8).unwrap_or(order);
|
||||
return try_parse(MakerNoteKind::Pentax, offset + 10, offset, ord);
|
||||
}
|
||||
if (note.starts_with(b"SONY") || note.starts_with(b"VHAB \0"))
|
||||
&& let Some(m) = try_parse(MakerNoteKind::Sony, offset + 12, 0, order)
|
||||
{
|
||||
return Some(m);
|
||||
}
|
||||
let _ = make;
|
||||
try_parse(MakerNoteKind::Plain, offset, 0, order)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{IfdBuilder, TiffWriter, Value, tags};
|
||||
|
||||
/// Build a TIFF whose Exif IFD carries `note` as the maker note, return (bytes, note offset, len).
|
||||
fn with_note(order: ByteOrder, note: Vec<u8>) -> (Vec<u8>, u64, u64) {
|
||||
let mut exif = IfdBuilder::new();
|
||||
exif.set(tags::MAKER_NOTE, Value::Undefined(note));
|
||||
let mut ifd0 = IfdBuilder::new();
|
||||
ifd0.set(tags::MAKE, Value::Ascii("Test".into()));
|
||||
ifd0.set_child(tags::EXIF_IFD, exif);
|
||||
let bytes = TiffWriter { order, bigtiff: false }.write(&[ifd0]).unwrap();
|
||||
let t = crate::Tiff::parse(&bytes).unwrap();
|
||||
let e = t.exif().unwrap().get(tags::MAKER_NOTE).unwrap();
|
||||
let (off, len) = (e.offset, e.count() as u64);
|
||||
(bytes, off, len)
|
||||
}
|
||||
|
||||
/// An IFD with one SHORT entry (tag 1, value 7) and one out-of-line LONG[2] entry at `rel` (relative to base).
|
||||
fn tiny_ifd(order: ByteOrder, rel_values: u32) -> Vec<u8> {
|
||||
let mut v = Vec::new();
|
||||
order.put_u16(&mut v, 2);
|
||||
order.put_u16(&mut v, 1);
|
||||
order.put_u16(&mut v, 3);
|
||||
order.put_u32(&mut v, 1);
|
||||
order.put_u16(&mut v, 7);
|
||||
order.put_u16(&mut v, 0);
|
||||
order.put_u16(&mut v, 2);
|
||||
order.put_u16(&mut v, 4);
|
||||
order.put_u32(&mut v, 2);
|
||||
order.put_u32(&mut v, rel_values);
|
||||
order.put_u32(&mut v, 0);
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nikon_v3_uses_embedded_header_base() {
|
||||
for order in [ByteOrder::Little, ByteOrder::Big] {
|
||||
let mut note = b"Nikon\0\x02\x10\0\0".to_vec();
|
||||
let hdr_at = note.len();
|
||||
note.extend_from_slice(if order == ByteOrder::Little { b"II" } else { b"MM" });
|
||||
order.put_u16(&mut note, 42);
|
||||
order.put_u32(&mut note, 8);
|
||||
// IFD at +8 (relative to embedded header), values after IFD: 8 + 2 + 24 + 4 = 38
|
||||
note.extend(tiny_ifd(order, 38));
|
||||
order.put_u32(&mut note, 111);
|
||||
order.put_u32(&mut note, 222);
|
||||
let (bytes, off, len) = with_note(order, note);
|
||||
let m = parse_makernote(&bytes, off, len, order, "NIKON CORPORATION").unwrap();
|
||||
assert_eq!(m.kind, MakerNoteKind::NikonV3);
|
||||
assert_eq!(m.base, off + hdr_at as u64);
|
||||
assert_eq!(m.ifd.u32(1), Some(7));
|
||||
assert_eq!(m.ifd.u64s(2).unwrap(), vec![111, 222]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fujifilm_offsets_relative_to_note() {
|
||||
let mut note = b"FUJIFILM".to_vec();
|
||||
ByteOrder::Little.put_u32(&mut note, 12);
|
||||
// IFD at 12 (relative to note), values at 12 + 30 = 42
|
||||
note.extend(tiny_ifd(ByteOrder::Little, 42));
|
||||
ByteOrder::Little.put_u32(&mut note, 5);
|
||||
ByteOrder::Little.put_u32(&mut note, 6);
|
||||
// enclosing file is big-endian: Fuji notes are always little-endian
|
||||
let (bytes, off, len) = with_note(ByteOrder::Big, note);
|
||||
let m = parse_makernote(&bytes, off, len, ByteOrder::Big, "FUJIFILM").unwrap();
|
||||
assert_eq!(m.kind, MakerNoteKind::Fujifilm);
|
||||
assert_eq!(m.order, ByteOrder::Little);
|
||||
assert_eq!(m.ifd.u64s(2).unwrap(), vec![5, 6]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_canon_style_note() {
|
||||
// Plain IFD whose out-of-line offset is relative to the TIFF header: we need the absolute
|
||||
// position, which we only know after writing. Write once to learn the offset, then patch.
|
||||
let order = ByteOrder::Little;
|
||||
let mut note = tiny_ifd(order, 0);
|
||||
note.extend_from_slice(&[0; 8]);
|
||||
let (mut bytes, off, len) = with_note(order, note);
|
||||
let values_abs = off as u32 + 30;
|
||||
bytes[off as usize + 22..off as usize + 26].copy_from_slice(&values_abs.to_le_bytes());
|
||||
bytes[off as usize + 30..off as usize + 34].copy_from_slice(&9u32.to_le_bytes());
|
||||
let m = parse_makernote(&bytes, off, len, order, "Canon").unwrap();
|
||||
assert_eq!(m.kind, MakerNoteKind::Plain);
|
||||
assert_eq!(m.ifd.u64s(2).unwrap(), vec![9, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garbage_note_is_none() {
|
||||
let (bytes, off, len) = with_note(ByteOrder::Little, vec![0xff; 40]);
|
||||
assert!(parse_makernote(&bytes, off, len, ByteOrder::Little, "X").is_none());
|
||||
assert!(parse_makernote(&bytes, 1 << 40, 10, ByteOrder::Little, "X").is_none());
|
||||
assert!(parse_makernote(&bytes, off, u64::MAX, ByteOrder::Little, "X").is_none());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
//! Bounds-checked IFD parsing.
|
||||
|
||||
use crate::{ByteOrder, Entry, FieldType, Ifd, Result, Tiff, TiffError, Value, tags};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Limits applied while parsing. The defaults are generous for real files and safe for hostile ones.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ParseOptions {
|
||||
/// Maximum number of IFDs visited in total (chain + children).
|
||||
pub max_ifds: usize,
|
||||
/// Maximum nesting depth of child IFDs.
|
||||
pub max_depth: usize,
|
||||
/// Maximum number of entries read from one IFD.
|
||||
pub max_entries: usize,
|
||||
/// Maximum total bytes of decoded values; `None` = `8 × input length + 1 MiB`.
|
||||
pub value_budget: Option<u64>,
|
||||
/// Follow `SubIFDs` / Exif / GPS / Interop pointers.
|
||||
pub follow_children: bool,
|
||||
/// Accept the TIFF-like magic numbers used by some raw formats (ORF `IIRO`/`IIRS`, RW2 `IIU`).
|
||||
pub accept_raw_magic: bool,
|
||||
}
|
||||
|
||||
impl Default for ParseOptions {
|
||||
fn default() -> Self {
|
||||
Self { max_ifds: 1024, max_depth: 10, max_entries: 8192, value_budget: None, follow_children: true, accept_raw_magic: true }
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the header: (byte order, bigtiff, first IFD offset).
|
||||
pub(crate) fn header(data: &[u8]) -> Result<(ByteOrder, bool, u64)> {
|
||||
header_with(data, true)
|
||||
}
|
||||
|
||||
fn header_with(data: &[u8], raw_magic: bool) -> Result<(ByteOrder, bool, u64)> {
|
||||
let order = match data.get(0..2) {
|
||||
Some(b"II") => ByteOrder::Little,
|
||||
Some(b"MM") => ByteOrder::Big,
|
||||
_ => return Err(TiffError::NotTiff),
|
||||
};
|
||||
let magic = order.read_u16(data, 2).ok_or(TiffError::NotTiff)?;
|
||||
match magic {
|
||||
42 => Ok((order, false, order.read_u32(data, 4).ok_or(TiffError::Truncated(4))? as u64)),
|
||||
43 => {
|
||||
let bytesize = order.read_u16(data, 4).ok_or(TiffError::Truncated(4))?;
|
||||
let zero = order.read_u16(data, 6).ok_or(TiffError::Truncated(6))?;
|
||||
if bytesize != 8 || zero != 0 {
|
||||
return Err(TiffError::NotTiff);
|
||||
}
|
||||
Ok((order, true, order.read_u64(data, 8).ok_or(TiffError::Truncated(8))?))
|
||||
}
|
||||
0x4f52 | 0x5352 | 0x0055 if raw_magic => Ok((order, false, order.read_u32(data, 4).ok_or(TiffError::Truncated(4))? as u64)),
|
||||
_ => Err(TiffError::NotTiff),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse(data: &[u8], opts: &ParseOptions) -> Result<Tiff> {
|
||||
let (order, big, first) = header_with(data, opts.accept_raw_magic)?;
|
||||
let mut ctx = Ctx::new(data, order, big, opts);
|
||||
let mut ifds = Vec::new();
|
||||
let mut off = first;
|
||||
while off != 0 {
|
||||
match ctx.ifd(off, 0, 0) {
|
||||
Ok((ifd, next)) => {
|
||||
ifds.push(ifd);
|
||||
off = next;
|
||||
}
|
||||
Err(e) => {
|
||||
if ifds.is_empty() {
|
||||
return Err(e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ifds.is_empty() {
|
||||
return Err(TiffError::Invalid("no IFDs".into()));
|
||||
}
|
||||
Ok(Tiff { order, bigtiff: big, ifds })
|
||||
}
|
||||
|
||||
/// Parse one IFD at absolute `offset` whose value offsets (and child IFD pointers) are relative to
|
||||
/// `base` (absolute position = `base + stored offset`). Returns the IFD and the absolute offset of the
|
||||
/// next IFD (0 when there is none). Used for maker notes, which often use their own offset base.
|
||||
pub fn parse_ifd_at(data: &[u8], offset: u64, order: ByteOrder, base: u64, bigtiff: bool, opts: &ParseOptions) -> Result<(Ifd, u64)> {
|
||||
let mut ctx = Ctx::new(data, order, bigtiff, opts);
|
||||
ctx.ifd(offset, base, 0)
|
||||
}
|
||||
|
||||
struct Ctx<'a> {
|
||||
data: &'a [u8],
|
||||
order: ByteOrder,
|
||||
big: bool,
|
||||
opts: &'a ParseOptions,
|
||||
visited: HashSet<u64>,
|
||||
budget: u64,
|
||||
}
|
||||
|
||||
impl<'a> Ctx<'a> {
|
||||
fn new(data: &'a [u8], order: ByteOrder, big: bool, opts: &'a ParseOptions) -> Self {
|
||||
let budget = opts.value_budget.unwrap_or((data.len() as u64).saturating_mul(8).saturating_add(1 << 20));
|
||||
Self { data, order, big, opts, visited: HashSet::new(), budget }
|
||||
}
|
||||
|
||||
fn read_off(&self, pos: usize) -> Option<u64> {
|
||||
if self.big { self.order.read_u64(self.data, pos) } else { self.order.read_u32(self.data, pos).map(|v| v as u64) }
|
||||
}
|
||||
|
||||
/// Returns (ifd, absolute next offset or 0).
|
||||
fn ifd(&mut self, offset: u64, base: u64, depth: usize) -> Result<(Ifd, u64)> {
|
||||
if depth > self.opts.max_depth {
|
||||
return Err(TiffError::Limit("IFD nesting depth"));
|
||||
}
|
||||
if self.visited.len() >= self.opts.max_ifds {
|
||||
return Err(TiffError::Limit("IFD count"));
|
||||
}
|
||||
if !self.visited.insert(offset) {
|
||||
return Err(TiffError::Invalid(format!("IFD loop at {offset}")));
|
||||
}
|
||||
let pos = usize::try_from(offset).map_err(|_| TiffError::Truncated(offset))?;
|
||||
let (n, head, entry_size, inline) = if self.big {
|
||||
(self.order.read_u64(self.data, pos).ok_or(TiffError::Truncated(offset))?, 8usize, 20usize, 8usize)
|
||||
} else {
|
||||
(self.order.read_u16(self.data, pos).ok_or(TiffError::Truncated(offset))? as u64, 2, 12, 4)
|
||||
};
|
||||
let table = pos + head;
|
||||
let fit = (self.data.len().saturating_sub(table) / entry_size) as u64;
|
||||
let n_read = n.min(fit).min(self.opts.max_entries as u64) as usize;
|
||||
if n == 0 {
|
||||
return Err(TiffError::Invalid(format!("empty IFD at {offset}")));
|
||||
}
|
||||
if n_read == 0 {
|
||||
return Err(TiffError::Truncated(offset));
|
||||
}
|
||||
let mut entries = Vec::with_capacity(n_read);
|
||||
for i in 0..n_read {
|
||||
let p = table + i * entry_size;
|
||||
if let Some(e) = self.entry(p, base, inline) {
|
||||
entries.push(e);
|
||||
}
|
||||
}
|
||||
entries.sort_by_key(|e| e.tag);
|
||||
entries.dedup_by_key(|e| e.tag);
|
||||
let next_pos = table + (n as usize).saturating_mul(entry_size);
|
||||
let next = if n as usize == n_read {
|
||||
match self.read_off(next_pos) {
|
||||
Some(0) | None => 0,
|
||||
Some(v) => base.checked_add(v).unwrap_or(0),
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mut ifd = Ifd { offset, entries, ..Default::default() };
|
||||
if self.opts.follow_children {
|
||||
self.children(&mut ifd, base, depth);
|
||||
}
|
||||
Ok((ifd, next))
|
||||
}
|
||||
|
||||
fn children(&mut self, ifd: &mut Ifd, base: u64, depth: usize) {
|
||||
if let Some(v) = ifd.value(tags::SUB_IFDS) {
|
||||
let offs = v.to_u64_vec();
|
||||
for o in offs {
|
||||
if let Some(abs) = base.checked_add(o)
|
||||
&& let Ok((sub, _)) = self.ifd(abs, base, depth + 1)
|
||||
{
|
||||
ifd.sub_ifds.push(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
let child = |tag: u16, ctx: &mut Self| -> Option<Box<Ifd>> {
|
||||
let o = ifd.value(tag)?.get_u64(0)?;
|
||||
let abs = base.checked_add(o)?;
|
||||
ctx.ifd(abs, base, depth + 1).ok().map(|(i, _)| Box::new(i))
|
||||
};
|
||||
let exif = child(tags::EXIF_IFD, self);
|
||||
let gps = child(tags::GPS_IFD, self);
|
||||
let interop = child(tags::INTEROP_IFD, self);
|
||||
ifd.exif = exif;
|
||||
ifd.gps = gps;
|
||||
ifd.interop = interop;
|
||||
}
|
||||
|
||||
fn entry(&mut self, p: usize, base: u64, inline: usize) -> Option<Entry> {
|
||||
let o = self.order;
|
||||
let tag = o.read_u16(self.data, p)?;
|
||||
let typ = FieldType::from_u16(o.read_u16(self.data, p + 2)?)?;
|
||||
let (count, field) = if self.big { (o.read_u64(self.data, p + 4)?, p + 12) } else { (o.read_u32(self.data, p + 4)? as u64, p + 8) };
|
||||
let total = count.checked_mul(typ.size() as u64)?;
|
||||
let start = if total <= inline as u64 {
|
||||
field as u64
|
||||
} else {
|
||||
let rel = self.read_off(field)?;
|
||||
base.checked_add(rel)?
|
||||
};
|
||||
let end = start.checked_add(total)?;
|
||||
if end > self.data.len() as u64 {
|
||||
return None;
|
||||
}
|
||||
if total > self.budget {
|
||||
return None;
|
||||
}
|
||||
self.budget -= total;
|
||||
let bytes = &self.data[start as usize..end as usize];
|
||||
Some(Entry { tag, value: decode(o, typ, bytes), offset: start })
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode `bytes` (exactly `count × size` long) as values of type `typ`.
|
||||
pub(crate) fn decode(o: ByteOrder, typ: FieldType, b: &[u8]) -> Value {
|
||||
fn arr<const N: usize>(c: &[u8]) -> [u8; N] {
|
||||
let mut a = [0u8; N];
|
||||
a.copy_from_slice(c);
|
||||
a
|
||||
}
|
||||
match typ {
|
||||
FieldType::Byte => Value::Byte(b.to_vec()),
|
||||
FieldType::Undefined => Value::Undefined(b.to_vec()),
|
||||
FieldType::Ascii => {
|
||||
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
|
||||
Value::Ascii(String::from_utf8_lossy(&b[..end]).into_owned())
|
||||
}
|
||||
FieldType::SByte => Value::SByte(b.iter().map(|&x| x as i8).collect()),
|
||||
FieldType::Short => Value::Short(b.chunks_exact(2).map(|c| o.u16(arr(c))).collect()),
|
||||
FieldType::SShort => Value::SShort(b.chunks_exact(2).map(|c| o.u16(arr(c)) as i16).collect()),
|
||||
FieldType::Long => Value::Long(b.chunks_exact(4).map(|c| o.u32(arr(c))).collect()),
|
||||
FieldType::Ifd => Value::Ifd(b.chunks_exact(4).map(|c| o.u32(arr(c))).collect()),
|
||||
FieldType::SLong => Value::SLong(b.chunks_exact(4).map(|c| o.u32(arr(c)) as i32).collect()),
|
||||
FieldType::Float => Value::Float(b.chunks_exact(4).map(|c| f32::from_bits(o.u32(arr(c)))).collect()),
|
||||
FieldType::Rational => Value::Rational(b.chunks_exact(8).map(|c| (o.u32(arr(&c[..4])), o.u32(arr(&c[4..])))).collect()),
|
||||
FieldType::SRational => Value::SRational(b.chunks_exact(8).map(|c| (o.u32(arr(&c[..4])) as i32, o.u32(arr(&c[4..])) as i32)).collect()),
|
||||
FieldType::Double => Value::Double(b.chunks_exact(8).map(|c| f64::from_bits(o.u64(arr(c)))).collect()),
|
||||
FieldType::Long8 => Value::Long8(b.chunks_exact(8).map(|c| o.u64(arr(c))).collect()),
|
||||
FieldType::Ifd8 => Value::Ifd8(b.chunks_exact(8).map(|c| o.u64(arr(c))).collect()),
|
||||
FieldType::SLong8 => Value::SLong8(b.chunks_exact(8).map(|c| o.u64(arr(c)) as i64).collect()),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
//! Tag numbers (TIFF 6.0, TIFF/EP, Exif 2.32, GPS, DNG 1.7). Public specification constants.
|
||||
|
||||
// --- TIFF baseline / extensions
|
||||
pub const NEW_SUBFILE_TYPE: u16 = 254;
|
||||
pub const SUBFILE_TYPE: u16 = 255;
|
||||
pub const IMAGE_WIDTH: u16 = 256;
|
||||
pub const IMAGE_LENGTH: u16 = 257;
|
||||
pub const BITS_PER_SAMPLE: u16 = 258;
|
||||
pub const COMPRESSION: u16 = 259;
|
||||
pub const PHOTOMETRIC: u16 = 262;
|
||||
pub const FILL_ORDER: u16 = 266;
|
||||
pub const DOCUMENT_NAME: u16 = 269;
|
||||
pub const IMAGE_DESCRIPTION: u16 = 270;
|
||||
pub const MAKE: u16 = 271;
|
||||
pub const MODEL: u16 = 272;
|
||||
pub const STRIP_OFFSETS: u16 = 273;
|
||||
pub const ORIENTATION: u16 = 274;
|
||||
pub const SAMPLES_PER_PIXEL: u16 = 277;
|
||||
pub const ROWS_PER_STRIP: u16 = 278;
|
||||
pub const STRIP_BYTE_COUNTS: u16 = 279;
|
||||
pub const X_RESOLUTION: u16 = 282;
|
||||
pub const Y_RESOLUTION: u16 = 283;
|
||||
pub const PLANAR_CONFIGURATION: u16 = 284;
|
||||
pub const RESOLUTION_UNIT: u16 = 296;
|
||||
pub const SOFTWARE: u16 = 305;
|
||||
pub const DATE_TIME: u16 = 306;
|
||||
pub const ARTIST: u16 = 315;
|
||||
pub const PREDICTOR: u16 = 317;
|
||||
pub const TILE_WIDTH: u16 = 322;
|
||||
pub const TILE_LENGTH: u16 = 323;
|
||||
pub const TILE_OFFSETS: u16 = 324;
|
||||
pub const TILE_BYTE_COUNTS: u16 = 325;
|
||||
pub const SUB_IFDS: u16 = 330;
|
||||
pub const EXTRA_SAMPLES: u16 = 338;
|
||||
pub const SAMPLE_FORMAT: u16 = 339;
|
||||
pub const JPEG_INTERCHANGE_FORMAT: u16 = 513;
|
||||
pub const JPEG_INTERCHANGE_FORMAT_LENGTH: u16 = 514;
|
||||
pub const YCBCR_SUBSAMPLING: u16 = 530;
|
||||
pub const XMP: u16 = 700;
|
||||
pub const RATING: u16 = 18246;
|
||||
pub const RATING_PERCENT: u16 = 18249;
|
||||
pub const CFA_REPEAT_PATTERN_DIM: u16 = 33421;
|
||||
pub const CFA_PATTERN_EP: u16 = 33422;
|
||||
pub const COPYRIGHT: u16 = 33432;
|
||||
pub const EXPOSURE_TIME_EP: u16 = 33434;
|
||||
pub const IPTC_NAA: u16 = 33723;
|
||||
pub const EXIF_IFD: u16 = 34665;
|
||||
pub const ICC_PROFILE: u16 = 34675;
|
||||
pub const GPS_IFD: u16 = 34853;
|
||||
pub const INTEROP_IFD: u16 = 40965;
|
||||
|
||||
// --- Exif private IFD
|
||||
pub const EXPOSURE_TIME: u16 = 33434;
|
||||
pub const F_NUMBER: u16 = 33437;
|
||||
pub const EXPOSURE_PROGRAM: u16 = 34850;
|
||||
pub const ISO_SPEED: u16 = 34855;
|
||||
pub const SENSITIVITY_TYPE: u16 = 34864;
|
||||
pub const RECOMMENDED_EXPOSURE_INDEX: u16 = 34866;
|
||||
pub const ISO_SPEED_RATINGS_EXT: u16 = 34867;
|
||||
pub const EXIF_VERSION: u16 = 36864;
|
||||
pub const DATE_TIME_ORIGINAL: u16 = 36867;
|
||||
pub const DATE_TIME_DIGITIZED: u16 = 36868;
|
||||
pub const OFFSET_TIME: u16 = 36880;
|
||||
pub const OFFSET_TIME_ORIGINAL: u16 = 36881;
|
||||
pub const OFFSET_TIME_DIGITIZED: u16 = 36882;
|
||||
pub const SHUTTER_SPEED_VALUE: u16 = 37377;
|
||||
pub const APERTURE_VALUE: u16 = 37378;
|
||||
pub const EXPOSURE_BIAS: u16 = 37380;
|
||||
pub const MAX_APERTURE: u16 = 37381;
|
||||
pub const METERING_MODE: u16 = 37383;
|
||||
pub const LIGHT_SOURCE: u16 = 37384;
|
||||
pub const FLASH: u16 = 37385;
|
||||
pub const FOCAL_LENGTH: u16 = 37386;
|
||||
pub const MAKER_NOTE: u16 = 37500;
|
||||
pub const USER_COMMENT: u16 = 37510;
|
||||
pub const SUBSEC_TIME: u16 = 37520;
|
||||
pub const SUBSEC_TIME_ORIGINAL: u16 = 37521;
|
||||
pub const SUBSEC_TIME_DIGITIZED: u16 = 37522;
|
||||
pub const PIXEL_X_DIMENSION: u16 = 40962;
|
||||
pub const PIXEL_Y_DIMENSION: u16 = 40963;
|
||||
pub const FOCAL_PLANE_X_RESOLUTION: u16 = 41486;
|
||||
pub const EXPOSURE_MODE: u16 = 41986;
|
||||
pub const WHITE_BALANCE: u16 = 41987;
|
||||
pub const FOCAL_LENGTH_35MM: u16 = 41989;
|
||||
pub const SCENE_CAPTURE_TYPE: u16 = 41990;
|
||||
pub const BODY_SERIAL_NUMBER: u16 = 42033;
|
||||
pub const LENS_SPECIFICATION: u16 = 42034;
|
||||
pub const LENS_MAKE: u16 = 42035;
|
||||
pub const LENS_MODEL: u16 = 42036;
|
||||
pub const LENS_SERIAL_NUMBER: u16 = 42037;
|
||||
|
||||
// --- GPS IFD
|
||||
pub const GPS_VERSION_ID: u16 = 0;
|
||||
pub const GPS_LATITUDE_REF: u16 = 1;
|
||||
pub const GPS_LATITUDE: u16 = 2;
|
||||
pub const GPS_LONGITUDE_REF: u16 = 3;
|
||||
pub const GPS_LONGITUDE: u16 = 4;
|
||||
pub const GPS_ALTITUDE_REF: u16 = 5;
|
||||
pub const GPS_ALTITUDE: u16 = 6;
|
||||
pub const GPS_TIME_STAMP: u16 = 7;
|
||||
pub const GPS_IMG_DIRECTION_REF: u16 = 16;
|
||||
pub const GPS_IMG_DIRECTION: u16 = 17;
|
||||
pub const GPS_DATE_STAMP: u16 = 29;
|
||||
|
||||
// --- DNG
|
||||
pub const DNG_VERSION: u16 = 50706;
|
||||
pub const DNG_BACKWARD_VERSION: u16 = 50707;
|
||||
pub const UNIQUE_CAMERA_MODEL: u16 = 50708;
|
||||
pub const LOCALIZED_CAMERA_MODEL: u16 = 50709;
|
||||
pub const CFA_PLANE_COLOR: u16 = 50710;
|
||||
pub const CFA_LAYOUT: u16 = 50711;
|
||||
pub const LINEARIZATION_TABLE: u16 = 50712;
|
||||
pub const BLACK_LEVEL_REPEAT_DIM: u16 = 50713;
|
||||
pub const BLACK_LEVEL: u16 = 50714;
|
||||
pub const BLACK_LEVEL_DELTA_H: u16 = 50715;
|
||||
pub const BLACK_LEVEL_DELTA_V: u16 = 50716;
|
||||
pub const WHITE_LEVEL: u16 = 50717;
|
||||
pub const DEFAULT_SCALE: u16 = 50718;
|
||||
pub const DEFAULT_CROP_ORIGIN: u16 = 50719;
|
||||
pub const DEFAULT_CROP_SIZE: u16 = 50720;
|
||||
pub const COLOR_MATRIX_1: u16 = 50721;
|
||||
pub const COLOR_MATRIX_2: u16 = 50722;
|
||||
pub const CAMERA_CALIBRATION_1: u16 = 50723;
|
||||
pub const CAMERA_CALIBRATION_2: u16 = 50724;
|
||||
pub const REDUCTION_MATRIX_1: u16 = 50725;
|
||||
pub const REDUCTION_MATRIX_2: u16 = 50726;
|
||||
pub const ANALOG_BALANCE: u16 = 50727;
|
||||
pub const AS_SHOT_NEUTRAL: u16 = 50728;
|
||||
pub const AS_SHOT_WHITE_XY: u16 = 50729;
|
||||
pub const BASELINE_EXPOSURE: u16 = 50730;
|
||||
pub const BASELINE_NOISE: u16 = 50731;
|
||||
pub const BASELINE_SHARPNESS: u16 = 50732;
|
||||
pub const BAYER_GREEN_SPLIT: u16 = 50733;
|
||||
pub const LINEAR_RESPONSE_LIMIT: u16 = 50734;
|
||||
pub const CAMERA_SERIAL_NUMBER: u16 = 50735;
|
||||
pub const LENS_INFO: u16 = 50736;
|
||||
pub const CHROMA_BLUR_RADIUS: u16 = 50737;
|
||||
pub const ANTI_ALIAS_STRENGTH: u16 = 50738;
|
||||
pub const SHADOW_SCALE: u16 = 50739;
|
||||
pub const DNG_PRIVATE_DATA: u16 = 50740;
|
||||
pub const MAKER_NOTE_SAFETY: u16 = 50741;
|
||||
pub const CALIBRATION_ILLUMINANT_1: u16 = 50778;
|
||||
pub const CALIBRATION_ILLUMINANT_2: u16 = 50779;
|
||||
pub const BEST_QUALITY_SCALE: u16 = 50780;
|
||||
pub const RAW_DATA_UNIQUE_ID: u16 = 50781;
|
||||
pub const ORIGINAL_RAW_FILE_NAME: u16 = 50827;
|
||||
pub const ACTIVE_AREA: u16 = 50829;
|
||||
pub const MASKED_AREAS: u16 = 50830;
|
||||
pub const AS_SHOT_ICC_PROFILE: u16 = 50831;
|
||||
pub const COLORIMETRIC_REFERENCE: u16 = 50879;
|
||||
pub const CAMERA_CALIBRATION_SIGNATURE: u16 = 50931;
|
||||
pub const PROFILE_CALIBRATION_SIGNATURE: u16 = 50932;
|
||||
pub const AS_SHOT_PROFILE_NAME: u16 = 50934;
|
||||
pub const PROFILE_NAME: u16 = 50936;
|
||||
pub const BASELINE_EXPOSURE_OFFSET: u16 = 51109;
|
||||
pub const DEFAULT_BLACK_RENDER: u16 = 51110;
|
||||
pub const FORWARD_MATRIX_1: u16 = 50964;
|
||||
pub const FORWARD_MATRIX_2: u16 = 50965;
|
||||
pub const PREVIEW_COLOR_SPACE: u16 = 50970;
|
||||
pub const CURRENT_PREVIEW_DATE_TIME: u16 = 50971;
|
||||
pub const OPCODE_LIST_1: u16 = 51008;
|
||||
pub const OPCODE_LIST_2: u16 = 51009;
|
||||
pub const OPCODE_LIST_3: u16 = 51022;
|
||||
pub const NOISE_PROFILE: u16 = 51041;
|
||||
pub const DEFAULT_USER_CROP: u16 = 51125;
|
||||
pub const CALIBRATION_ILLUMINANT_3: u16 = 52529;
|
||||
pub const CAMERA_CALIBRATION_3: u16 = 52530;
|
||||
pub const COLOR_MATRIX_3: u16 = 52531;
|
||||
pub const FORWARD_MATRIX_3: u16 = 52532;
|
||||
pub const ILLUMINANT_DATA_1: u16 = 52533;
|
||||
pub const ILLUMINANT_DATA_2: u16 = 52534;
|
||||
pub const ILLUMINANT_DATA_3: u16 = 52535;
|
||||
|
||||
// --- Compression codes (TIFF 6.0, DNG, and widely used registrations)
|
||||
pub mod compression {
|
||||
pub const NONE: u16 = 1;
|
||||
pub const LZW: u16 = 5;
|
||||
pub const OLD_JPEG: u16 = 6;
|
||||
pub const JPEG: u16 = 7;
|
||||
pub const ADOBE_DEFLATE: u16 = 8;
|
||||
pub const DEFLATE: u16 = 32946;
|
||||
pub const PACKBITS: u16 = 32773;
|
||||
/// DNG lossy JPEG (baseline DCT, 8-bit).
|
||||
pub const LOSSY_JPEG: u16 = 34892;
|
||||
pub const JPEG_XL: u16 = 52546;
|
||||
/// Nikon NEF Huffman compression.
|
||||
pub const NIKON: u16 = 34713;
|
||||
/// Sony ARW compressed (the value used by ARW files).
|
||||
pub const SONY_ARW: u16 = 32767;
|
||||
}
|
||||
|
||||
// --- PhotometricInterpretation
|
||||
pub mod photometric {
|
||||
pub const WHITE_IS_ZERO: u16 = 0;
|
||||
pub const BLACK_IS_ZERO: u16 = 1;
|
||||
pub const RGB: u16 = 2;
|
||||
pub const YCBCR: u16 = 6;
|
||||
pub const CFA: u16 = 32803;
|
||||
pub const LINEAR_RAW: u16 = 34892;
|
||||
}
|
||||
|
|
@ -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,395 @@
|
|||
use lightcraft_tiff::image::{Layout, chunk_bytes};
|
||||
use lightcraft_tiff::writer::{rational, srational};
|
||||
use lightcraft_tiff::{ByteOrder, FieldType, IfdBuilder, ImageData, ParseOptions, Tiff, TiffError, TiffWriter, Value, parse_ifd_at, tags};
|
||||
use proptest::prelude::*;
|
||||
|
||||
const ORDERS: [ByteOrder; 2] = [ByteOrder::Little, ByteOrder::Big];
|
||||
|
||||
fn all_values() -> Vec<(u16, Value)> {
|
||||
vec![
|
||||
(1000, Value::Byte(vec![1, 2, 3, 255])),
|
||||
(1001, Value::Ascii("hello tiff".into())),
|
||||
(1002, Value::Short(vec![1, 65535, 3])),
|
||||
(1003, Value::Long(vec![7])),
|
||||
(1004, Value::Rational(vec![(1, 3), (22, 7)])),
|
||||
(1005, Value::SByte(vec![-1, 0, 127, -128])),
|
||||
(1006, Value::Undefined(vec![9; 17])),
|
||||
(1007, Value::SShort(vec![-32768, 32767])),
|
||||
(1008, Value::SLong(vec![-5, i32::MAX])),
|
||||
(1009, Value::SRational(vec![(-1, 3), (5, -2)])),
|
||||
(1010, Value::Float(vec![1.5, -0.25, f32::MAX])),
|
||||
(1011, Value::Double(vec![std::f64::consts::PI])),
|
||||
(1012, Value::Ifd(vec![0])),
|
||||
(1013, Value::Short(vec![42])),
|
||||
(1014, Value::Ascii(String::new())),
|
||||
]
|
||||
}
|
||||
|
||||
fn big_values() -> Vec<(u16, Value)> {
|
||||
vec![(1020, Value::Long8(vec![1 << 40, 3])), (1021, Value::SLong8(vec![-(1 << 50)])), (1022, Value::Ifd8(vec![0]))]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_field_types_roundtrip_every_variant() {
|
||||
for order in ORDERS {
|
||||
for big in [false, true] {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
for (t, v) in all_values() {
|
||||
ifd.set(t, v);
|
||||
}
|
||||
if big {
|
||||
for (t, v) in big_values() {
|
||||
ifd.set(t, v);
|
||||
}
|
||||
}
|
||||
let bytes = TiffWriter::new(order, big).write(&[ifd]).unwrap();
|
||||
let t = Tiff::parse(&bytes).unwrap();
|
||||
assert_eq!(t.order, order);
|
||||
assert_eq!(t.bigtiff, big);
|
||||
assert_eq!(Tiff::sniff(&bytes), Some((order, big)));
|
||||
let ifd0 = &t.ifds[0];
|
||||
for (tag, v) in all_values().into_iter().chain(if big { big_values() } else { vec![] }) {
|
||||
assert_eq!(ifd0.value(tag), Some(&v), "tag {tag} order {order:?} big {big}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors() {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(1, Value::Rational(vec![(28, 10), (0, 0)]));
|
||||
ifd.set(2, Value::Short(vec![5, 6]));
|
||||
ifd.set(3, Value::SLong(vec![-3]));
|
||||
ifd.set(4, Value::Ascii("Canon ".into()));
|
||||
ifd.set(5, Value::Byte(b"<x:xmpmeta/>\0junk".to_vec()));
|
||||
let bytes = TiffWriter::default().write(&[ifd]).unwrap();
|
||||
let t = Tiff::parse(&bytes).unwrap();
|
||||
let i = &t.ifds[0];
|
||||
assert_eq!(i.f64(1), Some(2.8));
|
||||
assert_eq!(i.f64s(1).unwrap(), vec![2.8, 0.0]);
|
||||
assert_eq!(i.u32(2), Some(5));
|
||||
assert_eq!(i.u64s(2).unwrap(), vec![5, 6]);
|
||||
assert_eq!(i.u32(3), None);
|
||||
assert_eq!(i.i64(3), Some(-3));
|
||||
assert_eq!(i.string(4).as_deref(), Some("Canon"));
|
||||
assert_eq!(i.string(5).as_deref(), Some("<x:xmpmeta/>"));
|
||||
assert_eq!(i.get(4).unwrap().field_type(), FieldType::Ascii);
|
||||
assert!(i.u32(999).is_none());
|
||||
assert_eq!(t.find(2).map(|e| e.tag), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain_subifds_and_pointer_ifds() {
|
||||
for order in ORDERS {
|
||||
for big in [false, true] {
|
||||
let mut interop = IfdBuilder::new();
|
||||
interop.set(1, Value::Ascii("R98".into()));
|
||||
let mut exif = IfdBuilder::new();
|
||||
exif.set(tags::F_NUMBER, Value::Rational(vec![(56, 10)]));
|
||||
exif.set_child(tags::INTEROP_IFD, interop);
|
||||
let mut gps = IfdBuilder::new();
|
||||
gps.set(tags::GPS_LATITUDE_REF, Value::Ascii("N".into()));
|
||||
let mut ifd0 = IfdBuilder::new();
|
||||
ifd0.set(tags::MAKE, Value::Ascii("Maker".into()));
|
||||
ifd0.set_child(tags::EXIF_IFD, exif);
|
||||
ifd0.set_child(tags::GPS_IFD, gps);
|
||||
let mut raw = IfdBuilder::new();
|
||||
raw.set(tags::IMAGE_WIDTH, Value::Long(vec![4]));
|
||||
let mut nested = IfdBuilder::new();
|
||||
nested.set(tags::IMAGE_WIDTH, Value::Long(vec![2]));
|
||||
raw.add_sub_ifd(nested);
|
||||
ifd0.add_sub_ifd(raw);
|
||||
ifd0.add_sub_ifd(IfdBuilder::new().with(tags::IMAGE_WIDTH, Value::Short(vec![3])));
|
||||
let ifd1 = IfdBuilder::new().with(tags::IMAGE_WIDTH, Value::Short(vec![160]));
|
||||
let ifd2 = IfdBuilder::new().with(tags::IMAGE_WIDTH, Value::Short(vec![80]));
|
||||
let bytes = TiffWriter::new(order, big).write(&[ifd0, ifd1, ifd2]).unwrap();
|
||||
let t = Tiff::parse(&bytes).unwrap();
|
||||
assert_eq!(t.ifds.len(), 3);
|
||||
assert_eq!(t.ifds[1].u32(tags::IMAGE_WIDTH), Some(160));
|
||||
assert_eq!(t.ifds[2].u32(tags::IMAGE_WIDTH), Some(80));
|
||||
let i0 = &t.ifds[0];
|
||||
assert_eq!(i0.sub_ifds.len(), 2);
|
||||
assert_eq!(i0.sub_ifds[0].u32(tags::IMAGE_WIDTH), Some(4));
|
||||
assert_eq!(i0.sub_ifds[0].sub_ifds[0].u32(tags::IMAGE_WIDTH), Some(2));
|
||||
assert_eq!(i0.sub_ifds[1].u32(tags::IMAGE_WIDTH), Some(3));
|
||||
let exif = t.exif().unwrap();
|
||||
assert_eq!(exif.f64(tags::F_NUMBER), Some(5.6));
|
||||
assert_eq!(exif.interop.as_ref().unwrap().string(1).as_deref(), Some("R98"));
|
||||
assert_eq!(t.gps().unwrap().string(tags::GPS_LATITUDE_REF).as_deref(), Some("N"));
|
||||
// IFD0, sub0, sub0.sub, sub1, exif, interop, gps, ifd1, ifd2
|
||||
assert_eq!(t.all_ifds().len(), 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gradient(w: usize, h: usize) -> Vec<u8> {
|
||||
(0..w * h).map(|i| (i * 7 % 251) as u8).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_layout_and_bytes() {
|
||||
let (w, h) = (10usize, 7usize);
|
||||
let px = gradient(w, h);
|
||||
let rps = 3;
|
||||
let strips: Vec<Vec<u8>> = px.chunks(w * rps).map(|c| c.to_vec()).collect();
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(tags::IMAGE_WIDTH, Value::Long(vec![w as u32]));
|
||||
ifd.set(tags::IMAGE_LENGTH, Value::Long(vec![h as u32]));
|
||||
ifd.set(tags::BITS_PER_SAMPLE, Value::Short(vec![8]));
|
||||
ifd.set(tags::COMPRESSION, Value::Short(vec![1]));
|
||||
ifd.set_image(ImageData::Strips { rows_per_strip: rps as u32, strips });
|
||||
for order in ORDERS {
|
||||
for big in [false, true] {
|
||||
let bytes = TiffWriter::new(order, big).write(std::slice::from_ref(&ifd)).unwrap();
|
||||
let t = Tiff::parse(&bytes).unwrap();
|
||||
let info = t.ifds[0].image().unwrap();
|
||||
assert_eq!(info.layout, Layout::Strips { rows_per_strip: 3 });
|
||||
assert_eq!(info.grid(), (1, 3));
|
||||
let chunks = info.chunks(bytes.len() as u64);
|
||||
assert_eq!(chunks.len(), 3);
|
||||
assert_eq!(chunks[2].height, 1);
|
||||
let mut out = Vec::new();
|
||||
for c in &chunks {
|
||||
out.extend_from_slice(chunk_bytes(&bytes, c).unwrap());
|
||||
}
|
||||
assert_eq!(out, px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiles_layout_and_bytes() {
|
||||
let (w, h, tw, th) = (20usize, 13usize, 16usize, 8usize);
|
||||
let px = gradient(w, h);
|
||||
let (ta, td) = (w.div_ceil(tw), h.div_ceil(th));
|
||||
let mut tiles = Vec::new();
|
||||
for ty in 0..td {
|
||||
for tx in 0..ta {
|
||||
let mut t = vec![0u8; tw * th];
|
||||
for y in 0..th {
|
||||
for x in 0..tw {
|
||||
let (sx, sy) = (tx * tw + x, ty * th + y);
|
||||
if sx < w && sy < h {
|
||||
t[y * tw + x] = px[sy * w + sx];
|
||||
}
|
||||
}
|
||||
}
|
||||
tiles.push(t);
|
||||
}
|
||||
}
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(tags::IMAGE_WIDTH, Value::Short(vec![w as u16]));
|
||||
ifd.set(tags::IMAGE_LENGTH, Value::Short(vec![h as u16]));
|
||||
ifd.set_image(ImageData::Tiles { tile_width: tw as u32, tile_height: th as u32, tiles });
|
||||
let bytes = TiffWriter::new(ByteOrder::Big, false).write(&[ifd]).unwrap();
|
||||
let t = Tiff::parse(&bytes).unwrap();
|
||||
let info = t.ifds[0].image().unwrap();
|
||||
assert_eq!(info.grid(), (2, 2));
|
||||
let chunks = info.chunks(bytes.len() as u64);
|
||||
assert_eq!(chunks.len(), 4);
|
||||
let mut out = vec![0u8; w * h];
|
||||
for c in &chunks {
|
||||
let b = chunk_bytes(&bytes, c).unwrap();
|
||||
for y in 0..c.height as usize {
|
||||
for x in 0..c.width as usize {
|
||||
let (sx, sy) = (c.x as usize + x, c.y as usize + y);
|
||||
if sx < w && sy < h {
|
||||
out[sy * w + sx] = b[y * tw + x];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(out, px);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_byte_counts_are_inferred() {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(tags::IMAGE_WIDTH, Value::Long(vec![4]));
|
||||
ifd.set(tags::IMAGE_LENGTH, Value::Long(vec![2]));
|
||||
ifd.set_image(ImageData::Strips { rows_per_strip: 1, strips: vec![vec![1; 4], vec![2; 4]] });
|
||||
let bytes = TiffWriter::default().write(&[ifd]).unwrap();
|
||||
let t = Tiff::parse(&bytes).unwrap();
|
||||
let mut info = t.ifds[0].image().unwrap();
|
||||
info.byte_counts.clear();
|
||||
let c = info.chunks(bytes.len() as u64);
|
||||
assert_eq!(c[0].len, 4);
|
||||
assert_eq!(&chunk_bytes(&bytes, &c[0]).unwrap()[..4], &[1; 4]);
|
||||
assert!(c[1].len >= 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_headers() {
|
||||
assert_eq!(Tiff::parse(b""), Err(TiffError::NotTiff));
|
||||
assert_eq!(Tiff::parse(b"XX*\0\0\0\0\0"), Err(TiffError::NotTiff));
|
||||
assert_eq!(Tiff::parse(b"II\x2b\0\x04\0\0\0"), Err(TiffError::NotTiff));
|
||||
assert!(Tiff::parse(b"II*\0").is_err());
|
||||
assert!(Tiff::parse(b"II*\0\x08\0\0\0").is_err());
|
||||
assert!(Tiff::parse(b"II*\0\x08\0\0\0\0\0").is_err());
|
||||
assert!(Tiff::sniff(b"MM\0*\0\0\0\x08").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ifd_loops_are_detected() {
|
||||
// IFD0 at 8 whose next pointer points back to itself, and whose SubIFDs point to itself too.
|
||||
let mut b = b"II*\0\x08\0\0\0".to_vec();
|
||||
b.extend_from_slice(&1u16.to_le_bytes());
|
||||
b.extend_from_slice(&330u16.to_le_bytes());
|
||||
b.extend_from_slice(&4u16.to_le_bytes());
|
||||
b.extend_from_slice(&1u32.to_le_bytes());
|
||||
b.extend_from_slice(&8u32.to_le_bytes());
|
||||
b.extend_from_slice(&8u32.to_le_bytes());
|
||||
let t = Tiff::parse(&b).unwrap();
|
||||
assert_eq!(t.ifds.len(), 1);
|
||||
assert!(t.ifds[0].sub_ifds.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_entries_are_skipped() {
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(1, Value::Long(vec![1, 2, 3]));
|
||||
ifd.set(2, Value::Short(vec![5]));
|
||||
let mut bytes = TiffWriter::default().write(&[ifd]).unwrap();
|
||||
let t = Tiff::parse(&bytes).unwrap();
|
||||
let e = t.ifds[0].get(1).unwrap().clone();
|
||||
// point tag 1's value offset past EOF
|
||||
let ifd_off = t.ifds[0].offset as usize;
|
||||
let field = ifd_off + 2 + 8;
|
||||
assert_eq!(u32::from_le_bytes(bytes[field..field + 4].try_into().unwrap()) as u64, e.offset);
|
||||
bytes[field..field + 4].copy_from_slice(&0xffff_fff0u32.to_le_bytes());
|
||||
let t = Tiff::parse(&bytes).unwrap();
|
||||
assert!(t.ifds[0].get(1).is_none());
|
||||
assert_eq!(t.ifds[0].u32(2), Some(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_budget_limits_allocation() {
|
||||
// 50 entries all pointing at the same 1000-byte blob.
|
||||
let mut ifd = IfdBuilder::new();
|
||||
ifd.set(1, Value::Undefined(vec![7; 1000]));
|
||||
let bytes = TiffWriter::default().write(&[ifd]).unwrap();
|
||||
let t = Tiff::parse(&bytes).unwrap();
|
||||
let blob = t.ifds[0].get(1).unwrap().offset as u32;
|
||||
let mut b = b"II*\0".to_vec();
|
||||
b.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
||||
let mut file = bytes.clone();
|
||||
file[..8].copy_from_slice(&b);
|
||||
file.extend_from_slice(&50u16.to_le_bytes());
|
||||
for i in 0..50u16 {
|
||||
file.extend_from_slice(&i.to_le_bytes());
|
||||
file.extend_from_slice(&7u16.to_le_bytes());
|
||||
file.extend_from_slice(&1000u32.to_le_bytes());
|
||||
file.extend_from_slice(&blob.to_le_bytes());
|
||||
}
|
||||
file.extend_from_slice(&0u32.to_le_bytes());
|
||||
let opts = ParseOptions { value_budget: Some(10_000), ..Default::default() };
|
||||
let t = Tiff::parse_with(&file, &opts).unwrap();
|
||||
assert_eq!(t.ifds[0].entries.len(), 10);
|
||||
let t = Tiff::parse(&file).unwrap();
|
||||
assert_eq!(t.ifds[0].entries.len(), 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ifd_with_base() {
|
||||
// an IFD blob embedded at 100 whose offsets are relative to 100
|
||||
let mut blob = Vec::new();
|
||||
let o = ByteOrder::Big;
|
||||
o.put_u16(&mut blob, 1);
|
||||
o.put_u16(&mut blob, 5);
|
||||
o.put_u16(&mut blob, 4);
|
||||
o.put_u32(&mut blob, 3);
|
||||
o.put_u32(&mut blob, 18);
|
||||
o.put_u32(&mut blob, 0);
|
||||
for v in [10u32, 20, 30] {
|
||||
o.put_u32(&mut blob, v);
|
||||
}
|
||||
let mut data = vec![0u8; 100];
|
||||
data.extend(blob);
|
||||
let (ifd, next) = parse_ifd_at(&data, 100, o, 100, false, &ParseOptions::default()).unwrap();
|
||||
assert_eq!(next, 0);
|
||||
assert_eq!(ifd.u64s(5).unwrap(), vec![10, 20, 30]);
|
||||
assert_eq!(ifd.get(5).unwrap().offset, 118);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rationals() {
|
||||
assert_eq!(rational(2.8), (28, 10));
|
||||
assert_eq!(rational(0.004), (4, 1000));
|
||||
assert_eq!(rational(-1.0), (0, 1));
|
||||
assert_eq!(srational(-0.333), (-333, 1000));
|
||||
assert_eq!(srational(7.0), (7, 1));
|
||||
let (n, d) = rational(1.0 / 3.0);
|
||||
assert!((n as f64 / d as f64 - 1.0 / 3.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
fn sample_file() -> Vec<u8> {
|
||||
let mut exif = IfdBuilder::new();
|
||||
exif.set(tags::F_NUMBER, Value::Rational(vec![(4, 1)]));
|
||||
exif.set(tags::MAKER_NOTE, Value::Undefined(b"Nikon\0\x02\x10\0\0II*\0\x08\0\0\0\x01\0\x01\0\x03\0\x01\0\0\0\x05\0\0\0\0\0\0\0".to_vec()));
|
||||
let mut ifd0 = IfdBuilder::new();
|
||||
ifd0.set(tags::IMAGE_WIDTH, Value::Long(vec![8]));
|
||||
ifd0.set(tags::IMAGE_LENGTH, Value::Long(vec![4]));
|
||||
ifd0.set(tags::MAKE, Value::Ascii("NIKON".into()));
|
||||
ifd0.set_child(tags::EXIF_IFD, exif);
|
||||
ifd0.set_image(ImageData::Strips { rows_per_strip: 2, strips: vec![vec![1; 16], vec![2; 16]] });
|
||||
ifd0.add_sub_ifd(IfdBuilder::new().with(tags::IMAGE_WIDTH, Value::Long(vec![2])));
|
||||
TiffWriter::default().write(&[ifd0, IfdBuilder::new().with(1, Value::Short(vec![1]))]).unwrap()
|
||||
}
|
||||
|
||||
fn exercise(t: &Tiff, data: &[u8]) {
|
||||
for ifd in t.all_ifds() {
|
||||
if let Ok(info) = ifd.image() {
|
||||
for c in info.chunks(data.len() as u64).iter().take(64) {
|
||||
let _ = chunk_bytes(data, c);
|
||||
}
|
||||
}
|
||||
for e in &ifd.entries {
|
||||
let _ = e.value.to_f64_vec();
|
||||
let _ = e.value.as_str();
|
||||
}
|
||||
}
|
||||
if let Some(e) = t.exif().and_then(|x| x.get(tags::MAKER_NOTE)) {
|
||||
let _ = lightcraft_tiff::makernote::parse_makernote(data, e.offset, e.count() as u64, t.order, "NIKON");
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig { cases: 2000, .. ProptestConfig::default() })]
|
||||
|
||||
#[test]
|
||||
fn random_bytes_never_panic(mut data in proptest::collection::vec(any::<u8>(), 0..512), hdr in 0usize..4) {
|
||||
let heads: [&[u8]; 4] = [b"II*\0", b"MM\0*", b"II+\0\x08\0\0\0", b"MM\0+\0\x08\0\0"];
|
||||
let h = heads[hdr];
|
||||
if data.len() >= h.len() {
|
||||
data[..h.len()].copy_from_slice(h);
|
||||
}
|
||||
if let Ok(t) = Tiff::parse(&data) {
|
||||
exercise(&t, &data);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutated_files_never_panic(flips in proptest::collection::vec((any::<usize>(), any::<u8>()), 1..16), cut in any::<usize>()) {
|
||||
let mut data = sample_file();
|
||||
for (i, v) in flips {
|
||||
let n = data.len();
|
||||
data[i % n] = v;
|
||||
}
|
||||
let keep = if cut % 4 == 0 { cut % data.len() } else { data.len() };
|
||||
let data = &data[..keep];
|
||||
if let Ok(t) = Tiff::parse(data) {
|
||||
exercise(&t, data);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_short_arrays_roundtrip(v in proptest::collection::vec(any::<u16>(), 1..300), big in any::<bool>(), le in any::<bool>()) {
|
||||
let order = if le { ByteOrder::Little } else { ByteOrder::Big };
|
||||
let ifd = IfdBuilder::new().with(500, Value::Short(v.clone())).with(501, Value::Double(v.iter().map(|&x| x as f64 / 3.0).collect()));
|
||||
let bytes = TiffWriter::new(order, big).write(&[ifd]).unwrap();
|
||||
let t = Tiff::parse(&bytes).unwrap();
|
||||
prop_assert_eq!(t.ifds[0].value(500), Some(&Value::Short(v.clone())));
|
||||
prop_assert_eq!(t.ifds[0].f64s(501).unwrap().len(), v.len());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue