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
35#[cfg(feature = "cog-async")]
36pub mod cog_async;
37
38#[cfg(any(feature = "cog", feature = "cog-async"))]
39mod http_range;
40
41#[cfg(all(test, any(feature = "cog", feature = "cog-async")))]
42mod http_test_support;
43
44pub use error::{Error, Result};
45
46#[cfg(feature = "local")]
47use crs::CrsInfo;
48#[cfg(feature = "local")]
49use geokeys::GeoKeyDirectory;
50#[cfg(feature = "local")]
51use ndarray::ArrayD;
52#[cfg(feature = "local")]
53use std::collections::HashSet;
54#[cfg(feature = "local")]
55use std::path::Path;
56#[cfg(feature = "local")]
57use tiff_reader::{OpenOptions as TiffOpenOptions, TagValue, TiffFile, TiffSample};
58#[cfg(feature = "local")]
59use transform::GeoTransform;
60
61#[cfg(feature = "local")]
62use geotiff_core::tags::{
63    TAG_GDAL_NODATA, TAG_GEO_ASCII_PARAMS, TAG_GEO_DOUBLE_PARAMS, TAG_GEO_KEY_DIRECTORY,
64    TAG_MODEL_PIXEL_SCALE, TAG_MODEL_TIEPOINT, TAG_MODEL_TRANSFORMATION, TAG_NEW_SUBFILE_TYPE,
65    TAG_SUBFILE_TYPE,
66};
67
68#[cfg(feature = "local")]
69const MAX_SUBIFD_OVERVIEW_NODES: usize = 1024;
70#[cfg(feature = "local")]
71const MAX_SUBIFD_OVERVIEW_DEPTH: usize = 32;
72
73/// A GeoTIFF file handle with geospatial metadata.
74#[cfg(feature = "local")]
75pub struct GeoTiffFile {
76    tiff: TiffFile,
77    geo_metadata: GeoMetadata,
78    crs: CrsInfo,
79    geokeys: GeoKeyDirectory,
80    transform: Option<GeoTransform>,
81    base_ifd_index: usize,
82    overview_ifds: Vec<GeoImageIfd>,
83}
84
85#[cfg(feature = "local")]
86#[derive(Debug, Clone)]
87struct GeoImageIfd {
88    top_level_ifd_index: Option<usize>,
89    ifd: tiff_reader::Ifd,
90}
91
92#[cfg(feature = "local")]
93pub use tiff_reader::OpenOptions as GeoTiffOpenOptions;
94
95pub use geotiff_core::GeoMetadata;
96
97#[cfg(feature = "local")]
98impl GeoTiffFile {
99    /// Open a GeoTIFF file from disk.
100    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
101        Self::open_with_options(path, TiffOpenOptions::default())
102    }
103
104    /// Open a GeoTIFF file from disk with explicit TIFF decoder options.
105    pub fn open_with_options<P: AsRef<Path>>(path: P, options: GeoTiffOpenOptions) -> Result<Self> {
106        let tiff = TiffFile::open_with_options(path, options)?;
107        Self::from_tiff(tiff)
108    }
109
110    /// Open a GeoTIFF file from disk using memory-mapped I/O.
111    ///
112    /// # Safety
113    ///
114    /// The caller must guarantee that the mapped file is not mutated or
115    /// truncated while the returned `GeoTiffFile` is alive. This includes
116    /// writes through other file handles and writes from other processes.
117    pub unsafe fn open_mmap<P: AsRef<Path>>(path: P) -> Result<Self> {
118        unsafe { Self::open_mmap_with_options(path, TiffOpenOptions::default()) }
119    }
120
121    /// Open a GeoTIFF file from disk using memory-mapped I/O with explicit TIFF decoder options.
122    ///
123    /// # Safety
124    ///
125    /// The caller must guarantee that the mapped file is not mutated or
126    /// truncated while the returned `GeoTiffFile` is alive. This includes
127    /// writes through other file handles and writes from other processes.
128    pub unsafe fn open_mmap_with_options<P: AsRef<Path>>(
129        path: P,
130        options: GeoTiffOpenOptions,
131    ) -> Result<Self> {
132        let tiff = unsafe { TiffFile::open_mmap_with_options(path, options)? };
133        Self::from_tiff(tiff)
134    }
135
136    /// Open a GeoTIFF from an owned byte buffer.
137    pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
138        Self::from_bytes_with_options(data, TiffOpenOptions::default())
139    }
140
141    /// Open a GeoTIFF from bytes with explicit TIFF decoder options.
142    pub fn from_bytes_with_options(data: Vec<u8>, options: GeoTiffOpenOptions) -> Result<Self> {
143        let tiff = TiffFile::from_bytes_with_options(data, options)?;
144        Self::from_tiff(tiff)
145    }
146
147    pub(crate) fn from_tiff(tiff: TiffFile) -> Result<Self> {
148        let metadata_ifd_index = find_metadata_ifd_index(tiff.ifds())?;
149        let metadata_ifd = tiff.ifd(metadata_ifd_index)?;
150        let geokeys = parse_geokey_directory(metadata_ifd)?;
151        let crs = CrsInfo::from_geokeys(&geokeys);
152        let epsg = crs.epsg();
153        let tiepoints = parse_tiepoints(metadata_ifd)?;
154        let pixel_scale = parse_fixed_len_double_tag::<3>(metadata_ifd, TAG_MODEL_PIXEL_SCALE)?;
155        let transformation =
156            parse_fixed_len_double_tag::<16>(metadata_ifd, TAG_MODEL_TRANSFORMATION)?;
157        validate_model_georeferencing(&tiepoints, pixel_scale.as_ref(), transformation.as_ref())?;
158        let transform = transformation
159            .as_ref()
160            .map(GeoTransform::from_transformation_matrix)
161            .or_else(|| {
162                let tiepoint = tiepoints.first()?;
163                let scale = pixel_scale.as_ref()?;
164                Some(GeoTransform::from_tiepoint_and_scale_with_raster_type(
165                    tiepoint,
166                    scale,
167                    crs.raster_type_enum(),
168                ))
169            });
170        let base_ifd_index = find_base_ifd_index(tiff.ifds(), metadata_ifd_index);
171        let base_ifd = tiff.ifd(base_ifd_index)?;
172        let overview_ifds =
173            collect_overview_ifds(&tiff, base_ifd, base_ifd_index, metadata_ifd_index)?;
174        let geo_bounds = transform
175            .as_ref()
176            .map(|gt| gt.bounds(base_ifd.width(), base_ifd.height()));
177
178        let geo_metadata = GeoMetadata {
179            epsg,
180            tiepoints,
181            pixel_scale,
182            transformation,
183            nodata: parse_nodata(metadata_ifd)?,
184            band_count: base_ifd.samples_per_pixel() as u32,
185            width: base_ifd.width(),
186            height: base_ifd.height(),
187            geo_bounds,
188        };
189
190        Ok(Self {
191            tiff,
192            geo_metadata,
193            crs,
194            geokeys,
195            transform,
196            base_ifd_index,
197            overview_ifds,
198        })
199    }
200
201    /// Returns the underlying TIFF file.
202    pub fn tiff(&self) -> &TiffFile {
203        &self.tiff
204    }
205
206    /// Returns the parsed GeoTIFF metadata.
207    pub fn metadata(&self) -> &GeoMetadata {
208        &self.geo_metadata
209    }
210
211    /// Returns the EPSG code of the coordinate reference system, if present.
212    pub fn epsg(&self) -> Option<u32> {
213        self.geo_metadata.epsg
214    }
215
216    /// Returns the extracted CRS information.
217    pub fn crs(&self) -> &CrsInfo {
218        &self.crs
219    }
220
221    /// Returns the parsed GeoKey directory.
222    pub fn geokeys(&self) -> &GeoKeyDirectory {
223        &self.geokeys
224    }
225
226    /// Returns the affine transform, if present.
227    pub fn transform(&self) -> Option<&GeoTransform> {
228        self.transform.as_ref()
229    }
230
231    /// Returns the geographic bounds as `(min_x, min_y, max_x, max_y)`.
232    pub fn geo_bounds(&self) -> Option<[f64; 4]> {
233        self.geo_metadata.geo_bounds
234    }
235
236    /// Convert a pixel coordinate to map coordinates.
237    pub fn pixel_to_geo(&self, col: f64, row: f64) -> Option<(f64, f64)> {
238        self.transform
239            .map(|transform| transform.pixel_to_geo(col, row))
240    }
241
242    /// Convert map coordinates to pixel coordinates.
243    pub fn geo_to_pixel(&self, x: f64, y: f64) -> Option<(f64, f64)> {
244        self.transform
245            .and_then(|transform| transform.geo_to_pixel(x, y))
246    }
247
248    /// Returns the image width in pixels.
249    pub fn width(&self) -> u32 {
250        self.geo_metadata.width
251    }
252
253    /// Returns the image height in pixels.
254    pub fn height(&self) -> u32 {
255        self.geo_metadata.height
256    }
257
258    /// Returns the number of bands.
259    pub fn band_count(&self) -> u32 {
260        self.geo_metadata.band_count
261    }
262
263    /// Returns the nodata value, if set.
264    pub fn nodata(&self) -> Option<&str> {
265        self.geo_metadata.nodata.as_deref()
266    }
267
268    /// Returns the number of internal overview IFDs.
269    pub fn overview_count(&self) -> usize {
270        self.overview_ifds.len()
271    }
272
273    /// Returns the top-level TIFF IFD index of the requested overview.
274    ///
275    /// Overviews stored in SubIFDs return
276    /// `Error::OverviewHasNoTopLevelIfdIndex`.
277    pub fn overview_ifd_index(&self, overview_index: usize) -> Result<usize> {
278        self.overview_ifds
279            .get(overview_index)
280            .ok_or(Error::OverviewNotFound(overview_index))?
281            .top_level_ifd_index
282            .ok_or(Error::OverviewHasNoTopLevelIfdIndex(overview_index))
283    }
284
285    /// Returns the parsed TIFF IFD metadata for the requested overview.
286    pub fn overview_ifd(&self, overview_index: usize) -> Result<&tiff_reader::Ifd> {
287        self.overview_ifds
288            .get(overview_index)
289            .map(|overview| &overview.ifd)
290            .ok_or(Error::OverviewNotFound(overview_index))
291    }
292
293    /// Returns the TIFF IFD index of the base-resolution image.
294    pub fn base_ifd_index(&self) -> usize {
295        self.base_ifd_index
296    }
297
298    /// Decode the base-resolution raster into storage-domain typed samples.
299    pub fn read_raster<T: TiffSample>(&self) -> Result<ArrayD<T>> {
300        self.tiff
301            .read_image::<T>(self.base_ifd_index)
302            .map_err(Into::into)
303    }
304
305    /// Decode a base-resolution pixel window into storage-domain typed samples.
306    pub fn read_window<T: TiffSample>(
307        &self,
308        row_off: usize,
309        col_off: usize,
310        rows: usize,
311        cols: usize,
312    ) -> Result<ArrayD<T>> {
313        self.tiff
314            .read_window::<T>(self.base_ifd_index, row_off, col_off, rows, cols)
315            .map_err(Into::into)
316    }
317
318    /// Decode one base-resolution storage-domain band into a typed
319    /// `[height, width]` ndarray.
320    pub fn read_band<T: TiffSample>(&self, band_index: usize) -> Result<ArrayD<T>> {
321        self.tiff
322            .read_band::<T>(self.base_ifd_index, band_index)
323            .map_err(Into::into)
324    }
325
326    /// Decode a base-resolution window from one storage-domain band into a
327    /// typed `[rows, cols]` ndarray.
328    pub fn read_band_window<T: TiffSample>(
329        &self,
330        band_index: usize,
331        row_off: usize,
332        col_off: usize,
333        rows: usize,
334        cols: usize,
335    ) -> Result<ArrayD<T>> {
336        self.tiff
337            .read_band_window::<T>(
338                self.base_ifd_index,
339                band_index,
340                row_off,
341                col_off,
342                rows,
343                cols,
344            )
345            .map_err(Into::into)
346    }
347
348    /// Decode the base-resolution raster into color-decoded typed pixels.
349    pub fn read_decoded_raster<T: TiffSample>(&self) -> Result<ArrayD<T>> {
350        self.tiff
351            .read_decoded_image::<T>(self.base_ifd_index)
352            .map_err(Into::into)
353    }
354
355    /// Decode a base-resolution pixel window into color-decoded typed pixels.
356    pub fn read_decoded_window<T: TiffSample>(
357        &self,
358        row_off: usize,
359        col_off: usize,
360        rows: usize,
361        cols: usize,
362    ) -> Result<ArrayD<T>> {
363        self.tiff
364            .read_decoded_window::<T>(self.base_ifd_index, row_off, col_off, rows, cols)
365            .map_err(Into::into)
366    }
367
368    /// Decode an overview raster into storage-domain typed samples.
369    pub fn read_overview<T: TiffSample>(&self, overview_index: usize) -> Result<ArrayD<T>> {
370        let overview = self
371            .overview_ifds
372            .get(overview_index)
373            .ok_or(Error::OverviewNotFound(overview_index))?;
374        self.tiff
375            .read_image_from_ifd::<T>(&overview.ifd)
376            .map_err(Into::into)
377    }
378
379    /// Decode an overview raster into color-decoded typed pixels.
380    pub fn read_decoded_overview<T: TiffSample>(&self, overview_index: usize) -> Result<ArrayD<T>> {
381        let overview = self
382            .overview_ifds
383            .get(overview_index)
384            .ok_or(Error::OverviewNotFound(overview_index))?;
385        self.tiff
386            .read_decoded_image_from_ifd::<T>(&overview.ifd)
387            .map_err(Into::into)
388    }
389
390    /// Decode one overview storage-domain band into a typed `[height, width]`
391    /// ndarray.
392    pub fn read_overview_band<T: TiffSample>(
393        &self,
394        overview_index: usize,
395        band_index: usize,
396    ) -> 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_band_from_ifd::<T>(&overview.ifd, band_index)
403            .map_err(Into::into)
404    }
405
406    /// Decode an overview pixel window into storage-domain typed samples.
407    pub fn read_overview_window<T: TiffSample>(
408        &self,
409        overview_index: usize,
410        row_off: usize,
411        col_off: usize,
412        rows: usize,
413        cols: usize,
414    ) -> Result<ArrayD<T>> {
415        let overview = self
416            .overview_ifds
417            .get(overview_index)
418            .ok_or(Error::OverviewNotFound(overview_index))?;
419        self.tiff
420            .read_window_from_ifd::<T>(&overview.ifd, row_off, col_off, rows, cols)
421            .map_err(Into::into)
422    }
423
424    /// Decode an overview pixel window into color-decoded typed pixels.
425    pub fn read_decoded_overview_window<T: TiffSample>(
426        &self,
427        overview_index: usize,
428        row_off: usize,
429        col_off: usize,
430        rows: usize,
431        cols: usize,
432    ) -> Result<ArrayD<T>> {
433        let overview = self
434            .overview_ifds
435            .get(overview_index)
436            .ok_or(Error::OverviewNotFound(overview_index))?;
437        self.tiff
438            .read_decoded_window_from_ifd::<T>(&overview.ifd, row_off, col_off, rows, cols)
439            .map_err(Into::into)
440    }
441
442    /// Decode an overview pixel window from one storage-domain band into a
443    /// typed `[rows, cols]` ndarray.
444    pub fn read_overview_band_window<T: TiffSample>(
445        &self,
446        overview_index: usize,
447        band_index: usize,
448        row_off: usize,
449        col_off: usize,
450        rows: usize,
451        cols: usize,
452    ) -> Result<ArrayD<T>> {
453        let overview = self
454            .overview_ifds
455            .get(overview_index)
456            .ok_or(Error::OverviewNotFound(overview_index))?;
457        self.tiff
458            .read_band_window_from_ifd::<T>(&overview.ifd, band_index, row_off, col_off, rows, cols)
459            .map_err(Into::into)
460    }
461}
462
463#[cfg(feature = "local")]
464fn is_overview_ifd(base: &tiff_reader::Ifd, candidate: &tiff_reader::Ifd) -> bool {
465    let smaller = candidate.width() <= base.width()
466        && candidate.height() <= base.height()
467        && (candidate.width() < base.width() || candidate.height() < base.height());
468    if !smaller {
469        return false;
470    }
471
472    let same_layout = candidate.samples_per_pixel() == base.samples_per_pixel()
473        && candidate.bits_per_sample().ok() == base.bits_per_sample().ok()
474        && candidate.sample_format().ok() == base.sample_format().ok()
475        && candidate.photometric_interpretation() == base.photometric_interpretation();
476    if !same_layout {
477        return false;
478    }
479
480    has_reduced_resolution_flag(candidate)
481        || (candidate.tag(TAG_NEW_SUBFILE_TYPE).is_none()
482            && candidate.tag(TAG_SUBFILE_TYPE).is_none())
483}
484
485#[cfg(feature = "local")]
486fn collect_overview_ifds(
487    tiff: &TiffFile,
488    base_ifd: &tiff_reader::Ifd,
489    base_ifd_index: usize,
490    metadata_ifd_index: usize,
491) -> Result<Vec<GeoImageIfd>> {
492    let mut seen_offsets = HashSet::from([base_ifd.offset()]);
493    let mut overviews: Vec<GeoImageIfd> = Vec::new();
494    for (index, candidate) in tiff.ifds().iter().enumerate() {
495        if index != base_ifd_index
496            && index != metadata_ifd_index
497            && is_overview_ifd(base_ifd, candidate)
498            && seen_offsets.insert(candidate.offset())
499        {
500            overviews.push(GeoImageIfd {
501                top_level_ifd_index: Some(index),
502                ifd: candidate.clone(),
503            });
504        }
505    }
506
507    if let Some(offsets) = base_ifd.sub_ifd_offsets() {
508        collect_subifd_overviews(tiff, base_ifd, &offsets, &mut seen_offsets, &mut overviews)?;
509    }
510
511    overviews.sort_by(|lhs, rhs| {
512        rhs.ifd
513            .width()
514            .cmp(&lhs.ifd.width())
515            .then_with(|| rhs.ifd.height().cmp(&lhs.ifd.height()))
516            .then_with(|| lhs.top_level_ifd_index.cmp(&rhs.top_level_ifd_index))
517    });
518
519    Ok(overviews)
520}
521
522#[cfg(feature = "local")]
523fn collect_subifd_overviews(
524    tiff: &TiffFile,
525    base_ifd: &tiff_reader::Ifd,
526    offsets: &[u64],
527    seen_offsets: &mut HashSet<u64>,
528    overviews: &mut Vec<GeoImageIfd>,
529) -> Result<()> {
530    let mut pending = Vec::new();
531    let mut visited_nodes = 0usize;
532    push_subifd_offsets(offsets, 1, seen_offsets, &mut pending, &mut visited_nodes)?;
533
534    while let Some((offset, depth)) = pending.pop() {
535        let candidate = tiff.read_ifd_at_offset(offset)?;
536        if is_overview_ifd(base_ifd, &candidate) {
537            overviews.push(GeoImageIfd {
538                top_level_ifd_index: None,
539                ifd: candidate.clone(),
540            });
541        }
542
543        if let Some(child_offsets) = candidate.sub_ifd_offsets() {
544            push_subifd_offsets(
545                &child_offsets,
546                depth + 1,
547                seen_offsets,
548                &mut pending,
549                &mut visited_nodes,
550            )?;
551        }
552    }
553
554    Ok(())
555}
556
557#[cfg(feature = "local")]
558fn push_subifd_offsets(
559    offsets: &[u64],
560    depth: usize,
561    seen_offsets: &mut HashSet<u64>,
562    pending: &mut Vec<(u64, usize)>,
563    visited_nodes: &mut usize,
564) -> Result<()> {
565    if depth > MAX_SUBIFD_OVERVIEW_DEPTH {
566        return Err(Error::Other(format!(
567            "SubIFD overview traversal exceeds depth budget of {MAX_SUBIFD_OVERVIEW_DEPTH}"
568        )));
569    }
570
571    for &offset in offsets.iter().rev() {
572        if *visited_nodes >= MAX_SUBIFD_OVERVIEW_NODES {
573            return Err(Error::Other(format!(
574                "SubIFD overview traversal exceeds node budget of {MAX_SUBIFD_OVERVIEW_NODES}"
575            )));
576        }
577        *visited_nodes += 1;
578
579        if seen_offsets.insert(offset) {
580            pending.push((offset, depth));
581        }
582    }
583
584    Ok(())
585}
586
587#[cfg(feature = "local")]
588fn find_metadata_ifd_index(ifds: &[tiff_reader::Ifd]) -> Result<usize> {
589    ifds.iter()
590        .position(|ifd| ifd.tag(TAG_GEO_KEY_DIRECTORY).is_some())
591        .or_else(|| ifds.iter().position(has_model_georeferencing))
592        .ok_or(Error::NotGeoTiff)
593}
594
595#[cfg(feature = "local")]
596fn has_model_georeferencing(ifd: &tiff_reader::Ifd) -> bool {
597    ifd.tag(TAG_MODEL_TRANSFORMATION).is_some()
598        || (ifd.tag(TAG_MODEL_TIEPOINT).is_some() && ifd.tag(TAG_MODEL_PIXEL_SCALE).is_some())
599}
600
601#[cfg(feature = "local")]
602fn find_base_ifd_index(ifds: &[tiff_reader::Ifd], metadata_ifd_index: usize) -> usize {
603    let metadata_ifd = &ifds[metadata_ifd_index];
604    if !has_reduced_resolution_flag(metadata_ifd) {
605        return metadata_ifd_index;
606    }
607
608    ifds.iter()
609        .enumerate()
610        .skip(metadata_ifd_index + 1)
611        .find_map(|(index, ifd)| (!has_reduced_resolution_flag(ifd)).then_some(index))
612        .unwrap_or(metadata_ifd_index)
613}
614
615#[cfg(feature = "local")]
616fn has_reduced_resolution_flag(ifd: &tiff_reader::Ifd) -> bool {
617    ifd.tag(TAG_NEW_SUBFILE_TYPE)
618        .and_then(|tag| tag.value.as_u64())
619        .map(|flags| flags & 0x1 != 0)
620        .or_else(|| {
621            ifd.tag(TAG_SUBFILE_TYPE)
622                .and_then(|tag| tag.value.as_u16())
623                .map(|value| value == 2)
624        })
625        .unwrap_or(false)
626}
627
628#[cfg(feature = "local")]
629fn parse_geokey_directory(ifd: &tiff_reader::Ifd) -> Result<GeoKeyDirectory> {
630    let Some(tag) = ifd.tag(TAG_GEO_KEY_DIRECTORY) else {
631        return Ok(GeoKeyDirectory::new());
632    };
633    let TagValue::Short(directory) = &tag.value else {
634        return Err(invalid_geotiff_tag(
635            TAG_GEO_KEY_DIRECTORY,
636            "GeoKeyDirectoryTag must use SHORT values",
637        ));
638    };
639    let double_params = match ifd.tag(TAG_GEO_DOUBLE_PARAMS) {
640        Some(tag) => match &tag.value {
641            TagValue::Double(values) => values.as_slice(),
642            _ => {
643                return Err(invalid_geotiff_tag(
644                    TAG_GEO_DOUBLE_PARAMS,
645                    "GeoDoubleParamsTag must use DOUBLE values",
646                ))
647            }
648        },
649        None => &[],
650    };
651    let ascii_params = match ifd.tag(TAG_GEO_ASCII_PARAMS) {
652        Some(tag) => match &tag.value {
653            TagValue::Ascii(value) => value.as_str(),
654            _ => {
655                return Err(invalid_geotiff_tag(
656                    TAG_GEO_ASCII_PARAMS,
657                    "GeoAsciiParamsTag must use ASCII values",
658                ))
659            }
660        },
661        None => "",
662    };
663    GeoKeyDirectory::parse(directory, double_params, ascii_params)
664        .ok_or(Error::InvalidGeoKeyDirectory)
665}
666
667#[cfg(feature = "local")]
668fn parse_fixed_len_double_tag<const N: usize>(
669    ifd: &tiff_reader::Ifd,
670    tag_code: u16,
671) -> Result<Option<[f64; N]>> {
672    let Some(tag) = ifd.tag(tag_code) else {
673        return Ok(None);
674    };
675    let TagValue::Double(values) = &tag.value else {
676        return Err(invalid_geotiff_tag(tag_code, "tag must use DOUBLE values"));
677    };
678    if values.len() != N {
679        return Err(invalid_geotiff_tag(
680            tag_code,
681            format!("expected exactly {N} values, got {}", values.len()),
682        ));
683    }
684    let mut out = [0.0; N];
685    out.copy_from_slice(values);
686    Ok(Some(out))
687}
688
689#[cfg(feature = "local")]
690fn parse_tiepoints(ifd: &tiff_reader::Ifd) -> Result<Vec<[f64; 6]>> {
691    let Some(tag) = ifd.tag(TAG_MODEL_TIEPOINT) else {
692        return Ok(Vec::new());
693    };
694    let TagValue::Double(values) = &tag.value else {
695        return Err(invalid_geotiff_tag(
696            TAG_MODEL_TIEPOINT,
697            "ModelTiepointTag must use DOUBLE values",
698        ));
699    };
700    if values.is_empty() || values.len() % 6 != 0 {
701        return Err(invalid_geotiff_tag(
702            TAG_MODEL_TIEPOINT,
703            format!(
704                "expected a non-empty multiple of 6 values, got {}",
705                values.len()
706            ),
707        ));
708    }
709    Ok(values
710        .chunks_exact(6)
711        .map(|chunk| [chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5]])
712        .collect())
713}
714
715#[cfg(feature = "local")]
716fn parse_nodata(ifd: &tiff_reader::Ifd) -> Result<Option<String>> {
717    let Some(tag) = ifd.tag(TAG_GDAL_NODATA) else {
718        return Ok(None);
719    };
720    match &tag.value {
721        TagValue::Ascii(value) => Ok(Some(value.clone())),
722        _ => Err(invalid_geotiff_tag(
723            TAG_GDAL_NODATA,
724            "GDAL_NODATA must use an ASCII value",
725        )),
726    }
727}
728
729#[cfg(feature = "local")]
730fn invalid_geotiff_tag(tag: u16, reason: impl Into<String>) -> Error {
731    Error::InvalidGeoTiffTag {
732        tag,
733        reason: reason.into(),
734    }
735}
736
737#[cfg(feature = "local")]
738fn validate_model_georeferencing(
739    tiepoints: &[[f64; 6]],
740    pixel_scale: Option<&[f64; 3]>,
741    transformation: Option<&[f64; 16]>,
742) -> Result<()> {
743    if tiepoints.iter().flatten().any(|value| !value.is_finite()) {
744        return Err(invalid_geotiff_tag(
745            TAG_MODEL_TIEPOINT,
746            "tiepoint values must be finite",
747        ));
748    }
749    if let Some(scale) = pixel_scale {
750        if !scale.iter().all(|value| value.is_finite())
751            || scale[0] <= 0.0
752            || scale[1] <= 0.0
753            || scale[2] < 0.0
754        {
755            return Err(invalid_geotiff_tag(
756                TAG_MODEL_PIXEL_SCALE,
757                "pixel scale must contain finite positive X/Y and non-negative Z values",
758            ));
759        }
760        if tiepoints.is_empty() {
761            return Err(invalid_geotiff_tag(
762                TAG_MODEL_PIXEL_SCALE,
763                "pixel scale requires at least one model tiepoint",
764            ));
765        }
766    }
767    if let Some(matrix) = transformation {
768        if !matrix.iter().all(|value| value.is_finite()) {
769            return Err(invalid_geotiff_tag(
770                TAG_MODEL_TRANSFORMATION,
771                "transformation matrix values must be finite",
772            ));
773        }
774        let transform = GeoTransform::from_transformation_matrix(matrix);
775        if transform
776            .geo_to_pixel(transform.origin_x, transform.origin_y)
777            .is_none()
778        {
779            return Err(invalid_geotiff_tag(
780                TAG_MODEL_TRANSFORMATION,
781                "transformation matrix must contain an invertible 2D affine transform",
782            ));
783        }
784        if pixel_scale.is_some() || !tiepoints.is_empty() {
785            return Err(invalid_geotiff_tag(
786                TAG_MODEL_TRANSFORMATION,
787                "ModelTransformationTag is mutually exclusive with ModelPixelScaleTag and ModelTiepointTag",
788            ));
789        }
790    }
791    Ok(())
792}
793
794#[cfg(test)]
795#[cfg(feature = "local")]
796mod tests {
797    use std::fs;
798    use std::path::PathBuf;
799    use std::time::{SystemTime, UNIX_EPOCH};
800
801    use super::{
802        GeoTiffFile, MAX_SUBIFD_OVERVIEW_DEPTH, MAX_SUBIFD_OVERVIEW_NODES, TAG_GDAL_NODATA,
803        TAG_MODEL_PIXEL_SCALE,
804    };
805
806    #[derive(Clone)]
807    struct TestIfdSpec {
808        entries: Vec<(u16, u16, u32, Vec<u8>)>,
809        image_data: Vec<u8>,
810    }
811
812    fn le_u16(value: u16) -> [u8; 2] {
813        value.to_le_bytes()
814    }
815
816    fn le_u32(value: u32) -> [u8; 4] {
817        value.to_le_bytes()
818    }
819
820    fn le_f64(value: f64) -> [u8; 8] {
821        value.to_le_bytes()
822    }
823
824    fn temp_geotiff_path(test_name: &str) -> PathBuf {
825        let nanos = SystemTime::now()
826            .duration_since(UNIX_EPOCH)
827            .unwrap()
828            .as_nanos();
829        std::env::temp_dir().join(format!(
830            "geotiff-rust-{test_name}-{}-{nanos}.tif",
831            std::process::id()
832        ))
833    }
834
835    fn inline_short(value: u16) -> Vec<u8> {
836        let mut bytes = [0u8; 4];
837        bytes[..2].copy_from_slice(&le_u16(value));
838        bytes.to_vec()
839    }
840
841    #[allow(clippy::too_many_arguments)]
842    fn build_lerc2_header_v2(
843        width: u32,
844        height: u32,
845        valid_pixel_count: u32,
846        image_type: i32,
847        max_z_error: f64,
848        z_min: f64,
849        z_max: f64,
850        payload_len: usize,
851    ) -> Vec<u8> {
852        let blob_size = 58 + 4 + payload_len;
853        let mut bytes = Vec::with_capacity(blob_size);
854        bytes.extend_from_slice(b"Lerc2 ");
855        bytes.extend_from_slice(&2i32.to_le_bytes());
856        bytes.extend_from_slice(&height.to_le_bytes());
857        bytes.extend_from_slice(&width.to_le_bytes());
858        bytes.extend_from_slice(&valid_pixel_count.to_le_bytes());
859        bytes.extend_from_slice(&8i32.to_le_bytes());
860        bytes.extend_from_slice(&(blob_size as i32).to_le_bytes());
861        bytes.extend_from_slice(&image_type.to_le_bytes());
862        bytes.extend_from_slice(&max_z_error.to_le_bytes());
863        bytes.extend_from_slice(&z_min.to_le_bytes());
864        bytes.extend_from_slice(&z_max.to_le_bytes());
865        bytes
866    }
867
868    fn build_classic_tiff(ifds: &[TestIfdSpec]) -> Vec<u8> {
869        let mut ifd_offsets = Vec::with_capacity(ifds.len());
870        let mut cursor = 8usize;
871        for ifd in ifds {
872            ifd_offsets.push(cursor as u32);
873            let deferred_len: usize = ifd
874                .entries
875                .iter()
876                .filter(|(tag, _, _, value)| *tag != 273 && value.len() > 4)
877                .map(|(_, _, _, value)| value.len())
878                .sum();
879            cursor += 2 + ifd.entries.len() * 12 + 4 + ifd.image_data.len() + deferred_len;
880        }
881
882        let mut bytes = Vec::with_capacity(cursor);
883        bytes.extend_from_slice(b"II");
884        bytes.extend_from_slice(&le_u16(42));
885        bytes.extend_from_slice(&le_u32(ifd_offsets.first().copied().unwrap_or(0)));
886
887        for (ifd_index, ifd) in ifds.iter().enumerate() {
888            let ifd_offset = ifd_offsets[ifd_index] as usize;
889            debug_assert_eq!(bytes.len(), ifd_offset);
890
891            let ifd_size = 2 + ifd.entries.len() * 12 + 4;
892            let mut next_data_offset = ifd_offset + ifd_size;
893            let image_offset = next_data_offset as u32;
894            next_data_offset += ifd.image_data.len();
895
896            bytes.extend_from_slice(&le_u16(ifd.entries.len() as u16));
897            let mut deferred = Vec::new();
898            for (tag, ty, count, value) in &ifd.entries {
899                bytes.extend_from_slice(&le_u16(*tag));
900                bytes.extend_from_slice(&le_u16(*ty));
901                bytes.extend_from_slice(&le_u32(*count));
902                if *tag == 273 {
903                    bytes.extend_from_slice(&le_u32(image_offset));
904                } else if value.len() <= 4 {
905                    let mut inline = [0u8; 4];
906                    inline[..value.len()].copy_from_slice(value);
907                    bytes.extend_from_slice(&inline);
908                } else {
909                    bytes.extend_from_slice(&le_u32(next_data_offset as u32));
910                    next_data_offset += value.len();
911                    deferred.push(value.clone());
912                }
913            }
914
915            let next_ifd_offset = ifd_offsets.get(ifd_index + 1).copied().unwrap_or(0);
916            bytes.extend_from_slice(&le_u32(next_ifd_offset));
917            bytes.extend_from_slice(&ifd.image_data);
918            for value in deferred {
919                bytes.extend_from_slice(&value);
920            }
921            debug_assert_eq!(bytes.len(), next_data_offset);
922        }
923
924        bytes
925    }
926
927    fn build_simple_geotiff(pixel_is_point: bool) -> Vec<u8> {
928        let image_data = vec![10u8, 20, 30, 40];
929        let tiepoints = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0];
930        let scales = [2.0, 2.0, 0.0];
931        let geo_keys = if pixel_is_point {
932            vec![
933                1, 1, 0, 3, // header
934                1024, 0, 1, 2, // model type = Geographic
935                1025, 0, 1, 2, // raster type = PixelIsPoint
936                2048, 0, 1, 4326, // EPSG:4326
937            ]
938        } else {
939            vec![
940                1, 1, 0, 2, // header
941                1024, 0, 1, 2, // model type = Geographic
942                2048, 0, 1, 4326, // EPSG:4326
943            ]
944        };
945        let nodata = b"-9999\0".to_vec();
946
947        build_classic_tiff(&[TestIfdSpec {
948            image_data,
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                    scales.iter().flat_map(|value| le_f64(*value)).collect(),
963                ),
964                (
965                    33922u16,
966                    12u16,
967                    6u32,
968                    tiepoints.iter().flat_map(|value| le_f64(*value)).collect(),
969                ),
970                (
971                    34735u16,
972                    3u16,
973                    geo_keys.len() as u32,
974                    geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
975                ),
976                (42113u16, 2u16, nodata.len() as u32, nodata),
977            ],
978        }])
979    }
980
981    fn build_simple_lerc_geotiff() -> Vec<u8> {
982        let tiepoints = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0];
983        let scales = [2.0, 2.0, 0.0];
984        let geo_keys = vec![
985            1, 1, 0, 2, // header
986            1024, 0, 1, 2, // model type = Geographic
987            2048, 0, 1, 4326, // EPSG:4326
988        ];
989
990        let mut image_data = build_lerc2_header_v2(2, 2, 4, 6, 0.0, 1.0, 4.0, 1 + 16);
991        image_data.extend_from_slice(&0u32.to_le_bytes());
992        image_data.push(1);
993        for value in [1.0f32, 2.0, 3.0, 4.0] {
994            image_data.extend_from_slice(&value.to_le_bytes());
995        }
996        let image_len = image_data.len() as u32;
997
998        build_classic_tiff(&[TestIfdSpec {
999            image_data,
1000            entries: vec![
1001                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
1002                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
1003                (258u16, 3u16, 1u32, inline_short(32)),
1004                (259u16, 3u16, 1u32, inline_short(34887)),
1005                (273u16, 4u16, 1u32, vec![]),
1006                (277u16, 3u16, 1u32, inline_short(1)),
1007                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
1008                (279u16, 4u16, 1u32, le_u32(image_len).to_vec()),
1009                (339u16, 3u16, 1u32, inline_short(3)),
1010                (
1011                    33550u16,
1012                    12u16,
1013                    3u32,
1014                    scales.iter().flat_map(|value| le_f64(*value)).collect(),
1015                ),
1016                (
1017                    33922u16,
1018                    12u16,
1019                    6u32,
1020                    tiepoints.iter().flat_map(|value| le_f64(*value)).collect(),
1021                ),
1022                (
1023                    34735u16,
1024                    3u16,
1025                    geo_keys.len() as u32,
1026                    geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
1027                ),
1028            ],
1029        }])
1030    }
1031
1032    fn overwrite_classic_inline_long_tag(bytes: &mut [u8], tag_code: u16, value: u32) {
1033        let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
1034        let mut offset = 10usize;
1035        for _ in 0..entry_count {
1036            let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
1037            if code == tag_code {
1038                bytes[offset + 8..offset + 12].copy_from_slice(&le_u32(value));
1039                return;
1040            }
1041            offset += 12;
1042        }
1043        panic!("tag {tag_code} not found in classic TIFF");
1044    }
1045
1046    fn classic_inline_long_tag(bytes: &[u8], tag_code: u16) -> u32 {
1047        let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
1048        let mut offset = 10usize;
1049        for _ in 0..entry_count {
1050            let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
1051            if code == tag_code {
1052                return u32::from_le_bytes(bytes[offset + 8..offset + 12].try_into().unwrap());
1053            }
1054            offset += 12;
1055        }
1056        panic!("tag {tag_code} not found in classic TIFF");
1057    }
1058
1059    fn overwrite_classic_tag_type_or_count(
1060        bytes: &mut [u8],
1061        tag_code: u16,
1062        tag_type: Option<u16>,
1063        count: Option<u32>,
1064    ) {
1065        let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
1066        let mut offset = 10usize;
1067        for _ in 0..entry_count {
1068            let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
1069            if code == tag_code {
1070                if let Some(tag_type) = tag_type {
1071                    bytes[offset + 2..offset + 4].copy_from_slice(&le_u16(tag_type));
1072                }
1073                if let Some(count) = count {
1074                    bytes[offset + 4..offset + 8].copy_from_slice(&le_u32(count));
1075                }
1076                return;
1077            }
1078            offset += 12;
1079        }
1080        panic!("tag {tag_code} not found in classic TIFF");
1081    }
1082
1083    fn overwrite_first_ifd_next_pointer(bytes: &mut [u8], value: u32) {
1084        let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
1085        let pointer_offset = 10 + entry_count * 12;
1086        bytes[pointer_offset..pointer_offset + 4].copy_from_slice(&le_u32(value));
1087    }
1088
1089    fn overwrite_classic_inline_long_tag_at(
1090        bytes: &mut [u8],
1091        ifd_offset: usize,
1092        tag_code: u16,
1093        value: u32,
1094    ) {
1095        let entry_count = u16::from_le_bytes([bytes[ifd_offset], bytes[ifd_offset + 1]]) as usize;
1096        let mut offset = ifd_offset + 2;
1097        for _ in 0..entry_count {
1098            let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
1099            if code == tag_code {
1100                bytes[offset + 8..offset + 12].copy_from_slice(&le_u32(value));
1101                return;
1102            }
1103            offset += 12;
1104        }
1105        panic!("tag {tag_code} not found in classic TIFF at offset {ifd_offset}");
1106    }
1107
1108    fn first_ifd_next_pointer(bytes: &[u8]) -> u32 {
1109        let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
1110        let pointer_offset = 10 + entry_count * 12;
1111        u32::from_le_bytes([
1112            bytes[pointer_offset],
1113            bytes[pointer_offset + 1],
1114            bytes[pointer_offset + 2],
1115            bytes[pointer_offset + 3],
1116        ])
1117    }
1118
1119    fn ifd_next_pointer(bytes: &[u8], ifd_offset: usize) -> u32 {
1120        let entry_count = u16::from_le_bytes([bytes[ifd_offset], bytes[ifd_offset + 1]]) as usize;
1121        let pointer_offset = ifd_offset + 2 + entry_count * 12;
1122        u32::from_le_bytes([
1123            bytes[pointer_offset],
1124            bytes[pointer_offset + 1],
1125            bytes[pointer_offset + 2],
1126            bytes[pointer_offset + 3],
1127        ])
1128    }
1129
1130    fn overwrite_classic_long_tag_values(bytes: &mut [u8], tag_code: u16, values: &[u32]) {
1131        overwrite_classic_long_tag_values_at(bytes, 8, tag_code, values);
1132    }
1133
1134    fn overwrite_classic_long_tag_values_at(
1135        bytes: &mut [u8],
1136        ifd_offset: usize,
1137        tag_code: u16,
1138        values: &[u32],
1139    ) {
1140        let entry_count = u16::from_le_bytes([bytes[ifd_offset], bytes[ifd_offset + 1]]) as usize;
1141        let mut offset = ifd_offset + 2;
1142        for _ in 0..entry_count {
1143            let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
1144            if code == tag_code {
1145                let type_code = u16::from_le_bytes([bytes[offset + 2], bytes[offset + 3]]);
1146                let count = u32::from_le_bytes([
1147                    bytes[offset + 4],
1148                    bytes[offset + 5],
1149                    bytes[offset + 6],
1150                    bytes[offset + 7],
1151                ]) as usize;
1152                assert_eq!(type_code, 4, "tag {tag_code} is not a LONG tag");
1153                assert_eq!(count, values.len(), "tag {tag_code} value count mismatch");
1154
1155                if values.len() == 1 {
1156                    bytes[offset + 8..offset + 12].copy_from_slice(&le_u32(values[0]));
1157                } else {
1158                    let value_offset = u32::from_le_bytes([
1159                        bytes[offset + 8],
1160                        bytes[offset + 9],
1161                        bytes[offset + 10],
1162                        bytes[offset + 11],
1163                    ]) as usize;
1164                    for (index, value) in values.iter().enumerate() {
1165                        let value_offset = value_offset + index * 4;
1166                        bytes[value_offset..value_offset + 4].copy_from_slice(&le_u32(*value));
1167                    }
1168                }
1169                return;
1170            }
1171            offset += 12;
1172        }
1173        panic!("tag {tag_code} not found in classic TIFF at offset {ifd_offset}");
1174    }
1175
1176    fn top_level_ifd_offsets_after_first(bytes: &[u8], count: usize) -> Vec<u32> {
1177        let mut offsets = Vec::with_capacity(count);
1178        let mut offset = first_ifd_next_pointer(bytes);
1179        while offset != 0 && offsets.len() < count {
1180            offsets.push(offset);
1181            offset = ifd_next_pointer(bytes, offset as usize);
1182        }
1183        assert_eq!(offsets.len(), count);
1184        offsets
1185    }
1186
1187    fn subifd_budget_base(subifd_count: u32) -> TestIfdSpec {
1188        let geo_keys = [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326];
1189        TestIfdSpec {
1190            image_data: vec![0u8],
1191            entries: vec![
1192                (256u16, 4u16, 1u32, le_u32(64).to_vec()),
1193                (257u16, 4u16, 1u32, le_u32(64).to_vec()),
1194                (258u16, 3u16, 1u32, inline_short(8)),
1195                (259u16, 3u16, 1u32, inline_short(1)),
1196                (273u16, 4u16, 1u32, vec![]),
1197                (277u16, 3u16, 1u32, inline_short(1)),
1198                (278u16, 4u16, 1u32, le_u32(64).to_vec()),
1199                (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1200                (
1201                    330u16,
1202                    4u16,
1203                    subifd_count,
1204                    vec![0; subifd_count as usize * 4],
1205                ),
1206                (
1207                    33550u16,
1208                    12u16,
1209                    3u32,
1210                    [2.0, 2.0, 0.0]
1211                        .iter()
1212                        .flat_map(|value| le_f64(*value))
1213                        .collect(),
1214                ),
1215                (
1216                    33922u16,
1217                    12u16,
1218                    6u32,
1219                    [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1220                        .iter()
1221                        .flat_map(|value| le_f64(*value))
1222                        .collect(),
1223                ),
1224                (
1225                    34735u16,
1226                    3u16,
1227                    geo_keys.len() as u32,
1228                    geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
1229                ),
1230            ],
1231        }
1232    }
1233
1234    fn subifd_budget_overview(has_child: bool) -> TestIfdSpec {
1235        let mut entries = vec![
1236            (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1237            (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1238            (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1239            (258u16, 3u16, 1u32, inline_short(8)),
1240            (259u16, 3u16, 1u32, inline_short(1)),
1241            (273u16, 4u16, 1u32, vec![]),
1242            (277u16, 3u16, 1u32, inline_short(1)),
1243            (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1244            (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1245        ];
1246        if has_child {
1247            entries.push((330u16, 4u16, 1u32, le_u32(0).to_vec()));
1248        }
1249        TestIfdSpec {
1250            image_data: vec![0u8],
1251            entries,
1252        }
1253    }
1254
1255    fn build_geotiff_exceeding_subifd_node_budget() -> Vec<u8> {
1256        let child_count = MAX_SUBIFD_OVERVIEW_NODES + 1;
1257        let mut ifds = Vec::with_capacity(child_count + 1);
1258        ifds.push(subifd_budget_base(child_count as u32));
1259        ifds.extend((0..child_count).map(|_| subifd_budget_overview(false)));
1260
1261        let mut bytes = build_classic_tiff(&ifds);
1262        let child_offsets = top_level_ifd_offsets_after_first(&bytes, child_count);
1263        overwrite_classic_long_tag_values(&mut bytes, 330, &child_offsets);
1264        overwrite_first_ifd_next_pointer(&mut bytes, 0);
1265        bytes
1266    }
1267
1268    fn build_geotiff_exceeding_subifd_depth_budget() -> Vec<u8> {
1269        let child_count = MAX_SUBIFD_OVERVIEW_DEPTH + 1;
1270        let mut ifds = Vec::with_capacity(child_count + 1);
1271        ifds.push(subifd_budget_base(1));
1272        ifds.extend((0..child_count).map(|index| subifd_budget_overview(index + 1 < child_count)));
1273
1274        let mut bytes = build_classic_tiff(&ifds);
1275        let child_offsets = top_level_ifd_offsets_after_first(&bytes, child_count);
1276        overwrite_classic_long_tag_values(&mut bytes, 330, &child_offsets[..1]);
1277        for offsets in child_offsets.windows(2) {
1278            overwrite_classic_long_tag_values_at(
1279                &mut bytes,
1280                offsets[0] as usize,
1281                330,
1282                &offsets[1..],
1283            );
1284        }
1285        overwrite_first_ifd_next_pointer(&mut bytes, 0);
1286        bytes
1287    }
1288
1289    fn build_geotiff_with_overview() -> Vec<u8> {
1290        let base = TestIfdSpec {
1291            image_data: vec![10u8, 20, 30, 40],
1292            entries: vec![
1293                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
1294                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
1295                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1296                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1297                (273u16, 4u16, 1u32, vec![]),
1298                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1299                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
1300                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
1301                (
1302                    33550u16,
1303                    12u16,
1304                    3u32,
1305                    [2.0, 2.0, 0.0]
1306                        .iter()
1307                        .flat_map(|value| le_f64(*value))
1308                        .collect(),
1309                ),
1310                (
1311                    33922u16,
1312                    12u16,
1313                    6u32,
1314                    [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1315                        .iter()
1316                        .flat_map(|value| le_f64(*value))
1317                        .collect(),
1318                ),
1319                (
1320                    34735u16,
1321                    3u16,
1322                    12u32,
1323                    [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326]
1324                        .iter()
1325                        .flat_map(|value| le_u16(*value))
1326                        .collect(),
1327                ),
1328            ],
1329        };
1330        let overview = TestIfdSpec {
1331            image_data: vec![99u8],
1332            entries: vec![
1333                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1334                (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1335                (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1336                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1337                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1338                (273u16, 4u16, 1u32, vec![]),
1339                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1340                (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1341                (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1342            ],
1343        };
1344
1345        build_classic_tiff(&[base, overview])
1346    }
1347
1348    fn build_geotiff_with_subifd_overview() -> Vec<u8> {
1349        let base = TestIfdSpec {
1350            image_data: vec![10u8, 20, 30, 40],
1351            entries: vec![
1352                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
1353                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
1354                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1355                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1356                (273u16, 4u16, 1u32, vec![]),
1357                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1358                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
1359                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
1360                (330u16, 4u16, 1u32, le_u32(0).to_vec()),
1361                (
1362                    33550u16,
1363                    12u16,
1364                    3u32,
1365                    [2.0, 2.0, 0.0]
1366                        .iter()
1367                        .flat_map(|value| le_f64(*value))
1368                        .collect(),
1369                ),
1370                (
1371                    33922u16,
1372                    12u16,
1373                    6u32,
1374                    [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1375                        .iter()
1376                        .flat_map(|value| le_f64(*value))
1377                        .collect(),
1378                ),
1379                (
1380                    34735u16,
1381                    3u16,
1382                    12u32,
1383                    [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326]
1384                        .iter()
1385                        .flat_map(|value| le_u16(*value))
1386                        .collect(),
1387                ),
1388            ],
1389        };
1390        let overview = TestIfdSpec {
1391            image_data: vec![99u8],
1392            entries: vec![
1393                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1394                (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1395                (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1396                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1397                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1398                (273u16, 4u16, 1u32, vec![]),
1399                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1400                (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1401                (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1402            ],
1403        };
1404
1405        let mut bytes = build_classic_tiff(&[base, overview]);
1406        let child_ifd_offset = first_ifd_next_pointer(&bytes);
1407        overwrite_classic_inline_long_tag(&mut bytes, 330, child_ifd_offset);
1408        overwrite_first_ifd_next_pointer(&mut bytes, 0);
1409        bytes
1410    }
1411
1412    fn build_geotiff_with_nested_subifd_overviews() -> Vec<u8> {
1413        let base = TestIfdSpec {
1414            image_data: (1u8..=16).collect(),
1415            entries: vec![
1416                (256u16, 4u16, 1u32, le_u32(4).to_vec()),
1417                (257u16, 4u16, 1u32, le_u32(4).to_vec()),
1418                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1419                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1420                (273u16, 4u16, 1u32, vec![]),
1421                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1422                (278u16, 4u16, 1u32, le_u32(4).to_vec()),
1423                (279u16, 4u16, 1u32, le_u32(16).to_vec()),
1424                (330u16, 4u16, 1u32, le_u32(0).to_vec()),
1425                (
1426                    33550u16,
1427                    12u16,
1428                    3u32,
1429                    [2.0, 2.0, 0.0]
1430                        .iter()
1431                        .flat_map(|value| le_f64(*value))
1432                        .collect(),
1433                ),
1434                (
1435                    33922u16,
1436                    12u16,
1437                    6u32,
1438                    [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1439                        .iter()
1440                        .flat_map(|value| le_f64(*value))
1441                        .collect(),
1442                ),
1443                (
1444                    34735u16,
1445                    3u16,
1446                    12u32,
1447                    [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326]
1448                        .iter()
1449                        .flat_map(|value| le_u16(*value))
1450                        .collect(),
1451                ),
1452            ],
1453        };
1454        let overview = TestIfdSpec {
1455            image_data: vec![50u8, 60, 70, 80],
1456            entries: vec![
1457                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1458                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
1459                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
1460                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1461                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1462                (273u16, 4u16, 1u32, vec![]),
1463                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1464                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
1465                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
1466                (330u16, 4u16, 1u32, le_u32(0).to_vec()),
1467            ],
1468        };
1469        let nested = TestIfdSpec {
1470            image_data: vec![99u8],
1471            entries: vec![
1472                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1473                (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1474                (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1475                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1476                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1477                (273u16, 4u16, 1u32, vec![]),
1478                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1479                (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1480                (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1481            ],
1482        };
1483
1484        let mut bytes = build_classic_tiff(&[base, overview, nested]);
1485        let child_ifd_offset = first_ifd_next_pointer(&bytes);
1486        let grandchild_ifd_offset = ifd_next_pointer(&bytes, child_ifd_offset as usize);
1487        overwrite_classic_inline_long_tag(&mut bytes, 330, child_ifd_offset);
1488        overwrite_classic_inline_long_tag_at(
1489            &mut bytes,
1490            child_ifd_offset as usize,
1491            330,
1492            grandchild_ifd_offset,
1493        );
1494        overwrite_first_ifd_next_pointer(&mut bytes, 0);
1495        bytes
1496    }
1497
1498    fn build_cog_like_geotiff_with_ghost_ifd() -> Vec<u8> {
1499        let geo_keys = [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326];
1500        let ghost = TestIfdSpec {
1501            image_data: vec![0u8],
1502            entries: vec![
1503                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1504                (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1505                (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1506                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1507                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1508                (273u16, 4u16, 1u32, vec![]),
1509                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1510                (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1511                (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1512                (
1513                    33550u16,
1514                    12u16,
1515                    3u32,
1516                    [2.0, 2.0, 0.0]
1517                        .iter()
1518                        .flat_map(|value| le_f64(*value))
1519                        .collect(),
1520                ),
1521                (
1522                    33922u16,
1523                    12u16,
1524                    6u32,
1525                    [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1526                        .iter()
1527                        .flat_map(|value| le_f64(*value))
1528                        .collect(),
1529                ),
1530                (
1531                    34735u16,
1532                    3u16,
1533                    geo_keys.len() as u32,
1534                    geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
1535                ),
1536            ],
1537        };
1538        let overview = TestIfdSpec {
1539            image_data: vec![50u8, 60, 70, 80],
1540            entries: vec![
1541                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1542                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
1543                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
1544                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1545                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1546                (273u16, 4u16, 1u32, vec![]),
1547                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1548                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
1549                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
1550            ],
1551        };
1552        let base = TestIfdSpec {
1553            image_data: (1u8..=16).collect(),
1554            entries: vec![
1555                (256u16, 4u16, 1u32, le_u32(4).to_vec()),
1556                (257u16, 4u16, 1u32, le_u32(4).to_vec()),
1557                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1558                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1559                (273u16, 4u16, 1u32, vec![]),
1560                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1561                (278u16, 4u16, 1u32, le_u32(4).to_vec()),
1562                (279u16, 4u16, 1u32, le_u32(16).to_vec()),
1563            ],
1564        };
1565
1566        build_classic_tiff(&[ghost, overview, base])
1567    }
1568
1569    #[test]
1570    fn parses_geotiff_metadata_and_reads_raster() {
1571        let file = GeoTiffFile::from_bytes(build_simple_geotiff(false)).unwrap();
1572        assert_eq!(file.epsg(), Some(4326));
1573        assert_eq!(file.width(), 2);
1574        assert_eq!(file.height(), 2);
1575        assert_eq!(file.band_count(), 1);
1576        assert_eq!(file.nodata(), Some("-9999"));
1577        assert_eq!(file.geo_bounds(), Some([100.0, 196.0, 104.0, 200.0]));
1578
1579        let raster = file.read_raster::<u8>().unwrap();
1580        assert_eq!(raster.shape(), &[2, 2]);
1581        let (values, offset) = raster.into_raw_vec_and_offset();
1582        assert_eq!(offset, Some(0));
1583        assert_eq!(values, vec![10, 20, 30, 40]);
1584    }
1585
1586    #[test]
1587    fn open_mmap_reads_geotiff_from_file() {
1588        let bytes = build_simple_geotiff(false);
1589        let path = temp_geotiff_path("open_mmap_reads_geotiff_from_file");
1590        fs::write(&path, &bytes).unwrap();
1591
1592        let file = unsafe { GeoTiffFile::open_mmap(&path).unwrap() };
1593        assert_eq!(file.epsg(), Some(4326));
1594        assert_eq!(file.tiff().raw_bytes(), Some(bytes.as_slice()));
1595
1596        let raster = file.read_raster::<u8>().unwrap();
1597        let (values, offset) = raster.into_raw_vec_and_offset();
1598        assert_eq!(offset, Some(0));
1599        assert_eq!(values, vec![10, 20, 30, 40]);
1600
1601        drop(file);
1602        let _ = fs::remove_file(path);
1603    }
1604
1605    #[test]
1606    fn parses_geotiff_metadata_and_reads_lerc_raster() {
1607        let file = GeoTiffFile::from_bytes(build_simple_lerc_geotiff()).unwrap();
1608        assert_eq!(file.epsg(), Some(4326));
1609        assert_eq!(file.width(), 2);
1610        assert_eq!(file.height(), 2);
1611
1612        let raster = file.read_raster::<f32>().unwrap();
1613        assert_eq!(raster.shape(), &[2, 2]);
1614        let (values, offset) = raster.into_raw_vec_and_offset();
1615        assert_eq!(offset, Some(0));
1616        assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0]);
1617    }
1618
1619    #[test]
1620    fn pixel_is_point_metadata_shifts_bounds_to_outer_edges() {
1621        let file = GeoTiffFile::from_bytes(build_simple_geotiff(true)).unwrap();
1622        assert_eq!(file.geo_bounds(), Some([99.0, 197.0, 103.0, 201.0]));
1623
1624        let transform = file.transform().unwrap();
1625        let (center_x, center_y) = transform.pixel_to_geo(0.5, 0.5);
1626        assert_eq!((center_x, center_y), (100.0, 200.0));
1627    }
1628
1629    #[test]
1630    fn discovers_reduced_resolution_overviews() {
1631        let file = GeoTiffFile::from_bytes(build_geotiff_with_overview()).unwrap();
1632        assert_eq!(file.overview_count(), 1);
1633        assert_eq!(file.overview_ifd_index(0).unwrap(), 1);
1634
1635        let overview = file.read_overview::<u8>(0).unwrap();
1636        assert_eq!(overview.shape(), &[1, 1]);
1637        let (values, offset) = overview.into_raw_vec_and_offset();
1638        assert_eq!(offset, Some(0));
1639        assert_eq!(values, vec![99]);
1640    }
1641
1642    #[test]
1643    fn discovers_and_reads_subifd_overviews() {
1644        let file = GeoTiffFile::from_bytes(build_geotiff_with_subifd_overview()).unwrap();
1645        assert_eq!(file.overview_count(), 1);
1646        assert!(matches!(
1647            file.overview_ifd_index(0).unwrap_err(),
1648            crate::error::Error::OverviewHasNoTopLevelIfdIndex(0)
1649        ));
1650        assert_eq!(file.overview_ifd(0).unwrap().width(), 1);
1651        assert_eq!(file.overview_ifd(0).unwrap().height(), 1);
1652
1653        let overview = file.read_overview::<u8>(0).unwrap();
1654        assert_eq!(overview.shape(), &[1, 1]);
1655        let (values, offset) = overview.into_raw_vec_and_offset();
1656        assert_eq!(offset, Some(0));
1657        assert_eq!(values, vec![99]);
1658    }
1659
1660    #[test]
1661    fn deduplicates_overviews_referenced_by_both_ifd_chains() {
1662        let mut bytes = build_geotiff_with_subifd_overview();
1663        let child_ifd_offset = classic_inline_long_tag(&bytes, 330);
1664        overwrite_first_ifd_next_pointer(&mut bytes, child_ifd_offset);
1665
1666        let file = GeoTiffFile::from_bytes(bytes).unwrap();
1667        assert_eq!(file.overview_count(), 1);
1668        assert_eq!(file.overview_ifd_index(0).unwrap(), 1);
1669    }
1670
1671    #[test]
1672    fn rejects_malformed_geotiff_tag_types_and_counts() {
1673        let mut wrong_type = build_simple_geotiff(false);
1674        overwrite_classic_tag_type_or_count(&mut wrong_type, TAG_GDAL_NODATA, Some(1), None);
1675        assert!(matches!(
1676            GeoTiffFile::from_bytes(wrong_type),
1677            Err(crate::error::Error::InvalidGeoTiffTag {
1678                tag: TAG_GDAL_NODATA,
1679                ..
1680            })
1681        ));
1682
1683        let mut wrong_count = build_simple_geotiff(false);
1684        overwrite_classic_tag_type_or_count(&mut wrong_count, TAG_MODEL_PIXEL_SCALE, None, Some(4));
1685        assert!(matches!(
1686            GeoTiffFile::from_bytes(wrong_count),
1687            Err(crate::error::Error::InvalidGeoTiffTag {
1688                tag: TAG_MODEL_PIXEL_SCALE,
1689                ..
1690            })
1691        ));
1692    }
1693
1694    #[test]
1695    fn overview_candidates_must_not_grow_either_dimension() {
1696        let mut bytes = build_geotiff_with_overview();
1697        let overview_offset = first_ifd_next_pointer(&bytes) as usize;
1698        overwrite_classic_inline_long_tag_at(&mut bytes, overview_offset, 257, 3);
1699
1700        let file = GeoTiffFile::from_bytes(bytes).unwrap();
1701        assert_eq!(file.overview_count(), 0);
1702    }
1703
1704    #[test]
1705    fn discovers_nested_subifd_overviews() {
1706        let file = GeoTiffFile::from_bytes(build_geotiff_with_nested_subifd_overviews()).unwrap();
1707        assert_eq!(file.overview_count(), 2);
1708        assert_eq!(file.overview_ifd(0).unwrap().width(), 2);
1709        assert_eq!(file.overview_ifd(1).unwrap().width(), 1);
1710        assert!(matches!(
1711            file.overview_ifd_index(0).unwrap_err(),
1712            crate::error::Error::OverviewHasNoTopLevelIfdIndex(0)
1713        ));
1714        assert!(matches!(
1715            file.overview_ifd_index(1).unwrap_err(),
1716            crate::error::Error::OverviewHasNoTopLevelIfdIndex(1)
1717        ));
1718
1719        let first = file.read_overview::<u8>(0).unwrap();
1720        assert_eq!(first.shape(), &[2, 2]);
1721        let second = file.read_overview::<u8>(1).unwrap();
1722        assert_eq!(second.shape(), &[1, 1]);
1723        assert_eq!(second[[0, 0]], 99);
1724    }
1725
1726    #[test]
1727    fn rejects_subifd_overview_node_budget() {
1728        let error = match GeoTiffFile::from_bytes(build_geotiff_exceeding_subifd_node_budget()) {
1729            Ok(_) => panic!("expected SubIFD overview node budget error"),
1730            Err(error) => error,
1731        };
1732        assert!(error
1733            .to_string()
1734            .contains("SubIFD overview traversal exceeds node budget"));
1735    }
1736
1737    #[test]
1738    fn rejects_subifd_overview_depth_budget() {
1739        let error = match GeoTiffFile::from_bytes(build_geotiff_exceeding_subifd_depth_budget()) {
1740            Ok(_) => panic!("expected SubIFD overview depth budget error"),
1741            Err(error) => error,
1742        };
1743        assert!(error
1744            .to_string()
1745            .contains("SubIFD overview traversal exceeds depth budget"));
1746    }
1747
1748    #[test]
1749    fn reads_base_raster_window() {
1750        let file = GeoTiffFile::from_bytes(build_simple_geotiff(false)).unwrap();
1751        let window = file.read_window::<u8>(1, 0, 1, 2).unwrap();
1752        assert_eq!(window.shape(), &[1, 2]);
1753        let (values, offset) = window.into_raw_vec_and_offset();
1754        assert_eq!(offset, Some(0));
1755        assert_eq!(values, vec![30, 40]);
1756    }
1757
1758    #[test]
1759    fn reads_overview_window() {
1760        let file = GeoTiffFile::from_bytes(build_geotiff_with_overview()).unwrap();
1761        let window = file.read_overview_window::<u8>(0, 0, 0, 1, 1).unwrap();
1762        assert_eq!(window.shape(), &[1, 1]);
1763        let (values, offset) = window.into_raw_vec_and_offset();
1764        assert_eq!(offset, Some(0));
1765        assert_eq!(values, vec![99]);
1766    }
1767
1768    #[test]
1769    fn prefers_non_ghost_base_ifd_for_cog_like_layouts() {
1770        let file = GeoTiffFile::from_bytes(build_cog_like_geotiff_with_ghost_ifd()).unwrap();
1771        assert_eq!(file.base_ifd_index(), 2);
1772        assert_eq!(file.width(), 4);
1773        assert_eq!(file.height(), 4);
1774        assert_eq!(file.geo_bounds(), Some([100.0, 192.0, 108.0, 200.0]));
1775        assert_eq!(file.overview_count(), 1);
1776        assert_eq!(file.overview_ifd_index(0).unwrap(), 1);
1777
1778        let base = file.read_raster::<u8>().unwrap();
1779        assert_eq!(base.shape(), &[4, 4]);
1780        let (values, offset) = base.into_raw_vec_and_offset();
1781        assert_eq!(offset, Some(0));
1782        assert_eq!(values, (1u8..=16).collect::<Vec<_>>());
1783
1784        let overview = file.read_overview::<u8>(0).unwrap();
1785        assert_eq!(overview.shape(), &[2, 2]);
1786        let (values, offset) = overview.into_raw_vec_and_offset();
1787        assert_eq!(offset, Some(0));
1788        assert_eq!(values, vec![50, 60, 70, 80]);
1789    }
1790
1791    #[test]
1792    fn rejects_zero_rows_per_strip_without_panicking() {
1793        let mut bytes = build_simple_geotiff(false);
1794        overwrite_classic_inline_long_tag(&mut bytes, 278, 0);
1795
1796        let file = GeoTiffFile::from_bytes(bytes).unwrap();
1797        assert_eq!(file.epsg(), Some(4326));
1798
1799        let error = file.tiff().read_image_bytes(0).unwrap_err();
1800        assert!(error.to_string().contains("RowsPerStrip"));
1801    }
1802}