Skip to content

Commit

Permalink
Allow arbitrary number of samples as long as all have the same bit de…
Browse files Browse the repository at this point in the history
…pth (#215)
  • Loading branch information
fintelia committed Oct 4, 2023
1 parent a67f505 commit 4a2e768
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 48 deletions.
68 changes: 40 additions & 28 deletions src/decoder/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag,
};
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::sync::Arc;

Expand Down Expand Up @@ -64,9 +64,9 @@ pub(crate) struct Image {
pub ifd: Option<Directory>,
pub width: u32,
pub height: u32,
pub bits_per_sample: Vec<u8>,
pub bits_per_sample: u8,
#[allow(unused)]
pub samples: u8,
pub samples: u16,
pub sample_format: Vec<SampleFormat>,
pub photometric_interpretation: PhotometricInterpretation,
pub compression_method: CompressionMethod,
Expand Down Expand Up @@ -134,12 +134,14 @@ impl Image {
None
};

let samples = tag_reader
let samples: u16 = tag_reader
.find_tag(Tag::SamplesPerPixel)?
.map(Value::into_u16)
.transpose()?
.unwrap_or(1)
.try_into()?;
.unwrap_or(1);
if samples == 0 {
return Err(TiffFormatError::SamplesPerPixelIsZero.into());
}

let sample_format = match tag_reader.find_tag_uint_vec(Tag::SampleFormat)? {
Some(vals) => {
Expand All @@ -158,12 +160,22 @@ impl Image {
None => vec![SampleFormat::Uint],
};

let bits_per_sample = match samples {
1 | 3 | 4 => tag_reader
.find_tag_uint_vec(Tag::BitsPerSample)?
.unwrap_or_else(|| vec![1]),
_ => return Err(TiffUnsupportedError::UnsupportedSampleDepth(samples).into()),
};
let bits_per_sample: Vec<u8> = tag_reader
.find_tag_uint_vec(Tag::BitsPerSample)?
.unwrap_or_else(|| vec![1]);

// 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
.find_tag(Tag::Predictor)?
Expand Down Expand Up @@ -284,7 +296,7 @@ impl Image {
ifd: Some(ifd),
width,
height,
bits_per_sample,
bits_per_sample: bits_per_sample[0],
samples,
sample_format,
photometric_interpretation,
Expand All @@ -302,9 +314,9 @@ impl Image {

pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
match self.photometric_interpretation {
PhotometricInterpretation::RGB => match self.bits_per_sample[..] {
[r, g, b] if [r, r] == [g, b] => Ok(ColorType::RGB(r)),
[r, g, b, a] if [r, r, r] == [g, b, a] => Ok(ColorType::RGBA(r)),
PhotometricInterpretation::RGB => match self.samples {
3 => Ok(ColorType::RGB(self.bits_per_sample)),
4 => Ok(ColorType::RGBA(self.bits_per_sample)),
// FIXME: We should _ignore_ other components. In particular:
// > 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
Expand All @@ -313,39 +325,39 @@ impl Image {
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
self.bits_per_sample.clone(),
vec![self.bits_per_sample; self.samples as usize],
),
)),
},
PhotometricInterpretation::CMYK => match self.bits_per_sample[..] {
[c, m, y, k] if [c, c, c] == [m, y, k] => Ok(ColorType::CMYK(c)),
PhotometricInterpretation::CMYK => match self.samples {
4 => Ok(ColorType::CMYK(self.bits_per_sample)),
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
self.bits_per_sample.clone(),
vec![self.bits_per_sample; self.samples as usize],
),
)),
},
PhotometricInterpretation::YCbCr => match self.bits_per_sample[..] {
[y, cb, cr] if [y, y] == [cb, cr] => Ok(ColorType::YCbCr(y)),
PhotometricInterpretation::YCbCr => match self.samples {
3 => Ok(ColorType::YCbCr(self.bits_per_sample)),
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
self.bits_per_sample.clone(),
vec![self.bits_per_sample; self.samples as usize],
),
)),
},
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
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
self.bits_per_sample.clone(),
vec![self.bits_per_sample; self.samples as usize],
),
)),
}
Expand Down Expand Up @@ -448,7 +460,7 @@ impl Image {
/// * `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::Chunky => self.samples.into(),
PlanarConfiguration::Planar => 1,
}
}
Expand All @@ -457,7 +469,7 @@ impl Image {
pub(crate) fn strips_per_pixel(&self) -> usize {
match self.planar_config {
PlanarConfiguration::Chunky => 1,
PlanarConfiguration::Planar => self.bits_per_sample.len(),
PlanarConfiguration::Planar => self.samples.into(),
}
}

Expand Down
10 changes: 2 additions & 8 deletions src/decoder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ impl<R: Read + Seek> Decoder<R> {
ifd: None,
width: 0,
height: 0,
bits_per_sample: vec![1],
bits_per_sample: 1,
samples: 1,
sample_format: vec![SampleFormat::Uint],
photometric_interpretation: PhotometricInterpretation::BlackIsZero,
Expand Down Expand Up @@ -1064,13 +1064,7 @@ impl<R: Read + Seek> Decoder<R> {
None => return Err(TiffError::LimitsExceeded),
};

let max_sample_bits = self
.image()
.bits_per_sample
.iter()
.cloned()
.max()
.unwrap_or(8);
let max_sample_bits = self.image().bits_per_sample;
match self
.image()
.sample_format
Expand Down
6 changes: 6 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub enum TiffFormatError {
StripTileTagConflict,
CycleInOffsets,
JpegDecoder(JpegDecoderError),
SamplesPerPixelIsZero,
}

impl fmt::Display for TiffFormatError {
Expand Down Expand Up @@ -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."),
CycleInOffsets => write!(fmt, "File contained a cycle in the list of IFDs"),
JpegDecoder(ref error) => write!(fmt, "{}", error),
SamplesPerPixelIsZero => write!(fmt, "Samples per pixel is zero"),
}
}
}
Expand All @@ -146,6 +148,7 @@ impl fmt::Display for TiffFormatError {
pub enum TiffUnsupportedError {
FloatingPointPredictor(ColorType),
HorizontalPredictor(ColorType),
InconsistentBitsPerSample(Vec<u8>),
InterpretationWithBits(PhotometricInterpretation, Vec<u8>),
UnknownInterpretation,
UnknownCompressionMethod,
Expand Down Expand Up @@ -174,6 +177,9 @@ impl fmt::Display for TiffUnsupportedError {
"Horizontal predictor for {:?} is unsupported.",
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!(
fmt,
"{:?} with {:?} bits per sample is unsupported",
Expand Down
15 changes: 3 additions & 12 deletions tests/decode_images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,7 @@ fn fuzzer_testcase5() {
178, 178, 178,
];

let mut decoder = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap();

let _ = decoder.read_image().unwrap_err();
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
}

#[test]
Expand All @@ -394,7 +392,7 @@ fn fuzzer_testcase1() {
}

#[test]
fn test_stripped_image_overflow() {
fn fuzzer_testcase6() {
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,
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,
Expand All @@ -403,14 +401,7 @@ fn test_stripped_image_overflow() {
178, 178,
];

let mut decoder = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap();

let err = decoder.read_image().unwrap_err();

match err {
tiff::TiffError::LimitsExceeded => {}
unexpected => panic!("Unexpected error {}", unexpected),
}
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
}

#[test]
Expand Down

0 comments on commit 4a2e768

Please sign in to comment.