From cbdfc35dcc4fac21d5786b34960cfd52b64886ad Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Skutnik Date: Sat, 3 Aug 2024 18:07:12 +0400 Subject: [PATCH 01/12] Added predictor for colortype --- src/encoder/colortype.rs | 88 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/encoder/colortype.rs b/src/encoder/colortype.rs index 1946dafa..b08e13a8 100644 --- a/src/encoder/colortype.rs +++ b/src/encoder/colortype.rs @@ -1,5 +1,21 @@ use crate::tags::{PhotometricInterpretation, SampleFormat}; +macro_rules! integer_horizontal_predict { + () => { + fn horizontal_predict(row: &[Self::Inner]) -> Vec { + let sample_size = Self::SAMPLE_FORMAT.len(); + let mut result = Vec::with_capacity(row.len()); + + result.extend_from_slice(&row[0..=sample_size - 1]); + result.extend( + (sample_size..row.len()).map(|i| row[i].wrapping_sub(row[i - sample_size])), + ); + + result + } + }; +} + /// Trait for different colortypes that can be encoded. pub trait ColorType { /// The type of each sample of this colortype @@ -10,6 +26,8 @@ pub trait ColorType { const BITS_PER_SAMPLE: &'static [u16]; /// The value of the tiff tag `SampleFormat` const SAMPLE_FORMAT: &'static [SampleFormat]; + + fn horizontal_predict(row: &[Self::Inner]) -> Vec; } pub struct Gray8; @@ -18,6 +36,8 @@ impl ColorType for Gray8 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const BITS_PER_SAMPLE: &'static [u16] = &[8]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint]; + + integer_horizontal_predict!(); } pub struct GrayI8; @@ -26,6 +46,8 @@ impl ColorType for GrayI8 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const BITS_PER_SAMPLE: &'static [u16] = &[8]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int]; + + integer_horizontal_predict!(); } pub struct Gray16; @@ -34,6 +56,8 @@ impl ColorType for Gray16 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const BITS_PER_SAMPLE: &'static [u16] = &[16]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint]; + + integer_horizontal_predict!(); } pub struct GrayI16; @@ -42,6 +66,8 @@ impl ColorType for GrayI16 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const BITS_PER_SAMPLE: &'static [u16] = &[16]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int]; + + integer_horizontal_predict!(); } pub struct Gray32; @@ -50,6 +76,8 @@ impl ColorType for Gray32 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const BITS_PER_SAMPLE: &'static [u16] = &[32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint]; + + integer_horizontal_predict!(); } pub struct GrayI32; @@ -58,6 +86,8 @@ impl ColorType for GrayI32 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const BITS_PER_SAMPLE: &'static [u16] = &[32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int]; + + integer_horizontal_predict!(); } pub struct Gray32Float; @@ -66,6 +96,10 @@ impl ColorType for Gray32Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const BITS_PER_SAMPLE: &'static [u16] = &[32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; + + fn horizontal_predict(_: &[Self::Inner]) -> Vec { + unreachable!() + } } pub struct Gray64; @@ -74,6 +108,8 @@ impl ColorType for Gray64 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const BITS_PER_SAMPLE: &'static [u16] = &[64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint]; + + integer_horizontal_predict!(); } pub struct GrayI64; @@ -82,6 +118,8 @@ impl ColorType for GrayI64 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const BITS_PER_SAMPLE: &'static [u16] = &[64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int]; + + integer_horizontal_predict!(); } pub struct Gray64Float; @@ -90,6 +128,10 @@ impl ColorType for Gray64Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; const BITS_PER_SAMPLE: &'static [u16] = &[64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; + + fn horizontal_predict(_: &[Self::Inner]) -> Vec { + unreachable!() + } } pub struct RGB8; @@ -98,6 +140,8 @@ impl ColorType for RGB8 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; + + integer_horizontal_predict!(); } pub struct RGB16; @@ -106,6 +150,8 @@ impl ColorType for RGB16 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; + + integer_horizontal_predict!(); } pub struct RGB32; @@ -114,6 +160,8 @@ impl ColorType for RGB32 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; + + integer_horizontal_predict!(); } pub struct RGB32Float; @@ -122,6 +170,9 @@ impl ColorType for RGB32Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; + fn horizontal_predict(_: &[Self::Inner]) -> Vec { + unreachable!() + } } pub struct RGB64; @@ -130,6 +181,8 @@ impl ColorType for RGB64 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; + + integer_horizontal_predict!(); } pub struct RGB64Float; @@ -138,6 +191,9 @@ impl ColorType for RGB64Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; + fn horizontal_predict(_: &[Self::Inner]) -> Vec { + unreachable!() + } } pub struct RGBA8; @@ -146,6 +202,8 @@ impl ColorType for RGBA8 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; + + integer_horizontal_predict!(); } pub struct RGBA16; @@ -154,6 +212,8 @@ impl ColorType for RGBA16 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; + + integer_horizontal_predict!(); } pub struct RGBA32; @@ -162,6 +222,8 @@ impl ColorType for RGBA32 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; + + integer_horizontal_predict!(); } pub struct RGBA32Float; @@ -170,6 +232,9 @@ impl ColorType for RGBA32Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; + fn horizontal_predict(_: &[Self::Inner]) -> Vec { + unreachable!() + } } pub struct RGBA64; @@ -178,6 +243,8 @@ impl ColorType for RGBA64 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; + + integer_horizontal_predict!(); } pub struct RGBA64Float; @@ -186,6 +253,9 @@ impl ColorType for RGBA64Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; + fn horizontal_predict(_: &[Self::Inner]) -> Vec { + unreachable!() + } } pub struct CMYK8; @@ -194,6 +264,8 @@ impl ColorType for CMYK8 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; + + integer_horizontal_predict!(); } pub struct CMYK16; @@ -202,6 +274,8 @@ impl ColorType for CMYK16 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; + + integer_horizontal_predict!(); } pub struct CMYK32; @@ -210,6 +284,8 @@ impl ColorType for CMYK32 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; + + integer_horizontal_predict!(); } pub struct CMYK32Float; @@ -218,6 +294,10 @@ impl ColorType for CMYK32Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; + + fn horizontal_predict(_: &[Self::Inner]) -> Vec { + unreachable!() + } } pub struct CMYK64; @@ -226,6 +306,8 @@ impl ColorType for CMYK64 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4]; + + integer_horizontal_predict!(); } pub struct CMYK64Float; @@ -234,6 +316,10 @@ impl ColorType for CMYK64Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; + + fn horizontal_predict(_: &[Self::Inner]) -> Vec { + unreachable!() + } } pub struct YCbCr8; @@ -242,4 +328,6 @@ impl ColorType for YCbCr8 { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::YCbCr; const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3]; + + integer_horizontal_predict!(); } From d3e355400a8a6f8652c89497939d0c88afdce695 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Skutnik Date: Sat, 3 Aug 2024 19:26:03 +0400 Subject: [PATCH 02/12] Add predictor docs --- src/encoder/mod.rs | 43 +++++++++++++++++++++++++++++++++++++++---- src/tags.rs | 6 ++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 152a77a6..7b62dc29 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -24,6 +24,16 @@ use self::colortype::*; use self::compression::*; use self::writer::*; +/// Type of prediction to prepare the image with. +/// +/// Image data can be very unpredictable, and thus very hard to compress. Predictors are simple +/// passes ran over the image data to prepare it for compression. This is mostly used for LZW +/// compression, where using [Predictor::Horizontal] we see a 35% improvement in compression +/// ratio over the unpredicted compression ! +/// +/// [Predictor::FloatingPoint] is currently not supported. +pub type Predictor = crate::tags::Predictor; + /// Encoder for Tiff and BigTiff files. /// /// With this type you can get a `DirectoryEncoder` or a `ImageEncoder` @@ -52,6 +62,7 @@ use self::writer::*; pub struct TiffEncoder { writer: TiffWriter, kind: PhantomData, + predictor: Predictor, } /// Constructor functions to create standard Tiff files. @@ -83,6 +94,7 @@ impl TiffEncoder { let mut encoder = TiffEncoder { writer: TiffWriter::new(writer), kind: PhantomData, + predictor: Predictor::None, }; K::write_header(&mut encoder.writer)?; @@ -90,6 +102,14 @@ impl TiffEncoder { Ok(encoder) } + /// Set the predictor used to simplify the file before writing it. This is very useful when + /// writing a file compressed using LZW + pub fn set_predictor(mut self, predictor: Predictor) -> TiffResult { + self.predictor = predictor; + + Ok(self) + } + /// Create a [`DirectoryEncoder`] to encode an ifd directory. pub fn new_directory(&mut self) -> TiffResult> { DirectoryEncoder::new(&mut self.writer) @@ -113,7 +133,7 @@ impl TiffEncoder { compression: D, ) -> TiffResult> { let encoder = DirectoryEncoder::new(&mut self.writer)?; - ImageEncoder::with_compression(encoder, width, height, compression) + ImageEncoder::with_compression(encoder, width, height, compression, self.predictor) } /// Convenience function to write an entire image from memory. @@ -144,7 +164,7 @@ impl TiffEncoder { { let encoder = DirectoryEncoder::new(&mut self.writer)?; let image: ImageEncoder = - ImageEncoder::with_compression(encoder, width, height, compression)?; + ImageEncoder::with_compression(encoder, width, height, compression, self.predictor)?; image.write_data(data) } } @@ -330,6 +350,7 @@ pub struct ImageEncoder< strip_byte_count: Vec, dropped: bool, compression: D, + predictor: Predictor, _phantom: ::std::marker::PhantomData, } @@ -340,7 +361,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression> where D: Default, { - Self::with_compression(encoder, width, height, D::default()) + Self::with_compression(encoder, width, height, D::default(), Predictor::None) } fn with_compression( @@ -348,6 +369,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression> width: u32, height: u32, compression: D, + predictor: Predictor, ) -> TiffResult { if width == 0 || height == 0 { return Err(TiffError::FormatError(TiffFormatError::InvalidDimensions( @@ -372,6 +394,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression> encoder.write_tag(Tag::ImageWidth, width)?; encoder.write_tag(Tag::ImageLength, height)?; encoder.write_tag(Tag::Compression, D::COMPRESSION_METHOD.to_u16())?; + encoder.write_tag(Tag::Predictor, predictor.to_u16())?; encoder.write_tag(Tag::BitsPerSample, ::BITS_PER_SAMPLE)?; let sample_format: Vec<_> = ::SAMPLE_FORMAT.iter().map(|s| s.to_u16()).collect(); @@ -400,6 +423,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression> strip_byte_count: Vec::new(), dropped: false, compression, + predictor, _phantom: ::std::marker::PhantomData, }) } @@ -432,7 +456,18 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression> } // Write the (possible compressed) data to the encoder. - let offset = self.encoder.write_data(value)?; + let offset = match self.predictor { + Predictor::None => self.encoder.write_data(value)?, + Predictor::Horizontal => { + let predicted: Vec = value + .chunks(self.row_samples as usize) + .flat_map(|row| T::horizontal_predict(row).into_iter()) + .collect(); + self.encoder.write_data(predicted.as_slice())? + } + _ => unreachable!(), + }; + let byte_count = self.encoder.last_written() as usize; self.strip_offsets.push(K::convert_offset(offset)?); diff --git a/src/tags.rs b/src/tags.rs index 3b86dc1f..6c18fa52 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -209,8 +209,14 @@ pub enum PlanarConfiguration(u16) { tags! { pub enum Predictor(u16) { + /// No changes were made to the data None = 1, + /// The images' rows were processed to contain the difference of each pixel from the previous one. + /// + /// This means that instead of having in order `[r1, g1. b1, r2, g2 ...]` you will find + /// `[r1, g1, b1, r2-r1, g2-g1, b2-b1, r3-r2, g3-g2, ...]` Horizontal = 2, + /// Not currently supported FloatingPoint = 3, } } From 829d433d8a13a621a264efe6ef30fbebb7101006 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Skutnik Date: Sat, 3 Aug 2024 20:48:47 +0400 Subject: [PATCH 03/12] Remove unnecessary compression generic argument --- src/encoder/mod.rs | 111 ++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 58 deletions(-) diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 7b62dc29..25d8cb86 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -21,6 +21,7 @@ mod tiff_value; mod writer; use self::colortype::*; +use self::compression::Compression as Comp; use self::compression::*; use self::writer::*; @@ -33,6 +34,36 @@ use self::writer::*; /// /// [Predictor::FloatingPoint] is currently not supported. pub type Predictor = crate::tags::Predictor; +pub type DeflateLevel = compression::DeflateLevel; + +#[derive(Default, Clone, Copy)] +pub enum Compression { + #[default] + Uncompressed, + Lzw, + Deflate(DeflateLevel), + Packbits, +} + +impl Compression { + fn tag(&self) -> CompressionMethod { + match self { + Compression::Uncompressed => CompressionMethod::None, + Compression::Lzw => CompressionMethod::LZW, + Compression::Deflate(_) => CompressionMethod::Deflate, + Compression::Packbits => CompressionMethod::PackBits, + } + } + + fn get_algorithm(&self) -> Compressor { + match self { + Compression::Uncompressed => compression::Uncompressed {}.get_algorithm(), + Compression::Lzw => compression::Lzw {}.get_algorithm(), + Compression::Deflate(level) => compression::Deflate::with_level(*level).get_algorithm(), + Compression::Packbits => compression::Packbits {}.get_algorithm(), + } + } +} /// Encoder for Tiff and BigTiff files. /// @@ -63,6 +94,7 @@ pub struct TiffEncoder { writer: TiffWriter, kind: PhantomData, predictor: Predictor, + compression: Compression, } /// Constructor functions to create standard Tiff files. @@ -95,6 +127,7 @@ impl TiffEncoder { writer: TiffWriter::new(writer), kind: PhantomData, predictor: Predictor::None, + compression: Compression::Uncompressed, }; K::write_header(&mut encoder.writer)?; @@ -104,10 +137,16 @@ impl TiffEncoder { /// Set the predictor used to simplify the file before writing it. This is very useful when /// writing a file compressed using LZW - pub fn set_predictor(mut self, predictor: Predictor) -> TiffResult { + pub fn with_predictor(mut self, predictor: Predictor) -> Self { self.predictor = predictor; - Ok(self) + self + } + + pub fn with_compression(mut self, compression: Compression) -> Self { + self.compression = compression; + + self } /// Create a [`DirectoryEncoder`] to encode an ifd directory. @@ -120,20 +159,9 @@ impl TiffEncoder { &mut self, width: u32, height: u32, - ) -> TiffResult> { - let encoder = DirectoryEncoder::new(&mut self.writer)?; - ImageEncoder::new(encoder, width, height) - } - - /// Create an [`ImageEncoder`] to encode an image one slice at a time. - pub fn new_image_with_compression( - &mut self, - width: u32, - height: u32, - compression: D, - ) -> TiffResult> { + ) -> TiffResult> { let encoder = DirectoryEncoder::new(&mut self.writer)?; - ImageEncoder::with_compression(encoder, width, height, compression, self.predictor) + ImageEncoder::new(encoder, width, height, self.compression, self.predictor) } /// Convenience function to write an entire image from memory. @@ -147,24 +175,8 @@ impl TiffEncoder { [C::Inner]: TiffValue, { let encoder = DirectoryEncoder::new(&mut self.writer)?; - let image: ImageEncoder = ImageEncoder::new(encoder, width, height)?; - image.write_data(data) - } - - /// Convenience function to write an entire image from memory with a given compression. - pub fn write_image_with_compression( - &mut self, - width: u32, - height: u32, - compression: D, - data: &[C::Inner], - ) -> TiffResult<()> - where - [C::Inner]: TiffValue, - { - let encoder = DirectoryEncoder::new(&mut self.writer)?; - let image: ImageEncoder = - ImageEncoder::with_compression(encoder, width, height, compression, self.predictor)?; + let image: ImageEncoder = + ImageEncoder::new(encoder, width, height, self.compression, self.predictor)?; image.write_data(data) } } @@ -332,13 +344,7 @@ impl<'a, W: Write + Seek, K: TiffKind> Drop for DirectoryEncoder<'a, W, K> { /// # } /// ``` /// You can also call write_data function wich will encode by strip and finish -pub struct ImageEncoder< - 'a, - W: 'a + Write + Seek, - C: ColorType, - K: TiffKind, - D: Compression = Uncompressed, -> { +pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> { encoder: DirectoryEncoder<'a, W, K>, strip_idx: u64, strip_count: u64, @@ -349,26 +355,17 @@ pub struct ImageEncoder< strip_offsets: Vec, strip_byte_count: Vec, dropped: bool, - compression: D, + compression: Compression, predictor: Predictor, _phantom: ::std::marker::PhantomData, } -impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression> - ImageEncoder<'a, W, T, K, D> -{ - fn new(encoder: DirectoryEncoder<'a, W, K>, width: u32, height: u32) -> TiffResult - where - D: Default, - { - Self::with_compression(encoder, width, height, D::default(), Predictor::None) - } - - fn with_compression( +impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> { + fn new( mut encoder: DirectoryEncoder<'a, W, K>, width: u32, height: u32, - compression: D, + compression: Compression, predictor: Predictor, ) -> TiffResult { if width == 0 || height == 0 { @@ -383,7 +380,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression> // Limit the strip size to prevent potential memory and security issues. // Also keep the multiple strip handling 'oiled' let rows_per_strip = { - match D::COMPRESSION_METHOD { + match compression.tag() { CompressionMethod::PackBits => 1, // Each row must be packed separately. Do not compress across row boundaries _ => (1_000_000 + row_bytes - 1) / row_bytes, } @@ -393,7 +390,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression> encoder.write_tag(Tag::ImageWidth, width)?; encoder.write_tag(Tag::ImageLength, height)?; - encoder.write_tag(Tag::Compression, D::COMPRESSION_METHOD.to_u16())?; + encoder.write_tag(Tag::Compression, compression.tag().to_u16())?; encoder.write_tag(Tag::Predictor, predictor.to_u16())?; encoder.write_tag(Tag::BitsPerSample, ::BITS_PER_SAMPLE)?; @@ -587,9 +584,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression> } } -impl<'a, W: Write + Seek, C: ColorType, K: TiffKind, D: Compression> Drop - for ImageEncoder<'a, W, C, K, D> -{ +impl<'a, W: Write + Seek, C: ColorType, K: TiffKind> Drop for ImageEncoder<'a, W, C, K> { fn drop(&mut self) { if !self.dropped { let _ = self.finish_internal(); From 3ffc54a438a77a6699c19a136201e595e457e0af Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Skutnik Date: Sat, 3 Aug 2024 23:00:33 +0400 Subject: [PATCH 04/12] Add predictor/compression sanity checks --- src/encoder/mod.rs | 30 +++++++++++++++++++++++++----- src/error.rs | 12 ++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 25d8cb86..39090fb6 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -10,8 +10,8 @@ use std::{ }; use crate::{ - error::TiffResult, - tags::{CompressionMethod, ResolutionUnit, Tag}, + error::{TiffResult, UsageError}, + tags::{CompressionMethod, ResolutionUnit, SampleFormat, Tag}, TiffError, TiffFormatError, }; @@ -36,7 +36,7 @@ use self::writer::*; pub type Predictor = crate::tags::Predictor; pub type DeflateLevel = compression::DeflateLevel; -#[derive(Default, Clone, Copy)] +#[derive(Default, Clone, Copy, PartialEq)] pub enum Compression { #[default] Uncompressed, @@ -135,14 +135,17 @@ impl TiffEncoder { Ok(encoder) } - /// Set the predictor used to simplify the file before writing it. This is very useful when - /// writing a file compressed using LZW + /// Set the predictor to use + /// + /// A predictor is used to simplify the file before writing it. This is very + /// useful when writing a file compressed using LZW as it can improve efficiency pub fn with_predictor(mut self, predictor: Predictor) -> Self { self.predictor = predictor; self } + /// Set the compression method to use pub fn with_compression(mut self, compression: Compression) -> Self { self.compression = compression; @@ -361,6 +364,21 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> { } impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> { + fn sanity_check(compression: Compression, predictor: Predictor) -> TiffResult<()> { + match (predictor, compression, T::SAMPLE_FORMAT[0]) { + (Predictor::Horizontal, Compression::Uncompressed, _) => Err(TiffError::UsageError( + UsageError::PredictorCompressionMismatch, + )), + (Predictor::Horizontal, _, SampleFormat::IEEEFP | SampleFormat::Void) => { + Err(TiffError::UsageError(UsageError::PredictorIncompatible)) + } + (Predictor::FloatingPoint, _, _) => { + Err(TiffError::UsageError(UsageError::PredictorUnavailable)) + } + _ => Ok(()), + } + } + fn new( mut encoder: DirectoryEncoder<'a, W, K>, width: u32, @@ -374,6 +392,8 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, ))); } + Self::sanity_check(compression, predictor)?; + let row_samples = u64::from(width) * u64::try_from(::BITS_PER_SAMPLE.len())?; let row_bytes = row_samples * u64::from(::BYTE_LEN); diff --git a/src/error.rs b/src/error.rs index 7dc781e0..955da670 100644 --- a/src/error.rs +++ b/src/error.rs @@ -236,6 +236,9 @@ impl fmt::Display for TiffUnsupportedError { pub enum UsageError { InvalidChunkType(ChunkType, ChunkType), InvalidChunkIndex(u32), + PredictorCompressionMismatch, + PredictorIncompatible, + PredictorUnavailable, } impl fmt::Display for UsageError { @@ -250,6 +253,15 @@ impl fmt::Display for UsageError { ) } InvalidChunkIndex(index) => write!(fmt, "Image chunk index ({}) requested.", index), + PredictorCompressionMismatch => write!( + fmt, + "The requested predictor is not compatible with the requested compression" + ), + PredictorIncompatible => write!( + fmt, + "The requested predictor is not compatible with the image's format" + ), + PredictorUnavailable => write!(fmt, "The requested predictor is not available"), } } } From ef80908737282db7ac67df6fc5ab0d0439660a26 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Skutnik Date: Sat, 3 Aug 2024 23:35:51 +0400 Subject: [PATCH 05/12] Update tests --- tests/encode_images_with_compression.rs | 33 ++++++++++++------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/tests/encode_images_with_compression.rs b/tests/encode_images_with_compression.rs index 1cf178f2..2855c516 100644 --- a/tests/encode_images_with_compression.rs +++ b/tests/encode_images_with_compression.rs @@ -6,7 +6,7 @@ use tiff::{ encoder::{ colortype::{self, ColorType}, compression::*, - TiffEncoder, TiffValue, + Compression, TiffEncoder, TiffValue, }, }; @@ -18,15 +18,12 @@ trait TestImage: From: fn reference_data(&self) -> &[::Inner]; fn generate_pixel(x: u32, y: u32) -> [::Inner; NUM_CHANNELS]; - fn compress( - &self, - encoder: &mut TiffEncoder, - compression: C, - ) where + fn compress(&self, encoder: &mut TiffEncoder) + where [::Inner]: TiffValue, { let image = encoder - .new_image_with_compression::(Self::WIDTH, Self::HEIGHT, compression) + .new_image::(Self::WIDTH, Self::HEIGHT) .unwrap(); image.write_data(self.reference_data()).unwrap(); } @@ -94,7 +91,7 @@ impl TestImage<1> for TestImageGrayscale { } } -fn encode_decode_with_compression(compression: C) { +fn encode_decode_with_compression(compression: Compression) { let mut data = Cursor::new(Vec::new()); let image_rgb = TestImageColor::generate(); @@ -103,9 +100,11 @@ fn encode_decode_with_compression(compression: C) { // Encode tiff with compression { // Create a multipage image with 2 images - let mut encoder = TiffEncoder::new(&mut data).unwrap(); - image_rgb.compress(&mut encoder, compression.clone()); - image_grayscale.compress(&mut encoder, compression); + let mut encoder = TiffEncoder::new(&mut data) + .unwrap() + .with_compression(compression); + image_rgb.compress(&mut encoder); + image_grayscale.compress(&mut encoder); } // Decode tiff @@ -136,22 +135,22 @@ fn encode_decode_with_compression(compression: C) { #[test] fn encode_decode_without_compression() { - encode_decode_with_compression(Uncompressed::default()); + encode_decode_with_compression(Compression::Uncompressed); } #[test] fn encode_decode_with_lzw() { - encode_decode_with_compression(Lzw::default()); + encode_decode_with_compression(Compression::Lzw); } #[test] fn encode_decode_with_deflate() { - encode_decode_with_compression(Deflate::with_level(DeflateLevel::Fast)); - encode_decode_with_compression(Deflate::with_level(DeflateLevel::Balanced)); - encode_decode_with_compression(Deflate::with_level(DeflateLevel::Best)); + encode_decode_with_compression(Compression::Deflate(DeflateLevel::Fast)); + encode_decode_with_compression(Compression::Deflate(DeflateLevel::Balanced)); + encode_decode_with_compression(Compression::Deflate(DeflateLevel::Best)); } #[test] fn encode_decode_with_packbits() { - encode_decode_with_compression(Packbits::default()); + encode_decode_with_compression(Compression::Packbits); } From 950811888dbc23e37e97264d9aafe3937620bcad Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Skutnik Date: Mon, 19 Aug 2024 21:40:19 +0300 Subject: [PATCH 06/12] Implement default manually for 1.61 compat --- src/encoder/mod.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 39090fb6..539c4b7e 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -36,15 +36,20 @@ use self::writer::*; pub type Predictor = crate::tags::Predictor; pub type DeflateLevel = compression::DeflateLevel; -#[derive(Default, Clone, Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq)] pub enum Compression { - #[default] Uncompressed, Lzw, Deflate(DeflateLevel), Packbits, } +impl Default for Compression { + fn default() -> Self { + Self::Uncompressed + } +} + impl Compression { fn tag(&self) -> CompressionMethod { match self { From e00fa6ba5da2da42e137d4901f51c76b5cf7d9b1 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Skutnik Date: Thu, 19 Sep 2024 19:43:22 +0300 Subject: [PATCH 07/12] Avoid collecting a flat_map --- src/encoder/mod.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 539c4b7e..13bc3677 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -480,13 +480,15 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, // Write the (possible compressed) data to the encoder. let offset = match self.predictor { Predictor::None => self.encoder.write_data(value)?, - Predictor::Horizontal => { - let predicted: Vec = value - .chunks(self.row_samples as usize) - .flat_map(|row| T::horizontal_predict(row).into_iter()) - .collect(); - self.encoder.write_data(predicted.as_slice())? - } + Predictor::Horizontal => value + .chunks(self.row_samples as usize) + .map(|row| { + self.encoder + .write_data(T::horizontal_predict(row).as_slice()) + }) + .collect::, _>>()? + .into_iter() + .sum(), _ => unreachable!(), }; From 25b22b55f5dfe250e22aa8df56ccee2a035556d9 Mon Sep 17 00:00:00 2001 From: Kornel Date: Fri, 20 Sep 2024 02:19:06 +0100 Subject: [PATCH 08/12] Avoid per-row allocations --- src/encoder/colortype.rs | 43 +++++++++++++++++++++++++--------------- src/encoder/mod.rs | 20 +++++++++---------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/encoder/colortype.rs b/src/encoder/colortype.rs index b08e13a8..722b8815 100644 --- a/src/encoder/colortype.rs +++ b/src/encoder/colortype.rs @@ -2,16 +2,27 @@ use crate::tags::{PhotometricInterpretation, SampleFormat}; macro_rules! integer_horizontal_predict { () => { - fn horizontal_predict(row: &[Self::Inner]) -> Vec { + fn horizontal_predict(row: &[Self::Inner], result: &mut Vec) { let sample_size = Self::SAMPLE_FORMAT.len(); - let mut result = Vec::with_capacity(row.len()); - - result.extend_from_slice(&row[0..=sample_size - 1]); + if row.len() < sample_size { + debug_assert!(false); + return; + } + let (start, rest) = row.split_at(sample_size); + + result.clear(); + if result.capacity() - result.len() < row.len() { + return; + } + result.extend_from_slice(start); + if result.capacity() - result.len() < rest.len() { + return; + } result.extend( - (sample_size..row.len()).map(|i| row[i].wrapping_sub(row[i - sample_size])), + row.into_iter() + .zip(rest) + .map(|(prev, current)| current.wrapping_sub(*prev)), ); - - result } }; } @@ -27,7 +38,7 @@ pub trait ColorType { /// The value of the tiff tag `SampleFormat` const SAMPLE_FORMAT: &'static [SampleFormat]; - fn horizontal_predict(row: &[Self::Inner]) -> Vec; + fn horizontal_predict(row: &[Self::Inner], result: &mut Vec); } pub struct Gray8; @@ -97,7 +108,7 @@ impl ColorType for Gray32Float { const BITS_PER_SAMPLE: &'static [u16] = &[32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; - fn horizontal_predict(_: &[Self::Inner]) -> Vec { + fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { unreachable!() } } @@ -129,7 +140,7 @@ impl ColorType for Gray64Float { const BITS_PER_SAMPLE: &'static [u16] = &[64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; - fn horizontal_predict(_: &[Self::Inner]) -> Vec { + fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { unreachable!() } } @@ -170,7 +181,7 @@ impl ColorType for RGB32Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; - fn horizontal_predict(_: &[Self::Inner]) -> Vec { + fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { unreachable!() } } @@ -191,7 +202,7 @@ impl ColorType for RGB64Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; - fn horizontal_predict(_: &[Self::Inner]) -> Vec { + fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { unreachable!() } } @@ -232,7 +243,7 @@ impl ColorType for RGBA32Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; - fn horizontal_predict(_: &[Self::Inner]) -> Vec { + fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { unreachable!() } } @@ -253,7 +264,7 @@ impl ColorType for RGBA64Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; - fn horizontal_predict(_: &[Self::Inner]) -> Vec { + fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { unreachable!() } } @@ -295,7 +306,7 @@ impl ColorType for CMYK32Float { const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; - fn horizontal_predict(_: &[Self::Inner]) -> Vec { + fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { unreachable!() } } @@ -317,7 +328,7 @@ impl ColorType for CMYK64Float { const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; - fn horizontal_predict(_: &[Self::Inner]) -> Vec { + fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { unreachable!() } } diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 13bc3677..9d679ecd 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -480,16 +480,16 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, // Write the (possible compressed) data to the encoder. let offset = match self.predictor { Predictor::None => self.encoder.write_data(value)?, - Predictor::Horizontal => value - .chunks(self.row_samples as usize) - .map(|row| { - self.encoder - .write_data(T::horizontal_predict(row).as_slice()) - }) - .collect::, _>>()? - .into_iter() - .sum(), - _ => unreachable!(), + Predictor::Horizontal => { + let mut row_result = Vec::with_capacity(self.row_samples as _); + let mut offset = 0; + for row in value.chunks_exact(self.row_samples as usize) { + T::horizontal_predict(row, &mut row_result); + offset += self.encoder.write_data(row_result.as_slice())?; + } + offset + } + _ => unimplemented!(), }; let byte_count = self.encoder.last_written() as usize; From c1557c2bb15b49375ff7ec17141340858a1838ab Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Skutnik Date: Fri, 20 Sep 2024 13:34:05 +0300 Subject: [PATCH 09/12] Add predictor tests --- tests/predict.rs | 230 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 tests/predict.rs diff --git a/tests/predict.rs b/tests/predict.rs new file mode 100644 index 00000000..a7bb4e76 --- /dev/null +++ b/tests/predict.rs @@ -0,0 +1,230 @@ +extern crate tiff; + +use tiff::decoder::{Decoder, DecodingResult}; +use tiff::encoder::{colortype, Compression, Predictor, TiffEncoder}; +use tiff::ColorType; + +use std::fs::File; +use std::io::{Cursor, Seek, SeekFrom}; +use std::path::PathBuf; + +const TEST_IMAGE_DIR: &str = "./tests/images/"; + +macro_rules! test_predict { + ($name:ident, $buffer:ident, $buffer_ty:ty) => { + fn $name>( + file: &str, + expected_type: ColorType, + ) { + let path = PathBuf::from(TEST_IMAGE_DIR).join(file); + let file = File::open(path).expect("Cannot find test image!"); + let mut decoder = Decoder::new(file).expect("Cannot create decoder!"); + + assert_eq!(decoder.colortype().unwrap(), expected_type); + let image_data = match decoder.read_image().unwrap() { + DecodingResult::$buffer(res) => res, + _ => panic!("Wrong data type"), + }; + + let mut predicted = C::horizontal_predict(&image_data); + + let sample_size = C::SAMPLE_FORMAT.len(); + + (0..sample_size).for_each(|i| { + assert_eq!(predicted[i], image_data[i]); + }); + + (sample_size..image_data.len()).for_each(|i| { + predicted[i] = predicted[i].wrapping_add(predicted[i - sample_size]); + assert_eq!(predicted[i], image_data[i]); + }); + } + }; +} + +test_predict!(test_u8_predict, U8, u8); +test_predict!(test_i8_predict, I8, i8); +test_predict!(test_u16_predict, U16, u16); +test_predict!(test_i16_predict, I16, i16); +test_predict!(test_u32_predict, U32, u32); +test_predict!(test_u64_predict, U64, u64); + +#[test] +fn test_gray_u8_predict() { + test_u8_predict::("minisblack-1c-8b.tiff", ColorType::Gray(8)); +} + +#[test] +fn test_gray_i8_predict() { + test_i8_predict::("minisblack-1c-i8b.tiff", ColorType::Gray(8)); +} + +#[test] +fn test_rgb_u8_predict() { + test_u8_predict::("rgb-3c-8b.tiff", ColorType::RGB(8)); +} + +#[test] +fn test_cmyk_u8_predict() { + test_u8_predict::("cmyk-3c-8b.tiff", ColorType::CMYK(8)); +} + +#[test] +fn test_gray_u16_predict() { + test_u16_predict::("minisblack-1c-16b.tiff", ColorType::Gray(16)); +} + +#[test] +fn test_gray_i16_predict() { + test_i16_predict::("minisblack-1c-i16b.tiff", ColorType::Gray(16)); +} + +#[test] +fn test_rgb_u16_predict() { + test_u16_predict::("rgb-3c-16b.tiff", ColorType::RGB(16)); +} + +#[test] +fn test_cmyk_u16_predict() { + test_u16_predict::("cmyk-3c-16b.tiff", ColorType::CMYK(16)); +} + +#[test] +fn test_gray_u32_predict() { + test_u32_predict::("gradient-1c-32b.tiff", ColorType::Gray(32)); +} + +#[test] +fn test_rgb_u32_predict() { + test_u32_predict::("gradient-3c-32b.tiff", ColorType::RGB(32)); +} + +#[test] +fn test_gray_u64_predict() { + test_u64_predict::("gradient-1c-64b.tiff", ColorType::Gray(64)); +} + +#[test] +fn test_rgb_u64_predict() { + test_u64_predict::("gradient-3c-64b.tiff", ColorType::RGB(64)); +} + +#[test] +fn test_ycbcr_u8_predict() { + test_u8_predict::("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8)); +} + +macro_rules! test_predict_roundtrip { + ($name:ident, $buffer:ident, $buffer_ty:ty) => { + fn $name>( + file: &str, + expected_type: ColorType, + ) { + let path = PathBuf::from(TEST_IMAGE_DIR).join(file); + let img_file = File::open(path).expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + assert_eq!(decoder.colortype().unwrap(), expected_type); + + let image_data = match decoder.read_image().unwrap() { + DecodingResult::$buffer(res) => res, + _ => panic!("Wrong data type"), + }; + + let mut file = Cursor::new(Vec::new()); + { + let mut tiff = TiffEncoder::new(&mut file) + .unwrap() + .with_predictor(Predictor::Horizontal); + + let (width, height) = decoder.dimensions().unwrap(); + tiff.write_image::(width, height, &image_data).unwrap(); + } + file.seek(SeekFrom::Start(0)).unwrap(); + { + let mut decoder = Decoder::new(&mut file).unwrap(); + if let DecodingResult::$buffer(img_res) = + decoder.read_image().expect("Decoding image failed") + { + assert_eq!(image_data, img_res); + } else { + panic!("Wrong data type"); + } + } + } + }; +} + +test_predict_roundtrip!(test_u8_predict_roundtrip, U8, u8); +test_predict_roundtrip!(test_i8_predict_roundtrip, I8, i8); +test_predict_roundtrip!(test_u16_predict_roundtrip, U16, u16); +test_predict_roundtrip!(test_i16_predict_roundtrip, I16, i16); +test_predict_roundtrip!(test_u32_predict_roundtrip, U32, u32); +test_predict_roundtrip!(test_u64_predict_roundtrip, U64, u64); + +#[test] +fn test_gray_u8_predict_roundtrip() { + test_u8_predict_roundtrip::("minisblack-1c-8b.tiff", ColorType::Gray(8)); +} + +#[test] +fn test_gray_i8_predict_roundtrip() { + test_i8_predict_roundtrip::("minisblack-1c-i8b.tiff", ColorType::Gray(8)); +} + +#[test] +fn test_rgb_u8_predict_roundtrip() { + test_u8_predict_roundtrip::("rgb-3c-8b.tiff", ColorType::RGB(8)); +} + +#[test] +fn test_cmyk_u8_predict_roundtrip() { + test_u8_predict_roundtrip::("cmyk-3c-8b.tiff", ColorType::CMYK(8)); +} + +#[test] +fn test_gray_u16_predict_roundtrip() { + test_u16_predict_roundtrip::("minisblack-1c-16b.tiff", ColorType::Gray(16)); +} + +#[test] +fn test_gray_i16_predict_roundtrip() { + test_i16_predict_roundtrip::( + "minisblack-1c-i16b.tiff", + ColorType::Gray(16), + ); +} + +#[test] +fn test_rgb_u16_predict_roundtrip() { + test_u16_predict_roundtrip::("rgb-3c-16b.tiff", ColorType::RGB(16)); +} + +#[test] +fn test_cmyk_u16_predict_roundtrip() { + test_u16_predict_roundtrip::("cmyk-3c-16b.tiff", ColorType::CMYK(16)); +} + +#[test] +fn test_gray_u32_predict_roundtrip() { + test_u32_predict_roundtrip::("gradient-1c-32b.tiff", ColorType::Gray(32)); +} + +#[test] +fn test_rgb_u32_predict_roundtrip() { + test_u32_predict_roundtrip::("gradient-3c-32b.tiff", ColorType::RGB(32)); +} + +#[test] +fn test_gray_u64_predict_roundtrip() { + test_u64_predict_roundtrip::("gradient-1c-64b.tiff", ColorType::Gray(64)); +} + +#[test] +fn test_rgb_u64_predict_roundtrip() { + test_u64_predict_roundtrip::("gradient-3c-64b.tiff", ColorType::RGB(64)); +} + +#[test] +fn test_ycbcr_u8_predict_roundtrip() { + test_u8_predict_roundtrip::("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8)); +} From 3479224d518291b7fce803a9c0d01e5b13506b69 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Skutnik Date: Fri, 20 Sep 2024 13:35:05 +0300 Subject: [PATCH 10/12] Allow useless but valid prediction on uncompressed images --- src/encoder/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 9d679ecd..dfb60c33 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -371,9 +371,6 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> { impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> { fn sanity_check(compression: Compression, predictor: Predictor) -> TiffResult<()> { match (predictor, compression, T::SAMPLE_FORMAT[0]) { - (Predictor::Horizontal, Compression::Uncompressed, _) => Err(TiffError::UsageError( - UsageError::PredictorCompressionMismatch, - )), (Predictor::Horizontal, _, SampleFormat::IEEEFP | SampleFormat::Void) => { Err(TiffError::UsageError(UsageError::PredictorIncompatible)) } From 9c3a51b0a5e10bc6e7d41ccf2c00269066742934 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Skutnik Date: Fri, 20 Sep 2024 13:39:57 +0300 Subject: [PATCH 11/12] Write predicted buffer at once --- src/encoder/colortype.rs | 7 +++---- src/encoder/mod.rs | 6 ++---- tests/predict.rs | 3 ++- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/encoder/colortype.rs b/src/encoder/colortype.rs index 722b8815..462cf626 100644 --- a/src/encoder/colortype.rs +++ b/src/encoder/colortype.rs @@ -4,20 +4,19 @@ macro_rules! integer_horizontal_predict { () => { fn horizontal_predict(row: &[Self::Inner], result: &mut Vec) { let sample_size = Self::SAMPLE_FORMAT.len(); + if row.len() < sample_size { debug_assert!(false); return; } + let (start, rest) = row.split_at(sample_size); - result.clear(); - if result.capacity() - result.len() < row.len() { - return; - } result.extend_from_slice(start); if result.capacity() - result.len() < rest.len() { return; } + result.extend( row.into_iter() .zip(rest) diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index dfb60c33..c510474b 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -478,13 +478,11 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, let offset = match self.predictor { Predictor::None => self.encoder.write_data(value)?, Predictor::Horizontal => { - let mut row_result = Vec::with_capacity(self.row_samples as _); - let mut offset = 0; + let mut row_result = Vec::with_capacity(value.len()); for row in value.chunks_exact(self.row_samples as usize) { T::horizontal_predict(row, &mut row_result); - offset += self.encoder.write_data(row_result.as_slice())?; } - offset + self.encoder.write_data(row_result.as_slice())? } _ => unimplemented!(), }; diff --git a/tests/predict.rs b/tests/predict.rs index a7bb4e76..a61eb6cb 100644 --- a/tests/predict.rs +++ b/tests/predict.rs @@ -26,7 +26,8 @@ macro_rules! test_predict { _ => panic!("Wrong data type"), }; - let mut predicted = C::horizontal_predict(&image_data); + let mut predicted = Vec::with_capacity(image_data.len()); + C::horizontal_predict(&image_data, &mut predicted); let sample_size = C::SAMPLE_FORMAT.len(); From 1c033289aefbbd015c4147822c6f1c7dc9f177f1 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Skutnik Date: Sat, 21 Sep 2024 23:12:29 +0300 Subject: [PATCH 12/12] Fix horizontal prediction decoding --- src/decoder/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 60fb8424..09d530bc 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -265,21 +265,21 @@ fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: usize) { 9..=16 => { for i in (samples * 2..buf.len()).step_by(2) { let v = u16::from_ne_bytes(buf[i..][..2].try_into().unwrap()); - let p = u16::from_ne_bytes(buf[i - samples..][..2].try_into().unwrap()); + let p = u16::from_ne_bytes(buf[i - 2 * samples..][..2].try_into().unwrap()); buf[i..][..2].copy_from_slice(&(v.wrapping_add(p)).to_ne_bytes()); } } 17..=32 => { for i in (samples * 4..buf.len()).step_by(4) { let v = u32::from_ne_bytes(buf[i..][..4].try_into().unwrap()); - let p = u32::from_ne_bytes(buf[i - samples..][..4].try_into().unwrap()); + let p = u32::from_ne_bytes(buf[i - 4 * samples..][..4].try_into().unwrap()); buf[i..][..4].copy_from_slice(&(v.wrapping_add(p)).to_ne_bytes()); } } 33..=64 => { for i in (samples * 8..buf.len()).step_by(8) { let v = u64::from_ne_bytes(buf[i..][..8].try_into().unwrap()); - let p = u64::from_ne_bytes(buf[i - samples..][..8].try_into().unwrap()); + let p = u64::from_ne_bytes(buf[i - 8 * samples..][..8].try_into().unwrap()); buf[i..][..8].copy_from_slice(&(v.wrapping_add(p)).to_ne_bytes()); } }