Skip to main content

tiff_reader/
ifd.rs

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