diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 89b0209..89a0dc8 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -3,7 +3,9 @@ use super::stream::{ByteOrder, DeflateReader, JpegReader, LZWReader, PackBitsRea use super::tag_reader::TagReader; use super::{fp_predict_f32, fp_predict_f64, DecodingBuffer, Limits}; use super::{stream::SmartReader, ChunkType}; -use crate::tags::{CompressionMethod, PhotometricInterpretation, Predictor, SampleFormat, Tag}; +use crate::tags::{ + CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag, +}; use crate::{ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError}; use std::convert::{TryFrom, TryInto}; use std::io::{self, Cursor, Read, Seek}; @@ -71,6 +73,7 @@ pub(crate) struct Image { pub predictor: Predictor, pub jpeg_tables: Option>>, pub chunk_type: ChunkType, + pub planar_config: PlanarConfiguration, pub strip_decoder: Option, pub tile_attributes: Option, pub chunk_offsets: Vec, @@ -173,6 +176,23 @@ impl Image { .transpose()? .unwrap_or(Predictor::None); + let planar_config = tag_reader + .find_tag(Tag::PlanarConfiguration)? + .map(Value::into_u16) + .transpose()? + .map(|p| { + PlanarConfiguration::from_u16(p).ok_or(TiffError::FormatError( + TiffFormatError::UnknownPlanarConfiguration(p), + )) + }) + .transpose()? + .unwrap_or(PlanarConfiguration::Chunky); + + let planes = match planar_config { + PlanarConfiguration::Chunky => 1, + PlanarConfiguration::Planar => samples, + }; + let chunk_type; let chunk_offsets; let chunk_bytes; @@ -206,7 +226,7 @@ impl Image { if chunk_offsets.len() != chunk_bytes.len() || rows_per_strip == 0 || u32::try_from(chunk_offsets.len())? - != height.saturating_sub(1) / rows_per_strip + 1 + != (height.saturating_sub(1) / rows_per_strip + 1) * planes as u32 { return Err(TiffError::FormatError( TiffFormatError::InconsistentSizesEncountered, @@ -245,7 +265,8 @@ impl Image { let tile = tile_attributes.as_ref().unwrap(); if chunk_offsets.len() != chunk_bytes.len() - || chunk_offsets.len() != tile.tiles_down() * tile.tiles_across() + || chunk_offsets.len() + != tile.tiles_down() * tile.tiles_across() * planes as usize { return Err(TiffError::FormatError( TiffFormatError::InconsistentSizesEncountered, @@ -271,6 +292,7 @@ impl Image { jpeg_tables, predictor, chunk_type, + planar_config, strip_decoder, tile_attributes, chunk_offsets, @@ -395,6 +417,28 @@ impl Image { }) } + /// Samples per pixel within chunk. + /// + /// In planar config, samples are stored in separate strips/chunks, also called bands. + /// + /// Example with `bits_per_sample = [8, 8, 8]` and `PhotometricInterpretation::RGB`: + /// * `PlanarConfiguration::Chunky` -> 3 (RGBRGBRGB...) + /// * `PlanarConfiguration::Planar` -> 1 (RRR...) (GGG...) (BBB...) + pub(crate) fn samples_per_pixel(&self) -> usize { + match self.planar_config { + PlanarConfiguration::Chunky => self.bits_per_sample.len(), + PlanarConfiguration::Planar => 1, + } + } + + /// Number of strips per pixel. + pub(crate) fn strips_per_pixel(&self) -> usize { + match self.planar_config { + PlanarConfiguration::Chunky => 1, + PlanarConfiguration::Planar => self.bits_per_sample.len(), + } + } + pub(crate) fn chunk_file_range(&self, chunk: u32) -> TiffResult<(u64, u64)> { let file_offset = self .chunk_offsets @@ -434,7 +478,10 @@ impl Image { match self.chunk_type { ChunkType::Strip => { - let strip_height_without_padding = chunk_index + let strip_attrs = self.strip_decoder.as_ref().unwrap(); + let strips_per_band = + self.height.saturating_sub(1) / strip_attrs.rows_per_strip + 1; + let strip_height_without_padding = (chunk_index % strips_per_band) .checked_mul(dims.1) .and_then(|x| self.height.checked_sub(x)) .ok_or(TiffError::UsageError(UsageError::InvalidChunkIndex( @@ -528,7 +575,7 @@ impl Image { let compression_method = self.compression_method; let photometric_interpretation = self.photometric_interpretation; let predictor = self.predictor; - let samples = self.bits_per_sample.len(); + let samples = self.samples_per_pixel(); let chunk_dims = self.chunk_dimensions()?; let data_dims = self.chunk_data_dimensions(chunk_index)?; diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 0612db9..8a980e0 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -10,7 +10,8 @@ use crate::{ use self::ifd::Directory; use self::image::Image; use crate::tags::{ - CompressionMethod, PhotometricInterpretation, Predictor, SampleFormat, Tag, Type, + CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, + Tag, Type, }; use self::stream::{ByteOrder, EndianReader, SmartReader}; @@ -579,6 +580,7 @@ impl Decoder { jpeg_tables: None, predictor: Predictor::None, chunk_type: ChunkType::Strip, + planar_config: PlanarConfiguration::Chunky, strip_decoder: None, tile_attributes: None, chunk_offsets: Vec::new(), @@ -1016,7 +1018,12 @@ impl Decoder { None => return Err(TiffError::IntSizeError), }; - Ok(height / rows_per_strip) + let strips = match self.image().planar_config { + PlanarConfiguration::Chunky => height / rows_per_strip, + PlanarConfiguration::Planar => height / rows_per_strip * self.image().samples as u32, + }; + + Ok(strips) } /// Number of tiles in image @@ -1051,7 +1058,7 @@ impl Decoder { fn result_buffer(&self, width: usize, height: usize) -> TiffResult { let buffer_size = match width .checked_mul(height) - .and_then(|x| x.checked_mul(self.image().bits_per_sample.len())) + .and_then(|x| x.checked_mul(self.image().samples_per_pixel())) { Some(s) => s, None => return Err(TiffError::LimitsExceeded), @@ -1146,7 +1153,7 @@ impl Decoder { )); } - let samples = self.image().bits_per_sample.len(); + let samples = self.image().samples_per_pixel(); if samples == 0 { return Err(TiffError::FormatError( TiffFormatError::InconsistentSizesEncountered, @@ -1156,7 +1163,12 @@ impl Decoder { let chunks_across = ((width - 1) / chunk_dimensions.0 + 1) as usize; let strip_samples = width as usize * chunk_dimensions.1 as usize * samples; - for chunk in 0..self.image().chunk_offsets.len() { + let image_chunks = self.image().chunk_offsets.len() / self.image().strips_per_pixel(); + // For multi-band images, only the first band is read. + // Possible improvements: + // * pass requested band as parameter + // * collect bands to a RGB encoding result in case of RGB bands + for chunk in 0..image_chunks { self.goto_offset_u64(self.image().chunk_offsets[chunk])?; let x = chunk % chunks_across; diff --git a/src/error.rs b/src/error.rs index a401c6f..fe00104 100644 --- a/src/error.rs +++ b/src/error.rs @@ -66,6 +66,7 @@ pub enum TiffFormatError { InvalidTagValueType(Tag), RequiredTagNotFound(Tag), UnknownPredictor(u16), + UnknownPlanarConfiguration(u16), ByteExpected(Value), UnsignedIntegerExpected(Value), SignedIntegerExpected(Value), @@ -113,6 +114,9 @@ impl fmt::Display for TiffFormatError { UnknownPredictor(ref predictor) => { write!(fmt, "Unknown predictor “{}” encountered", predictor) } + UnknownPlanarConfiguration(ref planar_config) => { + write!(fmt, "Unknown planar configuration “{}” encountered", planar_config) + } ByteExpected(ref val) => write!(fmt, "Expected byte, {:?} found.", val), UnsignedIntegerExpected(ref val) => { write!(fmt, "Expected unsigned integer, {:?} found.", val) diff --git a/tests/decode_images.rs b/tests/decode_images.rs index 9d5b809..f82c6ec 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -285,6 +285,48 @@ fn test_tiled_incremental() { } } +#[test] +fn test_planar_rgb_u8() { + // gdal_translate tiled-rgb-u8.tif planar-rgb-u8.tif -co INTERLEAVE=BAND -co COMPRESS=LZW -co PROFILE=BASELINE + let file = "planar-rgb-u8.tif"; + let expected_type = ColorType::RGB(8); + + let path = PathBuf::from(TEST_IMAGE_DIR).join(file); + let img_file = File::open(path).expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + assert_eq!(decoder.colortype().unwrap(), expected_type); + + let chunks = decoder.strip_count().unwrap(); + assert_eq!(chunks as usize, 72); + + // convert -quiet planar-rgb-u8.tif[0] -crop 1x1+0+0 txt: + // 0,0: (73,51,30) #49331E srgb(73,51,30) + + // 1st band (red) + match decoder.read_chunk(0).unwrap() { + DecodingResult::U8(chunk) => { + assert_eq!(chunk[0], 73); + } + _ => panic!("Wrong bit depth"), + } + // 2nd band (green) + match decoder.read_chunk(chunks / 3).unwrap() { + DecodingResult::U8(chunk) => { + assert_eq!(chunk[0], 51); + } + _ => panic!("Wrong bit depth"), + } + // 3rd band (blue) + match decoder.read_chunk(chunks / 3 * 2).unwrap() { + DecodingResult::U8(chunk) => { + assert_eq!(chunk[0], 30); + } + _ => panic!("Wrong bit depth"), + } + + test_image_sum_u8(file, ColorType::RGB(8), 15417630); +} + #[test] fn test_div_zero() { use tiff::{TiffError, TiffFormatError}; diff --git a/tests/images/planar-rgb-u8.tif b/tests/images/planar-rgb-u8.tif new file mode 100644 index 0000000..8de45d6 Binary files /dev/null and b/tests/images/planar-rgb-u8.tif differ