Skip to content

Added compression and f32 support for TIFF encoding/decoding. #2251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 82 additions & 4 deletions src/codecs/tiff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -362,7 +363,68 @@ impl<R: BufRead + Seek> ImageDecoder for TiffDecoder<R> {

/// Encoder for tiff images
pub struct TiffEncoder<W> {
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] {
Expand Down Expand Up @@ -407,8 +469,19 @@ fn u8_slice_as_pod<P: bytemuck::Pod>(buf: &[u8]) -> ImageResult<std::borrow::Cow

impl<W: Write + Seek> TiffEncoder<W> {
/// Create a new encoder that writes its output to `w`
pub fn new(w: W) -> TiffEncoder<W> {
TiffEncoder { w }
pub fn new(writer: W) -> TiffEncoder<W> {
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`.
Expand Down Expand Up @@ -437,7 +510,12 @@ impl<W: Write + Seek> TiffEncoder<W> {
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::<Gray8>(width, height, buf),
ExtendedColorType::Rgb8 => encoder.write_image::<RGB8>(width, height, buf),
Expand Down
53 changes: 53 additions & 0 deletions tests/reference_images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How long does this test take? I could imagine that compressing all the input files with balanced compression could take a while, though I don't know how many / how large they are.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0.35s and 0.53s respectively. Without the IO for compressed output it is 0.33 and 0.45 so I'll remove the file write. Seems fine although may we want to restrict it to a particular set of files with enough coverage at some point.

);

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() {
Expand Down
Loading