-
Notifications
You must be signed in to change notification settings - Fork 80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CCITT group 4 (Fax4) decoding support #229
base: master
Are you sure you want to change the base?
Conversation
Probably check |
OK. I've done some research and found that
To conform with how everything in the wild I'd argue we just keep this hardcoded the way it is? |
hahaha. Yea, I think just keep it as is. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some comments. I think might also be a missing check that fax compression is only used for bilevel images?
src/decoder/image.rs
Outdated
// 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| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you pass reader.take(compressed_length).bytes()
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be reader.take(compressed_length).bytes().map(|b| b.unwrap())
instead. Is that OK with you, in terms of
- performance impact: you'd get a branch for every single byte
- error handling: you'd get a panic instead of an
Err
, and I (a Rust neophyte) don't think there's a way around that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The bit reader in the fax decoder pulls single bytes anway. I started changing the api to accept an Iterator of Result<u8, E>. (so reader.take(compressed_bytes).bytes()
will work)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could also see if taking a BufRead
impl would improve performance over having to call into the reader for each individual byte. We're currently in the process of converting various parts of the image-rs APIs to use those over normal Read
impls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I do not think it is worth worrying too much about micro-optimizations here. It makes sense to handle malicious inputs without blowing up the heap, but in practice all of the images I'm seeing are crappy low-res scans of checks from god-knows-what device a regional bank purchased in 1993. Really doubt there are many images of this format in the wild that demand fully optimized best-case performance
src/decoder/image.rs
Outdated
let width = u16::try_from(self.width)?; | ||
let height = u16::try_from(self.height)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these be the chunk dimensions rather than the image dimensions?
src/decoder/image.rs
Outdated
CompressionMethod::Fax4 => { | ||
let width = u16::try_from(self.width)?; | ||
let height = u16::try_from(self.height)?; | ||
let mut out: Vec<u8> = Vec::with_capacity(usize::from(width) * usize::from(height)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have two concerns here:
- This is likely to cause fuzzer OOMs because just setting a few header fields is enough to cause a ~4GB allocation.
- The size calculation assumes that each pixel takes one byte, but I'd expect
Gray(1)
to mean one bit per pixel
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. Going to push back a bit on (2), but you're the library maintainer so LMK if I'm looking at this wrong. I expect that this crate reports back what the tiff file says in its header, which is Gray(1)
. Then in the outer crate (image
) we'd do this:
diff --git a/src/codecs/tiff.rs b/src/codecs/tiff.rs
index 9f4dd735..256215c5 100644
--- a/src/codecs/tiff.rs
+++ b/src/codecs/tiff.rs
@@ -53,6 +53,7 @@ where
};
let color_type = match tiff_color_type {
+ tiff::ColorType::Gray(1) => ColorType::L8,
Does that make sense? The alternatives would be to alter the following to special case this and override what's coming from the header: https://github.com/image-rs/image-tiff/blob/master/src/decoder/image.rs#L351
Alternatively we could add an extra data type to represent the actual output.
I'm OK with any of these options, just want to lay them out. What would you like as both the author/maintainer of this library and the primary consumer of its API in the image
crate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This crate should already decode Gray(1)
encoded images into packed 1-bit per sample outputs.
To support the image crate use case, I'd expect that this create should either exposes an optional flag to expand sub 8-bit channels (like PNG does), or else not perform any expansion and have code in the image crate to do the conversion from packed 1-bit per sample into L8 encoded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm working at getting packed 1-bit samples working here and, TBH, it's quite a bit of fiddly work. I've got the decoder portion outputting 1-bit samples but the rest of the crate seems to assume it's byte-addressable. I am looking at the chunking (de-chunking?) code in fn expand_chunk
and there's a lot that assumes samples are individually addressable. Should I go and start altering this to change this assumption? Is this something you could provide more guidance on?
To step back a bit: especially if the image
crate, presumably the main consumer of this, just goes and re-expands the samples back to individual bytes, how much does it offer to have this crate offer 1-bit samples? There's a pretty straightforward path to getting this working in the image
crate with this decoder without worrying about any of this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I've looked a bit further and expand_chunk
is the difficult part here.
There are three different code paths:
- chunk is as wide as image but with no padding
- there's right-padding and we're using a floating point predictor
- other cases
All of these assume individually addressable pixels. What kind of changes to these abstractions do you propose we make to get this working?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realized the reason I thought there was sub-byte sample support was because there's a stalled-out PR for it that I lost track of.
I'll hopefully have a bit more time this weekend, but briefly:
- My recollection is that TIFF packs multiple samples per byte across pixels but each row is always an integer number of bytes (i.e. it is padded out to a multiple of 8 bits).
- To handle the first case of the chunk width matching the image width, the byte layout should match what's in the file so you can just copy byte-by-byte.
- The floating point predictor implies floating point samples, which are always 16-bit, 32-bit, or 64-bit. Thus you don't have to handle it.
- The final case can only be hit for tiled images. For here, I think it would be reasonable to return an unsupported error if the tile_width * bit_depth isn't a multiple of 8. With that assumption I don't think the code should be too hard
A final note is to watch out for integer overflow when using u32
or usize
. It only takes 512 MB to store 2^32 bits, so a 32-bit system could plausibly want to decode a TIFF file containing more pixels than fit in a usize
. And even if the decoded image won't fit in RAM, we still want to be able to return a graceful error rather than panicking due to integer overflow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. I'll be away all next week but I'd like to pick this up again after that. Thanks for the context here.
src/decoder/image.rs
Outdated
@@ -368,6 +369,7 @@ impl Image { | |||
} | |||
|
|||
fn create_reader<'r, R: 'r + Read>( | |||
&self, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please pass in the relevant width/height as arguments rather than taking self
Ideally, I'd like to see the |
I don't see a reason why incremental decoding could not be implemented. It would be pretty easy to write something like
|
Yeah, I think an interface like that would work. Then it could be wrapped in another object that implemented |
Appreciate the feedback and all the back-and-forth here. I'm happy to help out as much as I can here to push this forward, but I don't want to step on @s3bk's feet if he's working on these changes. |
@stephenjudkins I pushed changes to the fax repo. There are strange differences with the last line again. I need to re-encode my samples with libtiff to check if the error is in the sample data or my code. But you should be able to use the code to adapt this PR to the next version. |
What is the state of this? Anything one can do? |
I don't know, but from what a I remember, the fax tests are all passing now. |
Seems that something with the |
Sorry! I dropped this but would like to pick it back up. Can we confirm that the upstream |
src/decoder/image.rs
Outdated
let width = u16::try_from(dimensions.0)?; | ||
let height = u16::try_from(dimensions.1)?; | ||
|
||
struct Group4Reader<R2> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to see this moved into decoder/stream.rs alongside the other decompressors
src/decoder/mod.rs
Outdated
@@ -1066,7 +1066,10 @@ impl<R: Read + Seek> Decoder<R> { | |||
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm slightly suspicious that this will round incorrectly if the buffer_size isn't a multiple of 8
OK, I've made some changes, and I've gotta say...I'm putting in a lot of work to support 1-bit samples, both here and in the If you feel strongly on keeping the 1-bit image option I'll add the flag but there are a lot of edge cases I'm pretty worried about testing. Are you sure that you want this? It's frustrating to write a bunch of code that I suspect will never be used by anyone and might introduce bugs into other code paths. |
@stephenjudkins I assume that refers to images like this, with 1 bit per sample?: If so, that is exactly what I am looking for... |
Files are decoded and encoded correctly, but bit-wise verification fails for some files that have a strange EOF. |
OK. I'm still working on this, and, since this code does not currently support < 8 bit samples it's introducing some significant and pervasive changes across several code paths that I don't reasonably expect to be able to test. Would you like to help me by finding some other files that could test this? Do you know of any 1-bit tile-based (instead of strip-based) files I could check? |
@stephenjudkins I made a fix to the sub 8-bpp decoding in #252. It should now be working. If you want to make test images, you can use gdal_translate:
For instance, this is a fax4 compressed version of the test image from that other PR: tiled-gray-i1-fax4.tif.zip (inside a ZIP archive because GitHub doesn't allow uploading TIFF files) |
# Conflicts: # src/decoder/image.rs # src/decoder/mod.rs
Alright, I've added support for bit-sized samples as well as an optional flag (a la the PNG crate) to expand these to 8bpp. I've confirmed in the upstream |
See draft PR https://github.com/image-rs/image/pull/2377/files |
This adds |
This adds support for decoding CCIT group 4 tiff images by using the
fax
crate.I've found that the photometric interpretation tag is ignored by all the extant decoders I've found, and attempting to correctly interpret it will break some subset of images. Unfortunate, but I suspect it's best to follow the herd here?