Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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]]
Expand Down
13 changes: 11 additions & 2 deletions src/decoder/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Box<dyn Read + 'r>> {
Ok(match compression_method {
CompressionMethod::None => Box::new(reader),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -805,7 +813,7 @@ impl Image {

pub(crate) fn expand_chunk(
&self,
reader: &mut ValueReader<impl Read>,
reader: &mut ValueReader<impl Read + Seek>,
buf: &mut [u8],
layout: &ReadoutLayout,
chunk_index: u32,
Expand Down Expand Up @@ -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 {
Expand Down
57 changes: 57 additions & 0 deletions src/decoder/stream.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -354,6 +356,61 @@ impl<R: Read> Read for Group4Reader<R> {
}
}

#[cfg(feature = "webp")]
pub struct WebPReader {
inner: Cursor<Vec<u8>>,
}

#[cfg(feature = "webp")]
impl WebPReader {
pub fn new<R: Read + Seek>(
reader: R,
compressed_length: u64,
samples: u16,
) -> crate::TiffResult<Self> {
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<usize> {
self.inner.read(buf)
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
3 changes: 3 additions & 0 deletions src/tags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ pub enum CompressionMethod(u16) unknown(

// Self-assigned by libtiff
ZSTD = 0xC350,

// Self-assigned by libtiff
WebP = 0xC351,
}
}

Expand Down
4 changes: 4 additions & 0 deletions tests/COPYRIGHT
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 24 additions & 1 deletion tests/decode_geotiff_images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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();
}
}
}
File renamed without changes.
Binary file added tests/geotiff/usda_naip_256_webp_z3.tif
Binary file not shown.
Loading