From 23d1867f75c4172176437a46ce9ee8b075e09c00 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:38:48 +0100 Subject: [PATCH] Recognize YCbCr/Chomra subsampling tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In particular we avoid mistaking the absence of this tag for an absence of subsampling. The specification instead requires it to mean half the samples in both directions—a mode that we do not support yet. Instead of returning erroneous samples or an error in the data stream (which would appear too short) we return an error when asked to layout the read buffer for these images. --- src/decoder/image.rs | 28 ++++++++++++++++++++++++++++ src/decoder/mod.rs | 1 + src/error.rs | 6 ++++++ src/tags.rs | 5 +++++ 4 files changed, 40 insertions(+) diff --git a/src/decoder/image.rs b/src/decoder/image.rs index eef5d0a..70573d8 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -85,6 +85,7 @@ pub(crate) struct Image { pub tile_attributes: Option, pub chunk_offsets: Vec, pub chunk_bytes: Vec, + pub chroma_subsampling: (u16, u16), } /// Describes how to read a tile-aligned portion of the image. @@ -275,6 +276,25 @@ impl Image { .transpose()? .unwrap_or(PlanarConfiguration::Chunky); + let ycbcr_subsampling = tag_reader.find_tag_uint_vec::(Tag::ChromaSubsampling)?; + + let chroma_subsampling = if let Some(subsamples) = &ycbcr_subsampling { + let [a, b] = subsamples.as_slice() else { + return Err(TiffError::FormatError(TiffFormatError::InvalidCountForTag( + Tag::ChromaSubsampling, + subsamples.len(), + ))); + }; + + // ImageWidth and ImageLength are constrained to be integer multiples of + // YCbCrSubsampleHoriz and YCbCrSubsampleVert respectively. TileWidth and TileLength + // have the same constraints. RowsPerStrip must be an integer multiple of + // YCbCrSubsampleVert. + (*a, *b) + } else { + (2, 2) + }; + let planes = match planar_config { PlanarConfiguration::Chunky => 1, PlanarConfiguration::Planar => samples, @@ -386,6 +406,7 @@ impl Image { tile_attributes, chunk_offsets, chunk_bytes, + chroma_subsampling, }) } @@ -715,6 +736,13 @@ 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) { + return Err(TiffError::UnsupportedError( + TiffUnsupportedError::ChromaSubsampling, + )); + } + Ok(ReadoutLayout { planar_config: self.planar_config, color, diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index cc31572..40523b4 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -860,6 +860,7 @@ impl Decoder { tile_attributes: None, chunk_offsets: Vec::new(), chunk_bytes: Vec::new(), + chroma_subsampling: (2, 2), }, }; decoder.next_image()?; diff --git a/src/error.rs b/src/error.rs index e0c06cb..eb4fc86 100644 --- a/src/error.rs +++ b/src/error.rs @@ -90,6 +90,9 @@ quick_error! { InvalidTypeForTag { display("tag has invalid type") } + InvalidCountForTag(tag: Tag, len: usize) { + display("tag `{tag:?}` with incorrect number of elements ({len}) encountered") + } StripTileTagConflict { display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found") } @@ -156,6 +159,9 @@ quick_error! { UnsupportedInterpretation(interpretation: PhotometricInterpretation) { display("unsupported photometric interpretation \"{interpretation:?}\"") } + ChromaSubsampling { + display("chroma subsampling of YCbCr color is unsupported") + } MisalignedTileBoundaries { display("tile rows are not aligned to byte boundaries") } diff --git a/src/tags.rs b/src/tags.rs index f456f65..91f18f4 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -122,6 +122,11 @@ pub enum Tag(u16) unknown( SMaxSampleValue = 341, // TODO add support // JPEG JPEGTables = 347, + // Subsampling + #[doc(alias = "YCbCrSubsampling")] + ChromaSubsampling = 530, // TODO add support + #[doc(alias = "YCbCrPositioning")] + ChromaPositioning = 531, // TODO add support // GeoTIFF ModelPixelScaleTag = 33550, // (SoftDesk) ModelTransformationTag = 34264, // (JPL Carto Group)