Skip to main content

gamut_tiff/
encoder.rs

1//! The TIFF encoder.
2
3use gamut_core::{
4    Bilevel, Cmyk8, Dimensions, EncodeImage, Error, Gray8, ImageRef, Indexed8, Result, Rgb8, Rgba8,
5};
6
7use crate::compression::{Compression, ccitt, lzw, packbits, predictor};
8use crate::ifd::{PhotometricInterpretation, Predictor};
9use crate::palette::Palette8;
10use crate::{tags, writer};
11use gamut_ifd::{ByteOrder, Ifd, Value, Variant};
12
13/// The on-disk sample layout of an image, shared by the 8-bit and bilevel encode paths.
14struct SampleLayout {
15    spp: usize,
16    bits_per_sample: u16,
17    stored_row_bytes: usize,
18    photometric: PhotometricInterpretation,
19}
20
21/// Encoder for baseline TIFF images.
22///
23/// Writes chunky (`PlanarConfiguration = 1`) strips, optionally PackBits-compressed
24/// ([`Self::with_compression`]). Supports 8-bit grayscale/RGB and 1-bit bilevel; richer colour
25/// modes and compression schemes are added in later phases. Emits classic TIFF by default, or
26/// BigTIFF (64-bit offsets) when [`Self::with_big_tiff`] is set.
27#[derive(Debug, Clone)]
28pub struct TiffEncoder {
29    order: ByteOrder,
30    compression: Compression,
31    predictor: Predictor,
32    tiling: Option<(u32, u32)>,
33    big_tiff: bool,
34}
35
36impl Default for TiffEncoder {
37    fn default() -> Self {
38        Self {
39            order: ByteOrder::LittleEndian,
40            compression: Compression::None,
41            predictor: Predictor::None,
42            tiling: None,
43            big_tiff: false,
44        }
45    }
46}
47
48impl TiffEncoder {
49    /// Creates an encoder that writes little-endian (`II`) TIFF.
50    #[must_use]
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Returns a copy of this encoder that writes in the given byte order.
56    #[must_use]
57    pub fn with_byte_order(mut self, order: ByteOrder) -> Self {
58        self.order = order;
59        self
60    }
61
62    /// Returns a copy of this encoder that compresses image data with `compression`.
63    #[must_use]
64    pub fn with_compression(mut self, compression: Compression) -> Self {
65        self.compression = compression;
66        self
67    }
68
69    /// Returns a copy of this encoder that applies `predictor` before compression.
70    ///
71    /// [`Predictor::HorizontalDifferencing`] requires 8-bit samples and pairs well with LZW.
72    #[must_use]
73    pub fn with_predictor(mut self, predictor: Predictor) -> Self {
74        self.predictor = predictor;
75        self
76    }
77
78    /// Returns a copy of this encoder that writes the image as tiles of `tile_width × tile_height`
79    /// pixels instead of strips.
80    ///
81    /// Both dimensions must be positive multiples of 16. Tiling is currently supported for 8-bit
82    /// images compressed with `None`/PackBits/LZW (no predictor).
83    #[must_use]
84    pub fn with_tiling(mut self, tile_width: u32, tile_height: u32) -> Self {
85        self.tiling = Some((tile_width, tile_height));
86        self
87    }
88
89    /// Returns a copy of this encoder that writes BigTIFF (magic `43`, 64-bit offsets) instead of
90    /// classic TIFF.
91    ///
92    /// BigTIFF only widens the container's structural fields; every colour mode, compression
93    /// scheme, strip/tile layout, and multi-page feature applies unchanged, so this composes with
94    /// the other builders. Its 64-bit offsets let a file exceed the 4 GiB classic limit. A reader
95    /// detects the variant from the header magic, so no decoder flag is needed. Defaults to off.
96    #[must_use]
97    pub fn with_big_tiff(mut self, big_tiff: bool) -> Self {
98        self.big_tiff = big_tiff;
99        self
100    }
101
102    /// The container variant this encoder writes (BigTIFF when [`Self::with_big_tiff`] is set).
103    fn variant(&self) -> Variant {
104        if self.big_tiff {
105            Variant::Big
106        } else {
107            Variant::Classic
108        }
109    }
110
111    /// Encodes an 8-bit palette-colour image: one [`Indexed8`] sample per pixel selecting an entry
112    /// of `palette`.
113    ///
114    /// `indices` is the `width * height` index buffer (already validated by [`ImageRef`]); `palette`
115    /// is the 256-entry colour table. Returns the number of bytes written. Palette colour does not
116    /// fit the single-buffer [`EncodeImage`] shape (it needs the separate colour table), so it stays
117    /// an inherent method.
118    pub fn encode_palette8(
119        &self,
120        indices: ImageRef<'_, Indexed8>,
121        palette: &Palette8,
122        out: &mut Vec<u8>,
123    ) -> Result<usize> {
124        let w = indices.width() as usize;
125        let colormap = palette.to_tiff_colormap();
126        self.encode_packed(
127            indices.as_samples(),
128            indices.dimensions(),
129            &SampleLayout {
130                spp: 1,
131                bits_per_sample: 8,
132                stored_row_bytes: w,
133                photometric: PhotometricInterpretation::Palette,
134            },
135            &[(tags::COLOR_MAP, Value::Short(colormap))],
136            out,
137        )
138    }
139
140    fn encode_8bit(
141        &self,
142        pixels: &[u8],
143        dims: Dimensions,
144        spp: usize,
145        photometric: PhotometricInterpretation,
146        out: &mut Vec<u8>,
147    ) -> Result<usize> {
148        // The caller is an EncodeImage impl handing us an ImageRef-validated buffer, so
149        // pixels.len() == width * height * spp holds and the product cannot overflow.
150        let row_bytes = dims.width as usize * spp;
151        debug_assert_eq!(pixels.len(), row_bytes * dims.height as usize);
152        self.encode_packed(
153            pixels,
154            dims,
155            &SampleLayout {
156                spp,
157                bits_per_sample: 8,
158                stored_row_bytes: row_bytes,
159                photometric,
160            },
161            &[],
162            out,
163        )
164    }
165
166    /// Lays out an image from already-packed sample bytes (`height * stored_row_bytes`), applying
167    /// the strip codec and building the directory.
168    fn encode_packed(
169        &self,
170        packed: &[u8],
171        dims: Dimensions,
172        layout: &SampleLayout,
173        extra_fields: &[(u16, Value)],
174        out: &mut Vec<u8>,
175    ) -> Result<usize> {
176        if let Some((tw, tl)) = self.tiling {
177            return self.encode_tiled(packed, dims, layout, extra_fields, tw, tl, out);
178        }
179        let (ifd, strips) = self.build_strip_image(packed, dims, layout, extra_fields)?;
180        let bytes = writer::write_image(self.order, self.variant(), &ifd, &strips);
181        out.extend_from_slice(&bytes);
182        Ok(bytes.len())
183    }
184
185    /// Builds one strip image's directory (without `StripOffsets`/`StripByteCounts`) and its
186    /// compressed strips, applying the predictor and strip codec.
187    fn build_strip_image(
188        &self,
189        packed: &[u8],
190        dims: Dimensions,
191        layout: &SampleLayout,
192        extra_fields: &[(u16, Value)],
193    ) -> Result<(Ifd, Vec<Vec<u8>>)> {
194        let h = dims.height as usize;
195        let stored_row_bytes = layout.stored_row_bytes;
196
197        // Apply the horizontal-differencing predictor (8-bit only) before compression.
198        let predicting = self.predictor == Predictor::HorizontalDifferencing;
199        if predicting && layout.bits_per_sample != 8 {
200            return Err(Error::Unsupported("TIFF: predictor requires 8-bit samples"));
201        }
202        let predicted = predicting.then(|| {
203            let mut buf = packed.to_vec();
204            predictor::forward(&mut buf, stored_row_bytes, layout.spp);
205            buf
206        });
207        let packed: &[u8] = predicted.as_deref().unwrap_or(packed);
208
209        // Partition rows into strips of roughly 8 KB (TIFF 6.0 §7), then apply the strip codec.
210        let rows_per_strip = (8192 / stored_row_bytes.max(1)).clamp(1, h);
211        let mut strips: Vec<Vec<u8>> = Vec::new();
212        let mut row = 0;
213        while row < h {
214            let rows = rows_per_strip.min(h - row);
215            let start = row * stored_row_bytes;
216            let raw = &packed[start..start + rows * stored_row_bytes];
217            strips.push(self.compress_strip(raw, dims, layout)?);
218            row += rows;
219        }
220
221        let mut ifd = Ifd::new();
222        ifd.set(tags::IMAGE_WIDTH, dim_value(dims.width));
223        ifd.set(tags::IMAGE_LENGTH, dim_value(dims.height));
224        ifd.set(
225            tags::BITS_PER_SAMPLE,
226            Value::Short(vec![layout.bits_per_sample; layout.spp]),
227        );
228        ifd.set(
229            tags::COMPRESSION,
230            Value::Short(vec![self.compression.code()]),
231        );
232        ifd.set(
233            tags::PHOTOMETRIC_INTERPRETATION,
234            Value::Short(vec![layout.photometric.code()]),
235        );
236        ifd.set(
237            tags::SAMPLES_PER_PIXEL,
238            Value::Short(vec![layout.spp as u16]),
239        );
240        ifd.set(tags::ROWS_PER_STRIP, dim_value(rows_per_strip as u32));
241        ifd.set(tags::X_RESOLUTION, Value::Rational(vec![(72, 1)]));
242        ifd.set(tags::Y_RESOLUTION, Value::Rational(vec![(72, 1)]));
243        ifd.set(tags::RESOLUTION_UNIT, Value::Short(vec![2])); // inch
244        if predicting {
245            ifd.set(tags::PREDICTOR, Value::Short(vec![2]));
246        }
247        for (tag, value) in extra_fields {
248            ifd.set(*tag, value.clone());
249        }
250        Ok((ifd, strips))
251    }
252
253    /// Encodes several 8-bit [`Rgb8`] images as the pages of one multi-page TIFF.
254    ///
255    /// Each page is a validated [`ImageRef`]. Returns the number of bytes written.
256    ///
257    /// # Errors
258    ///
259    /// Returns [`Error::InvalidInput`] if `pages` is empty.
260    pub fn encode_pages_rgb8(
261        &self,
262        pages: &[ImageRef<'_, Rgb8>],
263        out: &mut Vec<u8>,
264    ) -> Result<usize> {
265        if pages.is_empty() {
266            return Err(Error::InvalidInput("TIFF: no pages to encode"));
267        }
268        let total = pages.len() as u16;
269        let mut images: Vec<(Ifd, Vec<Vec<u8>>)> = Vec::with_capacity(pages.len());
270        for (i, page) in pages.iter().enumerate() {
271            let row_bytes = page.width() as usize * 3;
272            let extra = [
273                (tags::NEW_SUBFILE_TYPE, Value::Long(vec![2])), // bit 1: page of a multi-page image
274                (tags::PAGE_NUMBER, Value::Short(vec![i as u16, total])),
275            ];
276            images.push(self.build_strip_image(
277                page.as_samples(),
278                page.dimensions(),
279                &SampleLayout {
280                    spp: 3,
281                    bits_per_sample: 8,
282                    stored_row_bytes: row_bytes,
283                    photometric: PhotometricInterpretation::Rgb,
284                },
285                &extra,
286            )?);
287        }
288        let bytes = writer::write_multipage(self.order, self.variant(), &images);
289        out.extend_from_slice(&bytes);
290        Ok(bytes.len())
291    }
292
293    /// Applies the selected compression to one strip's already-packed bytes.
294    fn compress_strip(
295        &self,
296        raw: &[u8],
297        dims: Dimensions,
298        layout: &SampleLayout,
299    ) -> Result<Vec<u8>> {
300        let row_bytes = layout.stored_row_bytes;
301        match self.compression {
302            Compression::CcittRle => {
303                if layout.bits_per_sample != 1 {
304                    return Err(Error::Unsupported(
305                        "TIFF: Modified Huffman requires a bilevel image",
306                    ));
307                }
308                ccitt::mh_encode_strip(raw, row_bytes, dims.width as usize)
309            }
310            Compression::CcittGroup4Fax => {
311                if layout.bits_per_sample != 1 {
312                    return Err(Error::Unsupported(
313                        "TIFF: Group 4 fax requires a bilevel image",
314                    ));
315                }
316                let rows = raw.len() / row_bytes;
317                ccitt::g4_encode_strip(raw, row_bytes, rows, dims.width as usize)
318            }
319            _ => self.compress_bytes(raw, row_bytes),
320        }
321    }
322
323    /// Byte-level compression of one strip/tile (the schemes that work on raw bytes).
324    fn compress_bytes(&self, raw: &[u8], row_bytes: usize) -> Result<Vec<u8>> {
325        match self.compression {
326            Compression::None => Ok(raw.to_vec()),
327            Compression::PackBits => {
328                let mut out = Vec::new();
329                for row in raw.chunks(row_bytes) {
330                    packbits::encode_row(row, &mut out);
331                }
332                Ok(out)
333            }
334            Compression::Lzw => Ok(lzw::encode(raw)),
335            _ => Err(Error::Unsupported(
336                "TIFF: unsupported compression for encoding",
337            )),
338        }
339    }
340
341    /// Lays out an 8-bit image as a grid of `tile_w × tile_h` tiles (edge tiles zero-padded).
342    #[allow(clippy::too_many_arguments)]
343    fn encode_tiled(
344        &self,
345        packed: &[u8],
346        dims: Dimensions,
347        layout: &SampleLayout,
348        extra_fields: &[(u16, Value)],
349        tile_w: u32,
350        tile_h: u32,
351        out: &mut Vec<u8>,
352    ) -> Result<usize> {
353        if layout.bits_per_sample != 8 {
354            return Err(Error::Unsupported(
355                "TIFF: tiling supported only for 8-bit images so far",
356            ));
357        }
358        if self.predictor != Predictor::None {
359            return Err(Error::Unsupported(
360                "TIFF: predictor with tiling not supported yet",
361            ));
362        }
363        let (tw, th) = (tile_w as usize, tile_h as usize);
364        if tw == 0 || th == 0 || tw % 16 != 0 || th % 16 != 0 {
365            return Err(Error::InvalidInput(
366                "TIFF: tile dimensions must be positive multiples of 16",
367            ));
368        }
369        let (w, h, spp) = (dims.width as usize, dims.height as usize, layout.spp);
370        let stored_row_bytes = layout.stored_row_bytes;
371        let tile_row_bytes = tw * spp;
372        let tiles_across = w.div_ceil(tw);
373        let tiles_down = h.div_ceil(th);
374
375        let mut tiles: Vec<Vec<u8>> = Vec::with_capacity(tiles_across * tiles_down);
376        for ty in 0..tiles_down {
377            for tx in 0..tiles_across {
378                let mut tile = vec![0u8; th * tile_row_bytes];
379                for r in 0..th {
380                    let src_row = ty * th + r;
381                    if src_row >= h {
382                        break;
383                    }
384                    let copy_cols = tw.min(w - tx * tw);
385                    let src = (src_row * stored_row_bytes) + (tx * tw) * spp;
386                    let dst = r * tile_row_bytes;
387                    tile[dst..dst + copy_cols * spp]
388                        .copy_from_slice(&packed[src..src + copy_cols * spp]);
389                }
390                tiles.push(self.compress_bytes(&tile, tile_row_bytes)?);
391            }
392        }
393
394        let mut ifd = Ifd::new();
395        ifd.set(tags::IMAGE_WIDTH, dim_value(dims.width));
396        ifd.set(tags::IMAGE_LENGTH, dim_value(dims.height));
397        ifd.set(
398            tags::BITS_PER_SAMPLE,
399            Value::Short(vec![layout.bits_per_sample; spp]),
400        );
401        ifd.set(
402            tags::COMPRESSION,
403            Value::Short(vec![self.compression.code()]),
404        );
405        ifd.set(
406            tags::PHOTOMETRIC_INTERPRETATION,
407            Value::Short(vec![layout.photometric.code()]),
408        );
409        ifd.set(tags::SAMPLES_PER_PIXEL, Value::Short(vec![spp as u16]));
410        ifd.set(tags::TILE_WIDTH, dim_value(tile_w));
411        ifd.set(tags::TILE_LENGTH, dim_value(tile_h));
412        ifd.set(tags::X_RESOLUTION, Value::Rational(vec![(72, 1)]));
413        ifd.set(tags::Y_RESOLUTION, Value::Rational(vec![(72, 1)]));
414        ifd.set(tags::RESOLUTION_UNIT, Value::Short(vec![2])); // inch
415        for (tag, value) in extra_fields {
416            ifd.set(*tag, value.clone());
417        }
418
419        let bytes = writer::write_image_tiled(self.order, self.variant(), &ifd, &tiles);
420        out.extend_from_slice(&bytes);
421        Ok(bytes.len())
422    }
423}
424
425impl EncodeImage<Gray8> for TiffEncoder {
426    fn encode_image(&self, image: ImageRef<'_, Gray8>, out: &mut Vec<u8>) -> Result<usize> {
427        self.encode_8bit(
428            image.as_samples(),
429            image.dimensions(),
430            1,
431            PhotometricInterpretation::BlackIsZero,
432            out,
433        )
434    }
435}
436
437impl EncodeImage<Rgb8> for TiffEncoder {
438    fn encode_image(&self, image: ImageRef<'_, Rgb8>, out: &mut Vec<u8>) -> Result<usize> {
439        self.encode_8bit(
440            image.as_samples(),
441            image.dimensions(),
442            3,
443            PhotometricInterpretation::Rgb,
444            out,
445        )
446    }
447}
448
449impl EncodeImage<Cmyk8> for TiffEncoder {
450    /// `PhotometricInterpretation = Separated` (5); each sample is ink coverage (0 = 0 %, 255 = 100 %).
451    fn encode_image(&self, image: ImageRef<'_, Cmyk8>, out: &mut Vec<u8>) -> Result<usize> {
452        self.encode_8bit(
453            image.as_samples(),
454            image.dimensions(),
455            4,
456            PhotometricInterpretation::Cmyk,
457            out,
458        )
459    }
460}
461
462impl EncodeImage<Rgba8> for TiffEncoder {
463    /// Stores the fourth sample as *unassociated* alpha (`ExtraSamples = 2`, not premultiplied).
464    fn encode_image(&self, image: ImageRef<'_, Rgba8>, out: &mut Vec<u8>) -> Result<usize> {
465        let row_bytes = image.width() as usize * 4;
466        self.encode_packed(
467            image.as_samples(),
468            image.dimensions(),
469            &SampleLayout {
470                spp: 4,
471                bits_per_sample: 8,
472                stored_row_bytes: row_bytes,
473                photometric: PhotometricInterpretation::Rgb,
474            },
475            &[(tags::EXTRA_SAMPLES, Value::Short(vec![2]))],
476            out,
477        )
478    }
479}
480
481impl EncodeImage<Bilevel> for TiffEncoder {
482    /// Packs one byte per pixel (`0` = black, non-zero = white) MSB-first into bits, `BlackIsZero`.
483    fn encode_image(&self, image: ImageRef<'_, Bilevel>, out: &mut Vec<u8>) -> Result<usize> {
484        let (w, h) = (image.width() as usize, image.height() as usize);
485        let pixels = image.as_samples();
486        let stored_row_bytes = w.div_ceil(8);
487        let mut packed = vec![0u8; stored_row_bytes * h];
488        for y in 0..h {
489            let row = &pixels[y * w..(y + 1) * w];
490            let dst = &mut packed[y * stored_row_bytes..(y + 1) * stored_row_bytes];
491            for (x, &p) in row.iter().enumerate() {
492                if p != 0 {
493                    dst[x / 8] |= 0x80 >> (x % 8);
494                }
495            }
496        }
497        self.encode_packed(
498            &packed,
499            image.dimensions(),
500            &SampleLayout {
501                spp: 1,
502                bits_per_sample: 1,
503                stored_row_bytes,
504                photometric: PhotometricInterpretation::BlackIsZero,
505            },
506            &[],
507            out,
508        )
509    }
510}
511
512/// Stores a dimension/count as `SHORT` when it fits, else `LONG` (both are valid per §2).
513fn dim_value(n: u32) -> Value {
514    if n <= u32::from(u16::MAX) {
515        Value::Short(vec![n as u16])
516    } else {
517        Value::Long(vec![n])
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524
525    #[test]
526    fn image_ref_rejects_mismatched_buffer() {
527        // Validation now lives at the ImageRef boundary, so a wrong-length or zero-sized buffer
528        // can't even be constructed for the encoder's pixel types.
529        let dims = Dimensions {
530            width: 2,
531            height: 2,
532        };
533        assert!(ImageRef::<Rgb8>::new(&[0; 11], dims).is_err());
534        assert!(ImageRef::<Gray8>::new(&[0; 3], dims).is_err());
535        assert!(ImageRef::<Bilevel>::new(&[0; 3], dims).is_err());
536        assert!(
537            ImageRef::<Rgb8>::new(
538                &[],
539                Dimensions {
540                    width: 0,
541                    height: 1
542                }
543            )
544            .is_err()
545        );
546    }
547
548    #[test]
549    fn writes_a_well_formed_header() {
550        let enc = TiffEncoder::new();
551        let mut out = Vec::new();
552        let n = enc
553            .encode_image(
554                ImageRef::<Rgb8>::new(
555                    &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
556                    Dimensions {
557                        width: 2,
558                        height: 2,
559                    },
560                )
561                .unwrap(),
562                &mut out,
563            )
564            .expect("encode");
565        assert_eq!(n, out.len());
566        assert_eq!(&out[0..2], b"II");
567        // Classic TIFF by default: magic 42.
568        assert_eq!(out[2], 42);
569    }
570
571    #[test]
572    fn with_big_tiff_emits_bigtiff_header() {
573        let mut out = Vec::new();
574        TiffEncoder::new()
575            .with_big_tiff(true)
576            .encode_image(
577                ImageRef::<Rgb8>::new(
578                    &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
579                    Dimensions {
580                        width: 2,
581                        height: 2,
582                    },
583                )
584                .unwrap(),
585                &mut out,
586            )
587            .expect("encode");
588        // Magic 43, the fixed offset-size 8, and a 16-byte header (first IFD at offset >= 16).
589        let (order, variant, first) = gamut_ifd::read_header(&out).expect("header");
590        assert_eq!(order, ByteOrder::LittleEndian);
591        assert_eq!(variant, Variant::Big);
592        assert_eq!(out[2], 0x2b);
593        assert!(first >= 16);
594    }
595}