Support decoding WebP compressed TIFFs (#336)
Co-authored-by: A. Molzer <5550310+197g@users.noreply.github.com>
This commit is contained in:
1 parent
99a7b8cba7
commit
a0191ed1a7
8 files changed
+101
-3
No files matched your search
@@ -26,6 +26,7 @@ flate2 = { version = "1.0.20", optional = true }
|
|||||||
weezl = { version = "0.1.10", optional = true }
|
weezl = { version = "0.1.10", optional = true }
|
||||||
zstd = { version = "0.13", optional = true }
|
zstd = { version = "0.13", optional = true }
|
||||||
zune-jpeg = { version = "0.5.1", optional = true }
|
zune-jpeg = { version = "0.5.1", optional = true }
|
||||||
|
image-webp = {version = "0.2.4", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.3.1"
|
criterion = "0.3.1"
|
||||||
@@ -40,6 +41,7 @@ deflate = ["dep:flate2"]
|
|||||||
fax = ["dep:fax34"]
|
fax = ["dep:fax34"]
|
||||||
jpeg = ["dep:zune-jpeg"]
|
jpeg = ["dep:zune-jpeg"]
|
||||||
lzw = ["dep:weezl"]
|
lzw = ["dep:weezl"]
|
||||||
|
webp = ["dep:image-webp"]
|
||||||
zstd = ["dep:zstd"]
|
zstd = ["dep:zstd"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
|
|||||||
+11
-2
@@ -526,13 +526,14 @@ impl Image {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_reader<'r, R: 'r + Read>(
|
fn create_reader<'r, R: 'r + Read + Seek>(
|
||||||
reader: R,
|
reader: R,
|
||||||
compression_method: CompressionMethod,
|
compression_method: CompressionMethod,
|
||||||
compressed_length: u64,
|
compressed_length: u64,
|
||||||
// FIXME: these should be `expect` attributes or we choose another way of passing them.
|
// 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 = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>,
|
||||||
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] dimensions: (u32, u32),
|
#[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>> {
|
) -> TiffResult<Box<dyn Read + 'r>> {
|
||||||
Ok(match compression_method {
|
Ok(match compression_method {
|
||||||
CompressionMethod::None => Box::new(reader),
|
CompressionMethod::None => Box::new(reader),
|
||||||
@@ -607,6 +608,13 @@ impl Image {
|
|||||||
reader,
|
reader,
|
||||||
compressed_length,
|
compressed_length,
|
||||||
)?),
|
)?),
|
||||||
|
#[cfg(feature = "webp")]
|
||||||
|
CompressionMethod::WebP => Box::new(super::stream::WebPReader::new(
|
||||||
|
reader,
|
||||||
|
compressed_length,
|
||||||
|
samples,
|
||||||
|
)?),
|
||||||
|
|
||||||
method => {
|
method => {
|
||||||
return Err(TiffError::UnsupportedError(
|
return Err(TiffError::UnsupportedError(
|
||||||
TiffUnsupportedError::UnsupportedCompressionMethod(method),
|
TiffUnsupportedError::UnsupportedCompressionMethod(method),
|
||||||
@@ -805,7 +813,7 @@ impl Image {
|
|||||||
|
|
||||||
pub(crate) fn expand_chunk(
|
pub(crate) fn expand_chunk(
|
||||||
&self,
|
&self,
|
||||||
reader: &mut ValueReader<impl Read>,
|
reader: &mut ValueReader<impl Read + Seek>,
|
||||||
buf: &mut [u8],
|
buf: &mut [u8],
|
||||||
layout: &ReadoutLayout,
|
layout: &ReadoutLayout,
|
||||||
chunk_index: u32,
|
chunk_index: u32,
|
||||||
@@ -919,6 +927,7 @@ impl Image {
|
|||||||
*compressed_bytes,
|
*compressed_bytes,
|
||||||
self.jpeg_tables.as_deref().map(|a| &**a),
|
self.jpeg_tables.as_deref().map(|a| &**a),
|
||||||
chunk_dims,
|
chunk_dims,
|
||||||
|
self.samples,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if is_output_chunk_rows && is_all_bits {
|
if is_output_chunk_rows && is_all_bits {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
//! All IO functionality needed for TIFF decoding
|
//! All IO functionality needed for TIFF decoding
|
||||||
|
#[cfg(feature = "webp")]
|
||||||
|
use std::io::Cursor;
|
||||||
use std::io::{self, BufRead, BufReader, Read, Seek, Take};
|
use std::io::{self, BufRead, BufReader, Read, Seek, Take};
|
||||||
|
|
||||||
pub use crate::tags::ByteOrder;
|
pub use crate::tags::ByteOrder;
|
||||||
@@ -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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -267,6 +267,9 @@ pub enum CompressionMethod(u16) unknown(
|
|||||||
|
|
||||||
// Self-assigned by libtiff
|
// Self-assigned by libtiff
|
||||||
ZSTD = 0xC350,
|
ZSTD = 0xC350,
|
||||||
|
|
||||||
|
// Self-assigned by libtiff
|
||||||
|
WebP = 0xC351,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,7 @@ subsubifd.tif:
|
|||||||
url: https://data.kitware.com/api/v1/file/hashsum/sha512/372ca32735c8a8fdbe2286dabead9e779da63f10ba81eda84625e5273f76d74ca1a47a978f67e9c00c12f7f72009c7b2c07a641e643bb0c463812f4ae7f15d6e/download
|
url: https://data.kitware.com/api/v1/file/hashsum/sha512/372ca32735c8a8fdbe2286dabead9e779da63f10ba81eda84625e5273f76d74ca1a47a978f67e9c00c12f7f72009c7b2c07a641e643bb0c463812f4ae7f15d6e/download
|
||||||
credit: https://github.com/DigitalSlideArchive/tifftools/commit/4d00c70dbce828262f86c1431f7c66b1748965ac
|
credit: https://github.com/DigitalSlideArchive/tifftools/commit/4d00c70dbce828262f86c1431f7c66b1748965ac
|
||||||
license: CC0
|
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
|
||||||
@@ -7,7 +7,7 @@ use tiff::ColorType;
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
const TEST_IMAGE_DIR: &str = "./tests/images";
|
const TEST_IMAGE_DIR: &str = "./tests/geotiff";
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_geo_tiff() {
|
fn test_geo_tiff() {
|
||||||
@@ -61,3 +61,26 @@ fn test_geo_tiff() {
|
|||||||
assert_eq!(data.len(), 500);
|
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 not shown.
Reference in new issue
Block a user