Skip to main content

rasterkit/tiff/
ifd.rs

1//! Image File Directory (IFD) structures and methods
2//!
3//! This module implements the core TIFF IFD (Image File Directory) structures
4//! that store metadata about images in a TIFF file. IFDs are organized as
5//! collections of tag entries, with each tag describing an aspect of the image.
6
7use std::collections::HashMap;
8use std::fmt;
9use crate::tiff::constants::{field_types, tags};
10use log::{debug, info, trace};
11use crate::utils::tag_utils;
12
13/// Represents an Image File Directory (IFD) in a TIFF file
14///
15/// An IFD contains metadata about an image, stored as a series of tag entries.
16/// TIFF files can contain multiple IFDs, each describing a separate image in
17/// a multipage TIFF.
18#[derive(Debug, Clone)]
19pub struct IFD {
20    /// Entries in this IFD
21    pub entries: Vec<IFDEntry>,
22    /// IFD number (0-based)
23    pub number: usize,
24    /// Offset to this IFD in the file
25    pub offset: u64,
26    /// Cached tag values for quick lookup
27    tag_map: HashMap<u16, IFDEntry>,
28}
29
30/// Represents an entry in an Image File Directory (IFD)
31///
32/// Each entry describes one aspect of the image (dimensions, color space,
33/// compression, etc.) using a tag-value pair. The field_type determines
34/// how to interpret the value or offset.
35#[derive(Debug, Clone)]
36pub struct IFDEntry {
37    /// TIFF tag identifier
38    pub tag: u16,
39    /// Field type
40    pub field_type: u16,
41    /// Number of values
42    pub count: u64,
43    /// Value or offset to values
44    pub value_offset: u64,
45}
46
47impl IFDEntry {
48    /// Creates a new IFD entry
49    ///
50    /// This constructs a tag entry with the specified parameters.
51    /// For small values, value_offset contains the actual value.
52    /// For larger values, it contains an offset to where the value is stored.
53    pub fn new(tag: u16, field_type: u16, count: u64, value_offset: u64) -> Self {
54        let tag_name = tag_utils::get_tag_name(tag);
55        let field_type_name = tag_utils::get_field_type_name(field_type);
56
57        debug!("Creating new IFD entry: tag={} ({}), type={} ({}), count={}, offset/value={}",
58               tag, tag_name, field_type, field_type_name, count, value_offset);
59
60        Self {
61            tag,
62            field_type,
63            count,
64            value_offset,
65        }
66    }
67
68    /// Get the size in bytes for this entry's field type
69    ///
70    /// Different TIFF field types take up different amounts of space.
71    /// This method returns how many bytes a single value of this entry's type requires.
72    pub fn get_field_type_size(&self) -> usize {
73        match self.field_type {
74            field_types::BYTE | field_types::ASCII | field_types::SBYTE | field_types::UNDEFINED => 1,
75            field_types::SHORT | field_types::SSHORT => 2,
76            field_types::LONG | field_types::SLONG | field_types::FLOAT => 4,
77            field_types::RATIONAL | field_types::SRATIONAL | field_types::DOUBLE => 8,
78            field_types::LONG8 | field_types::SLONG8 | field_types::IFD8 => 8,
79            _ => {
80                debug!("Unknown field type: {}, assuming 1 byte", self.field_type);
81                1 // Default to 1 byte
82            }
83        }
84    }
85
86    /// Determines if the value is stored inline in value_offset
87    /// rather than at the offset location
88    ///
89    /// TIFF format allows small values to be stored directly in the IFD entry
90    /// rather than requiring a separate data area. This method determines
91    /// if this entry's value is stored inline or at an external offset.
92    pub fn is_value_inline(&self, is_big_tiff: bool) -> bool {
93        let total_size = self.get_field_type_size() * self.count as usize;
94        let inline_size = if is_big_tiff { 8 } else { 4 };
95
96        let is_inline = total_size <= inline_size;
97        let tag_name = tag_utils::get_tag_name(self.tag);
98
99        trace!("Tag {} ({}) value storage: {}bytes, {} inline (max {}bytes)",
100              self.tag, tag_name, total_size,
101              if is_inline { "is" } else { "not" }, inline_size);
102
103        is_inline
104    }
105
106    /// Returns a human-readable description of this entry
107    ///
108    /// This is useful for debugging and logging purposes.
109    pub fn description(&self) -> String {
110        let tag_name = tag_utils::get_tag_name(self.tag);
111        let field_type_name = tag_utils::get_field_type_name(self.field_type);
112
113        // Special handling for common tags to provide more meaningful output
114        let value_display = match self.tag {
115            tags::COMPRESSION => format!("{} ({})",
116                                         self.value_offset,
117                                         tag_utils::get_compression_name(self.value_offset)),
118
119            tags::PHOTOMETRIC_INTERPRETATION => format!("{} ({})",
120                                                        self.value_offset,
121                                                        tag_utils::get_photometric_name(self.value_offset)),
122
123            _ => self.value_offset.to_string()
124        };
125
126        format!("Tag: {} ({}), Type: {} ({}), Count: {}, Value/Offset: {}",
127                self.tag, tag_name, self.field_type, field_type_name, self.count, value_display)
128    }
129}
130
131impl IFD {
132    /// Creates a new IFD
133    ///
134    /// Initializes an empty Image File Directory with the specified
135    /// number (index) and file offset.
136    pub fn new(number: usize, offset: u64) -> Self {
137        info!("Creating new IFD #{} at offset {}", number, offset);
138
139        Self {
140            entries: Vec::new(),
141            number,
142            offset,
143            tag_map: HashMap::new(),
144        }
145    }
146
147    /// Adds an entry to this IFD
148    ///
149    /// This method adds a tag entry to the IFD and also updates the
150    /// lookup cache for fast access by tag number.
151    pub fn add_entry(&mut self, entry: IFDEntry) {
152        trace!("Adding entry to IFD #{}: {}", self.number, entry.description());
153
154        self.tag_map.insert(entry.tag, entry.clone());
155        self.entries.push(entry);
156    }
157
158    /// Gets a tag value (value_offset) directly
159    ///
160    /// This is a convenience method for quickly retrieving the value/offset
161    /// field of a tag without having to access the full entry.
162    pub fn get_tag_value(&self, tag: u16) -> Option<u64> {
163        let value = self.tag_map.get(&tag).map(|entry| entry.value_offset);
164        let tag_name = tag_utils::get_tag_name(tag);
165
166        if let Some(val) = value {
167            trace!("Found tag {} ({}) in IFD #{}: value/offset={}", tag, tag_name, self.number, val);
168        } else {
169            trace!("Tag {} ({}) not found in IFD #{}", tag, tag_name, self.number);
170        }
171
172        value
173    }
174
175    /// Checks if this IFD has a specific tag
176    ///
177    /// Returns true if the tag exists in this IFD, false otherwise.
178    pub fn has_tag(&self, tag: u16) -> bool {
179        let has_tag = self.tag_map.contains_key(&tag);
180        let tag_name = tag_utils::get_tag_name(tag);
181
182        trace!("Checking if IFD #{} has tag {} ({}): {}",
183               self.number, tag, tag_name, has_tag);
184
185        has_tag
186    }
187
188    /// Gets an IFD entry by tag
189    ///
190    /// Returns the full IFD entry for the specified tag, if it exists.
191    pub fn get_entry(&self, tag: u16) -> Option<&IFDEntry> {
192        let entry = self.tag_map.get(&tag);
193        let tag_name = tag_utils::get_tag_name(tag);
194
195        if entry.is_some() {
196            trace!("Retrieved entry for tag {} ({}) from IFD #{}", tag, tag_name, self.number);
197        }
198
199        entry
200    }
201
202    /// Gets the dimensions of the image described by this IFD
203    ///
204    /// Returns the width and height of the image if both tags are present.
205    pub fn get_dimensions(&self) -> Option<(u64, u64)> {
206        let width = self.get_tag_value(tags::IMAGE_WIDTH)?;
207        let height = self.get_tag_value(tags::IMAGE_LENGTH)?;
208
209        debug!("Image dimensions from IFD #{}: {}x{}", self.number, width, height);
210
211        Some((width, height))
212    }
213
214    /// Returns number of samples per pixel (default 1 if not specified)
215    ///
216    /// This indicates how many color channels the image has:
217    /// 1 for grayscale, 3 for RGB, 4 for RGBA, etc.
218    pub fn get_samples_per_pixel(&self) -> u64 {
219        let samples = self.get_tag_value(tags::SAMPLES_PER_PIXEL).unwrap_or(1);
220        debug!("Samples per pixel from IFD #{}: {}", self.number, samples);
221        samples
222    }
223
224    /// Gets all entries for this IFD
225    ///
226    /// Returns a reference to the entries vector.
227    pub fn get_entries(&self) -> &Vec<IFDEntry> {
228        &self.entries
229    }
230
231    /// Gets the number of entries in this IFD
232    ///
233    /// Returns the count of tag entries.
234    pub fn entry_count(&self) -> usize {
235        self.entries.len()
236    }
237}
238
239impl fmt::Display for IFD {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        writeln!(f, "IFD #{} (offset: {})", self.number, self.offset)?;
242        writeln!(f, "  Number of entries: {}", self.entries.len())?;
243
244        if let Some((width, height)) = self.get_dimensions() {
245            writeln!(f, "  Dimensions: {}x{}", width, height)?;
246        }
247
248        writeln!(f, "  Samples per pixel: {}", self.get_samples_per_pixel())?;
249
250        // Enhanced tag list with names
251        writeln!(f, "  Tags:")?;
252        for entry in &self.entries {
253            let tag_name = tag_utils::get_tag_name(entry.tag);
254            let field_type_name = tag_utils::get_field_type_name(entry.field_type);
255
256            // Special handling for known tags for more meaningful output
257            let value_display = match entry.tag {
258                tags::COMPRESSION => format!("{} ({})",
259                                             entry.value_offset,
260                                             tag_utils::get_compression_name(entry.value_offset)),
261
262                tags::PHOTOMETRIC_INTERPRETATION => format!("{} ({})",
263                                                            entry.value_offset,
264                                                            tag_utils::get_photometric_name(entry.value_offset)),
265
266                _ => entry.value_offset.to_string()
267            };
268
269            writeln!(f, "    {} ({}): {} [{}]",
270                     entry.tag, tag_name, value_display, field_type_name)?;
271        }
272
273        Ok(())
274    }
275}