Recognize YCbCr/Chomra subsampling tags

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.
This commit is contained in:
A. Molzer committed 2026-01-05 21:14:40 +01:00
1 parent d38fba4268
commit 23d1867f75
4 files changed
+40

No files matched your search

+28
View File
@@ -85,6 +85,7 @@ pub(crate) struct Image {
pub tile_attributes: Option<TileAttributes>,
pub chunk_offsets: Vec<u64>,
pub chunk_bytes: Vec<u64>,
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::<u16>(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,
+1
View File
@@ -860,6 +860,7 @@ impl<R: Read + Seek> Decoder<R> {
tile_attributes: None,
chunk_offsets: Vec::new(),
chunk_bytes: Vec::new(),
chroma_subsampling: (2, 2),
},
};
decoder.next_image()?;
+6
View File
@@ -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")
}
+5
View File
@@ -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)