From 0ca1276b83383bfd6fd115933b92ffc9b4a732f5 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:58:40 +0200 Subject: [PATCH 1/4] Test compression of tiff images --- tests/reference_images.rs | 63 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/reference_images.rs b/tests/reference_images.rs index a682317d87..a9395d3b1d 100644 --- a/tests/reference_images.rs +++ b/tests/reference_images.rs @@ -39,6 +39,69 @@ where } } +#[cfg(feature = "tiff")] +#[test] +fn compress_deflate() { + use image::codecs::tiff::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 mut out_path = base.clone(); + out_path.push(OUTPUT_DIR); + out_path.push("compressed_tiff"); + out_path.push("deflate"); + std::fs::create_dir_all(&out_path).unwrap(); + out_path.push(path.file_name().unwrap()); + let encoder = TiffEncoder::new(fs::File::create(&out_path).unwrap()).with_compression( + tiff::encoder::Compression::Deflate(tiff::encoder::DeflateLevel::Balanced), + ); + img.write_with_encoder(encoder).unwrap(); + }) +} + +#[cfg(feature = "tiff")] +#[test] +fn compress_lzw() { + use image::codecs::tiff::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 mut out_path = base.clone(); + out_path.push(OUTPUT_DIR); + out_path.push("compressed_tiff"); + out_path.push("lzw"); + std::fs::create_dir_all(&out_path).unwrap(); + out_path.push(path.file_name().unwrap()); + let encoder = TiffEncoder::new(fs::File::create(&out_path).unwrap()) + .with_compression(tiff::encoder::Compression::Lzw); + img.write_with_encoder(encoder).unwrap(); + }) +} + #[cfg(feature = "png")] #[test] fn render_images() { From ece042dbd9985a1ce2195915fa80a06911f7c0fc Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:52:46 +0200 Subject: [PATCH 2/4] Add compression support to Tiff encoder --- src/codecs/tiff.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/codecs/tiff.rs b/src/codecs/tiff.rs index 2abbc53bd2..09b98b17d1 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; use tiff::tags::Tag; use crate::color::{ColorType, ExtendedColorType}; @@ -363,6 +364,7 @@ impl ImageDecoder for TiffDecoder { /// Encoder for tiff images pub struct TiffEncoder { w: W, + comp: Compression, } fn cmyk_to_rgb(cmyk: &[u8]) -> [u8; 3] { @@ -408,7 +410,16 @@ 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 } + TiffEncoder { + w, + comp: Compression::default(), + } + } + + /// Set the image compression setting + pub fn with_compression(mut self, comp: Compression) -> Self { + self.comp = comp; + self } /// Encodes the image `image` that has dimensions `width` and `height` and `ColorType` `c`. @@ -438,6 +449,14 @@ impl TiffEncoder { ); let mut encoder = tiff::encoder::TiffEncoder::new(self.w).map_err(ImageError::from_tiff_encode)?; + + match self.comp { + Compression::Uncompressed => {} + Compression::Lzw | Compression::Deflate(_) | Compression::Packbits => { + encoder = encoder.with_compression(self.comp); + } + } + match color_type { ExtendedColorType::L8 => encoder.write_image::(width, height, buf), ExtendedColorType::Rgb8 => encoder.write_image::(width, height, buf), From 5363c9a3852b0ea53bfb3e07dae0cd96d8773c94 Mon Sep 17 00:00:00 2001 From: "Sunip K. Mukherjee" Date: Sat, 10 Aug 2024 11:52:47 -0400 Subject: [PATCH 3/4] Tiff crate internal compression types hidden in public interface --- src/codecs/tiff.rs | 76 ++++++++++++++++++++++++++++++++++----- tests/reference_images.rs | 15 ++++---- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/codecs/tiff.rs b/src/codecs/tiff.rs index 09b98b17d1..b13a75adf2 100644 --- a/src/codecs/tiff.rs +++ b/src/codecs/tiff.rs @@ -10,7 +10,7 @@ use std::marker::PhantomData; use std::mem; use tiff::decoder::{Decoder, DecodingResult}; -use tiff::encoder::Compression; +use tiff::encoder::{Compression, DeflateLevel}; use tiff::tags::Tag; use crate::color::{ColorType, ExtendedColorType}; @@ -367,6 +367,60 @@ pub struct TiffEncoder { comp: Compression, } +/// Compression types supported by the TIFF format +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CompressionType { + /// No compression + Uncompressed, + /// LZW compression + Lzw, + /// Deflate compression + Deflate(TiffDeflateLevel), + /// Bit packing compression + Packbits, +} + +impl Default for CompressionType { + fn default() -> Self { + CompressionType::Deflate(Default::default()) + } +} + +/// 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] { let c = f32::from(cmyk[0]); let m = f32::from(cmyk[1]); @@ -410,18 +464,24 @@ 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 { + let comp = CompressionType::default().into_tiff(); + TiffEncoder { w, comp } + } + + /// Create a new encoder that writes its output to `w` with `CompressionType` `compression`. + /// + /// It is best to view the options as a _hint_ to the implementation on the smallest or fastest + /// option for encoding a particular image. That is, using options that map directly to a TIFF + /// image parameter will use this parameter where possible. But variants that have no direct + /// mapping may be interpreted differently in minor versions. The exact output is expressly + /// __not__ part of the SemVer stability guarantee. + pub fn new_with_compression(w: W, comp: CompressionType) -> Self { TiffEncoder { w, - comp: Compression::default(), + comp: comp.into_tiff(), } } - /// Set the image compression setting - pub fn with_compression(mut self, comp: Compression) -> Self { - self.comp = comp; - self - } - /// Encodes the image `image` that has dimensions `width` and `height` and `ColorType` `c`. /// /// 16-bit types assume the buffer is native endian. diff --git a/tests/reference_images.rs b/tests/reference_images.rs index a9395d3b1d..bb85cd8405 100644 --- a/tests/reference_images.rs +++ b/tests/reference_images.rs @@ -42,7 +42,7 @@ where #[cfg(feature = "tiff")] #[test] fn compress_deflate() { - use image::codecs::tiff::TiffEncoder; + use image::codecs::tiff::{CompressionType, TiffDeflateLevel, TiffEncoder}; process_images(IMAGE_DIR, Some("tiff"), |base, path, _| { println!("compress_images {}", path.display()); @@ -64,8 +64,9 @@ fn compress_deflate() { out_path.push("deflate"); std::fs::create_dir_all(&out_path).unwrap(); out_path.push(path.file_name().unwrap()); - let encoder = TiffEncoder::new(fs::File::create(&out_path).unwrap()).with_compression( - tiff::encoder::Compression::Deflate(tiff::encoder::DeflateLevel::Balanced), + let encoder = TiffEncoder::new_with_compression( + fs::File::create(&out_path).unwrap(), + CompressionType::Deflate(TiffDeflateLevel::Balanced), ); img.write_with_encoder(encoder).unwrap(); }) @@ -74,7 +75,7 @@ fn compress_deflate() { #[cfg(feature = "tiff")] #[test] fn compress_lzw() { - use image::codecs::tiff::TiffEncoder; + use image::codecs::tiff::{CompressionType, TiffEncoder}; process_images(IMAGE_DIR, Some("tiff"), |base, path, _| { println!("compress_images {}", path.display()); @@ -96,8 +97,10 @@ fn compress_lzw() { out_path.push("lzw"); std::fs::create_dir_all(&out_path).unwrap(); out_path.push(path.file_name().unwrap()); - let encoder = TiffEncoder::new(fs::File::create(&out_path).unwrap()) - .with_compression(tiff::encoder::Compression::Lzw); + let encoder = TiffEncoder::new_with_compression( + fs::File::create(&out_path).unwrap(), + CompressionType::Lzw, + ); img.write_with_encoder(encoder).unwrap(); }) } From 84c6abaab406a8dd774999e88d11ffcfbaca02ad Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:16:51 +0200 Subject: [PATCH 4/4] Style nits for tiff encoder --- src/codecs/tiff.rs | 43 +++++++++++++++++++-------------------- tests/reference_images.rs | 29 ++++++++------------------ 2 files changed, 29 insertions(+), 43 deletions(-) diff --git a/src/codecs/tiff.rs b/src/codecs/tiff.rs index b13a75adf2..a95058a969 100644 --- a/src/codecs/tiff.rs +++ b/src/codecs/tiff.rs @@ -363,18 +363,24 @@ impl ImageDecoder for TiffDecoder { /// Encoder for tiff images pub struct TiffEncoder { - w: W, - comp: Compression, + 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, @@ -382,7 +388,7 @@ pub enum CompressionType { impl Default for CompressionType { fn default() -> Self { - CompressionType::Deflate(Default::default()) + CompressionType::Lzw } } @@ -463,22 +469,18 @@ 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 { - let comp = CompressionType::default().into_tiff(); - TiffEncoder { w, comp } + pub fn new(writer: W) -> TiffEncoder { + TiffEncoder { + writer, + compression: CompressionType::default().into_tiff(), + } } - /// Create a new encoder that writes its output to `w` with `CompressionType` `compression`. - /// - /// It is best to view the options as a _hint_ to the implementation on the smallest or fastest - /// option for encoding a particular image. That is, using options that map directly to a TIFF - /// image parameter will use this parameter where possible. But variants that have no direct - /// mapping may be interpreted differently in minor versions. The exact output is expressly - /// __not__ part of the SemVer stability guarantee. - pub fn new_with_compression(w: W, comp: CompressionType) -> Self { + /// Create a new encoder that writes its output with [`CompressionType`] `compression`. + pub fn new_with_compression(writer: W, comp: CompressionType) -> Self { TiffEncoder { - w, - comp: comp.into_tiff(), + writer, + compression: comp.into_tiff(), } } @@ -508,13 +510,10 @@ 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)?; - match self.comp { - Compression::Uncompressed => {} - Compression::Lzw | Compression::Deflate(_) | Compression::Packbits => { - encoder = encoder.with_compression(self.comp); - } + if !matches!(self.compression, Compression::Uncompressed) { + encoder = encoder.with_compression(self.compression); } match color_type { diff --git a/tests/reference_images.rs b/tests/reference_images.rs index bb85cd8405..05f2e2e861 100644 --- a/tests/reference_images.rs +++ b/tests/reference_images.rs @@ -41,10 +41,10 @@ where #[cfg(feature = "tiff")] #[test] -fn compress_deflate() { +fn tiff_compress_deflate() { use image::codecs::tiff::{CompressionType, TiffDeflateLevel, TiffEncoder}; - process_images(IMAGE_DIR, Some("tiff"), |base, path, _| { + process_images(IMAGE_DIR, Some("tiff"), |_base, path, _| { println!("compress_images {}", path.display()); let img = match image::open(&path) { Ok(img) => img, @@ -58,26 +58,21 @@ fn compress_deflate() { Err(err) => panic!("decoding of {path:?} failed with: {err}"), }; - let mut out_path = base.clone(); - out_path.push(OUTPUT_DIR); - out_path.push("compressed_tiff"); - out_path.push("deflate"); - std::fs::create_dir_all(&out_path).unwrap(); - out_path.push(path.file_name().unwrap()); let encoder = TiffEncoder::new_with_compression( - fs::File::create(&out_path).unwrap(), + std::io::Cursor::new(vec![]), CompressionType::Deflate(TiffDeflateLevel::Balanced), ); + img.write_with_encoder(encoder).unwrap(); }) } #[cfg(feature = "tiff")] #[test] -fn compress_lzw() { +fn tiff_compress_lzw() { use image::codecs::tiff::{CompressionType, TiffEncoder}; - process_images(IMAGE_DIR, Some("tiff"), |base, path, _| { + process_images(IMAGE_DIR, Some("tiff"), |_base, path, _| { println!("compress_images {}", path.display()); let img = match image::open(&path) { Ok(img) => img, @@ -91,16 +86,8 @@ fn compress_lzw() { Err(err) => panic!("decoding of {path:?} failed with: {err}"), }; - let mut out_path = base.clone(); - out_path.push(OUTPUT_DIR); - out_path.push("compressed_tiff"); - out_path.push("lzw"); - std::fs::create_dir_all(&out_path).unwrap(); - out_path.push(path.file_name().unwrap()); - let encoder = TiffEncoder::new_with_compression( - fs::File::create(&out_path).unwrap(), - CompressionType::Lzw, - ); + let encoder = + TiffEncoder::new_with_compression(std::io::Cursor::new(vec![]), CompressionType::Lzw); img.write_with_encoder(encoder).unwrap(); }) }