feat: support 16-bit CIELab decoding

This commit is contained in:
aarshivv committed 2026-07-24 13:40:56 +05:30
1 parent 31919c7bfc
commit 3436a2222b
3 files changed
+218 -38

No files matched your search

+16 -11
View File
@@ -597,18 +597,23 @@ impl Image {
),
)),
},
// 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
// a +128 bias. For 8-bit we re-bias a/b in post-processing (see
// `cielab_to_icclab`) so callers see the same encoding as ICCLab.
//
// 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),
// but it uses L=uint16 with a/b=int16 — a mixed layout that `SampleFormat`,
// which applies uniformly to every sample, cannot express. Left as follow-up.
// CIE L*a*b* (TIFF6 Section 23, pp 110-115) encodes a/b as signed integers
// (int8 at 8-bit, int16 at 16-bit) with 0 neutral, while ICC L*a*b*
// (photometric=9) uses unsigned samples with the neutral at the middle of
// the range. `SampleFormat` applies uniformly to all samples, so it can't
// describe the L=unsigned + a/b=signed mix; instead we take a/b as signed
// from the photometric interpretation (requiring `SampleFormat::Uint`, like
// the ICCLab arm) and re-bias them to the unsigned ICCLab encoding in
// post-processing (see `cielab_to_icclab`). libtiff does the same, decoding
// both depths (`putcontig8bitCIELab8` / `putcontig8bitCIELab16`, selected by
// `initCIELabConversion` in tif_getimage.c line 2216
// @ 04884181e8903915038be553bfab404302d98ea0).
PhotometricInterpretation::CIELab => match self.photometric_samples {
3 if matches!(self.bits_per_sample, 8) => Ok(ColorType::Lab(self.bits_per_sample)),
3 if matches!(self.bits_per_sample, 8 | 16)
&& matches!(self.sample_format, SampleFormat::Uint) =>
{
Ok(ColorType::Lab(self.bits_per_sample))
}
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
PhotometricInterpretation::CIELab,
+24 -8
View File
@@ -713,15 +713,31 @@ fn invert_colors(
/// 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.
/// CIELab (photometric=8) stores a/b as signed integers where 0 is neutral;
/// ICCLab (photometric=9) stores them as unsigned with the neutral shifted to
/// the middle of the range. The two differ by exactly half the sample range, so
/// the conversion is a wrapping add of 2^(bits-1) — i.e. flipping the sign bit —
/// applied per a/b sample. `bit_depth` is 8 or 16 (guaranteed by `colortype`);
/// samples are in native byte order here (post `fix_endianness_and_predict`).
/// 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);
fn cielab_to_icclab(buf: &mut [u8], bit_depth: u8) {
match bit_depth {
16 => {
for pixel in buf.as_chunks_mut::<6>().0 {
// pixel is [L, a, b] as three native-endian u16 samples.
let (samples, _) = pixel.as_chunks_mut::<2>();
for s in &mut samples[1..=2] {
*s = u16::from_ne_bytes(*s).wrapping_add(0x8000).to_ne_bytes();
}
}
}
_ => {
for [_, a, b] in buf.as_chunks_mut::<3>().0 {
*a = a.wrapping_add(128);
*b = b.wrapping_add(128);
}
}
}
}
@@ -739,7 +755,7 @@ fn post_process_row(
) -> TiffResult<()> {
match photometric_interpretation {
PhotometricInterpretation::WhiteIsZero => invert_colors(buf, color_type, sample_format)?,
PhotometricInterpretation::CIELab => cielab_to_icclab(buf),
PhotometricInterpretation::CIELab => cielab_to_icclab(buf, color_type.bit_depth()),
_ => {}
}
Ok(())