1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum TiffLayout {
32 Strips {
34 rows: u32,
36 },
37 Tiles {
39 width: u32,
41 height: u32,
43 },
44}
45
46impl Default for TiffLayout {
47 fn default() -> Self {
48 Self::Strips { rows: 64 }
51 }
52}
53
54#[derive(Debug)]
56pub struct TiffEncoder {
57 layout: TiffLayout,
58 deflate: Option<Level>,
59 state: Option<State>,
60 icc: Option<Vec<u8>>,
62}
63
64#[derive(Debug)]
65struct State {
66 descriptor: ImageDescriptor,
67 pending: Vec<u8>,
69 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 #[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 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 #[must_use]
126 pub const fn with_deflate(mut self, level: Level) -> Self {
127 self.deflate = Some(level);
128 self
129 }
130
131 #[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 #[must_use]
149 pub const fn layout(&self) -> TiffLayout {
150 self.layout
151 }
152
153 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 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
237type 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 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 if matches!(
339 descriptor.pixel,
340 PixelFormat::Rgba8 | PixelFormat::Rgba16 | PixelFormat::GrayA8
341 ) {
342 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 fields.push((
361 tag::ICC_PROFILE,
362 7,
363 profile.iter().map(|&b| u32::from(b)).collect(),
364 ));
365 }
366 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
380fn 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 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 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 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 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
484const 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
498fn 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 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 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 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 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}