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