Skip to content

Commit a0191ed

Browse files
fintelia197g
andauthored
Support decoding WebP compressed TIFFs (#336)
Co-authored-by: A. Molzer <5550310+197g@users.noreply.github.com>
1 parent 99a7b8c commit a0191ed

File tree

8 files changed

+101
-3
lines changed

8 files changed

+101
-3
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ flate2 = { version = "1.0.20", optional = true }
2626
weezl = { version = "0.1.10", optional = true }
2727
zstd = { version = "0.13", optional = true }
2828
zune-jpeg = { version = "0.5.1", optional = true }
29+
image-webp = {version = "0.2.4", optional = true }
2930

3031
[dev-dependencies]
3132
criterion = "0.3.1"
@@ -40,6 +41,7 @@ deflate = ["dep:flate2"]
4041
fax = ["dep:fax34"]
4142
jpeg = ["dep:zune-jpeg"]
4243
lzw = ["dep:weezl"]
44+
webp = ["dep:image-webp"]
4345
zstd = ["dep:zstd"]
4446

4547
[[bench]]

src/decoder/image.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,13 +526,14 @@ impl Image {
526526
}
527527
}
528528

529-
fn create_reader<'r, R: 'r + Read>(
529+
fn create_reader<'r, R: 'r + Read + Seek>(
530530
reader: R,
531531
compression_method: CompressionMethod,
532532
compressed_length: u64,
533533
// FIXME: these should be `expect` attributes or we choose another way of passing them.
534534
#[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>,
535535
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] dimensions: (u32, u32),
536+
#[cfg_attr(not(feature = "webp"), allow(unused_variables))] samples: u16,
536537
) -> TiffResult<Box<dyn Read + 'r>> {
537538
Ok(match compression_method {
538539
CompressionMethod::None => Box::new(reader),
@@ -607,6 +608,13 @@ impl Image {
607608
reader,
608609
compressed_length,
609610
)?),
611+
#[cfg(feature = "webp")]
612+
CompressionMethod::WebP => Box::new(super::stream::WebPReader::new(
613+
reader,
614+
compressed_length,
615+
samples,
616+
)?),
617+
610618
method => {
611619
return Err(TiffError::UnsupportedError(
612620
TiffUnsupportedError::UnsupportedCompressionMethod(method),
@@ -805,7 +813,7 @@ impl Image {
805813

806814
pub(crate) fn expand_chunk(
807815
&self,
808-
reader: &mut ValueReader<impl Read>,
816+
reader: &mut ValueReader<impl Read + Seek>,
809817
buf: &mut [u8],
810818
layout: &ReadoutLayout,
811819
chunk_index: u32,
@@ -919,6 +927,7 @@ impl Image {
919927
*compressed_bytes,
920928
self.jpeg_tables.as_deref().map(|a| &**a),
921929
chunk_dims,
930+
self.samples,
922931
)?;
923932

924933
if is_output_chunk_rows && is_all_bits {

src/decoder/stream.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
//! All IO functionality needed for TIFF decoding
2+
#[cfg(feature = "webp")]
3+
use std::io::Cursor;
24
use std::io::{self, BufRead, BufReader, Read, Seek, Take};
35

46
pub use crate::tags::ByteOrder;
@@ -354,6 +356,61 @@ impl<R: Read> Read for Group4Reader<R> {
354356
}
355357
}
356358

359+
#[cfg(feature = "webp")]
360+
pub struct WebPReader {
361+
inner: Cursor<Vec<u8>>,
362+
}
363+
364+
#[cfg(feature = "webp")]
365+
impl WebPReader {
366+
pub fn new<R: Read + Seek>(
367+
reader: R,
368+
compressed_length: u64,
369+
samples: u16,
370+
) -> crate::TiffResult<Self> {
371+
let mut decoder =
372+
image_webp::WebPDecoder::new(io::BufReader::new(reader.take(compressed_length)))
373+
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
374+
375+
if !(samples == 4 || (samples == 3 && !decoder.has_alpha())) {
376+
return Err(io::Error::new(
377+
io::ErrorKind::InvalidData,
378+
"bad sample count for WebP compressed data",
379+
)
380+
.into());
381+
}
382+
383+
let total_bytes =
384+
samples as usize * decoder.dimensions().0 as usize * decoder.dimensions().1 as usize;
385+
let mut data = vec![0; total_bytes];
386+
387+
decoder
388+
.read_image(&mut data)
389+
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
390+
391+
// Add a fully opaque alpha channel if needed
392+
if samples == 4 && !decoder.has_alpha() {
393+
for i in (0..(total_bytes / 4)).rev() {
394+
data[i * 4 + 3] = 255;
395+
data[i * 4 + 2] = data[i * 3 + 2];
396+
data[i * 4 + 1] = data[i * 3 + 1];
397+
data[i * 4] = data[i * 3];
398+
}
399+
}
400+
401+
Ok(Self {
402+
inner: Cursor::new(data),
403+
})
404+
}
405+
}
406+
407+
#[cfg(feature = "webp")]
408+
impl Read for WebPReader {
409+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
410+
self.inner.read(buf)
411+
}
412+
}
413+
357414
#[cfg(test)]
358415
mod test {
359416
use super::*;

src/tags.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ pub enum CompressionMethod(u16) unknown(
267267

268268
// Self-assigned by libtiff
269269
ZSTD = 0xC350,
270+
271+
// Self-assigned by libtiff
272+
WebP = 0xC351,
270273
}
271274
}
272275

tests/COPYRIGHT

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@ subsubifd.tif:
22
url: https://data.kitware.com/api/v1/file/hashsum/sha512/372ca32735c8a8fdbe2286dabead9e779da63f10ba81eda84625e5273f76d74ca1a47a978f67e9c00c12f7f72009c7b2c07a641e643bb0c463812f4ae7f15d6e/download
33
credit: https://github.com/DigitalSlideArchive/tifftools/commit/4d00c70dbce828262f86c1431f7c66b1748965ac
44
license: CC0
5+
usda/*.tif
6+
url: https://registry.opendata.aws/naip/index.html
7+
credit: NAIP on AWS was accessed on 2026-01-28 from https://registry.opendata.aws/naip.
8+
license: Public Domain

tests/decode_geotiff_images.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use tiff::ColorType;
77
use std::fs::File;
88
use std::path::PathBuf;
99

10-
const TEST_IMAGE_DIR: &str = "./tests/images";
10+
const TEST_IMAGE_DIR: &str = "./tests/geotiff";
1111

1212
#[test]
1313
fn test_geo_tiff() {
@@ -61,3 +61,26 @@ fn test_geo_tiff() {
6161
assert_eq!(data.len(), 500);
6262
}
6363
}
64+
65+
#[cfg(feature = "webp")]
66+
#[test]
67+
fn test_webp_tiff() {
68+
let filenames = ["usda_naip_256_webp_z3.tif"];
69+
let mut buffer = DecodingResult::U8(vec![]);
70+
71+
for filename in filenames.iter() {
72+
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
73+
let img_file = File::open(path).expect("Cannot find test image!");
74+
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
75+
76+
loop {
77+
decoder.read_image_to_buffer(&mut buffer).unwrap();
78+
79+
if !decoder.more_images() {
80+
break;
81+
}
82+
83+
decoder.next_image().unwrap();
84+
}
85+
}
86+
}
20.1 KB
Binary file not shown.

0 commit comments

Comments
 (0)