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//!
7//! # Example
8//!
9//! ```no_run
10//! # #[cfg(feature = "local")]
11//! # fn main() -> Result<(), geotiff_reader::Error> {
12//! use geotiff_reader::GeoTiffFile;
13//!
14//! let file = GeoTiffFile::open("dem.tif")?;
15//! println!("EPSG: {:?}", file.epsg());
16//! println!("bounds: {:?}", file.geo_bounds());
17//! println!("size: {}x{}", file.width(), file.height());
18//! # Ok(())
19//! # }
20//! # #[cfg(not(feature = "local"))]
21//! # fn main() {}
22//! ```
23
24pub mod crs;
25pub mod error;
26pub mod geokeys;
27pub mod transform;
28
29#[cfg(feature = "cog")]
30pub mod cog;
31
32pub use error::{Error, Result};
33
34#[cfg(feature = "local")]
35use crs::CrsInfo;
36#[cfg(feature = "local")]
37use geokeys::GeoKeyDirectory;
38#[cfg(feature = "local")]
39use ndarray::ArrayD;
40#[cfg(feature = "local")]
41use std::path::Path;
42#[cfg(feature = "local")]
43use tiff_reader::{OpenOptions as TiffOpenOptions, TagValue, TiffFile, TiffSample};
44#[cfg(feature = "local")]
45use transform::GeoTransform;
46
47#[cfg(feature = "local")]
48const TAG_MODEL_PIXEL_SCALE: u16 = 33550;
49#[cfg(feature = "local")]
50const TAG_MODEL_TIEPOINT: u16 = 33922;
51#[cfg(feature = "local")]
52const TAG_MODEL_TRANSFORMATION: u16 = 34264;
53#[cfg(feature = "local")]
54const TAG_GEO_KEY_DIRECTORY: u16 = 34735;
55#[cfg(feature = "local")]
56const TAG_GEO_DOUBLE_PARAMS: u16 = 34736;
57#[cfg(feature = "local")]
58const TAG_GEO_ASCII_PARAMS: u16 = 34737;
59#[cfg(feature = "local")]
60const TAG_GDAL_NODATA: u16 = 42113;
61#[cfg(feature = "local")]
62const TAG_NEW_SUBFILE_TYPE: u16 = 254;
63#[cfg(feature = "local")]
64const TAG_SUBFILE_TYPE: u16 = 255;
65
66/// A GeoTIFF file handle with geospatial metadata.
67#[cfg(feature = "local")]
68pub struct GeoTiffFile {
69    tiff: TiffFile,
70    geo_metadata: GeoMetadata,
71    crs: CrsInfo,
72    geokeys: GeoKeyDirectory,
73    transform: Option<GeoTransform>,
74    overview_ifds: Vec<usize>,
75}
76
77#[cfg(feature = "local")]
78pub use tiff_reader::OpenOptions as GeoTiffOpenOptions;
79
80/// Parsed geospatial metadata from GeoKeys and model tags.
81#[derive(Debug, Clone)]
82pub struct GeoMetadata {
83    /// EPSG code for the coordinate reference system, if present.
84    pub epsg: Option<u32>,
85    /// Model tiepoints: (I, J, K, X, Y, Z) tuples.
86    pub tiepoints: Vec<[f64; 6]>,
87    /// Pixel scale: (ScaleX, ScaleY, ScaleZ).
88    pub pixel_scale: Option<[f64; 3]>,
89    /// 4x4 model transformation matrix (row-major), if present.
90    pub transformation: Option<[f64; 16]>,
91    /// Nodata value as a string (parsed from GDAL_NODATA tag).
92    pub nodata: Option<String>,
93    /// Number of bands (samples per pixel).
94    pub band_count: u32,
95    /// Image width in pixels.
96    pub width: u32,
97    /// Image height in pixels.
98    pub height: u32,
99    /// Geographic bounds derived from the transform.
100    pub geo_bounds: Option<[f64; 4]>,
101}
102
103#[cfg(feature = "local")]
104impl GeoTiffFile {
105    /// Open a GeoTIFF file from disk.
106    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
107        Self::open_with_options(path, TiffOpenOptions::default())
108    }
109
110    /// Open a GeoTIFF file from disk with explicit TIFF decoder options.
111    pub fn open_with_options<P: AsRef<Path>>(path: P, options: GeoTiffOpenOptions) -> Result<Self> {
112        let tiff = TiffFile::open_with_options(path, options)?;
113        Self::from_tiff(tiff)
114    }
115
116    /// Open a GeoTIFF from an owned byte buffer.
117    pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
118        Self::from_bytes_with_options(data, TiffOpenOptions::default())
119    }
120
121    /// Open a GeoTIFF from bytes with explicit TIFF decoder options.
122    pub fn from_bytes_with_options(data: Vec<u8>, options: GeoTiffOpenOptions) -> Result<Self> {
123        let tiff = TiffFile::from_bytes_with_options(data, options)?;
124        Self::from_tiff(tiff)
125    }
126
127    pub(crate) fn from_tiff(tiff: TiffFile) -> Result<Self> {
128        let ifd = tiff.ifd(0)?;
129        let geokeys = parse_geokey_directory(ifd)?;
130        let crs = CrsInfo::from_geokeys(&geokeys);
131        let epsg = crs.epsg();
132        let tiepoints = parse_tiepoints(ifd);
133        let pixel_scale =
134            parse_fixed_len_double_tag::<3>(ifd.tag(TAG_MODEL_PIXEL_SCALE).map(|tag| &tag.value));
135        let transformation = parse_fixed_len_double_tag::<16>(
136            ifd.tag(TAG_MODEL_TRANSFORMATION).map(|tag| &tag.value),
137        );
138        let transform = transformation
139            .as_ref()
140            .map(GeoTransform::from_transformation_matrix)
141            .or_else(|| {
142                let tiepoint = tiepoints.first()?;
143                let scale = pixel_scale.as_ref()?;
144                Some(GeoTransform::from_tiepoint_and_scale_with_raster_type(
145                    tiepoint,
146                    scale,
147                    crs.raster_type_enum(),
148                ))
149            });
150        let geo_bounds = transform
151            .as_ref()
152            .map(|gt| gt.bounds(ifd.width(), ifd.height()));
153        let overview_ifds = tiff
154            .ifds()
155            .iter()
156            .enumerate()
157            .skip(1)
158            .filter_map(|(index, candidate)| is_overview_ifd(ifd, candidate).then_some(index))
159            .collect();
160
161        let geo_metadata = GeoMetadata {
162            epsg,
163            tiepoints,
164            pixel_scale,
165            transformation,
166            nodata: parse_nodata(ifd),
167            band_count: ifd.samples_per_pixel() as u32,
168            width: ifd.width(),
169            height: ifd.height(),
170            geo_bounds,
171        };
172
173        Ok(Self {
174            tiff,
175            geo_metadata,
176            crs,
177            geokeys,
178            transform,
179            overview_ifds,
180        })
181    }
182
183    /// Returns the underlying TIFF file.
184    pub fn tiff(&self) -> &TiffFile {
185        &self.tiff
186    }
187
188    /// Returns the parsed GeoTIFF metadata.
189    pub fn metadata(&self) -> &GeoMetadata {
190        &self.geo_metadata
191    }
192
193    /// Returns the EPSG code of the coordinate reference system, if present.
194    pub fn epsg(&self) -> Option<u32> {
195        self.geo_metadata.epsg
196    }
197
198    /// Returns the extracted CRS information.
199    pub fn crs(&self) -> &CrsInfo {
200        &self.crs
201    }
202
203    /// Returns the parsed GeoKey directory.
204    pub fn geokeys(&self) -> &GeoKeyDirectory {
205        &self.geokeys
206    }
207
208    /// Returns the affine transform, if present.
209    pub fn transform(&self) -> Option<&GeoTransform> {
210        self.transform.as_ref()
211    }
212
213    /// Returns the geographic bounds as `(min_x, min_y, max_x, max_y)`.
214    pub fn geo_bounds(&self) -> Option<[f64; 4]> {
215        self.geo_metadata.geo_bounds
216    }
217
218    /// Convert a pixel coordinate to map coordinates.
219    pub fn pixel_to_geo(&self, col: f64, row: f64) -> Option<(f64, f64)> {
220        self.transform
221            .map(|transform| transform.pixel_to_geo(col, row))
222    }
223
224    /// Convert map coordinates to pixel coordinates.
225    pub fn geo_to_pixel(&self, x: f64, y: f64) -> Option<(f64, f64)> {
226        self.transform
227            .and_then(|transform| transform.geo_to_pixel(x, y))
228    }
229
230    /// Returns the image width in pixels.
231    pub fn width(&self) -> u32 {
232        self.geo_metadata.width
233    }
234
235    /// Returns the image height in pixels.
236    pub fn height(&self) -> u32 {
237        self.geo_metadata.height
238    }
239
240    /// Returns the number of bands.
241    pub fn band_count(&self) -> u32 {
242        self.geo_metadata.band_count
243    }
244
245    /// Returns the nodata value, if set.
246    pub fn nodata(&self) -> Option<&str> {
247        self.geo_metadata.nodata.as_deref()
248    }
249
250    /// Returns the number of internal overview IFDs.
251    pub fn overview_count(&self) -> usize {
252        self.overview_ifds.len()
253    }
254
255    /// Returns the TIFF IFD index of the requested overview.
256    pub fn overview_ifd_index(&self, overview_index: usize) -> Result<usize> {
257        self.overview_ifds
258            .get(overview_index)
259            .copied()
260            .ok_or(Error::OverviewNotFound(overview_index))
261    }
262
263    /// Decode the base-resolution raster into a typed ndarray.
264    pub fn read_raster<T: TiffSample>(&self) -> Result<ArrayD<T>> {
265        self.tiff.read_image::<T>(0).map_err(Into::into)
266    }
267
268    /// Decode a base-resolution pixel window into a typed ndarray.
269    pub fn read_window<T: TiffSample>(
270        &self,
271        row_off: usize,
272        col_off: usize,
273        rows: usize,
274        cols: usize,
275    ) -> Result<ArrayD<T>> {
276        self.tiff
277            .read_window::<T>(0, row_off, col_off, rows, cols)
278            .map_err(Into::into)
279    }
280
281    /// Decode an overview raster into a typed ndarray.
282    pub fn read_overview<T: TiffSample>(&self, overview_index: usize) -> Result<ArrayD<T>> {
283        let ifd_index = self.overview_ifd_index(overview_index)?;
284        self.tiff.read_image::<T>(ifd_index).map_err(Into::into)
285    }
286
287    /// Decode an overview pixel window into a typed ndarray.
288    pub fn read_overview_window<T: TiffSample>(
289        &self,
290        overview_index: usize,
291        row_off: usize,
292        col_off: usize,
293        rows: usize,
294        cols: usize,
295    ) -> Result<ArrayD<T>> {
296        let ifd_index = self.overview_ifd_index(overview_index)?;
297        self.tiff
298            .read_window::<T>(ifd_index, row_off, col_off, rows, cols)
299            .map_err(Into::into)
300    }
301}
302
303#[cfg(feature = "local")]
304fn is_overview_ifd(base: &tiff_reader::Ifd, candidate: &tiff_reader::Ifd) -> bool {
305    let smaller = candidate.width() < base.width() || candidate.height() < base.height();
306    if !smaller {
307        return false;
308    }
309
310    let same_layout = candidate.samples_per_pixel() == base.samples_per_pixel()
311        && candidate.bits_per_sample() == base.bits_per_sample()
312        && candidate.sample_format() == base.sample_format()
313        && candidate.photometric_interpretation() == base.photometric_interpretation();
314    if !same_layout {
315        return false;
316    }
317
318    candidate
319        .tag(TAG_NEW_SUBFILE_TYPE)
320        .and_then(|tag| tag.value.as_u64())
321        .map(|flags| flags & 0x1 != 0)
322        .or_else(|| {
323            candidate
324                .tag(TAG_SUBFILE_TYPE)
325                .and_then(|tag| tag.value.as_u16())
326                .map(|value| value == 2)
327        })
328        .unwrap_or(true)
329}
330
331#[cfg(feature = "local")]
332fn parse_geokey_directory(ifd: &tiff_reader::Ifd) -> Result<GeoKeyDirectory> {
333    let directory = ifd
334        .tag(TAG_GEO_KEY_DIRECTORY)
335        .and_then(|tag| match &tag.value {
336            TagValue::Short(values) => Some(values.as_slice()),
337            _ => None,
338        })
339        .ok_or(Error::NotGeoTiff)?;
340    let double_params = ifd
341        .tag(TAG_GEO_DOUBLE_PARAMS)
342        .and_then(|tag| tag.value.as_f64_vec())
343        .unwrap_or_default();
344    let ascii_params = ifd
345        .tag(TAG_GEO_ASCII_PARAMS)
346        .and_then(|tag| tag.value.as_str())
347        .unwrap_or("");
348    GeoKeyDirectory::parse(directory, &double_params, ascii_params)
349        .ok_or(Error::InvalidGeoKeyDirectory)
350}
351
352#[cfg(feature = "local")]
353fn parse_fixed_len_double_tag<const N: usize>(value: Option<&TagValue>) -> Option<[f64; N]> {
354    let values = value.and_then(TagValue::as_f64_vec)?;
355    if values.len() < N {
356        return None;
357    }
358    let mut out = [0.0; N];
359    out.copy_from_slice(&values[..N]);
360    Some(out)
361}
362
363#[cfg(feature = "local")]
364fn parse_tiepoints(ifd: &tiff_reader::Ifd) -> Vec<[f64; 6]> {
365    let values = ifd
366        .tag(TAG_MODEL_TIEPOINT)
367        .and_then(|tag| tag.value.as_f64_vec())
368        .unwrap_or_default();
369    values
370        .chunks_exact(6)
371        .map(|chunk| [chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5]])
372        .collect()
373}
374
375#[cfg(feature = "local")]
376fn parse_nodata(ifd: &tiff_reader::Ifd) -> Option<String> {
377    ifd.tag(TAG_GDAL_NODATA)
378        .and_then(|tag| tag.value.as_str())
379        .map(ToOwned::to_owned)
380}
381
382#[cfg(test)]
383#[cfg(feature = "local")]
384mod tests {
385    use super::GeoTiffFile;
386
387    #[derive(Clone)]
388    struct TestIfdSpec {
389        entries: Vec<(u16, u16, u32, Vec<u8>)>,
390        image_data: Vec<u8>,
391    }
392
393    fn le_u16(value: u16) -> [u8; 2] {
394        value.to_le_bytes()
395    }
396
397    fn le_u32(value: u32) -> [u8; 4] {
398        value.to_le_bytes()
399    }
400
401    fn le_f64(value: f64) -> [u8; 8] {
402        value.to_le_bytes()
403    }
404
405    fn build_classic_tiff(ifds: &[TestIfdSpec]) -> Vec<u8> {
406        let mut ifd_offsets = Vec::with_capacity(ifds.len());
407        let mut cursor = 8usize;
408        for ifd in ifds {
409            ifd_offsets.push(cursor as u32);
410            let deferred_len: usize = ifd
411                .entries
412                .iter()
413                .filter(|(tag, _, _, value)| *tag != 273 && value.len() > 4)
414                .map(|(_, _, _, value)| value.len())
415                .sum();
416            cursor += 2 + ifd.entries.len() * 12 + 4 + ifd.image_data.len() + deferred_len;
417        }
418
419        let mut bytes = Vec::with_capacity(cursor);
420        bytes.extend_from_slice(b"II");
421        bytes.extend_from_slice(&le_u16(42));
422        bytes.extend_from_slice(&le_u32(ifd_offsets.first().copied().unwrap_or(0)));
423
424        for (ifd_index, ifd) in ifds.iter().enumerate() {
425            let ifd_offset = ifd_offsets[ifd_index] as usize;
426            debug_assert_eq!(bytes.len(), ifd_offset);
427
428            let ifd_size = 2 + ifd.entries.len() * 12 + 4;
429            let mut next_data_offset = ifd_offset + ifd_size;
430            let image_offset = next_data_offset as u32;
431            next_data_offset += ifd.image_data.len();
432
433            bytes.extend_from_slice(&le_u16(ifd.entries.len() as u16));
434            let mut deferred = Vec::new();
435            for (tag, ty, count, value) in &ifd.entries {
436                bytes.extend_from_slice(&le_u16(*tag));
437                bytes.extend_from_slice(&le_u16(*ty));
438                bytes.extend_from_slice(&le_u32(*count));
439                if *tag == 273 {
440                    bytes.extend_from_slice(&le_u32(image_offset));
441                } else if value.len() <= 4 {
442                    let mut inline = [0u8; 4];
443                    inline[..value.len()].copy_from_slice(value);
444                    bytes.extend_from_slice(&inline);
445                } else {
446                    bytes.extend_from_slice(&le_u32(next_data_offset as u32));
447                    next_data_offset += value.len();
448                    deferred.push(value.clone());
449                }
450            }
451
452            let next_ifd_offset = ifd_offsets.get(ifd_index + 1).copied().unwrap_or(0);
453            bytes.extend_from_slice(&le_u32(next_ifd_offset));
454            bytes.extend_from_slice(&ifd.image_data);
455            for value in deferred {
456                bytes.extend_from_slice(&value);
457            }
458            debug_assert_eq!(bytes.len(), next_data_offset);
459        }
460
461        bytes
462    }
463
464    fn build_simple_geotiff(pixel_is_point: bool) -> Vec<u8> {
465        let image_data = vec![10u8, 20, 30, 40];
466        let tiepoints = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0];
467        let scales = [2.0, 2.0, 0.0];
468        let geo_keys = if pixel_is_point {
469            vec![
470                1, 1, 0, 3, // header
471                1024, 0, 1, 2, // model type = Geographic
472                1025, 0, 1, 2, // raster type = PixelIsPoint
473                2048, 0, 1, 4326, // EPSG:4326
474            ]
475        } else {
476            vec![
477                1, 1, 0, 2, // header
478                1024, 0, 1, 2, // model type = Geographic
479                2048, 0, 1, 4326, // EPSG:4326
480            ]
481        };
482        let nodata = b"-9999\0".to_vec();
483
484        build_classic_tiff(&[TestIfdSpec {
485            image_data,
486            entries: vec![
487                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
488                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
489                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
490                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
491                (273u16, 4u16, 1u32, vec![]),
492                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
493                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
494                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
495                (
496                    33550u16,
497                    12u16,
498                    3u32,
499                    scales.iter().flat_map(|value| le_f64(*value)).collect(),
500                ),
501                (
502                    33922u16,
503                    12u16,
504                    6u32,
505                    tiepoints.iter().flat_map(|value| le_f64(*value)).collect(),
506                ),
507                (
508                    34735u16,
509                    3u16,
510                    geo_keys.len() as u32,
511                    geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
512                ),
513                (42113u16, 2u16, nodata.len() as u32, nodata),
514            ],
515        }])
516    }
517
518    fn build_geotiff_with_overview() -> Vec<u8> {
519        let base = TestIfdSpec {
520            image_data: vec![10u8, 20, 30, 40],
521            entries: vec![
522                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
523                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
524                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
525                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
526                (273u16, 4u16, 1u32, vec![]),
527                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
528                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
529                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
530                (
531                    33550u16,
532                    12u16,
533                    3u32,
534                    [2.0, 2.0, 0.0]
535                        .iter()
536                        .flat_map(|value| le_f64(*value))
537                        .collect(),
538                ),
539                (
540                    33922u16,
541                    12u16,
542                    6u32,
543                    [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
544                        .iter()
545                        .flat_map(|value| le_f64(*value))
546                        .collect(),
547                ),
548                (
549                    34735u16,
550                    3u16,
551                    12u32,
552                    [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326]
553                        .iter()
554                        .flat_map(|value| le_u16(*value))
555                        .collect(),
556                ),
557            ],
558        };
559        let overview = TestIfdSpec {
560            image_data: vec![99u8],
561            entries: vec![
562                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
563                (256u16, 4u16, 1u32, le_u32(1).to_vec()),
564                (257u16, 4u16, 1u32, le_u32(1).to_vec()),
565                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
566                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
567                (273u16, 4u16, 1u32, vec![]),
568                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
569                (278u16, 4u16, 1u32, le_u32(1).to_vec()),
570                (279u16, 4u16, 1u32, le_u32(1).to_vec()),
571            ],
572        };
573
574        build_classic_tiff(&[base, overview])
575    }
576
577    #[test]
578    fn parses_geotiff_metadata_and_reads_raster() {
579        let file = GeoTiffFile::from_bytes(build_simple_geotiff(false)).unwrap();
580        assert_eq!(file.epsg(), Some(4326));
581        assert_eq!(file.width(), 2);
582        assert_eq!(file.height(), 2);
583        assert_eq!(file.band_count(), 1);
584        assert_eq!(file.nodata(), Some("-9999"));
585        assert_eq!(file.geo_bounds(), Some([100.0, 196.0, 104.0, 200.0]));
586
587        let raster = file.read_raster::<u8>().unwrap();
588        assert_eq!(raster.shape(), &[2, 2]);
589        let (values, offset) = raster.into_raw_vec_and_offset();
590        assert_eq!(offset, Some(0));
591        assert_eq!(values, vec![10, 20, 30, 40]);
592    }
593
594    #[test]
595    fn pixel_is_point_metadata_shifts_bounds_to_outer_edges() {
596        let file = GeoTiffFile::from_bytes(build_simple_geotiff(true)).unwrap();
597        assert_eq!(file.geo_bounds(), Some([99.0, 197.0, 103.0, 201.0]));
598
599        let transform = file.transform().unwrap();
600        let (center_x, center_y) = transform.pixel_to_geo(0.5, 0.5);
601        assert_eq!((center_x, center_y), (100.0, 200.0));
602    }
603
604    #[test]
605    fn discovers_reduced_resolution_overviews() {
606        let file = GeoTiffFile::from_bytes(build_geotiff_with_overview()).unwrap();
607        assert_eq!(file.overview_count(), 1);
608        assert_eq!(file.overview_ifd_index(0).unwrap(), 1);
609
610        let overview = file.read_overview::<u8>(0).unwrap();
611        assert_eq!(overview.shape(), &[1, 1]);
612        let (values, offset) = overview.into_raw_vec_and_offset();
613        assert_eq!(offset, Some(0));
614        assert_eq!(values, vec![99]);
615    }
616
617    #[test]
618    fn reads_base_raster_window() {
619        let file = GeoTiffFile::from_bytes(build_simple_geotiff(false)).unwrap();
620        let window = file.read_window::<u8>(1, 0, 1, 2).unwrap();
621        assert_eq!(window.shape(), &[1, 2]);
622        let (values, offset) = window.into_raw_vec_and_offset();
623        assert_eq!(offset, Some(0));
624        assert_eq!(values, vec![30, 40]);
625    }
626
627    #[test]
628    fn reads_overview_window() {
629        let file = GeoTiffFile::from_bytes(build_geotiff_with_overview()).unwrap();
630        let window = file.read_overview_window::<u8>(0, 0, 0, 1, 1).unwrap();
631        assert_eq!(window.shape(), &[1, 1]);
632        let (values, offset) = window.into_raw_vec_and_offset();
633        assert_eq!(offset, Some(0));
634        assert_eq!(values, vec![99]);
635    }
636}