Skip to main content

oxigdal_geotiff/
lib.rs

1//! OxiGDAL GeoTIFF Driver - Pure Rust GeoTIFF/COG Support
2//!
3//! This crate provides a pure Rust implementation of GeoTIFF and Cloud Optimized
4//! GeoTIFF (COG) reading and writing capabilities.
5//!
6//! # Features
7//!
8//! - `std` (default) - Enable standard library support
9//! - `async` - Enable async I/O support
10//! - `deflate` (default) - DEFLATE/zlib compression
11//! - `lzw` (default) - LZW compression
12//! - `zstd` - ZSTD compression
13//! - `jpeg` - JPEG compression (planned)
14//! - `webp` - WebP compression (planned)
15//!
16//! # Example
17//!
18//! ```ignore
19//! use oxigdal_geotiff::cog::CogReader;
20//! use oxigdal_core::io::FileDataSource;
21//!
22//! let source = FileDataSource::open("image.tif")?;
23//! let reader = CogReader::open(source)?;
24//!
25//! println!("Image size: {}x{}", reader.width(), reader.height());
26//! println!("Tile size: {:?}", reader.tile_size());
27//! println!("Overview count: {}", reader.overview_count());
28//!
29//! // Read a tile
30//! let tile_data = reader.read_tile(0, 0, 0)?;
31//! ```
32
33#![warn(clippy::all)]
34// Pedantic disabled to reduce noise - default clippy::all is sufficient
35// #![warn(clippy::pedantic)]
36#![deny(clippy::unwrap_used)]
37#![allow(clippy::module_name_repetitions)]
38// Allow dead code for internal writer components
39#![allow(dead_code)]
40// Allow expect() for internal invariant checks
41#![allow(clippy::expect_used)]
42// Allow too many arguments for complex geospatial operations
43#![allow(clippy::too_many_arguments)]
44// Allow clamp patterns for raster data normalization
45#![allow(clippy::manual_clamp)]
46// Allow push after creation for buffer building patterns
47#![allow(clippy::vec_init_then_push)]
48// Allow partial documentation during development
49#![allow(missing_docs)]
50
51pub mod adaptive_tiling;
52pub mod band_algebra;
53pub mod cog;
54pub mod color_space;
55pub mod compression;
56pub mod geokeys;
57pub mod jpeg_codec;
58pub mod lerc_codec;
59pub mod overviews;
60pub mod tiff;
61pub mod writer;
62
63// Re-export commonly used types
64pub use cog::CogReader;
65pub use geokeys::{GeoKey, GeoKeyDirectory, ModelType, RasterType};
66pub use tiff::{Compression, ImageInfo, PhotometricInterpretation, TiffFile, TiffHeader, TiffTag};
67pub use writer::{
68    CogWriter, CogWriterOptions, GeoTiffWriter, GeoTiffWriterOptions, OverviewResampling,
69    WriterConfig,
70};
71
72use oxigdal_core::buffer::RasterBuffer;
73use oxigdal_core::error::{FormatError, OxiGdalError, Result};
74use oxigdal_core::io::DataSource;
75use oxigdal_core::types::{
76    ColorInterpretation, GeoTransform, NoDataValue, RasterDataType, RasterMetadata,
77};
78
79/// Generates WKT string from GeoKeys
80///
81/// # Arguments
82/// * `geo_keys` - Optional reference to GeoKeyDirectory
83///
84/// # Returns
85/// WKT string if CRS information is available
86fn parse_geokeys_to_wkt(geo_keys: Option<&GeoKeyDirectory>) -> Option<String> {
87    let geo_keys = geo_keys?;
88    let epsg_code = geo_keys.epsg_code()?;
89
90    // Generate WKT based on EPSG code
91    // For comprehensive WKT, we'd need a full EPSG database, but we can handle common cases
92    Some(match epsg_code {
93        // WGS 84
94        4326 => {
95            r#"GEOGCS["WGS 84",
96    DATUM["WGS_1984",
97        SPHEROID["WGS 84",6378137,298.257223563,
98            AUTHORITY["EPSG","7030"]],
99        AUTHORITY["EPSG","6326"]],
100    PRIMEM["Greenwich",0,
101        AUTHORITY["EPSG","8901"]],
102    UNIT["degree",0.0174532925199433,
103        AUTHORITY["EPSG","9122"]],
104    AXIS["Latitude",NORTH],
105    AXIS["Longitude",EAST],
106    AUTHORITY["EPSG","4326"]]"#
107                .to_string()
108        }
109        // WGS 84 / Pseudo-Mercator (Web Mercator)
110        3857 => {
111            r#"PROJCS["WGS 84 / Pseudo-Mercator",
112    GEOGCS["WGS 84",
113        DATUM["WGS_1984",
114            SPHEROID["WGS 84",6378137,298.257223563,
115                AUTHORITY["EPSG","7030"]],
116            AUTHORITY["EPSG","6326"]],
117        PRIMEM["Greenwich",0,
118            AUTHORITY["EPSG","8901"]],
119        UNIT["degree",0.0174532925199433,
120            AUTHORITY["EPSG","9122"]],
121        AUTHORITY["EPSG","4326"]],
122    PROJECTION["Mercator_1SP"],
123    PARAMETER["central_meridian",0],
124    PARAMETER["scale_factor",1],
125    PARAMETER["false_easting",0],
126    PARAMETER["false_northing",0],
127    UNIT["metre",1,
128        AUTHORITY["EPSG","9001"]],
129    AXIS["Easting",EAST],
130    AXIS["Northing",NORTH],
131    EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs"],
132    AUTHORITY["EPSG","3857"]]"#
133                .to_string()
134        }
135        // WGS 84 / UTM zones (Northern Hemisphere: 32601-32660)
136        32601..=32660 => {
137            let zone = epsg_code - 32600;
138            format!(
139                r#"PROJCS["WGS 84 / UTM zone {}N",
140    GEOGCS["WGS 84",
141        DATUM["WGS_1984",
142            SPHEROID["WGS 84",6378137,298.257223563,
143                AUTHORITY["EPSG","7030"]],
144            AUTHORITY["EPSG","6326"]],
145        PRIMEM["Greenwich",0,
146            AUTHORITY["EPSG","8901"]],
147        UNIT["degree",0.0174532925199433,
148            AUTHORITY["EPSG","9122"]],
149        AUTHORITY["EPSG","4326"]],
150    PROJECTION["Transverse_Mercator"],
151    PARAMETER["latitude_of_origin",0],
152    PARAMETER["central_meridian",{}],
153    PARAMETER["scale_factor",0.9996],
154    PARAMETER["false_easting",500000],
155    PARAMETER["false_northing",0],
156    UNIT["metre",1,
157        AUTHORITY["EPSG","9001"]],
158    AXIS["Easting",EAST],
159    AXIS["Northing",NORTH],
160    AUTHORITY["EPSG","{}""]]"#,
161                zone,
162                zone as i32 * 6 - 183,
163                epsg_code
164            )
165        }
166        // WGS 84 / UTM zones (Southern Hemisphere: 32701-32760)
167        32701..=32760 => {
168            let zone = epsg_code - 32700;
169            format!(
170                r#"PROJCS["WGS 84 / UTM zone {}S",
171    GEOGCS["WGS 84",
172        DATUM["WGS_1984",
173            SPHEROID["WGS 84",6378137,298.257223563,
174                AUTHORITY["EPSG","7030"]],
175            AUTHORITY["EPSG","6326"]],
176        PRIMEM["Greenwich",0,
177            AUTHORITY["EPSG","8901"]],
178        UNIT["degree",0.0174532925199433,
179            AUTHORITY["EPSG","9122"]],
180        AUTHORITY["EPSG","4326"]],
181    PROJECTION["Transverse_Mercator"],
182    PARAMETER["latitude_of_origin",0],
183    PARAMETER["central_meridian",{}],
184    PARAMETER["scale_factor",0.9996],
185    PARAMETER["false_easting",500000],
186    PARAMETER["false_northing",10000000],
187    UNIT["metre",1,
188        AUTHORITY["EPSG","9001"]],
189    AXIS["Easting",EAST],
190    AXIS["Northing",NORTH],
191    AUTHORITY["EPSG","{}""]]"#,
192                zone,
193                zone as i32 * 6 - 183,
194                epsg_code
195            )
196        }
197        // NAD83
198        4269 => {
199            r#"GEOGCS["NAD83",
200    DATUM["North_American_Datum_1983",
201        SPHEROID["GRS 1980",6378137,298.257222101,
202            AUTHORITY["EPSG","7019"]],
203        AUTHORITY["EPSG","6269"]],
204    PRIMEM["Greenwich",0,
205        AUTHORITY["EPSG","8901"]],
206    UNIT["degree",0.0174532925199433,
207        AUTHORITY["EPSG","9122"]],
208    AXIS["Latitude",NORTH],
209    AXIS["Longitude",EAST],
210    AUTHORITY["EPSG","4269"]]"#
211                .to_string()
212        }
213        // NAD27
214        4267 => {
215            r#"GEOGCS["NAD27",
216    DATUM["North_American_Datum_1927",
217        SPHEROID["Clarke 1866",6378206.4,294.978698213898,
218            AUTHORITY["EPSG","7008"]],
219        AUTHORITY["EPSG","6267"]],
220    PRIMEM["Greenwich",0,
221        AUTHORITY["EPSG","8901"]],
222    UNIT["degree",0.0174532925199433,
223        AUTHORITY["EPSG","9122"]],
224    AXIS["Latitude",NORTH],
225    AXIS["Longitude",EAST],
226    AUTHORITY["EPSG","4267"]]"#
227                .to_string()
228        }
229        // For other EPSG codes, use a simple reference
230        _ => format!("EPSG:{}", epsg_code),
231    })
232}
233
234/// Parses color interpretation from photometric interpretation
235///
236/// # Arguments
237/// * `photometric` - The photometric interpretation from TIFF
238/// * `samples_per_pixel` - Number of samples (bands) per pixel
239///
240/// # Returns
241/// Vector of color interpretations for each band
242fn parse_photometric_interpretation(
243    photometric: PhotometricInterpretation,
244    samples_per_pixel: u16,
245) -> Vec<ColorInterpretation> {
246    match photometric {
247        PhotometricInterpretation::WhiteIsZero | PhotometricInterpretation::BlackIsZero => {
248            // Grayscale - might have alpha channel
249            if samples_per_pixel == 1 {
250                vec![ColorInterpretation::Gray]
251            } else if samples_per_pixel == 2 {
252                vec![ColorInterpretation::Gray, ColorInterpretation::Alpha]
253            } else {
254                // Multiple grayscale bands
255                vec![ColorInterpretation::Gray; samples_per_pixel as usize]
256            }
257        }
258        PhotometricInterpretation::Rgb => {
259            // RGB or RGBA
260            match samples_per_pixel {
261                1 => vec![ColorInterpretation::Red],
262                2 => vec![ColorInterpretation::Red, ColorInterpretation::Green],
263                3 => vec![
264                    ColorInterpretation::Red,
265                    ColorInterpretation::Green,
266                    ColorInterpretation::Blue,
267                ],
268                4 => vec![
269                    ColorInterpretation::Red,
270                    ColorInterpretation::Green,
271                    ColorInterpretation::Blue,
272                    ColorInterpretation::Alpha,
273                ],
274                _ => {
275                    // More than 4 bands - treat extras as undefined
276                    let mut interp = vec![
277                        ColorInterpretation::Red,
278                        ColorInterpretation::Green,
279                        ColorInterpretation::Blue,
280                    ];
281                    if samples_per_pixel > 3 {
282                        interp.push(ColorInterpretation::Alpha);
283                    }
284                    for _ in 4..samples_per_pixel {
285                        interp.push(ColorInterpretation::Undefined);
286                    }
287                    interp
288                }
289            }
290        }
291        PhotometricInterpretation::Palette => {
292            // Palette color - index plus optional alpha
293            if samples_per_pixel == 1 {
294                vec![ColorInterpretation::PaletteIndex]
295            } else if samples_per_pixel == 2 {
296                vec![
297                    ColorInterpretation::PaletteIndex,
298                    ColorInterpretation::Alpha,
299                ]
300            } else {
301                vec![ColorInterpretation::PaletteIndex; samples_per_pixel as usize]
302            }
303        }
304        PhotometricInterpretation::Cmyk => {
305            // CMYK
306            match samples_per_pixel {
307                1 => vec![ColorInterpretation::Cyan],
308                2 => vec![ColorInterpretation::Cyan, ColorInterpretation::Magenta],
309                3 => vec![
310                    ColorInterpretation::Cyan,
311                    ColorInterpretation::Magenta,
312                    ColorInterpretation::Yellow,
313                ],
314                4 => vec![
315                    ColorInterpretation::Cyan,
316                    ColorInterpretation::Magenta,
317                    ColorInterpretation::Yellow,
318                    ColorInterpretation::Black,
319                ],
320                _ => {
321                    // More than 4 bands - treat extras as undefined
322                    let mut interp = vec![
323                        ColorInterpretation::Cyan,
324                        ColorInterpretation::Magenta,
325                        ColorInterpretation::Yellow,
326                        ColorInterpretation::Black,
327                    ];
328                    for _ in 4..samples_per_pixel {
329                        interp.push(ColorInterpretation::Undefined);
330                    }
331                    interp
332                }
333            }
334        }
335        PhotometricInterpretation::YCbCr => {
336            // YCbCr
337            match samples_per_pixel {
338                1 => vec![ColorInterpretation::YCbCrY],
339                2 => vec![ColorInterpretation::YCbCrY, ColorInterpretation::YCbCrCb],
340                3 => vec![
341                    ColorInterpretation::YCbCrY,
342                    ColorInterpretation::YCbCrCb,
343                    ColorInterpretation::YCbCrCr,
344                ],
345                _ => {
346                    // More than 3 bands - add alpha or undefined
347                    let mut interp = vec![
348                        ColorInterpretation::YCbCrY,
349                        ColorInterpretation::YCbCrCb,
350                        ColorInterpretation::YCbCrCr,
351                    ];
352                    if samples_per_pixel > 3 {
353                        interp.push(ColorInterpretation::Alpha);
354                    }
355                    for _ in 4..samples_per_pixel {
356                        interp.push(ColorInterpretation::Undefined);
357                    }
358                    interp
359                }
360            }
361        }
362        // For other photometric interpretations (TransparencyMask, CIE Lab, etc.)
363        _ => vec![ColorInterpretation::Undefined; samples_per_pixel as usize],
364    }
365}
366
367/// GeoTIFF reader (high-level API)
368pub struct GeoTiffReader<S: DataSource> {
369    cog_reader: CogReader<S>,
370    geo_transform: Option<GeoTransform>,
371    nodata: NoDataValue,
372}
373
374impl<S: DataSource> GeoTiffReader<S> {
375    /// Opens a GeoTIFF file
376    ///
377    /// # Errors
378    /// Returns an error if the file cannot be opened or parsed
379    pub fn open(source: S) -> Result<Self> {
380        let cog_reader = CogReader::open(source)?;
381
382        // Extract geotransform
383        let geo_transform = cog_reader.geo_transform()?;
384
385        // Extract nodata
386        let nodata = cog_reader.nodata()?;
387
388        Ok(Self {
389            cog_reader,
390            geo_transform,
391            nodata,
392        })
393    }
394
395    /// Returns the image width
396    #[must_use]
397    pub fn width(&self) -> u64 {
398        self.cog_reader.width()
399    }
400
401    /// Returns the image height
402    #[must_use]
403    pub fn height(&self) -> u64 {
404        self.cog_reader.height()
405    }
406
407    /// Returns the number of bands
408    #[must_use]
409    pub fn band_count(&self) -> u32 {
410        u32::from(self.cog_reader.primary_info().samples_per_pixel)
411    }
412
413    /// Returns the data type
414    #[must_use]
415    pub fn data_type(&self) -> Option<RasterDataType> {
416        self.cog_reader.primary_info().data_type()
417    }
418
419    /// Returns the tile size
420    #[must_use]
421    pub fn tile_size(&self) -> Option<(u32, u32)> {
422        self.cog_reader.tile_size()
423    }
424
425    /// Returns the number of overview levels
426    #[must_use]
427    pub fn overview_count(&self) -> usize {
428        self.cog_reader.overview_count()
429    }
430
431    /// Returns the GeoTransform
432    #[must_use]
433    pub fn geo_transform(&self) -> Option<&GeoTransform> {
434        self.geo_transform.as_ref()
435    }
436
437    /// Returns the NoData value
438    #[must_use]
439    pub const fn nodata(&self) -> NoDataValue {
440        self.nodata
441    }
442
443    /// Returns the EPSG code
444    #[must_use]
445    pub fn epsg_code(&self) -> Option<u32> {
446        self.cog_reader.epsg_code()
447    }
448
449    /// Returns the compression scheme
450    #[must_use]
451    pub fn compression(&self) -> Compression {
452        self.cog_reader.primary_info().compression
453    }
454
455    /// Returns the number of tiles in X and Y directions
456    #[must_use]
457    pub fn tile_count(&self) -> (u32, u32) {
458        self.cog_reader.tile_count()
459    }
460
461    /// Reads a tile
462    ///
463    /// # Errors
464    /// Returns an error if the tile cannot be read
465    pub fn read_tile(&self, level: usize, tile_x: u32, tile_y: u32) -> Result<Vec<u8>> {
466        self.cog_reader.read_tile(level, tile_x, tile_y)
467    }
468
469    /// Reads a tile as a RasterBuffer
470    ///
471    /// # Errors
472    /// Returns an error if the tile cannot be read
473    pub fn read_tile_buffer(&self, level: usize, tile_x: u32, tile_y: u32) -> Result<RasterBuffer> {
474        let data = self.read_tile(level, tile_x, tile_y)?;
475        let info = self.cog_reader.primary_info();
476
477        let tile_width = info.tile_width.unwrap_or(info.width as u32) as u64;
478        let tile_height = info.tile_height.unwrap_or(info.height as u32) as u64;
479        let data_type =
480            info.data_type()
481                .ok_or(OxiGdalError::Format(FormatError::InvalidDataType {
482                    type_id: 0,
483                }))?;
484
485        RasterBuffer::new(data, tile_width, tile_height, data_type, self.nodata)
486    }
487
488    /// Returns the raster metadata
489    #[must_use]
490    pub fn metadata(&self) -> RasterMetadata {
491        let info = self.cog_reader.primary_info();
492
493        // Generate WKT from GeoKeys
494        let crs_wkt = parse_geokeys_to_wkt(self.cog_reader.geo_keys());
495
496        // Parse color interpretation from photometric
497        let color_interpretation =
498            parse_photometric_interpretation(info.photometric, info.samples_per_pixel);
499
500        RasterMetadata {
501            width: info.width,
502            height: info.height,
503            band_count: u32::from(info.samples_per_pixel),
504            data_type: info.data_type().unwrap_or(RasterDataType::UInt8),
505            geo_transform: self.geo_transform,
506            crs_wkt,
507            nodata: self.nodata,
508            color_interpretation,
509            layout: oxigdal_core::types::PixelLayout::Tiled {
510                tile_width: info.tile_width.unwrap_or(256),
511                tile_height: info.tile_height.unwrap_or(256),
512            },
513            driver_metadata: Vec::new(),
514        }
515    }
516
517    /// Reads a band's data
518    ///
519    /// # Errors
520    /// Returns an error if reading fails
521    pub fn read_band(&self, level: usize, _band: usize) -> Result<Vec<u8>> {
522        // Read all tiles/strips and combine them
523        let (tiles_x, tiles_y) = self.tile_count();
524        let info = self.cog_reader.primary_info();
525
526        let width = info.width as usize;
527        let height = info.height as usize;
528        let bytes_per_sample = (info.bits_per_sample.first().copied().unwrap_or(8) / 8) as usize;
529        let samples_per_pixel = info.samples_per_pixel as usize;
530
531        let mut result = vec![0u8; width * height * bytes_per_sample * samples_per_pixel];
532
533        // Determine if this is tiled or striped layout
534        let is_tiled = info.tile_width.is_some() && info.tile_height.is_some();
535
536        let (tile_width, default_tile_height) = if is_tiled {
537            (
538                info.tile_width.unwrap_or(width as u32) as usize,
539                info.tile_height.unwrap_or(height as u32) as usize,
540            )
541        } else {
542            // Striped layout
543            (width, info.rows_per_strip.unwrap_or(height as u32) as usize)
544        };
545
546        for ty in 0..tiles_y {
547            for tx in 0..tiles_x {
548                let tile_data = self.read_tile(level, tx, ty)?;
549
550                // Copy tile/strip data to result
551                let x_start = tx as usize * tile_width;
552                let y_start = ty as usize * default_tile_height;
553
554                // Calculate actual height of this tile/strip (may be smaller for last one)
555                let actual_rows = (height - y_start).min(default_tile_height);
556
557                // For tiled layouts: tile_data always has full tile_width stride
558                // For striped layouts: tile_data has image width stride
559                let src_stride = if is_tiled { tile_width } else { width };
560
561                for row in 0..actual_rows {
562                    let dst_y = y_start + row;
563                    if dst_y >= height {
564                        break;
565                    }
566
567                    let src_offset = row * src_stride * bytes_per_sample * samples_per_pixel;
568                    let dst_offset = dst_y * width * bytes_per_sample * samples_per_pixel
569                        + x_start * bytes_per_sample * samples_per_pixel;
570
571                    let copy_width = tile_width.min(width - x_start);
572                    let copy_bytes = copy_width * bytes_per_sample * samples_per_pixel;
573
574                    if src_offset + copy_bytes <= tile_data.len()
575                        && dst_offset + copy_bytes <= result.len()
576                    {
577                        result[dst_offset..dst_offset + copy_bytes]
578                            .copy_from_slice(&tile_data[src_offset..src_offset + copy_bytes]);
579                    }
580                }
581            }
582        }
583
584        Ok(result)
585    }
586
587    /// Creates a new reader (alias for `open`)
588    ///
589    /// # Errors
590    /// Returns an error if the file cannot be opened or parsed
591    pub fn new(source: S) -> Result<Self> {
592        Self::open(source)
593    }
594}
595
596/// Checks if data looks like a TIFF file
597#[must_use]
598pub fn is_tiff(data: &[u8]) -> bool {
599    if data.len() < 4 {
600        return false;
601    }
602
603    // Check for TIFF magic
604    (data[0] == 0x49 && data[1] == 0x49 && data[2] == 0x2A && data[3] == 0x00)  // Little-endian classic
605        || (data[0] == 0x4D && data[1] == 0x4D && data[2] == 0x00 && data[3] == 0x2A) // Big-endian classic
606        || (data[0] == 0x49 && data[1] == 0x49 && data[2] == 0x2B && data[3] == 0x00) // Little-endian BigTIFF
607        || (data[0] == 0x4D && data[1] == 0x4D && data[2] == 0x00 && data[3] == 0x2B) // Big-endian BigTIFF
608}
609
610/// Checks if a TIFF appears to be a COG
611pub fn is_cog<S: DataSource>(source: &S) -> Result<bool> {
612    let tiff = TiffFile::parse(source)?;
613    let validation = cog::validate_cog(&tiff, source);
614    Ok(validation.is_valid)
615}
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620    use crate::geokeys::GeoKeyEntry;
621
622    #[test]
623    fn test_is_tiff() {
624        // Classic TIFF, little-endian
625        assert!(is_tiff(&[0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00]));
626
627        // Classic TIFF, big-endian
628        assert!(is_tiff(&[0x4D, 0x4D, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x08]));
629
630        // BigTIFF, little-endian
631        assert!(is_tiff(&[0x49, 0x49, 0x2B, 0x00, 0x08, 0x00, 0x00, 0x00]));
632
633        // Not TIFF
634        assert!(!is_tiff(&[0x89, 0x50, 0x4E, 0x47])); // PNG
635        assert!(!is_tiff(&[0xFF, 0xD8, 0xFF])); // JPEG
636        assert!(!is_tiff(&[]));
637    }
638
639    #[test]
640    fn test_parse_geokeys_to_wkt_none() {
641        // Test with None input
642        let wkt = parse_geokeys_to_wkt(None);
643        assert!(wkt.is_none());
644    }
645
646    #[test]
647    fn test_parse_geokeys_to_wkt_epsg_4326() {
648        // Create a mock GeoKeyDirectory with EPSG:4326
649        let geo_dir = GeoKeyDirectory {
650            version: 1,
651            key_revision_major: 1,
652            key_revision_minor: 0,
653            entries: vec![GeoKeyEntry {
654                key_id: GeoKey::GeographicType as u16,
655                tiff_tag_location: 0,
656                count: 1,
657                value_offset: 4326,
658            }],
659            double_params: Vec::new(),
660            ascii_params: String::new(),
661        };
662
663        let wkt = parse_geokeys_to_wkt(Some(&geo_dir));
664        assert!(wkt.is_some());
665        let wkt_str = wkt.unwrap_or_default();
666        assert!(wkt_str.contains("WGS 84"));
667        assert!(wkt_str.contains("EPSG"));
668        assert!(wkt_str.contains("4326"));
669    }
670
671    #[test]
672    fn test_parse_geokeys_to_wkt_epsg_3857() {
673        // Create a mock GeoKeyDirectory with EPSG:3857 (Web Mercator)
674        let geo_dir = GeoKeyDirectory {
675            version: 1,
676            key_revision_major: 1,
677            key_revision_minor: 0,
678            entries: vec![GeoKeyEntry {
679                key_id: GeoKey::ProjectedCsType as u16,
680                tiff_tag_location: 0,
681                count: 1,
682                value_offset: 3857,
683            }],
684            double_params: Vec::new(),
685            ascii_params: String::new(),
686        };
687
688        let wkt = parse_geokeys_to_wkt(Some(&geo_dir));
689        assert!(wkt.is_some());
690        let wkt_str = wkt.unwrap_or_default();
691        assert!(wkt_str.contains("Pseudo-Mercator"));
692        assert!(wkt_str.contains("3857"));
693    }
694
695    #[test]
696    fn test_parse_geokeys_to_wkt_utm_north() {
697        // Create a mock GeoKeyDirectory with EPSG:32632 (UTM Zone 32N)
698        let geo_dir = GeoKeyDirectory {
699            version: 1,
700            key_revision_major: 1,
701            key_revision_minor: 0,
702            entries: vec![GeoKeyEntry {
703                key_id: GeoKey::ProjectedCsType as u16,
704                tiff_tag_location: 0,
705                count: 1,
706                value_offset: 32632,
707            }],
708            double_params: Vec::new(),
709            ascii_params: String::new(),
710        };
711
712        let wkt = parse_geokeys_to_wkt(Some(&geo_dir));
713        assert!(wkt.is_some());
714        let wkt_str = wkt.unwrap_or_default();
715        assert!(wkt_str.contains("UTM zone 32N"));
716        assert!(wkt_str.contains("32632"));
717        assert!(wkt_str.contains("central_meridian"));
718    }
719
720    #[test]
721    fn test_parse_geokeys_to_wkt_utm_south() {
722        // Create a mock GeoKeyDirectory with EPSG:32732 (UTM Zone 32S)
723        let geo_dir = GeoKeyDirectory {
724            version: 1,
725            key_revision_major: 1,
726            key_revision_minor: 0,
727            entries: vec![GeoKeyEntry {
728                key_id: GeoKey::ProjectedCsType as u16,
729                tiff_tag_location: 0,
730                count: 1,
731                value_offset: 32732,
732            }],
733            double_params: Vec::new(),
734            ascii_params: String::new(),
735        };
736
737        let wkt = parse_geokeys_to_wkt(Some(&geo_dir));
738        assert!(wkt.is_some());
739        let wkt_str = wkt.unwrap_or_default();
740        assert!(wkt_str.contains("UTM zone 32S"));
741        assert!(wkt_str.contains("32732"));
742        assert!(wkt_str.contains("false_northing"));
743    }
744
745    #[test]
746    fn test_parse_geokeys_to_wkt_nad83() {
747        // Create a mock GeoKeyDirectory with EPSG:4269 (NAD83)
748        let geo_dir = GeoKeyDirectory {
749            version: 1,
750            key_revision_major: 1,
751            key_revision_minor: 0,
752            entries: vec![GeoKeyEntry {
753                key_id: GeoKey::GeographicType as u16,
754                tiff_tag_location: 0,
755                count: 1,
756                value_offset: 4269,
757            }],
758            double_params: Vec::new(),
759            ascii_params: String::new(),
760        };
761
762        let wkt = parse_geokeys_to_wkt(Some(&geo_dir));
763        assert!(wkt.is_some());
764        let wkt_str = wkt.unwrap_or_default();
765        assert!(wkt_str.contains("NAD83"));
766        assert!(wkt_str.contains("4269"));
767    }
768
769    #[test]
770    fn test_parse_geokeys_to_wkt_unknown_epsg() {
771        // Create a mock GeoKeyDirectory with an unknown EPSG code
772        let geo_dir = GeoKeyDirectory {
773            version: 1,
774            key_revision_major: 1,
775            key_revision_minor: 0,
776            entries: vec![GeoKeyEntry {
777                key_id: GeoKey::ProjectedCsType as u16,
778                tiff_tag_location: 0,
779                count: 1,
780                value_offset: 9999,
781            }],
782            double_params: Vec::new(),
783            ascii_params: String::new(),
784        };
785
786        let wkt = parse_geokeys_to_wkt(Some(&geo_dir));
787        assert!(wkt.is_some());
788        assert_eq!(wkt.unwrap_or_default(), "EPSG:9999");
789    }
790
791    #[test]
792    fn test_parse_photometric_gray_single() {
793        let interp = parse_photometric_interpretation(PhotometricInterpretation::BlackIsZero, 1);
794        assert_eq!(interp.len(), 1);
795        assert_eq!(interp[0], ColorInterpretation::Gray);
796    }
797
798    #[test]
799    fn test_parse_photometric_gray_with_alpha() {
800        let interp = parse_photometric_interpretation(PhotometricInterpretation::WhiteIsZero, 2);
801        assert_eq!(interp.len(), 2);
802        assert_eq!(interp[0], ColorInterpretation::Gray);
803        assert_eq!(interp[1], ColorInterpretation::Alpha);
804    }
805
806    #[test]
807    fn test_parse_photometric_rgb() {
808        let interp = parse_photometric_interpretation(PhotometricInterpretation::Rgb, 3);
809        assert_eq!(interp.len(), 3);
810        assert_eq!(interp[0], ColorInterpretation::Red);
811        assert_eq!(interp[1], ColorInterpretation::Green);
812        assert_eq!(interp[2], ColorInterpretation::Blue);
813    }
814
815    #[test]
816    fn test_parse_photometric_rgba() {
817        let interp = parse_photometric_interpretation(PhotometricInterpretation::Rgb, 4);
818        assert_eq!(interp.len(), 4);
819        assert_eq!(interp[0], ColorInterpretation::Red);
820        assert_eq!(interp[1], ColorInterpretation::Green);
821        assert_eq!(interp[2], ColorInterpretation::Blue);
822        assert_eq!(interp[3], ColorInterpretation::Alpha);
823    }
824
825    #[test]
826    fn test_parse_photometric_palette() {
827        let interp = parse_photometric_interpretation(PhotometricInterpretation::Palette, 1);
828        assert_eq!(interp.len(), 1);
829        assert_eq!(interp[0], ColorInterpretation::PaletteIndex);
830    }
831
832    #[test]
833    fn test_parse_photometric_cmyk() {
834        let interp = parse_photometric_interpretation(PhotometricInterpretation::Cmyk, 4);
835        assert_eq!(interp.len(), 4);
836        assert_eq!(interp[0], ColorInterpretation::Cyan);
837        assert_eq!(interp[1], ColorInterpretation::Magenta);
838        assert_eq!(interp[2], ColorInterpretation::Yellow);
839        assert_eq!(interp[3], ColorInterpretation::Black);
840    }
841
842    #[test]
843    fn test_parse_photometric_ycbcr() {
844        let interp = parse_photometric_interpretation(PhotometricInterpretation::YCbCr, 3);
845        assert_eq!(interp.len(), 3);
846        assert_eq!(interp[0], ColorInterpretation::YCbCrY);
847        assert_eq!(interp[1], ColorInterpretation::YCbCrCb);
848        assert_eq!(interp[2], ColorInterpretation::YCbCrCr);
849    }
850
851    #[test]
852    fn test_parse_photometric_ycbcr_with_alpha() {
853        let interp = parse_photometric_interpretation(PhotometricInterpretation::YCbCr, 4);
854        assert_eq!(interp.len(), 4);
855        assert_eq!(interp[0], ColorInterpretation::YCbCrY);
856        assert_eq!(interp[1], ColorInterpretation::YCbCrCb);
857        assert_eq!(interp[2], ColorInterpretation::YCbCrCr);
858        assert_eq!(interp[3], ColorInterpretation::Alpha);
859    }
860
861    #[test]
862    fn test_parse_photometric_rgb_extra_bands() {
863        let interp = parse_photometric_interpretation(PhotometricInterpretation::Rgb, 6);
864        assert_eq!(interp.len(), 6);
865        assert_eq!(interp[0], ColorInterpretation::Red);
866        assert_eq!(interp[1], ColorInterpretation::Green);
867        assert_eq!(interp[2], ColorInterpretation::Blue);
868        assert_eq!(interp[3], ColorInterpretation::Alpha);
869        assert_eq!(interp[4], ColorInterpretation::Undefined);
870        assert_eq!(interp[5], ColorInterpretation::Undefined);
871    }
872
873    #[test]
874    fn test_parse_photometric_undefined() {
875        // Test with an uncommon photometric interpretation
876        let interp =
877            parse_photometric_interpretation(PhotometricInterpretation::TransparencyMask, 2);
878        assert_eq!(interp.len(), 2);
879        assert_eq!(interp[0], ColorInterpretation::Undefined);
880        assert_eq!(interp[1], ColorInterpretation::Undefined);
881    }
882}