diff --git a/Cargo.toml b/Cargo.toml index c07f807f..b2b6f0d5 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 a1bda194..53303283 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 6e41a9ea..e12ac4e8 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 26338793..245d203c 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 064bfe06..0d8a28af 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 92ef7482..88edf0be 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 00000000..f3d0657c Binary files /dev/null and b/tests/geotiff/usda_naip_256_webp_z3.tif differ