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