Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03979faef8 | ||
|
|
857973cb21 | ||
|
|
cef21c47fb | ||
|
|
a0191ed1a7 | ||
|
|
99a7b8cba7 | ||
|
|
418ea182c3 | ||
|
|
529fa64e13 | ||
|
|
a51b23091e | ||
|
|
ac65bd8477 |
No files matched your search
+28
@@ -1,3 +1,31 @@
|
|||||||
|
# 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:
|
||||||
|
- Planar files can now be read with `Decoder::read_image_to_buffer` if the
|
||||||
|
limits allow for such a large buffer size. Previously only the first plane
|
||||||
|
was read and the decoder will attempt a fallback to this behavior if the
|
||||||
|
limits do not allow all planes to be allocated.
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- An off-by-one error in a `debug_assert` of `ValueBuffer` caused its use in
|
||||||
|
`IfdDecoder::find_tag_buf` to panic outside release mode when the buffer
|
||||||
|
exactly matched the required size.
|
||||||
|
|
||||||
|
Additions:
|
||||||
|
- The methods `ColorType::{num_samples, bit_depth}` are now public. This
|
||||||
|
promises that we only have homogeneous color depths in this major version.
|
||||||
|
|
||||||
# Version 0.11.1
|
# Version 0.11.1
|
||||||
|
|
||||||
Fixes:
|
Fixes:
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tiff"
|
name = "tiff"
|
||||||
version = "0.11.1"
|
version = "0.11.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
@@ -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]]
|
||||||
|
|||||||
+104
-1
@@ -37,6 +37,7 @@ fn debug_planes(
|
|||||||
(width, height): (u32, u32),
|
(width, height): (u32, u32),
|
||||||
) {
|
) {
|
||||||
let (depth, mut tupltype) = match colortype {
|
let (depth, mut tupltype) = match colortype {
|
||||||
|
// Note: we will expand, so this is in fact not BLACKANDWHITE
|
||||||
tiff::ColorType::Gray(_) => (1, "GRAYSCALE"),
|
tiff::ColorType::Gray(_) => (1, "GRAYSCALE"),
|
||||||
tiff::ColorType::RGB(_) => (3, "RGB"),
|
tiff::ColorType::RGB(_) => (3, "RGB"),
|
||||||
tiff::ColorType::RGBA(_) => (4, "RGB_ALPHA"),
|
tiff::ColorType::RGBA(_) => (4, "RGB_ALPHA"),
|
||||||
@@ -49,8 +50,57 @@ fn debug_planes(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Number of samples in a line and number of sample lines in the image, depending on whether
|
||||||
|
// they are planar or not. This determines the layout of the image buffer when expanding bit
|
||||||
|
// packed samples to a full byte (we support power-of-two sample sizes, positive and negative,
|
||||||
|
// but only those).
|
||||||
|
let (swidth, sheight) = if layout.planes > 1 {
|
||||||
|
(
|
||||||
|
u64::from(width) * u64::from(colortype.num_samples()),
|
||||||
|
u64::from(height),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
u64::from(width),
|
||||||
|
u64::from(height) * u64::from(colortype.num_samples()),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut fallback_buffer;
|
||||||
let maxval = match data.as_buffer(0) {
|
let maxval = match data.as_buffer(0) {
|
||||||
DecodingBuffer::U8(_) => u64::from(u8::MAX),
|
// `U8` is also our representation of bit-packed color. PAM uses the smallest number of
|
||||||
|
// *bytes* needed to represent a sample but we must also adhere to maxval. We could explode
|
||||||
|
// them here (e.g. represent 1-bit as `{0, 1}`) but we'd also set `maxval` accordingly and
|
||||||
|
// this requires tupltype to be `BLACKANDWHITE` so introducing a bunch of complexity. If
|
||||||
|
// you want to do that in your real code, go ahead.
|
||||||
|
DecodingBuffer::U8(buf) => match colortype.bit_depth() {
|
||||||
|
8 => u64::from(u8::MAX),
|
||||||
|
4 => {
|
||||||
|
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
||||||
|
expand_4bit(buf, swidth, &mut fallback_buffer);
|
||||||
|
*data = DecodingResult::U8(fallback_buffer);
|
||||||
|
u64::from(u8::MAX)
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
||||||
|
expand_2bit(buf, swidth, &mut fallback_buffer);
|
||||||
|
*data = DecodingResult::U8(fallback_buffer);
|
||||||
|
u64::from(u8::MAX)
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
||||||
|
expand_1bit(buf, swidth, &mut fallback_buffer);
|
||||||
|
*data = DecodingResult::U8(fallback_buffer);
|
||||||
|
u64::from(u8::MAX)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
eprintln!(
|
||||||
|
"Unsupported bit depth for U8 buffer: {}",
|
||||||
|
colortype.bit_depth()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
DecodingBuffer::U16(_) => u64::from(u16::MAX),
|
DecodingBuffer::U16(_) => u64::from(u16::MAX),
|
||||||
DecodingBuffer::U32(_) => u64::from(u32::MAX),
|
DecodingBuffer::U32(_) => u64::from(u32::MAX),
|
||||||
DecodingBuffer::U64(_) => u64::MAX,
|
DecodingBuffer::U64(_) => u64::MAX,
|
||||||
@@ -101,3 +151,56 @@ fn debug_planes(
|
|||||||
.expect("Failed to write PAM data");
|
.expect("Failed to write PAM data");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Put exactly the ith `SCALE`-bit chunk of `val` into the least significant bits.
|
||||||
|
#[inline(always)]
|
||||||
|
fn bits_extract<const SCALE: u8>(val: u8, i: u8) -> u8 {
|
||||||
|
// Shift left to discard high bits, then right to bring desired bits down. This way also
|
||||||
|
// conveniently avoids all cases of overflow.
|
||||||
|
(val << (i * SCALE)) >> (8 - SCALE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_1bit(from: &[u8], samples_per_padded: u64, into: &mut [u8]) {
|
||||||
|
let stride = samples_per_padded.div_ceil(8);
|
||||||
|
|
||||||
|
let from_row = from.chunks_exact(stride as usize);
|
||||||
|
let into_row = into.chunks_exact_mut(samples_per_padded as usize);
|
||||||
|
|
||||||
|
for (from_row, into_row) in from_row.zip(into_row) {
|
||||||
|
for (into, &from) in into_row.chunks_mut(8).zip(from_row) {
|
||||||
|
let data =
|
||||||
|
core::array::from_fn::<u8, 8, _>(|i| 0xff * bits_extract::<1>(from, i as u8));
|
||||||
|
into.copy_from_slice(&data[..into.len()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_2bit(from: &[u8], samples_per_padded: u64, into: &mut [u8]) {
|
||||||
|
let stride = samples_per_padded.div_ceil(4);
|
||||||
|
|
||||||
|
let from_row = from.chunks_exact(stride as usize);
|
||||||
|
let into_row = into.chunks_exact_mut(samples_per_padded as usize);
|
||||||
|
|
||||||
|
for (from_row, into_row) in from_row.zip(into_row) {
|
||||||
|
for (into, &from) in into_row.chunks_mut(4).zip(from_row) {
|
||||||
|
let data =
|
||||||
|
core::array::from_fn::<u8, 4, _>(|i| 0xaa * bits_extract::<2>(from, i as u8));
|
||||||
|
into.copy_from_slice(&data[..into.len()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_4bit(from: &[u8], samples_per_padded: u64, into: &mut [u8]) {
|
||||||
|
let stride = samples_per_padded.div_ceil(2);
|
||||||
|
|
||||||
|
let from_row = from.chunks_exact(stride as usize);
|
||||||
|
let into_row = into.chunks_exact_mut(samples_per_padded as usize);
|
||||||
|
|
||||||
|
for (from_row, into_row) in from_row.zip(into_row) {
|
||||||
|
for (into, &from) in into_row.chunks_mut(2).zip(from_row) {
|
||||||
|
let data =
|
||||||
|
core::array::from_fn::<u8, 2, _>(|i| 0x88 * bits_extract::<4>(from, i as u8));
|
||||||
|
into.copy_from_slice(&data[..into.len()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
+6
-1
@@ -1538,7 +1538,7 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the IFD decoder for our current image IFD.
|
/// Get the IFD decoder for our current image IFD.
|
||||||
fn image_ifd(&mut self) -> IfdDecoder<'_> {
|
pub fn image_ifd(&mut self) -> IfdDecoder<'_> {
|
||||||
IfdDecoder {
|
IfdDecoder {
|
||||||
inner: tag_reader::TagReader {
|
inner: tag_reader::TagReader {
|
||||||
decoder: &mut self.value_reader,
|
decoder: &mut self.value_reader,
|
||||||
@@ -1885,6 +1885,11 @@ impl IfdDecoder<'_> {
|
|||||||
pub fn get_tag_ascii_string(&mut self, tag: Tag) -> TiffResult<String> {
|
pub fn get_tag_ascii_string(&mut self, tag: Tag) -> TiffResult<String> {
|
||||||
self.get_tag(tag)?.into_string()
|
self.get_tag(tag)?.into_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inspect the raw underlying directory.
|
||||||
|
pub fn directory(&self) -> &Directory {
|
||||||
|
self.inner.ifd
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'l> IfdDecoder<'l> {
|
impl<'l> IfdDecoder<'l> {
|
||||||
|
|||||||
@@ -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::*;
|
||||||
|
|||||||
+4
-2
@@ -51,7 +51,8 @@ pub enum ColorType {
|
|||||||
Multiband { bit_depth: u8, num_samples: u16 },
|
Multiband { bit_depth: u8, num_samples: u16 },
|
||||||
}
|
}
|
||||||
impl ColorType {
|
impl ColorType {
|
||||||
fn bit_depth(&self) -> u8 {
|
/// The number of bits used to store each sample.
|
||||||
|
pub fn bit_depth(&self) -> u8 {
|
||||||
match *self {
|
match *self {
|
||||||
ColorType::Gray(b)
|
ColorType::Gray(b)
|
||||||
| ColorType::RGB(b)
|
| ColorType::RGB(b)
|
||||||
@@ -66,7 +67,8 @@ impl ColorType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn num_samples(&self) -> u16 {
|
/// The number of samples in each representation of a single color.
|
||||||
|
pub fn num_samples(&self) -> u16 {
|
||||||
match *self {
|
match *self {
|
||||||
ColorType::Gray(_) => 1,
|
ColorType::Gray(_) => 1,
|
||||||
ColorType::RGB(_) => 3,
|
ColorType::RGB(_) => 3,
|
||||||
|
|||||||
+16
@@ -1,4 +1,5 @@
|
|||||||
use crate::encoder::TiffValue;
|
use crate::encoder::TiffValue;
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
macro_rules! tags {
|
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).
|
// practice it just returns garbage and the validity does not matter greatly to us).
|
||||||
pub struct IfdPointer(pub u64);
|
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! {
|
tags! {
|
||||||
/// The type of an IFD entry (a 2 byte field).
|
/// The type of an IFD entry (a 2 byte field).
|
||||||
pub enum Type(u16) {
|
pub enum Type(u16) {
|
||||||
@@ -267,6 +280,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