Merge pull request #386 from aarshivv/palette-cielab-transparency-lzw
support palette expansion, CIE L*a*b*, transparency mask, and lzw end…
This commit is contained in:
3 files changed
+221
-40
No files matched your search
+60
-36
@@ -597,20 +597,25 @@ impl Image {
|
|||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
// Unsupported due to inherently heterogeneous sample types. This is represented as:
|
// CIE L*a*b* (TIFF6 Section 23, pp 110-115) encodes a/b as signed int8
|
||||||
// ```
|
// (-128..=127) while ICC L*a*b* (photometric=9) uses unsigned samples with
|
||||||
// struct TiffLab /* Interpretation 8* {
|
// a +128 bias. For 8-bit we re-bias a/b in post-processing (see
|
||||||
// pub L: u8, // SampleFormat::Uint
|
// `cielab_to_icclab`) so callers see the same encoding as ICCLab.
|
||||||
// pub a: i8, // SampleFormat::Int
|
//
|
||||||
// pub b: i8, // SampleFormat::Int
|
// Only 8-bit is accepted here. 16-bit CIELab is a valid format that libtiff
|
||||||
// }
|
// does decode (`putcontig8bitCIELab16`, selected by `initCIELabConversion`
|
||||||
// ```
|
// in tif_getimage.c line 2216 @ 04884181e8903915038be553bfab404302d98ea0),
|
||||||
PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
|
// but it uses L=uint16 with a/b=int16 — a mixed layout that `SampleFormat`,
|
||||||
TiffUnsupportedError::InterpretationWithBits(
|
// which applies uniformly to every sample, cannot express. Left as follow-up.
|
||||||
PhotometricInterpretation::CIELab,
|
PhotometricInterpretation::CIELab => match self.photometric_samples {
|
||||||
vec![self.bits_per_sample; self.samples as usize],
|
3 if matches!(self.bits_per_sample, 8) => Ok(ColorType::Lab(self.bits_per_sample)),
|
||||||
),
|
_ => Err(TiffError::UnsupportedError(
|
||||||
)),
|
TiffUnsupportedError::InterpretationWithBits(
|
||||||
|
PhotometricInterpretation::CIELab,
|
||||||
|
vec![self.bits_per_sample; self.samples as usize],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
},
|
||||||
PhotometricInterpretation::CIELogL => match self.samples {
|
PhotometricInterpretation::CIELogL => match self.samples {
|
||||||
1 if matches!(self.bits_per_sample, 32)
|
1 if matches!(self.bits_per_sample, 32)
|
||||||
&& matches!(self.sample_format, SampleFormat::IEEEFP) =>
|
&& matches!(self.sample_format, SampleFormat::IEEEFP) =>
|
||||||
@@ -666,14 +671,16 @@ impl Image {
|
|||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
PhotometricInterpretation::RGBPalette => Ok(ColorType::Palette(self.bits_per_sample)),
|
PhotometricInterpretation::RGBPalette => Ok(ColorType::Palette(self.bits_per_sample)),
|
||||||
PhotometricInterpretation::TransparencyMask | PhotometricInterpretation::Unknown(_) => {
|
// TransparencyMask is a single-channel mask, treat as grayscale.
|
||||||
Err(TiffError::UnsupportedError(
|
PhotometricInterpretation::TransparencyMask => {
|
||||||
TiffUnsupportedError::InterpretationWithBits(
|
Ok(ColorType::Gray(self.bits_per_sample))
|
||||||
self.photometric_interpretation,
|
|
||||||
vec![self.bits_per_sample; self.samples as usize],
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
PhotometricInterpretation::Unknown(_) => Err(TiffError::UnsupportedError(
|
||||||
|
TiffUnsupportedError::InterpretationWithBits(
|
||||||
|
self.photometric_interpretation,
|
||||||
|
vec![self.bits_per_sample; self.samples as usize],
|
||||||
|
),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1046,6 +1053,7 @@ impl Image {
|
|||||||
| ColorType::YCbCr(n)
|
| ColorType::YCbCr(n)
|
||||||
| ColorType::Gray(n)
|
| ColorType::Gray(n)
|
||||||
| ColorType::Palette(n)
|
| ColorType::Palette(n)
|
||||||
|
| ColorType::Lab(n)
|
||||||
| ColorType::Multiband {
|
| ColorType::Multiband {
|
||||||
bit_depth: n,
|
bit_depth: n,
|
||||||
num_samples: _,
|
num_samples: _,
|
||||||
@@ -1057,6 +1065,7 @@ impl Image {
|
|||||||
| ColorType::YCbCr(n)
|
| ColorType::YCbCr(n)
|
||||||
| ColorType::Gray(n)
|
| ColorType::Gray(n)
|
||||||
| ColorType::Palette(n)
|
| ColorType::Palette(n)
|
||||||
|
| ColorType::Lab(n)
|
||||||
| ColorType::Multiband {
|
| ColorType::Multiband {
|
||||||
bit_depth: n,
|
bit_depth: n,
|
||||||
num_samples: _,
|
num_samples: _,
|
||||||
@@ -1214,9 +1223,12 @@ impl Image {
|
|||||||
// the row is exact).
|
// the row is exact).
|
||||||
super::mask_samples_to_bit_depth(out_row, tiff_bps, out_bps, self.sample_format);
|
super::mask_samples_to_bit_depth(out_row, tiff_bps, out_bps, self.sample_format);
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
super::post_process_row(
|
||||||
super::invert_colors(out_row, color_type, self.sample_format)?;
|
out_row,
|
||||||
}
|
photometric_interpretation,
|
||||||
|
color_type,
|
||||||
|
self.sample_format,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
} else if is_output_chunk_rows && is_all_bits {
|
} else if is_output_chunk_rows && is_all_bits {
|
||||||
// Here we can read directly into the output buffer itself.
|
// Here we can read directly into the output buffer itself.
|
||||||
@@ -1244,9 +1256,12 @@ impl Image {
|
|||||||
self.sample_format,
|
self.sample_format,
|
||||||
);
|
);
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
super::post_process_row(
|
||||||
super::invert_colors(tile, color_type, self.sample_format)?;
|
tile,
|
||||||
}
|
photometric_interpretation,
|
||||||
|
color_type,
|
||||||
|
self.sample_format,
|
||||||
|
)?;
|
||||||
} else if chunk_row_bytes > data_row_bytes && self.predictor == Predictor::FloatingPoint {
|
} else if chunk_row_bytes > data_row_bytes && self.predictor == Predictor::FloatingPoint {
|
||||||
// The floating point predictor shuffles the padding bytes into the encoded output, so
|
// The floating point predictor shuffles the padding bytes into the encoded output, so
|
||||||
// this case is handled specially when needed.
|
// this case is handled specially when needed.
|
||||||
@@ -1272,9 +1287,12 @@ impl Image {
|
|||||||
}
|
}
|
||||||
// No masking here: the floating point predictor requires
|
// No masking here: the floating point predictor requires
|
||||||
// IEEEFP samples, which are never depth-promoted.
|
// IEEEFP samples, which are never depth-promoted.
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
super::post_process_row(
|
||||||
super::invert_colors(row, color_type, self.sample_format)?;
|
row,
|
||||||
}
|
photometric_interpretation,
|
||||||
|
color_type,
|
||||||
|
self.sample_format,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
} else if is_all_bits {
|
} else if is_all_bits {
|
||||||
// We read row-by-row but each row fits in its output buffer.
|
// We read row-by-row but each row fits in its output buffer.
|
||||||
@@ -1311,9 +1329,12 @@ impl Image {
|
|||||||
self.sample_format,
|
self.sample_format,
|
||||||
);
|
);
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
super::post_process_row(
|
||||||
super::invert_colors(row, color_type, self.sample_format)?;
|
row,
|
||||||
}
|
photometric_interpretation,
|
||||||
|
color_type,
|
||||||
|
self.sample_format,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// The encoded data potentially takes up more space than the output data so we must be
|
// The encoded data potentially takes up more space than the output data so we must be
|
||||||
@@ -1363,9 +1384,12 @@ impl Image {
|
|||||||
self.sample_format,
|
self.sample_format,
|
||||||
);
|
);
|
||||||
|
|
||||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
super::post_process_row(
|
||||||
super::invert_colors(row, color_type, self.sample_format)?;
|
row,
|
||||||
}
|
photometric_interpretation,
|
||||||
|
color_type,
|
||||||
|
self.sample_format,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+36
-2
@@ -3,8 +3,8 @@ use std::io::{self, Read, Seek};
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use crate::tags::{
|
use crate::tags::{
|
||||||
ByteOrder, IfdPointer, PlanarConfiguration, Predictor, SampleFormat, Tag, TiffVariant, Type,
|
ByteOrder, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat,
|
||||||
ValueBuffer,
|
Tag, TiffVariant, Type, ValueBuffer,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
|
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
|
||||||
@@ -711,6 +711,40 @@ fn invert_colors(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Re-bias CIE L*a*b* a/b channels from signed to unsigned encoding.
|
||||||
|
///
|
||||||
|
/// CIELab (photometric=8) stores a/b as int8 where 0 is neutral.
|
||||||
|
/// ICCLab (photometric=9) stores them as uint8 where 128 is neutral.
|
||||||
|
/// Adding 128 (wrapping) converts between the two representations.
|
||||||
|
/// See libtiff's `TIFFCIELabToXYZ` for reference (tif_color.c line 45
|
||||||
|
/// @ 04884181e8903915038be553bfab404302d98ea0).
|
||||||
|
fn cielab_to_icclab(buf: &mut [u8]) {
|
||||||
|
for [_, a, b] in buf.as_chunks_mut::<3>().0 {
|
||||||
|
*a = a.wrapping_add(128);
|
||||||
|
*b = b.wrapping_add(128);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply photometric-interpretation-specific post-processing to a decoded row.
|
||||||
|
///
|
||||||
|
/// Some interpretations need a fixup once the raw samples are in the output
|
||||||
|
/// buffer: `WhiteIsZero` inverts intensities, and `CIELab` re-biases the signed
|
||||||
|
/// a/b channels into the unsigned ICCLab encoding. These are mutually exclusive,
|
||||||
|
/// so at most one applies to any given row.
|
||||||
|
fn post_process_row(
|
||||||
|
buf: &mut [u8],
|
||||||
|
photometric_interpretation: PhotometricInterpretation,
|
||||||
|
color_type: ColorType,
|
||||||
|
sample_format: SampleFormat,
|
||||||
|
) -> TiffResult<()> {
|
||||||
|
match photometric_interpretation {
|
||||||
|
PhotometricInterpretation::WhiteIsZero => invert_colors(buf, color_type, sample_format)?,
|
||||||
|
PhotometricInterpretation::CIELab => cielab_to_icclab(buf),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Fix endianness. If `byte_order` matches the host, then conversion is a no-op.
|
/// Fix endianness. If `byte_order` matches the host, then conversion is a no-op.
|
||||||
fn fix_endianness(buf: &mut [u8], byte_order: ByteOrder, bit_depth: u8) {
|
fn fix_endianness(buf: &mut [u8], byte_order: ByteOrder, bit_depth: u8) {
|
||||||
let host = ByteOrder::native();
|
let host = ByteOrder::native();
|
||||||
|
|||||||
+125
-2
@@ -1,11 +1,11 @@
|
|||||||
extern crate tiff;
|
extern crate tiff;
|
||||||
|
|
||||||
use tiff::decoder::{ifd, Decoder, DecodingSampleBuffer};
|
use tiff::decoder::{ifd, Decoder, DecodingSampleBuffer};
|
||||||
use tiff::tags::{ByteOrder, Type};
|
use tiff::tags::{ByteOrder, PhotometricInterpretation, Type};
|
||||||
use tiff::ColorType;
|
use tiff::ColorType;
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Read, Seek};
|
use std::io::{Cursor, Read, Seek};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
const TEST_IMAGE_DIR: &str = "./tests/images/";
|
const TEST_IMAGE_DIR: &str = "./tests/images/";
|
||||||
@@ -1059,3 +1059,126 @@ fn test_gray_u12_hpredict() {
|
|||||||
// Delta-encoded 12-bit values: [100, 200, 300, 400, 1000, 2000, 3000, 4000]
|
// Delta-encoded 12-bit values: [100, 200, 300, 400, 1000, 2000, 3000, 4000]
|
||||||
test_image_sum_u16("hpredict-1c-12b.tiff", ColorType::Gray(12), 11000);
|
test_image_sum_u16("hpredict-1c-12b.tiff", ColorType::Gray(12), 11000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- TransparencyMask ----
|
||||||
|
|
||||||
|
/// Build a minimal uncompressed TIFF in memory with the given photometric interpretation.
|
||||||
|
/// Returns a 2x2 image with the provided pixel data as a single strip.
|
||||||
|
fn build_raw_tiff(
|
||||||
|
photometric: PhotometricInterpretation,
|
||||||
|
bits_per_sample: u16,
|
||||||
|
samples_per_pixel: u16,
|
||||||
|
pixel_data: &[u8],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let width: u16 = 2;
|
||||||
|
let height: u16 = 2;
|
||||||
|
let num_tags: u16 = 8;
|
||||||
|
let ifd_offset: u32 = 8;
|
||||||
|
let ifd_size = 2 + (num_tags as u32) * 12 + 4;
|
||||||
|
let strip_offset = 8 + ifd_size;
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
buf.extend_from_slice(b"II");
|
||||||
|
buf.extend_from_slice(&42u16.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&ifd_offset.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&num_tags.to_le_bytes());
|
||||||
|
|
||||||
|
// IFD entries (must be ascending tag order). Tag IDs stay raw for the
|
||||||
|
// ascending-order property; the field type uses the `Type` enum.
|
||||||
|
for &(tag, typ, value) in &[
|
||||||
|
(256u16, Type::SHORT, width as u32), // ImageWidth
|
||||||
|
(257, Type::SHORT, height as u32), // ImageLength
|
||||||
|
(258, Type::SHORT, bits_per_sample as u32), // BitsPerSample
|
||||||
|
(259, Type::SHORT, 1), // Compression = None
|
||||||
|
(262, Type::SHORT, photometric.to_u16() as u32), // PhotometricInterpretation
|
||||||
|
(273, Type::LONG, strip_offset), // StripOffsets
|
||||||
|
(277, Type::SHORT, samples_per_pixel as u32), // SamplesPerPixel
|
||||||
|
(279, Type::LONG, pixel_data.len() as u32), // StripByteCounts
|
||||||
|
] {
|
||||||
|
buf.extend_from_slice(&tag.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&typ.to_u16().to_le_bytes());
|
||||||
|
buf.extend_from_slice(&1u32.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.extend_from_slice(&0u32.to_le_bytes()); // next IFD = 0
|
||||||
|
buf.extend_from_slice(pixel_data);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transparency_mask() {
|
||||||
|
// 2x2 mask: values 0, 255, 128, 64
|
||||||
|
let data = build_raw_tiff(
|
||||||
|
PhotometricInterpretation::TransparencyMask,
|
||||||
|
8,
|
||||||
|
1,
|
||||||
|
&[0, 255, 128, 64],
|
||||||
|
);
|
||||||
|
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(8));
|
||||||
|
|
||||||
|
let result = decoder.read_image().unwrap();
|
||||||
|
match result {
|
||||||
|
DecodingSampleBuffer::U8(pixels) => {
|
||||||
|
// TransparencyMask is decoded as grayscale
|
||||||
|
assert_eq!(pixels.len(), 4);
|
||||||
|
assert_eq!(pixels[0], 0);
|
||||||
|
assert_eq!(pixels[1], 255);
|
||||||
|
assert_eq!(pixels[2], 128);
|
||||||
|
assert_eq!(pixels[3], 64);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected U8 buffer"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CIE L*a*b* ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cielab_decode() {
|
||||||
|
// 2x2 CIELab image: 3 bytes per pixel (L, a, b)
|
||||||
|
// L=128, a=0 (signed neutral), b=0 (signed neutral)
|
||||||
|
// After re-biasing: a=128 (unsigned neutral), b=128 (unsigned neutral)
|
||||||
|
let pixels: Vec<u8> = vec![
|
||||||
|
128, 0, 0, // pixel 0: L=128, a=0, b=0 (neutral gray)
|
||||||
|
255, 50, 50, // pixel 1: L=255, a=+50, b=+50
|
||||||
|
100, 206, 206, // pixel 2: L=100, a=-50 (as i8), b=-50 (as i8)
|
||||||
|
0, 128, 128, // pixel 3: L=0, a=-128 (as i8), b=-128 (as i8)
|
||||||
|
];
|
||||||
|
|
||||||
|
let data = build_raw_tiff(PhotometricInterpretation::CIELab, 8, 3, &pixels);
|
||||||
|
let mut decoder = Decoder::open(Cursor::new(&data)).unwrap();
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(decoder.colortype().unwrap(), ColorType::Lab(8));
|
||||||
|
|
||||||
|
let result = decoder.read_image().unwrap();
|
||||||
|
match result {
|
||||||
|
DecodingSampleBuffer::U8(decoded) => {
|
||||||
|
assert_eq!(decoded.len(), 12); // 4 pixels * 3 channels
|
||||||
|
|
||||||
|
// Pixel 0: L=128, a=0+128=128, b=0+128=128
|
||||||
|
assert_eq!(decoded[0], 128);
|
||||||
|
assert_eq!(decoded[1], 128);
|
||||||
|
assert_eq!(decoded[2], 128);
|
||||||
|
|
||||||
|
// Pixel 1: L=255, a=50+128=178, b=50+128=178
|
||||||
|
assert_eq!(decoded[3], 255);
|
||||||
|
assert_eq!(decoded[4], 178);
|
||||||
|
assert_eq!(decoded[5], 178);
|
||||||
|
|
||||||
|
// Pixel 2: L=100, a=206+128=78 (wrapping), b=206+128=78
|
||||||
|
assert_eq!(decoded[6], 100);
|
||||||
|
assert_eq!(decoded[7], 78);
|
||||||
|
assert_eq!(decoded[8], 78);
|
||||||
|
|
||||||
|
// Pixel 3: L=0, a=128+128=0 (wrapping), b=128+128=0
|
||||||
|
assert_eq!(decoded[9], 0);
|
||||||
|
assert_eq!(decoded[10], 0);
|
||||||
|
assert_eq!(decoded[11], 0);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected U8 buffer"),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user