Skip to main content

hpr_io/geotiff/
mod.rs

1//! A launch site's height from a user's elevation file: a GeoTIFF digital elevation model (DEM)
2//! on a geographic (latitude and longitude) grid.
3//!
4//! A GeoTIFF is a TIFF image whose pixels are heights and whose tags say where on Earth they lie.
5//! The caller reads the whole file into memory and passes its bytes; [`ElevationRaster::parse`]
6//! reads the tags, and [`ElevationRaster::height_at`] finds the pixel a latitude and longitude
7//! fall in and decodes only the tile or strip that holds it, so a lookup adds about one tile's
8//! memory to the file's. The TIFF itself (codecs, predictors, tiles, strips, byte order, BigTIFF)
9//! is decoded by image-rs's `tiff` crate ([docs.rs][tiff]); this module reads the geographic tags
10//! as the OGC GeoTIFF Standard 1.1 (OGC 19-008r4, 2019) defines them, and where GDAL reads a file
11//! differently from the standard it either follows GDAL or refuses the file, so a height read
12//! here is the one GDAL's readers give for the same point.
13//!
14//! **Where a pixel lies.** The file ties raster coordinates to longitude and latitude with a
15//! tiepoint `(I, J) ↦ (X, Y)` and a pixel size `(S_x, S_y)` (`ModelTiepointTag`,
16//! `ModelPixelScaleTag`; §7.3), or with an affine matrix (`ModelTransformationTag`; its terms
17//! `a, b, d` and `e, f, h` give `X = a·I + b·J + d`, `Y = e·I + f·J + h`). A positive `S_y` means
18//! latitude falls as rows go down (Requirement 10.4). As GDAL does, both become the longitude and
19//! latitude of the outer corner of pixel `(0, 0)` and a signed pixel size:
20//!
21//! `λ₀ = X − I·S_x`, `φ₀ = Y − J·(−S_y)`, `Δλ = S_x`, `Δφ = −S_y`, or from the matrix
22//! `λ₀ = d`, `Δλ = a`, `φ₀ = h`, `Δφ = f`.
23//!
24//! The raster type (`GTRasterTypeGeoKey`, §7.2.1) says what a raster coordinate names. In the
25//! usual *pixel is area*, `(0, 0)` is the outer corner of the first pixel; in *pixel is point* it
26//! is that pixel's center, so the corner is half a pixel back: `λ₀ −= Δλ/2`, `φ₀ −= Δφ/2`. A point
27//! `(φ, λ)` then lies in column `⌊(λ − λ₀)/Δλ⌋` and row `⌊(φ − φ₀)/Δφ⌋`: the pixel whose area
28//! holds it, with no interpolation between pixels. On a 1-arc-second grid (about 31 m by 26 m at
29//! 33° N) that is the height of the ground within about 20 m of the site. A point within about
30//! 10⁻¹³ of a pixel's width of an edge can fall on either side, here and in GDAL, by rounding.
31//!
32//! Refused where the standard and GDAL disagree, or where GDAL needs more than this reads: a
33//! negative `S_y` (GDAL reads it as north-up, the standard as south-up), both a pixel scale and a
34//! matrix (the standard forbids it; GDAL takes the scale), a rotated matrix, several tiepoints
35//! (ground control points) and an internal nodata mask.
36//!
37//! **What is read.** One band of unsigned or signed 8-, 16- or 32-bit integers, or 32- or 64-bit
38//! floats; uncompressed, LZW, Deflate or PackBits, with or without the horizontal or
39//! floating-point predictor; tiles or strips; little- or big-endian; classic TIFF or BigTIFF.
40//! The first image in the file is the one read (later ones are a cloud-optimized GeoTIFF's
41//! overviews). The CRS must be geographic (`GTModelTypeGeoKey` 2, or a geodetic CRS key with no
42//! model type, as GeoTIFF 1.0 writers leave it; GDAL reads the latter as a local CRS, at the same
43//! pixel positions) in degrees from Greenwich, and
44//! one of [`NEAR_WGS84`]: datums within a few meters of WGS 84, where a point's WGS 84 latitude
45//! and longitude read the right pixel to within a few meters (more near the rupture of a large
46//! earthquake since the datum was fixed; the list gives examples). Its EPSG code is reported. Any
47//! other, and a projected file (UTM, say), is refused with [`GeoTiffError::Unsupported`] naming
48//! its code; `gdalwarp -t_srs EPSG:4326 in.tif out.tif` turns it into one this reads.
49//!
50//! **Heights.** A pixel's raw value `v` becomes a height in meters as `(v·scale + offset)·unit`,
51//! with GDAL's scale and offset:
52//!
53//! - `S_z` from `ModelPixelScaleTag` and `Z₀ − z₀·S_z` from the tiepoint's heights, where GDAL
54//!   certainly applies them: a GeoTIFF 1.1 directory of model type 2 naming a vertical CRS from
55//!   [`VERTICAL_CRS_UNITS`], with no `VerticalDatumGeoKey`, beside a geographic CRS other than
56//!   WGS 84 3D. With no vertical key, or in a GeoTIFF 1.0 directory (where GDAL drops the vertical
57//!   CRS), they are ignored, as GDAL ignores them. Between those, whether GDAL applies them turns
58//!   on how it resolves the keys, and the file is refused unless they give GDAL's own scale 1 and
59//!   offset 0 and `GDAL_METADATA` gives no other scale.
60//! - Otherwise the `scale` and `offset` items of GDAL's `GDAL_METADATA` tag; otherwise 1 and 0.
61//!   GDAL matches that tag's items with quirks, so an item with one of the roles read here is
62//!   refused if it has a namespace, a capital in an attribute's name, a sample that isn't plain
63//!   digits, a value that isn't a single text node (CDATA included), or the `IMAGE_STRUCTURE`
64//!   domain. GDAL's own skips (no name, no sample, another band) are skipped.
65//!
66//! The unit is the one the file states by `VerticalUnitsGeoKey` (meters, international feet or US
67//! survey feet), by a vertical CRS from [`VERTICAL_CRS_UNITS`], or by the `unittype` item of
68//! `GDAL_METADATA`; two that disagree are refused, as is a vertical CRS off the list (GDAL takes
69//! its unit from EPSG's registry, whatever the key says). Vertical keys GDAL drops with their unit,
70//! or reads by rules of its own, are refused: a private value (above 32767) in any of them
71//! (dropped with a model type, read without one), any beside WGS 84 3D, `VerticalDatumGeoKey`
72//! 6030 beside WGS 84 with model type 2 (GDAL makes it WGS 84 3D), and any with no model type and
73//! no unit key. In a GeoTIFF 1.0 directory GDAL drops the vertical CRS but keeps its unit; this
74//! module reports both; with no model type GDAL's local CRS ignores `VerticalGeoKey`, which this
75//! module reports too. A unit name is read after trimming ASCII blanks; GDAL drops leading blanks
76//! typed as they are and keeps the rest (`"ft "` is feet here). A file that states no unit is read as meters, flagged by
77//! [`RasterInfo::vertical_unit_stated`]; GDAL reports no unit there, except beside a vertical
78//! datum key alone, where it assumes meters too; a file in feet that states none reads 3.28 times too
79//! high. The vertical datum (`VerticalGeoKey`, NAVD88 or EGM2008, say) is reported, not applied.
80//! A value equal to the file's nodata value (GDAL's `GDAL_NODATA` tag) or a NaN reads as no
81//! height; a nodata value the sample type can't hold exactly, such as 12.5 on integers, matches
82//! nothing.
83//!
84//! **Bounds on a hostile file.** A tile or strip larger than [`MAX_CHUNK_BYTES`] decoded is
85//! refused at [`ElevationRaster::parse`], and [`ElevationRaster::values`] grows the raster
86//! fallibly as rows of tiles decode. `GDAL_METADATA` nested more than 16 deep is refused before
87//! it is parsed. The `tiff` crate prints one debug line to standard error when a tag's value
88//! passes its 1 MiB limit (its own `dbg!`).
89//!
90//! **Guide:** [A launch site's elevation][guide] walks through an example and says how the reader
91//! is checked: against GDAL's reading, through rasterio, of seven files and a whole USGS tile.
92//! The choices are in [ADR-128, a site's height from a user's GeoTIFF][adr].
93//!
94//! ```
95//! use hpr_io::geotiff::ElevationRaster;
96//!
97//! // A real program reads its file: `let bytes = std::fs::read(path)?;`.
98//! let bytes = include_bytes!("../../tests/fixtures/geotiff/usgs-f32-lzw-fp-tiles.tif");
99//! let raster = ElevationRaster::parse(bytes)?;
100//! // Spaceport America's runway: 1,400.691 m in the USGS's terrain model.
101//! let height_m = raster.height_at(32.99, -106.97)?;
102//! assert_eq!(height_m.map(|h| (h * 1000.0).round() / 1000.0), Some(1400.691));
103//! # Ok::<(), hpr_io::geotiff::GeoTiffError>(())
104//! ```
105//!
106//! [guide]: https://nrdptel.github.io/hpr-sim/elevation.html#from-an-elevation-file-of-your-own
107//! [adr]: https://github.com/nrdptel/hpr-sim/blob/main/docs/DECISIONS.md#adr-128-m53c2-a-sites-height-from-a-users-geotiff-held-to-rasterios-reading-2026-09-30
108//! [tiff]: https://docs.rs/tiff/0.11.3/tiff/
109
110use std::io::Cursor;
111
112use serde::{Deserialize, Serialize};
113use tiff::decoder::{ChunkType, Decoder, DecodingResult};
114use tiff::tags::{CompressionMethod, PhotometricInterpretation, SampleFormat, Tag};
115
116/// `GTModelTypeGeoKey` (OGC 19-008r4 §7.2.2): 1 projected, 2 geographic, 3 geocentric.
117const MODEL_TYPE_KEY: u16 = 1024;
118/// `GTRasterTypeGeoKey` (§7.2.1): 1 pixel is area, 2 pixel is point.
119const RASTER_TYPE_KEY: u16 = 1025;
120/// `GeodeticCRSGeoKey` (§7.4.3), `GeographicTypeGeoKey` in GeoTIFF 1.0.
121const GEODETIC_CRS_KEY: u16 = 2048;
122/// `PrimeMeridianGeoKey` (§7.5.3).
123const PRIME_MERIDIAN_KEY: u16 = 2051;
124/// `GeogAngularUnitsGeoKey` (§7.5.1).
125const ANGULAR_UNITS_KEY: u16 = 2054;
126/// `ProjectedCRSGeoKey` (§7.4.2).
127const PROJECTED_CRS_KEY: u16 = 3072;
128/// `VerticalGeoKey` (§7.4.4), `VerticalCSTypeGeoKey` in GeoTIFF 1.0.
129const VERTICAL_CRS_KEY: u16 = 4096;
130const VERTICAL_DATUM_KEY: u16 = 4098;
131/// `VerticalUnitsGeoKey` (§7.5.1).
132const VERTICAL_UNITS_KEY: u16 = 4099;
133/// GDAL's `GDAL_METADATA` TIFF tag: an XML list of items, a band's scale and offset among them.
134const GDAL_METADATA_TAG: u16 = 42112;
135/// GDAL's `GDAL_NODATA` TIFF tag: the nodata value as ASCII text.
136const GDAL_NODATA_TAG: u16 = 42113;
137/// EPSG's Greenwich prime meridian.
138const GREENWICH: u16 = 8901;
139/// The most images the reader walks looking for an internal mask; a cloud-optimized GeoTIFF has
140/// one per overview level, a handful.
141const MAX_IMAGES_WALKED: usize = 64;
142
143/// The most pixels [`ElevationRaster::values`] decodes into one vector: 2²⁸, 2 GiB of `f64`.
144pub const MAX_VALUES_PIXELS: u64 = 1 << 28;
145
146/// The largest tile or strip, decoded, a file may have: 256 MiB, the `tiff` crate's own limit
147/// on a decoded chunk, which its padding of a floating-point tile would otherwise bypass.
148pub const MAX_CHUNK_BYTES: u64 = 256 << 20;
149
150/// The geographic CRSs read, by EPSG code: datums whose latitude and longitude lie within a few
151/// meters of WGS 84's, so a point given in WGS 84 reads the right pixel, or its neighbour on a
152/// grid finer than a few meters. They part by plate motion since each was fixed, and by
153/// earthquakes: near the rupture of a large one since a datum was fixed, such as Chile's in 2010
154/// for SIRGAS 2000 or Wenchuan's in 2008 for CGCS2000, the ground moved several meters. JGD2000
155/// is left out: Japan's 2011 earthquake moved its north-east by more than 5 m, and JGD2011
156/// replaced it.
157pub const NEAR_WGS84: &[(u16, &str)] = &[
158    (4326, "WGS 84"),
159    (4979, "WGS 84, 3D"),
160    (4269, "NAD83"),
161    (4152, "NAD83(HARN)"),
162    (4759, "NAD83(NSRS2007)"),
163    (6318, "NAD83(2011)"),
164    (4617, "NAD83(CSRS)"),
165    (4258, "ETRS89"),
166    (4283, "GDA94"),
167    (7844, "GDA2020"),
168    (4167, "NZGD2000"),
169    (6668, "JGD2011"),
170    (4674, "SIRGAS 2000"),
171    (4490, "CGCS2000"),
172];
173
174/// Why a GeoTIFF elevation file could not be read, or a height not taken from it.
175#[derive(Debug, Clone, PartialEq, thiserror::Error)]
176#[non_exhaustive]
177pub enum GeoTiffError {
178    /// The TIFF decoder refused the file or one of its tiles.
179    #[error("not a readable TIFF: {0}")]
180    Tiff(String),
181    /// A tag or key the reader needs is absent.
182    #[error("the file has no {what}")]
183    Missing {
184        /// The tag or key.
185        what: &'static str,
186    },
187    /// A tag or key holds a value the standard does not allow.
188    #[error("{what} is malformed: {reason}")]
189    Malformed {
190        /// The tag or key.
191        what: &'static str,
192        /// What is wrong with it.
193        reason: String,
194    },
195    /// The file is valid but uses something this reader does not read.
196    #[error("{what} {value} is not read{hint}")]
197    Unsupported {
198        /// The feature.
199        what: &'static str,
200        /// Its value in the file.
201        value: String,
202        /// What to do instead, with a leading separator, or empty.
203        hint: &'static str,
204    },
205    /// A latitude or longitude that is not a place.
206    #[error("latitude {latitude_deg}° and longitude {longitude_deg}° are not a place")]
207    Location {
208        /// Latitude asked for, degrees.
209        latitude_deg: f64,
210        /// Longitude asked for, degrees.
211        longitude_deg: f64,
212    },
213    /// The point is not over the raster.
214    #[error(
215        "latitude {latitude_deg}° and longitude {longitude_deg}° are outside the raster, which spans latitudes {:.6}° to {:.6}° and longitudes {:.6}° to {:.6}°",
216        bounds.south_deg, bounds.north_deg, bounds.west_deg, bounds.east_deg
217    )]
218    Outside {
219        /// Latitude asked for, degrees.
220        latitude_deg: f64,
221        /// Longitude asked for, degrees.
222        longitude_deg: f64,
223        /// The raster's edges.
224        bounds: Bounds,
225    },
226    /// A pixel past the raster's last row or column.
227    #[error("pixel (row {}, column {}) is outside a raster of {width} by {height}", pixel.row, pixel.col)]
228    NoSuchPixel {
229        /// The pixel asked for.
230        pixel: Pixel,
231        /// The raster's columns.
232        width: u32,
233        /// Its rows.
234        height: u32,
235    },
236    /// The raster or one of its tiles is too large to decode.
237    #[error("{what} of {size} is more than the {limit} decoded at once")]
238    TooLarge {
239        /// What is too large, with its unit.
240        what: &'static str,
241        /// Its size.
242        size: u64,
243        /// The limit.
244        limit: u64,
245    },
246}
247
248impl From<tiff::TiffError> for GeoTiffError {
249    fn from(e: tiff::TiffError) -> Self {
250        GeoTiffError::Tiff(e.to_string())
251    }
252}
253
254/// A pixel's place in the raster, counted from 0 at the first row and column.
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256pub struct Pixel {
257    /// Row, from the first (northernmost when north is up).
258    pub row: u32,
259    /// Column, from the first (westernmost when east is right).
260    pub col: u32,
261}
262
263/// A raster's edges, degrees.
264#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
265pub struct Bounds {
266    /// Southern edge.
267    pub south_deg: f64,
268    /// Northern edge.
269    pub north_deg: f64,
270    /// Western edge.
271    pub west_deg: f64,
272    /// Eastern edge.
273    pub east_deg: f64,
274}
275
276/// What a raster coordinate names (`GTRasterTypeGeoKey`, OGC 19-008r4 §7.2.1).
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
278#[non_exhaustive]
279pub enum RasterType {
280    /// Raster coordinate `(0, 0)` is the outer corner of the first pixel (code 1, the default).
281    PixelIsArea,
282    /// Raster coordinate `(0, 0)` is the center of the first pixel (code 2).
283    PixelIsPoint,
284}
285
286/// The unit a pixel's value is in, converted to meters by [`VerticalUnit::meters`].
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
288#[non_exhaustive]
289pub enum VerticalUnit {
290    /// EPSG 9001. Written before the move to US spelling as `Metre`, which is still read and never written.
291    #[serde(alias = "Metre")]
292    Meter,
293    /// The international foot, 0.3048 m exactly (EPSG 9002).
294    Foot,
295    /// The US survey foot, 1200/3937 m (EPSG 9003).
296    UsSurveyFoot,
297}
298
299impl VerticalUnit {
300    /// The unit's length in meters.
301    #[must_use]
302    pub fn meters(self) -> f64 {
303        match self {
304            VerticalUnit::Meter => 1.0,
305            VerticalUnit::Foot => 0.3048,
306            VerticalUnit::UsSurveyFoot => 1200.0 / 3937.0,
307        }
308    }
309
310    fn from_epsg(code: u16) -> Option<Self> {
311        match code {
312            9001 => Some(VerticalUnit::Meter),
313            9002 => Some(VerticalUnit::Foot),
314            9003 => Some(VerticalUnit::UsSurveyFoot),
315            _ => None,
316        }
317    }
318}
319
320/// The vertical CRSs read, by EPSG code, with their unit from EPSG's registry: (code, unit). All
321/// are gravity-related heights, positive up. A file naming another is refused, as GDAL would take
322/// its unit from the registry, which this reader doesn't hold.
323pub const VERTICAL_CRS_UNITS: &[(u16, VerticalUnit)] = &[
324    (3855, VerticalUnit::Meter),        // EGM2008 height
325    (5701, VerticalUnit::Meter),        // ODN height
326    (5703, VerticalUnit::Meter),        // NAVD88 height
327    (5714, VerticalUnit::Meter),        // MSL height
328    (5773, VerticalUnit::Meter),        // EGM96 height
329    (5798, VerticalUnit::Meter),        // EGM84 height
330    (6360, VerticalUnit::UsSurveyFoot), // NAVD88 height (ftUS)
331    (8228, VerticalUnit::Foot),         // NAVD88 height (ft)
332];
333
334/// The sample type of the raster's pixels.
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
336#[non_exhaustive]
337pub enum SampleType {
338    /// Unsigned 8-bit integers.
339    U8,
340    /// Signed 8-bit integers.
341    I8,
342    /// Unsigned 16-bit integers.
343    U16,
344    /// Signed 16-bit integers.
345    I16,
346    /// Unsigned 32-bit integers.
347    U32,
348    /// Signed 32-bit integers.
349    I32,
350    /// IEEE 754 single precision.
351    F32,
352    /// IEEE 754 double precision.
353    F64,
354}
355
356impl SampleType {
357    /// Bytes per sample.
358    fn bytes(self) -> u64 {
359        match self {
360            SampleType::U8 | SampleType::I8 => 1,
361            SampleType::U16 | SampleType::I16 => 2,
362            SampleType::U32 | SampleType::I32 | SampleType::F32 => 4,
363            SampleType::F64 => 8,
364        }
365    }
366
367    /// The nodata value as a sample of this type can hold it, or `None` if none can.
368    fn round_nodata(self, nodata: f64) -> Option<f64> {
369        let integral = |lo: f64, hi: f64| {
370            (nodata.fract() == 0.0 && (lo..=hi).contains(&nodata)).then_some(nodata)
371        };
372        match self {
373            SampleType::U8 => integral(0.0, f64::from(u8::MAX)),
374            SampleType::I8 => integral(f64::from(i8::MIN), f64::from(i8::MAX)),
375            SampleType::U16 => integral(0.0, f64::from(u16::MAX)),
376            SampleType::I16 => integral(f64::from(i16::MIN), f64::from(i16::MAX)),
377            SampleType::U32 => integral(0.0, f64::from(u32::MAX)),
378            SampleType::I32 => integral(f64::from(i32::MIN), f64::from(i32::MAX)),
379            // Rounded to nearest, as GDAL's GeoTIFF reader rounds it; a NaN stays NaN and is
380            // matched by `is_nan`.
381            #[expect(clippy::cast_possible_truncation, reason = "f32 nodata is f32-rounded")]
382            SampleType::F32 => Some(f64::from(nodata as f32)),
383            SampleType::F64 => Some(nodata),
384        }
385    }
386}
387
388/// What an elevation file says about itself.
389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
390#[non_exhaustive]
391pub struct RasterInfo {
392    /// Columns.
393    pub width: u32,
394    /// Rows.
395    pub height: u32,
396    /// Longitude of the outer corner of pixel `(0, 0)` (its west edge when `Δλ > 0`), degrees.
397    pub corner_longitude_deg: f64,
398    /// Latitude of the outer corner of pixel `(0, 0)` (its north edge when `Δφ < 0`), degrees.
399    pub corner_latitude_deg: f64,
400    /// `Δλ`: longitude change from one column to the next, degrees.
401    pub pixel_longitude_deg: f64,
402    /// `Δφ`: latitude change from one row to the next, degrees; negative when north is up.
403    pub pixel_latitude_deg: f64,
404    /// What the file's tiepoint names; already applied to the corner above.
405    pub raster_type: RasterType,
406    /// The geographic CRS's EPSG code, one of [`NEAR_WGS84`].
407    pub geographic_crs_epsg: u16,
408    /// The vertical CRS's EPSG code, if the file names one (a user-defined or private code,
409    /// 32767 and above, is not one).
410    pub vertical_crs_epsg: Option<u16>,
411    /// The unit pixel values are in, after the scale and offset.
412    pub vertical_unit: VerticalUnit,
413    /// Whether the file states that unit (by `VerticalUnitsGeoKey`, a vertical CRS this reader
414    /// knows, or a unit type in GDAL's `GDAL_METADATA`); `false` means meters were assumed.
415    pub vertical_unit_stated: bool,
416    /// The pixels' scale: a raw value `v` stands for `v·scale + offset` in the vertical unit.
417    pub scale: f64,
418    /// The pixels' offset, in the vertical unit.
419    pub offset: f64,
420    /// The pixels' sample type.
421    pub sample: SampleType,
422    /// The nodata value, rounded to the sample type, if the file has one a sample can hold.
423    pub nodata: Option<f64>,
424}
425
426impl RasterInfo {
427    /// The pixel holding a point, by the module's rule, or `None` if the point is off the
428    /// raster. The longitude is tried as given and then 360° to either side, so a file on 0° to
429    /// 360° reads a longitude given on −180° to 180°.
430    #[must_use]
431    pub fn pixel_of(&self, latitude_deg: f64, longitude_deg: f64) -> Option<Pixel> {
432        let row = Self::index(
433            latitude_deg,
434            self.corner_latitude_deg,
435            self.pixel_latitude_deg,
436            self.height,
437        )?;
438        [longitude_deg, longitude_deg + 360.0, longitude_deg - 360.0]
439            .into_iter()
440            .find_map(|lon| {
441                Self::index(
442                    lon,
443                    self.corner_longitude_deg,
444                    self.pixel_longitude_deg,
445                    self.width,
446                )
447            })
448            .map(|col| Pixel { row, col })
449    }
450
451    fn index(at: f64, corner: f64, step: f64, count: u32) -> Option<u32> {
452        let i = ((at - corner) / step).floor();
453        // `i` is an integer-valued f64 below `count` (at most u32::MAX), so the cast is exact.
454        #[expect(
455            clippy::cast_possible_truncation,
456            clippy::cast_sign_loss,
457            reason = "checked to lie in 0..count"
458        )]
459        (i >= 0.0 && i < f64::from(count)).then_some(i as u32)
460    }
461
462    /// The raster's edges.
463    #[must_use]
464    pub fn bounds(&self) -> Bounds {
465        let lat_far = self.corner_latitude_deg + f64::from(self.height) * self.pixel_latitude_deg;
466        let lon_far = self.corner_longitude_deg + f64::from(self.width) * self.pixel_longitude_deg;
467        Bounds {
468            south_deg: self.corner_latitude_deg.min(lat_far),
469            north_deg: self.corner_latitude_deg.max(lat_far),
470            west_deg: self.corner_longitude_deg.min(lon_far),
471            east_deg: self.corner_longitude_deg.max(lon_far),
472        }
473    }
474
475    /// A raw value as a height in meters: `(v·scale + offset)·unit`.
476    #[must_use]
477    pub fn meters(&self, value: f64) -> f64 {
478        (value * self.scale + self.offset) * self.vertical_unit.meters()
479    }
480}
481
482/// A GeoTIFF elevation file, its tags read and its pixels left packed in the borrowed bytes.
483#[derive(Clone)]
484pub struct ElevationRaster<'a> {
485    bytes: &'a [u8],
486    info: RasterInfo,
487    chunk_type: ChunkType,
488    chunk_width: u32,
489    chunk_height: u32,
490}
491
492impl std::fmt::Debug for ElevationRaster<'_> {
493    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
494        f.debug_struct("ElevationRaster")
495            .field("bytes", &self.bytes.len())
496            .field("info", &self.info)
497            .field("chunk_type", &self.chunk_type)
498            .field("chunk_width", &self.chunk_width)
499            .field("chunk_height", &self.chunk_height)
500            .finish()
501    }
502}
503
504impl<'a> ElevationRaster<'a> {
505    /// Reads a GeoTIFF's tags: its size, sample type, layout, georeferencing, scale, units and
506    /// nodata value.
507    ///
508    /// # Errors
509    ///
510    /// [`GeoTiffError`] if the TIFF is unreadable, its georeferencing is missing, malformed or
511    /// one the module docs list as refused, its CRS is not one of [`NEAR_WGS84`] in degrees, its
512    /// pixels are not one band of a [`SampleType`] in a codec it reads, or a tile is larger than
513    /// [`MAX_CHUNK_BYTES`].
514    pub fn parse(bytes: &'a [u8]) -> Result<Self, GeoTiffError> {
515        let mut decoder = Decoder::new(Cursor::new(bytes))?;
516        let (width, height) = decoder.dimensions()?;
517        if width == 0 || height == 0 {
518            return Err(GeoTiffError::Malformed {
519                what: "the image size",
520                reason: format!("{width} by {height} pixels"),
521            });
522        }
523        let samples: u16 = decoder
524            .find_tag_unsigned(Tag::SamplesPerPixel)?
525            .unwrap_or(1);
526        if samples != 1 {
527            return Err(GeoTiffError::Unsupported {
528                what: "a pixel of samples numbering",
529                value: samples.to_string(),
530                hint: "; an elevation file has one band",
531            });
532        }
533        let photometric: Option<u16> = decoder.find_tag_unsigned(Tag::PhotometricInterpretation)?;
534        if photometric == Some(PhotometricInterpretation::WhiteIsZero.to_u16()) {
535            return Err(GeoTiffError::Unsupported {
536                what: "photometric interpretation",
537                value: "WhiteIsZero".into(),
538                hint: "; its values would read inverted",
539            });
540        }
541        compression(&mut decoder)?;
542        let sample = sample_type(&mut decoder)?;
543        let keys = GeoKeys::read(&mut decoder)?;
544        let raster_type = match keys.get(RASTER_TYPE_KEY)? {
545            None | Some(1) => RasterType::PixelIsArea,
546            Some(2) => RasterType::PixelIsPoint,
547            Some(other) => {
548                return Err(GeoTiffError::Unsupported {
549                    what: "GTRasterTypeGeoKey",
550                    value: other.to_string(),
551                    hint: "",
552                });
553            }
554        };
555        let geographic_crs_epsg = geographic_crs(&keys)?;
556        vertical_kept_by_gdal(&keys, geographic_crs_epsg)?;
557        let (vertical_crs_epsg, vertical_unit, vertical_unit_stated) = vertical(&keys)?;
558        let georef = transform(&mut decoder)?;
559        let [mut lon0, lon_step, mut lat0, lat_step] = georef.corner;
560        if raster_type == RasterType::PixelIsPoint {
561            lon0 -= lon_step * 0.5;
562            lat0 -= lat_step * 0.5;
563        }
564        let heights = if keys.minor == 1
565            && keys.get(MODEL_TYPE_KEY)? == Some(2)
566            && vertical_crs_epsg.is_some_and(|c| VERTICAL_CRS_UNITS.iter().any(|(v, _)| *v == c))
567            && !keys.has(VERTICAL_DATUM_KEY)
568            && geographic_crs_epsg != 4979
569        {
570            ZTerms::Applied
571        } else if keys.minor == 0
572            || [VERTICAL_CRS_KEY, VERTICAL_DATUM_KEY, VERTICAL_UNITS_KEY]
573                .iter()
574                .all(|&k| !keys.has(k))
575        {
576            ZTerms::Ignored
577        } else {
578            ZTerms::Unknown
579        };
580        let (scale, offset, unit) = scale_offset(&mut decoder, &georef, heights)?;
581        let (vertical_unit, vertical_unit_stated) = match unit {
582            Some(unit) if vertical_unit_stated && unit != vertical_unit => {
583                return Err(GeoTiffError::Unsupported {
584                    what: "a vertical unit given twice, differently:",
585                    value: format!("{vertical_unit:?} by the GeoKeys, {unit:?} by GDAL_METADATA"),
586                    hint: "",
587                });
588            }
589            Some(unit) => (unit, true),
590            None => (vertical_unit, vertical_unit_stated),
591        };
592        let nodata = nodata(&mut decoder)?.and_then(|v| sample.round_nodata(v));
593        let chunk_type = decoder.get_chunk_type();
594        // The decoder has validated its tile or strip attributes; this cannot fail after `new`.
595        let (chunk_width, chunk_height) = decoder.chunk_dimensions();
596        if chunk_width == 0 || chunk_height == 0 {
597            return Err(GeoTiffError::Malformed {
598                what: "the tile or strip size",
599                reason: format!("{chunk_width} by {chunk_height} pixels"),
600            });
601        }
602        // A strip's rows past the image's are not decoded; a tile's padding is.
603        let rows = match chunk_type {
604            ChunkType::Strip => chunk_height.min(height),
605            ChunkType::Tile => chunk_height,
606        };
607        // In u128: a tile's width and length are each up to 2³² − 1, so their product times
608        // 8 bytes can pass a u64.
609        let chunk_bytes = u128::from(chunk_width) * u128::from(rows) * u128::from(sample.bytes());
610        if chunk_bytes > u128::from(MAX_CHUNK_BYTES) {
611            return Err(GeoTiffError::TooLarge {
612                what: "a tile or strip, in bytes,",
613                size: u64::try_from(chunk_bytes).unwrap_or(u64::MAX),
614                limit: MAX_CHUNK_BYTES,
615            });
616        }
617        // Every chunk index is below the chunk count, so `locate`'s u32 sums cannot overflow.
618        let chunks = u64::from(height.div_ceil(chunk_height))
619            * match chunk_type {
620                ChunkType::Strip => 1,
621                ChunkType::Tile => u64::from(width.div_ceil(chunk_width)),
622            };
623        if chunks > u64::from(u32::MAX) {
624            return Err(GeoTiffError::Malformed {
625                what: "the tile layout",
626                reason: format!("{chunks} tiles"),
627            });
628        }
629        no_mask(&mut decoder)?;
630        Ok(ElevationRaster {
631            bytes,
632            info: RasterInfo {
633                width,
634                height,
635                corner_longitude_deg: lon0,
636                corner_latitude_deg: lat0,
637                pixel_longitude_deg: lon_step,
638                pixel_latitude_deg: lat_step,
639                raster_type,
640                geographic_crs_epsg,
641                vertical_crs_epsg,
642                vertical_unit,
643                vertical_unit_stated,
644                scale,
645                offset,
646                sample,
647                nodata,
648            },
649            chunk_type,
650            chunk_width,
651            chunk_height,
652        })
653    }
654
655    /// What the file says about itself.
656    #[must_use]
657    pub fn info(&self) -> &RasterInfo {
658        &self.info
659    }
660
661    /// The ground's height at a site, meters above the file's vertical datum: the value of the
662    /// pixel holding the point ([`RasterInfo::pixel_of`]) through [`RasterInfo::meters`]. `None`
663    /// where that pixel is nodata.
664    ///
665    /// # Errors
666    ///
667    /// [`GeoTiffError::Location`] for a latitude outside ±90° or a longitude that is not finite,
668    /// [`GeoTiffError::Outside`] off the raster, and [`GeoTiffError::Tiff`] if the pixel's tile
669    /// will not decode.
670    pub fn height_at(
671        &self,
672        latitude_deg: f64,
673        longitude_deg: f64,
674    ) -> Result<Option<f64>, GeoTiffError> {
675        let value = self.value_at(latitude_deg, longitude_deg)?;
676        Ok(value.map(|v| self.info.meters(v)))
677    }
678
679    /// The raw value of the pixel holding a point, before the scale, offset and unit; `None`
680    /// where it is nodata. This is the value GDAL reads at the point.
681    ///
682    /// # Errors
683    ///
684    /// As [`ElevationRaster::height_at`].
685    pub fn value_at(
686        &self,
687        latitude_deg: f64,
688        longitude_deg: f64,
689    ) -> Result<Option<f64>, GeoTiffError> {
690        let pixel = self.place(latitude_deg, longitude_deg)?;
691        self.pixel(pixel)
692    }
693
694    /// The raw value of a pixel, decoding only its tile or strip; `None` where it is nodata.
695    ///
696    /// # Errors
697    ///
698    /// [`GeoTiffError::NoSuchPixel`] for a pixel off the raster, [`GeoTiffError::Tiff`] if its
699    /// tile will not decode.
700    pub fn pixel(&self, pixel: Pixel) -> Result<Option<f64>, GeoTiffError> {
701        if pixel.row >= self.info.height || pixel.col >= self.info.width {
702            return Err(GeoTiffError::NoSuchPixel {
703                pixel,
704                width: self.info.width,
705                height: self.info.height,
706            });
707        }
708        let (index, at) = self.locate(pixel);
709        let mut decoder = Decoder::new(Cursor::new(self.bytes))?;
710        let chunk = decoder.read_chunk(index)?;
711        let value = chunk_sample(&chunk, index, at)?;
712        Ok(self.unless_nodata(value))
713    }
714
715    /// The raw values at several points, decoding each tile or strip they fall in once: what
716    /// [`ElevationRaster::value_at`] gives for each, in order, its error included. A tile that
717    /// will not decode fails only the points in it.
718    #[must_use]
719    pub fn values_at(&self, points: &[(f64, f64)]) -> Vec<Result<Option<f64>, GeoTiffError>> {
720        let mut out: Vec<Result<Option<f64>, GeoTiffError>> = Vec::with_capacity(points.len());
721        let mut wanted: Vec<(u32, usize, usize)> = Vec::new();
722        for (i, &(latitude_deg, longitude_deg)) in points.iter().enumerate() {
723            match self.place(latitude_deg, longitude_deg) {
724                Ok(pixel) => {
725                    let (index, at) = self.locate(pixel);
726                    wanted.push((index, at, i));
727                    out.push(Ok(None));
728                }
729                Err(e) => out.push(Err(e)),
730            }
731        }
732        wanted.sort_unstable();
733        let mut decoder = Decoder::new(Cursor::new(self.bytes)).map_err(GeoTiffError::from);
734        let mut decoded: Option<(u32, Result<DecodingResult, GeoTiffError>)> = None;
735        for (index, at, i) in wanted {
736            if decoded.as_ref().is_none_or(|(d, _)| *d != index) {
737                let chunk = match &mut decoder {
738                    Ok(decoder) => decoder.read_chunk(index).map_err(GeoTiffError::from),
739                    Err(e) => Err(e.clone()),
740                };
741                decoded = Some((index, chunk));
742            }
743            if let (Some((_, chunk)), Some(slot)) = (&decoded, out.get_mut(i)) {
744                *slot = match chunk {
745                    Ok(chunk) => chunk_sample(chunk, index, at).map(|v| self.unless_nodata(v)),
746                    Err(e) => Err(e.clone()),
747                };
748            }
749        }
750        out
751    }
752
753    /// The pixel holding a point, or why there is none.
754    fn place(&self, latitude_deg: f64, longitude_deg: f64) -> Result<Pixel, GeoTiffError> {
755        if !(latitude_deg.abs() <= 90.0 && longitude_deg.is_finite()) {
756            return Err(GeoTiffError::Location {
757                latitude_deg,
758                longitude_deg,
759            });
760        }
761        self.info
762            .pixel_of(latitude_deg, longitude_deg)
763            .ok_or_else(|| GeoTiffError::Outside {
764                latitude_deg,
765                longitude_deg,
766                bounds: self.info.bounds(),
767            })
768    }
769
770    /// The tile or strip holding a pixel, which must be on the raster, and the pixel's place in
771    /// it once decoded. A chunk decodes to its data's size, without the padding of the last
772    /// tiles, so its row length is the data's width. The index is below the chunk count, which
773    /// `parse` holds within a u32.
774    fn locate(&self, pixel: Pixel) -> (u32, usize) {
775        let Pixel { row, col } = pixel;
776        let (index, chunk_row, chunk_col, data_width) = match self.chunk_type {
777            ChunkType::Strip => (
778                row / self.chunk_height,
779                row % self.chunk_height,
780                col,
781                self.info.width,
782            ),
783            ChunkType::Tile => {
784                let across = self.info.width.div_ceil(self.chunk_width);
785                let col0 = (col / self.chunk_width) * self.chunk_width;
786                (
787                    (row / self.chunk_height) * across + col / self.chunk_width,
788                    row % self.chunk_height,
789                    col - col0,
790                    self.chunk_width.min(self.info.width - col0),
791                )
792            }
793        };
794        let at = u64::from(chunk_row) * u64::from(data_width) + u64::from(chunk_col);
795        // Under one chunk's size, which `parse` holds under MAX_CHUNK_BYTES; a value that did
796        // not fit a usize would read as a short chunk.
797        (index, usize::try_from(at).unwrap_or(usize::MAX))
798    }
799
800    /// Every pixel's raw value, row by row from the first; NaN where it is nodata. Each tile or
801    /// strip is decoded once.
802    ///
803    /// # Errors
804    ///
805    /// [`GeoTiffError::TooLarge`] past [`MAX_VALUES_PIXELS`] or when the memory can't be had,
806    /// [`GeoTiffError::Tiff`] if a tile will not decode.
807    pub fn values(&self) -> Result<Vec<f64>, GeoTiffError> {
808        let (width, height) = (self.info.width, self.info.height);
809        let pixels = u64::from(width) * u64::from(height);
810        let too_large = || GeoTiffError::TooLarge {
811            what: "a raster, in pixels,",
812            size: pixels,
813            limit: MAX_VALUES_PIXELS,
814        };
815        if pixels > MAX_VALUES_PIXELS {
816            return Err(too_large());
817        }
818        let n = usize::try_from(pixels).map_err(|_| too_large())?;
819        let mut decoder = Decoder::new(Cursor::new(self.bytes))?;
820        let mut out = Vec::new();
821        let (across, down) = match self.chunk_type {
822            ChunkType::Strip => (1, height.div_ceil(self.chunk_height)),
823            ChunkType::Tile => (
824                width.div_ceil(self.chunk_width),
825                height.div_ceil(self.chunk_height),
826            ),
827        };
828        let chunk_width = match self.chunk_type {
829            ChunkType::Strip => width,
830            ChunkType::Tile => self.chunk_width,
831        };
832        for chunk_row in 0..down {
833            for chunk_col in 0..across {
834                let index = chunk_row * across + chunk_col;
835                let chunk = decoder.read_chunk(index)?;
836                let col0 = chunk_col * chunk_width;
837                let row0 = chunk_row * self.chunk_height;
838                let data_width = chunk_width.min(width - col0);
839                let data_height = self.chunk_height.min(height - row0);
840                if chunk_col == 0 {
841                    // The raster grows as rows of tiles decode, doubling up to its size, so a
842                    // file whose tiles fail early allocates little. A tall tile grows it by its
843                    // whole row; `MAX_VALUES_PIXELS` bounds that.
844                    let rows = u64::from(row0 + data_height) * u64::from(width);
845                    let rows = usize::try_from(rows).map_err(|_| too_large())?;
846                    if rows > out.capacity() {
847                        let target = rows.max(out.capacity().saturating_mul(2)).min(n);
848                        out.try_reserve_exact(target - out.len())
849                            .map_err(|_| too_large())?;
850                    }
851                    out.resize(rows, f64::NAN);
852                }
853                for r in 0..data_height {
854                    for c in 0..data_width {
855                        let at = u64::from(r) * u64::from(data_width) + u64::from(c);
856                        let at = usize::try_from(at).unwrap_or(usize::MAX);
857                        let value = chunk_sample(&chunk, index, at)?;
858                        let to = u64::from(row0 + r) * u64::from(width) + u64::from(col0 + c);
859                        // Within the rows grown above.
860                        if let Some(slot) = usize::try_from(to).ok().and_then(|to| out.get_mut(to))
861                        {
862                            *slot = self.unless_nodata(value).unwrap_or(f64::NAN);
863                        }
864                    }
865                }
866            }
867        }
868        Ok(out)
869    }
870
871    fn unless_nodata(&self, value: f64) -> Option<f64> {
872        let nodata = self.info.nodata.is_some_and(|n| value == n);
873        (!(nodata || value.is_nan())).then_some(value)
874    }
875}
876
877/// Sample `at` of decoded chunk `index`, or why the chunk is short.
878fn chunk_sample(chunk: &DecodingResult, index: u32, at: usize) -> Result<f64, GeoTiffError> {
879    sample_at(chunk, at).ok_or_else(|| GeoTiffError::Malformed {
880        what: "a tile or strip",
881        reason: format!("chunk {index} holds no pixel {at}"),
882    })
883}
884
885/// Sample `at` of a decoded chunk, as `f64` (exact for every type read).
886fn sample_at(chunk: &DecodingResult, at: usize) -> Option<f64> {
887    match chunk {
888        DecodingResult::U8(v) => v.get(at).map(|&x| f64::from(x)),
889        DecodingResult::I8(v) => v.get(at).map(|&x| f64::from(x)),
890        DecodingResult::U16(v) => v.get(at).map(|&x| f64::from(x)),
891        DecodingResult::I16(v) => v.get(at).map(|&x| f64::from(x)),
892        DecodingResult::U32(v) => v.get(at).map(|&x| f64::from(x)),
893        DecodingResult::I32(v) => v.get(at).map(|&x| f64::from(x)),
894        DecodingResult::F32(v) => v.get(at).map(|&x| f64::from(x)),
895        DecodingResult::F64(v) => v.get(at).copied(),
896        // `sample_type` refuses these before any chunk is decoded.
897        DecodingResult::U64(_) | DecodingResult::I64(_) | DecodingResult::F16(_) => None,
898    }
899}
900
901/// Refuses a codec this build leaves out, naming it, before any pixel is read.
902fn compression<R: std::io::Read + std::io::Seek>(
903    decoder: &mut Decoder<R>,
904) -> Result<(), GeoTiffError> {
905    let code: u16 = decoder
906        .find_tag_unsigned(Tag::Compression)?
907        .unwrap_or(CompressionMethod::None.to_u16());
908    let name = match CompressionMethod::from_u16(code) {
909        Some(
910            CompressionMethod::None
911            | CompressionMethod::LZW
912            | CompressionMethod::Deflate
913            | CompressionMethod::OldDeflate
914            | CompressionMethod::PackBits,
915        ) => return Ok(()),
916        Some(CompressionMethod::ZSTD) => "zstd",
917        Some(CompressionMethod::WebP) => "WebP",
918        Some(CompressionMethod::JPEG | CompressionMethod::ModernJPEG) => "JPEG",
919        Some(CompressionMethod::Fax3 | CompressionMethod::Fax4 | CompressionMethod::Huffman) => {
920            "fax"
921        }
922        _ => "an unlisted codec",
923    };
924    Err(GeoTiffError::Unsupported {
925        what: "compression",
926        value: format!("{code} ({name})"),
927        hint: "; LZW, Deflate and PackBits are read: `gdal_translate -co COMPRESS=DEFLATE in.tif out.tif` rewrites it",
928    })
929}
930
931/// Refuses a file holding an internal mask (an image with bit 4 of `NewSubfileType`), which
932/// GDAL reads as nodata and this reader would not. The decoder is left on a later image.
933fn no_mask<R: std::io::Read + std::io::Seek>(decoder: &mut Decoder<R>) -> Result<(), GeoTiffError> {
934    for _ in 0..MAX_IMAGES_WALKED {
935        if !decoder.more_images() {
936            return Ok(());
937        }
938        // A later image the decoder can't read is no mask GDAL would apply either: the walk
939        // stops there, the first image being the one read.
940        if decoder.next_image().is_err() {
941            return Ok(());
942        }
943        let Ok(kind) = decoder.find_tag_unsigned::<u32>(Tag::NewSubfileType) else {
944            return Ok(());
945        };
946        let kind = kind.unwrap_or(0);
947        if kind & 4 != 0 {
948            return Err(GeoTiffError::Unsupported {
949                what: "an internal nodata mask, NewSubfileType",
950                value: kind.to_string(),
951                hint: "; `gdalwarp -dstnodata <value> in.tif out.tif` turns it into a nodata value",
952            });
953        }
954    }
955    Ok(())
956}
957
958fn sample_type<R: std::io::Read + std::io::Seek>(
959    decoder: &mut Decoder<R>,
960) -> Result<SampleType, GeoTiffError> {
961    let bits: Vec<u16> = decoder.get_tag_u16_vec(Tag::BitsPerSample)?;
962    let format: u16 = decoder
963        .find_tag_unsigned_vec::<u16>(Tag::SampleFormat)?
964        .and_then(|v| v.first().copied())
965        .unwrap_or(SampleFormat::Uint.to_u16());
966    let bits = bits.first().copied().unwrap_or(1);
967    let uint = SampleFormat::Uint.to_u16();
968    let int = SampleFormat::Int.to_u16();
969    let float = SampleFormat::IEEEFP.to_u16();
970    Ok(match (format, bits) {
971        (f, 8) if f == uint => SampleType::U8,
972        (f, 8) if f == int => SampleType::I8,
973        (f, 16) if f == uint => SampleType::U16,
974        (f, 16) if f == int => SampleType::I16,
975        (f, 32) if f == uint => SampleType::U32,
976        (f, 32) if f == int => SampleType::I32,
977        (f, 32) if f == float => SampleType::F32,
978        (f, 64) if f == float => SampleType::F64,
979        (f, b) => {
980            return Err(GeoTiffError::Unsupported {
981                what: "a sample of format and bits",
982                value: format!("{f}, {b}"),
983                hint: "",
984            });
985        }
986    })
987}
988
989/// The GeoKey directory's SHORT keys (OGC 19-008r4 §7.1.2): those stored in the directory itself.
990struct GeoKeys {
991    /// The directory's minor revision: 0 for GeoTIFF 1.0, 1 for 1.1.
992    minor: u16,
993    /// (key, location, value): location 0 means `value` is the key's value.
994    entries: Vec<(u16, u16, u16)>,
995}
996
997impl GeoKeys {
998    fn read<R: std::io::Read + std::io::Seek>(
999        decoder: &mut Decoder<R>,
1000    ) -> Result<Self, GeoTiffError> {
1001        let Some(dir) = decoder.find_tag_unsigned_vec::<u16>(Tag::GeoKeyDirectoryTag)? else {
1002            return Err(GeoTiffError::Missing {
1003                what: "GeoKeyDirectoryTag (34735), so it is not a GeoTIFF",
1004            });
1005        };
1006        let malformed = |reason: String| GeoTiffError::Malformed {
1007            what: "GeoKeyDirectoryTag",
1008            reason,
1009        };
1010        let [version, revision, minor, count, ..] = dir[..] else {
1011            return Err(malformed(format!(
1012                "{} values, under the header's 4",
1013                dir.len()
1014            )));
1015        };
1016        if version != 1 || revision != 1 {
1017            return Err(malformed(format!(
1018                "version {version}, revision {revision}; the standard's are 1 and 1"
1019            )));
1020        }
1021        let needed = 4 + 4 * usize::from(count);
1022        if dir.len() < needed {
1023            return Err(malformed(format!(
1024                "{count} keys need {needed} values, it has {}",
1025                dir.len()
1026            )));
1027        }
1028        let entries = dir[4..needed]
1029            .as_chunks::<4>()
1030            .0
1031            .iter()
1032            .map(|e| (e[0], e[1], e[3]))
1033            .collect();
1034        Ok(GeoKeys { minor, entries })
1035    }
1036
1037    /// Whether the directory holds `key` at all.
1038    fn has(&self, key: u16) -> bool {
1039        self.entries.iter().any(|e| e.0 == key)
1040    }
1041
1042    /// A SHORT key's value, `None` if absent. A key repeated is refused.
1043    fn get(&self, key: u16) -> Result<Option<u16>, GeoTiffError> {
1044        let mut found = self.entries.iter().filter(|e| e.0 == key);
1045        let Some(&(_, location, value)) = found.next() else {
1046            return Ok(None);
1047        };
1048        if found.next().is_some() {
1049            return Err(GeoTiffError::Malformed {
1050                what: "GeoKeyDirectoryTag",
1051                reason: format!("key {key} appears twice"),
1052            });
1053        }
1054        if location != 0 {
1055            return Err(GeoTiffError::Malformed {
1056                what: "GeoKeyDirectoryTag",
1057                reason: format!("key {key} is a SHORT but is stored in tag {location}"),
1058            });
1059        }
1060        Ok(Some(value))
1061    }
1062}
1063
1064fn geographic_crs(keys: &GeoKeys) -> Result<u16, GeoTiffError> {
1065    let projected = keys.get(PROJECTED_CRS_KEY)?;
1066    let geodetic = keys.get(GEODETIC_CRS_KEY)?;
1067    match keys.get(MODEL_TYPE_KEY)? {
1068        Some(2) => {}
1069        // GeoTIFF 1.0 files from some writers name a geodetic CRS without a model type.
1070        None if projected.is_none() && geodetic.is_some() => {}
1071        None if projected.is_none() => {
1072            return Err(GeoTiffError::Missing {
1073                what: "GTModelTypeGeoKey (1024) or a geodetic CRS (2048)",
1074            });
1075        }
1076        Some(1) | None => {
1077            return Err(GeoTiffError::Unsupported {
1078                what: "a projected CRS, EPSG",
1079                value: projected.map_or_else(|| "unnamed".into(), |c| c.to_string()),
1080                hint: "; reproject it to latitude and longitude, as with `gdalwarp -t_srs EPSG:4326 in.tif out.tif`",
1081            });
1082        }
1083        Some(other) => {
1084            return Err(GeoTiffError::Unsupported {
1085                what: "GTModelTypeGeoKey",
1086                value: other.to_string(),
1087                hint: "; only a geographic CRS (2) is read",
1088            });
1089        }
1090    }
1091    let Some(code) = geodetic.filter(|c| NEAR_WGS84.iter().any(|(near, _)| near == c)) else {
1092        return Err(GeoTiffError::Unsupported {
1093            what: "a geographic CRS, EPSG",
1094            value: geodetic.map_or_else(|| "unnamed".into(), |c| c.to_string()),
1095            hint: "; only datums within a few meters of WGS 84 are read (`NEAR_WGS84`): `gdalwarp -t_srs EPSG:4326 in.tif out.tif` converts it",
1096        });
1097    };
1098    match keys.get(ANGULAR_UNITS_KEY)? {
1099        // 9102 is EPSG's degree, 9122 its degree as a CRS's unit.
1100        None | Some(9102 | 9122) => {}
1101        Some(other) => {
1102            return Err(GeoTiffError::Unsupported {
1103                what: "GeogAngularUnitsGeoKey",
1104                value: other.to_string(),
1105                hint: "; only degrees are read",
1106            });
1107        }
1108    }
1109    match keys.get(PRIME_MERIDIAN_KEY)? {
1110        None => {}
1111        Some(GREENWICH) => {}
1112        Some(other) => {
1113            return Err(GeoTiffError::Unsupported {
1114                what: "a prime meridian, EPSG",
1115                value: other.to_string(),
1116                hint: "; only Greenwich (8901) is read",
1117            });
1118        }
1119    }
1120    Ok(code)
1121}
1122
1123/// Refuses vertical keys GDAL 3.12.2 drops, unit and all, or reads by rules of its own
1124/// (`gt_wkt_srs.cpp`, each case measured through rasterio 1.5.2): a private value (above 32767)
1125/// in any of them (dropped with a model type, read otherwise), any beside WGS 84 3D,
1126/// `VerticalDatumGeoKey` 6030 beside WGS 84 with model type 2 (which GDAL turns into WGS 84 3D),
1127/// and any with no model type and no unit key (GDAL drops the vertical CRS, or the whole CRS).
1128/// With model type 2 this reader would read a unit there that GDAL doesn't report; with none,
1129/// GDAL reads the unit key alone, and the refusal is conservative.
1130fn vertical_kept_by_gdal(keys: &GeoKeys, geographic_crs_epsg: u16) -> Result<(), GeoTiffError> {
1131    let mut present = Vec::new();
1132    for key in [VERTICAL_CRS_KEY, VERTICAL_DATUM_KEY, VERTICAL_UNITS_KEY] {
1133        if let Some(value) = keys.get(key)? {
1134            present.push((key, value));
1135        }
1136    }
1137    let dropped = |value: &str| GeoTiffError::Unsupported {
1138        what: "vertical keys GDAL drops or reads by rules of its own:",
1139        value: value.to_string(),
1140        hint: "; the heights' unit is uncertain",
1141    };
1142    if let Some((key, value)) = present.iter().find(|(_, value)| *value > 32767) {
1143        return Err(dropped(&format!(
1144            "key {key} holds the private value {value}"
1145        )));
1146    }
1147    if !present.is_empty() && geographic_crs_epsg == 4979 {
1148        return Err(dropped("vertical keys beside WGS 84 3D (EPSG:4979)"));
1149    }
1150    if geographic_crs_epsg == 4326
1151        && keys.get(MODEL_TYPE_KEY)? == Some(2)
1152        && keys.get(VERTICAL_DATUM_KEY)? == Some(6030)
1153    {
1154        return Err(dropped(
1155            "VerticalDatumGeoKey 6030 beside WGS 84 with model type 2, which GDAL reads as WGS 84 3D",
1156        ));
1157    }
1158    if keys.get(MODEL_TYPE_KEY)?.is_none() && !present.is_empty() && !keys.has(VERTICAL_UNITS_KEY) {
1159        return Err(dropped("vertical keys with no model type and no unit key"));
1160    }
1161    Ok(())
1162}
1163
1164fn vertical(keys: &GeoKeys) -> Result<(Option<u16>, VerticalUnit, bool), GeoTiffError> {
1165    // 0 is "undefined" and 32767 "user-defined" (those above, private, are refused before): none
1166    // is an EPSG code.
1167    let crs = keys
1168        .get(VERTICAL_CRS_KEY)?
1169        .filter(|c| (1..32767).contains(c));
1170    let user_defined = keys.get(VERTICAL_CRS_KEY)?.is_some_and(|c| c >= 32767);
1171    let stated = match keys.get(VERTICAL_UNITS_KEY)? {
1172        Some(code) => Some(
1173            VerticalUnit::from_epsg(code).ok_or(GeoTiffError::Unsupported {
1174                what: "VerticalUnitsGeoKey",
1175                value: code.to_string(),
1176                hint: "; meters (9001), feet (9002) and US survey feet (9003) are read",
1177            })?,
1178        ),
1179        None => None,
1180    };
1181    let from_crs = crs.and_then(|c| {
1182        VERTICAL_CRS_UNITS
1183            .iter()
1184            .find(|(code, _)| *code == c)
1185            .map(|&(_, unit)| unit)
1186    });
1187    match (stated, from_crs, crs) {
1188        // A vertical CRS this reader doesn't know: GDAL takes its unit from EPSG's registry,
1189        // whatever `VerticalUnitsGeoKey` says, and its unit could be feet.
1190        (_, None, Some(4979)) => Err(GeoTiffError::Unsupported {
1191            what: "a vertical CRS this reader doesn't know, EPSG",
1192            value: "4979".to_string(),
1193            hint: ": WGS 84 3D, whose heights are above the ellipsoid, not sea level; \
1194                   `gdalwarp -t_srs EPSG:4326+3855 in.tif out.tif` converts them to EGM2008 \
1195                   where PROJ has its geoid grid",
1196        }),
1197        (_, None, Some(code)) => Err(GeoTiffError::Unsupported {
1198            what: "a vertical CRS this reader doesn't know, EPSG",
1199            value: code.to_string(),
1200            hint: "; its unit is unknown here (`VERTICAL_CRS_UNITS` lists those known)",
1201        }),
1202        // GDAL takes a known CRS's unit and ignores the key; the file's writer may have meant
1203        // the key. Neither reading is safe.
1204        (Some(key), Some(of_crs), Some(code)) if key != of_crs => Err(GeoTiffError::Unsupported {
1205            what: "a vertical unit given twice, differently:",
1206            value: format!(
1207                "{key:?} by VerticalUnitsGeoKey, {of_crs:?} by vertical CRS EPSG:{code}"
1208            ),
1209            hint: "",
1210        }),
1211        (Some(unit), _, _) | (None, Some(unit), _) => Ok((crs, unit, true)),
1212        (None, None, None) if user_defined => Err(GeoTiffError::Unsupported {
1213            what: "a user-defined vertical CRS without VerticalUnitsGeoKey",
1214            value: "VerticalGeoKey 32767".to_string(),
1215            hint: "; its unit is unknown here",
1216        }),
1217        (None, None, None) => Ok((None, VerticalUnit::Meter, false)),
1218    }
1219}
1220
1221/// The georeferencing: `corner` is `[λ₀, Δλ, φ₀, Δφ]` for pixel is area, and `z` the pixel
1222/// scale's `S_z` and the tiepoint's `z₀` and `Z₀`, when the file has them.
1223struct Georef {
1224    corner: [f64; 4],
1225    z: Option<[f64; 3]>,
1226}
1227
1228/// The georeferencing from the tiepoint and pixel scale or the transformation matrix (the module
1229/// docs).
1230fn transform<R: std::io::Read + std::io::Seek>(
1231    decoder: &mut Decoder<R>,
1232) -> Result<Georef, GeoTiffError> {
1233    let scale = decoder.find_tag(Tag::ModelPixelScaleTag)?;
1234    let tiepoint = decoder.find_tag(Tag::ModelTiepointTag)?;
1235    let matrix = decoder.find_tag(Tag::ModelTransformationTag)?;
1236    let doubles = |what: &'static str, v: tiff::decoder::ifd::Value| {
1237        v.into_f64_vec().map_err(|e| GeoTiffError::Malformed {
1238            what,
1239            reason: e.to_string(),
1240        })
1241    };
1242    let out = match (scale, tiepoint, matrix) {
1243        (Some(_), _, Some(_)) => {
1244            return Err(GeoTiffError::Malformed {
1245                what: "the georeferencing",
1246                reason: "ModelPixelScaleTag and ModelTransformationTag together, which the standard forbids (§7.1.1)".into(),
1247            });
1248        }
1249        (Some(scale), Some(tiepoint), None) => {
1250            let scale = doubles("ModelPixelScaleTag", scale)?;
1251            let tie = doubles("ModelTiepointTag", tiepoint)?;
1252            let [sx, sy, ref rest @ ..] = scale[..] else {
1253                return Err(GeoTiffError::Malformed {
1254                    what: "ModelPixelScaleTag",
1255                    reason: format!("{} values, not 3", scale.len()),
1256                });
1257            };
1258            let [i, j, z0, x, y, z] = tie[..] else {
1259                return Err(GeoTiffError::Unsupported {
1260                    what: "ModelTiepointTag with values numbering",
1261                    value: tie.len().to_string(),
1262                    hint: "; one tiepoint (6 values) with a pixel scale is read, not ground control points",
1263                });
1264            };
1265            if sy < 0.0 {
1266                return Err(GeoTiffError::Unsupported {
1267                    what: "a negative ModelPixelScaleTag Y,",
1268                    value: sy.to_string(),
1269                    hint: "; the standard reads it south-up and GDAL north-up",
1270                });
1271            }
1272            // GDAL's order of operations, so a corner reads as GDAL reads it.
1273            let dlat = -sy;
1274            Georef {
1275                corner: [x - i * sx, sx, y - j * dlat, dlat],
1276                z: rest.first().map(|&sz| [sz, z0, z]),
1277            }
1278        }
1279        (None, _, Some(matrix)) => {
1280            let m = doubles("ModelTransformationTag", matrix)?;
1281            // The standard's letters: X = a·I + b·J + d, Y = e·I + f·J + h.
1282            let [a, b, _c, d, e, f, _g, h, ..] = m[..] else {
1283                return Err(GeoTiffError::Malformed {
1284                    what: "ModelTransformationTag",
1285                    reason: format!("{} values, not 16", m.len()),
1286                });
1287            };
1288            if m.len() != 16 {
1289                return Err(GeoTiffError::Malformed {
1290                    what: "ModelTransformationTag",
1291                    reason: format!("{} values, not 16", m.len()),
1292                });
1293            }
1294            if b != 0.0 || e != 0.0 {
1295                return Err(GeoTiffError::Unsupported {
1296                    what: "a rotated or sheared raster, with terms b and e",
1297                    value: format!("{b}, {e}"),
1298                    hint: "; `gdalwarp` turns it north-up",
1299                });
1300            }
1301            Georef {
1302                corner: [d, a, h, f],
1303                z: None,
1304            }
1305        }
1306        (None, _, None) => {
1307            return Err(GeoTiffError::Missing {
1308                what: "ModelPixelScaleTag or ModelTransformationTag",
1309            });
1310        }
1311        (Some(_), None, None) => {
1312            return Err(GeoTiffError::Missing {
1313                what: "ModelTiepointTag",
1314            });
1315        }
1316    };
1317    let c = out.corner;
1318    if !(c.iter().all(|v| v.is_finite()) && c[1] != 0.0 && c[3] != 0.0) {
1319        return Err(GeoTiffError::Malformed {
1320            what: "the georeferencing",
1321            reason: format!(
1322                "corner ({}, {}) and pixel size ({}, {})",
1323                c[0], c[2], c[1], c[3]
1324            ),
1325        });
1326    }
1327    Ok(out)
1328}
1329
1330/// Whether GDAL takes a scale and offset from `S_z` and the tiepoint's heights. It does for one
1331/// band when its CRS is vertical, which depends on the directory's revision and on how GDAL and
1332/// PROJ resolve the vertical keys (GDAL 3.12.2's `gt_wkt_srs.cpp`, with model type 2, drops the
1333/// vertical CRS for a private key value and for `VerticalDatumGeoKey` 6030 beside WGS 84, and the
1334/// whole CRS beside WGS 84 3D; with no model type it builds a local CRS, with a vertical part only
1335/// when there is a unit key). Certain: applied for a GeoTIFF 1.1 directory of model type 2 naming
1336/// a vertical CRS this reader knows, with no datum key, beside any geographic CRS but WGS 84 3D;
1337/// ignored for a 1.0 directory (GDAL drops its vertical CRS, rasterio 1.5.2 shows) and for one
1338/// with no vertical key. Anything between is refused, unless the tags give GDAL's own scale 1 and
1339/// offset 0 and `GDAL_METADATA` no other scale.
1340#[derive(Clone, Copy, PartialEq, Eq)]
1341enum ZTerms {
1342    Applied,
1343    Ignored,
1344    Unknown,
1345}
1346
1347/// The pixels' scale and offset as GDAL sets them (the module docs), and `GDAL_METADATA`'s unit
1348/// if it gives one: from the pixel scale's `S_z` and the tiepoint's heights where [`ZTerms`]
1349/// applies them, otherwise from `GDAL_METADATA`.
1350fn scale_offset<R: std::io::Read + std::io::Seek>(
1351    decoder: &mut Decoder<R>,
1352    georef: &Georef,
1353    heights: ZTerms,
1354) -> Result<(f64, f64, Option<VerticalUnit>), GeoTiffError> {
1355    let from_tags = match georef.z {
1356        // GDAL's rule: one band, a vertical CRS, and any of S_z, z₀, Z₀ non-zero.
1357        Some([sz, z0, z]) if sz != 0.0 || z0 != 0.0 || z != 0.0 => match heights {
1358            ZTerms::Applied => Some((sz, z - z0 * sz)),
1359            ZTerms::Ignored => None,
1360            // GDAL's own pair, read the same applied or not: unless GDAL_METADATA gives another,
1361            // which wins only where GDAL drops the vertical CRS, and is refused below.
1362            ZTerms::Unknown if (sz, z - z0 * sz) == (1.0, 0.0) => Some((1.0, 0.0)),
1363            ZTerms::Unknown => {
1364                return Err(GeoTiffError::Unsupported {
1365                    what: "heights in ModelPixelScaleTag or ModelTiepointTag,",
1366                    value: format!("S_z {sz}, z₀ {z0}, Z₀ {z}"),
1367                    hint: ", with vertical keys GDAL may or may not read as a vertical CRS",
1368                });
1369            }
1370        },
1371        _ => None,
1372    };
1373    let metadata = gdal_metadata(decoder)?;
1374    let from_metadata = (metadata.scale.is_some() || metadata.offset.is_some()).then(|| {
1375        (
1376            metadata.scale.unwrap_or(1.0),
1377            metadata.offset.unwrap_or(0.0),
1378        )
1379    });
1380    let (scale, offset) = match (from_tags, from_metadata) {
1381        (Some(tags), Some(meta)) if tags != meta => {
1382            return Err(GeoTiffError::Unsupported {
1383                what: "a pixel scale and offset given twice, differently:",
1384                value: format!("{tags:?} by the tags, {meta:?} by GDAL_METADATA"),
1385                hint: "",
1386            });
1387        }
1388        (Some(pair), _) | (None, Some(pair)) => pair,
1389        (None, None) => (1.0, 0.0),
1390    };
1391    if !(scale.is_finite() && scale != 0.0 && offset.is_finite()) {
1392        return Err(GeoTiffError::Malformed {
1393            what: "the pixel scale and offset",
1394            reason: format!("scale {scale}, offset {offset}"),
1395        });
1396    }
1397    Ok((scale, offset, metadata.unit))
1398}
1399
1400/// What GDAL's `GDAL_METADATA` tag says of band 1.
1401#[derive(Default)]
1402struct Metadata {
1403    scale: Option<f64>,
1404    offset: Option<f64>,
1405    unit: Option<VerticalUnit>,
1406}
1407
1408/// The deepest nesting `GDAL_METADATA` may have: GDAL writes a root and its items, two levels.
1409const MAX_METADATA_DEPTH: usize = 16;
1410
1411/// The blanks C's number parsing skips; Rust's `trim` would also take Unicode spaces, which GDAL
1412/// keeps.
1413const ASCII_BLANK: [char; 6] = [' ', '\t', '\n', '\r', '\u{b}', '\u{c}'];
1414
1415/// A unit as GDAL's `unittype` item names it; `None` for an empty name.
1416fn unit_type(name: &str) -> Result<Option<VerticalUnit>, GeoTiffError> {
1417    let lower = name.trim_matches(ASCII_BLANK).to_ascii_lowercase();
1418    Ok(Some(match lower.as_str() {
1419        "" => return Ok(None),
1420        "m" | "metre" | "meter" | "metres" | "meters" => VerticalUnit::Meter,
1421        "ft" | "foot" | "feet" | "international foot" => VerticalUnit::Foot,
1422        "us survey foot" | "us survey feet" | "ftus" | "us-ft" => VerticalUnit::UsSurveyFoot,
1423        _ => {
1424            return Err(GeoTiffError::Unsupported {
1425                what: "a GDAL_METADATA unit type",
1426                value: format!("{name:?}"),
1427                hint: "; meters, feet and US survey feet are read",
1428            });
1429        }
1430    }))
1431}
1432
1433/// The first band's `scale`, `offset` and `unittype` items in GDAL's `GDAL_METADATA` XML.
1434fn gdal_metadata<R: std::io::Read + std::io::Seek>(
1435    decoder: &mut Decoder<R>,
1436) -> Result<Metadata, GeoTiffError> {
1437    let Some(value) = decoder.find_tag(Tag::Unknown(GDAL_METADATA_TAG))? else {
1438        return Ok(Metadata::default());
1439    };
1440    let malformed = |reason: String| GeoTiffError::Malformed {
1441        what: "GDAL_METADATA",
1442        reason,
1443    };
1444    let text = value.into_string().map_err(|e| malformed(e.to_string()))?;
1445    let text = text.trim_matches(|c: char| c == '\0' || c.is_whitespace());
1446    // roxmltree recurses on nesting; the tag is the file's, so its depth is bounded first.
1447    let depth = crate::ork::document::deepest_nesting(text);
1448    if depth > MAX_METADATA_DEPTH {
1449        return Err(malformed(format!(
1450            "nested {depth} deep, past {MAX_METADATA_DEPTH}"
1451        )));
1452    }
1453    let document = roxmltree::Document::parse(text).map_err(|e| malformed(e.to_string()))?;
1454    let mut metadata = Metadata::default();
1455    let root = document.root_element();
1456    let named = |n: &roxmltree::Node, name: &str| {
1457        n.is_element() && n.tag_name().name().eq_ignore_ascii_case(name)
1458    };
1459    if !named(&root, "GDALMetadata") {
1460        return Ok(metadata);
1461    }
1462    let odd = |reason: &str| GeoTiffError::Unsupported {
1463        what: "GDAL_METADATA",
1464        value: reason.to_string(),
1465        hint: ", not in the form GDAL writes, which GDAL may read differently",
1466    };
1467    if root.tag_name().namespace().is_some() {
1468        return Err(odd("a GDALMetadata root in an XML namespace"));
1469    }
1470    // GDAL's matching has quirks (attribute names in any case and compared with their prefix,
1471    // C's `atoi` for the sample, text only when it is an item's one child, CDATA a child of its
1472    // own). An item any of whose `role` attributes names a role read here is read only in the
1473    // form GDAL writes, `<Item name=".." sample="0" role="scale">0.25</Item>`, and refused in any
1474    // other; GDAL's measured skips (no name, no sample, another band) are skipped.
1475    let ours = |value: &str| {
1476        matches!(
1477            value.to_ascii_lowercase().as_str(),
1478            "scale" | "offset" | "unittype"
1479        )
1480    };
1481    for item in root.children().filter(|n| named(n, "Item")) {
1482        if !item
1483            .attributes()
1484            .any(|a| a.name().eq_ignore_ascii_case("role") && ours(a.value()))
1485        {
1486            continue;
1487        }
1488        if item.tag_name().namespace().is_some()
1489            || item.attributes().any(|a| {
1490                a.namespace().is_some() || a.name().bytes().any(|b| b.is_ascii_uppercase())
1491            })
1492        {
1493            return Err(odd(
1494                "an item with a namespace or an attribute name in capitals",
1495            ));
1496        }
1497        let (Some(_), Some(sample)) = (item.attribute("name"), item.attribute("sample")) else {
1498            continue;
1499        };
1500        let band = (!sample.is_empty() && sample.bytes().all(|b| b.is_ascii_digit()))
1501            .then(|| sample.parse::<i32>().ok())
1502            .flatten()
1503            .ok_or_else(|| odd("an item whose sample is not a plain number"))?;
1504        if band != 0 {
1505            continue;
1506        }
1507        if item
1508            .attribute("domain")
1509            .is_some_and(|d| d.eq_ignore_ascii_case("IMAGE_STRUCTURE"))
1510        {
1511            return Err(odd("a scale, offset or unit in the IMAGE_STRUCTURE domain"));
1512        }
1513        // Past the checks above the one `role` is plain and lower-case, and names a role read here.
1514        let role = item.attribute("role").unwrap_or("").to_ascii_lowercase();
1515        // roxmltree joins CDATA to the text beside it; GDAL keeps them apart, so look at the source.
1516        if document.input_text()[item.range()].contains("<![CDATA[") {
1517            return Err(odd("an item whose value is not plain text"));
1518        }
1519        let mut children = item.children();
1520        let (text, source) = match (children.next(), children.next()) {
1521            (None, _) => continue,
1522            (Some(only), None) if only.is_text() => (
1523                only.text().unwrap_or("").trim_matches(ASCII_BLANK),
1524                &document.input_text()[only.range()],
1525            ),
1526            _ => return Err(odd("an item whose value is not plain text")),
1527        };
1528        if text.is_empty() {
1529            // GDAL drops blanks typed as they are, but keeps one written as a character
1530            // reference, and reads it as 0 or a blank unit.
1531            if source.contains('&') {
1532                return Err(odd("an item whose value is a blank character reference"));
1533            }
1534            continue;
1535        }
1536        let slot = match role.as_str() {
1537            "scale" => &mut metadata.scale,
1538            "offset" => &mut metadata.offset,
1539            "unittype" => {
1540                metadata.unit = unit_type(text)?;
1541                continue;
1542            }
1543            // The checks above leave one of the three.
1544            _ => continue,
1545        };
1546        *slot = Some(
1547            text.parse::<f64>()
1548                .map_err(|_| malformed(format!("{text:?} is not a number")))?,
1549        );
1550    }
1551    Ok(metadata)
1552}
1553
1554fn nodata<R: std::io::Read + std::io::Seek>(
1555    decoder: &mut Decoder<R>,
1556) -> Result<Option<f64>, GeoTiffError> {
1557    let Some(value) = decoder.find_tag(Tag::Unknown(GDAL_NODATA_TAG))? else {
1558        return Ok(None);
1559    };
1560    let text = value.into_string().map_err(|e| GeoTiffError::Malformed {
1561        what: "GDAL_NODATA",
1562        reason: e.to_string(),
1563    })?;
1564    let text = text.trim_matches(|c: char| c == '\0' || c.is_whitespace());
1565    text.parse::<f64>()
1566        .map(Some)
1567        .map_err(|_| GeoTiffError::Malformed {
1568            what: "GDAL_NODATA",
1569            reason: format!("{text:?} is not a number"),
1570        })
1571}
1572
1573#[cfg(test)]
1574mod tests;