diff --git a/Cargo.toml b/Cargo.toml index cf43511..629b450 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,9 @@ num_enum = "0.7.3" # Match the version used by pyo3-object-store object_store = { git = "https://github.com/apache/arrow-rs", rev = "7a15e4b47ca97df2edef689c9f2ebd2f3888b79e" } thiserror = "1" -tiff = "0.9" tokio = { version = "1.43.0", optional = true } weezl = "0.1.0" [dev-dependencies] +tiff = "0.9.1" tokio = { version = "1.9", features = ["macros", "fs", "rt-multi-thread"] } diff --git a/src/async_reader.rs b/src/async_reader.rs index cfd2108..e115084 100644 --- a/src/async_reader.rs +++ b/src/async_reader.rs @@ -169,7 +169,7 @@ pub enum Endianness { #[derive(Debug)] pub(crate) struct AsyncCursor { reader: Box, - offset: usize, + offset: u64, endianness: Endianness, } @@ -212,7 +212,7 @@ impl AsyncCursor { } /// Read the given number of bytes, advancing the internal cursor state by the same amount. - pub(crate) async fn read(&mut self, length: usize) -> Result { + pub(crate) async fn read(&mut self, length: u64) -> Result { let range = self.offset as _..(self.offset + length) as _; self.offset += length; let bytes = self.reader.get_bytes(range).await?; @@ -281,15 +281,15 @@ impl AsyncCursor { } /// Advance cursor position by a set amount - pub(crate) fn advance(&mut self, amount: usize) { + pub(crate) fn advance(&mut self, amount: u64) { self.offset += amount; } - pub(crate) fn seek(&mut self, offset: usize) { + pub(crate) fn seek(&mut self, offset: u64) { self.offset = offset; } - pub(crate) fn position(&self) -> usize { + pub(crate) fn position(&self) -> u64 { self.offset } } diff --git a/src/cog.rs b/src/cog.rs index 84d2bd2..4567a1b 100644 --- a/src/cog.rs +++ b/src/cog.rs @@ -1,6 +1,7 @@ use crate::async_reader::AsyncCursor; use crate::error::Result; use crate::ifd::ImageFileDirectories; +use crate::tiff::{TiffError, TiffFormatError}; use crate::AsyncFileReader; #[derive(Debug)] @@ -8,6 +9,8 @@ pub struct COGReader { #[allow(dead_code)] cursor: AsyncCursor, ifds: ImageFileDirectories, + #[allow(dead_code)] + bigtiff: bool, } impl COGReader { @@ -15,14 +18,39 @@ impl COGReader { let mut cursor = AsyncCursor::try_open_tiff(reader).await?; let version = cursor.read_u16().await?; - // Assert it's a standard non-big tiff - assert_eq!(version, 42); - - let first_ifd_location = cursor.read_u32().await?; - - let ifds = ImageFileDirectories::open(&mut cursor, first_ifd_location as usize).await?; - - Ok(Self { cursor, ifds }) + let bigtiff = match version { + 42 => false, + 43 => { + // Read bytesize of offsets (in bigtiff it's alway 8 but provide a way to move to 16 some day) + if cursor.read_u16().await? != 8 { + return Err( + TiffError::FormatError(TiffFormatError::TiffSignatureNotFound).into(), + ); + } + // This constant should always be 0 + if cursor.read_u16().await? != 0 { + return Err( + TiffError::FormatError(TiffFormatError::TiffSignatureNotFound).into(), + ); + } + true + } + _ => return Err(TiffError::FormatError(TiffFormatError::TiffSignatureInvalid).into()), + }; + + let first_ifd_location = if bigtiff { + cursor.read_u64().await? + } else { + cursor.read_u32().await?.into() + }; + + let ifds = ImageFileDirectories::open(&mut cursor, first_ifd_location, bigtiff).await?; + + Ok(Self { + cursor, + ifds, + bigtiff, + }) } pub fn ifds(&self) -> &ImageFileDirectories { diff --git a/src/decoder.rs b/src/decoder.rs index a3964a3..17f1cd3 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -4,10 +4,10 @@ use std::io::{Cursor, Read}; use bytes::Bytes; use flate2::bufread::ZlibDecoder; -use tiff::tags::{CompressionMethod, PhotometricInterpretation}; -use tiff::{TiffError, TiffUnsupportedError}; use crate::error::Result; +use crate::tiff::tags::{CompressionMethod, PhotometricInterpretation}; +use crate::tiff::{TiffError, TiffUnsupportedError}; /// A registry of decoders. #[derive(Debug)] diff --git a/src/error.rs b/src/error.rs index 9215a01..e279417 100644 --- a/src/error.rs +++ b/src/error.rs @@ -22,7 +22,7 @@ pub enum AiocogeoError { ObjectStore(#[from] object_store::Error), #[error(transparent)] - TIFFError(#[from] tiff::TiffError), + InternalTIFFError(#[from] crate::tiff::TiffError), } /// Crate-specific result type. diff --git a/src/geo/geo_key_directory.rs b/src/geo/geo_key_directory.rs index dcb9e71..3ef6127 100644 --- a/src/geo/geo_key_directory.rs +++ b/src/geo/geo_key_directory.rs @@ -3,8 +3,9 @@ use std::collections::HashMap; use num_enum::{IntoPrimitive, TryFromPrimitive}; -use tiff::decoder::ifd::Value; -use tiff::{TiffError, TiffResult}; + +use crate::tiff::Value; +use crate::tiff::{TiffError, TiffResult}; /// Geospatial TIFF tag variants #[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive, IntoPrimitive, Eq, Hash)] diff --git a/src/ifd.rs b/src/ifd.rs index 5422b6d..9fdc596 100644 --- a/src/ifd.rs +++ b/src/ifd.rs @@ -4,17 +4,17 @@ use std::ops::Range; use bytes::Bytes; use num_enum::TryFromPrimitive; -use tiff::decoder::ifd::Value; -use tiff::tags::{ - CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, ResolutionUnit, - SampleFormat, Tag, Type, -}; -use tiff::TiffError; use crate::async_reader::AsyncCursor; use crate::decoder::{decode_tile, DecoderRegistry}; use crate::error::{AiocogeoError, Result}; use crate::geo::{AffineTransform, GeoKeyDirectory, GeoKeyTag}; +use crate::tiff::tags::{ + CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, ResolutionUnit, + SampleFormat, Tag, Type, +}; +use crate::tiff::TiffError; +use crate::tiff::Value; use crate::AsyncFileReader; const DOCUMENT_NAME: u16 = 269; @@ -38,12 +38,16 @@ impl AsRef<[ImageFileDirectory]> for ImageFileDirectories { } impl ImageFileDirectories { - pub(crate) async fn open(cursor: &mut AsyncCursor, ifd_offset: usize) -> Result { + pub(crate) async fn open( + cursor: &mut AsyncCursor, + ifd_offset: u64, + bigtiff: bool, + ) -> Result { let mut next_ifd_offset = Some(ifd_offset); let mut ifds = vec![]; while let Some(offset) = next_ifd_offset { - let ifd = ImageFileDirectory::read(cursor, offset).await?; + let ifd = ImageFileDirectory::read(cursor, offset, bigtiff).await?; next_ifd_offset = ifd.next_ifd_offset(); ifds.push(ifd); } @@ -75,7 +79,7 @@ pub struct ImageFileDirectory { pub(crate) image_description: Option, - pub(crate) strip_offsets: Option>, + pub(crate) strip_offsets: Option>, pub(crate) orientation: Option, @@ -88,7 +92,7 @@ pub struct ImageFileDirectory { pub(crate) rows_per_strip: Option, - pub(crate) strip_byte_counts: Option>, + pub(crate) strip_byte_counts: Option>, pub(crate) min_sample_value: Option>, pub(crate) max_sample_value: Option>, @@ -155,8 +159,8 @@ pub struct ImageFileDirectory { pub(crate) tile_width: Option, pub(crate) tile_height: Option, - pub(crate) tile_offsets: Option>, - pub(crate) tile_byte_counts: Option>, + pub(crate) tile_offsets: Option>, + pub(crate) tile_byte_counts: Option>, pub(crate) extra_samples: Option>, @@ -176,41 +180,62 @@ pub struct ImageFileDirectory { // gdal_metadata pub(crate) other_tags: HashMap, - pub(crate) next_ifd_offset: Option, + pub(crate) next_ifd_offset: Option, } impl ImageFileDirectory { /// Read and parse the IFD starting at the given file offset - async fn read(cursor: &mut AsyncCursor, ifd_start: usize) -> Result { + async fn read(cursor: &mut AsyncCursor, ifd_start: u64, bigtiff: bool) -> Result { cursor.seek(ifd_start); - let tag_count = cursor.read_u16().await?; + let tag_count = if bigtiff { + cursor.read_u64().await? + } else { + cursor.read_u16().await?.into() + }; let mut tags = HashMap::with_capacity(tag_count as usize); for _ in 0..tag_count { - let (tag_name, tag_value) = read_tag(cursor).await?; + let (tag_name, tag_value) = read_tag(cursor, bigtiff).await?; tags.insert(tag_name, tag_value); } + dbg!(&tags); + + // Tag 2 bytes + // Type 2 bytes + // Count: + // - bigtiff: 8 bytes + // - else: 4 bytes + // Value: + // - bigtiff: 8 bytes either a pointer the value itself + // - else: 4 bytes either a pointer the value itself + let ifd_entry_byte_size = if bigtiff { 20 } else { 12 }; + // The size of `tag_count` that we read above + let tag_count_byte_size = if bigtiff { 8 } else { 2 }; + + // Reset the cursor position before reading the next ifd offset + cursor.seek(ifd_start + (ifd_entry_byte_size * tag_count) + tag_count_byte_size); + + let next_ifd_offset = if bigtiff { + cursor.read_u64().await? + } else { + cursor.read_u32().await?.into() + }; - cursor.seek(ifd_start + (12 * tag_count as usize) + 2); - - let next_ifd_offset = cursor.read_u32().await?; + // If the ifd_offset is 0, stop let next_ifd_offset = if next_ifd_offset == 0 { None } else { - Some(next_ifd_offset as usize) + Some(next_ifd_offset) }; Self::from_tags(tags, next_ifd_offset) } - fn next_ifd_offset(&self) -> Option { + fn next_ifd_offset(&self) -> Option { self.next_ifd_offset } - fn from_tags( - mut tag_data: HashMap, - next_ifd_offset: Option, - ) -> Result { + fn from_tags(mut tag_data: HashMap, next_ifd_offset: Option) -> Result { let mut new_subfile_type = None; let mut image_width = None; let mut image_height = None; @@ -267,11 +292,17 @@ impl ImageFileDirectory { PhotometricInterpretation::from_u16(value.into_u16()?) } Tag::ImageDescription => image_description = Some(value.into_string()?), - Tag::StripOffsets => strip_offsets = Some(value.into_u32_vec()?), + Tag::StripOffsets => { + dbg!(&value); + strip_offsets = Some(value.into_u64_vec()?) + } Tag::Orientation => orientation = Some(value.into_u16()?), Tag::SamplesPerPixel => samples_per_pixel = Some(value.into_u16()?), Tag::RowsPerStrip => rows_per_strip = Some(value.into_u32()?), - Tag::StripByteCounts => strip_byte_counts = Some(value.into_u32_vec()?), + Tag::StripByteCounts => { + dbg!(&value); + strip_byte_counts = Some(value.into_u64_vec()?) + } Tag::MinSampleValue => min_sample_value = Some(value.into_u16_vec()?), Tag::MaxSampleValue => max_sample_value = Some(value.into_u16_vec()?), Tag::XResolution => match value { @@ -296,8 +327,8 @@ impl ImageFileDirectory { Tag::ColorMap => color_map = Some(value.into_u16_vec()?), Tag::TileWidth => tile_width = Some(value.into_u32()?), Tag::TileLength => tile_height = Some(value.into_u32()?), - Tag::TileOffsets => tile_offsets = Some(value.into_u32_vec()?), - Tag::TileByteCounts => tile_byte_counts = Some(value.into_u32_vec()?), + Tag::TileOffsets => tile_offsets = Some(value.into_u64_vec()?), + Tag::TileByteCounts => tile_byte_counts = Some(value.into_u64_vec()?), Tag::ExtraSamples => extra_samples = Some(value.into_u16_vec()?), Tag::SampleFormat => { let values = value.into_u16_vec()?; @@ -411,14 +442,19 @@ impl ImageFileDirectory { // https://web.archive.org/web/20240329145253/https://www.awaresystems.be/imaging/tiff/tifftags/planarconfiguration.html PlanarConfiguration::Chunky } else { - panic!("planar_configuration not found and samples_per_pixel not 1") + dbg!(planar_configuration); + dbg!(samples_per_pixel); + println!("planar_configuration not found and samples_per_pixel not 1"); + PlanarConfiguration::Chunky }; Ok(Self { new_subfile_type, image_width: image_width.expect("image_width not found"), image_height: image_height.expect("image_height not found"), bits_per_sample: bits_per_sample.expect("bits per sample not found"), - compression: compression.expect("compression not found"), + // Defaults to no compression + // https://web.archive.org/web/20240329145331/https://www.awaresystems.be/imaging/tiff/tifftags/compression.html + compression: compression.unwrap_or(CompressionMethod::None), photometric_interpretation: photometric_interpretation .expect("photometric interpretation not found"), document_name, @@ -493,7 +529,7 @@ impl ImageFileDirectory { self.image_description.as_deref() } - pub fn strip_offsets(&self) -> Option<&[u32]> { + pub fn strip_offsets(&self) -> Option<&[u64]> { self.strip_offsets.as_deref() } @@ -514,7 +550,7 @@ impl ImageFileDirectory { self.rows_per_strip } - pub fn strip_byte_counts(&self) -> Option<&[u32]> { + pub fn strip_byte_counts(&self) -> Option<&[u64]> { self.strip_byte_counts.as_deref() } @@ -591,10 +627,10 @@ impl ImageFileDirectory { self.tile_height } - pub fn tile_offsets(&self) -> Option<&[u32]> { + pub fn tile_offsets(&self) -> Option<&[u64]> { self.tile_offsets.as_deref() } - pub fn tile_byte_counts(&self) -> Option<&[u32]> { + pub fn tile_byte_counts(&self) -> Option<&[u64]> { self.tile_byte_counts.as_deref() } @@ -793,20 +829,27 @@ impl ImageFileDirectory { } /// Read a single tag from the cursor -async fn read_tag(cursor: &mut AsyncCursor) -> Result<(Tag, Value)> { - let code = cursor.read_u16().await?; - let tag_name = Tag::from_u16_exhaustive(code); +async fn read_tag(cursor: &mut AsyncCursor, bigtiff: bool) -> Result<(Tag, Value)> { + let start_cursor_position = cursor.position(); - let current_cursor_position = cursor.position(); + let tag_name = Tag::from_u16_exhaustive(cursor.read_u16().await?); let tag_type_code = cursor.read_u16().await?; - let tag_type = Type::from_u16(tag_type_code).expect("Unknown tag type {tag_type_code}"); - let count = cursor.read_u32().await? as usize; + let tag_type = Type::from_u16(tag_type_code).expect( + "Unknown tag type {tag_type_code}. TODO: we should skip entries with unknown tag types.", + ); + dbg!(tag_name, tag_type); + let count = if bigtiff { + cursor.read_u64().await? + } else { + cursor.read_u32().await?.into() + }; - let tag_value = read_tag_value(cursor, tag_type, count).await?; + let tag_value = read_tag_value(cursor, tag_type, count, bigtiff).await?; // TODO: better handle management of cursor state - cursor.seek(current_cursor_position + 10); + let ifd_entry_size = if bigtiff { 20 } else { 12 }; + cursor.seek(start_cursor_position + ifd_entry_size); Ok((tag_name, tag_value)) } @@ -819,8 +862,8 @@ async fn read_tag(cursor: &mut AsyncCursor) -> Result<(Tag, Value)> { async fn read_tag_value( cursor: &mut AsyncCursor, tag_type: Type, - count: usize, - // length: usize, + count: u64, + bigtiff: bool, ) -> Result { // Case 1: there are no values so we can return immediately. if count == 0 { @@ -837,7 +880,6 @@ async fn read_tag_value( | Type::RATIONAL | Type::SRATIONAL | Type::IFD8 => 8, - t => panic!("unexpected type {t:?}"), }; let value_byte_length = count.checked_mul(tag_size).unwrap(); @@ -845,7 +887,29 @@ async fn read_tag_value( // Case 2: there is one value. if count == 1 { // 2a: the value is 5-8 bytes and we're in BigTiff mode. - // We don't support bigtiff yet + dbg!("case 2a"); + if bigtiff && value_byte_length > 4 && value_byte_length <= 8 { + let mut data = cursor.read(value_byte_length).await?; + + return Ok(match tag_type { + Type::LONG8 => Value::UnsignedBig(data.read_u64()?), + Type::SLONG8 => Value::SignedBig(data.read_i64()?), + Type::DOUBLE => Value::Double(data.read_f64()?), + Type::RATIONAL => Value::Rational(data.read_u32()?, data.read_u32()?), + Type::SRATIONAL => Value::SRational(data.read_i32()?, data.read_i32()?), + Type::IFD8 => Value::IfdBig(data.read_u64()?), + Type::BYTE + | Type::SBYTE + | Type::ASCII + | Type::UNDEFINED + | Type::SHORT + | Type::SSHORT + | Type::LONG + | Type::SLONG + | Type::FLOAT + | Type::IFD => unreachable!(), + }); + } // NOTE: we should only be reading value_byte_length when it's 4 bytes or fewer. Right now // we're reading even if it's 8 bytes, but then only using the first 4 bytes of this @@ -853,6 +917,7 @@ async fn read_tag_value( let mut data = cursor.read(value_byte_length).await?; // 2b: the value is at most 4 bytes or doesn't fit in the offset field. + dbg!("case 2b"); return Ok(match tag_type { Type::BYTE | Type::UNDEFINED => Value::Byte(data.read_u8()?), Type::SBYTE => Value::Signed(data.read_i8()? as i32), @@ -871,29 +936,29 @@ async fn read_tag_value( } Type::LONG8 => { let offset = data.read_u32()?; - cursor.seek(offset as usize); + cursor.seek(offset as _); Value::UnsignedBig(cursor.read_u64().await?) } Type::SLONG8 => { let offset = data.read_u32()?; - cursor.seek(offset as usize); + cursor.seek(offset as _); Value::SignedBig(cursor.read_i64().await?) } Type::DOUBLE => { let offset = data.read_u32()?; - cursor.seek(offset as usize); + cursor.seek(offset as _); Value::Double(cursor.read_f64().await?) } Type::RATIONAL => { let offset = data.read_u32()?; - cursor.seek(offset as usize); + cursor.seek(offset as _); let numerator = cursor.read_u32().await?; let denominator = cursor.read_u32().await?; Value::Rational(numerator, denominator) } Type::SRATIONAL => { let offset = data.read_u32()?; - cursor.seek(offset as usize); + cursor.seek(offset as _); let numerator = cursor.read_i32().await?; let denominator = cursor.read_i32().await?; Value::SRational(numerator, denominator) @@ -901,17 +966,21 @@ async fn read_tag_value( Type::IFD => Value::Ifd(data.read_u32()?), Type::IFD8 => { let offset = data.read_u32()?; - cursor.seek(offset as usize); + cursor.seek(offset as _); Value::IfdBig(cursor.read_u64().await?) } - t => panic!("unexpected tag type {t:?}"), }); } // Case 3: There is more than one value, but it fits in the offset field. - if value_byte_length <= 4 { + if value_byte_length <= 4 || bigtiff && value_byte_length <= 8 { + dbg!("case 3"); let mut data = cursor.read(value_byte_length).await?; - cursor.advance(4 - value_byte_length); + if bigtiff { + cursor.advance(8 - value_byte_length); + } else { + cursor.advance(4 - value_byte_length); + } match tag_type { Type::BYTE | Type::UNDEFINED => { @@ -933,7 +1002,7 @@ async fn read_tag_value( } } Type::ASCII => { - let mut buf = vec![0; count]; + let mut buf = vec![0; count as usize]; data.read_exact(&mut buf)?; if buf.is_ascii() && buf.ends_with(&[0]) { let v = std::str::from_utf8(&buf) @@ -995,76 +1064,80 @@ async fn read_tag_value( | Type::IFD8 => { unreachable!() } - t => panic!("unexpected tag type {t:?}"), } } // Seek cursor - let offset = cursor.read_u32().await?; - cursor.seek(offset as usize); + let offset = if bigtiff { + cursor.read_u64().await? + } else { + cursor.read_u32().await?.into() + }; + cursor.seek(offset); // Case 4: there is more than one value, and it doesn't fit in the offset field. + dbg!("case 4"); match tag_type { // TODO check if this could give wrong results // at a different endianess of file/computer. Type::BYTE | Type::UNDEFINED => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::Byte(cursor.read_u8().await?)) } Ok(Value::List(v)) } Type::SBYTE => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::Signed(cursor.read_i8().await? as i32)) } Ok(Value::List(v)) } Type::SHORT => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::Short(cursor.read_u16().await?)) } Ok(Value::List(v)) } Type::SSHORT => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::Signed(cursor.read_i16().await? as i32)) } Ok(Value::List(v)) } Type::LONG => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::Unsigned(cursor.read_u32().await?)) } Ok(Value::List(v)) } Type::SLONG => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::Signed(cursor.read_i32().await?)) } Ok(Value::List(v)) } Type::FLOAT => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::Float(cursor.read_f32().await?)) } Ok(Value::List(v)) } Type::DOUBLE => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::Double(cursor.read_f64().await?)) } Ok(Value::List(v)) } Type::RATIONAL => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::Rational( cursor.read_u32().await?, @@ -1074,7 +1147,7 @@ async fn read_tag_value( Ok(Value::List(v)) } Type::SRATIONAL => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::SRational( cursor.read_i32().await?, @@ -1084,37 +1157,36 @@ async fn read_tag_value( Ok(Value::List(v)) } Type::LONG8 => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::UnsignedBig(cursor.read_u64().await?)) } Ok(Value::List(v)) } Type::SLONG8 => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::SignedBig(cursor.read_i64().await?)) } Ok(Value::List(v)) } Type::IFD => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::Ifd(cursor.read_u32().await?)) } Ok(Value::List(v)) } Type::IFD8 => { - let mut v = Vec::with_capacity(count); + let mut v = Vec::with_capacity(count as _); for _ in 0..count { v.push(Value::IfdBig(cursor.read_u64().await?)) } Ok(Value::List(v)) } Type::ASCII => { - let n = count; - let mut out = vec![0; n]; - let mut buf = cursor.read(n).await?; + let mut out = vec![0; count as _]; + let mut buf = cursor.read(count).await?; buf.read_exact(&mut out)?; // Strings may be null-terminated, so we trim anything downstream of the null byte @@ -1125,6 +1197,5 @@ async fn read_tag_value( String::from_utf8(out).map_err(|err| AiocogeoError::General(err.to_string()))?, )) } - t => panic!("unexpected tag type {t:?}"), } } diff --git a/src/lib.rs b/src/lib.rs index 60776f9..28aab48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod decoder; pub mod error; pub mod geo; mod ifd; +pub mod tiff; pub use async_reader::{AsyncFileReader, ObjectReader, PrefetchReader}; pub use cog::COGReader; diff --git a/src/tiff/error.rs b/src/tiff/error.rs new file mode 100644 index 0000000..2fd70d2 --- /dev/null +++ b/src/tiff/error.rs @@ -0,0 +1,390 @@ +use std::error::Error; +use std::fmt; +use std::fmt::Display; +use std::io; +use std::str; +use std::string; +use std::sync::Arc; + +use jpeg::UnsupportedFeature; + +use super::ifd::Value; +use super::tags::{ + CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag, +}; + +/// Tiff error kinds. +#[derive(Debug)] +#[allow(clippy::enum_variant_names)] +pub enum TiffError { + /// The Image is not formatted properly. + FormatError(TiffFormatError), + + /// The Decoder does not support features required by the image. + UnsupportedError(TiffUnsupportedError), + + /// An I/O Error occurred while decoding the image. + IoError(io::Error), + + /// An integer conversion to or from a platform size failed, either due to + /// limits of the platform size or limits of the format. + IntSizeError, + + /// The image does not support the requested operation + UsageError(UsageError), +} + +/// The image is not formatted properly. +/// +/// This indicates that the encoder producing the image might behave incorrectly or that the input +/// file has been corrupted. +/// +/// The list of variants may grow to incorporate errors of future features. Matching against this +/// exhaustively is not covered by interface stability guarantees. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum TiffFormatError { + TiffSignatureNotFound, + TiffSignatureInvalid, + ImageFileDirectoryNotFound, + InconsistentSizesEncountered, + UnexpectedCompressedData { + actual_bytes: usize, + required_bytes: usize, + }, + InconsistentStripSamples { + actual_samples: usize, + required_samples: usize, + }, + InvalidDimensions(u32, u32), + InvalidTag, + InvalidTagValueType(Tag), + RequiredTagNotFound(Tag), + UnknownPredictor(u16), + UnknownPlanarConfiguration(u16), + ByteExpected(Value), + SignedByteExpected(Value), + SignedShortExpected(Value), + UnsignedIntegerExpected(Value), + SignedIntegerExpected(Value), + Format(String), + RequiredTagEmpty(Tag), + StripTileTagConflict, + CycleInOffsets, + JpegDecoder(JpegDecoderError), + SamplesPerPixelIsZero, +} + +impl fmt::Display for TiffFormatError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + use self::TiffFormatError::*; + match *self { + TiffSignatureNotFound => write!(fmt, "TIFF signature not found."), + TiffSignatureInvalid => write!(fmt, "TIFF signature invalid."), + ImageFileDirectoryNotFound => write!(fmt, "Image file directory not found."), + InconsistentSizesEncountered => write!(fmt, "Inconsistent sizes encountered."), + UnexpectedCompressedData { + actual_bytes, + required_bytes, + } => { + write!( + fmt, + "Decompression returned different amount of bytes than expected: got {}, expected {}.", + actual_bytes, required_bytes + ) + } + InconsistentStripSamples { + actual_samples, + required_samples, + } => { + write!( + fmt, + "Inconsistent elements in strip: got {}, expected {}.", + actual_samples, required_samples + ) + } + InvalidDimensions(width, height) => write!(fmt, "Invalid dimensions: {}x{}.", width, height), + InvalidTag => write!(fmt, "Image contains invalid tag."), + InvalidTagValueType(ref tag) => { + write!(fmt, "Tag `{:?}` did not have the expected value type.", tag) + } + RequiredTagNotFound(ref tag) => write!(fmt, "Required tag `{:?}` not found.", tag), + UnknownPredictor(ref predictor) => { + write!(fmt, "Unknown predictor “{}” encountered", predictor) + } + UnknownPlanarConfiguration(ref planar_config) => { + write!(fmt, "Unknown planar configuration “{}” encountered", planar_config) + } + ByteExpected(ref val) => write!(fmt, "Expected byte, {:?} found.", val), + SignedByteExpected(ref val) => write!(fmt, "Expected signed byte, {:?} found.", val), + SignedShortExpected(ref val) => write!(fmt, "Expected signed short, {:?} found.", val), + UnsignedIntegerExpected(ref val) => { + write!(fmt, "Expected unsigned integer, {:?} found.", val) + } + SignedIntegerExpected(ref val) => { + write!(fmt, "Expected signed integer, {:?} found.", val) + } + Format(ref val) => write!(fmt, "Invalid format: {:?}.", val), + RequiredTagEmpty(ref val) => write!(fmt, "Required tag {:?} was empty.", val), + StripTileTagConflict => write!(fmt, "File should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found."), + CycleInOffsets => write!(fmt, "File contained a cycle in the list of IFDs"), + JpegDecoder(ref error) => write!(fmt, "{}", error), + SamplesPerPixelIsZero => write!(fmt, "Samples per pixel is zero"), + } + } +} + +/// The Decoder does not support features required by the image. +/// +/// This only captures known failures for which the standard either does not require support or an +/// implementation has been planned but not yet completed. Some variants may become unused over +/// time and will then get deprecated before being removed. +/// +/// The list of variants may grow. Matching against this exhaustively is not covered by interface +/// stability guarantees. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum TiffUnsupportedError { + // FloatingPointPredictor(ColorType), + // HorizontalPredictor(ColorType), + InconsistentBitsPerSample(Vec), + InterpretationWithBits(PhotometricInterpretation, Vec), + UnknownInterpretation, + UnknownCompressionMethod, + UnsupportedCompressionMethod(CompressionMethod), + UnsupportedSampleDepth(u8), + UnsupportedSampleFormat(Vec), + // UnsupportedColorType(ColorType), + UnsupportedBitsPerChannel(u8), + UnsupportedPlanarConfig(Option), + UnsupportedDataType, + UnsupportedInterpretation(PhotometricInterpretation), + UnsupportedJpegFeature(UnsupportedFeature), + MisalignedTileBoundaries, +} + +impl fmt::Display for TiffUnsupportedError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + use self::TiffUnsupportedError::*; + match *self { + // FloatingPointPredictor(color_type) => write!( + // fmt, + // "Floating point predictor for {:?} is unsupported.", + // color_type + // ), + // HorizontalPredictor(color_type) => write!( + // fmt, + // "Horizontal predictor for {:?} is unsupported.", + // color_type + // ), + InconsistentBitsPerSample(ref bits_per_sample) => { + write!(fmt, "Inconsistent bits per sample: {:?}.", bits_per_sample) + } + InterpretationWithBits(ref photometric_interpretation, ref bits_per_sample) => write!( + fmt, + "{:?} with {:?} bits per sample is unsupported", + photometric_interpretation, bits_per_sample + ), + UnknownInterpretation => write!( + fmt, + "The image is using an unknown photometric interpretation." + ), + UnknownCompressionMethod => write!(fmt, "Unknown compression method."), + UnsupportedCompressionMethod(method) => { + write!(fmt, "Compression method {:?} is unsupported", method) + } + UnsupportedSampleDepth(samples) => { + write!(fmt, "{} samples per pixel is unsupported.", samples) + } + UnsupportedSampleFormat(ref formats) => { + write!(fmt, "Sample format {:?} is unsupported.", formats) + } + // UnsupportedColorType(color_type) => { + // write!(fmt, "Color type {:?} is unsupported", color_type) + // } + UnsupportedBitsPerChannel(bits) => { + write!(fmt, "{} bits per channel not supported", bits) + } + UnsupportedPlanarConfig(config) => { + write!(fmt, "Unsupported planar configuration “{:?}”.", config) + } + UnsupportedDataType => write!(fmt, "Unsupported data type."), + UnsupportedInterpretation(interpretation) => { + write!( + fmt, + "Unsupported photometric interpretation \"{:?}\".", + interpretation + ) + } + UnsupportedJpegFeature(ref unsupported_feature) => { + write!(fmt, "Unsupported JPEG feature {:?}", unsupported_feature) + } + MisalignedTileBoundaries => write!(fmt, "Tile rows are not aligned to byte boundaries"), + } + } +} + +/// User attempted to use the Decoder in a way that is incompatible with a specific image. +/// +/// For example: attempting to read a tile from a stripped image. +#[derive(Debug)] +pub enum UsageError { + // InvalidChunkType(ChunkType, ChunkType), + InvalidChunkIndex(u32), + PredictorCompressionMismatch, + PredictorIncompatible, + PredictorUnavailable, +} + +impl fmt::Display for UsageError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + use self::UsageError::*; + match *self { + // InvalidChunkType(expected, actual) => { + // write!( + // fmt, + // "Requested operation is only valid for images with chunk encoding of type: {:?}, got {:?}.", + // expected, actual + // ) + // } + InvalidChunkIndex(index) => write!(fmt, "Image chunk index ({}) requested.", index), + PredictorCompressionMismatch => write!( + fmt, + "The requested predictor is not compatible with the requested compression" + ), + PredictorIncompatible => write!( + fmt, + "The requested predictor is not compatible with the image's format" + ), + PredictorUnavailable => write!(fmt, "The requested predictor is not available"), + } + } +} + +impl fmt::Display for TiffError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + TiffError::FormatError(ref e) => write!(fmt, "Format error: {}", e), + TiffError::UnsupportedError(ref f) => write!( + fmt, + "The Decoder does not support the \ + image format `{}`", + f + ), + TiffError::IoError(ref e) => e.fmt(fmt), + TiffError::IntSizeError => write!(fmt, "Platform or format size limits exceeded"), + TiffError::UsageError(ref e) => write!(fmt, "Usage error: {}", e), + } + } +} + +impl Error for TiffError { + fn description(&self) -> &str { + match *self { + TiffError::FormatError(..) => "Format error", + TiffError::UnsupportedError(..) => "Unsupported error", + TiffError::IoError(..) => "IO error", + TiffError::IntSizeError => "Platform or format size limits exceeded", + TiffError::UsageError(..) => "Invalid usage", + } + } + + fn cause(&self) -> Option<&dyn Error> { + match *self { + TiffError::IoError(ref e) => Some(e), + _ => None, + } + } +} + +impl From for TiffError { + fn from(err: io::Error) -> TiffError { + TiffError::IoError(err) + } +} + +impl From for TiffError { + fn from(_err: str::Utf8Error) -> TiffError { + TiffError::FormatError(TiffFormatError::InvalidTag) + } +} + +impl From for TiffError { + fn from(_err: string::FromUtf8Error) -> TiffError { + TiffError::FormatError(TiffFormatError::InvalidTag) + } +} + +impl From for TiffError { + fn from(err: TiffFormatError) -> TiffError { + TiffError::FormatError(err) + } +} + +impl From for TiffError { + fn from(err: TiffUnsupportedError) -> TiffError { + TiffError::UnsupportedError(err) + } +} + +impl From for TiffError { + fn from(err: UsageError) -> TiffError { + TiffError::UsageError(err) + } +} + +impl From for TiffError { + fn from(_err: std::num::TryFromIntError) -> TiffError { + TiffError::IntSizeError + } +} + +// impl From for TiffError { +// fn from(err: LzwError) -> TiffError { +// match err { +// LzwError::InvalidCode => TiffError::FormatError(TiffFormatError::Format(String::from( +// "LZW compressed data corrupted", +// ))), +// } +// } +// } + +#[derive(Debug, Clone)] +pub struct JpegDecoderError { + inner: Arc, +} + +impl JpegDecoderError { + fn new(error: jpeg::Error) -> Self { + Self { + inner: Arc::new(error), + } + } +} + +impl PartialEq for JpegDecoderError { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.inner, &other.inner) + } +} + +impl Display for JpegDecoderError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.fmt(f) + } +} + +impl From for TiffError { + fn from(error: JpegDecoderError) -> Self { + TiffError::FormatError(TiffFormatError::JpegDecoder(error)) + } +} + +impl From for TiffError { + fn from(error: jpeg::Error) -> Self { + JpegDecoderError::new(error).into() + } +} + +/// Result of an image decoding/encoding process +pub type TiffResult = Result; diff --git a/src/tiff/ifd.rs b/src/tiff/ifd.rs new file mode 100644 index 0000000..48f9ac7 --- /dev/null +++ b/src/tiff/ifd.rs @@ -0,0 +1,327 @@ +//! Function for reading TIFF tags + +use std::vec; + +use self::Value::{ + Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, RationalBig, SRational, SRationalBig, + Short, Signed, SignedBig, SignedByte, SignedShort, Unsigned, UnsignedBig, +}; +use super::error::{TiffError, TiffFormatError, TiffResult}; + +#[allow(unused_qualifications)] +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum Value { + Byte(u8), + Short(u16), + SignedByte(i8), + SignedShort(i16), + Signed(i32), + SignedBig(i64), + Unsigned(u32), + UnsignedBig(u64), + Float(f32), + Double(f64), + List(Vec), + Rational(u32, u32), + RationalBig(u64, u64), + SRational(i32, i32), + SRationalBig(i64, i64), + Ascii(String), + Ifd(u32), + IfdBig(u64), +} + +impl Value { + pub fn into_u8(self) -> TiffResult { + match self { + Byte(val) => Ok(val), + val => Err(TiffError::FormatError(TiffFormatError::ByteExpected(val))), + } + } + pub fn into_i8(self) -> TiffResult { + match self { + SignedByte(val) => Ok(val), + val => Err(TiffError::FormatError(TiffFormatError::SignedByteExpected( + val, + ))), + } + } + + pub fn into_u16(self) -> TiffResult { + match self { + Short(val) => Ok(val), + Unsigned(val) => Ok(u16::try_from(val)?), + UnsignedBig(val) => Ok(u16::try_from(val)?), + val => Err(TiffError::FormatError( + TiffFormatError::UnsignedIntegerExpected(val), + )), + } + } + + pub fn into_i16(self) -> TiffResult { + match self { + SignedByte(val) => Ok(val.into()), + SignedShort(val) => Ok(val), + Signed(val) => Ok(i16::try_from(val)?), + SignedBig(val) => Ok(i16::try_from(val)?), + val => Err(TiffError::FormatError( + TiffFormatError::SignedShortExpected(val), + )), + } + } + + pub fn into_u32(self) -> TiffResult { + match self { + Short(val) => Ok(val.into()), + Unsigned(val) => Ok(val), + UnsignedBig(val) => Ok(u32::try_from(val)?), + Ifd(val) => Ok(val), + IfdBig(val) => Ok(u32::try_from(val)?), + val => Err(TiffError::FormatError( + TiffFormatError::UnsignedIntegerExpected(val), + )), + } + } + + pub fn into_i32(self) -> TiffResult { + match self { + SignedByte(val) => Ok(val.into()), + SignedShort(val) => Ok(val.into()), + Signed(val) => Ok(val), + SignedBig(val) => Ok(i32::try_from(val)?), + val => Err(TiffError::FormatError( + TiffFormatError::SignedIntegerExpected(val), + )), + } + } + + pub fn into_u64(self) -> TiffResult { + match self { + Short(val) => Ok(val.into()), + Unsigned(val) => Ok(val.into()), + UnsignedBig(val) => Ok(val), + Ifd(val) => Ok(val.into()), + IfdBig(val) => Ok(val), + val => Err(TiffError::FormatError( + TiffFormatError::UnsignedIntegerExpected(val), + )), + } + } + + pub fn into_i64(self) -> TiffResult { + match self { + SignedByte(val) => Ok(val.into()), + SignedShort(val) => Ok(val.into()), + Signed(val) => Ok(val.into()), + SignedBig(val) => Ok(val), + val => Err(TiffError::FormatError( + TiffFormatError::SignedIntegerExpected(val), + )), + } + } + + pub fn into_f32(self) -> TiffResult { + match self { + Float(val) => Ok(val), + val => Err(TiffError::FormatError( + TiffFormatError::SignedIntegerExpected(val), + )), + } + } + + pub fn into_f64(self) -> TiffResult { + match self { + Double(val) => Ok(val), + val => Err(TiffError::FormatError( + TiffFormatError::SignedIntegerExpected(val), + )), + } + } + + pub fn into_string(self) -> TiffResult { + match self { + Ascii(val) => Ok(val), + val => Err(TiffError::FormatError( + TiffFormatError::SignedIntegerExpected(val), + )), + } + } + + pub fn into_u32_vec(self) -> TiffResult> { + match self { + List(vec) => { + let mut new_vec = Vec::with_capacity(vec.len()); + for v in vec { + new_vec.push(v.into_u32()?) + } + Ok(new_vec) + } + Unsigned(val) => Ok(vec![val]), + UnsignedBig(val) => Ok(vec![u32::try_from(val)?]), + Rational(numerator, denominator) => Ok(vec![numerator, denominator]), + RationalBig(numerator, denominator) => { + Ok(vec![u32::try_from(numerator)?, u32::try_from(denominator)?]) + } + Ifd(val) => Ok(vec![val]), + IfdBig(val) => Ok(vec![u32::try_from(val)?]), + Ascii(val) => Ok(val.chars().map(u32::from).collect()), + val => Err(TiffError::FormatError( + TiffFormatError::UnsignedIntegerExpected(val), + )), + } + } + + pub fn into_u8_vec(self) -> TiffResult> { + match self { + List(vec) => { + let mut new_vec = Vec::with_capacity(vec.len()); + for v in vec { + new_vec.push(v.into_u8()?) + } + Ok(new_vec) + } + Byte(val) => Ok(vec![val]), + + val => Err(TiffError::FormatError( + TiffFormatError::UnsignedIntegerExpected(val), + )), + } + } + + pub fn into_u16_vec(self) -> TiffResult> { + match self { + List(vec) => { + let mut new_vec = Vec::with_capacity(vec.len()); + for v in vec { + new_vec.push(v.into_u16()?) + } + Ok(new_vec) + } + Short(val) => Ok(vec![val]), + val => Err(TiffError::FormatError( + TiffFormatError::UnsignedIntegerExpected(val), + )), + } + } + + pub fn into_i32_vec(self) -> TiffResult> { + match self { + List(vec) => { + let mut new_vec = Vec::with_capacity(vec.len()); + for v in vec { + match v { + SRational(numerator, denominator) => { + new_vec.push(numerator); + new_vec.push(denominator); + } + SRationalBig(numerator, denominator) => { + new_vec.push(i32::try_from(numerator)?); + new_vec.push(i32::try_from(denominator)?); + } + _ => new_vec.push(v.into_i32()?), + } + } + Ok(new_vec) + } + SignedByte(val) => Ok(vec![val.into()]), + SignedShort(val) => Ok(vec![val.into()]), + Signed(val) => Ok(vec![val]), + SignedBig(val) => Ok(vec![i32::try_from(val)?]), + SRational(numerator, denominator) => Ok(vec![numerator, denominator]), + SRationalBig(numerator, denominator) => { + Ok(vec![i32::try_from(numerator)?, i32::try_from(denominator)?]) + } + val => Err(TiffError::FormatError( + TiffFormatError::SignedIntegerExpected(val), + )), + } + } + + pub fn into_f32_vec(self) -> TiffResult> { + match self { + List(vec) => { + let mut new_vec = Vec::with_capacity(vec.len()); + for v in vec { + new_vec.push(v.into_f32()?) + } + Ok(new_vec) + } + Float(val) => Ok(vec![val]), + val => Err(TiffError::FormatError( + TiffFormatError::UnsignedIntegerExpected(val), + )), + } + } + + pub fn into_f64_vec(self) -> TiffResult> { + match self { + List(vec) => { + let mut new_vec = Vec::with_capacity(vec.len()); + for v in vec { + new_vec.push(v.into_f64()?) + } + Ok(new_vec) + } + Double(val) => Ok(vec![val]), + val => Err(TiffError::FormatError( + TiffFormatError::UnsignedIntegerExpected(val), + )), + } + } + + pub fn into_u64_vec(self) -> TiffResult> { + match self { + List(vec) => { + let mut new_vec = Vec::with_capacity(vec.len()); + for v in vec { + new_vec.push(v.into_u64()?) + } + Ok(new_vec) + } + Byte(val) => Ok(vec![val.into()]), + Short(val) => Ok(vec![val.into()]), + Unsigned(val) => Ok(vec![val.into()]), + UnsignedBig(val) => Ok(vec![val]), + Rational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]), + RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]), + Ifd(val) => Ok(vec![val.into()]), + IfdBig(val) => Ok(vec![val]), + Ascii(val) => Ok(val.chars().map(u32::from).map(u64::from).collect()), + val => Err(TiffError::FormatError( + TiffFormatError::UnsignedIntegerExpected(val), + )), + } + } + + pub fn into_i64_vec(self) -> TiffResult> { + match self { + List(vec) => { + let mut new_vec = Vec::with_capacity(vec.len()); + for v in vec { + match v { + SRational(numerator, denominator) => { + new_vec.push(numerator.into()); + new_vec.push(denominator.into()); + } + SRationalBig(numerator, denominator) => { + new_vec.push(numerator); + new_vec.push(denominator); + } + _ => new_vec.push(v.into_i64()?), + } + } + Ok(new_vec) + } + SignedByte(val) => Ok(vec![val.into()]), + SignedShort(val) => Ok(vec![val.into()]), + Signed(val) => Ok(vec![val.into()]), + SignedBig(val) => Ok(vec![val]), + SRational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]), + SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]), + val => Err(TiffError::FormatError( + TiffFormatError::SignedIntegerExpected(val), + )), + } + } +} diff --git a/src/tiff/mod.rs b/src/tiff/mod.rs new file mode 100644 index 0000000..7de0bd1 --- /dev/null +++ b/src/tiff/mod.rs @@ -0,0 +1,8 @@ +//! Vendored content from tiff crate + +mod error; +mod ifd; +pub mod tags; + +pub(crate) use error::{TiffError, TiffFormatError, TiffResult, TiffUnsupportedError}; +pub(crate) use ifd::Value; diff --git a/src/tiff/tags.rs b/src/tiff/tags.rs new file mode 100644 index 0000000..798ddaa --- /dev/null +++ b/src/tiff/tags.rs @@ -0,0 +1,245 @@ +#![allow(clippy::no_effect)] + +macro_rules! tags { + { + // Permit arbitrary meta items, which include documentation. + $( #[$enum_attr:meta] )* + $vis:vis enum $name:ident($ty:tt) $(unknown($unknown_doc:literal))* { + // Each of the `Name = Val,` permitting documentation. + $($(#[$ident_attr:meta])* $tag:ident = $val:expr,)* + } + } => { + $( #[$enum_attr] )* + #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] + #[non_exhaustive] + pub enum $name { + $($(#[$ident_attr])* $tag,)* + $( + #[doc = $unknown_doc] + Unknown($ty), + )* + } + + impl $name { + #[inline(always)] + fn __from_inner_type(n: $ty) -> Result { + match n { + $( $val => Ok($name::$tag), )* + n => Err(n), + } + } + + #[inline(always)] + fn __to_inner_type(&self) -> $ty { + match *self { + $( $name::$tag => $val, )* + $( $name::Unknown(n) => { $unknown_doc; n }, )* + } + } + } + + tags!($name, $ty, $($unknown_doc)*); + }; + // For u16 tags, provide direct inherent primitive conversion methods. + ($name:tt, u16, $($unknown_doc:literal)*) => { + impl $name { + #[inline(always)] + pub fn from_u16(val: u16) -> Option { + Self::__from_inner_type(val).ok() + } + + $( + #[inline(always)] + pub fn from_u16_exhaustive(val: u16) -> Self { + $unknown_doc; + Self::__from_inner_type(val).unwrap_or_else(|_| $name::Unknown(val)) + } + )* + + #[inline(always)] + pub fn to_u16(&self) -> u16 { + Self::__to_inner_type(self) + } + } + }; + // For other tag types, do nothing for now. With concat_idents one could + // provide inherent conversion methods for all types. + ($name:tt, $ty:tt, $($unknown_doc:literal)*) => {}; +} + +// Note: These tags appear in the order they are mentioned in the TIFF reference +tags! { +/// TIFF tags +pub enum Tag(u16) unknown("A private or extension tag") { + // Baseline tags: + Artist = 315, + // grayscale images PhotometricInterpretation 1 or 3 + BitsPerSample = 258, + CellLength = 265, // TODO add support + CellWidth = 264, // TODO add support + // palette-color images (PhotometricInterpretation 3) + ColorMap = 320, // TODO add support + Compression = 259, // TODO add support for 2 and 32773 + Copyright = 33_432, + DateTime = 306, + ExtraSamples = 338, // TODO add support + FillOrder = 266, // TODO add support + FreeByteCounts = 289, // TODO add support + FreeOffsets = 288, // TODO add support + GrayResponseCurve = 291, // TODO add support + GrayResponseUnit = 290, // TODO add support + HostComputer = 316, + ImageDescription = 270, + ImageLength = 257, + ImageWidth = 256, + Make = 271, + MaxSampleValue = 281, // TODO add support + MinSampleValue = 280, // TODO add support + Model = 272, + NewSubfileType = 254, // TODO add support + Orientation = 274, // TODO add support + PhotometricInterpretation = 262, + PlanarConfiguration = 284, + ResolutionUnit = 296, // TODO add support + RowsPerStrip = 278, + SamplesPerPixel = 277, + Software = 305, + StripByteCounts = 279, + StripOffsets = 273, + SubfileType = 255, // TODO add support + Threshholding = 263, // TODO add support + XResolution = 282, + YResolution = 283, + // Advanced tags + Predictor = 317, + TileWidth = 322, + TileLength = 323, + TileOffsets = 324, + TileByteCounts = 325, + // Data Sample Format + SampleFormat = 339, + SMinSampleValue = 340, // TODO add support + SMaxSampleValue = 341, // TODO add support + // JPEG + JPEGTables = 347, + // GeoTIFF + ModelPixelScaleTag = 33550, // (SoftDesk) + ModelTransformationTag = 34264, // (JPL Carto Group) + ModelTiepointTag = 33922, // (Intergraph) + GeoKeyDirectoryTag = 34735, // (SPOT) + GeoDoubleParamsTag = 34736, // (SPOT) + GeoAsciiParamsTag = 34737, // (SPOT) + GdalNodata = 42113, // Contains areas with missing data +} +} + +tags! { +/// The type of an IFD entry (a 2 byte field). +pub enum Type(u16) { + /// 8-bit unsigned integer + BYTE = 1, + /// 8-bit byte that contains a 7-bit ASCII code; the last byte must be zero + ASCII = 2, + /// 16-bit unsigned integer + SHORT = 3, + /// 32-bit unsigned integer + LONG = 4, + /// Fraction stored as two 32-bit unsigned integers + RATIONAL = 5, + /// 8-bit signed integer + SBYTE = 6, + /// 8-bit byte that may contain anything, depending on the field + UNDEFINED = 7, + /// 16-bit signed integer + SSHORT = 8, + /// 32-bit signed integer + SLONG = 9, + /// Fraction stored as two 32-bit signed integers + SRATIONAL = 10, + /// 32-bit IEEE floating point + FLOAT = 11, + /// 64-bit IEEE floating point + DOUBLE = 12, + /// 32-bit unsigned integer (offset) + IFD = 13, + /// BigTIFF 64-bit unsigned integer + LONG8 = 16, + /// BigTIFF 64-bit signed integer + SLONG8 = 17, + /// BigTIFF 64-bit unsigned integer (offset) + IFD8 = 18, +} +} + +tags! { +/// See [TIFF compression tags](https://www.awaresystems.be/imaging/tiff/tifftags/compression.html) +/// for reference. +pub enum CompressionMethod(u16) unknown("A custom compression method") { + None = 1, + Huffman = 2, + Fax3 = 3, + Fax4 = 4, + LZW = 5, + JPEG = 6, + // "Extended JPEG" or "new JPEG" style + ModernJPEG = 7, + Deflate = 8, + OldDeflate = 0x80B2, + PackBits = 0x8005, + + // Self-assigned by libtiff + ZSTD = 0xC350, +} +} + +tags! { +pub enum PhotometricInterpretation(u16) { + WhiteIsZero = 0, + BlackIsZero = 1, + RGB = 2, + RGBPalette = 3, + TransparencyMask = 4, + CMYK = 5, + YCbCr = 6, + CIELab = 8, +} +} + +tags! { +pub enum PlanarConfiguration(u16) { + Chunky = 1, + Planar = 2, +} +} + +tags! { +pub enum Predictor(u16) { + /// No changes were made to the data + None = 1, + /// The images' rows were processed to contain the difference of each pixel from the previous one. + /// + /// 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, + /// Not currently supported + FloatingPoint = 3, +} +} + +tags! { +/// Type to represent resolution units +pub enum ResolutionUnit(u16) { + None = 1, + Inch = 2, + Centimeter = 3, +} +} + +tags! { +pub enum SampleFormat(u16) unknown("An unknown extension sample format") { + Uint = 1, + Int = 2, + IEEEFP = 3, + Void = 4, +} +} diff --git a/tests/image_tiff/decode_bigtiff_images.rs b/tests/image_tiff/decode_bigtiff_images.rs index 9113f42..a90ee19 100644 --- a/tests/image_tiff/decode_bigtiff_images.rs +++ b/tests/image_tiff/decode_bigtiff_images.rs @@ -1,46 +1,34 @@ extern crate tiff; -use tiff::decoder::Decoder; -use tiff::tags::Tag; -use tiff::ColorType; +use async_tiff::tiff::tags::PhotometricInterpretation; -use std::fs::File; -use std::path::PathBuf; +use crate::image_tiff::util::open_tiff; -const TEST_IMAGE_DIR: &str = "./tests/images/bigtiff"; - -#[test] -fn test_big_tiff() { - let filenames = ["BigTIFF.tif", "BigTIFFMotorola.tif", "BigTIFFLong.tif"]; +#[tokio::test] +async fn test_big_tiff() { + let filenames = [ + "bigtiff/BigTIFF.tif", + "bigtiff/BigTIFFMotorola.tif", + "bigtiff/BigTIFFLong.tif", + ]; for filename in filenames.iter() { - let path = PathBuf::from(TEST_IMAGE_DIR).join(filename); - let img_file = File::open(path).expect("Cannot find test image!"); - let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); - assert_eq!( - decoder.dimensions().expect("Cannot get dimensions"), - (64, 64) - ); + let tiff = open_tiff(filename).await; + let ifd = &tiff.ifds().as_ref()[0]; + assert_eq!(ifd.image_height(), 64); + assert_eq!(ifd.image_width(), 64); assert_eq!( - decoder.colortype().expect("Cannot get colortype"), - ColorType::RGB(8) + ifd.photometric_interpretation(), + PhotometricInterpretation::RGB ); + assert!(ifd.bits_per_sample().iter().all(|x| *x == 8)); assert_eq!( - decoder - .get_tag_u64(Tag::StripOffsets) - .expect("Cannot get StripOffsets"), - 16 + ifd.strip_offsets().expect("Cannot get StripOffsets"), + vec![16] ); + assert_eq!(ifd.rows_per_strip().expect("Cannot get RowsPerStrip"), 64); assert_eq!( - decoder - .get_tag_u64(Tag::RowsPerStrip) - .expect("Cannot get RowsPerStrip"), - 64 + ifd.strip_byte_counts().expect("Cannot get StripByteCounts"), + vec![12288] ); - assert_eq!( - decoder - .get_tag_u64(Tag::StripByteCounts) - .expect("Cannot get StripByteCounts"), - 12288 - ) } } diff --git a/tests/image_tiff/decode_geotiff_images.rs b/tests/image_tiff/decode_geotiff_images.rs index 83457ec..da2f058 100644 --- a/tests/image_tiff/decode_geotiff_images.rs +++ b/tests/image_tiff/decode_geotiff_images.rs @@ -1,23 +1,13 @@ extern crate tiff; -use async_tiff::{COGReader, ObjectReader}; -use object_store::local::LocalFileSystem; - -use std::env::current_dir; -use std::sync::Arc; - -const TEST_IMAGE_DIR: &str = "tests/image_tiff/images"; +use crate::image_tiff::util::open_tiff; #[tokio::test] async fn test_geo_tiff() { let filenames = ["geo-5b.tif"]; - let store = Arc::new(LocalFileSystem::new_with_prefix(current_dir().unwrap()).unwrap()); - for filename in filenames.iter() { - let path = format!("{TEST_IMAGE_DIR}/{filename}"); - let reader = ObjectReader::new(store.clone(), path.as_str().into()); - let image_reader = COGReader::try_open(Box::new(reader)).await.unwrap(); - let ifd = &image_reader.ifds().as_ref()[0]; + let tiff = open_tiff(filename).await; + let ifd = &tiff.ifds().as_ref()[0]; dbg!(&ifd); assert_eq!(ifd.image_height(), 10); assert_eq!(ifd.image_width(), 10); diff --git a/tests/image_tiff/decode_images.rs b/tests/image_tiff/decode_images.rs index 6d0bf7f..811b54e 100644 --- a/tests/image_tiff/decode_images.rs +++ b/tests/image_tiff/decode_images.rs @@ -1,20 +1,8 @@ extern crate tiff; -use async_tiff::{COGReader, ObjectReader}; -use object_store::local::LocalFileSystem; -use tiff::tags::PhotometricInterpretation; +use async_tiff::tiff::tags::PhotometricInterpretation; -use std::env::current_dir; -use std::sync::Arc; - -const TEST_IMAGE_DIR: &str = "tests/image_tiff/images/"; - -async fn open_tiff(filename: &str) -> COGReader { - let store = Arc::new(LocalFileSystem::new_with_prefix(current_dir().unwrap()).unwrap()); - let path = format!("{TEST_IMAGE_DIR}/{filename}"); - let reader = Box::new(ObjectReader::new(store.clone(), path.as_str().into())); - COGReader::try_open(reader).await.unwrap() -} +use crate::image_tiff::util::open_tiff; #[tokio::test] async fn cmyk_u8() { diff --git a/tests/image_tiff/mod.rs b/tests/image_tiff/mod.rs index 5f252c8..efbabdf 100644 --- a/tests/image_tiff/mod.rs +++ b/tests/image_tiff/mod.rs @@ -1,2 +1,4 @@ +mod decode_bigtiff_images; mod decode_geotiff_images; mod decode_images; +mod util; diff --git a/tests/image_tiff/util.rs b/tests/image_tiff/util.rs new file mode 100644 index 0000000..431f749 --- /dev/null +++ b/tests/image_tiff/util.rs @@ -0,0 +1,14 @@ +use std::env::current_dir; +use std::sync::Arc; + +use async_tiff::{COGReader, ObjectReader}; +use object_store::local::LocalFileSystem; + +const TEST_IMAGE_DIR: &str = "tests/image_tiff/images/"; + +pub(crate) async fn open_tiff(filename: &str) -> COGReader { + let store = Arc::new(LocalFileSystem::new_with_prefix(current_dir().unwrap()).unwrap()); + let path = format!("{TEST_IMAGE_DIR}/{filename}"); + let reader = Box::new(ObjectReader::new(store.clone(), path.as_str().into())); + COGReader::try_open(reader).await.unwrap() +}