Merge pull request #199 from pka/planar-config

Basic support for planar configuration
This commit is contained in:
Jonathan Behrens authored and GitHub committed 2023-06-10 12:46:20 -07:00
commit 473868a7bb
5 files changed
+115 -10

No files matched your search

+52 -5
View File
@@ -3,7 +3,9 @@ use super::stream::{ByteOrder, DeflateReader, JpegReader, LZWReader, PackBitsRea
use super::tag_reader::TagReader; use super::tag_reader::TagReader;
use super::{fp_predict_f32, fp_predict_f64, DecodingBuffer, Limits}; use super::{fp_predict_f32, fp_predict_f64, DecodingBuffer, Limits};
use super::{stream::SmartReader, ChunkType}; 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 crate::{ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError};
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
use std::io::{self, Cursor, Read, Seek}; use std::io::{self, Cursor, Read, Seek};
@@ -71,6 +73,7 @@ pub(crate) struct Image {
pub predictor: Predictor, pub predictor: Predictor,
pub jpeg_tables: Option<Arc<Vec<u8>>>, pub jpeg_tables: Option<Arc<Vec<u8>>>,
pub chunk_type: ChunkType, pub chunk_type: ChunkType,
pub planar_config: PlanarConfiguration,
pub strip_decoder: Option<StripDecodeState>, pub strip_decoder: Option<StripDecodeState>,
pub tile_attributes: Option<TileAttributes>, pub tile_attributes: Option<TileAttributes>,
pub chunk_offsets: Vec<u64>, pub chunk_offsets: Vec<u64>,
@@ -173,6 +176,23 @@ impl Image {
.transpose()? .transpose()?
.unwrap_or(Predictor::None); .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_type;
let chunk_offsets; let chunk_offsets;
let chunk_bytes; let chunk_bytes;
@@ -206,7 +226,7 @@ impl Image {
if chunk_offsets.len() != chunk_bytes.len() if chunk_offsets.len() != chunk_bytes.len()
|| rows_per_strip == 0 || rows_per_strip == 0
|| u32::try_from(chunk_offsets.len())? || 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( return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered, TiffFormatError::InconsistentSizesEncountered,
@@ -245,7 +265,8 @@ impl Image {
let tile = tile_attributes.as_ref().unwrap(); let tile = tile_attributes.as_ref().unwrap();
if chunk_offsets.len() != chunk_bytes.len() 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( return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered, TiffFormatError::InconsistentSizesEncountered,
@@ -271,6 +292,7 @@ impl Image {
jpeg_tables, jpeg_tables,
predictor, predictor,
chunk_type, chunk_type,
planar_config,
strip_decoder, strip_decoder,
tile_attributes, tile_attributes,
chunk_offsets, 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)> { pub(crate) fn chunk_file_range(&self, chunk: u32) -> TiffResult<(u64, u64)> {
let file_offset = self let file_offset = self
.chunk_offsets .chunk_offsets
@@ -434,7 +478,10 @@ impl Image {
match self.chunk_type { match self.chunk_type {
ChunkType::Strip => { 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) .checked_mul(dims.1)
.and_then(|x| self.height.checked_sub(x)) .and_then(|x| self.height.checked_sub(x))
.ok_or(TiffError::UsageError(UsageError::InvalidChunkIndex( .ok_or(TiffError::UsageError(UsageError::InvalidChunkIndex(
@@ -528,7 +575,7 @@ impl Image {
let compression_method = self.compression_method; let compression_method = self.compression_method;
let photometric_interpretation = self.photometric_interpretation; let photometric_interpretation = self.photometric_interpretation;
let predictor = self.predictor; let predictor = self.predictor;
let samples = self.bits_per_sample.len(); let samples = self.samples_per_pixel();
let chunk_dims = self.chunk_dimensions()?; let chunk_dims = self.chunk_dimensions()?;
let data_dims = self.chunk_data_dimensions(chunk_index)?; let data_dims = self.chunk_data_dimensions(chunk_index)?;
+17 -5
View File
@@ -10,7 +10,8 @@ use crate::{
use self::ifd::Directory; use self::ifd::Directory;
use self::image::Image; use self::image::Image;
use crate::tags::{ use crate::tags::{
CompressionMethod, PhotometricInterpretation, Predictor, SampleFormat, Tag, Type, CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat,
Tag, Type,
}; };
use self::stream::{ByteOrder, EndianReader, SmartReader}; use self::stream::{ByteOrder, EndianReader, SmartReader};
@@ -579,6 +580,7 @@ impl<R: Read + Seek> Decoder<R> {
jpeg_tables: None, jpeg_tables: None,
predictor: Predictor::None, predictor: Predictor::None,
chunk_type: ChunkType::Strip, chunk_type: ChunkType::Strip,
planar_config: PlanarConfiguration::Chunky,
strip_decoder: None, strip_decoder: None,
tile_attributes: None, tile_attributes: None,
chunk_offsets: Vec::new(), chunk_offsets: Vec::new(),
@@ -1016,7 +1018,12 @@ impl<R: Read + Seek> Decoder<R> {
None => return Err(TiffError::IntSizeError), 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 /// Number of tiles in image
@@ -1051,7 +1058,7 @@ impl<R: Read + Seek> Decoder<R> {
fn result_buffer(&self, width: usize, height: usize) -> TiffResult<DecodingResult> { fn result_buffer(&self, width: usize, height: usize) -> TiffResult<DecodingResult> {
let buffer_size = match width let buffer_size = match width
.checked_mul(height) .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, Some(s) => s,
None => return Err(TiffError::LimitsExceeded), None => return Err(TiffError::LimitsExceeded),
@@ -1146,7 +1153,7 @@ impl<R: Read + Seek> Decoder<R> {
)); ));
} }
let samples = self.image().bits_per_sample.len(); let samples = self.image().samples_per_pixel();
if samples == 0 { if samples == 0 {
return Err(TiffError::FormatError( return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered, TiffFormatError::InconsistentSizesEncountered,
@@ -1156,7 +1163,12 @@ impl<R: Read + Seek> Decoder<R> {
let chunks_across = ((width - 1) / chunk_dimensions.0 + 1) as usize; let chunks_across = ((width - 1) / chunk_dimensions.0 + 1) as usize;
let strip_samples = width as usize * chunk_dimensions.1 as usize * samples; 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])?; self.goto_offset_u64(self.image().chunk_offsets[chunk])?;
let x = chunk % chunks_across; let x = chunk % chunks_across;
+4
View File
@@ -66,6 +66,7 @@ pub enum TiffFormatError {
InvalidTagValueType(Tag), InvalidTagValueType(Tag),
RequiredTagNotFound(Tag), RequiredTagNotFound(Tag),
UnknownPredictor(u16), UnknownPredictor(u16),
UnknownPlanarConfiguration(u16),
ByteExpected(Value), ByteExpected(Value),
UnsignedIntegerExpected(Value), UnsignedIntegerExpected(Value),
SignedIntegerExpected(Value), SignedIntegerExpected(Value),
@@ -113,6 +114,9 @@ impl fmt::Display for TiffFormatError {
UnknownPredictor(ref predictor) => { UnknownPredictor(ref predictor) => {
write!(fmt, "Unknown predictor “{}” encountered", 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), ByteExpected(ref val) => write!(fmt, "Expected byte, {:?} found.", val),
UnsignedIntegerExpected(ref val) => { UnsignedIntegerExpected(ref val) => {
write!(fmt, "Expected unsigned integer, {:?} found.", val) write!(fmt, "Expected unsigned integer, {:?} found.", val)
+42
View File
@@ -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] #[test]
fn test_div_zero() { fn test_div_zero() {
use tiff::{TiffError, TiffFormatError}; use tiff::{TiffError, TiffFormatError};
Binary file not shown.