Skip to main content

otf_pixels_codec_tiff/
encoder.rs

1//! The TIFF encoder.
2//!
3//! Writes baseline TIFF: little-endian, one directory, either strips or tiles,
4//! with no compression or Deflate. That is deliberately narrower than what the
5//! decoder reads — a decoder must accept what the world produces, an encoder
6//! only has to produce something correct — and it covers what a pipeline
7//! actually needs to write.
8//!
9//! # Why tiled output matters
10//!
11//! A tiled TIFF is the only v1 format a later pipeline can read back with
12//! genuine random access. Writing one is therefore how a caller stores an
13//! intermediate that will be re-read in pieces, which is the pattern behind
14//! image pyramids and tile servers.
15//!
16//! # Memory
17//!
18//! Strip output streams: a strip is written as soon as its rows arrive, so
19//! peak memory is one strip. Tiled output buffers rows until a full band of
20//! tiles is available, which is one tile height rather than the image.
21
22use otf_pixels_compress::{Level, zlib_compress};
23use otf_pixels_core::{
24    Density, EncodeOptions, Encoder, ImageDescriptor, PixelFormat, PixelsError, ResolutionUnit,
25    Result, Sink,
26};
27
28use crate::decoder::is_metadata_tag;
29use crate::ifd::{ByteOrder, tag};
30use otf_pixels_core::exif::{Exif, ExifEntry};
31
32/// How an encoder arranges pixels in the file.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum TiffLayout {
35    /// Horizontal bands, `rows` tall.
36    Strips {
37        /// Rows per strip.
38        rows: u32,
39    },
40    /// A grid of tiles. Both dimensions must be multiples of 16.
41    Tiles {
42        /// Tile width.
43        width: u32,
44        /// Tile height.
45        height: u32,
46    },
47}
48
49impl Default for TiffLayout {
50    fn default() -> Self {
51        // 64 rows is a common default and keeps a strip comfortably inside
52        // cache for the widths a pipeline usually produces.
53        Self::Strips { rows: 64 }
54    }
55}
56
57/// Encodes a baseline TIFF.
58#[derive(Debug)]
59pub struct TiffEncoder {
60    layout: TiffLayout,
61    deflate: Option<Level>,
62    state: Option<State>,
63    /// The ICC profile to store in tag 34675, if any.
64    icc: Option<Vec<u8>>,
65    /// The EXIF block whose tags join the directory, if any.
66    exif: Option<Vec<u8>>,
67    /// The XMP packet to write as tag 700, if any.
68    xmp: Option<Vec<u8>>,
69    /// The resolution to write, if any.
70    density: Option<Density>,
71}
72
73#[derive(Debug)]
74struct State {
75    descriptor: ImageDescriptor,
76    /// Rows accumulated but not yet emitted as a chunk.
77    pending: Vec<u8>,
78    /// Compressed chunks, in file order.
79    chunks: Vec<Vec<u8>>,
80    rows_written: u32,
81}
82
83impl Default for TiffEncoder {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl TiffEncoder {
90    /// An encoder writing uncompressed strips.
91    #[must_use]
92    pub const fn new() -> Self {
93        Self {
94            layout: TiffLayout::Strips { rows: 64 },
95            deflate: None,
96            state: None,
97            icc: None,
98            exif: None,
99            xmp: None,
100            density: None,
101        }
102    }
103
104    /// An encoder writing the given layout.
105    ///
106    /// # Errors
107    ///
108    /// Returns [`PixelsError::InvalidArgument`] for a zero-sized layout, or
109    /// tile dimensions that are not multiples of 16 — which TIFF 6.0
110    /// §Section 15 requires and every reader relies on.
111    pub fn with_layout(mut self, layout: TiffLayout) -> Result<Self> {
112        match layout {
113            TiffLayout::Strips { rows: 0 } => {
114                return Err(PixelsError::invalid_argument("rows", "must be non-zero"));
115            }
116            TiffLayout::Tiles { width, height } => {
117                if width == 0 || height == 0 {
118                    return Err(PixelsError::invalid_argument(
119                        "tile",
120                        "tile dimensions must be non-zero",
121                    ));
122                }
123                if width % 16 != 0 || height % 16 != 0 {
124                    return Err(PixelsError::invalid_argument(
125                        "tile",
126                        format!("tile {width}x{height} must be a multiple of 16"),
127                    ));
128                }
129            }
130            TiffLayout::Strips { .. } => {}
131        }
132        self.layout = layout;
133        Ok(self)
134    }
135
136    /// An encoder compressing with Deflate at `level`.
137    #[must_use]
138    pub const fn with_deflate(mut self, level: Level) -> Self {
139        self.deflate = Some(level);
140        self
141    }
142
143    /// An encoder configured from generic encode options.
144    ///
145    /// TIFF is lossless, so quality is read as compression effort: below the
146    /// midpoint means store uncompressed and prioritise speed, above it means
147    /// Deflate at a level scaled from the remaining range.
148    #[must_use]
149    pub fn from_options(options: &EncodeOptions) -> Self {
150        let quality = u32::from(options.quality.clamp(1, 100));
151        let mut encoder = Self::new();
152        if quality > 50 {
153            let level = ((quality - 50) * 9 / 50).clamp(1, 9) as u8;
154            encoder.deflate = Some(Level::new(level).unwrap_or(Level::DEFAULT));
155        }
156        encoder
157    }
158
159    /// The layout this encoder writes.
160    #[must_use]
161    pub const fn layout(&self) -> TiffLayout {
162        self.layout
163    }
164
165    /// Emit whatever complete chunks the pending rows allow.
166    fn flush_chunks(&mut self, final_flush: bool) -> Result<()> {
167        let Some(state) = self.state.as_mut() else {
168            return Ok(());
169        };
170        let row_bytes = state.descriptor.row_bytes();
171        let width = state.descriptor.width;
172
173        match self.layout {
174            TiffLayout::Strips { rows } => {
175                let chunk_bytes = row_bytes * rows as usize;
176                while state.pending.len() >= chunk_bytes
177                    || (final_flush && !state.pending.is_empty())
178                {
179                    let take = chunk_bytes.min(state.pending.len());
180                    let block: Vec<u8> = state.pending.drain(..take).collect();
181                    let compressed = match self.deflate {
182                        None => block,
183                        Some(level) => {
184                            zlib_compress(&block, level).map_err(crate::compress_error)?
185                        }
186                    };
187                    state.chunks.push(compressed);
188                    if !final_flush {
189                        break;
190                    }
191                }
192            }
193            TiffLayout::Tiles {
194                width: tile_width,
195                height: tile_height,
196            } => {
197                let band_bytes = row_bytes * tile_height as usize;
198                while state.pending.len() >= band_bytes
199                    || (final_flush && !state.pending.is_empty())
200                {
201                    let take = band_bytes.min(state.pending.len());
202                    let band: Vec<u8> = state.pending.drain(..take).collect();
203                    let rows_in_band = take / row_bytes.max(1);
204                    let bpp = state.descriptor.pixel.bytes_per_pixel();
205                    let across = width.div_ceil(tile_width);
206
207                    for column in 0..across {
208                        // Tiles are always stored whole; the edge ones are
209                        // padded rather than clipped, which is what every
210                        // reader expects and what the decoder assumes.
211                        let mut tile = vec![0_u8; tile_width as usize * tile_height as usize * bpp];
212                        for row in 0..tile_height as usize {
213                            if row >= rows_in_band {
214                                break;
215                            }
216                            for pixel in 0..tile_width as usize {
217                                let x = column as usize * tile_width as usize + pixel;
218                                if x >= width as usize {
219                                    break;
220                                }
221                                let from = row * row_bytes + x * bpp;
222                                let to = (row * tile_width as usize + pixel) * bpp;
223                                let (Some(source), Some(target)) =
224                                    (band.get(from..from + bpp), tile.get_mut(to..to + bpp))
225                                else {
226                                    continue;
227                                };
228                                target.copy_from_slice(source);
229                            }
230                        }
231                        let compressed = match self.deflate {
232                            None => tile,
233                            Some(level) => {
234                                zlib_compress(&tile, level).map_err(crate::compress_error)?
235                            }
236                        };
237                        state.chunks.push(compressed);
238                    }
239                    if !final_flush {
240                        break;
241                    }
242                }
243            }
244        }
245        Ok(())
246    }
247}
248
249/// The tags a baseline TIFF needs, as (tag, type code, values).
250type Field = (u16, u16, Vec<u32>);
251
252/// A tag beyond the baseline ones, its values already encoded.
253#[derive(Debug)]
254enum Extra {
255    /// A field type, count and the values' bytes, little-endian.
256    Raw(u16, u32, Vec<u8>),
257    /// A sub-directory (Exif, GPS), written after the values.
258    Sub(Vec<ExifEntry>),
259}
260
261/// A RATIONAL's eight bytes, little-endian, for `value` to 1/10000.
262fn rational(value: f64) -> Vec<u8> {
263    let n = (value * 10_000.0).round().clamp(0.0, f64::from(u32::MAX)) as u32;
264    let mut out = n.to_le_bytes().to_vec();
265    out.extend_from_slice(&10_000_u32.to_le_bytes());
266    out
267}
268
269impl Encoder for TiffEncoder {
270    fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
271        if self.state.is_some() {
272            return Err(PixelsError::invalid_argument(
273                "profile",
274                "the ICC profile must be set before write_header",
275            ));
276        }
277        self.icc = profile.map(<[u8]>::to_vec);
278        Ok(())
279    }
280
281    /// The block's descriptive tags and its Exif and GPS directories join
282    /// the image's own directory, as TIFF keeps EXIF; layout tags in it are
283    /// ignored, and IFD1's thumbnail is dropped.
284    fn set_exif(&mut self, exif: Option<&[u8]>) -> Result<()> {
285        if self.state.is_some() {
286            return Err(PixelsError::invalid_argument(
287                "exif",
288                "the EXIF block must be set before write_header",
289            ));
290        }
291        self.exif = exif.map(<[u8]>::to_vec);
292        Ok(())
293    }
294
295    fn set_xmp(&mut self, xmp: Option<&[u8]>) -> Result<()> {
296        if self.state.is_some() {
297            return Err(PixelsError::invalid_argument(
298                "xmp",
299                "the XMP packet must be set before write_header",
300            ));
301        }
302        self.xmp = xmp.map(<[u8]>::to_vec);
303        Ok(())
304    }
305
306    fn set_density(&mut self, density: Option<Density>) -> Result<()> {
307        if self.state.is_some() {
308            return Err(PixelsError::invalid_argument(
309                "density",
310                "the density must be set before write_header",
311            ));
312        }
313        self.density = density;
314        Ok(())
315    }
316
317    fn write_header(&mut self, desc: &ImageDescriptor, _sink: &mut dyn Sink) -> Result<()> {
318        if self.state.is_some() {
319            return Err(PixelsError::invalid_argument(
320                "descriptor",
321                "write_header called more than once",
322            ));
323        }
324        // The directory carries offsets that are not known until every chunk
325        // has been compressed, so nothing is written until `finish`.
326        supported_format(desc.pixel)?;
327        self.state = Some(State {
328            descriptor: *desc,
329            pending: Vec::new(),
330            chunks: Vec::new(),
331            rows_written: 0,
332        });
333        Ok(())
334    }
335
336    fn write_row(&mut self, row: &[u8], _sink: &mut dyn Sink) -> Result<()> {
337        let Some(state) = self.state.as_mut() else {
338            return Err(PixelsError::invalid_argument(
339                "row",
340                "write_row called before write_header",
341            ));
342        };
343        let expected = state.descriptor.row_bytes();
344        if row.len() != expected {
345            return Err(PixelsError::invalid_argument(
346                "row",
347                format!("row is {} bytes, expected {expected}", row.len()),
348            ));
349        }
350        if state.rows_written >= state.descriptor.height {
351            return Err(PixelsError::invalid_argument(
352                "row",
353                format!("more than {} rows written", state.descriptor.height),
354            ));
355        }
356        state.pending.extend_from_slice(row);
357        state.rows_written += 1;
358        self.flush_chunks(false)?;
359        Ok(())
360    }
361
362    fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
363        {
364            let Some(state) = self.state.as_ref() else {
365                return Err(PixelsError::malformed(
366                    "tiff",
367                    "finish called before write_header",
368                ));
369            };
370            if state.rows_written != state.descriptor.height {
371                return Err(PixelsError::malformed(
372                    "tiff",
373                    format!(
374                        "{} of {} rows written; a partial image is never emitted",
375                        state.rows_written, state.descriptor.height
376                    ),
377                ));
378            }
379        }
380        self.flush_chunks(true)?;
381
382        let Some(state) = self.state.as_mut() else {
383            return Err(PixelsError::graph("tiff encoder state vanished"));
384        };
385        let order = ByteOrder::Little;
386        let descriptor = state.descriptor;
387        let (channels, bits) = sample_shape(descriptor.pixel);
388        let photometric = if channels >= 3 { 2_u32 } else { 1 };
389        let compression = if self.deflate.is_some() { 8_u32 } else { 1 };
390
391        let counts: Vec<u32> = state.chunks.iter().map(|c| c.len() as u32).collect();
392        let mut fields: Vec<Field> = vec![
393            (tag::IMAGE_WIDTH, 4, vec![descriptor.width]),
394            (tag::IMAGE_LENGTH, 4, vec![descriptor.height]),
395            (tag::BITS_PER_SAMPLE, 3, vec![bits; channels as usize]),
396            (tag::COMPRESSION, 3, vec![compression]),
397            (tag::PHOTOMETRIC, 3, vec![photometric]),
398            (tag::SAMPLES_PER_PIXEL, 3, vec![channels]),
399            (tag::PLANAR_CONFIG, 3, vec![1]),
400        ];
401        // Alpha has to be declared, or a reader shows a four-channel image as
402        // RGB plus an unknown extra rather than as RGBA.
403        if matches!(
404            descriptor.pixel,
405            PixelFormat::Rgba8 | PixelFormat::Rgba16 | PixelFormat::GrayA8
406        ) {
407            // 2 = unassociated alpha, which is what SPEC §Formats promises.
408            fields.push((tag::EXTRA_SAMPLES, 3, vec![2]));
409        }
410        match self.layout {
411            TiffLayout::Strips { rows } => {
412                fields.push((tag::ROWS_PER_STRIP, 4, vec![rows]));
413                fields.push((tag::STRIP_OFFSETS, 4, vec![0; counts.len()]));
414                fields.push((tag::STRIP_BYTE_COUNTS, 4, counts.clone()));
415            }
416            TiffLayout::Tiles { width, height } => {
417                fields.push((tag::TILE_WIDTH, 3, vec![width]));
418                fields.push((tag::TILE_LENGTH, 3, vec![height]));
419                fields.push((tag::TILE_OFFSETS, 4, vec![0; counts.len()]));
420                fields.push((tag::TILE_BYTE_COUNTS, 4, counts.clone()));
421            }
422        }
423        if let Some(profile) = &self.icc {
424            // UNDEFINED (7): one byte per value.
425            fields.push((
426                tag::ICC_PROFILE,
427                7,
428                profile.iter().map(|&b| u32::from(b)).collect(),
429            ));
430        }
431        let mut extras: Vec<(u16, Extra)> = Vec::new();
432        if let Some(tree) = self.exif.as_deref().and_then(Exif::parse) {
433            let tree = tree.with_byte_order(false).retain(is_metadata_tag);
434            for e in tree.ifd0 {
435                match e.sub {
436                    // Only the Exif and GPS directories belong in IFD0.
437                    Some(sub) if e.tag == 0x8769 || e.tag == 0x8825 => {
438                        extras.push((e.tag, Extra::Sub(sub)))
439                    }
440                    Some(_) => {}
441                    None => extras.push((e.tag, Extra::Raw(e.kind, e.count, e.data))),
442                }
443            }
444        }
445        if let Some(d) = self.density {
446            extras.retain(|(t, _)| !matches!(t, 282 | 283 | 296));
447            extras.push((282, Extra::Raw(5, 1, rational(d.x))));
448            extras.push((283, Extra::Raw(5, 1, rational(d.y))));
449            let unit: u16 = if d.unit == ResolutionUnit::Centimetre {
450                3
451            } else {
452                2
453            };
454            extras.push((296, Extra::Raw(3, 1, unit.to_le_bytes().to_vec())));
455        }
456        if let Some(xmp) = &self.xmp {
457            let count = u32::try_from(xmp.len()).unwrap_or(u32::MAX);
458            extras.push((700, Extra::Raw(1, count, xmp.clone())));
459        }
460        // A tag the baseline set already has wins over a copied one.
461        extras.retain(|(t, _)| !fields.iter().any(|(f, _, _)| f == t));
462        // Tags must appear in ascending order; readers are entitled to binary
463        // search, and libtiff warns loudly about files that get this wrong.
464        fields.sort_by_key(|(tag, _, _)| *tag);
465        extras.sort_by_key(|(tag, _)| *tag);
466
467        let out = assemble(order, &fields, &extras, &state.chunks)?;
468        sink.write_all(&out)?;
469        sink.flush()?;
470
471        state.chunks = Vec::new();
472        state.pending = Vec::new();
473        Ok(())
474    }
475}
476
477/// Lay out header, directory, value heap and pixel data.
478fn assemble(
479    order: ByteOrder,
480    fields: &[Field],
481    extras: &[(u16, Extra)],
482    chunks: &[Vec<u8>],
483) -> Result<Vec<u8>> {
484    let count = fields.len() + extras.len();
485    let directory_at = 8_usize;
486    let directory_size = 2 + count * 12 + 4;
487    let heap_at = directory_at + directory_size;
488
489    // First pass: work out where each field's values live and how big the
490    // heap is, so the pixel data's offset is known before it is written.
491    let mut heap = Vec::new();
492    let mut value_offsets: Vec<Option<usize>> = Vec::with_capacity(count);
493    for (_, type_code, values) in fields {
494        let size = type_size(*type_code);
495        if values.len() * size > 4 {
496            value_offsets.push(Some(heap_at + heap.len()));
497            for &value in values {
498                push_value(&mut heap, order, *type_code, value);
499            }
500            // A value array must start on an even boundary (§Section 2).
501            if heap.len() % 2 == 1 {
502                heap.push(0);
503            }
504        } else {
505            value_offsets.push(None);
506        }
507    }
508
509    // The extra tags' values, then their sub-directories, each on an even
510    // boundary.
511    let writer = Exif {
512        big_endian: false,
513        ifd0: Vec::new(),
514    };
515    let mut extra_entries: Vec<(u16, u16, u32, [u8; 4])> = Vec::with_capacity(extras.len());
516    for (tag_id, extra) in extras {
517        match extra {
518            Extra::Raw(kind, n, bytes) if bytes.len() <= 4 => {
519                let mut inline = [0_u8; 4];
520                for (slot, &b) in inline.iter_mut().zip(bytes) {
521                    *slot = b;
522                }
523                extra_entries.push((*tag_id, *kind, *n, inline));
524            }
525            Extra::Raw(kind, n, bytes) => {
526                let at = (heap_at + heap.len()) as u32;
527                heap.extend_from_slice(bytes);
528                if heap.len() % 2 == 1 {
529                    heap.push(0);
530                }
531                extra_entries.push((*tag_id, *kind, *n, at.to_le_bytes()));
532            }
533            Extra::Sub(_) => extra_entries.push((*tag_id, 4, 1, [0; 4])),
534        }
535    }
536    for ((_, extra), entry) in extras.iter().zip(&mut extra_entries) {
537        if let Extra::Sub(entries) = extra {
538            let at = (heap_at + heap.len()) as u32;
539            heap.extend_from_slice(&writer.sub_directory_bytes(entries, at));
540            if heap.len() % 2 == 1 {
541                heap.push(0);
542            }
543            entry.3 = at.to_le_bytes();
544        }
545    }
546
547    let data_at = heap_at + heap.len();
548    let mut offsets = Vec::with_capacity(chunks.len());
549    let mut running = data_at;
550    for chunk in chunks {
551        offsets.push(running as u32);
552        running += chunk.len();
553    }
554
555    // Second pass: emit the directory, patching the offset arrays.
556    let mut directory = Vec::with_capacity(directory_size);
557    directory.extend_from_slice(&order.write_u16(count as u16));
558    for (index, (tag_id, type_code, values)) in fields.iter().enumerate() {
559        directory.extend_from_slice(&order.write_u16(*tag_id));
560        directory.extend_from_slice(&order.write_u16(*type_code));
561        directory.extend_from_slice(&order.write_u32(values.len() as u32));
562
563        let is_offsets = *tag_id == tag::STRIP_OFFSETS || *tag_id == tag::TILE_OFFSETS;
564        let resolved: Vec<u32> = if is_offsets {
565            offsets.clone()
566        } else {
567            values.clone()
568        };
569
570        match value_offsets.get(index).copied().flatten() {
571            Some(at) => {
572                if is_offsets {
573                    // Patch the placeholder written during the first pass.
574                    let start = at - heap_at;
575                    for (position, &value) in resolved.iter().enumerate() {
576                        let slot = start + position * 4;
577                        if let Some(target) = heap.get_mut(slot..slot + 4) {
578                            target.copy_from_slice(&order.write_u32(value));
579                        }
580                    }
581                }
582                directory.extend_from_slice(&order.write_u32(at as u32));
583            }
584            None => {
585                let mut inline = Vec::with_capacity(4);
586                for &value in &resolved {
587                    push_value(&mut inline, order, *type_code, value);
588                }
589                inline.resize(4, 0);
590                directory.extend_from_slice(inline.get(..4).unwrap_or(&[0; 4]));
591            }
592        }
593    }
594    for (tag_id, kind, n, value) in &extra_entries {
595        directory.extend_from_slice(&order.write_u16(*tag_id));
596        directory.extend_from_slice(&order.write_u16(*kind));
597        directory.extend_from_slice(&order.write_u32(*n));
598        directory.extend_from_slice(value);
599    }
600    // Both lists are sorted; the directory as a whole must be.
601    let mut entries: Vec<[u8; 12]> = directory
602        .get(2..)
603        .unwrap_or_default()
604        .chunks_exact(12)
605        .map(|c| <[u8; 12]>::try_from(c).unwrap_or([0; 12]))
606        .collect();
607    entries.sort_by_key(|e| order.u16(e, 0));
608    directory.truncate(2);
609    for e in entries {
610        directory.extend_from_slice(&e);
611    }
612    directory.extend_from_slice(&order.write_u32(0));
613
614    let mut out = Vec::with_capacity(data_at + running.saturating_sub(data_at));
615    out.extend_from_slice(b"II");
616    out.extend_from_slice(&order.write_u16(42));
617    out.extend_from_slice(&order.write_u32(directory_at as u32));
618    out.extend_from_slice(&directory);
619    out.extend_from_slice(&heap);
620    for chunk in chunks {
621        out.extend_from_slice(chunk);
622    }
623    Ok(out)
624}
625
626const fn type_size(code: u16) -> usize {
627    match code {
628        3 => 2,
629        4 => 4,
630        _ => 1,
631    }
632}
633
634fn push_value(out: &mut Vec<u8>, order: ByteOrder, type_code: u16, value: u32) {
635    match type_code {
636        3 => out.extend_from_slice(&order.write_u16(value as u16)),
637        4 => out.extend_from_slice(&order.write_u32(value)),
638        _ => out.push(value as u8),
639    }
640}
641
642/// Channels and bits per sample for a pixel format.
643const fn sample_shape(format: PixelFormat) -> (u32, u32) {
644    match format {
645        PixelFormat::Gray8 => (1, 8),
646        PixelFormat::Gray16 => (1, 16),
647        PixelFormat::GrayA8 => (2, 8),
648        PixelFormat::Rgb8 => (3, 8),
649        PixelFormat::Rgba8 => (4, 8),
650        PixelFormat::Rgb16 => (3, 16),
651        PixelFormat::Rgba16 => (4, 16),
652        _ => (0, 0),
653    }
654}
655
656/// Reject a format TIFF's baseline cannot express.
657fn supported_format(format: PixelFormat) -> Result<()> {
658    if sample_shape(format).0 == 0 {
659        return Err(PixelsError::unsupported(format!(
660            "TIFF encoding needs an integer format; got {format}"
661        )));
662    }
663    Ok(())
664}
665
666#[cfg(test)]
667#[allow(
668    clippy::unwrap_used,
669    clippy::expect_used,
670    clippy::indexing_slicing,
671    clippy::panic,
672    reason = "tests operate on known-good values and assert shapes directly"
673)]
674mod tests {
675    use super::*;
676    use crate::decoder::TiffDecoder;
677    use otf_pixels_core::{Decoder, Limits};
678
679    fn encode(desc: &ImageDescriptor, raster: &[u8], encoder: TiffEncoder) -> Result<Vec<u8>> {
680        let mut encoder = encoder;
681        let mut out: Vec<u8> = Vec::new();
682        encoder.write_header(desc, &mut out)?;
683        for row in raster.chunks_exact(desc.row_bytes()) {
684            encoder.write_row(row, &mut out)?;
685        }
686        encoder.finish(&mut out)?;
687        Ok(out)
688    }
689
690    fn decode(bytes: &[u8]) -> Result<(ImageDescriptor, Vec<u8>)> {
691        let mut decoder = TiffDecoder::new(bytes, Limits::default())?;
692        let descriptor = decoder.descriptor();
693        let mut raster = Vec::new();
694        let mut row = vec![0_u8; descriptor.row_bytes()];
695        for _ in 0..descriptor.height {
696            decoder.read_row(&mut row)?;
697            raster.extend_from_slice(&row);
698        }
699        Ok((descriptor, raster))
700    }
701
702    fn sample(width: u32, height: u32, format: PixelFormat) -> (ImageDescriptor, Vec<u8>) {
703        let descriptor = ImageDescriptor::new(width, height, format).unwrap();
704        let len = descriptor.byte_len().unwrap();
705        let bytes = (0..len).map(|i| ((i * 37) % 251) as u8).collect();
706        (descriptor, bytes)
707    }
708
709    #[test]
710    fn every_supported_format_round_trips_through_strips() {
711        for format in [
712            PixelFormat::Gray8,
713            PixelFormat::Gray16,
714            PixelFormat::GrayA8,
715            PixelFormat::Rgb8,
716            PixelFormat::Rgba8,
717            PixelFormat::Rgb16,
718            PixelFormat::Rgba16,
719        ] {
720            let (descriptor, raster) = sample(37, 29, format);
721            let bytes = encode(&descriptor, &raster, TiffEncoder::new())
722                .unwrap_or_else(|e| panic!("{format}: {e}"));
723            let (out_desc, decoded) =
724                decode(&bytes).unwrap_or_else(|e| panic!("decoding {format}: {e}"));
725            assert_eq!(out_desc.pixel, format, "{format} changed format");
726            assert_eq!((out_desc.width, out_desc.height), (37, 29), "{format}");
727            assert_eq!(decoded, raster, "{format} did not round-trip");
728        }
729    }
730
731    #[test]
732    fn tiled_output_round_trips_and_is_random_access() {
733        // The layout the exit criterion turns on: what we write must be
734        // readable back with region capability, or a pyramid cannot be built.
735        let (descriptor, raster) = sample(100, 70, PixelFormat::Rgb8);
736        let encoder = TiffEncoder::new()
737            .with_layout(TiffLayout::Tiles {
738                width: 32,
739                height: 32,
740            })
741            .unwrap();
742        let bytes = encode(&descriptor, &raster, encoder).unwrap();
743
744        let decoder = TiffDecoder::new(&bytes[..], Limits::default()).unwrap();
745        assert_eq!(
746            decoder.capability(),
747            otf_pixels_core::DecodeCapability::Regions,
748            "our own tiled output must be random-access"
749        );
750        let (_, decoded) = decode(&bytes).unwrap();
751        assert_eq!(decoded, raster, "tiled output did not round-trip");
752    }
753
754    #[test]
755    fn deflate_round_trips_in_both_layouts() {
756        for layout in [
757            TiffLayout::Strips { rows: 16 },
758            TiffLayout::Tiles {
759                width: 16,
760                height: 16,
761            },
762        ] {
763            let (descriptor, raster) = sample(48, 40, PixelFormat::Rgb8);
764            let encoder = TiffEncoder::new()
765                .with_layout(layout)
766                .unwrap()
767                .with_deflate(Level::DEFAULT);
768            let bytes = encode(&descriptor, &raster, encoder).unwrap();
769            let (_, decoded) = decode(&bytes).unwrap();
770            assert_eq!(
771                decoded, raster,
772                "{layout:?} with deflate did not round-trip"
773            );
774        }
775    }
776
777    #[test]
778    fn deflate_actually_compresses() {
779        let descriptor = ImageDescriptor::new(128, 128, PixelFormat::Rgb8).unwrap();
780        let raster = vec![7_u8; descriptor.byte_len().unwrap()];
781        let plain = encode(&descriptor, &raster, TiffEncoder::new()).unwrap();
782        let packed = encode(
783            &descriptor,
784            &raster,
785            TiffEncoder::new().with_deflate(Level::BEST),
786        )
787        .unwrap();
788        assert!(
789            packed.len() * 10 < plain.len(),
790            "deflate produced {} bytes against {} uncompressed",
791            packed.len(),
792            plain.len()
793        );
794    }
795
796    #[test]
797    fn every_strip_and_tile_size_round_trips() {
798        // Sizes that divide the image evenly and sizes that do not, because
799        // the last chunk is where an encoder's arithmetic breaks.
800        let (descriptor, raster) = sample(70, 50, PixelFormat::Rgb8);
801        for rows in [1_u32, 7, 25, 50, 999] {
802            let encoder = TiffEncoder::new()
803                .with_layout(TiffLayout::Strips { rows })
804                .unwrap();
805            let bytes = encode(&descriptor, &raster, encoder).unwrap();
806            let (_, decoded) = decode(&bytes).unwrap();
807            assert_eq!(decoded, raster, "{rows} rows per strip");
808        }
809        for (width, height) in [(16_u32, 16_u32), (32, 16), (16, 48), (80, 64)] {
810            let encoder = TiffEncoder::new()
811                .with_layout(TiffLayout::Tiles { width, height })
812                .unwrap();
813            let bytes = encode(&descriptor, &raster, encoder).unwrap();
814            let (_, decoded) = decode(&bytes).unwrap();
815            assert_eq!(decoded, raster, "{width}x{height} tiles");
816        }
817    }
818
819    #[test]
820    fn tile_dimensions_must_be_multiples_of_sixteen() {
821        // §Section 15, and the decoder enforces it too — an encoder that
822        // emitted a 17-pixel tile would produce files our own reader rejects.
823        for (width, height) in [(17_u32, 16_u32), (16, 17), (0, 16), (16, 0)] {
824            assert!(
825                TiffEncoder::new()
826                    .with_layout(TiffLayout::Tiles { width, height })
827                    .is_err(),
828                "{width}x{height} should be rejected"
829            );
830        }
831        assert!(
832            TiffEncoder::new()
833                .with_layout(TiffLayout::Strips { rows: 0 })
834                .is_err()
835        );
836    }
837
838    #[test]
839    fn a_float_format_is_unsupported_not_a_panic() {
840        let descriptor = ImageDescriptor::new(4, 4, PixelFormat::RgbaF32).unwrap();
841        let mut encoder = TiffEncoder::new();
842        let mut out = Vec::new();
843        assert!(encoder.write_header(&descriptor, &mut out).is_err());
844        assert!(
845            out.is_empty(),
846            "nothing should be written for a rejected format"
847        );
848    }
849
850    #[test]
851    fn a_short_image_is_an_error_not_a_truncated_tiff() {
852        let (descriptor, _) = sample(8, 8, PixelFormat::Rgb8);
853        let mut encoder = TiffEncoder::new();
854        let mut out = Vec::new();
855        encoder.write_header(&descriptor, &mut out).unwrap();
856        encoder.write_row(&[0; 24], &mut out).unwrap();
857        assert!(encoder.finish(&mut out).is_err());
858    }
859
860    #[test]
861    fn extra_rows_and_wrong_lengths_are_errors() {
862        let descriptor = ImageDescriptor::new(4, 1, PixelFormat::Rgb8).unwrap();
863        let mut encoder = TiffEncoder::new();
864        let mut out = Vec::new();
865        encoder.write_header(&descriptor, &mut out).unwrap();
866        assert!(encoder.write_row(&[0; 11], &mut out).is_err(), "short row");
867        assert!(encoder.write_row(&[0; 13], &mut out).is_err(), "long row");
868        encoder.write_row(&[0; 12], &mut out).unwrap();
869        assert!(encoder.write_row(&[0; 12], &mut out).is_err(), "extra row");
870    }
871
872    #[test]
873    fn tags_are_written_in_ascending_order() {
874        // Readers are entitled to binary search the directory, and libtiff
875        // warns loudly about files that get this wrong.
876        let (descriptor, raster) = sample(16, 16, PixelFormat::Rgba8);
877        let bytes = encode(&descriptor, &raster, TiffEncoder::new()).unwrap();
878        let order = ByteOrder::Little;
879        let count = order.u16(&bytes, 8) as usize;
880        let mut previous = 0_u16;
881        for index in 0..count {
882            let tag_id = order.u16(&bytes, 10 + index * 12);
883            assert!(tag_id > previous, "tag {tag_id} follows {previous}");
884            previous = tag_id;
885        }
886    }
887
888    #[test]
889    fn encoding_is_deterministic() {
890        let (descriptor, raster) = sample(40, 30, PixelFormat::Rgb8);
891        let first = encode(&descriptor, &raster, TiffEncoder::new()).unwrap();
892        for _ in 0..4 {
893            assert_eq!(
894                encode(&descriptor, &raster, TiffEncoder::new()).unwrap(),
895                first,
896                "TIFF encoding is not deterministic"
897            );
898        }
899    }
900
901    #[test]
902    fn options_map_quality_onto_compression() {
903        assert!(
904            TiffEncoder::from_options(&EncodeOptions::default())
905                .deflate
906                .is_some()
907        );
908        let low = EncodeOptions::with_quality(10).unwrap();
909        assert!(
910            TiffEncoder::from_options(&low).deflate.is_none(),
911            "low quality should prioritise speed"
912        );
913    }
914}