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::Tag;
8
9/// A parsed Image File Directory (IFD).
10#[derive(Debug, Clone)]
11pub struct Ifd {
12    /// Tags in this IFD, sorted by tag code.
13    tags: Vec<Tag>,
14    /// Index of this IFD in the chain (0-based).
15    pub index: usize,
16}
17
18/// Raster layout information normalized from TIFF tags.
19#[derive(Debug, Clone, Copy)]
20pub struct RasterLayout {
21    pub width: usize,
22    pub height: usize,
23    pub samples_per_pixel: usize,
24    pub bits_per_sample: u16,
25    pub bytes_per_sample: usize,
26    pub sample_format: u16,
27    pub planar_configuration: u16,
28    pub predictor: u16,
29}
30
31impl RasterLayout {
32    pub fn pixel_stride_bytes(&self) -> usize {
33        self.samples_per_pixel * self.bytes_per_sample
34    }
35
36    pub fn row_bytes(&self) -> usize {
37        self.width * self.pixel_stride_bytes()
38    }
39
40    pub fn sample_plane_row_bytes(&self) -> usize {
41        self.width * self.bytes_per_sample
42    }
43}
44
45// Well-known TIFF tag codes.
46pub const TAG_IMAGE_WIDTH: u16 = 256;
47pub const TAG_IMAGE_LENGTH: u16 = 257;
48pub const TAG_BITS_PER_SAMPLE: u16 = 258;
49pub const TAG_COMPRESSION: u16 = 259;
50pub const TAG_PHOTOMETRIC_INTERPRETATION: u16 = 262;
51pub const TAG_STRIP_OFFSETS: u16 = 273;
52pub const TAG_SAMPLES_PER_PIXEL: u16 = 277;
53pub const TAG_ROWS_PER_STRIP: u16 = 278;
54pub const TAG_STRIP_BYTE_COUNTS: u16 = 279;
55pub const TAG_PLANAR_CONFIGURATION: u16 = 284;
56pub const TAG_PREDICTOR: u16 = 317;
57pub const TAG_TILE_WIDTH: u16 = 322;
58pub const TAG_TILE_LENGTH: u16 = 323;
59pub const TAG_TILE_OFFSETS: u16 = 324;
60pub const TAG_TILE_BYTE_COUNTS: u16 = 325;
61pub const TAG_SAMPLE_FORMAT: u16 = 339;
62
63impl Ifd {
64    /// Look up a tag by its code.
65    pub fn tag(&self, code: u16) -> Option<&Tag> {
66        self.tags
67            .binary_search_by_key(&code, |tag| tag.code)
68            .ok()
69            .map(|index| &self.tags[index])
70    }
71
72    /// Returns all tags in this IFD.
73    pub fn tags(&self) -> &[Tag] {
74        &self.tags
75    }
76
77    /// Image width in pixels.
78    pub fn width(&self) -> u32 {
79        self.tag_u32(TAG_IMAGE_WIDTH).unwrap_or(0)
80    }
81
82    /// Image height in pixels.
83    pub fn height(&self) -> u32 {
84        self.tag_u32(TAG_IMAGE_LENGTH).unwrap_or(0)
85    }
86
87    /// Bits per sample for each channel.
88    pub fn bits_per_sample(&self) -> Vec<u16> {
89        self.tag(TAG_BITS_PER_SAMPLE)
90            .and_then(|tag| tag.value.as_u16_slice().map(|values| values.to_vec()))
91            .unwrap_or_else(|| vec![1])
92    }
93
94    /// Compression scheme (1 = none, 5 = LZW, 8 = Deflate, ...).
95    pub fn compression(&self) -> u16 {
96        self.tag_u16(TAG_COMPRESSION).unwrap_or(1)
97    }
98
99    /// Photometric interpretation.
100    pub fn photometric_interpretation(&self) -> Option<u16> {
101        self.tag_u16(TAG_PHOTOMETRIC_INTERPRETATION)
102    }
103
104    /// Number of samples (bands) per pixel.
105    pub fn samples_per_pixel(&self) -> u16 {
106        self.tag_u16(TAG_SAMPLES_PER_PIXEL).unwrap_or(1)
107    }
108
109    /// Returns `true` if this IFD uses tiled layout.
110    pub fn is_tiled(&self) -> bool {
111        self.tag(TAG_TILE_WIDTH).is_some() && self.tag(TAG_TILE_LENGTH).is_some()
112    }
113
114    /// Tile width (only for tiled IFDs).
115    pub fn tile_width(&self) -> Option<u32> {
116        self.tag_u32(TAG_TILE_WIDTH)
117    }
118
119    /// Tile height (only for tiled IFDs).
120    pub fn tile_height(&self) -> Option<u32> {
121        self.tag_u32(TAG_TILE_LENGTH)
122    }
123
124    /// Rows per strip. Defaults to the image height when not present.
125    pub fn rows_per_strip(&self) -> Option<u32> {
126        Some(
127            self.tag_u32(TAG_ROWS_PER_STRIP)
128                .unwrap_or_else(|| self.height()),
129        )
130    }
131
132    /// Sample format for each channel.
133    pub fn sample_format(&self) -> Vec<u16> {
134        self.tag(TAG_SAMPLE_FORMAT)
135            .and_then(|tag| tag.value.as_u16_slice().map(|values| values.to_vec()))
136            .unwrap_or_else(|| vec![1])
137    }
138
139    /// Planar configuration. Defaults to chunky (1).
140    pub fn planar_configuration(&self) -> u16 {
141        self.tag_u16(TAG_PLANAR_CONFIGURATION).unwrap_or(1)
142    }
143
144    /// Predictor. Defaults to no predictor (1).
145    pub fn predictor(&self) -> u16 {
146        self.tag_u16(TAG_PREDICTOR).unwrap_or(1)
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 = Tag::parse_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 = Tag::parse_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, RasterLayout, TAG_BITS_PER_SAMPLE, TAG_IMAGE_LENGTH, TAG_IMAGE_WIDTH,
402        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}