Skip to main content

geotiff_reader/
lib.rs

1//! Pure-Rust GeoTIFF reader with optional HTTP range-backed remote access.
2//!
3//! Supports:
4//! - **GeoTIFF**: TIFF files with GeoKey metadata (EPSG codes, CRS, tiepoints, pixel scale)
5//! - **COG**: overview discovery plus optional remote open via HTTP range requests
6//! - **Reads**: full rasters, windows, overviews, and single storage-domain bands
7//! - **Compression passthrough**: any compression supported by `tiff-reader`, including TIFF
8//!   `LERC`, `LERC+DEFLATE`, and, with the `zstd` feature enabled on `tiff-reader`, `LERC+ZSTD`
9//!
10//! # Example
11//!
12//! ```no_run
13//! # #[cfg(feature = "local")]
14//! # fn main() -> Result<(), geotiff_reader::Error> {
15//! use geotiff_reader::GeoTiffFile;
16//!
17//! let file = GeoTiffFile::open("dem.tif")?;
18//! println!("EPSG: {:?}", file.epsg());
19//! println!("bounds: {:?}", file.geo_bounds());
20//! println!("size: {}x{}", file.width(), file.height());
21//! # Ok(())
22//! # }
23//! # #[cfg(not(feature = "local"))]
24//! # fn main() {}
25//! ```
26
27pub mod crs;
28pub mod error;
29pub mod geokeys;
30pub mod transform;
31
32#[cfg(feature = "cog")]
33pub mod cog;
34
35pub use error::{Error, Result};
36
37#[cfg(feature = "local")]
38use crs::CrsInfo;
39#[cfg(feature = "local")]
40use geokeys::GeoKeyDirectory;
41#[cfg(feature = "local")]
42use ndarray::ArrayD;
43#[cfg(feature = "local")]
44use std::collections::HashSet;
45#[cfg(feature = "local")]
46use std::path::Path;
47#[cfg(feature = "local")]
48use tiff_reader::{OpenOptions as TiffOpenOptions, TagValue, TiffFile, TiffSample};
49#[cfg(feature = "local")]
50use transform::GeoTransform;
51
52#[cfg(feature = "local")]
53use geotiff_core::tags::{
54    TAG_GDAL_NODATA, TAG_GEO_ASCII_PARAMS, TAG_GEO_DOUBLE_PARAMS, TAG_GEO_KEY_DIRECTORY,
55    TAG_MODEL_PIXEL_SCALE, TAG_MODEL_TIEPOINT, TAG_MODEL_TRANSFORMATION, TAG_NEW_SUBFILE_TYPE,
56    TAG_SUBFILE_TYPE,
57};
58
59/// A GeoTIFF file handle with geospatial metadata.
60#[cfg(feature = "local")]
61pub struct GeoTiffFile {
62    tiff: TiffFile,
63    geo_metadata: GeoMetadata,
64    crs: CrsInfo,
65    geokeys: GeoKeyDirectory,
66    transform: Option<GeoTransform>,
67    base_ifd_index: usize,
68    overview_ifds: Vec<GeoImageIfd>,
69}
70
71#[cfg(feature = "local")]
72#[derive(Debug, Clone)]
73struct GeoImageIfd {
74    top_level_ifd_index: Option<usize>,
75    ifd: tiff_reader::Ifd,
76}
77
78#[cfg(feature = "local")]
79pub use tiff_reader::OpenOptions as GeoTiffOpenOptions;
80
81pub use geotiff_core::GeoMetadata;
82
83#[cfg(feature = "local")]
84impl GeoTiffFile {
85    /// Open a GeoTIFF file from disk.
86    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
87        Self::open_with_options(path, TiffOpenOptions::default())
88    }
89
90    /// Open a GeoTIFF file from disk with explicit TIFF decoder options.
91    pub fn open_with_options<P: AsRef<Path>>(path: P, options: GeoTiffOpenOptions) -> Result<Self> {
92        let tiff = TiffFile::open_with_options(path, options)?;
93        Self::from_tiff(tiff)
94    }
95
96    /// Open a GeoTIFF file from disk using memory-mapped I/O.
97    ///
98    /// # Safety
99    ///
100    /// The caller must guarantee that the mapped file is not mutated or
101    /// truncated while the returned `GeoTiffFile` is alive. This includes
102    /// writes through other file handles and writes from other processes.
103    pub unsafe fn open_mmap<P: AsRef<Path>>(path: P) -> Result<Self> {
104        unsafe { Self::open_mmap_with_options(path, TiffOpenOptions::default()) }
105    }
106
107    /// Open a GeoTIFF file from disk using memory-mapped I/O with explicit TIFF decoder options.
108    ///
109    /// # Safety
110    ///
111    /// The caller must guarantee that the mapped file is not mutated or
112    /// truncated while the returned `GeoTiffFile` is alive. This includes
113    /// writes through other file handles and writes from other processes.
114    pub unsafe fn open_mmap_with_options<P: AsRef<Path>>(
115        path: P,
116        options: GeoTiffOpenOptions,
117    ) -> Result<Self> {
118        let tiff = unsafe { TiffFile::open_mmap_with_options(path, options)? };
119        Self::from_tiff(tiff)
120    }
121
122    /// Open a GeoTIFF from an owned byte buffer.
123    pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
124        Self::from_bytes_with_options(data, TiffOpenOptions::default())
125    }
126
127    /// Open a GeoTIFF from bytes with explicit TIFF decoder options.
128    pub fn from_bytes_with_options(data: Vec<u8>, options: GeoTiffOpenOptions) -> Result<Self> {
129        let tiff = TiffFile::from_bytes_with_options(data, options)?;
130        Self::from_tiff(tiff)
131    }
132
133    pub(crate) fn from_tiff(tiff: TiffFile) -> Result<Self> {
134        let metadata_ifd_index = find_metadata_ifd_index(tiff.ifds())?;
135        let metadata_ifd = tiff.ifd(metadata_ifd_index)?;
136        let geokeys = parse_geokey_directory(metadata_ifd)?;
137        let crs = CrsInfo::from_geokeys(&geokeys);
138        let epsg = crs.epsg();
139        let tiepoints = parse_tiepoints(metadata_ifd);
140        let pixel_scale = parse_fixed_len_double_tag::<3>(
141            metadata_ifd
142                .tag(TAG_MODEL_PIXEL_SCALE)
143                .map(|tag| &tag.value),
144        );
145        let transformation = parse_fixed_len_double_tag::<16>(
146            metadata_ifd
147                .tag(TAG_MODEL_TRANSFORMATION)
148                .map(|tag| &tag.value),
149        );
150        let transform = transformation
151            .as_ref()
152            .map(GeoTransform::from_transformation_matrix)
153            .or_else(|| {
154                let tiepoint = tiepoints.first()?;
155                let scale = pixel_scale.as_ref()?;
156                Some(GeoTransform::from_tiepoint_and_scale_with_raster_type(
157                    tiepoint,
158                    scale,
159                    crs.raster_type_enum(),
160                ))
161            });
162        let base_ifd_index = find_base_ifd_index(tiff.ifds(), metadata_ifd_index);
163        let base_ifd = tiff.ifd(base_ifd_index)?;
164        let overview_ifds =
165            collect_overview_ifds(&tiff, base_ifd, base_ifd_index, metadata_ifd_index)?;
166        let geo_bounds = transform
167            .as_ref()
168            .map(|gt| gt.bounds(base_ifd.width(), base_ifd.height()));
169
170        let geo_metadata = GeoMetadata {
171            epsg,
172            tiepoints,
173            pixel_scale,
174            transformation,
175            nodata: parse_nodata(metadata_ifd),
176            band_count: base_ifd.samples_per_pixel() as u32,
177            width: base_ifd.width(),
178            height: base_ifd.height(),
179            geo_bounds,
180        };
181
182        Ok(Self {
183            tiff,
184            geo_metadata,
185            crs,
186            geokeys,
187            transform,
188            base_ifd_index,
189            overview_ifds,
190        })
191    }
192
193    /// Returns the underlying TIFF file.
194    pub fn tiff(&self) -> &TiffFile {
195        &self.tiff
196    }
197
198    /// Returns the parsed GeoTIFF metadata.
199    pub fn metadata(&self) -> &GeoMetadata {
200        &self.geo_metadata
201    }
202
203    /// Returns the EPSG code of the coordinate reference system, if present.
204    pub fn epsg(&self) -> Option<u32> {
205        self.geo_metadata.epsg
206    }
207
208    /// Returns the extracted CRS information.
209    pub fn crs(&self) -> &CrsInfo {
210        &self.crs
211    }
212
213    /// Returns the parsed GeoKey directory.
214    pub fn geokeys(&self) -> &GeoKeyDirectory {
215        &self.geokeys
216    }
217
218    /// Returns the affine transform, if present.
219    pub fn transform(&self) -> Option<&GeoTransform> {
220        self.transform.as_ref()
221    }
222
223    /// Returns the geographic bounds as `(min_x, min_y, max_x, max_y)`.
224    pub fn geo_bounds(&self) -> Option<[f64; 4]> {
225        self.geo_metadata.geo_bounds
226    }
227
228    /// Convert a pixel coordinate to map coordinates.
229    pub fn pixel_to_geo(&self, col: f64, row: f64) -> Option<(f64, f64)> {
230        self.transform
231            .map(|transform| transform.pixel_to_geo(col, row))
232    }
233
234    /// Convert map coordinates to pixel coordinates.
235    pub fn geo_to_pixel(&self, x: f64, y: f64) -> Option<(f64, f64)> {
236        self.transform
237            .and_then(|transform| transform.geo_to_pixel(x, y))
238    }
239
240    /// Returns the image width in pixels.
241    pub fn width(&self) -> u32 {
242        self.geo_metadata.width
243    }
244
245    /// Returns the image height in pixels.
246    pub fn height(&self) -> u32 {
247        self.geo_metadata.height
248    }
249
250    /// Returns the number of bands.
251    pub fn band_count(&self) -> u32 {
252        self.geo_metadata.band_count
253    }
254
255    /// Returns the nodata value, if set.
256    pub fn nodata(&self) -> Option<&str> {
257        self.geo_metadata.nodata.as_deref()
258    }
259
260    /// Returns the number of internal overview IFDs.
261    pub fn overview_count(&self) -> usize {
262        self.overview_ifds.len()
263    }
264
265    /// Returns the top-level TIFF IFD index of the requested overview.
266    ///
267    /// Overviews stored in SubIFDs return
268    /// `Error::OverviewHasNoTopLevelIfdIndex`.
269    pub fn overview_ifd_index(&self, overview_index: usize) -> Result<usize> {
270        self.overview_ifds
271            .get(overview_index)
272            .ok_or(Error::OverviewNotFound(overview_index))?
273            .top_level_ifd_index
274            .ok_or(Error::OverviewHasNoTopLevelIfdIndex(overview_index))
275    }
276
277    /// Returns the parsed TIFF IFD metadata for the requested overview.
278    pub fn overview_ifd(&self, overview_index: usize) -> Result<&tiff_reader::Ifd> {
279        self.overview_ifds
280            .get(overview_index)
281            .map(|overview| &overview.ifd)
282            .ok_or(Error::OverviewNotFound(overview_index))
283    }
284
285    /// Returns the TIFF IFD index of the base-resolution image.
286    pub fn base_ifd_index(&self) -> usize {
287        self.base_ifd_index
288    }
289
290    /// Decode the base-resolution raster into storage-domain typed samples.
291    pub fn read_raster<T: TiffSample>(&self) -> Result<ArrayD<T>> {
292        self.tiff
293            .read_image::<T>(self.base_ifd_index)
294            .map_err(Into::into)
295    }
296
297    /// Decode a base-resolution pixel window into storage-domain typed samples.
298    pub fn read_window<T: TiffSample>(
299        &self,
300        row_off: usize,
301        col_off: usize,
302        rows: usize,
303        cols: usize,
304    ) -> Result<ArrayD<T>> {
305        self.tiff
306            .read_window::<T>(self.base_ifd_index, row_off, col_off, rows, cols)
307            .map_err(Into::into)
308    }
309
310    /// Decode one base-resolution storage-domain band into a typed
311    /// `[height, width]` ndarray.
312    pub fn read_band<T: TiffSample>(&self, band_index: usize) -> Result<ArrayD<T>> {
313        self.tiff
314            .read_band::<T>(self.base_ifd_index, band_index)
315            .map_err(Into::into)
316    }
317
318    /// Decode a base-resolution window from one storage-domain band into a
319    /// typed `[rows, cols]` ndarray.
320    pub fn read_band_window<T: TiffSample>(
321        &self,
322        band_index: usize,
323        row_off: usize,
324        col_off: usize,
325        rows: usize,
326        cols: usize,
327    ) -> Result<ArrayD<T>> {
328        self.tiff
329            .read_band_window::<T>(
330                self.base_ifd_index,
331                band_index,
332                row_off,
333                col_off,
334                rows,
335                cols,
336            )
337            .map_err(Into::into)
338    }
339
340    /// Decode the base-resolution raster into color-decoded typed pixels.
341    pub fn read_decoded_raster<T: TiffSample>(&self) -> Result<ArrayD<T>> {
342        self.tiff
343            .read_decoded_image::<T>(self.base_ifd_index)
344            .map_err(Into::into)
345    }
346
347    /// Decode a base-resolution pixel window into color-decoded typed pixels.
348    pub fn read_decoded_window<T: TiffSample>(
349        &self,
350        row_off: usize,
351        col_off: usize,
352        rows: usize,
353        cols: usize,
354    ) -> Result<ArrayD<T>> {
355        self.tiff
356            .read_decoded_window::<T>(self.base_ifd_index, row_off, col_off, rows, cols)
357            .map_err(Into::into)
358    }
359
360    /// Decode the base-resolution raster into storage-domain typed samples.
361    ///
362    /// This is an explicit alias for [`Self::read_raster`].
363    pub fn read_raster_samples<T: TiffSample>(&self) -> Result<ArrayD<T>> {
364        self.tiff
365            .read_image_samples::<T>(self.base_ifd_index)
366            .map_err(Into::into)
367    }
368
369    /// Decode a base-resolution pixel window into storage-domain typed samples.
370    ///
371    /// This is an explicit alias for [`Self::read_window`].
372    pub fn read_window_samples<T: TiffSample>(
373        &self,
374        row_off: usize,
375        col_off: usize,
376        rows: usize,
377        cols: usize,
378    ) -> Result<ArrayD<T>> {
379        self.tiff
380            .read_window_samples::<T>(self.base_ifd_index, row_off, col_off, rows, cols)
381            .map_err(Into::into)
382    }
383
384    /// Decode an overview raster into storage-domain typed samples.
385    pub fn read_overview<T: TiffSample>(&self, overview_index: usize) -> Result<ArrayD<T>> {
386        let overview = self
387            .overview_ifds
388            .get(overview_index)
389            .ok_or(Error::OverviewNotFound(overview_index))?;
390        self.tiff
391            .read_image_from_ifd::<T>(&overview.ifd)
392            .map_err(Into::into)
393    }
394
395    /// Decode an overview raster into color-decoded typed pixels.
396    pub fn read_decoded_overview<T: TiffSample>(&self, overview_index: usize) -> Result<ArrayD<T>> {
397        let overview = self
398            .overview_ifds
399            .get(overview_index)
400            .ok_or(Error::OverviewNotFound(overview_index))?;
401        self.tiff
402            .read_decoded_image_from_ifd::<T>(&overview.ifd)
403            .map_err(Into::into)
404    }
405
406    /// Decode an overview raster into storage-domain typed samples.
407    ///
408    /// This is an explicit alias for [`Self::read_overview`].
409    pub fn read_overview_samples<T: TiffSample>(&self, overview_index: usize) -> Result<ArrayD<T>> {
410        let overview = self
411            .overview_ifds
412            .get(overview_index)
413            .ok_or(Error::OverviewNotFound(overview_index))?;
414        self.tiff
415            .read_image_samples_from_ifd::<T>(&overview.ifd)
416            .map_err(Into::into)
417    }
418
419    /// Decode one overview storage-domain band into a typed `[height, width]`
420    /// ndarray.
421    pub fn read_overview_band<T: TiffSample>(
422        &self,
423        overview_index: usize,
424        band_index: usize,
425    ) -> Result<ArrayD<T>> {
426        let overview = self
427            .overview_ifds
428            .get(overview_index)
429            .ok_or(Error::OverviewNotFound(overview_index))?;
430        self.tiff
431            .read_band_from_ifd::<T>(&overview.ifd, band_index)
432            .map_err(Into::into)
433    }
434
435    /// Decode an overview pixel window into storage-domain typed samples.
436    pub fn read_overview_window<T: TiffSample>(
437        &self,
438        overview_index: usize,
439        row_off: usize,
440        col_off: usize,
441        rows: usize,
442        cols: usize,
443    ) -> Result<ArrayD<T>> {
444        let overview = self
445            .overview_ifds
446            .get(overview_index)
447            .ok_or(Error::OverviewNotFound(overview_index))?;
448        self.tiff
449            .read_window_from_ifd::<T>(&overview.ifd, row_off, col_off, rows, cols)
450            .map_err(Into::into)
451    }
452
453    /// Decode an overview pixel window into color-decoded typed pixels.
454    pub fn read_decoded_overview_window<T: TiffSample>(
455        &self,
456        overview_index: usize,
457        row_off: usize,
458        col_off: usize,
459        rows: usize,
460        cols: usize,
461    ) -> Result<ArrayD<T>> {
462        let overview = self
463            .overview_ifds
464            .get(overview_index)
465            .ok_or(Error::OverviewNotFound(overview_index))?;
466        self.tiff
467            .read_decoded_window_from_ifd::<T>(&overview.ifd, row_off, col_off, rows, cols)
468            .map_err(Into::into)
469    }
470
471    /// Decode an overview pixel window from one storage-domain band into a
472    /// typed `[rows, cols]` ndarray.
473    pub fn read_overview_band_window<T: TiffSample>(
474        &self,
475        overview_index: usize,
476        band_index: usize,
477        row_off: usize,
478        col_off: usize,
479        rows: usize,
480        cols: usize,
481    ) -> Result<ArrayD<T>> {
482        let overview = self
483            .overview_ifds
484            .get(overview_index)
485            .ok_or(Error::OverviewNotFound(overview_index))?;
486        self.tiff
487            .read_band_window_from_ifd::<T>(&overview.ifd, band_index, row_off, col_off, rows, cols)
488            .map_err(Into::into)
489    }
490
491    /// Decode an overview pixel window into storage-domain typed samples.
492    ///
493    /// This is an explicit alias for [`Self::read_overview_window`].
494    pub fn read_overview_window_samples<T: TiffSample>(
495        &self,
496        overview_index: usize,
497        row_off: usize,
498        col_off: usize,
499        rows: usize,
500        cols: usize,
501    ) -> Result<ArrayD<T>> {
502        let overview = self
503            .overview_ifds
504            .get(overview_index)
505            .ok_or(Error::OverviewNotFound(overview_index))?;
506        self.tiff
507            .read_window_samples_from_ifd::<T>(&overview.ifd, row_off, col_off, rows, cols)
508            .map_err(Into::into)
509    }
510}
511
512#[cfg(feature = "local")]
513fn is_overview_ifd(base: &tiff_reader::Ifd, candidate: &tiff_reader::Ifd) -> bool {
514    let smaller = candidate.width() < base.width() || candidate.height() < base.height();
515    if !smaller {
516        return false;
517    }
518
519    let same_layout = candidate.samples_per_pixel() == base.samples_per_pixel()
520        && candidate.bits_per_sample() == base.bits_per_sample()
521        && candidate.sample_format() == base.sample_format()
522        && candidate.photometric_interpretation() == base.photometric_interpretation();
523    if !same_layout {
524        return false;
525    }
526
527    has_reduced_resolution_flag(candidate)
528        || (candidate.tag(TAG_NEW_SUBFILE_TYPE).is_none()
529            && candidate.tag(TAG_SUBFILE_TYPE).is_none())
530}
531
532#[cfg(feature = "local")]
533fn collect_overview_ifds(
534    tiff: &TiffFile,
535    base_ifd: &tiff_reader::Ifd,
536    base_ifd_index: usize,
537    metadata_ifd_index: usize,
538) -> Result<Vec<GeoImageIfd>> {
539    let mut overviews: Vec<GeoImageIfd> = tiff
540        .ifds()
541        .iter()
542        .enumerate()
543        .filter(|(index, candidate)| {
544            *index != base_ifd_index
545                && *index != metadata_ifd_index
546                && is_overview_ifd(base_ifd, candidate)
547        })
548        .map(|(index, candidate)| GeoImageIfd {
549            top_level_ifd_index: Some(index),
550            ifd: candidate.clone(),
551        })
552        .collect();
553
554    if let Some(offsets) = base_ifd.sub_ifd_offsets() {
555        let mut seen_offsets = HashSet::new();
556        collect_subifd_overviews(tiff, base_ifd, &offsets, &mut seen_offsets, &mut overviews)?;
557    }
558
559    overviews.sort_by(|lhs, rhs| {
560        rhs.ifd
561            .width()
562            .cmp(&lhs.ifd.width())
563            .then_with(|| rhs.ifd.height().cmp(&lhs.ifd.height()))
564            .then_with(|| lhs.top_level_ifd_index.cmp(&rhs.top_level_ifd_index))
565    });
566
567    Ok(overviews)
568}
569
570#[cfg(feature = "local")]
571fn collect_subifd_overviews(
572    tiff: &TiffFile,
573    base_ifd: &tiff_reader::Ifd,
574    offsets: &[u64],
575    seen_offsets: &mut HashSet<u64>,
576    overviews: &mut Vec<GeoImageIfd>,
577) -> Result<()> {
578    for &offset in offsets {
579        if !seen_offsets.insert(offset) {
580            continue;
581        }
582
583        let candidate = tiff.read_ifd_at_offset(offset)?;
584        if is_overview_ifd(base_ifd, &candidate) {
585            overviews.push(GeoImageIfd {
586                top_level_ifd_index: None,
587                ifd: candidate.clone(),
588            });
589        }
590        if let Some(child_offsets) = candidate.sub_ifd_offsets() {
591            collect_subifd_overviews(tiff, base_ifd, &child_offsets, seen_offsets, overviews)?;
592        }
593    }
594    Ok(())
595}
596
597#[cfg(feature = "local")]
598fn find_metadata_ifd_index(ifds: &[tiff_reader::Ifd]) -> Result<usize> {
599    ifds.iter()
600        .position(|ifd| ifd.tag(TAG_GEO_KEY_DIRECTORY).is_some())
601        .or_else(|| ifds.iter().position(has_model_georeferencing))
602        .ok_or(Error::NotGeoTiff)
603}
604
605#[cfg(feature = "local")]
606fn has_model_georeferencing(ifd: &tiff_reader::Ifd) -> bool {
607    ifd.tag(TAG_MODEL_TRANSFORMATION).is_some()
608        || (ifd.tag(TAG_MODEL_TIEPOINT).is_some() && ifd.tag(TAG_MODEL_PIXEL_SCALE).is_some())
609}
610
611#[cfg(feature = "local")]
612fn find_base_ifd_index(ifds: &[tiff_reader::Ifd], metadata_ifd_index: usize) -> usize {
613    let metadata_ifd = &ifds[metadata_ifd_index];
614    if !has_reduced_resolution_flag(metadata_ifd) {
615        return metadata_ifd_index;
616    }
617
618    ifds.iter()
619        .enumerate()
620        .skip(metadata_ifd_index + 1)
621        .find_map(|(index, ifd)| (!has_reduced_resolution_flag(ifd)).then_some(index))
622        .unwrap_or(metadata_ifd_index)
623}
624
625#[cfg(feature = "local")]
626fn has_reduced_resolution_flag(ifd: &tiff_reader::Ifd) -> bool {
627    ifd.tag(TAG_NEW_SUBFILE_TYPE)
628        .and_then(|tag| tag.value.as_u64())
629        .map(|flags| flags & 0x1 != 0)
630        .or_else(|| {
631            ifd.tag(TAG_SUBFILE_TYPE)
632                .and_then(|tag| tag.value.as_u16())
633                .map(|value| value == 2)
634        })
635        .unwrap_or(false)
636}
637
638#[cfg(feature = "local")]
639fn parse_geokey_directory(ifd: &tiff_reader::Ifd) -> Result<GeoKeyDirectory> {
640    let Some(directory) = ifd
641        .tag(TAG_GEO_KEY_DIRECTORY)
642        .and_then(|tag| match &tag.value {
643            TagValue::Short(values) => Some(values.as_slice()),
644            _ => None,
645        })
646    else {
647        return Ok(GeoKeyDirectory::new());
648    };
649    let double_params = ifd
650        .tag(TAG_GEO_DOUBLE_PARAMS)
651        .and_then(|tag| tag.value.as_f64_vec())
652        .unwrap_or_default();
653    let ascii_params = ifd
654        .tag(TAG_GEO_ASCII_PARAMS)
655        .and_then(|tag| tag.value.as_str())
656        .unwrap_or("");
657    GeoKeyDirectory::parse(directory, &double_params, ascii_params)
658        .ok_or(Error::InvalidGeoKeyDirectory)
659}
660
661#[cfg(feature = "local")]
662fn parse_fixed_len_double_tag<const N: usize>(value: Option<&TagValue>) -> Option<[f64; N]> {
663    let values = value.and_then(TagValue::as_f64_vec)?;
664    if values.len() < N {
665        return None;
666    }
667    let mut out = [0.0; N];
668    out.copy_from_slice(&values[..N]);
669    Some(out)
670}
671
672#[cfg(feature = "local")]
673fn parse_tiepoints(ifd: &tiff_reader::Ifd) -> Vec<[f64; 6]> {
674    let values = ifd
675        .tag(TAG_MODEL_TIEPOINT)
676        .and_then(|tag| tag.value.as_f64_vec())
677        .unwrap_or_default();
678    values
679        .chunks_exact(6)
680        .map(|chunk| [chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5]])
681        .collect()
682}
683
684#[cfg(feature = "local")]
685fn parse_nodata(ifd: &tiff_reader::Ifd) -> Option<String> {
686    ifd.tag(TAG_GDAL_NODATA)
687        .and_then(|tag| tag.value.as_str())
688        .map(ToOwned::to_owned)
689}
690
691#[cfg(test)]
692#[cfg(feature = "local")]
693mod tests {
694    use std::fs;
695    use std::path::PathBuf;
696    use std::time::{SystemTime, UNIX_EPOCH};
697
698    use super::GeoTiffFile;
699
700    #[derive(Clone)]
701    struct TestIfdSpec {
702        entries: Vec<(u16, u16, u32, Vec<u8>)>,
703        image_data: Vec<u8>,
704    }
705
706    fn le_u16(value: u16) -> [u8; 2] {
707        value.to_le_bytes()
708    }
709
710    fn le_u32(value: u32) -> [u8; 4] {
711        value.to_le_bytes()
712    }
713
714    fn le_f64(value: f64) -> [u8; 8] {
715        value.to_le_bytes()
716    }
717
718    fn temp_geotiff_path(test_name: &str) -> PathBuf {
719        let nanos = SystemTime::now()
720            .duration_since(UNIX_EPOCH)
721            .unwrap()
722            .as_nanos();
723        std::env::temp_dir().join(format!(
724            "geotiff-rust-{test_name}-{}-{nanos}.tif",
725            std::process::id()
726        ))
727    }
728
729    fn inline_short(value: u16) -> Vec<u8> {
730        let mut bytes = [0u8; 4];
731        bytes[..2].copy_from_slice(&le_u16(value));
732        bytes.to_vec()
733    }
734
735    #[allow(clippy::too_many_arguments)]
736    fn build_lerc2_header_v2(
737        width: u32,
738        height: u32,
739        valid_pixel_count: u32,
740        image_type: i32,
741        max_z_error: f64,
742        z_min: f64,
743        z_max: f64,
744        payload_len: usize,
745    ) -> Vec<u8> {
746        let blob_size = 58 + 4 + payload_len;
747        let mut bytes = Vec::with_capacity(blob_size);
748        bytes.extend_from_slice(b"Lerc2 ");
749        bytes.extend_from_slice(&2i32.to_le_bytes());
750        bytes.extend_from_slice(&height.to_le_bytes());
751        bytes.extend_from_slice(&width.to_le_bytes());
752        bytes.extend_from_slice(&valid_pixel_count.to_le_bytes());
753        bytes.extend_from_slice(&8i32.to_le_bytes());
754        bytes.extend_from_slice(&(blob_size as i32).to_le_bytes());
755        bytes.extend_from_slice(&image_type.to_le_bytes());
756        bytes.extend_from_slice(&max_z_error.to_le_bytes());
757        bytes.extend_from_slice(&z_min.to_le_bytes());
758        bytes.extend_from_slice(&z_max.to_le_bytes());
759        bytes
760    }
761
762    fn build_classic_tiff(ifds: &[TestIfdSpec]) -> Vec<u8> {
763        let mut ifd_offsets = Vec::with_capacity(ifds.len());
764        let mut cursor = 8usize;
765        for ifd in ifds {
766            ifd_offsets.push(cursor as u32);
767            let deferred_len: usize = ifd
768                .entries
769                .iter()
770                .filter(|(tag, _, _, value)| *tag != 273 && value.len() > 4)
771                .map(|(_, _, _, value)| value.len())
772                .sum();
773            cursor += 2 + ifd.entries.len() * 12 + 4 + ifd.image_data.len() + deferred_len;
774        }
775
776        let mut bytes = Vec::with_capacity(cursor);
777        bytes.extend_from_slice(b"II");
778        bytes.extend_from_slice(&le_u16(42));
779        bytes.extend_from_slice(&le_u32(ifd_offsets.first().copied().unwrap_or(0)));
780
781        for (ifd_index, ifd) in ifds.iter().enumerate() {
782            let ifd_offset = ifd_offsets[ifd_index] as usize;
783            debug_assert_eq!(bytes.len(), ifd_offset);
784
785            let ifd_size = 2 + ifd.entries.len() * 12 + 4;
786            let mut next_data_offset = ifd_offset + ifd_size;
787            let image_offset = next_data_offset as u32;
788            next_data_offset += ifd.image_data.len();
789
790            bytes.extend_from_slice(&le_u16(ifd.entries.len() as u16));
791            let mut deferred = Vec::new();
792            for (tag, ty, count, value) in &ifd.entries {
793                bytes.extend_from_slice(&le_u16(*tag));
794                bytes.extend_from_slice(&le_u16(*ty));
795                bytes.extend_from_slice(&le_u32(*count));
796                if *tag == 273 {
797                    bytes.extend_from_slice(&le_u32(image_offset));
798                } else if value.len() <= 4 {
799                    let mut inline = [0u8; 4];
800                    inline[..value.len()].copy_from_slice(value);
801                    bytes.extend_from_slice(&inline);
802                } else {
803                    bytes.extend_from_slice(&le_u32(next_data_offset as u32));
804                    next_data_offset += value.len();
805                    deferred.push(value.clone());
806                }
807            }
808
809            let next_ifd_offset = ifd_offsets.get(ifd_index + 1).copied().unwrap_or(0);
810            bytes.extend_from_slice(&le_u32(next_ifd_offset));
811            bytes.extend_from_slice(&ifd.image_data);
812            for value in deferred {
813                bytes.extend_from_slice(&value);
814            }
815            debug_assert_eq!(bytes.len(), next_data_offset);
816        }
817
818        bytes
819    }
820
821    fn build_simple_geotiff(pixel_is_point: bool) -> Vec<u8> {
822        let image_data = vec![10u8, 20, 30, 40];
823        let tiepoints = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0];
824        let scales = [2.0, 2.0, 0.0];
825        let geo_keys = if pixel_is_point {
826            vec![
827                1, 1, 0, 3, // header
828                1024, 0, 1, 2, // model type = Geographic
829                1025, 0, 1, 2, // raster type = PixelIsPoint
830                2048, 0, 1, 4326, // EPSG:4326
831            ]
832        } else {
833            vec![
834                1, 1, 0, 2, // header
835                1024, 0, 1, 2, // model type = Geographic
836                2048, 0, 1, 4326, // EPSG:4326
837            ]
838        };
839        let nodata = b"-9999\0".to_vec();
840
841        build_classic_tiff(&[TestIfdSpec {
842            image_data,
843            entries: vec![
844                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
845                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
846                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
847                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
848                (273u16, 4u16, 1u32, vec![]),
849                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
850                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
851                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
852                (
853                    33550u16,
854                    12u16,
855                    3u32,
856                    scales.iter().flat_map(|value| le_f64(*value)).collect(),
857                ),
858                (
859                    33922u16,
860                    12u16,
861                    6u32,
862                    tiepoints.iter().flat_map(|value| le_f64(*value)).collect(),
863                ),
864                (
865                    34735u16,
866                    3u16,
867                    geo_keys.len() as u32,
868                    geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
869                ),
870                (42113u16, 2u16, nodata.len() as u32, nodata),
871            ],
872        }])
873    }
874
875    fn build_simple_lerc_geotiff() -> Vec<u8> {
876        let tiepoints = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0];
877        let scales = [2.0, 2.0, 0.0];
878        let geo_keys = vec![
879            1, 1, 0, 2, // header
880            1024, 0, 1, 2, // model type = Geographic
881            2048, 0, 1, 4326, // EPSG:4326
882        ];
883
884        let mut image_data = build_lerc2_header_v2(2, 2, 4, 6, 0.0, 1.0, 4.0, 1 + 16);
885        image_data.extend_from_slice(&0u32.to_le_bytes());
886        image_data.push(1);
887        for value in [1.0f32, 2.0, 3.0, 4.0] {
888            image_data.extend_from_slice(&value.to_le_bytes());
889        }
890        let image_len = image_data.len() as u32;
891
892        build_classic_tiff(&[TestIfdSpec {
893            image_data,
894            entries: vec![
895                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
896                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
897                (258u16, 3u16, 1u32, inline_short(32)),
898                (259u16, 3u16, 1u32, inline_short(34887)),
899                (273u16, 4u16, 1u32, vec![]),
900                (277u16, 3u16, 1u32, inline_short(1)),
901                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
902                (279u16, 4u16, 1u32, le_u32(image_len).to_vec()),
903                (339u16, 3u16, 1u32, inline_short(3)),
904                (
905                    33550u16,
906                    12u16,
907                    3u32,
908                    scales.iter().flat_map(|value| le_f64(*value)).collect(),
909                ),
910                (
911                    33922u16,
912                    12u16,
913                    6u32,
914                    tiepoints.iter().flat_map(|value| le_f64(*value)).collect(),
915                ),
916                (
917                    34735u16,
918                    3u16,
919                    geo_keys.len() as u32,
920                    geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
921                ),
922            ],
923        }])
924    }
925
926    fn overwrite_classic_inline_long_tag(bytes: &mut [u8], tag_code: u16, value: u32) {
927        let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
928        let mut offset = 10usize;
929        for _ in 0..entry_count {
930            let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
931            if code == tag_code {
932                bytes[offset + 8..offset + 12].copy_from_slice(&le_u32(value));
933                return;
934            }
935            offset += 12;
936        }
937        panic!("tag {tag_code} not found in classic TIFF");
938    }
939
940    fn overwrite_first_ifd_next_pointer(bytes: &mut [u8], value: u32) {
941        let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
942        let pointer_offset = 10 + entry_count * 12;
943        bytes[pointer_offset..pointer_offset + 4].copy_from_slice(&le_u32(value));
944    }
945
946    fn overwrite_classic_inline_long_tag_at(
947        bytes: &mut [u8],
948        ifd_offset: usize,
949        tag_code: u16,
950        value: u32,
951    ) {
952        let entry_count = u16::from_le_bytes([bytes[ifd_offset], bytes[ifd_offset + 1]]) as usize;
953        let mut offset = ifd_offset + 2;
954        for _ in 0..entry_count {
955            let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
956            if code == tag_code {
957                bytes[offset + 8..offset + 12].copy_from_slice(&le_u32(value));
958                return;
959            }
960            offset += 12;
961        }
962        panic!("tag {tag_code} not found in classic TIFF at offset {ifd_offset}");
963    }
964
965    fn first_ifd_next_pointer(bytes: &[u8]) -> u32 {
966        let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
967        let pointer_offset = 10 + entry_count * 12;
968        u32::from_le_bytes([
969            bytes[pointer_offset],
970            bytes[pointer_offset + 1],
971            bytes[pointer_offset + 2],
972            bytes[pointer_offset + 3],
973        ])
974    }
975
976    fn ifd_next_pointer(bytes: &[u8], ifd_offset: usize) -> u32 {
977        let entry_count = u16::from_le_bytes([bytes[ifd_offset], bytes[ifd_offset + 1]]) as usize;
978        let pointer_offset = ifd_offset + 2 + entry_count * 12;
979        u32::from_le_bytes([
980            bytes[pointer_offset],
981            bytes[pointer_offset + 1],
982            bytes[pointer_offset + 2],
983            bytes[pointer_offset + 3],
984        ])
985    }
986
987    fn build_geotiff_with_overview() -> Vec<u8> {
988        let base = TestIfdSpec {
989            image_data: vec![10u8, 20, 30, 40],
990            entries: vec![
991                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
992                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
993                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
994                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
995                (273u16, 4u16, 1u32, vec![]),
996                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
997                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
998                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
999                (
1000                    33550u16,
1001                    12u16,
1002                    3u32,
1003                    [2.0, 2.0, 0.0]
1004                        .iter()
1005                        .flat_map(|value| le_f64(*value))
1006                        .collect(),
1007                ),
1008                (
1009                    33922u16,
1010                    12u16,
1011                    6u32,
1012                    [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1013                        .iter()
1014                        .flat_map(|value| le_f64(*value))
1015                        .collect(),
1016                ),
1017                (
1018                    34735u16,
1019                    3u16,
1020                    12u32,
1021                    [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326]
1022                        .iter()
1023                        .flat_map(|value| le_u16(*value))
1024                        .collect(),
1025                ),
1026            ],
1027        };
1028        let overview = TestIfdSpec {
1029            image_data: vec![99u8],
1030            entries: vec![
1031                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1032                (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1033                (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1034                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1035                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1036                (273u16, 4u16, 1u32, vec![]),
1037                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1038                (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1039                (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1040            ],
1041        };
1042
1043        build_classic_tiff(&[base, overview])
1044    }
1045
1046    fn build_geotiff_with_subifd_overview() -> Vec<u8> {
1047        let base = TestIfdSpec {
1048            image_data: vec![10u8, 20, 30, 40],
1049            entries: vec![
1050                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
1051                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
1052                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1053                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1054                (273u16, 4u16, 1u32, vec![]),
1055                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1056                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
1057                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
1058                (330u16, 4u16, 1u32, le_u32(0).to_vec()),
1059                (
1060                    33550u16,
1061                    12u16,
1062                    3u32,
1063                    [2.0, 2.0, 0.0]
1064                        .iter()
1065                        .flat_map(|value| le_f64(*value))
1066                        .collect(),
1067                ),
1068                (
1069                    33922u16,
1070                    12u16,
1071                    6u32,
1072                    [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1073                        .iter()
1074                        .flat_map(|value| le_f64(*value))
1075                        .collect(),
1076                ),
1077                (
1078                    34735u16,
1079                    3u16,
1080                    12u32,
1081                    [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326]
1082                        .iter()
1083                        .flat_map(|value| le_u16(*value))
1084                        .collect(),
1085                ),
1086            ],
1087        };
1088        let overview = TestIfdSpec {
1089            image_data: vec![99u8],
1090            entries: vec![
1091                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1092                (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1093                (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1094                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1095                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1096                (273u16, 4u16, 1u32, vec![]),
1097                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1098                (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1099                (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1100            ],
1101        };
1102
1103        let mut bytes = build_classic_tiff(&[base, overview]);
1104        let child_ifd_offset = first_ifd_next_pointer(&bytes);
1105        overwrite_classic_inline_long_tag(&mut bytes, 330, child_ifd_offset);
1106        overwrite_first_ifd_next_pointer(&mut bytes, 0);
1107        bytes
1108    }
1109
1110    fn build_geotiff_with_nested_subifd_overviews() -> Vec<u8> {
1111        let base = TestIfdSpec {
1112            image_data: (1u8..=16).collect(),
1113            entries: vec![
1114                (256u16, 4u16, 1u32, le_u32(4).to_vec()),
1115                (257u16, 4u16, 1u32, le_u32(4).to_vec()),
1116                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1117                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1118                (273u16, 4u16, 1u32, vec![]),
1119                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1120                (278u16, 4u16, 1u32, le_u32(4).to_vec()),
1121                (279u16, 4u16, 1u32, le_u32(16).to_vec()),
1122                (330u16, 4u16, 1u32, le_u32(0).to_vec()),
1123                (
1124                    33550u16,
1125                    12u16,
1126                    3u32,
1127                    [2.0, 2.0, 0.0]
1128                        .iter()
1129                        .flat_map(|value| le_f64(*value))
1130                        .collect(),
1131                ),
1132                (
1133                    33922u16,
1134                    12u16,
1135                    6u32,
1136                    [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1137                        .iter()
1138                        .flat_map(|value| le_f64(*value))
1139                        .collect(),
1140                ),
1141                (
1142                    34735u16,
1143                    3u16,
1144                    12u32,
1145                    [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326]
1146                        .iter()
1147                        .flat_map(|value| le_u16(*value))
1148                        .collect(),
1149                ),
1150            ],
1151        };
1152        let overview = TestIfdSpec {
1153            image_data: vec![50u8, 60, 70, 80],
1154            entries: vec![
1155                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1156                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
1157                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
1158                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1159                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1160                (273u16, 4u16, 1u32, vec![]),
1161                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1162                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
1163                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
1164                (330u16, 4u16, 1u32, le_u32(0).to_vec()),
1165            ],
1166        };
1167        let nested = TestIfdSpec {
1168            image_data: vec![99u8],
1169            entries: vec![
1170                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1171                (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1172                (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1173                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1174                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1175                (273u16, 4u16, 1u32, vec![]),
1176                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1177                (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1178                (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1179            ],
1180        };
1181
1182        let mut bytes = build_classic_tiff(&[base, overview, nested]);
1183        let child_ifd_offset = first_ifd_next_pointer(&bytes);
1184        let grandchild_ifd_offset = ifd_next_pointer(&bytes, child_ifd_offset as usize);
1185        overwrite_classic_inline_long_tag(&mut bytes, 330, child_ifd_offset);
1186        overwrite_classic_inline_long_tag_at(
1187            &mut bytes,
1188            child_ifd_offset as usize,
1189            330,
1190            grandchild_ifd_offset,
1191        );
1192        overwrite_first_ifd_next_pointer(&mut bytes, 0);
1193        bytes
1194    }
1195
1196    fn build_cog_like_geotiff_with_ghost_ifd() -> Vec<u8> {
1197        let geo_keys = [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326];
1198        let ghost = TestIfdSpec {
1199            image_data: vec![0u8],
1200            entries: vec![
1201                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1202                (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1203                (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1204                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1205                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1206                (273u16, 4u16, 1u32, vec![]),
1207                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1208                (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1209                (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1210                (
1211                    33550u16,
1212                    12u16,
1213                    3u32,
1214                    [2.0, 2.0, 0.0]
1215                        .iter()
1216                        .flat_map(|value| le_f64(*value))
1217                        .collect(),
1218                ),
1219                (
1220                    33922u16,
1221                    12u16,
1222                    6u32,
1223                    [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1224                        .iter()
1225                        .flat_map(|value| le_f64(*value))
1226                        .collect(),
1227                ),
1228                (
1229                    34735u16,
1230                    3u16,
1231                    geo_keys.len() as u32,
1232                    geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
1233                ),
1234            ],
1235        };
1236        let overview = TestIfdSpec {
1237            image_data: vec![50u8, 60, 70, 80],
1238            entries: vec![
1239                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1240                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
1241                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
1242                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1243                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1244                (273u16, 4u16, 1u32, vec![]),
1245                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1246                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
1247                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
1248            ],
1249        };
1250        let base = TestIfdSpec {
1251            image_data: (1u8..=16).collect(),
1252            entries: vec![
1253                (256u16, 4u16, 1u32, le_u32(4).to_vec()),
1254                (257u16, 4u16, 1u32, le_u32(4).to_vec()),
1255                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1256                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1257                (273u16, 4u16, 1u32, vec![]),
1258                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1259                (278u16, 4u16, 1u32, le_u32(4).to_vec()),
1260                (279u16, 4u16, 1u32, le_u32(16).to_vec()),
1261            ],
1262        };
1263
1264        build_classic_tiff(&[ghost, overview, base])
1265    }
1266
1267    #[test]
1268    fn parses_geotiff_metadata_and_reads_raster() {
1269        let file = GeoTiffFile::from_bytes(build_simple_geotiff(false)).unwrap();
1270        assert_eq!(file.epsg(), Some(4326));
1271        assert_eq!(file.width(), 2);
1272        assert_eq!(file.height(), 2);
1273        assert_eq!(file.band_count(), 1);
1274        assert_eq!(file.nodata(), Some("-9999"));
1275        assert_eq!(file.geo_bounds(), Some([100.0, 196.0, 104.0, 200.0]));
1276
1277        let raster = file.read_raster::<u8>().unwrap();
1278        assert_eq!(raster.shape(), &[2, 2]);
1279        let (values, offset) = raster.into_raw_vec_and_offset();
1280        assert_eq!(offset, Some(0));
1281        assert_eq!(values, vec![10, 20, 30, 40]);
1282    }
1283
1284    #[test]
1285    fn open_mmap_reads_geotiff_from_file() {
1286        let bytes = build_simple_geotiff(false);
1287        let path = temp_geotiff_path("open_mmap_reads_geotiff_from_file");
1288        fs::write(&path, &bytes).unwrap();
1289
1290        let file = unsafe { GeoTiffFile::open_mmap(&path).unwrap() };
1291        assert_eq!(file.epsg(), Some(4326));
1292        assert_eq!(file.tiff().raw_bytes(), Some(bytes.as_slice()));
1293
1294        let raster = file.read_raster::<u8>().unwrap();
1295        let (values, offset) = raster.into_raw_vec_and_offset();
1296        assert_eq!(offset, Some(0));
1297        assert_eq!(values, vec![10, 20, 30, 40]);
1298
1299        drop(file);
1300        let _ = fs::remove_file(path);
1301    }
1302
1303    #[test]
1304    fn parses_geotiff_metadata_and_reads_lerc_raster() {
1305        let file = GeoTiffFile::from_bytes(build_simple_lerc_geotiff()).unwrap();
1306        assert_eq!(file.epsg(), Some(4326));
1307        assert_eq!(file.width(), 2);
1308        assert_eq!(file.height(), 2);
1309
1310        let raster = file.read_raster::<f32>().unwrap();
1311        assert_eq!(raster.shape(), &[2, 2]);
1312        let (values, offset) = raster.into_raw_vec_and_offset();
1313        assert_eq!(offset, Some(0));
1314        assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0]);
1315    }
1316
1317    #[test]
1318    fn pixel_is_point_metadata_shifts_bounds_to_outer_edges() {
1319        let file = GeoTiffFile::from_bytes(build_simple_geotiff(true)).unwrap();
1320        assert_eq!(file.geo_bounds(), Some([99.0, 197.0, 103.0, 201.0]));
1321
1322        let transform = file.transform().unwrap();
1323        let (center_x, center_y) = transform.pixel_to_geo(0.5, 0.5);
1324        assert_eq!((center_x, center_y), (100.0, 200.0));
1325    }
1326
1327    #[test]
1328    fn discovers_reduced_resolution_overviews() {
1329        let file = GeoTiffFile::from_bytes(build_geotiff_with_overview()).unwrap();
1330        assert_eq!(file.overview_count(), 1);
1331        assert_eq!(file.overview_ifd_index(0).unwrap(), 1);
1332
1333        let overview = file.read_overview::<u8>(0).unwrap();
1334        assert_eq!(overview.shape(), &[1, 1]);
1335        let (values, offset) = overview.into_raw_vec_and_offset();
1336        assert_eq!(offset, Some(0));
1337        assert_eq!(values, vec![99]);
1338    }
1339
1340    #[test]
1341    fn discovers_and_reads_subifd_overviews() {
1342        let file = GeoTiffFile::from_bytes(build_geotiff_with_subifd_overview()).unwrap();
1343        assert_eq!(file.overview_count(), 1);
1344        assert!(matches!(
1345            file.overview_ifd_index(0).unwrap_err(),
1346            crate::error::Error::OverviewHasNoTopLevelIfdIndex(0)
1347        ));
1348        assert_eq!(file.overview_ifd(0).unwrap().width(), 1);
1349        assert_eq!(file.overview_ifd(0).unwrap().height(), 1);
1350
1351        let overview = file.read_overview::<u8>(0).unwrap();
1352        assert_eq!(overview.shape(), &[1, 1]);
1353        let (values, offset) = overview.into_raw_vec_and_offset();
1354        assert_eq!(offset, Some(0));
1355        assert_eq!(values, vec![99]);
1356    }
1357
1358    #[test]
1359    fn discovers_nested_subifd_overviews() {
1360        let file = GeoTiffFile::from_bytes(build_geotiff_with_nested_subifd_overviews()).unwrap();
1361        assert_eq!(file.overview_count(), 2);
1362        assert_eq!(file.overview_ifd(0).unwrap().width(), 2);
1363        assert_eq!(file.overview_ifd(1).unwrap().width(), 1);
1364        assert!(matches!(
1365            file.overview_ifd_index(0).unwrap_err(),
1366            crate::error::Error::OverviewHasNoTopLevelIfdIndex(0)
1367        ));
1368        assert!(matches!(
1369            file.overview_ifd_index(1).unwrap_err(),
1370            crate::error::Error::OverviewHasNoTopLevelIfdIndex(1)
1371        ));
1372
1373        let first = file.read_overview::<u8>(0).unwrap();
1374        assert_eq!(first.shape(), &[2, 2]);
1375        let second = file.read_overview::<u8>(1).unwrap();
1376        assert_eq!(second.shape(), &[1, 1]);
1377        assert_eq!(second[[0, 0]], 99);
1378    }
1379
1380    #[test]
1381    fn reads_base_raster_window() {
1382        let file = GeoTiffFile::from_bytes(build_simple_geotiff(false)).unwrap();
1383        let window = file.read_window::<u8>(1, 0, 1, 2).unwrap();
1384        assert_eq!(window.shape(), &[1, 2]);
1385        let (values, offset) = window.into_raw_vec_and_offset();
1386        assert_eq!(offset, Some(0));
1387        assert_eq!(values, vec![30, 40]);
1388    }
1389
1390    #[test]
1391    fn reads_overview_window() {
1392        let file = GeoTiffFile::from_bytes(build_geotiff_with_overview()).unwrap();
1393        let window = file.read_overview_window::<u8>(0, 0, 0, 1, 1).unwrap();
1394        assert_eq!(window.shape(), &[1, 1]);
1395        let (values, offset) = window.into_raw_vec_and_offset();
1396        assert_eq!(offset, Some(0));
1397        assert_eq!(values, vec![99]);
1398    }
1399
1400    #[test]
1401    fn prefers_non_ghost_base_ifd_for_cog_like_layouts() {
1402        let file = GeoTiffFile::from_bytes(build_cog_like_geotiff_with_ghost_ifd()).unwrap();
1403        assert_eq!(file.base_ifd_index(), 2);
1404        assert_eq!(file.width(), 4);
1405        assert_eq!(file.height(), 4);
1406        assert_eq!(file.geo_bounds(), Some([100.0, 192.0, 108.0, 200.0]));
1407        assert_eq!(file.overview_count(), 1);
1408        assert_eq!(file.overview_ifd_index(0).unwrap(), 1);
1409
1410        let base = file.read_raster::<u8>().unwrap();
1411        assert_eq!(base.shape(), &[4, 4]);
1412        let (values, offset) = base.into_raw_vec_and_offset();
1413        assert_eq!(offset, Some(0));
1414        assert_eq!(values, (1u8..=16).collect::<Vec<_>>());
1415
1416        let overview = file.read_overview::<u8>(0).unwrap();
1417        assert_eq!(overview.shape(), &[2, 2]);
1418        let (values, offset) = overview.into_raw_vec_and_offset();
1419        assert_eq!(offset, Some(0));
1420        assert_eq!(values, vec![50, 60, 70, 80]);
1421    }
1422
1423    #[test]
1424    fn rejects_zero_rows_per_strip_without_panicking() {
1425        let mut bytes = build_simple_geotiff(false);
1426        overwrite_classic_inline_long_tag(&mut bytes, 278, 0);
1427
1428        let file = GeoTiffFile::from_bytes(bytes).unwrap();
1429        assert_eq!(file.epsg(), Some(4326));
1430
1431        let error = file.tiff().read_image_bytes(0).unwrap_err();
1432        assert!(error.to_string().contains("RowsPerStrip"));
1433    }
1434}