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