diff --git a/src/decoder/ifd.rs b/src/decoder/ifd.rs index 2be016b..51553d2 100644 --- a/src/decoder/ifd.rs +++ b/src/decoder/ifd.rs @@ -122,6 +122,8 @@ pub enum Type(u16) { SSHORT = 8, SLONG = 9, SRATIONAL = 10, + /// BigTIFF 64-bit unsigned integer + LONG8 = 16, } } diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index a3aff91..f879df6 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -19,6 +19,10 @@ pub enum DecodingResult { U8(Vec), /// A vector of unsigned words U16(Vec), + /// A vector of 32 bit unsigned ints + U32(Vec), + /// A vector of 64 bit unsigned ints + U64(Vec), } impl DecodingResult { @@ -38,10 +42,28 @@ impl DecodingResult { } } + fn new_u32(size: usize, limits: &Limits) -> TiffResult { + if size > limits.decoding_buffer_size / 4 { + Err(TiffError::LimitsExceeded) + } else { + Ok(DecodingResult::U32(vec![0; size])) + } + } + + fn new_u64(size: usize, limits: &Limits) -> TiffResult { + if size > limits.decoding_buffer_size / 8 { + Err(TiffError::LimitsExceeded) + } else { + Ok(DecodingResult::U64(vec![0; size])) + } + } + pub fn as_buffer(&mut self, start: usize) -> DecodingBuffer { match *self { DecodingResult::U8(ref mut buf) => DecodingBuffer::U8(&mut buf[start..]), DecodingResult::U16(ref mut buf) => DecodingBuffer::U16(&mut buf[start..]), + DecodingResult::U32(ref mut buf) => DecodingBuffer::U32(&mut buf[start..]), + DecodingResult::U64(ref mut buf) => DecodingBuffer::U64(&mut buf[start..]), } } } @@ -52,6 +74,10 @@ pub enum DecodingBuffer<'a> { U8(&'a mut [u8]), /// A slice of unsigned words U16(&'a mut [u16]), + /// A slice of 32 bit unsigned ints + U32(&'a mut [u32]), + /// A slice of 64 bit unsigned ints + U64(&'a mut [u64]), } impl<'a> DecodingBuffer<'a> { @@ -59,6 +85,8 @@ impl<'a> DecodingBuffer<'a> { match *self { DecodingBuffer::U8(ref buf) => buf.len(), DecodingBuffer::U16(ref buf) => buf.len(), + DecodingBuffer::U32(ref buf) => buf.len(), + DecodingBuffer::U64(ref buf) => buf.len(), } } @@ -66,6 +94,8 @@ impl<'a> DecodingBuffer<'a> { match *self { DecodingBuffer::U8(_) => 1, DecodingBuffer::U16(_) => 2, + DecodingBuffer::U32(_) => 4, + DecodingBuffer::U64(_) => 8, } } @@ -76,6 +106,8 @@ impl<'a> DecodingBuffer<'a> { match *self { DecodingBuffer::U8(ref mut buf) => DecodingBuffer::U8(buf), DecodingBuffer::U16(ref mut buf) => DecodingBuffer::U16(buf), + DecodingBuffer::U32(ref mut buf) => DecodingBuffer::U32(buf), + DecodingBuffer::U64(ref mut buf) => DecodingBuffer::U64(buf), } } } @@ -233,6 +265,18 @@ impl Wrapping for u16 { } } +impl Wrapping for u32 { + fn wrapping_add(&self, other: Self) -> Self { + u32::wrapping_add(*self, other) + } +} + +impl Wrapping for u64 { + fn wrapping_add(&self, other: Self) -> Self { + u64::wrapping_add(*self, other) + } +} + fn rev_hpredict_nsamp(image: &mut [T], size: (u32, u32), samples: usize) where T: Copy + Wrapping, @@ -250,9 +294,9 @@ where fn rev_hpredict(image: DecodingBuffer, size: (u32, u32), color_type: ColorType) -> TiffResult<()> { let samples = match color_type { - ColorType::Gray(8) | ColorType::Gray(16) => 1, - ColorType::RGB(8) | ColorType::RGB(16) => 3, - ColorType::RGBA(8) | ColorType::RGBA(16) | ColorType::CMYK(8) => 4, + ColorType::Gray(8) | ColorType::Gray(16) | ColorType::Gray(32) | ColorType::Gray(64) => 1, + ColorType::RGB(8) | ColorType::RGB(16) | ColorType::RGB(32) | ColorType::RGB(64) => 3, + ColorType::RGBA(8) | ColorType::RGBA(16) | ColorType::RGBA(32) | ColorType::RGBA(64) | ColorType::CMYK(8) => 4, _ => { return Err(TiffError::UnsupportedError( TiffUnsupportedError::HorizontalPredictor(color_type), @@ -266,6 +310,12 @@ fn rev_hpredict(image: DecodingBuffer, size: (u32, u32), color_type: ColorType) DecodingBuffer::U16(buf) => { rev_hpredict_nsamp(buf, size, samples); } + DecodingBuffer::U32(buf) => { + rev_hpredict_nsamp(buf, size, samples); + } + DecodingBuffer::U64(buf) => { + rev_hpredict_nsamp(buf, size, samples); + } } Ok(()) } @@ -301,19 +351,18 @@ impl Decoder { pub fn colortype(&mut self) -> TiffResult { match self.photometric_interpretation { - // TODO: catch also [ 8, 8, 8, _] this does not work due to a bug in rust atm - PhotometricInterpretation::RGB if self.bits_per_sample == [8, 8, 8, 8] => { - Ok(ColorType::RGBA(8)) - } - PhotometricInterpretation::RGB if self.bits_per_sample == [8, 8, 8] => { - Ok(ColorType::RGB(8)) - } - PhotometricInterpretation::RGB if self.bits_per_sample == [16, 16, 16, 16] => { - Ok(ColorType::RGBA(16)) - } - PhotometricInterpretation::RGB if self.bits_per_sample == [16, 16, 16] => { - Ok(ColorType::RGB(16)) - } + 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)), + _ => Err(TiffError::UnsupportedError( + TiffUnsupportedError::InterpretationWithBits( + self.photometric_interpretation, + self.bits_per_sample.clone(), + ), + )), + } + }, PhotometricInterpretation::CMYK if self.bits_per_sample == [8, 8, 8, 8] => { Ok(ColorType::CMYK(8)) } @@ -659,6 +708,34 @@ impl Decoder { reader.read_u16_into(&mut buffer[..bytes / 2])?; bytes / 2 } + (ColorType::RGBA(32), DecodingBuffer::U32(ref mut buffer)) + | (ColorType::RGB(32), DecodingBuffer::U32(ref mut buffer)) => { + reader.read_u32_into(&mut buffer[..bytes / 4])?; + bytes / 4 + } + (ColorType::RGBA(64), DecodingBuffer::U64(ref mut buffer)) + | (ColorType::RGB(64), DecodingBuffer::U64(ref mut buffer)) => { + reader.read_u64_into(&mut buffer[..bytes / 8])?; + bytes / 8 + } + (ColorType::Gray(64), DecodingBuffer::U64(ref mut buffer)) => { + reader.read_u64_into(&mut buffer[..bytes / 8])?; + if self.photometric_interpretation == PhotometricInterpretation::WhiteIsZero { + for datum in buffer[..bytes / 8].iter_mut() { + *datum = 0xffff_ffff_ffff_ffff - *datum + } + } + bytes / 8 + } + (ColorType::Gray(32), DecodingBuffer::U32(ref mut buffer)) => { + reader.read_u32_into(&mut buffer[..bytes / 4])?; + if self.photometric_interpretation == PhotometricInterpretation::WhiteIsZero { + for datum in buffer[..bytes / 4].iter_mut() { + *datum = 0xffff_ffff - *datum + } + } + bytes / 4 + } (ColorType::Gray(16), DecodingBuffer::U16(ref mut buffer)) => { reader.read_u16_into(&mut buffer[..bytes / 2])?; if self.photometric_interpretation == PhotometricInterpretation::WhiteIsZero { @@ -806,6 +883,8 @@ impl Decoder { let mut result = match self.bits_per_sample.iter().cloned().max().unwrap_or(8) { n if n <= 8 => DecodingResult::new_u8(buffer_size, &self.limits)?, n if n <= 16 => DecodingResult::new_u16(buffer_size, &self.limits)?, + n if n <= 32 => DecodingResult::new_u32(buffer_size, &self.limits)?, + n if n <= 64 => DecodingResult::new_u64(buffer_size, &self.limits)?, n => { return Err(TiffError::UnsupportedError( TiffUnsupportedError::UnsupportedBitsPerChannel(n), @@ -834,6 +913,8 @@ impl Decoder { let mut result = match self.bits_per_sample.iter().cloned().max().unwrap_or(8) { n if n <= 8 => DecodingResult::new_u8(buffer_size, &self.limits)?, n if n <= 16 => DecodingResult::new_u16(buffer_size, &self.limits)?, + n if n <= 32 => DecodingResult::new_u32(buffer_size, &self.limits)?, + n if n <= 64 => DecodingResult::new_u64(buffer_size, &self.limits)?, n => { return Err(TiffError::UnsupportedError( TiffUnsupportedError::UnsupportedBitsPerChannel(n), diff --git a/src/decoder/stream.rs b/src/decoder/stream.rs index fe5d20c..caf4e3c 100644 --- a/src/decoder/stream.rs +++ b/src/decoder/stream.rs @@ -59,6 +59,39 @@ pub trait EndianReader: Read { } } + #[inline(always)] + fn read_u32_into(&mut self, buffer: &mut [u32]) -> Result<(), io::Error> { + match self.byte_order() { + ByteOrder::LittleEndian => { + ::read_u32_into::(self, buffer) + } + ByteOrder::BigEndian => { + ::read_u32_into::(self, buffer) + } + } + } + + /// Reads an u64 + #[inline(always)] + fn read_u64(&mut self) -> Result { + match self.byte_order() { + ByteOrder::LittleEndian => ::read_u64::(self), + ByteOrder::BigEndian => ::read_u64::(self), + } + } + + #[inline(always)] + fn read_u64_into(&mut self, buffer: &mut [u64]) -> Result<(), io::Error> { + match self.byte_order() { + ByteOrder::LittleEndian => { + ::read_u64_into::(self, buffer) + } + ByteOrder::BigEndian => { + ::read_u64_into::(self, buffer) + } + } + } + /// Reads an i32 #[inline(always)] fn read_i32(&mut self) -> Result { diff --git a/src/encoder/colortype.rs b/src/encoder/colortype.rs index a348ee7..2f49fdb 100644 --- a/src/encoder/colortype.rs +++ b/src/encoder/colortype.rs @@ -24,6 +24,20 @@ impl ColorType for Gray16 { const BITS_PER_SAMPLE: &'static [u16] = &[16]; } +pub struct Gray32; +impl ColorType for Gray32 { + type Inner = u32; + const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; + const BITS_PER_SAMPLE: &'static [u16] = &[32]; +} + +pub struct Gray64; +impl ColorType for Gray64 { + type Inner = u64; + const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero; + const BITS_PER_SAMPLE: &'static [u16] = &[64]; +} + pub struct RGB8; impl ColorType for RGB8 { type Inner = u8; @@ -38,6 +52,20 @@ impl ColorType for RGB16 { const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16]; } +pub struct RGB32; +impl ColorType for RGB32 { + type Inner = u32; + const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; + const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32]; +} + +pub struct RGB64; +impl ColorType for RGB64 { + type Inner = u64; + const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; + const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64]; +} + pub struct RGBA8; impl ColorType for RGBA8 { type Inner = u8; @@ -52,6 +80,20 @@ impl ColorType for RGBA16 { const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16]; } +pub struct RGBA32; +impl ColorType for RGBA32 { + type Inner = u16; + const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; + const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; +} + +pub struct RGBA64; +impl ColorType for RGBA64 { + type Inner = u16; + const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; + const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; +} + pub struct CMYK8; impl ColorType for CMYK8 { type Inner = u8; diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index f5b662c..9eda804 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -142,6 +142,23 @@ impl TiffValue for [i32] { } } +impl TiffValue for [u64] { + const BYTE_LEN: u32 = 8; + const FIELD_TYPE: ifd::Type = ifd::Type::LONG8; + + fn count(&self) -> u32 { + self.len() as u32 + } + + fn write(&self, writer: &mut TiffWriter) -> TiffResult<()> { + // We write using nativeedian so this sould be safe + let slice = + unsafe { ::std::slice::from_raw_parts(self.as_ptr() as *const u8, self.len() * 8) }; + writer.write_bytes(slice)?; + Ok(()) + } +} + impl TiffValue for [Rational] { const BYTE_LEN: u32 = 8; const FIELD_TYPE: ifd::Type = ifd::Type::RATIONAL; @@ -258,6 +275,20 @@ impl TiffValue for i32 { } } +impl TiffValue for u64 { + const BYTE_LEN: u32 = 8; + const FIELD_TYPE: ifd::Type = ifd::Type::LONG8; + + fn count(&self) -> u32 { + 1 + } + + fn write(&self, writer: &mut TiffWriter) -> TiffResult<()> { + writer.write_u64(*self)?; + Ok(()) + } +} + impl TiffValue for Rational { const BYTE_LEN: u32 = 8; const FIELD_TYPE: ifd::Type = ifd::Type::RATIONAL; diff --git a/tests/decode_images.rs b/tests/decode_images.rs index 7c91988..910d43e 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -44,6 +44,18 @@ fn test_rgb_u8() { } } +#[test] +fn test_gray_u12() { + let img_file = + File::open("./tests/images/12bit.cropped.tiff").expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(12)); + assert!(match decoder.read_image() { + Err(tiff::TiffError::UnsupportedError(tiff::TiffUnsupportedError::UnsupportedColorType(_))) => true, + _ => false, + }); +} + #[test] fn test_gray_u16() { let img_file = @@ -64,6 +76,58 @@ fn test_gray_u16() { } } +#[test] +fn test_gray_u32() { + let img_file = + File::open("./tests/images/gradient-1c-32b.tiff").expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(32)); + let img_res = decoder.read_image().unwrap(); + if let DecodingResult::U32(res) = img_res { + let mut res_sum: u64 = 0; + for x in res { + res_sum += x as u64; + } + + assert_eq!(res_sum, 549892913787); + } + else { + panic!("Wrong bit depth") + } +} + +#[test] +fn test_gray_u64() { + let img_file = + File::open("./tests/images/gradient-1c-64b.tiff").expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(64)); + let img_res = decoder.read_image().unwrap(); + if let DecodingResult::U64(res) = img_res { + let mut res_sum: u64 = 0; + for x in res { + res_sum += x as u64; + } + + assert_eq!(res_sum, 549892913787); + } + else { + panic!("Wrong bit depth") + } +} + +#[test] +fn test_rgb_u12() { + let img_file = + File::open("./tests/images/12bit.cropped.rgb.tiff").expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(12)); + assert!(match decoder.read_image() { + Err(tiff::TiffError::UnsupportedError(tiff::TiffUnsupportedError::UnsupportedColorType(_))) => true, + _ => false, + }); +} + #[test] fn test_rgb_u16() { let img_file = File::open("./tests/images/rgb-3c-16b.tiff").expect("Cannot find test image!"); @@ -83,6 +147,44 @@ fn test_rgb_u16() { } } +#[test] +fn test_rgb_u32() { + let img_file = File::open("./tests/images/gradient-3c-32b.tiff").expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(32)); + let img_res = decoder.read_image().unwrap(); + if let DecodingResult::U32(res) = img_res { + let mut res_sum: u64 = 0; + for x in res { + res_sum += x as u64; + } + + assert_eq!(res_sum, 2030834111716); + } + else { + panic!("Wrong bit depth") + } +} + +#[test] +fn test_rgb_u64() { + let img_file = File::open("./tests/images/gradient-3c-64b.tiff").expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(64)); + let img_res = decoder.read_image().unwrap(); + if let DecodingResult::U64(res) = img_res { + let mut res_sum: u64 = 0; + for x in res { + res_sum += x as u64; + } + + assert_eq!(res_sum, 2030834111716); + } + else { + panic!("Wrong bit depth") + } +} + #[test] fn test_string_tags() { // these files have null-terminated strings for their Software tag. One has extra bytes after diff --git a/tests/encode_images.rs b/tests/encode_images.rs index 6256cec..f3ca92f 100644 --- a/tests/encode_images.rs +++ b/tests/encode_images.rs @@ -177,6 +177,128 @@ fn test_rgb_u16_roundtrip() { } } +#[test] +fn test_gray_u32_roundtrip() { + let img_file = + File::open("./tests/images/gradient-1c-32b.tiff").expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(32)); + + let image_data = match decoder.read_image().unwrap() { + DecodingResult::U32(res) => res, + _ => panic!("Wrong data type"), + }; + + let mut file = tempfile::tempfile().unwrap(); + { + let mut tiff = TiffEncoder::new(&mut file).unwrap(); + + let (width, height) = decoder.dimensions().unwrap(); + tiff.write_image::(width, height, &image_data) + .unwrap(); + } + file.seek(SeekFrom::Start(0)).unwrap(); + { + let mut decoder = Decoder::new(&mut file).unwrap(); + if let DecodingResult::U32(img_res) = decoder.read_image().unwrap() { + assert_eq!(image_data, img_res); + } else { + panic!("Wrong data type"); + } + } +} + +#[test] +fn test_rgb_u32_roundtrip() { + let img_file = File::open("./tests/images/gradient-3c-32b.tiff").expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(32)); + + let image_data = match decoder.read_image().unwrap() { + DecodingResult::U32(res) => res, + _ => panic!("Wrong data type"), + }; + + let mut file = tempfile::tempfile().unwrap(); + { + let mut tiff = TiffEncoder::new(&mut file).unwrap(); + + let (width, height) = decoder.dimensions().unwrap(); + tiff.write_image::(width, height, &image_data) + .unwrap(); + } + file.seek(SeekFrom::Start(0)).unwrap(); + { + let mut decoder = Decoder::new(&mut file).unwrap(); + if let DecodingResult::U32(img_res) = decoder.read_image().unwrap() { + assert_eq!(image_data, img_res); + } else { + panic!("Wrong data type"); + } + } +} + +#[test] +fn test_gray_u64_roundtrip() { + let img_file = + File::open("./tests/images/gradient-1c-64b.tiff").expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(64)); + + let image_data = match decoder.read_image().unwrap() { + DecodingResult::U64(res) => res, + _ => panic!("Wrong data type"), + }; + + let mut file = tempfile::tempfile().unwrap(); + { + let mut tiff = TiffEncoder::new(&mut file).unwrap(); + + let (width, height) = decoder.dimensions().unwrap(); + tiff.write_image::(width, height, &image_data) + .unwrap(); + } + file.seek(SeekFrom::Start(0)).unwrap(); + { + let mut decoder = Decoder::new(&mut file).unwrap(); + if let DecodingResult::U64(img_res) = decoder.read_image().unwrap() { + assert_eq!(image_data, img_res); + } else { + panic!("Wrong data type"); + } + } +} + +#[test] +fn test_rgb_u64_roundtrip() { + let img_file = File::open("./tests/images/gradient-3c-64b.tiff").expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(64)); + + let image_data = match decoder.read_image().unwrap() { + DecodingResult::U64(res) => res, + _ => panic!("Wrong data type"), + }; + + let mut file = tempfile::tempfile().unwrap(); + { + let mut tiff = TiffEncoder::new(&mut file).unwrap(); + + let (width, height) = decoder.dimensions().unwrap(); + tiff.write_image::(width, height, &image_data) + .unwrap(); + } + file.seek(SeekFrom::Start(0)).unwrap(); + { + let mut decoder = Decoder::new(&mut file).unwrap(); + if let DecodingResult::U64(img_res) = decoder.read_image().unwrap() { + assert_eq!(image_data, img_res); + } else { + panic!("Wrong data type"); + } + } +} + #[test] fn test_multiple_byte() { let mut data = Cursor::new(Vec::new()); @@ -273,10 +395,10 @@ fn test_multipage_image() { // write first grayscale image (2x2 16-bit) let img1: Vec = [1, 2, 3, 4].to_vec(); - img_encoder.write_image::(2, 2, &img1[..]); + img_encoder.write_image::(2, 2, &img1[..]).unwrap(); // write second grayscale image (3x3 8-bit) let img2: Vec = [9, 8, 7, 6, 5, 4, 3, 2, 1].to_vec(); - img_encoder.write_image::(3, 3, &img2[..]); + img_encoder.write_image::(3, 3, &img2[..]).unwrap(); } // seek to the beginning of the file, so that it can be decoded diff --git a/tests/images/12bit.cropped.rgb.tiff b/tests/images/12bit.cropped.rgb.tiff new file mode 100644 index 0000000..8c8bb58 Binary files /dev/null and b/tests/images/12bit.cropped.rgb.tiff differ diff --git a/tests/images/12bit.cropped.tiff b/tests/images/12bit.cropped.tiff new file mode 100644 index 0000000..f075b6e Binary files /dev/null and b/tests/images/12bit.cropped.tiff differ diff --git a/tests/images/gradient-1c-32b.tiff b/tests/images/gradient-1c-32b.tiff new file mode 100644 index 0000000..3b05740 Binary files /dev/null and b/tests/images/gradient-1c-32b.tiff differ diff --git a/tests/images/gradient-1c-64b.tiff b/tests/images/gradient-1c-64b.tiff new file mode 100644 index 0000000..43dec0c Binary files /dev/null and b/tests/images/gradient-1c-64b.tiff differ diff --git a/tests/images/gradient-3c-32b.tiff b/tests/images/gradient-3c-32b.tiff new file mode 100644 index 0000000..d76596e Binary files /dev/null and b/tests/images/gradient-3c-32b.tiff differ diff --git a/tests/images/gradient-3c-64b.tiff b/tests/images/gradient-3c-64b.tiff new file mode 100644 index 0000000..0c5c4dc Binary files /dev/null and b/tests/images/gradient-3c-64b.tiff differ