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
Aurelia Molzer 1bd6293857 Merge pull request #329 from image-rs/fixup-value-buffer-debug-condition
Fix debug condition of ValueBuffer
2026-01-13 14:53:12 +00:00
A. Molzer 87341f9075 Fix debug condition of ValueBuffer
Off-by-one, this was meant to check a minimum length that includes
equality but instead tested a valid index.
2026-01-13 15:37:22 +01:00
Aurelia Molzer 43343f64cd Merge pull request #328 from image-rs/reduce-package-size
Remove benchmark images from distribution
2026-01-13 09:12:59 +00:00
Aurelia Molzer be04326264 Remove benchmark images from distribution 2026-01-13 10:06:59 +01:00
Aurelia Molzer 93a68f52f5 Merge pull request #327 from image-rs/release-0.11.1
Metadata for release 0.11.1
2026-01-13 09:04:30 +00:00
5 changed files with 595 additions and 13 deletions

No files matched your search

+1 -1
View File
@@ -14,7 +14,7 @@ authors = ["The image-rs Developers"]
repository = "https://github.com/image-rs/image-tiff" repository = "https://github.com/image-rs/image-tiff"
categories = ["multimedia::images", "multimedia::encoding"] categories = ["multimedia::images", "multimedia::encoding"]
exclude = ["tests/images/*", "tests/fuzz_images/*"] exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
[dependencies] [dependencies]
half = { version = "2.4.1" } half = { version = "2.4.1" }
+241 -10
View File
@@ -2,24 +2,25 @@ pub use tiff_value::*;
use std::{ use std::{
cmp, cmp,
io::{self, Seek, Write}, io::{self, Read, Seek, Write},
marker::PhantomData, marker::PhantomData,
mem, mem,
num::{NonZeroU64, TryFromIntError}, num::{NonZeroU64, TryFromIntError},
}; };
use crate::{ use crate::{
decoder::ifd::Entry, decoder::{ifd::Entry, Decoder},
error::{TiffResult, UsageError}, error::{TiffResult, UsageError},
tags::{ tags::{
ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation, ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation,
ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer, PlanarConfiguration, ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
}, },
Directory, TiffError, TiffFormatError, Directory, TiffError, TiffFormatError,
}; };
pub mod colortype; pub mod colortype;
pub mod compression; pub mod compression;
mod tag_filter;
mod tiff_value; mod tiff_value;
mod writer; mod writer;
@@ -40,6 +41,8 @@ pub type Predictor = crate::tags::Predictor;
#[cfg(feature = "deflate")] #[cfg(feature = "deflate")]
pub type DeflateLevel = compression::DeflateLevel; pub type DeflateLevel = compression::DeflateLevel;
pub use tag_filter::TagFilter;
#[derive(Clone, Copy, PartialEq, Default)] #[derive(Clone, Copy, PartialEq, Default)]
pub enum Compression { pub enum Compression {
#[default] #[default]
@@ -434,6 +437,148 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
self.directory.extend(entries); 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. /// Define the parent directory.
/// ///
/// Each directory has an offset based link to its successor, forming a linked list in the /// Each directory has an offset based link to its successor, forming a linked list in the
@@ -460,17 +605,21 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
} }
fn write_directory(&mut self) -> TiffResult<u64> { 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. // Start by turning all buffered unwritten values into entries.
let offset = self.writer.offset(); let offset = writer.offset();
K::write_entry_count(self.writer, self.directory.len())?; K::write_entry_count(writer, directory.len())?;
let offset_bytes = mem::size_of::<K::OffsetType>(); let offset_bytes = mem::size_of::<K::OffsetType>();
for (tag, entry) in self.directory.iter() { for (tag, entry) in directory.iter() {
self.writer.write_u16(tag.to_u16())?; writer.write_u16(tag.to_u16())?;
self.writer.write_u16(entry.field_type().to_u16())?; writer.write_u16(entry.field_type().to_u16())?;
let count = K::convert_offset(entry.count())?; let count = K::convert_offset(entry.count())?;
count.write(self.writer)?; count.write(writer)?;
self.writer.write_bytes(&entry.offset()[..offset_bytes])?; writer.write_bytes(&entry.offset()[..offset_bytes])?;
} }
Ok(offset) Ok(offset)
@@ -582,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. /// Type to encode images strip by strip.
/// ///
/// You should call `finish` on this when you are finished with it. /// You should call `finish` on this when you are finished with it.
@@ -822,6 +1037,22 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
Ok(()) 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 /// Set image resolution
pub fn resolution(&mut self, unit: ResolutionUnit, value: Rational) { pub fn resolution(&mut self, unit: ResolutionUnit, value: Rational) {
self.encoder 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,
}
+60 -2
View File
@@ -390,7 +390,7 @@ impl ValueBuffer {
debug_assert!({ debug_assert!({
self.ty self.ty
.value_bytes(self.count) .value_bytes(self.count)
.is_ok_and(|n| n < self.bytes.len() as u64) .is_ok_and(|n| n <= self.bytes.len() as u64)
}); });
self.count self.count
@@ -407,6 +407,32 @@ impl ValueBuffer {
&mut self.bytes[..len] &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. /// Change the byte order of the value representation.
pub fn set_byte_order(&mut self, byte_order: ByteOrder) { pub fn set_byte_order(&mut self, byte_order: ByteOrder) {
let len = self.assumed_len_from_count(); let len = self.assumed_len_from_count();
@@ -439,7 +465,7 @@ impl ValueBuffer {
pub(crate) fn assume_type(&mut self, ty: Type, count: u64, bo: ByteOrder) { pub(crate) fn assume_type(&mut self, ty: Type, count: u64, bo: ByteOrder) {
debug_assert!({ debug_assert!({
ty.value_bytes(count) ty.value_bytes(count)
.is_ok_and(|n| n < self.bytes.len() as u64) .is_ok_and(|n| n <= self.bytes.len() as u64)
}); });
self.byte_order = bo; self.byte_order = bo;
@@ -531,3 +557,35 @@ pub(crate) enum EndianBytes {
Four, Four,
Eight, Eight,
} }
#[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);
}
}
+37
View File
@@ -0,0 +1,37 @@
use tiff::{
decoder::Decoder,
encoder::{colortype::Gray8, EncodeMetadataOptions, TiffEncoder},
tags::Tag,
};
#[test]
fn copies_exif_data() {
let src = std::path::PathBuf::from(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/images/predictor-3-gray-f32.tif"
));
let src = std::fs::File::open(src).unwrap();
let mut src = Decoder::new(src).unwrap();
let mut data = std::io::Cursor::new(vec![]);
{
let mut dst = TiffEncoder::new(&mut data).unwrap();
let mut img = dst.new_image::<Gray8>(1, 1).unwrap();
let report = img
.write_metadata_copy_from(&mut src, EncodeMetadataOptions::default())
.unwrap();
assert_eq!(report.copied, 8);
assert_eq!(report.filtered, 0, "There are no tags we do not recognize");
img.write_data(&[0x80]).unwrap();
}
data.set_position(0);
{
let mut roundtrip = Decoder::new(data).unwrap();
assert!(roundtrip.find_tag(Tag::DateTime).unwrap().is_some());
}
}