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::path::Path;
44#[cfg(feature = "local")]
45use tiff_reader::{OpenOptions as TiffOpenOptions, TagValue, TiffFile, TiffSample};
46#[cfg(feature = "local")]
47use transform::GeoTransform;
48
49#[cfg(feature = "local")]
50use geotiff_core::tags::{
51    TAG_GDAL_NODATA, TAG_GEO_ASCII_PARAMS, TAG_GEO_DOUBLE_PARAMS, TAG_GEO_KEY_DIRECTORY,
52    TAG_MODEL_PIXEL_SCALE, TAG_MODEL_TIEPOINT, TAG_MODEL_TRANSFORMATION, TAG_NEW_SUBFILE_TYPE,
53    TAG_SUBFILE_TYPE,
54};
55
56/// A GeoTIFF file handle with geospatial metadata.
57#[cfg(feature = "local")]
58pub struct GeoTiffFile {
59    tiff: TiffFile,
60    geo_metadata: GeoMetadata,
61    crs: CrsInfo,
62    geokeys: GeoKeyDirectory,
63    transform: Option<GeoTransform>,
64    base_ifd_index: usize,
65    overview_ifds: Vec<usize>,
66}
67
68#[cfg(feature = "local")]
69pub use tiff_reader::OpenOptions as GeoTiffOpenOptions;
70
71pub use geotiff_core::GeoMetadata;
72
73#[cfg(feature = "local")]
74impl GeoTiffFile {
75    /// Open a GeoTIFF file from disk.
76    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
77        Self::open_with_options(path, TiffOpenOptions::default())
78    }
79
80    /// Open a GeoTIFF file from disk with explicit TIFF decoder options.
81    pub fn open_with_options<P: AsRef<Path>>(path: P, options: GeoTiffOpenOptions) -> Result<Self> {
82        let tiff = TiffFile::open_with_options(path, options)?;
83        Self::from_tiff(tiff)
84    }
85
86    /// Open a GeoTIFF from an owned byte buffer.
87    pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
88        Self::from_bytes_with_options(data, TiffOpenOptions::default())
89    }
90
91    /// Open a GeoTIFF from bytes with explicit TIFF decoder options.
92    pub fn from_bytes_with_options(data: Vec<u8>, options: GeoTiffOpenOptions) -> Result<Self> {
93        let tiff = TiffFile::from_bytes_with_options(data, options)?;
94        Self::from_tiff(tiff)
95    }
96
97    pub(crate) fn from_tiff(tiff: TiffFile) -> Result<Self> {
98        let metadata_ifd_index = find_metadata_ifd_index(tiff.ifds())?;
99        let metadata_ifd = tiff.ifd(metadata_ifd_index)?;
100        let geokeys = parse_geokey_directory(metadata_ifd)?;
101        let crs = CrsInfo::from_geokeys(&geokeys);
102        let epsg = crs.epsg();
103        let tiepoints = parse_tiepoints(metadata_ifd);
104        let pixel_scale = parse_fixed_len_double_tag::<3>(
105            metadata_ifd
106                .tag(TAG_MODEL_PIXEL_SCALE)
107                .map(|tag| &tag.value),
108        );
109        let transformation = parse_fixed_len_double_tag::<16>(
110            metadata_ifd
111                .tag(TAG_MODEL_TRANSFORMATION)
112                .map(|tag| &tag.value),
113        );
114        let transform = transformation
115            .as_ref()
116            .map(GeoTransform::from_transformation_matrix)
117            .or_else(|| {
118                let tiepoint = tiepoints.first()?;
119                let scale = pixel_scale.as_ref()?;
120                Some(GeoTransform::from_tiepoint_and_scale_with_raster_type(
121                    tiepoint,
122                    scale,
123                    crs.raster_type_enum(),
124                ))
125            });
126        let base_ifd_index = find_base_ifd_index(tiff.ifds(), metadata_ifd_index);
127        let base_ifd = tiff.ifd(base_ifd_index)?;
128        let geo_bounds = transform
129            .as_ref()
130            .map(|gt| gt.bounds(base_ifd.width(), base_ifd.height()));
131        let overview_ifds = tiff
132            .ifds()
133            .iter()
134            .enumerate()
135            .filter_map(|(index, candidate)| {
136                (index != base_ifd_index
137                    && index != metadata_ifd_index
138                    && is_overview_ifd(base_ifd, candidate))
139                .then_some(index)
140            })
141            .collect();
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 TIFF IFD index of the requested overview.
239    pub fn overview_ifd_index(&self, overview_index: usize) -> Result<usize> {
240        self.overview_ifds
241            .get(overview_index)
242            .copied()
243            .ok_or(Error::OverviewNotFound(overview_index))
244    }
245
246    /// Returns the TIFF IFD index of the base-resolution image.
247    pub fn base_ifd_index(&self) -> usize {
248        self.base_ifd_index
249    }
250
251    /// Decode the base-resolution raster into a typed ndarray.
252    pub fn read_raster<T: TiffSample>(&self) -> Result<ArrayD<T>> {
253        self.tiff
254            .read_image::<T>(self.base_ifd_index)
255            .map_err(Into::into)
256    }
257
258    /// Decode a base-resolution pixel window into a typed ndarray.
259    pub fn read_window<T: TiffSample>(
260        &self,
261        row_off: usize,
262        col_off: usize,
263        rows: usize,
264        cols: usize,
265    ) -> Result<ArrayD<T>> {
266        self.tiff
267            .read_window::<T>(self.base_ifd_index, row_off, col_off, rows, cols)
268            .map_err(Into::into)
269    }
270
271    /// Decode an overview raster into a typed ndarray.
272    pub fn read_overview<T: TiffSample>(&self, overview_index: usize) -> Result<ArrayD<T>> {
273        let ifd_index = self.overview_ifd_index(overview_index)?;
274        self.tiff.read_image::<T>(ifd_index).map_err(Into::into)
275    }
276
277    /// Decode an overview pixel window into a typed ndarray.
278    pub fn read_overview_window<T: TiffSample>(
279        &self,
280        overview_index: usize,
281        row_off: usize,
282        col_off: usize,
283        rows: usize,
284        cols: usize,
285    ) -> Result<ArrayD<T>> {
286        let ifd_index = self.overview_ifd_index(overview_index)?;
287        self.tiff
288            .read_window::<T>(ifd_index, row_off, col_off, rows, cols)
289            .map_err(Into::into)
290    }
291}
292
293#[cfg(feature = "local")]
294fn is_overview_ifd(base: &tiff_reader::Ifd, candidate: &tiff_reader::Ifd) -> bool {
295    let smaller = candidate.width() < base.width() || candidate.height() < base.height();
296    if !smaller {
297        return false;
298    }
299
300    let same_layout = candidate.samples_per_pixel() == base.samples_per_pixel()
301        && candidate.bits_per_sample() == base.bits_per_sample()
302        && candidate.sample_format() == base.sample_format()
303        && candidate.photometric_interpretation() == base.photometric_interpretation();
304    if !same_layout {
305        return false;
306    }
307
308    has_reduced_resolution_flag(candidate)
309        || (candidate.tag(TAG_NEW_SUBFILE_TYPE).is_none()
310            && candidate.tag(TAG_SUBFILE_TYPE).is_none())
311}
312
313#[cfg(feature = "local")]
314fn find_metadata_ifd_index(ifds: &[tiff_reader::Ifd]) -> Result<usize> {
315    ifds.iter()
316        .position(|ifd| ifd.tag(TAG_GEO_KEY_DIRECTORY).is_some())
317        .ok_or(Error::NotGeoTiff)
318}
319
320#[cfg(feature = "local")]
321fn find_base_ifd_index(ifds: &[tiff_reader::Ifd], metadata_ifd_index: usize) -> usize {
322    let metadata_ifd = &ifds[metadata_ifd_index];
323    if !has_reduced_resolution_flag(metadata_ifd) {
324        return metadata_ifd_index;
325    }
326
327    ifds.iter()
328        .enumerate()
329        .skip(metadata_ifd_index + 1)
330        .find_map(|(index, ifd)| (!has_reduced_resolution_flag(ifd)).then_some(index))
331        .unwrap_or(metadata_ifd_index)
332}
333
334#[cfg(feature = "local")]
335fn has_reduced_resolution_flag(ifd: &tiff_reader::Ifd) -> bool {
336    ifd.tag(TAG_NEW_SUBFILE_TYPE)
337        .and_then(|tag| tag.value.as_u64())
338        .map(|flags| flags & 0x1 != 0)
339        .or_else(|| {
340            ifd.tag(TAG_SUBFILE_TYPE)
341                .and_then(|tag| tag.value.as_u16())
342                .map(|value| value == 2)
343        })
344        .unwrap_or(false)
345}
346
347#[cfg(feature = "local")]
348fn parse_geokey_directory(ifd: &tiff_reader::Ifd) -> Result<GeoKeyDirectory> {
349    let directory = ifd
350        .tag(TAG_GEO_KEY_DIRECTORY)
351        .and_then(|tag| match &tag.value {
352            TagValue::Short(values) => Some(values.as_slice()),
353            _ => None,
354        })
355        .ok_or(Error::NotGeoTiff)?;
356    let double_params = ifd
357        .tag(TAG_GEO_DOUBLE_PARAMS)
358        .and_then(|tag| tag.value.as_f64_vec())
359        .unwrap_or_default();
360    let ascii_params = ifd
361        .tag(TAG_GEO_ASCII_PARAMS)
362        .and_then(|tag| tag.value.as_str())
363        .unwrap_or("");
364    GeoKeyDirectory::parse(directory, &double_params, ascii_params)
365        .ok_or(Error::InvalidGeoKeyDirectory)
366}
367
368#[cfg(feature = "local")]
369fn parse_fixed_len_double_tag<const N: usize>(value: Option<&TagValue>) -> Option<[f64; N]> {
370    let values = value.and_then(TagValue::as_f64_vec)?;
371    if values.len() < N {
372        return None;
373    }
374    let mut out = [0.0; N];
375    out.copy_from_slice(&values[..N]);
376    Some(out)
377}
378
379#[cfg(feature = "local")]
380fn parse_tiepoints(ifd: &tiff_reader::Ifd) -> Vec<[f64; 6]> {
381    let values = ifd
382        .tag(TAG_MODEL_TIEPOINT)
383        .and_then(|tag| tag.value.as_f64_vec())
384        .unwrap_or_default();
385    values
386        .chunks_exact(6)
387        .map(|chunk| [chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5]])
388        .collect()
389}
390
391#[cfg(feature = "local")]
392fn parse_nodata(ifd: &tiff_reader::Ifd) -> Option<String> {
393    ifd.tag(TAG_GDAL_NODATA)
394        .and_then(|tag| tag.value.as_str())
395        .map(ToOwned::to_owned)
396}
397
398#[cfg(test)]
399#[cfg(feature = "local")]
400mod tests {
401    use super::GeoTiffFile;
402
403    #[derive(Clone)]
404    struct TestIfdSpec {
405        entries: Vec<(u16, u16, u32, Vec<u8>)>,
406        image_data: Vec<u8>,
407    }
408
409    fn le_u16(value: u16) -> [u8; 2] {
410        value.to_le_bytes()
411    }
412
413    fn le_u32(value: u32) -> [u8; 4] {
414        value.to_le_bytes()
415    }
416
417    fn le_f64(value: f64) -> [u8; 8] {
418        value.to_le_bytes()
419    }
420
421    fn inline_short(value: u16) -> Vec<u8> {
422        let mut bytes = [0u8; 4];
423        bytes[..2].copy_from_slice(&le_u16(value));
424        bytes.to_vec()
425    }
426
427    #[allow(clippy::too_many_arguments)]
428    fn build_lerc2_header_v2(
429        width: u32,
430        height: u32,
431        valid_pixel_count: u32,
432        image_type: i32,
433        max_z_error: f64,
434        z_min: f64,
435        z_max: f64,
436        payload_len: usize,
437    ) -> Vec<u8> {
438        let blob_size = 58 + 4 + payload_len;
439        let mut bytes = Vec::with_capacity(blob_size);
440        bytes.extend_from_slice(b"Lerc2 ");
441        bytes.extend_from_slice(&2i32.to_le_bytes());
442        bytes.extend_from_slice(&height.to_le_bytes());
443        bytes.extend_from_slice(&width.to_le_bytes());
444        bytes.extend_from_slice(&valid_pixel_count.to_le_bytes());
445        bytes.extend_from_slice(&8i32.to_le_bytes());
446        bytes.extend_from_slice(&(blob_size as i32).to_le_bytes());
447        bytes.extend_from_slice(&image_type.to_le_bytes());
448        bytes.extend_from_slice(&max_z_error.to_le_bytes());
449        bytes.extend_from_slice(&z_min.to_le_bytes());
450        bytes.extend_from_slice(&z_max.to_le_bytes());
451        bytes
452    }
453
454    fn build_classic_tiff(ifds: &[TestIfdSpec]) -> Vec<u8> {
455        let mut ifd_offsets = Vec::with_capacity(ifds.len());
456        let mut cursor = 8usize;
457        for ifd in ifds {
458            ifd_offsets.push(cursor as u32);
459            let deferred_len: usize = ifd
460                .entries
461                .iter()
462                .filter(|(tag, _, _, value)| *tag != 273 && value.len() > 4)
463                .map(|(_, _, _, value)| value.len())
464                .sum();
465            cursor += 2 + ifd.entries.len() * 12 + 4 + ifd.image_data.len() + deferred_len;
466        }
467
468        let mut bytes = Vec::with_capacity(cursor);
469        bytes.extend_from_slice(b"II");
470        bytes.extend_from_slice(&le_u16(42));
471        bytes.extend_from_slice(&le_u32(ifd_offsets.first().copied().unwrap_or(0)));
472
473        for (ifd_index, ifd) in ifds.iter().enumerate() {
474            let ifd_offset = ifd_offsets[ifd_index] as usize;
475            debug_assert_eq!(bytes.len(), ifd_offset);
476
477            let ifd_size = 2 + ifd.entries.len() * 12 + 4;
478            let mut next_data_offset = ifd_offset + ifd_size;
479            let image_offset = next_data_offset as u32;
480            next_data_offset += ifd.image_data.len();
481
482            bytes.extend_from_slice(&le_u16(ifd.entries.len() as u16));
483            let mut deferred = Vec::new();
484            for (tag, ty, count, value) in &ifd.entries {
485                bytes.extend_from_slice(&le_u16(*tag));
486                bytes.extend_from_slice(&le_u16(*ty));
487                bytes.extend_from_slice(&le_u32(*count));
488                if *tag == 273 {
489                    bytes.extend_from_slice(&le_u32(image_offset));
490                } else if value.len() <= 4 {
491                    let mut inline = [0u8; 4];
492                    inline[..value.len()].copy_from_slice(value);
493                    bytes.extend_from_slice(&inline);
494                } else {
495                    bytes.extend_from_slice(&le_u32(next_data_offset as u32));
496                    next_data_offset += value.len();
497                    deferred.push(value.clone());
498                }
499            }
500
501            let next_ifd_offset = ifd_offsets.get(ifd_index + 1).copied().unwrap_or(0);
502            bytes.extend_from_slice(&le_u32(next_ifd_offset));
503            bytes.extend_from_slice(&ifd.image_data);
504            for value in deferred {
505                bytes.extend_from_slice(&value);
506            }
507            debug_assert_eq!(bytes.len(), next_data_offset);
508        }
509
510        bytes
511    }
512
513    fn build_simple_geotiff(pixel_is_point: bool) -> Vec<u8> {
514        let image_data = vec![10u8, 20, 30, 40];
515        let tiepoints = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0];
516        let scales = [2.0, 2.0, 0.0];
517        let geo_keys = if pixel_is_point {
518            vec![
519                1, 1, 0, 3, // header
520                1024, 0, 1, 2, // model type = Geographic
521                1025, 0, 1, 2, // raster type = PixelIsPoint
522                2048, 0, 1, 4326, // EPSG:4326
523            ]
524        } else {
525            vec![
526                1, 1, 0, 2, // header
527                1024, 0, 1, 2, // model type = Geographic
528                2048, 0, 1, 4326, // EPSG:4326
529            ]
530        };
531        let nodata = b"-9999\0".to_vec();
532
533        build_classic_tiff(&[TestIfdSpec {
534            image_data,
535            entries: vec![
536                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
537                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
538                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
539                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
540                (273u16, 4u16, 1u32, vec![]),
541                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
542                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
543                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
544                (
545                    33550u16,
546                    12u16,
547                    3u32,
548                    scales.iter().flat_map(|value| le_f64(*value)).collect(),
549                ),
550                (
551                    33922u16,
552                    12u16,
553                    6u32,
554                    tiepoints.iter().flat_map(|value| le_f64(*value)).collect(),
555                ),
556                (
557                    34735u16,
558                    3u16,
559                    geo_keys.len() as u32,
560                    geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
561                ),
562                (42113u16, 2u16, nodata.len() as u32, nodata),
563            ],
564        }])
565    }
566
567    fn build_simple_lerc_geotiff() -> Vec<u8> {
568        let tiepoints = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0];
569        let scales = [2.0, 2.0, 0.0];
570        let geo_keys = vec![
571            1, 1, 0, 2, // header
572            1024, 0, 1, 2, // model type = Geographic
573            2048, 0, 1, 4326, // EPSG:4326
574        ];
575
576        let mut image_data = build_lerc2_header_v2(2, 2, 4, 6, 0.0, 1.0, 4.0, 1 + 16);
577        image_data.extend_from_slice(&0u32.to_le_bytes());
578        image_data.push(1);
579        for value in [1.0f32, 2.0, 3.0, 4.0] {
580            image_data.extend_from_slice(&value.to_le_bytes());
581        }
582        let image_len = image_data.len() as u32;
583
584        build_classic_tiff(&[TestIfdSpec {
585            image_data,
586            entries: vec![
587                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
588                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
589                (258u16, 3u16, 1u32, inline_short(32)),
590                (259u16, 3u16, 1u32, inline_short(34887)),
591                (273u16, 4u16, 1u32, vec![]),
592                (277u16, 3u16, 1u32, inline_short(1)),
593                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
594                (279u16, 4u16, 1u32, le_u32(image_len).to_vec()),
595                (339u16, 3u16, 1u32, inline_short(3)),
596                (
597                    33550u16,
598                    12u16,
599                    3u32,
600                    scales.iter().flat_map(|value| le_f64(*value)).collect(),
601                ),
602                (
603                    33922u16,
604                    12u16,
605                    6u32,
606                    tiepoints.iter().flat_map(|value| le_f64(*value)).collect(),
607                ),
608                (
609                    34735u16,
610                    3u16,
611                    geo_keys.len() as u32,
612                    geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
613                ),
614            ],
615        }])
616    }
617
618    fn overwrite_classic_inline_long_tag(bytes: &mut [u8], tag_code: u16, value: u32) {
619        let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
620        let mut offset = 10usize;
621        for _ in 0..entry_count {
622            let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
623            if code == tag_code {
624                bytes[offset + 8..offset + 12].copy_from_slice(&le_u32(value));
625                return;
626            }
627            offset += 12;
628        }
629        panic!("tag {tag_code} not found in classic TIFF");
630    }
631
632    fn build_geotiff_with_overview() -> Vec<u8> {
633        let base = TestIfdSpec {
634            image_data: vec![10u8, 20, 30, 40],
635            entries: vec![
636                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
637                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
638                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
639                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
640                (273u16, 4u16, 1u32, vec![]),
641                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
642                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
643                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
644                (
645                    33550u16,
646                    12u16,
647                    3u32,
648                    [2.0, 2.0, 0.0]
649                        .iter()
650                        .flat_map(|value| le_f64(*value))
651                        .collect(),
652                ),
653                (
654                    33922u16,
655                    12u16,
656                    6u32,
657                    [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
658                        .iter()
659                        .flat_map(|value| le_f64(*value))
660                        .collect(),
661                ),
662                (
663                    34735u16,
664                    3u16,
665                    12u32,
666                    [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326]
667                        .iter()
668                        .flat_map(|value| le_u16(*value))
669                        .collect(),
670                ),
671            ],
672        };
673        let overview = TestIfdSpec {
674            image_data: vec![99u8],
675            entries: vec![
676                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
677                (256u16, 4u16, 1u32, le_u32(1).to_vec()),
678                (257u16, 4u16, 1u32, le_u32(1).to_vec()),
679                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
680                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
681                (273u16, 4u16, 1u32, vec![]),
682                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
683                (278u16, 4u16, 1u32, le_u32(1).to_vec()),
684                (279u16, 4u16, 1u32, le_u32(1).to_vec()),
685            ],
686        };
687
688        build_classic_tiff(&[base, overview])
689    }
690
691    fn build_cog_like_geotiff_with_ghost_ifd() -> Vec<u8> {
692        let geo_keys = [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326];
693        let ghost = TestIfdSpec {
694            image_data: vec![0u8],
695            entries: vec![
696                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
697                (256u16, 4u16, 1u32, le_u32(1).to_vec()),
698                (257u16, 4u16, 1u32, le_u32(1).to_vec()),
699                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
700                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
701                (273u16, 4u16, 1u32, vec![]),
702                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
703                (278u16, 4u16, 1u32, le_u32(1).to_vec()),
704                (279u16, 4u16, 1u32, le_u32(1).to_vec()),
705                (
706                    33550u16,
707                    12u16,
708                    3u32,
709                    [2.0, 2.0, 0.0]
710                        .iter()
711                        .flat_map(|value| le_f64(*value))
712                        .collect(),
713                ),
714                (
715                    33922u16,
716                    12u16,
717                    6u32,
718                    [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
719                        .iter()
720                        .flat_map(|value| le_f64(*value))
721                        .collect(),
722                ),
723                (
724                    34735u16,
725                    3u16,
726                    geo_keys.len() as u32,
727                    geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
728                ),
729            ],
730        };
731        let overview = TestIfdSpec {
732            image_data: vec![50u8, 60, 70, 80],
733            entries: vec![
734                (254u16, 4u16, 1u32, le_u32(1).to_vec()),
735                (256u16, 4u16, 1u32, le_u32(2).to_vec()),
736                (257u16, 4u16, 1u32, le_u32(2).to_vec()),
737                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
738                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
739                (273u16, 4u16, 1u32, vec![]),
740                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
741                (278u16, 4u16, 1u32, le_u32(2).to_vec()),
742                (279u16, 4u16, 1u32, le_u32(4).to_vec()),
743            ],
744        };
745        let base = TestIfdSpec {
746            image_data: (1u8..=16).collect(),
747            entries: vec![
748                (256u16, 4u16, 1u32, le_u32(4).to_vec()),
749                (257u16, 4u16, 1u32, le_u32(4).to_vec()),
750                (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
751                (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
752                (273u16, 4u16, 1u32, vec![]),
753                (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
754                (278u16, 4u16, 1u32, le_u32(4).to_vec()),
755                (279u16, 4u16, 1u32, le_u32(16).to_vec()),
756            ],
757        };
758
759        build_classic_tiff(&[ghost, overview, base])
760    }
761
762    #[test]
763    fn parses_geotiff_metadata_and_reads_raster() {
764        let file = GeoTiffFile::from_bytes(build_simple_geotiff(false)).unwrap();
765        assert_eq!(file.epsg(), Some(4326));
766        assert_eq!(file.width(), 2);
767        assert_eq!(file.height(), 2);
768        assert_eq!(file.band_count(), 1);
769        assert_eq!(file.nodata(), Some("-9999"));
770        assert_eq!(file.geo_bounds(), Some([100.0, 196.0, 104.0, 200.0]));
771
772        let raster = file.read_raster::<u8>().unwrap();
773        assert_eq!(raster.shape(), &[2, 2]);
774        let (values, offset) = raster.into_raw_vec_and_offset();
775        assert_eq!(offset, Some(0));
776        assert_eq!(values, vec![10, 20, 30, 40]);
777    }
778
779    #[test]
780    fn parses_geotiff_metadata_and_reads_lerc_raster() {
781        let file = GeoTiffFile::from_bytes(build_simple_lerc_geotiff()).unwrap();
782        assert_eq!(file.epsg(), Some(4326));
783        assert_eq!(file.width(), 2);
784        assert_eq!(file.height(), 2);
785
786        let raster = file.read_raster::<f32>().unwrap();
787        assert_eq!(raster.shape(), &[2, 2]);
788        let (values, offset) = raster.into_raw_vec_and_offset();
789        assert_eq!(offset, Some(0));
790        assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0]);
791    }
792
793    #[test]
794    fn pixel_is_point_metadata_shifts_bounds_to_outer_edges() {
795        let file = GeoTiffFile::from_bytes(build_simple_geotiff(true)).unwrap();
796        assert_eq!(file.geo_bounds(), Some([99.0, 197.0, 103.0, 201.0]));
797
798        let transform = file.transform().unwrap();
799        let (center_x, center_y) = transform.pixel_to_geo(0.5, 0.5);
800        assert_eq!((center_x, center_y), (100.0, 200.0));
801    }
802
803    #[test]
804    fn discovers_reduced_resolution_overviews() {
805        let file = GeoTiffFile::from_bytes(build_geotiff_with_overview()).unwrap();
806        assert_eq!(file.overview_count(), 1);
807        assert_eq!(file.overview_ifd_index(0).unwrap(), 1);
808
809        let overview = file.read_overview::<u8>(0).unwrap();
810        assert_eq!(overview.shape(), &[1, 1]);
811        let (values, offset) = overview.into_raw_vec_and_offset();
812        assert_eq!(offset, Some(0));
813        assert_eq!(values, vec![99]);
814    }
815
816    #[test]
817    fn reads_base_raster_window() {
818        let file = GeoTiffFile::from_bytes(build_simple_geotiff(false)).unwrap();
819        let window = file.read_window::<u8>(1, 0, 1, 2).unwrap();
820        assert_eq!(window.shape(), &[1, 2]);
821        let (values, offset) = window.into_raw_vec_and_offset();
822        assert_eq!(offset, Some(0));
823        assert_eq!(values, vec![30, 40]);
824    }
825
826    #[test]
827    fn reads_overview_window() {
828        let file = GeoTiffFile::from_bytes(build_geotiff_with_overview()).unwrap();
829        let window = file.read_overview_window::<u8>(0, 0, 0, 1, 1).unwrap();
830        assert_eq!(window.shape(), &[1, 1]);
831        let (values, offset) = window.into_raw_vec_and_offset();
832        assert_eq!(offset, Some(0));
833        assert_eq!(values, vec![99]);
834    }
835
836    #[test]
837    fn prefers_non_ghost_base_ifd_for_cog_like_layouts() {
838        let file = GeoTiffFile::from_bytes(build_cog_like_geotiff_with_ghost_ifd()).unwrap();
839        assert_eq!(file.base_ifd_index(), 2);
840        assert_eq!(file.width(), 4);
841        assert_eq!(file.height(), 4);
842        assert_eq!(file.geo_bounds(), Some([100.0, 192.0, 108.0, 200.0]));
843        assert_eq!(file.overview_count(), 1);
844        assert_eq!(file.overview_ifd_index(0).unwrap(), 1);
845
846        let base = file.read_raster::<u8>().unwrap();
847        assert_eq!(base.shape(), &[4, 4]);
848        let (values, offset) = base.into_raw_vec_and_offset();
849        assert_eq!(offset, Some(0));
850        assert_eq!(values, (1u8..=16).collect::<Vec<_>>());
851
852        let overview = file.read_overview::<u8>(0).unwrap();
853        assert_eq!(overview.shape(), &[2, 2]);
854        let (values, offset) = overview.into_raw_vec_and_offset();
855        assert_eq!(offset, Some(0));
856        assert_eq!(values, vec![50, 60, 70, 80]);
857    }
858
859    #[test]
860    fn rejects_zero_rows_per_strip_without_panicking() {
861        let mut bytes = build_simple_geotiff(false);
862        overwrite_classic_inline_long_tag(&mut bytes, 278, 0);
863
864        let file = GeoTiffFile::from_bytes(bytes).unwrap();
865        assert_eq!(file.epsg(), Some(4326));
866
867        let error = file.tiff().read_image_bytes(0).unwrap_err();
868        assert!(error.to_string().contains("RowsPerStrip"));
869    }
870}