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