Skip to main content

gamut_ifd/
entry.rs

1//! The container variant, the IFD entries, and the directory that holds them.
2
3use crate::value::Value;
4
5/// Which variant of the TIFF container a stream uses, distinguished by the header magic number.
6///
7/// The two variants share an identical tag/IFD model; they differ only in the width of the
8/// structural fields — BigTIFF widens every file offset (and the IFD entry count and per-field
9/// value count) from 32 to 64 bits so a file may exceed 4 GiB (`references/tiff/bigtiff.html`).
10/// The [`Variant::Big`] arm — and every 64-bit width it selects — exists only when the `bigtiff`
11/// feature is enabled.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum Variant {
14    /// Classic TIFF 6.0: magic `42`, an 8-byte header, 12-byte IFD entries, 32-bit offsets.
15    Classic,
16    /// BigTIFF: magic `43`, a 16-byte header, 20-byte IFD entries, 64-bit offsets.
17    #[cfg(feature = "bigtiff")]
18    Big,
19}
20
21impl Variant {
22    /// The header magic number (`42` for classic, `43` for BigTIFF).
23    #[must_use]
24    pub fn magic(self) -> u16 {
25        match self {
26            Variant::Classic => 42,
27            #[cfg(feature = "bigtiff")]
28            Variant::Big => 43,
29        }
30    }
31
32    /// The size of the image file header in bytes (8 classic, 16 BigTIFF).
33    #[must_use]
34    pub fn header_size(self) -> usize {
35        match self {
36            Variant::Classic => 8,
37            #[cfg(feature = "bigtiff")]
38            Variant::Big => 16,
39        }
40    }
41
42    /// The on-disk size of one IFD entry in bytes (12 classic, 20 BigTIFF).
43    #[must_use]
44    pub fn entry_size(self) -> usize {
45        match self {
46            Variant::Classic => 12,
47            #[cfg(feature = "bigtiff")]
48            Variant::Big => 20,
49        }
50    }
51
52    /// The size of an IFD's leading entry-count field in bytes (2 classic, 8 BigTIFF).
53    #[must_use]
54    pub fn count_size(self) -> usize {
55        match self {
56            Variant::Classic => 2,
57            #[cfg(feature = "bigtiff")]
58            Variant::Big => 8,
59        }
60    }
61
62    /// The size of a file offset (first-IFD pointer, next-IFD pointer, value offset) in bytes
63    /// (4 classic, 8 BigTIFF).
64    #[must_use]
65    pub fn offset_size(self) -> usize {
66        match self {
67            Variant::Classic => 4,
68            #[cfg(feature = "bigtiff")]
69            Variant::Big => 8,
70        }
71    }
72
73    /// The largest value, in bytes, that is stored inline in an IFD entry rather than out of line
74    /// (4 classic, 8 BigTIFF — equal to [`Self::offset_size`]).
75    #[must_use]
76    pub fn inline_threshold(self) -> usize {
77        self.offset_size()
78    }
79}
80
81/// One field (entry) of an Image File Directory: a tag and its value.
82///
83/// On disk this is a 12-byte (classic) or 20-byte (BigTIFF) record — tag, field type, value count,
84/// and a value-or-offset word — but once decoded only the tag and the resolved [`Value`] matter;
85/// the field type and count are recoverable from the value.
86#[derive(Debug, Clone, PartialEq)]
87pub struct Field {
88    /// The 16-bit tag identifying the field (e.g. `256` for `ImageWidth`).
89    pub tag: u16,
90    /// The field's value.
91    pub value: Value,
92}
93
94/// A parsed Image File Directory — one node in the IFD chain (a TIFF page, or an EXIF/GPS/Interop
95/// sub-directory).
96///
97/// Fields are kept sorted in ascending tag order, as required on disk (TIFF 6.0 §2); the accessors
98/// preserve that invariant.
99#[derive(Debug, Clone, Default, PartialEq)]
100pub struct Ifd {
101    fields: Vec<Field>,
102}
103
104impl Ifd {
105    /// Creates an empty directory.
106    #[must_use]
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    /// Returns the directory's fields, sorted by ascending tag.
112    #[must_use]
113    pub fn fields(&self) -> &[Field] {
114        &self.fields
115    }
116
117    /// Returns the value of `tag`, or `None` if absent.
118    #[must_use]
119    pub fn get(&self, tag: u16) -> Option<&Value> {
120        self.fields
121            .binary_search_by_key(&tag, |f| f.tag)
122            .ok()
123            .map(|i| &self.fields[i].value)
124    }
125
126    /// Returns `tag` coerced to a single `u32` (accepting `BYTE`/`SHORT`/`LONG`).
127    #[must_use]
128    pub fn get_u32(&self, tag: u16) -> Option<u32> {
129        self.get(tag).and_then(Value::as_u32)
130    }
131
132    /// Returns `tag` coerced to a `Vec<u32>` (accepting `BYTE`/`SHORT`/`LONG`).
133    #[must_use]
134    pub fn get_u32_vec(&self, tag: u16) -> Option<Vec<u32>> {
135        self.get(tag).and_then(Value::as_u32_vec)
136    }
137
138    /// Inserts or replaces the value of `tag`, keeping the fields sorted.
139    pub fn set(&mut self, tag: u16, value: Value) {
140        match self.fields.binary_search_by_key(&tag, |f| f.tag) {
141            Ok(i) => self.fields[i].value = value,
142            Err(i) => self.fields.insert(i, Field { tag, value }),
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn variant_layout_constants() {
153        assert_eq!(Variant::Classic.magic(), 42);
154        assert_eq!(Variant::Classic.header_size(), 8);
155        assert_eq!(Variant::Classic.entry_size(), 12);
156        assert_eq!(Variant::Classic.count_size(), 2);
157        assert_eq!(Variant::Classic.offset_size(), 4);
158        // The inline threshold equals the offset size: values up to that many bytes pack inline.
159        assert_eq!(Variant::Classic.inline_threshold(), 4);
160    }
161
162    #[cfg(feature = "bigtiff")]
163    #[test]
164    fn bigtiff_variant_layout_constants() {
165        assert_eq!(Variant::Big.magic(), 43);
166        assert_eq!(Variant::Big.header_size(), 16);
167        assert_eq!(Variant::Big.entry_size(), 20);
168        assert_eq!(Variant::Big.count_size(), 8);
169        assert_eq!(Variant::Big.offset_size(), 8);
170        assert_eq!(Variant::Big.inline_threshold(), 8);
171    }
172
173    #[test]
174    fn ifd_keeps_fields_sorted_and_replaces() {
175        // Tag numbers are used literally here: tag semantics live in the consuming codec
176        // (e.g. gamut-tiff's `tags` module), not in this structural core. 256/257/259 are
177        // ImageWidth/ImageLength/Compression.
178        let mut ifd = Ifd::new();
179        ifd.set(259, Value::Short(vec![1]));
180        ifd.set(256, Value::Short(vec![4]));
181        ifd.set(257, Value::Short(vec![3]));
182        let order: Vec<u16> = ifd.fields().iter().map(|f| f.tag).collect();
183        assert_eq!(order, vec![256, 257, 259]);
184        ifd.set(256, Value::Short(vec![8]));
185        assert_eq!(ifd.get_u32(256), Some(8));
186        assert_eq!(ifd.fields().len(), 3);
187    }
188}