1use crate::ccitt::{decode_ccitt, reverse_bits_in_place, CcittVariant, FillOrder};
7use crate::compress::{unpack_deflate, unpack_lzw, unpack_packbits, unpack_zstd};
8use crate::error::{Result, TiffError as Error};
9use crate::ifd::{find, parse_header, parse_ifd, ByteOrder, Entry};
10use crate::image::{TiffImage, TiffPixelFormat, TiffPlane};
11use crate::types::*;
12
13const MAX_IMAGE_PIXELS: u64 = 256 * 1024 * 1024;
24
25pub struct DecodedTiff {
32 pub frame: TiffImage,
33 pub width: u32,
34 pub height: u32,
35 pub pixel_format: TiffPixelFormat,
36}
37
38pub fn decode_tiff(input: &[u8]) -> Result<DecodedTiff> {
41 let header = parse_header(input)?;
42 let bo = header.byte_order;
43 let variant = header.variant;
44 let (entries, _next_ifd) = parse_ifd(input, bo, variant, header.first_ifd_offset)?;
45 let frame = decode_ifd(input, bo, &entries)?;
46 let pf = frame.pixel_format;
47 Ok(DecodedTiff {
48 width: frame.width,
49 height: frame.height,
50 pixel_format: pf,
51 frame,
52 })
53}
54
55pub fn decode_tiff_all(input: &[u8]) -> Result<Vec<TiffImage>> {
58 let header = parse_header(input)?;
59 let bo = header.byte_order;
60 let variant = header.variant;
61 let mut out = Vec::new();
62 let mut next = header.first_ifd_offset;
63 let mut visited: Vec<u64> = Vec::new();
64 while next != 0 {
65 if visited.contains(&next) {
69 return Err(Error::invalid("TIFF: cyclic next-IFD pointer"));
70 }
71 visited.push(next);
72 let (entries, n) = parse_ifd(input, bo, variant, next)?;
73 out.push(decode_ifd(input, bo, &entries)?);
74 next = n;
75 }
76 if out.is_empty() {
77 return Err(Error::invalid("TIFF: no IFDs in file"));
78 }
79 Ok(out)
80}
81
82fn decode_ifd(input: &[u8], bo: ByteOrder, entries: &[Entry]) -> Result<TiffImage> {
84 let width = find(entries, TAG_IMAGE_WIDTH)
86 .ok_or_else(|| Error::invalid("TIFF: missing ImageWidth"))?
87 .as_u32(bo)?;
88 let height = find(entries, TAG_IMAGE_LENGTH)
89 .ok_or_else(|| Error::invalid("TIFF: missing ImageLength"))?
90 .as_u32(bo)?;
91 if width == 0 || height == 0 {
92 return Err(Error::invalid("TIFF: zero dimension"));
93 }
94 if (width as u64).saturating_mul(height as u64) > MAX_IMAGE_PIXELS {
103 return Err(Error::invalid(format!(
104 "TIFF: image too large ({width}x{height} > {MAX_IMAGE_PIXELS} pixels)"
105 )));
106 }
107
108 let compression = find(entries, TAG_COMPRESSION)
109 .map(|e| e.as_u32(bo))
110 .transpose()?
111 .unwrap_or(COMPRESSION_NONE as u32) as u16;
112 let photometric = find(entries, TAG_PHOTOMETRIC_INTERPRETATION)
113 .map(|e| e.as_u32(bo))
114 .transpose()?
115 .ok_or_else(|| Error::invalid("TIFF: missing PhotometricInterpretation"))?
116 as u16;
117 let samples_per_pixel = find(entries, TAG_SAMPLES_PER_PIXEL)
118 .map(|e| e.as_u32(bo))
119 .transpose()?
120 .unwrap_or(1) as u16;
121 let bits_per_sample =
122 decode_bits_per_sample(find(entries, TAG_BITS_PER_SAMPLE), bo, samples_per_pixel)?;
123 let planar = find(entries, TAG_PLANAR_CONFIGURATION)
124 .map(|e| e.as_u32(bo))
125 .transpose()?
126 .unwrap_or(PLANAR_CHUNKY as u32) as u16;
127 if planar != PLANAR_CHUNKY && planar != PLANAR_SEPARATE {
128 return Err(Error::invalid(format!(
129 "TIFF: PlanarConfiguration={planar} unknown (spec defines only 1 and 2)"
130 )));
131 }
132 let planar = if samples_per_pixel == 1 {
139 PLANAR_CHUNKY
140 } else {
141 planar
142 };
143
144 let predictor = find(entries, TAG_PREDICTOR)
145 .map(|e| e.as_u32(bo))
146 .transpose()?
147 .unwrap_or(PREDICTOR_NONE as u32) as u16;
148 if predictor != PREDICTOR_NONE && predictor != PREDICTOR_HORIZONTAL {
149 return Err(Error::invalid(format!(
150 "TIFF: Predictor={predictor} not supported"
151 )));
152 }
153
154 let sample_format = if let Some(sf_entry) = find(entries, TAG_SAMPLE_FORMAT) {
174 let sf = sf_entry.as_u32_vec(bo)?;
180 if sf.len() != samples_per_pixel as usize && sf.len() != 1 {
181 return Err(Error::invalid(format!(
182 "TIFF: SampleFormat count {} != SamplesPerPixel {}",
183 sf.len(),
184 samples_per_pixel
185 )));
186 }
187 if !sf.iter().all(|&v| v == sf[0]) {
188 return Err(Error::Unsupported(
189 "TIFF: per-component SampleFormat values must be uniform".into(),
190 ));
191 }
192 let fmt = sf[0] as u16;
193 match fmt {
194 SAMPLE_FORMAT_UINT | SAMPLE_FORMAT_UNDEFINED => SAMPLE_FORMAT_UINT,
195 SAMPLE_FORMAT_SINT => SAMPLE_FORMAT_SINT,
196 SAMPLE_FORMAT_IEEE_FP => {
197 return Err(Error::Unsupported(
198 "TIFF: SampleFormat=3 (IEEE floating-point) not supported; \
199 §SampleFormat requires readers that cannot handle the value to \
200 terminate gracefully"
201 .into(),
202 ));
203 }
204 other => {
205 return Err(Error::invalid(format!(
206 "TIFF: SampleFormat={other} unknown (spec defines 1..=4)"
207 )));
208 }
209 }
210 } else {
211 SAMPLE_FORMAT_UINT
212 };
213
214 if let Some(orient_entry) = find(entries, TAG_ORIENTATION) {
233 let orient = orient_entry.as_u32(bo)? as u16;
234 match orient {
235 1 => {
236 }
240 2..=8 => {
241 return Err(Error::Unsupported(format!(
242 "TIFF: Orientation={orient} not supported; \
243 §Orientation page 36 states 'Support for orientations \
244 other than 1 is not a Baseline TIFF requirement'"
245 )));
246 }
247 other => {
248 return Err(Error::invalid(format!(
249 "TIFF: Orientation={other} unknown (spec defines 1..=8)"
250 )));
251 }
252 }
253 }
254
255 if let Some(unit_entry) = find(entries, TAG_RESOLUTION_UNIT) {
271 let unit = unit_entry.as_u32(bo)? as u16;
272 match unit {
273 RESOLUTION_UNIT_NONE | RESOLUTION_UNIT_INCH | RESOLUTION_UNIT_CENTIMETER => {
274 }
277 other => {
278 return Err(Error::invalid(format!(
279 "TIFF: ResolutionUnit={other} unknown (spec defines 1..=3)"
280 )));
281 }
282 }
283 }
284
285 if let Some(es_entry) = find(entries, TAG_EXTRA_SAMPLES) {
316 let es = es_entry.as_u32_vec(bo)?;
317 let color_counts: &[u16] = match photometric {
325 PHOTO_WHITE_IS_ZERO | PHOTO_BLACK_IS_ZERO | PHOTO_PALETTE | PHOTO_TRANSPARENCY_MASK => {
326 &[1]
327 }
328 PHOTO_RGB | PHOTO_YCBCR => &[3],
329 PHOTO_CMYK => &[4],
330 PHOTO_CIELAB => &[3, 1],
331 _ => &[],
334 };
335 if !color_counts.is_empty() {
336 let m = es.len() as u64;
337 let ok = color_counts
338 .iter()
339 .any(|&c| samples_per_pixel as u64 == c as u64 + m);
340 if !ok {
341 return Err(Error::invalid(format!(
342 "TIFF: ExtraSamples count {m} does not leave a color-component \
343 count photometric={photometric} defines \
344 (SamplesPerPixel={samples_per_pixel})"
345 )));
346 }
347 }
348 for &v in &es {
349 if v == EXTRA_SAMPLE_UNSPECIFIED as u32 || v == EXTRA_SAMPLE_UNASSOCIATED_ALPHA as u32 {
350 } else if v == EXTRA_SAMPLE_ASSOCIATED_ALPHA as u32 {
353 return Err(Error::Unsupported(format!(
354 "TIFF: ExtraSamples={v} (associated alpha, pre-multiplied color) \
355 not supported; dropping the alpha component would mis-render \
356 the pre-multiplied color components"
357 )));
358 } else {
359 return Err(Error::invalid(format!(
360 "TIFF: ExtraSamples={v} unknown (spec defines 0..=2)"
361 )));
362 }
363 }
364 }
365
366 let bps_first = bits_per_sample[0];
368 if !bits_per_sample.iter().all(|&b| b == bps_first) {
369 return Err(Error::invalid(
370 "TIFF: per-channel BitsPerSample must be uniform in this build",
371 ));
372 }
373 if bps_first != 1 && bps_first != 4 && bps_first != 8 && bps_first != 16 {
374 return Err(Error::invalid(format!(
375 "TIFF: BitsPerSample={bps_first} not supported"
376 )));
377 }
378
379 let fill_order_raw = find(entries, TAG_FILL_ORDER)
390 .map(|e| e.as_u32(bo))
391 .transpose()?
392 .unwrap_or(1) as u16;
393 let fill_order = match fill_order_raw {
394 1 => FillOrder::MsbFirst,
395 2 => {
396 let allowed_compression = matches!(
400 compression,
401 COMPRESSION_NONE
402 | COMPRESSION_CCITT_HUFFMAN
403 | COMPRESSION_CCITT_T4
404 | COMPRESSION_CCITT_T6
405 );
406 if bps_first != 1 || !allowed_compression {
407 return Err(Error::invalid(format!(
408 "TIFF: FillOrder=2 only valid for BitsPerSample=1 uncompressed/CCITT \
409 (got bps={bps_first}, compression={compression})"
410 )));
411 }
412 FillOrder::LsbFirst
413 }
414 n => {
415 return Err(Error::invalid(format!(
416 "TIFF: FillOrder={n} unknown (spec defines only 1 and 2)"
417 )));
418 }
419 };
420 let t4_options = find(entries, TAG_T4_OPTIONS)
421 .map(|e| e.as_u32(bo))
422 .transpose()?
423 .unwrap_or(0);
424 let t6_options = find(entries, TAG_T6_OPTIONS)
425 .map(|e| e.as_u32(bo))
426 .transpose()?
427 .unwrap_or(0);
428
429 #[cfg(feature = "registry")]
435 if compression == COMPRESSION_JPEG_NEW {
436 if planar == PLANAR_SEPARATE {
443 return Err(Error::Unsupported(
444 "TIFF/JPEG: PlanarConfiguration=2 (separate planes) not supported".into(),
445 ));
446 }
447 return decode_ifd_jpeg(
448 input,
449 entries,
450 bo,
451 width,
452 height,
453 samples_per_pixel,
454 bps_first,
455 photometric,
456 );
457 }
458 #[cfg(not(feature = "registry"))]
463 if compression == COMPRESSION_JPEG_NEW {
464 return Err(Error::Unsupported(
465 "TIFF: Compression=7 (JPEG-in-TIFF) requires the `registry` feature".into(),
466 ));
467 }
468 if compression == COMPRESSION_JPEG_OLD {
471 return Err(Error::Unsupported(
472 "TIFF: Compression=6 (old-style JPEG) is deprecated by TIFF Tech Note 2; \
473 writers should emit Compression=7 instead"
474 .into(),
475 ));
476 }
477
478 let pixel_buf = if find(entries, TAG_TILE_WIDTH).is_some() {
479 if planar == PLANAR_SEPARATE {
480 decode_tiles_planar(
481 input,
482 entries,
483 bo,
484 width,
485 height,
486 samples_per_pixel,
487 bps_first,
488 compression,
489 predictor,
490 t4_options,
491 t6_options,
492 fill_order,
493 )?
494 } else {
495 decode_tiles(
496 input,
497 entries,
498 bo,
499 width,
500 height,
501 samples_per_pixel,
502 bps_first,
503 compression,
504 predictor,
505 t4_options,
506 t6_options,
507 fill_order,
508 )?
509 }
510 } else if planar == PLANAR_SEPARATE {
511 decode_strips_planar(
512 input,
513 entries,
514 bo,
515 width,
516 height,
517 samples_per_pixel,
518 bps_first,
519 compression,
520 predictor,
521 t4_options,
522 t6_options,
523 fill_order,
524 )?
525 } else {
526 decode_strips(
527 input,
528 entries,
529 bo,
530 width,
531 height,
532 samples_per_pixel,
533 bps_first,
534 compression,
535 predictor,
536 t4_options,
537 t6_options,
538 fill_order,
539 )?
540 };
541
542 if sample_format == SAMPLE_FORMAT_SINT {
575 let grayscale = matches!(photometric, PHOTO_BLACK_IS_ZERO | PHOTO_WHITE_IS_ZERO);
576 if !grayscale || samples_per_pixel != 1 || !(bps_first == 8 || bps_first == 16) {
577 return Err(Error::Unsupported(format!(
578 "TIFF: SampleFormat=2 (signed integer) supported only for 8-/16-bit \
579 grayscale (photometric 0/1, SamplesPerPixel=1); got photometric={photometric} \
580 samples_per_pixel={samples_per_pixel} bits_per_sample={bps_first}"
581 )));
582 }
583 }
584
585 let (image, _pf) = match (photometric, samples_per_pixel, bps_first) {
587 (PHOTO_BLACK_IS_ZERO, 1, 1) | (PHOTO_WHITE_IS_ZERO, 1, 1) => {
588 let inv = photometric == PHOTO_WHITE_IS_ZERO;
589 let row_bytes = ((width as u64).div_ceil(8)) as usize;
590 (
591 build_gray8_from_1bpp(&pixel_buf, width, height, row_bytes, inv),
592 TiffPixelFormat::Gray8,
593 )
594 }
595 (PHOTO_TRANSPARENCY_MASK, 1, 1) => {
612 let row_bytes = ((width as u64).div_ceil(8)) as usize;
613 (
614 build_gray8_from_1bpp(&pixel_buf, width, height, row_bytes, false),
615 TiffPixelFormat::Gray8,
616 )
617 }
618 (PHOTO_BLACK_IS_ZERO, 1, 4) | (PHOTO_WHITE_IS_ZERO, 1, 4) => {
619 let inv = photometric == PHOTO_WHITE_IS_ZERO;
620 let row_bytes = ((width as u64).div_ceil(2)) as usize;
621 (
622 build_gray8_from_4bpp(&pixel_buf, width, height, row_bytes, inv),
623 TiffPixelFormat::Gray8,
624 )
625 }
626 (PHOTO_BLACK_IS_ZERO, 1, 8) | (PHOTO_WHITE_IS_ZERO, 1, 8) => {
627 let inv = photometric == PHOTO_WHITE_IS_ZERO;
628 let signed = sample_format == SAMPLE_FORMAT_SINT;
629 (
630 build_gray8(&pixel_buf, width, height, inv, signed),
631 TiffPixelFormat::Gray8,
632 )
633 }
634 (PHOTO_BLACK_IS_ZERO, 1, 16) | (PHOTO_WHITE_IS_ZERO, 1, 16) => {
635 let inv = photometric == PHOTO_WHITE_IS_ZERO;
636 let signed = sample_format == SAMPLE_FORMAT_SINT;
637 (
638 build_gray16le(&pixel_buf, width, height, bo, inv, signed),
639 TiffPixelFormat::Gray16Le,
640 )
641 }
642 (PHOTO_RGB, 3, 8) => (
643 build_rgb24(&pixel_buf, width, height),
644 TiffPixelFormat::Rgb24,
645 ),
646 (PHOTO_RGB, 3, 16) => (
647 build_rgb48le(&pixel_buf, width, height, bo),
648 TiffPixelFormat::Rgb48Le,
649 ),
650 (PHOTO_RGB, n, 8) if n >= 4 => {
651 (
659 build_rgb_from_n_chunky_8bit(&pixel_buf, width, height, n as usize),
660 TiffPixelFormat::Rgb24,
661 )
662 }
663 (PHOTO_PALETTE, 1, b @ (4 | 8)) => {
664 let cm = find(entries, TAG_COLOR_MAP)
665 .ok_or_else(|| Error::invalid("TIFF: palette image missing ColorMap"))?
666 .as_u32_vec(bo)?;
667 let palette = parse_colormap(&cm, b)?;
668 let row_bytes = if b == 8 {
669 width as usize
670 } else {
671 ((width as u64).div_ceil(2)) as usize
672 };
673 (
674 build_rgb24_from_palette(&pixel_buf, width, height, &palette, b, row_bytes),
675 TiffPixelFormat::Rgb24,
676 )
677 }
678 (PHOTO_CMYK, 4, 8) => (
679 build_rgb24_from_cmyk(&pixel_buf, width, height),
680 TiffPixelFormat::Rgb24,
681 ),
682 (PHOTO_YCBCR, 3, 8) => {
683 let (sh, sv) = match find(entries, TAG_YCBCR_SUBSAMPLING) {
686 Some(e) => {
687 let v = e.as_u32_vec(bo)?;
688 if v.len() < 2 {
689 return Err(Error::invalid("TIFF: YCbCrSubSampling too short"));
690 }
691 (v[0] as u16, v[1] as u16)
692 }
693 None => (2, 2),
694 };
695 (
696 build_rgb24_from_ycbcr(&pixel_buf, width, height, sh, sv)?,
697 TiffPixelFormat::Rgb24,
698 )
699 }
700 (PHOTO_CIELAB, 3, 8) => (
717 build_rgb24_from_cielab(&pixel_buf, width, height),
718 TiffPixelFormat::Rgb24,
719 ),
720 (PHOTO_CIELAB, 1, 8) => (
727 build_gray8_from_cielab_l(&pixel_buf, width, height),
728 TiffPixelFormat::Gray8,
729 ),
730 (p, s, b) => {
731 return Err(Error::invalid(format!(
732 "TIFF: photometric={p} samples_per_pixel={s} bits_per_sample={b} not supported"
733 )))
734 }
735 };
736
737 Ok(image)
738}
739
740#[allow(clippy::too_many_arguments)]
741fn decode_strips(
742 input: &[u8],
743 entries: &[Entry],
744 bo: ByteOrder,
745 width: u32,
746 height: u32,
747 samples_per_pixel: u16,
748 bps_first: u16,
749 compression: u16,
750 predictor: u16,
751 t4_options: u32,
752 t6_options: u32,
753 fill_order: FillOrder,
754) -> Result<Vec<u8>> {
755 let rows_per_strip = find(entries, TAG_ROWS_PER_STRIP)
756 .map(|e| e.as_u32(bo))
757 .transpose()?
758 .unwrap_or(height); let strip_offsets = find(entries, TAG_STRIP_OFFSETS)
761 .ok_or_else(|| Error::invalid("TIFF: missing StripOffsets"))?
762 .as_u64_vec(bo)?;
763 let strip_byte_counts = find(entries, TAG_STRIP_BYTE_COUNTS)
764 .ok_or_else(|| Error::invalid("TIFF: missing StripByteCounts"))?
765 .as_u64_vec(bo)?;
766 if strip_offsets.len() != strip_byte_counts.len() {
767 return Err(Error::invalid(
768 "TIFF: StripOffsets / StripByteCounts length mismatch",
769 ));
770 }
771
772 let photometric = find(entries, TAG_PHOTOMETRIC_INTERPRETATION)
783 .map(|e| e.as_u32(bo))
784 .transpose()?
785 .unwrap_or(0);
786 let ycbcr_subsampling: Option<(usize, usize)> = if photometric == PHOTO_YCBCR as u32
787 && samples_per_pixel == 3
788 && bps_first == 8
789 && !matches!(compression, COMPRESSION_JPEG_OLD | COMPRESSION_JPEG_NEW)
790 {
791 let (sh, sv) = match find(entries, TAG_YCBCR_SUBSAMPLING) {
792 Some(e) => {
793 let v = e.as_u32_vec(bo)?;
794 if v.len() < 2 {
795 return Err(Error::invalid("TIFF: YCbCrSubSampling too short"));
796 }
797 (v[0] as usize, v[1] as usize)
798 }
799 None => (2, 2),
801 };
802 if sh == 0 || sv == 0 {
803 return Err(Error::invalid("TIFF: YCbCrSubSampling must be > 0"));
804 }
805 if (sh, sv) != (1, 1) {
806 Some((sh, sv))
807 } else {
808 None
809 }
810 } else {
811 None
812 };
813
814 let bits_per_row = (width as u64) * (samples_per_pixel as u64) * (bps_first as u64);
817 let row_bytes = bits_per_row.div_ceil(8) as usize;
818
819 let ycbcr_strip_bytes = |rows: u32| -> usize {
825 if let Some((sh, sv)) = ycbcr_subsampling {
826 let block_w = (width as usize).div_ceil(sh);
827 let block_rows = (rows as usize).div_ceil(sv);
828 block_w * block_rows * (sh * sv + 2)
829 } else {
830 row_bytes * rows as usize
831 }
832 };
833
834 let buf_cap = if ycbcr_subsampling.is_some() {
835 ycbcr_strip_bytes(height)
836 } else {
837 row_bytes * height as usize
838 };
839 let mut pixel_buf: Vec<u8> = Vec::with_capacity(buf_cap);
840 let mut rows_done: u32 = 0;
841 for (i, (&offset, &byte_count)) in strip_offsets
842 .iter()
843 .zip(strip_byte_counts.iter())
844 .enumerate()
845 {
846 let start = offset as usize;
847 let end = start
848 .checked_add(byte_count as usize)
849 .ok_or_else(|| Error::invalid(format!("TIFF: strip {i} length overflow")))?;
850 if end > input.len() {
851 return Err(Error::invalid(format!("TIFF: strip {i} extends past EOF")));
852 }
853 let raw = &input[start..end];
854 let rows_this_strip = rows_per_strip.min(height - rows_done);
855 let expected = ycbcr_strip_bytes(rows_this_strip);
856 let ccitt = if matches!(
857 compression,
858 COMPRESSION_CCITT_HUFFMAN | COMPRESSION_CCITT_T4 | COMPRESSION_CCITT_T6
859 ) {
860 Some(CcittParams {
861 width,
862 rows: rows_this_strip,
863 fill: fill_order,
864 t4_options,
865 t6_options,
866 })
867 } else {
868 None
869 };
870 let decompressed = decompress_block(raw, expected, compression, ccitt)?;
871 if decompressed.len() < expected {
872 return Err(Error::invalid(format!(
873 "TIFF: strip {i} short after decompress: got {} bytes, expected {}",
874 decompressed.len(),
875 expected
876 )));
877 }
878 let mut strip = decompressed[..expected].to_vec();
880 if compression == COMPRESSION_NONE && fill_order == FillOrder::LsbFirst && bps_first == 1 {
887 reverse_bits_in_place(&mut strip);
888 }
889 if predictor == PREDICTOR_HORIZONTAL {
890 if ycbcr_subsampling.is_some() {
896 return Err(Error::invalid(
897 "TIFF: Predictor=2 with chroma-subsampled YCbCr (TIFF 6.0 §21 data units) \
898 is unsupported — §14 differencing has no defined meaning over the \
899 packed data-unit layout",
900 ));
901 }
902 apply_horizontal_predictor(
903 &mut strip,
904 width as usize,
905 rows_this_strip as usize,
906 samples_per_pixel as usize,
907 bps_first as usize,
908 row_bytes,
909 bo,
910 )?;
911 }
912 pixel_buf.extend_from_slice(&strip);
913 rows_done += rows_this_strip;
914 if rows_done >= height {
915 break;
916 }
917 }
918 if rows_done < height {
919 return Err(Error::invalid("TIFF: strips did not cover full image"));
920 }
921 Ok(pixel_buf)
922}
923
924#[allow(clippy::too_many_arguments)]
925fn decode_tiles(
926 input: &[u8],
927 entries: &[Entry],
928 bo: ByteOrder,
929 width: u32,
930 height: u32,
931 samples_per_pixel: u16,
932 bps_first: u16,
933 compression: u16,
934 predictor: u16,
935 t4_options: u32,
936 t6_options: u32,
937 fill_order: FillOrder,
938) -> Result<Vec<u8>> {
939 let tile_w = find(entries, TAG_TILE_WIDTH)
940 .ok_or_else(|| Error::invalid("TIFF: missing TileWidth"))?
941 .as_u32(bo)?;
942 let tile_h = find(entries, TAG_TILE_LENGTH)
943 .ok_or_else(|| Error::invalid("TIFF: missing TileLength"))?
944 .as_u32(bo)?;
945 if tile_w == 0 || tile_h == 0 {
946 return Err(Error::invalid("TIFF: zero tile dimension"));
947 }
948 let tile_offsets = find(entries, TAG_TILE_OFFSETS)
949 .ok_or_else(|| Error::invalid("TIFF: missing TileOffsets"))?
950 .as_u64_vec(bo)?;
951 let tile_byte_counts = find(entries, TAG_TILE_BYTE_COUNTS)
952 .ok_or_else(|| Error::invalid("TIFF: missing TileByteCounts"))?
953 .as_u64_vec(bo)?;
954 if tile_offsets.len() != tile_byte_counts.len() {
955 return Err(Error::invalid(
956 "TIFF: TileOffsets / TileByteCounts length mismatch",
957 ));
958 }
959
960 let tiles_across = (width as u64).div_ceil(tile_w as u64) as u32;
961 let tiles_down = (height as u64).div_ceil(tile_h as u64) as u32;
962 let expected_tiles = (tiles_across as usize) * (tiles_down as usize);
963 if tile_offsets.len() != expected_tiles {
964 return Err(Error::invalid(format!(
965 "TIFF: TileOffsets length {} != expected {expected_tiles}",
966 tile_offsets.len()
967 )));
968 }
969
970 let bits_per_sample = bps_first as u64;
971 let bits_per_tile_row = (tile_w as u64) * (samples_per_pixel as u64) * bits_per_sample;
972 let tile_row_bytes = bits_per_tile_row.div_ceil(8) as usize;
973 let tile_size_bytes = tile_row_bytes * tile_h as usize;
974
975 let bits_per_image_row = (width as u64) * (samples_per_pixel as u64) * bits_per_sample;
976 let image_row_bytes = bits_per_image_row.div_ceil(8) as usize;
977 let mut out = vec![0u8; image_row_bytes * height as usize];
978
979 for ty in 0..tiles_down {
980 for tx in 0..tiles_across {
981 let idx = (ty * tiles_across + tx) as usize;
982 let off = tile_offsets[idx] as usize;
983 let bc = tile_byte_counts[idx] as usize;
984 let end = off
985 .checked_add(bc)
986 .ok_or_else(|| Error::invalid("TIFF: tile length overflow"))?;
987 if end > input.len() {
988 return Err(Error::invalid("TIFF: tile extends past EOF"));
989 }
990 let raw = &input[off..end];
991 let ccitt = if matches!(
992 compression,
993 COMPRESSION_CCITT_HUFFMAN | COMPRESSION_CCITT_T4 | COMPRESSION_CCITT_T6
994 ) {
995 Some(CcittParams {
996 width: tile_w,
997 rows: tile_h,
998 fill: fill_order,
999 t4_options,
1000 t6_options,
1001 })
1002 } else {
1003 None
1004 };
1005 let mut tile = decompress_block(raw, tile_size_bytes, compression, ccitt)?;
1006 if tile.len() < tile_size_bytes {
1007 return Err(Error::invalid("TIFF: tile short after decompress"));
1008 }
1009 tile.truncate(tile_size_bytes);
1010 if compression == COMPRESSION_NONE
1016 && fill_order == FillOrder::LsbFirst
1017 && bps_first == 1
1018 {
1019 reverse_bits_in_place(&mut tile);
1020 }
1021 if predictor == PREDICTOR_HORIZONTAL {
1022 apply_horizontal_predictor(
1023 &mut tile,
1024 tile_w as usize,
1025 tile_h as usize,
1026 samples_per_pixel as usize,
1027 bps_first as usize,
1028 tile_row_bytes,
1029 bo,
1030 )?;
1031 }
1032
1033 let bits_per_dst_origin =
1056 (tx as u64) * (tile_w as u64) * (samples_per_pixel as u64) * (bps_first as u64);
1057 if bits_per_dst_origin % 8 != 0 {
1058 return Err(Error::invalid(
1062 "TIFF: tile column boundary is not byte-aligned (TileWidth must be a \
1063 multiple of 16 per TIFF 6.0 §15)",
1064 ));
1065 }
1066 let dst_origin_x = (bits_per_dst_origin / 8) as usize;
1067 let visible_w =
1068 ((width as i64) - (tx as i64) * (tile_w as i64)).min(tile_w as i64) as usize;
1069 let visible_h =
1070 ((height as i64) - (ty as i64) * (tile_h as i64)).min(tile_h as i64) as usize;
1071 let visible_row_bytes =
1072 ((visible_w as u64) * (samples_per_pixel as u64) * (bps_first as u64)).div_ceil(8)
1073 as usize;
1074 let dst_origin_y = ty as usize * tile_h as usize;
1075 for r in 0..visible_h {
1076 let src_off = r * tile_row_bytes;
1077 let dst_off = (dst_origin_y + r) * image_row_bytes + dst_origin_x;
1078 out[dst_off..dst_off + visible_row_bytes]
1079 .copy_from_slice(&tile[src_off..src_off + visible_row_bytes]);
1080 }
1081 }
1082 }
1083
1084 Ok(out)
1085}
1086
1087#[allow(clippy::too_many_arguments)]
1106fn decode_strips_planar(
1107 input: &[u8],
1108 entries: &[Entry],
1109 bo: ByteOrder,
1110 width: u32,
1111 height: u32,
1112 samples_per_pixel: u16,
1113 bps_first: u16,
1114 compression: u16,
1115 predictor: u16,
1116 t4_options: u32,
1117 t6_options: u32,
1118 fill_order: FillOrder,
1119) -> Result<Vec<u8>> {
1120 if samples_per_pixel < 2 {
1121 return Err(Error::invalid(
1126 "TIFF: PlanarConfiguration=2 with SamplesPerPixel<2 is meaningless",
1127 ));
1128 }
1129 if bps_first % 8 != 0 {
1130 return Err(Error::invalid(
1137 "TIFF: PlanarConfiguration=2 at sub-byte bit depths not supported",
1138 ));
1139 }
1140 let rows_per_strip = find(entries, TAG_ROWS_PER_STRIP)
1141 .map(|e| e.as_u32(bo))
1142 .transpose()?
1143 .unwrap_or(height);
1144 let strip_offsets = find(entries, TAG_STRIP_OFFSETS)
1145 .ok_or_else(|| Error::invalid("TIFF: missing StripOffsets"))?
1146 .as_u64_vec(bo)?;
1147 let strip_byte_counts = find(entries, TAG_STRIP_BYTE_COUNTS)
1148 .ok_or_else(|| Error::invalid("TIFF: missing StripByteCounts"))?
1149 .as_u64_vec(bo)?;
1150 if strip_offsets.len() != strip_byte_counts.len() {
1151 return Err(Error::invalid(
1152 "TIFF: StripOffsets / StripByteCounts length mismatch",
1153 ));
1154 }
1155 let strips_per_image = (height as u64).div_ceil(rows_per_strip as u64) as usize;
1156 let expected_entries = strips_per_image * samples_per_pixel as usize;
1157 if strip_offsets.len() != expected_entries {
1158 return Err(Error::invalid(format!(
1159 "TIFF: PlanarConfiguration=2 expects {expected_entries} strip entries \
1160 (SamplesPerPixel={samples_per_pixel} × StripsPerImage={strips_per_image}), got {}",
1161 strip_offsets.len()
1162 )));
1163 }
1164
1165 let bits_per_plane_row = (width as u64) * (bps_first as u64);
1170 let plane_row_bytes = bits_per_plane_row.div_ceil(8) as usize;
1171
1172 let spp = samples_per_pixel as usize;
1173 let mut planes: Vec<Vec<u8>> = Vec::with_capacity(spp);
1174 for plane in 0..spp {
1175 let mut plane_buf: Vec<u8> = Vec::with_capacity(plane_row_bytes * height as usize);
1176 let mut rows_done: u32 = 0;
1177 let plane_start = plane * strips_per_image;
1178 for s in 0..strips_per_image {
1179 let i = plane_start + s;
1180 let offset = strip_offsets[i];
1181 let byte_count = strip_byte_counts[i];
1182 let start = offset as usize;
1183 let end = start.checked_add(byte_count as usize).ok_or_else(|| {
1184 Error::invalid(format!("TIFF: plane-{plane} strip {s} length overflow"))
1185 })?;
1186 if end > input.len() {
1187 return Err(Error::invalid(format!(
1188 "TIFF: plane-{plane} strip {s} extends past EOF"
1189 )));
1190 }
1191 let raw = &input[start..end];
1192 let rows_this_strip = rows_per_strip.min(height - rows_done);
1193 let expected = plane_row_bytes * rows_this_strip as usize;
1194 let ccitt = if matches!(
1195 compression,
1196 COMPRESSION_CCITT_HUFFMAN | COMPRESSION_CCITT_T4 | COMPRESSION_CCITT_T6
1197 ) {
1198 Some(CcittParams {
1199 width,
1200 rows: rows_this_strip,
1201 fill: fill_order,
1202 t4_options,
1203 t6_options,
1204 })
1205 } else {
1206 None
1207 };
1208 let decompressed = decompress_block(raw, expected, compression, ccitt)?;
1209 if decompressed.len() < expected {
1210 return Err(Error::invalid(format!(
1211 "TIFF: plane-{plane} strip {s} short after decompress: got {}, expected {expected}",
1212 decompressed.len()
1213 )));
1214 }
1215 let mut strip = decompressed[..expected].to_vec();
1216 if compression == COMPRESSION_NONE
1217 && fill_order == FillOrder::LsbFirst
1218 && bps_first == 1
1219 {
1220 reverse_bits_in_place(&mut strip);
1221 }
1222 if predictor == PREDICTOR_HORIZONTAL {
1223 apply_horizontal_predictor(
1230 &mut strip,
1231 width as usize,
1232 rows_this_strip as usize,
1233 1,
1234 bps_first as usize,
1235 plane_row_bytes,
1236 bo,
1237 )?;
1238 }
1239 plane_buf.extend_from_slice(&strip);
1240 rows_done += rows_this_strip;
1241 if rows_done >= height {
1242 break;
1243 }
1244 }
1245 if rows_done < height {
1246 return Err(Error::invalid(format!(
1247 "TIFF: plane-{plane} strips did not cover full image"
1248 )));
1249 }
1250 planes.push(plane_buf);
1251 }
1252 interleave_planes(&planes, width, height, samples_per_pixel, bps_first)
1253}
1254
1255#[allow(clippy::too_many_arguments)]
1263fn decode_tiles_planar(
1264 input: &[u8],
1265 entries: &[Entry],
1266 bo: ByteOrder,
1267 width: u32,
1268 height: u32,
1269 samples_per_pixel: u16,
1270 bps_first: u16,
1271 compression: u16,
1272 predictor: u16,
1273 t4_options: u32,
1274 t6_options: u32,
1275 fill_order: FillOrder,
1276) -> Result<Vec<u8>> {
1277 if samples_per_pixel < 2 {
1278 return Err(Error::invalid(
1279 "TIFF: PlanarConfiguration=2 with SamplesPerPixel<2 is meaningless",
1280 ));
1281 }
1282 if bps_first % 8 != 0 {
1283 return Err(Error::invalid(
1284 "TIFF: planar tiled images at sub-byte bit depths not supported",
1285 ));
1286 }
1287 let tile_w = find(entries, TAG_TILE_WIDTH)
1288 .ok_or_else(|| Error::invalid("TIFF: missing TileWidth"))?
1289 .as_u32(bo)?;
1290 let tile_h = find(entries, TAG_TILE_LENGTH)
1291 .ok_or_else(|| Error::invalid("TIFF: missing TileLength"))?
1292 .as_u32(bo)?;
1293 if tile_w == 0 || tile_h == 0 {
1294 return Err(Error::invalid("TIFF: zero tile dimension"));
1295 }
1296 let tile_offsets = find(entries, TAG_TILE_OFFSETS)
1297 .ok_or_else(|| Error::invalid("TIFF: missing TileOffsets"))?
1298 .as_u64_vec(bo)?;
1299 let tile_byte_counts = find(entries, TAG_TILE_BYTE_COUNTS)
1300 .ok_or_else(|| Error::invalid("TIFF: missing TileByteCounts"))?
1301 .as_u64_vec(bo)?;
1302 if tile_offsets.len() != tile_byte_counts.len() {
1303 return Err(Error::invalid(
1304 "TIFF: TileOffsets / TileByteCounts length mismatch",
1305 ));
1306 }
1307 let tiles_across = (width as u64).div_ceil(tile_w as u64) as u32;
1308 let tiles_down = (height as u64).div_ceil(tile_h as u64) as u32;
1309 let tiles_per_plane = (tiles_across as usize) * (tiles_down as usize);
1310 let expected_tiles = tiles_per_plane * samples_per_pixel as usize;
1311 if tile_offsets.len() != expected_tiles {
1312 return Err(Error::invalid(format!(
1313 "TIFF: PlanarConfiguration=2 expects {expected_tiles} tile entries \
1314 (SamplesPerPixel={samples_per_pixel} × TilesPerImage={tiles_per_plane}), got {}",
1315 tile_offsets.len()
1316 )));
1317 }
1318
1319 let sample_bytes = (bps_first / 8) as usize;
1320 let bits_per_plane_tile_row = (tile_w as u64) * (bps_first as u64);
1321 let tile_row_bytes = bits_per_plane_tile_row.div_ceil(8) as usize;
1322 let tile_size_bytes = tile_row_bytes * tile_h as usize;
1323 let bits_per_plane_image_row = (width as u64) * (bps_first as u64);
1324 let plane_image_row_bytes = bits_per_plane_image_row.div_ceil(8) as usize;
1325
1326 let spp = samples_per_pixel as usize;
1327 let mut planes: Vec<Vec<u8>> = Vec::with_capacity(spp);
1328 for plane in 0..spp {
1329 let mut plane_buf = vec![0u8; plane_image_row_bytes * height as usize];
1330 let plane_start = plane * tiles_per_plane;
1331 for ty in 0..tiles_down {
1332 for tx in 0..tiles_across {
1333 let idx = plane_start + (ty * tiles_across + tx) as usize;
1334 let off = tile_offsets[idx] as usize;
1335 let bc = tile_byte_counts[idx] as usize;
1336 let end = off.checked_add(bc).ok_or_else(|| {
1337 Error::invalid(format!("TIFF: plane-{plane} tile length overflow"))
1338 })?;
1339 if end > input.len() {
1340 return Err(Error::invalid(format!(
1341 "TIFF: plane-{plane} tile extends past EOF"
1342 )));
1343 }
1344 let raw = &input[off..end];
1345 let ccitt = if matches!(
1346 compression,
1347 COMPRESSION_CCITT_HUFFMAN | COMPRESSION_CCITT_T4 | COMPRESSION_CCITT_T6
1348 ) {
1349 Some(CcittParams {
1350 width: tile_w,
1351 rows: tile_h,
1352 fill: fill_order,
1353 t4_options,
1354 t6_options,
1355 })
1356 } else {
1357 None
1358 };
1359 let mut tile = decompress_block(raw, tile_size_bytes, compression, ccitt)?;
1360 if tile.len() < tile_size_bytes {
1361 return Err(Error::invalid(format!(
1362 "TIFF: plane-{plane} tile short after decompress"
1363 )));
1364 }
1365 tile.truncate(tile_size_bytes);
1366 if compression == COMPRESSION_NONE
1367 && fill_order == FillOrder::LsbFirst
1368 && bps_first == 1
1369 {
1370 reverse_bits_in_place(&mut tile);
1371 }
1372 if predictor == PREDICTOR_HORIZONTAL {
1373 apply_horizontal_predictor(
1374 &mut tile,
1375 tile_w as usize,
1376 tile_h as usize,
1377 1,
1378 bps_first as usize,
1379 tile_row_bytes,
1380 bo,
1381 )?;
1382 }
1383 let visible_w =
1384 ((width as i64) - (tx as i64) * (tile_w as i64)).min(tile_w as i64) as usize;
1385 let visible_h =
1386 ((height as i64) - (ty as i64) * (tile_h as i64)).min(tile_h as i64) as usize;
1387 let visible_row_bytes = visible_w * sample_bytes;
1388 let dst_origin_x = tx as usize * tile_w as usize * sample_bytes;
1389 let dst_origin_y = ty as usize * tile_h as usize;
1390 for r in 0..visible_h {
1391 let src_off = r * tile_row_bytes;
1392 let dst_off = (dst_origin_y + r) * plane_image_row_bytes + dst_origin_x;
1393 plane_buf[dst_off..dst_off + visible_row_bytes]
1394 .copy_from_slice(&tile[src_off..src_off + visible_row_bytes]);
1395 }
1396 }
1397 }
1398 planes.push(plane_buf);
1399 }
1400 interleave_planes(&planes, width, height, samples_per_pixel, bps_first)
1401}
1402
1403fn interleave_planes(
1411 planes: &[Vec<u8>],
1412 width: u32,
1413 height: u32,
1414 samples_per_pixel: u16,
1415 bps_first: u16,
1416) -> Result<Vec<u8>> {
1417 let sample_bytes = (bps_first / 8) as usize;
1418 let spp = samples_per_pixel as usize;
1419 let pixels = (width as usize) * (height as usize);
1420 let plane_bytes = pixels * sample_bytes;
1421 for (i, p) in planes.iter().enumerate() {
1422 if p.len() != plane_bytes {
1423 return Err(Error::invalid(format!(
1424 "TIFF: plane {i} has {} bytes, expected {plane_bytes}",
1425 p.len()
1426 )));
1427 }
1428 }
1429 let mut out = vec![0u8; pixels * spp * sample_bytes];
1430 for px in 0..pixels {
1431 for (c, plane) in planes.iter().enumerate() {
1432 let src = &plane[px * sample_bytes..(px + 1) * sample_bytes];
1433 let dst_off = (px * spp + c) * sample_bytes;
1434 out[dst_off..dst_off + sample_bytes].copy_from_slice(src);
1435 }
1436 }
1437 Ok(out)
1438}
1439
1440#[derive(Debug, Clone, Copy)]
1444struct CcittParams {
1445 width: u32,
1446 rows: u32,
1447 fill: FillOrder,
1448 t4_options: u32,
1449 t6_options: u32,
1450}
1451
1452fn decompress_block(
1453 raw: &[u8],
1454 expected: usize,
1455 compression: u16,
1456 ccitt: Option<CcittParams>,
1457) -> Result<Vec<u8>> {
1458 match compression {
1459 COMPRESSION_NONE => Ok(raw.to_vec()),
1460 COMPRESSION_PACKBITS => unpack_packbits(raw, expected),
1461 COMPRESSION_LZW => unpack_lzw(raw, expected),
1462 COMPRESSION_DEFLATE_ADOBE => unpack_deflate(raw, expected),
1463 COMPRESSION_ZSTD => unpack_zstd(raw, expected),
1470 COMPRESSION_CCITT_HUFFMAN => {
1471 let p = ccitt
1472 .ok_or_else(|| Error::invalid("TIFF: CCITT compression requires CcittParams"))?;
1473 decode_ccitt(raw, p.width, p.rows, CcittVariant::ModifiedHuffman, p.fill)
1474 }
1475 COMPRESSION_CCITT_T4 => {
1476 let p = ccitt
1477 .ok_or_else(|| Error::invalid("TIFF: CCITT-T4 compression requires CcittParams"))?;
1478 let eol_byte_aligned = p.t4_options & T4OPT_EOL_BYTE_ALIGNED != 0;
1487 let variant = if p.t4_options & T4OPT_2D_CODING != 0 {
1488 CcittVariant::T4TwoD { eol_byte_aligned }
1489 } else {
1490 CcittVariant::T4OneD { eol_byte_aligned }
1491 };
1492 decode_ccitt(raw, p.width, p.rows, variant, p.fill)
1493 }
1494 COMPRESSION_CCITT_T6 => {
1495 let p = ccitt
1496 .ok_or_else(|| Error::invalid("TIFF: CCITT-T6 compression requires CcittParams"))?;
1497 if p.t6_options & !T6OPT_UNCOMPRESSED != 0 {
1508 return Err(Error::invalid(format!(
1509 "TIFF: T6Options has undefined bits set (0x{:x}); only bit 1 is defined",
1510 p.t6_options
1511 )));
1512 }
1513 decode_ccitt(raw, p.width, p.rows, CcittVariant::T6, p.fill)
1514 }
1515 other => Err(Error::invalid(format!(
1516 "TIFF: Compression={other} not supported"
1517 ))),
1518 }
1519}
1520
1521fn decode_bits_per_sample(
1522 entry: Option<&crate::ifd::Entry>,
1523 bo: ByteOrder,
1524 spp: u16,
1525) -> Result<Vec<u16>> {
1526 match entry {
1527 None => Ok(vec![1; spp as usize]), Some(e) => {
1529 if e.count as u16 != spp {
1530 return Err(Error::invalid(format!(
1531 "TIFF: BitsPerSample count {} != SamplesPerPixel {}",
1532 e.count, spp
1533 )));
1534 }
1535 let v = e.as_u32_vec(bo)?;
1536 Ok(v.into_iter().map(|b| b as u16).collect())
1537 }
1538 }
1539}
1540
1541fn apply_horizontal_predictor(
1548 buf: &mut [u8],
1549 width: usize,
1550 rows: usize,
1551 samples: usize,
1552 bps: usize,
1553 row_bytes: usize,
1554 bo: ByteOrder,
1555) -> Result<()> {
1556 if width == 0 || rows == 0 {
1557 return Ok(());
1558 }
1559 match bps {
1560 8 => {
1561 for r in 0..rows {
1562 let row = &mut buf[r * row_bytes..r * row_bytes + width * samples];
1563 for x in samples..(width * samples) {
1564 row[x] = row[x].wrapping_add(row[x - samples]);
1565 }
1566 }
1567 }
1568 16 => {
1569 for r in 0..rows {
1570 let row = &mut buf[r * row_bytes..r * row_bytes + width * samples * 2];
1571 let pixels = width * samples;
1572 for x in samples..pixels {
1575 let cur_off = x * 2;
1576 let prev_off = (x - samples) * 2;
1577 let cur = bo.read_u16(&row[cur_off..cur_off + 2]);
1578 let prev = bo.read_u16(&row[prev_off..prev_off + 2]);
1579 let new = cur.wrapping_add(prev);
1580 let bytes = match bo {
1581 ByteOrder::Little => new.to_le_bytes(),
1582 ByteOrder::Big => new.to_be_bytes(),
1583 };
1584 row[cur_off] = bytes[0];
1585 row[cur_off + 1] = bytes[1];
1586 }
1587 }
1588 }
1589 4 => {
1590 for r in 0..rows {
1592 let row_off = r * row_bytes;
1593 let mut tmp: Vec<u8> = Vec::with_capacity(width * samples);
1595 for x in 0..(width * samples) {
1596 let byte = buf[row_off + x / 2];
1597 let n = if x & 1 == 0 { byte >> 4 } else { byte & 0x0F };
1598 tmp.push(n);
1599 }
1600 for x in samples..(width * samples) {
1601 tmp[x] = tmp[x].wrapping_add(tmp[x - samples]) & 0x0F;
1602 }
1603 for (x, b) in tmp.iter().enumerate() {
1605 let off = row_off + x / 2;
1606 if x & 1 == 0 {
1607 buf[off] = (buf[off] & 0x0F) | ((b & 0x0F) << 4);
1608 } else {
1609 buf[off] = (buf[off] & 0xF0) | (b & 0x0F);
1610 }
1611 }
1612 }
1613 }
1614 _ => {
1615 return Err(Error::invalid(format!(
1616 "TIFF: predictor at bits_per_sample={bps} unsupported"
1617 )))
1618 }
1619 }
1620 Ok(())
1621}
1622
1623fn build_gray8(src: &[u8], w: u32, h: u32, invert: bool, signed: bool) -> TiffImage {
1628 let stride = w as usize;
1629 let mut data = src[..stride * h as usize].to_vec();
1630 if signed {
1637 for b in data.iter_mut() {
1638 *b ^= 0x80;
1639 }
1640 }
1641 if invert {
1642 for b in data.iter_mut() {
1643 *b = 255 - *b;
1644 }
1645 }
1646 TiffImage {
1647 width: w,
1648 height: h,
1649 pixel_format: TiffPixelFormat::Gray8,
1650 planes: vec![TiffPlane { stride, data }],
1651 }
1652}
1653
1654fn build_gray8_from_4bpp(src: &[u8], w: u32, h: u32, row_bytes: usize, invert: bool) -> TiffImage {
1655 let stride = w as usize;
1656 let mut data = Vec::with_capacity(stride * h as usize);
1657 for y in 0..h as usize {
1658 let row = &src[y * row_bytes..y * row_bytes + row_bytes];
1659 for x in 0..w as usize {
1660 let byte = row[x / 2];
1661 let n = if x & 1 == 0 { byte >> 4 } else { byte & 0x0F };
1662 let v = (n << 4) | n;
1664 data.push(if invert { 255 - v } else { v });
1665 }
1666 }
1667 TiffImage {
1668 width: w,
1669 height: h,
1670 pixel_format: TiffPixelFormat::Gray8,
1671 planes: vec![TiffPlane { stride, data }],
1672 }
1673}
1674
1675fn build_gray8_from_1bpp(src: &[u8], w: u32, h: u32, row_bytes: usize, invert: bool) -> TiffImage {
1676 let stride = w as usize;
1677 let mut data = Vec::with_capacity(stride * h as usize);
1678 for y in 0..h as usize {
1679 let row = &src[y * row_bytes..y * row_bytes + row_bytes];
1680 for x in 0..w as usize {
1681 let byte = row[x / 8];
1682 let bit = (byte >> (7 - (x % 8))) & 1;
1683 let v = if bit == 1 { 255 } else { 0 };
1685 data.push(if invert { 255 - v } else { v });
1686 }
1687 }
1688 TiffImage {
1689 width: w,
1690 height: h,
1691 pixel_format: TiffPixelFormat::Gray8,
1692 planes: vec![TiffPlane { stride, data }],
1693 }
1694}
1695
1696fn build_gray16le(
1697 src: &[u8],
1698 w: u32,
1699 h: u32,
1700 bo: ByteOrder,
1701 invert: bool,
1702 signed: bool,
1703) -> TiffImage {
1704 let stride = w as usize * 2;
1705 let n = (w * h) as usize;
1706 let mut data = Vec::with_capacity(stride * h as usize);
1707 for i in 0..n {
1708 let mut v = bo.read_u16(&src[i * 2..i * 2 + 2]);
1709 if signed {
1713 v ^= 0x8000;
1714 }
1715 let v = if invert { 0xFFFF - v } else { v };
1716 data.extend_from_slice(&v.to_le_bytes());
1717 }
1718 TiffImage {
1719 width: w,
1720 height: h,
1721 pixel_format: TiffPixelFormat::Gray16Le,
1722 planes: vec![TiffPlane { stride, data }],
1723 }
1724}
1725
1726fn build_rgb24(src: &[u8], w: u32, h: u32) -> TiffImage {
1727 let stride = w as usize * 3;
1728 let data = src[..stride * h as usize].to_vec();
1729 TiffImage {
1730 width: w,
1731 height: h,
1732 pixel_format: TiffPixelFormat::Rgb24,
1733 planes: vec![TiffPlane { stride, data }],
1734 }
1735}
1736
1737fn build_rgb_from_n_chunky_8bit(src: &[u8], w: u32, h: u32, n: usize) -> TiffImage {
1738 let stride = w as usize * 3;
1739 let mut data = Vec::with_capacity(stride * h as usize);
1740 for y in 0..h as usize {
1741 let row = &src[y * w as usize * n..(y + 1) * w as usize * n];
1742 for x in 0..w as usize {
1743 data.push(row[x * n]);
1744 data.push(row[x * n + 1]);
1745 data.push(row[x * n + 2]);
1746 }
1747 }
1748 TiffImage {
1749 width: w,
1750 height: h,
1751 pixel_format: TiffPixelFormat::Rgb24,
1752 planes: vec![TiffPlane { stride, data }],
1753 }
1754}
1755
1756fn build_rgb48le(src: &[u8], w: u32, h: u32, bo: ByteOrder) -> TiffImage {
1757 let stride = w as usize * 6;
1758 let pixels = (w * h) as usize;
1759 let mut data = Vec::with_capacity(stride * h as usize);
1760 for i in 0..pixels {
1761 let off = i * 6;
1762 for c in 0..3 {
1763 let v = bo.read_u16(&src[off + c * 2..off + c * 2 + 2]);
1764 data.extend_from_slice(&v.to_le_bytes());
1765 }
1766 }
1767 TiffImage {
1768 width: w,
1769 height: h,
1770 pixel_format: TiffPixelFormat::Rgb48Le,
1771 planes: vec![TiffPlane { stride, data }],
1772 }
1773}
1774
1775fn parse_colormap(words: &[u32], bps: u16) -> Result<Vec<[u8; 3]>> {
1776 let entries = 1usize << bps;
1779 if words.len() < 3 * entries {
1780 return Err(Error::invalid(format!(
1781 "TIFF: ColorMap has {} entries, expected {}",
1782 words.len(),
1783 3 * entries
1784 )));
1785 }
1786 let mut out = Vec::with_capacity(entries);
1787 for i in 0..entries {
1788 let r = (words[i] >> 8) as u8;
1789 let g = (words[entries + i] >> 8) as u8;
1790 let b = (words[2 * entries + i] >> 8) as u8;
1791 out.push([r, g, b]);
1792 }
1793 Ok(out)
1794}
1795
1796fn build_rgb24_from_palette(
1797 src: &[u8],
1798 w: u32,
1799 h: u32,
1800 palette: &[[u8; 3]],
1801 bps: u16,
1802 row_bytes: usize,
1803) -> TiffImage {
1804 let stride = w as usize * 3;
1805 let mut data = Vec::with_capacity(stride * h as usize);
1806 for y in 0..h as usize {
1807 let row = &src[y * row_bytes..y * row_bytes + row_bytes];
1808 for x in 0..w as usize {
1809 let idx = match bps {
1810 8 => row[x] as usize,
1811 4 => {
1812 let byte = row[x / 2];
1813 (if x & 1 == 0 { byte >> 4 } else { byte & 0x0F }) as usize
1814 }
1815 _ => 0,
1816 };
1817 let p = palette.get(idx).copied().unwrap_or([0, 0, 0]);
1818 data.push(p[0]);
1819 data.push(p[1]);
1820 data.push(p[2]);
1821 }
1822 }
1823 TiffImage {
1824 width: w,
1825 height: h,
1826 pixel_format: TiffPixelFormat::Rgb24,
1827 planes: vec![TiffPlane { stride, data }],
1828 }
1829}
1830
1831fn build_rgb24_from_cmyk(src: &[u8], w: u32, h: u32) -> TiffImage {
1838 let stride = w as usize * 3;
1839 let pixels = (w * h) as usize;
1840 let mut data = Vec::with_capacity(stride * h as usize);
1841 for i in 0..pixels {
1842 let off = i * 4;
1843 let c = src[off] as u32;
1844 let m = src[off + 1] as u32;
1845 let y = src[off + 2] as u32;
1846 let k = src[off + 3] as u32;
1847 let r = ((255 - c) * (255 - k) / 255) as u8;
1850 let g = ((255 - m) * (255 - k) / 255) as u8;
1851 let b = ((255 - y) * (255 - k) / 255) as u8;
1852 data.push(r);
1853 data.push(g);
1854 data.push(b);
1855 }
1856 TiffImage {
1857 width: w,
1858 height: h,
1859 pixel_format: TiffPixelFormat::Rgb24,
1860 planes: vec![TiffPlane { stride, data }],
1861 }
1862}
1863
1864fn build_rgb24_from_ycbcr(src: &[u8], w: u32, h: u32, sh: u16, sv: u16) -> Result<TiffImage> {
1872 if sh == 0 || sv == 0 {
1873 return Err(Error::invalid("TIFF: YCbCrSubSampling must be > 0"));
1874 }
1875 if !matches!(
1876 (sh, sv),
1877 (1, 1) | (2, 1) | (2, 2) | (1, 2) | (4, 1) | (4, 2)
1878 ) {
1879 return Err(Error::invalid(format!(
1880 "TIFF: YCbCrSubSampling=({sh},{sv}) not supported"
1881 )));
1882 }
1883 let block_w = (w as usize).div_ceil(sh as usize);
1884 let block_h = (h as usize).div_ceil(sv as usize);
1885 let block_size_bytes = (sh as usize) * (sv as usize) + 2; let expected = block_w * block_h * block_size_bytes;
1887 if src.len() < expected {
1888 return Err(Error::invalid(format!(
1889 "TIFF/YCbCr: pixel buffer too small (got {}, need {expected})",
1890 src.len()
1891 )));
1892 }
1893
1894 let mut data = vec![0u8; (w as usize) * 3 * h as usize];
1897 for by in 0..block_h {
1898 for bx in 0..block_w {
1899 let block_off = (by * block_w + bx) * block_size_bytes;
1900 let cb = src[block_off + (sh as usize) * (sv as usize)] as i32;
1901 let cr = src[block_off + (sh as usize) * (sv as usize) + 1] as i32;
1902 for sy in 0..(sv as usize) {
1903 for sx in 0..(sh as usize) {
1904 let y_val = src[block_off + sy * (sh as usize) + sx] as i32;
1905 let px = bx * (sh as usize) + sx;
1906 let py = by * (sv as usize) + sy;
1907 if px >= w as usize || py >= h as usize {
1908 continue;
1909 }
1910 let (r, g, b) = ycbcr_to_rgb(y_val, cb, cr);
1911 let dst = (py * (w as usize) + px) * 3;
1912 data[dst] = r;
1913 data[dst + 1] = g;
1914 data[dst + 2] = b;
1915 }
1916 }
1917 }
1918 }
1919 Ok(TiffImage {
1920 width: w,
1921 height: h,
1922 pixel_format: TiffPixelFormat::Rgb24,
1923 planes: vec![TiffPlane {
1924 stride: w as usize * 3,
1925 data,
1926 }],
1927 })
1928}
1929
1930fn ycbcr_to_rgb(y: i32, cb: i32, cr: i32) -> (u8, u8, u8) {
1934 let cb = cb - 128;
1935 let cr = cr - 128;
1936 let r = y + ((91881 * cr + 32768) >> 16);
1940 let g = y - ((22554 * cb + 46802 * cr + 32768) >> 16);
1941 let b = y + ((116130 * cb + 32768) >> 16);
1942 (clamp_u8(r), clamp_u8(g), clamp_u8(b))
1943}
1944
1945fn clamp_u8(v: i32) -> u8 {
1946 v.clamp(0, 255) as u8
1947}
1948
1949fn build_rgb24_from_cielab(src: &[u8], w: u32, h: u32) -> TiffImage {
1967 let stride = w as usize * 3;
1968 let pixels = (w as usize) * (h as usize);
1969 let mut data = Vec::with_capacity(stride * h as usize);
1970 for triple in src.chunks_exact(3).take(pixels) {
1971 let l_byte = triple[0] as f64;
1974 let a_signed = triple[1] as i8 as f64;
1979 let b_signed = triple[2] as i8 as f64;
1980 let l = l_byte * (100.0 / 255.0);
1981 let (r, g, b) = cielab_to_rgb_byte(l, a_signed, b_signed);
1982 data.push(r);
1983 data.push(g);
1984 data.push(b);
1985 }
1986 TiffImage {
1987 width: w,
1988 height: h,
1989 pixel_format: TiffPixelFormat::Rgb24,
1990 planes: vec![TiffPlane { stride, data }],
1991 }
1992}
1993
1994fn build_gray8_from_cielab_l(src: &[u8], w: u32, h: u32) -> TiffImage {
2003 let stride = w as usize;
2004 let pixels = (w as usize) * (h as usize);
2005 let mut data = Vec::with_capacity(pixels);
2006 for &byte in src.iter().take(pixels) {
2007 let l = byte as f64 * (100.0 / 255.0);
2008 let y_lin = lab_l_to_y_linear(l);
2014 data.push(linear_to_srgb_byte(y_lin));
2015 }
2016 TiffImage {
2017 width: w,
2018 height: h,
2019 pixel_format: TiffPixelFormat::Gray8,
2020 planes: vec![TiffPlane { stride, data }],
2021 }
2022}
2023
2024fn cielab_to_rgb_byte(l_star: f64, a_star: f64, b_star: f64) -> (u8, u8, u8) {
2051 const XN: f64 = 0.95047;
2055 const YN: f64 = 1.00000;
2056 const ZN: f64 = 1.08883;
2057
2058 let fy = (l_star + 16.0) / 116.0;
2070 let fx = a_star / 500.0 + fy;
2071 let fz = fy - b_star / 200.0;
2072
2073 let y_yn = if l_star > 7.999625 {
2074 fy.powi(3)
2077 } else {
2078 l_star / 903.3
2079 };
2080 let fx3 = fx.powi(3);
2081 let x_xn = if fx3 > 0.008856 {
2082 fx3
2083 } else {
2084 (fx - 16.0 / 116.0) / 7.787
2085 };
2086 let fz3 = fz.powi(3);
2087 let z_zn = if fz3 > 0.008856 {
2088 fz3
2089 } else {
2090 (fz - 16.0 / 116.0) / 7.787
2091 };
2092
2093 let x = x_xn * XN;
2094 let y = y_yn * YN;
2095 let z = z_zn * ZN;
2096
2097 let r_lin = 1.9103738257 * x - 0.5337689371 * y - 0.2891315088 * z;
2112 let g_lin = -0.9844441268 * x + 1.9985203510 * y - 0.0278510303 * z;
2113 let b_lin = 0.0584818293 * x - 0.1187239812 * y + 0.9017445257 * z;
2114
2115 (
2116 linear_to_srgb_byte(r_lin),
2117 linear_to_srgb_byte(g_lin),
2118 linear_to_srgb_byte(b_lin),
2119 )
2120}
2121
2122fn lab_l_to_y_linear(l_star: f64) -> f64 {
2126 if l_star > 7.999625 {
2127 ((l_star + 16.0) / 116.0).powi(3)
2128 } else {
2129 l_star / 903.3
2130 }
2131}
2132
2133fn linear_to_srgb_byte(v: f64) -> u8 {
2137 let c = v.clamp(0.0, 1.0);
2138 let encoded = if c <= 0.0031308 {
2141 12.92 * c
2142 } else {
2143 1.055 * c.powf(1.0 / 2.4) - 0.055
2144 };
2145 (encoded * 255.0 + 0.5).clamp(0.0, 255.0) as u8
2146}
2147
2148#[cfg(feature = "registry")]
2156#[allow(clippy::too_many_arguments)]
2157fn decode_ifd_jpeg(
2158 input: &[u8],
2159 entries: &[Entry],
2160 bo: ByteOrder,
2161 width: u32,
2162 height: u32,
2163 samples_per_pixel: u16,
2164 bps_first: u16,
2165 photometric: u16,
2166) -> Result<TiffImage> {
2167 if bps_first != 8 {
2171 return Err(Error::Unsupported(format!(
2172 "TIFF/JPEG: BitsPerSample={bps_first} (only 8-bit is supported in this build)"
2173 )));
2174 }
2175 let want_planes = match (photometric, samples_per_pixel) {
2179 (PHOTO_BLACK_IS_ZERO, 1) | (PHOTO_WHITE_IS_ZERO, 1) => 1,
2180 (PHOTO_RGB, 3) => 3,
2181 (PHOTO_YCBCR, 3) => 3,
2182 (PHOTO_CMYK, 4) => 1,
2190 (p, s) => {
2191 return Err(Error::invalid(format!(
2192 "TIFF/JPEG: photometric={p} samples_per_pixel={s} not supported"
2193 )));
2194 }
2195 };
2196 let _ = want_planes;
2197
2198 let tables: Option<&[u8]> = find(entries, TAG_JPEG_TABLES).map(|e| e.data.as_slice());
2202
2203 let (pixel_format, dst_row_stride, dst_size) = match photometric {
2211 PHOTO_BLACK_IS_ZERO | PHOTO_WHITE_IS_ZERO => (
2212 TiffPixelFormat::Gray8,
2213 width as usize,
2214 width as usize * height as usize,
2215 ),
2216 PHOTO_RGB | PHOTO_YCBCR | PHOTO_CMYK => (
2217 TiffPixelFormat::Rgb24,
2218 width as usize * 3,
2219 width as usize * 3 * height as usize,
2220 ),
2221 _ => unreachable!("photometric vetted above"),
2222 };
2223 let mut dst = vec![0u8; dst_size];
2224
2225 let invert = photometric == PHOTO_WHITE_IS_ZERO;
2226 let want_yuv = photometric == PHOTO_YCBCR;
2227
2228 if find(entries, TAG_TILE_WIDTH).is_some() {
2230 decode_ifd_jpeg_tiles(
2231 input,
2232 entries,
2233 bo,
2234 width,
2235 height,
2236 photometric,
2237 tables,
2238 invert,
2239 want_yuv,
2240 &mut dst,
2241 dst_row_stride,
2242 )?;
2243 } else {
2244 decode_ifd_jpeg_strips(
2245 input,
2246 entries,
2247 bo,
2248 width,
2249 height,
2250 photometric,
2251 tables,
2252 invert,
2253 want_yuv,
2254 &mut dst,
2255 dst_row_stride,
2256 )?;
2257 }
2258
2259 Ok(TiffImage {
2260 width,
2261 height,
2262 pixel_format,
2263 planes: vec![TiffPlane {
2264 stride: dst_row_stride,
2265 data: dst,
2266 }],
2267 })
2268}
2269
2270#[cfg(feature = "registry")]
2271#[allow(clippy::too_many_arguments)]
2272fn decode_ifd_jpeg_strips(
2273 input: &[u8],
2274 entries: &[Entry],
2275 bo: ByteOrder,
2276 width: u32,
2277 height: u32,
2278 photometric: u16,
2279 tables: Option<&[u8]>,
2280 invert: bool,
2281 want_yuv: bool,
2282 dst: &mut [u8],
2283 dst_row_stride: usize,
2284) -> Result<()> {
2285 use crate::jpeg::decode_segment;
2286
2287 let rows_per_strip = find(entries, TAG_ROWS_PER_STRIP)
2288 .map(|e| e.as_u32(bo))
2289 .transpose()?
2290 .unwrap_or(height);
2291 let strip_offsets = find(entries, TAG_STRIP_OFFSETS)
2292 .ok_or_else(|| Error::invalid("TIFF: missing StripOffsets"))?
2293 .as_u64_vec(bo)?;
2294 let strip_byte_counts = find(entries, TAG_STRIP_BYTE_COUNTS)
2295 .ok_or_else(|| Error::invalid("TIFF: missing StripByteCounts"))?
2296 .as_u64_vec(bo)?;
2297 if strip_offsets.len() != strip_byte_counts.len() {
2298 return Err(Error::invalid(
2299 "TIFF/JPEG: StripOffsets / StripByteCounts length mismatch",
2300 ));
2301 }
2302
2303 let mut rows_done: u32 = 0;
2304 for (i, (&offset, &byte_count)) in strip_offsets
2305 .iter()
2306 .zip(strip_byte_counts.iter())
2307 .enumerate()
2308 {
2309 let start = offset as usize;
2310 let end = start
2311 .checked_add(byte_count as usize)
2312 .ok_or_else(|| Error::invalid(format!("TIFF: strip {i} length overflow")))?;
2313 if end > input.len() {
2314 return Err(Error::invalid(format!("TIFF: strip {i} extends past EOF")));
2315 }
2316 let raw = &input[start..end];
2317 let rows_this_strip = rows_per_strip.min(height - rows_done);
2318
2319 let seg = decode_segment(tables, raw, width, rows_this_strip, photometric)?;
2320 composite_segment(
2321 &seg,
2322 width,
2323 rows_this_strip,
2324 dst,
2325 dst_row_stride,
2326 0,
2327 rows_done,
2328 invert,
2329 want_yuv,
2330 photometric,
2331 )?;
2332 rows_done += rows_this_strip;
2333 if rows_done >= height {
2334 break;
2335 }
2336 }
2337 if rows_done < height {
2338 return Err(Error::invalid("TIFF/JPEG: strips did not cover full image"));
2339 }
2340 Ok(())
2341}
2342
2343#[cfg(feature = "registry")]
2344#[allow(clippy::too_many_arguments)]
2345fn decode_ifd_jpeg_tiles(
2346 input: &[u8],
2347 entries: &[Entry],
2348 bo: ByteOrder,
2349 width: u32,
2350 height: u32,
2351 photometric: u16,
2352 tables: Option<&[u8]>,
2353 invert: bool,
2354 want_yuv: bool,
2355 dst: &mut [u8],
2356 dst_row_stride: usize,
2357) -> Result<()> {
2358 use crate::jpeg::decode_segment;
2359
2360 let tile_w = find(entries, TAG_TILE_WIDTH)
2361 .ok_or_else(|| Error::invalid("TIFF: missing TileWidth"))?
2362 .as_u32(bo)?;
2363 let tile_h = find(entries, TAG_TILE_LENGTH)
2364 .ok_or_else(|| Error::invalid("TIFF: missing TileLength"))?
2365 .as_u32(bo)?;
2366 if tile_w == 0 || tile_h == 0 {
2367 return Err(Error::invalid("TIFF: zero tile dimension"));
2368 }
2369 let tile_offsets = find(entries, TAG_TILE_OFFSETS)
2370 .ok_or_else(|| Error::invalid("TIFF: missing TileOffsets"))?
2371 .as_u64_vec(bo)?;
2372 let tile_byte_counts = find(entries, TAG_TILE_BYTE_COUNTS)
2373 .ok_or_else(|| Error::invalid("TIFF: missing TileByteCounts"))?
2374 .as_u64_vec(bo)?;
2375 if tile_offsets.len() != tile_byte_counts.len() {
2376 return Err(Error::invalid(
2377 "TIFF/JPEG: TileOffsets / TileByteCounts length mismatch",
2378 ));
2379 }
2380 let tiles_across = (width as u64).div_ceil(tile_w as u64) as u32;
2381 let tiles_down = (height as u64).div_ceil(tile_h as u64) as u32;
2382 let expected_tiles = (tiles_across as usize) * (tiles_down as usize);
2383 if tile_offsets.len() != expected_tiles {
2384 return Err(Error::invalid(format!(
2385 "TIFF/JPEG: TileOffsets length {} != expected {expected_tiles}",
2386 tile_offsets.len()
2387 )));
2388 }
2389
2390 for ty in 0..tiles_down {
2391 for tx in 0..tiles_across {
2392 let idx = (ty * tiles_across + tx) as usize;
2393 let off = tile_offsets[idx] as usize;
2394 let bc = tile_byte_counts[idx] as usize;
2395 let end = off
2396 .checked_add(bc)
2397 .ok_or_else(|| Error::invalid("TIFF/JPEG: tile length overflow"))?;
2398 if end > input.len() {
2399 return Err(Error::invalid("TIFF/JPEG: tile extends past EOF"));
2400 }
2401 let raw = &input[off..end];
2402 let seg = decode_segment(tables, raw, tile_w, tile_h, photometric)?;
2403 let visible_w = ((width as i64) - (tx as i64) * (tile_w as i64))
2404 .min(tile_w as i64)
2405 .max(0) as u32;
2406 let visible_h = ((height as i64) - (ty as i64) * (tile_h as i64))
2407 .min(tile_h as i64)
2408 .max(0) as u32;
2409 composite_segment(
2410 &seg,
2411 visible_w,
2412 visible_h,
2413 dst,
2414 dst_row_stride,
2415 tx * tile_w,
2416 ty * tile_h,
2417 invert,
2418 want_yuv,
2419 photometric,
2420 )?;
2421 }
2422 }
2423 Ok(())
2424}
2425
2426#[cfg(feature = "registry")]
2429#[allow(clippy::too_many_arguments)]
2430fn composite_segment(
2431 seg: &crate::jpeg::JpegSegment,
2432 visible_w: u32,
2433 visible_h: u32,
2434 dst: &mut [u8],
2435 dst_row_stride: usize,
2436 dst_x: u32,
2437 dst_y: u32,
2438 invert: bool,
2439 want_yuv: bool,
2440 photometric: u16,
2441) -> Result<()> {
2442 use crate::jpeg::{
2443 composite_cmyk_to_rgb, composite_gray, composite_rgb_packed, composite_rgb_planar,
2444 composite_yuv_to_rgb, JpegPixelFormat,
2445 };
2446 match seg.pixel_format {
2447 JpegPixelFormat::Gray8 => composite_gray(
2448 seg,
2449 visible_w,
2450 visible_h,
2451 dst,
2452 dst_row_stride,
2453 dst_x,
2454 dst_y,
2455 invert,
2456 ),
2457 JpegPixelFormat::Yuv444P
2458 | JpegPixelFormat::Yuv422P
2459 | JpegPixelFormat::Yuv420P
2460 | JpegPixelFormat::Yuv411P => {
2461 if !want_yuv && photometric != PHOTO_RGB {
2462 return Err(Error::invalid(format!(
2463 "TIFF/JPEG: YUV-output JPEG segment but TIFF photometric={photometric}"
2464 )));
2465 }
2466 if want_yuv {
2472 composite_yuv_to_rgb(seg, visible_w, visible_h, dst, dst_row_stride, dst_x, dst_y)
2473 } else {
2474 composite_rgb_planar(seg, visible_w, visible_h, dst, dst_row_stride, dst_x, dst_y)
2475 }
2476 }
2477 JpegPixelFormat::Rgb24 => {
2478 if photometric != PHOTO_RGB {
2479 return Err(Error::invalid(format!(
2480 "TIFF/JPEG: RGB-output JPEG but TIFF photometric={photometric}"
2481 )));
2482 }
2483 composite_rgb_planar(seg, visible_w, visible_h, dst, dst_row_stride, dst_x, dst_y)
2484 }
2485 JpegPixelFormat::Rgb24Packed => {
2486 if photometric != PHOTO_RGB {
2487 return Err(Error::invalid(format!(
2488 "TIFF/JPEG: packed-RGB-output JPEG but TIFF photometric={photometric}"
2489 )));
2490 }
2491 composite_rgb_packed(seg, visible_w, visible_h, dst, dst_row_stride, dst_x, dst_y)
2492 }
2493 JpegPixelFormat::Cmyk8 => {
2494 if photometric != PHOTO_CMYK {
2495 return Err(Error::invalid(format!(
2496 "TIFF/JPEG: CMYK-output JPEG but TIFF photometric={photometric}"
2497 )));
2498 }
2499 composite_cmyk_to_rgb(seg, visible_w, visible_h, dst, dst_row_stride, dst_x, dst_y)
2500 }
2501 }
2502}