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