Skip to content

Commit 12c9853

Browse files
authored
Merge pull request #297 from image-rs/fax4-photometric-interpretation
Verify WhiteIsZero invert, add Gray(1) color type
2 parents e67bac7 + 1556713 commit 12c9853

File tree

6 files changed

+55
-8
lines changed

6 files changed

+55
-8
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ half = { version = "2.4.1" }
2121
quick-error = "2.0.1"
2222

2323
# Rename to avoid confusion with the feature name.
24-
fax34 = { package = "fax", version = "0.2.4", optional = true }
24+
fax34 = { package = "fax", version = "0.2.6", optional = true }
2525
flate2 = { version = "1.0.20", optional = true }
2626
weezl = { version = "0.1.10", optional = true }
2727
zstd = { version = "0.13", optional = true }

src/decoder/image.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,9 @@ impl Image {
344344
),
345345
)),
346346
},
347+
// TODO: treatment of WhiteIsZero is not quite consistent with `invert_colors` that is
348+
// later called when that interpretation is read. That function does not support
349+
// Multiband as a color type and will error. It's unclear how to resolve that exactly.
347350
PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero => {
348351
match self.samples {
349352
1 => Ok(ColorType::Gray(self.bits_per_sample)),
@@ -684,7 +687,7 @@ impl Image {
684687
);
685688
}
686689
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
687-
super::invert_colors(tile, color_type, self.sample_format);
690+
super::invert_colors(tile, color_type, self.sample_format)?;
688691
}
689692
} else if chunk_row_bytes > data_row_bytes && self.predictor == Predictor::FloatingPoint {
690693
// The floating point predictor shuffles the padding bytes into the encoded output, so
@@ -701,7 +704,7 @@ impl Image {
701704
_ => unreachable!(),
702705
}
703706
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
704-
super::invert_colors(row, color_type, self.sample_format);
707+
super::invert_colors(row, color_type, self.sample_format)?;
705708
}
706709
}
707710
} else {
@@ -723,7 +726,7 @@ impl Image {
723726
predictor,
724727
);
725728
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
726-
super::invert_colors(row, color_type, self.sample_format);
729+
super::invert_colors(row, color_type, self.sample_format)?;
727730
}
728731
}
729732
}

src/decoder/mod.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -475,11 +475,22 @@ fn fix_endianness_and_predict(
475475
}
476476
}
477477

478-
fn invert_colors(buf: &mut [u8], color_type: ColorType, sample_format: SampleFormat) {
478+
fn invert_colors(
479+
buf: &mut [u8],
480+
color_type: ColorType,
481+
sample_format: SampleFormat,
482+
) -> TiffResult<()> {
479483
match (color_type, sample_format) {
480-
(ColorType::Gray(8), SampleFormat::Uint) => {
484+
// Where pixels do not cross a byte boundary
485+
(ColorType::Gray(1 | 2 | 4 | 8), SampleFormat::Uint) => {
481486
for x in buf {
482-
*x = 0xff - *x;
487+
// Equivalent to both of the following:
488+
//
489+
// *x = 0xff - *x
490+
// *x = !*x
491+
//
492+
// since -x = !x+1
493+
*x = !*x;
483494
}
484495
}
485496
(ColorType::Gray(16), SampleFormat::Uint) => {
@@ -512,8 +523,14 @@ fn invert_colors(buf: &mut [u8], color_type: ColorType, sample_format: SampleFor
512523
x.copy_from_slice(&(1.0 - v).to_ne_bytes());
513524
}
514525
}
515-
_ => {}
526+
_ => {
527+
return Err(TiffError::UnsupportedError(
528+
TiffUnsupportedError::UnknownInterpretation,
529+
))
530+
}
516531
}
532+
533+
Ok(())
517534
}
518535

519536
/// Fix endianness. If `byte_order` matches the host, then conversion is a no-op.

src/decoder/stream.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,13 +322,34 @@ impl<R: Read> Read for Group4Reader<R> {
322322
self.y += 1;
323323

324324
// We known `transitions` yields exactly `self.width` items (per doc).
325+
// FIXME: performance. We do not need an individual pixel iterator, filling
326+
// memory especially for long runs can be much quicker. The `transitions` are
327+
// the positions at which each run-length ends, i.e. the prefix sum of run
328+
// lengths. Runs in fax4 start with white.
325329
let transitions = fax34::decoder::pels(self.decoder.transition(), self.width);
326330

327331
let buffer = self.line_buf.get_mut();
328332
buffer.resize(usize::from(self.width).div_ceil(8), 0u8);
329333

330334
let target = &mut buffer[..];
331335

336+
// Note: it may seem strange to treat black as 0b1 and white as 0b0 despite all
337+
// our streams by default decoding as-if PhotometricInterpretation::BlackIsMin.
338+
// This is however consistent with libtiff. It seems that fax4's "White"
339+
// differs from what libtiff thinks of as "White". For content, a line of data
340+
// is in runlength encoding of white-black-white-black always starting with
341+
// white. In libtiff, the loop always does both colors in one go and the
342+
// structure is:
343+
//
344+
// ```
345+
// for (; runs < erun; runs += 2)
346+
// // white run
347+
// do { *lp++ = 0L; } while (…)
348+
// // black run
349+
// do { *lp++ = -1L; } while (…)
350+
// ```
351+
//
352+
// So indeed the Fax4::White run is implemented by filling with zeros.
332353
let mut bits = transitions.map(|c| match c {
333354
fax34::Color::Black => true,
334355
fax34::Color::White => false,

tests/decode_images.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,3 +678,9 @@ fn bytes_gray_f32() {
678678
fn test_fax4() {
679679
test_image_sum_u8("fax4.tiff", ColorType::Gray(1), 7802706);
680680
}
681+
682+
#[test]
683+
#[cfg(feature = "fax")]
684+
fn test_fax4_white_is_min() {
685+
test_image_sum_u8("imagemagick_group4.tiff", ColorType::Gray(1), 3742820);
686+
}
1.19 KB
Binary file not shown.

0 commit comments

Comments
 (0)