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#[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: Option<usize>,
96 offset: u64,
102}
103
104impl Ifd {
105 pub fn offset(&self) -> u64 {
107 self.offset
108 }
109
110 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 pub fn tags(&self) -> &[Tag] {
120 &self.tags
121 }
122
123 pub fn width(&self) -> u32 {
125 self.tag_u32(TAG_IMAGE_WIDTH).unwrap_or(0)
126 }
127
128 pub fn height(&self) -> u32 {
130 self.tag_u32(TAG_IMAGE_LENGTH).unwrap_or(0)
131 }
132
133 pub fn bits_per_sample(&self) -> Result<Vec<u16>> {
140 self.checked_tag_u16_values(TAG_BITS_PER_SAMPLE)
141 }
142
143 pub fn compression(&self) -> u16 {
145 self.tag_u16(TAG_COMPRESSION).unwrap_or(1)
146 }
147
148 pub fn photometric_interpretation(&self) -> Option<u16> {
150 self.tag_u16(TAG_PHOTOMETRIC_INTERPRETATION)
151 }
152
153 pub fn photometric_interpretation_enum(&self) -> Option<PhotometricInterpretation> {
156 PhotometricInterpretation::from_code(self.photometric_interpretation().unwrap_or(1))
157 }
158
159 pub fn samples_per_pixel(&self) -> u16 {
161 self.tag_u16(TAG_SAMPLES_PER_PIXEL).unwrap_or(1)
162 }
163
164 pub fn is_tiled(&self) -> bool {
166 self.tag(TAG_TILE_WIDTH).is_some() && self.tag(TAG_TILE_LENGTH).is_some()
167 }
168
169 pub fn tile_width(&self) -> Option<u32> {
171 self.tag_u32(TAG_TILE_WIDTH)
172 }
173
174 pub fn tile_height(&self) -> Option<u32> {
176 self.tag_u32(TAG_TILE_LENGTH)
177 }
178
179 pub fn rows_per_strip(&self) -> u32 {
181 self.tag_u32(TAG_ROWS_PER_STRIP)
182 .unwrap_or_else(|| self.height())
183 }
184
185 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 pub fn planar_configuration(&self) -> u16 {
221 self.tag_u16(TAG_PLANAR_CONFIGURATION).unwrap_or(1)
222 }
223
224 pub fn predictor(&self) -> u16 {
226 self.tag_u16(TAG_PREDICTOR).unwrap_or(1)
227 }
228
229 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 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 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 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 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 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 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 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 pub fn strip_offsets(&self) -> Option<Vec<u64>> {
466 self.tag_u64_list(TAG_STRIP_OFFSETS)
467 }
468
469 pub fn strip_byte_counts(&self) -> Option<Vec<u64>> {
471 self.tag_u64_list(TAG_STRIP_BYTE_COUNTS)
472 }
473
474 pub fn tile_offsets(&self) -> Option<Vec<u64>> {
476 self.tag_u64_list(TAG_TILE_OFFSETS)
477 }
478
479 pub fn tile_byte_counts(&self) -> Option<Vec<u64>> {
481 self.tag_u64_list(TAG_TILE_BYTE_COUNTS)
482 }
483
484 pub fn sub_ifd_offsets(&self) -> Option<Vec<u64>> {
486 self.tag_u64_list(TAG_SUB_IFDS)
487 }
488
489 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 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
636pub fn parse_ifd_chain(source: &dyn TiffSource, header: &TiffHeader) -> Result<Vec<Ifd>> {
638 parse_ifd_chain_with_budgets(source, header, ParseBudgets::default())
639}
640
641pub 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
687pub 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
692pub 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
962fn 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
996fn 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}