Skip to main content

geotiff_core/
geokeys.rs

1//! GeoKey directory parsing and construction (TIFF tag 34735).
2//!
3//! The GeoKey directory is stored as a TIFF SHORT array with the structure:
4//! - Header: KeyDirectoryVersion, KeyRevision, MinorRevision, NumberOfKeys
5//! - Entries: KeyID, TIFFTagLocation, Count, ValueOffset (repeated)
6//!
7//! GeoKeys reference values either inline (location=0), from the
8//! GeoDoubleParams tag (34736), or from the GeoAsciiParams tag (34737).
9
10// Well-known GeoKey IDs.
11pub const GT_MODEL_TYPE: u16 = 1024;
12pub const GT_RASTER_TYPE: u16 = 1025;
13pub const GEOGRAPHIC_TYPE: u16 = 2048;
14pub const GEOG_CITATION: u16 = 2049;
15pub const GEOG_GEODETIC_DATUM: u16 = 2050;
16pub const GEOG_ANGULAR_UNITS: u16 = 2054;
17pub const PROJECTED_CS_TYPE: u16 = 3072;
18pub const PROJ_CITATION: u16 = 3073;
19pub const PROJECTION: u16 = 3074;
20pub const PROJ_COORD_TRANS: u16 = 3075;
21pub const PROJ_LINEAR_UNITS: u16 = 3076;
22pub const VERTICAL_CS_TYPE: u16 = 4096;
23pub const VERTICAL_DATUM: u16 = 4098;
24pub const VERTICAL_UNITS: u16 = 4099;
25
26/// A parsed GeoKey entry.
27#[derive(Debug, Clone)]
28pub struct GeoKey {
29    pub id: u16,
30    pub value: GeoKeyValue,
31}
32
33/// The value of a GeoKey.
34#[derive(Debug, Clone)]
35pub enum GeoKeyValue {
36    /// Short value stored inline.
37    Short(u16),
38    /// Double value(s) from GeoDoubleParams.
39    Double(Vec<f64>),
40    /// ASCII string from GeoAsciiParams.
41    Ascii(String),
42}
43
44/// Parsed GeoKey directory.
45#[derive(Debug, Clone)]
46pub struct GeoKeyDirectory {
47    pub version: u16,
48    pub major_revision: u16,
49    pub minor_revision: u16,
50    pub keys: Vec<GeoKey>,
51}
52
53impl GeoKeyDirectory {
54    /// Create an empty directory with default version (1.1.0).
55    pub fn new() -> Self {
56        Self {
57            version: 1,
58            major_revision: 1,
59            minor_revision: 0,
60            keys: Vec::new(),
61        }
62    }
63
64    /// Parse the GeoKey directory from the three GeoTIFF tags.
65    ///
66    /// - `directory`: contents of tag 34735 (SHORT array)
67    /// - `double_params`: contents of tag 34736 (DOUBLE array), may be empty
68    /// - `ascii_params`: contents of tag 34737 (ASCII), may be empty
69    pub fn parse(directory: &[u16], double_params: &[f64], ascii_params: &str) -> Option<Self> {
70        if directory.len() < 4 {
71            return None;
72        }
73
74        let version = directory[0];
75        let major_revision = directory[1];
76        let minor_revision = directory[2];
77        let num_keys = directory[3] as usize;
78
79        if directory.len() < 4 + num_keys * 4 {
80            return None;
81        }
82
83        let mut keys = Vec::with_capacity(num_keys);
84        for i in 0..num_keys {
85            let base = 4 + i * 4;
86            let key_id = directory[base];
87            let location = directory[base + 1];
88            let count = directory[base + 2] as usize;
89            let value_offset = directory[base + 3];
90
91            let value = match location {
92                0 => {
93                    // Value is the offset itself (short).
94                    GeoKeyValue::Short(value_offset)
95                }
96                34736 => {
97                    // Value is in GeoDoubleParams.
98                    let start = value_offset as usize;
99                    let end = start + count;
100                    if end <= double_params.len() {
101                        GeoKeyValue::Double(double_params[start..end].to_vec())
102                    } else {
103                        continue;
104                    }
105                }
106                34737 => {
107                    // Value is in GeoAsciiParams.
108                    let start = value_offset as usize;
109                    let end = start + count;
110                    if let Some(raw) = ascii_params.get(start..end) {
111                        let s = raw.trim_end_matches('|').trim_end_matches('\0').to_string();
112                        GeoKeyValue::Ascii(s)
113                    } else {
114                        continue;
115                    }
116                }
117                _ => continue,
118            };
119
120            keys.push(GeoKey { id: key_id, value });
121        }
122
123        Some(Self {
124            version,
125            major_revision,
126            minor_revision,
127            keys,
128        })
129    }
130
131    /// Look up a GeoKey by ID.
132    pub fn get(&self, id: u16) -> Option<&GeoKey> {
133        self.keys.iter().find(|k| k.id == id)
134    }
135
136    /// Get a short value for a key.
137    pub fn get_short(&self, id: u16) -> Option<u16> {
138        self.get(id).and_then(|k| match &k.value {
139            GeoKeyValue::Short(v) => Some(*v),
140            _ => None,
141        })
142    }
143
144    /// Get an ASCII value for a key.
145    pub fn get_ascii(&self, id: u16) -> Option<&str> {
146        self.get(id).and_then(|k| match &k.value {
147            GeoKeyValue::Ascii(s) => Some(s.as_str()),
148            _ => None,
149        })
150    }
151
152    /// Get double value(s) for a key.
153    pub fn get_double(&self, id: u16) -> Option<&[f64]> {
154        self.get(id).and_then(|k| match &k.value {
155            GeoKeyValue::Double(v) => Some(v.as_slice()),
156            _ => None,
157        })
158    }
159
160    /// Insert or replace a GeoKey.
161    pub fn set(&mut self, id: u16, value: GeoKeyValue) {
162        if let Some(existing) = self.keys.iter_mut().find(|k| k.id == id) {
163            existing.value = value;
164        } else {
165            self.keys.push(GeoKey { id, value });
166        }
167    }
168
169    /// Remove a GeoKey by ID.
170    pub fn remove(&mut self, id: u16) {
171        self.keys.retain(|k| k.id != id);
172    }
173
174    /// Serialize the directory into the three TIFF tag payloads.
175    ///
176    /// Returns `(directory_shorts, double_params, ascii_params)`.
177    /// Keys are sorted by ID per spec. Short values go inline (location=0),
178    /// Double values reference the double_params array (location=34736),
179    /// Ascii values reference the ascii_params string (location=34737).
180    pub fn serialize(&self) -> (Vec<u16>, Vec<f64>, String) {
181        let mut sorted_keys = self.keys.clone();
182        sorted_keys.sort_by_key(|k| k.id);
183
184        let mut directory = Vec::new();
185        let mut double_params = Vec::new();
186        let mut ascii_params = String::new();
187
188        // Header: version, major_revision, minor_revision, num_keys
189        directory.push(self.version);
190        directory.push(self.major_revision);
191        directory.push(self.minor_revision);
192        directory.push(sorted_keys.len() as u16);
193
194        for key in &sorted_keys {
195            directory.push(key.id);
196            match &key.value {
197                GeoKeyValue::Short(v) => {
198                    directory.push(0); // location: inline
199                    directory.push(1); // count
200                    directory.push(*v); // value
201                }
202                GeoKeyValue::Double(v) => {
203                    directory.push(34736); // location: GeoDoubleParams
204                    directory.push(v.len() as u16);
205                    directory.push(double_params.len() as u16); // offset
206                    double_params.extend_from_slice(v);
207                }
208                GeoKeyValue::Ascii(s) => {
209                    directory.push(34737); // location: GeoAsciiParams
210                    let ascii_with_pipe = format!("{}|", s);
211                    directory.push(ascii_with_pipe.len() as u16);
212                    directory.push(ascii_params.len() as u16); // offset
213                    ascii_params.push_str(&ascii_with_pipe);
214                }
215            }
216        }
217
218        (directory, double_params, ascii_params)
219    }
220}
221
222impl Default for GeoKeyDirectory {
223    fn default() -> Self {
224        Self::new()
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn parse_roundtrip() {
234        let mut dir = GeoKeyDirectory::new();
235        dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
236        dir.set(GEOGRAPHIC_TYPE, GeoKeyValue::Short(4326));
237        dir.set(GEOG_CITATION, GeoKeyValue::Ascii("WGS 84".into()));
238
239        let (shorts, doubles, ascii) = dir.serialize();
240        let parsed = GeoKeyDirectory::parse(&shorts, &doubles, &ascii).unwrap();
241
242        assert_eq!(parsed.get_short(GT_MODEL_TYPE), Some(2));
243        assert_eq!(parsed.get_short(GEOGRAPHIC_TYPE), Some(4326));
244        assert_eq!(parsed.get_ascii(GEOG_CITATION), Some("WGS 84"));
245    }
246
247    #[test]
248    fn set_replaces_existing() {
249        let mut dir = GeoKeyDirectory::new();
250        dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
251        dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
252        assert_eq!(dir.get_short(GT_MODEL_TYPE), Some(2));
253        assert_eq!(dir.keys.len(), 1);
254    }
255
256    #[test]
257    fn remove_key() {
258        let mut dir = GeoKeyDirectory::new();
259        dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
260        dir.remove(GT_MODEL_TYPE);
261        assert!(dir.get(GT_MODEL_TYPE).is_none());
262    }
263
264    #[test]
265    fn parse_skips_invalid_ascii_subslice_without_panicking() {
266        let directory = [
267            1u16,
268            1,
269            0,
270            1, // header
271            GEOG_CITATION,
272            34737,
273            1,
274            1, // byte offsets that are invalid for lossy UTF-8
275        ];
276        let ascii = String::from_utf8_lossy(&[0xff, b'|']).into_owned();
277
278        let parsed = GeoKeyDirectory::parse(&directory, &[], &ascii).unwrap();
279        assert!(parsed.get_ascii(GEOG_CITATION).is_none());
280    }
281}