Allow arbitrary number of samples as long as all have the same bit depth (#215)

This commit is contained in:
Jonathan Behrens authored and GitHub committed 2023-10-04 09:45:03 -07:00
1 parent a67f50568c
commit 4a2e76858b
4 files changed
+51 -48

No files matched your search

+40 -28
View File
@@ -7,7 +7,7 @@ use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag, 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;
use std::io::{self, Cursor, Read, Seek}; use std::io::{self, Cursor, Read, Seek};
use std::sync::Arc; use std::sync::Arc;
@@ -64,9 +64,9 @@ pub(crate) struct Image {
pub ifd: Option<Directory>, pub ifd: Option<Directory>,
pub width: u32, pub width: u32,
pub height: u32, pub height: u32,
pub bits_per_sample: Vec<u8>, pub bits_per_sample: u8,
#[allow(unused)] #[allow(unused)]
pub samples: u8, pub samples: u16,
pub sample_format: Vec<SampleFormat>, pub sample_format: Vec<SampleFormat>,
pub photometric_interpretation: PhotometricInterpretation, pub photometric_interpretation: PhotometricInterpretation,
pub compression_method: CompressionMethod, pub compression_method: CompressionMethod,
@@ -134,12 +134,14 @@ impl Image {
None None
}; };
let samples = tag_reader let samples: u16 = tag_reader
.find_tag(Tag::SamplesPerPixel)? .find_tag(Tag::SamplesPerPixel)?
.map(Value::into_u16) .map(Value::into_u16)
.transpose()? .transpose()?
.unwrap_or(1) .unwrap_or(1);
.try_into()?; if samples == 0 {
return Err(TiffFormatError::SamplesPerPixelIsZero.into());
}
let sample_format = match tag_reader.find_tag_uint_vec(Tag::SampleFormat)? { let sample_format = match tag_reader.find_tag_uint_vec(Tag::SampleFormat)? {
Some(vals) => { Some(vals) => {
@@ -158,12 +160,22 @@ impl Image {
None => vec![SampleFormat::Uint], None => vec![SampleFormat::Uint],
}; };
let bits_per_sample = match samples { let bits_per_sample: Vec<u8> = tag_reader
1 | 3 | 4 => tag_reader .find_tag_uint_vec(Tag::BitsPerSample)?
.find_tag_uint_vec(Tag::BitsPerSample)? .unwrap_or_else(|| vec![1]);
.unwrap_or_else(|| vec![1]),
_ => return Err(TiffUnsupportedError::UnsupportedSampleDepth(samples).into()), // Technically bits_per_sample.len() should be *equal* to samples, but libtiff also allows
}; // it to be a single value that applies to all samples.
if bits_per_sample.len() != samples.into() && bits_per_sample.len() != 1 {
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
));
}
// This library (and libtiff) do not support mixed sample formats.
if bits_per_sample.iter().any(|&b| b != bits_per_sample[0]) {
return Err(TiffUnsupportedError::InconsistentBitsPerSample(bits_per_sample).into());
}
let predictor = tag_reader let predictor = tag_reader
.find_tag(Tag::Predictor)? .find_tag(Tag::Predictor)?
@@ -284,7 +296,7 @@ impl Image {
ifd: Some(ifd), ifd: Some(ifd),
width, width,
height, height,
bits_per_sample, bits_per_sample: bits_per_sample[0],
samples, samples,
sample_format, sample_format,
photometric_interpretation, photometric_interpretation,
@@ -302,9 +314,9 @@ impl Image {
pub(crate) fn colortype(&self) -> TiffResult<ColorType> { pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
match self.photometric_interpretation { match self.photometric_interpretation {
PhotometricInterpretation::RGB => match self.bits_per_sample[..] { PhotometricInterpretation::RGB => match self.samples {
[r, g, b] if [r, r] == [g, b] => Ok(ColorType::RGB(r)), 3 => Ok(ColorType::RGB(self.bits_per_sample)),
[r, g, b, a] if [r, r, r] == [g, b, a] => Ok(ColorType::RGBA(r)), 4 => Ok(ColorType::RGBA(self.bits_per_sample)),
// FIXME: We should _ignore_ other components. In particular: // FIXME: We should _ignore_ other components. In particular:
// > Beware of extra components. Some TIFF files may have more components per pixel // > Beware of extra components. Some TIFF files may have more components per pixel
// than you think. A Baseline TIFF reader must skip over them gracefully,using the // than you think. A Baseline TIFF reader must skip over them gracefully,using the
@@ -313,39 +325,39 @@ impl Image {
_ => Err(TiffError::UnsupportedError( _ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits( TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation, self.photometric_interpretation,
self.bits_per_sample.clone(), vec![self.bits_per_sample; self.samples as usize],
), ),
)), )),
}, },
PhotometricInterpretation::CMYK => match self.bits_per_sample[..] { PhotometricInterpretation::CMYK => match self.samples {
[c, m, y, k] if [c, c, c] == [m, y, k] => Ok(ColorType::CMYK(c)), 4 => Ok(ColorType::CMYK(self.bits_per_sample)),
_ => Err(TiffError::UnsupportedError( _ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits( TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation, self.photometric_interpretation,
self.bits_per_sample.clone(), vec![self.bits_per_sample; self.samples as usize],
), ),
)), )),
}, },
PhotometricInterpretation::YCbCr => match self.bits_per_sample[..] { PhotometricInterpretation::YCbCr => match self.samples {
[y, cb, cr] if [y, y] == [cb, cr] => Ok(ColorType::YCbCr(y)), 3 => Ok(ColorType::YCbCr(self.bits_per_sample)),
_ => Err(TiffError::UnsupportedError( _ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits( TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation, self.photometric_interpretation,
self.bits_per_sample.clone(), vec![self.bits_per_sample; self.samples as usize],
), ),
)), )),
}, },
PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero
if self.bits_per_sample.len() == 1 => if self.samples == 1 =>
{ {
Ok(ColorType::Gray(self.bits_per_sample[0])) Ok(ColorType::Gray(self.bits_per_sample))
} }
// TODO: this is bad we should not fail at this point // TODO: this is bad we should not fail at this point
_ => Err(TiffError::UnsupportedError( _ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits( TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation, self.photometric_interpretation,
self.bits_per_sample.clone(), vec![self.bits_per_sample; self.samples as usize],
), ),
)), )),
} }
@@ -448,7 +460,7 @@ impl Image {
/// * `PlanarConfiguration::Planar` -> 1 (RRR...) (GGG...) (BBB...) /// * `PlanarConfiguration::Planar` -> 1 (RRR...) (GGG...) (BBB...)
pub(crate) fn samples_per_pixel(&self) -> usize { pub(crate) fn samples_per_pixel(&self) -> usize {
match self.planar_config { match self.planar_config {
PlanarConfiguration::Chunky => self.bits_per_sample.len(), PlanarConfiguration::Chunky => self.samples.into(),
PlanarConfiguration::Planar => 1, PlanarConfiguration::Planar => 1,
} }
} }
@@ -457,7 +469,7 @@ impl Image {
pub(crate) fn strips_per_pixel(&self) -> usize { pub(crate) fn strips_per_pixel(&self) -> usize {
match self.planar_config { match self.planar_config {
PlanarConfiguration::Chunky => 1, PlanarConfiguration::Chunky => 1,
PlanarConfiguration::Planar => self.bits_per_sample.len(), PlanarConfiguration::Planar => self.samples.into(),
} }
} }
+2 -8
View File
@@ -572,7 +572,7 @@ impl<R: Read + Seek> Decoder<R> {
ifd: None, ifd: None,
width: 0, width: 0,
height: 0, height: 0,
bits_per_sample: vec![1], bits_per_sample: 1,
samples: 1, samples: 1,
sample_format: vec![SampleFormat::Uint], sample_format: vec![SampleFormat::Uint],
photometric_interpretation: PhotometricInterpretation::BlackIsZero, photometric_interpretation: PhotometricInterpretation::BlackIsZero,
@@ -1064,13 +1064,7 @@ impl<R: Read + Seek> Decoder<R> {
None => return Err(TiffError::LimitsExceeded), None => return Err(TiffError::LimitsExceeded),
}; };
let max_sample_bits = self let max_sample_bits = self.image().bits_per_sample;
.image()
.bits_per_sample
.iter()
.cloned()
.max()
.unwrap_or(8);
match self match self
.image() .image()
.sample_format .sample_format
+6
View File
@@ -75,6 +75,7 @@ pub enum TiffFormatError {
StripTileTagConflict, StripTileTagConflict,
CycleInOffsets, CycleInOffsets,
JpegDecoder(JpegDecoderError), JpegDecoder(JpegDecoderError),
SamplesPerPixelIsZero,
} }
impl fmt::Display for TiffFormatError { impl fmt::Display for TiffFormatError {
@@ -129,6 +130,7 @@ impl fmt::Display for TiffFormatError {
StripTileTagConflict => write!(fmt, "File should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found."), StripTileTagConflict => write!(fmt, "File should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found."),
CycleInOffsets => write!(fmt, "File contained a cycle in the list of IFDs"), CycleInOffsets => write!(fmt, "File contained a cycle in the list of IFDs"),
JpegDecoder(ref error) => write!(fmt, "{}", error), JpegDecoder(ref error) => write!(fmt, "{}", error),
SamplesPerPixelIsZero => write!(fmt, "Samples per pixel is zero"),
} }
} }
} }
@@ -146,6 +148,7 @@ impl fmt::Display for TiffFormatError {
pub enum TiffUnsupportedError { pub enum TiffUnsupportedError {
FloatingPointPredictor(ColorType), FloatingPointPredictor(ColorType),
HorizontalPredictor(ColorType), HorizontalPredictor(ColorType),
InconsistentBitsPerSample(Vec<u8>),
InterpretationWithBits(PhotometricInterpretation, Vec<u8>), InterpretationWithBits(PhotometricInterpretation, Vec<u8>),
UnknownInterpretation, UnknownInterpretation,
UnknownCompressionMethod, UnknownCompressionMethod,
@@ -174,6 +177,9 @@ impl fmt::Display for TiffUnsupportedError {
"Horizontal predictor for {:?} is unsupported.", "Horizontal predictor for {:?} is unsupported.",
color_type color_type
), ),
InconsistentBitsPerSample(ref bits_per_sample) => {
write!(fmt, "Inconsistent bits per sample: {:?}.", bits_per_sample)
}
InterpretationWithBits(ref photometric_interpretation, ref bits_per_sample) => write!( InterpretationWithBits(ref photometric_interpretation, ref bits_per_sample) => write!(
fmt, fmt,
"{:?} with {:?} bits per sample is unsupported", "{:?} with {:?} bits per sample is unsupported",
+3 -12
View File
@@ -375,9 +375,7 @@ fn fuzzer_testcase5() {
178, 178, 178, 178, 178, 178,
]; ];
let mut decoder = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap(); let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let _ = decoder.read_image().unwrap_err();
} }
#[test] #[test]
@@ -394,7 +392,7 @@ fn fuzzer_testcase1() {
} }
#[test] #[test]
fn test_stripped_image_overflow() { fn fuzzer_testcase6() {
let image = [ let image = [
73, 73, 42, 0, 8, 0, 0, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 100, 0, 0, 148, 1, 1, 4, 0, 1, 0, 73, 73, 42, 0, 8, 0, 0, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 100, 0, 0, 148, 1, 1, 4, 0, 1, 0,
0, 0, 158, 0, 0, 251, 3, 1, 3, 255, 254, 255, 255, 0, 1, 0, 0, 0, 6, 1, 3, 0, 1, 0, 0, 0, 0, 0, 158, 0, 0, 251, 3, 1, 3, 255, 254, 255, 255, 0, 1, 0, 0, 0, 6, 1, 3, 0, 1, 0, 0, 0,
@@ -403,14 +401,7 @@ fn test_stripped_image_overflow() {
178, 178, 178, 178,
]; ];
let mut decoder = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap(); let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
let err = decoder.read_image().unwrap_err();
match err {
tiff::TiffError::LimitsExceeded => {}
unexpected => panic!("Unexpected error {}", unexpected),
}
} }
#[test] #[test]