Skip to main content

tiff_reader/
header.rs

1use crate::error::{Error, Result};
2
3/// Byte order indicator from the TIFF header.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum ByteOrder {
6    LittleEndian,
7    BigEndian,
8}
9
10/// Parsed TIFF/BigTIFF file header.
11#[derive(Debug, Clone)]
12pub struct TiffHeader {
13    pub byte_order: ByteOrder,
14    /// 42 for classic TIFF, 43 for BigTIFF.
15    pub version: u16,
16    /// Offset to the first IFD.
17    pub first_ifd_offset: u64,
18}
19
20impl TiffHeader {
21    /// Returns `true` if this is a BigTIFF file (version 43).
22    pub fn is_bigtiff(&self) -> bool {
23        self.version == 43
24    }
25
26    /// Parse the TIFF header from raw bytes.
27    pub fn parse(data: &[u8]) -> Result<Self> {
28        if data.len() < 8 {
29            return Err(Error::InvalidMagic);
30        }
31
32        let byte_order = match &data[0..2] {
33            b"II" => ByteOrder::LittleEndian,
34            b"MM" => ByteOrder::BigEndian,
35            _ => return Err(Error::InvalidMagic),
36        };
37
38        let read_u16 = |offset: usize| -> u16 {
39            let bytes = [data[offset], data[offset + 1]];
40            match byte_order {
41                ByteOrder::LittleEndian => u16::from_le_bytes(bytes),
42                ByteOrder::BigEndian => u16::from_be_bytes(bytes),
43            }
44        };
45
46        let version = read_u16(2);
47
48        match version {
49            42 => {
50                // Classic TIFF: 4-byte IFD offset at position 4
51                let read_u32 = |offset: usize| -> u32 {
52                    let bytes: [u8; 4] = data[offset..offset + 4].try_into().unwrap();
53                    match byte_order {
54                        ByteOrder::LittleEndian => u32::from_le_bytes(bytes),
55                        ByteOrder::BigEndian => u32::from_be_bytes(bytes),
56                    }
57                };
58                let first_ifd_offset = read_u32(4) as u64;
59                Ok(Self {
60                    byte_order,
61                    version,
62                    first_ifd_offset,
63                })
64            }
65            43 => {
66                // BigTIFF: 2-byte offset size (must be 8), 2-byte reserved, 8-byte IFD offset
67                if data.len() < 16 {
68                    return Err(Error::InvalidMagic);
69                }
70                let offset_size = read_u16(4);
71                if offset_size != 8 {
72                    return Err(Error::UnsupportedVersion(version));
73                }
74                // bytes 6-7: reserved (must be 0)
75                let read_u64 = |offset: usize| -> u64 {
76                    let bytes: [u8; 8] = data[offset..offset + 8].try_into().unwrap();
77                    match byte_order {
78                        ByteOrder::LittleEndian => u64::from_le_bytes(bytes),
79                        ByteOrder::BigEndian => u64::from_be_bytes(bytes),
80                    }
81                };
82                let first_ifd_offset = read_u64(8);
83                Ok(Self {
84                    byte_order,
85                    version,
86                    first_ifd_offset,
87                })
88            }
89            _ => Err(Error::UnsupportedVersion(version)),
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn parse_little_endian_classic() {
100        // II, version 42, IFD offset at 8
101        let data = b"II\x2a\x00\x08\x00\x00\x00";
102        let header = TiffHeader::parse(data).unwrap();
103        assert_eq!(header.byte_order, ByteOrder::LittleEndian);
104        assert_eq!(header.version, 42);
105        assert_eq!(header.first_ifd_offset, 8);
106        assert!(!header.is_bigtiff());
107    }
108
109    #[test]
110    fn parse_big_endian_classic() {
111        // MM, version 42, IFD offset at 8
112        let data = b"MM\x00\x2a\x00\x00\x00\x08";
113        let header = TiffHeader::parse(data).unwrap();
114        assert_eq!(header.byte_order, ByteOrder::BigEndian);
115        assert_eq!(header.version, 42);
116        assert_eq!(header.first_ifd_offset, 8);
117    }
118
119    #[test]
120    fn parse_bigtiff() {
121        // II, version 43, offset size 8, reserved 0, IFD offset at 16
122        let mut data = Vec::new();
123        data.extend_from_slice(b"II"); // byte order
124        data.extend_from_slice(&43u16.to_le_bytes()); // version
125        data.extend_from_slice(&8u16.to_le_bytes()); // offset size
126        data.extend_from_slice(&0u16.to_le_bytes()); // reserved
127        data.extend_from_slice(&16u64.to_le_bytes()); // first IFD offset
128        let header = TiffHeader::parse(&data).unwrap();
129        assert!(header.is_bigtiff());
130        assert_eq!(header.first_ifd_offset, 16);
131    }
132
133    #[test]
134    fn reject_invalid_magic() {
135        let data = b"XX\x2a\x00\x08\x00\x00\x00";
136        assert!(TiffHeader::parse(data).is_err());
137    }
138}