Skip to main content

tiff_reader/
header.rs

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