Merge pull request #374 from image-rs/value-read-provider
Add a `Read`-er for entry values
This commit is contained in:
5 files changed
+417
-2
No files matched your search
@@ -0,0 +1,362 @@
|
|||||||
|
use std::io::{self, Cursor, Read, Seek};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
decoder::{ifd::Entry, stream::EndianReader, ValueReader},
|
||||||
|
tags::{ByteOrder, IfdPointer, Type},
|
||||||
|
TiffResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct EntryBytesReader<'lt, R> {
|
||||||
|
inner: &'lt mut EndianReader<R>,
|
||||||
|
variant: Variant,
|
||||||
|
}
|
||||||
|
|
||||||
|
const OFFSET_BUFFER_LEN: usize = 8;
|
||||||
|
|
||||||
|
enum Variant {
|
||||||
|
Immediate {
|
||||||
|
/// An in-value reader if the offset field contains the data. Note that we rotate it to the
|
||||||
|
/// back of this buffer, not the front, so that `Cursor` already contains the right end.
|
||||||
|
/// We do not let you seek back beyond our start.
|
||||||
|
data: Cursor<[u8; OFFSET_BUFFER_LEN]>,
|
||||||
|
value_bytes: u8,
|
||||||
|
},
|
||||||
|
InFile {
|
||||||
|
// the length relative to the base so we can seek relative without querying the position.
|
||||||
|
value_bytes: u64,
|
||||||
|
remainder: u64,
|
||||||
|
base: u64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'lt, R> EntryBytesReader<'lt, R> {
|
||||||
|
pub(super) fn from_entry(inner: &'lt mut ValueReader<R>, entry: Entry) -> TiffResult<Self>
|
||||||
|
where
|
||||||
|
R: Read + Seek,
|
||||||
|
{
|
||||||
|
let offset: [_; OFFSET_BUFFER_LEN] = *entry.offset_raw();
|
||||||
|
let limit = if inner.bigtiff { 8 } else { 4 };
|
||||||
|
assert!(limit as usize <= OFFSET_BUFFER_LEN);
|
||||||
|
|
||||||
|
let value_bytes = entry.field_type().value_bytes(entry.count())?;
|
||||||
|
|
||||||
|
if value_bytes <= limit {
|
||||||
|
// We move the data to the back of the array whereas it is in the front right now.
|
||||||
|
let mut value = offset;
|
||||||
|
value.rotate_left(value_bytes as usize);
|
||||||
|
let mut cursor = Cursor::new(value);
|
||||||
|
cursor.set_position(OFFSET_BUFFER_LEN as u64 - value_bytes);
|
||||||
|
|
||||||
|
Ok(EntryBytesReader {
|
||||||
|
inner: &mut inner.reader,
|
||||||
|
variant: Variant::Immediate {
|
||||||
|
data: cursor,
|
||||||
|
value_bytes: value_bytes as u8,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// remainder > limit >= 0 so this is an unreachable panic.
|
||||||
|
let pointer = if inner.bigtiff {
|
||||||
|
let mut offset = offset;
|
||||||
|
|
||||||
|
inner
|
||||||
|
.reader
|
||||||
|
.byte_order
|
||||||
|
.convert(Type::IFD8, &mut offset[..], ByteOrder::native());
|
||||||
|
|
||||||
|
IfdPointer(u64::from_ne_bytes(offset))
|
||||||
|
} else {
|
||||||
|
let mut offset = *offset.first_chunk::<4>().unwrap();
|
||||||
|
|
||||||
|
inner
|
||||||
|
.reader
|
||||||
|
.byte_order
|
||||||
|
.convert(Type::IFD, &mut offset[..], ByteOrder::native());
|
||||||
|
|
||||||
|
let offset = u32::from_ne_bytes(offset);
|
||||||
|
IfdPointer(u64::from(offset))
|
||||||
|
};
|
||||||
|
|
||||||
|
inner.reader.goto_offset(pointer.0)?;
|
||||||
|
|
||||||
|
Ok(EntryBytesReader {
|
||||||
|
inner: &mut inner.reader,
|
||||||
|
variant: Variant::InFile {
|
||||||
|
value_bytes,
|
||||||
|
remainder: value_bytes,
|
||||||
|
base: pointer.0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read> Read for EntryBytesReader<'_, R> {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
match &mut self.variant {
|
||||||
|
Variant::Immediate { data, .. } => data.read(buf),
|
||||||
|
Variant::InFile { remainder, .. } => {
|
||||||
|
let bound = usize::try_from(*remainder).unwrap_or(usize::MAX);
|
||||||
|
let avail = buf.len().min(bound);
|
||||||
|
let read = self.inner.inner().read(&mut buf[..avail])?;
|
||||||
|
|
||||||
|
// On a well-behaved `Read`: read <= avail <= bound <=(mathematical) remainder
|
||||||
|
let consume = read as u64;
|
||||||
|
*remainder -= consume;
|
||||||
|
Ok(read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
|
||||||
|
match &mut self.variant {
|
||||||
|
Variant::Immediate { data, .. } => data.read_exact(buf),
|
||||||
|
Variant::InFile { remainder, .. } => {
|
||||||
|
let bound = usize::try_from(*remainder).unwrap_or(usize::MAX);
|
||||||
|
|
||||||
|
// Fail early if we would be sure to over-read.
|
||||||
|
if buf.len() > bound {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::UnexpectedEof,
|
||||||
|
"Tiff value shorter than the requested buffer size",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// since buf.len() <= bound <=(mathematical) remainder
|
||||||
|
let consume = buf.len() as u64;
|
||||||
|
self.inner.inner().read_exact(buf)?;
|
||||||
|
*remainder -= consume;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read + Seek> Seek for EntryBytesReader<'_, R> {
|
||||||
|
fn seek(&mut self, offset: io::SeekFrom) -> io::Result<u64> {
|
||||||
|
let (value_bytes, remainder) = match &self.variant {
|
||||||
|
Variant::Immediate { data, value_bytes } => {
|
||||||
|
let remainder = data.get_ref().len() as u64 - data.position();
|
||||||
|
(*value_bytes as u64, remainder)
|
||||||
|
}
|
||||||
|
&Variant::InFile {
|
||||||
|
value_bytes,
|
||||||
|
remainder,
|
||||||
|
base: _,
|
||||||
|
} => (value_bytes, remainder),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize to an offset from the start
|
||||||
|
let offset_from_start = match offset {
|
||||||
|
io::SeekFrom::Start(n) => Some(n),
|
||||||
|
io::SeekFrom::Current(n) => {
|
||||||
|
let front = value_bytes - remainder;
|
||||||
|
front.checked_add_signed(n)
|
||||||
|
}
|
||||||
|
io::SeekFrom::End(n) => value_bytes.checked_add_signed(n),
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some((offset, remainder)) = offset_from_start.and_then(|n| {
|
||||||
|
let remainder = value_bytes.checked_sub(n)?;
|
||||||
|
Some((n, remainder))
|
||||||
|
}) else {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"seek out of bounds of entry value",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
match &mut self.variant {
|
||||||
|
Variant::Immediate { data, .. } => {
|
||||||
|
let start = OFFSET_BUFFER_LEN as u64 - value_bytes;
|
||||||
|
data.set_position(start + offset);
|
||||||
|
Ok(offset)
|
||||||
|
}
|
||||||
|
&mut Variant::InFile { base, .. } => {
|
||||||
|
// We checked this against overflow on construction. Ignore the offset we get.
|
||||||
|
self.inner
|
||||||
|
.inner()
|
||||||
|
.seek(io::SeekFrom::Start(base + offset))?;
|
||||||
|
|
||||||
|
self.variant = Variant::InFile {
|
||||||
|
value_bytes,
|
||||||
|
remainder,
|
||||||
|
base,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stream_position(&mut self) -> io::Result<u64> {
|
||||||
|
Ok(match &self.variant {
|
||||||
|
Variant::Immediate { data, value_bytes } => {
|
||||||
|
// Recall the value is at the end of the buffer.
|
||||||
|
let from_end = OFFSET_BUFFER_LEN as u64 - data.position();
|
||||||
|
u64::from(*value_bytes) - from_end
|
||||||
|
}
|
||||||
|
// Note: we might think about using the inner `stream_position` here. However that
|
||||||
|
// exposes something interesting: If the internal stream is modified concurrently from
|
||||||
|
// another instance (e.g. a shared `File`) then we may lose track of it anyways.
|
||||||
|
// Especially the `Read` implementation is not bounded on `Seek` so it could not
|
||||||
|
// acquire the position. And it should not since that is just a different form of race
|
||||||
|
// where we risk the position being modified by a different thread just as well.
|
||||||
|
//
|
||||||
|
// So in the interest of least surprise we just assume that we can track the position
|
||||||
|
// accurately here and that the inner reader uses for instance `read_at` consistently.
|
||||||
|
// We own the value (or mutable reference) it after all.
|
||||||
|
//
|
||||||
|
// Also this one MUST be compatible with `seek(SeekFrom::Current(0))` as per trait
|
||||||
|
// defintion—so at least keep those consistent if changing anything.
|
||||||
|
&Variant::InFile {
|
||||||
|
remainder,
|
||||||
|
value_bytes,
|
||||||
|
..
|
||||||
|
} => value_bytes - remainder,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{decoder::Decoder, encoder, tags};
|
||||||
|
use std::io::{Read as _, Seek as _, SeekFrom};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_seek_tag_bytes() -> Result<(), crate::error::TiffError> {
|
||||||
|
let file = std::fs::File::open(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/tests/images/int8_rgb.tif"
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut decoder = Decoder::open(file).unwrap();
|
||||||
|
decoder.next_directory().unwrap();
|
||||||
|
|
||||||
|
let mut buffer = crate::tags::ValueBuffer::default();
|
||||||
|
|
||||||
|
decoder
|
||||||
|
.current_ifd()
|
||||||
|
.find_tag_buf(tags::Tag::StripByteCounts, &mut buffer)
|
||||||
|
.expect("some strip byte count is present");
|
||||||
|
|
||||||
|
let bytes_of_value = buffer.as_bytes().len() as u64;
|
||||||
|
|
||||||
|
let mut reader = decoder
|
||||||
|
.read_tag(tags::Tag::StripByteCounts)?
|
||||||
|
.expect("some strip byte count is present");
|
||||||
|
|
||||||
|
let mut data = vec![];
|
||||||
|
reader.read_to_end(&mut data)?;
|
||||||
|
|
||||||
|
// `ValueBuffer` also stores its data in the file endianess until converted.
|
||||||
|
assert_eq!(reader.stream_position()?, bytes_of_value);
|
||||||
|
assert_eq!(buffer.as_bytes(), data);
|
||||||
|
|
||||||
|
reader.seek(SeekFrom::Start(0))?;
|
||||||
|
assert_eq!(reader.stream_position()?, 0);
|
||||||
|
reader.read_to_end(&mut data)?;
|
||||||
|
assert_eq!(reader.stream_position()?, bytes_of_value);
|
||||||
|
|
||||||
|
let (first, second) = data.split_at(bytes_of_value as usize);
|
||||||
|
assert_eq!(first, second);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(clippy::seek_from_current)] // We want to test that functionality.
|
||||||
|
fn seek_from_end() -> Result<(), crate::error::TiffError> {
|
||||||
|
const ARTIST: &str = "99 little bugs in the code";
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
let mut data = std::io::Cursor::new(vec![]);
|
||||||
|
let mut encoder = encoder::TiffEncoder::new(&mut data)?;
|
||||||
|
|
||||||
|
let mut dir = encoder.image_directory()?;
|
||||||
|
dir.write_tag(tags::Tag::Artist, ARTIST)?;
|
||||||
|
dir.finish()?;
|
||||||
|
|
||||||
|
data.into_inner()
|
||||||
|
};
|
||||||
|
|
||||||
|
let file = std::io::Cursor::new(data);
|
||||||
|
let mut decoder = Decoder::open(file).unwrap();
|
||||||
|
decoder.next_directory().unwrap();
|
||||||
|
|
||||||
|
let mut reader = decoder
|
||||||
|
.read_tag(tags::Tag::Artist)?
|
||||||
|
.expect("the artist is present?");
|
||||||
|
|
||||||
|
let mut value = vec![];
|
||||||
|
reader.read_to_end(&mut value)?;
|
||||||
|
|
||||||
|
let (&nul, value) = value.split_last().unwrap();
|
||||||
|
assert_eq!(value, ARTIST.as_bytes());
|
||||||
|
assert_eq!(nul, b'\0');
|
||||||
|
|
||||||
|
assert_eq!(reader.seek(SeekFrom::End(0))?, ARTIST.len() as u64 + 1);
|
||||||
|
assert!(reader.seek(SeekFrom::End(1)).is_err());
|
||||||
|
assert_eq!(reader.stream_position()?, ARTIST.len() as u64 + 1);
|
||||||
|
|
||||||
|
// Check relative seeks by comparing against the "code"
|
||||||
|
let code_offset = reader.seek(SeekFrom::End(-5))?;
|
||||||
|
let mut is_code = [0u8; 4];
|
||||||
|
reader.read_exact(&mut is_code)?;
|
||||||
|
assert_eq!(is_code, *value.last_chunk::<4>().unwrap());
|
||||||
|
|
||||||
|
assert_eq!(reader.seek(SeekFrom::Start(0))?, 0);
|
||||||
|
assert_eq!(reader.stream_position()?, 0);
|
||||||
|
assert_eq!(reader.seek(SeekFrom::Current(0))?, 0);
|
||||||
|
assert_eq!(reader.stream_position()?, 0);
|
||||||
|
assert!(reader.seek(SeekFrom::Current(-1)).is_err());
|
||||||
|
assert_eq!(reader.stream_position()?, 0);
|
||||||
|
|
||||||
|
reader.seek(SeekFrom::Start(code_offset))?;
|
||||||
|
reader.read_exact(&mut is_code)?;
|
||||||
|
assert_eq!(reader.stream_position()?, ARTIST.len() as u64);
|
||||||
|
assert_eq!(is_code, *value.last_chunk::<4>().unwrap());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check that the reader from an `offset` field also works.
|
||||||
|
#[test]
|
||||||
|
fn read_from_offset() -> Result<(), crate::error::TiffError> {
|
||||||
|
let data = {
|
||||||
|
let mut data = std::io::Cursor::new(vec![]);
|
||||||
|
let mut encoder = encoder::TiffEncoder::new(&mut data)?;
|
||||||
|
|
||||||
|
let mut dir = encoder.image_directory()?;
|
||||||
|
dir.write_tag(
|
||||||
|
tags::Tag::PlanarConfiguration,
|
||||||
|
tags::PlanarConfiguration::Planar,
|
||||||
|
)?;
|
||||||
|
dir.finish()?;
|
||||||
|
|
||||||
|
data.into_inner()
|
||||||
|
};
|
||||||
|
|
||||||
|
let file = std::io::Cursor::new(data);
|
||||||
|
let mut decoder = Decoder::open(file).unwrap();
|
||||||
|
decoder.next_directory().unwrap();
|
||||||
|
let bo = decoder.byte_order();
|
||||||
|
|
||||||
|
let mut reader = decoder
|
||||||
|
.read_tag(tags::Tag::PlanarConfiguration)?
|
||||||
|
.expect("some artist is present");
|
||||||
|
|
||||||
|
let mut buf = [0; 2];
|
||||||
|
reader.read_exact(&mut buf)?;
|
||||||
|
|
||||||
|
bo.convert(tags::Type::SHORT, &mut buf, tags::ByteOrder::BigEndian);
|
||||||
|
assert_eq!(buf, [0, 2]);
|
||||||
|
|
||||||
|
// Check relative seeks, this item is only two bytes long.
|
||||||
|
assert!(reader.seek(SeekFrom::End(-3)).is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -393,7 +393,7 @@ impl Entry {
|
|||||||
self.count
|
self.count
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn offset(&self) -> &[u8] {
|
pub(crate) fn offset_raw(&self) -> &[u8; 8] {
|
||||||
&self.offset
|
&self.offset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use self::stream::EndianReader;
|
|||||||
|
|
||||||
mod buffer;
|
mod buffer;
|
||||||
mod cycles;
|
mod cycles;
|
||||||
|
mod entry_reader;
|
||||||
pub mod ifd;
|
pub mod ifd;
|
||||||
mod image;
|
mod image;
|
||||||
mod logluv;
|
mod logluv;
|
||||||
@@ -22,6 +23,7 @@ mod stream;
|
|||||||
mod tag_reader;
|
mod tag_reader;
|
||||||
|
|
||||||
pub use self::buffer::{DecodingSampleBuffer, DecodingSampleSlice, DecodingSampleType};
|
pub use self::buffer::{DecodingSampleBuffer, DecodingSampleSlice, DecodingSampleType};
|
||||||
|
pub use self::entry_reader::EntryBytesReader;
|
||||||
|
|
||||||
#[deprecated = "Renamed to `DecodingSampleBuffer`"]
|
#[deprecated = "Renamed to `DecodingSampleBuffer`"]
|
||||||
pub type DecodingResult = DecodingSampleBuffer;
|
pub type DecodingResult = DecodingSampleBuffer;
|
||||||
@@ -850,6 +852,15 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
self.value_reader.reader.byte_order
|
self.value_reader.reader.byte_order
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if this file is a BigTIFF or a standard TIFF.
|
||||||
|
pub fn variant(&self) -> TiffVariant {
|
||||||
|
if self.value_reader.bigtiff {
|
||||||
|
TiffVariant::BigTiff
|
||||||
|
} else {
|
||||||
|
TiffVariant::Standard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn read_ifd_offset(&mut self) -> Result<u64, io::Error> {
|
pub fn read_ifd_offset(&mut self) -> Result<u64, io::Error> {
|
||||||
if self.value_reader.bigtiff {
|
if self.value_reader.bigtiff {
|
||||||
@@ -943,6 +954,40 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
Ok(val)
|
Ok(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a byte reader for the value of an arbitrary IFD entry.
|
||||||
|
///
|
||||||
|
/// Small values are stored inline inside the offset field of the entry. The threshold for this
|
||||||
|
/// depends on the [`Self::variant`] of the file. Interpret entries that have not been
|
||||||
|
/// extracted from an IFD of the current decoder with care.
|
||||||
|
///
|
||||||
|
/// The reader returned form this method reads raw bytes. Values retrieved in this way are not
|
||||||
|
/// yet converted to the host endianess.
|
||||||
|
///
|
||||||
|
/// Returns an error if the entry count exceeds the possible offset values or if the offset
|
||||||
|
/// of the entry would overflow the offset in the underlying stream. Otherwise returns a reader
|
||||||
|
/// that yields the bytes of the value.
|
||||||
|
///
|
||||||
|
/// ## Design Notes
|
||||||
|
///
|
||||||
|
/// This method does not appear on [`IfdDecoder`] as we assume the reader type may be important
|
||||||
|
/// to you. It also allows forward compatibility to additional trait implementations without
|
||||||
|
/// demanding them from all types as `dyn` would. There is also no `M×N` instantiation problem
|
||||||
|
/// with this method: you get one type per reader type and monomorphic methods from the traits
|
||||||
|
/// it implements.
|
||||||
|
pub fn read_entry(&mut self, entry: ifd::Entry) -> TiffResult<EntryBytesReader<'_, R>> {
|
||||||
|
EntryBytesReader::from_entry(&mut self.value_reader, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility to create a reader like [`Self::read_entry`], from the current IFD.
|
||||||
|
///
|
||||||
|
/// Returns an `Ok(None)` if the tag is not found in the current directory.
|
||||||
|
pub fn read_tag(&mut self, tag: Tag) -> TiffResult<Option<EntryBytesReader<'_, R>>> {
|
||||||
|
match self.current_ifd().find_entry(tag) {
|
||||||
|
None => Ok(None),
|
||||||
|
Some(entry) => self.read_entry(entry).map(Some),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Reads a TIFF IFA offset/value field
|
/// Reads a TIFF IFA offset/value field
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn read_offset_u64(&mut self) -> Result<[u8; 8], io::Error> {
|
pub fn read_offset_u64(&mut self) -> Result<[u8; 8], io::Error> {
|
||||||
|
|||||||
+2
-1
@@ -488,7 +488,8 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
|||||||
self.writer.write_u16(entry.field_type().to_u16())?;
|
self.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(self.writer)?;
|
||||||
self.writer.write_bytes(&entry.offset()[..offset_bytes])?;
|
self.writer
|
||||||
|
.write_bytes(&entry.offset_raw()[..offset_bytes])?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(offset)
|
Ok(offset)
|
||||||
|
|||||||
@@ -567,6 +567,13 @@ impl ValueBuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create an empty value buffer with `Byte` type. This type choice is arbitrary.
|
||||||
|
impl Default for ValueBuffer {
|
||||||
|
fn default() -> Self {
|
||||||
|
ValueBuffer::empty(Type::BYTE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Byte order of the TIFF file.
|
/// Byte order of the TIFF file.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum ByteOrder {
|
pub enum ByteOrder {
|
||||||
|
|||||||
Reference in new issue
Block a user