1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum TiffLayout {
35 Strips {
37 rows: u32,
39 },
40 Tiles {
42 width: u32,
44 height: u32,
46 },
47}
48
49impl Default for TiffLayout {
50 fn default() -> Self {
51 Self::Strips { rows: 64 }
54 }
55}
56
57#[derive(Debug)]
59pub struct TiffEncoder {
60 layout: TiffLayout,
61 deflate: Option<Level>,
62 state: Option<State>,
63 icc: Option<Vec<u8>>,
65 exif: Option<Vec<u8>>,
67 xmp: Option<Vec<u8>>,
69 density: Option<Density>,
71}
72
73#[derive(Debug)]
74struct State {
75 descriptor: ImageDescriptor,
76 pending: Vec<u8>,
78 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 #[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 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 #[must_use]
138 pub const fn with_deflate(mut self, level: Level) -> Self {
139 self.deflate = Some(level);
140 self
141 }
142
143 #[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 #[must_use]
161 pub const fn layout(&self) -> TiffLayout {
162 self.layout
163 }
164
165 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 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
249type Field = (u16, u16, Vec<u32>);
251
252#[derive(Debug)]
254enum Extra {
255 Raw(u16, u32, Vec<u8>),
257 Sub(Vec<ExifEntry>),
259}
260
261fn 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 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 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 if matches!(
404 descriptor.pixel,
405 PixelFormat::Rgba8 | PixelFormat::Rgba16 | PixelFormat::GrayA8
406 ) {
407 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 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 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 extras.retain(|(t, _)| !fields.iter().any(|(f, _, _)| f == t));
462 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
477fn 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 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 if heap.len() % 2 == 1 {
502 heap.push(0);
503 }
504 } else {
505 value_offsets.push(None);
506 }
507 }
508
509 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 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 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 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
642const 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
656fn 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 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 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 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 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}