From 983c5baea03481630f62d47807cb94d907458263 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Sun, 1 Feb 2026 02:10:53 +0100 Subject: [PATCH] Introduce LogL(uv) encodings --- src/decoder/image.rs | 88 ++++++- src/decoder/logluv.rs | 541 ++++++++++++++++++++++++++++++++++++++++++ src/decoder/mod.rs | 1 + src/tags.rs | 40 ++++ 4 files changed, 667 insertions(+), 3 deletions(-) create mode 100644 src/decoder/logluv.rs diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 7972f19..8edac3a 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -409,11 +409,29 @@ impl Image { } }; + let mut sample_format = sample_format; + let mut bits_per_sample = bits_per_sample[0]; + + // These compression methods only work for specific sample types. They pack each pixel into + // a pixel format. And when we decompress them we run an inverse transform inside the + // decompression step because that makes sense. (Even for 'raw' data you will not be able + // to handle the samples well otherwise). All we are doing is setting up the internal + // buffer here. There's an additional transform before the color readout as yo will receive + // the color as 32-bit floats (the encoding relies on very specific quantization so + // everything else would be very lossy). + if let CompressionMethod::SgiLog = compression_method { + sample_format = SampleFormat::IEEEFP; + bits_per_sample = 32; + } else if let CompressionMethod::SgiLog24 = compression_method { + sample_format = SampleFormat::IEEEFP; + bits_per_sample = 32; + } + Ok(Image { ifd: Some(ifd), width, height, - bits_per_sample: bits_per_sample[0], + bits_per_sample, samples, extra_samples, photometric_samples, @@ -523,6 +541,46 @@ impl Image { vec![self.bits_per_sample; self.samples as usize], ), )), + PhotometricInterpretation::CIELogL => match self.samples { + 1 if matches!(self.bits_per_sample, 32) + && matches!(self.sample_format, SampleFormat::IEEEFP) => + { + // We do this transform internally while reading. + Ok(ColorType::Gray(32)) + } + _ => Err(TiffError::UnsupportedError( + TiffUnsupportedError::InterpretationWithBits( + self.photometric_interpretation, + vec![self.bits_per_sample; self.samples as usize], + ), + )), + }, + PhotometricInterpretation::CIELogLuv => match self.samples { + // Only fully supported for the compression format `SGILog`/`SGILog24` which must + // be used with 16-bit per sample. + // + // It's actually an interesting point what we should output here. Note that in + // libtiff there is a RAW mode (a single unsigned sample per pixel) as well as a + // 32-bit floating point mode for XYZ data. The `Lu'v'` variant is a heavily + // customized quantization so we should not return it directly. The format itself + // describes being based off `xyY` so that may make sense but there is a pole in + // the relation to `XYZ` (at y=0) which is absent in the chosen quantization since + // we assume a bias of `.5` so `v >= 1/820` and thus `y >= 0.49/820` in practice. + // Also we lose a bit of accuracy with just 16-bit precision. + // + // In `libtiff` data is always transformed through XYZ. So let's do that. + 3 if matches!(self.bits_per_sample, 32) + && matches!(self.sample_format, SampleFormat::IEEEFP) => + { + Ok(ColorType::RGB(32)) + } + _ => Err(TiffError::UnsupportedError( + TiffUnsupportedError::InterpretationWithBits( + self.photometric_interpretation, + vec![self.bits_per_sample; self.samples as usize], + ), + )), + }, // Unsupported due to extra unfiltering and conversion steps. We need to find the // Decode tag (SRATIONAL; 2 * SamplesPerPixel) and apply the following conversion: // @@ -554,11 +612,35 @@ impl Image { compressed_length: u64, // FIXME: these should be `expect` attributes or we choose another way of passing them. #[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>, - #[cfg_attr(not(feature = "fax"), allow(unused_variables))] dimensions: (u32, u32), - #[cfg_attr(not(feature = "webp"), allow(unused_variables))] samples: u16, + dimensions: (u32, u32), + samples: u16, ) -> TiffResult> { Ok(match compression_method { CompressionMethod::None => Box::new(reader), + CompressionMethod::SgiLog => match samples { + 1 => Box::new(super::logluv::LogLuv16::new( + reader, + compressed_length, + dimensions.0, + )?), + 3 => Box::new(super::logluv::LogLuv32::new( + reader, + compressed_length, + dimensions.0, + )?), + _ => { + return Err(TiffError::UnsupportedError( + TiffUnsupportedError::UnsupportedCompressionMethod( + CompressionMethod::SgiLog, + ), + )) + } + }, + CompressionMethod::SgiLog24 => Box::new(super::logluv::LogLuv24::new( + reader, + compressed_length, + dimensions.0, + )?), #[cfg(feature = "lzw")] CompressionMethod::LZW => Box::new(super::stream::LZWReader::new( reader, diff --git a/src/decoder/logluv.rs b/src/decoder/logluv.rs new file mode 100644 index 0000000..6216fbf --- /dev/null +++ b/src/decoder/logluv.rs @@ -0,0 +1,541 @@ +use std::io::{Read, Take}; + +use crate::TiffError; + +pub struct LogLuv24 { + reader: Take, + /// The required buffer size. We only decompressed all samples at once. + chunk_sz: usize, +} + +impl LogLuv24 { + pub fn new(reader: R, n: u64, npixels: u32) -> Result + where + R: Read, + { + let chunk_sz = usize::try_from( + u64::from(npixels) + .checked_mul(4 * 3) + .ok_or(TiffError::LimitsExceeded)?, + )?; + + let reader = reader.take(n); + Ok(LogLuv24 { reader, chunk_sz }) + } + + fn expand_buf(buf: &mut [u8]) { + let data_len = buf.len() / 4; + + for i in (0..data_len / 3).rev() { + let ch = buf.as_chunks_mut::<3>().0[i]; + let [a, b, c] = ch.map(u32::from); + + let raw = a << 16 | b << 8 | c; + let l = (raw >> 14) & 0x3ff; + let uv = decode_quantized_uv(raw & 0x3fff); + + let by = dequantize_24_luma(l); + let (u, v) = dequantize_24(uv); + + let s = 1. / (6. * u - 16. * v + 12.); + let x = 9. * u * s; + let y = 4. * v * s; + + let bx = x / y * by; + let bz = (1. - x - y) / y * by; + + let [r, g, b] = xyz_to_rgb([bx, by, bz]); + + let into = &mut buf.as_chunks_mut::<12>().0[i]; + into[0..4].copy_from_slice(&f32::to_ne_bytes(r)); + into[4..8].copy_from_slice(&f32::to_ne_bytes(g)); + into[8..12].copy_from_slice(&f32::to_ne_bytes(b)); + } + } +} + +impl Read for LogLuv24 { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.read_exact(buf)?; + Ok(buf.len()) + } + + // FIXME: this is the interface we have for compressed chunks but it is not optimal for what we + // want. Note `fill_buf` is not super helpful since it does not guarantee any size of the + // filled buffer but we need 24-bits at a time. Two code paths is even more awkward. + fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> { + if !buf.len().is_multiple_of(self.chunk_sz) { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "buffer size {0} must match the chunk size for compression method SGILog24 {1}", + buf.len(), + self.chunk_sz, + ), + )); + } + + let data_len = buf.len() / 4; + self.reader.read_exact(&mut buf[..data_len])?; + Self::expand_buf(buf); + + Ok(()) + } +} + +fn packbits_decode( + reader: &mut R, + buf: &mut [[u8; N]], +) -> std::io::Result<()> { + for offset in (0..N).rev() { + let mut i = 0; + let mut code = [0u8; 1]; + + loop { + let Some(pixels) = buf.get_mut(i..) else { + break; + }; + + if pixels.is_empty() { + break; + } + + let pixels = pixels.iter_mut(); + reader.read_exact(&mut code)?; + let [repeat] = code; + + if repeat >= 128 { + reader.read_exact(&mut code)?; + let [value] = code; + let rc = (repeat - 128) + 2; + + for (_, px) in (0..rc).zip(pixels) { + px[offset] = value; + } + + i += usize::from(rc); + } else { + for (_, px) in (0..repeat).zip(pixels) { + reader.read_exact(&mut code)?; + let [value] = code; + px[offset] = value; + } + + i += usize::from(repeat); + } + } + } + + Ok(()) +} + +pub struct LogLuv32 { + reader: Take, + /// The required buffer size. We only decompressed all samples at once. + chunk_sz: usize, +} + +impl LogLuv32 { + pub fn new(reader: R, n: u64, npixels: u32) -> Result { + let chunk_sz = usize::try_from( + u64::from(npixels) + .checked_mul(4 * 3) + .ok_or(TiffError::LimitsExceeded)?, + )?; + + let reader = reader.take(n); + Ok(LogLuv32 { reader, chunk_sz }) + } + + fn decode_rle_row(&mut self, row: &mut [u8]) -> std::io::Result<()> + where + R: Read, + { + // 32-bit per pixel format. + let n_pixels = self.chunk_sz / 12; + // buffer for reading (using only 32-bit per pixel) + let raw = &mut row.as_chunks_mut::<4>().0[..n_pixels]; + packbits_decode::<_, 4>(&mut self.reader, raw)?; + + Ok(()) + } + + fn expand_row(row: &mut [u8]) { + let uvscale = 1.0 / 410.0; + let n_pixels = row.len() / 12; + + // Work backwards, the 32-bit compressed raw data is at the front of the row buffer and + // this avoids overwriting data before we read it. + for i in (0..n_pixels).rev() { + let raw = row.as_chunks_mut::<4>().0[i]; + + let l = u16::from(raw[0]) << 8 | u16::from(raw[1]); + let by = log16_to_y(l); + + let u = uvscale * (f32::from(raw[2]) + 0.5); + let v = uvscale * (f32::from(raw[3]) + 0.5); + + let s = 1. / (6. * u - 16. * v + 12.); + let x = 9. * u * s; + let y = 4. * v * s; + + let bx = x / y * by; + let bz = (1. - x - y) / y * by; + + let [r, g, b] = xyz_to_rgb([bx, by, bz]); + let into = &mut row.as_chunks_mut::<12>().0[i]; + into[0..4].copy_from_slice(&f32::to_ne_bytes(r)); + into[4..8].copy_from_slice(&f32::to_ne_bytes(g)); + into[8..12].copy_from_slice(&f32::to_ne_bytes(b)); + } + } +} + +impl Read for LogLuv32 { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.read_exact(buf)?; + Ok(buf.len()) + } + + // FIXME: See `LogLuv24::read_exact` + fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> { + for row in buf.chunks_exact_mut(self.chunk_sz) { + self.decode_rle_row(row)?; + Self::expand_row(row); + } + + Ok(()) + } +} + +/// Pack-Bits like encoding for a single 16-bit log-L sample. +pub struct LogLuv16 { + reader: Take, + /// The required buffer size. We only decompressed all samples at once. + chunk_sz: usize, +} + +impl LogLuv16 { + pub fn new(reader: R, n: u64, npixels: u32) -> Result { + let chunk_sz = usize::try_from( + u64::from(npixels) + .checked_mul(4) + .ok_or(TiffError::LimitsExceeded)?, + )?; + + let reader = reader.take(n); + Ok(LogLuv16 { reader, chunk_sz }) + } + + fn decode_rle_row(&mut self, row: &mut [u8]) -> std::io::Result<()> + where + R: Read, + { + // 32-bit per pixel format. + let n_pixels = self.chunk_sz / 4; + // buffer for reading (16-bit per pixel) + let raw = &mut row.as_chunks_mut::<2>().0[..n_pixels]; + packbits_decode::<_, 2>(&mut self.reader, raw)?; + + Ok(()) + } + + fn expand_row(row: &mut [u8]) { + let n_pixels = row.len() / 4; + + // Work backwards, the 32-bit compressed raw data is at the front of the row buffer and + // this avoids overwriting data before we read it. + for i in (0..n_pixels).rev() { + let raw = row.as_chunks_mut::<2>().0[i]; + + let l = u16::from(raw[0]) << 8 | u16::from(raw[1]); + let by = log16_to_y(l); + + let into = &mut row.as_chunks_mut::<4>().0[i]; + into[0..4].copy_from_slice(&f32::to_ne_bytes(by)); + } + } +} + +impl Read for LogLuv16 { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.read_exact(buf)?; + Ok(buf.len()) + } + + // FIXME: See `LogLuv24::read_exact` + fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> { + for row in buf.chunks_exact_mut(self.chunk_sz) { + self.decode_rle_row(row)?; + Self::expand_row(row); + } + + Ok(()) + } +} + +// Table information from: `libtiff`, 1997 by Greg Ward Larson, SGI +#[doc(alias = "UV_SQSIZ")] +const UV_QUANTIZATION_WIDTH: f32 = 0.003500; + +// The `v` value corresonding to the bottom most row (quantized v' = 0). +const UV_VSTART: f32 = 0.016940; + +/// Number of distinct 'u' rows, each at a different v level. Each is quantized individually with +/// the first quantization level starting at its `ustart` and `UV_QUANTIZATION_WIDTH` wide. +const UV_NVS: usize = 163; + +/// Contrary to the original C code we calculate the dependent values (cumulative sum). +struct UvRow { + ustart: f32, + num_us: u16, +} + +#[rustfmt::skip] +const UV_ROWS: [UvRow; UV_NVS] = [ + UvRow { ustart: 0.247663, num_us: 4, }, + UvRow { ustart: 0.243779, num_us: 6, }, + UvRow { ustart: 0.241684, num_us: 7, }, + UvRow { ustart: 0.237874, num_us: 9, }, + UvRow { ustart: 0.235906, num_us: 10, }, + UvRow { ustart: 0.232153, num_us: 12, }, + UvRow { ustart: 0.228352, num_us: 14, }, + UvRow { ustart: 0.226259, num_us: 15, }, + UvRow { ustart: 0.222371, num_us: 17, }, + UvRow { ustart: 0.220410, num_us: 18, }, + UvRow { ustart: 0.214710, num_us: 21, }, + UvRow { ustart: 0.212714, num_us: 22, }, + UvRow { ustart: 0.210721, num_us: 23, }, + UvRow { ustart: 0.204976, num_us: 26, }, + UvRow { ustart: 0.202986, num_us: 27, }, + UvRow { ustart: 0.199245, num_us: 29, }, + UvRow { ustart: 0.195525, num_us: 31, }, + UvRow { ustart: 0.193560, num_us: 32, }, + UvRow { ustart: 0.189878, num_us: 34, }, + UvRow { ustart: 0.186216, num_us: 36, }, + UvRow { ustart: 0.186216, num_us: 36, }, + UvRow { ustart: 0.182592, num_us: 38, }, + UvRow { ustart: 0.179003, num_us: 40, }, + UvRow { ustart: 0.175466, num_us: 42, }, + UvRow { ustart: 0.172001, num_us: 44, }, + UvRow { ustart: 0.172001, num_us: 44, }, + UvRow { ustart: 0.168612, num_us: 46, }, + UvRow { ustart: 0.168612, num_us: 46, }, + UvRow { ustart: 0.163575, num_us: 49, }, + UvRow { ustart: 0.158642, num_us: 52, }, + UvRow { ustart: 0.158642, num_us: 52, }, + UvRow { ustart: 0.158642, num_us: 52, }, + UvRow { ustart: 0.153815, num_us: 55, }, + UvRow { ustart: 0.153815, num_us: 55, }, + UvRow { ustart: 0.149097, num_us: 58, }, + UvRow { ustart: 0.149097, num_us: 58, }, + UvRow { ustart: 0.142746, num_us: 62, }, + UvRow { ustart: 0.142746, num_us: 62, }, + UvRow { ustart: 0.142746, num_us: 62, }, + UvRow { ustart: 0.138270, num_us: 65, }, + UvRow { ustart: 0.138270, num_us: 65, }, + UvRow { ustart: 0.138270, num_us: 65, }, + UvRow { ustart: 0.132166, num_us: 69, }, + UvRow { ustart: 0.132166, num_us: 69, }, + UvRow { ustart: 0.126204, num_us: 73, }, + UvRow { ustart: 0.126204, num_us: 73, }, + UvRow { ustart: 0.126204, num_us: 73, }, + UvRow { ustart: 0.120381, num_us: 77, }, + UvRow { ustart: 0.120381, num_us: 77, }, + UvRow { ustart: 0.120381, num_us: 77, }, + UvRow { ustart: 0.120381, num_us: 77, }, + UvRow { ustart: 0.112962, num_us: 82, }, + UvRow { ustart: 0.112962, num_us: 82, }, + UvRow { ustart: 0.112962, num_us: 82, }, + UvRow { ustart: 0.107450, num_us: 86, }, + UvRow { ustart: 0.107450, num_us: 86, }, + UvRow { ustart: 0.107450, num_us: 86, }, + UvRow { ustart: 0.107450, num_us: 86, }, + UvRow { ustart: 0.100343, num_us: 91, }, + UvRow { ustart: 0.100343, num_us: 91, }, + UvRow { ustart: 0.100343, num_us: 91, }, + UvRow { ustart: 0.095126, num_us: 95, }, + UvRow { ustart: 0.095126, num_us: 95, }, + UvRow { ustart: 0.095126, num_us: 95, }, + UvRow { ustart: 0.095126, num_us: 95, }, + UvRow { ustart: 0.088276, num_us: 100, }, + UvRow { ustart: 0.088276, num_us: 100, }, + UvRow { ustart: 0.088276, num_us: 100, }, + UvRow { ustart: 0.088276, num_us: 100, }, + UvRow { ustart: 0.081523, num_us: 105, }, + UvRow { ustart: 0.081523, num_us: 105, }, + UvRow { ustart: 0.081523, num_us: 105, }, + UvRow { ustart: 0.081523, num_us: 105, }, + UvRow { ustart: 0.074861, num_us: 110, }, + UvRow { ustart: 0.074861, num_us: 110, }, + UvRow { ustart: 0.074861, num_us: 110, }, + UvRow { ustart: 0.074861, num_us: 110, }, + UvRow { ustart: 0.068290, num_us: 115, }, + UvRow { ustart: 0.068290, num_us: 115, }, + UvRow { ustart: 0.068290, num_us: 115, }, + UvRow { ustart: 0.068290, num_us: 115, }, + UvRow { ustart: 0.063573, num_us: 119, }, + UvRow { ustart: 0.063573, num_us: 119, }, + UvRow { ustart: 0.063573, num_us: 119, }, + UvRow { ustart: 0.063573, num_us: 119, }, + UvRow { ustart: 0.057219, num_us: 124, }, + UvRow { ustart: 0.057219, num_us: 124, }, + UvRow { ustart: 0.057219, num_us: 124, }, + UvRow { ustart: 0.057219, num_us: 124, }, + UvRow { ustart: 0.050985, num_us: 129, }, + UvRow { ustart: 0.050985, num_us: 129, }, + UvRow { ustart: 0.050985, num_us: 129, }, + UvRow { ustart: 0.050985, num_us: 129, }, + UvRow { ustart: 0.050985, num_us: 129, }, + UvRow { ustart: 0.044859, num_us: 134, }, + UvRow { ustart: 0.044859, num_us: 134, }, + UvRow { ustart: 0.044859, num_us: 134, }, + UvRow { ustart: 0.044859, num_us: 134, }, + UvRow { ustart: 0.040571, num_us: 138, }, + UvRow { ustart: 0.040571, num_us: 138, }, + UvRow { ustart: 0.040571, num_us: 138, }, + UvRow { ustart: 0.040571, num_us: 138, }, + UvRow { ustart: 0.036339, num_us: 142, }, + UvRow { ustart: 0.036339, num_us: 142, }, + UvRow { ustart: 0.036339, num_us: 142, }, + UvRow { ustart: 0.036339, num_us: 142, }, + UvRow { ustart: 0.032139, num_us: 146, }, + UvRow { ustart: 0.032139, num_us: 146, }, + UvRow { ustart: 0.032139, num_us: 146, }, + UvRow { ustart: 0.032139, num_us: 146, }, + UvRow { ustart: 0.027947, num_us: 150, }, + UvRow { ustart: 0.027947, num_us: 150, }, + UvRow { ustart: 0.027947, num_us: 150, }, + UvRow { ustart: 0.023739, num_us: 154, }, + UvRow { ustart: 0.023739, num_us: 154, }, + UvRow { ustart: 0.023739, num_us: 154, }, + UvRow { ustart: 0.023739, num_us: 154, }, + UvRow { ustart: 0.019504, num_us: 158, }, + UvRow { ustart: 0.019504, num_us: 158, }, + UvRow { ustart: 0.019504, num_us: 158, }, + UvRow { ustart: 0.016976, num_us: 161, }, + UvRow { ustart: 0.016976, num_us: 161, }, + UvRow { ustart: 0.016976, num_us: 161, }, + UvRow { ustart: 0.016976, num_us: 161, }, + UvRow { ustart: 0.012639, num_us: 165, }, + UvRow { ustart: 0.012639, num_us: 165, }, + UvRow { ustart: 0.012639, num_us: 165, }, + UvRow { ustart: 0.009991, num_us: 168, }, + UvRow { ustart: 0.009991, num_us: 168, }, + UvRow { ustart: 0.009991, num_us: 168, }, + UvRow { ustart: 0.009016, num_us: 170, }, + UvRow { ustart: 0.009016, num_us: 170, }, + UvRow { ustart: 0.009016, num_us: 170, }, + UvRow { ustart: 0.006217, num_us: 173, }, + UvRow { ustart: 0.006217, num_us: 173, }, + UvRow { ustart: 0.005097, num_us: 175, }, + UvRow { ustart: 0.005097, num_us: 175, }, + UvRow { ustart: 0.005097, num_us: 175, }, + UvRow { ustart: 0.003909, num_us: 177, }, + UvRow { ustart: 0.003909, num_us: 177, }, + UvRow { ustart: 0.002340, num_us: 177, }, + UvRow { ustart: 0.002389, num_us: 170, }, + UvRow { ustart: 0.001068, num_us: 164, }, + UvRow { ustart: 0.001653, num_us: 157, }, + UvRow { ustart: 0.000717, num_us: 150, }, + UvRow { ustart: 0.001614, num_us: 143, }, + UvRow { ustart: 0.000270, num_us: 136, }, + UvRow { ustart: 0.000484, num_us: 129, }, + UvRow { ustart: 0.001103, num_us: 123, }, + UvRow { ustart: 0.001242, num_us: 115, }, + UvRow { ustart: 0.001188, num_us: 109, }, + UvRow { ustart: 0.001011, num_us: 103, }, + UvRow { ustart: 0.000709, num_us: 97, }, + UvRow { ustart: 0.000301, num_us: 89, }, + UvRow { ustart: 0.002416, num_us: 82, }, + UvRow { ustart: 0.003251, num_us: 76, }, + UvRow { ustart: 0.003246, num_us: 69, }, + UvRow { ustart: 0.004141, num_us: 62, }, + UvRow { ustart: 0.005963, num_us: 55, }, + UvRow { ustart: 0.008839, num_us: 47, }, + UvRow { ustart: 0.010490, num_us: 40, }, + UvRow { ustart: 0.016994, num_us: 31, }, + UvRow { ustart: 0.023659, num_us: 21, }, +]; + +const UV_CUMULATIVE: [u16; UV_NVS] = { + let mut arr = [0u16; UV_NVS]; + let mut sum = 0; + let mut i = 0; + while i < UV_NVS { + arr[i] = sum; + sum += UV_ROWS[i].num_us; + i += 1; + } + arr +}; + +const UV_NDIVS: u16 = UV_CUMULATIVE[UV_NVS - 1] + UV_ROWS[UV_NVS - 1].num_us; + +fn decode_quantized_uv(uv: u32) -> (u8, u8) { + if uv >= UV_NDIVS as u32 { + return (0, 0); + } + + // Find the start of the row, or the index after that. + let v_index = match UV_CUMULATIVE.binary_search(&(uv as u16)) { + Ok(n) => n, + Err(n) => n - 1, + }; + + let u_index = uv as u16 - UV_CUMULATIVE[v_index]; + (u_index as u8, v_index as u8) +} + +fn dequantize_24_luma(l: u32) -> f32 { + if l == 0 { + 0.0 + } else { + let le = (l & 0x3ff) as f32; + let exponent = ((le / 64.0).floor() as i32) - 24; + let mantissa = le - (exponent as f32 + 24.0) * 64.0; + (mantissa + 64.0) / 64.0 * 2f32.powi(exponent) + } +} + +fn dequantize_24((u, v): (u8, u8)) -> (f32, f32) { + let v_f = UV_VSTART + (v as f32) * UV_QUANTIZATION_WIDTH; + let u_row = &UV_ROWS[v as usize]; + let u_f = u_row.ustart + (u as f32) * UV_QUANTIZATION_WIDTH; + (u_f, v_f) +} + +fn log16_to_y(l: u16) -> f32 { + if l == 0 { + return 0.0; + } + + let le = f32::from(l & 0x7fff); + // Y = exp(M_LN2 / 256. * (Le + .5) - M_LN2 * 64.); + let y = (std::f32::consts::LN_2 / 256.0 * (le + 0.5) - std::f32::consts::LN_2 * 64.0).exp(); + + if l & 0x8000 != 0 { + -y + } else { + y + } +} + +fn xyz_to_rgb([l, u, v]: [f32; 3]) -> [f32; 3] { + // XYZ to sRGB primaries at D65 + let r = 3.2404542 * l + -1.5371385 * u + -0.4985314 * v; + let g = -0.969266 * l + 1.8760108 * u + 0.0415560 * v; + let b = 0.0556434 * l + -0.2040259 * u + 1.0572252 * v; + [r, g, b] +} + +#[test] +fn test_decode_quantized_uv() { + // Test some known values from the original C code. + assert_eq!(decode_quantized_uv(0), (0, 0)); + assert_eq!(decode_quantized_uv(3), (3, 0)); + assert_eq!(decode_quantized_uv(4), (0, 1)); + assert_eq!(decode_quantized_uv(9), (5, 1)); + assert_eq!(decode_quantized_uv(16288), (20, 162)); + assert_eq!(decode_quantized_uv(16289), (0, 0)); // Out of range +} diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 12009f3..ba423ef 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -19,6 +19,7 @@ use self::stream::{ByteOrder, EndianReader}; mod cycles; pub mod ifd; mod image; +mod logluv; mod stream; mod tag_reader; diff --git a/src/tags.rs b/src/tags.rs index 9d18084..ab1ffba 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -283,6 +283,14 @@ pub enum CompressionMethod(u16) unknown( // Self-assigned by libtiff WebP = 0xC351, + + // Self-assigned by libtiff/SGI + // A packbit like scheme on specific 32-bit samples (16 sint luma, 8 sint u,v). Only really + // compatible with PhotometricInterpretation CIELogLuv. + SgiLog = 34676, + // A packbit like scheme on specific 24-bit samples (10 packed bits luma, 14-bit uv index). + // Only really compatible with PhotometricInterpretation CIELogLuv. + SgiLog24 = 34677, } } @@ -295,9 +303,41 @@ pub enum PhotometricInterpretation(u16) { TransparencyMask = 4, CMYK = 5, YCbCr = 6, + /// Biased Lab with all coefficients stored as unsigned integers. CIELab = 8, IccLab = 9, ItuLab = 10, + /// Logarithmic L* color space, + /// + /// see: + /// References: + /// + /// The `uv` coordinates are based on: + /// + /// ```text + /// (6*u - 16*v + 12) * (-2x + 12y + 3) = 36 + /// + /// u' = 4x / ( -2x + 12y + 3 ) + /// v' = 9y / ( -2x + 12y + 3 ) + /// x = 9u' / (6u' - 16v' + 12) + /// y = 4v' / (6u' - 16v' + 12) + /// ``` + /// + /// Since `(-2x + 12y + 3) <= 15` it holds `(6u' - 16v' + 12) >= 36/15` for all XYZ colors + /// which makes the back transform to `xyY` well-defined. Also `v' <= 9/15 = 0.6` and also + /// by practical means `u' <= 2/3`. + /// + /// The compression format that is supposed to be used with this constrains them as well. It + /// encodes each component as a 16-bit unsigned integer scaled and clamped to a quantization + /// using 256 out of 410 levels or an index into a table based on a quantization to 177 levels + /// each `0.0035` (~1/286) wide and 163 levels of `v` (same quantization width). + /// + /// > Larson, G.W., ``Overcoming Gamut and Dynamic Range Limitations in Digital Images,'' Proceedings of the Sixth Color Imaging Conference, November 1998. + CIELogL = 32844, + /// Logarithmic L*u'v' color space, + /// + /// See references under [`Self::CIELogL`]. + CIELogLuv = 32845, } }