Skip to main content

exif_oxide/
tiff_types.rs

1//! TIFF format types and structures
2//!
3//! This module contains the fundamental TIFF data structures used throughout the EXIF parsing
4//! process, translating ExifTool's TIFF format definitions from lib/Image/ExifTool/Exif.pm.
5//! These types handle byte order detection, format validation, and IFD entry parsing.
6
7use crate::types::{ExifError, Result};
8
9/// TIFF format types mapping to ExifTool's format system
10/// ExifTool: lib/Image/ExifTool/Exif.pm @formatName array
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[repr(u8)]
13pub enum TiffFormat {
14    Byte = 1,       // int8u
15    Ascii = 2,      // string
16    Short = 3,      // int16u
17    Long = 4,       // int32u
18    Rational = 5,   // rational64u
19    SByte = 6,      // int8s
20    Undefined = 7,  // undef
21    SShort = 8,     // int16s
22    SLong = 9,      // int32s
23    SRational = 10, // rational64s
24    Float = 11,     // float
25    Double = 12,    // double
26    Ifd = 13,       // ifd
27}
28
29impl TiffFormat {
30    /// Get byte size for this format type
31    /// ExifTool: lib/Image/ExifTool/Exif.pm @formatSize array
32    pub fn byte_size(self) -> usize {
33        match self {
34            TiffFormat::Byte | TiffFormat::Ascii | TiffFormat::SByte | TiffFormat::Undefined => 1,
35            TiffFormat::Short | TiffFormat::SShort => 2,
36            TiffFormat::Long | TiffFormat::SLong | TiffFormat::Float | TiffFormat::Ifd => 4,
37            TiffFormat::Rational | TiffFormat::SRational | TiffFormat::Double => 8,
38        }
39    }
40
41    /// Create from format number, following ExifTool's validation
42    /// ExifTool: lib/Image/ExifTool/Exif.pm:6352 format validation
43    pub fn from_u16(format: u16) -> Result<Self> {
44        match format {
45            1 => Ok(TiffFormat::Byte),
46            2 => Ok(TiffFormat::Ascii),
47            3 => Ok(TiffFormat::Short),
48            4 => Ok(TiffFormat::Long),
49            5 => Ok(TiffFormat::Rational),
50            6 => Ok(TiffFormat::SByte),
51            7 => Ok(TiffFormat::Undefined),
52            8 => Ok(TiffFormat::SShort),
53            9 => Ok(TiffFormat::SLong),
54            10 => Ok(TiffFormat::SRational),
55            11 => Ok(TiffFormat::Float),
56            12 => Ok(TiffFormat::Double),
57            13 => Ok(TiffFormat::Ifd),
58            _ => Err(ExifError::ParseError(format!(
59                "Invalid TIFF format type: {format}"
60            ))),
61        }
62    }
63}
64
65/// Byte order for TIFF data
66/// ExifTool: lib/Image/ExifTool/Exif.pm TIFF header validation
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum ByteOrder {
69    LittleEndian, // "II" - Intel format
70    BigEndian,    // "MM" - Motorola format
71}
72
73impl ByteOrder {
74    /// Read u16 value respecting byte order
75    pub fn read_u16(self, data: &[u8], offset: usize) -> Result<u16> {
76        if offset + 2 > data.len() {
77            return Err(ExifError::ParseError("Not enough data for u16".to_string()));
78        }
79        let bytes = &data[offset..offset + 2];
80        Ok(match self {
81            ByteOrder::LittleEndian => u16::from_le_bytes([bytes[0], bytes[1]]),
82            ByteOrder::BigEndian => u16::from_be_bytes([bytes[0], bytes[1]]),
83        })
84    }
85
86    /// Read u32 value respecting byte order  
87    pub fn read_u32(self, data: &[u8], offset: usize) -> Result<u32> {
88        if offset + 4 > data.len() {
89            return Err(ExifError::ParseError("Not enough data for u32".to_string()));
90        }
91        let bytes = &data[offset..offset + 4];
92        Ok(match self {
93            ByteOrder::LittleEndian => u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
94            ByteOrder::BigEndian => u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
95        })
96    }
97}
98
99/// TIFF header structure
100/// ExifTool: lib/Image/ExifTool/Exif.pm TIFF header validation
101#[derive(Debug, Clone)]
102pub struct TiffHeader {
103    pub byte_order: ByteOrder,
104    pub magic: u16,       // Should be 42 (0x002A) for TIFF or 85 (0x0055) for RW2
105    pub ifd0_offset: u32, // Offset to first IFD
106}
107
108impl TiffHeader {
109    /// Parse TIFF header from data
110    /// ExifTool: lib/Image/ExifTool/Exif.pm:6174-6248 header processing
111    pub fn parse(data: &[u8]) -> Result<Self> {
112        if data.len() < 8 {
113            return Err(ExifError::ParseError(
114                "TIFF header too short (need 8 bytes)".to_string(),
115            ));
116        }
117
118        // Detect byte order from first 2 bytes
119        let byte_order = match &data[0..2] {
120            [0x49, 0x49] => ByteOrder::LittleEndian, // "II"
121            [0x4D, 0x4D] => ByteOrder::BigEndian,    // "MM"
122            _ => {
123                return Err(ExifError::ParseError(
124                    "Invalid TIFF byte order marker".to_string(),
125                ))
126            }
127        };
128
129        // Read magic number (should be 42 for standard TIFF or 85 for RW2)
130        // ExifTool: Panasonic RW2 files use magic number 85 (0x0055) instead of 42 (0x002A)
131        let magic = byte_order.read_u16(data, 2)?;
132        if magic != 42 && magic != 85 {
133            return Err(ExifError::ParseError(format!(
134                "Invalid TIFF magic number: {magic} (expected 42 or 85 for RW2)"
135            )));
136        }
137
138        // Read IFD0 offset
139        let ifd0_offset = byte_order.read_u32(data, 4)?;
140
141        Ok(TiffHeader {
142            byte_order,
143            magic,
144            ifd0_offset,
145        })
146    }
147}
148
149/// IFD entry structure (12 bytes each)
150/// ExifTool: lib/Image/ExifTool/Exif.pm:6347-6351 entry reading
151#[derive(Debug, Clone)]
152pub struct IfdEntry {
153    pub tag_id: u16,
154    pub format: TiffFormat,
155    pub count: u32,
156    pub value_or_offset: u32,
157}
158
159impl IfdEntry {
160    /// Parse IFD entry from 12-byte data block
161    /// ExifTool: lib/Image/ExifTool/Exif.pm:6348-6350 entry structure
162    pub fn parse(data: &[u8], offset: usize, byte_order: ByteOrder) -> Result<Self> {
163        if offset + 12 > data.len() {
164            return Err(ExifError::ParseError(
165                "Not enough data for IFD entry".to_string(),
166            ));
167        }
168
169        let tag_id = byte_order.read_u16(data, offset)?;
170        let format_num = byte_order.read_u16(data, offset + 2)?;
171        let format = TiffFormat::from_u16(format_num)?;
172        let count = byte_order.read_u32(data, offset + 4)?;
173        let value_or_offset = byte_order.read_u32(data, offset + 8)?;
174
175        Ok(IfdEntry {
176            tag_id,
177            format,
178            count,
179            value_or_offset,
180        })
181    }
182
183    /// Calculate total size of this entry's data
184    /// ExifTool: lib/Image/ExifTool/Exif.pm:6390 size calculation
185    pub fn data_size(&self) -> u32 {
186        // Protect against overflow with large count values
187        self.count.saturating_mul(self.format.byte_size() as u32)
188    }
189
190    /// Check if value is stored inline (≤4 bytes) or as offset
191    /// ExifTool: lib/Image/ExifTool/Exif.pm:6392 inline vs offset logic
192    pub fn is_inline(&self) -> bool {
193        self.data_size() <= 4
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_tiff_format_byte_size() {
203        assert_eq!(TiffFormat::Byte.byte_size(), 1);
204        assert_eq!(TiffFormat::Short.byte_size(), 2);
205        assert_eq!(TiffFormat::Long.byte_size(), 4);
206        assert_eq!(TiffFormat::Rational.byte_size(), 8);
207    }
208
209    #[test]
210    fn test_tiff_format_from_u16() {
211        assert_eq!(TiffFormat::from_u16(1).unwrap(), TiffFormat::Byte);
212        assert_eq!(TiffFormat::from_u16(2).unwrap(), TiffFormat::Ascii);
213        assert_eq!(TiffFormat::from_u16(3).unwrap(), TiffFormat::Short);
214        assert!(TiffFormat::from_u16(99).is_err());
215    }
216
217    #[test]
218    fn test_byte_order_read() {
219        let data = [0x12, 0x34, 0x56, 0x78];
220
221        // Little-endian
222        let le = ByteOrder::LittleEndian;
223        assert_eq!(le.read_u16(&data, 0).unwrap(), 0x3412);
224        assert_eq!(le.read_u32(&data, 0).unwrap(), 0x78563412);
225
226        // Big-endian
227        let be = ByteOrder::BigEndian;
228        assert_eq!(be.read_u16(&data, 0).unwrap(), 0x1234);
229        assert_eq!(be.read_u32(&data, 0).unwrap(), 0x12345678);
230    }
231
232    #[test]
233    fn test_tiff_header_parse() {
234        // Little-endian TIFF header
235        let le_data = [
236            0x49, 0x49, // "II" - little-endian
237            0x2A, 0x00, // Magic: 42 (LE)
238            0x08, 0x00, 0x00, 0x00, // IFD0 offset: 8 (LE)
239        ];
240
241        let header = TiffHeader::parse(&le_data).unwrap();
242        assert_eq!(header.byte_order, ByteOrder::LittleEndian);
243        assert_eq!(header.magic, 42);
244        assert_eq!(header.ifd0_offset, 8);
245
246        // Big-endian TIFF header
247        let be_data = [
248            0x4D, 0x4D, // "MM" - big-endian
249            0x00, 0x2A, // Magic: 42 (BE)
250            0x00, 0x00, 0x00, 0x08, // IFD0 offset: 8 (BE)
251        ];
252
253        let header = TiffHeader::parse(&be_data).unwrap();
254        assert_eq!(header.byte_order, ByteOrder::BigEndian);
255        assert_eq!(header.magic, 42);
256        assert_eq!(header.ifd0_offset, 8);
257    }
258
259    #[test]
260    fn test_ifd_entry_parse() {
261        let data = [
262            0x01, 0x0F, // Tag ID: 0x010F (Make)
263            0x00, 0x02, // Format: 2 (ASCII)
264            0x00, 0x00, 0x00, 0x06, // Count: 6
265            0x00, 0x00, 0x00, 0x26, // Value/Offset: 0x26
266        ];
267
268        let entry = IfdEntry::parse(&data, 0, ByteOrder::BigEndian).unwrap();
269        assert_eq!(entry.tag_id, 0x010F);
270        assert_eq!(entry.format, TiffFormat::Ascii);
271        assert_eq!(entry.count, 6);
272        assert_eq!(entry.value_or_offset, 0x26);
273        assert!(!entry.is_inline()); // 6 bytes > 4, so not inline
274    }
275}