From efb23b648ae9626165f8c0ae2528420a3cfe4ca7 Mon Sep 17 00:00:00 2001 From: Stephen Judkins Date: Fri, 5 Apr 2024 11:57:22 -0700 Subject: [PATCH 01/12] CCITT group 4 (Fax4) decoding support --- Cargo.toml | 1 + src/decoder/image.rs | 20 +++++++++++++++++++- tests/decode_images.rs | 5 +++++ tests/images/fax4.tiff | Bin 0 -> 33630 bytes 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 tests/images/fax4.tiff diff --git a/Cargo.toml b/Cargo.toml index baf432fd..64593f04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ 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" +fax = { path = "../fax" } [dev-dependencies] criterion = "0.3.1" diff --git a/src/decoder/image.rs b/src/decoder/image.rs index af206787..3ce4f66c 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -7,6 +7,7 @@ use crate::tags::{ CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag, }; use crate::{ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError}; +use fax; use std::io::{self, Cursor, Read, Seek}; use std::sync::Arc; @@ -368,6 +369,7 @@ impl Image { } fn create_reader<'r, R: 'r + Read>( + &self, reader: R, photometric_interpretation: PhotometricInterpretation, compression_method: CompressionMethod, @@ -447,6 +449,22 @@ impl Image { Box::new(Cursor::new(data)) } + CompressionMethod::Fax4 => { + let width = u16::try_from(self.width)?; + let height = u16::try_from(self.height)?; + let mut out: Vec = Vec::with_capacity(usize::from(width) * usize::from(height)); + + let mut buffer = Vec::with_capacity(usize::try_from(compressed_length)?); + reader.take(compressed_length).read_to_end(&mut buffer)?; + + fax::decoder::decode_g4(buffer.into_iter(), width, Some(height), |transitions| { + out.extend(fax::decoder::pels(transitions, width).map(|c| match c { + fax::Color::Black => 255, // fax4-encoded images in tiff files appear to be inverted from those in PDFs + fax::Color::White => 0, + })) + }); + Box::new(Cursor::new(out)) + } method => { return Err(TiffError::UnsupportedError( TiffUnsupportedError::UnsupportedCompressionMethod(method), @@ -633,7 +651,7 @@ impl Image { let padding_right = chunk_dims.0 - data_dims.0; - let mut reader = Self::create_reader( + let mut reader = self.create_reader( reader, photometric_interpretation, compression_method, diff --git a/tests/decode_images.rs b/tests/decode_images.rs index 77f48e9d..50d8141d 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -510,3 +510,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), 62384985); +} diff --git a/tests/images/fax4.tiff b/tests/images/fax4.tiff new file mode 100644 index 0000000000000000000000000000000000000000..e74dc8ae72df1ed01373a3f94ede4a6e4e86cc2d GIT binary patch literal 33630 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 Mf7K}-o}?ZAADa92ga7~l literal 0 HcmV?d00001 From ec5a2e6c5701a31ef9f54fd527469760e8c70157 Mon Sep 17 00:00:00 2001 From: Stephen Judkins Date: Fri, 5 Apr 2024 14:41:26 -0700 Subject: [PATCH 02/12] update fax dependency --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 64593f04..e7091354 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ 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" -fax = { path = "../fax" } +fax = "0.2.2" [dev-dependencies] criterion = "0.3.1" From e3900f11cf6546535426a2960dbf7b5daaccfae8 Mon Sep 17 00:00:00 2001 From: Stephen Judkins Date: Fri, 5 Apr 2024 14:41:37 -0700 Subject: [PATCH 03/12] Leave comment explaining the situation --- src/decoder/image.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 3ce4f66c..c0210efa 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -457,9 +457,12 @@ impl Image { let mut buffer = Vec::with_capacity(usize::try_from(compressed_length)?); reader.take(compressed_length).read_to_end(&mut buffer)?; + // all extant tiff/fax4 decoders I've found always assume that the photometric interpretation + // is `WhiteIsZero`, ignoring the tag. ImageMagick appears to generate fax4-encoded tiffs + // with the tag incorrectly set to `BlackIsZero`. fax::decoder::decode_g4(buffer.into_iter(), width, Some(height), |transitions| { out.extend(fax::decoder::pels(transitions, width).map(|c| match c { - fax::Color::Black => 255, // fax4-encoded images in tiff files appear to be inverted from those in PDFs + fax::Color::Black => 255, fax::Color::White => 0, })) }); From f475d6dff5ffbb418bb606281a3df67c501b873d Mon Sep 17 00:00:00 2001 From: Stephen Judkins Date: Mon, 8 Apr 2024 13:07:47 -0700 Subject: [PATCH 04/12] review changes: pass in width/height, unwrap reader bytes --- src/decoder/image.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/decoder/image.rs b/src/decoder/image.rs index c0210efa..2c393155 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -369,7 +369,8 @@ impl Image { } fn create_reader<'r, R: 'r + Read>( - &self, + width: u32, + height: u32, reader: R, photometric_interpretation: PhotometricInterpretation, compression_method: CompressionMethod, @@ -450,17 +451,14 @@ impl Image { Box::new(Cursor::new(data)) } CompressionMethod::Fax4 => { - let width = u16::try_from(self.width)?; - let height = u16::try_from(self.height)?; + let width = u16::try_from(width)?; + let height = u16::try_from(height)?; let mut out: Vec = Vec::with_capacity(usize::from(width) * usize::from(height)); - let mut buffer = Vec::with_capacity(usize::try_from(compressed_length)?); - reader.take(compressed_length).read_to_end(&mut buffer)?; - // all extant tiff/fax4 decoders I've found always assume that the photometric interpretation // is `WhiteIsZero`, ignoring the tag. ImageMagick appears to generate fax4-encoded tiffs // with the tag incorrectly set to `BlackIsZero`. - fax::decoder::decode_g4(buffer.into_iter(), width, Some(height), |transitions| { + fax::decoder::decode_g4(reader.bytes().map(|b| b.unwrap() ), width, Some(height), |transitions| { out.extend(fax::decoder::pels(transitions, width).map(|c| match c { fax::Color::Black => 255, fax::Color::White => 0, @@ -654,7 +652,9 @@ impl Image { let padding_right = chunk_dims.0 - data_dims.0; - let mut reader = self.create_reader( + let mut reader = Self::create_reader( + self.width, + self.height, reader, photometric_interpretation, compression_method, From fbe26e98889e3f7bc1760631c95ffeda982c4251 Mon Sep 17 00:00:00 2001 From: Stephen Judkins Date: Mon, 8 Apr 2024 13:08:20 -0700 Subject: [PATCH 05/12] format --- src/decoder/image.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 2c393155..6035aec5 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -458,12 +458,17 @@ impl Image { // all extant tiff/fax4 decoders I've found always assume that the photometric interpretation // is `WhiteIsZero`, ignoring the tag. ImageMagick appears to generate fax4-encoded tiffs // with the tag incorrectly set to `BlackIsZero`. - fax::decoder::decode_g4(reader.bytes().map(|b| b.unwrap() ), width, Some(height), |transitions| { - out.extend(fax::decoder::pels(transitions, width).map(|c| match c { - fax::Color::Black => 255, - fax::Color::White => 0, - })) - }); + fax::decoder::decode_g4( + reader.bytes().map(|b| b.unwrap()), + width, + Some(height), + |transitions| { + out.extend(fax::decoder::pels(transitions, width).map(|c| match c { + fax::Color::Black => 255, + fax::Color::White => 0, + })) + }, + ); Box::new(Cursor::new(out)) } method => { From de076fad39b3571eec1e686218b0ab95d84a462c Mon Sep 17 00:00:00 2001 From: Stephen Judkins Date: Tue, 9 Apr 2024 15:25:42 -0700 Subject: [PATCH 06/12] `Reader` instance for group 4-decoded images, that buffer one line at a time --- Cargo.toml | 2 +- src/decoder/image.rs | 74 ++++++++++++++++++++++++++++++-------------- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e7091354..57c35274 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ 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" -fax = "0.2.2" +fax = { git = "https://github.com/stephenjudkins/fax.git", rev = "7dde1564500cfc046f0489a87761a5d9111fc205" } [dev-dependencies] criterion = "0.3.1" diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 6035aec5..9fc2647e 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -8,6 +8,9 @@ use crate::tags::{ }; use crate::{ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError}; use fax; +use fax::decoder::Group4Decoder; +use std::borrow::Borrow; +use std::collections::VecDeque; use std::io::{self, Cursor, Read, Seek}; use std::sync::Arc; @@ -369,8 +372,7 @@ impl Image { } fn create_reader<'r, R: 'r + Read>( - width: u32, - height: u32, + dimensions: (u32, u32), reader: R, photometric_interpretation: PhotometricInterpretation, compression_method: CompressionMethod, @@ -451,25 +453,52 @@ impl Image { Box::new(Cursor::new(data)) } CompressionMethod::Fax4 => { - let width = u16::try_from(width)?; - let height = u16::try_from(height)?; - let mut out: Vec = Vec::with_capacity(usize::from(width) * usize::from(height)); - - // all extant tiff/fax4 decoders I've found always assume that the photometric interpretation - // is `WhiteIsZero`, ignoring the tag. ImageMagick appears to generate fax4-encoded tiffs - // with the tag incorrectly set to `BlackIsZero`. - fax::decoder::decode_g4( - reader.bytes().map(|b| b.unwrap()), - width, - Some(height), - |transitions| { - out.extend(fax::decoder::pels(transitions, width).map(|c| match c { - fax::Color::Black => 255, - fax::Color::White => 0, - })) - }, - ); - Box::new(Cursor::new(out)) + let width = u16::try_from(dimensions.0)?; + let height = u16::try_from(dimensions.1)?; + + struct Group4Reader { + decoder: fax::decoder::Group4Decoder>, + line_buf: VecDeque, + y: u16, + height: u16, + width: u16, + } + + impl Read for Group4Reader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if self.line_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; + self.line_buf.extend( + // all extant tiff/fax4 decoders I've found always assume that the photometric interpretation + // is `WhiteIsZero`, ignoring the tag. ImageMagick appears to generate fax4-encoded tiffs + // with the tag incorrectly set to `BlackIsZero`. + fax::decoder::pels(self.decoder.transition(), self.width) + .map(|c| match c { + fax::Color::Black => 255, + fax::Color::White => 0, + }), + ); + } + } + } + self.line_buf.read(buf) + } + } + + Box::new(Group4Reader { + decoder: fax::decoder::Group4Decoder::new(reader.bytes(), width)?, + line_buf: VecDeque::with_capacity(usize::from(width)), + y: 0, + width: width, + height: height, + }) } method => { return Err(TiffError::UnsupportedError( @@ -658,8 +687,7 @@ impl Image { let padding_right = chunk_dims.0 - data_dims.0; let mut reader = Self::create_reader( - self.width, - self.height, + chunk_dims, reader, photometric_interpretation, compression_method, From 2cfe50f33eb46e107ef7ccf7684711c3b25a92a4 Mon Sep 17 00:00:00 2001 From: Stephen Judkins Date: Tue, 9 Apr 2024 15:29:38 -0700 Subject: [PATCH 07/12] decode only explicit number of bytes --- src/decoder/image.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 9fc2647e..a12cf6fb 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -493,7 +493,7 @@ impl Image { } Box::new(Group4Reader { - decoder: fax::decoder::Group4Decoder::new(reader.bytes(), width)?, + decoder: fax::decoder::Group4Decoder::new(reader.take(compressed_length).bytes(), width)?, line_buf: VecDeque::with_capacity(usize::from(width)), y: 0, width: width, From d6ab46b1f8e64159d09d1bf8d8c7ca549cdc61f5 Mon Sep 17 00:00:00 2001 From: Stephen Judkins Date: Tue, 9 Apr 2024 23:02:19 -0700 Subject: [PATCH 08/12] bit-addressable output (doesn't work yet) --- Cargo.toml | 1 + src/decoder/image.rs | 54 ++++++++++++++++++++++++++++---------------- src/decoder/mod.rs | 5 +++- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 57c35274..a34274cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ weezl = "0.1.0" jpeg = { package = "jpeg-decoder", version = "0.3.0", default-features = false } flate2 = "1.0.20" fax = { git = "https://github.com/stephenjudkins/fax.git", rev = "7dde1564500cfc046f0489a87761a5d9111fc205" } +bitvec = "1.0.1" [dev-dependencies] criterion = "0.3.1" diff --git a/src/decoder/image.rs b/src/decoder/image.rs index a12cf6fb..df540640 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -7,11 +7,12 @@ use crate::tags::{ CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag, }; use crate::{ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError}; +use bitvec; use fax; use fax::decoder::Group4Decoder; use std::borrow::Borrow; use std::collections::VecDeque; -use std::io::{self, Cursor, Read, Seek}; +use std::io::{self, Cursor, Read, Seek, Write}; use std::sync::Arc; #[derive(Debug)] @@ -458,7 +459,8 @@ impl Image { struct Group4Reader { decoder: fax::decoder::Group4Decoder>, - line_buf: VecDeque, + bits: bitvec::vec::BitVec, + byte_buf: VecDeque, y: u16, height: u16, width: u16, @@ -466,35 +468,47 @@ impl Image { impl Read for Group4Reader { fn read(&mut self, buf: &mut [u8]) -> io::Result { - if self.line_buf.is_empty() && self.y < self.height { - let next = self.decoder.advance().map_err(|e| { - std::io::Error::new(std::io::ErrorKind::Other, e) - })?; - + 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::End => { + // eprintln!("DONE!"); + self.byte_buf.extend(self.bits.as_raw_slice()) + } fax::decoder::DecodeStatus::Incomplete => { self.y += 1; - self.line_buf.extend( - // all extant tiff/fax4 decoders I've found always assume that the photometric interpretation - // is `WhiteIsZero`, ignoring the tag. ImageMagick appears to generate fax4-encoded tiffs - // with the tag incorrectly set to `BlackIsZero`. + // eprintln!("{:?}", self.y); + for c in fax::decoder::pels(self.decoder.transition(), self.width) - .map(|c| match c { - fax::Color::Black => 255, - fax::Color::White => 0, - }), - ); + { + self.bits.push(match c { + fax::Color::Black => true, + fax::Color::White => false, + }); + if self.bits.len() == 8 { + self.byte_buf.extend(self.bits.as_raw_slice()); + self.bits.clear() + } + } } } } - self.line_buf.read(buf) + // eprintln!("{:?}: {:?} / {:?}", self.y, self.byte_buf.len(), self.bits.len()); + + self.byte_buf.read(buf) } } Box::new(Group4Reader { - decoder: fax::decoder::Group4Decoder::new(reader.take(compressed_length).bytes(), width)?, - line_buf: VecDeque::with_capacity(usize::from(width)), + decoder: fax::decoder::Group4Decoder::new( + reader.take(compressed_length).bytes(), + width, + )?, + bits: bitvec::vec::BitVec::new(), + byte_buf: VecDeque::new(), y: 0, width: width, height: height, diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 574caff7..9911faef 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -1066,7 +1066,10 @@ impl Decoder { 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), + n if n < 8 => { + DecodingResult::new_u8(buffer_size / 8 * usize::from(n), &self.limits) + } + 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), From 6db153fd8d6cd689436a777aa9837d5fee086057 Mon Sep 17 00:00:00 2001 From: Stephen Judkins Date: Fri, 1 Nov 2024 14:21:47 -0700 Subject: [PATCH 09/12] Support both bit- and byte-addressed output based on config --- Cargo.toml | 2 +- src/decoder/image.rs | 105 +++++++++++++---------------------------- src/decoder/mod.rs | 48 +++++++++++++++---- src/decoder/stream.rs | 74 +++++++++++++++++++++++++++++ tests/decode_images.rs | 2 +- 5 files changed, 149 insertions(+), 82 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a34274cb..f905607d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +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" -fax = { git = "https://github.com/stephenjudkins/fax.git", rev = "7dde1564500cfc046f0489a87761a5d9111fc205" } 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 df540640..07cbe65c 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::{fp_predict_f32, fp_predict_f64, DecodingBuffer, Limits}; use super::{stream::SmartReader, ChunkType}; @@ -7,12 +7,7 @@ use crate::tags::{ CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag, }; use crate::{ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError}; -use bitvec; -use fax; -use fax::decoder::Group4Decoder; -use std::borrow::Borrow; -use std::collections::VecDeque; -use std::io::{self, Cursor, Read, Seek, Write}; +use std::io::{self, Cursor, Read, Seek}; use std::sync::Arc; #[derive(Debug)] @@ -82,6 +77,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 { @@ -90,6 +86,7 @@ impl Image { ifd: Directory, limits: &Limits, bigtiff: bool, + expand_samples_to_bytes: bool, ) -> TiffResult { let mut tag_reader = TagReader { reader, @@ -313,6 +310,7 @@ impl Image { tile_attributes, chunk_offsets, chunk_bytes, + expand_samples_to_bytes, }) } @@ -353,7 +351,13 @@ impl Image { }, PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero => { match self.samples { - 1 => Ok(ColorType::Gray(self.bits_per_sample)), + 1 => { + if self.bits_per_sample < 8 && self.expand_samples_to_bytes { + Ok(ColorType::Gray(8)) + } else { + Ok(ColorType::Gray(self.bits_per_sample)) + } + } _ => Ok(ColorType::Multiband { bit_depth: self.bits_per_sample, num_samples: self.samples, @@ -379,6 +383,7 @@ impl Image { 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), @@ -453,67 +458,12 @@ impl Image { Box::new(Cursor::new(data)) } - CompressionMethod::Fax4 => { - let width = u16::try_from(dimensions.0)?; - let height = u16::try_from(dimensions.1)?; - - struct Group4Reader { - decoder: fax::decoder::Group4Decoder>, - bits: bitvec::vec::BitVec, - byte_buf: VecDeque, - y: u16, - height: u16, - width: u16, - } - - 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 => { - // eprintln!("DONE!"); - self.byte_buf.extend(self.bits.as_raw_slice()) - } - fax::decoder::DecodeStatus::Incomplete => { - self.y += 1; - // eprintln!("{:?}", self.y); - for c in - fax::decoder::pels(self.decoder.transition(), self.width) - { - self.bits.push(match c { - fax::Color::Black => true, - fax::Color::White => false, - }); - if self.bits.len() == 8 { - self.byte_buf.extend(self.bits.as_raw_slice()); - self.bits.clear() - } - } - } - } - } - // eprintln!("{:?}: {:?} / {:?}", self.y, self.byte_buf.len(), self.bits.len()); - - self.byte_buf.read(buf) - } - } - - Box::new(Group4Reader { - decoder: fax::decoder::Group4Decoder::new( - reader.take(compressed_length).bytes(), - width, - )?, - bits: bitvec::vec::BitVec::new(), - byte_buf: VecDeque::new(), - y: 0, - width: width, - height: height, - }) - } + CompressionMethod::Fax4 => Box::new(Group4Reader::new( + dimensions, + reader, + compressed_length, + expand_samples_to_bytes, + )?), method => { return Err(TiffError::UnsupportedError( TiffUnsupportedError::UnsupportedCompressionMethod(method), @@ -596,7 +546,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(); @@ -698,7 +654,11 @@ impl Image { let chunk_dims = self.chunk_dimensions()?; let data_dims = self.chunk_data_dimensions(chunk_index)?; - let padding_right = chunk_dims.0 - data_dims.0; + let padding_right = if self.bits_per_sample >= 8 { + chunk_dims.0 - data_dims.0 + } else { + 0 + }; let mut reader = Self::create_reader( chunk_dims, @@ -707,6 +667,7 @@ impl Image { compression_method, *compressed_bytes, self.jpeg_tables.as_deref().map(|a| &**a), + self.expand_samples_to_bytes, )?; if output_width == data_dims.0 as usize && padding_right == 0 { @@ -744,7 +705,7 @@ impl Image { } } else { for row in 0..data_dims.1 as usize { - let row_start = row * output_width * samples; + let row_start = row * data_dims.0 as usize * samples; let row_end = row_start + data_dims.0 as usize * samples; let row = &mut buffer.as_bytes_mut()[(row_start * byte_len)..(row_end * byte_len)]; diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 9911faef..ad8bfd4c 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -305,6 +305,7 @@ where ifd_offsets: Vec, seen_ifds: HashSet, image: Image, + options: DecoderOptions, } trait Wrapping { @@ -511,9 +512,21 @@ fn fix_endianness(buf: &mut DecodingBuffer, byte_order: ByteOrder) { }; } +#[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 { @@ -584,7 +597,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) @@ -636,7 +651,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 { @@ -676,7 +697,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(()) } @@ -1055,7 +1082,15 @@ impl Decoder { } fn result_buffer(&self, width: usize, height: usize) -> TiffResult { - let buffer_size = match width + let max_sample_bits = self.image().bits_per_sample; + + let stride = if !self.options.expand_samples_to_bytes && max_sample_bits < 8 { + width.div_ceil(8) + } else { + width + }; + + let buffer_size = match stride .checked_mul(height) .and_then(|x| x.checked_mul(self.image().samples_per_pixel())) { @@ -1063,12 +1098,9 @@ impl Decoder { None => return Err(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 / 8 * usize::from(n), &self.limits) - } + n if n < 8 => DecodingResult::new_u8(buffer_size, &self.limits), 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), diff --git a/src/decoder/stream.rs b/src/decoder/stream.rs index 8a995b09..26597c62 100644 --- a/src/decoder/stream.rs +++ b/src/decoder/stream.rs @@ -1,5 +1,8 @@ //! All IO functionality needed for TIFF decoding +use crate::TiffResult; +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 +255,77 @@ impl Read for PackBitsReader { } } +pub struct Group4Reader { + decoder: Group4Decoder>>, + bits: bitvec::vec::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::vec::BitVec::new(), + byte_buf: VecDeque::new(), + width: width, + height: height, + y: 0, + expand_samples_to_bytes: expand_samples_to_bytes, + }) + } + + pub fn dump_bits(&mut self) -> () { + if self.expand_samples_to_bytes { + self.byte_buf.extend( + self.bits + .iter() + .by_vals() + .map(|b| if b { 0xFF } else { 0x00 }), + ); + } else { + self.byte_buf.extend(self.bits.as_raw_slice()); + } + self.bits.clear() + } +} + +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 => self.dump_bits(), + fax::decoder::DecodeStatus::Incomplete => { + self.y += 1; + for c in fax::decoder::pels(self.decoder.transition(), self.width) { + self.bits.push(match c { + fax::Color::Black => true, + fax::Color::White => false, + }); + } + self.dump_bits(); + } + } + } + self.byte_buf.read(buf) + } +} + /// /// ## SmartReader Reader /// diff --git a/tests/decode_images.rs b/tests/decode_images.rs index 50d8141d..92af5028 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -513,5 +513,5 @@ fn test_predictor_3_gray_f32() { #[test] fn test_fax4() { - test_image_sum_u8("fax4.tiff", ColorType::Gray(1), 62384985); + test_image_sum_u8("fax4.tiff", ColorType::Gray(1), 7802706); } From 8e0bdaacd4e65426a60027555422cd07d838de0e Mon Sep 17 00:00:00 2001 From: Stephen Judkins Date: Fri, 1 Nov 2024 14:32:55 -0700 Subject: [PATCH 10/12] post-merge cleanup --- src/decoder/image.rs | 8 +------- src/decoder/mod.rs | 5 ++--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/decoder/image.rs b/src/decoder/image.rs index d4c5f7eb..7d7f113e 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -351,13 +351,7 @@ impl Image { }, PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero => { match self.samples { - 1 => { - if self.bits_per_sample < 8 && self.expand_samples_to_bytes { - Ok(ColorType::Gray(8)) - } else { - Ok(ColorType::Gray(self.bits_per_sample)) - } - } + 1 => Ok(ColorType::Gray(self.bits_per_sample)), _ => Ok(ColorType::Multiband { bit_depth: self.bits_per_sample, num_samples: self.samples, diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 756c9d72..d7f5a51f 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -998,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) @@ -1015,8 +1015,7 @@ impl Decoder { match self.image().sample_format { SampleFormat::Uint => match max_sample_bits { - n if n < 8 => DecodingResult::new_u8(buffer_size, &self.limits), - n if n == 8 => DecodingResult::new_u8(buffer_size, &self.limits), + 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), From ce025bcf8c066e4c0fec750d374dc222d51dde95 Mon Sep 17 00:00:00 2001 From: Stephen Judkins Date: Fri, 1 Nov 2024 15:06:18 -0700 Subject: [PATCH 11/12] Simplify decoder --- src/decoder/stream.rs | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/decoder/stream.rs b/src/decoder/stream.rs index 26597c62..38b5efec 100644 --- a/src/decoder/stream.rs +++ b/src/decoder/stream.rs @@ -1,6 +1,8 @@ //! 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}; @@ -257,7 +259,6 @@ impl Read for PackBitsReader { pub struct Group4Reader { decoder: Group4Decoder>>, - bits: bitvec::vec::BitVec, byte_buf: VecDeque, height: u16, width: u16, @@ -277,7 +278,6 @@ impl Group4Reader { Ok(Self { decoder: Group4Decoder::new(reader.take(compressed_length).bytes(), width)?, - bits: bitvec::vec::BitVec::new(), byte_buf: VecDeque::new(), width: width, height: height, @@ -285,20 +285,6 @@ impl Group4Reader { expand_samples_to_bytes: expand_samples_to_bytes, }) } - - pub fn dump_bits(&mut self) -> () { - if self.expand_samples_to_bytes { - self.byte_buf.extend( - self.bits - .iter() - .by_vals() - .map(|b| if b { 0xFF } else { 0x00 }), - ); - } else { - self.byte_buf.extend(self.bits.as_raw_slice()); - } - self.bits.clear() - } } impl Read for Group4Reader { @@ -308,17 +294,25 @@ impl Read for Group4Reader { .decoder .advance() .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + match next { - fax::decoder::DecodeStatus::End => self.dump_bits(), + fax::decoder::DecodeStatus::End => (), fax::decoder::DecodeStatus::Incomplete => { self.y += 1; - for c in fax::decoder::pels(self.decoder.transition(), self.width) { - self.bits.push(match c { + 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 { + let mut bits: BitVec = BitVec::new(); + bits.extend(transitions.map(|c| match c { fax::Color::Black => true, fax::Color::White => false, - }); + })); + self.byte_buf.extend(bits.as_raw_slice()); } - self.dump_bits(); } } } From 52553711d46ae6e68ae174d9d28408d65b411bcc Mon Sep 17 00:00:00 2001 From: Stephen Judkins Date: Fri, 1 Nov 2024 15:11:27 -0700 Subject: [PATCH 12/12] don't allocate for every row --- src/decoder/stream.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/decoder/stream.rs b/src/decoder/stream.rs index 38b5efec..9c795f6c 100644 --- a/src/decoder/stream.rs +++ b/src/decoder/stream.rs @@ -259,6 +259,7 @@ impl Read for PackBitsReader { pub struct Group4Reader { decoder: Group4Decoder>>, + bits: BitVec, byte_buf: VecDeque, height: u16, width: u16, @@ -278,6 +279,7 @@ impl Group4Reader { Ok(Self { decoder: Group4Decoder::new(reader.take(compressed_length).bytes(), width)?, + bits: BitVec::new(), byte_buf: VecDeque::new(), width: width, height: height, @@ -306,12 +308,12 @@ impl Read for Group4Reader { fax::Color::White => 0x00, })) } else { - let mut bits: BitVec = BitVec::new(); - bits.extend(transitions.map(|c| match c { + self.bits.extend(transitions.map(|c| match c { fax::Color::Black => true, fax::Color::White => false, })); - self.byte_buf.extend(bits.as_raw_slice()); + self.byte_buf.extend(self.bits.as_raw_slice()); + self.bits.clear(); } } }