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::{checked_tag_value_byte_len, parse_tag_bigtiff, parse_tag_classic, Tag};
8
9pub use tiff_core::constants::{
10    TAG_BITS_PER_SAMPLE, TAG_COLOR_MAP, TAG_COMPRESSION, TAG_EXTRA_SAMPLES, TAG_IMAGE_LENGTH,
11    TAG_IMAGE_WIDTH, TAG_INK_SET, TAG_LERC_PARAMETERS, TAG_PHOTOMETRIC_INTERPRETATION,
12    TAG_PLANAR_CONFIGURATION, TAG_PREDICTOR, TAG_REFERENCE_BLACK_WHITE, TAG_ROWS_PER_STRIP,
13    TAG_SAMPLES_PER_PIXEL, TAG_SAMPLE_FORMAT, TAG_STRIP_BYTE_COUNTS, TAG_STRIP_OFFSETS,
14    TAG_SUB_IFDS, TAG_TILE_BYTE_COUNTS, TAG_TILE_LENGTH, TAG_TILE_OFFSETS, TAG_TILE_WIDTH,
15    TAG_YCBCR_POSITIONING, TAG_YCBCR_SUBSAMPLING,
16};
17pub use tiff_core::RasterLayout;
18
19pub use tiff_core::{
20    ColorMap, ColorModel, ExtraSample, InkSet, LercAdditionalCompression,
21    PhotometricInterpretation, YCbCrPositioning,
22};
23
24/// Budgets that bound TIFF IFD parsing allocations.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct ParseBudgets {
27    /// Maximum number of IFDs to follow in a chain.
28    pub max_ifds: usize,
29    /// Maximum tag entries allowed in one IFD.
30    pub max_ifd_entries: usize,
31    /// Maximum encoded value bytes allowed for one tag value.
32    pub max_tag_value_bytes: usize,
33    /// Maximum encoded value bytes allowed across all parsed tag values.
34    pub max_metadata_value_bytes: usize,
35}
36
37impl Default for ParseBudgets {
38    fn default() -> Self {
39        Self {
40            max_ifds: 10_000,
41            max_ifd_entries: 65_536,
42            max_tag_value_bytes: 128 * 1024 * 1024,
43            max_metadata_value_bytes: 512 * 1024 * 1024,
44        }
45    }
46}
47
48#[derive(Default)]
49struct ParseBudgetUsage {
50    metadata_value_bytes: usize,
51}
52
53impl ParseBudgetUsage {
54    fn consume_tag_value_bytes(
55        &mut self,
56        tag: u16,
57        bytes: usize,
58        budgets: ParseBudgets,
59    ) -> Result<()> {
60        let total = self
61            .metadata_value_bytes
62            .checked_add(bytes)
63            .ok_or_else(|| Error::InvalidTagValue {
64                tag,
65                reason: "aggregate metadata value byte length overflows usize".into(),
66            })?;
67        if total > budgets.max_metadata_value_bytes {
68            return Err(Error::InvalidTagValue {
69                tag,
70                reason: format!(
71                    "aggregate metadata value byte length {total} exceeds parse budget {}",
72                    budgets.max_metadata_value_bytes
73                ),
74            });
75        }
76        self.metadata_value_bytes = total;
77        Ok(())
78    }
79}
80
81/// Parsed TIFF `LercParameters` tag payload.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct LercParameters {
84    pub version: u32,
85    pub additional_compression: LercAdditionalCompression,
86}
87
88/// A parsed Image File Directory (IFD).
89#[derive(Debug, Clone)]
90pub struct Ifd {
91    /// Tags in this IFD, sorted by tag code.
92    tags: Vec<Tag>,
93    /// Index of this IFD in the chain (0-based).
94    pub index: usize,
95}
96
97impl Ifd {
98    /// Look up a tag by its code.
99    pub fn tag(&self, code: u16) -> Option<&Tag> {
100        self.tags
101            .binary_search_by_key(&code, |tag| tag.code)
102            .ok()
103            .map(|index| &self.tags[index])
104    }
105
106    /// Returns all tags in this IFD.
107    pub fn tags(&self) -> &[Tag] {
108        &self.tags
109    }
110
111    /// Image width in pixels.
112    pub fn width(&self) -> u32 {
113        self.tag_u32(TAG_IMAGE_WIDTH).unwrap_or(0)
114    }
115
116    /// Image height in pixels.
117    pub fn height(&self) -> u32 {
118        self.tag_u32(TAG_IMAGE_LENGTH).unwrap_or(0)
119    }
120
121    /// Bits per sample for each channel.
122    pub fn bits_per_sample(&self) -> Vec<u16> {
123        self.tag(TAG_BITS_PER_SAMPLE)
124            .and_then(|tag| tag.value.as_u16_slice().map(|values| values.to_vec()))
125            .unwrap_or_else(|| vec![1])
126    }
127
128    /// Compression scheme (1 = none, 5 = LZW, 8 = Deflate, ...).
129    pub fn compression(&self) -> u16 {
130        self.tag_u16(TAG_COMPRESSION).unwrap_or(1)
131    }
132
133    /// Photometric interpretation.
134    pub fn photometric_interpretation(&self) -> Option<u16> {
135        self.tag_u16(TAG_PHOTOMETRIC_INTERPRETATION)
136    }
137
138    /// Typed photometric interpretation, defaulting to `MinIsBlack` when the
139    /// TIFF tag is omitted.
140    pub fn photometric_interpretation_enum(&self) -> Option<PhotometricInterpretation> {
141        PhotometricInterpretation::from_code(self.photometric_interpretation().unwrap_or(1))
142    }
143
144    /// Number of samples (bands) per pixel.
145    pub fn samples_per_pixel(&self) -> u16 {
146        self.tag_u16(TAG_SAMPLES_PER_PIXEL).unwrap_or(1)
147    }
148
149    /// Returns `true` if this IFD uses tiled layout.
150    pub fn is_tiled(&self) -> bool {
151        self.tag(TAG_TILE_WIDTH).is_some() && self.tag(TAG_TILE_LENGTH).is_some()
152    }
153
154    /// Tile width (only for tiled IFDs).
155    pub fn tile_width(&self) -> Option<u32> {
156        self.tag_u32(TAG_TILE_WIDTH)
157    }
158
159    /// Tile height (only for tiled IFDs).
160    pub fn tile_height(&self) -> Option<u32> {
161        self.tag_u32(TAG_TILE_LENGTH)
162    }
163
164    /// Rows per strip. Defaults to the image height when not present.
165    pub fn rows_per_strip(&self) -> Option<u32> {
166        Some(
167            self.tag_u32(TAG_ROWS_PER_STRIP)
168                .unwrap_or_else(|| self.height()),
169        )
170    }
171
172    /// Sample format for each channel.
173    pub fn sample_format(&self) -> Vec<u16> {
174        self.tag(TAG_SAMPLE_FORMAT)
175            .and_then(|tag| tag.value.as_u16_slice().map(|values| values.to_vec()))
176            .unwrap_or_else(|| vec![1])
177    }
178
179    /// Planar configuration. Defaults to chunky (1).
180    pub fn planar_configuration(&self) -> u16 {
181        self.tag_u16(TAG_PLANAR_CONFIGURATION).unwrap_or(1)
182    }
183
184    /// Predictor. Defaults to no predictor (1).
185    pub fn predictor(&self) -> u16 {
186        self.tag_u16(TAG_PREDICTOR).unwrap_or(1)
187    }
188
189    /// TIFF-side LERC parameters, when present.
190    pub fn lerc_parameters(&self) -> Result<Option<LercParameters>> {
191        let Some(tag) = self.tag(TAG_LERC_PARAMETERS) else {
192            return Ok(None);
193        };
194        let values = tag.value.as_u32_slice().ok_or(Error::UnexpectedTagType {
195            tag: TAG_LERC_PARAMETERS,
196            expected: "LONG",
197            actual: tag.tag_type.to_code(),
198        })?;
199        if values.len() < 2 {
200            return Err(Error::InvalidTagValue {
201                tag: TAG_LERC_PARAMETERS,
202                reason: "LercParameters must contain at least version and additional compression"
203                    .into(),
204            });
205        }
206        let additional_compression =
207            LercAdditionalCompression::from_code(values[1]).ok_or(Error::InvalidTagValue {
208                tag: TAG_LERC_PARAMETERS,
209                reason: format!("unsupported LERC additional compression code {}", values[1]),
210            })?;
211        Ok(Some(LercParameters {
212            version: values[0],
213            additional_compression,
214        }))
215    }
216
217    /// TIFF ExtraSamples semantics.
218    pub fn extra_samples(&self) -> Result<Vec<ExtraSample>> {
219        let Some(tag) = self.tag(TAG_EXTRA_SAMPLES) else {
220            return Ok(Vec::new());
221        };
222        let values = tag.value.as_u16_slice().ok_or(Error::UnexpectedTagType {
223            tag: TAG_EXTRA_SAMPLES,
224            expected: "SHORT",
225            actual: tag.tag_type.to_code(),
226        })?;
227        Ok(values.iter().copied().map(ExtraSample::from_code).collect())
228    }
229
230    /// TIFF ColorMap values for palette images.
231    pub fn color_map(&self) -> Result<Option<ColorMap>> {
232        let Some(tag) = self.tag(TAG_COLOR_MAP) else {
233            return Ok(None);
234        };
235        let values = tag.value.as_u16_slice().ok_or(Error::UnexpectedTagType {
236            tag: TAG_COLOR_MAP,
237            expected: "SHORT",
238            actual: tag.tag_type.to_code(),
239        })?;
240        ColorMap::from_tag_values(values)
241            .map(Some)
242            .map_err(|reason| Error::InvalidTagValue {
243                tag: TAG_COLOR_MAP,
244                reason,
245            })
246    }
247
248    /// TIFF InkSet semantics for separated photometric data.
249    pub fn ink_set(&self) -> Result<Option<InkSet>> {
250        let Some(tag) = self.tag(TAG_INK_SET) else {
251            return Ok(None);
252        };
253        let value = tag.value.as_u16().ok_or(Error::UnexpectedTagType {
254            tag: TAG_INK_SET,
255            expected: "SHORT",
256            actual: tag.tag_type.to_code(),
257        })?;
258        Ok(Some(InkSet::from_code(value)))
259    }
260
261    /// TIFF YCbCr chroma subsampling factors.
262    pub fn ycbcr_subsampling(&self) -> Result<Option<[u16; 2]>> {
263        let Some(tag) = self.tag(TAG_YCBCR_SUBSAMPLING) else {
264            return Ok(None);
265        };
266        let values = tag.value.as_u16_slice().ok_or(Error::UnexpectedTagType {
267            tag: TAG_YCBCR_SUBSAMPLING,
268            expected: "SHORT",
269            actual: tag.tag_type.to_code(),
270        })?;
271        match values {
272            [h, v] => Ok(Some([*h, *v])),
273            _ => Err(Error::InvalidTagValue {
274                tag: TAG_YCBCR_SUBSAMPLING,
275                reason: format!("expected 2 SHORT values, found {}", values.len()),
276            }),
277        }
278    }
279
280    /// TIFF YCbCr sample positioning.
281    pub fn ycbcr_positioning(&self) -> Result<Option<YCbCrPositioning>> {
282        let Some(tag) = self.tag(TAG_YCBCR_POSITIONING) else {
283            return Ok(None);
284        };
285        let value = tag.value.as_u16().ok_or(Error::UnexpectedTagType {
286            tag: TAG_YCBCR_POSITIONING,
287            expected: "SHORT",
288            actual: tag.tag_type.to_code(),
289        })?;
290        Ok(Some(YCbCrPositioning::from_code(value)))
291    }
292
293    /// TIFF ReferenceBlackWhite values normalized to `f64`.
294    pub fn reference_black_white(&self) -> Result<Option<[f64; 6]>> {
295        let Some(tag) = self.tag(TAG_REFERENCE_BLACK_WHITE) else {
296            return Ok(None);
297        };
298        let values = tag.value.as_f64_vec().ok_or(Error::UnexpectedTagType {
299            tag: TAG_REFERENCE_BLACK_WHITE,
300            expected: "RATIONAL or DOUBLE",
301            actual: tag.tag_type.to_code(),
302        })?;
303        match values.as_slice() {
304            [a, b, c, d, e, f] => Ok(Some([*a, *b, *c, *d, *e, *f])),
305            _ => Err(Error::InvalidTagValue {
306                tag: TAG_REFERENCE_BLACK_WHITE,
307                reason: format!("expected 6 values, found {}", values.len()),
308            }),
309        }
310    }
311
312    /// Structured color-model metadata synthesized from TIFF photometric and
313    /// ancillary color tags.
314    pub fn color_model(&self) -> Result<ColorModel> {
315        let photometric = self
316            .photometric_interpretation_enum()
317            .ok_or(Error::InvalidTagValue {
318                tag: TAG_PHOTOMETRIC_INTERPRETATION,
319                reason: format!(
320                    "unsupported photometric interpretation {}",
321                    self.photometric_interpretation().unwrap_or(1)
322                ),
323            })?;
324        let samples_per_pixel = self.samples_per_pixel();
325        let extra_samples = self.extra_samples()?;
326
327        match photometric {
328            PhotometricInterpretation::MinIsWhite => Ok(ColorModel::Grayscale {
329                white_is_zero: true,
330                extra_samples: resolve_fixed_model_extra_samples(
331                    photometric,
332                    samples_per_pixel,
333                    1,
334                    extra_samples,
335                )?,
336            }),
337            PhotometricInterpretation::MinIsBlack => Ok(ColorModel::Grayscale {
338                white_is_zero: false,
339                extra_samples: resolve_fixed_model_extra_samples(
340                    photometric,
341                    samples_per_pixel,
342                    1,
343                    extra_samples,
344                )?,
345            }),
346            PhotometricInterpretation::Rgb => Ok(ColorModel::Rgb {
347                extra_samples: resolve_fixed_model_extra_samples(
348                    photometric,
349                    samples_per_pixel,
350                    3,
351                    extra_samples,
352                )?,
353            }),
354            PhotometricInterpretation::Palette => {
355                let color_map = self.color_map()?.ok_or(Error::InvalidImageLayout(
356                    "palette TIFF is missing ColorMap".into(),
357                ))?;
358                Ok(ColorModel::Palette {
359                    color_map,
360                    extra_samples: resolve_fixed_model_extra_samples(
361                        photometric,
362                        samples_per_pixel,
363                        1,
364                        extra_samples,
365                    )?,
366                })
367            }
368            PhotometricInterpretation::Mask => Ok(ColorModel::TransparencyMask),
369            PhotometricInterpretation::Separated => {
370                let ink_set = self.ink_set()?.unwrap_or(InkSet::Cmyk);
371                if ink_set == InkSet::Cmyk {
372                    let extra_samples = resolve_fixed_model_extra_samples(
373                        photometric,
374                        samples_per_pixel,
375                        4,
376                        extra_samples,
377                    )?;
378                    Ok(ColorModel::Cmyk { extra_samples })
379                } else {
380                    let color_channels = samples_per_pixel
381                        .checked_sub(extra_samples.len() as u16)
382                        .ok_or_else(|| {
383                            Error::InvalidImageLayout(format!(
384                                "{} photometric interpretation defines more ExtraSamples than total channels",
385                                photometric_name(photometric)
386                            ))
387                        })?;
388                    Ok(ColorModel::Separated {
389                        ink_set,
390                        color_channels,
391                        extra_samples,
392                    })
393                }
394            }
395            PhotometricInterpretation::YCbCr => Ok(ColorModel::YCbCr {
396                subsampling: self.ycbcr_subsampling()?.unwrap_or([1, 1]),
397                positioning: self
398                    .ycbcr_positioning()?
399                    .unwrap_or(YCbCrPositioning::Centered),
400                extra_samples: resolve_fixed_model_extra_samples(
401                    photometric,
402                    samples_per_pixel,
403                    3,
404                    extra_samples,
405                )?,
406            }),
407            PhotometricInterpretation::CieLab => Ok(ColorModel::CieLab {
408                extra_samples: resolve_fixed_model_extra_samples(
409                    photometric,
410                    samples_per_pixel,
411                    3,
412                    extra_samples,
413                )?,
414            }),
415        }
416    }
417
418    /// Strip offsets as normalized `u64`s.
419    pub fn strip_offsets(&self) -> Option<Vec<u64>> {
420        self.tag_u64_list(TAG_STRIP_OFFSETS)
421    }
422
423    /// Strip byte counts as normalized `u64`s.
424    pub fn strip_byte_counts(&self) -> Option<Vec<u64>> {
425        self.tag_u64_list(TAG_STRIP_BYTE_COUNTS)
426    }
427
428    /// Tile offsets as normalized `u64`s.
429    pub fn tile_offsets(&self) -> Option<Vec<u64>> {
430        self.tag_u64_list(TAG_TILE_OFFSETS)
431    }
432
433    /// Tile byte counts as normalized `u64`s.
434    pub fn tile_byte_counts(&self) -> Option<Vec<u64>> {
435        self.tag_u64_list(TAG_TILE_BYTE_COUNTS)
436    }
437
438    /// SubIFD offsets as normalized `u64`s.
439    pub fn sub_ifd_offsets(&self) -> Option<Vec<u64>> {
440        self.tag_u64_list(TAG_SUB_IFDS)
441    }
442
443    /// Normalize and validate the raster layout for typed reads.
444    pub fn raster_layout(&self) -> Result<RasterLayout> {
445        let width = self.width();
446        let height = self.height();
447        if width == 0 || height == 0 {
448            return Err(Error::InvalidImageLayout(format!(
449                "image dimensions must be positive, got {}x{}",
450                width, height
451            )));
452        }
453
454        let samples_per_pixel = self.samples_per_pixel();
455        if samples_per_pixel == 0 {
456            return Err(Error::InvalidImageLayout(
457                "SamplesPerPixel must be greater than zero".into(),
458            ));
459        }
460        let samples_per_pixel = samples_per_pixel as usize;
461
462        let bits = normalize_u16_values(
463            TAG_BITS_PER_SAMPLE,
464            self.bits_per_sample(),
465            samples_per_pixel,
466            1,
467        )?;
468        let formats = normalize_u16_values(
469            TAG_SAMPLE_FORMAT,
470            self.sample_format(),
471            samples_per_pixel,
472            1,
473        )?;
474
475        let first_bits = bits[0];
476        let first_format = formats[0];
477        if !bits.iter().all(|&value| value == first_bits) {
478            return Err(Error::InvalidImageLayout(
479                "mixed BitsPerSample values are not supported".into(),
480            ));
481        }
482        if !formats.iter().all(|&value| value == first_format) {
483            return Err(Error::InvalidImageLayout(
484                "mixed SampleFormat values are not supported".into(),
485            ));
486        }
487        if !matches!(first_format, 1..=3) {
488            return Err(Error::UnsupportedSampleFormat(first_format));
489        }
490        validate_sample_encoding(first_format, first_bits)?;
491
492        let planar_configuration = self.planar_configuration();
493        if !matches!(planar_configuration, 1 | 2) {
494            return Err(Error::UnsupportedPlanarConfiguration(planar_configuration));
495        }
496
497        let predictor = self.predictor();
498        if !matches!(predictor, 1..=3) {
499            return Err(Error::UnsupportedPredictor(predictor));
500        }
501        if first_bits < 8 && predictor != 1 {
502            return Err(Error::InvalidImageLayout(
503                "predictors are not supported for sub-byte sample encodings".into(),
504            ));
505        }
506
507        validate_color_model(self, samples_per_pixel as u16, first_bits)?;
508
509        Ok(RasterLayout {
510            width: width as usize,
511            height: height as usize,
512            samples_per_pixel,
513            bits_per_sample: first_bits,
514            bytes_per_sample: usize::from(first_bits.div_ceil(8)),
515            sample_format: first_format,
516            planar_configuration,
517            predictor,
518        })
519    }
520
521    /// Normalize the raster layout produced by high-level pixel reads.
522    ///
523    /// This layout reflects color-decoded output rather than the on-disk sample
524    /// organization. For example, palette and YCbCr rasters decode to RGB
525    /// pixels, and sub-byte integer rasters expand to 8-bit samples.
526    pub fn decoded_raster_layout(&self) -> Result<RasterLayout> {
527        let storage = self.raster_layout()?;
528        let color_model = self.color_model()?;
529        let decoded_samples = match &color_model {
530            ColorModel::Palette { extra_samples, .. } => 3 + extra_samples.len(),
531            ColorModel::Cmyk { extra_samples } => 3 + extra_samples.len(),
532            ColorModel::YCbCr { extra_samples, .. } => 3 + extra_samples.len(),
533            ColorModel::Grayscale { extra_samples, .. } => 1 + extra_samples.len(),
534            ColorModel::Rgb { extra_samples } => 3 + extra_samples.len(),
535            ColorModel::Separated {
536                color_channels,
537                extra_samples,
538                ..
539            } => *color_channels as usize + extra_samples.len(),
540            ColorModel::CieLab { extra_samples } => 3 + extra_samples.len(),
541            ColorModel::TransparencyMask => 1,
542        };
543        let (sample_format, bits_per_sample) = match &color_model {
544            ColorModel::Palette { color_map, .. } => {
545                if color_map_is_u8_equivalent(color_map) {
546                    (1, 8)
547                } else {
548                    (1, 16)
549                }
550            }
551            ColorModel::YCbCr { .. } | ColorModel::Cmyk { .. } => {
552                if storage.sample_format != 1 {
553                    return Err(Error::InvalidImageLayout(
554                        "decoded YCbCr/CMYK reads require unsigned integer source samples".into(),
555                    ));
556                }
557                (1, decoded_uint_bits(storage.bits_per_sample))
558            }
559            _ => (
560                storage.sample_format,
561                decoded_bits(storage.sample_format, storage.bits_per_sample)?,
562            ),
563        };
564
565        Ok(RasterLayout {
566            width: storage.width,
567            height: storage.height,
568            samples_per_pixel: decoded_samples,
569            bits_per_sample,
570            bytes_per_sample: usize::from(bits_per_sample.div_ceil(8)),
571            sample_format,
572            planar_configuration: 1,
573            predictor: 1,
574        })
575    }
576
577    fn tag_u16(&self, code: u16) -> Option<u16> {
578        self.tag(code).and_then(|tag| tag.value.as_u16())
579    }
580
581    fn tag_u32(&self, code: u16) -> Option<u32> {
582        self.tag(code).and_then(|tag| tag.value.as_u32())
583    }
584
585    fn tag_u64_list(&self, code: u16) -> Option<Vec<u64>> {
586        self.tag(code).and_then(|tag| tag.value.as_u64_vec())
587    }
588}
589
590/// Parse the chain of IFDs starting from the header's first IFD offset.
591pub fn parse_ifd_chain(source: &dyn TiffSource, header: &TiffHeader) -> Result<Vec<Ifd>> {
592    parse_ifd_chain_with_budgets(source, header, ParseBudgets::default())
593}
594
595/// Parse the chain of IFDs using explicit allocation budgets.
596pub fn parse_ifd_chain_with_budgets(
597    source: &dyn TiffSource,
598    header: &TiffHeader,
599    budgets: ParseBudgets,
600) -> Result<Vec<Ifd>> {
601    let mut ifds = Vec::new();
602    let mut offset = header.first_ifd_offset;
603    let mut index = 0usize;
604    let mut seen_offsets = HashSet::new();
605    let mut usage = ParseBudgetUsage::default();
606
607    while offset != 0 {
608        if index >= budgets.max_ifds {
609            return Err(Error::Other(format!(
610                "IFD chain exceeds parse budget of {} IFDs",
611                budgets.max_ifds
612            )));
613        }
614        if !seen_offsets.insert(offset) {
615            return Err(Error::InvalidImageLayout(format!(
616                "IFD chain contains a loop at offset {offset}"
617            )));
618        }
619        if offset >= source.len() {
620            return Err(Error::Truncated {
621                offset,
622                needed: 2,
623                available: source.len().saturating_sub(offset),
624            });
625        }
626
627        let (tags, next_offset) = read_ifd(source, header, offset, budgets, &mut usage)?;
628
629        ifds.push(Ifd { tags, index });
630        offset = next_offset;
631        index += 1;
632    }
633
634    Ok(ifds)
635}
636
637/// Parse a single IFD at the given file offset.
638pub fn parse_ifd_at(source: &dyn TiffSource, header: &TiffHeader, offset: u64) -> Result<Ifd> {
639    parse_ifd_at_with_budgets(source, header, offset, ParseBudgets::default())
640}
641
642/// Parse a single IFD at the given file offset using explicit allocation budgets.
643pub fn parse_ifd_at_with_budgets(
644    source: &dyn TiffSource,
645    header: &TiffHeader,
646    offset: u64,
647    budgets: ParseBudgets,
648) -> Result<Ifd> {
649    let mut usage = ParseBudgetUsage::default();
650    let (tags, _) = read_ifd(source, header, offset, budgets, &mut usage)?;
651    Ok(Ifd {
652        tags,
653        index: usize::try_from(offset).unwrap_or(usize::MAX),
654    })
655}
656
657fn read_ifd(
658    source: &dyn TiffSource,
659    header: &TiffHeader,
660    offset: u64,
661    budgets: ParseBudgets,
662    usage: &mut ParseBudgetUsage,
663) -> Result<(Vec<Tag>, u64)> {
664    let entry_count_size = if header.is_bigtiff() { 8usize } else { 2usize };
665    let entry_size = if header.is_bigtiff() {
666        20usize
667    } else {
668        12usize
669    };
670    let next_offset_size = if header.is_bigtiff() { 8usize } else { 4usize };
671
672    let count_bytes = source.read_exact_at(offset, entry_count_size)?;
673    let mut count_cursor = Cursor::new(&count_bytes, header.byte_order);
674    let count = if header.is_bigtiff() {
675        usize::try_from(count_cursor.read_u64()?).map_err(|_| {
676            Error::InvalidImageLayout("BigTIFF entry count does not fit in usize".into())
677        })?
678    } else {
679        count_cursor.read_u16()? as usize
680    };
681    if count > budgets.max_ifd_entries {
682        return Err(Error::InvalidImageLayout(format!(
683            "IFD entry count {count} exceeds parse budget {}",
684            budgets.max_ifd_entries
685        )));
686    }
687
688    let entries_len = count
689        .checked_mul(entry_size)
690        .and_then(|v| v.checked_add(next_offset_size))
691        .ok_or_else(|| Error::InvalidImageLayout("IFD byte length overflows usize".into()))?;
692    let body = source.read_exact_at(offset + entry_count_size as u64, entries_len)?;
693    let mut cursor = Cursor::new(&body, header.byte_order);
694
695    if header.is_bigtiff() {
696        let tags = parse_tags_bigtiff(
697            &mut cursor,
698            count,
699            source,
700            header.byte_order,
701            budgets,
702            usage,
703        )?;
704        let next = cursor.read_u64()?;
705        Ok((tags, next))
706    } else {
707        let tags = parse_tags_classic(
708            &mut cursor,
709            count,
710            source,
711            header.byte_order,
712            budgets,
713            usage,
714        )?;
715        let next = cursor.read_u32()? as u64;
716        Ok((tags, next))
717    }
718}
719
720fn normalize_u16_values(
721    tag: u16,
722    values: Vec<u16>,
723    expected_len: usize,
724    default_value: u16,
725) -> Result<Vec<u16>> {
726    match values.len() {
727        0 => Ok(vec![default_value; expected_len]),
728        1 if expected_len > 1 => Ok(vec![values[0]; expected_len]),
729        len if len == expected_len => Ok(values),
730        len => Err(Error::InvalidTagValue {
731            tag,
732            reason: format!("expected 1 or {expected_len} values, found {len}"),
733        }),
734    }
735}
736
737fn resolve_fixed_model_extra_samples(
738    photometric: PhotometricInterpretation,
739    samples_per_pixel: u16,
740    base_samples: u16,
741    mut extra_samples: Vec<ExtraSample>,
742) -> Result<Vec<ExtraSample>> {
743    let implied_extra_samples = samples_per_pixel.checked_sub(base_samples).ok_or_else(|| {
744        Error::InvalidImageLayout(format!(
745            "{} photometric interpretation requires at least {base_samples} samples, got {samples_per_pixel}",
746            photometric_name(photometric)
747        ))
748    })?;
749    if extra_samples.len() > implied_extra_samples as usize {
750        return Err(Error::InvalidImageLayout(format!(
751            "{} photometric interpretation has {} total channels but {} ExtraSamples",
752            photometric_name(photometric),
753            samples_per_pixel,
754            extra_samples.len()
755        )));
756    }
757    extra_samples.resize(implied_extra_samples as usize, ExtraSample::Unspecified);
758    Ok(extra_samples)
759}
760
761fn photometric_name(photometric: PhotometricInterpretation) -> &'static str {
762    match photometric {
763        PhotometricInterpretation::MinIsWhite => "MinIsWhite",
764        PhotometricInterpretation::MinIsBlack => "MinIsBlack",
765        PhotometricInterpretation::Rgb => "RGB",
766        PhotometricInterpretation::Palette => "Palette",
767        PhotometricInterpretation::Mask => "TransparencyMask",
768        PhotometricInterpretation::Separated => "Separated",
769        PhotometricInterpretation::YCbCr => "YCbCr",
770        PhotometricInterpretation::CieLab => "CIELab",
771    }
772}
773
774fn validate_sample_encoding(sample_format: u16, bits_per_sample: u16) -> Result<()> {
775    let supported = match sample_format {
776        1 => matches!(bits_per_sample, 1 | 2 | 4 | 8 | 16 | 32 | 64),
777        2 => matches!(bits_per_sample, 8 | 16 | 32 | 64),
778        3 => matches!(bits_per_sample, 32 | 64),
779        _ => false,
780    };
781    if !supported {
782        return Err(Error::UnsupportedBitsPerSample(bits_per_sample));
783    }
784    Ok(())
785}
786
787fn decoded_uint_bits(bits_per_sample: u16) -> u16 {
788    bits_per_sample.max(8)
789}
790
791fn decoded_bits(sample_format: u16, bits_per_sample: u16) -> Result<u16> {
792    if sample_format == 1 {
793        Ok(decoded_uint_bits(bits_per_sample))
794    } else {
795        validate_sample_encoding(sample_format, bits_per_sample)?;
796        Ok(bits_per_sample)
797    }
798}
799
800fn color_map_is_u8_equivalent(color_map: &ColorMap) -> bool {
801    color_map
802        .red()
803        .iter()
804        .chain(color_map.green().iter())
805        .chain(color_map.blue().iter())
806        .all(|&value| value % 257 == 0)
807}
808
809fn validate_color_model(ifd: &Ifd, samples_per_pixel: u16, bits_per_sample: u16) -> Result<()> {
810    let color_model = ifd.color_model()?;
811
812    match &color_model {
813        ColorModel::Grayscale { extra_samples, .. } => {
814            validate_expected_samples(samples_per_pixel, 1, extra_samples.len())?;
815        }
816        ColorModel::Palette {
817            color_map,
818            extra_samples,
819        } => {
820            let expected_entries = 1usize.checked_shl(bits_per_sample as u32).ok_or_else(|| {
821                Error::InvalidImageLayout(format!(
822                    "palette BitsPerSample {bits_per_sample} exceeds usize shift width"
823                ))
824            })?;
825            if color_map.len() != expected_entries {
826                return Err(Error::InvalidImageLayout(format!(
827                    "palette ColorMap has {} entries but BitsPerSample={} requires {}",
828                    color_map.len(),
829                    bits_per_sample,
830                    expected_entries
831                )));
832            }
833            validate_expected_samples(samples_per_pixel, 1, extra_samples.len())?;
834        }
835        ColorModel::Rgb { extra_samples } => {
836            validate_expected_samples(samples_per_pixel, 3, extra_samples.len())?;
837        }
838        ColorModel::TransparencyMask => {
839            validate_expected_samples(samples_per_pixel, 1, 0)?;
840        }
841        ColorModel::Cmyk { extra_samples } => {
842            validate_expected_samples(samples_per_pixel, 4, extra_samples.len())?;
843        }
844        ColorModel::Separated {
845            color_channels,
846            extra_samples,
847            ..
848        } => {
849            if *color_channels == 0 {
850                return Err(Error::InvalidImageLayout(
851                    "separated photometric interpretation must have at least one base ink channel"
852                        .into(),
853                ));
854            }
855            validate_expected_samples(samples_per_pixel, *color_channels, extra_samples.len())?;
856        }
857        ColorModel::YCbCr {
858            subsampling,
859            extra_samples,
860            ..
861        } => {
862            if subsampling.contains(&0) {
863                return Err(Error::InvalidImageLayout(format!(
864                    "YCbCr subsampling {:?} must be positive",
865                    subsampling
866                )));
867            }
868            if *subsampling != [1, 1] && !extra_samples.is_empty() {
869                return Err(Error::InvalidImageLayout(
870                    "subsampled YCbCr with ExtraSamples is not supported".into(),
871                ));
872            }
873            if *subsampling != [1, 1] && ifd.predictor() != 1 {
874                return Err(Error::InvalidImageLayout(
875                    "subsampled YCbCr does not support TIFF predictors".into(),
876                ));
877            }
878            validate_expected_samples(samples_per_pixel, 3, extra_samples.len())?;
879        }
880        ColorModel::CieLab { extra_samples } => {
881            validate_expected_samples(samples_per_pixel, 3, extra_samples.len())?;
882        }
883    }
884
885    Ok(())
886}
887
888fn validate_expected_samples(
889    samples_per_pixel: u16,
890    base_samples: u16,
891    extra_sample_count: usize,
892) -> Result<()> {
893    let expected_samples = base_samples
894        .checked_add(extra_sample_count as u16)
895        .ok_or_else(|| Error::InvalidImageLayout("samples per pixel overflow".into()))?;
896    if samples_per_pixel != expected_samples {
897        return Err(Error::InvalidImageLayout(format!(
898            "SamplesPerPixel={samples_per_pixel} does not match color model base channels {base_samples} plus {extra_sample_count} ExtraSamples"
899        )));
900    }
901    Ok(())
902}
903
904/// Parse classic TIFF IFD entries (12 bytes each).
905fn parse_tags_classic(
906    cursor: &mut Cursor<'_>,
907    count: usize,
908    source: &dyn TiffSource,
909    byte_order: ByteOrder,
910    budgets: ParseBudgets,
911    usage: &mut ParseBudgetUsage,
912) -> Result<Vec<Tag>> {
913    let mut tags = Vec::with_capacity(count);
914    for _ in 0..count {
915        let code = cursor.read_u16()?;
916        let type_code = cursor.read_u16()?;
917        let value_count = cursor.read_u32()? as u64;
918        let value_offset_bytes = cursor.read_bytes(4)?;
919        let value_bytes =
920            checked_tag_value_byte_len(code, type_code, value_count, budgets.max_tag_value_bytes)?;
921        usage.consume_tag_value_bytes(code, value_bytes, budgets)?;
922        let tag = parse_tag_classic(
923            code,
924            type_code,
925            value_count,
926            value_offset_bytes,
927            source,
928            byte_order,
929            budgets.max_tag_value_bytes,
930        )?;
931        tags.push(tag);
932    }
933    tags.sort_by_key(|tag| tag.code);
934    Ok(tags)
935}
936
937/// Parse BigTIFF IFD entries (20 bytes each).
938fn parse_tags_bigtiff(
939    cursor: &mut Cursor<'_>,
940    count: usize,
941    source: &dyn TiffSource,
942    byte_order: ByteOrder,
943    budgets: ParseBudgets,
944    usage: &mut ParseBudgetUsage,
945) -> Result<Vec<Tag>> {
946    let mut tags = Vec::with_capacity(count);
947    for _ in 0..count {
948        let code = cursor.read_u16()?;
949        let type_code = cursor.read_u16()?;
950        let value_count = cursor.read_u64()?;
951        let value_offset_bytes = cursor.read_bytes(8)?;
952        let value_bytes =
953            checked_tag_value_byte_len(code, type_code, value_count, budgets.max_tag_value_bytes)?;
954        usage.consume_tag_value_bytes(code, value_bytes, budgets)?;
955        let tag = parse_tag_bigtiff(
956            code,
957            type_code,
958            value_count,
959            value_offset_bytes,
960            source,
961            byte_order,
962            budgets.max_tag_value_bytes,
963        )?;
964        tags.push(tag);
965    }
966    tags.sort_by_key(|tag| tag.code);
967    Ok(tags)
968}
969
970#[cfg(test)]
971mod tests {
972    use super::{
973        ColorModel, ExtraSample, Ifd, InkSet, LercAdditionalCompression, RasterLayout,
974        TAG_BITS_PER_SAMPLE, TAG_COLOR_MAP, TAG_EXTRA_SAMPLES, TAG_IMAGE_LENGTH, TAG_IMAGE_WIDTH,
975        TAG_INK_SET, TAG_LERC_PARAMETERS, TAG_PHOTOMETRIC_INTERPRETATION, TAG_SAMPLES_PER_PIXEL,
976        TAG_SAMPLE_FORMAT, TAG_YCBCR_SUBSAMPLING,
977    };
978    use crate::tag::{Tag, TagType, TagValue};
979
980    fn make_ifd(tags: Vec<Tag>) -> Ifd {
981        let mut tags = tags;
982        tags.sort_by_key(|tag| tag.code);
983        Ifd { tags, index: 0 }
984    }
985
986    #[test]
987    fn normalizes_single_value_sample_tags() {
988        let ifd = make_ifd(vec![
989            Tag {
990                code: TAG_IMAGE_WIDTH,
991                tag_type: TagType::Long,
992                count: 1,
993                value: TagValue::Long(vec![10]),
994            },
995            Tag {
996                code: TAG_IMAGE_LENGTH,
997                tag_type: TagType::Long,
998                count: 1,
999                value: TagValue::Long(vec![5]),
1000            },
1001            Tag {
1002                code: TAG_SAMPLES_PER_PIXEL,
1003                tag_type: TagType::Short,
1004                count: 1,
1005                value: TagValue::Short(vec![3]),
1006            },
1007            Tag {
1008                code: TAG_BITS_PER_SAMPLE,
1009                tag_type: TagType::Short,
1010                count: 1,
1011                value: TagValue::Short(vec![16]),
1012            },
1013            Tag {
1014                code: TAG_SAMPLE_FORMAT,
1015                tag_type: TagType::Short,
1016                count: 1,
1017                value: TagValue::Short(vec![1]),
1018            },
1019        ]);
1020
1021        let layout = ifd.raster_layout().unwrap();
1022        assert_eq!(layout.width, 10);
1023        assert_eq!(layout.height, 5);
1024        assert_eq!(layout.samples_per_pixel, 3);
1025        assert_eq!(layout.bytes_per_sample, 2);
1026    }
1027
1028    #[test]
1029    fn rejects_mixed_sample_formats() {
1030        let ifd = make_ifd(vec![
1031            Tag {
1032                code: TAG_IMAGE_WIDTH,
1033                tag_type: TagType::Long,
1034                count: 1,
1035                value: TagValue::Long(vec![1]),
1036            },
1037            Tag {
1038                code: TAG_IMAGE_LENGTH,
1039                tag_type: TagType::Long,
1040                count: 1,
1041                value: TagValue::Long(vec![1]),
1042            },
1043            Tag {
1044                code: TAG_SAMPLES_PER_PIXEL,
1045                tag_type: TagType::Short,
1046                count: 1,
1047                value: TagValue::Short(vec![2]),
1048            },
1049            Tag {
1050                code: TAG_BITS_PER_SAMPLE,
1051                tag_type: TagType::Short,
1052                count: 2,
1053                value: TagValue::Short(vec![16, 16]),
1054            },
1055            Tag {
1056                code: TAG_SAMPLE_FORMAT,
1057                tag_type: TagType::Short,
1058                count: 2,
1059                value: TagValue::Short(vec![1, 3]),
1060            },
1061        ]);
1062
1063        assert!(ifd.raster_layout().is_err());
1064    }
1065
1066    #[test]
1067    fn raster_layout_helpers_match_expected_strides() {
1068        let layout = RasterLayout {
1069            width: 4,
1070            height: 3,
1071            samples_per_pixel: 2,
1072            bits_per_sample: 16,
1073            bytes_per_sample: 2,
1074            sample_format: 1,
1075            planar_configuration: 1,
1076            predictor: 1,
1077        };
1078        assert_eq!(layout.pixel_stride_bytes(), 4);
1079        assert_eq!(layout.row_bytes(), 16);
1080        assert_eq!(layout.sample_plane_row_bytes(), 8);
1081    }
1082
1083    #[test]
1084    fn parses_lerc_parameters() {
1085        let ifd = make_ifd(vec![Tag {
1086            code: TAG_LERC_PARAMETERS,
1087            tag_type: TagType::Long,
1088            count: 2,
1089            value: TagValue::Long(vec![4, 2]),
1090        }]);
1091
1092        let params = ifd.lerc_parameters().unwrap().unwrap();
1093        assert_eq!(params.version, 4);
1094        assert_eq!(
1095            params.additional_compression,
1096            LercAdditionalCompression::Zstd
1097        );
1098    }
1099
1100    #[test]
1101    fn parses_palette_color_model_and_extra_alpha() {
1102        let ifd = make_ifd(vec![
1103            Tag::new(TAG_IMAGE_WIDTH, TagValue::Long(vec![2])),
1104            Tag::new(TAG_IMAGE_LENGTH, TagValue::Long(vec![2])),
1105            Tag::new(TAG_SAMPLES_PER_PIXEL, TagValue::Short(vec![2])),
1106            Tag::new(TAG_BITS_PER_SAMPLE, TagValue::Short(vec![8, 8])),
1107            Tag::new(TAG_SAMPLE_FORMAT, TagValue::Short(vec![1, 1])),
1108            Tag::new(TAG_PHOTOMETRIC_INTERPRETATION, TagValue::Short(vec![3])),
1109            Tag::new(TAG_EXTRA_SAMPLES, TagValue::Short(vec![2])),
1110            Tag::new(
1111                TAG_COLOR_MAP,
1112                TagValue::Short(
1113                    (0u16..256)
1114                        .chain((0u16..256).map(|value| value.saturating_mul(2)))
1115                        .chain((0u16..256).map(|value| value.saturating_mul(3)))
1116                        .collect(),
1117                ),
1118            ),
1119        ]);
1120
1121        let model = ifd.color_model().unwrap();
1122        match model {
1123            ColorModel::Palette {
1124                color_map,
1125                extra_samples,
1126            } => {
1127                assert_eq!(color_map.len(), 256);
1128                assert_eq!(extra_samples, vec![ExtraSample::UnassociatedAlpha]);
1129            }
1130            other => panic!("unexpected color model: {other:?}"),
1131        }
1132
1133        let layout = ifd.raster_layout().unwrap();
1134        assert_eq!(layout.samples_per_pixel, 2);
1135    }
1136
1137    #[test]
1138    fn parses_cmyk_color_model() {
1139        let ifd = make_ifd(vec![
1140            Tag::new(TAG_IMAGE_WIDTH, TagValue::Long(vec![1])),
1141            Tag::new(TAG_IMAGE_LENGTH, TagValue::Long(vec![1])),
1142            Tag::new(TAG_SAMPLES_PER_PIXEL, TagValue::Short(vec![4])),
1143            Tag::new(TAG_BITS_PER_SAMPLE, TagValue::Short(vec![8, 8, 8, 8])),
1144            Tag::new(TAG_SAMPLE_FORMAT, TagValue::Short(vec![1, 1, 1, 1])),
1145            Tag::new(TAG_PHOTOMETRIC_INTERPRETATION, TagValue::Short(vec![5])),
1146            Tag::new(TAG_INK_SET, TagValue::Short(vec![1])),
1147        ]);
1148
1149        assert!(matches!(
1150            ifd.color_model().unwrap(),
1151            ColorModel::Cmyk { .. }
1152        ));
1153        assert_eq!(ifd.ink_set().unwrap(), Some(InkSet::Cmyk));
1154        assert_eq!(ifd.raster_layout().unwrap().samples_per_pixel, 4);
1155    }
1156
1157    #[test]
1158    fn rejects_palette_without_colormap() {
1159        let ifd = make_ifd(vec![
1160            Tag::new(TAG_IMAGE_WIDTH, TagValue::Long(vec![1])),
1161            Tag::new(TAG_IMAGE_LENGTH, TagValue::Long(vec![1])),
1162            Tag::new(TAG_SAMPLES_PER_PIXEL, TagValue::Short(vec![1])),
1163            Tag::new(TAG_BITS_PER_SAMPLE, TagValue::Short(vec![8])),
1164            Tag::new(TAG_SAMPLE_FORMAT, TagValue::Short(vec![1])),
1165            Tag::new(TAG_PHOTOMETRIC_INTERPRETATION, TagValue::Short(vec![3])),
1166        ]);
1167
1168        let error = ifd.raster_layout().unwrap_err();
1169        assert!(
1170            matches!(error, crate::error::Error::InvalidImageLayout(message) if message.contains("ColorMap"))
1171        );
1172    }
1173
1174    #[test]
1175    fn accepts_subsampled_ycbcr_storage_layouts() {
1176        let ifd = make_ifd(vec![
1177            Tag::new(TAG_IMAGE_WIDTH, TagValue::Long(vec![2])),
1178            Tag::new(TAG_IMAGE_LENGTH, TagValue::Long(vec![2])),
1179            Tag::new(TAG_SAMPLES_PER_PIXEL, TagValue::Short(vec![3])),
1180            Tag::new(TAG_BITS_PER_SAMPLE, TagValue::Short(vec![8, 8, 8])),
1181            Tag::new(TAG_SAMPLE_FORMAT, TagValue::Short(vec![1, 1, 1])),
1182            Tag::new(TAG_PHOTOMETRIC_INTERPRETATION, TagValue::Short(vec![6])),
1183            Tag::new(TAG_YCBCR_SUBSAMPLING, TagValue::Short(vec![2, 2])),
1184        ]);
1185
1186        let layout = ifd.raster_layout().unwrap();
1187        assert_eq!(layout.samples_per_pixel, 3);
1188        assert_eq!(ifd.decoded_raster_layout().unwrap().samples_per_pixel, 3);
1189    }
1190}