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}