diff --git a/src/codecs/tiff.rs b/src/codecs/tiff.rs index 2abbc53bd2..a95058a969 100644 --- a/src/codecs/tiff.rs +++ b/src/codecs/tiff.rs @@ -10,6 +10,7 @@ use std::marker::PhantomData; use std::mem; use tiff::decoder::{Decoder, DecodingResult}; +use tiff::encoder::{Compression, DeflateLevel}; use tiff::tags::Tag; use crate::color::{ColorType, ExtendedColorType}; @@ -362,7 +363,68 @@ impl ImageDecoder for TiffDecoder { /// Encoder for tiff images pub struct TiffEncoder { - w: W, + writer: W, + compression: Compression, +} + +/// Compression types supported by the TIFF format +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum CompressionType { + /// No compression + Uncompressed, + /// LZW compression + Lzw, + /// Deflate compression + /// + /// It is best to view the level options as a _hint_ to the implementation on the smallest or + /// fastest option for encoding a particular image. These have no direct mapping to any + /// particular attribute and may be interpreted differently in minor versions. The exact output + /// is expressly __not__ part of the SemVer stability guarantee. + Deflate(TiffDeflateLevel), + /// Bit packing compression + Packbits, +} + +impl Default for CompressionType { + fn default() -> Self { + CompressionType::Lzw + } +} + +/// The level of compression used by the Deflate algorithm. +/// It allows trading compression ratio for compression speed. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] +#[non_exhaustive] +pub enum TiffDeflateLevel { + /// The fastest possible compression mode. + Fast = 1, + /// The conserative choice between speed and ratio. + #[default] + Balanced = 6, + /// The best compression available with Deflate. + Best = 9, +} + +impl TiffDeflateLevel { + fn into_tiff(self: TiffDeflateLevel) -> DeflateLevel { + match self { + TiffDeflateLevel::Fast => DeflateLevel::Fast, + TiffDeflateLevel::Balanced => DeflateLevel::Balanced, + TiffDeflateLevel::Best => DeflateLevel::Best, + } + } +} + +impl CompressionType { + fn into_tiff(self: CompressionType) -> Compression { + match self { + CompressionType::Uncompressed => Compression::Uncompressed, + CompressionType::Lzw => Compression::Lzw, + CompressionType::Deflate(lvl) => Compression::Deflate(lvl.into_tiff()), + CompressionType::Packbits => Compression::Packbits, + } + } } fn cmyk_to_rgb(cmyk: &[u8]) -> [u8; 3] { @@ -407,8 +469,19 @@ fn u8_slice_as_pod(buf: &[u8]) -> ImageResult TiffEncoder { /// Create a new encoder that writes its output to `w` - pub fn new(w: W) -> TiffEncoder { - TiffEncoder { w } + pub fn new(writer: W) -> TiffEncoder { + TiffEncoder { + writer, + compression: CompressionType::default().into_tiff(), + } + } + + /// Create a new encoder that writes its output with [`CompressionType`] `compression`. + pub fn new_with_compression(writer: W, comp: CompressionType) -> Self { + TiffEncoder { + writer, + compression: comp.into_tiff(), + } } /// Encodes the image `image` that has dimensions `width` and `height` and `ColorType` `c`. @@ -437,7 +510,12 @@ impl TiffEncoder { buf.len(), ); let mut encoder = - tiff::encoder::TiffEncoder::new(self.w).map_err(ImageError::from_tiff_encode)?; + tiff::encoder::TiffEncoder::new(self.writer).map_err(ImageError::from_tiff_encode)?; + + if !matches!(self.compression, Compression::Uncompressed) { + encoder = encoder.with_compression(self.compression); + } + match color_type { ExtendedColorType::L8 => encoder.write_image::(width, height, buf), ExtendedColorType::Rgb8 => encoder.write_image::(width, height, buf), diff --git a/tests/reference_images.rs b/tests/reference_images.rs index a682317d87..05f2e2e861 100644 --- a/tests/reference_images.rs +++ b/tests/reference_images.rs @@ -39,6 +39,59 @@ where } } +#[cfg(feature = "tiff")] +#[test] +fn tiff_compress_deflate() { + use image::codecs::tiff::{CompressionType, TiffDeflateLevel, TiffEncoder}; + + process_images(IMAGE_DIR, Some("tiff"), |_base, path, _| { + println!("compress_images {}", path.display()); + let img = match image::open(&path) { + Ok(img) => img, + // Do not fail on unsupported error + // This might happen because the testsuite contains unsupported images + // or because a specific decoder included via a feature. + Err(image::ImageError::Unsupported(e)) => { + println!("UNSUPPORTED {}: {e}", path.display()); + return; + } + Err(err) => panic!("decoding of {path:?} failed with: {err}"), + }; + + let encoder = TiffEncoder::new_with_compression( + std::io::Cursor::new(vec![]), + CompressionType::Deflate(TiffDeflateLevel::Balanced), + ); + + img.write_with_encoder(encoder).unwrap(); + }) +} + +#[cfg(feature = "tiff")] +#[test] +fn tiff_compress_lzw() { + use image::codecs::tiff::{CompressionType, TiffEncoder}; + + process_images(IMAGE_DIR, Some("tiff"), |_base, path, _| { + println!("compress_images {}", path.display()); + let img = match image::open(&path) { + Ok(img) => img, + // Do not fail on unsupported error + // This might happen because the testsuite contains unsupported images + // or because a specific decoder included via a feature. + Err(image::ImageError::Unsupported(e)) => { + println!("UNSUPPORTED {}: {e}", path.display()); + return; + } + Err(err) => panic!("decoding of {path:?} failed with: {err}"), + }; + + let encoder = + TiffEncoder::new_with_compression(std::io::Cursor::new(vec![]), CompressionType::Lzw); + img.write_with_encoder(encoder).unwrap(); + }) +} + #[cfg(feature = "png")] #[test] fn render_images() {