diff --git a/src/decoder/stream.rs b/src/decoder/stream.rs index 01dc70a..29535f2 100644 --- a/src/decoder/stream.rs +++ b/src/decoder/stream.rs @@ -141,14 +141,32 @@ pub struct LZWReader { impl LZWReader { /// Wraps a reader pub fn new(reader: R, compressed_length: usize) -> LZWReader { - let configuration = + let mut buffered = BufReader::with_capacity( + (32 * 1024).min(compressed_length), + reader.take(u64::try_from(compressed_length).unwrap()), + ); + + // `Compression = 5` covers both the current LZW variant and the pre-TIFF-6.0 + // "old-style" one, with no tag to distinguish them. New-style is MSB-first with + // the "early change" code-size switch, and its stream begins with the Clear code + // (256) -> first byte 0x80. Old-style is LSB-first *without* the early change + // (GIF-style increment timing), and begins with 0x00 followed by a byte with bit + // 0 set. This is the heuristic libtiff uses to select its compatibility decoder; + // on a short/failed peek we default to the modern variant. + let old_style = matches!( + buffered.fill_buf(), + Ok(head) if head.len() >= 2 && head[0] == 0x00 && head[1] & 0x01 != 0 + ); + + let configuration = if old_style { + weezl::decode::Configuration::new(weezl::BitOrder::Lsb, 8) + } else { weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8) - .with_yield_on_full_buffer(true); + } + .with_yield_on_full_buffer(true); + Self { - reader: BufReader::with_capacity( - (32 * 1024).min(compressed_length), - reader.take(u64::try_from(compressed_length).unwrap()), - ), + reader: buffered, decoder: configuration.build(), } } diff --git a/tests/decode_images.rs b/tests/decode_images.rs index 619fc37..babd054 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -1060,6 +1060,16 @@ fn test_gray_u12_hpredict() { test_image_sum_u16("hpredict-1c-12b.tiff", ColorType::Gray(12), 11000); } +// ---- Old-style LZW ---- + +#[test] +fn test_old_style_lzw() { + // Pre-TIFF-6.0 "old-style" LZW: LSB-first bit order with no early-change size + // switch. Auto-detected from the stream (first byte 0x00, second byte odd). + // Same image as libtiffpic's quad-lzw.tif; the decoded pixels must match. + test_image_sum_u8("quad-lzw-compat.tiff", ColorType::RGB(8), 21048924); +} + // ---- TransparencyMask ---- /// Build a minimal uncompressed 2x2 TIFF in memory, in the requested byte order, diff --git a/tests/decode_libtiffpic.rs b/tests/decode_libtiffpic.rs index 05097f3..18032fa 100644 --- a/tests/decode_libtiffpic.rs +++ b/tests/decode_libtiffpic.rs @@ -190,7 +190,7 @@ const FILES: &[(&str, Option)] = &[ ("tests/libtiffpic/oxford.tif", Some(0x355d2e3e)), ("tests/libtiffpic/pc260001.tif", Some(0xdb56b753)), ("tests/libtiffpic/quad-jpeg.tif", Some(0xcba22475)), - ("tests/libtiffpic/quad-lzw.tif", Some(0)), // invalid LZW stream + ("tests/libtiffpic/quad-lzw.tif", Some(0x6055f9ee)), // old-style (LSB) LZW ("tests/libtiffpic/quad-tile.tif", Some(0x6055f9ee)), ("tests/libtiffpic/README", None), ("tests/libtiffpic/smallliz.tif", Some(0)), // unsupported YCbCr chroma subsampling