diff --git a/Cargo.toml b/Cargo.toml index 7cdd0f5..5b84039 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,10 @@ byteorder = "1" bytes = "1.7.0" flate2 = "1.0.20" futures = "0.3.31" -jpeg = { package = "jpeg-decoder", version = "0.3.0", default-features = false } jpeg2k = { version = "0.10.1", optional = true } lerc = { version = "0.2.1", optional = true } lzma-rust2 = { version = "0.15.7", optional = true, features = ["xz"] } +mozjpeg = { version = "0.10", default-features = false } ndarray = { version = "0.17", optional = true } num_enum = "0.7.3" object_store = { version = "0.13", optional = true } diff --git a/python/Cargo.lock b/python/Cargo.lock index 71fcd29..23c03f4 100644 --- a/python/Cargo.lock +++ b/python/Cargo.lock @@ -32,6 +32,12 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-tiff" version = "0.2.0" @@ -42,10 +48,10 @@ dependencies = [ "bytes", "flate2", "futures", - "jpeg-decoder", "jpeg2k", "lerc", "lzma-rust2", + "mozjpeg", "num_enum", "object_store", "reqwest 0.13.2", @@ -319,6 +325,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -850,12 +862,6 @@ dependencies = [ "libc", ] -[[package]] -name = "jpeg-decoder" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" - [[package]] name = "jpeg2k" version = "0.10.1" @@ -1025,6 +1031,30 @@ dependencies = [ "pxfm", ] +[[package]] +name = "mozjpeg" +version = "0.10.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7891b80aaa86097d38d276eb98b3805d6280708c4e0a1e6f6aed9380c51fec9" +dependencies = [ + "arrayvec", + "bytemuck", + "libc", + "mozjpeg-sys", + "rgb", +] + +[[package]] +name = "mozjpeg-sys" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f0dc668bf9bf888c88e2fb1ab16a406d2c380f1d082b20d51dd540ab2aa70c1" +dependencies = [ + "cc", + "dunce", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1627,6 +1657,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.14" diff --git a/python/src/decoder.rs b/python/src/decoder.rs index da167ba..2aeb990 100644 --- a/python/src/decoder.rs +++ b/python/src/decoder.rs @@ -82,6 +82,7 @@ impl Decoder for PyDecoder { _samples_per_pixel: u16, _bits_per_sample: u16, _lerc_parameters: Option<&[u32]>, + _reference_black_white: Option<&[f64; 6]>, ) -> AsyncTiffResult> { Python::attach(|py| self.call(py, buffer)) .map_err(|err| AsyncTiffError::General(err.to_string())) diff --git a/python/tests/test_integration_rasterio.py b/python/tests/test_integration_rasterio.py index 9282e55..901439c 100644 --- a/python/tests/test_integration_rasterio.py +++ b/python/tests/test_integration_rasterio.py @@ -41,11 +41,9 @@ ("rasterio", "uint8_rgb_deflate_block64_cog"), ("rasterio", "uint8_rgb_webp_block64_cog"), ("rasterio", "uint8_rgba_webp_block64_cog"), - # Ycbcr subsampling not implemented - # ("rio-tiler", "cog_rgb_with_stats"), + ("rio-tiler", "cog_rgb_with_stats"), ("umbra", "sydney_airport_GEC"), - # Ycbcr subsampling not implemented - # ("vantor", "maxar_opendata_yellowstone_visual"), + ("vantor", "maxar_opendata_yellowstone_visual"), ], ) async def test_read( @@ -68,6 +66,9 @@ async def test_read( x_count, y_count = tile_count with load_rasterio(file_name, variant=variant) as rasterio_ds: + img_width = rasterio_ds.width + img_height = rasterio_ds.height + for x in range(x_count): for y in range(y_count): tile = await ifd.fetch_tile(x, y) @@ -78,10 +79,21 @@ async def test_read( rasterio_data = rasterio_ds.read(window=rasterio_window, boundless=True) + # Clip to valid image region — JPEG edge tiles contain padded data beyond + # image boundaries that rasterio zeroes out with boundless=True. + valid_cols = min(tile_width, img_width - x * tile_width) + valid_rows = min(tile_height, img_height - y * tile_height) + if ifd.planar_configuration == PlanarConfiguration.Chunky: - np.testing.assert_array_equal(data, reshape_as_image(rasterio_data)) + np.testing.assert_array_equal( + data[:valid_rows, :valid_cols], + reshape_as_image(rasterio_data)[:valid_rows, :valid_cols], + ) else: - np.testing.assert_array_equal(data, rasterio_data) + np.testing.assert_array_equal( + data[..., :valid_rows, :valid_cols], + rasterio_data[..., :valid_rows, :valid_cols], + ) def create_window(tile_width: int, tile_height: int, x: int, y: int) -> Window: diff --git a/src/decoder.rs b/src/decoder.rs index 0266d5d..c95a8f1 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -3,9 +3,11 @@ use std::collections::HashMap; use std::fmt::Debug; use std::io::{Cursor, Read}; +use std::panic; use bytes::Bytes; use flate2::bufread::ZlibDecoder; +use mozjpeg::{ColorSpace as MozColorSpace, Decompress}; use crate::error::{AsyncTiffError, AsyncTiffResult, TiffError, TiffUnsupportedError}; use crate::tags::{Compression, PhotometricInterpretation}; @@ -70,6 +72,7 @@ impl Default for DecoderRegistry { /// A trait to decode a TIFF tile. pub trait Decoder: Debug + Send + Sync { /// Decode a TIFF tile. + #[allow(clippy::too_many_arguments)] fn decode_tile( &self, buffer: Bytes, @@ -78,6 +81,7 @@ pub trait Decoder: Debug + Send + Sync { samples_per_pixel: u16, bits_per_sample: u16, lerc_parameters: Option<&[u32]>, + reference_black_white: Option<&[f64; 6]>, ) -> AsyncTiffResult>; } @@ -94,6 +98,7 @@ impl Decoder for DeflateDecoder { _samples_per_pixel: u16, _bits_per_sample: u16, _lerc_parameters: Option<&[u32]>, + _reference_black_white: Option<&[f64; 6]>, ) -> AsyncTiffResult> { let mut decoder = ZlibDecoder::new(Cursor::new(buffer)); let mut buf = Vec::new(); @@ -115,8 +120,14 @@ impl Decoder for JPEGDecoder { _samples_per_pixel: u16, _bits_per_sample: u16, _lerc_parameters: Option<&[u32]>, + reference_black_white: Option<&[f64; 6]>, ) -> AsyncTiffResult> { - decode_modern_jpeg(buffer, photometric_interpretation, jpeg_tables) + decode_modern_jpeg( + buffer, + photometric_interpretation, + jpeg_tables, + reference_black_white, + ) } } @@ -156,6 +167,7 @@ impl Decoder for LercDecoder { _samples_per_pixel: u16, _bits_per_sample: u16, lerc_parameters: Option<&[u32]>, + _reference_black_white: Option<&[f64; 6]>, ) -> AsyncTiffResult> { // LercParameters[1] is the inner compression type: // 0 = none, 1 = deflate, 2 = zstd @@ -213,6 +225,7 @@ impl Decoder for LZMADecoder { _samples_per_pixel: u16, _bits_per_sample: u16, _lerc_parameters: Option<&[u32]>, + _reference_black_white: Option<&[f64; 6]>, ) -> AsyncTiffResult> { use bytes::Buf; use lzma_rust2::XzReader; @@ -237,6 +250,7 @@ impl Decoder for LZWDecoder { _samples_per_pixel: u16, _bits_per_sample: u16, _lerc_parameters: Option<&[u32]>, + _reference_black_white: Option<&[f64; 6]>, ) -> AsyncTiffResult> { // https://github.com/image-rs/image-tiff/blob/90ae5b8e54356a35e266fb24e969aafbcb26e990/src/decoder/stream.rs#L147 let mut decoder = weezl::decode::Decoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8); @@ -260,6 +274,7 @@ impl Decoder for JPEG2kDecoder { _samples_per_pixel: u16, _bits_per_sample: u16, _lerc_parameters: Option<&[u32]>, + _reference_black_white: Option<&[f64; 6]>, ) -> AsyncTiffResult> { let decoder = jpeg2k::DecodeParameters::new(); @@ -294,6 +309,7 @@ impl Decoder for WebPDecoder { samples_per_pixel: u16, bits_per_sample: u16, _lerc_parameters: Option<&[u32]>, + _reference_black_white: Option<&[f64; 6]>, ) -> AsyncTiffResult> { let decoded = webp::Decoder::new(&buffer) .decode() @@ -330,6 +346,7 @@ impl Decoder for UncompressedDecoder { _samples_per_pixel: u16, _bits_per_sample: u16, _lerc_parameters: Option<&[u32]>, + _reference_black_white: Option<&[f64; 6]>, ) -> AsyncTiffResult> { Ok(buffer.to_vec()) } @@ -348,6 +365,7 @@ impl Decoder for ZstdDecoder { _samples_per_pixel: u16, _bits_per_sample: u16, _lerc_parameters: Option<&[u32]>, + _reference_black_white: Option<&[f64; 6]>, ) -> AsyncTiffResult> { let mut decoder = zstd::Decoder::new(Cursor::new(buffer))?; let mut buf = Vec::new(); @@ -356,56 +374,163 @@ impl Decoder for ZstdDecoder { } } -// https://github.com/image-rs/image-tiff/blob/3bfb43e83e31b0da476832067ada68a82b378b7b/src/decoder/image.rs#L389-L450 fn decode_modern_jpeg( buf: Bytes, photometric_interpretation: PhotometricInterpretation, jpeg_tables: Option<&[u8]>, + reference_black_white: Option<&[f64; 6]>, ) -> AsyncTiffResult> { - // Construct new jpeg_reader wrapping a SmartReader. - // // JPEG compression in TIFF allows saving quantization and/or huffman tables in one central - // location. These `jpeg_tables` are simply prepended to the remaining jpeg image data. Because - // these `jpeg_tables` start with a `SOI` (HEX: `0xFFD8`) or __start of image__ marker which is - // also at the beginning of the remaining JPEG image data and would confuse the JPEG renderer, - // one of these has to be taken off. In this case the first two bytes of the remaining JPEG - // data is removed because it follows `jpeg_tables`. Similary, `jpeg_tables` ends with a `EOI` - // (HEX: `0xFFD9`) or __end of image__ marker, this has to be removed as well (last two bytes - // of `jpeg_tables`). - let reader = Cursor::new(buf); - - let jpeg_reader = match jpeg_tables { - Some(jpeg_tables) => { - let mut reader = reader; - reader.read_exact(&mut [0; 2])?; - - Box::new(Cursor::new(&jpeg_tables[..jpeg_tables.len() - 2]).chain(reader)) - as Box + // location. These `jpeg_tables` are simply prepended to the remaining jpeg image data. + // `jpeg_tables` starts with SOI (0xFFD8) and ends with EOI (0xFFD9); strip them off before + // concatenating with the tile data (which starts with its own SOI). + let jpeg_data: Vec = match jpeg_tables { + Some(tables) => { + // Strip SOI from tile data (first 2 bytes) and EOI from tables (last 2 bytes), + // then prepend the stripped tables to the tile data. + let stripped_tables = &tables[..tables.len() - 2]; + let stripped_tile = &buf[2..]; + let mut data = Vec::with_capacity(stripped_tables.len() + stripped_tile.len()); + data.extend_from_slice(stripped_tables); + data.extend_from_slice(stripped_tile); + data } - None => Box::new(reader), + None => buf.to_vec(), }; - let mut decoder = jpeg::Decoder::new(jpeg_reader); - - match photometric_interpretation { - PhotometricInterpretation::RGB => decoder.set_color_transform(jpeg::ColorTransform::RGB), - PhotometricInterpretation::WhiteIsZero - | PhotometricInterpretation::BlackIsZero - | PhotometricInterpretation::TransparencyMask => { - decoder.set_color_transform(jpeg::ColorTransform::None) - } - PhotometricInterpretation::CMYK => decoder.set_color_transform(jpeg::ColorTransform::CMYK), - PhotometricInterpretation::YCbCr => { - decoder.set_color_transform(jpeg::ColorTransform::YCbCr) - } - photometric_interpretation => { - return Err(TiffError::UnsupportedError( - TiffUnsupportedError::UnsupportedInterpretation(photometric_interpretation), - ) - .into()); + // mozjpeg (libjpeg-turbo) uses panic-based error handling internally. + // All mozjpeg calls must be wrapped in catch_unwind per the library's requirements. + // See: https://docs.rs/mozjpeg/latest/mozjpeg/ + let result = panic::catch_unwind(|| -> std::io::Result> { + // Use mozjpeg (libjpeg-turbo) for decoding to match libtiff/GDAL/rasterio output exactly. + // For YCbCr, decode to raw YCbCr (no internal color conversion) then apply the + // TIFF-correct color conversion using the ReferenceBlackWhite tag. + if photometric_interpretation == PhotometricInterpretation::YCbCr { + // Decode to upsampled interleaved YCbCr (no internal color conversion by libjpeg), + // then apply the TIFF-correct color conversion using the ReferenceBlackWhite tag. + let mut decompress = + Decompress::new_mem(&jpeg_data)?.to_colorspace(MozColorSpace::JCS_YCbCr)?; + let ycbcr = decompress.read_scanlines::()?; + return Ok(ycbcr_to_rgb(&ycbcr, reference_black_white)); } + + let moz_cs = match photometric_interpretation { + PhotometricInterpretation::RGB => MozColorSpace::JCS_RGB, + PhotometricInterpretation::WhiteIsZero + | PhotometricInterpretation::BlackIsZero + | PhotometricInterpretation::TransparencyMask => MozColorSpace::JCS_GRAYSCALE, + PhotometricInterpretation::CMYK => MozColorSpace::JCS_CMYK, + _ => { + // Unsupported colorspaces are handled outside catch_unwind via the outer match. + // Return an empty vec as sentinel; the outer code handles the error. + return Ok(vec![]); + } + }; + + let mut decompress = Decompress::new_mem(&jpeg_data)?.to_colorspace(moz_cs)?; + decompress.read_scanlines::() + }); + + // Handle unsupported photometric interpretation (avoids passing non-UnwindSafe types into closure) + if !matches!( + photometric_interpretation, + PhotometricInterpretation::RGB + | PhotometricInterpretation::YCbCr + | PhotometricInterpretation::WhiteIsZero + | PhotometricInterpretation::BlackIsZero + | PhotometricInterpretation::TransparencyMask + | PhotometricInterpretation::CMYK + ) { + return Err( + TiffError::UnsupportedError(TiffUnsupportedError::UnsupportedInterpretation( + photometric_interpretation, + )) + .into(), + ); + } + + result + .map_err(|_| AsyncTiffError::General("JPEG decode panicked".to_string()))? + .map_err(|e| AsyncTiffError::General(format!("JPEG decode error: {e}"))) +} + +/// Convert upsampled YCbCr pixels to RGB replicating libtiff's exact fixed-point arithmetic. +/// +/// Replicates `TIFFYCbCrToRGBInit` + `TIFFYCbCrtoRGB` from libtiff/tif_color.c, including +/// the `Code2V` ReferenceBlackWhite scaling and the 16-bit fixed-point lookup tables. +fn ycbcr_to_rgb(ycbcr: &[u8], reference_black_white: Option<&[f64; 6]>) -> Vec { + // Default TIFF ReferenceBlackWhite for YCbCr + let rbw = reference_black_white + .copied() + .unwrap_or([0.0, 255.0, 128.0, 255.0, 128.0, 255.0]); + let [rb0, rw0, rb2, rw2, rb4, rw4] = rbw; + + // libtiff uses SHIFT=16, ONE_HALF=1<<15, FIX(x) = (x*(1<<16)+0.5) as i32 + const SHIFT: u32 = 16; + const ONE_HALF: i64 = 1 << 15; + + // CCIR 601 luma coefficients (ITU-R BT.601) + const LUMA_RED: f64 = 0.299; + const LUMA_GREEN: f64 = 0.587; + const LUMA_BLUE: f64 = 0.114; + + let fix = |x: f64| -> i64 { (x * (1i64 << SHIFT) as f64 + 0.5) as i64 }; + + // libtiff D1..D4 fixed-point matrix coefficients + let d1 = fix(2.0 - 2.0 * LUMA_RED); // Cr -> R + let d2 = fix(-(LUMA_RED * (2.0 - 2.0 * LUMA_RED) / LUMA_GREEN)); // Cr -> G + let d3 = fix(2.0 - 2.0 * LUMA_BLUE); // Cb -> B + let d4 = fix(-(LUMA_BLUE * (2.0 - 2.0 * LUMA_BLUE) / LUMA_GREEN)); // Cb -> G + + // Code2V: maps raw code to scaled value using ReferenceBlackWhite + let code2v = |c: i32, rb: f64, rw: f64, cr: f64| -> i32 { + let denom = if (rw - rb).abs() > f64::EPSILON { + rw - rb + } else { + 1.0 + }; + // CLAMPw to [-128*32, 128*32] + ((c as f64 - rb) * cr / denom).clamp(-128.0 * 32.0, 128.0 * 32.0) as i32 + }; + + // Build lookup tables indexed by raw 0..=255 pixel values. + // libtiff loop: for (i=0, x=-128; i<256; i++, x++) + let mut y_tab = [0i32; 256]; + let mut cr_r_tab = [0i32; 256]; + let mut cb_b_tab = [0i32; 256]; + let mut cr_g_tab = [0i64; 256]; // stored unshifted + let mut cb_g_tab = [0i64; 256]; // stored unshifted + ONE_HALF + + for i in 0usize..256 { + let x = i as i32 - 128; + y_tab[i] = code2v(x + 128, rb0, rw0, 255.0); + + let cr = code2v(x, rb4 - 128.0, rw4 - 128.0, 127.0) as i64; + cr_r_tab[i] = ((d1 * cr + ONE_HALF) >> SHIFT) as i32; + cr_g_tab[i] = d2 * cr; + + let cb = code2v(x, rb2 - 128.0, rw2 - 128.0, 127.0) as i64; + cb_b_tab[i] = ((d3 * cb + ONE_HALF) >> SHIFT) as i32; + cb_g_tab[i] = d4 * cb + ONE_HALF; + } + + let n_pixels = ycbcr.len() / 3; + let mut rgb = vec![0u8; n_pixels * 3]; + + for i in 0..n_pixels { + let y = ycbcr[i * 3] as usize; + let cb = ycbcr[i * 3 + 1] as usize; + let cr = ycbcr[i * 3 + 2] as usize; + + let yv = y_tab[y]; + let r = yv + cr_r_tab[cr]; + let g = yv + ((cb_g_tab[cb] + cr_g_tab[cr]) >> SHIFT) as i32; + let b = yv + cb_b_tab[cb]; + + rgb[i * 3] = r.clamp(0, 255) as u8; + rgb[i * 3 + 1] = g.clamp(0, 255) as u8; + rgb[i * 3 + 2] = b.clamp(0, 255) as u8; } - let data = decoder.decode()?; - Ok(data) + rgb } diff --git a/src/error.rs b/src/error.rs index ac8ec0c..215df6c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,7 +4,6 @@ use std::error::Error; use std::fmt::Debug; use std::{fmt, io, str, string}; -use jpeg::UnsupportedFeature; use thiserror::Error; use crate::tag_value::TagValue; @@ -32,10 +31,6 @@ pub enum AsyncTiffError { #[error(transparent)] IOError(#[from] std::io::Error), - /// Error while decoding JPEG data. - #[error(transparent)] - JPEGDecodingError(#[from] jpeg::Error), - /// Error while decoding JPEG2000 data. #[cfg(feature = "jpeg2k")] #[error(transparent)] @@ -210,7 +205,7 @@ pub enum TiffUnsupportedError { UnsupportedPlanarConfig(Option), UnsupportedDataType, UnsupportedInterpretation(PhotometricInterpretation), - UnsupportedJpegFeature(UnsupportedFeature), + UnsupportedJpegFeature(String), MisalignedTileBoundaries, } diff --git a/src/ifd.rs b/src/ifd.rs index 4303248..9765d27 100644 --- a/src/ifd.rs +++ b/src/ifd.rs @@ -133,6 +133,18 @@ pub struct ImageFileDirectory { pub(crate) jpeg_tables: Option, + /// Specifies a pair of headroom and footroom image data values (codes) for each pixel + /// component. + /// + /// The first component code within a pair is associated with ReferenceBlack, and the second is + /// associated with ReferenceWhite. The ordering of pairs is the same as those for pixel + /// components of the PhotometricInterpretation type. ReferenceBlackWhite can be applied to + /// images with a PhotometricInterpretation value of RGB or YCbCr. ReferenceBlackWhite is not + /// used with other PhotometricInterpretation values. + /// + /// + pub(crate) reference_black_white: Option<[f64; 6]>, + pub(crate) copyright: Option, // Geospatial tags @@ -188,6 +200,7 @@ impl ImageFileDirectory { let mut extra_samples = None; let mut sample_format = None; let mut jpeg_tables = None; + let mut reference_black_white = None; let mut copyright = None; let mut geo_key_directory_data = None; let mut model_pixel_scale = None; @@ -268,6 +281,14 @@ impl ImageFileDirectory { ); } Tag::JPEGTables => jpeg_tables = Some(value.into_u8_vec()?.into()), + Tag::ReferenceBlackWhite => { + // alternating numerator/denominator for RATIONAL values + let vals = value.into_f64_vec()?; + if vals.len() != 6 { + return Err(TiffError::FormatError(TiffFormatError::InvalidTag)); + } + reference_black_white = Some(vals.try_into().expect("Convert Vec to [f64; 6]")); + } Tag::Copyright => copyright = Some(value.into_string()?), // Geospatial tags @@ -423,6 +444,7 @@ impl ImageFileDirectory { .unwrap_or(vec![SampleFormat::Uint; samples_per_pixel as _]), copyright, jpeg_tables, + reference_black_white, geo_key_directory, model_pixel_scale, model_tiepoint, @@ -642,6 +664,14 @@ impl ImageFileDirectory { self.jpeg_tables.as_deref() } + /// Headroom and footroom codes for each pixel component used for YCbCr and RGB images. + /// + /// Six values as `[Y_black, Y_white, Cb_black, Cb_white, Cr_black, Cr_white]`. + /// + pub fn reference_black_white(&self) -> Option<&[f64; 6]> { + self.reference_black_white.as_ref() + } + /// Copyright notice. /// pub fn copyright(&self) -> Option<&str> { @@ -924,6 +954,7 @@ impl CompressedBytes { photometric_interpretation: ifd.photometric_interpretation, jpeg_tables: ifd.jpeg_tables.clone(), lerc_parameters: ifd.lerc_parameters.clone(), + reference_black_white: ifd.reference_black_white, } } } diff --git a/src/tag_value.rs b/src/tag_value.rs index 4b4d4e9..5be073f 100644 --- a/src/tag_value.rs +++ b/src/tag_value.rs @@ -167,6 +167,10 @@ impl TagValue { pub fn into_f64(self) -> TiffResult { match self { Double(val) => Ok(val), + Rational(numerator, denominator) => Ok(numerator as f64 / denominator as f64), + RationalBig(numerator, denominator) => Ok(numerator as f64 / denominator as f64), + SRational(numerator, denominator) => Ok(numerator as f64 / denominator as f64), + SRationalBig(numerator, denominator) => Ok(numerator as f64 / denominator as f64), val => Err(TiffError::FormatError( TiffFormatError::SignedIntegerExpected(val), )), diff --git a/src/tags.rs b/src/tags.rs index 24f7f0d..083e7de 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -130,6 +130,11 @@ pub enum Tag(u16) unknown("A private or extension tag") { SMaxSampleValue = 341, // JPEG JPEGTables = 347, + // YCbCr + YCbCrCoefficients = 529, + YCbCrSubSampling = 530, + YCbCrPositioning = 531, + ReferenceBlackWhite = 532, // GeoTIFF ModelPixelScale = 33550, // (SoftDesk) ModelTransformation = 34264, // (JPL Carto Group) diff --git a/src/tile.rs b/src/tile.rs index 5b586b2..25b3971 100644 --- a/src/tile.rs +++ b/src/tile.rs @@ -36,6 +36,8 @@ pub struct Tile { /// LERC parameters from the LercParameters tag: [version, compression_type, ...] /// compression_type: 0 = none, 1 = deflate, 2 = zstd pub(crate) lerc_parameters: Option>, + /// ReferenceBlackWhite tag values: [Y_black, Y_white, Cb_black, Cb_white, Cr_black, Cr_white] + pub(crate) reference_black_white: Option<[f64; 6]>, } impl Tile { @@ -98,6 +100,7 @@ impl Tile { self.samples_per_pixel, bits_per_sample, self.lerc_parameters.as_deref(), + self.reference_black_white.as_ref(), )?, CompressedBytes::Planar(band_bytes) => { let bytes_per_sample = (bits_per_sample as usize).div_ceil(8); @@ -113,6 +116,7 @@ impl Tile { 1, bits_per_sample, self.lerc_parameters.as_deref(), + self.reference_black_white.as_ref(), )?; result.extend_from_slice(&decoded_band); }