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