From a0191ed1a77803970fb8113658f3474b07879434 Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Thu, 29 Jan 2026 10:53:00 -0800 Subject: [PATCH] Support decoding WebP compressed TIFFs (#336) Co-authored-by: A. Molzer <5550310+197g@users.noreply.github.com> --- Cargo.toml | 2 + src/decoder/image.rs | 13 +++++- src/decoder/stream.rs | 57 ++++++++++++++++++++++++ src/tags.rs | 3 ++ tests/COPYRIGHT | 4 ++ tests/decode_geotiff_images.rs | 25 ++++++++++- tests/{images => geotiff}/geo-5b.tif | Bin tests/geotiff/usda_naip_256_webp_z3.tif | Bin 0 -> 20532 bytes 8 files changed, 101 insertions(+), 3 deletions(-) rename tests/{images => geotiff}/geo-5b.tif (100%) create mode 100644 tests/geotiff/usda_naip_256_webp_z3.tif diff --git a/Cargo.toml b/Cargo.toml index c07f807..b2b6f0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ flate2 = { version = "1.0.20", optional = true } weezl = { version = "0.1.10", optional = true } zstd = { version = "0.13", optional = true } zune-jpeg = { version = "0.5.1", optional = true } +image-webp = {version = "0.2.4", optional = true } [dev-dependencies] criterion = "0.3.1" @@ -40,6 +41,7 @@ deflate = ["dep:flate2"] fax = ["dep:fax34"] jpeg = ["dep:zune-jpeg"] lzw = ["dep:weezl"] +webp = ["dep:image-webp"] zstd = ["dep:zstd"] [[bench]] diff --git a/src/decoder/image.rs b/src/decoder/image.rs index a1bda19..5330328 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -526,13 +526,14 @@ impl Image { } } - fn create_reader<'r, R: 'r + Read>( + fn create_reader<'r, R: 'r + Read + Seek>( reader: R, compression_method: CompressionMethod, compressed_length: u64, // FIXME: these should be `expect` attributes or we choose another way of passing them. #[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>, #[cfg_attr(not(feature = "fax"), allow(unused_variables))] dimensions: (u32, u32), + #[cfg_attr(not(feature = "webp"), allow(unused_variables))] samples: u16, ) -> TiffResult> { Ok(match compression_method { CompressionMethod::None => Box::new(reader), @@ -607,6 +608,13 @@ impl Image { reader, compressed_length, )?), + #[cfg(feature = "webp")] + CompressionMethod::WebP => Box::new(super::stream::WebPReader::new( + reader, + compressed_length, + samples, + )?), + method => { return Err(TiffError::UnsupportedError( TiffUnsupportedError::UnsupportedCompressionMethod(method), @@ -805,7 +813,7 @@ impl Image { pub(crate) fn expand_chunk( &self, - reader: &mut ValueReader, + reader: &mut ValueReader, buf: &mut [u8], layout: &ReadoutLayout, chunk_index: u32, @@ -919,6 +927,7 @@ impl Image { *compressed_bytes, self.jpeg_tables.as_deref().map(|a| &**a), chunk_dims, + self.samples, )?; if is_output_chunk_rows && is_all_bits { diff --git a/src/decoder/stream.rs b/src/decoder/stream.rs index 6e41a9e..e12ac4e 100644 --- a/src/decoder/stream.rs +++ b/src/decoder/stream.rs @@ -1,4 +1,6 @@ //! All IO functionality needed for TIFF decoding +#[cfg(feature = "webp")] +use std::io::Cursor; use std::io::{self, BufRead, BufReader, Read, Seek, Take}; pub use crate::tags::ByteOrder; @@ -354,6 +356,61 @@ impl Read for Group4Reader { } } +#[cfg(feature = "webp")] +pub struct WebPReader { + inner: Cursor>, +} + +#[cfg(feature = "webp")] +impl WebPReader { + pub fn new( + reader: R, + compressed_length: u64, + samples: u16, + ) -> crate::TiffResult { + let mut decoder = + image_webp::WebPDecoder::new(io::BufReader::new(reader.take(compressed_length))) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + if !(samples == 4 || (samples == 3 && !decoder.has_alpha())) { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "bad sample count for WebP compressed data", + ) + .into()); + } + + let total_bytes = + samples as usize * decoder.dimensions().0 as usize * decoder.dimensions().1 as usize; + let mut data = vec![0; total_bytes]; + + decoder + .read_image(&mut data) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + // Add a fully opaque alpha channel if needed + if samples == 4 && !decoder.has_alpha() { + for i in (0..(total_bytes / 4)).rev() { + data[i * 4 + 3] = 255; + data[i * 4 + 2] = data[i * 3 + 2]; + data[i * 4 + 1] = data[i * 3 + 1]; + data[i * 4] = data[i * 3]; + } + } + + Ok(Self { + inner: Cursor::new(data), + }) + } +} + +#[cfg(feature = "webp")] +impl Read for WebPReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.inner.read(buf) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/src/tags.rs b/src/tags.rs index 2633879..245d203 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -267,6 +267,9 @@ pub enum CompressionMethod(u16) unknown( // Self-assigned by libtiff ZSTD = 0xC350, + + // Self-assigned by libtiff + WebP = 0xC351, } } diff --git a/tests/COPYRIGHT b/tests/COPYRIGHT index 064bfe0..0d8a28a 100644 --- a/tests/COPYRIGHT +++ b/tests/COPYRIGHT @@ -2,3 +2,7 @@ subsubifd.tif: url: https://data.kitware.com/api/v1/file/hashsum/sha512/372ca32735c8a8fdbe2286dabead9e779da63f10ba81eda84625e5273f76d74ca1a47a978f67e9c00c12f7f72009c7b2c07a641e643bb0c463812f4ae7f15d6e/download credit: https://github.com/DigitalSlideArchive/tifftools/commit/4d00c70dbce828262f86c1431f7c66b1748965ac license: CC0 +usda/*.tif + url: https://registry.opendata.aws/naip/index.html + credit: NAIP on AWS was accessed on 2026-01-28 from https://registry.opendata.aws/naip. + license: Public Domain diff --git a/tests/decode_geotiff_images.rs b/tests/decode_geotiff_images.rs index 92ef748..88edf0b 100644 --- a/tests/decode_geotiff_images.rs +++ b/tests/decode_geotiff_images.rs @@ -7,7 +7,7 @@ use tiff::ColorType; use std::fs::File; use std::path::PathBuf; -const TEST_IMAGE_DIR: &str = "./tests/images"; +const TEST_IMAGE_DIR: &str = "./tests/geotiff"; #[test] fn test_geo_tiff() { @@ -61,3 +61,26 @@ fn test_geo_tiff() { assert_eq!(data.len(), 500); } } + +#[cfg(feature = "webp")] +#[test] +fn test_webp_tiff() { + let filenames = ["usda_naip_256_webp_z3.tif"]; + let mut buffer = DecodingResult::U8(vec![]); + + for filename in filenames.iter() { + let path = PathBuf::from(TEST_IMAGE_DIR).join(filename); + let img_file = File::open(path).expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + + loop { + decoder.read_image_to_buffer(&mut buffer).unwrap(); + + if !decoder.more_images() { + break; + } + + decoder.next_image().unwrap(); + } + } +} diff --git a/tests/images/geo-5b.tif b/tests/geotiff/geo-5b.tif similarity index 100% rename from tests/images/geo-5b.tif rename to tests/geotiff/geo-5b.tif diff --git a/tests/geotiff/usda_naip_256_webp_z3.tif b/tests/geotiff/usda_naip_256_webp_z3.tif new file mode 100644 index 0000000000000000000000000000000000000000..f3d0657c152f35ef79753adc2e2803f8f131229e GIT binary patch literal 20532 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

KGfE{@t6%R~!KMrm6HEiv0~#>l_cd=3Y=$bN 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 b_QLMN0~XMq&pZ(Q#~%8}9{QhlL+Sqk(LGVW literal 0 HcmV?d00001