Add basic support for planar configuration
This commit is contained in:
1 parent
0da9770916
commit
42feeba8da
5 files changed
+119
-11
No files matched your search
+56
-6
@@ -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<Arc<Vec<u8>>>,
|
||||
pub chunk_type: ChunkType,
|
||||
pub planar_config: PlanarConfiguration,
|
||||
pub strip_decoder: Option<StripDecodeState>,
|
||||
pub tile_attributes: Option<TileAttributes>,
|
||||
pub chunk_offsets: Vec<u64>,
|
||||
@@ -174,6 +177,18 @@ 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 chunk_type;
|
||||
let chunk_offsets;
|
||||
let chunk_bytes;
|
||||
@@ -201,13 +216,22 @@ impl Image {
|
||||
.map(Value::into_u32)
|
||||
.transpose()?
|
||||
.unwrap_or(height);
|
||||
if rows_per_strip == 0 {
|
||||
return Err(TiffError::FormatError(
|
||||
TiffFormatError::InconsistentSizesEncountered,
|
||||
));
|
||||
}
|
||||
let num_chunks = match planar_config {
|
||||
PlanarConfiguration::Chunky => height.saturating_sub(1) / rows_per_strip + 1,
|
||||
PlanarConfiguration::Planar => {
|
||||
(height.saturating_sub(1) / rows_per_strip + 1) * samples as u32
|
||||
}
|
||||
};
|
||||
strip_decoder = Some(StripDecodeState { rows_per_strip });
|
||||
tile_attributes = None;
|
||||
|
||||
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
|
||||
|| u32::try_from(chunk_offsets.len())? != num_chunks
|
||||
{
|
||||
return Err(TiffError::FormatError(
|
||||
TiffFormatError::InconsistentSizesEncountered,
|
||||
@@ -272,6 +296,7 @@ impl Image {
|
||||
jpeg_tables,
|
||||
predictor,
|
||||
chunk_type,
|
||||
planar_config,
|
||||
strip_decoder,
|
||||
tile_attributes,
|
||||
chunk_offsets,
|
||||
@@ -396,6 +421,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
|
||||
@@ -435,7 +482,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(
|
||||
@@ -525,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)?;
|
||||
|
||||
+17
-5
@@ -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<R: Read + Seek> Decoder<R> {
|
||||
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<R: Read + Seek> Decoder<R> {
|
||||
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
|
||||
@@ -1050,7 +1057,7 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
fn result_buffer(&self, width: usize, height: usize) -> TiffResult<DecodingResult> {
|
||||
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),
|
||||
@@ -1145,7 +1152,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 {
|
||||
return Err(TiffError::FormatError(
|
||||
TiffFormatError::InconsistentSizesEncountered,
|
||||
@@ -1155,7 +1162,12 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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};
|
||||
|
||||
Binary file not shown.
Reference in new issue
Block a user