1pub mod crs;
28pub mod error;
29pub mod geokeys;
30pub mod transform;
31
32#[cfg(feature = "cog")]
33pub mod cog;
34
35#[cfg(feature = "cog-async")]
36pub mod cog_async;
37
38#[cfg(any(feature = "cog", feature = "cog-async"))]
39mod http_range;
40
41#[cfg(all(test, any(feature = "cog", feature = "cog-async")))]
42mod http_test_support;
43
44pub use error::{Error, Result};
45
46#[cfg(feature = "local")]
47use crs::CrsInfo;
48#[cfg(feature = "local")]
49use geokeys::GeoKeyDirectory;
50#[cfg(feature = "local")]
51use ndarray::ArrayD;
52#[cfg(feature = "local")]
53use std::collections::HashSet;
54#[cfg(feature = "local")]
55use std::path::Path;
56#[cfg(feature = "local")]
57use tiff_reader::{OpenOptions as TiffOpenOptions, TagValue, TiffFile, TiffSample};
58#[cfg(feature = "local")]
59use transform::GeoTransform;
60
61#[cfg(feature = "local")]
62use geotiff_core::tags::{
63 TAG_GDAL_NODATA, TAG_GEO_ASCII_PARAMS, TAG_GEO_DOUBLE_PARAMS, TAG_GEO_KEY_DIRECTORY,
64 TAG_MODEL_PIXEL_SCALE, TAG_MODEL_TIEPOINT, TAG_MODEL_TRANSFORMATION, TAG_NEW_SUBFILE_TYPE,
65 TAG_SUBFILE_TYPE,
66};
67
68#[cfg(feature = "local")]
69const MAX_SUBIFD_OVERVIEW_NODES: usize = 1024;
70#[cfg(feature = "local")]
71const MAX_SUBIFD_OVERVIEW_DEPTH: usize = 32;
72
73#[cfg(feature = "local")]
75pub struct GeoTiffFile {
76 tiff: TiffFile,
77 geo_metadata: GeoMetadata,
78 crs: CrsInfo,
79 geokeys: GeoKeyDirectory,
80 transform: Option<GeoTransform>,
81 base_ifd_index: usize,
82 overview_ifds: Vec<GeoImageIfd>,
83}
84
85#[cfg(feature = "local")]
86#[derive(Debug, Clone)]
87struct GeoImageIfd {
88 top_level_ifd_index: Option<usize>,
89 ifd: tiff_reader::Ifd,
90}
91
92#[cfg(feature = "local")]
93pub use tiff_reader::OpenOptions as GeoTiffOpenOptions;
94
95pub use geotiff_core::GeoMetadata;
96
97#[cfg(feature = "local")]
98impl GeoTiffFile {
99 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
101 Self::open_with_options(path, TiffOpenOptions::default())
102 }
103
104 pub fn open_with_options<P: AsRef<Path>>(path: P, options: GeoTiffOpenOptions) -> Result<Self> {
106 let tiff = TiffFile::open_with_options(path, options)?;
107 Self::from_tiff(tiff)
108 }
109
110 pub unsafe fn open_mmap<P: AsRef<Path>>(path: P) -> Result<Self> {
118 unsafe { Self::open_mmap_with_options(path, TiffOpenOptions::default()) }
119 }
120
121 pub unsafe fn open_mmap_with_options<P: AsRef<Path>>(
129 path: P,
130 options: GeoTiffOpenOptions,
131 ) -> Result<Self> {
132 let tiff = unsafe { TiffFile::open_mmap_with_options(path, options)? };
133 Self::from_tiff(tiff)
134 }
135
136 pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
138 Self::from_bytes_with_options(data, TiffOpenOptions::default())
139 }
140
141 pub fn from_bytes_with_options(data: Vec<u8>, options: GeoTiffOpenOptions) -> Result<Self> {
143 let tiff = TiffFile::from_bytes_with_options(data, options)?;
144 Self::from_tiff(tiff)
145 }
146
147 pub(crate) fn from_tiff(tiff: TiffFile) -> Result<Self> {
148 let metadata_ifd_index = find_metadata_ifd_index(tiff.ifds())?;
149 let metadata_ifd = tiff.ifd(metadata_ifd_index)?;
150 let geokeys = parse_geokey_directory(metadata_ifd)?;
151 let crs = CrsInfo::from_geokeys(&geokeys);
152 let epsg = crs.epsg();
153 let tiepoints = parse_tiepoints(metadata_ifd)?;
154 let pixel_scale = parse_fixed_len_double_tag::<3>(metadata_ifd, TAG_MODEL_PIXEL_SCALE)?;
155 let transformation =
156 parse_fixed_len_double_tag::<16>(metadata_ifd, TAG_MODEL_TRANSFORMATION)?;
157 validate_model_georeferencing(&tiepoints, pixel_scale.as_ref(), transformation.as_ref())?;
158 let transform = transformation
159 .as_ref()
160 .map(GeoTransform::from_transformation_matrix)
161 .or_else(|| {
162 let tiepoint = tiepoints.first()?;
163 let scale = pixel_scale.as_ref()?;
164 Some(GeoTransform::from_tiepoint_and_scale_with_raster_type(
165 tiepoint,
166 scale,
167 crs.raster_type_enum(),
168 ))
169 });
170 let base_ifd_index = find_base_ifd_index(tiff.ifds(), metadata_ifd_index);
171 let base_ifd = tiff.ifd(base_ifd_index)?;
172 let overview_ifds =
173 collect_overview_ifds(&tiff, base_ifd, base_ifd_index, metadata_ifd_index)?;
174 let geo_bounds = transform
175 .as_ref()
176 .map(|gt| gt.bounds(base_ifd.width(), base_ifd.height()));
177
178 let geo_metadata = GeoMetadata {
179 epsg,
180 tiepoints,
181 pixel_scale,
182 transformation,
183 nodata: parse_nodata(metadata_ifd)?,
184 band_count: base_ifd.samples_per_pixel() as u32,
185 width: base_ifd.width(),
186 height: base_ifd.height(),
187 geo_bounds,
188 };
189
190 Ok(Self {
191 tiff,
192 geo_metadata,
193 crs,
194 geokeys,
195 transform,
196 base_ifd_index,
197 overview_ifds,
198 })
199 }
200
201 pub fn tiff(&self) -> &TiffFile {
203 &self.tiff
204 }
205
206 pub fn metadata(&self) -> &GeoMetadata {
208 &self.geo_metadata
209 }
210
211 pub fn epsg(&self) -> Option<u32> {
213 self.geo_metadata.epsg
214 }
215
216 pub fn crs(&self) -> &CrsInfo {
218 &self.crs
219 }
220
221 pub fn geokeys(&self) -> &GeoKeyDirectory {
223 &self.geokeys
224 }
225
226 pub fn transform(&self) -> Option<&GeoTransform> {
228 self.transform.as_ref()
229 }
230
231 pub fn geo_bounds(&self) -> Option<[f64; 4]> {
233 self.geo_metadata.geo_bounds
234 }
235
236 pub fn pixel_to_geo(&self, col: f64, row: f64) -> Option<(f64, f64)> {
238 self.transform
239 .map(|transform| transform.pixel_to_geo(col, row))
240 }
241
242 pub fn geo_to_pixel(&self, x: f64, y: f64) -> Option<(f64, f64)> {
244 self.transform
245 .and_then(|transform| transform.geo_to_pixel(x, y))
246 }
247
248 pub fn width(&self) -> u32 {
250 self.geo_metadata.width
251 }
252
253 pub fn height(&self) -> u32 {
255 self.geo_metadata.height
256 }
257
258 pub fn band_count(&self) -> u32 {
260 self.geo_metadata.band_count
261 }
262
263 pub fn nodata(&self) -> Option<&str> {
265 self.geo_metadata.nodata.as_deref()
266 }
267
268 pub fn overview_count(&self) -> usize {
270 self.overview_ifds.len()
271 }
272
273 pub fn overview_ifd_index(&self, overview_index: usize) -> Result<usize> {
278 self.overview_ifds
279 .get(overview_index)
280 .ok_or(Error::OverviewNotFound(overview_index))?
281 .top_level_ifd_index
282 .ok_or(Error::OverviewHasNoTopLevelIfdIndex(overview_index))
283 }
284
285 pub fn overview_ifd(&self, overview_index: usize) -> Result<&tiff_reader::Ifd> {
287 self.overview_ifds
288 .get(overview_index)
289 .map(|overview| &overview.ifd)
290 .ok_or(Error::OverviewNotFound(overview_index))
291 }
292
293 pub fn base_ifd_index(&self) -> usize {
295 self.base_ifd_index
296 }
297
298 pub fn read_raster<T: TiffSample>(&self) -> Result<ArrayD<T>> {
300 self.tiff
301 .read_image::<T>(self.base_ifd_index)
302 .map_err(Into::into)
303 }
304
305 pub fn read_window<T: TiffSample>(
307 &self,
308 row_off: usize,
309 col_off: usize,
310 rows: usize,
311 cols: usize,
312 ) -> Result<ArrayD<T>> {
313 self.tiff
314 .read_window::<T>(self.base_ifd_index, row_off, col_off, rows, cols)
315 .map_err(Into::into)
316 }
317
318 pub fn read_band<T: TiffSample>(&self, band_index: usize) -> Result<ArrayD<T>> {
321 self.tiff
322 .read_band::<T>(self.base_ifd_index, band_index)
323 .map_err(Into::into)
324 }
325
326 pub fn read_band_window<T: TiffSample>(
329 &self,
330 band_index: usize,
331 row_off: usize,
332 col_off: usize,
333 rows: usize,
334 cols: usize,
335 ) -> Result<ArrayD<T>> {
336 self.tiff
337 .read_band_window::<T>(
338 self.base_ifd_index,
339 band_index,
340 row_off,
341 col_off,
342 rows,
343 cols,
344 )
345 .map_err(Into::into)
346 }
347
348 pub fn read_decoded_raster<T: TiffSample>(&self) -> Result<ArrayD<T>> {
350 self.tiff
351 .read_decoded_image::<T>(self.base_ifd_index)
352 .map_err(Into::into)
353 }
354
355 pub fn read_decoded_window<T: TiffSample>(
357 &self,
358 row_off: usize,
359 col_off: usize,
360 rows: usize,
361 cols: usize,
362 ) -> Result<ArrayD<T>> {
363 self.tiff
364 .read_decoded_window::<T>(self.base_ifd_index, row_off, col_off, rows, cols)
365 .map_err(Into::into)
366 }
367
368 pub fn read_overview<T: TiffSample>(&self, overview_index: usize) -> Result<ArrayD<T>> {
370 let overview = self
371 .overview_ifds
372 .get(overview_index)
373 .ok_or(Error::OverviewNotFound(overview_index))?;
374 self.tiff
375 .read_image_from_ifd::<T>(&overview.ifd)
376 .map_err(Into::into)
377 }
378
379 pub fn read_decoded_overview<T: TiffSample>(&self, overview_index: usize) -> Result<ArrayD<T>> {
381 let overview = self
382 .overview_ifds
383 .get(overview_index)
384 .ok_or(Error::OverviewNotFound(overview_index))?;
385 self.tiff
386 .read_decoded_image_from_ifd::<T>(&overview.ifd)
387 .map_err(Into::into)
388 }
389
390 pub fn read_overview_band<T: TiffSample>(
393 &self,
394 overview_index: usize,
395 band_index: usize,
396 ) -> Result<ArrayD<T>> {
397 let overview = self
398 .overview_ifds
399 .get(overview_index)
400 .ok_or(Error::OverviewNotFound(overview_index))?;
401 self.tiff
402 .read_band_from_ifd::<T>(&overview.ifd, band_index)
403 .map_err(Into::into)
404 }
405
406 pub fn read_overview_window<T: TiffSample>(
408 &self,
409 overview_index: usize,
410 row_off: usize,
411 col_off: usize,
412 rows: usize,
413 cols: usize,
414 ) -> Result<ArrayD<T>> {
415 let overview = self
416 .overview_ifds
417 .get(overview_index)
418 .ok_or(Error::OverviewNotFound(overview_index))?;
419 self.tiff
420 .read_window_from_ifd::<T>(&overview.ifd, row_off, col_off, rows, cols)
421 .map_err(Into::into)
422 }
423
424 pub fn read_decoded_overview_window<T: TiffSample>(
426 &self,
427 overview_index: usize,
428 row_off: usize,
429 col_off: usize,
430 rows: usize,
431 cols: usize,
432 ) -> Result<ArrayD<T>> {
433 let overview = self
434 .overview_ifds
435 .get(overview_index)
436 .ok_or(Error::OverviewNotFound(overview_index))?;
437 self.tiff
438 .read_decoded_window_from_ifd::<T>(&overview.ifd, row_off, col_off, rows, cols)
439 .map_err(Into::into)
440 }
441
442 pub fn read_overview_band_window<T: TiffSample>(
445 &self,
446 overview_index: usize,
447 band_index: usize,
448 row_off: usize,
449 col_off: usize,
450 rows: usize,
451 cols: usize,
452 ) -> Result<ArrayD<T>> {
453 let overview = self
454 .overview_ifds
455 .get(overview_index)
456 .ok_or(Error::OverviewNotFound(overview_index))?;
457 self.tiff
458 .read_band_window_from_ifd::<T>(&overview.ifd, band_index, row_off, col_off, rows, cols)
459 .map_err(Into::into)
460 }
461}
462
463#[cfg(feature = "local")]
464fn is_overview_ifd(base: &tiff_reader::Ifd, candidate: &tiff_reader::Ifd) -> bool {
465 let smaller = candidate.width() <= base.width()
466 && candidate.height() <= base.height()
467 && (candidate.width() < base.width() || candidate.height() < base.height());
468 if !smaller {
469 return false;
470 }
471
472 let same_layout = candidate.samples_per_pixel() == base.samples_per_pixel()
473 && candidate.bits_per_sample().ok() == base.bits_per_sample().ok()
474 && candidate.sample_format().ok() == base.sample_format().ok()
475 && candidate.photometric_interpretation() == base.photometric_interpretation();
476 if !same_layout {
477 return false;
478 }
479
480 has_reduced_resolution_flag(candidate)
481 || (candidate.tag(TAG_NEW_SUBFILE_TYPE).is_none()
482 && candidate.tag(TAG_SUBFILE_TYPE).is_none())
483}
484
485#[cfg(feature = "local")]
486fn collect_overview_ifds(
487 tiff: &TiffFile,
488 base_ifd: &tiff_reader::Ifd,
489 base_ifd_index: usize,
490 metadata_ifd_index: usize,
491) -> Result<Vec<GeoImageIfd>> {
492 let mut seen_offsets = HashSet::from([base_ifd.offset()]);
493 let mut overviews: Vec<GeoImageIfd> = Vec::new();
494 for (index, candidate) in tiff.ifds().iter().enumerate() {
495 if index != base_ifd_index
496 && index != metadata_ifd_index
497 && is_overview_ifd(base_ifd, candidate)
498 && seen_offsets.insert(candidate.offset())
499 {
500 overviews.push(GeoImageIfd {
501 top_level_ifd_index: Some(index),
502 ifd: candidate.clone(),
503 });
504 }
505 }
506
507 if let Some(offsets) = base_ifd.sub_ifd_offsets() {
508 collect_subifd_overviews(tiff, base_ifd, &offsets, &mut seen_offsets, &mut overviews)?;
509 }
510
511 overviews.sort_by(|lhs, rhs| {
512 rhs.ifd
513 .width()
514 .cmp(&lhs.ifd.width())
515 .then_with(|| rhs.ifd.height().cmp(&lhs.ifd.height()))
516 .then_with(|| lhs.top_level_ifd_index.cmp(&rhs.top_level_ifd_index))
517 });
518
519 Ok(overviews)
520}
521
522#[cfg(feature = "local")]
523fn collect_subifd_overviews(
524 tiff: &TiffFile,
525 base_ifd: &tiff_reader::Ifd,
526 offsets: &[u64],
527 seen_offsets: &mut HashSet<u64>,
528 overviews: &mut Vec<GeoImageIfd>,
529) -> Result<()> {
530 let mut pending = Vec::new();
531 let mut visited_nodes = 0usize;
532 push_subifd_offsets(offsets, 1, seen_offsets, &mut pending, &mut visited_nodes)?;
533
534 while let Some((offset, depth)) = pending.pop() {
535 let candidate = tiff.read_ifd_at_offset(offset)?;
536 if is_overview_ifd(base_ifd, &candidate) {
537 overviews.push(GeoImageIfd {
538 top_level_ifd_index: None,
539 ifd: candidate.clone(),
540 });
541 }
542
543 if let Some(child_offsets) = candidate.sub_ifd_offsets() {
544 push_subifd_offsets(
545 &child_offsets,
546 depth + 1,
547 seen_offsets,
548 &mut pending,
549 &mut visited_nodes,
550 )?;
551 }
552 }
553
554 Ok(())
555}
556
557#[cfg(feature = "local")]
558fn push_subifd_offsets(
559 offsets: &[u64],
560 depth: usize,
561 seen_offsets: &mut HashSet<u64>,
562 pending: &mut Vec<(u64, usize)>,
563 visited_nodes: &mut usize,
564) -> Result<()> {
565 if depth > MAX_SUBIFD_OVERVIEW_DEPTH {
566 return Err(Error::Other(format!(
567 "SubIFD overview traversal exceeds depth budget of {MAX_SUBIFD_OVERVIEW_DEPTH}"
568 )));
569 }
570
571 for &offset in offsets.iter().rev() {
572 if *visited_nodes >= MAX_SUBIFD_OVERVIEW_NODES {
573 return Err(Error::Other(format!(
574 "SubIFD overview traversal exceeds node budget of {MAX_SUBIFD_OVERVIEW_NODES}"
575 )));
576 }
577 *visited_nodes += 1;
578
579 if seen_offsets.insert(offset) {
580 pending.push((offset, depth));
581 }
582 }
583
584 Ok(())
585}
586
587#[cfg(feature = "local")]
588fn find_metadata_ifd_index(ifds: &[tiff_reader::Ifd]) -> Result<usize> {
589 ifds.iter()
590 .position(|ifd| ifd.tag(TAG_GEO_KEY_DIRECTORY).is_some())
591 .or_else(|| ifds.iter().position(has_model_georeferencing))
592 .ok_or(Error::NotGeoTiff)
593}
594
595#[cfg(feature = "local")]
596fn has_model_georeferencing(ifd: &tiff_reader::Ifd) -> bool {
597 ifd.tag(TAG_MODEL_TRANSFORMATION).is_some()
598 || (ifd.tag(TAG_MODEL_TIEPOINT).is_some() && ifd.tag(TAG_MODEL_PIXEL_SCALE).is_some())
599}
600
601#[cfg(feature = "local")]
602fn find_base_ifd_index(ifds: &[tiff_reader::Ifd], metadata_ifd_index: usize) -> usize {
603 let metadata_ifd = &ifds[metadata_ifd_index];
604 if !has_reduced_resolution_flag(metadata_ifd) {
605 return metadata_ifd_index;
606 }
607
608 ifds.iter()
609 .enumerate()
610 .skip(metadata_ifd_index + 1)
611 .find_map(|(index, ifd)| (!has_reduced_resolution_flag(ifd)).then_some(index))
612 .unwrap_or(metadata_ifd_index)
613}
614
615#[cfg(feature = "local")]
616fn has_reduced_resolution_flag(ifd: &tiff_reader::Ifd) -> bool {
617 ifd.tag(TAG_NEW_SUBFILE_TYPE)
618 .and_then(|tag| tag.value.as_u64())
619 .map(|flags| flags & 0x1 != 0)
620 .or_else(|| {
621 ifd.tag(TAG_SUBFILE_TYPE)
622 .and_then(|tag| tag.value.as_u16())
623 .map(|value| value == 2)
624 })
625 .unwrap_or(false)
626}
627
628#[cfg(feature = "local")]
629fn parse_geokey_directory(ifd: &tiff_reader::Ifd) -> Result<GeoKeyDirectory> {
630 let Some(tag) = ifd.tag(TAG_GEO_KEY_DIRECTORY) else {
631 return Ok(GeoKeyDirectory::new());
632 };
633 let TagValue::Short(directory) = &tag.value else {
634 return Err(invalid_geotiff_tag(
635 TAG_GEO_KEY_DIRECTORY,
636 "GeoKeyDirectoryTag must use SHORT values",
637 ));
638 };
639 let double_params = match ifd.tag(TAG_GEO_DOUBLE_PARAMS) {
640 Some(tag) => match &tag.value {
641 TagValue::Double(values) => values.as_slice(),
642 _ => {
643 return Err(invalid_geotiff_tag(
644 TAG_GEO_DOUBLE_PARAMS,
645 "GeoDoubleParamsTag must use DOUBLE values",
646 ))
647 }
648 },
649 None => &[],
650 };
651 let ascii_params = match ifd.tag(TAG_GEO_ASCII_PARAMS) {
652 Some(tag) => match &tag.value {
653 TagValue::Ascii(value) => value.as_str(),
654 _ => {
655 return Err(invalid_geotiff_tag(
656 TAG_GEO_ASCII_PARAMS,
657 "GeoAsciiParamsTag must use ASCII values",
658 ))
659 }
660 },
661 None => "",
662 };
663 GeoKeyDirectory::parse(directory, double_params, ascii_params)
664 .ok_or(Error::InvalidGeoKeyDirectory)
665}
666
667#[cfg(feature = "local")]
668fn parse_fixed_len_double_tag<const N: usize>(
669 ifd: &tiff_reader::Ifd,
670 tag_code: u16,
671) -> Result<Option<[f64; N]>> {
672 let Some(tag) = ifd.tag(tag_code) else {
673 return Ok(None);
674 };
675 let TagValue::Double(values) = &tag.value else {
676 return Err(invalid_geotiff_tag(tag_code, "tag must use DOUBLE values"));
677 };
678 if values.len() != N {
679 return Err(invalid_geotiff_tag(
680 tag_code,
681 format!("expected exactly {N} values, got {}", values.len()),
682 ));
683 }
684 let mut out = [0.0; N];
685 out.copy_from_slice(values);
686 Ok(Some(out))
687}
688
689#[cfg(feature = "local")]
690fn parse_tiepoints(ifd: &tiff_reader::Ifd) -> Result<Vec<[f64; 6]>> {
691 let Some(tag) = ifd.tag(TAG_MODEL_TIEPOINT) else {
692 return Ok(Vec::new());
693 };
694 let TagValue::Double(values) = &tag.value else {
695 return Err(invalid_geotiff_tag(
696 TAG_MODEL_TIEPOINT,
697 "ModelTiepointTag must use DOUBLE values",
698 ));
699 };
700 if values.is_empty() || values.len() % 6 != 0 {
701 return Err(invalid_geotiff_tag(
702 TAG_MODEL_TIEPOINT,
703 format!(
704 "expected a non-empty multiple of 6 values, got {}",
705 values.len()
706 ),
707 ));
708 }
709 Ok(values
710 .chunks_exact(6)
711 .map(|chunk| [chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5]])
712 .collect())
713}
714
715#[cfg(feature = "local")]
716fn parse_nodata(ifd: &tiff_reader::Ifd) -> Result<Option<String>> {
717 let Some(tag) = ifd.tag(TAG_GDAL_NODATA) else {
718 return Ok(None);
719 };
720 match &tag.value {
721 TagValue::Ascii(value) => Ok(Some(value.clone())),
722 _ => Err(invalid_geotiff_tag(
723 TAG_GDAL_NODATA,
724 "GDAL_NODATA must use an ASCII value",
725 )),
726 }
727}
728
729#[cfg(feature = "local")]
730fn invalid_geotiff_tag(tag: u16, reason: impl Into<String>) -> Error {
731 Error::InvalidGeoTiffTag {
732 tag,
733 reason: reason.into(),
734 }
735}
736
737#[cfg(feature = "local")]
738fn validate_model_georeferencing(
739 tiepoints: &[[f64; 6]],
740 pixel_scale: Option<&[f64; 3]>,
741 transformation: Option<&[f64; 16]>,
742) -> Result<()> {
743 if tiepoints.iter().flatten().any(|value| !value.is_finite()) {
744 return Err(invalid_geotiff_tag(
745 TAG_MODEL_TIEPOINT,
746 "tiepoint values must be finite",
747 ));
748 }
749 if let Some(scale) = pixel_scale {
750 if !scale.iter().all(|value| value.is_finite())
751 || scale[0] <= 0.0
752 || scale[1] <= 0.0
753 || scale[2] < 0.0
754 {
755 return Err(invalid_geotiff_tag(
756 TAG_MODEL_PIXEL_SCALE,
757 "pixel scale must contain finite positive X/Y and non-negative Z values",
758 ));
759 }
760 if tiepoints.is_empty() {
761 return Err(invalid_geotiff_tag(
762 TAG_MODEL_PIXEL_SCALE,
763 "pixel scale requires at least one model tiepoint",
764 ));
765 }
766 }
767 if let Some(matrix) = transformation {
768 if !matrix.iter().all(|value| value.is_finite()) {
769 return Err(invalid_geotiff_tag(
770 TAG_MODEL_TRANSFORMATION,
771 "transformation matrix values must be finite",
772 ));
773 }
774 let transform = GeoTransform::from_transformation_matrix(matrix);
775 if transform
776 .geo_to_pixel(transform.origin_x, transform.origin_y)
777 .is_none()
778 {
779 return Err(invalid_geotiff_tag(
780 TAG_MODEL_TRANSFORMATION,
781 "transformation matrix must contain an invertible 2D affine transform",
782 ));
783 }
784 if pixel_scale.is_some() || !tiepoints.is_empty() {
785 return Err(invalid_geotiff_tag(
786 TAG_MODEL_TRANSFORMATION,
787 "ModelTransformationTag is mutually exclusive with ModelPixelScaleTag and ModelTiepointTag",
788 ));
789 }
790 }
791 Ok(())
792}
793
794#[cfg(test)]
795#[cfg(feature = "local")]
796mod tests {
797 use std::fs;
798 use std::path::PathBuf;
799 use std::time::{SystemTime, UNIX_EPOCH};
800
801 use super::{
802 GeoTiffFile, MAX_SUBIFD_OVERVIEW_DEPTH, MAX_SUBIFD_OVERVIEW_NODES, TAG_GDAL_NODATA,
803 TAG_MODEL_PIXEL_SCALE,
804 };
805
806 #[derive(Clone)]
807 struct TestIfdSpec {
808 entries: Vec<(u16, u16, u32, Vec<u8>)>,
809 image_data: Vec<u8>,
810 }
811
812 fn le_u16(value: u16) -> [u8; 2] {
813 value.to_le_bytes()
814 }
815
816 fn le_u32(value: u32) -> [u8; 4] {
817 value.to_le_bytes()
818 }
819
820 fn le_f64(value: f64) -> [u8; 8] {
821 value.to_le_bytes()
822 }
823
824 fn temp_geotiff_path(test_name: &str) -> PathBuf {
825 let nanos = SystemTime::now()
826 .duration_since(UNIX_EPOCH)
827 .unwrap()
828 .as_nanos();
829 std::env::temp_dir().join(format!(
830 "geotiff-rust-{test_name}-{}-{nanos}.tif",
831 std::process::id()
832 ))
833 }
834
835 fn inline_short(value: u16) -> Vec<u8> {
836 let mut bytes = [0u8; 4];
837 bytes[..2].copy_from_slice(&le_u16(value));
838 bytes.to_vec()
839 }
840
841 #[allow(clippy::too_many_arguments)]
842 fn build_lerc2_header_v2(
843 width: u32,
844 height: u32,
845 valid_pixel_count: u32,
846 image_type: i32,
847 max_z_error: f64,
848 z_min: f64,
849 z_max: f64,
850 payload_len: usize,
851 ) -> Vec<u8> {
852 let blob_size = 58 + 4 + payload_len;
853 let mut bytes = Vec::with_capacity(blob_size);
854 bytes.extend_from_slice(b"Lerc2 ");
855 bytes.extend_from_slice(&2i32.to_le_bytes());
856 bytes.extend_from_slice(&height.to_le_bytes());
857 bytes.extend_from_slice(&width.to_le_bytes());
858 bytes.extend_from_slice(&valid_pixel_count.to_le_bytes());
859 bytes.extend_from_slice(&8i32.to_le_bytes());
860 bytes.extend_from_slice(&(blob_size as i32).to_le_bytes());
861 bytes.extend_from_slice(&image_type.to_le_bytes());
862 bytes.extend_from_slice(&max_z_error.to_le_bytes());
863 bytes.extend_from_slice(&z_min.to_le_bytes());
864 bytes.extend_from_slice(&z_max.to_le_bytes());
865 bytes
866 }
867
868 fn build_classic_tiff(ifds: &[TestIfdSpec]) -> Vec<u8> {
869 let mut ifd_offsets = Vec::with_capacity(ifds.len());
870 let mut cursor = 8usize;
871 for ifd in ifds {
872 ifd_offsets.push(cursor as u32);
873 let deferred_len: usize = ifd
874 .entries
875 .iter()
876 .filter(|(tag, _, _, value)| *tag != 273 && value.len() > 4)
877 .map(|(_, _, _, value)| value.len())
878 .sum();
879 cursor += 2 + ifd.entries.len() * 12 + 4 + ifd.image_data.len() + deferred_len;
880 }
881
882 let mut bytes = Vec::with_capacity(cursor);
883 bytes.extend_from_slice(b"II");
884 bytes.extend_from_slice(&le_u16(42));
885 bytes.extend_from_slice(&le_u32(ifd_offsets.first().copied().unwrap_or(0)));
886
887 for (ifd_index, ifd) in ifds.iter().enumerate() {
888 let ifd_offset = ifd_offsets[ifd_index] as usize;
889 debug_assert_eq!(bytes.len(), ifd_offset);
890
891 let ifd_size = 2 + ifd.entries.len() * 12 + 4;
892 let mut next_data_offset = ifd_offset + ifd_size;
893 let image_offset = next_data_offset as u32;
894 next_data_offset += ifd.image_data.len();
895
896 bytes.extend_from_slice(&le_u16(ifd.entries.len() as u16));
897 let mut deferred = Vec::new();
898 for (tag, ty, count, value) in &ifd.entries {
899 bytes.extend_from_slice(&le_u16(*tag));
900 bytes.extend_from_slice(&le_u16(*ty));
901 bytes.extend_from_slice(&le_u32(*count));
902 if *tag == 273 {
903 bytes.extend_from_slice(&le_u32(image_offset));
904 } else if value.len() <= 4 {
905 let mut inline = [0u8; 4];
906 inline[..value.len()].copy_from_slice(value);
907 bytes.extend_from_slice(&inline);
908 } else {
909 bytes.extend_from_slice(&le_u32(next_data_offset as u32));
910 next_data_offset += value.len();
911 deferred.push(value.clone());
912 }
913 }
914
915 let next_ifd_offset = ifd_offsets.get(ifd_index + 1).copied().unwrap_or(0);
916 bytes.extend_from_slice(&le_u32(next_ifd_offset));
917 bytes.extend_from_slice(&ifd.image_data);
918 for value in deferred {
919 bytes.extend_from_slice(&value);
920 }
921 debug_assert_eq!(bytes.len(), next_data_offset);
922 }
923
924 bytes
925 }
926
927 fn build_simple_geotiff(pixel_is_point: bool) -> Vec<u8> {
928 let image_data = vec![10u8, 20, 30, 40];
929 let tiepoints = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0];
930 let scales = [2.0, 2.0, 0.0];
931 let geo_keys = if pixel_is_point {
932 vec![
933 1, 1, 0, 3, 1024, 0, 1, 2, 1025, 0, 1, 2, 2048, 0, 1, 4326, ]
938 } else {
939 vec![
940 1, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326, ]
944 };
945 let nodata = b"-9999\0".to_vec();
946
947 build_classic_tiff(&[TestIfdSpec {
948 image_data,
949 entries: vec![
950 (256u16, 4u16, 1u32, le_u32(2).to_vec()),
951 (257u16, 4u16, 1u32, le_u32(2).to_vec()),
952 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
953 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
954 (273u16, 4u16, 1u32, vec![]),
955 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
956 (278u16, 4u16, 1u32, le_u32(2).to_vec()),
957 (279u16, 4u16, 1u32, le_u32(4).to_vec()),
958 (
959 33550u16,
960 12u16,
961 3u32,
962 scales.iter().flat_map(|value| le_f64(*value)).collect(),
963 ),
964 (
965 33922u16,
966 12u16,
967 6u32,
968 tiepoints.iter().flat_map(|value| le_f64(*value)).collect(),
969 ),
970 (
971 34735u16,
972 3u16,
973 geo_keys.len() as u32,
974 geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
975 ),
976 (42113u16, 2u16, nodata.len() as u32, nodata),
977 ],
978 }])
979 }
980
981 fn build_simple_lerc_geotiff() -> Vec<u8> {
982 let tiepoints = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0];
983 let scales = [2.0, 2.0, 0.0];
984 let geo_keys = vec![
985 1, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326, ];
989
990 let mut image_data = build_lerc2_header_v2(2, 2, 4, 6, 0.0, 1.0, 4.0, 1 + 16);
991 image_data.extend_from_slice(&0u32.to_le_bytes());
992 image_data.push(1);
993 for value in [1.0f32, 2.0, 3.0, 4.0] {
994 image_data.extend_from_slice(&value.to_le_bytes());
995 }
996 let image_len = image_data.len() as u32;
997
998 build_classic_tiff(&[TestIfdSpec {
999 image_data,
1000 entries: vec![
1001 (256u16, 4u16, 1u32, le_u32(2).to_vec()),
1002 (257u16, 4u16, 1u32, le_u32(2).to_vec()),
1003 (258u16, 3u16, 1u32, inline_short(32)),
1004 (259u16, 3u16, 1u32, inline_short(34887)),
1005 (273u16, 4u16, 1u32, vec![]),
1006 (277u16, 3u16, 1u32, inline_short(1)),
1007 (278u16, 4u16, 1u32, le_u32(2).to_vec()),
1008 (279u16, 4u16, 1u32, le_u32(image_len).to_vec()),
1009 (339u16, 3u16, 1u32, inline_short(3)),
1010 (
1011 33550u16,
1012 12u16,
1013 3u32,
1014 scales.iter().flat_map(|value| le_f64(*value)).collect(),
1015 ),
1016 (
1017 33922u16,
1018 12u16,
1019 6u32,
1020 tiepoints.iter().flat_map(|value| le_f64(*value)).collect(),
1021 ),
1022 (
1023 34735u16,
1024 3u16,
1025 geo_keys.len() as u32,
1026 geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
1027 ),
1028 ],
1029 }])
1030 }
1031
1032 fn overwrite_classic_inline_long_tag(bytes: &mut [u8], tag_code: u16, value: u32) {
1033 let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
1034 let mut offset = 10usize;
1035 for _ in 0..entry_count {
1036 let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
1037 if code == tag_code {
1038 bytes[offset + 8..offset + 12].copy_from_slice(&le_u32(value));
1039 return;
1040 }
1041 offset += 12;
1042 }
1043 panic!("tag {tag_code} not found in classic TIFF");
1044 }
1045
1046 fn classic_inline_long_tag(bytes: &[u8], tag_code: u16) -> u32 {
1047 let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
1048 let mut offset = 10usize;
1049 for _ in 0..entry_count {
1050 let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
1051 if code == tag_code {
1052 return u32::from_le_bytes(bytes[offset + 8..offset + 12].try_into().unwrap());
1053 }
1054 offset += 12;
1055 }
1056 panic!("tag {tag_code} not found in classic TIFF");
1057 }
1058
1059 fn overwrite_classic_tag_type_or_count(
1060 bytes: &mut [u8],
1061 tag_code: u16,
1062 tag_type: Option<u16>,
1063 count: Option<u32>,
1064 ) {
1065 let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
1066 let mut offset = 10usize;
1067 for _ in 0..entry_count {
1068 let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
1069 if code == tag_code {
1070 if let Some(tag_type) = tag_type {
1071 bytes[offset + 2..offset + 4].copy_from_slice(&le_u16(tag_type));
1072 }
1073 if let Some(count) = count {
1074 bytes[offset + 4..offset + 8].copy_from_slice(&le_u32(count));
1075 }
1076 return;
1077 }
1078 offset += 12;
1079 }
1080 panic!("tag {tag_code} not found in classic TIFF");
1081 }
1082
1083 fn overwrite_first_ifd_next_pointer(bytes: &mut [u8], value: u32) {
1084 let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
1085 let pointer_offset = 10 + entry_count * 12;
1086 bytes[pointer_offset..pointer_offset + 4].copy_from_slice(&le_u32(value));
1087 }
1088
1089 fn overwrite_classic_inline_long_tag_at(
1090 bytes: &mut [u8],
1091 ifd_offset: usize,
1092 tag_code: u16,
1093 value: u32,
1094 ) {
1095 let entry_count = u16::from_le_bytes([bytes[ifd_offset], bytes[ifd_offset + 1]]) as usize;
1096 let mut offset = ifd_offset + 2;
1097 for _ in 0..entry_count {
1098 let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
1099 if code == tag_code {
1100 bytes[offset + 8..offset + 12].copy_from_slice(&le_u32(value));
1101 return;
1102 }
1103 offset += 12;
1104 }
1105 panic!("tag {tag_code} not found in classic TIFF at offset {ifd_offset}");
1106 }
1107
1108 fn first_ifd_next_pointer(bytes: &[u8]) -> u32 {
1109 let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
1110 let pointer_offset = 10 + entry_count * 12;
1111 u32::from_le_bytes([
1112 bytes[pointer_offset],
1113 bytes[pointer_offset + 1],
1114 bytes[pointer_offset + 2],
1115 bytes[pointer_offset + 3],
1116 ])
1117 }
1118
1119 fn ifd_next_pointer(bytes: &[u8], ifd_offset: usize) -> u32 {
1120 let entry_count = u16::from_le_bytes([bytes[ifd_offset], bytes[ifd_offset + 1]]) as usize;
1121 let pointer_offset = ifd_offset + 2 + entry_count * 12;
1122 u32::from_le_bytes([
1123 bytes[pointer_offset],
1124 bytes[pointer_offset + 1],
1125 bytes[pointer_offset + 2],
1126 bytes[pointer_offset + 3],
1127 ])
1128 }
1129
1130 fn overwrite_classic_long_tag_values(bytes: &mut [u8], tag_code: u16, values: &[u32]) {
1131 overwrite_classic_long_tag_values_at(bytes, 8, tag_code, values);
1132 }
1133
1134 fn overwrite_classic_long_tag_values_at(
1135 bytes: &mut [u8],
1136 ifd_offset: usize,
1137 tag_code: u16,
1138 values: &[u32],
1139 ) {
1140 let entry_count = u16::from_le_bytes([bytes[ifd_offset], bytes[ifd_offset + 1]]) as usize;
1141 let mut offset = ifd_offset + 2;
1142 for _ in 0..entry_count {
1143 let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
1144 if code == tag_code {
1145 let type_code = u16::from_le_bytes([bytes[offset + 2], bytes[offset + 3]]);
1146 let count = u32::from_le_bytes([
1147 bytes[offset + 4],
1148 bytes[offset + 5],
1149 bytes[offset + 6],
1150 bytes[offset + 7],
1151 ]) as usize;
1152 assert_eq!(type_code, 4, "tag {tag_code} is not a LONG tag");
1153 assert_eq!(count, values.len(), "tag {tag_code} value count mismatch");
1154
1155 if values.len() == 1 {
1156 bytes[offset + 8..offset + 12].copy_from_slice(&le_u32(values[0]));
1157 } else {
1158 let value_offset = u32::from_le_bytes([
1159 bytes[offset + 8],
1160 bytes[offset + 9],
1161 bytes[offset + 10],
1162 bytes[offset + 11],
1163 ]) as usize;
1164 for (index, value) in values.iter().enumerate() {
1165 let value_offset = value_offset + index * 4;
1166 bytes[value_offset..value_offset + 4].copy_from_slice(&le_u32(*value));
1167 }
1168 }
1169 return;
1170 }
1171 offset += 12;
1172 }
1173 panic!("tag {tag_code} not found in classic TIFF at offset {ifd_offset}");
1174 }
1175
1176 fn top_level_ifd_offsets_after_first(bytes: &[u8], count: usize) -> Vec<u32> {
1177 let mut offsets = Vec::with_capacity(count);
1178 let mut offset = first_ifd_next_pointer(bytes);
1179 while offset != 0 && offsets.len() < count {
1180 offsets.push(offset);
1181 offset = ifd_next_pointer(bytes, offset as usize);
1182 }
1183 assert_eq!(offsets.len(), count);
1184 offsets
1185 }
1186
1187 fn subifd_budget_base(subifd_count: u32) -> TestIfdSpec {
1188 let geo_keys = [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326];
1189 TestIfdSpec {
1190 image_data: vec![0u8],
1191 entries: vec![
1192 (256u16, 4u16, 1u32, le_u32(64).to_vec()),
1193 (257u16, 4u16, 1u32, le_u32(64).to_vec()),
1194 (258u16, 3u16, 1u32, inline_short(8)),
1195 (259u16, 3u16, 1u32, inline_short(1)),
1196 (273u16, 4u16, 1u32, vec![]),
1197 (277u16, 3u16, 1u32, inline_short(1)),
1198 (278u16, 4u16, 1u32, le_u32(64).to_vec()),
1199 (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1200 (
1201 330u16,
1202 4u16,
1203 subifd_count,
1204 vec![0; subifd_count as usize * 4],
1205 ),
1206 (
1207 33550u16,
1208 12u16,
1209 3u32,
1210 [2.0, 2.0, 0.0]
1211 .iter()
1212 .flat_map(|value| le_f64(*value))
1213 .collect(),
1214 ),
1215 (
1216 33922u16,
1217 12u16,
1218 6u32,
1219 [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1220 .iter()
1221 .flat_map(|value| le_f64(*value))
1222 .collect(),
1223 ),
1224 (
1225 34735u16,
1226 3u16,
1227 geo_keys.len() as u32,
1228 geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
1229 ),
1230 ],
1231 }
1232 }
1233
1234 fn subifd_budget_overview(has_child: bool) -> TestIfdSpec {
1235 let mut entries = vec![
1236 (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1237 (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1238 (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1239 (258u16, 3u16, 1u32, inline_short(8)),
1240 (259u16, 3u16, 1u32, inline_short(1)),
1241 (273u16, 4u16, 1u32, vec![]),
1242 (277u16, 3u16, 1u32, inline_short(1)),
1243 (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1244 (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1245 ];
1246 if has_child {
1247 entries.push((330u16, 4u16, 1u32, le_u32(0).to_vec()));
1248 }
1249 TestIfdSpec {
1250 image_data: vec![0u8],
1251 entries,
1252 }
1253 }
1254
1255 fn build_geotiff_exceeding_subifd_node_budget() -> Vec<u8> {
1256 let child_count = MAX_SUBIFD_OVERVIEW_NODES + 1;
1257 let mut ifds = Vec::with_capacity(child_count + 1);
1258 ifds.push(subifd_budget_base(child_count as u32));
1259 ifds.extend((0..child_count).map(|_| subifd_budget_overview(false)));
1260
1261 let mut bytes = build_classic_tiff(&ifds);
1262 let child_offsets = top_level_ifd_offsets_after_first(&bytes, child_count);
1263 overwrite_classic_long_tag_values(&mut bytes, 330, &child_offsets);
1264 overwrite_first_ifd_next_pointer(&mut bytes, 0);
1265 bytes
1266 }
1267
1268 fn build_geotiff_exceeding_subifd_depth_budget() -> Vec<u8> {
1269 let child_count = MAX_SUBIFD_OVERVIEW_DEPTH + 1;
1270 let mut ifds = Vec::with_capacity(child_count + 1);
1271 ifds.push(subifd_budget_base(1));
1272 ifds.extend((0..child_count).map(|index| subifd_budget_overview(index + 1 < child_count)));
1273
1274 let mut bytes = build_classic_tiff(&ifds);
1275 let child_offsets = top_level_ifd_offsets_after_first(&bytes, child_count);
1276 overwrite_classic_long_tag_values(&mut bytes, 330, &child_offsets[..1]);
1277 for offsets in child_offsets.windows(2) {
1278 overwrite_classic_long_tag_values_at(
1279 &mut bytes,
1280 offsets[0] as usize,
1281 330,
1282 &offsets[1..],
1283 );
1284 }
1285 overwrite_first_ifd_next_pointer(&mut bytes, 0);
1286 bytes
1287 }
1288
1289 fn build_geotiff_with_overview() -> Vec<u8> {
1290 let base = TestIfdSpec {
1291 image_data: vec![10u8, 20, 30, 40],
1292 entries: vec![
1293 (256u16, 4u16, 1u32, le_u32(2).to_vec()),
1294 (257u16, 4u16, 1u32, le_u32(2).to_vec()),
1295 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1296 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1297 (273u16, 4u16, 1u32, vec![]),
1298 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1299 (278u16, 4u16, 1u32, le_u32(2).to_vec()),
1300 (279u16, 4u16, 1u32, le_u32(4).to_vec()),
1301 (
1302 33550u16,
1303 12u16,
1304 3u32,
1305 [2.0, 2.0, 0.0]
1306 .iter()
1307 .flat_map(|value| le_f64(*value))
1308 .collect(),
1309 ),
1310 (
1311 33922u16,
1312 12u16,
1313 6u32,
1314 [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1315 .iter()
1316 .flat_map(|value| le_f64(*value))
1317 .collect(),
1318 ),
1319 (
1320 34735u16,
1321 3u16,
1322 12u32,
1323 [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326]
1324 .iter()
1325 .flat_map(|value| le_u16(*value))
1326 .collect(),
1327 ),
1328 ],
1329 };
1330 let overview = TestIfdSpec {
1331 image_data: vec![99u8],
1332 entries: vec![
1333 (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1334 (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1335 (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1336 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1337 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1338 (273u16, 4u16, 1u32, vec![]),
1339 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1340 (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1341 (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1342 ],
1343 };
1344
1345 build_classic_tiff(&[base, overview])
1346 }
1347
1348 fn build_geotiff_with_subifd_overview() -> Vec<u8> {
1349 let base = TestIfdSpec {
1350 image_data: vec![10u8, 20, 30, 40],
1351 entries: vec![
1352 (256u16, 4u16, 1u32, le_u32(2).to_vec()),
1353 (257u16, 4u16, 1u32, le_u32(2).to_vec()),
1354 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1355 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1356 (273u16, 4u16, 1u32, vec![]),
1357 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1358 (278u16, 4u16, 1u32, le_u32(2).to_vec()),
1359 (279u16, 4u16, 1u32, le_u32(4).to_vec()),
1360 (330u16, 4u16, 1u32, le_u32(0).to_vec()),
1361 (
1362 33550u16,
1363 12u16,
1364 3u32,
1365 [2.0, 2.0, 0.0]
1366 .iter()
1367 .flat_map(|value| le_f64(*value))
1368 .collect(),
1369 ),
1370 (
1371 33922u16,
1372 12u16,
1373 6u32,
1374 [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1375 .iter()
1376 .flat_map(|value| le_f64(*value))
1377 .collect(),
1378 ),
1379 (
1380 34735u16,
1381 3u16,
1382 12u32,
1383 [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326]
1384 .iter()
1385 .flat_map(|value| le_u16(*value))
1386 .collect(),
1387 ),
1388 ],
1389 };
1390 let overview = TestIfdSpec {
1391 image_data: vec![99u8],
1392 entries: vec![
1393 (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1394 (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1395 (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1396 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1397 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1398 (273u16, 4u16, 1u32, vec![]),
1399 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1400 (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1401 (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1402 ],
1403 };
1404
1405 let mut bytes = build_classic_tiff(&[base, overview]);
1406 let child_ifd_offset = first_ifd_next_pointer(&bytes);
1407 overwrite_classic_inline_long_tag(&mut bytes, 330, child_ifd_offset);
1408 overwrite_first_ifd_next_pointer(&mut bytes, 0);
1409 bytes
1410 }
1411
1412 fn build_geotiff_with_nested_subifd_overviews() -> Vec<u8> {
1413 let base = TestIfdSpec {
1414 image_data: (1u8..=16).collect(),
1415 entries: vec![
1416 (256u16, 4u16, 1u32, le_u32(4).to_vec()),
1417 (257u16, 4u16, 1u32, le_u32(4).to_vec()),
1418 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1419 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1420 (273u16, 4u16, 1u32, vec![]),
1421 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1422 (278u16, 4u16, 1u32, le_u32(4).to_vec()),
1423 (279u16, 4u16, 1u32, le_u32(16).to_vec()),
1424 (330u16, 4u16, 1u32, le_u32(0).to_vec()),
1425 (
1426 33550u16,
1427 12u16,
1428 3u32,
1429 [2.0, 2.0, 0.0]
1430 .iter()
1431 .flat_map(|value| le_f64(*value))
1432 .collect(),
1433 ),
1434 (
1435 33922u16,
1436 12u16,
1437 6u32,
1438 [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1439 .iter()
1440 .flat_map(|value| le_f64(*value))
1441 .collect(),
1442 ),
1443 (
1444 34735u16,
1445 3u16,
1446 12u32,
1447 [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326]
1448 .iter()
1449 .flat_map(|value| le_u16(*value))
1450 .collect(),
1451 ),
1452 ],
1453 };
1454 let overview = TestIfdSpec {
1455 image_data: vec![50u8, 60, 70, 80],
1456 entries: vec![
1457 (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1458 (256u16, 4u16, 1u32, le_u32(2).to_vec()),
1459 (257u16, 4u16, 1u32, le_u32(2).to_vec()),
1460 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1461 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1462 (273u16, 4u16, 1u32, vec![]),
1463 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1464 (278u16, 4u16, 1u32, le_u32(2).to_vec()),
1465 (279u16, 4u16, 1u32, le_u32(4).to_vec()),
1466 (330u16, 4u16, 1u32, le_u32(0).to_vec()),
1467 ],
1468 };
1469 let nested = TestIfdSpec {
1470 image_data: vec![99u8],
1471 entries: vec![
1472 (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1473 (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1474 (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1475 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1476 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1477 (273u16, 4u16, 1u32, vec![]),
1478 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1479 (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1480 (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1481 ],
1482 };
1483
1484 let mut bytes = build_classic_tiff(&[base, overview, nested]);
1485 let child_ifd_offset = first_ifd_next_pointer(&bytes);
1486 let grandchild_ifd_offset = ifd_next_pointer(&bytes, child_ifd_offset as usize);
1487 overwrite_classic_inline_long_tag(&mut bytes, 330, child_ifd_offset);
1488 overwrite_classic_inline_long_tag_at(
1489 &mut bytes,
1490 child_ifd_offset as usize,
1491 330,
1492 grandchild_ifd_offset,
1493 );
1494 overwrite_first_ifd_next_pointer(&mut bytes, 0);
1495 bytes
1496 }
1497
1498 fn build_cog_like_geotiff_with_ghost_ifd() -> Vec<u8> {
1499 let geo_keys = [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326];
1500 let ghost = TestIfdSpec {
1501 image_data: vec![0u8],
1502 entries: vec![
1503 (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1504 (256u16, 4u16, 1u32, le_u32(1).to_vec()),
1505 (257u16, 4u16, 1u32, le_u32(1).to_vec()),
1506 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1507 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1508 (273u16, 4u16, 1u32, vec![]),
1509 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1510 (278u16, 4u16, 1u32, le_u32(1).to_vec()),
1511 (279u16, 4u16, 1u32, le_u32(1).to_vec()),
1512 (
1513 33550u16,
1514 12u16,
1515 3u32,
1516 [2.0, 2.0, 0.0]
1517 .iter()
1518 .flat_map(|value| le_f64(*value))
1519 .collect(),
1520 ),
1521 (
1522 33922u16,
1523 12u16,
1524 6u32,
1525 [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
1526 .iter()
1527 .flat_map(|value| le_f64(*value))
1528 .collect(),
1529 ),
1530 (
1531 34735u16,
1532 3u16,
1533 geo_keys.len() as u32,
1534 geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
1535 ),
1536 ],
1537 };
1538 let overview = TestIfdSpec {
1539 image_data: vec![50u8, 60, 70, 80],
1540 entries: vec![
1541 (254u16, 4u16, 1u32, le_u32(1).to_vec()),
1542 (256u16, 4u16, 1u32, le_u32(2).to_vec()),
1543 (257u16, 4u16, 1u32, le_u32(2).to_vec()),
1544 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1545 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1546 (273u16, 4u16, 1u32, vec![]),
1547 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1548 (278u16, 4u16, 1u32, le_u32(2).to_vec()),
1549 (279u16, 4u16, 1u32, le_u32(4).to_vec()),
1550 ],
1551 };
1552 let base = TestIfdSpec {
1553 image_data: (1u8..=16).collect(),
1554 entries: vec![
1555 (256u16, 4u16, 1u32, le_u32(4).to_vec()),
1556 (257u16, 4u16, 1u32, le_u32(4).to_vec()),
1557 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
1558 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1559 (273u16, 4u16, 1u32, vec![]),
1560 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
1561 (278u16, 4u16, 1u32, le_u32(4).to_vec()),
1562 (279u16, 4u16, 1u32, le_u32(16).to_vec()),
1563 ],
1564 };
1565
1566 build_classic_tiff(&[ghost, overview, base])
1567 }
1568
1569 #[test]
1570 fn parses_geotiff_metadata_and_reads_raster() {
1571 let file = GeoTiffFile::from_bytes(build_simple_geotiff(false)).unwrap();
1572 assert_eq!(file.epsg(), Some(4326));
1573 assert_eq!(file.width(), 2);
1574 assert_eq!(file.height(), 2);
1575 assert_eq!(file.band_count(), 1);
1576 assert_eq!(file.nodata(), Some("-9999"));
1577 assert_eq!(file.geo_bounds(), Some([100.0, 196.0, 104.0, 200.0]));
1578
1579 let raster = file.read_raster::<u8>().unwrap();
1580 assert_eq!(raster.shape(), &[2, 2]);
1581 let (values, offset) = raster.into_raw_vec_and_offset();
1582 assert_eq!(offset, Some(0));
1583 assert_eq!(values, vec![10, 20, 30, 40]);
1584 }
1585
1586 #[test]
1587 fn open_mmap_reads_geotiff_from_file() {
1588 let bytes = build_simple_geotiff(false);
1589 let path = temp_geotiff_path("open_mmap_reads_geotiff_from_file");
1590 fs::write(&path, &bytes).unwrap();
1591
1592 let file = unsafe { GeoTiffFile::open_mmap(&path).unwrap() };
1593 assert_eq!(file.epsg(), Some(4326));
1594 assert_eq!(file.tiff().raw_bytes(), Some(bytes.as_slice()));
1595
1596 let raster = file.read_raster::<u8>().unwrap();
1597 let (values, offset) = raster.into_raw_vec_and_offset();
1598 assert_eq!(offset, Some(0));
1599 assert_eq!(values, vec![10, 20, 30, 40]);
1600
1601 drop(file);
1602 let _ = fs::remove_file(path);
1603 }
1604
1605 #[test]
1606 fn parses_geotiff_metadata_and_reads_lerc_raster() {
1607 let file = GeoTiffFile::from_bytes(build_simple_lerc_geotiff()).unwrap();
1608 assert_eq!(file.epsg(), Some(4326));
1609 assert_eq!(file.width(), 2);
1610 assert_eq!(file.height(), 2);
1611
1612 let raster = file.read_raster::<f32>().unwrap();
1613 assert_eq!(raster.shape(), &[2, 2]);
1614 let (values, offset) = raster.into_raw_vec_and_offset();
1615 assert_eq!(offset, Some(0));
1616 assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0]);
1617 }
1618
1619 #[test]
1620 fn pixel_is_point_metadata_shifts_bounds_to_outer_edges() {
1621 let file = GeoTiffFile::from_bytes(build_simple_geotiff(true)).unwrap();
1622 assert_eq!(file.geo_bounds(), Some([99.0, 197.0, 103.0, 201.0]));
1623
1624 let transform = file.transform().unwrap();
1625 let (center_x, center_y) = transform.pixel_to_geo(0.5, 0.5);
1626 assert_eq!((center_x, center_y), (100.0, 200.0));
1627 }
1628
1629 #[test]
1630 fn discovers_reduced_resolution_overviews() {
1631 let file = GeoTiffFile::from_bytes(build_geotiff_with_overview()).unwrap();
1632 assert_eq!(file.overview_count(), 1);
1633 assert_eq!(file.overview_ifd_index(0).unwrap(), 1);
1634
1635 let overview = file.read_overview::<u8>(0).unwrap();
1636 assert_eq!(overview.shape(), &[1, 1]);
1637 let (values, offset) = overview.into_raw_vec_and_offset();
1638 assert_eq!(offset, Some(0));
1639 assert_eq!(values, vec![99]);
1640 }
1641
1642 #[test]
1643 fn discovers_and_reads_subifd_overviews() {
1644 let file = GeoTiffFile::from_bytes(build_geotiff_with_subifd_overview()).unwrap();
1645 assert_eq!(file.overview_count(), 1);
1646 assert!(matches!(
1647 file.overview_ifd_index(0).unwrap_err(),
1648 crate::error::Error::OverviewHasNoTopLevelIfdIndex(0)
1649 ));
1650 assert_eq!(file.overview_ifd(0).unwrap().width(), 1);
1651 assert_eq!(file.overview_ifd(0).unwrap().height(), 1);
1652
1653 let overview = file.read_overview::<u8>(0).unwrap();
1654 assert_eq!(overview.shape(), &[1, 1]);
1655 let (values, offset) = overview.into_raw_vec_and_offset();
1656 assert_eq!(offset, Some(0));
1657 assert_eq!(values, vec![99]);
1658 }
1659
1660 #[test]
1661 fn deduplicates_overviews_referenced_by_both_ifd_chains() {
1662 let mut bytes = build_geotiff_with_subifd_overview();
1663 let child_ifd_offset = classic_inline_long_tag(&bytes, 330);
1664 overwrite_first_ifd_next_pointer(&mut bytes, child_ifd_offset);
1665
1666 let file = GeoTiffFile::from_bytes(bytes).unwrap();
1667 assert_eq!(file.overview_count(), 1);
1668 assert_eq!(file.overview_ifd_index(0).unwrap(), 1);
1669 }
1670
1671 #[test]
1672 fn rejects_malformed_geotiff_tag_types_and_counts() {
1673 let mut wrong_type = build_simple_geotiff(false);
1674 overwrite_classic_tag_type_or_count(&mut wrong_type, TAG_GDAL_NODATA, Some(1), None);
1675 assert!(matches!(
1676 GeoTiffFile::from_bytes(wrong_type),
1677 Err(crate::error::Error::InvalidGeoTiffTag {
1678 tag: TAG_GDAL_NODATA,
1679 ..
1680 })
1681 ));
1682
1683 let mut wrong_count = build_simple_geotiff(false);
1684 overwrite_classic_tag_type_or_count(&mut wrong_count, TAG_MODEL_PIXEL_SCALE, None, Some(4));
1685 assert!(matches!(
1686 GeoTiffFile::from_bytes(wrong_count),
1687 Err(crate::error::Error::InvalidGeoTiffTag {
1688 tag: TAG_MODEL_PIXEL_SCALE,
1689 ..
1690 })
1691 ));
1692 }
1693
1694 #[test]
1695 fn overview_candidates_must_not_grow_either_dimension() {
1696 let mut bytes = build_geotiff_with_overview();
1697 let overview_offset = first_ifd_next_pointer(&bytes) as usize;
1698 overwrite_classic_inline_long_tag_at(&mut bytes, overview_offset, 257, 3);
1699
1700 let file = GeoTiffFile::from_bytes(bytes).unwrap();
1701 assert_eq!(file.overview_count(), 0);
1702 }
1703
1704 #[test]
1705 fn discovers_nested_subifd_overviews() {
1706 let file = GeoTiffFile::from_bytes(build_geotiff_with_nested_subifd_overviews()).unwrap();
1707 assert_eq!(file.overview_count(), 2);
1708 assert_eq!(file.overview_ifd(0).unwrap().width(), 2);
1709 assert_eq!(file.overview_ifd(1).unwrap().width(), 1);
1710 assert!(matches!(
1711 file.overview_ifd_index(0).unwrap_err(),
1712 crate::error::Error::OverviewHasNoTopLevelIfdIndex(0)
1713 ));
1714 assert!(matches!(
1715 file.overview_ifd_index(1).unwrap_err(),
1716 crate::error::Error::OverviewHasNoTopLevelIfdIndex(1)
1717 ));
1718
1719 let first = file.read_overview::<u8>(0).unwrap();
1720 assert_eq!(first.shape(), &[2, 2]);
1721 let second = file.read_overview::<u8>(1).unwrap();
1722 assert_eq!(second.shape(), &[1, 1]);
1723 assert_eq!(second[[0, 0]], 99);
1724 }
1725
1726 #[test]
1727 fn rejects_subifd_overview_node_budget() {
1728 let error = match GeoTiffFile::from_bytes(build_geotiff_exceeding_subifd_node_budget()) {
1729 Ok(_) => panic!("expected SubIFD overview node budget error"),
1730 Err(error) => error,
1731 };
1732 assert!(error
1733 .to_string()
1734 .contains("SubIFD overview traversal exceeds node budget"));
1735 }
1736
1737 #[test]
1738 fn rejects_subifd_overview_depth_budget() {
1739 let error = match GeoTiffFile::from_bytes(build_geotiff_exceeding_subifd_depth_budget()) {
1740 Ok(_) => panic!("expected SubIFD overview depth budget error"),
1741 Err(error) => error,
1742 };
1743 assert!(error
1744 .to_string()
1745 .contains("SubIFD overview traversal exceeds depth budget"));
1746 }
1747
1748 #[test]
1749 fn reads_base_raster_window() {
1750 let file = GeoTiffFile::from_bytes(build_simple_geotiff(false)).unwrap();
1751 let window = file.read_window::<u8>(1, 0, 1, 2).unwrap();
1752 assert_eq!(window.shape(), &[1, 2]);
1753 let (values, offset) = window.into_raw_vec_and_offset();
1754 assert_eq!(offset, Some(0));
1755 assert_eq!(values, vec![30, 40]);
1756 }
1757
1758 #[test]
1759 fn reads_overview_window() {
1760 let file = GeoTiffFile::from_bytes(build_geotiff_with_overview()).unwrap();
1761 let window = file.read_overview_window::<u8>(0, 0, 0, 1, 1).unwrap();
1762 assert_eq!(window.shape(), &[1, 1]);
1763 let (values, offset) = window.into_raw_vec_and_offset();
1764 assert_eq!(offset, Some(0));
1765 assert_eq!(values, vec![99]);
1766 }
1767
1768 #[test]
1769 fn prefers_non_ghost_base_ifd_for_cog_like_layouts() {
1770 let file = GeoTiffFile::from_bytes(build_cog_like_geotiff_with_ghost_ifd()).unwrap();
1771 assert_eq!(file.base_ifd_index(), 2);
1772 assert_eq!(file.width(), 4);
1773 assert_eq!(file.height(), 4);
1774 assert_eq!(file.geo_bounds(), Some([100.0, 192.0, 108.0, 200.0]));
1775 assert_eq!(file.overview_count(), 1);
1776 assert_eq!(file.overview_ifd_index(0).unwrap(), 1);
1777
1778 let base = file.read_raster::<u8>().unwrap();
1779 assert_eq!(base.shape(), &[4, 4]);
1780 let (values, offset) = base.into_raw_vec_and_offset();
1781 assert_eq!(offset, Some(0));
1782 assert_eq!(values, (1u8..=16).collect::<Vec<_>>());
1783
1784 let overview = file.read_overview::<u8>(0).unwrap();
1785 assert_eq!(overview.shape(), &[2, 2]);
1786 let (values, offset) = overview.into_raw_vec_and_offset();
1787 assert_eq!(offset, Some(0));
1788 assert_eq!(values, vec![50, 60, 70, 80]);
1789 }
1790
1791 #[test]
1792 fn rejects_zero_rows_per_strip_without_panicking() {
1793 let mut bytes = build_simple_geotiff(false);
1794 overwrite_classic_inline_long_tag(&mut bytes, 278, 0);
1795
1796 let file = GeoTiffFile::from_bytes(bytes).unwrap();
1797 assert_eq!(file.epsg(), Some(4326));
1798
1799 let error = file.tiff().read_image_bytes(0).unwrap_err();
1800 assert!(error.to_string().contains("RowsPerStrip"));
1801 }
1802}