Compare commits

...
Author SHA1 Message Date
A. Molzer 03979faef8 Metadata for 0.11.3 release 2026-02-07 08:38:03 +01:00
Aurelia Molzer 857973cb21 Merge pull request #339 from image-rs/file-listing-and-utils
File listing utils and example
2026-02-07 07:33:24 +00:00
A. Molzer cef21c47fb Add example listing a files directories 2026-02-07 08:22:34 +01:00
Jonathan BehrensandA. Molzer a0191ed1a7 Support decoding WebP compressed TIFFs (#336)
Co-authored-by: A. Molzer <5550310+197g@users.noreply.github.com>
2026-01-29 10:53:00 -08:00
Aurelia Molzer 99a7b8cba7 Merge pull request #334 from image-rs/release-0.11.2
Release notes for 0.11.2
2026-01-25 02:36:26 +00:00
11 changed files with 256 additions and 5 deletions

No files matched your search

+11
View File
@@ -1,3 +1,14 @@
# Version 0.11.3
Additions:
- Added support for `webp` compression (as per GDAL) under the `webp` feature flag (default: off).
- Added `Decoder::image_ifd`, creating an `IfdDecoder` of the current IFD.
- Added `IfdDecoder::directory`, returning the `Directory` underlying it.
- Added `LowerHex` and `UpwerHex` implementations for `IfdPointer`.
Notes:
- A new example `tiff-ls` demonstrates use of the new features.
# Version 0.11.2
Changes:
+3 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tiff"
version = "0.11.2"
version = "0.11.3"
edition = "2021"
resolver = "2"
@@ -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]]
+124
View File
@@ -0,0 +1,124 @@
use std::borrow::Cow;
use tiff::decoder::Decoder;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let Some(image) = std::env::args_os().nth(1) else {
eprintln!("Usage: decode FILE");
return Ok(());
};
let file = std::fs::File::open(image)?;
let io = std::io::BufReader::new(file);
let mut reader = Decoder::new(io)?;
loop {
ls_dir(&mut reader);
if !reader.more_images() {
break;
}
}
Ok(())
}
fn ls_dir<R: std::io::Read + std::io::Seek>(reader: &mut Decoder<R>) {
if let Some(ifd) = reader.ifd_pointer() {
println!("Directory at {ifd:x}");
}
println!("Name\tHex\tType\tCount");
let ifd = reader.image_ifd();
for (tag, entry) in ifd.directory().iter() {
let name: &'static str = match tag {
tiff::tags::Tag::Artist => "Artist",
tiff::tags::Tag::BitsPerSample => "BitsPerSample",
tiff::tags::Tag::CellLength => "CellLength",
tiff::tags::Tag::CellWidth => "CellWidth",
tiff::tags::Tag::ColorMap => "ColorMap",
tiff::tags::Tag::Compression => "Compression",
tiff::tags::Tag::DateTime => "DateTime",
tiff::tags::Tag::ExtraSamples => "ExtraSamples",
tiff::tags::Tag::FillOrder => "FillOrder",
tiff::tags::Tag::FreeByteCounts => "FreeByteCounts",
tiff::tags::Tag::FreeOffsets => "FreeOffsets",
tiff::tags::Tag::GrayResponseCurve => "GrayResponseCurve",
tiff::tags::Tag::GrayResponseUnit => "GrayResponseUnit",
tiff::tags::Tag::HostComputer => "HostComputer",
tiff::tags::Tag::ImageDescription => "ImageDescription",
tiff::tags::Tag::ImageLength => "ImageLength",
tiff::tags::Tag::ImageWidth => "ImageWidth",
tiff::tags::Tag::Make => "Make",
tiff::tags::Tag::MaxSampleValue => "MaxSampleValue",
tiff::tags::Tag::MinSampleValue => "MinSampleValue",
tiff::tags::Tag::Model => "Model",
tiff::tags::Tag::NewSubfileType => "NewSubfileType",
tiff::tags::Tag::Orientation => "Orientation",
tiff::tags::Tag::PhotometricInterpretation => "PhotometricInterpretation",
tiff::tags::Tag::PlanarConfiguration => "PlanarConfiguration",
tiff::tags::Tag::ResolutionUnit => "ResolutionUnit",
tiff::tags::Tag::RowsPerStrip => "RowsPerStrip",
tiff::tags::Tag::SamplesPerPixel => "SamplesPerPixel",
tiff::tags::Tag::Software => "Software",
tiff::tags::Tag::StripByteCounts => "StripByteCounts",
tiff::tags::Tag::StripOffsets => "StripOffsets",
tiff::tags::Tag::SubfileType => "SubfileType",
tiff::tags::Tag::Threshholding => "Threshholding",
tiff::tags::Tag::XResolution => "XResolution",
tiff::tags::Tag::YResolution => "YResolution",
tiff::tags::Tag::Predictor => "Predictor",
tiff::tags::Tag::TileWidth => "TileWidth",
tiff::tags::Tag::TileLength => "TileLength",
tiff::tags::Tag::TileOffsets => "TileOffsets",
tiff::tags::Tag::TileByteCounts => "TileByteCounts",
tiff::tags::Tag::SubIfd => "SubIfd",
tiff::tags::Tag::SampleFormat => "SampleFormat",
tiff::tags::Tag::SMinSampleValue => "SMinSampleValue",
tiff::tags::Tag::SMaxSampleValue => "SMaxSampleValue",
tiff::tags::Tag::JPEGTables => "JPEGTables",
tiff::tags::Tag::ChromaSubsampling => "ChromaSubsampling",
tiff::tags::Tag::ChromaPositioning => "ChromaPositioning",
tiff::tags::Tag::ModelPixelScaleTag => "ModelPixelScaleTag",
tiff::tags::Tag::ModelTransformationTag => "ModelTransformationTag",
tiff::tags::Tag::ModelTiepointTag => "ModelTiepointTag",
tiff::tags::Tag::Copyright => "Copyright",
tiff::tags::Tag::ExifDirectory => "ExifDirectory",
tiff::tags::Tag::GpsDirectory => "GpsDirectory",
tiff::tags::Tag::IccProfile => "IccProfile",
tiff::tags::Tag::GeoKeyDirectoryTag => "GeoKeyDirectoryTag",
tiff::tags::Tag::GeoDoubleParamsTag => "GeoDoubleParamsTag",
tiff::tags::Tag::GeoAsciiParamsTag => "GeoAsciiParamsTag",
tiff::tags::Tag::ExifVersion => "ExifVersion",
tiff::tags::Tag::GdalNodata => "GdalNodata",
_ => "<unknown>",
};
let ty: Cow<'static, str> = match entry.field_type() {
tiff::tags::Type::BYTE => "u8".into(),
tiff::tags::Type::ASCII => "ascii".into(),
tiff::tags::Type::SHORT => "u16".into(),
tiff::tags::Type::LONG => "u32".into(),
tiff::tags::Type::RATIONAL => "r32".into(),
tiff::tags::Type::SBYTE => "i8".into(),
tiff::tags::Type::UNDEFINED => "byte".into(),
tiff::tags::Type::SSHORT => "s16".into(),
tiff::tags::Type::SLONG => "s32".into(),
tiff::tags::Type::SRATIONAL => "sr32".into(),
tiff::tags::Type::FLOAT => "f32".into(),
tiff::tags::Type::DOUBLE => "f64".into(),
tiff::tags::Type::IFD => "ifd32".into(),
tiff::tags::Type::LONG8 => "u64".into(),
tiff::tags::Type::SLONG8 => "i64".into(),
tiff::tags::Type::IFD8 => "ifd64".into(),
other => format!("{:x}", other.to_u16()).into(),
};
eprintln!(
"{name:16}\t{tag:4x}\t{ty}\t{count}",
tag = tag.to_u16(),
count = entry.count(),
);
}
}
+11 -2
View File
@@ -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),
@@ -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<impl Read>,
reader: &mut ValueReader<impl Read + Seek>,
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 {
+6 -1
View File
@@ -1538,7 +1538,7 @@ impl<R: Read + Seek> Decoder<R> {
}
/// Get the IFD decoder for our current image IFD.
fn image_ifd(&mut self) -> IfdDecoder<'_> {
pub fn image_ifd(&mut self) -> IfdDecoder<'_> {
IfdDecoder {
inner: tag_reader::TagReader {
decoder: &mut self.value_reader,
@@ -1885,6 +1885,11 @@ impl IfdDecoder<'_> {
pub fn get_tag_ascii_string(&mut self, tag: Tag) -> TiffResult<String> {
self.get_tag(tag)?.into_string()
}
/// Inspect the raw underlying directory.
pub fn directory(&self) -> &Directory {
self.inner.ifd
}
}
impl<'l> IfdDecoder<'l> {
+57
View File
@@ -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<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::*;
+16
View File
@@ -1,4 +1,5 @@
use crate::encoder::TiffValue;
use core::fmt;
macro_rules! tags {
{
@@ -169,6 +170,18 @@ pub enum Tag(u16) unknown(
// practice it just returns garbage and the validity does not matter greatly to us).
pub struct IfdPointer(pub u64);
impl fmt::LowerHex for IfdPointer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::LowerHex::fmt(&self.0, f)
}
}
impl core::fmt::UpperHex for IfdPointer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::UpperHex::fmt(&self.0, f)
}
}
tags! {
/// The type of an IFD entry (a 2 byte field).
pub enum Type(u16) {
@@ -267,6 +280,9 @@ pub enum CompressionMethod(u16) unknown(
// Self-assigned by libtiff
ZSTD = 0xC350,
// Self-assigned by libtiff
WebP = 0xC351,
}
}
+4
View File
@@ -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
+24 -1
View File
@@ -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();
}
}
}
File renamed without changes.
Binary file not shown.