1use otf_pixels_compress::{LzwDecoder, inflate_to, zlib_decompress};
10use otf_pixels_core::{
11 ImageDescriptor, Limits, Orientation, PixelFormat, PixelsError, Region, Result, SampleKind,
12};
13
14use crate::ifd::{ByteOrder, Directory, tag};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum Compression {
19 None,
21 Lzw,
23 Deflate,
25 PackBits,
27}
28
29impl Compression {
30 pub fn from_tag(value: u32) -> Result<Self> {
39 match value {
40 1 => Ok(Self::None),
41 5 => Ok(Self::Lzw),
42 8 | 32_946 => Ok(Self::Deflate),
43 32_773 => Ok(Self::PackBits),
44 other => Err(PixelsError::unsupported(format!(
45 "TIFF compression {other} is not implemented"
46 ))),
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub enum Photometric {
54 WhiteIsZero,
56 BlackIsZero,
58 Rgb,
60 Palette,
62}
63
64impl Photometric {
65 pub fn from_tag(value: u32) -> Result<Self> {
71 match value {
72 0 => Ok(Self::WhiteIsZero),
73 1 => Ok(Self::BlackIsZero),
74 2 => Ok(Self::Rgb),
75 3 => Ok(Self::Palette),
76 other => Err(PixelsError::unsupported(format!(
77 "TIFF photometric interpretation {other} is not implemented"
78 ))),
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85pub enum Layout {
86 Strips {
88 rows_per_strip: u32,
90 },
91 Tiles {
96 width: u32,
98 height: u32,
100 },
101}
102
103impl Layout {
104 #[must_use]
106 pub const fn is_random_access(self) -> bool {
107 matches!(self, Self::Tiles { .. })
108 }
109}
110
111#[derive(Debug, Clone)]
113#[non_exhaustive]
114pub struct TiffImage {
115 pub descriptor: ImageDescriptor,
117 pub bits_per_sample: u32,
119 pub samples_per_pixel: u32,
121 pub photometric: Photometric,
123 pub compression: Compression,
125 pub layout: Layout,
127 pub offsets: Vec<u32>,
129 pub byte_counts: Vec<u32>,
131 pub predictor: bool,
133 pub color_map: Vec<u32>,
135 pub order: ByteOrder,
137 pub orientation: Orientation,
140 pub icc: Option<Vec<u8>>,
142}
143
144impl TiffImage {
145 pub fn from_directory(
154 directory: &Directory,
155 order: ByteOrder,
156 limits: &Limits,
157 ) -> Result<Self> {
158 let width = directory.require(tag::IMAGE_WIDTH, "ImageWidth")?;
159 let height = directory.require(tag::IMAGE_LENGTH, "ImageLength")?;
160
161 let samples_per_pixel = directory.value_or(tag::SAMPLES_PER_PIXEL, 1);
162 let bits = directory.values(tag::BITS_PER_SAMPLE);
163 let bits_per_sample = bits.first().copied().unwrap_or(1);
164 if bits.iter().any(|&b| b != bits_per_sample) {
168 return Err(PixelsError::unsupported(
169 "TIFF channels with differing bit depths are not implemented",
170 ));
171 }
172
173 let photometric =
174 Photometric::from_tag(directory.require(tag::PHOTOMETRIC, "Photometric")?)?;
175 let compression = Compression::from_tag(directory.value_or(tag::COMPRESSION, 1))?;
176
177 if directory.value_or(tag::PLANAR_CONFIG, 1) != 1 {
180 return Err(PixelsError::unsupported(
181 "TIFF planar configuration 2 is not implemented",
182 ));
183 }
184 let predictor = match directory.value_or(tag::PREDICTOR, 1) {
185 1 => false,
186 2 => true,
187 other => {
188 return Err(PixelsError::unsupported(format!(
189 "TIFF predictor {other} is not implemented"
190 )));
191 }
192 };
193 if directory.value_or(tag::SAMPLE_FORMAT, 1) != 1 {
194 return Err(PixelsError::unsupported(
195 "TIFF signed and float sample formats are not implemented",
196 ));
197 }
198
199 let (layout, offsets, byte_counts) = if directory.get(tag::TILE_OFFSETS).is_some() {
200 let tile_width = directory.require(tag::TILE_WIDTH, "TileWidth")?;
201 let tile_height = directory.require(tag::TILE_LENGTH, "TileLength")?;
202 if tile_width == 0 || tile_height == 0 {
208 return Err(PixelsError::malformed("tiff", "tile size is zero"));
209 }
210 if tile_width % 16 != 0 || tile_height % 16 != 0 {
211 return Err(PixelsError::malformed(
212 "tiff",
213 format!("tile size {tile_width}x{tile_height} is not a multiple of 16"),
214 ));
215 }
216 (
217 Layout::Tiles {
218 width: tile_width,
219 height: tile_height,
220 },
221 directory.values(tag::TILE_OFFSETS).to_vec(),
222 directory.values(tag::TILE_BYTE_COUNTS).to_vec(),
223 )
224 } else {
225 let rows_per_strip = directory.value_or(tag::ROWS_PER_STRIP, height).max(1);
228 (
229 Layout::Strips { rows_per_strip },
230 directory.values(tag::STRIP_OFFSETS).to_vec(),
231 directory.values(tag::STRIP_BYTE_COUNTS).to_vec(),
232 )
233 };
234
235 if offsets.is_empty() {
236 return Err(PixelsError::malformed(
237 "tiff",
238 "no strip or tile offsets; the pixels cannot be located",
239 ));
240 }
241 if byte_counts.len() < offsets.len() {
242 return Err(PixelsError::malformed(
243 "tiff",
244 format!(
245 "{} offsets but only {} byte counts",
246 offsets.len(),
247 byte_counts.len()
248 ),
249 ));
250 }
251
252 let pixel = output_format(photometric, bits_per_sample, samples_per_pixel)?;
253 let descriptor = ImageDescriptor::with_limits(width, height, pixel, limits)?;
255
256 Ok(Self {
257 descriptor,
258 bits_per_sample,
259 samples_per_pixel,
260 photometric,
261 compression,
262 layout,
263 offsets,
264 byte_counts,
265 predictor,
266 color_map: directory.values(tag::COLOR_MAP).to_vec(),
267 order,
268 orientation: u16::try_from(directory.value_or(tag::ORIENTATION, 1))
269 .ok()
270 .and_then(Orientation::from_exif)
271 .unwrap_or_default(),
272 icc: directory
273 .get(tag::ICC_PROFILE)
274 .map(|entry| entry.values.iter().map(|&v| v as u8).collect::<Vec<u8>>())
275 .filter(|profile| !profile.is_empty()),
276 })
277 }
278
279 #[must_use]
281 pub const fn chunks_across(&self) -> u32 {
282 match self.layout {
283 Layout::Strips { .. } => 1,
284 Layout::Tiles { width, .. } => self.descriptor.width.div_ceil(width),
285 }
286 }
287
288 #[must_use]
290 pub const fn chunks_down(&self) -> u32 {
291 match self.layout {
292 Layout::Strips { rows_per_strip } => self.descriptor.height.div_ceil(rows_per_strip),
293 Layout::Tiles { height, .. } => self.descriptor.height.div_ceil(height),
294 }
295 }
296
297 #[must_use]
303 pub fn chunk_region(&self, column: u32, row: u32) -> Region {
304 match self.layout {
305 Layout::Strips { rows_per_strip } => {
306 let y = row * rows_per_strip;
307 let height = rows_per_strip.min(self.descriptor.height.saturating_sub(y));
308 Region::new(0, y, self.descriptor.width, height)
309 }
310 Layout::Tiles { width, height } => {
311 let x = column * width;
312 let y = row * height;
313 Region::new(
314 x,
315 y,
316 width.min(self.descriptor.width.saturating_sub(x)),
317 height.min(self.descriptor.height.saturating_sub(y)),
318 )
319 }
320 }
321 }
322
323 #[must_use]
325 pub const fn chunk_stored_size(&self) -> (u32, u32) {
326 match self.layout {
327 Layout::Strips { rows_per_strip } => (self.descriptor.width, rows_per_strip),
328 Layout::Tiles { width, height } => (width, height),
329 }
330 }
331
332 #[must_use]
334 pub const fn chunk_row_bytes(&self) -> usize {
335 let (width, _) = self.chunk_stored_size();
336 let bits = width as usize * self.samples_per_pixel as usize * self.bits_per_sample as usize;
337 bits.div_ceil(8)
338 }
339
340 pub fn read_chunk(&self, data: &[u8], index: usize) -> Result<Vec<u8>> {
347 let offset = self.offsets.get(index).copied().unwrap_or(0) as usize;
348 let count = self.byte_counts.get(index).copied().unwrap_or(0) as usize;
349 let raw = data
350 .get(offset..offset.saturating_add(count))
351 .ok_or_else(|| {
352 PixelsError::malformed(
353 "tiff",
354 format!("chunk {index} at {offset}+{count} lies outside the file"),
355 )
356 })?;
357
358 let (_, stored_height) = self.chunk_stored_size();
359 let row_bytes = self.chunk_row_bytes();
360 let expected = row_bytes.saturating_mul(stored_height as usize);
363
364 let mut out = match self.compression {
365 Compression::None => raw.to_vec(),
366 Compression::Lzw => LzwDecoder::tiff()
367 .decode(raw, expected)
368 .map_err(crate::compress_error)?,
369 Compression::Deflate => {
370 match zlib_decompress(raw, expected) {
375 Ok(out) => out,
376 Err(_) => inflate_to(raw, expected).map_err(crate::compress_error)?,
377 }
378 }
379 Compression::PackBits => unpack_bits(raw, expected),
380 };
381
382 if self.predictor {
383 apply_predictor(&mut out, row_bytes, stored_height as usize, self);
384 }
385 Ok(out)
386 }
387}
388
389fn apply_predictor(data: &mut [u8], row_bytes: usize, rows: usize, image: &TiffImage) {
395 let channels = image.samples_per_pixel as usize;
396 match image.bits_per_sample {
397 8 => {
398 for row in 0..rows {
399 let start = row * row_bytes;
400 let Some(line) = data.get_mut(start..start + row_bytes) else {
401 break;
402 };
403 for index in channels..line.len() {
404 let previous = line.get(index - channels).copied().unwrap_or(0);
405 if let Some(slot) = line.get_mut(index) {
406 *slot = slot.wrapping_add(previous);
407 }
408 }
409 }
410 }
411 16 => {
412 let stride = channels * 2;
413 for row in 0..rows {
414 let start = row * row_bytes;
415 let Some(line) = data.get_mut(start..start + row_bytes) else {
416 break;
417 };
418 let mut index = stride;
419 while index + 1 < line.len() {
420 let previous = read16(line, index - stride, image.order);
421 let current = read16(line, index, image.order);
422 write16(line, index, current.wrapping_add(previous), image.order);
423 index += 2;
424 }
425 }
426 }
427 _ => {}
430 }
431}
432
433fn read16(data: &[u8], at: usize, order: ByteOrder) -> u16 {
434 order.u16(data, at)
435}
436
437fn write16(data: &mut [u8], at: usize, value: u16, order: ByteOrder) {
438 for (offset, byte) in order.write_u16(value).iter().enumerate() {
439 if let Some(slot) = data.get_mut(at + offset) {
440 *slot = *byte;
441 }
442 }
443}
444
445fn unpack_bits(data: &[u8], limit: usize) -> Vec<u8> {
452 let mut out = Vec::with_capacity(limit.min(1 << 20));
453 let mut at = 0;
454 while at < data.len() && out.len() < limit {
455 let header = data.get(at).copied().unwrap_or(0) as i8;
456 at += 1;
457 if header >= 0 {
458 let count = header as usize + 1;
459 let end = (at + count).min(data.len());
460 if let Some(run) = data.get(at..end) {
461 out.extend_from_slice(run);
462 }
463 at = end;
464 } else if header != -128 {
465 let count = (1 - i32::from(header)) as usize;
466 let Some(&byte) = data.get(at) else { break };
467 at += 1;
468 for _ in 0..count.min(limit.saturating_sub(out.len())) {
469 out.push(byte);
470 }
471 }
472 }
474 out
475}
476
477fn output_format(photometric: Photometric, bits: u32, samples: u32) -> Result<PixelFormat> {
479 let format = match (photometric, bits, samples) {
480 (Photometric::Palette, _, _) => PixelFormat::Rgb8,
484 (Photometric::WhiteIsZero | Photometric::BlackIsZero, 1 | 2 | 4 | 8, 1) => {
485 PixelFormat::Gray8
486 }
487 (Photometric::WhiteIsZero | Photometric::BlackIsZero, 16, 1) => PixelFormat::Gray16,
488 (Photometric::WhiteIsZero | Photometric::BlackIsZero, 8, 2) => PixelFormat::GrayA8,
489 (Photometric::Rgb, 8, 3) => PixelFormat::Rgb8,
490 (Photometric::Rgb, 8, 4) => PixelFormat::Rgba8,
491 (Photometric::Rgb, 16, 3) => PixelFormat::Rgb16,
492 (Photometric::Rgb, 16, 4) => PixelFormat::Rgba16,
493 _ => {
494 return Err(PixelsError::unsupported(format!(
495 "TIFF {photometric:?} with {samples} channels at {bits} bits is not implemented"
496 )));
497 }
498 };
499 debug_assert!(format.sample_kind() != SampleKind::F32);
500 Ok(format)
501}
502
503#[cfg(test)]
504#[allow(
505 clippy::unwrap_used,
506 clippy::expect_used,
507 clippy::indexing_slicing,
508 clippy::panic,
509 reason = "tests operate on known-good values and assert shapes directly"
510)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn packbits_decodes_the_specification_example() {
516 let encoded = [
519 0xFE_u8, 0xAA, 0x02, 0x80, 0x00, 0x2A, 0xFD, 0xAA, 0x03, 0x80, 0x00, 0x2A, 0x22, 0xF7,
520 0xAA,
521 ];
522 let expected = [
526 0xAA_u8, 0xAA, 0xAA, 0x80, 0x00, 0x2A, 0xAA, 0xAA, 0xAA, 0xAA, 0x80, 0x00, 0x2A, 0x22,
527 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
528 ];
529 assert_eq!(
530 expected.len(),
531 24,
532 "the specification's example is 24 bytes"
533 );
534 assert_eq!(unpack_bits(&encoded, 1000), expected);
535 }
536
537 #[test]
538 fn packbits_treats_minus_128_as_a_no_op() {
539 assert_eq!(unpack_bits(&[0x80, 0x00, 0x41], 100), vec![0x41]);
543 }
544
545 #[test]
546 fn packbits_never_exceeds_its_limit() {
547 let bomb = [0x81_u8, 0x55].repeat(10_000);
549 assert!(unpack_bits(&bomb, 512).len() <= 512);
550 }
551
552 #[test]
553 fn packbits_on_truncated_input_is_not_a_panic() {
554 for cut in 0..8 {
555 let _ = unpack_bits(&[0xFE, 0xAA, 0x02, 0x80][..cut.min(4)], 100);
556 }
557 assert!(unpack_bits(&[0x7F, 0x01, 0x02], 100).len() <= 3);
559 assert!(unpack_bits(&[0xFE], 100).is_empty());
561 }
562
563 #[test]
564 fn the_predictor_is_a_running_sum_along_each_row() {
565 let image = TiffImage {
566 descriptor: ImageDescriptor::new(4, 2, PixelFormat::Rgb8).unwrap(),
567 bits_per_sample: 8,
568 samples_per_pixel: 3,
569 photometric: Photometric::Rgb,
570 compression: Compression::None,
571 layout: Layout::Strips { rows_per_strip: 2 },
572 offsets: vec![0],
573 byte_counts: vec![24],
574 predictor: true,
575 color_map: Vec::new(),
576 order: ByteOrder::Little,
577 orientation: Orientation::Normal,
578 icc: None,
579 };
580 let mut data = vec![
582 10, 20, 30, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, ];
585 apply_predictor(&mut data, 12, 2, &image);
586 assert_eq!(
587 &data[..12],
588 [10, 20, 30, 11, 21, 31, 12, 22, 32, 13, 23, 33]
589 );
590 assert_eq!(&data[12..], [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]);
591 }
592
593 #[test]
594 fn unsupported_compression_and_photometrics_are_reported() {
595 assert!(Compression::from_tag(3).is_err(), "CCITT G3");
598 assert!(Compression::from_tag(7).is_err(), "new-style JPEG");
599 assert!(Photometric::from_tag(5).is_err(), "CMYK");
600 assert!(Photometric::from_tag(6).is_err(), "YCbCr");
601
602 assert_eq!(Compression::from_tag(1).unwrap(), Compression::None);
603 assert_eq!(Compression::from_tag(5).unwrap(), Compression::Lzw);
604 assert_eq!(Compression::from_tag(8).unwrap(), Compression::Deflate);
605 assert_eq!(
606 Compression::from_tag(32_946).unwrap(),
607 Compression::Deflate,
608 "Adobe's older deflate tag"
609 );
610 assert_eq!(
611 Compression::from_tag(32_773).unwrap(),
612 Compression::PackBits
613 );
614 }
615
616 #[test]
617 fn output_formats_follow_photometric_and_depth() {
618 assert_eq!(
619 output_format(Photometric::BlackIsZero, 8, 1).unwrap(),
620 PixelFormat::Gray8
621 );
622 assert_eq!(
623 output_format(Photometric::BlackIsZero, 1, 1).unwrap(),
624 PixelFormat::Gray8,
625 "bilevel expands to 8-bit grey"
626 );
627 assert_eq!(
628 output_format(Photometric::Rgb, 16, 4).unwrap(),
629 PixelFormat::Rgba16
630 );
631 assert_eq!(
632 output_format(Photometric::Palette, 8, 1).unwrap(),
633 PixelFormat::Rgb8,
634 "palette resolves to colour"
635 );
636 assert!(
637 output_format(Photometric::Rgb, 8, 5).is_err(),
638 "five channels"
639 );
640 }
641
642 #[test]
643 fn tile_regions_are_clipped_at_the_image_edge() {
644 let image = TiffImage {
645 descriptor: ImageDescriptor::new(100, 70, PixelFormat::Gray8).unwrap(),
646 bits_per_sample: 8,
647 samples_per_pixel: 1,
648 photometric: Photometric::BlackIsZero,
649 compression: Compression::None,
650 layout: Layout::Tiles {
651 width: 32,
652 height: 32,
653 },
654 offsets: vec![0; 12],
655 byte_counts: vec![1024; 12],
656 predictor: false,
657 color_map: Vec::new(),
658 order: ByteOrder::Little,
659 orientation: Orientation::Normal,
660 icc: None,
661 };
662 assert_eq!(image.chunks_across(), 4, "100 / 32 rounds up to 4");
663 assert_eq!(image.chunks_down(), 3, "70 / 32 rounds up to 3");
664
665 assert_eq!(image.chunk_region(0, 0), Region::new(0, 0, 32, 32));
667 assert_eq!(image.chunk_region(3, 0), Region::new(96, 0, 4, 32));
671 assert_eq!(image.chunk_region(0, 2), Region::new(0, 64, 32, 6));
672 assert_eq!(
673 image.chunk_stored_size(),
674 (32, 32),
675 "stored size is never clipped"
676 );
677 }
678
679 #[test]
680 fn strip_regions_span_the_full_width() {
681 let image = TiffImage {
682 descriptor: ImageDescriptor::new(50, 25, PixelFormat::Gray8).unwrap(),
683 bits_per_sample: 8,
684 samples_per_pixel: 1,
685 photometric: Photometric::BlackIsZero,
686 compression: Compression::None,
687 layout: Layout::Strips { rows_per_strip: 10 },
688 offsets: vec![0; 3],
689 byte_counts: vec![500; 3],
690 predictor: false,
691 color_map: Vec::new(),
692 order: ByteOrder::Little,
693 orientation: Orientation::Normal,
694 icc: None,
695 };
696 assert_eq!(image.chunks_across(), 1);
697 assert_eq!(image.chunks_down(), 3);
698 assert_eq!(image.chunk_region(0, 0), Region::new(0, 0, 50, 10));
699 assert_eq!(
700 image.chunk_region(0, 2),
701 Region::new(0, 20, 50, 5),
702 "the last strip is short"
703 );
704 assert!(!image.layout.is_random_access());
705 }
706}