From 85215e81e3cba3728da4fad6c75116ca748e395d Mon Sep 17 00:00:00 2001 From: Lilith River Date: Wed, 11 Mar 2026 00:39:37 -0600 Subject: [PATCH] Revert "feat: add YCbCr chroma subsampling support for non-JPEG compression" This reverts commit 83409cbdf3c1543be1da53df4e4cdb942203a4f7. --- src/decoder/image.rs | 29 +++++-------- src/decoder/stream.rs | 84 -------------------------------------- tests/decode_libtiffpic.rs | 8 ++-- 3 files changed, 14 insertions(+), 107 deletions(-) diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 0c60aa2..4593ccb 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -952,10 +952,15 @@ impl Image { // Only this color type interprets the tag, which is defined with a default of (2, 2) if matches!(color, ColorType::YCbCr(_)) && self.chroma_subsampling != (1, 1) { - // The JPEG library does upsampling for us. For other compression methods, - // the expand_chunk pipeline wraps the decompressor in a YCbCrUpsamplingReader - // that converts the subsampled block layout to full-resolution pixel rows. - if matches!(self.planar_config, PlanarConfiguration::Planar) { + // The JPEG library does upsampling for us and defines its buffers correctly + // (presumably). All other compression schemes are not supported.. + // + // NOTE: as explained in we may be better + // off supporting this tag by consistently upsampling, not by adjusting the buffer + // size. At least as a default this makes more sense and is much more permissive in + // case the compression stream disagrees with the tags (we would not have enough / or + // the wrong buffer layout if we only asked for subsampled planes in a planar layout). + if !matches!(self.compression_method, CompressionMethod::ModernJPEG) { return Err(TiffError::UnsupportedError( TiffUnsupportedError::ChromaSubsampling, )); @@ -1155,7 +1160,7 @@ impl Image { let is_all_bits = samples == data_samples; let is_output_chunk_rows = layout.row_stride == chunk_row_bytes; - let mut reader: Box = Self::create_reader( + let mut reader = Self::create_reader( reader.inner(), compression_method, *compressed_bytes, @@ -1165,20 +1170,6 @@ impl Image { self.fill_order, )?; - // For non-JPEG YCbCr with chroma subsampling, wrap the decompressor in an - // upsampling reader that converts the subsampled block layout to full-resolution - // pixel rows. JPEG handles upsampling internally. - if matches!(color_type, ColorType::YCbCr(_)) - && self.chroma_subsampling != (1, 1) - && !matches!(compression_method, CompressionMethod::ModernJPEG) - { - reader = Box::new(super::stream::YCbCrUpsamplingReader::new( - reader, - data_dims, - self.chroma_subsampling, - )); - } - // Extended bit depths (9-15, 17-31): packed N-bit samples must be unpacked to // standard-width output samples before endianness fix and predictor. let needs_unpacking = layout.tiff_bits_per_sample != layout.output_bits_per_sample diff --git a/src/decoder/stream.rs b/src/decoder/stream.rs index 205e365..9fb5e4e 100644 --- a/src/decoder/stream.rs +++ b/src/decoder/stream.rs @@ -507,90 +507,6 @@ impl Read for Group3Reader { } } -/// Converts YCbCr data from chroma-subsampled block layout to full-resolution -/// 3-bytes-per-pixel YCbCr output. Each block of h×v pixels stores h*v Y samples -/// plus one shared Cb and one shared Cr sample. This reader expands those into -/// per-pixel (Y, Cb, Cr) triples. -pub struct YCbCrUpsamplingReader { - reader: R, - width: u32, - horiz_sub: u32, - vert_sub: u32, - height: u32, - current_row: u32, - output_buf: io::Cursor>, -} - -impl YCbCrUpsamplingReader { - pub fn new(reader: R, dimensions: (u32, u32), chroma: (u16, u16)) -> Self { - Self { - reader, - width: dimensions.0, - horiz_sub: u32::from(chroma.0), - vert_sub: u32::from(chroma.1), - height: dimensions.1, - current_row: 0, - output_buf: io::Cursor::new(Vec::new()), - } - } -} - -impl Read for YCbCrUpsamplingReader { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - // If output buffer is exhausted, decode next block row - if self.output_buf.position() as usize >= self.output_buf.get_ref().len() { - if self.current_row >= self.height { - return Ok(0); - } - - let h = self.horiz_sub; - let v = self.vert_sub; - let blocks_across = self.width.div_ceil(h); - let block_size = (h * v + 2) as usize; - let block_row_bytes = blocks_across as usize * block_size; - - let rows_in_block = v.min(self.height - self.current_row) as usize; - - let mut subsampled = vec![0u8; block_row_bytes]; - self.reader.read_exact(&mut subsampled)?; - - let output_row_bytes = self.width as usize * 3; - let output = self.output_buf.get_mut(); - output.clear(); - output.resize(rows_in_block * output_row_bytes, 0); - - let mut src_offset = 0; - for bx in 0..blocks_across { - let x0 = (bx * h) as usize; - - let y_samples = &subsampled[src_offset..src_offset + (h * v) as usize]; - let cb = subsampled[src_offset + (h * v) as usize]; - let cr = subsampled[src_offset + (h * v) as usize + 1]; - src_offset += block_size; - - for dy in 0..rows_in_block { - for dx in 0..h as usize { - let x = x0 + dx; - if x >= self.width as usize { - break; - } - let y_val = y_samples[dy * h as usize + dx]; - let pixel_offset = dy * output_row_bytes + x * 3; - output[pixel_offset] = y_val; - output[pixel_offset + 1] = cb; - output[pixel_offset + 2] = cr; - } - } - } - - self.current_row += v; - self.output_buf.set_position(0); - } - - self.output_buf.read(buf) - } -} - #[cfg(feature = "webp")] pub struct WebPReader { inner: Cursor>, diff --git a/tests/decode_libtiffpic.rs b/tests/decode_libtiffpic.rs index 1d19801..2479fd7 100644 --- a/tests/decode_libtiffpic.rs +++ b/tests/decode_libtiffpic.rs @@ -173,7 +173,7 @@ const FILES: &[(&str, Option)] = &[ ("tests/libtiffpic/depth/genimages", None), ("tests/libtiffpic/depth/README.txt", None), ("tests/libtiffpic/depth/summary.txt", None), - ("tests/libtiffpic/dscf0013.tif", Some(0x6896e83a)), + ("tests/libtiffpic/dscf0013.tif", Some(0)), // unsupported YCbCr chroma subsampling ("tests/libtiffpic/fax2d.tif", Some(0xc2f30c6f)), // No support for direct CCITT Group 3 1D format ("tests/libtiffpic/g3test.g3", None), @@ -193,11 +193,11 @@ const FILES: &[(&str, Option)] = &[ ("tests/libtiffpic/quad-lzw.tif", Some(0)), // invalid LZW stream ("tests/libtiffpic/quad-tile.tif", Some(0x6055f9ee)), ("tests/libtiffpic/README", None), - ("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported Old JPEG compression (type 6) + ("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported YCbCr chroma subsampling ("tests/libtiffpic/strike.tif", Some(0x016b0f6a)), ("tests/libtiffpic/text.tif", Some(0)), // unsupported compression - ("tests/libtiffpic/ycbcr-cat.tif", Some(0x388a9d8f)), - ("tests/libtiffpic/zackthecat.tif", Some(0)), // unsupported Old JPEG compression (type 6) + ("tests/libtiffpic/ycbcr-cat.tif", Some(0)), // unsupported YCbCr chroma subsampling + ("tests/libtiffpic/zackthecat.tif", Some(0)), // unsupported YCbCr chroma subsampling ]; fn main() {