feat: add YCbCr chroma subsampling support for non-JPEG compression
Add YCbCrUpsamplingReader that converts chroma-subsampled YCbCr block data to full-resolution 3-bytes-per-pixel output. Each block of h×v pixels stores h*v Y samples plus one shared Cb and Cr sample; the reader expands these into per-pixel (Y, Cb, Cr) triples. This enables decoding YCbCr TIFFs with any supported compression method (uncompressed, LZW, Deflate, etc.), not just JPEG. Two more libtiffpic test images now decode with verified hashes: - dscf0013.tif (uncompressed YCbCr 2,1 subsampling) - ycbcr-cat.tif (LZW-compressed YCbCr 2,2 subsampling) The remaining two YCbCr test images (smallliz.tif, zackthecat.tif) use Old JPEG compression (type 6) which is a separate unsupported feature.
This commit is contained in:
1 parent
26301fc3f1
commit
83409cbdf3
3 files changed
+107
-14
No files matched your search
+19
-10
@@ -948,15 +948,10 @@ impl Image {
|
|||||||
|
|
||||||
// Only this color type interprets the tag, which is defined with a default of (2, 2)
|
// 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) {
|
if matches!(color, ColorType::YCbCr(_)) && self.chroma_subsampling != (1, 1) {
|
||||||
// The JPEG library does upsampling for us and defines its buffers correctly
|
// The JPEG library does upsampling for us. For other compression methods,
|
||||||
// (presumably). All other compression schemes are not supported..
|
// the expand_chunk pipeline wraps the decompressor in a YCbCrUpsamplingReader
|
||||||
//
|
// that converts the subsampled block layout to full-resolution pixel rows.
|
||||||
// NOTE: as explained in <fa225e820b96bef35f01bf4685654beeb4a8df0c> we may be better
|
if matches!(self.planar_config, PlanarConfiguration::Planar) {
|
||||||
// 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(
|
return Err(TiffError::UnsupportedError(
|
||||||
TiffUnsupportedError::ChromaSubsampling,
|
TiffUnsupportedError::ChromaSubsampling,
|
||||||
));
|
));
|
||||||
@@ -1156,7 +1151,7 @@ impl Image {
|
|||||||
let is_all_bits = samples == data_samples;
|
let is_all_bits = samples == data_samples;
|
||||||
let is_output_chunk_rows = layout.row_stride == chunk_row_bytes;
|
let is_output_chunk_rows = layout.row_stride == chunk_row_bytes;
|
||||||
|
|
||||||
let mut reader = Self::create_reader(
|
let mut reader: Box<dyn Read> = Self::create_reader(
|
||||||
reader.inner(),
|
reader.inner(),
|
||||||
compression_method,
|
compression_method,
|
||||||
*compressed_bytes,
|
*compressed_bytes,
|
||||||
@@ -1166,6 +1161,20 @@ impl Image {
|
|||||||
self.fill_order,
|
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
|
// Extended bit depths (9-15, 17-31): packed N-bit samples must be unpacked to
|
||||||
// standard-width output samples before endianness fix and predictor.
|
// standard-width output samples before endianness fix and predictor.
|
||||||
let needs_unpacking = layout.tiff_bits_per_sample != layout.output_bits_per_sample
|
let needs_unpacking = layout.tiff_bits_per_sample != layout.output_bits_per_sample
|
||||||
|
|||||||
@@ -507,6 +507,90 @@ 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<R: Read> {
|
||||||
|
reader: R,
|
||||||
|
width: u32,
|
||||||
|
horiz_sub: u32,
|
||||||
|
vert_sub: u32,
|
||||||
|
height: u32,
|
||||||
|
current_row: u32,
|
||||||
|
output_buf: io::Cursor<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read> YCbCrUpsamplingReader<R> {
|
||||||
|
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<R: Read> Read for YCbCrUpsamplingReader<R> {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
// 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")]
|
#[cfg(feature = "webp")]
|
||||||
pub struct WebPReader {
|
pub struct WebPReader {
|
||||||
inner: Cursor<Vec<u8>>,
|
inner: Cursor<Vec<u8>>,
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ const FILES: &[(&str, Option<u32>)] = &[
|
|||||||
("tests/libtiffpic/depth/genimages", None),
|
("tests/libtiffpic/depth/genimages", None),
|
||||||
("tests/libtiffpic/depth/README.txt", None),
|
("tests/libtiffpic/depth/README.txt", None),
|
||||||
("tests/libtiffpic/depth/summary.txt", None),
|
("tests/libtiffpic/depth/summary.txt", None),
|
||||||
("tests/libtiffpic/dscf0013.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
("tests/libtiffpic/dscf0013.tif", Some(0x6896e83a)),
|
||||||
("tests/libtiffpic/fax2d.tif", Some(0xc2f30c6f)),
|
("tests/libtiffpic/fax2d.tif", Some(0xc2f30c6f)),
|
||||||
// No support for direct CCITT Group 3 1D format
|
// No support for direct CCITT Group 3 1D format
|
||||||
("tests/libtiffpic/g3test.g3", None),
|
("tests/libtiffpic/g3test.g3", None),
|
||||||
@@ -193,11 +193,11 @@ const FILES: &[(&str, Option<u32>)] = &[
|
|||||||
("tests/libtiffpic/quad-lzw.tif", Some(0)), // invalid LZW stream
|
("tests/libtiffpic/quad-lzw.tif", Some(0)), // invalid LZW stream
|
||||||
("tests/libtiffpic/quad-tile.tif", Some(0x6055f9ee)),
|
("tests/libtiffpic/quad-tile.tif", Some(0x6055f9ee)),
|
||||||
("tests/libtiffpic/README", None),
|
("tests/libtiffpic/README", None),
|
||||||
("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported Old JPEG compression (type 6)
|
||||||
("tests/libtiffpic/strike.tif", Some(0x016b0f6a)),
|
("tests/libtiffpic/strike.tif", Some(0x016b0f6a)),
|
||||||
("tests/libtiffpic/text.tif", Some(0)), // unsupported compression
|
("tests/libtiffpic/text.tif", Some(0)), // unsupported compression
|
||||||
("tests/libtiffpic/ycbcr-cat.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
("tests/libtiffpic/ycbcr-cat.tif", Some(0x388a9d8f)),
|
||||||
("tests/libtiffpic/zackthecat.tif", Some(0)), // unsupported YCbCr chroma subsampling
|
("tests/libtiffpic/zackthecat.tif", Some(0)), // unsupported Old JPEG compression (type 6)
|
||||||
];
|
];
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
Reference in new issue
Block a user