Skip to main content

tiff_reader/
ifd.rs

1use std::collections::HashSet;
2
3use crate::error::{Error, Result};
4use crate::header::{ByteOrder, TiffHeader};
5use crate::io::Cursor;
6use crate::source::TiffSource;
7use crate::tag::{parse_tag_bigtiff, parse_tag_classic, Tag};
8
9pub use tiff_core::constants::{
10    TAG_BITS_PER_SAMPLE, TAG_COMPRESSION, TAG_IMAGE_LENGTH, TAG_IMAGE_WIDTH,
11    TAG_PHOTOMETRIC_INTERPRETATION, TAG_PLANAR_CONFIGURATION, TAG_PREDICTOR, TAG_ROWS_PER_STRIP,
12    TAG_SAMPLES_PER_PIXEL, TAG_SAMPLE_FORMAT, TAG_STRIP_BYTE_COUNTS, TAG_STRIP_OFFSETS,
13    TAG_TILE_BYTE_COUNTS, TAG_TILE_LENGTH, TAG_TILE_OFFSETS, TAG_TILE_WIDTH,
14};
15pub use tiff_core::RasterLayout;
16
17/// A parsed Image File Directory (IFD).
18#[derive(Debug, Clone)]
19pub struct Ifd {
20    /// Tags in this IFD, sorted by tag code.
21    tags: Vec<Tag>,
22    /// Index of this IFD in the chain (0-based).
23    pub index: usize,
24}
25
26impl Ifd {
27    /// Look up a tag by its code.
28    pub fn tag(&self, code: u16) -> Option<&Tag> {
29        self.tags
30            .binary_search_by_key(&code, |tag| tag.code)
31            .ok()
32            .map(|index| &self.tags[index])
33    }
34
35    /// Returns all tags in this IFD.
36    pub fn tags(&self) -> &[Tag] {
37        &self.tags
38    }
39
40    /// Image width in pixels.
41    pub fn width(&self) -> u32 {
42        self.tag_u32(TAG_IMAGE_WIDTH).unwrap_or(0)
43    }
44
45    /// Image height in pixels.
46    pub fn height(&self) -> u32 {
47        self.tag_u32(TAG_IMAGE_LENGTH).unwrap_or(0)
48    }
49
50    /// Bits per sample for each channel.
51    pub fn bits_per_sample(&self) -> Vec<u16> {
52        self.tag(TAG_BITS_PER_SAMPLE)
53            .and_then(|tag| tag.value.as_u16_slice().map(|values| values.to_vec()))
54            .unwrap_or_else(|| vec![1])
55    }
56
57    /// Compression scheme (1 = none, 5 = LZW, 8 = Deflate, ...).
58    pub fn compression(&self) -> u16 {
59        self.tag_u16(TAG_COMPRESSION).unwrap_or(1)
60    }
61
62    /// Photometric interpretation.
63    pub fn photometric_interpretation(&self) -> Option<u16> {
64        self.tag_u16(TAG_PHOTOMETRIC_INTERPRETATION)
65    }
66
67    /// Number of samples (bands) per pixel.
68    pub fn samples_per_pixel(&self) -> u16 {
69        self.tag_u16(TAG_SAMPLES_PER_PIXEL).unwrap_or(1)
70    }
71
72    /// Returns `true` if this IFD uses tiled layout.
73    pub fn is_tiled(&self) -> bool {
74        self.tag(TAG_TILE_WIDTH).is_some() && self.tag(TAG_TILE_LENGTH).is_some()
75    }
76
77    /// Tile width (only for tiled IFDs).
78    pub fn tile_width(&self) -> Option<u32> {
79        self.tag_u32(TAG_TILE_WIDTH)
80    }
81
82    /// Tile height (only for tiled IFDs).
83    pub fn tile_height(&self) -> Option<u32> {
84        self.tag_u32(TAG_TILE_LENGTH)
85    }
86
87    /// Rows per strip. Defaults to the image height when not present.
88    pub fn rows_per_strip(&self) -> Option<u32> {
89        Some(
90            self.tag_u32(TAG_ROWS_PER_STRIP)
91                .unwrap_or_else(|| self.height()),
92        )
93    }
94
95    /// Sample format for each channel.
96    pub fn sample_format(&self) -> Vec<u16> {
97        self.tag(TAG_SAMPLE_FORMAT)
98            .and_then(|tag| tag.value.as_u16_slice().map(|values| values.to_vec()))
99            .unwrap_or_else(|| vec![1])
100    }
101
102    /// Planar configuration. Defaults to chunky (1).
103    pub fn planar_configuration(&self) -> u16 {
104        self.tag_u16(TAG_PLANAR_CONFIGURATION).unwrap_or(1)
105    }
106
107    /// Predictor. Defaults to no predictor (1).
108    pub fn predictor(&self) -> u16 {
109        self.tag_u16(TAG_PREDICTOR).unwrap_or(1)
110    }
111
112    /// Strip offsets as normalized `u64`s.
113    pub fn strip_offsets(&self) -> Option<Vec<u64>> {
114        self.tag_u64_list(TAG_STRIP_OFFSETS)
115    }
116
117    /// Strip byte counts as normalized `u64`s.
118    pub fn strip_byte_counts(&self) -> Option<Vec<u64>> {
119        self.tag_u64_list(TAG_STRIP_BYTE_COUNTS)
120    }
121
122    /// Tile offsets as normalized `u64`s.
123    pub fn tile_offsets(&self) -> Option<Vec<u64>> {
124        self.tag_u64_list(TAG_TILE_OFFSETS)
125    }
126
127    /// Tile byte counts as normalized `u64`s.
128    pub fn tile_byte_counts(&self) -> Option<Vec<u64>> {
129        self.tag_u64_list(TAG_TILE_BYTE_COUNTS)
130    }
131
132    /// Normalize and validate the raster layout for typed reads.
133    pub fn raster_layout(&self) -> Result<RasterLayout> {
134        let width = self.width();
135        let height = self.height();
136        if width == 0 || height == 0 {
137            return Err(Error::InvalidImageLayout(format!(
138                "image dimensions must be positive, got {}x{}",
139                width, height
140            )));
141        }
142
143        let samples_per_pixel = self.samples_per_pixel();
144        if samples_per_pixel == 0 {
145            return Err(Error::InvalidImageLayout(
146                "SamplesPerPixel must be greater than zero".into(),
147            ));
148        }
149        let samples_per_pixel = samples_per_pixel as usize;
150
151        let bits = normalize_u16_values(
152            TAG_BITS_PER_SAMPLE,
153            self.bits_per_sample(),
154            samples_per_pixel,
155            1,
156        )?;
157        let formats = normalize_u16_values(
158            TAG_SAMPLE_FORMAT,
159            self.sample_format(),
160            samples_per_pixel,
161            1,
162        )?;
163
164        let first_bits = bits[0];
165        let first_format = formats[0];
166        if !bits.iter().all(|&value| value == first_bits) {
167            return Err(Error::InvalidImageLayout(
168                "mixed BitsPerSample values are not supported".into(),
169            ));
170        }
171        if !formats.iter().all(|&value| value == first_format) {
172            return Err(Error::InvalidImageLayout(
173                "mixed SampleFormat values are not supported".into(),
174            ));
175        }
176        if !matches!(first_bits, 8 | 16 | 32 | 64) {
177            return Err(Error::UnsupportedBitsPerSample(first_bits));
178        }
179        if !matches!(first_format, 1..=3) {
180            return Err(Error::UnsupportedSampleFormat(first_format));
181        }
182
183        let planar_configuration = self.planar_configuration();
184        if !matches!(planar_configuration, 1 | 2) {
185            return Err(Error::UnsupportedPlanarConfiguration(planar_configuration));
186        }
187
188        let predictor = self.predictor();
189        if !matches!(predictor, 1..=3) {
190            return Err(Error::UnsupportedPredictor(predictor));
191        }
192
193        Ok(RasterLayout {
194            width: width as usize,
195            height: height as usize,
196            samples_per_pixel,
197            bits_per_sample: first_bits,
198            bytes_per_sample: (first_bits / 8) as usize,
199            sample_format: first_format,
200            planar_configuration,
201            predictor,
202        })
203    }
204
205    fn tag_u16(&self, code: u16) -> Option<u16> {
206        self.tag(code).and_then(|tag| tag.value.as_u16())
207    }
208
209    fn tag_u32(&self, code: u16) -> Option<u32> {
210        self.tag(code).and_then(|tag| tag.value.as_u32())
211    }
212
213    fn tag_u64_list(&self, code: u16) -> Option<Vec<u64>> {
214        self.tag(code).and_then(|tag| tag.value.as_u64_vec())
215    }
216}
217
218/// Parse the chain of IFDs starting from the header's first IFD offset.
219pub fn parse_ifd_chain(source: &dyn TiffSource, header: &TiffHeader) -> Result<Vec<Ifd>> {
220    let mut ifds = Vec::new();
221    let mut offset = header.first_ifd_offset;
222    let mut index = 0usize;
223    let mut seen_offsets = HashSet::new();
224
225    while offset != 0 {
226        if !seen_offsets.insert(offset) {
227            return Err(Error::InvalidImageLayout(format!(
228                "IFD chain contains a loop at offset {offset}"
229            )));
230        }
231        if offset >= source.len() {
232            return Err(Error::Truncated {
233                offset,
234                needed: 2,
235                available: source.len().saturating_sub(offset),
236            });
237        }
238
239        let (tags, next_offset) = read_ifd(source, header, offset)?;
240
241        ifds.push(Ifd { tags, index });
242        offset = next_offset;
243        index += 1;
244
245        if index > 10_000 {
246            return Err(Error::Other("IFD chain exceeds 10,000 entries".into()));
247        }
248    }
249
250    Ok(ifds)
251}
252
253fn read_ifd(source: &dyn TiffSource, header: &TiffHeader, offset: u64) -> Result<(Vec<Tag>, u64)> {
254    let entry_count_size = if header.is_bigtiff() { 8usize } else { 2usize };
255    let entry_size = if header.is_bigtiff() {
256        20usize
257    } else {
258        12usize
259    };
260    let next_offset_size = if header.is_bigtiff() { 8usize } else { 4usize };
261
262    let count_bytes = source.read_exact_at(offset, entry_count_size)?;
263    let mut count_cursor = Cursor::new(&count_bytes, header.byte_order);
264    let count = if header.is_bigtiff() {
265        usize::try_from(count_cursor.read_u64()?).map_err(|_| {
266            Error::InvalidImageLayout("BigTIFF entry count does not fit in usize".into())
267        })?
268    } else {
269        count_cursor.read_u16()? as usize
270    };
271
272    let entries_len = count
273        .checked_mul(entry_size)
274        .and_then(|v| v.checked_add(next_offset_size))
275        .ok_or_else(|| Error::InvalidImageLayout("IFD byte length overflows usize".into()))?;
276    let body = source.read_exact_at(offset + entry_count_size as u64, entries_len)?;
277    let mut cursor = Cursor::new(&body, header.byte_order);
278
279    if header.is_bigtiff() {
280        let tags = parse_tags_bigtiff(&mut cursor, count, source, header.byte_order)?;
281        let next = cursor.read_u64()?;
282        Ok((tags, next))
283    } else {
284        let tags = parse_tags_classic(&mut cursor, count, source, header.byte_order)?;
285        let next = cursor.read_u32()? as u64;
286        Ok((tags, next))
287    }
288}
289
290fn normalize_u16_values(
291    tag: u16,
292    values: Vec<u16>,
293    expected_len: usize,
294    default_value: u16,
295) -> Result<Vec<u16>> {
296    match values.len() {
297        0 => Ok(vec![default_value; expected_len]),
298        1 if expected_len > 1 => Ok(vec![values[0]; expected_len]),
299        len if len == expected_len => Ok(values),
300        len => Err(Error::InvalidTagValue {
301            tag,
302            reason: format!("expected 1 or {expected_len} values, found {len}"),
303        }),
304    }
305}
306
307/// Parse classic TIFF IFD entries (12 bytes each).
308fn parse_tags_classic(
309    cursor: &mut Cursor<'_>,
310    count: usize,
311    source: &dyn TiffSource,
312    byte_order: ByteOrder,
313) -> Result<Vec<Tag>> {
314    let mut tags = Vec::with_capacity(count);
315    for _ in 0..count {
316        let code = cursor.read_u16()?;
317        let type_code = cursor.read_u16()?;
318        let value_count = cursor.read_u32()? as u64;
319        let value_offset_bytes = cursor.read_bytes(4)?;
320        let tag = parse_tag_classic(
321            code,
322            type_code,
323            value_count,
324            value_offset_bytes,
325            source,
326            byte_order,
327        )?;
328        tags.push(tag);
329    }
330    tags.sort_by_key(|tag| tag.code);
331    Ok(tags)
332}
333
334/// Parse BigTIFF IFD entries (20 bytes each).
335fn parse_tags_bigtiff(
336    cursor: &mut Cursor<'_>,
337    count: usize,
338    source: &dyn TiffSource,
339    byte_order: ByteOrder,
340) -> Result<Vec<Tag>> {
341    let mut tags = Vec::with_capacity(count);
342    for _ in 0..count {
343        let code = cursor.read_u16()?;
344        let type_code = cursor.read_u16()?;
345        let value_count = cursor.read_u64()?;
346        let value_offset_bytes = cursor.read_bytes(8)?;
347        let tag = parse_tag_bigtiff(
348            code,
349            type_code,
350            value_count,
351            value_offset_bytes,
352            source,
353            byte_order,
354        )?;
355        tags.push(tag);
356    }
357    tags.sort_by_key(|tag| tag.code);
358    Ok(tags)
359}
360
361#[cfg(test)]
362mod tests {
363    use super::{
364        Ifd, RasterLayout, TAG_BITS_PER_SAMPLE, TAG_IMAGE_LENGTH, TAG_IMAGE_WIDTH,
365        TAG_SAMPLES_PER_PIXEL, TAG_SAMPLE_FORMAT,
366    };
367    use crate::tag::{Tag, TagType, TagValue};
368
369    fn make_ifd(tags: Vec<Tag>) -> Ifd {
370        let mut tags = tags;
371        tags.sort_by_key(|tag| tag.code);
372        Ifd { tags, index: 0 }
373    }
374
375    #[test]
376    fn normalizes_single_value_sample_tags() {
377        let ifd = make_ifd(vec![
378            Tag {
379                code: TAG_IMAGE_WIDTH,
380                tag_type: TagType::Long,
381                count: 1,
382                value: TagValue::Long(vec![10]),
383            },
384            Tag {
385                code: TAG_IMAGE_LENGTH,
386                tag_type: TagType::Long,
387                count: 1,
388                value: TagValue::Long(vec![5]),
389            },
390            Tag {
391                code: TAG_SAMPLES_PER_PIXEL,
392                tag_type: TagType::Short,
393                count: 1,
394                value: TagValue::Short(vec![3]),
395            },
396            Tag {
397                code: TAG_BITS_PER_SAMPLE,
398                tag_type: TagType::Short,
399                count: 1,
400                value: TagValue::Short(vec![16]),
401            },
402            Tag {
403                code: TAG_SAMPLE_FORMAT,
404                tag_type: TagType::Short,
405                count: 1,
406                value: TagValue::Short(vec![1]),
407            },
408        ]);
409
410        let layout = ifd.raster_layout().unwrap();
411        assert_eq!(layout.width, 10);
412        assert_eq!(layout.height, 5);
413        assert_eq!(layout.samples_per_pixel, 3);
414        assert_eq!(layout.bytes_per_sample, 2);
415    }
416
417    #[test]
418    fn rejects_mixed_sample_formats() {
419        let ifd = make_ifd(vec![
420            Tag {
421                code: TAG_IMAGE_WIDTH,
422                tag_type: TagType::Long,
423                count: 1,
424                value: TagValue::Long(vec![1]),
425            },
426            Tag {
427                code: TAG_IMAGE_LENGTH,
428                tag_type: TagType::Long,
429                count: 1,
430                value: TagValue::Long(vec![1]),
431            },
432            Tag {
433                code: TAG_SAMPLES_PER_PIXEL,
434                tag_type: TagType::Short,
435                count: 1,
436                value: TagValue::Short(vec![2]),
437            },
438            Tag {
439                code: TAG_BITS_PER_SAMPLE,
440                tag_type: TagType::Short,
441                count: 2,
442                value: TagValue::Short(vec![16, 16]),
443            },
444            Tag {
445                code: TAG_SAMPLE_FORMAT,
446                tag_type: TagType::Short,
447                count: 2,
448                value: TagValue::Short(vec![1, 3]),
449            },
450        ]);
451
452        assert!(ifd.raster_layout().is_err());
453    }
454
455    #[test]
456    fn raster_layout_helpers_match_expected_strides() {
457        let layout = RasterLayout {
458            width: 4,
459            height: 3,
460            samples_per_pixel: 2,
461            bits_per_sample: 16,
462            bytes_per_sample: 2,
463            sample_format: 1,
464            planar_configuration: 1,
465            predictor: 1,
466        };
467        assert_eq!(layout.pixel_stride_bytes(), 4);
468        assert_eq!(layout.row_bytes(), 16);
469        assert_eq!(layout.sample_plane_row_bytes(), 8);
470    }
471}