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
10use std::error::Error;
11use std::fmt;
12
13// Well-known GeoKey IDs.
14pub const GT_MODEL_TYPE: u16 = 1024;
15pub const GT_RASTER_TYPE: u16 = 1025;
16pub const GT_CITATION: u16 = 1026;
17pub const GEODETIC_CRS_TYPE: u16 = 2048;
18pub const GEOGRAPHIC_TYPE: u16 = GEODETIC_CRS_TYPE;
19pub const GEODETIC_CITATION: u16 = 2049;
20pub const GEOG_CITATION: u16 = 2049;
21pub const GEODETIC_DATUM: u16 = 2050;
22pub const GEOG_GEODETIC_DATUM: u16 = 2050;
23pub const GEOG_ANGULAR_UNITS: u16 = 2054;
24pub const PROJECTED_CRS_TYPE: u16 = 3072;
25pub const PROJECTED_CS_TYPE: u16 = 3072;
26pub const PROJ_CITATION: u16 = 3073;
27pub const PROJECTION: u16 = 3074;
28pub const PROJ_COORD_TRANS: u16 = 3075;
29pub const PROJ_LINEAR_UNITS: u16 = 3076;
30pub const VERTICAL_CITATION: u16 = 4097;
31pub const VERTICAL_CS_TYPE: u16 = 4096;
32pub const VERTICAL_DATUM: u16 = 4098;
33pub const VERTICAL_UNITS: u16 = 4099;
34const GEO_DOUBLE_PARAMS_TAG: u16 = 34736;
35const GEO_ASCII_PARAMS_TAG: u16 = 34737;
36
37/// Error returned when a GeoKey directory cannot be represented in the
38/// GeoTIFF SHORT-based key directory format.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum GeoKeySerializeError {
41    /// The GeoKey directory header stores the key count as a SHORT.
42    TooManyKeys { count: usize },
43    /// A key references more parameter values than fit in a SHORT count.
44    ValueCountTooLarge { key_id: u16, tag: u16, count: usize },
45    /// A key's parameter start offset does not fit in a SHORT value offset.
46    ParameterOffsetTooLarge {
47        key_id: u16,
48        tag: u16,
49        offset: usize,
50    },
51}
52
53impl fmt::Display for GeoKeySerializeError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            Self::TooManyKeys { count } => {
57                write!(
58                    f,
59                    "GeoKey directory contains {count} keys, exceeding u16::MAX"
60                )
61            }
62            Self::ValueCountTooLarge { key_id, tag, count } => write!(
63                f,
64                "GeoKey {key_id} references {count} values in tag {tag}, exceeding u16::MAX"
65            ),
66            Self::ParameterOffsetTooLarge {
67                key_id,
68                tag,
69                offset,
70            } => write!(
71                f,
72                "GeoKey {key_id} parameter offset {offset} in tag {tag} exceeds u16::MAX"
73            ),
74        }
75    }
76}
77
78impl Error for GeoKeySerializeError {}
79
80/// A parsed GeoKey entry.
81#[derive(Debug, Clone)]
82pub struct GeoKey {
83    pub id: u16,
84    pub value: GeoKeyValue,
85}
86
87/// The value of a GeoKey.
88#[derive(Debug, Clone)]
89pub enum GeoKeyValue {
90    /// Short value stored inline.
91    Short(u16),
92    /// Double value(s) from GeoDoubleParams.
93    Double(Vec<f64>),
94    /// ASCII string from GeoAsciiParams.
95    Ascii(String),
96}
97
98/// Parsed GeoKey directory.
99#[derive(Debug, Clone)]
100pub struct GeoKeyDirectory {
101    pub version: u16,
102    pub major_revision: u16,
103    pub minor_revision: u16,
104    pub keys: Vec<GeoKey>,
105}
106
107impl GeoKeyDirectory {
108    /// Create an empty directory with default version (1.1.0).
109    pub fn new() -> Self {
110        Self {
111            version: 1,
112            major_revision: 1,
113            minor_revision: 0,
114            keys: Vec::new(),
115        }
116    }
117
118    /// Parse the GeoKey directory from the three GeoTIFF tags.
119    ///
120    /// - `directory`: contents of tag 34735 (SHORT array)
121    /// - `double_params`: contents of tag 34736 (DOUBLE array), may be empty
122    /// - `ascii_params`: contents of tag 34737 (ASCII), may be empty
123    pub fn parse(directory: &[u16], double_params: &[f64], ascii_params: &str) -> Option<Self> {
124        if directory.len() < 4 {
125            return None;
126        }
127
128        let version = directory[0];
129        let major_revision = directory[1];
130        let minor_revision = directory[2];
131        let num_keys = directory[3] as usize;
132
133        if directory.len() < 4 + num_keys * 4 {
134            return None;
135        }
136
137        let mut keys = Vec::with_capacity(num_keys);
138        for i in 0..num_keys {
139            let base = 4 + i * 4;
140            let key_id = directory[base];
141            let location = directory[base + 1];
142            let count = directory[base + 2] as usize;
143            let value_offset = directory[base + 3];
144
145            let value = match location {
146                0 => {
147                    // Value is the offset itself (short).
148                    GeoKeyValue::Short(value_offset)
149                }
150                34736 => {
151                    // Value is in GeoDoubleParams.
152                    let start = value_offset as usize;
153                    let end = start + count;
154                    if end <= double_params.len() {
155                        GeoKeyValue::Double(double_params[start..end].to_vec())
156                    } else {
157                        continue;
158                    }
159                }
160                34737 => {
161                    // Value is in GeoAsciiParams.
162                    let start = value_offset as usize;
163                    let end = start + count;
164                    if let Some(raw) = ascii_params.get(start..end) {
165                        let s = raw.trim_end_matches('|').trim_end_matches('\0').to_string();
166                        GeoKeyValue::Ascii(s)
167                    } else {
168                        continue;
169                    }
170                }
171                _ => continue,
172            };
173
174            keys.push(GeoKey { id: key_id, value });
175        }
176
177        Some(Self {
178            version,
179            major_revision,
180            minor_revision,
181            keys,
182        })
183    }
184
185    /// Look up a GeoKey by ID.
186    pub fn get(&self, id: u16) -> Option<&GeoKey> {
187        self.keys.iter().find(|k| k.id == id)
188    }
189
190    /// Get a short value for a key.
191    pub fn get_short(&self, id: u16) -> Option<u16> {
192        self.get(id).and_then(|k| match &k.value {
193            GeoKeyValue::Short(v) => Some(*v),
194            _ => None,
195        })
196    }
197
198    /// Get an ASCII value for a key.
199    pub fn get_ascii(&self, id: u16) -> Option<&str> {
200        self.get(id).and_then(|k| match &k.value {
201            GeoKeyValue::Ascii(s) => Some(s.as_str()),
202            _ => None,
203        })
204    }
205
206    /// Get double value(s) for a key.
207    pub fn get_double(&self, id: u16) -> Option<&[f64]> {
208        self.get(id).and_then(|k| match &k.value {
209            GeoKeyValue::Double(v) => Some(v.as_slice()),
210            _ => None,
211        })
212    }
213
214    /// Insert or replace a GeoKey.
215    pub fn set(&mut self, id: u16, value: GeoKeyValue) {
216        if let Some(existing) = self.keys.iter_mut().find(|k| k.id == id) {
217            existing.value = value;
218        } else {
219            self.keys.push(GeoKey { id, value });
220        }
221    }
222
223    /// Remove a GeoKey by ID.
224    pub fn remove(&mut self, id: u16) {
225        self.keys.retain(|k| k.id != id);
226    }
227
228    /// Serialize the directory into the three TIFF tag payloads.
229    ///
230    /// Returns `(directory_shorts, double_params, ascii_params)`.
231    /// Keys are sorted by ID per spec. Short values go inline (location=0),
232    /// Double values reference the double_params array (location=34736),
233    /// Ascii values reference the ascii_params string (location=34737).
234    pub fn serialize(&self) -> Result<(Vec<u16>, Vec<f64>, String), GeoKeySerializeError> {
235        let mut sorted_keys = self.keys.clone();
236        sorted_keys.sort_by_key(|k| k.id);
237        let key_count =
238            u16::try_from(sorted_keys.len()).map_err(|_| GeoKeySerializeError::TooManyKeys {
239                count: sorted_keys.len(),
240            })?;
241
242        let mut directory = Vec::new();
243        let mut double_params = Vec::new();
244        let mut ascii_params = String::new();
245
246        // Header: version, major_revision, minor_revision, num_keys
247        directory.push(self.version);
248        directory.push(self.major_revision);
249        directory.push(self.minor_revision);
250        directory.push(key_count);
251
252        for key in &sorted_keys {
253            directory.push(key.id);
254            match &key.value {
255                GeoKeyValue::Short(v) => {
256                    directory.push(0); // location: inline
257                    directory.push(1); // count
258                    directory.push(*v); // value
259                }
260                GeoKeyValue::Double(v) => {
261                    let count = checked_u16_len(key.id, GEO_DOUBLE_PARAMS_TAG, v.len())?;
262                    let offset =
263                        checked_u16_offset(key.id, GEO_DOUBLE_PARAMS_TAG, double_params.len())?;
264                    directory.push(GEO_DOUBLE_PARAMS_TAG); // location: GeoDoubleParams
265                    directory.push(count);
266                    directory.push(offset);
267                    double_params.extend_from_slice(v);
268                }
269                GeoKeyValue::Ascii(s) => {
270                    let ascii_with_pipe = format!("{}|", s);
271                    let count =
272                        checked_u16_len(key.id, GEO_ASCII_PARAMS_TAG, ascii_with_pipe.len())?;
273                    let offset =
274                        checked_u16_offset(key.id, GEO_ASCII_PARAMS_TAG, ascii_params.len())?;
275                    directory.push(GEO_ASCII_PARAMS_TAG); // location: GeoAsciiParams
276                    directory.push(count);
277                    directory.push(offset);
278                    ascii_params.push_str(&ascii_with_pipe);
279                }
280            }
281        }
282
283        Ok((directory, double_params, ascii_params))
284    }
285}
286
287fn checked_u16_len(key_id: u16, tag: u16, count: usize) -> Result<u16, GeoKeySerializeError> {
288    u16::try_from(count).map_err(|_| GeoKeySerializeError::ValueCountTooLarge {
289        key_id,
290        tag,
291        count,
292    })
293}
294
295fn checked_u16_offset(key_id: u16, tag: u16, offset: usize) -> Result<u16, GeoKeySerializeError> {
296    u16::try_from(offset).map_err(|_| GeoKeySerializeError::ParameterOffsetTooLarge {
297        key_id,
298        tag,
299        offset,
300    })
301}
302
303impl Default for GeoKeyDirectory {
304    fn default() -> Self {
305        Self::new()
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn parse_roundtrip() {
315        let mut dir = GeoKeyDirectory::new();
316        dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
317        dir.set(GEOGRAPHIC_TYPE, GeoKeyValue::Short(4326));
318        dir.set(GEOG_CITATION, GeoKeyValue::Ascii("WGS 84".into()));
319
320        let (shorts, doubles, ascii) = dir.serialize().unwrap();
321        let parsed = GeoKeyDirectory::parse(&shorts, &doubles, &ascii).unwrap();
322
323        assert_eq!(parsed.get_short(GT_MODEL_TYPE), Some(2));
324        assert_eq!(parsed.get_short(GEOGRAPHIC_TYPE), Some(4326));
325        assert_eq!(parsed.get_ascii(GEOG_CITATION), Some("WGS 84"));
326    }
327
328    #[test]
329    fn set_replaces_existing() {
330        let mut dir = GeoKeyDirectory::new();
331        dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
332        dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
333        assert_eq!(dir.get_short(GT_MODEL_TYPE), Some(2));
334        assert_eq!(dir.keys.len(), 1);
335    }
336
337    #[test]
338    fn remove_key() {
339        let mut dir = GeoKeyDirectory::new();
340        dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
341        dir.remove(GT_MODEL_TYPE);
342        assert!(dir.get(GT_MODEL_TYPE).is_none());
343    }
344
345    #[test]
346    fn parse_skips_invalid_ascii_subslice_without_panicking() {
347        let directory = [
348            1u16,
349            1,
350            0,
351            1, // header
352            GEOG_CITATION,
353            34737,
354            1,
355            1, // byte offsets that are invalid for lossy UTF-8
356        ];
357        let ascii = String::from_utf8_lossy(&[0xff, b'|']).into_owned();
358
359        let parsed = GeoKeyDirectory::parse(&directory, &[], &ascii).unwrap();
360        assert!(parsed.get_ascii(GEOG_CITATION).is_none());
361    }
362
363    #[test]
364    fn serialize_rejects_too_many_keys() {
365        let mut dir = GeoKeyDirectory::new();
366        dir.keys = (0..=u16::MAX as usize)
367            .map(|index| GeoKey {
368                id: index as u16,
369                value: GeoKeyValue::Short(1),
370            })
371            .collect();
372
373        let err = dir.serialize().unwrap_err();
374        assert_eq!(
375            err,
376            GeoKeySerializeError::TooManyKeys {
377                count: u16::MAX as usize + 1
378            }
379        );
380    }
381
382    #[test]
383    fn serialize_rejects_oversized_double_value_count() {
384        let mut dir = GeoKeyDirectory::new();
385        dir.set(
386            GT_CITATION,
387            GeoKeyValue::Double(vec![1.0; u16::MAX as usize + 1]),
388        );
389
390        let err = dir.serialize().unwrap_err();
391        assert_eq!(
392            err,
393            GeoKeySerializeError::ValueCountTooLarge {
394                key_id: GT_CITATION,
395                tag: GEO_DOUBLE_PARAMS_TAG,
396                count: u16::MAX as usize + 1
397            }
398        );
399    }
400
401    #[test]
402    fn serialize_rejects_oversized_ascii_parameter_offset() {
403        let mut dir = GeoKeyDirectory::new();
404        dir.set(
405            GEOG_CITATION,
406            GeoKeyValue::Ascii("a".repeat(u16::MAX as usize - 1)),
407        );
408        dir.set(PROJ_CITATION, GeoKeyValue::Ascii("b".to_string()));
409        dir.set(VERTICAL_CITATION, GeoKeyValue::Ascii("c".to_string()));
410
411        let err = dir.serialize().unwrap_err();
412        assert_eq!(
413            err,
414            GeoKeySerializeError::ParameterOffsetTooLarge {
415                key_id: VERTICAL_CITATION,
416                tag: GEO_ASCII_PARAMS_TAG,
417                offset: u16::MAX as usize + 2
418            }
419        );
420    }
421}