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