1pub mod crs;
27pub mod error;
28pub mod geokeys;
29pub mod transform;
30
31#[cfg(feature = "cog")]
32pub mod cog;
33
34pub use error::{Error, Result};
35
36#[cfg(feature = "local")]
37use crs::CrsInfo;
38#[cfg(feature = "local")]
39use geokeys::GeoKeyDirectory;
40#[cfg(feature = "local")]
41use ndarray::ArrayD;
42#[cfg(feature = "local")]
43use std::path::Path;
44#[cfg(feature = "local")]
45use tiff_reader::{OpenOptions as TiffOpenOptions, TagValue, TiffFile, TiffSample};
46#[cfg(feature = "local")]
47use transform::GeoTransform;
48
49#[cfg(feature = "local")]
50use geotiff_core::tags::{
51 TAG_GDAL_NODATA, TAG_GEO_ASCII_PARAMS, TAG_GEO_DOUBLE_PARAMS, TAG_GEO_KEY_DIRECTORY,
52 TAG_MODEL_PIXEL_SCALE, TAG_MODEL_TIEPOINT, TAG_MODEL_TRANSFORMATION, TAG_NEW_SUBFILE_TYPE,
53 TAG_SUBFILE_TYPE,
54};
55
56#[cfg(feature = "local")]
58pub struct GeoTiffFile {
59 tiff: TiffFile,
60 geo_metadata: GeoMetadata,
61 crs: CrsInfo,
62 geokeys: GeoKeyDirectory,
63 transform: Option<GeoTransform>,
64 base_ifd_index: usize,
65 overview_ifds: Vec<usize>,
66}
67
68#[cfg(feature = "local")]
69pub use tiff_reader::OpenOptions as GeoTiffOpenOptions;
70
71pub use geotiff_core::GeoMetadata;
72
73#[cfg(feature = "local")]
74impl GeoTiffFile {
75 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
77 Self::open_with_options(path, TiffOpenOptions::default())
78 }
79
80 pub fn open_with_options<P: AsRef<Path>>(path: P, options: GeoTiffOpenOptions) -> Result<Self> {
82 let tiff = TiffFile::open_with_options(path, options)?;
83 Self::from_tiff(tiff)
84 }
85
86 pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
88 Self::from_bytes_with_options(data, TiffOpenOptions::default())
89 }
90
91 pub fn from_bytes_with_options(data: Vec<u8>, options: GeoTiffOpenOptions) -> Result<Self> {
93 let tiff = TiffFile::from_bytes_with_options(data, options)?;
94 Self::from_tiff(tiff)
95 }
96
97 pub(crate) fn from_tiff(tiff: TiffFile) -> Result<Self> {
98 let metadata_ifd_index = find_metadata_ifd_index(tiff.ifds())?;
99 let metadata_ifd = tiff.ifd(metadata_ifd_index)?;
100 let geokeys = parse_geokey_directory(metadata_ifd)?;
101 let crs = CrsInfo::from_geokeys(&geokeys);
102 let epsg = crs.epsg();
103 let tiepoints = parse_tiepoints(metadata_ifd);
104 let pixel_scale = parse_fixed_len_double_tag::<3>(
105 metadata_ifd
106 .tag(TAG_MODEL_PIXEL_SCALE)
107 .map(|tag| &tag.value),
108 );
109 let transformation = parse_fixed_len_double_tag::<16>(
110 metadata_ifd
111 .tag(TAG_MODEL_TRANSFORMATION)
112 .map(|tag| &tag.value),
113 );
114 let transform = transformation
115 .as_ref()
116 .map(GeoTransform::from_transformation_matrix)
117 .or_else(|| {
118 let tiepoint = tiepoints.first()?;
119 let scale = pixel_scale.as_ref()?;
120 Some(GeoTransform::from_tiepoint_and_scale_with_raster_type(
121 tiepoint,
122 scale,
123 crs.raster_type_enum(),
124 ))
125 });
126 let base_ifd_index = find_base_ifd_index(tiff.ifds(), metadata_ifd_index);
127 let base_ifd = tiff.ifd(base_ifd_index)?;
128 let geo_bounds = transform
129 .as_ref()
130 .map(|gt| gt.bounds(base_ifd.width(), base_ifd.height()));
131 let overview_ifds = tiff
132 .ifds()
133 .iter()
134 .enumerate()
135 .filter_map(|(index, candidate)| {
136 (index != base_ifd_index
137 && index != metadata_ifd_index
138 && is_overview_ifd(base_ifd, candidate))
139 .then_some(index)
140 })
141 .collect();
142
143 let geo_metadata = GeoMetadata {
144 epsg,
145 tiepoints,
146 pixel_scale,
147 transformation,
148 nodata: parse_nodata(metadata_ifd),
149 band_count: base_ifd.samples_per_pixel() as u32,
150 width: base_ifd.width(),
151 height: base_ifd.height(),
152 geo_bounds,
153 };
154
155 Ok(Self {
156 tiff,
157 geo_metadata,
158 crs,
159 geokeys,
160 transform,
161 base_ifd_index,
162 overview_ifds,
163 })
164 }
165
166 pub fn tiff(&self) -> &TiffFile {
168 &self.tiff
169 }
170
171 pub fn metadata(&self) -> &GeoMetadata {
173 &self.geo_metadata
174 }
175
176 pub fn epsg(&self) -> Option<u32> {
178 self.geo_metadata.epsg
179 }
180
181 pub fn crs(&self) -> &CrsInfo {
183 &self.crs
184 }
185
186 pub fn geokeys(&self) -> &GeoKeyDirectory {
188 &self.geokeys
189 }
190
191 pub fn transform(&self) -> Option<&GeoTransform> {
193 self.transform.as_ref()
194 }
195
196 pub fn geo_bounds(&self) -> Option<[f64; 4]> {
198 self.geo_metadata.geo_bounds
199 }
200
201 pub fn pixel_to_geo(&self, col: f64, row: f64) -> Option<(f64, f64)> {
203 self.transform
204 .map(|transform| transform.pixel_to_geo(col, row))
205 }
206
207 pub fn geo_to_pixel(&self, x: f64, y: f64) -> Option<(f64, f64)> {
209 self.transform
210 .and_then(|transform| transform.geo_to_pixel(x, y))
211 }
212
213 pub fn width(&self) -> u32 {
215 self.geo_metadata.width
216 }
217
218 pub fn height(&self) -> u32 {
220 self.geo_metadata.height
221 }
222
223 pub fn band_count(&self) -> u32 {
225 self.geo_metadata.band_count
226 }
227
228 pub fn nodata(&self) -> Option<&str> {
230 self.geo_metadata.nodata.as_deref()
231 }
232
233 pub fn overview_count(&self) -> usize {
235 self.overview_ifds.len()
236 }
237
238 pub fn overview_ifd_index(&self, overview_index: usize) -> Result<usize> {
240 self.overview_ifds
241 .get(overview_index)
242 .copied()
243 .ok_or(Error::OverviewNotFound(overview_index))
244 }
245
246 pub fn base_ifd_index(&self) -> usize {
248 self.base_ifd_index
249 }
250
251 pub fn read_raster<T: TiffSample>(&self) -> Result<ArrayD<T>> {
253 self.tiff
254 .read_image::<T>(self.base_ifd_index)
255 .map_err(Into::into)
256 }
257
258 pub fn read_window<T: TiffSample>(
260 &self,
261 row_off: usize,
262 col_off: usize,
263 rows: usize,
264 cols: usize,
265 ) -> Result<ArrayD<T>> {
266 self.tiff
267 .read_window::<T>(self.base_ifd_index, row_off, col_off, rows, cols)
268 .map_err(Into::into)
269 }
270
271 pub fn read_overview<T: TiffSample>(&self, overview_index: usize) -> Result<ArrayD<T>> {
273 let ifd_index = self.overview_ifd_index(overview_index)?;
274 self.tiff.read_image::<T>(ifd_index).map_err(Into::into)
275 }
276
277 pub fn read_overview_window<T: TiffSample>(
279 &self,
280 overview_index: usize,
281 row_off: usize,
282 col_off: usize,
283 rows: usize,
284 cols: usize,
285 ) -> Result<ArrayD<T>> {
286 let ifd_index = self.overview_ifd_index(overview_index)?;
287 self.tiff
288 .read_window::<T>(ifd_index, row_off, col_off, rows, cols)
289 .map_err(Into::into)
290 }
291}
292
293#[cfg(feature = "local")]
294fn is_overview_ifd(base: &tiff_reader::Ifd, candidate: &tiff_reader::Ifd) -> bool {
295 let smaller = candidate.width() < base.width() || candidate.height() < base.height();
296 if !smaller {
297 return false;
298 }
299
300 let same_layout = candidate.samples_per_pixel() == base.samples_per_pixel()
301 && candidate.bits_per_sample() == base.bits_per_sample()
302 && candidate.sample_format() == base.sample_format()
303 && candidate.photometric_interpretation() == base.photometric_interpretation();
304 if !same_layout {
305 return false;
306 }
307
308 has_reduced_resolution_flag(candidate)
309 || (candidate.tag(TAG_NEW_SUBFILE_TYPE).is_none()
310 && candidate.tag(TAG_SUBFILE_TYPE).is_none())
311}
312
313#[cfg(feature = "local")]
314fn find_metadata_ifd_index(ifds: &[tiff_reader::Ifd]) -> Result<usize> {
315 ifds.iter()
316 .position(|ifd| ifd.tag(TAG_GEO_KEY_DIRECTORY).is_some())
317 .ok_or(Error::NotGeoTiff)
318}
319
320#[cfg(feature = "local")]
321fn find_base_ifd_index(ifds: &[tiff_reader::Ifd], metadata_ifd_index: usize) -> usize {
322 let metadata_ifd = &ifds[metadata_ifd_index];
323 if !has_reduced_resolution_flag(metadata_ifd) {
324 return metadata_ifd_index;
325 }
326
327 ifds.iter()
328 .enumerate()
329 .skip(metadata_ifd_index + 1)
330 .find_map(|(index, ifd)| (!has_reduced_resolution_flag(ifd)).then_some(index))
331 .unwrap_or(metadata_ifd_index)
332}
333
334#[cfg(feature = "local")]
335fn has_reduced_resolution_flag(ifd: &tiff_reader::Ifd) -> bool {
336 ifd.tag(TAG_NEW_SUBFILE_TYPE)
337 .and_then(|tag| tag.value.as_u64())
338 .map(|flags| flags & 0x1 != 0)
339 .or_else(|| {
340 ifd.tag(TAG_SUBFILE_TYPE)
341 .and_then(|tag| tag.value.as_u16())
342 .map(|value| value == 2)
343 })
344 .unwrap_or(false)
345}
346
347#[cfg(feature = "local")]
348fn parse_geokey_directory(ifd: &tiff_reader::Ifd) -> Result<GeoKeyDirectory> {
349 let directory = ifd
350 .tag(TAG_GEO_KEY_DIRECTORY)
351 .and_then(|tag| match &tag.value {
352 TagValue::Short(values) => Some(values.as_slice()),
353 _ => None,
354 })
355 .ok_or(Error::NotGeoTiff)?;
356 let double_params = ifd
357 .tag(TAG_GEO_DOUBLE_PARAMS)
358 .and_then(|tag| tag.value.as_f64_vec())
359 .unwrap_or_default();
360 let ascii_params = ifd
361 .tag(TAG_GEO_ASCII_PARAMS)
362 .and_then(|tag| tag.value.as_str())
363 .unwrap_or("");
364 GeoKeyDirectory::parse(directory, &double_params, ascii_params)
365 .ok_or(Error::InvalidGeoKeyDirectory)
366}
367
368#[cfg(feature = "local")]
369fn parse_fixed_len_double_tag<const N: usize>(value: Option<&TagValue>) -> Option<[f64; N]> {
370 let values = value.and_then(TagValue::as_f64_vec)?;
371 if values.len() < N {
372 return None;
373 }
374 let mut out = [0.0; N];
375 out.copy_from_slice(&values[..N]);
376 Some(out)
377}
378
379#[cfg(feature = "local")]
380fn parse_tiepoints(ifd: &tiff_reader::Ifd) -> Vec<[f64; 6]> {
381 let values = ifd
382 .tag(TAG_MODEL_TIEPOINT)
383 .and_then(|tag| tag.value.as_f64_vec())
384 .unwrap_or_default();
385 values
386 .chunks_exact(6)
387 .map(|chunk| [chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5]])
388 .collect()
389}
390
391#[cfg(feature = "local")]
392fn parse_nodata(ifd: &tiff_reader::Ifd) -> Option<String> {
393 ifd.tag(TAG_GDAL_NODATA)
394 .and_then(|tag| tag.value.as_str())
395 .map(ToOwned::to_owned)
396}
397
398#[cfg(test)]
399#[cfg(feature = "local")]
400mod tests {
401 use super::GeoTiffFile;
402
403 #[derive(Clone)]
404 struct TestIfdSpec {
405 entries: Vec<(u16, u16, u32, Vec<u8>)>,
406 image_data: Vec<u8>,
407 }
408
409 fn le_u16(value: u16) -> [u8; 2] {
410 value.to_le_bytes()
411 }
412
413 fn le_u32(value: u32) -> [u8; 4] {
414 value.to_le_bytes()
415 }
416
417 fn le_f64(value: f64) -> [u8; 8] {
418 value.to_le_bytes()
419 }
420
421 fn inline_short(value: u16) -> Vec<u8> {
422 let mut bytes = [0u8; 4];
423 bytes[..2].copy_from_slice(&le_u16(value));
424 bytes.to_vec()
425 }
426
427 #[allow(clippy::too_many_arguments)]
428 fn build_lerc2_header_v2(
429 width: u32,
430 height: u32,
431 valid_pixel_count: u32,
432 image_type: i32,
433 max_z_error: f64,
434 z_min: f64,
435 z_max: f64,
436 payload_len: usize,
437 ) -> Vec<u8> {
438 let blob_size = 58 + 4 + payload_len;
439 let mut bytes = Vec::with_capacity(blob_size);
440 bytes.extend_from_slice(b"Lerc2 ");
441 bytes.extend_from_slice(&2i32.to_le_bytes());
442 bytes.extend_from_slice(&height.to_le_bytes());
443 bytes.extend_from_slice(&width.to_le_bytes());
444 bytes.extend_from_slice(&valid_pixel_count.to_le_bytes());
445 bytes.extend_from_slice(&8i32.to_le_bytes());
446 bytes.extend_from_slice(&(blob_size as i32).to_le_bytes());
447 bytes.extend_from_slice(&image_type.to_le_bytes());
448 bytes.extend_from_slice(&max_z_error.to_le_bytes());
449 bytes.extend_from_slice(&z_min.to_le_bytes());
450 bytes.extend_from_slice(&z_max.to_le_bytes());
451 bytes
452 }
453
454 fn build_classic_tiff(ifds: &[TestIfdSpec]) -> Vec<u8> {
455 let mut ifd_offsets = Vec::with_capacity(ifds.len());
456 let mut cursor = 8usize;
457 for ifd in ifds {
458 ifd_offsets.push(cursor as u32);
459 let deferred_len: usize = ifd
460 .entries
461 .iter()
462 .filter(|(tag, _, _, value)| *tag != 273 && value.len() > 4)
463 .map(|(_, _, _, value)| value.len())
464 .sum();
465 cursor += 2 + ifd.entries.len() * 12 + 4 + ifd.image_data.len() + deferred_len;
466 }
467
468 let mut bytes = Vec::with_capacity(cursor);
469 bytes.extend_from_slice(b"II");
470 bytes.extend_from_slice(&le_u16(42));
471 bytes.extend_from_slice(&le_u32(ifd_offsets.first().copied().unwrap_or(0)));
472
473 for (ifd_index, ifd) in ifds.iter().enumerate() {
474 let ifd_offset = ifd_offsets[ifd_index] as usize;
475 debug_assert_eq!(bytes.len(), ifd_offset);
476
477 let ifd_size = 2 + ifd.entries.len() * 12 + 4;
478 let mut next_data_offset = ifd_offset + ifd_size;
479 let image_offset = next_data_offset as u32;
480 next_data_offset += ifd.image_data.len();
481
482 bytes.extend_from_slice(&le_u16(ifd.entries.len() as u16));
483 let mut deferred = Vec::new();
484 for (tag, ty, count, value) in &ifd.entries {
485 bytes.extend_from_slice(&le_u16(*tag));
486 bytes.extend_from_slice(&le_u16(*ty));
487 bytes.extend_from_slice(&le_u32(*count));
488 if *tag == 273 {
489 bytes.extend_from_slice(&le_u32(image_offset));
490 } else if value.len() <= 4 {
491 let mut inline = [0u8; 4];
492 inline[..value.len()].copy_from_slice(value);
493 bytes.extend_from_slice(&inline);
494 } else {
495 bytes.extend_from_slice(&le_u32(next_data_offset as u32));
496 next_data_offset += value.len();
497 deferred.push(value.clone());
498 }
499 }
500
501 let next_ifd_offset = ifd_offsets.get(ifd_index + 1).copied().unwrap_or(0);
502 bytes.extend_from_slice(&le_u32(next_ifd_offset));
503 bytes.extend_from_slice(&ifd.image_data);
504 for value in deferred {
505 bytes.extend_from_slice(&value);
506 }
507 debug_assert_eq!(bytes.len(), next_data_offset);
508 }
509
510 bytes
511 }
512
513 fn build_simple_geotiff(pixel_is_point: bool) -> Vec<u8> {
514 let image_data = vec![10u8, 20, 30, 40];
515 let tiepoints = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0];
516 let scales = [2.0, 2.0, 0.0];
517 let geo_keys = if pixel_is_point {
518 vec![
519 1, 1, 0, 3, 1024, 0, 1, 2, 1025, 0, 1, 2, 2048, 0, 1, 4326, ]
524 } else {
525 vec![
526 1, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326, ]
530 };
531 let nodata = b"-9999\0".to_vec();
532
533 build_classic_tiff(&[TestIfdSpec {
534 image_data,
535 entries: vec![
536 (256u16, 4u16, 1u32, le_u32(2).to_vec()),
537 (257u16, 4u16, 1u32, le_u32(2).to_vec()),
538 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
539 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
540 (273u16, 4u16, 1u32, vec![]),
541 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
542 (278u16, 4u16, 1u32, le_u32(2).to_vec()),
543 (279u16, 4u16, 1u32, le_u32(4).to_vec()),
544 (
545 33550u16,
546 12u16,
547 3u32,
548 scales.iter().flat_map(|value| le_f64(*value)).collect(),
549 ),
550 (
551 33922u16,
552 12u16,
553 6u32,
554 tiepoints.iter().flat_map(|value| le_f64(*value)).collect(),
555 ),
556 (
557 34735u16,
558 3u16,
559 geo_keys.len() as u32,
560 geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
561 ),
562 (42113u16, 2u16, nodata.len() as u32, nodata),
563 ],
564 }])
565 }
566
567 fn build_simple_lerc_geotiff() -> Vec<u8> {
568 let tiepoints = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0];
569 let scales = [2.0, 2.0, 0.0];
570 let geo_keys = vec![
571 1, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326, ];
575
576 let mut image_data = build_lerc2_header_v2(2, 2, 4, 6, 0.0, 1.0, 4.0, 1 + 16);
577 image_data.extend_from_slice(&0u32.to_le_bytes());
578 image_data.push(1);
579 for value in [1.0f32, 2.0, 3.0, 4.0] {
580 image_data.extend_from_slice(&value.to_le_bytes());
581 }
582 let image_len = image_data.len() as u32;
583
584 build_classic_tiff(&[TestIfdSpec {
585 image_data,
586 entries: vec![
587 (256u16, 4u16, 1u32, le_u32(2).to_vec()),
588 (257u16, 4u16, 1u32, le_u32(2).to_vec()),
589 (258u16, 3u16, 1u32, inline_short(32)),
590 (259u16, 3u16, 1u32, inline_short(34887)),
591 (273u16, 4u16, 1u32, vec![]),
592 (277u16, 3u16, 1u32, inline_short(1)),
593 (278u16, 4u16, 1u32, le_u32(2).to_vec()),
594 (279u16, 4u16, 1u32, le_u32(image_len).to_vec()),
595 (339u16, 3u16, 1u32, inline_short(3)),
596 (
597 33550u16,
598 12u16,
599 3u32,
600 scales.iter().flat_map(|value| le_f64(*value)).collect(),
601 ),
602 (
603 33922u16,
604 12u16,
605 6u32,
606 tiepoints.iter().flat_map(|value| le_f64(*value)).collect(),
607 ),
608 (
609 34735u16,
610 3u16,
611 geo_keys.len() as u32,
612 geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
613 ),
614 ],
615 }])
616 }
617
618 fn overwrite_classic_inline_long_tag(bytes: &mut [u8], tag_code: u16, value: u32) {
619 let entry_count = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
620 let mut offset = 10usize;
621 for _ in 0..entry_count {
622 let code = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
623 if code == tag_code {
624 bytes[offset + 8..offset + 12].copy_from_slice(&le_u32(value));
625 return;
626 }
627 offset += 12;
628 }
629 panic!("tag {tag_code} not found in classic TIFF");
630 }
631
632 fn build_geotiff_with_overview() -> Vec<u8> {
633 let base = TestIfdSpec {
634 image_data: vec![10u8, 20, 30, 40],
635 entries: vec![
636 (256u16, 4u16, 1u32, le_u32(2).to_vec()),
637 (257u16, 4u16, 1u32, le_u32(2).to_vec()),
638 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
639 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
640 (273u16, 4u16, 1u32, vec![]),
641 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
642 (278u16, 4u16, 1u32, le_u32(2).to_vec()),
643 (279u16, 4u16, 1u32, le_u32(4).to_vec()),
644 (
645 33550u16,
646 12u16,
647 3u32,
648 [2.0, 2.0, 0.0]
649 .iter()
650 .flat_map(|value| le_f64(*value))
651 .collect(),
652 ),
653 (
654 33922u16,
655 12u16,
656 6u32,
657 [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
658 .iter()
659 .flat_map(|value| le_f64(*value))
660 .collect(),
661 ),
662 (
663 34735u16,
664 3u16,
665 12u32,
666 [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326]
667 .iter()
668 .flat_map(|value| le_u16(*value))
669 .collect(),
670 ),
671 ],
672 };
673 let overview = TestIfdSpec {
674 image_data: vec![99u8],
675 entries: vec![
676 (254u16, 4u16, 1u32, le_u32(1).to_vec()),
677 (256u16, 4u16, 1u32, le_u32(1).to_vec()),
678 (257u16, 4u16, 1u32, le_u32(1).to_vec()),
679 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
680 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
681 (273u16, 4u16, 1u32, vec![]),
682 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
683 (278u16, 4u16, 1u32, le_u32(1).to_vec()),
684 (279u16, 4u16, 1u32, le_u32(1).to_vec()),
685 ],
686 };
687
688 build_classic_tiff(&[base, overview])
689 }
690
691 fn build_cog_like_geotiff_with_ghost_ifd() -> Vec<u8> {
692 let geo_keys = [1u16, 1, 0, 2, 1024, 0, 1, 2, 2048, 0, 1, 4326];
693 let ghost = TestIfdSpec {
694 image_data: vec![0u8],
695 entries: vec![
696 (254u16, 4u16, 1u32, le_u32(1).to_vec()),
697 (256u16, 4u16, 1u32, le_u32(1).to_vec()),
698 (257u16, 4u16, 1u32, le_u32(1).to_vec()),
699 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
700 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
701 (273u16, 4u16, 1u32, vec![]),
702 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
703 (278u16, 4u16, 1u32, le_u32(1).to_vec()),
704 (279u16, 4u16, 1u32, le_u32(1).to_vec()),
705 (
706 33550u16,
707 12u16,
708 3u32,
709 [2.0, 2.0, 0.0]
710 .iter()
711 .flat_map(|value| le_f64(*value))
712 .collect(),
713 ),
714 (
715 33922u16,
716 12u16,
717 6u32,
718 [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]
719 .iter()
720 .flat_map(|value| le_f64(*value))
721 .collect(),
722 ),
723 (
724 34735u16,
725 3u16,
726 geo_keys.len() as u32,
727 geo_keys.iter().flat_map(|value| le_u16(*value)).collect(),
728 ),
729 ],
730 };
731 let overview = TestIfdSpec {
732 image_data: vec![50u8, 60, 70, 80],
733 entries: vec![
734 (254u16, 4u16, 1u32, le_u32(1).to_vec()),
735 (256u16, 4u16, 1u32, le_u32(2).to_vec()),
736 (257u16, 4u16, 1u32, le_u32(2).to_vec()),
737 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
738 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
739 (273u16, 4u16, 1u32, vec![]),
740 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
741 (278u16, 4u16, 1u32, le_u32(2).to_vec()),
742 (279u16, 4u16, 1u32, le_u32(4).to_vec()),
743 ],
744 };
745 let base = TestIfdSpec {
746 image_data: (1u8..=16).collect(),
747 entries: vec![
748 (256u16, 4u16, 1u32, le_u32(4).to_vec()),
749 (257u16, 4u16, 1u32, le_u32(4).to_vec()),
750 (258u16, 3u16, 1u32, [8, 0, 0, 0].to_vec()),
751 (259u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
752 (273u16, 4u16, 1u32, vec![]),
753 (277u16, 3u16, 1u32, [1, 0, 0, 0].to_vec()),
754 (278u16, 4u16, 1u32, le_u32(4).to_vec()),
755 (279u16, 4u16, 1u32, le_u32(16).to_vec()),
756 ],
757 };
758
759 build_classic_tiff(&[ghost, overview, base])
760 }
761
762 #[test]
763 fn parses_geotiff_metadata_and_reads_raster() {
764 let file = GeoTiffFile::from_bytes(build_simple_geotiff(false)).unwrap();
765 assert_eq!(file.epsg(), Some(4326));
766 assert_eq!(file.width(), 2);
767 assert_eq!(file.height(), 2);
768 assert_eq!(file.band_count(), 1);
769 assert_eq!(file.nodata(), Some("-9999"));
770 assert_eq!(file.geo_bounds(), Some([100.0, 196.0, 104.0, 200.0]));
771
772 let raster = file.read_raster::<u8>().unwrap();
773 assert_eq!(raster.shape(), &[2, 2]);
774 let (values, offset) = raster.into_raw_vec_and_offset();
775 assert_eq!(offset, Some(0));
776 assert_eq!(values, vec![10, 20, 30, 40]);
777 }
778
779 #[test]
780 fn parses_geotiff_metadata_and_reads_lerc_raster() {
781 let file = GeoTiffFile::from_bytes(build_simple_lerc_geotiff()).unwrap();
782 assert_eq!(file.epsg(), Some(4326));
783 assert_eq!(file.width(), 2);
784 assert_eq!(file.height(), 2);
785
786 let raster = file.read_raster::<f32>().unwrap();
787 assert_eq!(raster.shape(), &[2, 2]);
788 let (values, offset) = raster.into_raw_vec_and_offset();
789 assert_eq!(offset, Some(0));
790 assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0]);
791 }
792
793 #[test]
794 fn pixel_is_point_metadata_shifts_bounds_to_outer_edges() {
795 let file = GeoTiffFile::from_bytes(build_simple_geotiff(true)).unwrap();
796 assert_eq!(file.geo_bounds(), Some([99.0, 197.0, 103.0, 201.0]));
797
798 let transform = file.transform().unwrap();
799 let (center_x, center_y) = transform.pixel_to_geo(0.5, 0.5);
800 assert_eq!((center_x, center_y), (100.0, 200.0));
801 }
802
803 #[test]
804 fn discovers_reduced_resolution_overviews() {
805 let file = GeoTiffFile::from_bytes(build_geotiff_with_overview()).unwrap();
806 assert_eq!(file.overview_count(), 1);
807 assert_eq!(file.overview_ifd_index(0).unwrap(), 1);
808
809 let overview = file.read_overview::<u8>(0).unwrap();
810 assert_eq!(overview.shape(), &[1, 1]);
811 let (values, offset) = overview.into_raw_vec_and_offset();
812 assert_eq!(offset, Some(0));
813 assert_eq!(values, vec![99]);
814 }
815
816 #[test]
817 fn reads_base_raster_window() {
818 let file = GeoTiffFile::from_bytes(build_simple_geotiff(false)).unwrap();
819 let window = file.read_window::<u8>(1, 0, 1, 2).unwrap();
820 assert_eq!(window.shape(), &[1, 2]);
821 let (values, offset) = window.into_raw_vec_and_offset();
822 assert_eq!(offset, Some(0));
823 assert_eq!(values, vec![30, 40]);
824 }
825
826 #[test]
827 fn reads_overview_window() {
828 let file = GeoTiffFile::from_bytes(build_geotiff_with_overview()).unwrap();
829 let window = file.read_overview_window::<u8>(0, 0, 0, 1, 1).unwrap();
830 assert_eq!(window.shape(), &[1, 1]);
831 let (values, offset) = window.into_raw_vec_and_offset();
832 assert_eq!(offset, Some(0));
833 assert_eq!(values, vec![99]);
834 }
835
836 #[test]
837 fn prefers_non_ghost_base_ifd_for_cog_like_layouts() {
838 let file = GeoTiffFile::from_bytes(build_cog_like_geotiff_with_ghost_ifd()).unwrap();
839 assert_eq!(file.base_ifd_index(), 2);
840 assert_eq!(file.width(), 4);
841 assert_eq!(file.height(), 4);
842 assert_eq!(file.geo_bounds(), Some([100.0, 192.0, 108.0, 200.0]));
843 assert_eq!(file.overview_count(), 1);
844 assert_eq!(file.overview_ifd_index(0).unwrap(), 1);
845
846 let base = file.read_raster::<u8>().unwrap();
847 assert_eq!(base.shape(), &[4, 4]);
848 let (values, offset) = base.into_raw_vec_and_offset();
849 assert_eq!(offset, Some(0));
850 assert_eq!(values, (1u8..=16).collect::<Vec<_>>());
851
852 let overview = file.read_overview::<u8>(0).unwrap();
853 assert_eq!(overview.shape(), &[2, 2]);
854 let (values, offset) = overview.into_raw_vec_and_offset();
855 assert_eq!(offset, Some(0));
856 assert_eq!(values, vec![50, 60, 70, 80]);
857 }
858
859 #[test]
860 fn rejects_zero_rows_per_strip_without_panicking() {
861 let mut bytes = build_simple_geotiff(false);
862 overwrite_classic_inline_long_tag(&mut bytes, 278, 0);
863
864 let file = GeoTiffFile::from_bytes(bytes).unwrap();
865 assert_eq!(file.epsg(), Some(4326));
866
867 let error = file.tiff().read_image_bytes(0).unwrap_err();
868 assert!(error.to_string().contains("RowsPerStrip"));
869 }
870}