wsi_streamer/format/tiff/
parser.rs

1//! TIFF header and structure parsing.
2//!
3//! This module handles parsing of TIFF and BigTIFF file headers,
4//! which is the foundation for all subsequent parsing operations.
5//!
6//! # TIFF Header Structure
7//!
8//! ## Classic TIFF (8 bytes)
9//! ```text
10//! Bytes 0-1: Byte order (0x4949 = little-endian "II", 0x4D4D = big-endian "MM")
11//! Bytes 2-3: Version (42 = 0x002A)
12//! Bytes 4-7: Offset to first IFD (4 bytes)
13//! ```
14//!
15//! ## BigTIFF (16 bytes)
16//! ```text
17//! Bytes 0-1: Byte order (0x4949 = little-endian "II", 0x4D4D = big-endian "MM")
18//! Bytes 2-3: Version (43 = 0x002B)
19//! Bytes 4-5: Offset byte size (must be 8)
20//! Bytes 6-7: Reserved (must be 0)
21//! Bytes 8-15: Offset to first IFD (8 bytes)
22//! ```
23
24use std::collections::HashMap;
25
26use crate::error::TiffError;
27use crate::io::{read_u16_be, read_u16_le, read_u32_be, read_u32_le, read_u64_be, read_u64_le};
28
29use super::tags::{FieldType, TiffTag};
30
31// =============================================================================
32// Constants
33// =============================================================================
34
35/// Magic bytes indicating little-endian byte order ("II" for Intel)
36const BYTE_ORDER_LITTLE_ENDIAN: u16 = 0x4949;
37
38/// Magic bytes indicating big-endian byte order ("MM" for Motorola)
39const BYTE_ORDER_BIG_ENDIAN: u16 = 0x4D4D;
40
41/// Version number for classic TIFF
42const VERSION_TIFF: u16 = 42;
43
44/// Version number for BigTIFF
45const VERSION_BIGTIFF: u16 = 43;
46
47/// Size of classic TIFF header in bytes
48pub const TIFF_HEADER_SIZE: usize = 8;
49
50/// Size of BigTIFF header in bytes
51pub const BIGTIFF_HEADER_SIZE: usize = 16;
52
53// =============================================================================
54// ByteOrder
55// =============================================================================
56
57/// Byte order (endianness) of a TIFF file.
58///
59/// TIFF files declare their byte order in the first two bytes of the header.
60/// All multi-byte values in the file must be read respecting this order.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum ByteOrder {
63    /// Little-endian ("II" = Intel)
64    LittleEndian,
65    /// Big-endian ("MM" = Motorola)
66    BigEndian,
67}
68
69impl ByteOrder {
70    /// Read a u16 from a byte slice using this byte order.
71    #[inline]
72    pub fn read_u16(self, bytes: &[u8]) -> u16 {
73        match self {
74            ByteOrder::LittleEndian => read_u16_le(bytes),
75            ByteOrder::BigEndian => read_u16_be(bytes),
76        }
77    }
78
79    /// Read a u32 from a byte slice using this byte order.
80    #[inline]
81    pub fn read_u32(self, bytes: &[u8]) -> u32 {
82        match self {
83            ByteOrder::LittleEndian => read_u32_le(bytes),
84            ByteOrder::BigEndian => read_u32_be(bytes),
85        }
86    }
87
88    /// Read a u64 from a byte slice using this byte order.
89    #[inline]
90    pub fn read_u64(self, bytes: &[u8]) -> u64 {
91        match self {
92            ByteOrder::LittleEndian => read_u64_le(bytes),
93            ByteOrder::BigEndian => read_u64_be(bytes),
94        }
95    }
96}
97
98// =============================================================================
99// TiffHeader
100// =============================================================================
101
102/// Parsed TIFF file header.
103///
104/// Contains the essential information needed to begin parsing IFDs:
105/// - Byte order for reading all subsequent values
106/// - Whether this is classic TIFF or BigTIFF (affects entry sizes and offset widths)
107/// - Location of the first IFD
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct TiffHeader {
110    /// Byte order for all multi-byte values in the file
111    pub byte_order: ByteOrder,
112
113    /// Whether this is a BigTIFF file (64-bit offsets)
114    pub is_bigtiff: bool,
115
116    /// Offset to the first IFD in the file
117    pub first_ifd_offset: u64,
118}
119
120impl TiffHeader {
121    /// Parse a TIFF header from raw bytes.
122    ///
123    /// The input must contain at least 8 bytes for classic TIFF or 16 bytes for BigTIFF.
124    /// The function first reads enough to determine the format, then validates the rest.
125    ///
126    /// # Arguments
127    /// * `bytes` - Raw header bytes (at least 8 bytes, preferably 16 for BigTIFF support)
128    /// * `file_size` - Total file size (used to validate IFD offset)
129    ///
130    /// # Errors
131    /// - `InvalidMagic` if byte order bytes are not II or MM
132    /// - `InvalidVersion` if version is not 42 or 43
133    /// - `InvalidBigTiffOffsetSize` if BigTIFF offset size is not 8
134    /// - `FileTooSmall` if there aren't enough bytes for the header
135    /// - `InvalidIfdOffset` if the first IFD offset is outside the file
136    pub fn parse(bytes: &[u8], file_size: u64) -> Result<Self, TiffError> {
137        // Need at least 8 bytes to read the basic header
138        if bytes.len() < TIFF_HEADER_SIZE {
139            return Err(TiffError::FileTooSmall {
140                required: TIFF_HEADER_SIZE as u64,
141                actual: bytes.len() as u64,
142            });
143        }
144
145        // Read byte order (bytes 0-1)
146        // We read this as little-endian because we're checking for specific byte patterns
147        let magic = u16::from_le_bytes([bytes[0], bytes[1]]);
148        let byte_order = match magic {
149            BYTE_ORDER_LITTLE_ENDIAN => ByteOrder::LittleEndian,
150            BYTE_ORDER_BIG_ENDIAN => ByteOrder::BigEndian,
151            _ => return Err(TiffError::InvalidMagic(magic)),
152        };
153
154        // Read version (bytes 2-3) using the detected byte order
155        let version = byte_order.read_u16(&bytes[2..4]);
156
157        match version {
158            VERSION_TIFF => {
159                // Classic TIFF: 4-byte offset at bytes 4-7
160                let first_ifd_offset = byte_order.read_u32(&bytes[4..8]) as u64;
161
162                // Validate offset
163                if first_ifd_offset >= file_size {
164                    return Err(TiffError::InvalidIfdOffset(first_ifd_offset));
165                }
166
167                Ok(TiffHeader {
168                    byte_order,
169                    is_bigtiff: false,
170                    first_ifd_offset,
171                })
172            }
173            VERSION_BIGTIFF => {
174                // BigTIFF: need 16 bytes total
175                if bytes.len() < BIGTIFF_HEADER_SIZE {
176                    return Err(TiffError::FileTooSmall {
177                        required: BIGTIFF_HEADER_SIZE as u64,
178                        actual: bytes.len() as u64,
179                    });
180                }
181
182                // Bytes 4-5: offset byte size (must be 8)
183                let offset_size = byte_order.read_u16(&bytes[4..6]);
184                if offset_size != 8 {
185                    return Err(TiffError::InvalidBigTiffOffsetSize(offset_size));
186                }
187
188                // Bytes 6-7: reserved (should be 0, but we don't strictly require it)
189
190                // Bytes 8-15: first IFD offset (8 bytes)
191                let first_ifd_offset = byte_order.read_u64(&bytes[8..16]);
192
193                // Validate offset
194                if first_ifd_offset >= file_size {
195                    return Err(TiffError::InvalidIfdOffset(first_ifd_offset));
196                }
197
198                Ok(TiffHeader {
199                    byte_order,
200                    is_bigtiff: true,
201                    first_ifd_offset,
202                })
203            }
204            _ => Err(TiffError::InvalidVersion(version)),
205        }
206    }
207
208    /// Size of an IFD entry in bytes.
209    ///
210    /// Classic TIFF: 12 bytes (2 tag + 2 type + 4 count + 4 value/offset)
211    /// BigTIFF: 20 bytes (2 tag + 2 type + 8 count + 8 value/offset)
212    #[inline]
213    pub const fn ifd_entry_size(&self) -> usize {
214        if self.is_bigtiff {
215            20
216        } else {
217            12
218        }
219    }
220
221    /// Size of the entry count field at the start of an IFD.
222    ///
223    /// Classic TIFF: 2 bytes (u16)
224    /// BigTIFF: 8 bytes (u64)
225    #[inline]
226    pub const fn ifd_count_size(&self) -> usize {
227        if self.is_bigtiff {
228            8
229        } else {
230            2
231        }
232    }
233
234    /// Size of the next IFD offset field at the end of an IFD.
235    ///
236    /// Classic TIFF: 4 bytes (u32)
237    /// BigTIFF: 8 bytes (u64)
238    #[inline]
239    pub const fn ifd_next_offset_size(&self) -> usize {
240        if self.is_bigtiff {
241            8
242        } else {
243            4
244        }
245    }
246
247    /// Size of the value/offset field in an IFD entry.
248    ///
249    /// This determines the inline value threshold:
250    /// Classic TIFF: 4 bytes
251    /// BigTIFF: 8 bytes
252    #[inline]
253    pub const fn value_offset_size(&self) -> usize {
254        if self.is_bigtiff {
255            8
256        } else {
257            4
258        }
259    }
260}
261
262// =============================================================================
263// IfdEntry
264// =============================================================================
265
266/// A single entry in an IFD (Image File Directory).
267///
268/// Each entry describes one piece of metadata about the image. The value may be
269/// stored inline (in the `value_offset` field) or at a separate offset in the file.
270///
271/// ## Classic TIFF Entry Layout (12 bytes)
272/// ```text
273/// Bytes 0-1:  Tag ID (u16)
274/// Bytes 2-3:  Field type (u16)
275/// Bytes 4-7:  Count (u32)
276/// Bytes 8-11: Value or offset (u32)
277/// ```
278///
279/// ## BigTIFF Entry Layout (20 bytes)
280/// ```text
281/// Bytes 0-1:   Tag ID (u16)
282/// Bytes 2-3:   Field type (u16)
283/// Bytes 4-11:  Count (u64)
284/// Bytes 12-19: Value or offset (u64)
285/// ```
286#[derive(Debug, Clone)]
287pub struct IfdEntry {
288    /// The tag ID (may be a known TiffTag or unknown)
289    pub tag_id: u16,
290
291    /// The field type (None if unknown type)
292    pub field_type: Option<FieldType>,
293
294    /// Raw field type value (for error reporting)
295    pub field_type_raw: u16,
296
297    /// Number of values (not bytes!)
298    pub count: u64,
299
300    /// Raw bytes of the value/offset field.
301    /// For classic TIFF: 4 bytes, for BigTIFF: 8 bytes.
302    /// Contains either the actual value (if inline) or an offset to the value.
303    pub value_offset_bytes: Vec<u8>,
304
305    /// Whether the value is stored inline (true) or at an offset (false)
306    pub is_inline: bool,
307}
308
309impl IfdEntry {
310    /// Parse an IFD entry from raw bytes.
311    ///
312    /// # Arguments
313    /// * `bytes` - Raw entry bytes (12 for TIFF, 20 for BigTIFF)
314    /// * `header` - The TIFF header (provides byte order and format info)
315    fn parse(bytes: &[u8], header: &TiffHeader) -> Self {
316        let byte_order = header.byte_order;
317
318        // Tag ID (2 bytes)
319        let tag_id = byte_order.read_u16(&bytes[0..2]);
320
321        // Field type (2 bytes)
322        let field_type_raw = byte_order.read_u16(&bytes[2..4]);
323        let field_type = FieldType::from_u16(field_type_raw);
324
325        // Count and value/offset depend on format
326        let (count, value_offset_bytes) = if header.is_bigtiff {
327            // BigTIFF: 8-byte count, 8-byte value/offset
328            let count = byte_order.read_u64(&bytes[4..12]);
329            let value_offset_bytes = bytes[12..20].to_vec();
330            (count, value_offset_bytes)
331        } else {
332            // Classic TIFF: 4-byte count, 4-byte value/offset
333            let count = byte_order.read_u32(&bytes[4..8]) as u64;
334            let value_offset_bytes = bytes[8..12].to_vec();
335            (count, value_offset_bytes)
336        };
337
338        // Determine if value is inline
339        let is_inline = field_type
340            .map(|ft| ft.fits_inline(count, header.is_bigtiff))
341            .unwrap_or(false);
342
343        IfdEntry {
344            tag_id,
345            field_type,
346            field_type_raw,
347            count,
348            value_offset_bytes,
349            is_inline,
350        }
351    }
352
353    /// Get the known TiffTag for this entry, if recognized.
354    pub fn tag(&self) -> Option<TiffTag> {
355        TiffTag::from_u16(self.tag_id)
356    }
357
358    /// Get the offset to the value data (for non-inline values).
359    ///
360    /// # Arguments
361    /// * `byte_order` - The byte order to use for reading
362    ///
363    /// # Returns
364    /// The offset, or 0 if the value is inline (check `is_inline` first).
365    pub fn value_offset(&self, byte_order: ByteOrder) -> u64 {
366        if self.value_offset_bytes.len() == 8 {
367            byte_order.read_u64(&self.value_offset_bytes)
368        } else {
369            byte_order.read_u32(&self.value_offset_bytes) as u64
370        }
371    }
372
373    /// Read inline value as a single u16.
374    ///
375    /// # Arguments
376    /// * `byte_order` - The byte order to use for reading
377    ///
378    /// # Returns
379    /// The value, or None if not inline or count != 1 or wrong type.
380    pub fn inline_u16(&self, byte_order: ByteOrder) -> Option<u16> {
381        if !self.is_inline || self.count != 1 {
382            return None;
383        }
384        match self.field_type? {
385            FieldType::Short => Some(byte_order.read_u16(&self.value_offset_bytes)),
386            _ => None,
387        }
388    }
389
390    /// Read inline value as a single u32.
391    ///
392    /// # Arguments
393    /// * `byte_order` - The byte order to use for reading
394    ///
395    /// # Returns
396    /// The value, or None if not inline or count != 1 or wrong type.
397    pub fn inline_u32(&self, byte_order: ByteOrder) -> Option<u32> {
398        if !self.is_inline || self.count != 1 {
399            return None;
400        }
401        match self.field_type? {
402            FieldType::Short => Some(byte_order.read_u16(&self.value_offset_bytes) as u32),
403            FieldType::Long => Some(byte_order.read_u32(&self.value_offset_bytes)),
404            _ => None,
405        }
406    }
407
408    /// Read inline value as a single u64.
409    ///
410    /// # Arguments
411    /// * `byte_order` - The byte order to use for reading
412    ///
413    /// # Returns
414    /// The value, or None if not inline or count != 1 or wrong type.
415    pub fn inline_u64(&self, byte_order: ByteOrder) -> Option<u64> {
416        if !self.is_inline || self.count != 1 {
417            return None;
418        }
419        match self.field_type? {
420            FieldType::Short => Some(byte_order.read_u16(&self.value_offset_bytes) as u64),
421            FieldType::Long => Some(byte_order.read_u32(&self.value_offset_bytes) as u64),
422            FieldType::Long8 => {
423                if self.value_offset_bytes.len() >= 8 {
424                    Some(byte_order.read_u64(&self.value_offset_bytes))
425                } else {
426                    None
427                }
428            }
429            _ => None,
430        }
431    }
432
433    /// Calculate total byte size of the value data.
434    pub fn value_byte_size(&self) -> Option<u64> {
435        self.field_type
436            .map(|ft| ft.size_in_bytes() as u64 * self.count)
437    }
438}
439
440// =============================================================================
441// Ifd
442// =============================================================================
443
444/// A parsed Image File Directory (IFD).
445///
446/// An IFD contains metadata about one image in the TIFF file. WSI files typically
447/// have multiple IFDs: one for each pyramid level, plus label/macro images.
448///
449/// The entries are stored both as a vector (preserving order) and as a hashmap
450/// (for fast lookup by tag).
451#[derive(Debug, Clone)]
452pub struct Ifd {
453    /// All entries in this IFD, in file order
454    pub entries: Vec<IfdEntry>,
455
456    /// Entries indexed by tag ID for fast lookup
457    pub(crate) entries_by_tag: HashMap<u16, usize>,
458
459    /// Offset to the next IFD (0 if this is the last IFD)
460    pub next_ifd_offset: u64,
461}
462
463impl Ifd {
464    /// Parse an IFD from raw bytes.
465    ///
466    /// The bytes should start at the IFD offset and contain:
467    /// - Entry count (2 or 8 bytes depending on format)
468    /// - All entries (12 or 20 bytes each)
469    /// - Next IFD offset (4 or 8 bytes)
470    ///
471    /// # Arguments
472    /// * `bytes` - Raw IFD bytes
473    /// * `header` - The TIFF header
474    ///
475    /// # Errors
476    /// Returns an error if the bytes are too short for the declared entry count.
477    pub fn parse(bytes: &[u8], header: &TiffHeader) -> Result<Self, TiffError> {
478        let byte_order = header.byte_order;
479        let count_size = header.ifd_count_size();
480        let entry_size = header.ifd_entry_size();
481        let next_offset_size = header.ifd_next_offset_size();
482
483        // Read entry count
484        if bytes.len() < count_size {
485            return Err(TiffError::FileTooSmall {
486                required: count_size as u64,
487                actual: bytes.len() as u64,
488            });
489        }
490
491        let entry_count = if header.is_bigtiff {
492            byte_order.read_u64(&bytes[0..8])
493        } else {
494            byte_order.read_u16(&bytes[0..2]) as u64
495        };
496
497        // Calculate required size
498        let entries_start = count_size;
499        let entries_size = entry_count as usize * entry_size;
500        let next_offset_start = entries_start + entries_size;
501        let total_required = next_offset_start + next_offset_size;
502
503        if bytes.len() < total_required {
504            return Err(TiffError::FileTooSmall {
505                required: total_required as u64,
506                actual: bytes.len() as u64,
507            });
508        }
509
510        // Parse entries
511        let mut entries = Vec::with_capacity(entry_count as usize);
512        let mut entries_by_tag = HashMap::with_capacity(entry_count as usize);
513
514        for i in 0..entry_count as usize {
515            let entry_start = entries_start + i * entry_size;
516            let entry_bytes = &bytes[entry_start..entry_start + entry_size];
517            let entry = IfdEntry::parse(entry_bytes, header);
518
519            entries_by_tag.insert(entry.tag_id, entries.len());
520            entries.push(entry);
521        }
522
523        // Read next IFD offset
524        let next_ifd_offset = if header.is_bigtiff {
525            byte_order.read_u64(&bytes[next_offset_start..next_offset_start + 8])
526        } else {
527            byte_order.read_u32(&bytes[next_offset_start..next_offset_start + 4]) as u64
528        };
529
530        Ok(Ifd {
531            entries,
532            entries_by_tag,
533            next_ifd_offset,
534        })
535    }
536
537    /// Calculate the total size in bytes needed to read this IFD.
538    ///
539    /// This can be used to determine how many bytes to fetch before parsing.
540    /// Note: This is the size of the IFD structure itself, not including
541    /// any values stored at external offsets.
542    ///
543    /// # Arguments
544    /// * `entry_count` - Number of entries in the IFD
545    /// * `header` - The TIFF header
546    pub fn calculate_size(entry_count: u64, header: &TiffHeader) -> usize {
547        header.ifd_count_size()
548            + (entry_count as usize * header.ifd_entry_size())
549            + header.ifd_next_offset_size()
550    }
551
552    /// Get an entry by its tag ID.
553    pub fn get_entry(&self, tag_id: u16) -> Option<&IfdEntry> {
554        self.entries_by_tag
555            .get(&tag_id)
556            .map(|&idx| &self.entries[idx])
557    }
558
559    /// Get an entry by its known TiffTag.
560    pub fn get_entry_by_tag(&self, tag: TiffTag) -> Option<&IfdEntry> {
561        self.get_entry(tag.as_u16())
562    }
563
564    /// Get an inline u32 value for a tag.
565    ///
566    /// This is a convenience method for reading simple scalar values like
567    /// ImageWidth, ImageLength, TileWidth, etc.
568    pub fn get_u32(&self, tag: TiffTag, byte_order: ByteOrder) -> Option<u32> {
569        self.get_entry_by_tag(tag)?.inline_u32(byte_order)
570    }
571
572    /// Get an inline u64 value for a tag.
573    pub fn get_u64(&self, tag: TiffTag, byte_order: ByteOrder) -> Option<u64> {
574        self.get_entry_by_tag(tag)?.inline_u64(byte_order)
575    }
576
577    /// Get an inline u16 value for a tag.
578    pub fn get_u16(&self, tag: TiffTag, byte_order: ByteOrder) -> Option<u16> {
579        self.get_entry_by_tag(tag)?.inline_u16(byte_order)
580    }
581
582    /// Check if this IFD has tile organization (vs strip).
583    ///
584    /// Returns true if TileWidth and TileLength tags are present.
585    pub fn is_tiled(&self) -> bool {
586        self.get_entry_by_tag(TiffTag::TileWidth).is_some()
587            && self.get_entry_by_tag(TiffTag::TileLength).is_some()
588    }
589
590    /// Check if this IFD has strip organization.
591    ///
592    /// Returns true if StripOffsets tag is present.
593    pub fn is_stripped(&self) -> bool {
594        self.get_entry_by_tag(TiffTag::StripOffsets).is_some()
595    }
596
597    /// Get image width from this IFD.
598    pub fn image_width(&self, byte_order: ByteOrder) -> Option<u32> {
599        self.get_u32(TiffTag::ImageWidth, byte_order)
600    }
601
602    /// Get image height (length) from this IFD.
603    pub fn image_height(&self, byte_order: ByteOrder) -> Option<u32> {
604        self.get_u32(TiffTag::ImageLength, byte_order)
605    }
606
607    /// Get tile width from this IFD.
608    pub fn tile_width(&self, byte_order: ByteOrder) -> Option<u32> {
609        self.get_u32(TiffTag::TileWidth, byte_order)
610    }
611
612    /// Get tile height (length) from this IFD.
613    pub fn tile_height(&self, byte_order: ByteOrder) -> Option<u32> {
614        self.get_u32(TiffTag::TileLength, byte_order)
615    }
616
617    /// Get compression scheme from this IFD.
618    pub fn compression(&self, byte_order: ByteOrder) -> Option<u16> {
619        self.get_u16(TiffTag::Compression, byte_order)
620    }
621
622    /// Get the number of entries in this IFD.
623    pub fn entry_count(&self) -> usize {
624        self.entries.len()
625    }
626
627    /// Create an empty IFD (for testing).
628    #[cfg(test)]
629    pub fn empty() -> Self {
630        Ifd {
631            entries: Vec::new(),
632            entries_by_tag: HashMap::new(),
633            next_ifd_offset: 0,
634        }
635    }
636}
637
638// =============================================================================
639// Tests
640// =============================================================================
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645
646    // -------------------------------------------------------------------------
647    // ByteOrder Tests
648    // -------------------------------------------------------------------------
649
650    #[test]
651    fn test_byte_order_read_u16() {
652        let bytes = [0x01, 0x02];
653        assert_eq!(ByteOrder::LittleEndian.read_u16(&bytes), 0x0201);
654        assert_eq!(ByteOrder::BigEndian.read_u16(&bytes), 0x0102);
655    }
656
657    #[test]
658    fn test_byte_order_read_u32() {
659        let bytes = [0x01, 0x02, 0x03, 0x04];
660        assert_eq!(ByteOrder::LittleEndian.read_u32(&bytes), 0x04030201);
661        assert_eq!(ByteOrder::BigEndian.read_u32(&bytes), 0x01020304);
662    }
663
664    #[test]
665    fn test_byte_order_read_u64() {
666        let bytes = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
667        assert_eq!(ByteOrder::LittleEndian.read_u64(&bytes), 0x0807060504030201);
668        assert_eq!(ByteOrder::BigEndian.read_u64(&bytes), 0x0102030405060708);
669    }
670
671    // -------------------------------------------------------------------------
672    // TiffHeader Parsing Tests - Classic TIFF
673    // -------------------------------------------------------------------------
674
675    #[test]
676    fn test_parse_tiff_little_endian() {
677        // Little-endian TIFF with first IFD at offset 8
678        let header = [
679            0x49, 0x49, // II (little-endian)
680            0x2A, 0x00, // Version 42 (little-endian)
681            0x08, 0x00, 0x00, 0x00, // First IFD offset = 8 (little-endian)
682        ];
683
684        let result = TiffHeader::parse(&header, 1000).unwrap();
685        assert_eq!(result.byte_order, ByteOrder::LittleEndian);
686        assert!(!result.is_bigtiff);
687        assert_eq!(result.first_ifd_offset, 8);
688    }
689
690    #[test]
691    fn test_parse_tiff_big_endian() {
692        // Big-endian TIFF with first IFD at offset 8
693        let header = [
694            0x4D, 0x4D, // MM (big-endian)
695            0x00, 0x2A, // Version 42 (big-endian)
696            0x00, 0x00, 0x00, 0x08, // First IFD offset = 8 (big-endian)
697        ];
698
699        let result = TiffHeader::parse(&header, 1000).unwrap();
700        assert_eq!(result.byte_order, ByteOrder::BigEndian);
701        assert!(!result.is_bigtiff);
702        assert_eq!(result.first_ifd_offset, 8);
703    }
704
705    #[test]
706    fn test_parse_tiff_larger_offset() {
707        // Little-endian TIFF with first IFD at offset 1000
708        let header = [
709            0x49, 0x49, // II (little-endian)
710            0x2A, 0x00, // Version 42
711            0xE8, 0x03, 0x00, 0x00, // First IFD offset = 1000 (little-endian)
712        ];
713
714        let result = TiffHeader::parse(&header, 2000).unwrap();
715        assert_eq!(result.first_ifd_offset, 1000);
716    }
717
718    // -------------------------------------------------------------------------
719    // TiffHeader Parsing Tests - BigTIFF
720    // -------------------------------------------------------------------------
721
722    #[test]
723    fn test_parse_bigtiff_little_endian() {
724        // Little-endian BigTIFF with first IFD at offset 16
725        let header = [
726            0x49, 0x49, // II (little-endian)
727            0x2B, 0x00, // Version 43 (BigTIFF)
728            0x08, 0x00, // Offset size = 8
729            0x00, 0x00, // Reserved
730            0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // First IFD offset = 16
731        ];
732
733        let result = TiffHeader::parse(&header, 1000).unwrap();
734        assert_eq!(result.byte_order, ByteOrder::LittleEndian);
735        assert!(result.is_bigtiff);
736        assert_eq!(result.first_ifd_offset, 16);
737    }
738
739    #[test]
740    fn test_parse_bigtiff_big_endian() {
741        // Big-endian BigTIFF with first IFD at offset 16
742        let header = [
743            0x4D, 0x4D, // MM (big-endian)
744            0x00, 0x2B, // Version 43 (BigTIFF)
745            0x00, 0x08, // Offset size = 8
746            0x00, 0x00, // Reserved
747            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, // First IFD offset = 16
748        ];
749
750        let result = TiffHeader::parse(&header, 1000).unwrap();
751        assert_eq!(result.byte_order, ByteOrder::BigEndian);
752        assert!(result.is_bigtiff);
753        assert_eq!(result.first_ifd_offset, 16);
754    }
755
756    #[test]
757    fn test_parse_bigtiff_large_offset() {
758        // BigTIFF with 64-bit offset beyond 4GB
759        let header = [
760            0x49, 0x49, // II (little-endian)
761            0x2B, 0x00, // Version 43 (BigTIFF)
762            0x08, 0x00, // Offset size = 8
763            0x00, 0x00, // Reserved
764            0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // First IFD offset = 4GB
765        ];
766
767        let result = TiffHeader::parse(&header, 10_000_000_000).unwrap();
768        assert!(result.is_bigtiff);
769        assert_eq!(result.first_ifd_offset, 0x0000_0001_0000_0000); // 4GB
770    }
771
772    // -------------------------------------------------------------------------
773    // TiffHeader Parsing Tests - Error Cases
774    // -------------------------------------------------------------------------
775
776    #[test]
777    fn test_parse_invalid_magic() {
778        let header = [
779            0x00, 0x00, // Invalid magic
780            0x2A, 0x00, 0x08, 0x00, 0x00, 0x00,
781        ];
782
783        let result = TiffHeader::parse(&header, 1000);
784        assert!(matches!(result, Err(TiffError::InvalidMagic(0x0000))));
785    }
786
787    #[test]
788    fn test_parse_invalid_version() {
789        let header = [
790            0x49, 0x49, // II
791            0x00, 0x00, // Invalid version 0
792            0x08, 0x00, 0x00, 0x00,
793        ];
794
795        let result = TiffHeader::parse(&header, 1000);
796        assert!(matches!(result, Err(TiffError::InvalidVersion(0))));
797    }
798
799    #[test]
800    fn test_parse_bigtiff_invalid_offset_size() {
801        let header = [
802            0x49, 0x49, // II
803            0x2B, 0x00, // Version 43 (BigTIFF)
804            0x04, 0x00, // Invalid offset size = 4 (should be 8)
805            0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
806        ];
807
808        let result = TiffHeader::parse(&header, 1000);
809        assert!(matches!(
810            result,
811            Err(TiffError::InvalidBigTiffOffsetSize(4))
812        ));
813    }
814
815    #[test]
816    fn test_parse_file_too_small_tiff() {
817        let header = [0x49, 0x49, 0x2A, 0x00]; // Only 4 bytes
818
819        let result = TiffHeader::parse(&header, 1000);
820        assert!(matches!(
821            result,
822            Err(TiffError::FileTooSmall {
823                required: 8,
824                actual: 4
825            })
826        ));
827    }
828
829    #[test]
830    fn test_parse_file_too_small_bigtiff() {
831        // Valid TIFF header but BigTIFF needs 16 bytes
832        let header = [
833            0x49, 0x49, // II
834            0x2B, 0x00, // Version 43 (BigTIFF)
835            0x08, 0x00, // Offset size = 8
836            0x00, 0x00, // Only 8 bytes total
837        ];
838
839        let result = TiffHeader::parse(&header, 1000);
840        assert!(matches!(
841            result,
842            Err(TiffError::FileTooSmall {
843                required: 16,
844                actual: 8
845            })
846        ));
847    }
848
849    #[test]
850    fn test_parse_invalid_ifd_offset() {
851        // IFD offset beyond file size
852        let header = [
853            0x49, 0x49, // II
854            0x2A, 0x00, // Version 42
855            0xE8, 0x03, 0x00, 0x00, // First IFD offset = 1000
856        ];
857
858        let result = TiffHeader::parse(&header, 500); // File is only 500 bytes
859        assert!(matches!(result, Err(TiffError::InvalidIfdOffset(1000))));
860    }
861
862    // -------------------------------------------------------------------------
863    // TiffHeader Helper Methods Tests
864    // -------------------------------------------------------------------------
865
866    #[test]
867    fn test_ifd_entry_size() {
868        let tiff = TiffHeader {
869            byte_order: ByteOrder::LittleEndian,
870            is_bigtiff: false,
871            first_ifd_offset: 8,
872        };
873        assert_eq!(tiff.ifd_entry_size(), 12);
874
875        let bigtiff = TiffHeader {
876            byte_order: ByteOrder::LittleEndian,
877            is_bigtiff: true,
878            first_ifd_offset: 16,
879        };
880        assert_eq!(bigtiff.ifd_entry_size(), 20);
881    }
882
883    #[test]
884    fn test_ifd_count_size() {
885        let tiff = TiffHeader {
886            byte_order: ByteOrder::LittleEndian,
887            is_bigtiff: false,
888            first_ifd_offset: 8,
889        };
890        assert_eq!(tiff.ifd_count_size(), 2);
891
892        let bigtiff = TiffHeader {
893            byte_order: ByteOrder::LittleEndian,
894            is_bigtiff: true,
895            first_ifd_offset: 16,
896        };
897        assert_eq!(bigtiff.ifd_count_size(), 8);
898    }
899
900    #[test]
901    fn test_value_offset_size() {
902        let tiff = TiffHeader {
903            byte_order: ByteOrder::LittleEndian,
904            is_bigtiff: false,
905            first_ifd_offset: 8,
906        };
907        assert_eq!(tiff.value_offset_size(), 4);
908
909        let bigtiff = TiffHeader {
910            byte_order: ByteOrder::LittleEndian,
911            is_bigtiff: true,
912            first_ifd_offset: 16,
913        };
914        assert_eq!(bigtiff.value_offset_size(), 8);
915    }
916
917    // -------------------------------------------------------------------------
918    // IfdEntry Tests
919    // -------------------------------------------------------------------------
920
921    fn make_tiff_header() -> TiffHeader {
922        TiffHeader {
923            byte_order: ByteOrder::LittleEndian,
924            is_bigtiff: false,
925            first_ifd_offset: 8,
926        }
927    }
928
929    fn make_bigtiff_header() -> TiffHeader {
930        TiffHeader {
931            byte_order: ByteOrder::LittleEndian,
932            is_bigtiff: true,
933            first_ifd_offset: 16,
934        }
935    }
936
937    #[test]
938    fn test_ifd_entry_parse_tiff_inline_short() {
939        // Classic TIFF entry: ImageWidth = 1024 (SHORT type, count=1, inline)
940        // Tag 256 (0x0100), Type 3 (SHORT), Count 1, Value 1024 (0x0400)
941        let entry_bytes = [
942            0x00, 0x01, // Tag ID = 256 (ImageWidth) - little-endian
943            0x03, 0x00, // Type = 3 (SHORT)
944            0x01, 0x00, 0x00, 0x00, // Count = 1
945            0x00, 0x04, 0x00, 0x00, // Value = 1024 (inline)
946        ];
947
948        let header = make_tiff_header();
949        let entry = IfdEntry::parse(&entry_bytes, &header);
950
951        assert_eq!(entry.tag_id, 256);
952        assert_eq!(entry.tag(), Some(TiffTag::ImageWidth));
953        assert_eq!(entry.field_type, Some(FieldType::Short));
954        assert_eq!(entry.count, 1);
955        assert!(entry.is_inline);
956        assert_eq!(entry.inline_u16(header.byte_order), Some(1024));
957        assert_eq!(entry.inline_u32(header.byte_order), Some(1024));
958    }
959
960    #[test]
961    fn test_ifd_entry_parse_tiff_inline_long() {
962        // Classic TIFF entry: ImageWidth = 50000 (LONG type, count=1, inline)
963        // Tag 256 (0x0100), Type 4 (LONG), Count 1, Value 50000
964        let entry_bytes = [
965            0x00, 0x01, // Tag ID = 256 (ImageWidth)
966            0x04, 0x00, // Type = 4 (LONG)
967            0x01, 0x00, 0x00, 0x00, // Count = 1
968            0x50, 0xC3, 0x00, 0x00, // Value = 50000 (inline)
969        ];
970
971        let header = make_tiff_header();
972        let entry = IfdEntry::parse(&entry_bytes, &header);
973
974        assert_eq!(entry.tag_id, 256);
975        assert_eq!(entry.field_type, Some(FieldType::Long));
976        assert!(entry.is_inline);
977        assert_eq!(entry.inline_u32(header.byte_order), Some(50000));
978    }
979
980    #[test]
981    fn test_ifd_entry_parse_tiff_offset() {
982        // Classic TIFF entry: TileOffsets with count > 1 (stored at offset)
983        // Tag 324 (0x0144), Type 4 (LONG), Count 100, Offset 1000
984        let entry_bytes = [
985            0x44, 0x01, // Tag ID = 324 (TileOffsets)
986            0x04, 0x00, // Type = 4 (LONG)
987            0x64, 0x00, 0x00, 0x00, // Count = 100
988            0xE8, 0x03, 0x00, 0x00, // Offset = 1000
989        ];
990
991        let header = make_tiff_header();
992        let entry = IfdEntry::parse(&entry_bytes, &header);
993
994        assert_eq!(entry.tag_id, 324);
995        assert_eq!(entry.tag(), Some(TiffTag::TileOffsets));
996        assert_eq!(entry.field_type, Some(FieldType::Long));
997        assert_eq!(entry.count, 100);
998        assert!(!entry.is_inline); // Not inline because count * 4 > 4
999        assert_eq!(entry.value_offset(header.byte_order), 1000);
1000        assert_eq!(entry.value_byte_size(), Some(400)); // 100 * 4 bytes
1001    }
1002
1003    #[test]
1004    fn test_ifd_entry_parse_bigtiff_inline_long8() {
1005        // BigTIFF entry: ImageWidth = 100000 (LONG8 type, count=1, inline)
1006        let entry_bytes = [
1007            0x00, 0x01, // Tag ID = 256 (ImageWidth)
1008            0x10, 0x00, // Type = 16 (LONG8)
1009            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Count = 1
1010            0xA0, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // Value = 100000
1011        ];
1012
1013        let header = make_bigtiff_header();
1014        let entry = IfdEntry::parse(&entry_bytes, &header);
1015
1016        assert_eq!(entry.tag_id, 256);
1017        assert_eq!(entry.field_type, Some(FieldType::Long8));
1018        assert!(entry.is_inline);
1019        assert_eq!(entry.inline_u64(header.byte_order), Some(100000));
1020    }
1021
1022    #[test]
1023    fn test_ifd_entry_unknown_field_type() {
1024        // Entry with unknown field type (99)
1025        let entry_bytes = [
1026            0x00, 0x01, // Tag ID = 256
1027            0x63, 0x00, // Type = 99 (unknown)
1028            0x01, 0x00, 0x00, 0x00, // Count = 1
1029            0x00, 0x00, 0x00, 0x00, // Value
1030        ];
1031
1032        let header = make_tiff_header();
1033        let entry = IfdEntry::parse(&entry_bytes, &header);
1034
1035        assert_eq!(entry.field_type, None);
1036        assert_eq!(entry.field_type_raw, 99);
1037        assert!(!entry.is_inline); // Unknown types are not considered inline
1038    }
1039
1040    // -------------------------------------------------------------------------
1041    // Ifd Tests
1042    // -------------------------------------------------------------------------
1043
1044    #[test]
1045    fn test_ifd_parse_tiff_simple() {
1046        // Classic TIFF IFD with 3 entries:
1047        // - ImageWidth = 1024
1048        // - ImageLength = 768
1049        // - Compression = 7 (JPEG)
1050        // Next IFD offset = 500
1051        let ifd_bytes = [
1052            // Entry count = 3
1053            0x03, 0x00, // Entry 1: ImageWidth (256) = 1024
1054            0x00, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
1055            // Entry 2: ImageLength (257) = 768
1056            0x01, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
1057            // Entry 3: Compression (259) = 7
1058            0x03, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
1059            // Next IFD offset = 500
1060            0xF4, 0x01, 0x00, 0x00,
1061        ];
1062
1063        let header = make_tiff_header();
1064        let ifd = Ifd::parse(&ifd_bytes, &header).unwrap();
1065
1066        assert_eq!(ifd.entry_count(), 3);
1067        assert_eq!(ifd.next_ifd_offset, 500);
1068
1069        // Check values via convenience methods
1070        assert_eq!(ifd.image_width(header.byte_order), Some(1024));
1071        assert_eq!(ifd.image_height(header.byte_order), Some(768));
1072        assert_eq!(ifd.compression(header.byte_order), Some(7));
1073
1074        // Check entry lookup
1075        let width_entry = ifd.get_entry_by_tag(TiffTag::ImageWidth).unwrap();
1076        assert_eq!(width_entry.count, 1);
1077    }
1078
1079    #[test]
1080    fn test_ifd_parse_bigtiff() {
1081        // BigTIFF IFD with 2 entries
1082        let ifd_bytes = [
1083            // Entry count = 2 (8 bytes in BigTIFF)
1084            0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1085            // Entry 1: ImageWidth (256) = 50000 (LONG type, count=1)
1086            0x00, 0x01, // Tag
1087            0x04, 0x00, // Type = LONG
1088            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Count = 1
1089            0x50, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Value = 50000
1090            // Entry 2: ImageLength (257) = 40000
1091            0x01, 0x01, // Tag
1092            0x04, 0x00, // Type = LONG
1093            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Count = 1
1094            0x40, 0x9C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Value = 40000
1095            // Next IFD offset = 1000 (8 bytes)
1096            0xE8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1097        ];
1098
1099        let header = make_bigtiff_header();
1100        let ifd = Ifd::parse(&ifd_bytes, &header).unwrap();
1101
1102        assert_eq!(ifd.entry_count(), 2);
1103        assert_eq!(ifd.next_ifd_offset, 1000);
1104        assert_eq!(ifd.image_width(header.byte_order), Some(50000));
1105        assert_eq!(ifd.image_height(header.byte_order), Some(40000));
1106    }
1107
1108    #[test]
1109    fn test_ifd_parse_with_tiles() {
1110        // IFD with tile-related tags
1111        let ifd_bytes = [
1112            // Entry count = 4
1113            0x04, 0x00, // ImageWidth = 10000
1114            0x00, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x27, 0x00, 0x00,
1115            // ImageLength = 8000
1116            0x01, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x1F, 0x00, 0x00,
1117            // TileWidth (322) = 256
1118            0x42, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
1119            // TileLength (323) = 256
1120            0x43, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
1121            // Next IFD = 0 (no more IFDs)
1122            0x00, 0x00, 0x00, 0x00,
1123        ];
1124
1125        let header = make_tiff_header();
1126        let ifd = Ifd::parse(&ifd_bytes, &header).unwrap();
1127
1128        assert!(ifd.is_tiled());
1129        assert!(!ifd.is_stripped());
1130        assert_eq!(ifd.tile_width(header.byte_order), Some(256));
1131        assert_eq!(ifd.tile_height(header.byte_order), Some(256));
1132        assert_eq!(ifd.next_ifd_offset, 0);
1133    }
1134
1135    #[test]
1136    fn test_ifd_parse_big_endian() {
1137        // Big-endian IFD with 1 entry
1138        let ifd_bytes = [
1139            // Entry count = 1 (big-endian)
1140            0x00, 0x01, // ImageWidth = 2048 (big-endian)
1141            0x01, 0x00, // Tag = 256
1142            0x00, 0x03, // Type = SHORT
1143            0x00, 0x00, 0x00, 0x01, // Count = 1
1144            0x08, 0x00, 0x00, 0x00, // Value = 2048
1145            // Next IFD = 0
1146            0x00, 0x00, 0x00, 0x00,
1147        ];
1148
1149        let header = TiffHeader {
1150            byte_order: ByteOrder::BigEndian,
1151            is_bigtiff: false,
1152            first_ifd_offset: 8,
1153        };
1154
1155        let ifd = Ifd::parse(&ifd_bytes, &header).unwrap();
1156
1157        assert_eq!(ifd.entry_count(), 1);
1158        assert_eq!(ifd.image_width(header.byte_order), Some(2048));
1159    }
1160
1161    #[test]
1162    fn test_ifd_calculate_size() {
1163        let tiff_header = make_tiff_header();
1164        let bigtiff_header = make_bigtiff_header();
1165
1166        // Classic TIFF: 2 (count) + 10*12 (entries) + 4 (next) = 126 bytes
1167        assert_eq!(Ifd::calculate_size(10, &tiff_header), 126);
1168
1169        // BigTIFF: 8 (count) + 10*20 (entries) + 8 (next) = 216 bytes
1170        assert_eq!(Ifd::calculate_size(10, &bigtiff_header), 216);
1171    }
1172
1173    #[test]
1174    fn test_ifd_parse_error_too_small() {
1175        // IFD bytes too small for declared entry count
1176        let ifd_bytes = [
1177            0x05, 0x00, // Entry count = 5, but we only provide 2 entries worth
1178            0x00, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x01,
1179            0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
1180        ];
1181
1182        let header = make_tiff_header();
1183        let result = Ifd::parse(&ifd_bytes, &header);
1184
1185        assert!(matches!(result, Err(TiffError::FileTooSmall { .. })));
1186    }
1187
1188    #[test]
1189    fn test_ifd_get_entry_not_found() {
1190        // IFD with just ImageWidth
1191        let ifd_bytes = [
1192            0x01, 0x00, 0x00, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
1193            0x00, 0x00, 0x00, 0x00,
1194        ];
1195
1196        let header = make_tiff_header();
1197        let ifd = Ifd::parse(&ifd_bytes, &header).unwrap();
1198
1199        // Should find ImageWidth
1200        assert!(ifd.get_entry_by_tag(TiffTag::ImageWidth).is_some());
1201
1202        // Should not find ImageLength
1203        assert!(ifd.get_entry_by_tag(TiffTag::ImageLength).is_none());
1204        assert_eq!(ifd.image_height(header.byte_order), None);
1205    }
1206}