Merge pull request #342 from Shnatsel/float-predictor-encoding

Implement floating-point predictor (Predictor 3) encoding
This commit is contained in:
Aurelia Molzer authored and GitHub committed 2026-02-26 20:30:08 +00:00
commit d42db25c27
4 files changed
+296 -13

No files matched your search

+144 -8
View File
@@ -1,5 +1,97 @@
use crate::tags::{PhotometricInterpretation, SampleFormat};
/// Apply floating-point predictor encoding to a row of floating-point samples.
///
/// The floating-point predictor works by:
/// 1. Converting each value 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 float values
macro_rules! fp_predict {
($name:ident, $float_type:ty) => {
fn $name(row: &[$float_type], samples: usize, result: &mut Vec<u8>) {
const BYTE_SIZE: usize = core::mem::size_of::<$float_type>();
let num_values = row.len();
let byte_count = num_values * BYTE_SIZE;
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..BYTE_SIZE {
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.
// The prediction spans across quarter boundaries, just like the decoder's cumulative sum.
//
// We process elements in reverse in batches of 16 to enable autovectorization.
// Reverse order ensures that when we compute output[i] - output[i-samples],
// the value at output[i-samples] hasn't been overwritten yet (critical when
// samples < 16, which is the common case: 1, 3, 4, 5).
//
// 8 bits * 16 = 128 bits, the width of a typical SIMD register.
// Even if the target has no vector instructions, this still benefits from
// instruction-level parallelism, since unlike in decoding, all subtractions
// within a chunk are truly independent of each other.
let output = &mut result[start_len..byte_count + start_len];
let diff_len = byte_count - samples;
let remainder_len = diff_len % 16;
// Process full 16-byte chunks in reverse.
// We iterate from the end so that predecessor values (at lower indices)
// are still in their original state when we read them.
let mut chunk_end = byte_count;
while chunk_end >= samples + remainder_len + 16 {
let chunk_start = chunk_end - 16;
let prev_start = chunk_start - samples;
// Read all values in the chunk and their predecessors first.
// This makes all subtractions below independent, enabling vectorization.
let curr: [u8; 16] = output[chunk_start..chunk_end].try_into().unwrap();
let prev: [u8; 16] = output[prev_start..prev_start + 16].try_into().unwrap();
let chunk = &mut output[chunk_start..chunk_end];
chunk[0] = curr[0].wrapping_sub(prev[0]);
chunk[1] = curr[1].wrapping_sub(prev[1]);
chunk[2] = curr[2].wrapping_sub(prev[2]);
chunk[3] = curr[3].wrapping_sub(prev[3]);
chunk[4] = curr[4].wrapping_sub(prev[4]);
chunk[5] = curr[5].wrapping_sub(prev[5]);
chunk[6] = curr[6].wrapping_sub(prev[6]);
chunk[7] = curr[7].wrapping_sub(prev[7]);
chunk[8] = curr[8].wrapping_sub(prev[8]);
chunk[9] = curr[9].wrapping_sub(prev[9]);
chunk[10] = curr[10].wrapping_sub(prev[10]);
chunk[11] = curr[11].wrapping_sub(prev[11]);
chunk[12] = curr[12].wrapping_sub(prev[12]);
chunk[13] = curr[13].wrapping_sub(prev[13]);
chunk[14] = curr[14].wrapping_sub(prev[14]);
chunk[15] = curr[15].wrapping_sub(prev[15]);
chunk_end = chunk_start;
}
// Handle the leading remainder that doesn't fill a complete chunk of 16.
// This must be done last (after the reverse chunk loop) because these are
// the lowest indices, and the chunks above may read predecessor values
// from this range.
for i in (samples..samples + remainder_len).rev() {
output[i] = output[i].wrapping_sub(output[i - samples]);
}
}
};
}
fp_predict!(fp_predict_f32, f32);
fp_predict!(fp_predict_f64, f64);
macro_rules! integer_horizontal_predict {
() => {
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>) {
@@ -23,6 +115,10 @@ macro_rules! integer_horizontal_predict {
.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 +134,10 @@ pub trait ColorType {
const SAMPLE_FORMAT: &'static [SampleFormat];
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;
@@ -108,7 +208,11 @@ impl ColorType for Gray32Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
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 +244,11 @@ impl ColorType for Gray64Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
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 +288,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<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 +314,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<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 +360,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<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 +386,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<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 +434,11 @@ impl ColorType for CMYK32Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
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 +460,11 @@ impl ColorType for CMYK64Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
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)
}
}
+13 -3
View File
@@ -35,7 +35,8 @@ use self::writer::*;
/// compression, where using [Predictor::Horizontal] we see a 35% improvement in 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,13 @@ 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 mut row_result = Vec::with_capacity(std::mem::size_of_val(value));
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;
+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
/// `[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,
}
}
+136 -1
View File
@@ -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::<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 {
fn assert_tag_u32(&mut self, tag: u16) -> u32;
fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32>;