Compare commits

..
Author SHA1 Message Date
A. Molzer 32dda6e11f Add integration test for exif copy 2026-01-15 20:21:54 +01:00
A. Molzer 74191f92bb Change EXIF Applicability's None to passthrough
This allows it to address the case of copying all EXIF data into a
separate file. And as soon as our decoder can handle files without any
image data (missing any stripoffsets, etc.) in ways that do not return
errors we can hopefully do the opposite and merge the data again.
2026-01-15 20:21:54 +01:00
A. Molzer f057b5f47d Reduce tag_filter to a minimum, implement copy 2026-01-15 20:21:52 +01:00
A. Molzer b3ced063ef Write tag filter based on tag kind/'Level' 2026-01-14 17:40:40 +01:00
A. Molzer 76721c6893 Add conversion of ValueBuffer to IfdPointer
This mirrors what the other value type provides, for now private and
only for IfdPointer which we need for iterating an existing directory.
2026-01-14 17:40:40 +01:00
A. Molzer 348bbda906 Sketch a TagFilter option for copying metadata 2026-01-13 23:46:39 +01:00
106 changed files with 869 additions and 2573 deletions

No files matched your search

+4 -4
View File
@@ -9,14 +9,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
rust: ["1.88.0", stable, beta, nightly]
rust: ["1.85.0", stable, beta, nightly]
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@nightly
if: ${{ matrix.rust == '1.88.0' }}
if: ${{ matrix.rust == '1.85.0' }}
- name: Generate Cargo.lock with minimal-version dependencies
if: ${{ matrix.rust == '1.88.0' }}
if: ${{ matrix.rust == '1.85.0' }}
run: cargo -Zminimal-versions generate-lockfile
- uses: dtolnay/rust-toolchain@v1
@@ -29,7 +29,7 @@ jobs:
- name: build
run: cargo build -v
- name: test
if: ${{ matrix.rust != '1.88.0' }}
if: ${{ matrix.rust != '1.85.0' }}
run: cargo test -v && cargo doc -v
features:
-41
View File
@@ -1,44 +1,3 @@
# Version 0.12.0 (unreleased)
Additions:
- Added the `TiffHeader` type. It can be used to create a `Decoder` in a state
before the first IFD and allows opening files at an arbitrary first IFD
offset rather than one indicated by an encoded header at the start.
Changes:
- The IFD iteration methods on `Decoder` (`next_image`, `seek_to_image`, etc)
now come with a strong error guarantee. They will not modify the current
directory state if they fail. This includes the failure to interpret an IFD
as a supported image in the case of `*_image` variants.
# 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
Fixes:
+2 -15
View File
@@ -1,11 +1,11 @@
[package]
name = "tiff"
version = "0.11.3"
version = "0.11.1"
edition = "2021"
resolver = "2"
# note: when changed, also update test runner in `.github/workflows/rust.yml`
rust-version = "1.88.0"
rust-version = "1.85.0"
license = "MIT"
description = "TIFF decoding and encoding library in pure Rust"
@@ -26,14 +26,11 @@ 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"
# We may already depend on this via flate2
crc32fast = "1.5"
libtest-mimic = "0.8.1"
walkdir = "2.5"
[features]
default = ["deflate", "fax", "jpeg", "lzw"]
@@ -43,18 +40,8 @@ deflate = ["dep:flate2"]
fax = ["dep:fax34"]
jpeg = ["dep:zune-jpeg"]
lzw = ["dep:weezl"]
webp = ["dep:image-webp"]
zstd = ["dep:zstd"]
[[bench]]
name = "lzw"
harness = false
[[bench]]
name = "encode"
harness = false
[[test]]
name = "decode_libtiffpic"
path = "tests/decode_libtiffpic.rs"
harness = false
-44
View File
@@ -1,44 +0,0 @@
extern crate criterion;
extern crate tiff;
use criterion::{black_box, Criterion, Throughput};
use std::io::Cursor;
use tiff::encoder::colortype::RGB32Float;
use tiff::encoder::{Compression, DeflateLevel, Predictor, TiffEncoder};
const WIDTH: u32 = 500;
const HEIGHT: u32 = 500;
fn encode_image_f32_float_predictor_deflate(data: &[f32]) {
let mut buf = Cursor::new(Vec::with_capacity(WIDTH as usize * HEIGHT as usize * 4));
TiffEncoder::new(&mut buf)
.unwrap()
.with_predictor(Predictor::FloatingPoint)
.with_compression(Compression::Deflate(DeflateLevel::Fast))
.write_image::<RGB32Float>(WIDTH, HEIGHT, black_box(data))
.unwrap();
}
fn main() {
// Generate synthetic f32 image data with a gradient pattern
let data: Vec<f32> = (0..WIDTH as usize * HEIGHT as usize * 3)
.map(|i| {
let pixel = i / 3;
let x = (pixel % WIDTH as usize) as f32 / WIDTH as f32;
let y = (pixel / WIDTH as usize) as f32 / HEIGHT as f32;
x * 0.5 + y * 0.5
})
.collect();
let mut c = Criterion::default().configure_from_args();
let mut group = c.benchmark_group("tiff-encode");
group
.sample_size(20)
.throughput(Throughput::Bytes(
(WIDTH as u64) * (HEIGHT as u64) * 3 * std::mem::size_of::<f32>() as u64,
))
.bench_function("f32-float-predictor-deflate-fast", |b| {
b.iter(|| encode_image_f32_float_predictor_deflate(&data))
});
}
+5 -4
View File
@@ -8,12 +8,13 @@ use tiff::decoder::Decoder;
fn read_image(image: &[u8]) {
let image = std::io::Cursor::new(image);
let decoder = Decoder::open(black_box(image));
let decoder = Decoder::new(black_box(image));
let mut reader = decoder.unwrap();
while reader.more_images() {
while {
reader.read_image().unwrap();
}
reader.more_images()
} {}
}
fn main() {
+2 -130
View File
@@ -8,7 +8,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let file = std::fs::File::open(image)?;
let io = std::io::BufReader::new(file);
let mut reader = Decoder::open(io)?;
let mut reader = Decoder::new(io)?;
let mut data = DecodingResult::I8(vec![]);
@@ -36,33 +36,7 @@ fn debug_planes(
colortype: &tiff::ColorType,
(width, height): (u32, u32),
) {
match data {
DecodingResult::F16(v) => {
let samples = v
.iter()
.map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16)
.collect::<Vec<_>>();
*data = DecodingResult::U16(samples);
}
DecodingResult::F32(v) => {
let samples = v
.iter()
.map(|&f| (f64::from(f) * f64::from(u16::MAX)) as u16)
.collect::<Vec<_>>();
*data = DecodingResult::U16(samples);
}
DecodingResult::F64(v) => {
let samples = v
.iter()
.map(|&f| (f * f64::from(u16::MAX)) as u16)
.collect::<Vec<_>>();
*data = DecodingResult::U16(samples);
}
_ => {}
}
let (depth, mut tupltype) = match colortype {
// Note: we will expand, so this is in fact not BLACKANDWHITE
tiff::ColorType::Gray(_) => (1, "GRAYSCALE"),
tiff::ColorType::RGB(_) => (3, "RGB"),
tiff::ColorType::RGBA(_) => (4, "RGB_ALPHA"),
@@ -75,57 +49,8 @@ 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) {
// `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::U8(_) => u64::from(u8::MAX),
DecodingBuffer::U16(_) => u64::from(u16::MAX),
DecodingBuffer::U32(_) => u64::from(u32::MAX),
DecodingBuffer::U64(_) => u64::MAX,
@@ -176,56 +101,3 @@ fn debug_planes(
.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()]);
}
}
}
-121
View File
@@ -1,121 +0,0 @@
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::open(io)?;
while reader.more_images() {
reader.next_directory()?;
ls_dir(&mut reader);
}
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.current_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(),
);
}
}
+24 -175
View File
@@ -4,8 +4,8 @@ use super::tag_reader::TagReader;
use super::ChunkType;
use super::{predict_f16, predict_f32, predict_f64, ValueReader};
use crate::tags::{
ByteOrder, CompressionMethod, ExtraSamples, PhotometricInterpretation, PlanarConfiguration,
Predictor, SampleFormat, Tag,
CompressionMethod, ExtraSamples, PhotometricInterpretation, PlanarConfiguration, Predictor,
SampleFormat, Tag,
};
use crate::{
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
@@ -64,6 +64,7 @@ impl TileAttributes {
#[derive(Debug)]
pub(crate) struct Image {
pub ifd: Option<Directory>,
pub width: u32,
pub height: u32,
pub bits_per_sample: u8,
@@ -85,13 +86,6 @@ pub(crate) struct Image {
pub chunk_offsets: Vec<u64>,
pub chunk_bytes: Vec<u64>,
pub chroma_subsampling: (u16, u16),
/// Will decompression yield host-endian samples?
///
/// Standard compression algorithms run on pure bytes, taking the sample slice as it would be
/// encoded in the file. However some compression algorithms (currently only SGILog/SGILog24)
/// run on samples directly and thus skip the outer byte order entirely, except potentially
/// they have interior byte order treatment.
pub decompression_to_host_endian: bool,
}
/// Describes how to read a tile-aligned portion of the image.
@@ -146,35 +140,11 @@ pub(crate) struct ReadoutLayout {
}
impl Image {
pub(crate) fn no_image() -> Image {
Image {
width: 0,
height: 0,
bits_per_sample: 1,
samples: 1,
extra_samples: vec![],
photometric_samples: 1,
sample_format: SampleFormat::Uint,
photometric_interpretation: PhotometricInterpretation::BlackIsZero,
compression_method: CompressionMethod::None,
jpeg_tables: None,
predictor: Predictor::None,
chunk_type: ChunkType::Strip,
planar_config: PlanarConfiguration::Chunky,
strip_decoder: None,
tile_attributes: None,
chunk_offsets: Vec::new(),
chunk_bytes: Vec::new(),
chroma_subsampling: (2, 2),
decompression_to_host_endian: false,
}
}
pub fn from_reader<R: Read + Seek>(
decoder: &mut ValueReader<R>,
ifd: &Directory,
ifd: Directory,
) -> TiffResult<Image> {
let mut tag_reader = TagReader { decoder, ifd };
let mut tag_reader = TagReader { decoder, ifd: &ifd };
let width = tag_reader.require_tag(Tag::ImageWidth)?.into_u32()?;
let height = tag_reader.require_tag(Tag::ImageLength)?.into_u32()?;
@@ -360,26 +330,13 @@ impl Image {
strip_decoder = Some(StripDecodeState { rows_per_strip });
tile_attributes = None;
if chunk_offsets.len() != chunk_bytes.len() {
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
));
}
if rows_per_strip == 0 {
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
));
}
if u32::try_from(chunk_offsets.len())?
!= height.div_ceil(rows_per_strip) * planes as u32
if chunk_offsets.len() != chunk_bytes.len()
|| rows_per_strip == 0
|| u32::try_from(chunk_offsets.len())?
!= (height.saturating_sub(1) / rows_per_strip + 1) * planes as u32
{
return Err(TiffError::FormatError(
TiffFormatError::IncorrectChunkCount {
expected: height.div_ceil(rows_per_strip) * planes as u32,
found: chunk_offsets.len() as u32,
},
TiffFormatError::InconsistentSizesEncountered,
));
}
}
@@ -414,21 +371,12 @@ impl Image {
.into_u64_vec()?;
let tile = tile_attributes.as_ref().unwrap();
if chunk_offsets.len() != chunk_bytes.len() {
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
));
}
if chunk_offsets.len() != tile.tiles_down() * tile.tiles_across() * planes as usize
if chunk_offsets.len() != chunk_bytes.len()
|| chunk_offsets.len()
!= tile.tiles_down() * tile.tiles_across() * planes as usize
{
return Err(TiffError::FormatError(
TiffFormatError::IncorrectChunkCount {
expected: (tile.tiles_down() * tile.tiles_across() * planes as usize)
as u32,
found: chunk_offsets.len() as u32,
},
TiffFormatError::InconsistentSizesEncountered,
));
}
}
@@ -439,28 +387,11 @@ impl Image {
}
};
let mut sample_format = sample_format;
let mut bits_per_sample = bits_per_sample[0];
// These compression methods only work for specific sample types. They pack each pixel into
// a pixel format. And when we decompress them we run an inverse transform inside the
// decompression step because that makes sense. (Even for 'raw' data you will not be able
// to handle the samples well otherwise). All we are doing is setting up the internal
// buffer here. There's an additional transform before the color readout as yo will receive
// the color as 32-bit floats (the encoding relies on very specific quantization so
// everything else would be very lossy).
if let CompressionMethod::SgiLog = compression_method {
sample_format = SampleFormat::IEEEFP;
bits_per_sample = 32;
} else if let CompressionMethod::SgiLog24 = compression_method {
sample_format = SampleFormat::IEEEFP;
bits_per_sample = 32;
}
Ok(Image {
ifd: Some(ifd),
width,
height,
bits_per_sample,
bits_per_sample: bits_per_sample[0],
samples,
extra_samples,
photometric_samples,
@@ -476,10 +407,6 @@ impl Image {
chunk_offsets,
chunk_bytes,
chroma_subsampling,
decompression_to_host_endian: matches!(
compression_method,
CompressionMethod::SgiLog | CompressionMethod::SgiLog24
),
})
}
@@ -574,46 +501,6 @@ impl Image {
vec![self.bits_per_sample; self.samples as usize],
),
)),
PhotometricInterpretation::CIELogL => match self.samples {
1 if matches!(self.bits_per_sample, 32)
&& matches!(self.sample_format, SampleFormat::IEEEFP) =>
{
// We do this transform internally while reading.
Ok(ColorType::Gray(32))
}
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
vec![self.bits_per_sample; self.samples as usize],
),
)),
},
PhotometricInterpretation::CIELogLuv => match self.samples {
// Only fully supported for the compression format `SGILog`/`SGILog24` which must
// be used with 16-bit per sample.
//
// It's actually an interesting point what we should output here. Note that in
// libtiff there is a RAW mode (a single unsigned sample per pixel) as well as a
// 32-bit floating point mode for XYZ data. The `Lu'v'` variant is a heavily
// customized quantization so we should not return it directly. The format itself
// describes being based off `xyY` so that may make sense but there is a pole in
// the relation to `XYZ` (at y=0) which is absent in the chosen quantization since
// we assume a bias of `.5` so `v >= 1/820` and thus `y >= 0.49/820` in practice.
// Also we lose a bit of accuracy with just 16-bit precision.
//
// In `libtiff` data is always transformed through XYZ. So let's do that.
3 if matches!(self.bits_per_sample, 32)
&& matches!(self.sample_format, SampleFormat::IEEEFP) =>
{
Ok(ColorType::RGB(32))
}
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
vec![self.bits_per_sample; self.samples as usize],
),
)),
},
// Unsupported due to extra unfiltering and conversion steps. We need to find the
// Decode tag (SRATIONAL; 2 * SamplesPerPixel) and apply the following conversion:
//
@@ -639,41 +526,16 @@ impl Image {
}
}
fn create_reader<'r, R: 'r + Read + Seek>(
fn create_reader<'r, R: 'r + Read>(
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]>,
dimensions: (u32, u32),
samples: u16,
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] dimensions: (u32, u32),
) -> TiffResult<Box<dyn Read + 'r>> {
Ok(match compression_method {
CompressionMethod::None => Box::new(reader),
CompressionMethod::SgiLog => match samples {
1 => Box::new(super::logluv::LogLuv16::new(
reader,
compressed_length,
dimensions.0,
)?),
3 => Box::new(super::logluv::LogLuv32::new(
reader,
compressed_length,
dimensions.0,
)?),
_ => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedCompressionMethod(
CompressionMethod::SgiLog,
),
))
}
},
CompressionMethod::SgiLog24 => Box::new(super::logluv::LogLuv24::new(
reader,
compressed_length,
dimensions.0,
)?),
#[cfg(feature = "lzw")]
CompressionMethod::LZW => Box::new(super::stream::LZWReader::new(
reader,
@@ -745,13 +607,6 @@ 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),
@@ -950,7 +805,7 @@ impl Image {
pub(crate) fn expand_chunk(
&self,
reader: &mut ValueReader<impl Read + Seek>,
reader: &mut ValueReader<impl Read>,
buf: &mut [u8],
layout: &ReadoutLayout,
chunk_index: u32,
@@ -961,12 +816,7 @@ impl Image {
limits,
} = reader;
let decompressed_byte_order = if self.decompression_to_host_endian {
ByteOrder::native()
} else {
// Convert from the byte order of the file.
reader.byte_order
};
let byte_order = reader.byte_order;
// Validate that the color type is supported.
let color_type = layout.color;
@@ -1069,7 +919,6 @@ impl Image {
*compressed_bytes,
self.jpeg_tables.as_deref().map(|a| &**a),
chunk_dims,
self.samples,
)?;
if is_output_chunk_rows && is_all_bits {
@@ -1082,7 +931,7 @@ impl Image {
row,
color_type.bit_depth(),
samples,
decompressed_byte_order,
byte_order,
predictor,
);
}
@@ -1128,7 +977,7 @@ impl Image {
row,
color_type.bit_depth(),
samples,
decompressed_byte_order,
byte_order,
predictor,
);
@@ -1149,7 +998,7 @@ impl Image {
debug_assert!(bits_per_pixel >= photometric_bit_end);
if !bits_per_pixel.is_multiple_of(8) || !photometric_bit_end.is_multiple_of(8) {
if bits_per_pixel % 8 != 0 || photometric_bit_end % 8 != 0 {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
@@ -1169,7 +1018,7 @@ impl Image {
row,
color_type.bit_depth(),
samples,
decompressed_byte_order,
byte_order,
predictor,
);
-541
View File
@@ -1,541 +0,0 @@
use std::io::{Read, Take};
use crate::TiffError;
pub struct LogLuv24<R> {
reader: Take<R>,
/// The required buffer size. We only decompressed all samples at once.
chunk_sz: usize,
}
impl<R> LogLuv24<R> {
pub fn new(reader: R, n: u64, npixels: u32) -> Result<Self, TiffError>
where
R: Read,
{
let chunk_sz = usize::try_from(
u64::from(npixels)
.checked_mul(4 * 3)
.ok_or(TiffError::LimitsExceeded)?,
)?;
let reader = reader.take(n);
Ok(LogLuv24 { reader, chunk_sz })
}
fn expand_buf(buf: &mut [u8]) {
let data_len = buf.len() / 4;
for i in (0..data_len / 3).rev() {
let ch = buf.as_chunks_mut::<3>().0[i];
let [a, b, c] = ch.map(u32::from);
let raw = a << 16 | b << 8 | c;
let l = (raw >> 14) & 0x3ff;
let uv = decode_quantized_uv(raw & 0x3fff);
let by = dequantize_24_luma(l);
let (u, v) = dequantize_24(uv);
let s = 1. / (6. * u - 16. * v + 12.);
let x = 9. * u * s;
let y = 4. * v * s;
let bx = x / y * by;
let bz = (1. - x - y) / y * by;
let [r, g, b] = xyz_to_rgb([bx, by, bz]);
let into = &mut buf.as_chunks_mut::<12>().0[i];
into[0..4].copy_from_slice(&f32::to_ne_bytes(r));
into[4..8].copy_from_slice(&f32::to_ne_bytes(g));
into[8..12].copy_from_slice(&f32::to_ne_bytes(b));
}
}
}
impl<R: Read> Read for LogLuv24<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.read_exact(buf)?;
Ok(buf.len())
}
// FIXME: this is the interface we have for compressed chunks but it is not optimal for what we
// want. Note `fill_buf` is not super helpful since it does not guarantee any size of the
// filled buffer but we need 24-bits at a time. Two code paths is even more awkward.
fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
if !buf.len().is_multiple_of(self.chunk_sz) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"buffer size {0} must match the chunk size for compression method SGILog24 {1}",
buf.len(),
self.chunk_sz,
),
));
}
let data_len = buf.len() / 4;
self.reader.read_exact(&mut buf[..data_len])?;
Self::expand_buf(buf);
Ok(())
}
}
fn packbits_decode<R: Read, const N: usize>(
reader: &mut R,
buf: &mut [[u8; N]],
) -> std::io::Result<()> {
for offset in (0..N).rev() {
let mut i = 0;
let mut code = [0u8; 1];
loop {
let Some(pixels) = buf.get_mut(i..) else {
break;
};
if pixels.is_empty() {
break;
}
let pixels = pixels.iter_mut();
reader.read_exact(&mut code)?;
let [repeat] = code;
if repeat >= 128 {
reader.read_exact(&mut code)?;
let [value] = code;
let rc = (repeat - 128) + 2;
for (_, px) in (0..rc).zip(pixels) {
px[offset] = value;
}
i += usize::from(rc);
} else {
for (_, px) in (0..repeat).zip(pixels) {
reader.read_exact(&mut code)?;
let [value] = code;
px[offset] = value;
}
i += usize::from(repeat);
}
}
}
Ok(())
}
pub struct LogLuv32<R> {
reader: Take<R>,
/// The required buffer size. We only decompressed all samples at once.
chunk_sz: usize,
}
impl<R: Read> LogLuv32<R> {
pub fn new(reader: R, n: u64, npixels: u32) -> Result<Self, TiffError> {
let chunk_sz = usize::try_from(
u64::from(npixels)
.checked_mul(4 * 3)
.ok_or(TiffError::LimitsExceeded)?,
)?;
let reader = reader.take(n);
Ok(LogLuv32 { reader, chunk_sz })
}
fn decode_rle_row(&mut self, row: &mut [u8]) -> std::io::Result<()>
where
R: Read,
{
// 32-bit per pixel format.
let n_pixels = self.chunk_sz / 12;
// buffer for reading (using only 32-bit per pixel)
let raw = &mut row.as_chunks_mut::<4>().0[..n_pixels];
packbits_decode::<_, 4>(&mut self.reader, raw)?;
Ok(())
}
fn expand_row(row: &mut [u8]) {
let uvscale = 1.0 / 410.0;
let n_pixels = row.len() / 12;
// Work backwards, the 32-bit compressed raw data is at the front of the row buffer and
// this avoids overwriting data before we read it.
for i in (0..n_pixels).rev() {
let raw = row.as_chunks_mut::<4>().0[i];
let l = u16::from(raw[0]) << 8 | u16::from(raw[1]);
let by = log16_to_y(l);
let u = uvscale * (f32::from(raw[2]) + 0.5);
let v = uvscale * (f32::from(raw[3]) + 0.5);
let s = 1. / (6. * u - 16. * v + 12.);
let x = 9. * u * s;
let y = 4. * v * s;
let bx = x / y * by;
let bz = (1. - x - y) / y * by;
let [r, g, b] = xyz_to_rgb([bx, by, bz]);
let into = &mut row.as_chunks_mut::<12>().0[i];
into[0..4].copy_from_slice(&f32::to_ne_bytes(r));
into[4..8].copy_from_slice(&f32::to_ne_bytes(g));
into[8..12].copy_from_slice(&f32::to_ne_bytes(b));
}
}
}
impl<R: Read> Read for LogLuv32<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.read_exact(buf)?;
Ok(buf.len())
}
// FIXME: See `LogLuv24::read_exact`
fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
for row in buf.chunks_exact_mut(self.chunk_sz) {
self.decode_rle_row(row)?;
Self::expand_row(row);
}
Ok(())
}
}
/// Pack-Bits like encoding for a single 16-bit log-L sample.
pub struct LogLuv16<R> {
reader: Take<R>,
/// The required buffer size. We only decompressed all samples at once.
chunk_sz: usize,
}
impl<R: Read> LogLuv16<R> {
pub fn new(reader: R, n: u64, npixels: u32) -> Result<Self, TiffError> {
let chunk_sz = usize::try_from(
u64::from(npixels)
.checked_mul(4)
.ok_or(TiffError::LimitsExceeded)?,
)?;
let reader = reader.take(n);
Ok(LogLuv16 { reader, chunk_sz })
}
fn decode_rle_row(&mut self, row: &mut [u8]) -> std::io::Result<()>
where
R: Read,
{
// 32-bit per pixel format.
let n_pixels = self.chunk_sz / 4;
// buffer for reading (16-bit per pixel)
let raw = &mut row.as_chunks_mut::<2>().0[..n_pixels];
packbits_decode::<_, 2>(&mut self.reader, raw)?;
Ok(())
}
fn expand_row(row: &mut [u8]) {
let n_pixels = row.len() / 4;
// Work backwards, the 32-bit compressed raw data is at the front of the row buffer and
// this avoids overwriting data before we read it.
for i in (0..n_pixels).rev() {
let raw = row.as_chunks_mut::<2>().0[i];
let l = u16::from(raw[0]) << 8 | u16::from(raw[1]);
let by = log16_to_y(l);
let into = &mut row.as_chunks_mut::<4>().0[i];
into[0..4].copy_from_slice(&f32::to_ne_bytes(by));
}
}
}
impl<R: Read> Read for LogLuv16<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.read_exact(buf)?;
Ok(buf.len())
}
// FIXME: See `LogLuv24::read_exact`
fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
for row in buf.chunks_exact_mut(self.chunk_sz) {
self.decode_rle_row(row)?;
Self::expand_row(row);
}
Ok(())
}
}
// Table information from: `libtiff`, 1997 by Greg Ward Larson, SGI
#[doc(alias = "UV_SQSIZ")]
const UV_QUANTIZATION_WIDTH: f32 = 0.003500;
// The `v` value corresonding to the bottom most row (quantized v' = 0).
const UV_VSTART: f32 = 0.016940;
/// Number of distinct 'u' rows, each at a different v level. Each is quantized individually with
/// the first quantization level starting at its `ustart` and `UV_QUANTIZATION_WIDTH` wide.
const UV_NVS: usize = 163;
/// Contrary to the original C code we calculate the dependent values (cumulative sum).
struct UvRow {
ustart: f32,
num_us: u16,
}
#[rustfmt::skip]
const UV_ROWS: [UvRow; UV_NVS] = [
UvRow { ustart: 0.247663, num_us: 4, },
UvRow { ustart: 0.243779, num_us: 6, },
UvRow { ustart: 0.241684, num_us: 7, },
UvRow { ustart: 0.237874, num_us: 9, },
UvRow { ustart: 0.235906, num_us: 10, },
UvRow { ustart: 0.232153, num_us: 12, },
UvRow { ustart: 0.228352, num_us: 14, },
UvRow { ustart: 0.226259, num_us: 15, },
UvRow { ustart: 0.222371, num_us: 17, },
UvRow { ustart: 0.220410, num_us: 18, },
UvRow { ustart: 0.214710, num_us: 21, },
UvRow { ustart: 0.212714, num_us: 22, },
UvRow { ustart: 0.210721, num_us: 23, },
UvRow { ustart: 0.204976, num_us: 26, },
UvRow { ustart: 0.202986, num_us: 27, },
UvRow { ustart: 0.199245, num_us: 29, },
UvRow { ustart: 0.195525, num_us: 31, },
UvRow { ustart: 0.193560, num_us: 32, },
UvRow { ustart: 0.189878, num_us: 34, },
UvRow { ustart: 0.186216, num_us: 36, },
UvRow { ustart: 0.186216, num_us: 36, },
UvRow { ustart: 0.182592, num_us: 38, },
UvRow { ustart: 0.179003, num_us: 40, },
UvRow { ustart: 0.175466, num_us: 42, },
UvRow { ustart: 0.172001, num_us: 44, },
UvRow { ustart: 0.172001, num_us: 44, },
UvRow { ustart: 0.168612, num_us: 46, },
UvRow { ustart: 0.168612, num_us: 46, },
UvRow { ustart: 0.163575, num_us: 49, },
UvRow { ustart: 0.158642, num_us: 52, },
UvRow { ustart: 0.158642, num_us: 52, },
UvRow { ustart: 0.158642, num_us: 52, },
UvRow { ustart: 0.153815, num_us: 55, },
UvRow { ustart: 0.153815, num_us: 55, },
UvRow { ustart: 0.149097, num_us: 58, },
UvRow { ustart: 0.149097, num_us: 58, },
UvRow { ustart: 0.142746, num_us: 62, },
UvRow { ustart: 0.142746, num_us: 62, },
UvRow { ustart: 0.142746, num_us: 62, },
UvRow { ustart: 0.138270, num_us: 65, },
UvRow { ustart: 0.138270, num_us: 65, },
UvRow { ustart: 0.138270, num_us: 65, },
UvRow { ustart: 0.132166, num_us: 69, },
UvRow { ustart: 0.132166, num_us: 69, },
UvRow { ustart: 0.126204, num_us: 73, },
UvRow { ustart: 0.126204, num_us: 73, },
UvRow { ustart: 0.126204, num_us: 73, },
UvRow { ustart: 0.120381, num_us: 77, },
UvRow { ustart: 0.120381, num_us: 77, },
UvRow { ustart: 0.120381, num_us: 77, },
UvRow { ustart: 0.120381, num_us: 77, },
UvRow { ustart: 0.112962, num_us: 82, },
UvRow { ustart: 0.112962, num_us: 82, },
UvRow { ustart: 0.112962, num_us: 82, },
UvRow { ustart: 0.107450, num_us: 86, },
UvRow { ustart: 0.107450, num_us: 86, },
UvRow { ustart: 0.107450, num_us: 86, },
UvRow { ustart: 0.107450, num_us: 86, },
UvRow { ustart: 0.100343, num_us: 91, },
UvRow { ustart: 0.100343, num_us: 91, },
UvRow { ustart: 0.100343, num_us: 91, },
UvRow { ustart: 0.095126, num_us: 95, },
UvRow { ustart: 0.095126, num_us: 95, },
UvRow { ustart: 0.095126, num_us: 95, },
UvRow { ustart: 0.095126, num_us: 95, },
UvRow { ustart: 0.088276, num_us: 100, },
UvRow { ustart: 0.088276, num_us: 100, },
UvRow { ustart: 0.088276, num_us: 100, },
UvRow { ustart: 0.088276, num_us: 100, },
UvRow { ustart: 0.081523, num_us: 105, },
UvRow { ustart: 0.081523, num_us: 105, },
UvRow { ustart: 0.081523, num_us: 105, },
UvRow { ustart: 0.081523, num_us: 105, },
UvRow { ustart: 0.074861, num_us: 110, },
UvRow { ustart: 0.074861, num_us: 110, },
UvRow { ustart: 0.074861, num_us: 110, },
UvRow { ustart: 0.074861, num_us: 110, },
UvRow { ustart: 0.068290, num_us: 115, },
UvRow { ustart: 0.068290, num_us: 115, },
UvRow { ustart: 0.068290, num_us: 115, },
UvRow { ustart: 0.068290, num_us: 115, },
UvRow { ustart: 0.063573, num_us: 119, },
UvRow { ustart: 0.063573, num_us: 119, },
UvRow { ustart: 0.063573, num_us: 119, },
UvRow { ustart: 0.063573, num_us: 119, },
UvRow { ustart: 0.057219, num_us: 124, },
UvRow { ustart: 0.057219, num_us: 124, },
UvRow { ustart: 0.057219, num_us: 124, },
UvRow { ustart: 0.057219, num_us: 124, },
UvRow { ustart: 0.050985, num_us: 129, },
UvRow { ustart: 0.050985, num_us: 129, },
UvRow { ustart: 0.050985, num_us: 129, },
UvRow { ustart: 0.050985, num_us: 129, },
UvRow { ustart: 0.050985, num_us: 129, },
UvRow { ustart: 0.044859, num_us: 134, },
UvRow { ustart: 0.044859, num_us: 134, },
UvRow { ustart: 0.044859, num_us: 134, },
UvRow { ustart: 0.044859, num_us: 134, },
UvRow { ustart: 0.040571, num_us: 138, },
UvRow { ustart: 0.040571, num_us: 138, },
UvRow { ustart: 0.040571, num_us: 138, },
UvRow { ustart: 0.040571, num_us: 138, },
UvRow { ustart: 0.036339, num_us: 142, },
UvRow { ustart: 0.036339, num_us: 142, },
UvRow { ustart: 0.036339, num_us: 142, },
UvRow { ustart: 0.036339, num_us: 142, },
UvRow { ustart: 0.032139, num_us: 146, },
UvRow { ustart: 0.032139, num_us: 146, },
UvRow { ustart: 0.032139, num_us: 146, },
UvRow { ustart: 0.032139, num_us: 146, },
UvRow { ustart: 0.027947, num_us: 150, },
UvRow { ustart: 0.027947, num_us: 150, },
UvRow { ustart: 0.027947, num_us: 150, },
UvRow { ustart: 0.023739, num_us: 154, },
UvRow { ustart: 0.023739, num_us: 154, },
UvRow { ustart: 0.023739, num_us: 154, },
UvRow { ustart: 0.023739, num_us: 154, },
UvRow { ustart: 0.019504, num_us: 158, },
UvRow { ustart: 0.019504, num_us: 158, },
UvRow { ustart: 0.019504, num_us: 158, },
UvRow { ustart: 0.016976, num_us: 161, },
UvRow { ustart: 0.016976, num_us: 161, },
UvRow { ustart: 0.016976, num_us: 161, },
UvRow { ustart: 0.016976, num_us: 161, },
UvRow { ustart: 0.012639, num_us: 165, },
UvRow { ustart: 0.012639, num_us: 165, },
UvRow { ustart: 0.012639, num_us: 165, },
UvRow { ustart: 0.009991, num_us: 168, },
UvRow { ustart: 0.009991, num_us: 168, },
UvRow { ustart: 0.009991, num_us: 168, },
UvRow { ustart: 0.009016, num_us: 170, },
UvRow { ustart: 0.009016, num_us: 170, },
UvRow { ustart: 0.009016, num_us: 170, },
UvRow { ustart: 0.006217, num_us: 173, },
UvRow { ustart: 0.006217, num_us: 173, },
UvRow { ustart: 0.005097, num_us: 175, },
UvRow { ustart: 0.005097, num_us: 175, },
UvRow { ustart: 0.005097, num_us: 175, },
UvRow { ustart: 0.003909, num_us: 177, },
UvRow { ustart: 0.003909, num_us: 177, },
UvRow { ustart: 0.002340, num_us: 177, },
UvRow { ustart: 0.002389, num_us: 170, },
UvRow { ustart: 0.001068, num_us: 164, },
UvRow { ustart: 0.001653, num_us: 157, },
UvRow { ustart: 0.000717, num_us: 150, },
UvRow { ustart: 0.001614, num_us: 143, },
UvRow { ustart: 0.000270, num_us: 136, },
UvRow { ustart: 0.000484, num_us: 129, },
UvRow { ustart: 0.001103, num_us: 123, },
UvRow { ustart: 0.001242, num_us: 115, },
UvRow { ustart: 0.001188, num_us: 109, },
UvRow { ustart: 0.001011, num_us: 103, },
UvRow { ustart: 0.000709, num_us: 97, },
UvRow { ustart: 0.000301, num_us: 89, },
UvRow { ustart: 0.002416, num_us: 82, },
UvRow { ustart: 0.003251, num_us: 76, },
UvRow { ustart: 0.003246, num_us: 69, },
UvRow { ustart: 0.004141, num_us: 62, },
UvRow { ustart: 0.005963, num_us: 55, },
UvRow { ustart: 0.008839, num_us: 47, },
UvRow { ustart: 0.010490, num_us: 40, },
UvRow { ustart: 0.016994, num_us: 31, },
UvRow { ustart: 0.023659, num_us: 21, },
];
const UV_CUMULATIVE: [u16; UV_NVS] = {
let mut arr = [0u16; UV_NVS];
let mut sum = 0;
let mut i = 0;
while i < UV_NVS {
arr[i] = sum;
sum += UV_ROWS[i].num_us;
i += 1;
}
arr
};
const UV_NDIVS: u16 = UV_CUMULATIVE[UV_NVS - 1] + UV_ROWS[UV_NVS - 1].num_us;
fn decode_quantized_uv(uv: u32) -> (u8, u8) {
if uv >= UV_NDIVS as u32 {
return (0, 0);
}
// Find the start of the row, or the index after that.
let v_index = match UV_CUMULATIVE.binary_search(&(uv as u16)) {
Ok(n) => n,
Err(n) => n - 1,
};
let u_index = uv as u16 - UV_CUMULATIVE[v_index];
(u_index as u8, v_index as u8)
}
fn dequantize_24_luma(l: u32) -> f32 {
if l == 0 {
0.0
} else {
let le = (l & 0x3ff) as f32;
let exponent = ((le / 64.0).floor() as i32) - 24;
let mantissa = le - (exponent as f32 + 24.0) * 64.0;
(mantissa + 64.0) / 64.0 * 2f32.powi(exponent)
}
}
fn dequantize_24((u, v): (u8, u8)) -> (f32, f32) {
let v_f = UV_VSTART + (v as f32) * UV_QUANTIZATION_WIDTH;
let u_row = &UV_ROWS[v as usize];
let u_f = u_row.ustart + (u as f32) * UV_QUANTIZATION_WIDTH;
(u_f, v_f)
}
fn log16_to_y(l: u16) -> f32 {
if l == 0 {
return 0.0;
}
let le = f32::from(l & 0x7fff);
// Y = exp(M_LN2 / 256. * (Le + .5) - M_LN2 * 64.);
let y = (std::f32::consts::LN_2 / 256.0 * (le + 0.5) - std::f32::consts::LN_2 * 64.0).exp();
if l & 0x8000 != 0 {
-y
} else {
y
}
}
fn xyz_to_rgb([l, u, v]: [f32; 3]) -> [f32; 3] {
// XYZ to sRGB primaries at D65
let r = 3.2404542 * l + -1.5371385 * u + -0.4985314 * v;
let g = -0.969266 * l + 1.8760108 * u + 0.0415560 * v;
let b = 0.0556434 * l + -0.2040259 * u + 1.0572252 * v;
[r, g, b]
}
#[test]
fn test_decode_quantized_uv() {
// Test some known values from the original C code.
assert_eq!(decode_quantized_uv(0), (0, 0));
assert_eq!(decode_quantized_uv(3), (3, 0));
assert_eq!(decode_quantized_uv(4), (0, 1));
assert_eq!(decode_quantized_uv(9), (5, 1));
assert_eq!(decode_quantized_uv(16288), (20, 162));
assert_eq!(decode_quantized_uv(16289), (0, 0)); // Out of range
}
+171 -383
View File
@@ -4,8 +4,8 @@ use std::io::{self, Read, Seek};
use std::num::NonZeroUsize;
use crate::tags::{
ByteOrder, IfdPointer, PlanarConfiguration, Predictor, SampleFormat, Tag, TiffVariant, Type,
ValueBuffer,
CompressionMethod, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor,
SampleFormat, Tag, Type, ValueBuffer,
};
use crate::{
bytecast, ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError,
@@ -14,12 +14,11 @@ use crate::{
use half::f16;
use self::image::Image;
use self::stream::EndianReader;
use self::stream::{ByteOrder, EndianReader};
mod cycles;
pub mod ifd;
mod image;
mod logluv;
mod stream;
mod tag_reader;
@@ -505,14 +504,6 @@ impl Default for Limits {
}
}
/// Describe the header of TIFF file, including where to find the first image directory.
#[derive(Debug)]
pub struct TiffHeader {
pub byte_order: ByteOrder,
pub variant: TiffVariant,
pub first_ifd: IfdPointer,
}
/// The representation of a TIFF decoder
///
/// Currently does not support decoding of interlaced images
@@ -524,8 +515,8 @@ where
/// There are grouped for borrow checker reasons. This allows us to implement methods that
/// borrow the stream access and the other fields mutably at the same time.
value_reader: ValueReader<R>,
current_ifd_pointer: Option<IfdPointer>,
directory: Directory,
current_ifd: Option<IfdPointer>,
next_ifd: Option<IfdPointer>,
/// The IFDs we visited already in this chain of IFDs.
ifd_offsets: Vec<IfdPointer>,
/// Map from the ifd into the `ifd_offsets` ordered list.
@@ -544,8 +535,6 @@ struct ValueReader<R> {
}
/// Reads a directory's tag values from an underlying stream.
///
/// Created by [`Decoder::current_ifd`] and [`Decoder::read_directory_tags`].
pub struct IfdDecoder<'lt> {
inner: tag_reader::TagReader<'lt, dyn tag_reader::EntryDecoder + 'lt>,
}
@@ -727,29 +716,46 @@ fn invert_colors(
sample_format: SampleFormat,
) -> TiffResult<()> {
match (color_type, sample_format) {
(ColorType::Gray(1 | 2 | 4 | 8 | 16 | 32 | 64), SampleFormat::Uint) => {
// It's guaranteed that pixels do not cross a byte boundary.
// To invert a single N-bit pixel, we can calculate:
// (2^N - 1) - x = (2^N - 1) + !x + 1 (because -x = !x + 1)
// = 2^n + !x
// = !x (because modular arithmetic)
//
// Further, since !x operates on bits independently, negating bytes
// also works for multi-byte pixels.
// Where pixels do not cross a byte boundary
(ColorType::Gray(1 | 2 | 4 | 8), SampleFormat::Uint) => {
for x in buf {
// Equivalent to both of the following:
//
// *x = 0xff - *x
// *x = !*x
//
// since -x = !x+1
*x = !*x;
}
}
(ColorType::Gray(16), SampleFormat::Uint) => {
for x in buf.chunks_mut(2) {
let v = u16::from_ne_bytes(x.try_into().unwrap());
x.copy_from_slice(&(0xffff - v).to_ne_bytes());
}
}
(ColorType::Gray(32), SampleFormat::Uint) => {
for x in buf.chunks_mut(4) {
let v = u32::from_ne_bytes(x.try_into().unwrap());
x.copy_from_slice(&(0xffff_ffff - v).to_ne_bytes());
}
}
(ColorType::Gray(64), SampleFormat::Uint) => {
for x in buf.chunks_mut(8) {
let v = u64::from_ne_bytes(x.try_into().unwrap());
x.copy_from_slice(&(0xffff_ffff_ffff_ffff - v).to_ne_bytes());
}
}
(ColorType::Gray(32), SampleFormat::IEEEFP) => {
for x in buf.as_chunks_mut::<4>().0 {
let v = f32::from_ne_bytes(*x);
*x = (1.0 - v).to_ne_bytes();
for x in buf.chunks_mut(4) {
let v = f32::from_ne_bytes(x.try_into().unwrap());
x.copy_from_slice(&(1.0 - v).to_ne_bytes());
}
}
(ColorType::Gray(64), SampleFormat::IEEEFP) => {
for x in buf.as_chunks_mut::<8>().0 {
let v = f64::from_ne_bytes(*x);
*x = (1.0 - v).to_ne_bytes();
for x in buf.chunks_mut(8) {
let v = f64::from_ne_bytes(x.try_into().unwrap());
x.copy_from_slice(&(1.0 - v).to_ne_bytes());
}
}
_ => {
@@ -776,8 +782,8 @@ fn fix_endianness(buf: &mut [u8], byte_order: ByteOrder, bit_depth: u8) {
host.convert_endian_bytes(class, buf, byte_order);
}
impl TiffHeader {
pub fn parse<R: Read + Seek>(mut r: R) -> TiffResult<Self> {
impl<R: Read + Seek> Decoder<R> {
pub fn new(mut r: R) -> TiffResult<Decoder<R>> {
let mut endianess = Vec::with_capacity(2);
(&mut r).take(2).read_to_end(&mut endianess)?;
let byte_order = match &*endianess {
@@ -789,7 +795,6 @@ impl TiffHeader {
))
}
};
let mut reader = EndianReader::new(r, byte_order);
let bigtiff = match reader.read_u16()? {
@@ -816,155 +821,118 @@ impl TiffHeader {
}
};
let next_ifd = IfdPointer(if bigtiff {
reader.read_u64()?
let next_ifd = if bigtiff {
Some(reader.read_u64()?)
} else {
u64::from(reader.read_u32()?)
});
Some(u64::from(reader.read_u32()?))
}
.map(IfdPointer);
Ok(TiffHeader {
byte_order,
variant: if bigtiff {
TiffVariant::BigTiff
} else {
TiffVariant::Standard
},
first_ifd: next_ifd,
})
}
let current_ifd = *next_ifd.as_ref().unwrap();
let ifd_offsets = vec![current_ifd];
/// Open a file, without seeking to any IFD yet.
pub fn open<R: Read + Seek>(&self, r: R) -> Decoder<R> {
let reader = EndianReader::new(r, self.byte_order);
Decoder {
let mut decoder = Decoder {
value_reader: ValueReader {
reader,
bigtiff: matches!(self.variant, TiffVariant::BigTiff),
bigtiff,
limits: Default::default(),
},
ifd_offsets: vec![],
current_ifd_pointer: None,
directory: {
let mut dir = Directory::empty();
dir.set_next(Some(self.first_ifd));
dir
},
next_ifd,
ifd_offsets,
current_ifd: None,
seen_ifds: cycles::IfdCycles::new(),
image: Image::no_image(),
}
}
}
impl<R: Read + Seek> Decoder<R> {
/// Create a decoder and attempt to parse the first image directory.
///
/// To open a file without parsing any directory yet (for instance to retain a [`Decoder`] on
/// errors in the directory), use [`TiffHeader::parse`] and [`TiffHeader::open`] instead..
#[deprecated = "Prefer `Decoder::open`. These methods will become aliases in a future major release."]
pub fn new(mut r: R) -> TiffResult<Decoder<R>> {
let header = TiffHeader::parse(&mut r)?;
let mut decoder = header.open(r);
image: Image {
ifd: None,
width: 0,
height: 0,
bits_per_sample: 1,
samples: 1,
extra_samples: vec![],
photometric_samples: 1,
sample_format: SampleFormat::Uint,
photometric_interpretation: PhotometricInterpretation::BlackIsZero,
compression_method: CompressionMethod::None,
jpeg_tables: None,
predictor: Predictor::None,
chunk_type: ChunkType::Strip,
planar_config: PlanarConfiguration::Chunky,
strip_decoder: None,
tile_attributes: None,
chunk_offsets: Vec::new(),
chunk_bytes: Vec::new(),
chroma_subsampling: (2, 2),
},
};
decoder.next_image()?;
Ok(decoder)
}
/// Parse the image header from the underlying file and create a decoder positioned before its
/// first directory.
///
/// The [`TiffHeader`] type allows opening a file with a custom.
pub fn open(mut r: R) -> TiffResult<Decoder<R>> {
let header = TiffHeader::parse(&mut r)?;
Ok(header.open(r))
}
/// Configure allocation limits for all subsequent operations.
///
/// Limits apply to allocated return values such as from [`Decoder::read_image`] and
/// reallocations on [`ValueBuffer`] and [`DecodingResult`] arguments when they are passed as
/// mutable references to methods such as [`Decoder::read_image_to_buffer`]. They carry over to
/// the decoder returned form [`Self::current_ifd`].
pub fn with_limits(mut self, limits: Limits) -> Decoder<R> {
self.value_reader.limits = limits;
self
}
pub fn dimensions(&mut self) -> TiffResult<(u32, u32)> {
Ok((self.image.width, self.image.height))
Ok((self.image().width, self.image().height))
}
pub fn colortype(&mut self) -> TiffResult<ColorType> {
self.image.colortype()
self.image().colortype()
}
/// The offset of the directory representing the current image.
pub fn ifd_pointer(&mut self) -> Option<IfdPointer> {
self.current_ifd_pointer
self.current_ifd
}
fn image(&self) -> &Image {
&self.image
}
/// Loads the IFD at the specified index in the list, if one exists
///
/// Error guarantee: the current directory and [`Self::ifd_pointer`] is not modified when an
/// error is returned.
pub fn seek_to_directory(&mut self, ifd_index: usize) -> TiffResult<()> {
let (offset, directory) = self.find_nth_ifd(ifd_index)?;
self.image = Image::no_image();
self.current_ifd_pointer = Some(offset);
self.directory = directory;
Ok(())
}
/// Loads the IFD at the specified index in the list, if one exists
///
/// The directory is then decoded as an image.
///
/// Error guarantee: the current directory and [`Self::ifd_pointer`] is not modified when an
/// error is returned.
pub fn seek_to_image(&mut self, ifd_index: usize) -> TiffResult<()> {
let (offset, directory) = self.find_nth_ifd(ifd_index)?;
self.image = Image::from_reader(&mut self.value_reader, &directory)?;
self.current_ifd_pointer = Some(offset);
self.directory = directory;
Ok(())
}
// Check whether we have seen this IFD before, if so then the index will be less than the length of the list of ifd offsets
if ifd_index >= self.ifd_offsets.len() {
// We possibly need to load in the next IFD
if self.next_ifd.is_none() {
self.current_ifd = None;
/// Change the directory to the nth in the linked-list chain of IFDs.
fn find_nth_ifd(&mut self, ifd_index: usize) -> TiffResult<(IfdPointer, Directory)> {
// Check whether we have seen this IFD before, if so then the index will be less than the
// length of the list of ifd offsets If the index is within the list of ifds then we can
// load the selected image/IFD. Otherwise we iterate the chain until the selected index.
if let Some(&ifd_offset) = self.ifd_offsets.get(ifd_index) {
let directory = self.value_reader.read_directory(ifd_offset)?;
Ok((ifd_offset, directory))
} else {
// We do not know if we are at the last IFD in the chain if we previous did seek back.
// The `read_next_ifd_pointer` correctly handles this.
let mut chained_ifd = self.current_ifd_pointer;
let mut next_ifd = self.directory.next();
return Err(TiffError::FormatError(
TiffFormatError::ImageFileDirectoryNotFound,
));
}
loop {
// Follow the list until we find the one we want, or we reach the end, whichever happens first
let (ifd_pointer, ifd) = self.read_chained_ifd(chained_ifd, next_ifd)?;
chained_ifd = Some(ifd_pointer);
next_ifd = ifd.next();
let ifd = self.next_ifd()?;
if ifd.next().is_none() {
break;
}
if ifd_index < self.ifd_offsets.len() {
// We only end up here if initially `ifd_index >= self.ifd_offsets.len()` and
// each loop iteration increases the length of `self.ifd_offsets` by (at most)
// one.
debug_assert!(ifd_index + 1 == self.ifd_offsets.len());
return Ok((ifd_pointer, ifd));
break;
}
}
}
// If the index is within the list of ifds then we can load the selected image/IFD
if let Some(ifd_offset) = self.ifd_offsets.get(ifd_index) {
let ifd = self.value_reader.read_directory(*ifd_offset)?;
self.next_ifd = ifd.next();
self.current_ifd = Some(*ifd_offset);
self.image = Image::from_reader(&mut self.value_reader, ifd)?;
Ok(())
} else {
Err(TiffError::FormatError(
TiffFormatError::ImageFileDirectoryNotFound,
))
}
}
fn read_chained_ifd(
&mut self,
current_ifd_pointer: Option<IfdPointer>,
next_ifd: Option<IfdPointer>,
) -> TiffResult<(IfdPointer, Directory)> {
let Some(next_ifd) = next_ifd else {
fn next_ifd(&mut self) -> TiffResult<Directory> {
let Some(next_ifd) = self.next_ifd.take() else {
return Err(TiffError::FormatError(
TiffFormatError::ImageFileDirectoryNotFound,
));
@@ -976,48 +944,28 @@ impl<R: Read + Seek> Decoder<R> {
self.seen_ifds.insert_next(next_ifd, ifd.next())?;
// Extend the list of known IFD offsets in this chain, if needed.
if self.ifd_offsets.last().copied() == current_ifd_pointer {
if self.ifd_offsets.last().copied() == self.current_ifd {
self.ifd_offsets.push(next_ifd);
}
Ok((next_ifd, ifd))
}
self.current_ifd = Some(next_ifd);
self.next_ifd = ifd.next();
/// Reads the next IFD.
///
/// If there is no further image in the TIFF file a format error is returned.
/// To determine whether there are more images call `TIFFDecoder::more_images` instead.
///
/// Error guarantee: the current directory and [`Self::ifd_pointer`] is not modified when an error is
/// returned.
pub fn next_directory(&mut self) -> TiffResult<()> {
let next_ifd = self.directory.next();
let (offset, directory) = self.read_chained_ifd(self.current_ifd_pointer, next_ifd)?;
self.image = Image::no_image();
self.current_ifd_pointer = Some(offset);
self.directory = directory;
Ok(())
Ok(ifd)
}
/// Reads in the next image.
///
/// If there is no further image in the TIFF file a format error is returned.
/// To determine whether there are more images call `TIFFDecoder::more_images` instead.
///
/// Error guarantee: the current directory and [`Self::ifd_pointer`] is not modified when an error is
/// returned.
pub fn next_image(&mut self) -> TiffResult<()> {
let next_ifd = self.directory.next();
let (offset, directory) = self.read_chained_ifd(self.current_ifd_pointer, next_ifd)?;
self.image = Image::from_reader(&mut self.value_reader, &directory)?;
self.current_ifd_pointer = Some(offset);
self.directory = directory;
let ifd = self.next_ifd()?;
self.image = Image::from_reader(&mut self.value_reader, ifd)?;
Ok(())
}
/// Returns `true` if there is at least one more image available.
pub fn more_images(&self) -> bool {
self.directory.next().is_some()
self.next_ifd.is_some()
}
/// Returns the byte_order of the file.
@@ -1166,10 +1114,10 @@ impl<R: Read + Seek> Decoder<R> {
}
fn check_chunk_type(&self, expected: ChunkType) -> TiffResult<()> {
if expected != self.image.chunk_type {
if expected != self.image().chunk_type {
return Err(TiffError::UsageError(UsageError::InvalidChunkType(
expected,
self.image.chunk_type,
self.image().chunk_type,
)));
}
@@ -1178,27 +1126,27 @@ impl<R: Read + Seek> Decoder<R> {
/// The chunk type (Strips / Tiles) of the image
pub fn get_chunk_type(&self) -> ChunkType {
self.image.chunk_type
self.image().chunk_type
}
/// Number of strips in image
pub fn strip_count(&mut self) -> TiffResult<u32> {
self.check_chunk_type(ChunkType::Strip)?;
let rows_per_strip = self.image.strip_decoder.as_ref().unwrap().rows_per_strip;
let rows_per_strip = self.image().strip_decoder.as_ref().unwrap().rows_per_strip;
if rows_per_strip == 0 {
return Ok(0);
}
// rows_per_strip - 1 can never fail since we know it's at least 1
let height = match self.image.height.checked_add(rows_per_strip - 1) {
let height = match self.image().height.checked_add(rows_per_strip - 1) {
Some(h) => h,
None => return Err(TiffError::IntSizeError),
};
let strips = match self.image.planar_config {
let strips = match self.image().planar_config {
PlanarConfiguration::Chunky => height / rows_per_strip,
PlanarConfiguration::Planar => height / rows_per_strip * self.image.samples as u32,
PlanarConfiguration::Planar => height / rows_per_strip * self.image().samples as u32,
};
Ok(strips)
@@ -1207,7 +1155,7 @@ impl<R: Read + Seek> Decoder<R> {
/// Number of tiles in image
pub fn tile_count(&mut self) -> TiffResult<u32> {
self.check_chunk_type(ChunkType::Tile)?;
Ok(u32::try_from(self.image.chunk_offsets.len())?)
Ok(u32::try_from(self.image().chunk_offsets.len())?)
}
fn read_chunk_to_bytes(
@@ -1236,8 +1184,8 @@ impl<R: Read + Seek> Decoder<R> {
&mut self,
chunk_index: u32,
) -> TiffResult<BufferLayoutPreference> {
let data_dims = self.image.chunk_data_dimensions(chunk_index)?;
let readout = self.image.readout_for_size(data_dims.0, data_dims.1)?;
let data_dims = self.image().chunk_data_dimensions(chunk_index)?;
let readout = self.image().readout_for_size(data_dims.0, data_dims.1)?;
let extent = readout.result_extent_for_planes(0..1)?;
let sample_type = extent.sample_type();
@@ -1252,7 +1200,7 @@ impl<R: Read + Seek> Decoder<R> {
planes: 1,
plane_stride,
complete_len: layout.size(),
sample_format: self.image.sample_format,
sample_format: self.image().sample_format,
sample_type: Some(sample_type),
})
}
@@ -1269,15 +1217,15 @@ impl<R: Read + Seek> Decoder<R> {
&mut self,
code_unit: TiffCodingUnit,
) -> TiffResult<BufferLayoutPreference> {
match self.image.planar_config {
match self.image().planar_config {
PlanarConfiguration::Chunky => return self.image_chunk_buffer_layout(code_unit.0),
PlanarConfiguration::Planar => {}
}
let (width, height) = self.image.chunk_data_dimensions(code_unit.0)?;
let (width, height) = self.image().chunk_data_dimensions(code_unit.0)?;
let layout = self
.image
.image()
.readout_for_size(width, height)?
.to_plane_layout()?;
@@ -1295,9 +1243,9 @@ impl<R: Read + Seek> Decoder<R> {
///
/// Note that for planar images each chunk contains only one sample of the underlying data.
pub fn read_chunk(&mut self, chunk_index: u32) -> TiffResult<DecodingResult> {
let (width, height) = self.image.chunk_data_dimensions(chunk_index)?;
let (width, height) = self.image().chunk_data_dimensions(chunk_index)?;
let readout = self.image.readout_for_size(width, height)?;
let readout = self.image().readout_for_size(width, height)?;
let mut result = readout
.result_extent_for_planes(0..1)?
@@ -1316,9 +1264,9 @@ impl<R: Read + Seek> Decoder<R> {
///
/// Note that for planar images each chunk contains only one sample of the underlying data.
pub fn read_chunk_bytes(&mut self, chunk_index: u32, buffer: &mut [u8]) -> TiffResult<()> {
let (width, height) = self.image.chunk_data_dimensions(chunk_index)?;
let (width, height) = self.image().chunk_data_dimensions(chunk_index)?;
let layout = self.image.readout_for_size(width, height)?;
let layout = self.image().readout_for_size(width, height)?;
layout.assert_min_layout(buffer)?;
self.read_chunk_to_bytes(buffer, chunk_index, &layout)?;
@@ -1339,9 +1287,9 @@ impl<R: Read + Seek> Decoder<R> {
chunk_index: u32,
output_width: usize,
) -> TiffResult<()> {
let (width, height) = self.image.chunk_data_dimensions(chunk_index)?;
let (width, height) = self.image().chunk_data_dimensions(chunk_index)?;
let mut layout = self.image.readout_for_size(width, height)?;
let mut layout = self.image().readout_for_size(width, height)?;
layout.set_row_stride(output_width)?;
let extent = layout.result_extent_for_planes(0..1)?;
@@ -1371,8 +1319,8 @@ impl<R: Read + Seek> Decoder<R> {
slice: TiffCodingUnit,
buffer: &mut [u8],
) -> TiffResult<()> {
let (width, height) = self.image.chunk_data_dimensions(slice.0)?;
let readout = self.image.readout_for_size(width, height)?;
let (width, height) = self.image().chunk_data_dimensions(slice.0)?;
let readout = self.image().readout_for_size(width, height)?;
let ref layout @ image::PlaneLayout {
ref plane_offsets,
@@ -1394,7 +1342,7 @@ impl<R: Read + Seek> Decoder<R> {
for (idx, &plane_offset) in plane_offsets[..used_plane_offsets].iter().enumerate() {
let chunk = slice.0 + idx as u32 * readout.chunks_per_plane;
self.goto_offset_u64(self.image.chunk_offsets[chunk as usize])?;
self.goto_offset_u64(self.image().chunk_offsets[chunk as usize])?;
self.image.expand_chunk(
&mut self.value_reader,
@@ -1410,13 +1358,13 @@ impl<R: Read + Seek> Decoder<R> {
/// Returns the default chunk size for the current image. Any given chunk in the image is at most as large as
/// the value returned here. For the size of the data (chunk minus padding), use `chunk_data_dimensions`.
pub fn chunk_dimensions(&self) -> (u32, u32) {
self.image.chunk_dimensions().unwrap()
self.image().chunk_dimensions().unwrap()
}
/// Returns the size of the data in the chunk with the specified index. This is the default size of the chunk,
/// minus any padding.
pub fn chunk_data_dimensions(&self, chunk_index: u32) -> (u32, u32) {
self.image
self.image()
.chunk_data_dimensions(chunk_index)
.expect("invalid chunk_index")
}
@@ -1435,7 +1383,7 @@ impl<R: Read + Seek> Decoder<R> {
/// indicate the layout needed to read the first data plane. This will be fixed in a future
/// major version of `tiff`.
pub fn image_buffer_layout(&mut self) -> TiffResult<BufferLayoutPreference> {
let layout = self.image.readout_for_image()?.to_plane_layout()?;
let layout = self.image().readout_for_image()?.to_plane_layout()?;
Ok(BufferLayoutPreference::from_planes(&layout))
}
@@ -1448,18 +1396,16 @@ impl<R: Read + Seek> Decoder<R> {
/// # Bugs
///
/// When the image is stored as a planar configuration, this method will currently only read
/// the first sample's plane. This will be fixed in a future major version of `tiff`. To read
/// multiple planes, [`Self::read_image_to_buffer`] can be used instead.
/// the first sample's plane. This will be fixed in a future major version of `tiff`.
///
/// # Intent to deprecate
///
/// Use [`Self::read_image_to_buffer`] or a combination of [`DecodingResult::resize_to`] and
/// [`Self::read_image_bytes`] instead where possible, preserving the buffer across multiple
/// calls. This old method will likely keep its bugged planar behavior until it is fully
/// replaced, to ensure that existing code will not run into unexpectedly large allocations
/// that will error on limits instead.
/// Use `DecodingResult::resize_to` and `read_image_bytes` instead where possible, preserving
/// the buffer across multiple calls. This old method will likely keep its bugged planar
/// behavior until it is fully replaced, to ensure that existing code will not run into
/// unexpectedly large allocations that will error on limits instead.
pub fn read_image(&mut self) -> TiffResult<DecodingResult> {
let readout = self.image.readout_for_image()?;
let readout = self.image().readout_for_image()?;
let mut result = readout
.result_extent_for_planes(0..1)?
@@ -1476,31 +1422,6 @@ impl<R: Read + Seek> Decoder<R> {
/// configured limits, and then pass it to the underlying method. This is essentially a
/// type-safe wrapper around the raw [`Self::read_image_bytes`] method.
///
/// ## Planar behavior
///
/// If the image is stored as a planar configuration, an attempt is made to resize the buffer
/// to hold all planes. If that does not fit then only the first plane is read. Check the
/// buffer size against [`BufferLayoutPreference::complete_len`] to ensure that all planes were
/// read:
///
/// ```
/// use tiff::decoder::{Decoder, DecodingResult};
/// let mut result = DecodingResult::U8(vec![]);
///
/// let mut reader = /* */
/// # Decoder::open(std::io::Cursor::new(include_bytes!(concat!(
/// # env!("CARGO_MANIFEST_DIR"), "/tests/images/tiled-gray-i1.tif"
/// # )))).unwrap();
/// # reader.next_image().unwrap();
/// let layout = reader.read_image_to_buffer(&mut result)?;
///
/// if result.as_buffer(0).as_bytes().len() < layout.complete_len {
/// println!("Only the first plane was read");
/// }
///
/// # Ok::<_, tiff::TiffError>(())
/// ```
///
/// # Examples
///
/// ```
@@ -1509,10 +1430,10 @@ impl<R: Read + Seek> Decoder<R> {
/// let mut result = DecodingResult::I8(vec![]);
///
/// let mut reader = /* */
/// # Decoder::open(std::io::Cursor::new(include_bytes!(concat!(
/// # Decoder::new(std::io::Cursor::new(include_bytes!(concat!(
/// # env!("CARGO_MANIFEST_DIR"), "/tests/images/tiled-gray-i1.tif"
/// # )))).unwrap();
/// # reader.next_image().unwrap();
///
/// reader.read_image_to_buffer(&mut result)?;
///
/// # Ok::<_, tiff::TiffError>(())
@@ -1521,21 +1442,13 @@ impl<R: Read + Seek> Decoder<R> {
&mut self,
result: &mut DecodingResult,
) -> TiffResult<BufferLayoutPreference> {
let readout = self.image.readout_for_image()?;
let readout = self.image().readout_for_image()?;
let planes = readout.to_plane_layout()?;
let num_planes = if planes.total_bytes <= self.value_reader.limits.decoding_buffer_size {
planes.plane_offsets.len() as u16
} else {
1
};
let layout = BufferLayoutPreference::from_planes(&planes);
let extent = readout.result_extent_for_planes(0..num_planes)?;
// Compatibility: if this extent is too large our configured limits we will fall back to
// reading only the first plane.
result.resize_to_extent(extent, &self.value_reader.limits)?;
let extent = readout.result_extent_for_planes(0..1)?;
result.resize_to_extent(extent, &self.value_reader.limits)?;
self.read_image_bytes(result.as_buffer(0).as_bytes_mut())?;
Ok(layout)
@@ -1552,7 +1465,7 @@ impl<R: Read + Seek> Decoder<R> {
/// Returns an error if the buffer fits less than one plane. In particular, for non-planar
/// images returns an error if the buffer does not fit the required size.
pub fn read_image_bytes(&mut self, buffer: &mut [u8]) -> TiffResult<()> {
let readout = self.image.readout_for_image()?;
let readout = self.image().readout_for_image()?;
let ref layout @ image::PlaneLayout {
ref plane_offsets,
@@ -1576,7 +1489,7 @@ impl<R: Read + Seek> Decoder<R> {
for (idx, &plane_offset) in plane_offsets[..used_plane_offsets].iter().enumerate() {
let chunk = chunk + idx as u32 * readout.chunks_per_plane;
self.goto_offset_u64(self.image.chunk_offsets[chunk as usize])?;
self.goto_offset_u64(self.image().chunk_offsets[chunk as usize])?;
self.image.expand_chunk(
&mut self.value_reader,
@@ -1590,16 +1503,12 @@ impl<R: Read + Seek> Decoder<R> {
Ok(())
}
/// Get the IFD decoder for our current IFD.
///
/// This may be used with any directory, valid image or otherwise. When the decoder has not
/// read a directory yet (a state after [`TiffHeader::open`]) the decoder will represent an
/// empty directory with only the next pointer set.
pub fn current_ifd(&mut self) -> IfdDecoder<'_> {
/// Get the IFD decoder for our current image IFD.
fn image_ifd(&mut self) -> IfdDecoder<'_> {
IfdDecoder {
inner: tag_reader::TagReader {
decoder: &mut self.value_reader,
ifd: &self.directory,
ifd: self.image.ifd.as_ref().unwrap(),
},
}
}
@@ -1617,9 +1526,7 @@ impl<R: Read + Seek> Decoder<R> {
///
/// # use std::io::Cursor;
/// # let mut data = Cursor::new(vec![0]);
/// let mut decoder = Decoder::open(&mut data)?;
///
/// decoder.next_directory()?;
/// let mut decoder = Decoder::new(&mut data).unwrap();
/// let sub_ifds = decoder.get_tag(Tag::SubIfd)?.into_ifd_vec()?;
///
/// for ifd in sub_ifds {
@@ -1642,13 +1549,13 @@ impl<R: Read + Seek> Decoder<R> {
/// Tries to retrieve a tag from the current image directory.
/// Return `Ok(None)` if the tag is not present.
pub fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<ifd::Value>> {
self.current_ifd().find_tag(tag)
self.image_ifd().find_tag(tag)
}
/// Tries to retrieve a tag in the current image directory and convert it to the desired
/// unsigned type.
pub fn find_tag_unsigned<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<Option<T>> {
self.current_ifd().find_tag_unsigned(tag)
self.image_ifd().find_tag_unsigned(tag)
}
/// Tries to retrieve a vector of all a tag's values and convert them to the desired unsigned
@@ -1657,19 +1564,19 @@ impl<R: Read + Seek> Decoder<R> {
&mut self,
tag: Tag,
) -> TiffResult<Option<Vec<T>>> {
self.current_ifd().find_tag_unsigned_vec(tag)
self.image_ifd().find_tag_unsigned_vec(tag)
}
/// Tries to retrieve a tag from the current image directory and convert it to the desired
/// unsigned type. Returns an error if the tag is not present.
pub fn get_tag_unsigned<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<T> {
self.current_ifd().get_tag_unsigned(tag)
self.image_ifd().get_tag_unsigned(tag)
}
/// Tries to retrieve a tag from the current image directory.
/// Returns an error if the tag is not present
pub fn get_tag(&mut self, tag: Tag) -> TiffResult<ifd::Value> {
self.current_ifd().get_tag(tag)
self.image_ifd().get_tag(tag)
}
pub fn get_tag_u32(&mut self, tag: Tag) -> TiffResult<u32> {
@@ -1724,7 +1631,7 @@ impl<R: Read + Seek> Decoder<R> {
}
pub fn tag_iter(&mut self) -> impl Iterator<Item = TiffResult<(Tag, ifd::Value)>> + '_ {
self.current_ifd().tag_iter()
self.image_ifd().tag_iter()
}
}
@@ -1944,11 +1851,6 @@ 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> {
@@ -1968,7 +1870,7 @@ impl<'l> IfdDecoder<'l> {
mod tests {
use super::Decoder;
use crate::{
bytecast, encoder,
bytecast,
tags::{ByteOrder, Tag, ValueBuffer},
};
@@ -1980,11 +1882,9 @@ mod tests {
))
.unwrap();
let mut decoder = Decoder::open(file).unwrap();
let mut decoder = Decoder::new(file).unwrap();
let file_bo = decoder.byte_order();
decoder.next_image().unwrap();
let mut ifd = decoder.current_ifd();
let mut ifd = decoder.image_ifd();
{
let value = ifd
@@ -2031,116 +1931,4 @@ mod tests {
assert_eq!(&by_bytes[..3], &[8, 8, 8]);
}
}
#[test]
fn non_image_directory_between() {
let mut cursor = std::io::Cursor::new(vec![]);
{
let mut encoder = encoder::TiffEncoder::new(&mut cursor).unwrap();
encoder
.write_image::<encoder::colortype::Gray8>(1, 1, &[0])
.unwrap();
encoder.image_directory().unwrap().finish().unwrap();
encoder
.write_image::<encoder::colortype::Gray8>(1, 1, &[0])
.unwrap();
}
cursor.set_position(0);
let (first_ptr, second_ptr, third_ptr);
{
let mut decoder = Decoder::open(&mut cursor).unwrap();
decoder.next_image().unwrap();
first_ptr = decoder.ifd_pointer();
let first_ifd = decoder.current_ifd();
assert!(first_ifd.directory().get(Tag::ImageWidth).is_some());
assert!(decoder.more_images());
assert!(decoder.next_image().is_err());
let broken_ifd = decoder.current_ifd();
assert!(broken_ifd.directory().get(Tag::ImageWidth).is_some());
assert!(decoder.next_directory().is_ok());
second_ptr = decoder.ifd_pointer();
let second_ifd = decoder.current_ifd();
assert!(second_ifd.directory().get(Tag::ImageWidth).is_none());
assert!(decoder.more_images());
assert!(decoder.next_image().is_ok());
third_ptr = decoder.ifd_pointer();
let third_ifd = decoder.current_ifd();
assert!(third_ifd.directory().get(Tag::ImageWidth).is_some());
assert!(!decoder.more_images());
assert!(decoder.seek_to_image(1).is_err());
assert_eq!(decoder.ifd_pointer(), third_ptr);
assert!(decoder.seek_to_directory(1).is_ok());
assert_eq!(decoder.ifd_pointer(), second_ptr);
}
cursor.set_position(0);
{
let mut seek_immediately = Decoder::open(&mut cursor).unwrap();
assert!(seek_immediately.seek_to_image(0).is_ok());
assert_eq!(seek_immediately.ifd_pointer(), first_ptr);
assert!(seek_immediately.seek_to_image(1).is_err());
assert!(seek_immediately.seek_to_image(2).is_ok());
assert_eq!(seek_immediately.ifd_pointer(), third_ptr);
assert!(seek_immediately
.current_ifd()
.directory()
.get(Tag::ImageWidth)
.is_some());
}
cursor.set_position(0);
{
let mut seek_immediately = Decoder::open(&mut cursor).unwrap();
assert!(seek_immediately.seek_to_directory(1).is_ok());
assert_eq!(seek_immediately.ifd_pointer(), second_ptr);
assert!(seek_immediately
.current_ifd()
.directory()
.get(Tag::ImageWidth)
.is_none());
}
}
#[test]
fn non_image_directory_start() {
let mut cursor = std::io::Cursor::new(vec![]);
{
let mut encoder = encoder::TiffEncoder::new(&mut cursor).unwrap();
encoder.image_directory().unwrap().finish().unwrap();
encoder
.write_image::<encoder::colortype::Gray8>(1, 1, &[0])
.unwrap();
}
cursor.set_position(0);
let header = super::TiffHeader::parse(&mut cursor).unwrap();
let mut decoder = header.open(cursor);
let indicated_next = decoder.current_ifd().directory().next();
assert!(decoder.more_images());
assert_eq!(decoder.ifd_pointer(), None);
assert!(decoder.next_directory().is_ok());
let first_ifd = decoder.current_ifd();
assert!(first_ifd.directory().get(Tag::ImageWidth).is_none());
assert!(decoder.more_images());
assert_eq!(decoder.ifd_pointer(), indicated_next);
assert!(decoder.next_image().is_ok());
let second_ifd = decoder.current_ifd();
assert!(second_ifd.directory().get(Tag::ImageWidth).is_some());
assert!(!decoder.more_images());
}
}
+3 -60
View File
@@ -1,6 +1,4 @@
//! 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;
@@ -260,9 +258,9 @@ impl<R: Read> Read for PackBitsReader<R> {
pub struct Group4Reader<R: Read> {
decoder: fax34::decoder::Group4Decoder<io::Bytes<io::BufReader<io::Take<R>>>>,
line_buf: io::Cursor<Vec<u8>>,
height: u32,
height: u16,
width: u16,
y: u32,
y: u16,
}
#[cfg(feature = "fax")]
@@ -273,7 +271,7 @@ impl<R: Read> Group4Reader<R> {
compressed_length: u64,
) -> crate::TiffResult<Self> {
let width = u16::try_from(dimensions.0)?;
let height = dimensions.1;
let height = u16::try_from(dimensions.1)?;
Ok(Self {
decoder: fax34::decoder::Group4Decoder::new(
@@ -356,61 +354,6 @@ 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::*;
+8 -144
View File
@@ -1,97 +1,5 @@
use crate::tags::{PhotometricInterpretation, SampleFormat};
/// Apply floating-point predictor encoding to a row of floating-point samples.
///
/// The floating-point predictor works by:
/// 1. Converting each value to big-endian bytes
/// 2. Shuffling bytes so all first bytes are together, all second bytes, etc.
/// 3. Applying byte-level horizontal difference (wrapping_sub) across the entire buffer
///
/// The decoder reverses this by:
/// 1. Applying cumulative sum (wrapping_add) on the entire byte stream
/// 2. De-shuffling bytes back into float values
macro_rules! fp_predict {
($name:ident, $float_type:ty) => {
fn $name(row: &[$float_type], samples: usize, result: &mut Vec<u8>) {
const BYTE_SIZE: usize = core::mem::size_of::<$float_type>();
let num_values = row.len();
let byte_count = num_values * BYTE_SIZE;
let start_len = result.len();
// Allocate zeroed space so we can write to arbitrary positions
result.resize(start_len + byte_count, 0);
let output = &mut result[start_len..];
// Step 1 & 2: Convert to big-endian and scatter bytes
for (i, &value) in row.iter().enumerate() {
let bytes = value.to_be_bytes();
for b in 0..BYTE_SIZE {
output[b * num_values + i] = bytes[b];
}
}
// Step 3: Apply byte-level horizontal difference across the entire buffer.
// The prediction spans across quarter boundaries, just like the decoder's cumulative sum.
//
// We process elements in reverse in batches of 16 to enable autovectorization.
// Reverse order ensures that when we compute output[i] - output[i-samples],
// the value at output[i-samples] hasn't been overwritten yet (critical when
// samples < 16, which is the common case: 1, 3, 4, 5).
//
// 8 bits * 16 = 128 bits, the width of a typical SIMD register.
// Even if the target has no vector instructions, this still benefits from
// instruction-level parallelism, since unlike in decoding, all subtractions
// within a chunk are truly independent of each other.
let output = &mut result[start_len..byte_count + start_len];
let diff_len = byte_count - samples;
let remainder_len = diff_len % 16;
// Process full 16-byte chunks in reverse.
// We iterate from the end so that predecessor values (at lower indices)
// are still in their original state when we read them.
let mut chunk_end = byte_count;
while chunk_end >= samples + remainder_len + 16 {
let chunk_start = chunk_end - 16;
let prev_start = chunk_start - samples;
// Read all values in the chunk and their predecessors first.
// This makes all subtractions below independent, enabling vectorization.
let curr: [u8; 16] = output[chunk_start..chunk_end].try_into().unwrap();
let prev: [u8; 16] = output[prev_start..prev_start + 16].try_into().unwrap();
let chunk = &mut output[chunk_start..chunk_end];
chunk[0] = curr[0].wrapping_sub(prev[0]);
chunk[1] = curr[1].wrapping_sub(prev[1]);
chunk[2] = curr[2].wrapping_sub(prev[2]);
chunk[3] = curr[3].wrapping_sub(prev[3]);
chunk[4] = curr[4].wrapping_sub(prev[4]);
chunk[5] = curr[5].wrapping_sub(prev[5]);
chunk[6] = curr[6].wrapping_sub(prev[6]);
chunk[7] = curr[7].wrapping_sub(prev[7]);
chunk[8] = curr[8].wrapping_sub(prev[8]);
chunk[9] = curr[9].wrapping_sub(prev[9]);
chunk[10] = curr[10].wrapping_sub(prev[10]);
chunk[11] = curr[11].wrapping_sub(prev[11]);
chunk[12] = curr[12].wrapping_sub(prev[12]);
chunk[13] = curr[13].wrapping_sub(prev[13]);
chunk[14] = curr[14].wrapping_sub(prev[14]);
chunk[15] = curr[15].wrapping_sub(prev[15]);
chunk_end = chunk_start;
}
// Handle the leading remainder that doesn't fill a complete chunk of 16.
// This must be done last (after the reverse chunk loop) because these are
// the lowest indices, and the chunks above may read predecessor values
// from this range.
for i in (samples..samples + remainder_len).rev() {
output[i] = output[i].wrapping_sub(output[i - samples]);
}
}
};
}
fp_predict!(fp_predict_f32, f32);
fp_predict!(fp_predict_f64, f64);
macro_rules! integer_horizontal_predict {
() => {
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>) {
@@ -115,10 +23,6 @@ macro_rules! integer_horizontal_predict {
.map(|(prev, current)| current.wrapping_sub(*prev)),
);
}
fn floating_point_predict(_: &[Self::Inner], _: &mut Vec<u8>) {
unreachable!("floating-point predictor is only valid for floating-point sample types")
}
};
}
@@ -134,10 +38,6 @@ pub trait ColorType {
const SAMPLE_FORMAT: &'static [SampleFormat];
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>);
/// Apply floating-point predictor encoding to a row of samples.
/// This is only implemented for floating-point types; integer types will panic.
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>);
}
pub struct Gray8;
@@ -208,11 +108,7 @@ impl ColorType for Gray32Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
unreachable!()
}
}
@@ -244,11 +140,7 @@ impl ColorType for Gray64Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
unreachable!()
}
}
@@ -288,13 +180,8 @@ impl ColorType for RGB32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
unreachable!()
}
}
@@ -314,13 +201,8 @@ impl ColorType for RGB64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
unreachable!()
}
}
@@ -360,13 +242,8 @@ impl ColorType for RGBA32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
unreachable!()
}
}
@@ -386,13 +263,8 @@ impl ColorType for RGBA64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
unreachable!()
}
}
@@ -434,11 +306,7 @@ impl ColorType for CMYK32Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result)
unreachable!()
}
}
@@ -460,11 +328,7 @@ impl ColorType for CMYK64Float {
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!("horizontal predictor is not valid for floating-point sample types")
}
fn floating_point_predict(row: &[Self::Inner], result: &mut Vec<u8>) {
fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result)
unreachable!()
}
}
+246 -25
View File
@@ -2,24 +2,25 @@ pub use tiff_value::*;
use std::{
cmp,
io::{self, Seek, Write},
io::{self, Read, Seek, Write},
marker::PhantomData,
mem,
num::{NonZeroU64, TryFromIntError},
};
use crate::{
decoder::ifd::Entry,
decoder::{ifd::Entry, Decoder},
error::{TiffResult, UsageError},
tags::{
ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation,
ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
PlanarConfiguration, ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
},
Directory, TiffError, TiffFormatError,
};
pub mod colortype;
pub mod compression;
mod tag_filter;
mod tiff_value;
mod writer;
@@ -33,14 +34,15 @@ use self::writer::*;
/// Image data can be very unpredictable, and thus very hard to compress. Predictors are simple
/// passes ran over the image data to prepare it for compression. This is mostly used for LZW
/// compression, where using [Predictor::Horizontal] we see a 35% improvement in compression
/// ratio over the unpredicted compression!
/// ratio over the unpredicted compression !
///
/// [Predictor::Horizontal] is for integer sample types.
/// [Predictor::FloatingPoint] is for floating-point sample types (f32, f64).
/// [Predictor::FloatingPoint] is currently not supported.
pub type Predictor = crate::tags::Predictor;
#[cfg(feature = "deflate")]
pub type DeflateLevel = compression::DeflateLevel;
pub use tag_filter::TagFilter;
#[derive(Clone, Copy, PartialEq, Default)]
pub enum Compression {
#[default]
@@ -404,7 +406,7 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
/// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
/// stored in the entry's offset field.
pub fn write_entry_bytes(&mut self, ty: Type, data: &[u8]) -> TiffResult<Entry> {
if !data.len().is_multiple_of(usize::from(ty.byte_len())) {
if data.len() % usize::from(ty.byte_len()) != 0 {
return Err(TiffError::UsageError(UsageError::MismatchedEntryLength {
ty,
found: data.len(),
@@ -435,6 +437,148 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
self.directory.extend(entries);
}
/// Copy metadata from the IFD that a decoder refers to.
///
/// You generally want to use [`ImageEncoder::write_metadata_copy_from`] which automatically
/// fills out the [`EncodeMetadataApplicability`] as applicable to the chosen color space and
/// other sample configuration.
pub fn write_metadata_copy_from<R: Read + Seek>(
&mut self,
decoder: &mut Decoder<R>,
options: EncodeMetadataOptions<'_>,
kind: EncodeMetadataApplicability,
) -> TiffResult<EncodeMetadataReport> {
let mut report = EncodeMetadataReport {
copied: 0,
filtered: 0,
};
enum Role {
Ifd,
ExifPrivate,
GpsPrivate,
}
let ptr = decoder.ifd_pointer().unwrap();
let mut stack = vec![(ptr, Role::Ifd, usize::MAX)];
let mut buf = ValueBuffer::empty(Type::UNDEFINED);
// Directories which we will later write in reverse-pre-order.
let mut pre_order_dirs: Vec<(Directory, usize, Tag)> = vec![];
while let Some((ifd, role, parent)) = stack.pop() {
let dir = decoder.read_directory(ifd)?;
let mut created_dir = Directory::empty();
let filter = match role {
Role::Ifd => TagFilter::filter_primary,
Role::ExifPrivate => TagFilter::filter_exif_private,
Role::GpsPrivate => TagFilter::filter_gps_private,
};
let mut ifd_reader = decoder.read_directory_tags(&dir);
for (tag, entry) in dir.iter() {
let level = filter(options.tag_filter, tag.to_u16());
match options
.tag_filter
.should_keep_for_image_writer(level, &kind)
{
tag_filter::Choice::Ok => {}
tag_filter::Choice::Descend(subifd_kind) => {
// Only single pointers can be directories to descend to, avoid an overly
// large read by pulling that value out of the file.
if entry.count() != 1 {
report.filtered = report.filtered.saturating_add(1);
continue;
}
if ifd_reader.find_tag_buf(tag, &mut buf)?.is_none() {
// Should not happen (we're iterating the dict already), but be robust.
report.filtered = report.filtered.saturating_add(1);
continue;
};
let Some(ifd) = buf.as_ifd_pointer() else {
report.filtered = report.filtered.saturating_add(1);
continue;
};
let role = match subifd_kind {
tag_filter::SubifdKind::ExifPrivate => Role::ExifPrivate,
tag_filter::SubifdKind::Gps => Role::GpsPrivate,
tag_filter::SubifdKind::Interoperability => {
// Only applicable to compressed images.
report.filtered = report.filtered.saturating_add(1);
continue;
}
};
let this_as_parent = pre_order_dirs.len();
stack.push((ifd, role, this_as_parent));
}
tag_filter::Choice::Discard => continue,
tag_filter::Choice::Filtered
// Not supported yet.
| tag_filter::Choice::Special(tag_filter::Special::JpegThumbnail) => {
// Not to be copied.
report.filtered = report.filtered.saturating_add(1);
continue;
}
tag_filter::Choice::Special(tag_filter::Special::CompressedExclusive) => {
// We're not writing an App segment for a compressed file, are we?
continue;
}
}
if ifd_reader.find_tag_buf(tag, &mut buf)?.is_none() {
report.filtered = report.filtered.saturating_add(1);
continue;
}
// Re-encode the value to the target file.
buf.set_byte_order(ByteOrder::native());
let entry = self.write_entry_buf(&buf)?;
created_dir.extend([(tag, entry)]);
report.copied = report.copied.saturating_add(1);
}
let tag_in_parent = match role {
// should be unreachable? For now. But thumbnails would use this so we best be a
// little cautious here. Also thumbnails are weird, they do have image data but we
// would not copy that data itself with these methods. Just eh.
//
// Anyways the tree root will use this tag as a dummy.
Role::Ifd => Tag::SubIfd,
Role::ExifPrivate => Tag::ExifDirectory,
Role::GpsPrivate => Tag::GpsDirectory,
};
pre_order_dirs.push((created_dir, parent, tag_in_parent));
}
// Now post-order traverse the directory tree.
while let Some((directory, parent, tag)) = pre_order_dirs.pop() {
// The root is the items for the IFD itself.
if pre_order_dirs.is_empty() {
// We do not enter ourselves into a parent directory.
let _ = (parent, tag);
self.extend_from(&directory);
break;
}
let offset = Self::write_directory_inner(self.writer, &directory)?;
let offset = K::convert_offset(offset)?;
let entry = self.write_entry(offset)?;
let (parent_dir, _, _) = &mut pre_order_dirs[parent];
parent_dir.extend([(tag, entry)]);
}
Ok(report)
}
/// Define the parent directory.
///
/// Each directory has an offset based link to its successor, forming a linked list in the
@@ -461,17 +605,21 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
}
fn write_directory(&mut self) -> TiffResult<u64> {
Self::write_directory_inner(self.writer, &self.directory)
}
fn write_directory_inner(writer: &mut TiffWriter<W>, directory: &Directory) -> TiffResult<u64> {
// Start by turning all buffered unwritten values into entries.
let offset = self.writer.offset();
K::write_entry_count(self.writer, self.directory.len())?;
let offset = writer.offset();
K::write_entry_count(writer, directory.len())?;
let offset_bytes = mem::size_of::<K::OffsetType>();
for (tag, entry) in self.directory.iter() {
self.writer.write_u16(tag.to_u16())?;
self.writer.write_u16(entry.field_type().to_u16())?;
for (tag, entry) in directory.iter() {
writer.write_u16(tag.to_u16())?;
writer.write_u16(entry.field_type().to_u16())?;
let count = K::convert_offset(entry.count())?;
count.write(self.writer)?;
self.writer.write_bytes(&entry.offset()[..offset_bytes])?;
count.write(writer)?;
writer.write_bytes(&entry.offset()[..offset_bytes])?;
}
Ok(offset)
@@ -583,6 +731,72 @@ impl<'a, W: Write + Seek, K: TiffKind> Drop for DirectoryEncoder<'a, W, K> {
}
}
#[non_exhaustive]
#[derive(Clone, Copy)]
pub struct EncodeMetadataOptions<'lt> {
/// Several metadata kinds contain additional tags in further, linked directories that are only
/// referenced with an `Ifd`/`Ifd8` tag. These directories may have their own semantics, e.g. a
/// different set of tags or even new value types.
///
/// Since we know that the whole directory contains only metadata it would be possible to copy
/// all tags contained in it, however tags that can not be semantically validated might contain
/// data that is only valid within the original image or must be transformed during the copy.
/// For instance, JPEG thumbnails are embedded with separate offset and length fields (`0x0201`
/// and `0x202`). (This has caused the author mental harm, please do not consult me about it).
///
/// Setting this flag will skip such unknown tags instead of attempting a potentially incorrect
/// copy. However the set of copied metadata is then dependent on the `tiff` library.
pub tag_filter: &'lt TagFilter,
// TODO:
// Some tags are not allowed to be recorded or can be semantically validated. The library can
// skip these tags while copying metadata. For instance, `YCbCrCoefficients` _may_ be
// recorded for images in `YCbCr` photometric interpretation but _must not_ be recorded for
// images in any other interpretation (colorspace conversion should have been done during the
// transformation).
// pub validate_tags: enum ValidateTags {
// None,
// Skip,
// Error,
// Strict /* Error but also do not allow anything that can not be validated */
// },
}
impl Default for EncodeMetadataOptions<'_> {
fn default() -> Self {
EncodeMetadataOptions {
tag_filter: TagFilter::only_known_tags(),
}
}
}
/// Describe the kind of image for which metadata is recorded.
///
/// Metadata that is not applicable to the color space or the layout may be skipped. For instance,
/// subsampling basis position is only relevant on YCbCr / Chroma color photometric interpretation
/// where subsampling is defined.
#[non_exhaustive]
#[derive(Clone, Copy, Default)]
pub struct EncodeMetadataApplicability {
/// The photometric interpretation of the image data.
///
/// If `None` then metadata tags are not filtered, otherwise only tags applicable to the given
/// photometric interpretation are recorded.
pub photometric: Option<PhotometricInterpretation>,
/// The planar configuration of the image data.
///
/// If `None` then metadata tags are not filtered, otherwise only tags applicable to the given
/// configuration are recorded.
pub planar: Option<PlanarConfiguration>,
}
#[non_exhaustive]
pub struct EncodeMetadataReport {
/// Number of tags transferred (recursively).
pub copied: u64,
/// Number of tags encountered that were skipped due to the provided filter.
pub filtered: u64,
}
/// Type to encode images strip by strip.
///
/// You should call `finish` on this when you are finished with it.
@@ -636,14 +850,11 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> {
impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> {
fn sanity_check(compression: Compression, predictor: Predictor) -> TiffResult<()> {
match (predictor, compression, T::SAMPLE_FORMAT[0]) {
// Horizontal predictor is not valid for floating-point types
(Predictor::Horizontal, _, SampleFormat::IEEEFP | SampleFormat::Void) => {
Err(TiffError::UsageError(UsageError::PredictorIncompatible))
}
// Floating-point predictor is only valid for IEEEFP sample format
(Predictor::FloatingPoint, _, SampleFormat::IEEEFP) => Ok(()),
(Predictor::FloatingPoint, _, _) => {
Err(TiffError::UsageError(UsageError::PredictorIncompatible))
Err(TiffError::UsageError(UsageError::PredictorUnavailable))
}
_ => Ok(()),
}
@@ -777,13 +988,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
}
self.encoder.write_data(row_result.as_slice())?
}
Predictor::FloatingPoint => {
let mut row_result = Vec::with_capacity(std::mem::size_of_val(value));
for row in value.chunks_exact(self.row_samples as usize) {
T::floating_point_predict(row, &mut row_result);
}
self.encoder.write_data(row_result.as_slice())?
}
_ => unimplemented!(),
};
let byte_count = self.encoder.last_written() as usize;
@@ -832,6 +1037,22 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
Ok(())
}
/// Copy metadata from the IFD that a decoder refers to.
pub fn write_metadata_copy_from<R: Read + Seek>(
&mut self,
decoder: &mut Decoder<R>,
options: EncodeMetadataOptions<'_>,
) -> TiffResult<EncodeMetadataReport> {
self.encoder.write_metadata_copy_from(
decoder,
options,
EncodeMetadataApplicability {
photometric: Some(<T>::TIFF_VALUE),
planar: Some(PlanarConfiguration::Chunky),
},
)
}
/// Set image resolution
pub fn resolution(&mut self, unit: ResolutionUnit, value: Rational) {
self.encoder
+256
View File
@@ -0,0 +1,256 @@
use crate::tags::{PhotometricInterpretation, PlanarConfiguration};
/// A filter of tags copied in
/// [`DirectoryEncoder::write_metadata_from`][`super::DirectoryEncoder::write_metadata_from`].
pub struct TagFilter {
filter_level: FilterLevel,
}
impl TagFilter {
/// Return a filter that allows all tags from metadata subdirectories.
pub fn allow_all_in_subdirectories() -> &'static TagFilter {
static INSTANCE: TagFilter = TagFilter {
filter_level: FilterLevel::All,
};
&INSTANCE
}
/// Return a filter that has an explicit allow list of tags, for each of a known list of
/// metadata subdirectories.
pub fn only_known_tags() -> &'static TagFilter {
static INSTANCE: TagFilter = TagFilter {
filter_level: FilterLevel::Known,
};
&INSTANCE
}
/// FIXME: take a configuration by which to filter out complete metadata kinds.
/// FIXME: when we iterate over a whole directory of tags, this iteration is more expensive
/// than necessary. Tags within a directory should be ordered. We can thus perform a binary set
/// join which is more efficient than checking the individual filter on each. Does not matter
/// much in most cases though.
pub(crate) fn filter_primary(&self, tag: u16) -> Level {
let as_exif = self.filter_primary_exif(tag);
if !matches!(as_exif, Level::Unknown) {
return as_exif;
}
let as_gps = self.filter_primary_gps(tag);
if !matches!(as_gps, Level::Unknown) {
return as_gps;
}
let as_icc = self.filter_icc(tag);
if !matches!(as_icc, Level::Unknown) {
return as_icc;
}
Level::Unknown
}
/// As per EXIF V3.0 tags, Primary Image (0th IFD).
pub(crate) fn filter_primary_exif(&self, tag: u16) -> Level {
match tag {
// width, height, etc
0x0100..=0x0103 => Level::SampleLayout,
// photometric interpretation
0x0106 => Level::SampleLayout,
// data tied to the camera and customized description, potentially PI.
0x010f..=0x0110 => Level::Value,
// strip offsets
0x0111 => Level::SampleLayout,
// orientation
0x0112 => Level::Value,
// strip byte counts, samples
0x0115..=0x0117 => Level::SampleLayout,
// pixel dimensions (ratio). Should be generic enough.
0x011a..=0x011b => Level::Value,
// planar configuration
0x011c => Level::Photometric(Photometric::Planar),
// units for 0x011a,0x011b
0x0128 => Level::Value,
// transfer function
0x012d => Level::Photometric(Photometric::All),
// file data time, software pipeline. I'm wary of privacy implications.
0x0131..=0x0132 => Level::Value,
// artist, clearly PI unless you explicitly wish it to be attributed.
0x013b => Level::Value,
// Whitepoint, Chromaticities
0x013e..=0x013f => Level::Photometric(Photometric::All),
// The table lists these as Not allowed for anything but we may have a choice here for
// older models of images.
0x0201..=0x0202 => Level::Special(Special::JpegThumbnail),
0x0211..=0x0213 => Level::Photometric(Photometric::YCbCr),
0x0214 => Level::Photometric(Photometric::All),
// Copyright holder (not same as artist).
0x8298 => Level::Value,
0x8769 => Level::Ifd(SubifdKind::ExifPrivate),
_ => Level::Unknown,
}
}
pub(crate) fn filter_primary_gps(&self, tag: u16) -> Level {
match tag {
0x8825 => Level::Ifd(SubifdKind::Gps),
_ => Level::Unknown,
}
}
pub(crate) fn filter_icc(&self, tag: u16) -> Level {
match tag {
0x8ff3 => Level::Value,
_ => Level::Unknown,
}
}
pub(crate) fn filter_exif_private(&self, tag: u16) -> Level {
match tag {
// Maybe this is not personal identifying but some equipment may be very specifically
// known through these. And it's not critical to displaying it correctly.
0x829a => Level::Value,
0x829d => Level::Value,
0x8822 => Level::Value,
0x8824 => Level::Value,
0x8827 => Level::Value,
// Opto-electric coefficients
0x8828 => Level::Value,
0x8830..=0x8835 => Level::Value,
0x9000 => Level::Value,
0x9003..=0x9004 => Level::Value,
0x9010..=0x9012 => Level::Value,
// Not allowed for uncompressed data!
0x9101..=0x9102 => Level::Special(Special::CompressedExclusive),
0x9201..=0x9209 => Level::Value,
0x920a => Level::Value,
0x9214 => Level::Value,
0x927c => Level::Value,
0x9286 => Level::Value,
0x9290..=0x9292 => Level::Value,
0x9400..=0x9405 => Level::Value,
0xa000 => Level::Value,
0xa001 => Level::Photometric(Photometric::All),
0xa002..=0xa003 => Level::Special(Special::CompressedExclusive),
0xa004 => Level::Value,
0xa005 => Level::Ifd(SubifdKind::Interoperability),
0xa20b..=0xa20c => Level::Value,
0xa20e..=0xa210 => Level::Value,
0xa214..=0xa215 => Level::Value,
0xa217 => Level::Value,
0xa300..=0xa302 => Level::Value,
0xa401..=0xa40c => Level::Value,
0xa420 => Level::Value,
0xa430..=0xa43c => Level::Value,
0xa460..=0xa462 => Level::Value,
0xa500 => Level::Photometric(Photometric::All),
_ => Level::Unknown,
}
}
pub(crate) fn filter_gps_private(&self, tag: u16) -> Level {
match tag {
0x0000..=0x001f => Level::Value,
_ => Level::Unknown,
}
}
pub(crate) fn should_keep_for_image_writer(
&self,
level: Level,
target: &super::EncodeMetadataApplicability,
) -> Choice {
match level {
Level::Unknown if matches!(self.filter_level, FilterLevel::All) => Choice::Filtered,
// Sample Layout is controlled by the writer (dimensions, color type, …). Do not copy!
Level::SampleLayout => Choice::Discard,
Level::Ifd(subifd_kind) => Choice::Descend(subifd_kind),
Level::Photometric(photometric) => {
match (photometric, target.planar, target.photometric) {
(Photometric::All, _, _) => Choice::Ok,
(Photometric::Planar, Some(PlanarConfiguration::Planar) | None, _) => {
Choice::Ok
}
(Photometric::YCbCr, _, Some(PhotometricInterpretation::YCbCr) | None) => {
Choice::Ok
}
_ => Choice::Discard,
}
}
Level::Value => Choice::Ok,
Level::Special(special) => Choice::Special(special),
_ => Choice::Discard,
}
}
}
pub(crate) enum Choice {
Ok,
Descend(SubifdKind),
/// Tag is inapplicable to be copied.
Discard,
/// Tag was chosen not to be copied.
Filtered,
Special(Special),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FilterLevel {
All,
Known,
}
/// The level of support for a value.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Level {
Unknown,
/// Value describes the image data layout, which we will not want to write into the encoder's
/// directory where the image data is written later.
///
/// These are treated as privacy preserving.
SampleLayout,
/// Value is an IFD and must be copied by making a subdirectory.
///
/// These are treated as privacy preserving, independent of the tags within the subdirectory
/// which may not be. (For instance the presence of an empty directory is fine but we will omit
/// a GPS anyways).
Ifd(SubifdKind),
/// Value is related to photometric interpretation and should not be copied when the target
/// does not match the expected interpretation.
///
/// These are *generally* treated as privacy preserving.
Photometric(Photometric),
/// This is a value we know of.
/// FIXME: we do not differentiate by personal identifiable information and otherwise which is
/// quite important for some variations of copying metadata.
Value,
/// We recognize the tag but it can not be copied by itself through normal means.
///
/// Privacy treatment depends on the tag.
Special(Special),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Photometric {
All,
/// Do not copy for non-planar data.
Planar,
/// Do not copy for non-YCbCr data.
YCbCr,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SubifdKind {
ExifPrivate,
Gps,
Interoperability,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Special {
/// Thumbnail which we regard as PI since we do not deep-clean the JPEG structure.
JpegThumbnail,
/// May only occur in compressed (JPEG APP segments) data.
CompressedExclusive,
}
+2 -4
View File
@@ -172,7 +172,7 @@ impl<W: Write> TiffWriter<W> {
}
pub fn pad_word_boundary(&mut self) -> Result<(), io::Error> {
if !self.offset.is_multiple_of(4) {
if self.offset % 4 != 0 {
let padding = [0, 0, 0];
let padd_len = 4 - (self.offset % 4);
self.writer.write_all(&padding[..padd_len as usize])?;
@@ -241,9 +241,7 @@ fn encoder_on_short_writes() {
}
write.inner.set_position(0);
let mut tiff_reader = crate::decoder::Decoder::open(&mut write.inner).unwrap();
tiff_reader.next_image().unwrap();
let mut tiff_reader = crate::decoder::Decoder::new(&mut write.inner).unwrap();
let _layout = tiff_reader.read_image_to_buffer(&mut data).unwrap();
assert!(
-3
View File
@@ -70,9 +70,6 @@ quick_error! {
InconsistentSizesEncountered {
display("inconsistent sizes encountered")
}
IncorrectChunkCount {expected: u32, found: u32} {
display("incorrect number of chunk offsets encoded for the tile count of the image, expected {expected} but found {found}")
}
InvalidDimensions(width: u32, height: u32) {
display("invalid dimensions: {width}x{height}")
}
+2 -4
View File
@@ -51,8 +51,7 @@ pub enum ColorType {
Multiband { bit_depth: u8, num_samples: u16 },
}
impl ColorType {
/// The number of bits used to store each sample.
pub fn bit_depth(&self) -> u8 {
fn bit_depth(&self) -> u8 {
match *self {
ColorType::Gray(b)
| ColorType::RGB(b)
@@ -67,8 +66,7 @@ impl ColorType {
}
}
/// The number of samples in each representation of a single color.
pub fn num_samples(&self) -> u16 {
fn num_samples(&self) -> u16 {
match *self {
ColorType::Gray(_) => 1,
ColorType::RGB(_) => 3,
+57 -66
View File
@@ -1,5 +1,4 @@
use crate::encoder::TiffValue;
use core::fmt;
macro_rules! tags {
{
@@ -170,18 +169,6 @@ 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) {
@@ -280,17 +267,6 @@ pub enum CompressionMethod(u16) unknown(
// Self-assigned by libtiff
ZSTD = 0xC350,
// Self-assigned by libtiff
WebP = 0xC351,
// Self-assigned by libtiff/SGI
// A packbit like scheme on specific 32-bit samples (16 sint luma, 8 sint u,v). Only really
// compatible with PhotometricInterpretation CIELogLuv.
SgiLog = 34676,
// A packbit like scheme on specific 24-bit samples (10 packed bits luma, 14-bit uv index).
// Only really compatible with PhotometricInterpretation CIELogLuv.
SgiLog24 = 34677,
}
}
@@ -303,41 +279,9 @@ pub enum PhotometricInterpretation(u16) {
TransparencyMask = 4,
CMYK = 5,
YCbCr = 6,
/// Biased Lab with all coefficients stored as unsigned integers.
CIELab = 8,
IccLab = 9,
ItuLab = 10,
/// Logarithmic L* color space,
///
/// see: <https://web.archive.org/web/20250121120358/https://www.anyhere.com/gward/pixformat/tiffluv.html>
/// References: <https://doi.org/10.2352/CIC.1998.6.1.art00046>
///
/// The `uv` coordinates are based on:
///
/// ```text
/// (6*u - 16*v + 12) * (-2x + 12y + 3) = 36
///
/// u' = 4x / ( -2x + 12y + 3 )
/// v' = 9y / ( -2x + 12y + 3 )
/// x = 9u' / (6u' - 16v' + 12)
/// y = 4v' / (6u' - 16v' + 12)
/// ```
///
/// Since `(-2x + 12y + 3) <= 15` it holds `(6u' - 16v' + 12) >= 36/15` for all XYZ colors
/// which makes the back transform to `xyY` well-defined. Also `v' <= 9/15 = 0.6` and also
/// by practical means `u' <= 2/3`.
///
/// The compression format that is supposed to be used with this constrains them as well. It
/// encodes each component as a 16-bit unsigned integer scaled and clamped to a quantization
/// using 256 out of 410 levels or an index into a table based on a quantization to 177 levels
/// each `0.0035` (~1/286) wide and 163 levels of `v` (same quantization width).
///
/// > Larson, G.W., ``Overcoming Gamut and Dynamic Range Limitations in Digital Images,'' Proceedings of the Sixth Color Imaging Conference, November 1998.
CIELogL = 32844,
/// Logarithmic L*u'v' color space,
///
/// See references under [`Self::CIELogL`].
CIELogLuv = 32845,
}
}
@@ -357,9 +301,7 @@ pub enum Predictor(u16) {
/// This means that instead of having in order `[r1, g1. b1, r2, g2 ...]` you will find
/// `[r1, g1, b1, r2-r1, g2-g1, b2-b1, r3-r2, g3-g2, ...]`
Horizontal = 2,
/// Floating-point predictor for IEEE floating-point sample types.
///
/// This predictor byte-shuffles the data and applies byte-level horizontal differencing.
/// Not currently supported
FloatingPoint = 3,
}
}
@@ -465,6 +407,32 @@ impl ValueBuffer {
&mut self.bytes[..len]
}
/// See [`Value::into_ifd_pointer`](crate::decoder::ifd::Value::into_ifd_pointer).
pub(crate) fn as_ifd_pointer(&self) -> Option<IfdPointer> {
if self.count != 1 {
return None;
}
match self.ty {
// See [`ifd::Value::into_ifd_pointer`] there is enough legacy that uses `LONG` to
// refer to an offset into the file with the meaning of a directory pointer. That is
// rather annoying but legacy and not a big deal.
Type::IFD | Type::LONG => {
let mut buf = [0u8; 4];
buf.copy_from_slice(&self.bytes[..4]);
ByteOrder::native().convert(Type::IFD, &mut buf, self.byte_order);
Some(IfdPointer(u32::from_ne_bytes(buf).into()))
}
Type::IFD8 => {
let mut buf = [0u8; 8];
buf.copy_from_slice(&self.bytes[..8]);
ByteOrder::native().convert(Type::IFD8, &mut buf, self.byte_order);
Some(IfdPointer(u64::from_ne_bytes(buf)))
}
_ => None,
}
}
/// Change the byte order of the value representation.
pub fn set_byte_order(&mut self, byte_order: ByteOrder) {
let len = self.assumed_len_from_count();
@@ -590,11 +558,34 @@ pub(crate) enum EndianBytes {
Eight,
}
/// Defines how directories are laid out in the file.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TiffVariant {
/// TIFF-V6.0 with 32-bit offsets and counts.
Standard,
/// Extended TIFF with 64-bit offsets and counts and long directories.
BigTiff,
#[test]
fn convert_ifd_u32() {
let mut value = ValueBuffer::from_value(&crate::encoder::Ifd(0x42));
for bo in [ByteOrder::BigEndian, ByteOrder::LittleEndian] {
value.set_byte_order(bo);
let ptr = value
.as_ifd_pointer()
.expect("Should be convertible to IfdPointer");
// Should be in host byte order again.
assert_eq!(ptr.0, 0x42);
}
}
#[test]
fn convert_ifd_long8() {
let mut value = ValueBuffer::from_value(&crate::encoder::Ifd8(0x42));
for bo in [ByteOrder::BigEndian, ByteOrder::LittleEndian] {
value.set_byte_order(bo);
let ptr = value
.as_ifd_pointer()
.expect("Should be convertible to IfdPointer");
// Should be in host byte order again.
assert_eq!(ptr.0, 0x42);
}
}
-8
View File
@@ -2,11 +2,3 @@ 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
libtiffpic/
url: https://download.osgeo.org/libtiff/
credit: libtiff
license: CC0
+1 -3
View File
@@ -15,9 +15,7 @@ fn test_big_tiff() {
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::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(64, 64)
+5 -20
View File
@@ -16,9 +16,7 @@ fn test_white_ieee_fp16() {
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::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(256, 256)
@@ -27,7 +25,6 @@ fn test_white_ieee_fp16() {
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
for p in img {
assert!(p == half::f16::from_f32_const(1.0));
@@ -46,9 +43,7 @@ fn test_one_black_pixel_ieee_fp16() {
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::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(256, 256)
@@ -57,7 +52,6 @@ fn test_one_black_pixel_ieee_fp16() {
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
for (i, p) in img.iter().enumerate() {
if i == 0 {
@@ -80,9 +74,7 @@ fn test_pattern_horizontal_differencing_ieee_fp16() {
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::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(256, 256)
@@ -91,7 +83,6 @@ fn test_pattern_horizontal_differencing_ieee_fp16() {
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
// 0, 2, 5, 8, 12, 16, 255 are black
let black = [0, 2, 5, 8, 12, 16, 255];
@@ -116,9 +107,7 @@ fn test_pattern_predictor_ieee_fp16() {
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::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(256, 256)
@@ -127,7 +116,6 @@ fn test_pattern_predictor_ieee_fp16() {
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
// 0, 2, 5, 8, 12, 16, 255 are black
let black = [0, 2, 5, 8, 12, 16, 255];
@@ -187,9 +175,7 @@ fn test_predictor_ieee_fp16() {
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::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(16, 16)
@@ -198,7 +184,6 @@ fn test_predictor_ieee_fp16() {
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
for (exp, found) in std::iter::zip(pnm_values.iter(), img.iter()) {
assert!((exp - found.to_f32()).abs() < 0.0001);
+2 -21
View File
@@ -7,7 +7,7 @@ use tiff::ColorType;
use std::fs::File;
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/geotiff";
const TEST_IMAGE_DIR: &str = "./tests/images";
#[test]
fn test_geo_tiff() {
@@ -15,9 +15,8 @@ fn test_geo_tiff() {
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::open(img_file).expect("Cannot create decoder");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
decoder = decoder.with_limits(tiff::decoder::Limits::unlimited());
decoder.next_image().expect("Cannot read image");
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
@@ -62,21 +61,3 @@ 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::open(img_file).expect("Cannot create decoder");
while decoder.more_images() {
decoder.next_image().unwrap();
decoder.read_image_to_buffer(&mut buffer).unwrap();
}
}
}
+19 -67
View File
@@ -14,9 +14,7 @@ macro_rules! test_image_sum {
fn $name(file: &str, expected_type: ColorType, expected_sum: $sum_ty) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let img_res = decoder.read_image().unwrap();
@@ -46,9 +44,7 @@ test_image_sum!(test_image_sum_f64, F64, f64);
fn test_image_color_type_unsupported(file: &str, expected_type: ColorType) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(decoder.colortype().unwrap(), expected_type);
assert!(matches!(
decoder.read_image(),
@@ -170,8 +166,7 @@ fn test_string_tags() {
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::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
let software = decoder.get_tag(tiff::tags::Tag::Software).unwrap();
match software {
ifd::Value::Ascii(s) => assert_eq!(
@@ -195,8 +190,7 @@ fn test_decode_data() {
}
}
let file = File::open("./tests/decodedata-rgb-3c-8b.tiff").unwrap();
let mut decoder = Decoder::open(file).unwrap();
decoder.next_image().expect("Cannot read image IFD");
let mut decoder = Decoder::new(file).unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
@@ -242,8 +236,7 @@ fn test_tiled_rect_rgb_u8() {
fn test_inner_access() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-rect-rgb-u8.tif");
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
let c = decoder
@@ -329,8 +322,7 @@ fn test_tiled_incremental() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let tiles = decoder.tile_count().unwrap();
@@ -360,8 +352,7 @@ fn test_read_planar_bands() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let chunks = decoder.strip_count().unwrap();
@@ -405,8 +396,7 @@ fn test_div_zero() {
178, 178, 178,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let err = decoder.next_image().unwrap_err();
let err = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
match err {
TiffError::FormatError(TiffFormatError::StripTileTagConflict) => {}
@@ -424,8 +414,7 @@ fn test_too_many_value_bytes() {
0, 89, 89, 89, 89, 89, 89, 89, 89, 96, 1, 20, 89, 89, 89, 89, 18,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let error = decoder.next_image().unwrap_err();
let error = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
match error {
tiff::TiffError::LimitsExceeded => {}
@@ -443,8 +432,7 @@ fn fuzzer_testcase5() {
178, 178, 178,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
}
#[test]
@@ -457,8 +445,7 @@ fn fuzzer_testcase1() {
178,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
}
#[test]
@@ -471,8 +458,7 @@ fn fuzzer_testcase6() {
178, 178,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
}
#[test]
@@ -484,8 +470,7 @@ fn oom() {
0, 0, 0, 40, 0, 0, 0, 23, 1, 4, 0, 1, 0, 0, 0, 178, 48, 178, 178, 178, 178, 162, 178,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
}
#[test]
@@ -497,8 +482,7 @@ fn fuzzer_testcase4() {
0, 0, 0, 40, 0, 0, 0, 23, 1, 4, 0, 1, 0, 0, 0, 48, 178, 178, 178, 0, 1, 0, 13, 13,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
}
#[test]
@@ -514,8 +498,7 @@ fn fuzzer_testcase2() {
73,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
}
#[test]
@@ -531,8 +514,7 @@ fn invalid_jpeg_tag_2() {
0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 36, 73, 73, 0, 42, 36, 36, 36, 36, 0, 0, 8, 0,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
}
#[test]
@@ -545,8 +527,7 @@ fn fuzzer_testcase3() {
255, 255,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.next_image().unwrap_err();
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
}
#[test]
@@ -564,8 +545,7 @@ fn timeout() {
0, 0, 73, 73, 42, 0, 8, 0, 0, 0, 0, 0, 32,
];
let mut decoder = Decoder::open(std::io::Cursor::new(&image)).unwrap();
let error = decoder.next_image().unwrap_err();
let error = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
match error {
TiffError::FormatError(TiffFormatError::CycleInOffsets) => {}
@@ -603,8 +583,7 @@ fn test_image_bytes(
) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().unwrap();
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
let mut buffer = vec![0u8; 0];
for (idx, &(expected_type, expected_crc)) in expected.iter().enumerate() {
@@ -741,30 +720,3 @@ fn extra_bits_gray() {
fn extra_bits_rgb() {
test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGB(8), 0);
}
#[test]
#[cfg(feature = "fax")]
fn test_decode_huge_g4() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("big_g4.tif");
let img_file = File::open(path).expect("failed to open large G4 test image");
let mut decoder = Decoder::open(img_file).expect("failed to create decoder");
decoder.next_image().expect("failed to read image IFD");
let (width, height) = decoder.dimensions().expect("failed to get dimensions");
assert!(
height > u16::MAX as u32,
"Test image height {} is not large enough to test u16 overflow",
height
);
let result = decoder
.read_image()
.expect("failed to decode huge G4 image");
if let tiff::decoder::DecodingResult::U8(data) = result {
assert_eq!(data.len(), (width as usize * height as usize));
} else {
panic!("Unexpected decoding result type");
}
}
-165
View File
@@ -1,165 +0,0 @@
use std::collections::HashMap;
use std::{fs::File, path};
use tiff::decoder::{Decoder, DecodingResult};
use libtest_mimic::{Arguments, Failed, Trial};
use walkdir::WalkDir;
const REFERENCE_DIR: &str = "tests/libtiffpic";
// `find test/libtiffpic -type f | sort`
const FILES: &[(&str, bool)] = &[
("tests/libtiffpic/caspian.tif", true),
("tests/libtiffpic/cramps.tif", true),
// FIXME: See <https://github.com/image-rs/image-tiff/issues/338>, this has tiles as
// StripOffsets and has a RowsPerStrip.
("tests/libtiffpic/cramps-tile.tif", false),
("tests/libtiffpic/CVS/Entries", false),
("tests/libtiffpic/CVS/Repository", false),
("tests/libtiffpic/CVS/Root", false),
("tests/libtiffpic/depth/CVS/Entries", false),
("tests/libtiffpic/depth/CVS/Repository", false),
("tests/libtiffpic/depth/CVS/Root", false),
("tests/libtiffpic/depth/flower-minisblack-02.tif", true),
("tests/libtiffpic/depth/flower-minisblack-04.tif", true),
("tests/libtiffpic/depth/flower-minisblack-06.tif", true),
("tests/libtiffpic/depth/flower-minisblack-08.tif", true),
("tests/libtiffpic/depth/flower-minisblack-10.tif", true),
("tests/libtiffpic/depth/flower-minisblack-12.tif", true),
("tests/libtiffpic/depth/flower-minisblack-14.tif", true),
("tests/libtiffpic/depth/flower-minisblack-16.tif", true),
("tests/libtiffpic/depth/flower-minisblack-24.tif", true),
("tests/libtiffpic/depth/flower-minisblack-32.tif", true),
("tests/libtiffpic/depth/flower-palette-02.tif", true),
("tests/libtiffpic/depth/flower-palette-04.tif", true),
("tests/libtiffpic/depth/flower-palette-08.tif", true),
("tests/libtiffpic/depth/flower-palette-16.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-02.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-04.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-08.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-10.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-12.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-14.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-16.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-24.tif", true),
("tests/libtiffpic/depth/flower-rgb-contig-32.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-02.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-04.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-08.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-10.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-12.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-14.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-16.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-24.tif", true),
("tests/libtiffpic/depth/flower-rgb-planar-32.tif", true),
(
"tests/libtiffpic/depth/flower-separated-contig-08.tif",
true,
),
(
"tests/libtiffpic/depth/flower-separated-contig-16.tif",
true,
),
(
"tests/libtiffpic/depth/flower-separated-planar-08.tif",
true,
),
(
"tests/libtiffpic/depth/flower-separated-planar-16.tif",
true,
),
("tests/libtiffpic/depth/genimages", false),
("tests/libtiffpic/depth/README.txt", false),
("tests/libtiffpic/depth/summary.txt", false),
("tests/libtiffpic/dscf0013.tif", true),
("tests/libtiffpic/fax2d.tif", true),
// No support for direct CCITT Group 3 1D format
("tests/libtiffpic/g3test.g3", false),
("tests/libtiffpic/g3test.tif", true),
("tests/libtiffpic/jello.tif", true),
("tests/libtiffpic/jim___ah.tif", true),
("tests/libtiffpic/jim___cg.tif", true),
("tests/libtiffpic/jim___dg.tif", true),
("tests/libtiffpic/jim___gg.tif", true),
("tests/libtiffpic/ladoga.tif", true),
("tests/libtiffpic/off_l16.tif", true),
("tests/libtiffpic/off_luv24.tif", true),
("tests/libtiffpic/off_luv32.tif", true),
("tests/libtiffpic/oxford.tif", true),
("tests/libtiffpic/pc260001.tif", true),
("tests/libtiffpic/quad-jpeg.tif", true),
("tests/libtiffpic/quad-lzw.tif", true),
// FIXME: See <https://github.com/image-rs/image-tiff/issues/338>, this has tiles as
// StripOffsets and has a RowsPerStrip.
("tests/libtiffpic/quad-tile.tif", false),
("tests/libtiffpic/README", false),
("tests/libtiffpic/smallliz.tif", true),
("tests/libtiffpic/strike.tif", true),
("tests/libtiffpic/text.tif", true),
("tests/libtiffpic/ycbcr-cat.tif", true),
("tests/libtiffpic/zackthecat.tif", true),
];
fn main() {
let mut candidates: HashMap<_, _> = FILES.iter().copied().collect();
let mut trials = Vec::new();
for entry in WalkDir::new(REFERENCE_DIR) {
let entry = entry.unwrap();
if !entry.file_type().is_file() {
continue;
}
let Some(path) = entry.path().to_str() else {
continue;
};
let path = path.replace(std::path::MAIN_SEPARATOR, "/"); // always use UNIX path separators
// Mark this path as consumed in either case by removing it.
let Some(should_decode) = candidates.remove(path.as_str()) else {
panic!("File {} not in reference list", path);
};
// Check if we should turn it into a test case.
if !should_decode {
continue;
}
let path = path.to_string();
trials.push(Trial::test(
path.to_string(),
move || -> Result<(), Failed> {
decode_libtiffpic(&path)
.map_err(|e| Failed::from(format_args!("Decoding failed for {}: {}", path, e)))
},
));
}
if !candidates.is_empty() {
for (path, _) in candidates {
eprintln!("Reference file {} not found in directory walk", path);
}
panic!("Some reference files were not found");
}
let args = Arguments::from_args();
libtest_mimic::run(&args, trials).exit();
}
fn decode_libtiffpic(path: &str) -> Result<(), tiff::TiffError> {
let path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path);
let img_file = File::open(path)?;
let mut decoder = Decoder::open(img_file)?;
let mut buffer = DecodingResult::U8(vec![]);
while decoder.more_images() {
decoder.next_image()?;
let _layout = decoder.read_image_to_buffer(&mut buffer);
}
Ok(())
}
+3 -53
View File
@@ -1,4 +1,4 @@
use tiff::decoder::{Decoder, DecodingResult, Limits, TiffCodingUnit};
use tiff::decoder::{Decoder, TiffCodingUnit};
use std::fs::File;
use std::path::PathBuf;
@@ -10,9 +10,8 @@ fn read_all_planes() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
let file = File::open(path).unwrap();
let io = std::io::BufReader::new(file);
let mut tif = Decoder::open(io).unwrap();
tif.next_image().unwrap();
let mut tif = Decoder::new(io).unwrap();
let layout = tif.image_buffer_layout().unwrap();
assert_eq!(layout.planes, 3);
let plane_stride = layout.plane_stride.map_or(0, |x| x.get());
@@ -37,9 +36,8 @@ fn read_first_coding_unit() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
let file = File::open(path).unwrap();
let io = std::io::BufReader::new(file);
let mut tif = Decoder::open(io).unwrap();
tif.next_image().unwrap();
let mut tif = Decoder::new(io).unwrap();
let layout = tif.image_coding_unit_layout(TiffCodingUnit(0)).unwrap();
assert_eq!(layout.planes, 3);
@@ -76,51 +74,3 @@ fn read_first_coding_unit() {
}
}
}
#[test]
fn read_planes_by_limits() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
let file = File::open(path).unwrap();
let io = std::io::BufReader::new(file);
let mut limits = Limits::default();
let mut buffer = DecodingResult::U8(vec![]);
// Make room for 1 plane.
let mut tif = Decoder::open(io).unwrap();
tif = tif.with_limits({
limits.decoding_buffer_size = 374 * 499;
limits.clone()
});
tif.next_image().unwrap();
tif.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499);
// Make room for 2 planes.
tif = tif.with_limits({
limits.decoding_buffer_size = 374 * 499 * 2;
limits.clone()
});
tif.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499);
// Make room for 3 planes.
tif = tif.with_limits({
limits.decoding_buffer_size = 374 * 499 * 3;
limits.clone()
});
tif.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499 * 3);
// Make room for 4 planes?
tif = tif.with_limits({
limits.decoding_buffer_size = 374 * 499 * 4;
limits.clone()
});
tif.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499 * 3);
}
+14 -166
View File
@@ -1,7 +1,7 @@
extern crate tiff;
use tiff::decoder::{ifd, Decoder, DecodingResult};
use tiff::encoder::{colortype, Ifd, Ifd8, Predictor, SRational, TiffEncoder};
use tiff::encoder::{colortype, Ifd, Ifd8, SRational, TiffEncoder};
use tiff::tags::Tag;
use tiff::ColorType;
@@ -33,9 +33,7 @@ fn encode_decode() {
}
{
file.seek(SeekFrom::Start(0)).unwrap();
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
let mut decoder = Decoder::new(&mut file).unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
@@ -118,9 +116,7 @@ fn encode_decode_big() {
}
{
file.seek(SeekFrom::Start(0)).unwrap();
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
let mut decoder = Decoder::new(&mut file).unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
@@ -211,10 +207,8 @@ fn test_encode_ifd() {
// Rewind the cursor for reading
data.set_position(0);
{
let mut decoder = Decoder::open(&mut data).unwrap();
decoder.next_image().unwrap();
let mut decoder = Decoder::new(&mut data).unwrap();
assert_eq!(decoder.assert_tag_u32(65000), 42);
assert_eq!(decoder.assert_tag_u32_vec(65000), [42]);
@@ -253,9 +247,7 @@ macro_rules! test_roundtrip {
) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().unwrap();
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
@@ -276,8 +268,7 @@ macro_rules! test_roundtrip {
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
let mut decoder = Decoder::new(&mut file).unwrap();
decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::$buffer(img_res) = buffer {
assert_eq!(*image_data, img_res);
@@ -388,144 +379,6 @@ fn test_ycbcr_u8_roundtrip() {
test_u8_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
}
/// Test that encoding with the floating-point predictor works correctly
/// and can be decoded back to the original data.
#[test]
fn test_gray_f32_floating_point_predictor() {
// Generate test data: a gradient of f32 values
let width = 32u32;
let height = 32u32;
let mut image_data: Vec<f32> = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
// Create a gradient pattern
image_data.push((x as f32 + y as f32 * 0.1) / 100.0);
}
}
let mut file = Cursor::new(Vec::new());
{
let mut tiff = TiffEncoder::new(&mut file)
.unwrap()
.with_predictor(Predictor::FloatingPoint);
tiff.write_image::<colortype::Gray32Float>(width, height, &image_data)
.unwrap();
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(32));
let mut buffer = DecodingResult::F32(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::F32(img_res) = buffer {
assert_eq!(image_data.len(), img_res.len());
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
assert_eq!(
*orig, *decoded,
"Mismatch at value: orig={}, decoded={}",
orig, decoded
);
}
} else {
panic!("Wrong data type");
}
}
}
/// Test floating-point predictor with RGB f32 data
#[test]
fn test_rgb_f32_floating_point_predictor() {
let width = 16u32;
let height = 16u32;
let mut image_data: Vec<f32> = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
// RGB pattern
image_data.push(x as f32 / width as f32); // R
image_data.push(y as f32 / height as f32); // G
image_data.push((x + y) as f32 / (width + height) as f32); // B
}
}
let mut file = Cursor::new(Vec::new());
{
let mut tiff = TiffEncoder::new(&mut file)
.unwrap()
.with_predictor(Predictor::FloatingPoint);
tiff.write_image::<colortype::RGB32Float>(width, height, &image_data)
.unwrap();
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(32));
let mut buffer = DecodingResult::F32(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::F32(img_res) = buffer {
assert_eq!(image_data.len(), img_res.len());
for (i, (orig, decoded)) in image_data.iter().zip(img_res.iter()).enumerate() {
assert_eq!(
*orig, *decoded,
"Mismatch at index {}: orig={}, decoded={}",
i, orig, decoded
);
}
} else {
panic!("Wrong data type");
}
}
}
/// Test floating-point predictor with f64 data
#[test]
fn test_gray_f64_floating_point_predictor() {
let width = 16u32;
let height = 16u32;
let mut image_data: Vec<f64> = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
image_data.push((x as f64 + y as f64 * 0.1) / 100.0);
}
}
let mut file = Cursor::new(Vec::new());
{
let mut tiff = TiffEncoder::new(&mut file)
.unwrap()
.with_predictor(Predictor::FloatingPoint);
tiff.write_image::<colortype::Gray64Float>(width, height, &image_data)
.unwrap();
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(64));
let mut buffer = DecodingResult::F64(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::F64(img_res) = buffer {
assert_eq!(image_data.len(), img_res.len());
for (orig, decoded) in image_data.iter().zip(img_res.iter()) {
assert_eq!(
*orig, *decoded,
"Mismatch: orig={}, decoded={}",
orig, decoded
);
}
} else {
panic!("Wrong data type");
}
}
}
trait AssertDecode {
fn assert_tag_u32(&mut self, tag: u16) -> u32;
fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec<u32>;
@@ -603,8 +456,7 @@ fn test_multiple_byte() {
data.set_position(0);
{
let mut decoder = Decoder::open(&mut data).unwrap();
decoder.next_image().unwrap();
let mut decoder = Decoder::new(&mut data).unwrap();
assert_eq!(decoder.assert_tag_u32_vec(65000), [1]);
assert_eq!(decoder.assert_tag_u32_vec(65001), [1, 2]);
@@ -685,8 +537,7 @@ fn test_signed() {
//Rewind the cursor for reading
data.set_position(0);
{
let mut decoder = Decoder::open(&mut data).unwrap();
decoder.next_image().unwrap();
let mut decoder = Decoder::new(&mut data).unwrap();
assert_eq!(decoder.assert_tag_i32(65000), -1);
assert_eq!(decoder.assert_tag_i32_vec(65001), [-1]);
@@ -738,14 +589,13 @@ fn test_multipage_image() {
img_file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::open(&mut img_file).unwrap();
decoder.next_image().unwrap();
let mut img_decoder = Decoder::new(&mut img_file).unwrap();
// check the dimensions of the image in the first page
assert_eq!(decoder.dimensions().unwrap(), (2, 2));
decoder.next_image().unwrap();
assert_eq!(img_decoder.dimensions().unwrap(), (2, 2));
img_decoder.next_image().unwrap();
// check the dimensions of the image in the second page
assert_eq!(decoder.dimensions().unwrap(), (3, 3));
assert_eq!(img_decoder.dimensions().unwrap(), (3, 3));
}
}
@@ -774,8 +624,7 @@ fn test_rows_per_strip() {
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::open(&mut file).unwrap();
decoder.next_image().unwrap();
let mut decoder = Decoder::new(&mut file).unwrap();
assert_eq!(decoder.get_tag_u64(Tag::RowsPerStrip).unwrap(), 2);
assert_eq!(decoder.strip_count().unwrap(), 50);
@@ -815,10 +664,9 @@ fn test_auxiliary_directory() {
}
data.set_position(0);
let mut decoder = Decoder::open(&mut data).unwrap();
let mut decoder = Decoder::new(&mut data).unwrap();
{
decoder.next_image().unwrap();
assert_eq!(decoder.dimensions().unwrap(), (1, 1));
let _ = decoder.read_image().unwrap();
}
+1 -2
View File
@@ -110,8 +110,7 @@ fn encode_decode_with_compression(compression: Compression) {
// Decode tiff
data.set_position(0);
{
let mut decoder = Decoder::open(data).unwrap();
decoder.next_image().unwrap();
let mut decoder = Decoder::new(data).unwrap();
// Check the RGB image
assert_eq!(
+1 -2
View File
@@ -13,8 +13,7 @@ fn test_directory<F: Fn(File) -> bool>(path: &str, f: F) {
}
fn decode_tiff(file: File) -> TiffResult<()> {
let mut decoder = Decoder::open(file)?;
decoder.next_image()?;
let mut decoder = Decoder::new(file)?;
decoder.read_image()?;
Ok(())
}
Binary file not shown.
Binary file not shown.
File renamed without changes.
-28
View File
@@ -1,28 +0,0 @@
/cramps-tile.tif/1.1/Sat Nov 23 05:17:31 2002/-kb/
/cramps.tif/1.1/Sat Nov 23 05:17:31 2002/-kb/
/fax2d.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/g3test.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/jello.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/jim___ah.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/jim___cg.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/jim___dg.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/jim___gg.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/off_l16.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/off_luv24.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/off_luv32.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/oxford.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/quad-jpeg.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/quad-lzw.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/quad-tile.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/smallliz.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/strike.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/text.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
/ycbcr-cat.tif/1.1/Sat Nov 23 05:17:32 2002/-kb/
D/depth////
/README/1.2/Thu Dec 29 22:06:14 2005//
/caspian.tif/1.1/Thu Dec 29 22:06:43 2005/-kb/
/dscf0013.tif/1.1/Thu Dec 29 22:07:23 2005/-kb/
/g3test.g3/1.1/Thu Dec 29 22:07:27 2005/-kb/
/ladoga.tif/1.1/Thu Dec 29 22:07:29 2005/-kb/
/pc260001.tif/1.1/Mon Dec 26 14:54:00 2005/-kb/
/zackthecat.tif/1.1/Thu Dec 29 22:08:43 2005/-kb/
-1
View File
@@ -1 +0,0 @@
libtiffpic
-1
View File
@@ -1 +0,0 @@
:pserver:dron@cvs.maptools.org:/cvs/maptools/cvsroot
-101
View File
@@ -1,101 +0,0 @@
The following image files are included for testing. Note that a wide
variety of images can be generated using the tools provided with the
library (e.g. tiffcp, tiff2bw, tiffmedian, etc.); see below for some
hints on this.
PlanarConfiguration = 1 (packed data)
-------------------------------------
caspian.tif 279x220 64-bit floating point (deflate) Caspian Sea from space
cramps.tif 800x607 8-bit b&w (packbits) "cramps poster"
cramps-tile.tif 256x256 tiled version of cramps.tif (no compression)
dscf0013.tif 640x480 YCbCr digital camera image which lacks Reference
Black/White values. Contains EXIF SubIFD. No compression.
fax2d.tif 1728x1082 1-bit b&w (G3/2D) facsimile
g3test.g3 raw Group 3 encoded fax file
g3test.tif TIFF equivalent of g3test.g3 created by fax2tiff
jello.tif 256x192 8-bit RGB (lzw palette) Paul Heckbert "jello"
ladoga.tif 158x118 16-bit unsigned, single band, deflate
off_l16.tif 333x225 8-bit CIE LogL (SGILog) office from Greg Larson
off_luv24.tif 333x225 8-bit CIE LogLuv (SGILog24) office from " "
off_luv32.tif 333x225 8-bit CIE LogLuv (SGILog) office from " "
pc260001.tif 640x480 8-bit RGB digital camera image. Contains EXIF SubIFD.
No compression.
quad-jpeg.tif 512x384 8-bit YCbCr (jpeg) version of quad-lzw.tif
quad-lzw.tif 512x384 8-bit RGB (lzw) "quadric surfaces"
quad-tile.tif 512x384 tiled version of quad-lzw.tif (lzw)
strike.tif 256x200 8-bit RGBA (lzw) "bowling pins" from Pixar
text.tif 1512x359 4-bit b&w (thunderscan) am-express credit card
ycbcr-cat.tif 250x325 8-bit YCbCr (lzw) "kitty" created by rgb2ycbcr
smallliz.tif 160x160 8-bit YCbCr (OLD jpeg) lizard from HP**
zackthecat.tif 234x213 8-bit YCbCr (OLD jpeg) tiled "ZackTheCat" from NeXT**
** These images are in a deprecated format and are included only for testing
backwards compatibility. JPEG-encoded TIFF images use a different scheme
described in TIFF Technical Note #2 and implemented by libtiff.
PlanarConfiguration = 2 (separated samples)
-------------------------------------------
oxford.tif 601x81 8-bit RGB (lzw) screendump off oxford
The other images are from Hewlett Packard and exemplify the use of the
HalftoneHints tag (in their words):
The images are all the same subject, and should all appear the same
after rendering. Each of the images is slightly different as outlined
by the following table:
FileName ToneRange HalftoneHints
jim___cg.tif A Y
jim___dg.tif B N
jim___gg.tif B Y
The images with tone range A are 0-100% linear reflectance images,
in other words, fixed full gamut tone range. The images with tone
range B are what we call "normalized", meaning that the image has
been tone adjusted such the Highlight and Shadow are at gray levels
1 & 2**BitsPerSample-2.
Each of the three grayscale images should be rendered to look like
the halftone in pics/jim___ah.tif when printed on a Canon SX engine.
Generated Images
----------------
The file g3test.tif was created from g3test.g3 with the command:
fax2tiff -o t.tif -1 -p -M -R 98 g3test.g3
Tiled images can be created with tiffcp; e.g.
tiffcp -t -c none cramps.tif cramps-tile.tif
(but the tiled images included here were actually generated by the
Silicon Graphics Image Library).
The file quad-jpeg.tif was created from quad-lzw.tif with the command:
tiffcp -c jpeg quad-lzw.tif quad-jpeg.tif
To generate a tiled image with separated samples the following might
be used:
tiffcp -t -p separate quad-lzw.tif t.tif
[ Beware that tiffcp does not handle every format TIFF image. In particular,
when conversion between tiled and strip'd images is reliable only when
BitsPerSample is 8. ]
To generate a G4-encoded image with a single strip the following would work:
tiffcp -c g4 -r 1082 fax2d.tif t.tif
Contributing New Images
-----------------------
If you have images that are significantly different from those included
in this package and that do NOT HAVE A COPYRIGHT; feel free to send them
to me at sam@engr.sgi.com. I prefer to receive a URL to a location from
which I can retrieve the image by public FTP. However if that is not
possible you can send it to me in a mail message uuencode'd. Please do
not send large images by mail. Also, please do not send me an image
unless it is notably unique; e.g. not easily generated using the tools
I provide.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-40
View File
@@ -1,40 +0,0 @@
/README.txt/1.1/Sun Jan 30 23:03:00 2005//
/genimages/1.1/Mon Jan 31 02:49:00 2005//
/summary.txt/1.1/Mon Jan 31 02:49:00 2005//
/flower-minisblack-24.tif/1.2/Sun Nov 13 17:25:25 2005/-kb/
/flower-rgb-contig-24.tif/1.2/Sun Nov 13 17:22:46 2005/-kb/
/flower-rgb-planar-24.tif/1.2/Sun Nov 13 17:20:24 2005/-kb/
/flower-minisblack-02.tif/1.1/Thu Dec 29 22:08:43 2005/-kb/
/flower-minisblack-04.tif/1.1/Thu Dec 29 22:08:43 2005/-kb/
/flower-minisblack-06.tif/1.1/Thu Dec 29 22:08:43 2005/-kb/
/flower-minisblack-08.tif/1.1/Thu Dec 29 22:08:44 2005/-kb/
/flower-minisblack-10.tif/1.1/Thu Dec 29 22:08:44 2005/-kb/
/flower-minisblack-12.tif/1.1/Thu Dec 29 22:08:45 2005/-kb/
/flower-minisblack-14.tif/1.1/Thu Dec 29 22:08:45 2005/-kb/
/flower-minisblack-16.tif/1.1/Thu Dec 29 22:08:46 2005/-kb/
/flower-minisblack-32.tif/1.1/Thu Dec 29 22:08:47 2005/-kb/
/flower-palette-02.tif/1.1/Thu Dec 29 22:08:47 2005/-kb/
/flower-palette-04.tif/1.1/Thu Dec 29 22:08:47 2005/-kb/
/flower-palette-08.tif/1.1/Thu Dec 29 22:08:47 2005/-kb/
/flower-palette-16.tif/1.1/Thu Dec 29 22:08:49 2005/-kb/
/flower-rgb-contig-02.tif/1.1/Thu Dec 29 22:08:49 2005/-kb/
/flower-rgb-contig-04.tif/1.1/Thu Dec 29 22:08:49 2005/-kb/
/flower-rgb-contig-08.tif/1.1/Thu Dec 29 22:08:50 2005/-kb/
/flower-rgb-contig-10.tif/1.1/Thu Dec 29 22:08:51 2005/-kb/
/flower-rgb-contig-12.tif/1.1/Thu Dec 29 22:08:52 2005/-kb/
/flower-rgb-contig-14.tif/1.1/Thu Dec 29 22:08:53 2005/-kb/
/flower-rgb-contig-16.tif/1.1/Thu Dec 29 22:08:55 2005/-kb/
/flower-rgb-contig-32.tif/1.1/Thu Dec 29 22:08:58 2005/-kb/
/flower-rgb-planar-02.tif/1.1/Thu Dec 29 22:08:58 2005/-kb/
/flower-rgb-planar-04.tif/1.1/Thu Dec 29 22:08:58 2005/-kb/
/flower-rgb-planar-08.tif/1.1/Thu Dec 29 22:08:59 2005/-kb/
/flower-rgb-planar-10.tif/1.1/Thu Dec 29 22:09:00 2005/-kb/
/flower-rgb-planar-12.tif/1.1/Thu Dec 29 22:09:02 2005/-kb/
/flower-rgb-planar-14.tif/1.1/Thu Dec 29 22:09:02 2005/-kb/
/flower-rgb-planar-16.tif/1.1/Thu Dec 29 22:09:04 2005/-kb/
/flower-rgb-planar-32.tif/1.1/Thu Dec 29 22:09:08 2005/-kb/
/flower-separated-contig-08.tif/1.1/Thu Dec 29 22:09:10 2005/-kb/
/flower-separated-contig-16.tif/1.1/Thu Dec 29 22:09:12 2005/-kb/
/flower-separated-planar-08.tif/1.1/Thu Dec 29 22:09:12 2005/-kb/
/flower-separated-planar-16.tif/1.1/Thu Dec 29 22:09:14 2005/-kb/
D
-1
View File
@@ -1 +0,0 @@
libtiffpic/depth
-1
View File
@@ -1 +0,0 @@
:pserver:dron@cvs.maptools.org:/cvs/maptools/cvsroot
-9
View File
@@ -1,9 +0,0 @@
These sample TIFF image files are prepared by Bob Friesenhahn
<bfriesen@simple.dallas.tx.us> using a development version of
GraphicsMagick 1.2.
See the file summary.txt for a description of the images.
These files are hereby placed in the public domain.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-73
View File
@@ -1,73 +0,0 @@
#!/usr/bin/ksh
GM=/scratch/bfriesen/build/GraphicsMagick-32-static/utilities/gm
GM_STD_OPT="-compress none"
MASTER_IMAGE_ORIGINAL='/home/bfriesen/images/david/112_1201-flowers.jpg'
MASTER_IMAGE_GRAY=gray.miff
MASTER_IMAGE_RGB=rgb.miff
MASTER_IMAGE_CMYK=cmyk.miff
COLUMNS=73
ROWS=43
GRAY_DEPTHS='2 4 6 8 10 12 14 16 24 32'
PSEUDO_DEPTHS='2 4 8 16'
TRUE_COLORS='2 4 8 10 12 14 16 24 32'
CMYK_COLORS='8 16'
SUMMARY=summary.txt
echo > $SUMMARY
$GM convert $MASTER_IMAGE_ORIGINAL +profile '*' -depth 32 -type Grayscale -density 72x72 -geometry "${COLUMNS}x${ROWS}" $MASTER_IMAGE_GRAY
$GM convert $MASTER_IMAGE_ORIGINAL +profile '*' -depth 32 -density 72x72 -geometry "${COLUMNS}x${ROWS}" $MASTER_IMAGE_RGB
$GM convert $MASTER_IMAGE_ORIGINAL +profile '*' -colorspace cmyk -depth 32 -density 72x72 -geometry "${COLUMNS}x${ROWS}" $MASTER_IMAGE_CMYK
# Gray images
for depth in $GRAY_DEPTHS
do
outname=`printf "flower-minisblack-%02d.tif" $depth`
echo "$outname\t${COLUMNS}x${ROWS} $depth-bit minisblack gray image" >> $SUMMARY
$GM convert -verbose $MASTER_IMAGE_GRAY $GM_STD_OPT -type Grayscale -define tiff:bits-per-sample=$depth $outname
done
# Colormapped images
for depth in $PSEUDO_DEPTHS
do
colors=`echo "2^${depth}" | bc`
outname=`printf "flower-palette-%02d.tif" $depth`
echo "$outname\t${COLUMNS}x${ROWS} ${colors}-entry colormapped image" >> $SUMMARY
$GM convert -verbose $MASTER_IMAGE_RGB $GM_STD_OPT -colors $colors $outname
done
# Truecolor images
for depth in $TRUE_COLORS
do
outname=`printf "flower-rgb-contig-%02d.tif" $depth`
echo "$outname\t${COLUMNS}x${ROWS} $depth-bit contiguous RGB image" >> $SUMMARY
$GM convert -verbose $MASTER_IMAGE_RGB $GM_STD_OPT -type TrueColor -define tiff:bits-per-sample=$depth $outname
done
# Truecolor planar images
for depth in $TRUE_COLORS
do
outname=`printf "flower-rgb-planar-%02d.tif" $depth`
echo "$outname\t${COLUMNS}x${ROWS} $depth-bit seperated RGB image" >> $SUMMARY
$GM convert -verbose $MASTER_IMAGE_RGB $GM_STD_OPT -type TrueColor -interlace plane -define tiff:bits-per-sample=$depth $outname
done
# CMYK images
for depth in $CMYK_COLORS
do
outname=`printf "flower-separated-contig-%02d.tif" $depth`
echo "$outname\t${COLUMNS}x${ROWS} $depth-bit contiguous CMYK image" >> $SUMMARY
$GM convert -verbose $MASTER_IMAGE_RGB $GM_STD_OPT -type ColorSeparation -define tiff:bits-per-sample=$depth $outname
done
# CMYK planar images
for depth in $CMYK_COLORS
do
outname=`printf "flower-separated-planar-%02d.tif" $depth`
echo "$outname\t${COLUMNS}x${ROWS} $depth-bit separated CMYK image" >> $SUMMARY
$GM convert -verbose $MASTER_IMAGE_RGB $GM_STD_OPT -type ColorSeparation -interlace plane -define tiff:bits-per-sample=$depth $outname
done
rm -f $MASTER_IMAGE_GRAY $MASTER_IMAGE_RGB $MASTER_IMAGE_CMYK
-37
View File
@@ -1,37 +0,0 @@
flower-minisblack-02.tif 73x43 2-bit minisblack gray image
flower-minisblack-04.tif 73x43 4-bit minisblack gray image
flower-minisblack-06.tif 73x43 6-bit minisblack gray image
flower-minisblack-08.tif 73x43 8-bit minisblack gray image
flower-minisblack-10.tif 73x43 10-bit minisblack gray image
flower-minisblack-12.tif 73x43 12-bit minisblack gray image
flower-minisblack-14.tif 73x43 14-bit minisblack gray image
flower-minisblack-16.tif 73x43 16-bit minisblack gray image
flower-minisblack-24.tif 73x43 24-bit minisblack gray image
flower-minisblack-32.tif 73x43 32-bit minisblack gray image
flower-palette-02.tif 73x43 4-entry colormapped image
flower-palette-04.tif 73x43 16-entry colormapped image
flower-palette-08.tif 73x43 256-entry colormapped image
flower-palette-16.tif 73x43 65536-entry colormapped image
flower-rgb-contig-02.tif 73x43 2-bit contiguous RGB image
flower-rgb-contig-04.tif 73x43 4-bit contiguous RGB image
flower-rgb-contig-08.tif 73x43 8-bit contiguous RGB image
flower-rgb-contig-10.tif 73x43 10-bit contiguous RGB image
flower-rgb-contig-12.tif 73x43 12-bit contiguous RGB image
flower-rgb-contig-14.tif 73x43 14-bit contiguous RGB image
flower-rgb-contig-16.tif 73x43 16-bit contiguous RGB image
flower-rgb-contig-24.tif 73x43 24-bit contiguous RGB image
flower-rgb-contig-32.tif 73x43 32-bit contiguous RGB image
flower-rgb-planar-02.tif 73x43 2-bit seperated RGB image
flower-rgb-planar-04.tif 73x43 4-bit seperated RGB image
flower-rgb-planar-08.tif 73x43 8-bit seperated RGB image
flower-rgb-planar-10.tif 73x43 10-bit seperated RGB image
flower-rgb-planar-12.tif 73x43 12-bit seperated RGB image
flower-rgb-planar-14.tif 73x43 14-bit seperated RGB image
flower-rgb-planar-16.tif 73x43 16-bit seperated RGB image
flower-rgb-planar-24.tif 73x43 24-bit seperated RGB image
flower-rgb-planar-32.tif 73x43 32-bit seperated RGB image
flower-separated-contig-08.tif 73x43 8-bit contiguous CMYK image
flower-separated-contig-16.tif 73x43 16-bit contiguous CMYK image
flower-separated-planar-08.tif 73x43 8-bit separated CMYK image
flower-separated-planar-16.tif 73x43 16-bit separated CMYK image
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff. Show more