1use std::io::Cursor;
111
112use serde::{Deserialize, Serialize};
113use tiff::decoder::{ChunkType, Decoder, DecodingResult};
114use tiff::tags::{CompressionMethod, PhotometricInterpretation, SampleFormat, Tag};
115
116const MODEL_TYPE_KEY: u16 = 1024;
118const RASTER_TYPE_KEY: u16 = 1025;
120const GEODETIC_CRS_KEY: u16 = 2048;
122const PRIME_MERIDIAN_KEY: u16 = 2051;
124const ANGULAR_UNITS_KEY: u16 = 2054;
126const PROJECTED_CRS_KEY: u16 = 3072;
128const VERTICAL_CRS_KEY: u16 = 4096;
130const VERTICAL_DATUM_KEY: u16 = 4098;
131const VERTICAL_UNITS_KEY: u16 = 4099;
133const GDAL_METADATA_TAG: u16 = 42112;
135const GDAL_NODATA_TAG: u16 = 42113;
137const GREENWICH: u16 = 8901;
139const MAX_IMAGES_WALKED: usize = 64;
142
143pub const MAX_VALUES_PIXELS: u64 = 1 << 28;
145
146pub const MAX_CHUNK_BYTES: u64 = 256 << 20;
149
150pub 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#[derive(Debug, Clone, PartialEq, thiserror::Error)]
176#[non_exhaustive]
177pub enum GeoTiffError {
178 #[error("not a readable TIFF: {0}")]
180 Tiff(String),
181 #[error("the file has no {what}")]
183 Missing {
184 what: &'static str,
186 },
187 #[error("{what} is malformed: {reason}")]
189 Malformed {
190 what: &'static str,
192 reason: String,
194 },
195 #[error("{what} {value} is not read{hint}")]
197 Unsupported {
198 what: &'static str,
200 value: String,
202 hint: &'static str,
204 },
205 #[error("latitude {latitude_deg}° and longitude {longitude_deg}° are not a place")]
207 Location {
208 latitude_deg: f64,
210 longitude_deg: f64,
212 },
213 #[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_deg: f64,
221 longitude_deg: f64,
223 bounds: Bounds,
225 },
226 #[error("pixel (row {}, column {}) is outside a raster of {width} by {height}", pixel.row, pixel.col)]
228 NoSuchPixel {
229 pixel: Pixel,
231 width: u32,
233 height: u32,
235 },
236 #[error("{what} of {size} is more than the {limit} decoded at once")]
238 TooLarge {
239 what: &'static str,
241 size: u64,
243 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256pub struct Pixel {
257 pub row: u32,
259 pub col: u32,
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
265pub struct Bounds {
266 pub south_deg: f64,
268 pub north_deg: f64,
270 pub west_deg: f64,
272 pub east_deg: f64,
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
278#[non_exhaustive]
279pub enum RasterType {
280 PixelIsArea,
282 PixelIsPoint,
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
288#[non_exhaustive]
289pub enum VerticalUnit {
290 #[serde(alias = "Metre")]
292 Meter,
293 Foot,
295 UsSurveyFoot,
297}
298
299impl VerticalUnit {
300 #[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
320pub const VERTICAL_CRS_UNITS: &[(u16, VerticalUnit)] = &[
324 (3855, VerticalUnit::Meter), (5701, VerticalUnit::Meter), (5703, VerticalUnit::Meter), (5714, VerticalUnit::Meter), (5773, VerticalUnit::Meter), (5798, VerticalUnit::Meter), (6360, VerticalUnit::UsSurveyFoot), (8228, VerticalUnit::Foot), ];
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
336#[non_exhaustive]
337pub enum SampleType {
338 U8,
340 I8,
342 U16,
344 I16,
346 U32,
348 I32,
350 F32,
352 F64,
354}
355
356impl SampleType {
357 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 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 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
390#[non_exhaustive]
391pub struct RasterInfo {
392 pub width: u32,
394 pub height: u32,
396 pub corner_longitude_deg: f64,
398 pub corner_latitude_deg: f64,
400 pub pixel_longitude_deg: f64,
402 pub pixel_latitude_deg: f64,
404 pub raster_type: RasterType,
406 pub geographic_crs_epsg: u16,
408 pub vertical_crs_epsg: Option<u16>,
411 pub vertical_unit: VerticalUnit,
413 pub vertical_unit_stated: bool,
416 pub scale: f64,
418 pub offset: f64,
420 pub sample: SampleType,
422 pub nodata: Option<f64>,
424}
425
426impl RasterInfo {
427 #[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 #[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 #[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 #[must_use]
477 pub fn meters(&self, value: f64) -> f64 {
478 (value * self.scale + self.offset) * self.vertical_unit.meters()
479 }
480}
481
482#[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 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 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 let rows = match chunk_type {
604 ChunkType::Strip => chunk_height.min(height),
605 ChunkType::Tile => chunk_height,
606 };
607 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 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 #[must_use]
657 pub fn info(&self) -> &RasterInfo {
658 &self.info
659 }
660
661 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 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 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 #[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 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 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 (index, usize::try_from(at).unwrap_or(usize::MAX))
798 }
799
800 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 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 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
877fn 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
885fn 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 DecodingResult::U64(_) | DecodingResult::I64(_) | DecodingResult::F16(_) => None,
898 }
899}
900
901fn 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
931fn 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 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
989struct GeoKeys {
991 minor: u16,
993 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 fn has(&self, key: u16) -> bool {
1039 self.entries.iter().any(|e| e.0 == key)
1040 }
1041
1042 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 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 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
1123fn 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 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 (_, 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 (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
1221struct Georef {
1224 corner: [f64; 4],
1225 z: Option<[f64; 3]>,
1226}
1227
1228fn 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 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 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#[derive(Clone, Copy, PartialEq, Eq)]
1341enum ZTerms {
1342 Applied,
1343 Ignored,
1344 Unknown,
1345}
1346
1347fn 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 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 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#[derive(Default)]
1402struct Metadata {
1403 scale: Option<f64>,
1404 offset: Option<f64>,
1405 unit: Option<VerticalUnit>,
1406}
1407
1408const MAX_METADATA_DEPTH: usize = 16;
1410
1411const ASCII_BLANK: [char; 6] = [' ', '\t', '\n', '\r', '\u{b}', '\u{c}'];
1414
1415fn 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
1433fn 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 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 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 let role = item.attribute("role").unwrap_or("").to_ascii_lowercase();
1515 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 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 _ => 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;