Skip to main content

otf_pixels_codec_tiff/
image.rs

1//! The image a TIFF directory describes: layout, compression and pixel format.
2//!
3//! Separated from [`Directory`] on purpose: that module knows about tags, this
4//! one knows what tags *mean*. The split is what keeps "skip an unknown tag"
5//! and "reject an unsupported layout" different decisions.
6//!
7//! [`Directory`]: crate::Directory
8
9use 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/// How pixel data is compressed (TIFF 6.0 §Section 8, plus §Deflate).
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum Compression {
19    /// Stored uncompressed.
20    None,
21    /// LZW, TIFF's dialect.
22    Lzw,
23    /// The Deflate extension, both the official and the old Adobe tag.
24    Deflate,
25    /// PackBits run-length encoding, which baseline TIFF requires.
26    PackBits,
27}
28
29impl Compression {
30    /// The scheme for a Compression tag value.
31    ///
32    /// # Errors
33    ///
34    /// Returns [`PixelsError::Unsupported`] for a scheme this codec does not
35    /// implement — CCITT fax and old-style JPEG, principally. That is an
36    /// unsupported *layout*, not an exotic tag, so it is reported rather than
37    /// skipped: the pixels cannot be produced without it.
38    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/// How samples are interpreted.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub enum Photometric {
54    /// Zero is white — an inverted greyscale, common in scanned documents.
55    WhiteIsZero,
56    /// Zero is black.
57    BlackIsZero,
58    /// Red, green, blue.
59    Rgb,
60    /// Palette indices, resolved through the colour map.
61    Palette,
62}
63
64impl Photometric {
65    /// The interpretation for a Photometric tag value.
66    ///
67    /// # Errors
68    ///
69    /// Returns [`PixelsError::Unsupported`] for CMYK, YCbCr and the rest.
70    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/// Whether the pixels are stored in strips or tiles.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85pub enum Layout {
86    /// Horizontal bands the full width of the image.
87    Strips {
88        /// Rows in each strip but the last.
89        rows_per_strip: u32,
90    },
91    /// A grid of independently compressed rectangles.
92    ///
93    /// This is the layout that makes region random access possible, and
94    /// therefore the one M5's exit criterion turns on.
95    Tiles {
96        /// Tile width in pixels; always a multiple of 16.
97        width: u32,
98        /// Tile height in pixels; always a multiple of 16.
99        height: u32,
100    },
101}
102
103impl Layout {
104    /// Whether this layout supports decoding an arbitrary region cheaply.
105    #[must_use]
106    pub const fn is_random_access(self) -> bool {
107        matches!(self, Self::Tiles { .. })
108    }
109}
110
111/// Everything about a TIFF image that decoding needs.
112#[derive(Debug, Clone)]
113#[non_exhaustive]
114pub struct TiffImage {
115    /// The engine-facing descriptor.
116    pub descriptor: ImageDescriptor,
117    /// Bits in each stored sample.
118    pub bits_per_sample: u32,
119    /// Channels per stored pixel.
120    pub samples_per_pixel: u32,
121    /// How samples are interpreted.
122    pub photometric: Photometric,
123    /// How pixel data is compressed.
124    pub compression: Compression,
125    /// Strips or tiles.
126    pub layout: Layout,
127    /// Byte offset of each chunk.
128    pub offsets: Vec<u32>,
129    /// Compressed byte count of each chunk.
130    pub byte_counts: Vec<u32>,
131    /// Whether a horizontal differencing predictor was applied.
132    pub predictor: bool,
133    /// The colour map, for palette images: 3 * 2^bits 16-bit values.
134    pub color_map: Vec<u32>,
135    /// The byte order the file declared.
136    pub order: ByteOrder,
137    /// The `Orientation` tag; [`Orientation::Normal`] when absent or out of
138    /// range, which is metadata declined rather than an image refused.
139    pub orientation: Orientation,
140    /// The embedded ICC profile, if the directory carries one.
141    pub icc: Option<Vec<u8>>,
142}
143
144impl TiffImage {
145    /// Interpret a directory as an image.
146    ///
147    /// # Errors
148    ///
149    /// Returns [`PixelsError::Malformed`] for a directory missing tags the
150    /// pixels cannot be located without, [`PixelsError::Unsupported`] for a
151    /// layout this codec does not implement, or [`PixelsError::LimitExceeded`]
152    /// if the dimensions exceed `limits`.
153    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        // Mixed bit depths per channel are legal and vanishingly rare, and
165        // supporting them would complicate every unpack path. Rejecting is
166        // honest; silently using the first depth would corrupt the pixels.
167        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        // Planar (channel-separated) storage is legal but rare, and it changes
178        // every unpack path rather than adding one.
179        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            // TIFF 6.0 §Section 15 requires tile dimensions to be multiples
203            // of 16. Enforcing it is not pedantry: a corrupt TileWidth of 1
204            // turns a modest image into hundreds of thousands of chunks, each
205            // needing its own decompression, which is a CPU exhaustion vector
206            // reachable by flipping one bit.
207            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            // A missing RowsPerStrip means the whole image is one strip, which
226            // the specification states as the default of 2^32-1.
227            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        // Enforced before any buffer exists (SPEC §Safety).
254        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    /// The number of chunks (strips or tiles) across the image.
280    #[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    /// The number of chunks down the image.
289    #[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    /// The region of the image chunk `(column, row)` covers.
298    ///
299    /// Tiles are always whole even at the image edge — the specification pads
300    /// them — so the region is clipped for the caller's benefit rather than
301    /// describing what is stored.
302    #[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    /// The stored dimensions of chunk `(column, row)`, including any padding.
324    #[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    /// Bytes one stored row of a chunk occupies.
333    #[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    /// Decompress chunk `index` from `data`, returning stored samples.
341    ///
342    /// # Errors
343    ///
344    /// Returns [`PixelsError::Malformed`] if the chunk lies outside the file
345    /// or decompresses to something other than its declared size.
346    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        // The exact expected size is what makes a decompression bomb a
361        // malformed-input error rather than an allocation.
362        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                // The official tag (8) is zlib-wrapped; Adobe's older 32946 is
371                // the same in every file anyone has produced. Falling back to
372                // raw deflate covers the handful of writers that omit the
373                // wrapper rather than rejecting their files.
374                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
389/// Reverse horizontal differencing (TIFF §Predictor).
390///
391/// Each sample is stored as its difference from the sample one pixel to the
392/// left, which makes smooth gradients compress far better. Undoing it is a
393/// running sum along each row, per channel.
394fn 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        // The predictor is defined for 8- and 16-bit samples only; anything
428        // else is left alone rather than corrupted by a guess.
429        _ => {}
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
445/// Decode PackBits run-length encoding (TIFF §Section 9).
446///
447/// A length byte read as `i8`: 0..=127 means that many literals plus one,
448/// -1..=-127 means the next byte repeated `1 - n` times, -128 is a no-op. The
449/// signed reading is the whole trick, and reading it unsigned produces
450/// plausible garbage rather than an error.
451fn 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        // -128 is explicitly a no-op, which is why it is not an error.
473    }
474    out
475}
476
477/// The engine pixel format a TIFF's photometric and depth imply.
478fn output_format(photometric: Photometric, bits: u32, samples: u32) -> Result<PixelFormat> {
479    let format = match (photometric, bits, samples) {
480        // Palette images resolve through the colour map, which is 16-bit, but
481        // 8-bit output is what every consumer wants and loses nothing that a
482        // 256-entry table can express.
483        (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        // TIFF 6.0 §Section 9's own example, which is the only authority on
517        // the signed reading of the length byte.
518        let encoded = [
519            0xFE_u8, 0xAA, 0x02, 0x80, 0x00, 0x2A, 0xFD, 0xAA, 0x03, 0x80, 0x00, 0x2A, 0x22, 0xF7,
520            0xAA,
521        ];
522        // Twenty-four bytes: the final 0xF7 is -9, meaning ten repeats, not
523        // nine. That off-by-one is the whole reason the length byte's signed
524        // reading has to be exact.
525        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        // The specification is explicit that -128 means "do nothing", not
540        // "repeat 129 times", and getting it wrong corrupts every file that
541        // happens to contain the byte.
542        assert_eq!(unpack_bits(&[0x80, 0x00, 0x41], 100), vec![0x41]);
543    }
544
545    #[test]
546    fn packbits_never_exceeds_its_limit() {
547        // A run byte can claim 128 repeats; a stream of them is a bomb.
548        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        // A literal run claiming more bytes than remain.
558        assert!(unpack_bits(&[0x7F, 0x01, 0x02], 100).len() <= 3);
559        // A repeat run with no byte to repeat.
560        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        // Row of four RGB pixels, stored as differences from the left.
581        let mut data = vec![
582            10, 20, 30, 1, 1, 1, 1, 1, 1, 1, 1, 1, // row 0
583            5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, // row 1
584        ];
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        // CCITT fax and CMYK are unsupported *layouts*, not exotic tags: the
596        // pixels cannot be produced without them, so they are errors.
597        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        // Interior tiles are whole.
666        assert_eq!(image.chunk_region(0, 0), Region::new(0, 0, 32, 32));
667        // Edge tiles are clipped for the caller even though the stored tile is
668        // padded to full size — conflating the two is how a decoder writes
669        // padding into the image.
670        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}