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