Repository: image-rs/image-tiff
Pull request #240 (2024-08-03 19:25:12)

Add horizontal predictor support
While investigating #237, I came to the realization that the crate lacked predictor support at encoding. This PR attemps to merge the feature in the most efficient way.

`TiffEncoder` gains a fluent interface `with_predictor` allowing users to easily set their preferred method. Using this predictor with the LZW compression makes its average compression ratio go from 0.85:1 (yes, the compressed image is bigger) to 1.3:1.

With those changes underway, the clunkyness of the current interface was just too strong. Having the compression as a generic type on the `ImageEncoder` methods serves no purpose other than complicating the code; methods were duplicated `x` and `x_with_compression` to account for the type. Adding the predictor to this interface was out of the question: `x` `x_with_compression` `x_with_predictor` `x_with_compression_and_predictor` ? No thanks.

`TiffEncoder` also gains a `with_compression` fluent interface to set the desired file compression.

The final interface looks as such:
```rs
let mut encoder = TiffEncoder::<std::fs::File>::new(compressed)?
        .with_predictor(Predictor::Horizontal)
        .with_compression(Compression::Lzw);

    encoder.write_image::<colortype::RGB8>(
        photo.width(),
        photo.height(),
        photo.as_rgb8().expect("Wrong image format"),
    )?;
```

```diff
-    let mut encoder = TiffEncoder::<std::fs::File>::new(compressed)?;
+    let mut encoder = TiffEncoder::<std::fs::File>::new(compressed)?
+        .with_predictor(Predictor::Horizontal)
+        .with_compression(Compression::Lzw);
+
+    encoder.write_image::<colortype::RGB8>(
-    encoder.write_image_with_compression::<colortype::RGB8, Compresion::Lzw>(
         photo.width(),
         photo.height(),
-        Compression::Lzw,
         photo.as_rgb8().expect("Wrong image format"),
+    )?;
```

This means the `*_with_compression` methods are gone from the `TiffEncoder` interface, which means breaking changes.

spoutn1k: Any feedback ? If the breaking changes are not welcome I can try and work around them but I was able to build `image-rs/image` with no issues
spoutn1k: Fixed the grammar incompatible with rust 1.61 ! Please re-run the tests I cannot do it manually :(
spoutn1k: Any feedback ? I am using this to compress my images using rust instead of an external tool and if it is helpful to me I'm sure it could help someone else.
kornelski: Creating a new `Vec` per row is wasteful. You could pass `&mut Vec` to the predictor and have it append pixels directly to the final buffer.

spoutn1k: Well maybe but the method this function is used does not allow for modification of the buffer it passes:

https://github.com/image-rs/image-tiff/blob/950811888dbc23e37e97264d9aafe3937620bcad/src/encoder/mod.rs#L467-L490

So there would be copy at some point or another. I can look into changing that but I wanted to limit my changes.
kornelski: You're calling collect() there which creates a Vec anyway. 
Don't use flat_map + collect(). Make Vec::with_capacity() and append to it


spoutn1k: `[T::Inner]` does not have `Clone`, so if I want to create the `Vec` outside the predictor function, I will need to have this ...

I pushed a version that does not collect and writes strips as soon as they are predicted. If returning a `Vec` is still a performance concern, I will update the `ColorType` trait to do so.
kornelski: Can you check that the tests actually work?

I've replaced the predictor code with nothing:

```rust
Predictor::Horizontal => {
                0
}
```

and `cargo test --all` still passes.
kornelski: This is what I mean: https://github.com/spoutn1k/image-tiff/pull/1
spoutn1k: You are right I did not implement tests. Will do that.
spoutn1k: While implementing the tests, something was not right and the changes you made were producing corrupted images. Implementing the tests first then adding your changes.
spoutn1k: Images are corrupted from e00fa6ba5da2da42e137d4901f51c76b5cf7d9b1. Looks like the encoder does not like writing strip by strip.
spoutn1k: Most of the round-trip tests fail for datatypes bigger than `u8`. After some testing and manually compressing images it seems the implementation of the "deprediction" in the decoder is the culprit.
spoutn1k: Fixes #237 and #247 !

Patch:
diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs
index 60fb842..09d530b 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());
             }
         }
diff --git a/src/encoder/colortype.rs b/src/encoder/colortype.rs
index 1946daf..462cf62 100644
--- a/src/encoder/colortype.rs
+++ b/src/encoder/colortype.rs
@@ -1,5 +1,31 @@
 use crate::tags::{PhotometricInterpretation, SampleFormat};
 
+macro_rules! integer_horizontal_predict {
+    () => {
+        fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>) {
+            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.extend_from_slice(start);
+            if result.capacity() - result.len() < rest.len() {
+                return;
+            }
+
+            result.extend(
+                row.into_iter()
+                    .zip(rest)
+                    .map(|(prev, current)| current.wrapping_sub(*prev)),
+            );
+        }
+    };
+}
+
 /// Trait for different colortypes that can be encoded.
 pub trait ColorType {
     /// The type of each sample of this colortype
@@ -10,6 +36,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], result: &mut Vec<Self::Inner>);
 }
 
 pub struct Gray8;
@@ -18,6 +46,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 +56,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 +66,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 +76,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 +86,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 +96,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 +106,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], _: &mut Vec<Self::Inner>) {
+        unreachable!()
+    }
 }
 
 pub struct Gray64;
@@ -74,6 +118,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 +128,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 +138,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], _: &mut Vec<Self::Inner>) {
+        unreachable!()
+    }
 }
 
 pub struct RGB8;
@@ -98,6 +150,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 +160,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 +170,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 +180,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], _: &mut Vec<Self::Inner>) {
+        unreachable!()
+    }
 }
 
 pub struct RGB64;
@@ -130,6 +191,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 +201,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], _: &mut Vec<Self::Inner>) {
+        unreachable!()
+    }
 }
 
 pub struct RGBA8;
@@ -146,6 +212,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 +222,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 +232,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 +242,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], _: &mut Vec<Self::Inner>) {
+        unreachable!()
+    }
 }
 
 pub struct RGBA64;
@@ -178,6 +253,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 +263,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], _: &mut Vec<Self::Inner>) {
+        unreachable!()
+    }
 }
 
 pub struct CMYK8;
@@ -194,6 +274,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 +284,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 +294,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 +304,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], _: &mut Vec<Self::Inner>) {
+        unreachable!()
+    }
 }
 
 pub struct CMYK64;
@@ -226,6 +316,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 +326,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], _: &mut Vec<Self::Inner>) {
+        unreachable!()
+    }
 }
 
 pub struct YCbCr8;
@@ -242,4 +338,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!();
 }
diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs
index 152a77a..c510474 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,
 };
 
@@ -21,9 +21,55 @@ mod tiff_value;
 mod writer;
 
 use self::colortype::*;
+use self::compression::Compression as Comp;
 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;
+pub type DeflateLevel = compression::DeflateLevel;
+
+#[derive(Clone, Copy, PartialEq)]
+pub enum Compression {
+    Uncompressed,
+    Lzw,
+    Deflate(DeflateLevel),
+    Packbits,
+}
+
+impl Default for Compression {
+    fn default() -> Self {
+        Self::Uncompressed
+    }
+}
+
+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.
 ///
 /// With this type you can get a `DirectoryEncoder` or a `ImageEncoder`
@@ -52,6 +98,8 @@ use self::writer::*;
 pub struct TiffEncoder<W, K: TiffKind = TiffKindStandard> {
     writer: TiffWriter<W>,
     kind: PhantomData<K>,
+    predictor: Predictor,
+    compression: Compression,
 }
 
 /// Constructor functions to create standard Tiff files.
@@ -83,6 +131,8 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
         let mut encoder = TiffEncoder {
             writer: TiffWriter::new(writer),
             kind: PhantomData,
+            predictor: Predictor::None,
+            compression: Compression::Uncompressed,
         };
 
         K::write_header(&mut encoder.writer)?;
@@ -90,6 +140,23 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
         Ok(encoder)
     }
 
+    /// 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;
+
+        self
+    }
+
     /// Create a [`DirectoryEncoder`] to encode an ifd directory.
     pub fn new_directory(&mut self) -> TiffResult<DirectoryEncoder<W, K>> {
         DirectoryEncoder::new(&mut self.writer)
@@ -100,20 +167,9 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
         &mut self,
         width: u32,
         height: u32,
-    ) -> TiffResult<ImageEncoder<W, C, K, Uncompressed>> {
-        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<C: ColorType, D: Compression>(
-        &mut self,
-        width: u32,
-        height: u32,
-        compression: D,
-    ) -> TiffResult<ImageEncoder<W, C, K, D>> {
+    ) -> TiffResult<ImageEncoder<W, C, K>> {
         let encoder = DirectoryEncoder::new(&mut self.writer)?;
-        ImageEncoder::with_compression(encoder, width, height, compression)
+        ImageEncoder::new(encoder, width, height, self.compression, self.predictor)
     }
 
     /// Convenience function to write an entire image from memory.
@@ -127,24 +183,8 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
         [C::Inner]: TiffValue,
     {
         let encoder = DirectoryEncoder::new(&mut self.writer)?;
-        let image: ImageEncoder<W, C, K> = 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<C: ColorType, D: 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<W, C, K, D> =
-            ImageEncoder::with_compression(encoder, width, height, compression)?;
+        let image: ImageEncoder<W, C, K> =
+            ImageEncoder::new(encoder, width, height, self.compression, self.predictor)?;
         image.write_data(data)
     }
 }
@@ -312,13 +352,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,
@@ -329,25 +363,30 @@ pub struct ImageEncoder<
     strip_offsets: Vec<K::OffsetType>,
     strip_byte_count: Vec<K::OffsetType>,
     dropped: bool,
-    compression: D,
+    compression: Compression,
+    predictor: Predictor,
     _phantom: ::std::marker::PhantomData<C>,
 }
 
-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<Self>
-    where
-        D: Default,
-    {
-        Self::with_compression(encoder, width, height, D::default())
+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, _, SampleFormat::IEEEFP | SampleFormat::Void) => {
+                Err(TiffError::UsageError(UsageError::PredictorIncompatible))
+            }
+            (Predictor::FloatingPoint, _, _) => {
+                Err(TiffError::UsageError(UsageError::PredictorUnavailable))
+            }
+            _ => Ok(()),
+        }
     }
 
-    fn with_compression(
+    fn new(
         mut encoder: DirectoryEncoder<'a, W, K>,
         width: u32,
         height: u32,
-        compression: D,
+        compression: Compression,
+        predictor: Predictor,
     ) -> TiffResult<Self> {
         if width == 0 || height == 0 {
             return Err(TiffError::FormatError(TiffFormatError::InvalidDimensions(
@@ -355,13 +394,15 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
             )));
         }
 
+        Self::sanity_check(compression, predictor)?;
+
         let row_samples = u64::from(width) * u64::try_from(<T>::BITS_PER_SAMPLE.len())?;
         let row_bytes = row_samples * u64::from(<T::Inner>::BYTE_LEN);
 
         // 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,
             }
@@ -371,7 +412,8 @@ 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, <T>::BITS_PER_SAMPLE)?;
         let sample_format: Vec<_> = <T>::SAMPLE_FORMAT.iter().map(|s| s.to_u16()).collect();
@@ -400,6 +442,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 +475,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 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);
+                }
+                self.encoder.write_data(row_result.as_slice())?
+            }
+            _ => unimplemented!(),
+        };
+
         let byte_count = self.encoder.last_written() as usize;
 
         self.strip_offsets.push(K::convert_offset(offset)?);
@@ -552,9 +606,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();
diff --git a/src/error.rs b/src/error.rs
index 7dc781e..955da67 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"),
         }
     }
 }
diff --git a/src/tags.rs b/src/tags.rs
index 3b86dc1..6c18fa5 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,
 }
 }


Test patch:
diff --git a/tests/encode_images_with_compression.rs b/tests/encode_images_with_compression.rs
index 1cf178f..2855c51 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<const NUM_CHANNELS: usize>: From<Vec<<Self::Color as ColorType>:
     fn reference_data(&self) -> &[<Self::Color as ColorType>::Inner];
     fn generate_pixel(x: u32, y: u32) -> [<Self::Color as ColorType>::Inner; NUM_CHANNELS];
 
-    fn compress<C: Compression, W: Write + Seek>(
-        &self,
-        encoder: &mut TiffEncoder<W>,
-        compression: C,
-    ) where
+    fn compress<W: Write + Seek>(&self, encoder: &mut TiffEncoder<W>)
+    where
         [<Self::Color as ColorType>::Inner]: TiffValue,
     {
         let image = encoder
-            .new_image_with_compression::<Self::Color, C>(Self::WIDTH, Self::HEIGHT, compression)
+            .new_image::<Self::Color>(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<C: Compression + Clone>(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<C: Compression + Clone>(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<C: Compression + Clone>(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);
 }
diff --git a/tests/predict.rs b/tests/predict.rs
new file mode 100644
index 0000000..a61eb6c
--- /dev/null
+++ b/tests/predict.rs
@@ -0,0 +1,231 @@
+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<C: colortype::ColorType<Inner = $buffer_ty>>(
+            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 = Vec::with_capacity(image_data.len());
+            C::horizontal_predict(&image_data, &mut predicted);
+
+            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::<colortype::Gray8>("minisblack-1c-8b.tiff", ColorType::Gray(8));
+}
+
+#[test]
+fn test_gray_i8_predict() {
+    test_i8_predict::<colortype::GrayI8>("minisblack-1c-i8b.tiff", ColorType::Gray(8));
+}
+
+#[test]
+fn test_rgb_u8_predict() {
+    test_u8_predict::<colortype::RGB8>("rgb-3c-8b.tiff", ColorType::RGB(8));
+}
+
+#[test]
+fn test_cmyk_u8_predict() {
+    test_u8_predict::<colortype::CMYK8>("cmyk-3c-8b.tiff", ColorType::CMYK(8));
+}
+
+#[test]
+fn test_gray_u16_predict() {
+    test_u16_predict::<colortype::Gray16>("minisblack-1c-16b.tiff", ColorType::Gray(16));
+}
+
+#[test]
+fn test_gray_i16_predict() {
+    test_i16_predict::<colortype::GrayI16>("minisblack-1c-i16b.tiff", ColorType::Gray(16));
+}
+
+#[test]
+fn test_rgb_u16_predict() {
+    test_u16_predict::<colortype::RGB16>("rgb-3c-16b.tiff", ColorType::RGB(16));
+}
+
+#[test]
+fn test_cmyk_u16_predict() {
+    test_u16_predict::<colortype::CMYK16>("cmyk-3c-16b.tiff", ColorType::CMYK(16));
+}
+
+#[test]
+fn test_gray_u32_predict() {
+    test_u32_predict::<colortype::Gray32>("gradient-1c-32b.tiff", ColorType::Gray(32));
+}
+
+#[test]
+fn test_rgb_u32_predict() {
+    test_u32_predict::<colortype::RGB32>("gradient-3c-32b.tiff", ColorType::RGB(32));
+}
+
+#[test]
+fn test_gray_u64_predict() {
+    test_u64_predict::<colortype::Gray64>("gradient-1c-64b.tiff", ColorType::Gray(64));
+}
+
+#[test]
+fn test_rgb_u64_predict() {
+    test_u64_predict::<colortype::RGB64>("gradient-3c-64b.tiff", ColorType::RGB(64));
+}
+
+#[test]
+fn test_ycbcr_u8_predict() {
+    test_u8_predict::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
+}
+
+macro_rules! test_predict_roundtrip {
+    ($name:ident, $buffer:ident, $buffer_ty:ty) => {
+        fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
+            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::<C>(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::<colortype::Gray8>("minisblack-1c-8b.tiff", ColorType::Gray(8));
+}
+
+#[test]
+fn test_gray_i8_predict_roundtrip() {
+    test_i8_predict_roundtrip::<colortype::GrayI8>("minisblack-1c-i8b.tiff", ColorType::Gray(8));
+}
+
+#[test]
+fn test_rgb_u8_predict_roundtrip() {
+    test_u8_predict_roundtrip::<colortype::RGB8>("rgb-3c-8b.tiff", ColorType::RGB(8));
+}
+
+#[test]
+fn test_cmyk_u8_predict_roundtrip() {
+    test_u8_predict_roundtrip::<colortype::CMYK8>("cmyk-3c-8b.tiff", ColorType::CMYK(8));
+}
+
+#[test]
+fn test_gray_u16_predict_roundtrip() {
+    test_u16_predict_roundtrip::<colortype::Gray16>("minisblack-1c-16b.tiff", ColorType::Gray(16));
+}
+
+#[test]
+fn test_gray_i16_predict_roundtrip() {
+    test_i16_predict_roundtrip::<colortype::GrayI16>(
+        "minisblack-1c-i16b.tiff",
+        ColorType::Gray(16),
+    );
+}
+
+#[test]
+fn test_rgb_u16_predict_roundtrip() {
+    test_u16_predict_roundtrip::<colortype::RGB16>("rgb-3c-16b.tiff", ColorType::RGB(16));
+}
+
+#[test]
+fn test_cmyk_u16_predict_roundtrip() {
+    test_u16_predict_roundtrip::<colortype::CMYK16>("cmyk-3c-16b.tiff", ColorType::CMYK(16));
+}
+
+#[test]
+fn test_gray_u32_predict_roundtrip() {
+    test_u32_predict_roundtrip::<colortype::Gray32>("gradient-1c-32b.tiff", ColorType::Gray(32));
+}
+
+#[test]
+fn test_rgb_u32_predict_roundtrip() {
+    test_u32_predict_roundtrip::<colortype::RGB32>("gradient-3c-32b.tiff", ColorType::RGB(32));
+}
+
+#[test]
+fn test_gray_u64_predict_roundtrip() {
+    test_u64_predict_roundtrip::<colortype::Gray64>("gradient-1c-64b.tiff", ColorType::Gray(64));
+}
+
+#[test]
+fn test_rgb_u64_predict_roundtrip() {
+    test_u64_predict_roundtrip::<colortype::RGB64>("gradient-3c-64b.tiff", ColorType::RGB(64));
+}
+
+#[test]
+fn test_ycbcr_u8_predict_roundtrip() {
+    test_u8_predict_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
+}