diff --git a/src/decoder/image.rs b/src/decoder/image.rs index eef5d0a..a1bda19 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,23 @@ 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 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, + )); + } + } + Ok(ReadoutLayout { planar_config: self.planar_config, color, diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 53db108..79b693d 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/encoder/mod.rs b/src/encoder/mod.rs index 6c1a965..45024e3 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -11,7 +11,10 @@ use std::{ use crate::{ decoder::ifd::Entry, error::{TiffResult, UsageError}, - tags::{CompressionMethod, ExtraSamples, IfdPointer, ResolutionUnit, SampleFormat, Tag, Type}, + tags::{ + CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation, ResolutionUnit, + SampleFormat, Tag, Type, + }, Directory, TiffError, TiffFormatError, }; @@ -565,6 +568,12 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, encoder.write_tag(Tag::PhotometricInterpretation, ::TIFF_VALUE.to_u16())?; + if matches!(::TIFF_VALUE, PhotometricInterpretation::YCbCr) { + // The default for this tag is 2,2 for subsampling but we do not support such a + // transformation. Instead all samples must be provided. + encoder.write_tag(Tag::ChromaSubsampling, &[1u16, 1u16][..])?; + } + encoder.write_tag(Tag::RowsPerStrip, u32::try_from(rows_per_strip)?)?; encoder.write_tag( 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 07787d1..14cc383 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -128,6 +128,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)