diff --git a/python/Cargo.toml b/python/Cargo.toml index 4a58c7b..5e35095 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -22,6 +22,7 @@ bytes = "1.8" pyo3 = { version = "0.23.0", features = ["macros"] } pyo3-bytes = "0.1.2" thiserror = "1" +tiff = "0.9.1" [profile.release] lto = true diff --git a/python/python/async_tiff/enums.py b/python/python/async_tiff/enums.py new file mode 100644 index 0000000..724b77b --- /dev/null +++ b/python/python/async_tiff/enums.py @@ -0,0 +1,56 @@ +from enum import IntEnum + + +class CompressionMethod(IntEnum): + """ + See [TIFF compression + tags](https://www.awaresystems.be/imaging/tiff/tifftags/compression.html) for + reference. + """ + + Uncompressed = 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 + + +class PhotometricInterpretation(IntEnum): + WhiteIsZero = 0 + BlackIsZero = 1 + RGB = 2 + RGBPalette = 3 + TransparencyMask = 4 + CMYK = 5 + YCbCr = 6 + CIELab = 8 + + +class PlanarConfiguration(IntEnum): + Chunky = 1 + Planar = 2 + + +class Predictor(IntEnum): + Unknown = 1 + Horizontal = 2 + FloatingPoint = 3 + + +class ResolutionUnit(IntEnum): + Unknown = 1 + Inch = 2 + Centimeter = 3 + + +class SampleFormat(IntEnum): + Uint = 1 + Int = 2 + IEEEFP = 3 + Void = 4 diff --git a/python/src/enums.rs b/python/src/enums.rs new file mode 100644 index 0000000..d94ef4b --- /dev/null +++ b/python/src/enums.rs @@ -0,0 +1,127 @@ +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::types::{PyString, PyTuple}; +use tiff::tags::{ + CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, ResolutionUnit, + SampleFormat, +}; + +pub(crate) struct PyCompressionMethod(CompressionMethod); + +impl From for PyCompressionMethod { + fn from(value: CompressionMethod) -> Self { + Self(value) + } +} + +impl<'py> IntoPyObject<'py> for PyCompressionMethod { + type Target = PyAny; + type Output = Bound<'py, PyAny>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + to_py_enum_variant(py, intern!(py, "CompressionMethod"), self.0.to_u16()) + } +} + +pub(crate) struct PyPhotometricInterpretation(PhotometricInterpretation); + +impl From for PyPhotometricInterpretation { + fn from(value: PhotometricInterpretation) -> Self { + Self(value) + } +} + +impl<'py> IntoPyObject<'py> for PyPhotometricInterpretation { + type Target = PyAny; + type Output = Bound<'py, PyAny>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + to_py_enum_variant( + py, + intern!(py, "PhotometricInterpretation"), + self.0.to_u16(), + ) + } +} + +pub(crate) struct PyPlanarConfiguration(PlanarConfiguration); + +impl From for PyPlanarConfiguration { + fn from(value: PlanarConfiguration) -> Self { + Self(value) + } +} + +impl<'py> IntoPyObject<'py> for PyPlanarConfiguration { + type Target = PyAny; + type Output = Bound<'py, PyAny>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + to_py_enum_variant(py, intern!(py, "PlanarConfiguration"), self.0.to_u16()) + } +} + +pub(crate) struct PyResolutionUnit(ResolutionUnit); + +impl From for PyResolutionUnit { + fn from(value: ResolutionUnit) -> Self { + Self(value) + } +} + +impl<'py> IntoPyObject<'py> for PyResolutionUnit { + type Target = PyAny; + type Output = Bound<'py, PyAny>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + to_py_enum_variant(py, intern!(py, "ResolutionUnit"), self.0.to_u16()) + } +} + +pub(crate) struct PyPredictor(Predictor); + +impl From for PyPredictor { + fn from(value: Predictor) -> Self { + Self(value) + } +} + +impl<'py> IntoPyObject<'py> for PyPredictor { + type Target = PyAny; + type Output = Bound<'py, PyAny>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + to_py_enum_variant(py, intern!(py, "Predictor"), self.0.to_u16()) + } +} + +pub(crate) struct PySampleFormat(SampleFormat); + +impl From for PySampleFormat { + fn from(value: SampleFormat) -> Self { + Self(value) + } +} + +impl<'py> IntoPyObject<'py> for PySampleFormat { + type Target = PyAny; + type Output = Bound<'py, PyAny>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + to_py_enum_variant(py, intern!(py, "SampleFormat"), self.0.to_u16()) + } +} +fn to_py_enum_variant<'py>( + py: Python<'py>, + enum_name: &Bound<'py, PyString>, + value: u16, +) -> PyResult> { + let enums_mod = py.import(intern!(py, "async_tiff.enums"))?; + enums_mod.call_method1(enum_name, PyTuple::new(py, vec![value])?) +} diff --git a/python/src/geo.rs b/python/src/geo.rs new file mode 100644 index 0000000..ef33e71 --- /dev/null +++ b/python/src/geo.rs @@ -0,0 +1,202 @@ +use async_tiff::geo::GeoKeyDirectory; +use pyo3::prelude::*; + +#[pyclass(name = "GeoKeyDirectory")] +pub(crate) struct PyGeoKeyDirectory { + #[pyo3(get)] + model_type: Option, + #[pyo3(get)] + raster_type: Option, + #[pyo3(get)] + citation: Option, + #[pyo3(get)] + geographic_type: Option, + #[pyo3(get)] + geog_citation: Option, + #[pyo3(get)] + geog_geodetic_datum: Option, + #[pyo3(get)] + geog_prime_meridian: Option, + #[pyo3(get)] + geog_linear_units: Option, + #[pyo3(get)] + geog_linear_unit_size: Option, + #[pyo3(get)] + geog_angular_units: Option, + #[pyo3(get)] + geog_angular_unit_size: Option, + #[pyo3(get)] + geog_ellipsoid: Option, + #[pyo3(get)] + geog_semi_major_axis: Option, + #[pyo3(get)] + geog_semi_minor_axis: Option, + #[pyo3(get)] + geog_inv_flattening: Option, + #[pyo3(get)] + geog_azimuth_units: Option, + #[pyo3(get)] + geog_prime_meridian_long: Option, + + #[pyo3(get)] + projected_type: Option, + #[pyo3(get)] + proj_citation: Option, + #[pyo3(get)] + projection: Option, + #[pyo3(get)] + proj_coord_trans: Option, + #[pyo3(get)] + proj_linear_units: Option, + #[pyo3(get)] + proj_linear_unit_size: Option, + #[pyo3(get)] + proj_std_parallel1: Option, + #[pyo3(get)] + proj_std_parallel2: Option, + #[pyo3(get)] + proj_nat_origin_long: Option, + #[pyo3(get)] + proj_nat_origin_lat: Option, + #[pyo3(get)] + proj_false_easting: Option, + #[pyo3(get)] + proj_false_northing: Option, + #[pyo3(get)] + proj_false_origin_long: Option, + #[pyo3(get)] + proj_false_origin_lat: Option, + #[pyo3(get)] + proj_false_origin_easting: Option, + #[pyo3(get)] + proj_false_origin_northing: Option, + #[pyo3(get)] + proj_center_long: Option, + #[pyo3(get)] + proj_center_lat: Option, + #[pyo3(get)] + proj_center_easting: Option, + #[pyo3(get)] + proj_center_northing: Option, + #[pyo3(get)] + proj_scale_at_nat_origin: Option, + #[pyo3(get)] + proj_scale_at_center: Option, + #[pyo3(get)] + proj_azimuth_angle: Option, + #[pyo3(get)] + proj_straight_vert_pole_long: Option, + + #[pyo3(get)] + vertical: Option, + #[pyo3(get)] + vertical_citation: Option, + #[pyo3(get)] + vertical_datum: Option, + #[pyo3(get)] + vertical_units: Option, +} + +impl From for GeoKeyDirectory { + fn from(value: PyGeoKeyDirectory) -> Self { + Self { + model_type: value.model_type, + raster_type: value.raster_type, + citation: value.citation, + geographic_type: value.geographic_type, + geog_citation: value.geog_citation, + geog_geodetic_datum: value.geog_geodetic_datum, + geog_prime_meridian: value.geog_prime_meridian, + geog_linear_units: value.geog_linear_units, + geog_linear_unit_size: value.geog_linear_unit_size, + geog_angular_units: value.geog_angular_units, + geog_angular_unit_size: value.geog_angular_unit_size, + geog_ellipsoid: value.geog_ellipsoid, + geog_semi_major_axis: value.geog_semi_major_axis, + geog_semi_minor_axis: value.geog_semi_minor_axis, + geog_inv_flattening: value.geog_inv_flattening, + geog_azimuth_units: value.geog_azimuth_units, + geog_prime_meridian_long: value.geog_prime_meridian_long, + projected_type: value.projected_type, + proj_citation: value.proj_citation, + projection: value.projection, + proj_coord_trans: value.proj_coord_trans, + proj_linear_units: value.proj_linear_units, + proj_linear_unit_size: value.proj_linear_unit_size, + proj_std_parallel1: value.proj_std_parallel1, + proj_std_parallel2: value.proj_std_parallel2, + proj_nat_origin_long: value.proj_nat_origin_long, + proj_nat_origin_lat: value.proj_nat_origin_lat, + proj_false_easting: value.proj_false_easting, + proj_false_northing: value.proj_false_northing, + proj_false_origin_long: value.proj_false_origin_long, + proj_false_origin_lat: value.proj_false_origin_lat, + proj_false_origin_easting: value.proj_false_origin_easting, + proj_false_origin_northing: value.proj_false_origin_northing, + proj_center_long: value.proj_center_long, + proj_center_lat: value.proj_center_lat, + proj_center_easting: value.proj_center_easting, + proj_center_northing: value.proj_center_northing, + proj_scale_at_nat_origin: value.proj_scale_at_nat_origin, + proj_scale_at_center: value.proj_scale_at_center, + proj_azimuth_angle: value.proj_azimuth_angle, + proj_straight_vert_pole_long: value.proj_straight_vert_pole_long, + vertical: value.vertical, + vertical_citation: value.vertical_citation, + vertical_datum: value.vertical_datum, + vertical_units: value.vertical_units, + } + } +} + +impl From for PyGeoKeyDirectory { + fn from(value: GeoKeyDirectory) -> Self { + Self { + model_type: value.model_type, + raster_type: value.raster_type, + citation: value.citation, + geographic_type: value.geographic_type, + geog_citation: value.geog_citation, + geog_geodetic_datum: value.geog_geodetic_datum, + geog_prime_meridian: value.geog_prime_meridian, + geog_linear_units: value.geog_linear_units, + geog_linear_unit_size: value.geog_linear_unit_size, + geog_angular_units: value.geog_angular_units, + geog_angular_unit_size: value.geog_angular_unit_size, + geog_ellipsoid: value.geog_ellipsoid, + geog_semi_major_axis: value.geog_semi_major_axis, + geog_semi_minor_axis: value.geog_semi_minor_axis, + geog_inv_flattening: value.geog_inv_flattening, + geog_azimuth_units: value.geog_azimuth_units, + geog_prime_meridian_long: value.geog_prime_meridian_long, + projected_type: value.projected_type, + proj_citation: value.proj_citation, + projection: value.projection, + proj_coord_trans: value.proj_coord_trans, + proj_linear_units: value.proj_linear_units, + proj_linear_unit_size: value.proj_linear_unit_size, + proj_std_parallel1: value.proj_std_parallel1, + proj_std_parallel2: value.proj_std_parallel2, + proj_nat_origin_long: value.proj_nat_origin_long, + proj_nat_origin_lat: value.proj_nat_origin_lat, + proj_false_easting: value.proj_false_easting, + proj_false_northing: value.proj_false_northing, + proj_false_origin_long: value.proj_false_origin_long, + proj_false_origin_lat: value.proj_false_origin_lat, + proj_false_origin_easting: value.proj_false_origin_easting, + proj_false_origin_northing: value.proj_false_origin_northing, + proj_center_long: value.proj_center_long, + proj_center_lat: value.proj_center_lat, + proj_center_easting: value.proj_center_easting, + proj_center_northing: value.proj_center_northing, + proj_scale_at_nat_origin: value.proj_scale_at_nat_origin, + proj_scale_at_center: value.proj_scale_at_center, + proj_azimuth_angle: value.proj_azimuth_angle, + proj_straight_vert_pole_long: value.proj_straight_vert_pole_long, + vertical: value.vertical, + vertical_citation: value.vertical_citation, + vertical_datum: value.vertical_datum, + vertical_units: value.vertical_units, + } + } +} diff --git a/python/src/ifd.rs b/python/src/ifd.rs new file mode 100644 index 0000000..ce024e2 --- /dev/null +++ b/python/src/ifd.rs @@ -0,0 +1,217 @@ +use async_tiff::ImageFileDirectory; +use pyo3::prelude::*; + +use crate::enums::{ + PyCompressionMethod, PyPhotometricInterpretation, PyPlanarConfiguration, PyPredictor, + PyResolutionUnit, PySampleFormat, +}; +use crate::geo::PyGeoKeyDirectory; + +#[pyclass(name = "ImageFileDirectory")] +pub(crate) struct PyImageFileDirectory(ImageFileDirectory); + +#[pymethods] +impl PyImageFileDirectory { + #[getter] + pub fn new_subfile_type(&self) -> Option { + self.0.new_subfile_type() + } + + /// The number of columns in the image, i.e., the number of pixels per row. + #[getter] + pub fn image_width(&self) -> u32 { + self.0.image_width() + } + + /// The number of rows of pixels in the image. + #[getter] + pub fn image_height(&self) -> u32 { + self.0.image_height() + } + + #[getter] + pub fn bits_per_sample(&self) -> &[u16] { + self.0.bits_per_sample() + } + + #[getter] + pub fn compression(&self) -> PyCompressionMethod { + self.0.compression().into() + } + + #[getter] + pub fn photometric_interpretation(&self) -> PyPhotometricInterpretation { + self.0.photometric_interpretation().into() + } + + #[getter] + pub fn document_name(&self) -> Option<&str> { + self.0.document_name() + } + + #[getter] + pub fn image_description(&self) -> Option<&str> { + self.0.image_description() + } + + #[getter] + pub fn strip_offsets(&self) -> Option<&[u32]> { + self.0.strip_offsets() + } + + #[getter] + pub fn orientation(&self) -> Option { + self.0.orientation() + } + + /// The number of components per pixel. + /// + /// SamplesPerPixel is usually 1 for bilevel, grayscale, and palette-color images. + /// SamplesPerPixel is usually 3 for RGB images. If this value is higher, ExtraSamples should + /// give an indication of the meaning of the additional channels. + #[getter] + pub fn samples_per_pixel(&self) -> u16 { + self.0.samples_per_pixel() + } + + #[getter] + pub fn rows_per_strip(&self) -> Option { + self.0.rows_per_strip() + } + + #[getter] + pub fn strip_byte_counts(&self) -> Option<&[u32]> { + self.0.strip_byte_counts() + } + + #[getter] + pub fn min_sample_value(&self) -> Option<&[u16]> { + self.0.min_sample_value() + } + + #[getter] + pub fn max_sample_value(&self) -> Option<&[u16]> { + self.0.max_sample_value() + } + + /// The number of pixels per ResolutionUnit in the ImageWidth direction. + #[getter] + pub fn x_resolution(&self) -> Option { + self.0.x_resolution() + } + + /// The number of pixels per ResolutionUnit in the ImageLength direction. + #[getter] + pub fn y_resolution(&self) -> Option { + self.0.y_resolution() + } + + /// How the components of each pixel are stored. + /// + /// The specification defines these values: + /// + /// - Chunky format. The component values for each pixel are stored contiguously. For example, + /// for RGB data, the data is stored as RGBRGBRGB + /// - Planar format. The components are stored in separate component planes. For example, RGB + /// data is stored with the Red components in one component plane, the Green in another, and + /// the Blue in another. + /// + /// The specification adds a warning that PlanarConfiguration=2 is not in widespread use and + /// that Baseline TIFF readers are not required to support it. + /// + /// If SamplesPerPixel is 1, PlanarConfiguration is irrelevant, and need not be included. + #[getter] + pub fn planar_configuration(&self) -> PyPlanarConfiguration { + self.0.planar_configuration().into() + } + + #[getter] + pub fn resolution_unit(&self) -> Option { + self.0.resolution_unit().map(|x| x.into()) + } + + /// Name and version number of the software package(s) used to create the image. + #[getter] + pub fn software(&self) -> Option<&str> { + self.0.software() + } + + /// Date and time of image creation. + /// + /// The format is: "YYYY:MM:DD HH:MM:SS", with hours like those on a 24-hour clock, and one + /// space character between the date and the time. The length of the string, including the + /// terminating NUL, is 20 bytes. + #[getter] + pub fn date_time(&self) -> Option<&str> { + self.0.date_time() + } + + #[getter] + pub fn artist(&self) -> Option<&str> { + self.0.artist() + } + + #[getter] + pub fn host_computer(&self) -> Option<&str> { + self.0.host_computer() + } + + #[getter] + pub fn predictor(&self) -> Option { + self.0.predictor().map(|x| x.into()) + } + + #[getter] + pub fn tile_width(&self) -> u32 { + self.0.tile_width() + } + #[getter] + pub fn tile_height(&self) -> u32 { + self.0.tile_height() + } + + #[getter] + pub fn tile_offsets(&self) -> &[u32] { + self.0.tile_offsets() + } + #[getter] + pub fn tile_byte_counts(&self) -> &[u32] { + self.0.tile_byte_counts() + } + + #[getter] + pub fn extra_samples(&self) -> Option<&[u8]> { + self.0.extra_samples() + } + + #[getter] + pub fn sample_format(&self) -> Vec { + self.0.sample_format().iter().map(|x| (*x).into()).collect() + } + + #[getter] + pub fn jpeg_tables(&self) -> Option<&[u8]> { + self.0.jpeg_tables() + } + + #[getter] + pub fn copyright(&self) -> Option<&str> { + self.0.copyright() + } + + // Geospatial tags + #[getter] + pub fn geo_key_directory(&self) -> Option { + self.0.geo_key_directory().cloned().map(|x| x.into()) + } + + #[getter] + pub fn model_pixel_scale(&self) -> Option<&[f64]> { + self.0.model_pixel_scale() + } + + #[getter] + pub fn model_tiepoint(&self) -> Option<&[f64]> { + self.0.model_tiepoint() + } +} diff --git a/python/src/lib.rs b/python/src/lib.rs index 905b3c3..9e4ed38 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -1,7 +1,14 @@ #![deny(clippy::undocumented_unsafe_blocks)] +mod enums; +mod geo; +mod ifd; + use pyo3::prelude::*; +use crate::geo::PyGeoKeyDirectory; +use crate::ifd::PyImageFileDirectory; + const VERSION: &str = env!("CARGO_PKG_VERSION"); #[pyfunction] @@ -34,6 +41,8 @@ fn _async_tiff(py: Python, m: &Bound) -> PyResult<()> { check_debug_build(py)?; m.add_wrapped(wrap_pyfunction!(___version))?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/src/geo/geo_key_directory.rs b/src/geo/geo_key_directory.rs index 0a13a5b..dcb9e71 100644 --- a/src/geo/geo_key_directory.rs +++ b/src/geo/geo_key_directory.rs @@ -9,7 +9,7 @@ use tiff::{TiffError, TiffResult}; /// Geospatial TIFF tag variants #[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive, IntoPrimitive, Eq, Hash)] #[repr(u16)] -pub enum GeoKeyTag { +pub(crate) enum GeoKeyTag { // GeoTIFF configuration keys ModelType = 1024, RasterType = 1025, diff --git a/src/geo/mod.rs b/src/geo/mod.rs index fb68407..b504718 100644 --- a/src/geo/mod.rs +++ b/src/geo/mod.rs @@ -5,4 +5,5 @@ mod geo_key_directory; mod partial_reads; pub use affine::AffineTransform; -pub use geo_key_directory::{GeoKeyDirectory, GeoKeyTag}; +pub use geo_key_directory::GeoKeyDirectory; +pub(crate) use geo_key_directory::GeoKeyTag; diff --git a/src/ifd.rs b/src/ifd.rs index 0fc8c4d..d95fc9e 100644 --- a/src/ifd.rs +++ b/src/ifd.rs @@ -181,9 +181,9 @@ pub struct ImageFileDirectory { } impl ImageFileDirectory { - async fn read(cursor: &mut AsyncCursor, offset: usize) -> Result { - let ifd_start = offset; - cursor.seek(offset); + /// Read and parse the IFD starting at the given file offset + async fn read(cursor: &mut AsyncCursor, ifd_start: usize) -> Result { + cursor.seek(ifd_start); let tag_count = cursor.read_u16().await?; let mut tags = HashMap::with_capacity(tag_count as usize); @@ -446,6 +446,174 @@ impl ImageFileDirectory { }) } + pub fn new_subfile_type(&self) -> Option { + self.new_subfile_type + } + + /// The number of columns in the image, i.e., the number of pixels per row. + pub fn image_width(&self) -> u32 { + self.image_width + } + + /// The number of rows of pixels in the image. + pub fn image_height(&self) -> u32 { + self.image_height + } + + pub fn bits_per_sample(&self) -> &[u16] { + &self.bits_per_sample + } + + pub fn compression(&self) -> CompressionMethod { + self.compression + } + + pub fn photometric_interpretation(&self) -> PhotometricInterpretation { + self.photometric_interpretation + } + + pub fn document_name(&self) -> Option<&str> { + self.document_name.as_deref() + } + + pub fn image_description(&self) -> Option<&str> { + self.image_description.as_deref() + } + + pub fn strip_offsets(&self) -> Option<&[u32]> { + self.strip_offsets.as_deref() + } + + pub fn orientation(&self) -> Option { + self.orientation + } + + /// The number of components per pixel. + /// + /// SamplesPerPixel is usually 1 for bilevel, grayscale, and palette-color images. + /// SamplesPerPixel is usually 3 for RGB images. If this value is higher, ExtraSamples should + /// give an indication of the meaning of the additional channels. + pub fn samples_per_pixel(&self) -> u16 { + self.samples_per_pixel + } + + pub fn rows_per_strip(&self) -> Option { + self.rows_per_strip + } + + pub fn strip_byte_counts(&self) -> Option<&[u32]> { + self.strip_byte_counts.as_deref() + } + + pub fn min_sample_value(&self) -> Option<&[u16]> { + self.min_sample_value.as_deref() + } + + pub fn max_sample_value(&self) -> Option<&[u16]> { + self.max_sample_value.as_deref() + } + + /// The number of pixels per ResolutionUnit in the ImageWidth direction. + pub fn x_resolution(&self) -> Option { + self.x_resolution + } + + /// The number of pixels per ResolutionUnit in the ImageLength direction. + pub fn y_resolution(&self) -> Option { + self.y_resolution + } + + /// How the components of each pixel are stored. + /// + /// The specification defines these values: + /// + /// - Chunky format. The component values for each pixel are stored contiguously. For example, + /// for RGB data, the data is stored as RGBRGBRGB + /// - Planar format. The components are stored in separate component planes. For example, RGB + /// data is stored with the Red components in one component plane, the Green in another, and + /// the Blue in another. + /// + /// The specification adds a warning that PlanarConfiguration=2 is not in widespread use and + /// that Baseline TIFF readers are not required to support it. + /// + /// If SamplesPerPixel is 1, PlanarConfiguration is irrelevant, and need not be included. + pub fn planar_configuration(&self) -> PlanarConfiguration { + self.planar_configuration + } + + pub fn resolution_unit(&self) -> Option { + self.resolution_unit + } + + /// Name and version number of the software package(s) used to create the image. + pub fn software(&self) -> Option<&str> { + self.software.as_deref() + } + + /// Date and time of image creation. + /// + /// The format is: "YYYY:MM:DD HH:MM:SS", with hours like those on a 24-hour clock, and one + /// space character between the date and the time. The length of the string, including the + /// terminating NUL, is 20 bytes. + pub fn date_time(&self) -> Option<&str> { + self.date_time.as_deref() + } + + pub fn artist(&self) -> Option<&str> { + self.artist.as_deref() + } + + pub fn host_computer(&self) -> Option<&str> { + self.host_computer.as_deref() + } + + pub fn predictor(&self) -> Option { + self.predictor + } + + pub fn tile_width(&self) -> u32 { + self.tile_width + } + pub fn tile_height(&self) -> u32 { + self.tile_height + } + + pub fn tile_offsets(&self) -> &[u32] { + &self.tile_offsets + } + pub fn tile_byte_counts(&self) -> &[u32] { + &self.tile_byte_counts + } + + pub fn extra_samples(&self) -> Option<&[u8]> { + self.extra_samples.as_deref() + } + + pub fn sample_format(&self) -> &[SampleFormat] { + &self.sample_format + } + + pub fn jpeg_tables(&self) -> Option<&[u8]> { + self.jpeg_tables.as_deref() + } + + pub fn copyright(&self) -> Option<&str> { + self.copyright.as_deref() + } + + // Geospatial tags + pub fn geo_key_directory(&self) -> Option<&GeoKeyDirectory> { + self.geo_key_directory.as_ref() + } + + pub fn model_pixel_scale(&self) -> Option<&[f64]> { + self.model_pixel_scale.as_deref() + } + + pub fn model_tiepoint(&self) -> Option<&[f64]> { + self.model_tiepoint.as_deref() + } + /// Check if an IFD is masked based on a dictionary of tiff tags /// https://www.awaresystems.be/imaging/tiff/tifftags/newsubfiletype.html /// https://gdal.org/drivers/raster/gtiff.html#internal-nodata-masks @@ -492,27 +660,6 @@ impl ImageFileDirectory { } } - pub fn compression(&self) -> CompressionMethod { - self.compression - } - - pub fn bands(&self) -> u16 { - self.samples_per_pixel - } - - // pub fn dtype(&self) - - // pub fn nodata(&self) - - pub fn has_extra_samples(&self) -> bool { - self.extra_samples.is_some() - } - - /// Return the interleave of the IFD - pub fn interleave(&self) -> PlanarConfiguration { - self.planar_configuration - } - /// Returns true if this IFD contains a full resolution image (not an overview) pub fn is_full_resolution(&self) -> bool { if let Some(val) = self.new_subfile_type {