diff --git a/Cargo.toml b/Cargo.toml index baf432f..f905607 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ exclude = ["tests/images/*", "tests/fuzz_images/*"] weezl = "0.1.0" jpeg = { package = "jpeg-decoder", version = "0.3.0", default-features = false } flate2 = "1.0.20" +bitvec = "1.0.1" +fax = "0.2.4" [dev-dependencies] criterion = "0.3.1" diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 1a61640..7d7f113 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -1,5 +1,5 @@ use super::ifd::{Directory, Value}; -use super::stream::{ByteOrder, DeflateReader, LZWReader, PackBitsReader}; +use super::stream::{ByteOrder, DeflateReader, Group4Reader, LZWReader, PackBitsReader}; use super::tag_reader::TagReader; use super::{predict_f32, predict_f64, Limits}; use super::{stream::SmartReader, ChunkType}; @@ -76,6 +76,7 @@ pub(crate) struct Image { pub tile_attributes: Option, pub chunk_offsets: Vec, pub chunk_bytes: Vec, + pub expand_samples_to_bytes: bool, } impl Image { @@ -84,6 +85,7 @@ impl Image { ifd: Directory, limits: &Limits, bigtiff: bool, + expand_samples_to_bytes: bool, ) -> TiffResult { let mut tag_reader = TagReader { reader, @@ -308,6 +310,7 @@ impl Image { tile_attributes, chunk_offsets, chunk_bytes, + expand_samples_to_bytes, }) } @@ -368,11 +371,13 @@ impl Image { } fn create_reader<'r, R: 'r + Read>( + dimensions: (u32, u32), reader: R, photometric_interpretation: PhotometricInterpretation, compression_method: CompressionMethod, compressed_length: u64, jpeg_tables: Option<&[u8]>, + expand_samples_to_bytes: bool, ) -> TiffResult> { Ok(match compression_method { CompressionMethod::None => Box::new(reader), @@ -447,6 +452,12 @@ impl Image { Box::new(Cursor::new(data)) } + CompressionMethod::Fax4 => Box::new(Group4Reader::new( + dimensions, + reader, + compressed_length, + expand_samples_to_bytes, + )?), method => { return Err(TiffError::UnsupportedError( TiffUnsupportedError::UnsupportedCompressionMethod(method), @@ -529,7 +540,13 @@ impl Image { // Ignore potential vertical padding on the bottommost strip let strip_height = dims.1.min(strip_height_without_padding); - Ok((dims.0, strip_height)) + let strip_width = if !self.expand_samples_to_bytes && self.bits_per_sample < 8 { + (dims.0 * self.bits_per_sample as u32).div_ceil(8) + } else { + dims.0 + }; + + Ok((strip_width, strip_height)) } ChunkType::Tile => { let tile_attrs = self.tile_attributes.as_ref().unwrap(); @@ -638,11 +655,13 @@ impl Image { assert!(buf.len() >= output_row_stride * (data_dims.1 as usize - 1) + data_row_bytes); let mut reader = Self::create_reader( + chunk_dims, reader, photometric_interpretation, compression_method, *compressed_bytes, self.jpeg_tables.as_deref().map(|a| &**a), + self.expand_samples_to_bytes, )?; if output_row_stride == chunk_row_bytes as usize { diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 8b5ffbc..d7f5a51 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -253,6 +253,7 @@ where ifd_offsets: Vec, seen_ifds: HashSet, image: Image, + options: DecoderOptions, } fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: usize) { @@ -420,9 +421,21 @@ fn fix_endianness(buf: &mut [u8], byte_order: ByteOrder, bit_depth: u8) { }; } +#[derive(Debug)] +pub struct DecoderOptions { + pub expand_samples_to_bytes: bool, +} + impl Decoder { + pub fn new(r: R) -> TiffResult> { + let default_options = DecoderOptions { + expand_samples_to_bytes: false, + }; + Self::new_with_options(r, default_options) + } + /// Create a new decoder that decodes from the stream ```r``` - pub fn new(mut r: R) -> TiffResult> { + pub fn new_with_options(mut r: R, options: DecoderOptions) -> TiffResult> { let mut endianess = Vec::with_capacity(2); (&mut r).take(2).read_to_end(&mut endianess)?; let byte_order = match &*endianess { @@ -493,7 +506,9 @@ impl Decoder { tile_attributes: None, chunk_offsets: Vec::new(), chunk_bytes: Vec::new(), + expand_samples_to_bytes: options.expand_samples_to_bytes, }, + options: options, }; decoder.next_image()?; Ok(decoder) @@ -545,7 +560,13 @@ impl Decoder { if let Some(ifd_offset) = self.ifd_offsets.get(ifd_index) { let (ifd, _next_ifd) = Self::read_ifd(&mut self.reader, self.bigtiff, *ifd_offset)?; - self.image = Image::from_reader(&mut self.reader, ifd, &self.limits, self.bigtiff)?; + self.image = Image::from_reader( + &mut self.reader, + ifd, + &self.limits, + self.bigtiff, + self.options.expand_samples_to_bytes, + )?; Ok(()) } else { @@ -585,7 +606,13 @@ impl Decoder { pub fn next_image(&mut self) -> TiffResult<()> { let (ifd, _next_ifd) = self.next_ifd()?; - self.image = Image::from_reader(&mut self.reader, ifd, &self.limits, self.bigtiff)?; + self.image = Image::from_reader( + &mut self.reader, + ifd, + &self.limits, + self.bigtiff, + self.options.expand_samples_to_bytes, + )?; Ok(()) } @@ -971,7 +998,7 @@ impl Decoder { fn result_buffer(&self, width: usize, height: usize) -> TiffResult { let bits_per_sample = self.image().bits_per_sample; - let row_samples = if bits_per_sample >= 8 { + let row_samples = if bits_per_sample >= 8 || self.options.expand_samples_to_bytes { width } else { ((((width as u64) * bits_per_sample as u64) + 7) / 8) @@ -985,6 +1012,7 @@ impl Decoder { .ok_or(TiffError::LimitsExceeded)?; let max_sample_bits = self.image().bits_per_sample; + match self.image().sample_format { SampleFormat::Uint => match max_sample_bits { n if n <= 8 => DecodingResult::new_u8(buffer_size, &self.limits), diff --git a/src/decoder/stream.rs b/src/decoder/stream.rs index 8a995b0..9c795f6 100644 --- a/src/decoder/stream.rs +++ b/src/decoder/stream.rs @@ -1,5 +1,10 @@ //! All IO functionality needed for TIFF decoding +use crate::TiffResult; +use bitvec::order::Msb0; +use bitvec::vec::BitVec; +use fax::decoder::Group4Decoder; +use std::collections::VecDeque; use std::io::{self, BufRead, BufReader, Read, Seek, Take}; /// Byte order of the TIFF file. @@ -252,6 +257,71 @@ impl Read for PackBitsReader { } } +pub struct Group4Reader { + decoder: Group4Decoder>>, + bits: BitVec, + byte_buf: VecDeque, + height: u16, + width: u16, + y: u16, + expand_samples_to_bytes: bool, +} + +impl Group4Reader { + pub fn new( + dimensions: (u32, u32), + reader: R, + compressed_length: u64, + expand_samples_to_bytes: bool, + ) -> TiffResult { + let width = u16::try_from(dimensions.0)?; + let height = u16::try_from(dimensions.1)?; + + Ok(Self { + decoder: Group4Decoder::new(reader.take(compressed_length).bytes(), width)?, + bits: BitVec::new(), + byte_buf: VecDeque::new(), + width: width, + height: height, + y: 0, + expand_samples_to_bytes: expand_samples_to_bytes, + }) + } +} + +impl Read for Group4Reader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if self.byte_buf.is_empty() && self.y < self.height { + let next = self + .decoder + .advance() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + + match next { + fax::decoder::DecodeStatus::End => (), + fax::decoder::DecodeStatus::Incomplete => { + self.y += 1; + let transitions = fax::decoder::pels(self.decoder.transition(), self.width); + if self.expand_samples_to_bytes { + self.byte_buf.extend(transitions.map(|c| match c { + fax::Color::Black => 0xFF, + fax::Color::White => 0x00, + })) + } else { + self.bits.extend(transitions.map(|c| match c { + fax::Color::Black => true, + fax::Color::White => false, + })); + self.byte_buf.extend(self.bits.as_raw_slice()); + self.bits.clear(); + } + } + } + } + self.byte_buf.read(buf) + } +} + /// /// ## SmartReader Reader /// diff --git a/tests/decode_images.rs b/tests/decode_images.rs index 1c769d0..c64b755 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -515,3 +515,8 @@ fn test_predictor_3_rgb_f32() { fn test_predictor_3_gray_f32() { test_image_sum_f32("predictor-3-gray-f32.tif", ColorType::Gray(32), 20008.275); } + +#[test] +fn test_fax4() { + test_image_sum_u8("fax4.tiff", ColorType::Gray(1), 7802706); +} diff --git a/tests/images/fax4.tiff b/tests/images/fax4.tiff new file mode 100644 index 0000000..e74dc8a Binary files /dev/null and b/tests/images/fax4.tiff differ