From 3f2066bb2096adc8faec5d17c6c3eba39a0dc9ee Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Wed, 25 Feb 2026 23:05:35 +0000 Subject: [PATCH] Implement floating-point predictor (Predictor 3) encoding Add support for encoding TIFF images with the floating-point predictor, which improves compression ratios for floating-point image data. The floating-point predictor works by: 1. Converting floats to big-endian bytes 2. Shuffling bytes so all first bytes are together, all second bytes, etc. 3. Applying byte-level horizontal differencing across the buffer This complements the existing decoding support for Predictor 3. Supported types: Gray32Float, Gray64Float, RGB32Float, RGB64Float, RGBA32Float, RGBA64Float, CMYK32Float, CMYK64Float. Co-Authored-By: Claude Opus 4.5 --- src/encoder/colortype.rs | 121 +++++++++++++++++++++++++++++++--- src/encoder/mod.rs | 19 ++++-- src/tags.rs | 4 +- tests/encode_images.rs | 137 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 267 insertions(+), 14 deletions(-) diff --git a/src/encoder/colortype.rs b/src/encoder/colortype.rs index 7bb45d0..b8a09a3 100644 --- a/src/encoder/colortype.rs +++ b/src/encoder/colortype.rs @@ -1,5 +1,66 @@ use crate::tags::{PhotometricInterpretation, SampleFormat}; +/// Apply floating-point predictor encoding to a row of f32 samples. +/// +/// The floating-point predictor works by: +/// 1. Converting each f32 to big-endian bytes +/// 2. Shuffling bytes so all first bytes are together, all second bytes, etc. +/// 3. Applying byte-level horizontal difference (wrapping_sub) across the entire buffer +/// +/// The decoder reverses this by: +/// 1. Applying cumulative sum (wrapping_add) on the entire byte stream +/// 2. De-shuffling bytes back into f32 values +fn fp_predict_f32(row: &[f32], samples: usize, result: &mut Vec) { + let num_values = row.len(); + let byte_count = num_values * 4; + let start_len = result.len(); + + // Reserve space for the output + result.reserve(byte_count); + + // Step 1 & 2: Convert to big-endian and shuffle bytes + // All first bytes, then all second bytes, etc. + for byte_idx in 0..4 { + for &value in row { + let bytes = value.to_be_bytes(); + result.push(bytes[byte_idx]); + } + } + + // Step 3: Apply byte-level horizontal difference across the entire buffer + // We iterate backwards so that when we compute output[i] - output[i-samples], + // output[i-samples] still has its original value. + // The prediction spans across quarter boundaries, just like the decoder's cumulative sum. + let output = &mut result[start_len..]; + for i in (samples..byte_count).rev() { + output[i] = output[i].wrapping_sub(output[i - samples]); + } +} + +/// Apply floating-point predictor encoding to a row of f64 samples. +fn fp_predict_f64(row: &[f64], samples: usize, result: &mut Vec) { + let num_values = row.len(); + let byte_count = num_values * 8; + let start_len = result.len(); + + // Reserve space for the output + result.reserve(byte_count); + + // Step 1 & 2: Convert to big-endian and shuffle bytes + for byte_idx in 0..8 { + for &value in row { + let bytes = value.to_be_bytes(); + result.push(bytes[byte_idx]); + } + } + + // Step 3: Apply byte-level horizontal difference across the entire buffer + let output = &mut result[start_len..]; + for i in (samples..byte_count).rev() { + output[i] = output[i].wrapping_sub(output[i - samples]); + } +} + macro_rules! integer_horizontal_predict { () => { fn horizontal_predict(row: &[Self::Inner], result: &mut Vec) { @@ -23,6 +84,10 @@ macro_rules! integer_horizontal_predict { .map(|(prev, current)| current.wrapping_sub(*prev)), ); } + + fn floating_point_predict(_: &[Self::Inner], _: &mut Vec) { + unreachable!("floating-point predictor is only valid for floating-point sample types") + } }; } @@ -38,6 +103,10 @@ pub trait ColorType { const SAMPLE_FORMAT: &'static [SampleFormat]; fn horizontal_predict(row: &[Self::Inner], result: &mut Vec); + + /// Apply floating-point predictor encoding to a row of samples. + /// This is only implemented for floating-point types; integer types will panic. + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec); } pub struct Gray8; @@ -108,7 +177,11 @@ impl ColorType for Gray32Float { const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -140,7 +213,11 @@ impl ColorType for Gray64Float { const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -180,8 +257,13 @@ 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) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -201,8 +283,13 @@ 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) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -242,8 +329,13 @@ 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) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -263,8 +355,13 @@ 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) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -306,7 +403,11 @@ impl ColorType for CMYK32Float { const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -328,7 +429,11 @@ impl ColorType for CMYK64Float { const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result) } } diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index b043a4d..6a1f6d1 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -33,9 +33,10 @@ use self::writer::*; /// 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 ! +/// ratio over the unpredicted compression! /// -/// [Predictor::FloatingPoint] is currently not supported. +/// [Predictor::Horizontal] is for integer sample types. +/// [Predictor::FloatingPoint] is for floating-point sample types (f32, f64). pub type Predictor = crate::tags::Predictor; #[cfg(feature = "deflate")] pub type DeflateLevel = compression::DeflateLevel; @@ -635,11 +636,14 @@ 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]) { + // Horizontal predictor is not valid for floating-point types (Predictor::Horizontal, _, SampleFormat::IEEEFP | SampleFormat::Void) => { Err(TiffError::UsageError(UsageError::PredictorIncompatible)) } + // Floating-point predictor is only valid for IEEEFP sample format + (Predictor::FloatingPoint, _, SampleFormat::IEEEFP) => Ok(()), (Predictor::FloatingPoint, _, _) => { - Err(TiffError::UsageError(UsageError::PredictorUnavailable)) + Err(TiffError::UsageError(UsageError::PredictorIncompatible)) } _ => Ok(()), } @@ -773,7 +777,14 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, } self.encoder.write_data(row_result.as_slice())? } - _ => unimplemented!(), + Predictor::FloatingPoint => { + let byte_size = mem::size_of::(); + let mut row_result = Vec::with_capacity(value.len() * byte_size); + for row in value.chunks_exact(self.row_samples as usize) { + T::floating_point_predict(row, &mut row_result); + } + self.encoder.write_data(row_result.as_slice())? + } }; let byte_count = self.encoder.last_written() as usize; diff --git a/src/tags.rs b/src/tags.rs index ab1ffba..690c30c 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -357,7 +357,9 @@ pub enum Predictor(u16) { /// 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 + /// Floating-point predictor for IEEE floating-point sample types. + /// + /// This predictor byte-shuffles the data and applies byte-level horizontal differencing. FloatingPoint = 3, } } diff --git a/tests/encode_images.rs b/tests/encode_images.rs index df0dac9..3fb9dee 100644 --- a/tests/encode_images.rs +++ b/tests/encode_images.rs @@ -1,7 +1,7 @@ extern crate tiff; use tiff::decoder::{ifd, Decoder, DecodingResult}; -use tiff::encoder::{colortype, Ifd, Ifd8, SRational, TiffEncoder}; +use tiff::encoder::{colortype, Ifd, Ifd8, Predictor, SRational, TiffEncoder}; use tiff::tags::Tag; use tiff::ColorType; @@ -379,6 +379,141 @@ fn test_ycbcr_u8_roundtrip() { test_u8_roundtrip::("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8)); } +/// Test that encoding with the floating-point predictor works correctly +/// and can be decoded back to the original data. +#[test] +fn test_gray_f32_floating_point_predictor() { + // Generate test data: a gradient of f32 values + let width = 32u32; + let height = 32u32; + let mut image_data: Vec = Vec::with_capacity((width * height) as usize); + for y in 0..height { + for x in 0..width { + // Create a gradient pattern + image_data.push((x as f32 + y as f32 * 0.1) / 100.0); + } + } + + let mut file = Cursor::new(Vec::new()); + { + let mut tiff = TiffEncoder::new(&mut file) + .unwrap() + .with_predictor(Predictor::FloatingPoint); + tiff.write_image::(width, height, &image_data) + .unwrap(); + } + + file.seek(SeekFrom::Start(0)).unwrap(); + { + let mut decoder = Decoder::new(&mut file).unwrap(); + assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(32)); + + let mut buffer = DecodingResult::F32(vec![]); + decoder.read_image_to_buffer(&mut buffer).unwrap(); + if let DecodingResult::F32(img_res) = buffer { + assert_eq!(image_data.len(), img_res.len()); + for (orig, decoded) in image_data.iter().zip(img_res.iter()) { + assert_eq!( + *orig, *decoded, + "Mismatch at value: orig={}, decoded={}", + orig, decoded + ); + } + } else { + panic!("Wrong data type"); + } + } +} + +/// Test floating-point predictor with RGB f32 data +#[test] +fn test_rgb_f32_floating_point_predictor() { + let width = 16u32; + let height = 16u32; + let mut image_data: Vec = Vec::with_capacity((width * height * 3) as usize); + for y in 0..height { + for x in 0..width { + // RGB pattern + image_data.push(x as f32 / width as f32); // R + image_data.push(y as f32 / height as f32); // G + image_data.push((x + y) as f32 / (width + height) as f32); // B + } + } + + let mut file = Cursor::new(Vec::new()); + { + let mut tiff = TiffEncoder::new(&mut file) + .unwrap() + .with_predictor(Predictor::FloatingPoint); + tiff.write_image::(width, height, &image_data) + .unwrap(); + } + + file.seek(SeekFrom::Start(0)).unwrap(); + { + let mut decoder = Decoder::new(&mut file).unwrap(); + assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(32)); + + let mut buffer = DecodingResult::F32(vec![]); + decoder.read_image_to_buffer(&mut buffer).unwrap(); + if let DecodingResult::F32(img_res) = buffer { + assert_eq!(image_data.len(), img_res.len()); + for (i, (orig, decoded)) in image_data.iter().zip(img_res.iter()).enumerate() { + assert_eq!( + *orig, *decoded, + "Mismatch at index {}: orig={}, decoded={}", + i, orig, decoded + ); + } + } else { + panic!("Wrong data type"); + } + } +} + +/// Test floating-point predictor with f64 data +#[test] +fn test_gray_f64_floating_point_predictor() { + let width = 16u32; + let height = 16u32; + let mut image_data: Vec = Vec::with_capacity((width * height) as usize); + for y in 0..height { + for x in 0..width { + image_data.push((x as f64 + y as f64 * 0.1) / 100.0); + } + } + + let mut file = Cursor::new(Vec::new()); + { + let mut tiff = TiffEncoder::new(&mut file) + .unwrap() + .with_predictor(Predictor::FloatingPoint); + tiff.write_image::(width, height, &image_data) + .unwrap(); + } + + file.seek(SeekFrom::Start(0)).unwrap(); + { + let mut decoder = Decoder::new(&mut file).unwrap(); + assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(64)); + + let mut buffer = DecodingResult::F64(vec![]); + decoder.read_image_to_buffer(&mut buffer).unwrap(); + if let DecodingResult::F64(img_res) = buffer { + assert_eq!(image_data.len(), img_res.len()); + for (orig, decoded) in image_data.iter().zip(img_res.iter()) { + assert_eq!( + *orig, *decoded, + "Mismatch: orig={}, decoded={}", + orig, decoded + ); + } + } else { + panic!("Wrong data type"); + } + } +} + trait AssertDecode { fn assert_tag_u32(&mut self, tag: u16) -> u32; fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec;