Skip to content

Commit a3ab252

Browse files
authored
Merge pull request #319 from image-rs/ycbcr-subsampling
Recognize Ycbcr/Chroma subsampling tags
2 parents 234b2ba + bd64e43 commit a3ab252

File tree

5 files changed

+60
-1
lines changed

5 files changed

+60
-1
lines changed

src/decoder/image.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ pub(crate) struct Image {
8585
pub tile_attributes: Option<TileAttributes>,
8686
pub chunk_offsets: Vec<u64>,
8787
pub chunk_bytes: Vec<u64>,
88+
pub chroma_subsampling: (u16, u16),
8889
}
8990

9091
/// Describes how to read a tile-aligned portion of the image.
@@ -275,6 +276,25 @@ impl Image {
275276
.transpose()?
276277
.unwrap_or(PlanarConfiguration::Chunky);
277278

279+
let ycbcr_subsampling = tag_reader.find_tag_uint_vec::<u16>(Tag::ChromaSubsampling)?;
280+
281+
let chroma_subsampling = if let Some(subsamples) = &ycbcr_subsampling {
282+
let [a, b] = subsamples.as_slice() else {
283+
return Err(TiffError::FormatError(TiffFormatError::InvalidCountForTag(
284+
Tag::ChromaSubsampling,
285+
subsamples.len(),
286+
)));
287+
};
288+
289+
// ImageWidth and ImageLength are constrained to be integer multiples of
290+
// YCbCrSubsampleHoriz and YCbCrSubsampleVert respectively. TileWidth and TileLength
291+
// have the same constraints. RowsPerStrip must be an integer multiple of
292+
// YCbCrSubsampleVert.
293+
(*a, *b)
294+
} else {
295+
(2, 2)
296+
};
297+
278298
let planes = match planar_config {
279299
PlanarConfiguration::Chunky => 1,
280300
PlanarConfiguration::Planar => samples,
@@ -386,6 +406,7 @@ impl Image {
386406
tile_attributes,
387407
chunk_offsets,
388408
chunk_bytes,
409+
chroma_subsampling,
389410
})
390411
}
391412

@@ -715,6 +736,23 @@ impl Image {
715736
));
716737
}
717738

739+
// Only this color type interprets the tag, which is defined with a default of (2, 2)
740+
if matches!(color, ColorType::YCbCr(_)) && self.chroma_subsampling != (1, 1) {
741+
// The JPEG library does upsampling for us and defines its buffers correctly
742+
// (presumably). All other compression schemes are not supported..
743+
//
744+
// NOTE: as explained in <fa225e820b96bef35f01bf4685654beeb4a8df0c> we may be better
745+
// off supporting this tag by consistently upsampling, not by adjusting the buffer
746+
// size. At least as a default this makes more sense and is much more permissive in
747+
// case the compression stream disagrees with the tags (we would not have enough / or
748+
// the wrong buffer layout if we only asked for subsampled planes in a planar layout).
749+
if !matches!(self.compression_method, CompressionMethod::ModernJPEG) {
750+
return Err(TiffError::UnsupportedError(
751+
TiffUnsupportedError::ChromaSubsampling,
752+
));
753+
}
754+
}
755+
718756
Ok(ReadoutLayout {
719757
planar_config: self.planar_config,
720758
color,

src/decoder/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,7 @@ impl<R: Read + Seek> Decoder<R> {
860860
tile_attributes: None,
861861
chunk_offsets: Vec::new(),
862862
chunk_bytes: Vec::new(),
863+
chroma_subsampling: (2, 2),
863864
},
864865
};
865866
decoder.next_image()?;

src/encoder/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ use std::{
1111
use crate::{
1212
decoder::ifd::Entry,
1313
error::{TiffResult, UsageError},
14-
tags::{CompressionMethod, ExtraSamples, IfdPointer, ResolutionUnit, SampleFormat, Tag, Type},
14+
tags::{
15+
CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation, ResolutionUnit,
16+
SampleFormat, Tag, Type,
17+
},
1518
Directory, TiffError, TiffFormatError,
1619
};
1720

@@ -565,6 +568,12 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
565568

566569
encoder.write_tag(Tag::PhotometricInterpretation, <T>::TIFF_VALUE.to_u16())?;
567570

571+
if matches!(<T>::TIFF_VALUE, PhotometricInterpretation::YCbCr) {
572+
// The default for this tag is 2,2 for subsampling but we do not support such a
573+
// transformation. Instead all samples must be provided.
574+
encoder.write_tag(Tag::ChromaSubsampling, &[1u16, 1u16][..])?;
575+
}
576+
568577
encoder.write_tag(Tag::RowsPerStrip, u32::try_from(rows_per_strip)?)?;
569578

570579
encoder.write_tag(

src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ quick_error! {
9090
InvalidTypeForTag {
9191
display("tag has invalid type")
9292
}
93+
InvalidCountForTag(tag: Tag, len: usize) {
94+
display("tag `{tag:?}` with incorrect number of elements ({len}) encountered")
95+
}
9396
StripTileTagConflict {
9497
display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found")
9598
}
@@ -156,6 +159,9 @@ quick_error! {
156159
UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
157160
display("unsupported photometric interpretation \"{interpretation:?}\"")
158161
}
162+
ChromaSubsampling {
163+
display("chroma subsampling of YCbCr color is unsupported")
164+
}
159165
MisalignedTileBoundaries {
160166
display("tile rows are not aligned to byte boundaries")
161167
}

src/tags.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ pub enum Tag(u16) unknown(
128128
SMaxSampleValue = 341, // TODO add support
129129
// JPEG
130130
JPEGTables = 347,
131+
// Subsampling
132+
#[doc(alias = "YCbCrSubsampling")]
133+
ChromaSubsampling = 530, // TODO add support
134+
#[doc(alias = "YCbCrPositioning")]
135+
ChromaPositioning = 531, // TODO add support
131136
// GeoTIFF
132137
ModelPixelScaleTag = 33550, // (SoftDesk)
133138
ModelTransformationTag = 34264, // (JPL Carto Group)

0 commit comments

Comments
 (0)