Skip to main content

async_tiff/
ifd.rs

1use std::collections::HashMap;
2use std::ops::Range;
3
4use bytes::Bytes;
5use num_enum::TryFromPrimitive;
6
7use crate::error::{AsyncTiffError, AsyncTiffResult, TiffError};
8use crate::geo::{GeoKeyDirectory, GeoKeyTag};
9use crate::predictor::PredictorInfo;
10use crate::reader::{AsyncFileReader, Endianness};
11use crate::tag_value::TagValue;
12use crate::tags::{
13    CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, ResolutionUnit,
14    SampleFormat, Tag,
15};
16use crate::{DataType, Tile};
17
18const DOCUMENT_NAME: u16 = 269;
19
20/// An ImageFileDirectory representing Image content
21// The ordering of these tags matches the sorted order in TIFF spec Appendix A
22#[allow(dead_code)]
23#[derive(Debug, Clone, PartialEq)]
24pub struct ImageFileDirectory {
25    pub(crate) endianness: Endianness,
26
27    pub(crate) new_subfile_type: Option<u32>,
28
29    /// The number of columns in the image, i.e., the number of pixels per row.
30    pub(crate) image_width: u32,
31
32    /// The number of rows of pixels in the image.
33    pub(crate) image_height: u32,
34
35    pub(crate) bits_per_sample: Vec<u16>,
36
37    pub(crate) compression: CompressionMethod,
38
39    pub(crate) photometric_interpretation: PhotometricInterpretation,
40
41    pub(crate) document_name: Option<String>,
42
43    pub(crate) image_description: Option<String>,
44
45    pub(crate) strip_offsets: Option<Vec<u64>>,
46
47    pub(crate) orientation: Option<u16>,
48
49    /// The number of components per pixel.
50    ///
51    /// SamplesPerPixel is usually 1 for bilevel, grayscale, and palette-color images.
52    /// SamplesPerPixel is usually 3 for RGB images. If this value is higher, ExtraSamples should
53    /// give an indication of the meaning of the additional channels.
54    pub(crate) samples_per_pixel: u16,
55
56    pub(crate) rows_per_strip: Option<u32>,
57
58    pub(crate) strip_byte_counts: Option<Vec<u64>>,
59
60    pub(crate) min_sample_value: Option<Vec<u16>>,
61    pub(crate) max_sample_value: Option<Vec<u16>>,
62
63    /// The number of pixels per ResolutionUnit in the ImageWidth direction.
64    pub(crate) x_resolution: Option<f64>,
65
66    /// The number of pixels per ResolutionUnit in the ImageLength direction.
67    pub(crate) y_resolution: Option<f64>,
68
69    /// How the components of each pixel are stored.
70    ///
71    /// The specification defines these values:
72    ///
73    /// - Chunky format. The component values for each pixel are stored contiguously. For example,
74    ///   for RGB data, the data is stored as RGBRGBRGB
75    /// - Planar format. The components are stored in separate component planes. For example, RGB
76    ///   data is stored with the Red components in one component plane, the Green in another, and
77    ///   the Blue in another.
78    ///
79    /// The specification adds a warning that PlanarConfiguration=2 is not in widespread use and
80    /// that Baseline TIFF readers are not required to support it.
81    ///
82    /// If SamplesPerPixel is 1, PlanarConfiguration is irrelevant, and need not be included.
83    pub(crate) planar_configuration: PlanarConfiguration,
84
85    pub(crate) resolution_unit: Option<ResolutionUnit>,
86
87    /// Name and version number of the software package(s) used to create the image.
88    pub(crate) software: Option<String>,
89
90    /// Date and time of image creation.
91    ///
92    /// The format is: "YYYY:MM:DD HH:MM:SS", with hours like those on a 24-hour clock, and one
93    /// space character between the date and the time. The length of the string, including the
94    /// terminating NUL, is 20 bytes.
95    pub(crate) date_time: Option<String>,
96    pub(crate) artist: Option<String>,
97    pub(crate) host_computer: Option<String>,
98
99    pub(crate) predictor: Option<Predictor>,
100
101    /// A color map for palette color images.
102    ///
103    /// This field defines a Red-Green-Blue color map (often called a lookup table) for
104    /// palette-color images. In a palette-color image, a pixel value is used to index into an RGB
105    /// lookup table. For example, a palette-color pixel having a value of 0 would be displayed
106    /// according to the 0th Red, Green, Blue triplet.
107    ///
108    /// In a TIFF ColorMap, all the Red values come first, followed by the Green values, then the
109    /// Blue values. The number of values for each color is 2**BitsPerSample. Therefore, the
110    /// ColorMap field for an 8-bit palette-color image would have 3 * 256 values. The width of
111    /// each value is 16 bits, as implied by the type of SHORT. 0 represents the minimum intensity,
112    /// and 65535 represents the maximum intensity. Black is represented by 0,0,0, and white by
113    /// 65535, 65535, 65535.
114    ///
115    /// ColorMap must be included in all palette-color images.
116    ///
117    /// In Specification Supplement 1, support was added for ColorMaps containing other then RGB
118    /// values. This scheme includes the Indexed tag, with value 1, and a PhotometricInterpretation
119    /// different from PaletteColor then next denotes the colorspace of the ColorMap entries.
120    pub(crate) color_map: Option<Vec<u16>>,
121
122    pub(crate) tile_width: Option<u32>,
123    pub(crate) tile_height: Option<u32>,
124
125    pub(crate) tile_offsets: Option<Vec<u64>>,
126    pub(crate) tile_byte_counts: Option<Vec<u64>>,
127
128    pub(crate) extra_samples: Option<Vec<u16>>,
129
130    pub(crate) sample_format: Vec<SampleFormat>,
131
132    pub(crate) jpeg_tables: Option<Bytes>,
133
134    pub(crate) copyright: Option<String>,
135
136    // Geospatial tags
137    pub(crate) geo_key_directory: Option<GeoKeyDirectory>,
138    pub(crate) model_pixel_scale: Option<Vec<f64>>,
139    pub(crate) model_tiepoint: Option<Vec<f64>>,
140    pub(crate) model_transformation: Option<Vec<f64>>,
141
142    // GDAL tags
143    pub(crate) gdal_nodata: Option<String>,
144    pub(crate) gdal_metadata: Option<String>,
145    pub(crate) other_tags: HashMap<Tag, TagValue>,
146}
147
148impl ImageFileDirectory {
149    /// Create a new ImageFileDirectory from tag data
150    pub fn from_tags(
151        tag_data: HashMap<Tag, TagValue>,
152        endianness: Endianness,
153    ) -> AsyncTiffResult<Self> {
154        let mut new_subfile_type = None;
155        let mut image_width = None;
156        let mut image_height = None;
157        let mut bits_per_sample = None;
158        let mut compression = None;
159        let mut photometric_interpretation = None;
160        let mut document_name = None;
161        let mut image_description = None;
162        let mut strip_offsets = None;
163        let mut orientation = None;
164        let mut samples_per_pixel = None;
165        let mut rows_per_strip = None;
166        let mut strip_byte_counts = None;
167        let mut min_sample_value = None;
168        let mut max_sample_value = None;
169        let mut x_resolution = None;
170        let mut y_resolution = None;
171        let mut planar_configuration = None;
172        let mut resolution_unit = None;
173        let mut software = None;
174        let mut date_time = None;
175        let mut artist = None;
176        let mut host_computer = None;
177        let mut predictor = None;
178        let mut color_map = None;
179        let mut tile_width = None;
180        let mut tile_height = None;
181        let mut tile_offsets = None;
182        let mut tile_byte_counts = None;
183        let mut extra_samples = None;
184        let mut sample_format = None;
185        let mut jpeg_tables = None;
186        let mut copyright = None;
187        let mut geo_key_directory_data = None;
188        let mut model_pixel_scale = None;
189        let mut model_tiepoint = None;
190        let mut model_transformation = None;
191        let mut geo_ascii_params: Option<String> = None;
192        let mut geo_double_params: Option<Vec<f64>> = None;
193        let mut gdal_nodata = None;
194        let mut gdal_metadata = None;
195
196        let mut other_tags = HashMap::new();
197
198        tag_data.into_iter().try_for_each(|(tag, value)| {
199            match tag {
200                Tag::NewSubfileType => new_subfile_type = Some(value.into_u32()?),
201                Tag::ImageWidth => image_width = Some(value.into_u32()?),
202                Tag::ImageLength => image_height = Some(value.into_u32()?),
203                Tag::BitsPerSample => bits_per_sample = Some(value.into_u16_vec()?),
204                Tag::Compression => {
205                    compression = Some(CompressionMethod::from_u16_exhaustive(value.into_u16()?))
206                }
207                Tag::PhotometricInterpretation => {
208                    photometric_interpretation =
209                        PhotometricInterpretation::from_u16(value.into_u16()?)
210                }
211                Tag::ImageDescription => image_description = Some(value.into_string()?),
212                Tag::StripOffsets => strip_offsets = Some(value.into_u64_vec()?),
213                Tag::Orientation => orientation = Some(value.into_u16()?),
214                Tag::SamplesPerPixel => samples_per_pixel = Some(value.into_u16()?),
215                Tag::RowsPerStrip => rows_per_strip = Some(value.into_u32()?),
216                Tag::StripByteCounts => strip_byte_counts = Some(value.into_u64_vec()?),
217                Tag::MinSampleValue => min_sample_value = Some(value.into_u16_vec()?),
218                Tag::MaxSampleValue => max_sample_value = Some(value.into_u16_vec()?),
219                Tag::XResolution => match value {
220                    TagValue::Rational(n, d) => x_resolution = Some(n as f64 / d as f64),
221                    _ => unreachable!("Expected rational type for XResolution."),
222                },
223                Tag::YResolution => match value {
224                    TagValue::Rational(n, d) => y_resolution = Some(n as f64 / d as f64),
225                    _ => unreachable!("Expected rational type for YResolution."),
226                },
227                Tag::PlanarConfiguration => {
228                    planar_configuration = PlanarConfiguration::from_u16(value.into_u16()?)
229                }
230                Tag::ResolutionUnit => {
231                    resolution_unit = ResolutionUnit::from_u16(value.into_u16()?)
232                }
233                Tag::Software => software = Some(value.into_string()?),
234                Tag::DateTime => date_time = Some(value.into_string()?),
235                Tag::Artist => artist = Some(value.into_string()?),
236                Tag::HostComputer => host_computer = Some(value.into_string()?),
237                Tag::Predictor => predictor = Predictor::from_u16(value.into_u16()?),
238                Tag::ColorMap => color_map = Some(value.into_u16_vec()?),
239                Tag::TileWidth => tile_width = Some(value.into_u32()?),
240                Tag::TileLength => tile_height = Some(value.into_u32()?),
241                Tag::TileOffsets => tile_offsets = Some(value.into_u64_vec()?),
242                Tag::TileByteCounts => tile_byte_counts = Some(value.into_u64_vec()?),
243                Tag::ExtraSamples => extra_samples = Some(value.into_u16_vec()?),
244                Tag::SampleFormat => {
245                    let values = value.into_u16_vec()?;
246                    sample_format = Some(
247                        values
248                            .into_iter()
249                            .map(SampleFormat::from_u16_exhaustive)
250                            .collect(),
251                    );
252                }
253                Tag::JPEGTables => jpeg_tables = Some(value.into_u8_vec()?.into()),
254                Tag::Copyright => copyright = Some(value.into_string()?),
255
256                // Geospatial tags
257                // http://geotiff.maptools.org/spec/geotiff2.4.html
258                Tag::GeoKeyDirectory => geo_key_directory_data = Some(value.into_u16_vec()?),
259                Tag::ModelPixelScale => model_pixel_scale = Some(value.into_f64_vec()?),
260                Tag::ModelTiepoint => model_tiepoint = Some(value.into_f64_vec()?),
261                Tag::ModelTransformation => model_transformation = Some(value.into_f64_vec()?),
262                Tag::GeoAsciiParams => geo_ascii_params = Some(value.into_string()?),
263                Tag::GeoDoubleParams => geo_double_params = Some(value.into_f64_vec()?),
264                Tag::GdalNodata => gdal_nodata = Some(value.into_string()?),
265                Tag::GdalMetadata => gdal_metadata = Some(value.into_string()?),
266                // Tags for which the tiff crate doesn't have a hard-coded enum variant
267                Tag::Unknown(DOCUMENT_NAME) => document_name = Some(value.into_string()?),
268                _ => {
269                    other_tags.insert(tag, value);
270                }
271            };
272            Ok::<_, TiffError>(())
273        })?;
274
275        let mut geo_key_directory = None;
276
277        // We need to actually parse the GeoKeyDirectory after parsing all other tags because the
278        // GeoKeyDirectory relies on `GeoAsciiParamsTag` having been parsed.
279        if let Some(data) = geo_key_directory_data {
280            let mut chunks = data.chunks(4);
281
282            let header = chunks
283                .next()
284                .expect("If the geo key directory exists, a header should exist.");
285            let key_directory_version = header[0];
286            assert_eq!(key_directory_version, 1);
287
288            let key_revision = header[1];
289            assert_eq!(key_revision, 1);
290
291            let _key_minor_revision = header[2];
292            let number_of_keys = header[3];
293
294            let mut tags = HashMap::with_capacity(number_of_keys as usize);
295            for _ in 0..number_of_keys {
296                let chunk = chunks
297                    .next()
298                    .expect("There should be a chunk for each key.");
299
300                let key_id = chunk[0];
301                let tag_name = if let Ok(tag_name) = GeoKeyTag::try_from_primitive(key_id) {
302                    tag_name
303                } else {
304                    // Skip unknown GeoKeyTag ids. Some GeoTIFFs include keys that were proposed
305                    // but not included in the GeoTIFF spec. See
306                    // https://github.com/developmentseed/async-tiff/pull/131 and
307                    // https://github.com/virtual-zarr/virtual-tiff/issues/52
308                    continue;
309                };
310
311                let tag_location = chunk[1];
312                let count = chunk[2];
313                let value_offset = chunk[3];
314
315                if tag_location == 0 {
316                    tags.insert(tag_name, TagValue::Short(value_offset));
317                } else if Tag::from_u16_exhaustive(tag_location) == Tag::GeoAsciiParams {
318                    // If the tag_location points to the value of Tag::GeoAsciiParams, then we
319                    // need to extract a subslice from GeoAsciiParams
320
321                    let geo_ascii_params = geo_ascii_params
322                        .as_ref()
323                        .expect("GeoAsciiParamsTag exists but geo_ascii_params does not.");
324                    let value_offset = value_offset as usize;
325                    let mut s = &geo_ascii_params[value_offset..value_offset + count as usize];
326
327                    // It seems that this string subslice might always include the final |
328                    // character?
329                    if s.ends_with('|') {
330                        s = &s[0..s.len() - 1];
331                    }
332
333                    tags.insert(tag_name, TagValue::Ascii(s.to_string()));
334                } else if Tag::from_u16_exhaustive(tag_location) == Tag::GeoDoubleParams {
335                    // If the tag_location points to the value of Tag::GeoDoubleParams, then we
336                    // need to extract a subslice from GeoDoubleParams
337
338                    let geo_double_params = geo_double_params
339                        .as_ref()
340                        .expect("GeoDoubleParamsTag exists but geo_double_params does not.");
341                    let value_offset = value_offset as usize;
342                    let value = if count == 1 {
343                        TagValue::Double(geo_double_params[value_offset])
344                    } else {
345                        let x = geo_double_params[value_offset..value_offset + count as usize]
346                            .iter()
347                            .map(|val| TagValue::Double(*val))
348                            .collect();
349                        TagValue::List(x)
350                    };
351                    tags.insert(tag_name, value);
352                }
353            }
354            geo_key_directory = Some(GeoKeyDirectory::from_tags(tags)?);
355        }
356
357        let samples_per_pixel = samples_per_pixel.expect("samples_per_pixel not found");
358        let planar_configuration = if let Some(planar_configuration) = planar_configuration {
359            planar_configuration
360        } else if samples_per_pixel == 1 {
361            // If SamplesPerPixel is 1, PlanarConfiguration is irrelevant, and need not be included.
362            // https://web.archive.org/web/20240329145253/https://www.awaresystems.be/imaging/tiff/tifftags/planarconfiguration.html
363            PlanarConfiguration::Chunky
364        } else {
365            PlanarConfiguration::Chunky
366        };
367        Ok(Self {
368            endianness,
369            new_subfile_type,
370            image_width: image_width.expect("image_width not found"),
371            image_height: image_height.expect("image_height not found"),
372            bits_per_sample: bits_per_sample.expect("bits per sample not found"),
373            // Defaults to no compression
374            // https://web.archive.org/web/20240329145331/https://www.awaresystems.be/imaging/tiff/tifftags/compression.html
375            compression: compression.unwrap_or(CompressionMethod::None),
376            photometric_interpretation: photometric_interpretation
377                .expect("photometric interpretation not found"),
378            document_name,
379            image_description,
380            strip_offsets,
381            orientation,
382            samples_per_pixel,
383            rows_per_strip,
384            strip_byte_counts,
385            min_sample_value,
386            max_sample_value,
387            x_resolution,
388            y_resolution,
389            planar_configuration,
390            resolution_unit,
391            software,
392            date_time,
393            artist,
394            host_computer,
395            predictor,
396            color_map,
397            tile_width,
398            tile_height,
399            tile_offsets,
400            tile_byte_counts,
401            extra_samples,
402            // Uint8 is the default for SampleFormat
403            // https://web.archive.org/web/20240329145340/https://www.awaresystems.be/imaging/tiff/tifftags/sampleformat.html
404            sample_format: sample_format
405                .unwrap_or(vec![SampleFormat::Uint; samples_per_pixel as _]),
406            copyright,
407            jpeg_tables,
408            geo_key_directory,
409            model_pixel_scale,
410            model_tiepoint,
411            model_transformation,
412            gdal_nodata,
413            gdal_metadata,
414            other_tags,
415        })
416    }
417
418    /// A general indication of the kind of data contained in this subfile.
419    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/newsubfiletype.html>
420    pub fn new_subfile_type(&self) -> Option<u32> {
421        self.new_subfile_type
422    }
423
424    /// The number of columns in the image, i.e., the number of pixels per row.
425    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/imagewidth.html>
426    pub fn image_width(&self) -> u32 {
427        self.image_width
428    }
429
430    /// The number of rows of pixels in the image.
431    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/imagelength.html>
432    pub fn image_height(&self) -> u32 {
433        self.image_height
434    }
435
436    /// Number of bits per component.
437    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/bitspersample.html>
438    pub fn bits_per_sample(&self) -> &[u16] {
439        &self.bits_per_sample
440    }
441
442    /// Compression scheme used on the image data.
443    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/compression.html>
444    pub fn compression(&self) -> CompressionMethod {
445        self.compression
446    }
447
448    /// The color space of the image data.
449    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/photometricinterpretation.html>
450    pub fn photometric_interpretation(&self) -> PhotometricInterpretation {
451        self.photometric_interpretation
452    }
453
454    /// Document name.
455    pub fn document_name(&self) -> Option<&str> {
456        self.document_name.as_deref()
457    }
458
459    /// A string that describes the subject of the image.
460    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/imagedescription.html>
461    pub fn image_description(&self) -> Option<&str> {
462        self.image_description.as_deref()
463    }
464
465    /// For each strip, the byte offset of that strip.
466    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/stripoffsets.html>
467    pub fn strip_offsets(&self) -> Option<&[u64]> {
468        self.strip_offsets.as_deref()
469    }
470
471    /// The orientation of the image with respect to the rows and columns.
472    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/orientation.html>
473    pub fn orientation(&self) -> Option<u16> {
474        self.orientation
475    }
476
477    /// The number of components per pixel.
478    ///
479    /// SamplesPerPixel is usually 1 for bilevel, grayscale, and palette-color images.
480    /// SamplesPerPixel is usually 3 for RGB images. If this value is higher, ExtraSamples should
481    /// give an indication of the meaning of the additional channels.
482    pub fn samples_per_pixel(&self) -> u16 {
483        self.samples_per_pixel
484    }
485
486    /// The number of rows per strip.
487    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/rowsperstrip.html>
488    pub fn rows_per_strip(&self) -> Option<u32> {
489        self.rows_per_strip
490    }
491
492    /// For each strip, the number of bytes in the strip after compression.
493    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/stripbytecounts.html>
494    pub fn strip_byte_counts(&self) -> Option<&[u64]> {
495        self.strip_byte_counts.as_deref()
496    }
497
498    /// The minimum component value used.
499    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/minsamplevalue.html>
500    pub fn min_sample_value(&self) -> Option<&[u16]> {
501        self.min_sample_value.as_deref()
502    }
503
504    /// The maximum component value used.
505    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/maxsamplevalue.html>
506    pub fn max_sample_value(&self) -> Option<&[u16]> {
507        self.max_sample_value.as_deref()
508    }
509
510    /// The number of pixels per ResolutionUnit in the ImageWidth direction.
511    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/xresolution.html>
512    pub fn x_resolution(&self) -> Option<f64> {
513        self.x_resolution
514    }
515
516    /// The number of pixels per ResolutionUnit in the ImageLength direction.
517    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/yresolution.html>
518    pub fn y_resolution(&self) -> Option<f64> {
519        self.y_resolution
520    }
521
522    /// How the components of each pixel are stored.
523    ///
524    /// The specification defines these values:
525    ///
526    /// - Chunky format. The component values for each pixel are stored contiguously. For example,
527    ///   for RGB data, the data is stored as RGBRGBRGB
528    /// - Planar format. The components are stored in separate component planes. For example, RGB
529    ///   data is stored with the Red components in one component plane, the Green in another, and
530    ///   the Blue in another.
531    ///
532    /// The specification adds a warning that PlanarConfiguration=2 is not in widespread use and
533    /// that Baseline TIFF readers are not required to support it.
534    ///
535    /// If SamplesPerPixel is 1, PlanarConfiguration is irrelevant, and need not be included.
536    ///
537    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/planarconfiguration.html>
538    pub fn planar_configuration(&self) -> PlanarConfiguration {
539        self.planar_configuration
540    }
541
542    /// The unit of measurement for XResolution and YResolution.
543    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/resolutionunit.html>
544    pub fn resolution_unit(&self) -> Option<ResolutionUnit> {
545        self.resolution_unit
546    }
547
548    /// Name and version number of the software package(s) used to create the image.
549    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/software.html>
550    pub fn software(&self) -> Option<&str> {
551        self.software.as_deref()
552    }
553
554    /// Date and time of image creation.
555    ///
556    /// The format is: "YYYY:MM:DD HH:MM:SS", with hours like those on a 24-hour clock, and one
557    /// space character between the date and the time. The length of the string, including the
558    /// terminating NUL, is 20 bytes.
559    ///
560    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/datetime.html>
561    pub fn date_time(&self) -> Option<&str> {
562        self.date_time.as_deref()
563    }
564
565    /// Person who created the image.
566    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/artist.html>
567    pub fn artist(&self) -> Option<&str> {
568        self.artist.as_deref()
569    }
570
571    /// The computer and/or operating system in use at the time of image creation.
572    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/hostcomputer.html>
573    pub fn host_computer(&self) -> Option<&str> {
574        self.host_computer.as_deref()
575    }
576
577    /// A mathematical operator that is applied to the image data before an encoding scheme is
578    /// applied.
579    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/predictor.html>
580    pub fn predictor(&self) -> Option<Predictor> {
581        self.predictor
582    }
583
584    /// The tile width in pixels. This is the number of columns in each tile.
585    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/tilewidth.html>
586    pub fn tile_width(&self) -> Option<u32> {
587        self.tile_width
588    }
589
590    /// The tile length (height) in pixels. This is the number of rows in each tile.
591    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/tilelength.html>
592    pub fn tile_height(&self) -> Option<u32> {
593        self.tile_height
594    }
595
596    /// For each tile, the byte offset of that tile, as compressed and stored on disk.
597    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/tileoffsets.html>
598    pub fn tile_offsets(&self) -> Option<&[u64]> {
599        self.tile_offsets.as_deref()
600    }
601
602    /// For each tile, the number of (compressed) bytes in that tile.
603    /// <https://web.archive.org/web/20240329145339/https://www.awaresystems.be/imaging/tiff/tifftags/tilebytecounts.html>
604    pub fn tile_byte_counts(&self) -> Option<&[u64]> {
605        self.tile_byte_counts.as_deref()
606    }
607
608    /// Description of extra components.
609    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/extrasamples.html>
610    pub fn extra_samples(&self) -> Option<&[u16]> {
611        self.extra_samples.as_deref()
612    }
613
614    /// Specifies how to interpret each data sample in a pixel.
615    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/sampleformat.html>
616    pub fn sample_format(&self) -> &[SampleFormat] {
617        &self.sample_format
618    }
619
620    /// JPEG quantization and/or Huffman tables.
621    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/jpegtables.html>
622    pub fn jpeg_tables(&self) -> Option<&[u8]> {
623        self.jpeg_tables.as_deref()
624    }
625
626    /// Copyright notice.
627    /// <https://web.archive.org/web/20240329145250/https://www.awaresystems.be/imaging/tiff/tifftags/copyright.html>
628    pub fn copyright(&self) -> Option<&str> {
629        self.copyright.as_deref()
630    }
631
632    /// Geospatial tags
633    /// <https://web.archive.org/web/20240329145313/https://www.awaresystems.be/imaging/tiff/tifftags/geokeydirectorytag.html>
634    pub fn geo_key_directory(&self) -> Option<&GeoKeyDirectory> {
635        self.geo_key_directory.as_ref()
636    }
637
638    /// Used in interchangeable GeoTIFF files.
639    /// <https://web.archive.org/web/20240329145238/https://www.awaresystems.be/imaging/tiff/tifftags/modelpixelscaletag.html>
640    pub fn model_pixel_scale(&self) -> Option<&[f64]> {
641        self.model_pixel_scale.as_deref()
642    }
643
644    /// Used in interchangeable GeoTIFF files.
645    /// <https://web.archive.org/web/20240329145303/https://www.awaresystems.be/imaging/tiff/tifftags/modeltiepointtag.html>
646    pub fn model_tiepoint(&self) -> Option<&[f64]> {
647        self.model_tiepoint.as_deref()
648    }
649
650    /// Stores a full 4×4 affine transformation matrix that maps pixel/line coordinates directly
651    /// into model (map) coordinates.
652    pub fn model_transformation(&self) -> Option<&[f64]> {
653        self.model_transformation.as_deref()
654    }
655
656    /// GDAL NoData value
657    /// <https://gdal.org/en/stable/drivers/raster/gtiff.html#nodata-value>
658    pub fn gdal_nodata(&self) -> Option<&str> {
659        self.gdal_nodata.as_deref()
660    }
661
662    /// GDAL Metadata XML information
663    ///
664    /// Non standard metadata items are grouped together into a XML string stored in the non
665    /// standard `TIFFTAG_GDAL_METADATA` ASCII tag (code `42112`).
666    pub fn gdal_metadata(&self) -> Option<&str> {
667        self.gdal_metadata.as_deref()
668    }
669
670    /// Tags for which this crate doesn't have a hard-coded enum variant.
671    pub fn other_tags(&self) -> &HashMap<Tag, TagValue> {
672        &self.other_tags
673    }
674
675    /// Construct colormap from colormap tag
676    pub fn colormap(&self) -> Option<HashMap<usize, [u8; 3]>> {
677        fn cmap_transform(val: u16) -> u8 {
678            let val = ((val as f64 / 65535.0) * 255.0).floor();
679            if val >= 255.0 {
680                255
681            } else if val < 0.0 {
682                0
683            } else {
684                val as u8
685            }
686        }
687
688        if let Some(cmap_data) = &self.color_map {
689            let bits_per_sample = self.bits_per_sample[0];
690            let count = 2_usize.pow(bits_per_sample as u32);
691            let mut result = HashMap::new();
692
693            // TODO: support nodata
694            for idx in 0..count {
695                let color: [u8; 3] =
696                    std::array::from_fn(|i| cmap_transform(cmap_data[idx + i * count]));
697                // TODO: Handle nodata value
698
699                result.insert(idx, color);
700            }
701
702            Some(result)
703        } else {
704            None
705        }
706    }
707
708    fn get_tile_byte_range(&self, x: usize, y: usize) -> Option<Range<u64>> {
709        let tile_offsets = self.tile_offsets.as_deref()?;
710        let tile_byte_counts = self.tile_byte_counts.as_deref()?;
711        let idx = (y * self.tile_count()?.0) + x;
712        let offset = tile_offsets[idx] as usize;
713        // TODO: aiocogeo has a -1 here, but I think that was in error
714        let byte_count = tile_byte_counts[idx] as usize;
715        Some(offset as _..(offset + byte_count) as _)
716    }
717
718    /// Fetch the tile located at `x` column and `y` row using the provided reader.
719    pub async fn fetch_tile(
720        &self,
721        x: usize,
722        y: usize,
723        reader: &dyn AsyncFileReader,
724    ) -> AsyncTiffResult<Tile> {
725        let range = self
726            .get_tile_byte_range(x, y)
727            .ok_or(AsyncTiffError::General("Not a tiled TIFF".to_string()))?;
728        let compressed_bytes = reader.get_bytes(range).await?;
729        let data_type = DataType::from_tags(&self.sample_format, &self.bits_per_sample);
730        Ok(Tile {
731            x,
732            y,
733            data_type,
734            width: self.tile_width.unwrap_or(self.image_width),
735            height: self.tile_height.unwrap_or(self.image_height),
736            planar_configuration: self.planar_configuration,
737            samples_per_pixel: self.samples_per_pixel,
738            predictor: self.predictor.unwrap_or(Predictor::None),
739            predictor_info: PredictorInfo::from_ifd(self),
740            compressed_bytes,
741            compression_method: self.compression,
742            photometric_interpretation: self.photometric_interpretation,
743            jpeg_tables: self.jpeg_tables.clone(),
744        })
745    }
746
747    /// Fetch the tiles located at `x` column and `y` row using the provided reader.
748    pub async fn fetch_tiles(
749        &self,
750        x: &[usize],
751        y: &[usize],
752        reader: &dyn AsyncFileReader,
753    ) -> AsyncTiffResult<Vec<Tile>> {
754        assert_eq!(x.len(), y.len(), "x and y should have same len");
755
756        let predictor_info = PredictorInfo::from_ifd(self);
757        let data_type = DataType::from_tags(&self.sample_format, &self.bits_per_sample);
758
759        // 1: Get all the byte ranges for all tiles
760        let byte_ranges = x
761            .iter()
762            .zip(y)
763            .map(|(x, y)| {
764                self.get_tile_byte_range(*x, *y)
765                    .ok_or(AsyncTiffError::General("Not a tiled TIFF".to_string()))
766            })
767            .collect::<AsyncTiffResult<Vec<_>>>()?;
768
769        // 2: Fetch using `get_byte_ranges`
770        let buffers = reader.get_byte_ranges(byte_ranges).await?;
771
772        // 3: Create tile objects
773        let mut tiles = vec![];
774        for ((compressed_bytes, &x), &y) in buffers.into_iter().zip(x).zip(y) {
775            let tile = Tile {
776                x,
777                y,
778                data_type,
779                width: self.tile_width.unwrap_or(self.image_width),
780                height: self.tile_height.unwrap_or(self.image_height),
781                planar_configuration: self.planar_configuration,
782                samples_per_pixel: self.samples_per_pixel,
783                predictor: self.predictor.unwrap_or(Predictor::None),
784                predictor_info,
785                compressed_bytes,
786                compression_method: self.compression,
787                photometric_interpretation: self.photometric_interpretation,
788                jpeg_tables: self.jpeg_tables.clone(),
789            };
790            tiles.push(tile);
791        }
792        Ok(tiles)
793    }
794
795    /// Return the number of x/y tiles in the IFD
796    /// Returns `None` if this is not a tiled TIFF
797    pub fn tile_count(&self) -> Option<(usize, usize)> {
798        let x_count = (self.image_width as f64 / self.tile_width? as f64).ceil();
799        let y_count = (self.image_height as f64 / self.tile_height? as f64).ceil();
800        Some((x_count as usize, y_count as usize))
801    }
802}
803
804/// Calculate the actual pixel dimensions of a tile at position (x, y).
805///
806/// Edge tiles may be smaller than the nominal tile dimensions when the image
807/// dimensions are not exact multiples of the tile dimensions.
808///
809/// # Arguments
810/// * `x` - Tile column index (0-based)
811/// * `y` - Tile row index (0-based)
812/// * `image_width` - Total image width in pixels
813/// * `image_height` - Total image height in pixels
814/// * `tile_width` - Nominal tile width (None for stripped images)
815/// * `tile_height` - Nominal tile height (None for stripped images)
816/// * `rows_per_strip` - Rows per strip for stripped images (None for tiled images)
817///
818/// # Returns
819/// A tuple of (actual_width, actual_height) in pixels
820#[allow(dead_code)]
821// Note: this was originally implemented with the idea that the last tile (if unaligned) would be
822// this size, but apparently the end tile is still the same size as the others, just with padding.
823// Leaving this here in case it's useful later.
824pub(crate) fn compute_tile_dimensions(
825    x: usize,
826    y: usize,
827    image_width: u32,
828    image_height: u32,
829    tile_width: Option<u32>,
830    tile_height: Option<u32>,
831    rows_per_strip: Option<u32>,
832) -> (u32, u32) {
833    // For tiled images (both tile_width and tile_height must be present)
834    if let (Some(tile_width), Some(tile_height)) = (tile_width, tile_height) {
835        let x_offset = (x as u32) * tile_width;
836        let y_offset = (y as u32) * tile_height;
837
838        let actual_width = std::cmp::min(tile_width, image_width.saturating_sub(x_offset));
839        let actual_height = std::cmp::min(tile_height, image_height.saturating_sub(y_offset));
840
841        (actual_width, actual_height)
842    } else {
843        // For stripped images (or fallback)
844        let strip_height = rows_per_strip.unwrap_or(image_height);
845        let y_offset = (y as u32) * strip_height;
846        let actual_height = std::cmp::min(strip_height, image_height.saturating_sub(y_offset));
847
848        (image_width, actual_height)
849    }
850}
851
852#[cfg(test)]
853mod tests {
854    use super::*;
855
856    #[test]
857    fn test_tile_dimensions_full_tiles() {
858        // 512x512 image with 256x256 tiles - all tiles are full size
859        assert_eq!(
860            compute_tile_dimensions(0, 0, 512, 512, Some(256), Some(256), None),
861            (256, 256),
862            "Top-left tile should be full size"
863        );
864        assert_eq!(
865            compute_tile_dimensions(1, 0, 512, 512, Some(256), Some(256), None),
866            (256, 256),
867            "Top-right tile should be full size"
868        );
869        assert_eq!(
870            compute_tile_dimensions(0, 1, 512, 512, Some(256), Some(256), None),
871            (256, 256),
872            "Bottom-left tile should be full size"
873        );
874        assert_eq!(
875            compute_tile_dimensions(1, 1, 512, 512, Some(256), Some(256), None),
876            (256, 256),
877            "Bottom-right tile should be full size"
878        );
879    }
880
881    #[test]
882    fn test_tile_dimensions_edge_tiles() {
883        // 500x500 image with 256x256 tiles - edge tiles are partial
884        assert_eq!(
885            compute_tile_dimensions(0, 0, 500, 500, Some(256), Some(256), None),
886            (256, 256),
887            "Top-left tile should be full size"
888        );
889        assert_eq!(
890            compute_tile_dimensions(1, 0, 500, 500, Some(256), Some(256), None),
891            (244, 256),
892            "Top-right edge tile should be 244 pixels wide"
893        );
894        assert_eq!(
895            compute_tile_dimensions(0, 1, 500, 500, Some(256), Some(256), None),
896            (256, 244),
897            "Bottom-left edge tile should be 244 pixels tall"
898        );
899        assert_eq!(
900            compute_tile_dimensions(1, 1, 500, 500, Some(256), Some(256), None),
901            (244, 244),
902            "Bottom-right corner tile should be 244x244"
903        );
904    }
905
906    #[test]
907    fn test_strip_dimensions() {
908        // 1024x768 stripped image with 128 rows per strip
909        assert_eq!(
910            compute_tile_dimensions(0, 0, 1024, 768, None, None, Some(128)),
911            (1024, 128),
912            "First strip should be full width and height"
913        );
914        assert_eq!(
915            compute_tile_dimensions(0, 5, 1024, 768, None, None, Some(128)),
916            (1024, 128),
917            "Middle strip should be full size"
918        );
919        assert_eq!(
920            compute_tile_dimensions(0, 5, 1024, 768, None, None, Some(128)),
921            (1024, 128),
922            "Last strip (768 / 128 = 6 strips, index 5) should be full height"
923        );
924    }
925
926    #[test]
927    fn test_strip_dimensions_partial_last_strip() {
928        // 1024x700 stripped image with 128 rows per strip
929        // Last strip should be: 700 - (5 * 128) = 60 rows
930        assert_eq!(
931            compute_tile_dimensions(0, 0, 1024, 700, None, None, Some(128)),
932            (1024, 128),
933            "First strip should be full height"
934        );
935        assert_eq!(
936            compute_tile_dimensions(0, 5, 1024, 700, None, None, Some(128)),
937            (1024, 60),
938            "Last strip should be 60 pixels tall"
939        );
940    }
941
942    #[test]
943    fn test_single_tile_image() {
944        // Image smaller than tile size
945        assert_eq!(
946            compute_tile_dimensions(0, 0, 100, 100, Some(256), Some(256), None),
947            (100, 100),
948            "Single tile should match image dimensions"
949        );
950    }
951
952    #[test]
953    fn test_strip_default_height() {
954        // Stripped image with no rows_per_strip (defaults to full image height)
955        assert_eq!(
956            compute_tile_dimensions(0, 0, 1024, 768, None, None, None),
957            (1024, 768),
958            "Strip should default to full image height"
959        );
960    }
961}