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 <noreply@anthropic.com>
This commit is contained in:
Sergey "Shnatsel" DavidoffandClaude Opus 4.5 committed 2026-02-25 23:05:35 +00:00
1 parent 2ac8be1177
commit 3f2066bb20
4 files changed
+267 -14

No files matched your search

+113 -8
View File
@@ -1,5 +1,66 @@
use crate::tags::{PhotometricInterpretation, SampleFormat}; 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<u8>) {
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<u8>) {
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 { macro_rules! integer_horizontal_predict {
() => { () => {
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>) { fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>) {
@@ -23,6 +84,10 @@ macro_rules! integer_horizontal_predict {
.map(|(prev, current)| current.wrapping_sub(*prev)), .map(|(prev, current)| current.wrapping_sub(*prev)),
); );
} }
fn floating_point_predict(_: &[Self::Inner], _: &mut Vec<u8>) {
unreachable!("floating-point predictor is only valid for floating-point sample types")
}
}; };
} }
@@ -38,6 +103,10 @@ pub trait ColorType {
const SAMPLE_FORMAT: &'static [SampleFormat]; const SAMPLE_FORMAT: &'static [SampleFormat];
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>); fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>);
/// 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<u8>);
} }
pub struct Gray8; pub struct Gray8;
@@ -108,7 +177,11 @@ impl ColorType for Gray32Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
@@ -140,7 +213,11 @@ impl ColorType for Gray64Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
@@ -180,8 +257,13 @@ impl ColorType for RGB32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
@@ -201,8 +283,13 @@ impl ColorType for RGB64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
@@ -242,8 +329,13 @@ impl ColorType for RGBA32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
@@ -263,8 +355,13 @@ impl ColorType for RGBA64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
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]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
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]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) { fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!() unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
} }
} }
+15 -4
View File
@@ -33,9 +33,10 @@ use self::writer::*;
/// Image data can be very unpredictable, and thus very hard to compress. Predictors are simple /// 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 /// 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 /// 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; pub type Predictor = crate::tags::Predictor;
#[cfg(feature = "deflate")] #[cfg(feature = "deflate")]
pub type DeflateLevel = compression::DeflateLevel; 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> { impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> {
fn sanity_check(compression: Compression, predictor: Predictor) -> TiffResult<()> { fn sanity_check(compression: Compression, predictor: Predictor) -> TiffResult<()> {
match (predictor, compression, T::SAMPLE_FORMAT[0]) { match (predictor, compression, T::SAMPLE_FORMAT[0]) {
// Horizontal predictor is not valid for floating-point types
(Predictor::Horizontal, _, SampleFormat::IEEEFP | SampleFormat::Void) => { (Predictor::Horizontal, _, SampleFormat::IEEEFP | SampleFormat::Void) => {
Err(TiffError::UsageError(UsageError::PredictorIncompatible)) Err(TiffError::UsageError(UsageError::PredictorIncompatible))
} }
// Floating-point predictor is only valid for IEEEFP sample format
(Predictor::FloatingPoint, _, SampleFormat::IEEEFP) => Ok(()),
(Predictor::FloatingPoint, _, _) => { (Predictor::FloatingPoint, _, _) => {
Err(TiffError::UsageError(UsageError::PredictorUnavailable)) Err(TiffError::UsageError(UsageError::PredictorIncompatible))
} }
_ => Ok(()), _ => 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())? self.encoder.write_data(row_result.as_slice())?
} }
_ => unimplemented!(), Predictor::FloatingPoint => {
let byte_size = mem::size_of::<T::Inner>();
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; let byte_count = self.encoder.last_written() as usize;
+3 -1
View File
@@ -357,7 +357,9 @@ pub enum Predictor(u16) {
/// This means that instead of having in order `[r1, g1. b1, r2, g2 ...]` you will find /// 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, ...]` /// `[r1, g1, b1, r2-r1, g2-g1, b2-b1, r3-r2, g3-g2, ...]`
Horizontal = 2, 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, FloatingPoint = 3,
} }
} }
+136 -1
View File
@@ -1,7 +1,7 @@
extern crate tiff; extern crate tiff;
use tiff::decoder::{ifd, Decoder, DecodingResult}; 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::tags::Tag;
use tiff::ColorType; use tiff::ColorType;
@@ -379,6 +379,141 @@ fn test_ycbcr_u8_roundtrip() {
test_u8_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8)); test_u8_roundtrip::<colortype::YCbCr8>("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<f32> = 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::<colortype::Gray32Float>(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<f32> = 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::<colortype::RGB32Float>(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<f64> = 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::<colortype::Gray64Float>(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 { trait AssertDecode {
fn assert_tag_u32(&mut self, tag: u16) -> u32; fn assert_tag_u32(&mut self, tag: u16) -> u32;
fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32>; fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32>;