Skip to main content

gamut_tiff/
decoder.rs

1//! The TIFF decoder.
2
3use gamut_core::{Cmyk8, DecodeImage, Dimensions, Error, Gray8, ImageBuf, Result, Rgb8, Rgba8};
4
5use crate::compression::{Compression, ccitt, lzw, packbits, predictor};
6use crate::ifd::PhotometricInterpretation;
7use crate::palette::Palette8;
8use crate::tags;
9use gamut_ifd::{Ifd, read};
10
11/// Decoder for baseline TIFF images.
12///
13/// Reads chunky images compressed with `None` or PackBits: 8-bit grayscale/RGB, 1-bit bilevel,
14/// and 8-bit palette colour. Other compressions and colour modes return [`Error::Unsupported`]
15/// until their phases land.
16#[derive(Debug, Clone, Default)]
17pub struct TiffDecoder {
18    _private: (),
19}
20
21/// Upper bound on a decoded image's stored bytes, guarding against malformed huge dimensions and
22/// decompression bombs (64 MiB — e.g. a 4096×4096 RGBA image).
23const MAX_IMAGE_BYTES: usize = 64 << 20;
24
25/// An image decoded to interleaved 8-bit samples in `BlackIsZero`/RGB convention.
26struct DecodedImage {
27    dims: Dimensions,
28    samples_per_pixel: usize,
29    pixels: Vec<u8>,
30}
31
32/// How a decoded image's stored samples map to output pixels.
33enum Mode {
34    /// Grayscale; `white_is_zero` selects which sample value is white.
35    Gray { white_is_zero: bool },
36    /// Interleaved RGB.
37    Rgb,
38    /// Interleaved RGBA (RGB + one extra alpha sample).
39    Rgba,
40    /// Interleaved CMYK (4 separated ink samples).
41    Cmyk,
42    /// Palette colour: 8-bit indices into a [`Palette8`] colour table. Boxed because the 768-byte
43    /// table would otherwise dwarf the other variants.
44    Palette(Box<Palette8>),
45}
46
47impl TiffDecoder {
48    /// Creates a decoder.
49    #[must_use]
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Returns the number of pages (subfile IFDs) in a TIFF.
55    ///
56    /// # Errors
57    ///
58    /// Returns [`Error::InvalidInput`] if the file header or IFD chain is malformed.
59    pub fn page_count(&self, data: &[u8]) -> Result<usize> {
60        Ok(read(data)?.ifds.len())
61    }
62
63    /// Decodes page `page` of a multi-page TIFF to interleaved 8-bit [`Rgb8`] (page 0 is the first;
64    /// grayscale is replicated across channels, any alpha is dropped). Multi-page access is
65    /// TIFF-specific, so it stays inherent; the [`DecodeImage`] impls present page 0.
66    ///
67    /// # Errors
68    ///
69    /// Returns [`Error::InvalidInput`] for malformed input or an out-of-range page, or
70    /// [`Error::Unsupported`] for a feature not yet implemented.
71    pub fn decode_page(&self, data: &[u8], page: usize) -> Result<ImageBuf<Rgb8>> {
72        let img = decode_page_samples(data, page)?;
73        ImageBuf::new(present_rgb(&img)?, img.dims)
74    }
75}
76
77impl DecodeImage<Rgb8> for TiffDecoder {
78    /// Grayscale is replicated across channels; any alpha is dropped.
79    fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<Rgb8>> {
80        self.decode_page(data, 0)
81    }
82}
83
84impl DecodeImage<Rgba8> for TiffDecoder {
85    /// RGB gains opaque alpha; grayscale is replicated then made opaque.
86    fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<Rgba8>> {
87        let img = decode_page_samples(data, 0)?;
88        ImageBuf::new(present_rgba(&img)?, img.dims)
89    }
90}
91
92impl DecodeImage<Cmyk8> for TiffDecoder {
93    /// Errors unless the image is 4-sample; the samples pass through unchanged.
94    fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<Cmyk8>> {
95        let img = decode_page_samples(data, 0)?;
96        if img.samples_per_pixel != 4 {
97            return Err(Error::Unsupported("TIFF: image is not 4-sample CMYK"));
98        }
99        ImageBuf::new(img.pixels, img.dims)
100    }
101}
102
103impl DecodeImage<Gray8> for TiffDecoder {
104    /// Errors unless the image is single-sample; the samples pass through unchanged.
105    fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<Gray8>> {
106        let img = decode_page_samples(data, 0)?;
107        if img.samples_per_pixel != 1 {
108            return Err(Error::Unsupported("TIFF: image is not grayscale"));
109        }
110        ImageBuf::new(img.pixels, img.dims)
111    }
112}
113
114/// Presents decoded samples as interleaved 8-bit RGB (1 → replicated, 3 → as-is, 4 → alpha dropped).
115fn present_rgb(img: &DecodedImage) -> Result<Vec<u8>> {
116    let mut out = Vec::with_capacity(img.dims.width as usize * img.dims.height as usize * 3);
117    match img.samples_per_pixel {
118        1 => {
119            for &v in &img.pixels {
120                out.extend_from_slice(&[v, v, v]);
121            }
122        }
123        3 => out.extend_from_slice(&img.pixels),
124        4 => {
125            for px in img.pixels.chunks_exact(4) {
126                out.extend_from_slice(&px[0..3]);
127            }
128        }
129        _ => {
130            return Err(Error::Unsupported(
131                "TIFF: cannot present this sample layout as RGB",
132            ));
133        }
134    }
135    Ok(out)
136}
137
138/// Presents decoded samples as interleaved 8-bit RGBA (1 → replicated opaque, 3 → opaque, 4 → as-is).
139fn present_rgba(img: &DecodedImage) -> Result<Vec<u8>> {
140    let mut out = Vec::with_capacity(img.dims.width as usize * img.dims.height as usize * 4);
141    match img.samples_per_pixel {
142        1 => {
143            for &v in &img.pixels {
144                out.extend_from_slice(&[v, v, v, 255]);
145            }
146        }
147        3 => {
148            for px in img.pixels.chunks_exact(3) {
149                out.extend_from_slice(&[px[0], px[1], px[2], 255]);
150            }
151        }
152        4 => out.extend_from_slice(&img.pixels),
153        _ => {
154            return Err(Error::Unsupported(
155                "TIFF: cannot present this sample layout as RGBA",
156            ));
157        }
158    }
159    Ok(out)
160}
161
162/// Reads a required unsigned-integer tag.
163fn require_u32(ifd: &Ifd, tag: u16, what: &'static str) -> Result<u32> {
164    ifd.get_u32(tag).ok_or(Error::InvalidInput(what))
165}
166
167fn decode_page_samples(data: &[u8], page: usize) -> Result<DecodedImage> {
168    let file = read(data)?;
169    let ifd = file
170        .ifds
171        .get(page)
172        .ok_or(Error::InvalidInput("TIFF: page index out of range"))?;
173
174    let width = require_u32(ifd, tags::IMAGE_WIDTH, "TIFF: missing ImageWidth")? as usize;
175    let height = require_u32(ifd, tags::IMAGE_LENGTH, "TIFF: missing ImageLength")? as usize;
176    if width == 0 || height == 0 {
177        return Err(Error::InvalidInput("TIFF: zero-sized image"));
178    }
179
180    let compression = Compression::from_code(ifd.get_u32(tags::COMPRESSION).unwrap_or(1))
181        .ok_or(Error::Unsupported("TIFF: unknown compression"))?;
182    if !matches!(
183        compression,
184        Compression::None
185            | Compression::PackBits
186            | Compression::CcittRle
187            | Compression::CcittGroup4Fax
188            | Compression::Lzw
189    ) {
190        return Err(Error::Unsupported("TIFF: compression not supported yet"));
191    }
192    if ifd.get_u32(tags::PLANAR_CONFIGURATION).unwrap_or(1) != 1 {
193        return Err(Error::Unsupported(
194            "TIFF: planar configuration not supported yet",
195        ));
196    }
197
198    if ifd.get_u32(tags::FILL_ORDER).unwrap_or(1) != 1 {
199        return Err(Error::Unsupported("TIFF: FillOrder 2 not supported"));
200    }
201    let spp = ifd.get_u32(tags::SAMPLES_PER_PIXEL).unwrap_or(1) as usize;
202    let bits = ifd
203        .get_u32_vec(tags::BITS_PER_SAMPLE)
204        .unwrap_or_else(|| vec![1; spp]);
205    if bits.len() != spp || bits.iter().any(|&b| b != bits[0]) {
206        return Err(Error::Unsupported("TIFF: mixed bit depths not supported"));
207    }
208    let bps = bits[0];
209    if matches!(
210        compression,
211        Compression::CcittRle | Compression::CcittGroup4Fax
212    ) && bps != 1
213    {
214        return Err(Error::Unsupported(
215            "TIFF: CCITT coding requires a bilevel image",
216        ));
217    }
218    let use_predictor = match ifd.get_u32(tags::PREDICTOR).unwrap_or(1) {
219        1 => false,
220        2 => true,
221        _ => return Err(Error::Unsupported("TIFF: unknown predictor")),
222    };
223    if use_predictor && bps != 8 {
224        return Err(Error::Unsupported("TIFF: predictor requires 8-bit samples"));
225    }
226
227    let photometric = PhotometricInterpretation::from_code(require_u32(
228        ifd,
229        tags::PHOTOMETRIC_INTERPRETATION,
230        "TIFF: missing PhotometricInterpretation",
231    )?)
232    .ok_or(Error::Unsupported(
233        "TIFF: unknown photometric interpretation",
234    ))?;
235    // How stored samples become the decoded output (TIFF 6.0 §8 PhotometricInterpretation).
236    let mode = match (spp, bps, photometric) {
237        (1, 1 | 8, PhotometricInterpretation::WhiteIsZero) => Mode::Gray {
238            white_is_zero: true,
239        },
240        (1, 1 | 8, PhotometricInterpretation::BlackIsZero) => Mode::Gray {
241            white_is_zero: false,
242        },
243        (3, 8, PhotometricInterpretation::Rgb) => Mode::Rgb,
244        (4, 8, PhotometricInterpretation::Rgb) => Mode::Rgba,
245        (4, 8, PhotometricInterpretation::Cmyk) => Mode::Cmyk,
246        (1, 8, PhotometricInterpretation::Palette) => {
247            let cm = ifd
248                .get_u32_vec(tags::COLOR_MAP)
249                .ok_or(Error::InvalidInput("TIFF: palette image missing ColorMap"))?;
250            Mode::Palette(Box::new(Palette8::from_tiff_colormap(&cm)?))
251        }
252        _ => {
253            return Err(Error::Unsupported(
254                "TIFF: photometric/sample combination not supported yet",
255            ));
256        }
257    };
258
259    // Bytes of one stored (packed) row, before unpacking to 8-bit output samples.
260    let stored_row_bytes = match bps {
261        8 => width
262            .checked_mul(spp)
263            .ok_or(Error::InvalidInput("TIFF: image too large"))?,
264        1 => width.div_ceil(8), // spp == 1, guaranteed by the match above
265        _ => {
266            return Err(Error::Unsupported(
267                "TIFF: only 1- and 8-bit samples supported so far",
268            ));
269        }
270    };
271    let stored_total = stored_row_bytes
272        .checked_mul(height)
273        .ok_or(Error::InvalidInput("TIFF: image too large"))?;
274    if stored_total > MAX_IMAGE_BYTES {
275        return Err(Error::Unsupported("TIFF: image exceeds the size limit"));
276    }
277
278    // Reassemble the stored (packed) row bytes from tiles or strips.
279    let layout = Layout {
280        width,
281        height,
282        spp,
283        bps,
284        stored_row_bytes,
285        compression,
286    };
287    let mut packed = if ifd.get(tags::TILE_WIDTH).is_some() {
288        decode_tiles(ifd, data, &layout)?
289    } else {
290        decode_strips(ifd, data, &layout)?
291    };
292    debug_assert_eq!(packed.len(), stored_total);
293
294    // Reverse the horizontal-differencing predictor (8-bit only) before unpacking.
295    if use_predictor {
296        predictor::reverse(&mut packed, stored_row_bytes, spp);
297    }
298
299    // Unpack the stored bytes into 8-bit output samples per the photometric mode.
300    let (out_spp, pixels) = match mode {
301        Mode::Rgb => (3, packed),
302        Mode::Rgba | Mode::Cmyk => (4, packed),
303        Mode::Gray { white_is_zero } if bps == 8 => {
304            let mut px = packed;
305            if white_is_zero {
306                for v in &mut px {
307                    *v = 255 - *v;
308                }
309            }
310            (1, px)
311        }
312        Mode::Gray { white_is_zero } => {
313            // bps == 1: expand each MSB-first bit to a 0/255 sample.
314            let mut px = Vec::with_capacity(width * height);
315            for y in 0..height {
316                let row = &packed[y * stored_row_bytes..(y + 1) * stored_row_bytes];
317                for x in 0..width {
318                    let bit = (row[x / 8] >> (7 - (x % 8))) & 1;
319                    let white = if white_is_zero { bit == 0 } else { bit == 1 };
320                    px.push(if white { 255 } else { 0 });
321                }
322            }
323            (1, px)
324        }
325        Mode::Palette(palette) => {
326            // Each 8-bit index selects an RGB triple from the colour table.
327            let mut px = Vec::with_capacity(width * height * 3);
328            for &idx in &packed {
329                px.extend_from_slice(&palette.entry(idx));
330            }
331            (3, px)
332        }
333    };
334
335    Ok(DecodedImage {
336        dims: Dimensions {
337            width: width as u32,
338            height: height as u32,
339        },
340        samples_per_pixel: out_spp,
341        pixels,
342    })
343}
344
345/// The decoded image's storage parameters, shared by the strip and tile readers.
346struct Layout {
347    width: usize,
348    height: usize,
349    spp: usize,
350    bps: u32,
351    stored_row_bytes: usize,
352    compression: Compression,
353}
354
355/// Decompresses one strip/tile of byte-level data (`None`/PackBits/LZW) to `want` bytes.
356fn decompress_simple(raw: &[u8], want: usize, compression: Compression) -> Result<Vec<u8>> {
357    match compression {
358        Compression::None => raw
359            .get(..want)
360            .map(<[u8]>::to_vec)
361            .ok_or(Error::InvalidInput("TIFF: block shorter than expected")),
362        Compression::PackBits => packbits::decode(raw, want),
363        Compression::Lzw => lzw::decode(raw, want),
364        _ => Err(Error::Unsupported(
365            "TIFF: compression not supported for this layout",
366        )),
367    }
368}
369
370/// Reassembles the stored row bytes from strips.
371fn decode_strips(ifd: &Ifd, data: &[u8], l: &Layout) -> Result<Vec<u8>> {
372    let rows_per_strip = match ifd.get_u32(tags::ROWS_PER_STRIP) {
373        Some(0) | None => l.height,
374        Some(r) => (r as usize).min(l.height),
375    };
376    let offsets = ifd
377        .get_u32_vec(tags::STRIP_OFFSETS)
378        .ok_or(Error::InvalidInput("TIFF: missing StripOffsets"))?;
379    let counts = ifd
380        .get_u32_vec(tags::STRIP_BYTE_COUNTS)
381        .ok_or(Error::InvalidInput("TIFF: missing StripByteCounts"))?;
382    let strips = l.height.div_ceil(rows_per_strip);
383    if offsets.len() != strips || counts.len() != strips {
384        return Err(Error::InvalidInput("TIFF: strip count mismatch"));
385    }
386    let mut packed = Vec::with_capacity(l.stored_row_bytes * l.height);
387    for (i, (&off, &cnt)) in offsets.iter().zip(&counts).enumerate() {
388        let rows = rows_per_strip.min(l.height - i * rows_per_strip);
389        let want = rows * l.stored_row_bytes;
390        let raw = data
391            .get(off as usize..off as usize + cnt as usize)
392            .ok_or(Error::InvalidInput("TIFF: strip out of bounds"))?;
393        match l.compression {
394            Compression::CcittRle => {
395                packed.extend_from_slice(&ccitt::mh_decode_strip(raw, rows, l.width)?);
396            }
397            Compression::CcittGroup4Fax => {
398                packed.extend_from_slice(&ccitt::g4_decode_strip(raw, rows, l.width)?);
399            }
400            other => packed.extend_from_slice(&decompress_simple(raw, want, other)?),
401        }
402    }
403    Ok(packed)
404}
405
406/// Reassembles the stored row bytes from tiles (8-bit only), cropping the edge-tile padding.
407fn decode_tiles(ifd: &Ifd, data: &[u8], l: &Layout) -> Result<Vec<u8>> {
408    if l.bps != 8 {
409        return Err(Error::Unsupported(
410            "TIFF: tiled images supported only for 8-bit samples so far",
411        ));
412    }
413    let tw = ifd
414        .get_u32(tags::TILE_WIDTH)
415        .ok_or(Error::InvalidInput("TIFF: missing TileWidth"))? as usize;
416    let th = ifd
417        .get_u32(tags::TILE_LENGTH)
418        .ok_or(Error::InvalidInput("TIFF: missing TileLength"))? as usize;
419    if tw == 0 || th == 0 {
420        return Err(Error::InvalidInput("TIFF: zero tile dimension"));
421    }
422    let offsets = ifd
423        .get_u32_vec(tags::TILE_OFFSETS)
424        .ok_or(Error::InvalidInput("TIFF: missing TileOffsets"))?;
425    let counts = ifd
426        .get_u32_vec(tags::TILE_BYTE_COUNTS)
427        .ok_or(Error::InvalidInput("TIFF: missing TileByteCounts"))?;
428    let across = l.width.div_ceil(tw);
429    let down = l.height.div_ceil(th);
430    if offsets.len() != across * down || counts.len() != across * down {
431        return Err(Error::InvalidInput("TIFF: tile count mismatch"));
432    }
433    let tile_row_bytes = tw
434        .checked_mul(l.spp)
435        .ok_or(Error::InvalidInput("TIFF: tile too large"))?;
436    let tile_size = th
437        .checked_mul(tile_row_bytes)
438        .ok_or(Error::InvalidInput("TIFF: tile too large"))?;
439    if tile_size > MAX_IMAGE_BYTES {
440        return Err(Error::Unsupported("TIFF: tile exceeds the size limit"));
441    }
442    let mut packed = vec![0u8; l.stored_row_bytes * l.height];
443    for ty in 0..down {
444        for tx in 0..across {
445            let idx = ty * across + tx;
446            let (off, cnt) = (offsets[idx] as usize, counts[idx] as usize);
447            let raw = data
448                .get(off..off + cnt)
449                .ok_or(Error::InvalidInput("TIFF: tile out of bounds"))?;
450            let tile = decompress_simple(raw, tile_size, l.compression)?;
451            let copy_cols = tw.min(l.width - tx * tw);
452            for r in 0..th {
453                let dst_row = ty * th + r;
454                if dst_row >= l.height {
455                    break;
456                }
457                let src = r * tile_row_bytes;
458                let dst = dst_row * l.stored_row_bytes + tx * tw * l.spp;
459                packed[dst..dst + copy_cols * l.spp]
460                    .copy_from_slice(&tile[src..src + copy_cols * l.spp]);
461            }
462        }
463    }
464    Ok(packed)
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use crate::encoder::TiffEncoder;
471    use gamut_core::{EncodeImage, ImageRef};
472    use gamut_ifd::ByteOrder;
473
474    #[test]
475    fn rejects_truncated_file() {
476        let dec = TiffDecoder::new();
477        let got: Result<ImageBuf<Rgb8>> = dec.decode_image(&[]);
478        assert!(got.is_err());
479    }
480
481    #[test]
482    fn gray_roundtrips_both_orders() {
483        for order in [ByteOrder::LittleEndian, ByteOrder::BigEndian] {
484            let dims = Dimensions {
485                width: 5,
486                height: 3,
487            };
488            let pixels: Vec<u8> = (0..15).collect();
489            let mut tiff = Vec::new();
490            TiffEncoder::new()
491                .with_byte_order(order)
492                .encode_image(ImageRef::<Gray8>::new(&pixels, dims).unwrap(), &mut tiff)
493                .expect("encode");
494            let got: ImageBuf<Gray8> = TiffDecoder::new().decode_image(&tiff).expect("decode");
495            assert_eq!(got.dimensions(), dims);
496            assert_eq!(got.as_samples(), pixels.as_slice());
497        }
498    }
499}