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