Repository: DigitalSlideArchive/tifftools
Pull request #103: Convenience functions to set projection and GCPs
Author: login: annehaley
Created: 2025-04-09T19:42:16Z | Merged: 2025-04-18T12:44:03Z

This PR creates the functions `set_projection` and `set_gcps` in `commands.py`. These functions can be called directly, invoked through `tiff_set`, or invoked via the command line. Resolves #101.

Note: `pyproj` is required for the `set_projection` function. Running `pip install tifftools[geo]` will install `pyproj`.


Usage examples:
```
tifftools.commands.set_gcps(output_path, gcps, overwrite=True)
tifftools.commands.set_projection(output_path, projection, overwrite=True)
```
```
tifftools.tiff_set(str(output_path), overwrite=True, setlist=[
    ('projection', projection),
    ('gcps', gcps),
])
```
```
subprocess.run([
    'tifftools', 'set', str(output_path), '--overwrite',
    '--set', 'projection', projection,
    '--set', 'gcps', ' '.join(' '.join(str(g) for g in gcp) for gcp in gcps)
])
```


Example values for projection and gcps:
```
projection = 'epsg:4326`
gcps = [(-77.05, 38.88, 0, 0), (-77.04, 38.89, 100, 100)]
```
```
projection =  '+proj=aea +lat_0=23 +lon_0=-96 +lat_1=29.5 +lat_2=45.5 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'
gcps = [(1979142.78, 2368597.47, 0, 0),  (2055086.35, 2449556.39, 100, 100)]
```

Linked issue #101: Add convenience function for writing geospatial tags
It would be nice to include a function that allows a user to add a projection and ground control points to tiff tags. This would make writing geospatial images much easier and more accessible. I found that https://github.com/KipCrossing/geotiff/issues/25 discusses the possibility of leveraging this functionality when it is added. We would also use this function in [`large_image`](https://github.com/girder/large_image) to write geospatial images with the Zarr sink.

Diff:
diff --git a/pyproject.toml b/pyproject.toml
index cfd7f41..39ab7f8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,6 +28,9 @@ requires-python = ">=3.8"
 [project.scripts]
 tifftools = "tifftools:main"
 
+[project.optional-dependencies]
+geo = ['pyproj']
+
 [tool.setuptools]
 packages = { find = { exclude = ["tests*"] } } 
 
diff --git a/tests/test_set.py b/tests/test_set.py
index 751f30a..c03a3ad 100644
--- a/tests/test_set.py
+++ b/tests/test_set.py
@@ -138,3 +138,98 @@ def test_tiff_set_setfrom_missing(tmp_path, caplog):
     with caplog.at_level(logging.WARNING):
         tifftools.tiff_set(str(path) + ',1', dest, setfrom=[('InkNames', path)])
     assert 'is not in' in caplog.text
+
+
+def test_tiff_set_projection_and_gcps_with_pyproj(tmp_path):
+    expect_import_error = False
+    try:
+        import pyproj  # noqa: F401
+    except ImportError:
+        expect_import_error = True
+
+    path = datastore.fetch('d043-200.tif')
+    dest = tmp_path / 'results.tif'
+    projection = (
+        '+proj=aea +lat_0=23 +lon_0=-96 +lat_1=29.5 +lat_2=45.5'
+        ' +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'
+    )
+    gcps = [
+        (1979142.78, 2368597.47, 0, 0),
+        (2055086.35, 2449556.39, 100, 100),
+    ]
+    if expect_import_error:
+        with pytest.raises(tifftools.TifftoolsError):
+            tifftools.tiff_set(str(path), dest, setlist=[
+                ('projection', projection),
+                ('gcps', gcps),
+            ])
+    else:
+        tifftools.tiff_set(str(path), dest, setlist=[
+            ('projection', projection),
+            ('gcps', gcps),
+        ])
+        info = tifftools.read_tiff(str(dest))
+        assert info['ifds'][0]['tags'][int(tifftools.Tag.GeoKeyDirectoryTag)]['data'] == [
+            1, 1, 0, 13, 1024, 0, 1, 1,
+            1025, 0, 1, 1, 2049, 34737, 1, 0,
+            2054, 0, 1, 9102, 2057, 34736, 1, 0,
+            2059, 34736, 1, 1, 3078, 34736, 1, 2,
+            3079, 34736, 1, 3, 3081, 34736, 1, 4,
+            3080, 34736, 1, 5, 1026, 34737, 1, 1,
+            3075, 0, 1, 11, 3076, 0, 1, 9001,
+        ]
+        assert info['ifds'][0]['tags'][int(tifftools.Tag.GeoDoubleParamsTag)]['data'] == [
+            6378137.0, 298.257223563, 29.5, 45.5, 23.0, -96.0,
+        ]
+        assert info['ifds'][0]['tags'][int(tifftools.Tag.GeoAsciiParamsTag)
+                                       ]['data'] == 'WGS84|Albers Equal Area'
+        assert info['ifds'][0]['tags'][int(tifftools.Tag.ModelTiePointTag)]['data'] == [
+            0.0, 0.0, 0.0, 1979142.78, 2368597.47, 0.0,
+            100.0, 100.0, 0.0, 2055086.35, 2449556.39, 0.0,
+        ]
+
+
+@pytest.mark.parametrize('method', ['direct', 'tiff_set', 'main'])
+def test_tiff_set_projection_and_gcps_without_pyproj(tmp_path, method):
+    path = datastore.fetch('d043-200.tif')
+    dest = tmp_path / 'results.tif'
+    projection = 'epsg:4326'
+    gcps = [(-77.05, 38.88, 0, 0), (-77.04, 38.89, 100, 100)]
+    if method == 'direct':
+        tifftools.commands.set_gcps(str(path), gcps, output=str(dest), overwrite=True)
+        tifftools.commands.set_projection(str(path), projection, output=str(dest), overwrite=True)
+    elif method == 'tiff_set':
+        tifftools.tiff_set(str(path), dest, setlist=[
+            ('projection', projection),
+            ('gcps', gcps),
+        ])
+    elif method == 'main':
+        tifftools.main([
+            'set', str(path), str(dest),
+            '--set', 'projection', projection,
+            '--set', 'gcps', ' '.join(' '.join(str(g) for g in gcp) for gcp in gcps),
+        ])
+    info = tifftools.read_tiff(str(dest))
+    assert info['ifds'][0]['tags'][int(tifftools.Tag.GeoKeyDirectoryTag)]['data'] == [
+        1, 1, 0, 3, 1024, 0, 1, 2, 1025, 0, 1, 1, 2048, 0, 1, 4326,
+    ]
+
+
+def test_tiff_set_projection_edge_case(tmp_path):
+    try:
+        path = datastore.fetch('d043-200.tif')
+        result = tifftools.commands._set_projection(
+            path, '+proj=longlat +axis=esu',
+        )
+        assert result == [
+            (
+                'GeoKeyDirectoryTag',
+                ('1 1 0 6 1024 0 1 2 1025 0 1 1 2049 34737 1 0 '
+                 '2054 0 1 9102 2057 34736 1 0 2059 34736 1 1'),
+            ),
+            ('GeoDoubleParamsTag', '6378137.0 298.257223563'),
+            ('GeoAsciiParamsTag', 'WGS84'),
+        ]
+    except tifftools.TifftoolsError:
+        # pyproj not installed
+        pass
diff --git a/tifftools/commands.py b/tifftools/commands.py
index c60d21d..fa63ebb 100644
--- a/tifftools/commands.py
+++ b/tifftools/commands.py
@@ -10,7 +10,8 @@
 import sys
 import tempfile
 
-from .constants import Datatype, Tag, get_or_create_tag
+from .constants import (Datatype, EPSGTypes, GeoTiffAngularUnits, GeoTiffGeoKey, GeoTiffLinearUnits,
+                        GeoTiffTransformations, Tag, get_or_create_tag)
 from .exceptions import TifftoolsError
 from .tifftools import read_tiff, read_tiff_limit_ifds, write_tiff
 
@@ -512,7 +513,18 @@ def _tiff_set(source, output=None, setlist=None, unset=None, setfrom=None,
                 logger.info('Tag %s is not present', tag)
             ifd['tags'].pop(int(tag), None)
     if setlist is not None:
+        full_setlist = []
         for tagspec, value in setlist:
+            if tagspec.lower() == 'projection':
+                full_setlist += _set_projection(source, value,
+                                                output=output, overwrite=overwrite, **kwargs)
+            elif tagspec.lower() == 'gcps':
+                full_setlist += _set_gcps(source, value, output=output,
+                                          overwrite=overwrite, **kwargs)
+            else:
+                full_setlist.append((tagspec, value))
+
+        for tagspec, value in full_setlist:
             tag, datatype, ifd, data = _tagspec_to_ifd(tagspec, info, value)
             if data is not None:
                 ifd['tags'][int(tag)] = {
@@ -572,6 +584,163 @@ def tiff_set(source, output=None, overwrite=False, setlist=None, unset=None,
         _tiff_set(source, output, setlist, unset, setfrom, overwrite=overwrite, **kwargs)
 
 
+def _set_projection(source, projection, **kwargs):
+    """
+    Set geospatial projection in a tiff file.
+
+    :param source: the source path.
+    :param projection: the projection, expressed as an integer representing an EPSG code or
+        expressed as a string that can be interpreted by `pyproj.CRS.from_string()`.
+    :param kwargs: additional arguments to pass to `tifftools.commands.tiff_set()`.
+    """
+    geokeys = None
+    if isinstance(projection, str) and 'epsg:' in projection.lower():
+        projection = int(projection.lower().replace('epsg:', ''))
+    if isinstance(projection, int):
+        for geotype, codes in EPSGTypes.items():
+            if projection in codes:
+                geokeys = dict(
+                    GTModelType=(
+                        3 if geotype == 'geocentric' else
+                        2 if geotype == 'geographic_2d' else 1
+                    ),
+                    GTRasterType=1,
+                    GeographicType=projection,
+                )
+
+    if geokeys is None:
+        try:
+            import pyproj
+        except ImportError:
+            msg = (
+                f'pyproj is required to interpret projection {projection}. '
+                'Please install tifftools[geo].'
+            )
+            raise TifftoolsError(msg)
+
+        proj = pyproj.CRS.from_string(projection)
+        proj_dict = proj.to_dict()
+        angular_units = GeoTiffAngularUnits.get(proj.prime_meridian.unit_name).value
+        geokeys = dict(
+            GTModelType=3 if proj.is_geocentric else 2 if proj.is_geographic else 1,
+            GTRasterType=1,
+            GeographicType=proj.to_epsg(),
+            GeogCitation=proj_dict.get('datum'),
+            GeogAngularUnits=angular_units,
+            GeogSemiMajorAxis=proj.ellipsoid.semi_major_metre,
+            GeogInvFlattening=proj.ellipsoid.inverse_flattening,
+            ProjStdParallel1=proj_dict.get('lat_1'),
+            ProjStdParallel2=proj_dict.get('lat_2'),
+            ProjNatOriginLat=proj_dict.get('lat_0'),
+            ProjNatOriginLong=proj_dict.get('lon_0'),
+        )
+        if proj.coordinate_operation:
+            method_name = proj.coordinate_operation.method_name
+            transformation = GeoTiffTransformations.get(method_name.replace(' ', ''))
+            geokeys['GTCitation'] = method_name
+            geokeys['ProjCoordTrans'] = transformation
+        if proj_dict.get('units'):
+            linear_units = GeoTiffLinearUnits.get(proj_dict.get('units')).value
+            geokeys['ProjLinearUnits'] = linear_units
+
+    geokeys = {k: v for k, v in geokeys.items() if v is not None}
+
+    key_directory_version = 1
+    key_revision = 1
+    minor_revision = 0
+    number_of_keys = len(geokeys)
+    header = ' '.join(str(v) for v in [
+        key_directory_version,
+        key_revision,
+        minor_revision,
+        number_of_keys,
+    ])
+
+    geokey_tag = f'{header}'
+    doubles = []
+    asciis = []
+    for geokey, value in geokeys.items():
+        key_spec = GeoTiffGeoKey.get(geokey)
+        key_id = key_spec.value
+        data_type = key_spec.datatype
+        value_count = 1
+        if data_type == Datatype.SHORT:
+            tag_location = 0
+            value_offset = int(value)
+        elif data_type == Datatype.DOUBLE:
+            tag_location = Tag.get('GeoDoubleParamsTag').value
+            value_offset = len(doubles)
+            doubles.append(float(value))
+        else:
+            tag_location = Tag.get('GeoAsciiParamsTag').value
+            value_offset = len(asciis)
+            asciis.append(str(value))
+        geokey_tag += f' {key_id} {tag_location} {value_count} {value_offset}'
+    doubles_tag = ' '.join(str(v) for v in doubles)
+    asciis_tag = '|'.join(asciis)
+
+    return [
+        ('GeoKeyDirectoryTag', geokey_tag),
+        ('GeoDoubleParamsTag', doubles_tag),
+        ('GeoAsciiParamsTag', asciis_tag),
+    ]
+
+
+def set_projection(source, projection, **kwargs):
+    """
+    Set geospatial projection in a tiff file.
+
+    :param source: the source path.
+    :param projection: the projection, expressed as an integer representing an EPSG code or
+        expressed as a string that can be interpreted by `pyproj.CRS.from_string()`.
+    :param kwargs: additional arguments to pass to `tifftools.commands.tiff_set()`.
+    """
+    tiff_set(
+        source,
+        setlist=_set_projection(source, projection, **kwargs),
+        **kwargs,
+    )
+
+
+def _set_gcps(source, gcps, **kwargs):
+    """
+    Set geospatial Ground Control Points (GCPs) in a tiff file.
+
+    :param source: the source path.
+    :param gcps: a list of tuples of the form (cx, cy, px, py),
+        where cx and cy represent a coordinate in projection space
+        and px and py represent a pixel position.
+        This argument may also be passed a space-separated string of
+        these values as a flat list.
+    :param kwargs: additional arguments to pass to `tifftools.commands.tiff_set()`.
+    """
+    if isinstance(gcps, str):
+        gcps = list(zip(*[iter(gcps.split())] * 4))
+    tiepoint_tag = ' '.join(
+        f'{px} {py} 0 {cx} {cy} 0' for cx, cy, px, py in gcps
+    )
+    return [('ModelTiepointTag', tiepoint_tag)]
+
+
+def set_gcps(source, gcps, **kwargs):
+    """
+    Set geospatial Ground Control Points (GCPs) in a tiff file.
+
+    :param source: the source path.
+    :param gcps: a list of tuples of the form (cx, cy, px, py),
+        where cx and cy represent a coordinate in projection space
+        and px and py represent a pixel position.
+        This argument may also be passed a space-separated string of
+        these values as a flat list.
+    :param kwargs: additional arguments to pass to `tifftools.commands.tiff_set()`.
+    """
+    tiff_set(
+        source,
+        setlist=_set_gcps(source, gcps, **kwargs),
+        **kwargs,
+    )
+
+
 def main(args=None):
     from . import __version__
 
diff --git a/tifftools/constants.py b/tifftools/constants.py
index 455b8f2..a4e111b 100644
--- a/tifftools/constants.py
+++ b/tifftools/constants.py
@@ -505,29 +505,29 @@ def get_or_create_tag(key, tagSet=None, upperLimit=True, **tagOptions):
 
 # These aren't tiff tags; these are GeoTIFF GeoKey values.
 GeoTiffGeoKey = TiffConstantSet(TiffTag, {
-    1024: {'name': 'GTModelType', 'altnames': {'GTModelTypeGeoKey'}, 'datatype': Datatype.DOUBLE},
-    1025: {'name': 'GTRasterType', 'altnames': {'GTRasterTypeGeoKey'}, 'datatype': Datatype.DOUBLE},
+    1024: {'name': 'GTModelType', 'altnames': {'GTModelTypeGeoKey'}, 'datatype': Datatype.SHORT},
+    1025: {'name': 'GTRasterType', 'altnames': {'GTRasterTypeGeoKey'}, 'datatype': Datatype.SHORT},
     1026: {'name': 'GTCitation', 'altnames': {'GTCitationGeoKey'}, 'datatype': Datatype.ASCII},
-    2048: {'name': 'GeographicType', 'altnames': {'GeographicTypeGeoKey'}, 'datatype': Datatype.DOUBLE},
+    2048: {'name': 'GeographicType', 'altnames': {'GeographicTypeGeoKey'}, 'datatype': Datatype.SHORT},
     2049: {'name': 'GeogCitation', 'altnames': {'GeogCitationGeoKey'}, 'datatype': Datatype.ASCII},
-    2050: {'name': 'GeogGeodeticDatum', 'altnames': {'GeogGeodeticDatumGeoKey'}, 'datatype': Datatype.DOUBLE},
-    2051: {'name': 'GeogPrimeMeridian', 'altnames': {'GeogPrimeMeridianGeoKey'}, 'datatype': Datatype.DOUBLE},
-    2052: {'name': 'GeogLinearUnits', 'altnames': {'GeogLinearUnitsGeoKey'}, 'datatype': Datatype.DOUBLE},
+    2050: {'name': 'GeogGeodeticDatum', 'altnames': {'GeogGeodeticDatumGeoKey'}, 'datatype': Datatype.SHORT},
+    2051: {'name': 'GeogPrimeMeridian', 'altnames': {'GeogPrimeMeridianGeoKey'}, 'datatype': Datatype.SHORT},
+    2052: {'name': 'GeogLinearUnits', 'altnames': {'GeogLinearUnitsGeoKey'}, 'datatype': Datatype.SHORT},
     2053: {'name': 'GeogLinearUnitSize', 'altnames': {'GeogLinearUnitSizeGeoKey'}, 'datatype': Datatype.DOUBLE},
-    2054: {'name': 'GeogAngularUnits', 'altnames': {'GeogAngularUnitsGeoKey'}, 'datatype': Datatype.DOUBLE},
+    2054: {'name': 'GeogAngularUnits', 'altnames': {'GeogAngularUnitsGeoKey'}, 'datatype': Datatype.SHORT},
     2055: {'name': 'GeogAngularUnitSize', 'altnames': {'GeogAngularUnitSizeGeoKey'}, 'datatype': Datatype.DOUBLE},
-    2056: {'name': 'GeogEllipsoid', 'altnames': {'GeogEllipsoidGeoKey'}, 'datatype': Datatype.DOUBLE},
+    2056: {'name': 'GeogEllipsoid', 'altnames': {'GeogEllipsoidGeoKey'}, 'datatype': Datatype.SHORT},
     2057: {'name': 'GeogSemiMajorAxis', 'altnames': {'GeogSemiMajorAxisGeoKey'}, 'datatype': Datatype.DOUBLE},
     2058: {'name': 'GeogSemiMinorAxis', 'altnames': {'GeogSemiMinorAxisGeoKey'}, 'datatype': Datatype.DOUBLE},
     2059: {'name': 'GeogInvFlattening', 'altnames': {'GeogInvFlatteningGeoKey'}, 'datatype': Datatype.DOUBLE},
-    2060: {'name': 'GeogAzimuthUnits', 'altnames': {'GeogAzimuthUnitsGeoKey'}, 'datatype': Datatype.DOUBLE},
+    2060: {'name': 'GeogAzimuthUnits', 'altnames': {'GeogAzimuthUnitsGeoKey'}, 'datatype': Datatype.SHORT},
     2061: {'name': 'GeogPrimeMeridianLong', 'altnames': {'GeogPrimeMeridianLongGeoKey'}, 'datatype': Datatype.DOUBLE},
     2062: {'name': 'GeogTOWGS84', 'altnames': {'GeogTOWGS84GeoKey'}, 'datatype': Datatype.DOUBLE},
-    3072: {'name': 'ProjectedCSType', 'altnames': {'ProjectedCSTypeGeoKey'}, 'datatype': Datatype.DOUBLE},
+    3072: {'name': 'ProjectedCSType', 'altnames': {'ProjectedCSTypeGeoKey'}, 'datatype': Datatype.SHORT},
     3073: {'name': 'PCSCitation', 'altnames': {'PCSCitationGeoKey'}, 'datatype': Datatype.ASCII},
-    3074: {'name': 'Projection', 'altnames': {'ProjectionGeoKey'}, 'datatype': Datatype.DOUBLE},
-    3075: {'name': 'ProjCoordTrans', 'altnames': {'ProjCoordTransGeoKey'}, 'datatype': Datatype.DOUBLE},
-    3076: {'name': 'ProjLinearUnits', 'altnames': {'ProjLinearUnitsGeoKey'}, 'datatype': Datatype.DOUBLE},
+    3074: {'name': 'Projection', 'altnames': {'ProjectionGeoKey'}, 'datatype': Datatype.SHORT},
+    3075: {'name': 'ProjCoordTrans', 'altnames': {'ProjCoordTransGeoKey'}, 'datatype': Datatype.SHORT},
+    3076: {'name': 'ProjLinearUnits', 'altnames': {'ProjLinearUnitsGeoKey'}, 'datatype': Datatype.SHORT},
     3077: {'name': 'ProjLinearUnitSize', 'altnames': {'ProjLinearUnitSizeGeoKey'}, 'datatype': Datatype.DOUBLE},
     3078: {'name': 'ProjStdParallel1', 'altnames': {'ProjStdParallel1GeoKey', 'ProjStdParallel', 'ProjStdParallelGeoKey'}, 'datatype': Datatype.DOUBLE},
     3079: {'name': 'ProjStdParallel2', 'altnames': {'ProjStdParallel2GeoKey'}, 'datatype': Datatype.DOUBLE},
@@ -548,14 +548,256 @@ def get_or_create_tag(key, tagSet=None, upperLimit=True, **tagOptions):
     3094: {'name': 'ProjAzimuthAngle', 'altnames': {'ProjAzimuthAngleGeoKey'}, 'datatype': Datatype.DOUBLE},
     3095: {'name': 'ProjStraightVertPoleLong', 'altnames': {'ProjStraightVertPoleLongGeoKey'}, 'datatype': Datatype.DOUBLE},
     3096: {'name': 'ProjRectifiedGridAngle', 'altnames': {'ProjRectifiedGridAngleGeoKey'}, 'datatype': Datatype.DOUBLE},
-    4096: {'name': 'VerticalCSType', 'altnames': {'VerticalCSTypeGeoKey'}, 'datatype': Datatype.DOUBLE},
+    4096: {'name': 'VerticalCSType', 'altnames': {'VerticalCSTypeGeoKey'}, 'datatype': Datatype.SHORT},
     4097: {'name': 'VerticalCitation', 'altnames': {'VerticalCitationGeoKey'}, 'datatype': Datatype.ASCII},
-    4098: {'name': 'VerticalDatum', 'altnames': {'VerticalDatumGeoKey'}, 'datatype': Datatype.DOUBLE},
-    4099: {'name': 'VerticalUnits', 'altnames': {'VerticalUnitsGeoKey'}, 'datatype': Datatype.DOUBLE},
+    4098: {'name': 'VerticalDatum', 'altnames': {'VerticalDatumGeoKey'}, 'datatype': Datatype.SHORT},
+    4099: {'name': 'VerticalUnits', 'altnames': {'VerticalUnitsGeoKey'}, 'datatype': Datatype.SHORT},
     5120: {'name': 'CoordinateEpoch', 'altnames': {'CoordinateEpochGeoKey'}, 'datatype': Datatype.DOUBLE},
 })
 
 
+GeoTiffCSTypes = TiffConstantSet('GeoTiffCSTypes', {
+    4201: {'name': 'GCS_Adindan', 'altnames': {'Adindan'}},
+    4202: {'name': 'GCS_AGD66', 'altnames': {'AGD66'}},
+    4203: {'name': 'GCS_AGD84', 'altnames': {'AGD84'}},
+    4204: {'name': 'GCS_Ain_el_Abd', 'altnames': {'Ain_el_Abd'}},
+    4205: {'name': 'GCS_Afgooye', 'altnames': {'Afgooye'}},
+    4206: {'name': 'GCS_Agadez', 'altnames': {'Agadez'}},
+    4207: {'name': 'GCS_Lisbon', 'altnames': {'Lisbon'}},
+    4208: {'name': 'GCS_Aratu', 'altnames': {'Aratu'}},
+    4209: {'name': 'GCS_Arc_1950', 'altnames': {'Arc_1950'}},
+    4210: {'name': 'GCS_Arc_1960', 'altnames': {'Arc_1960'}},
+    4211: {'name': 'GCS_Batavia', 'altnames': {'Batavia'}},
+    4212: {'name': 'GCS_Barbados', 'altnames': {'Barbados'}},
+    4213: {'name': 'GCS_Beduaram', 'altnames': {'Beduaram'}},
+    4214: {'name': 'GCS_Beijing_1954', 'altnames': {'Beijing_1954'}},
+    4215: {'name': 'GCS_Belge_1950', 'altnames': {'Belge_1950'}},
+    4216: {'name': 'GCS_Bermuda_1957', 'altnames': {'Bermuda_1957'}},
+    4217: {'name': 'GCS_Bern_1898', 'altnames': {'Bern_1898'}},
+    4218: {'name': 'GCS_Bogota', 'altnames': {'Bogota'}},
+    4219: {'name': 'GCS_Bukit_Rimpah', 'altnames': {'Bukit_Rimpah'}},
+    4220: {'name': 'GCS_Camacupa', 'altnames': {'Camacupa'}},
+    4221: {'name': 'GCS_Campo_Inchauspe', 'altnames': {'Campo_Inchauspe'}},
+    4222: {'name': 'GCS_Cape', 'altnames': {'Cape'}},
+    4223: {'name': 'GCS_Carthage', 'altnames': {'Carthage'}},
+    4224: {'name': 'GCS_Chua', 'altnames': {'Chua'}},
+    4225: {'name': 'GCS_Corrego_Alegre', 'altnames': {'Corrego_Alegre'}},
+    4226: {'name': 'GCS_Cote_d_Ivoire', 'altnames': {'Cote_d_Ivoire'}},
+    4227: {'name': 'GCS_Deir_ez_Zor', 'altnames': {'Deir_ez_Zor'}},
+    4228: {'name': 'GCS_Douala', 'altnames': {'Douala'}},
+    4229: {'name': 'GCS_Egypt_1907', 'altnames': {'Egypt_1907'}},
+    4230: {'name': 'GCS_ED50', 'altnames': {'ED50'}},
+    4231: {'name': 'GCS_ED87', 'altnames': {'ED87'}},
+    4232: {'name': 'GCS_Fahud', 'altnames': {'Fahud'}},
+    4233: {'name': 'GCS_Gandajika_1970', 'altnames': {'Gandajika_1970'}},
+    4234: {'name': 'GCS_Garoua', 'altnames': {'Garoua'}},
+    4235: {'name': 'GCS_Guyane_Francaise', 'altnames': {'Guyane_Francaise'}},
+    4236: {'name': 'GCS_Hu_Tzu_Shan', 'altnames': {'Hu_Tzu_Shan'}},
+    4237: {'name': 'GCS_HD72', 'altnames': {'HD72'}},
+    4238: {'name': 'GCS_ID74', 'altnames': {'ID74'}},
+    4239: {'name': 'GCS_Indian_1954', 'altnames': {'Indian_1954'}},
+    4240: {'name': 'GCS_Indian_1975', 'altnames': {'Indian_1975'}},
+    4241: {'name': 'GCS_Jamaica_1875', 'altnames': {'Jamaica_1875'}},
+    4242: {'name': 'GCS_JAD69', 'altnames': {'JAD69'}},
+    4243: {'name': 'GCS_Kalianpur', 'altnames': {'Kalianpur'}},
+    4244: {'name': 'GCS_Kandawala', 'altnames': {'Kandawala'}},
+    4245: {'name': 'GCS_Kertau', 'altnames': {'Kertau'}},
+    4246: {'name': 'GCS_KOC', 'altnames': {'KOC'}},
+    4247: {'name': 'GCS_La_Canoa', 'altnames': {'La_Canoa'}},
+    4248: {'name': 'GCS_PSAD56', 'altnames': {'PSAD56'}},
+    4249: {'name': 'GCS_Lake', 'altnames': {'Lake'}},
+    4250: {'name': 'GCS_Leigon', 'altnames': {'Leigon'}},
+    4251: {'name': 'GCS_Liberia_1964', 'altnames': {'Liberia_1964'}},
+    4252: {'name': 'GCS_Lome', 'altnames': {'Lome'}},
+    4253: {'name': 'GCS_Luzon_1911', 'altnames': {'Luzon_1911'}},
+    4254: {'name': 'GCS_Hito_XVIII_1963', 'altnames': {'Hito_XVIII_1963'}},
+    4255: {'name': 'GCS_Herat_North', 'altnames': {'Herat_North'}},
+    4256: {'name': 'GCS_Mahe_1971', 'altnames': {'Mahe_1971'}},
+    4257: {'name': 'GCS_Makassar', 'altnames': {'Makassar'}},
+    4258: {'name': 'GCS_EUREF89', 'altnames': {'EUREF89'}},
+    4259: {'name': 'GCS_Malongo_1987', 'altnames': {'Malongo_1987'}},
+    4260: {'name': 'GCS_Manoca', 'altnames': {'Manoca'}},
+    4261: {'name': 'GCS_Merchich', 'altnames': {'Merchich'}},
+    4262: {'name': 'GCS_Massawa', 'altnames': {'Massawa'}},
+    4263: {'name': 'GCS_Minna', 'altnames': {'Minna'}},
+    4264: {'name': 'GCS_Mhast', 'altnames': {'Mhast'}},
+    4265: {'name': 'GCS_Monte_Mario', 'altnames': {'Monte_Mario'}},
+    4266: {'name': 'GCS_M_poraloko', 'altnames': {'M_poraloko'}},
+    4267: {'name': 'GCS_NAD27', 'altnames': {'NAD27'}},
+    4268: {'name': 'GCS_NAD_Michigan', 'altnames': {'NAD_Michigan'}},
+    4269: {'name': 'GCS_NAD83', 'altnames': {'NAD83'}},
+    4270: {'name': 'GCS_Nahrwan_1967', 'altnames': {'Nahrwan_1967'}},
+    4271: {'name': 'GCS_Naparima_1972', 'altnames': {'Naparima_1972'}},
+    4272: {'name': 'GCS_GD49', 'altnames': {'GD49'}},
+    4273: {'name': 'GCS_NGO_1948', 'altnames': {'NGO_1948'}},
+    4274: {'name': 'GCS_Datum_73', 'altnames': {'Datum_73'}},
+    4275: {'name': 'GCS_NTF', 'altnames': {'NTF'}},
+    4276: {'name': 'GCS_NSWC_9Z_2', 'altnames': {'NSWC_9Z_2'}},
+    4277: {'name': 'GCS_OSGB_1936', 'altnames': {'OSGB_1936'}},
+    4278: {'name': 'GCS_OSGB70', 'altnames': {'OSGB70'}},
+    4279: {'name': 'GCS_OS_SN80', 'altnames': {'OS_SN80'}},
+    4280: {'name': 'GCS_Padang', 'altnames': {'Padang'}},
+    4281: {'name': 'GCS_Palestine_1923', 'altnames': {'Palestine_1923'}},
+    4282: {'name': 'GCS_Pointe_Noire', 'altnames': {'Pointe_Noire'}},
+    4283: {'name': 'GCS_GDA94', 'altnames': {'GDA94'}},
+    4284: {'name': 'GCS_Pulkovo_1942', 'altnames': {'Pulkovo_1942'}},
+    4285: {'name': 'GCS_Qatar', 'altnames': {'Qatar'}},
+    4286: {'name': 'GCS_Qatar_1948', 'altnames': {'Qatar_1948'}},
+    4287: {'name': 'GCS_Qornoq', 'altnames': {'Qornoq'}},
+    4288: {'name': 'GCS_Loma_Quintana', 'altnames': {'Loma_Quintana'}},
+    4289: {'name': 'GCS_Amersfoort', 'altnames': {'Amersfoort'}},
+    4290: {'name': 'GCS_RT38', 'altnames': {'RT38'}},
+    4291: {'name': 'GCS_SAD69', 'altnames': {'SAD69'}},
+    4292: {'name': 'GCS_Sapper_Hill_1943', 'altnames': {'Sapper_Hill_1943'}},
+    4293: {'name': 'GCS_Schwarzeck', 'altnames': {'Schwarzeck'}},
+    4294: {'name': 'GCS_Segora', 'altnames': {'Segora'}},
+    4295: {'name': 'GCS_Serindung', 'altnames': {'Serindung'}},
+    4296: {'name': 'GCS_Sudan', 'altnames': {'Sudan'}},
+    4297: {'name': 'GCS_Tananarive', 'altnames': {'Tananarive'}},
+    4298: {'name': 'GCS_Timbalai_1948', 'altnames': {'Timbalai_1948'}},
+    4299: {'name': 'GCS_TM65', 'altnames': {'TM65'}},
+    4300: {'name': 'GCS_TM75', 'altnames': {'TM75'}},
+    4301: {'name': 'GCS_Tokyo', 'altnames': {'Tokyo'}},
+    4302: {'name': 'GCS_Trinidad_1903', 'altnames': {'Trinidad_1903'}},
+    4303: {'name': 'GCS_TC_1948', 'altnames': {'TC_1948'}},
+    4304: {'name': 'GCS_Voirol_1875', 'altnames': {'Voirol_1875'}},
+    4305: {'name': 'GCS_Voirol_Unifie', 'altnames': {'Voirol_Unifie'}},
+    4306: {'name': 'GCS_Bern_1938', 'altnames': {'Bern_1938'}},
+    4307: {'name': 'GCS_Nord_Sahara_1959', 'altnames': {'Nord_Sahara_1959'}},
+    4308: {'name': 'GCS_Stockholm_1938', 'altnames': {'Stockholm_1938'}},
+    4309: {'name': 'GCS_Yacare', 'altnames': {'Yacare'}},
+    4310: {'name': 'GCS_Yoff', 'altnames': {'Yoff'}},
+    4311: {'name': 'GCS_Zanderij', 'altnames': {'Zanderij'}},
+    4312: {'name': 'GCS_MGI', 'altnames': {'MGI'}},
+    4313: {'name': 'GCS_Belge_1972', 'altnames': {'Belge_1972'}},
+    4314: {'name': 'GCS_DHDN', 'altnames': {'DHDN'}},
+    4315: {'name': 'GCS_Conakry_1905', 'altnames': {'Conakry_1905'}},
+    4322: {'name': 'GCS_WGS_72', 'altnames': {'WGS_72'}},
+    4324: {'name': 'GCS_WGS_72BE', 'altnames': {'WGS_72BE'}},
+    4326: {'name': 'GCS_WGS_84', 'altnames': {'WGS_84'}},
+    4801: {'name': 'GCS_Bern_1898_Bern', 'altnames': {'Bern_1898_Bern'}},
+    4802: {'name': 'GCS_Bogota_Bogota', 'altnames': {'Bogota_Bogota'}},
+    4803: {'name': 'GCS_Lisbon_Lisbon', 'altnames': {'Lisbon_Lisbon'}},
+    4804: {'name': 'GCS_Makassar_Jakarta', 'altnames': {'Makassar_Jakarta'}},
+    4805: {'name': 'GCS_MGI_Ferro', 'altnames': {'MGI_Ferro'}},
+    4806: {'name': 'GCS_Monte_Mario_Rome', 'altnames': {'Monte_Mario_Rome'}},
+    4807: {'name': 'GCS_NTF_Paris', 'altnames': {'NTF_Paris'}},
+    4808: {'name': 'GCS_Padang_Jakarta', 'altnames': {'Padang_Jakarta'}},
+    4809: {'name': 'GCS_Belge_1950_Brussels', 'altnames': {'Belge_1950_Brussels'}},
+    4810: {'name': 'GCS_Tananarive_Paris', 'altnames': {'Tananarive_Paris'}},
+    4811: {'name': 'GCS_Voirol_1875_Paris', 'altnames': {'Voirol_1875_Paris'}},
+    4812: {'name': 'GCS_Voirol_Unifie_Paris', 'altnames': {'Voirol_Unifie_Paris'}},
+    4813: {'name': 'GCS_Batavia_Jakarta', 'altnames': {'Batavia_Jakarta'}},
+    4901: {'name': 'GCS_ATF_Paris', 'altnames': {'ATF_Paris'}},
+    4902: {'name': 'GCS_NDG_Paris', 'altnames': {'NDG_Paris'}},
+    4001: {'name': 'GCSE_Airy1830', 'altnames': {'Airy1830'}},
+    4002: {'name': 'GCSE_AiryModified1849', 'altnames': {'AiryModified1849'}},
+    4003: {'name': 'GCSE_AustralianNationalSpheroid', 'altnames': {'AustralianNationalSpheroid'}},
+    4004: {'name': 'GCSE_Bessel1841', 'altnames': {'Bessel1841'}},
+    4005: {'name': 'GCSE_BesselModified', 'altnames': {'BesselModified'}},
+    4006: {'name': 'GCSE_BesselNamibia', 'altnames': {'BesselNamibia'}},
+    4007: {'name': 'GCSE_Clarke1858', 'altnames': {'Clarke1858'}},
+    4008: {'name': 'GCSE_Clarke1866', 'altnames': {'Clarke1866'}},
+    4009: {'name': 'GCSE_Clarke1866Michigan', 'altnames': {'Clarke1866Michigan'}},
+    4010: {'name': 'GCSE_Clarke1880_Benoit', 'altnames': {'Clarke1880_Benoit'}},
+    4011: {'name': 'GCSE_Clarke1880_IGN', 'altnames': {'Clarke1880_IGN'}},
+    4012: {'name': 'GCSE_Clarke1880_RGS', 'altnames': {'Clarke1880_RGS'}},
+    4013: {'name': 'GCSE_Clarke1880_Arc', 'altnames': {'Clarke1880_Arc'}},
+    4014: {'name': 'GCSE_Clarke1880_SGA1922', 'altnames': {'Clarke1880_SGA1922'}},
+    4015: {'name': 'GCSE_Everest1830_1937Adjustment', 'altnames': {'Everest1830_1937Adjustment'}},
+    4016: {'name': 'GCSE_Everest1830_1967Definition', 'altnames': {'Everest1830_1967Definition'}},
+    4017: {'name': 'GCSE_Everest1830_1975Definition', 'altnames': {'Everest1830_1975Definition'}},
+    4018: {'name': 'GCSE_Everest1830Modified', 'altnames': {'Everest1830Modified'}},
+    4019: {'name': 'GCSE_GRS1980', 'altnames': {'GRS1980'}},
+    4020: {'name': 'GCSE_Helmert1906', 'altnames': {'Helmert1906'}},
+    4021: {'name': 'GCSE_IndonesianNationalSpheroid', 'altnames': {'IndonesianNationalSpheroid'}},
+    4022: {'name': 'GCSE_International1924', 'altnames': {'International1924'}},
+    4023: {'name': 'GCSE_International1967', 'altnames': {'International1967'}},
+    4024: {'name': 'GCSE_Krassowsky1940', 'altnames': {'Krassowsky1940'}},
+    4025: {'name': 'GCSE_NWL9D', 'altnames': {'NWL9D'}},
+    4026: {'name': 'GCSE_NWL10D', 'altnames': {'NWL10D'}},
+    4027: {'name': 'GCSE_Plessis1817', 'altnames': {'Plessis1817'}},
+    4028: {'name': 'GCSE_Struve1860', 'altnames': {'Struve1860'}},
+    4029: {'name': 'GCSE_WarOffice', 'altnames': {'WarOffice'}},
+    4030: {'name': 'GCSE_WGS84', 'altnames': {'WGS84'}},
+    4031: {'name': 'GCSE_GEM10C', 'altnames': {'GEM10C'}},
+    4032: {'name': 'GCSE_OSU86F', 'altnames': {'OSU86F'}},
+    4033: {'name': 'GCSE_OSU91A', 'altnames': {'OSU91A'}},
+    4034: {'name': 'GCSE_Clarke1880', 'altnames': {'Clarke1880'}},
+    4035: {'name': 'GCSE_Sphere', 'altnames': {'Sphere'}},
+})
+
+
+GeoTiffAngularUnits = TiffConstantSet('GeoTiffAngularUnits', {
+    9101: {'name': 'Angular_Radian', 'altnames': {'radian', 'rad'}},
+    9102: {'name': 'Angular_Degree', 'altnames': {'degree', 'deg'}},
+    9103: {'name': 'Angular_Arc_Minute', 'altnames': {'arc_minute', 'arcminute'}},
+    9104: {'name': 'Angular_Arc_Second', 'altnames': {'arc_second', 'arcsecond'}},
+    9105: {'name': 'Angular_Grad', 'altnames': {'grad'}},
+    9106: {'name': 'Angular_Gon', 'altnames': {'gon'}},
+    9107: {'name': 'Angular_DMS', 'altnames': {'dms'}},
+    9108: {'name': 'Angular_DMS_Hemisphere', 'altnames': {'dms_hemisphere', 'dms_hemi'}},
+})
+
+
+GeoTiffLinearUnits = TiffConstantSet('GeoTiffLinearUnits', {
+    9001: {'name': 'Linear_Meter', 'altnames': {'m', 'meter', 'meters'}},
+    9002: {'name': 'Linear_Foot', 'altnames': {'ft', 'foot', 'feet'}},
+    9003: {'name': 'Linear_Foot_US_Survey', 'altnames': {'ft_us'}},
+    9004: {'name': 'Linear_Foot_Modified_American', 'altnames': {'ft_am'}},
+    9005: {'name': 'Linear_Foot_Clarke', 'altnames': {'ft_cl'}},
+    9006: {'name': 'Linear_Foot_Indian', 'altnames': {'ft_in'}},
+    9007: {'name': 'Linear_Link', 'altnames': {'link'}},
+    9008: {'name': 'Linear_Link_Benoit', 'altnames': {'link_bn'}},
+    9009: {'name': 'Linear_Link_Sears', 'altnames': {'link_sr'}},
+    9010: {'name': 'Linear_Chain_Benoit', 'altnames': {'chain_bn'}},
+    9011: {'name': 'Linear_Chain_Sears', 'altnames': {'chain_sr'}},
+    9012: {'name': 'Linear_Yard_Sears', 'altnames': {'yd_sr'}},
+    9013: {'name': 'Linear_Yard_Indian', 'altnames': {'yd_in'}},
+    9014: {'name': 'Linear_Fathom', 'altnames': {'fathom'}},
+    9015: {'name': 'Linear_Mile_International_Nautical', 'altnames': {'kt', 'knot', 'knots', 'nmile', 'nmiles'}},
+})
+
+
+GeoTiffTransformations = TiffConstantSet('GeoTiffTransformations', {
+    1: {'name': 'CT_TransverseMercator', 'altnames': {'TransverseMercator', 'CT_GaussBoaga', 'GaussBoaga', 'CT_GaussKruger', 'GaussKruger'}},
+    2: {'name': 'CT_TransvMercator_Modified_Alaska', 'altnames': {'TransvMercator_Modified_Alaska', 'CT_AlaskaConformal', 'AlaskaConformal'}},
+    3: {'name': 'CT_ObliqueMercator', 'altnames': {'ObliqueMercator', 'CT_ObliqueMercator_Hotine', 'ObliqueMercator_Hotine'}},
+    4: {'name': 'CT_ObliqueMercator_Laborde', 'altnames': {'ObliqueMercator_Laborde'}},
+    5: {'name': 'CT_ObliqueMercator_Rosenmund', 'altnames': {'ObliqueMercator_Rosenmund', 'CT_SwissObliqueCylindrical', 'SwissObliqueCylindrical'}},
+    6: {'name': 'CT_ObliqueMercator_Spherical', 'altnames': {'ObliqueMercator_Spherical'}},
+    7: {'name': 'CT_Mercator', 'altnames': {'Mercator'}},
+    8: {'name': 'CT_LambertConfConic_2SP', 'altnames': {'LambertConfConic_2SP', 'CT_LambertConfConic', 'LambertConfConic'}},
+    9: {'name': 'CT_LambertConfConic_1SP', 'altnames': {'LambertConfConic_1SP', 'CT_LambertConfConic_Helmert', 'LambertConfConic_Helmert'}},
+    10: {'name': 'CT_LambertAzimEqualArea', 'altnames': {'LambertAzimEqualArea'}},
+    11: {'name': 'CT_AlbersEqualArea', 'altnames': {'AlbersEqualArea'}},
+    12: {'name': 'CT_AzimuthalEquidistant', 'altnames': {'AzimuthalEquidistant'}},
+    13: {'name': 'CT_EquidistantConic', 'altnames': {'EquidistantConic'}},
+    14: {'name': 'CT_Stereographic', 'altnames': {'Stereographic'}},
+    15: {'name': 'CT_PolarStereographic', 'altnames': {'PolarStereographic'}},
+    16: {'name': 'CT_ObliqueStereographic', 'altnames': {'ObliqueStereographic'}},
+    17: {'name': 'CT_Equirectangular', 'altnames': {'Equirectangular'}},
+    18: {'name': 'CT_CassiniSoldner', 'altnames': {'CassiniSoldner', 'CT_TransvEquidistCylindrical', 'TransvEquidistCylindrical'}},
+    19: {'name': 'CT_Gnomonic', 'altnames': {'Gnomonic'}},
+    20: {'name': 'CT_MillerCylindrical', 'altnames': {'MillerCylindrical'}},
+    21: {'name': 'CT_Orthographic', 'altnames': {'Orthographic'}},
+    22: {'name': 'CT_Polyconic', 'altnames': {'Polyconic'}},
+    23: {'name': 'CT_Robinson', 'altnames': {'Robinson'}},
+    24: {'name': 'CT_Sinusoidal', 'altnames': {'Sinusoidal'}},
+    25: {'name': 'CT_VanDerGrinten', 'altnames': {'VanDerGrinten'}},
+    26: {'name': 'CT_NewZealandMapGrid', 'altnames': {'NewZealandMapGrid'}},
+    27: {'name': 'CT_TransvMercator_SouthOriented', 'altnames': {'CTransvMercator_SouthOriented', 'CT_SouthOrientedGaussConformal', 'SouthOrientedGaussConformal'}},
+})
+
+
+# from https://spatialreference.org/explorer.html
+EPSGTypes = {
+    'projected': [*list(range(2000, 2008)), *list(range(2009, 2036)), *list(range(2039, 2063)), *list(range(2065, 2085)), *list(range(2087, 2091)), *list(range(2093, 2139)), 2154, *list(range(2157, 2163)), 2164, 2165, 2169, *list(range(2172, 2181)), *list(range(2188, 2191)), 2193, *list(range(2195, 2199)), *list(range(2200, 2214)), *list(range(2215, 2244)), *list(range(2246, 2291)), *list(range(2294, 2297)), 2299, 2301, *list(range(2303, 2400)), *list(range(2401, 2492)), *list(range(2494, 2550)), *list(range(2551, 2577)), *list(range(2578, 2600)), *list(range(2601, 2694)), *list(range(2695, 2889)), *list(range(2891, 2934)), *list(range(2935, 2944)), *list(range(2945, 2974)), *list(range(2975, 2979)), 2980, 2981, *list(range(2985, 2989)), *list(range(2991, 3038)), *list(range(3040, 3050)), *list(range(3052, 3073)), 3074, 3075, *list(range(3077, 3103)), *list(range(3106, 3143)), 3144, 3145, 3148, 3149, *list(range(3152, 3314)), *list(range(3316, 3349)), *list(range(3350, 3356)), 3358, *list(range(3360, 3366)), *list(range(3367, 3454)), *list(range(3455, 3752)), *list(range(3753, 3774)), *list(range(3775, 3778)), *list(range(3779, 3782)), 3783, 3784, *list(range(3788, 3792)), *list(range(3793, 3803)), 3812, *list(range(3814, 3817)), *list(range(3825, 3830)), *list(range(3832, 3842)), *list(range(3844, 3853)), 3854, 3857, *list(range(3873, 3886)), *list(range(3890, 3894)), 3912, 3920, *list(range(3942, 3951)), *list(range(3968, 3971)), 3976, 3978, 3979, *list(range(3986, 3990)), *list(range(3991, 3998)), 4026, 4037, 4038, *list(range(4048, 4052)), *list(range(4056, 4064)), 4071, 4082, 4083, 4087, *list(range(4093, 4097)), 4217, *list(range(4390, 4416)), *list(range(4417, 4435)), *list(range(4437, 4440)), *list(range(4455, 4458)), 4462, 4467, 4471, *list(range(4484, 4490)), *list(range(4491, 4555)), 4559, *list(range(4568, 4590)), 4647, *list(range(4652, 4657)), *list(range(4766, 4801)), 4812, 4822, 4826, 4839, *list(range(5014, 5019)), 5041, 5042, 5048, *list(range(5069, 5073)), *list(range(5105, 5131)), *list(range(5167, 5189)), 5221, *list(range(5223, 5226)), 5234, 5235, 5243, 5247, *list(range(5253, 5260)), 5266, *list(range(5269, 5276)), *list(range(5292, 5312)), 5316, 5320, 5321, 5325, *list(range(5329, 5332)), 5337, *list(range(5343, 5350)), *list(range(5355, 5358)), 5361, 5362, 5367, 5382, 5383, 5387, 5389, 5396, 5456, 5457, *list(range(5459, 5464)), 5469, 5472, *list(range(5479, 5483)), 5490, *list(range(5513, 5517)), *list(range(5518, 5521)), 5523, 5530, 5531, *list(range(5533, 5540)), *list(range(5550, 5553)), 5559, *list(range(5562, 5570)), 5588, 5589, 5596, *list(range(5623, 5626)), 5627, 5629, *list(range(5631, 5640)), 5641, 5643, 5644, 5646, *list(range(5649, 5656)), 5659, *list(range(5663, 5681)), *list(range(5682, 5686)), 5700, 5825, 5836, 5837, 5839, 5842, 5844, 5858, *list(range(5875, 5878)), 5879, 5880, 5887, *list(range(5896, 5900)), *list(range(5921, 5941)), *list(range(6050, 6126)), 6128, 6129, 6201, 6202, 6204, 6210, 6211, *list(range(6244, 6276)), 6307, 6312, 6316, *list(range(6328, 6349)), *list(range(6350, 6357)), 6362, *list(range(6366, 6373)), *list(range(6381, 6388)), 6391, *list(range(6393, 6517)), *list(range(6518, 6604)), *list(range(6605, 6638)), 6646, *list(range(6669, 6693)), 6703, *list(range(6707, 6710)), *list(range(6720, 6724)), *list(range(6736, 6739)), *list(range(6784, 6864)), 6867, 6868, 6870, 6875, 6876, 6879, 6880, *list(range(6884, 6888)), 6915, *list(range(6922, 6926)), *list(range(6931, 6934)), 6962, 6966, 6984, 6991, *list(range(7005, 7008)), *list(range(7057, 7071)), *list(range(7074, 7082)), *list(range(7109, 7129)), 7131, 7132, 7142, *list(range(7257, 7371)), *list(range(7374, 7377)), *list(range(7528, 7646)), *list(range(7692, 7697)), *list(range(7755, 7788)), *list(range(7791, 7796)), *list(range(7799, 7802)), 7803, 7805, *list(range(7825, 7832)), *list(range(7845, 7860)), 7877, 7878, 7882, 7883, 7887, 7899, 7991, 7992, *list(range(8013, 8033)), 8035, 8036, 8044, 8045, 8058, 8059, *list(range(8065, 8069)), 8082, 8083, 8088, *list(range(8090, 8094)), *list(range(8095, 8174)), 8177, *list(range(8179, 8183)), 8184, 8185, 8187, 8189, 8191, 8193, *list(range(8196, 8199)), *list(range(8200, 8211)), *list(range(8212, 8215)), 8216, 8218, 8220, 8222, *list(range(8224, 8227)), *list(range(8311, 8349)), 8352, 8353, *list(range(8379, 8386)), 8387, 8391, 8395, 8433, 8441, 8455, 8456, *list(range(8518, 8530)), 8531, *list(range(8533, 8537)), *list(range(8538, 8541)), *list(range(8677, 8680)), 8682, 8686, 8687, 8692, 8693, 8826, *list(range(8836, 8841)), *list(range(8857, 8860)), 8903, *list(range(8908, 8911)), 8950, 8951, 9039, 9040, 9141, 9149, 9150, *list(range(9154, 9160)), 9191, *list(range(9205, 9219)), 9221, 9222, 9249, 9250, 9252, 9254, 9265, *list(range(9271, 9274)), 9284, 9285, *list(range(9295, 9298)), 9300, 9311, 9354, *list(range(9356, 9361)), 9367, 9373, 9377, 9387, 9391, *list(range(9404, 9408)), 9456, 9473, *list(range(9476, 9483)), *list(range(9487, 9495)), 9498, 9549, 9674, 9678, 9680, *list(range(9697, 9700)), 9709, 9712, 9713, 9716, 9741, 9748, 9749, 9761, 9766, 9793, 9794, *list(range(9821, 9866)), 9869, 9874, 9875, 9880, 9895, 9943, 9945, 9947, 9967, 9972, 9977, 10160, 10183, 10188, 10194, 10199, 10207, 10212, 10217, 10222, 10227, 10235, 10240, 10250, 10254, 10258, 10262, 10266, 10270, 10275, 10280, *list(range(10285, 10292)), 10306, *list(range(10314, 10318)), 10329, *list(range(10448, 10466)), 10471, 10477, 10481, 10516, 10592, 10594, 10596, 10598, 10601, 10603, 10622, 10626, 10632, 10641, 10665, 10674, 10699, 10702, *list(range(10726, 10730)), *list(range(10731, 10734)), 10744, 10745, 10759, 10773, 10801, 10802, 10820, *list(range(11114, 11119)), 20002, *list(range(20004, 20033)), 20042, *list(range(20047, 20051)), *list(range(20135, 20139)), *list(range(20249, 20259)), *list(range(20349, 20357)), *list(range(20436, 20441)), 20499, 20538, 20539, 20790, 20791, *list(range(20822, 20825)), *list(range(20904, 20933)), *list(range(20934, 20937)), *list(range(21004, 21033)), *list(range(21035, 21038)), *list(range(21095, 21098)), *list(range(21148, 21151)), *list(range(21207, 21265)), 21291, 21292, *list(range(21307, 21365)), *list(range(21413, 21424)), *list(range(21453, 21464)), 21500, *list(range(21780, 21783)), 21818, *list(range(21896, 21900)), 22032, 22033, 22091, 22092, *list(range(22171, 22178)), *list(range(22181, 22188)), *list(range(22191, 22198)), *list(range(22207, 22223)), *list(range(22229, 22233)), 22234, 22235, 22239, 22240, *list(range(22243, 22251)), *list(range(22262, 22266)), 22275, 22277, 22279, 22281, 22283, 22285, 22287, 22289, 22291, 22293, 22300, *list(range(22307, 22323)), 22332, 22337, 22338, *list(range(22348, 22358)), 22391, 22392, *list(range(22407, 22423)), *list(range(22462, 22466)), *list(range(22521, 22526)), *list(range(22607, 22623)), 22639, *list(range(22641, 22647)), *list(range(22648, 22658)), 22700, *list(range(22707, 22723)), 22739, *list(range(22762, 22766)), 22770, 22780, *list(range(22807, 22823)), *list(range(22991, 22995)), *list(range(23028, 23039)), 23090, 23095, 23239, 23240, *list(range(23301, 23334)), 23700, *list(range(23830, 23853)), *list(range(23866, 23873)), *list(range(23877, 23885)), *list(range(23887, 23895)), *list(range(23946, 23949)), 24047, 24048, 24100, 24200, 24305, 24306, *list(range(24311, 24314)), *list(range(24342, 24348)), *list(range(24370, 24384)), 24500, 24547, 24548, 24600, *list(range(24718, 24721)), *list(range(24817, 24822)), *list(range(24877, 24883)), *list(range(24891, 24894)), 25000, 25231, *list(range(25391, 25396)), *list(range(25828, 25838)), 25884, 25932, 26191, 26192, 26194, 26195, 26237, 26331, 26332, *list(range(26391, 26394)), 26632, 26692, *list(range(26701, 26723)), *list(range(26729, 26747)), *list(range(26748, 26761)), *list(range(26766, 26788)), *list(range(26791, 26800)), *list(range(26847, 26871)), *list(range(26891, 26900)), *list(range(26901, 26924)), *list(range(26929, 26947)), *list(range(26948, 26979)), *list(range(26980, 26999)), 27039, 27040, 27120, 27200, *list(range(27205, 27233)), *list(range(27258, 27261)), 27291, 27292, *list(range(27391, 27399)), 27429, 27493, 27500, *list(range(27561, 27565)), *list(range(27571, 27575)), *list(range(27700, 27708)), *list(range(28191, 28194)), 28232, *list(range(28348, 28359)), *list(range(28404, 28433)), 28600, 28991, 28992, 29101, *list(range(29168, 29173)), *list(range(29187, 29196)), 29220, 29221, 29333, 29371, 29373, 29375, 29377, 29379, 29381, 29383, 29385, 29701, 29702, 29738, 29739, 29849, 29850, *list(range(29871, 29875)), *list(range(29901, 29904)), *list(range(30161, 30180)), 30200, 30339, 30340, *list(range(30491, 30495)), *list(range(30729, 30733)), 30791, 30792, 31028, 31121, 31154, 31170, 31171, *list(range(31251, 31260)), *list(range(31281, 31291)), 31300, 31370, *list(range(31466, 31470)), 31528, 31529, 31600, 31838, 31839, 31901, *list(range(31965, 32004)), *list(range(32005, 32018)), *list(range(32019, 32029)), 32030, 32031, *list(range(32033, 32036)), *list(range(32037, 32059)), *list(range(32064, 32068)), *list(range(32081, 32087)), *list(range(32098, 32101)), 32104, *list(range(32107, 32131)), *list(range(32133, 32160)), 32161, *list(range(32164, 32168)), *list(range(32181, 32200)), *list(range(32201, 32261)), *list(range(32301, 32361)), *list(range(32401, 32461)), *list(range(32501, 32561)), *list(range(32600, 32662)), *list(range(32664, 32668)), *list(range(32700, 32762)), 32766],
+    'geographic_2d': [3819, 3821, 3824, 3889, 3906, 4023, 4046, 4075, 4081, *list(range(4120, 4125)), *list(range(4127, 4140)), *list(range(4141, 4172)), *list(range(4173, 4177)), *list(range(4178, 4185)), *list(range(4188, 4217)), *list(range(4218, 4226)), 4227, *list(range(4229, 4233)), *list(range(4236, 4260)), *list(range(4261, 4264)), *list(range(4265, 4268)), *list(range(4269, 4280)), *list(range(4281, 4287)), 4288, 4289, 4292, 4293, 4295, *list(range(4297, 4305)), *list(range(4306, 4317)), 4318, 4319, 4322, 4324, 4326, 4463, 4470, 4475, 4483, 4490, 4555, 4558, *list(range(4600, 4631)), 4632, 4633, *list(range(4636, 4640)), *list(range(4641, 4645)), 4646, *list(range(4657, 4681)), *list(range(4682, 4685)), *list(range(4686, 4731)), *list(range(4732, 4766)), *list(range(4801, 4808)), *list(range(4809, 4812)), *list(range(4813, 4819)), 4820, 4821, 4823, 4824, 4901, 4903, 4904, 5013, 5132, 5228, 5229, 5233, 5246, 5252, 5264, 5324, 5340, 5354, 5360, 5365, 5371, 5373, 5381, 5393, 5451, 5464, 5467, 5489, 5524, 5527, 5546, 5561, 5593, 5681, 5886, 6135, 6207, 6311, 6318, 6322, 6325, 6365, 6668, 6706, 6783, *list(range(6881, 6884)), 6892, 6894, 6983, 6990, 7035, 7037, 7039, 7041, 7073, 7084, 7086, 7133, 7136, 7139, 7373, 7683, 7686, 7798, 7844, 7881, 7886, 8042, 8043, 8086, 8232, 8237, 8240, 8246, 8249, 8252, 8255, 8351, 8427, 8428, 8431, 8545, 8685, 8694, 8699, 8818, 8860, 8888, 8900, 8902, 8907, 8949, *list(range(8972, 9001)), 9003, 9006, 9009, 9012, 9014, 9017, 9019, *list(range(9053, 9058)), *list(range(9059, 9070)), 9072, 9075, 9140, 9148, 9153, 9248, 9251, 9253, 9294, 9299, 9309, 9333, 9364, 9372, 9380, 9384, 9403, 9453, 9470, 9474, 9475, 9547, 9696, 9702, 9739, 9755, 9758, 9763, 9777, 9779, 9782, 9784, 9866, 9871, 9939, 9964, 9969, 9974, 9990, 10158, 10175, 10178, 10185, 10191, 10196, 10204, 10209, 10214, 10219, 10224, 10229, 10237, 10249, 10252, 10256, 10260, 10265, 10268, 10272, 10277, 10284, 10299, 10305, 10307, 10310, 10312, 10328, 10345, 10346, 10414, 10468, 10475, 10571, 10606, 10623, 10628, 10636, 10639, 10671, 10673, 10690, 10725, 10736, 10739, 10758, 10762, 10781, 10785, 10800, 20033, 20041, 20046],
+    'geocentric': [3822, 3887, 4000, 4039, 4073, 4079, 4465, 4468, 4473, 4479, 4481, 4556, 4882, 4884, 4886, 4888, 4890, 4892, 4894, 4896, 4897, 4899, 4906, 4908, *list(range(4910, 4921)), 4922, 4924, 4926, 4928, 4930, 4932, 4934, 4936, 4938, 4940, 4942, 4944, 4946, 4948, 4950, 4952, 4954, 4956, 4958, 4960, 4962, 4964, 4966, 4970, 4974, 4976, 4978, 4980, 4982, 4984, 4986, 4988, 4990, 4992, 4994, 4996, 4998, 5011, 5244, 5250, 5262, 5322, 5332, 5341, 5352, 5358, 5363, 5368, 5369, 5379, 5391, 5487, 5544, 5558, 5591, 5828, 5884, 6133, 6309, 6317, 6320, 6323, 6363, 6666, 6704, 6781, 6934, 6981, 6988, 7071, 7134, 7137, 7371, 7656, 7658, 7660, 7662, 7664, 7677, 7679, 7681, 7684, 7789, 7796, 7815, 7842, 7879, 7884, 7914, 7916, 7918, 7920, 7922, 7924, 7926, 7928, 7930, 8084, 8227, 8230, 8233, 8238, 8242, 8247, 8250, 8253, 8397, 8401, 8425, 8429, 8541, 8543, 8683, 8697, 8816, 8898, 8905, 8915, 8917, 8919, 8921, 8923, 8925, 8927, 8929, 8931, 8933, 8935, 8937, 8939, 8941, 8943, 8945, 8947, 9001, 9004, 9007, 9010, 9015, 9070, 9073, 9138, 9146, 9151, 9266, 9292, 9307, 9331, 9378, 9468, 9545, 9694, 9700, 9753, 9775, 9780, 9892, 9988, 10176, 10282, 10297, 10303, 10308, 10326, 10412, 10473, 10569, 10604, 10634, 10637, 10669, 10688, 10723, 10734, 10737, 10760, 10779, 10783, 10798, 20039, 20044],
+}
+
+
 def EstimateJpegQuality(jpegTables):
     try:
         qtables = jpegTables.split(b'\xff\xdb', 1)[1]
diff --git a/tox.ini b/tox.ini
index 47d3498..a34f7ed 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,6 +12,8 @@ deps =
   pytest-cov
   pytest-xdist
   pyyaml
+extras =
+  py38,py39,py310,py311,py312,py313: geo
 commands =
   pytest --cov {envsitepackagesdir}/tifftools {posargs}
 


Reviews:
user: login: manthey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T19:49:22Z
user: login: annehaley
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T19:55:04Z
user: login: manthey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T19:57:32Z
user: login: manthey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T19:58:24Z
user: login: annehaley
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T19:59:36Z
user: login: manthey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T14:00:59Z
user: login: manthey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T14:03:38Z
user: login: manthey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T14:19:37Z
user: login: annehaley
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T15:33:24Z
user: login: manthey
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T16:04:18Z

Review comments:
diff_hunk: @@ -26,6 +26,7 @@ def prerelease_local_scheme(version):
     name='tifftools',
     use_scm_version={'local_scheme': prerelease_local_scheme, 'fallback_version': '0.0.0'},
     setup_requires=['setuptools-scm'],
+    install_requires=['pyproj'],
path: setup.py
user: login: manthey
body: Please make this some sort of extras_require.  It is convenient to have _no_ dependencies for the core library.
created_at: 2025-04-09T19:49:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,6 +26,7 @@ def prerelease_local_scheme(version):
     name='tifftools',
     use_scm_version={'local_scheme': prerelease_local_scheme, 'fallback_version': '0.0.0'},
     setup_requires=['setuptools-scm'],
+    install_requires=['pyproj'],
path: setup.py
user: login: annehaley
body: Sure, what do we want the extra to be called? Maybe something like `geo` or `geospatial`?
created_at: 2025-04-09T19:55:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,6 +26,7 @@ def prerelease_local_scheme(version):
     name='tifftools',
     use_scm_version={'local_scheme': prerelease_local_scheme, 'fallback_version': '0.0.0'},
     setup_requires=['setuptools-scm'],
+    install_requires=['pyproj'],
path: setup.py
user: login: manthey
body: Either `geo` or `geotiff`.  I'll note that I don't think pyproj publishes wheels for pypy, so any tests that depend on pyproj will be a pain to run on those environments.
created_at: 2025-04-09T19:57:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,6 +26,7 @@ def prerelease_local_scheme(version):
     name='tifftools',
     use_scm_version={'local_scheme': prerelease_local_scheme, 'fallback_version': '0.0.0'},
     setup_requires=['setuptools-scm'],
+    install_requires=['pyproj'],
path: setup.py
user: login: manthey
body: And, note the recent commit that removed setup.py in favor of just using pyproject.toml (trying to keep this modern).
created_at: 2025-04-09T19:58:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,6 +26,7 @@ def prerelease_local_scheme(version):
     name='tifftools',
     use_scm_version={'local_scheme': prerelease_local_scheme, 'fallback_version': '0.0.0'},
     setup_requires=['setuptools-scm'],
+    install_requires=['pyproj'],
path: setup.py
user: login: annehaley
body: Gotcha, I'll update my branch with latest master
created_at: 2025-04-09T19:59:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -548,14 +548,256 @@ def get_or_create_tag(key, tagSet=None, upperLimit=True, **tagOptions):
     3094: {'name': 'ProjAzimuthAngle', 'altnames': {'ProjAzimuthAngleGeoKey'}, 'datatype': Datatype.DOUBLE},
     3095: {'name': 'ProjStraightVertPoleLong', 'altnames': {'ProjStraightVertPoleLongGeoKey'}, 'datatype': Datatype.DOUBLE},
     3096: {'name': 'ProjRectifiedGridAngle', 'altnames': {'ProjRectifiedGridAngleGeoKey'}, 'datatype': Datatype.DOUBLE},
-    4096: {'name': 'VerticalCSType', 'altnames': {'VerticalCSTypeGeoKey'}, 'datatype': Datatype.DOUBLE},
+    4096: {'name': 'VerticalCSType', 'altnames': {'VerticalCSTypeGeoKey'}, 'datatype': Datatype.SHORT},
     4097: {'name': 'VerticalCitation', 'altnames': {'VerticalCitationGeoKey'}, 'datatype': Datatype.ASCII},
-    4098: {'name': 'VerticalDatum', 'altnames': {'VerticalDatumGeoKey'}, 'datatype': Datatype.DOUBLE},
-    4099: {'name': 'VerticalUnits', 'altnames': {'VerticalUnitsGeoKey'}, 'datatype': Datatype.DOUBLE},
+    4098: {'name': 'VerticalDatum', 'altnames': {'VerticalDatumGeoKey'}, 'datatype': Datatype.SHORT},
+    4099: {'name': 'VerticalUnits', 'altnames': {'VerticalUnitsGeoKey'}, 'datatype': Datatype.SHORT},
     5120: {'name': 'CoordinateEpoch', 'altnames': {'CoordinateEpochGeoKey'}, 'datatype': Datatype.DOUBLE},
 })
 
 
+GeoTiffCSTypes = TiffConstantSet('GeoTiffCSTypes', {
+    4201: {'name': 'GCS_Adindan', 'altnames': {'Adindan'}},
+    4202: {'name': 'GCS_AGD66', 'altnames': {'AGD66'}},
+    4203: {'name': 'GCS_AGD84', 'altnames': {'AGD84'}},
+    4204: {'name': 'GCS_Ain_el_Abd', 'altnames': {'Ain_el_Abd'}},
+    4205: {'name': 'GCS_Afgooye', 'altnames': {'Afgooye'}},
+    4206: {'name': 'GCS_Agadez', 'altnames': {'Agadez'}},
+    4207: {'name': 'GCS_Lisbon', 'altnames': {'Lisbon'}},
+    4208: {'name': 'GCS_Aratu', 'altnames': {'Aratu'}},
+    4209: {'name': 'GCS_Arc_1950', 'altnames': {'Arc_1950'}},
+    4210: {'name': 'GCS_Arc_1960', 'altnames': {'Arc_1960'}},
+    4211: {'name': 'GCS_Batavia', 'altnames': {'Batavia'}},
+    4212: {'name': 'GCS_Barbados', 'altnames': {'Barbados'}},
+    4213: {'name': 'GCS_Beduaram', 'altnames': {'Beduaram'}},
+    4214: {'name': 'GCS_Beijing_1954', 'altnames': {'Beijing_1954'}},
+    4215: {'name': 'GCS_Belge_1950', 'altnames': {'Belge_1950'}},
+    4216: {'name': 'GCS_Bermuda_1957', 'altnames': {'Bermuda_1957'}},
+    4217: {'name': 'GCS_Bern_1898', 'altnames': {'Bern_1898'}},
+    4218: {'name': 'GCS_Bogota', 'altnames': {'Bogota'}},
+    4219: {'name': 'GCS_Bukit_Rimpah', 'altnames': {'Bukit_Rimpah'}},
+    4220: {'name': 'GCS_Camacupa', 'altnames': {'Camacupa'}},
+    4221: {'name': 'GCS_Campo_Inchauspe', 'altnames': {'Campo_Inchauspe'}},
+    4222: {'name': 'GCS_Cape', 'altnames': {'Cape'}},
+    4223: {'name': 'GCS_Carthage', 'altnames': {'Carthage'}},
+    4224: {'name': 'GCS_Chua', 'altnames': {'Chua'}},
+    4225: {'name': 'GCS_Corrego_Alegre', 'altnames': {'Corrego_Alegre'}},
+    4226: {'name': 'GCS_Cote_d_Ivoire', 'altnames': {'Cote_d_Ivoire'}},
+    4227: {'name': 'GCS_Deir_ez_Zor', 'altnames': {'Deir_ez_Zor'}},
+    4228: {'name': 'GCS_Douala', 'altnames': {'Douala'}},
+    4229: {'name': 'GCS_Egypt_1907', 'altnames': {'Egypt_1907'}},
+    4230: {'name': 'GCS_ED50', 'altnames': {'ED50'}},
+    4231: {'name': 'GCS_ED87', 'altnames': {'ED87'}},
+    4232: {'name': 'GCS_Fahud', 'altnames': {'Fahud'}},
+    4233: {'name': 'GCS_Gandajika_1970', 'altnames': {'Gandajika_1970'}},
+    4234: {'name': 'GCS_Garoua', 'altnames': {'Garoua'}},
+    4235: {'name': 'GCS_Guyane_Francaise', 'altnames': {'Guyane_Francaise'}},
+    4236: {'name': 'GCS_Hu_Tzu_Shan', 'altnames': {'Hu_Tzu_Shan'}},
+    4237: {'name': 'GCS_HD72', 'altnames': {'HD72'}},
+    4238: {'name': 'GCS_ID74', 'altnames': {'ID74'}},
+    4239: {'name': 'GCS_Indian_1954', 'altnames': {'Indian_1954'}},
+    4240: {'name': 'GCS_Indian_1975', 'altnames': {'Indian_1975'}},
+    4241: {'name': 'GCS_Jamaica_1875', 'altnames': {'Jamaica_1875'}},
+    4242: {'name': 'GCS_JAD69', 'altnames': {'JAD69'}},
+    4243: {'name': 'GCS_Kalianpur', 'altnames': {'Kalianpur'}},
+    4244: {'name': 'GCS_Kandawala', 'altnames': {'Kandawala'}},
+    4245: {'name': 'GCS_Kertau', 'altnames': {'Kertau'}},
+    4246: {'name': 'GCS_KOC', 'altnames': {'KOC'}},
+    4247: {'name': 'GCS_La_Canoa', 'altnames': {'La_Canoa'}},
+    4248: {'name': 'GCS_PSAD56', 'altnames': {'PSAD56'}},
+    4249: {'name': 'GCS_Lake', 'altnames': {'Lake'}},
+    4250: {'name': 'GCS_Leigon', 'altnames': {'Leigon'}},
+    4251: {'name': 'GCS_Liberia_1964', 'altnames': {'Liberia_1964'}},
+    4252: {'name': 'GCS_Lome', 'altnames': {'Lome'}},
+    4253: {'name': 'GCS_Luzon_1911', 'altnames': {'Luzon_1911'}},
+    4254: {'name': 'GCS_Hito_XVIII_1963', 'altnames': {'Hito_XVIII_1963'}},
+    4255: {'name': 'GCS_Herat_North', 'altnames': {'Herat_North'}},
+    4256: {'name': 'GCS_Mahe_1971', 'altnames': {'Mahe_1971'}},
+    4257: {'name': 'GCS_Makassar', 'altnames': {'Makassar'}},
+    4258: {'name': 'GCS_EUREF89', 'altnames': {'EUREF89'}},
+    4259: {'name': 'GCS_Malongo_1987', 'altnames': {'Malongo_1987'}},
+    4260: {'name': 'GCS_Manoca', 'altnames': {'Manoca'}},
+    4261: {'name': 'GCS_Merchich', 'altnames': {'Merchich'}},
+    4262: {'name': 'GCS_Massawa', 'altnames': {'Massawa'}},
+    4263: {'name': 'GCS_Minna', 'altnames': {'Minna'}},
+    4264: {'name': 'GCS_Mhast', 'altnames': {'Mhast'}},
+    4265: {'name': 'GCS_Monte_Mario', 'altnames': {'Monte_Mario'}},
+    4266: {'name': 'GCS_M_poraloko', 'altnames': {'M_poraloko'}},
+    4267: {'name': 'GCS_NAD27', 'altnames': {'NAD27'}},
+    4268: {'name': 'GCS_NAD_Michigan', 'altnames': {'NAD_Michigan'}},
+    4269: {'name': 'GCS_NAD83', 'altnames': {'NAD83'}},
+    4270: {'name': 'GCS_Nahrwan_1967', 'altnames': {'Nahrwan_1967'}},
+    4271: {'name': 'GCS_Naparima_1972', 'altnames': {'Naparima_1972'}},
+    4272: {'name': 'GCS_GD49', 'altnames': {'GD49'}},
+    4273: {'name': 'GCS_NGO_1948', 'altnames': {'NGO_1948'}},
+    4274: {'name': 'GCS_Datum_73', 'altnames': {'Datum_73'}},
+    4275: {'name': 'GCS_NTF', 'altnames': {'NTF'}},
+    4276: {'name': 'GCS_NSWC_9Z_2', 'altnames': {'NSWC_9Z_2'}},
+    4277: {'name': 'GCS_OSGB_1936', 'altnames': {'OSGB_1936'}},
+    4278: {'name': 'GCS_OSGB70', 'altnames': {'OSGB70'}},
+    4279: {'name': 'GCS_OS_SN80', 'altnames': {'OS_SN80'}},
+    4280: {'name': 'GCS_Padang', 'altnames': {'Padang'}},
+    4281: {'name': 'GCS_Palestine_1923', 'altnames': {'Palestine_1923'}},
+    4282: {'name': 'GCS_Pointe_Noire', 'altnames': {'Pointe_Noire'}},
+    4283: {'name': 'GCS_GDA94', 'altnames': {'GDA94'}},
+    4284: {'name': 'GCS_Pulkovo_1942', 'altnames': {'Pulkovo_1942'}},
+    4285: {'name': 'GCS_Qatar', 'altnames': {'Qatar'}},
+    4286: {'name': 'GCS_Qatar_1948', 'altnames': {'Qatar_1948'}},
+    4287: {'name': 'GCS_Qornoq', 'altnames': {'Qornoq'}},
+    4288: {'name': 'GCS_Loma_Quintana', 'altnames': {'Loma_Quintana'}},
+    4289: {'name': 'GCS_Amersfoort', 'altnames': {'Amersfoort'}},
+    4290: {'name': 'GCS_RT38', 'altnames': {'RT38'}},
+    4291: {'name': 'GCS_SAD69', 'altnames': {'SAD69'}},
+    4292: {'name': 'GCS_Sapper_Hill_1943', 'altnames': {'Sapper_Hill_1943'}},
+    4293: {'name': 'GCS_Schwarzeck', 'altnames': {'Schwarzeck'}},
+    4294: {'name': 'GCS_Segora', 'altnames': {'Segora'}},
+    4295: {'name': 'GCS_Serindung', 'altnames': {'Serindung'}},
+    4296: {'name': 'GCS_Sudan', 'altnames': {'Sudan'}},
+    4297: {'name': 'GCS_Tananarive', 'altnames': {'Tananarive'}},
+    4298: {'name': 'GCS_Timbalai_1948', 'altnames': {'Timbalai_1948'}},
+    4299: {'name': 'GCS_TM65', 'altnames': {'TM65'}},
+    4300: {'name': 'GCS_TM75', 'altnames': {'TM75'}},
+    4301: {'name': 'GCS_Tokyo', 'altnames': {'Tokyo'}},
+    4302: {'name': 'GCS_Trinidad_1903', 'altnames': {'Trinidad_1903'}},
+    4303: {'name': 'GCS_TC_1948', 'altnames': {'TC_1948'}},
+    4304: {'name': 'GCS_Voirol_1875', 'altnames': {'Voirol_1875'}},
+    4305: {'name': 'GCS_Voirol_Unifie', 'altnames': {'Voirol_Unifie'}},
+    4306: {'name': 'GCS_Bern_1938', 'altnames': {'Bern_1938'}},
+    4307: {'name': 'GCS_Nord_Sahara_1959', 'altnames': {'Nord_Sahara_1959'}},
+    4308: {'name': 'GCS_Stockholm_1938', 'altnames': {'Stockholm_1938'}},
+    4309: {'name': 'GCS_Yacare', 'altnames': {'Yacare'}},
+    4310: {'name': 'GCS_Yoff', 'altnames': {'Yoff'}},
+    4311: {'name': 'GCS_Zanderij', 'altnames': {'Zanderij'}},
+    4312: {'name': 'GCS_MGI', 'altnames': {'MGI'}},
+    4313: {'name': 'GCS_Belge_1972', 'altnames': {'Belge_1972'}},
+    4314: {'name': 'GCS_DHDN', 'altnames': {'DHDN'}},
+    4315: {'name': 'GCS_Conakry_1905', 'altnames': {'Conakry_1905'}},
+    4322: {'name': 'GCS_WGS_72', 'altnames': {'WGS_72'}},
+    4324: {'name': 'GCS_WGS_72BE', 'altnames': {'WGS_72BE'}},
+    4326: {'name': 'GCS_WGS_84', 'altnames': {'WGS_84'}},
+    4801: {'name': 'GCS_Bern_1898_Bern', 'altnames': {'Bern_1898_Bern'}},
+    4802: {'name': 'GCS_Bogota_Bogota', 'altnames': {'Bogota_Bogota'}},
+    4803: {'name': 'GCS_Lisbon_Lisbon', 'altnames': {'Lisbon_Lisbon'}},
+    4804: {'name': 'GCS_Makassar_Jakarta', 'altnames': {'Makassar_Jakarta'}},
+    4805: {'name': 'GCS_MGI_Ferro', 'altnames': {'MGI_Ferro'}},
+    4806: {'name': 'GCS_Monte_Mario_Rome', 'altnames': {'Monte_Mario_Rome'}},
+    4807: {'name': 'GCS_NTF_Paris', 'altnames': {'NTF_Paris'}},
+    4808: {'name': 'GCS_Padang_Jakarta', 'altnames': {'Padang_Jakarta'}},
+    4809: {'name': 'GCS_Belge_1950_Brussels', 'altnames': {'Belge_1950_Brussels'}},
+    4810: {'name': 'GCS_Tananarive_Paris', 'altnames': {'Tananarive_Paris'}},
+    4811: {'name': 'GCS_Voirol_1875_Paris', 'altnames': {'Voirol_1875_Paris'}},
+    4812: {'name': 'GCS_Voirol_Unifie_Paris', 'altnames': {'Voirol_Unifie_Paris'}},
+    4813: {'name': 'GCS_Batavia_Jakarta', 'altnames': {'Batavia_Jakarta'}},
+    4901: {'name': 'GCS_ATF_Paris', 'altnames': {'ATF_Paris'}},
+    4902: {'name': 'GCS_NDG_Paris', 'altnames': {'NDG_Paris'}},
+    4001: {'name': 'GCSE_Airy1830', 'altnames': {'Airy1830'}},
+    4002: {'name': 'GCSE_AiryModified1849', 'altnames': {'AiryModified1849'}},
+    4003: {'name': 'GCSE_AustralianNationalSpheroid', 'altnames': {'AustralianNationalSpheroid'}},
+    4004: {'name': 'GCSE_Bessel1841', 'altnames': {'Bessel1841'}},
+    4005: {'name': 'GCSE_BesselModified', 'altnames': {'BesselModified'}},
+    4006: {'name': 'GCSE_BesselNamibia', 'altnames': {'BesselNamibia'}},
+    4007: {'name': 'GCSE_Clarke1858', 'altnames': {'Clarke1858'}},
+    4008: {'name': 'GCSE_Clarke1866', 'altnames': {'Clarke1866'}},
+    4009: {'name': 'GCSE_Clarke1866Michigan', 'altnames': {'Clarke1866Michigan'}},
+    4010: {'name': 'GCSE_Clarke1880_Benoit', 'altnames': {'Clarke1880_Benoit'}},
+    4011: {'name': 'GCSE_Clarke1880_IGN', 'altnames': {'Clarke1880_IGN'}},
+    4012: {'name': 'GCSE_Clarke1880_RGS', 'altnames': {'Clarke1880_RGS'}},
+    4013: {'name': 'GCSE_Clarke1880_Arc', 'altnames': {'Clarke1880_Arc'}},
+    4014: {'name': 'GCSE_Clarke1880_SGA1922', 'altnames': {'Clarke1880_SGA1922'}},
+    4015: {'name': 'GCSE_Everest1830_1937Adjustment', 'altnames': {'Everest1830_1937Adjustment'}},
+    4016: {'name': 'GCSE_Everest1830_1967Definition', 'altnames': {'Everest1830_1967Definition'}},
+    4017: {'name': 'GCSE_Everest1830_1975Definition', 'altnames': {'Everest1830_1975Definition'}},
+    4018: {'name': 'GCSE_Everest1830Modified', 'altnames': {'Everest1830Modified'}},
+    4019: {'name': 'GCSE_GRS1980', 'altnames': {'GRS1980'}},
+    4020: {'name': 'GCSE_Helmert1906', 'altnames': {'Helmert1906'}},
+    4021: {'name': 'GCSE_IndonesianNationalSpheroid', 'altnames': {'IndonesianNationalSpheroid'}},
+    4022: {'name': 'GCSE_International1924', 'altnames': {'International1924'}},
+    4023: {'name': 'GCSE_International1967', 'altnames': {'International1967'}},
+    4024: {'name': 'GCSE_Krassowsky1940', 'altnames': {'Krassowsky1940'}},
+    4025: {'name': 'GCSE_NWL9D', 'altnames': {'NWL9D'}},
+    4026: {'name': 'GCSE_NWL10D', 'altnames': {'NWL10D'}},
+    4027: {'name': 'GCSE_Plessis1817', 'altnames': {'Plessis1817'}},
+    4028: {'name': 'GCSE_Struve1860', 'altnames': {'Struve1860'}},
+    4029: {'name': 'GCSE_WarOffice', 'altnames': {'WarOffice'}},
+    4030: {'name': 'GCSE_WGS84', 'altnames': {'WGS84'}},
+    4031: {'name': 'GCSE_GEM10C', 'altnames': {'GEM10C'}},
+    4032: {'name': 'GCSE_OSU86F', 'altnames': {'OSU86F'}},
+    4033: {'name': 'GCSE_OSU91A', 'altnames': {'OSU91A'}},
+    4034: {'name': 'GCSE_Clarke1880', 'altnames': {'Clarke1880'}},
+    4035: {'name': 'GCSE_Sphere', 'altnames': {'Sphere'}},
+})
+
+
+GeoTiffAngularUnits = TiffConstantSet('GeoTiffAngularUnits', {
+    9101: {'name': 'Angular_Radian', 'altnames': {'radian', 'rad'}},
+    9102: {'name': 'Angular_Degree', 'altnames': {'degree', 'deg'}},
+    9103: {'name': 'Angular_Arc_Minute', 'altnames': {'arc_minute', 'arcminute'}},
+    9104: {'name': 'Angular_Arc_Second', 'altnames': {'arc_second', 'arcsecond'}},
+    9105: {'name': 'Angular_Grad', 'altnames': {'grad'}},
+    9106: {'name': 'Angular_Gon', 'altnames': {'gon'}},
+    9107: {'name': 'Angular_DMS', 'altnames': {'dms'}},
+    9108: {'name': 'Angular_DMS_Hemisphere', 'altnames': {'dms_hemisphere', 'dms_hemi'}},
+})
+
+
+GeoTiffLinearUnits = TiffConstantSet('GeoTiffLinearUnits', {
+    9001: {'name': 'Linear_Meter', 'altnames': {'m', 'meter', 'meters'}},
+    9002: {'name': 'Linear_Foot', 'altnames': {'ft', 'foot', 'feet'}},
+    9003: {'name': 'Linear_Foot_US_Survey', 'altnames': {'ft_us'}},
+    9004: {'name': 'Linear_Foot_Modified_American', 'altnames': {'ft_am'}},
+    9005: {'name': 'Linear_Foot_Clarke', 'altnames': {'ft_cl'}},
+    9006: {'name': 'Linear_Foot_Indian', 'altnames': {'ft_in'}},
+    9007: {'name': 'Linear_Link', 'altnames': {'link'}},
+    9008: {'name': 'Linear_Link_Benoit', 'altnames': {'link_bn'}},
+    9009: {'name': 'Linear_Link_Sears', 'altnames': {'link_sr'}},
+    9010: {'name': 'Linear_Chain_Benoit', 'altnames': {'chain_bn'}},
+    9011: {'name': 'Linear_Chain_Sears', 'altnames': {'chain_sr'}},
+    9012: {'name': 'Linear_Yard_Sears', 'altnames': {'yd_sr'}},
+    9013: {'name': 'Linear_Yard_Indian', 'altnames': {'yd_in'}},
+    9014: {'name': 'Linear_Fathom', 'altnames': {'fathom'}},
+    9015: {'name': 'Linear_Mile_International_Nautical', 'altnames': {'mi', 'mile', 'miles'}},
path: tifftools/constants.py
user: login: manthey
body: Nautical Miles shouldn't be abbreviated as mi/mile/miles, but should be kt/knot/knots/nmile/nmiles.  Nautical miles are really the arc length of 1 arcminute at the surface of the earth.
created_at: 2025-04-16T14:00:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -548,14 +548,256 @@ def get_or_create_tag(key, tagSet=None, upperLimit=True, **tagOptions):
     3094: {'name': 'ProjAzimuthAngle', 'altnames': {'ProjAzimuthAngleGeoKey'}, 'datatype': Datatype.DOUBLE},
     3095: {'name': 'ProjStraightVertPoleLong', 'altnames': {'ProjStraightVertPoleLongGeoKey'}, 'datatype': Datatype.DOUBLE},
     3096: {'name': 'ProjRectifiedGridAngle', 'altnames': {'ProjRectifiedGridAngleGeoKey'}, 'datatype': Datatype.DOUBLE},
-    4096: {'name': 'VerticalCSType', 'altnames': {'VerticalCSTypeGeoKey'}, 'datatype': Datatype.DOUBLE},
+    4096: {'name': 'VerticalCSType', 'altnames': {'VerticalCSTypeGeoKey'}, 'datatype': Datatype.SHORT},
     4097: {'name': 'VerticalCitation', 'altnames': {'VerticalCitationGeoKey'}, 'datatype': Datatype.ASCII},
-    4098: {'name': 'VerticalDatum', 'altnames': {'VerticalDatumGeoKey'}, 'datatype': Datatype.DOUBLE},
-    4099: {'name': 'VerticalUnits', 'altnames': {'VerticalUnitsGeoKey'}, 'datatype': Datatype.DOUBLE},
+    4098: {'name': 'VerticalDatum', 'altnames': {'VerticalDatumGeoKey'}, 'datatype': Datatype.SHORT},
+    4099: {'name': 'VerticalUnits', 'altnames': {'VerticalUnitsGeoKey'}, 'datatype': Datatype.SHORT},
     5120: {'name': 'CoordinateEpoch', 'altnames': {'CoordinateEpochGeoKey'}, 'datatype': Datatype.DOUBLE},
 })
 
 
+GeoTiffCSTypes = TiffConstantSet('GeoTiffCSTypes', {
+    4201: {'name': 'GCS_Adindan', 'altnames': {'Adindan'}},
+    4202: {'name': 'GCS_AGD66', 'altnames': {'AGD66'}},
+    4203: {'name': 'GCS_AGD84', 'altnames': {'AGD84'}},
+    4204: {'name': 'GCS_Ain_el_Abd', 'altnames': {'Ain_el_Abd'}},
+    4205: {'name': 'GCS_Afgooye', 'altnames': {'Afgooye'}},
+    4206: {'name': 'GCS_Agadez', 'altnames': {'Agadez'}},
+    4207: {'name': 'GCS_Lisbon', 'altnames': {'Lisbon'}},
+    4208: {'name': 'GCS_Aratu', 'altnames': {'Aratu'}},
+    4209: {'name': 'GCS_Arc_1950', 'altnames': {'Arc_1950'}},
+    4210: {'name': 'GCS_Arc_1960', 'altnames': {'Arc_1960'}},
+    4211: {'name': 'GCS_Batavia', 'altnames': {'Batavia'}},
+    4212: {'name': 'GCS_Barbados', 'altnames': {'Barbados'}},
+    4213: {'name': 'GCS_Beduaram', 'altnames': {'Beduaram'}},
+    4214: {'name': 'GCS_Beijing_1954', 'altnames': {'Beijing_1954'}},
+    4215: {'name': 'GCS_Belge_1950', 'altnames': {'Belge_1950'}},
+    4216: {'name': 'GCS_Bermuda_1957', 'altnames': {'Bermuda_1957'}},
+    4217: {'name': 'GCS_Bern_1898', 'altnames': {'Bern_1898'}},
+    4218: {'name': 'GCS_Bogota', 'altnames': {'Bogota'}},
+    4219: {'name': 'GCS_Bukit_Rimpah', 'altnames': {'Bukit_Rimpah'}},
+    4220: {'name': 'GCS_Camacupa', 'altnames': {'Camacupa'}},
+    4221: {'name': 'GCS_Campo_Inchauspe', 'altnames': {'Campo_Inchauspe'}},
+    4222: {'name': 'GCS_Cape', 'altnames': {'Cape'}},
+    4223: {'name': 'GCS_Carthage', 'altnames': {'Carthage'}},
+    4224: {'name': 'GCS_Chua', 'altnames': {'Chua'}},
+    4225: {'name': 'GCS_Corrego_Alegre', 'altnames': {'Corrego_Alegre'}},
+    4226: {'name': 'GCS_Cote_d_Ivoire', 'altnames': {'Cote_d_Ivoire'}},
+    4227: {'name': 'GCS_Deir_ez_Zor', 'altnames': {'Deir_ez_Zor'}},
+    4228: {'name': 'GCS_Douala', 'altnames': {'Douala'}},
+    4229: {'name': 'GCS_Egypt_1907', 'altnames': {'Egypt_1907'}},
+    4230: {'name': 'GCS_ED50', 'altnames': {'ED50'}},
+    4231: {'name': 'GCS_ED87', 'altnames': {'ED87'}},
+    4232: {'name': 'GCS_Fahud', 'altnames': {'Fahud'}},
+    4233: {'name': 'GCS_Gandajika_1970', 'altnames': {'Gandajika_1970'}},
+    4234: {'name': 'GCS_Garoua', 'altnames': {'Garoua'}},
+    4235: {'name': 'GCS_Guyane_Francaise', 'altnames': {'Guyane_Francaise'}},
+    4236: {'name': 'GCS_Hu_Tzu_Shan', 'altnames': {'Hu_Tzu_Shan'}},
+    4237: {'name': 'GCS_HD72', 'altnames': {'HD72'}},
+    4238: {'name': 'GCS_ID74', 'altnames': {'ID74'}},
+    4239: {'name': 'GCS_Indian_1954', 'altnames': {'Indian_1954'}},
+    4240: {'name': 'GCS_Indian_1975', 'altnames': {'Indian_1975'}},
+    4241: {'name': 'GCS_Jamaica_1875', 'altnames': {'Jamaica_1875'}},
+    4242: {'name': 'GCS_JAD69', 'altnames': {'JAD69'}},
+    4243: {'name': 'GCS_Kalianpur', 'altnames': {'Kalianpur'}},
+    4244: {'name': 'GCS_Kandawala', 'altnames': {'Kandawala'}},
+    4245: {'name': 'GCS_Kertau', 'altnames': {'Kertau'}},
+    4246: {'name': 'GCS_KOC', 'altnames': {'KOC'}},
+    4247: {'name': 'GCS_La_Canoa', 'altnames': {'La_Canoa'}},
+    4248: {'name': 'GCS_PSAD56', 'altnames': {'PSAD56'}},
+    4249: {'name': 'GCS_Lake', 'altnames': {'Lake'}},
+    4250: {'name': 'GCS_Leigon', 'altnames': {'Leigon'}},
+    4251: {'name': 'GCS_Liberia_1964', 'altnames': {'Liberia_1964'}},
+    4252: {'name': 'GCS_Lome', 'altnames': {'Lome'}},
+    4253: {'name': 'GCS_Luzon_1911', 'altnames': {'Luzon_1911'}},
+    4254: {'name': 'GCS_Hito_XVIII_1963', 'altnames': {'Hito_XVIII_1963'}},
+    4255: {'name': 'GCS_Herat_North', 'altnames': {'Herat_North'}},
+    4256: {'name': 'GCS_Mahe_1971', 'altnames': {'Mahe_1971'}},
+    4257: {'name': 'GCS_Makassar', 'altnames': {'Makassar'}},
+    4258: {'name': 'GCS_EUREF89', 'altnames': {'EUREF89'}},
+    4259: {'name': 'GCS_Malongo_1987', 'altnames': {'Malongo_1987'}},
+    4260: {'name': 'GCS_Manoca', 'altnames': {'Manoca'}},
+    4261: {'name': 'GCS_Merchich', 'altnames': {'Merchich'}},
+    4262: {'name': 'GCS_Massawa', 'altnames': {'Massawa'}},
+    4263: {'name': 'GCS_Minna', 'altnames': {'Minna'}},
+    4264: {'name': 'GCS_Mhast', 'altnames': {'Mhast'}},
+    4265: {'name': 'GCS_Monte_Mario', 'altnames': {'Monte_Mario'}},
+    4266: {'name': 'GCS_M_poraloko', 'altnames': {'M_poraloko'}},
+    4267: {'name': 'GCS_NAD27', 'altnames': {'NAD27'}},
+    4268: {'name': 'GCS_NAD_Michigan', 'altnames': {'NAD_Michigan'}},
+    4269: {'name': 'GCS_NAD83', 'altnames': {'NAD83'}},
+    4270: {'name': 'GCS_Nahrwan_1967', 'altnames': {'Nahrwan_1967'}},
+    4271: {'name': 'GCS_Naparima_1972', 'altnames': {'Naparima_1972'}},
+    4272: {'name': 'GCS_GD49', 'altnames': {'GD49'}},
+    4273: {'name': 'GCS_NGO_1948', 'altnames': {'NGO_1948'}},
+    4274: {'name': 'GCS_Datum_73', 'altnames': {'Datum_73'}},
+    4275: {'name': 'GCS_NTF', 'altnames': {'NTF'}},
+    4276: {'name': 'GCS_NSWC_9Z_2', 'altnames': {'NSWC_9Z_2'}},
+    4277: {'name': 'GCS_OSGB_1936', 'altnames': {'OSGB_1936'}},
+    4278: {'name': 'GCS_OSGB70', 'altnames': {'OSGB70'}},
+    4279: {'name': 'GCS_OS_SN80', 'altnames': {'OS_SN80'}},
+    4280: {'name': 'GCS_Padang', 'altnames': {'Padang'}},
+    4281: {'name': 'GCS_Palestine_1923', 'altnames': {'Palestine_1923'}},
+    4282: {'name': 'GCS_Pointe_Noire', 'altnames': {'Pointe_Noire'}},
+    4283: {'name': 'GCS_GDA94', 'altnames': {'GDA94'}},
+    4284: {'name': 'GCS_Pulkovo_1942', 'altnames': {'Pulkovo_1942'}},
+    4285: {'name': 'GCS_Qatar', 'altnames': {'Qatar'}},
+    4286: {'name': 'GCS_Qatar_1948', 'altnames': {'Qatar_1948'}},
+    4287: {'name': 'GCS_Qornoq', 'altnames': {'Qornoq'}},
+    4288: {'name': 'GCS_Loma_Quintana', 'altnames': {'Loma_Quintana'}},
+    4289: {'name': 'GCS_Amersfoort', 'altnames': {'Amersfoort'}},
+    4290: {'name': 'GCS_RT38', 'altnames': {'RT38'}},
+    4291: {'name': 'GCS_SAD69', 'altnames': {'SAD69'}},
+    4292: {'name': 'GCS_Sapper_Hill_1943', 'altnames': {'Sapper_Hill_1943'}},
+    4293: {'name': 'GCS_Schwarzeck', 'altnames': {'Schwarzeck'}},
+    4294: {'name': 'GCS_Segora', 'altnames': {'Segora'}},
+    4295: {'name': 'GCS_Serindung', 'altnames': {'Serindung'}},
+    4296: {'name': 'GCS_Sudan', 'altnames': {'Sudan'}},
+    4297: {'name': 'GCS_Tananarive', 'altnames': {'Tananarive'}},
+    4298: {'name': 'GCS_Timbalai_1948', 'altnames': {'Timbalai_1948'}},
+    4299: {'name': 'GCS_TM65', 'altnames': {'TM65'}},
+    4300: {'name': 'GCS_TM75', 'altnames': {'TM75'}},
+    4301: {'name': 'GCS_Tokyo', 'altnames': {'Tokyo'}},
+    4302: {'name': 'GCS_Trinidad_1903', 'altnames': {'Trinidad_1903'}},
+    4303: {'name': 'GCS_TC_1948', 'altnames': {'TC_1948'}},
+    4304: {'name': 'GCS_Voirol_1875', 'altnames': {'Voirol_1875'}},
+    4305: {'name': 'GCS_Voirol_Unifie', 'altnames': {'Voirol_Unifie'}},
+    4306: {'name': 'GCS_Bern_1938', 'altnames': {'Bern_1938'}},
+    4307: {'name': 'GCS_Nord_Sahara_1959', 'altnames': {'Nord_Sahara_1959'}},
+    4308: {'name': 'GCS_Stockholm_1938', 'altnames': {'Stockholm_1938'}},
+    4309: {'name': 'GCS_Yacare', 'altnames': {'Yacare'}},
+    4310: {'name': 'GCS_Yoff', 'altnames': {'Yoff'}},
+    4311: {'name': 'GCS_Zanderij', 'altnames': {'Zanderij'}},
+    4312: {'name': 'GCS_MGI', 'altnames': {'MGI'}},
+    4313: {'name': 'GCS_Belge_1972', 'altnames': {'Belge_1972'}},
+    4314: {'name': 'GCS_DHDN', 'altnames': {'DHDN'}},
+    4315: {'name': 'GCS_Conakry_1905', 'altnames': {'Conakry_1905'}},
+    4322: {'name': 'GCS_WGS_72', 'altnames': {'WGS_72'}},
+    4324: {'name': 'GCS_WGS_72BE', 'altnames': {'WGS_72BE'}},
+    4326: {'name': 'GCS_WGS_84', 'altnames': {'WGS_84'}},
+    4801: {'name': 'GCS_Bern_1898_Bern', 'altnames': {'Bern_1898_Bern'}},
+    4802: {'name': 'GCS_Bogota_Bogota', 'altnames': {'Bogota_Bogota'}},
+    4803: {'name': 'GCS_Lisbon_Lisbon', 'altnames': {'Lisbon_Lisbon'}},
+    4804: {'name': 'GCS_Makassar_Jakarta', 'altnames': {'Makassar_Jakarta'}},
+    4805: {'name': 'GCS_MGI_Ferro', 'altnames': {'MGI_Ferro'}},
+    4806: {'name': 'GCS_Monte_Mario_Rome', 'altnames': {'Monte_Mario_Rome'}},
+    4807: {'name': 'GCS_NTF_Paris', 'altnames': {'NTF_Paris'}},
+    4808: {'name': 'GCS_Padang_Jakarta', 'altnames': {'Padang_Jakarta'}},
+    4809: {'name': 'GCS_Belge_1950_Brussels', 'altnames': {'Belge_1950_Brussels'}},
+    4810: {'name': 'GCS_Tananarive_Paris', 'altnames': {'Tananarive_Paris'}},
+    4811: {'name': 'GCS_Voirol_1875_Paris', 'altnames': {'Voirol_1875_Paris'}},
+    4812: {'name': 'GCS_Voirol_Unifie_Paris', 'altnames': {'Voirol_Unifie_Paris'}},
+    4813: {'name': 'GCS_Batavia_Jakarta', 'altnames': {'Batavia_Jakarta'}},
+    4901: {'name': 'GCS_ATF_Paris', 'altnames': {'ATF_Paris'}},
+    4902: {'name': 'GCS_NDG_Paris', 'altnames': {'NDG_Paris'}},
+    4001: {'name': 'GCSE_Airy1830', 'altnames': {'Airy1830'}},
+    4002: {'name': 'GCSE_AiryModified1849', 'altnames': {'AiryModified1849'}},
+    4003: {'name': 'GCSE_AustralianNationalSpheroid', 'altnames': {'AustralianNationalSpheroid'}},
+    4004: {'name': 'GCSE_Bessel1841', 'altnames': {'Bessel1841'}},
+    4005: {'name': 'GCSE_BesselModified', 'altnames': {'BesselModified'}},
+    4006: {'name': 'GCSE_BesselNamibia', 'altnames': {'BesselNamibia'}},
+    4007: {'name': 'GCSE_Clarke1858', 'altnames': {'Clarke1858'}},
+    4008: {'name': 'GCSE_Clarke1866', 'altnames': {'Clarke1866'}},
+    4009: {'name': 'GCSE_Clarke1866Michigan', 'altnames': {'Clarke1866Michigan'}},
+    4010: {'name': 'GCSE_Clarke1880_Benoit', 'altnames': {'Clarke1880_Benoit'}},
+    4011: {'name': 'GCSE_Clarke1880_IGN', 'altnames': {'Clarke1880_IGN'}},
+    4012: {'name': 'GCSE_Clarke1880_RGS', 'altnames': {'Clarke1880_RGS'}},
+    4013: {'name': 'GCSE_Clarke1880_Arc', 'altnames': {'Clarke1880_Arc'}},
+    4014: {'name': 'GCSE_Clarke1880_SGA1922', 'altnames': {'Clarke1880_SGA1922'}},
+    4015: {'name': 'GCSE_Everest1830_1937Adjustment', 'altnames': {'Everest1830_1937Adjustment'}},
+    4016: {'name': 'GCSE_Everest1830_1967Definition', 'altnames': {'Everest1830_1967Definition'}},
+    4017: {'name': 'GCSE_Everest1830_1975Definition', 'altnames': {'Everest1830_1975Definition'}},
+    4018: {'name': 'GCSE_Everest1830Modified', 'altnames': {'Everest1830Modified'}},
+    4019: {'name': 'GCSE_GRS1980', 'altnames': {'GRS1980'}},
+    4020: {'name': 'GCSE_Helmert1906', 'altnames': {'Helmert1906'}},
+    4021: {'name': 'GCSE_IndonesianNationalSpheroid', 'altnames': {'IndonesianNationalSpheroid'}},
+    4022: {'name': 'GCSE_International1924', 'altnames': {'International1924'}},
+    4023: {'name': 'GCSE_International1967', 'altnames': {'International1967'}},
+    4024: {'name': 'GCSE_Krassowsky1940', 'altnames': {'Krassowsky1940'}},
+    4025: {'name': 'GCSE_NWL9D', 'altnames': {'NWL9D'}},
+    4026: {'name': 'GCSE_NWL10D', 'altnames': {'NWL10D'}},
+    4027: {'name': 'GCSE_Plessis1817', 'altnames': {'Plessis1817'}},
+    4028: {'name': 'GCSE_Struve1860', 'altnames': {'Struve1860'}},
+    4029: {'name': 'GCSE_WarOffice', 'altnames': {'WarOffice'}},
+    4030: {'name': 'GCSE_WGS84', 'altnames': {'WGS84'}},
+    4031: {'name': 'GCSE_GEM10C', 'altnames': {'GEM10C'}},
+    4032: {'name': 'GCSE_OSU86F', 'altnames': {'OSU86F'}},
+    4033: {'name': 'GCSE_OSU91A', 'altnames': {'OSU91A'}},
+    4034: {'name': 'GCSE_Clarke1880', 'altnames': {'Clarke1880'}},
+    4035: {'name': 'GCSE_Sphere', 'altnames': {'Sphere'}},
+})
+
+
+GeoTiffAngularUnits = TiffConstantSet('GeoTiffAngularUnits', {
+    9101: {'name': 'Angular_Radian', 'altnames': {'radian', 'rad'}},
+    9102: {'name': 'Angular_Degree', 'altnames': {'degree', 'deg'}},
+    9103: {'name': 'Angular_Arc_Minute', 'altnames': {'arc_minute', 'arcminute'}},
+    9104: {'name': 'Angular_Arc_Second', 'altnames': {'arc_second', 'arcsecond'}},
+    9105: {'name': 'Angular_Grad', 'altnames': {'grad'}},
+    9106: {'name': 'Angular_Gon', 'altnames': {'gon'}},
+    9107: {'name': 'Angular_DMS', 'altnames': {'dms'}},
+    9108: {'name': 'Angular_DMS_Hemisphere', 'altnames': {'dms_hemisphere', 'dms_hemi'}},
+})
+
+
+GeoTiffLinearUnits = TiffConstantSet('GeoTiffLinearUnits', {
+    9001: {'name': 'Linear_Meter', 'altnames': {'m', 'meter', 'meters'}},
+    9002: {'name': 'Linear_Foot', 'altnames': {'ft', 'foot', 'feet'}},
+    9003: {'name': 'Linear_Foot_US_Survey', 'altnames': {'ft_us'}},
+    9004: {'name': 'Linear_Foot_Modified_American', 'altnames': {'ft_am'}},
+    9005: {'name': 'Linear_Foot_Clarke', 'altnames': {'ft_cl'}},
+    9006: {'name': 'Linear_Foot_Indian', 'altnames': {'ft_in'}},
+    9007: {'name': 'Linear_Link', 'altnames': {'link'}},
+    9008: {'name': 'Linear_Link_Benoit', 'altnames': {'link_bn'}},
+    9009: {'name': 'Linear_Link_Sears', 'altnames': {'link_sr'}},
+    9010: {'name': 'Linear_Chain_Benoit', 'altnames': {'chain_bn'}},
+    9011: {'name': 'Linear_Chain_Sears', 'altnames': {'chain_sr'}},
+    9012: {'name': 'Linear_Yard_Sears', 'altnames': {'yd_sr'}},
+    9013: {'name': 'Linear_Yard_Indian', 'altnames': {'yd_in'}},
+    9014: {'name': 'Linear_Fathom', 'altnames': {'fathom'}},
+    9015: {'name': 'Linear_Mile_International_Nautical', 'altnames': {'mi', 'mile', 'miles'}},
path: tifftools/constants.py
user: login: manthey
body: They are also sometimes abbreviated NM (but since we are case-insensitive, that looks far too much like an SI unit).
created_at: 2025-04-16T14:03:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -572,6 +584,163 @@ def tiff_set(source, output=None, overwrite=False, setlist=None, unset=None,
         _tiff_set(source, output, setlist, unset, setfrom, overwrite=overwrite, **kwargs)
 
 
+def _set_projection(source, projection, **kwargs):
+    """
+    Set geospatial projection in a tiff file.
+
+    :param source: the source path.
+    :param projection: the projection, expressed as an integer representing an EPSG code or
+        expressed as a string that can be interpreted by `pyproj.CRS.from_string()`.
+    :param kwargs: additional arguments to pass to `tifftools.commands.tiff_set()`.
+    """
+    geokeys = None
+    if isinstance(projection, str) and 'epsg:' in projection.lower():
+        projection = int(projection.lower().replace('epsg:', ''))
+    if isinstance(projection, int):
+        for geotype, codes in EPSGTypes.items():
+            if projection in codes:
+                geokeys = dict(
+                    GTModelType=(
+                        3 if geotype == 'geocentric' else
+                        2 if geotype == 'geographic_2d' else 1
+                    ),
+                    GTRasterType=1,
+                    GeographicType=projection,
+                )
+
+    if geokeys is None:
+        try:
+            import pyproj
+        except ImportError:
+            msg = (
+                f'pyproj is required to interpret projection {projection}. '
+                'Please install tifftools[geo].'
+            )
+            raise TifftoolsError(msg)
+
+        proj = pyproj.CRS.from_string(projection)
+        proj_dict = proj.to_dict()
+        angular_units = GeoTiffAngularUnits.get(proj.prime_meridian.unit_name).value
+        geokeys = dict(
+            GTModelType=3 if proj.is_geocentric else 2 if proj.is_geographic else 1,
+            GTRasterType=1,
+            GeographicType=proj.to_epsg(),
+            GeogCitation=proj_dict.get('datum'),
+            GeogAngularUnits=angular_units,
+            GeogSemiMajorAxis=proj.ellipsoid.semi_major_metre,
+            GeogInvFlattening=proj.ellipsoid.inverse_flattening,
+            ProjStdParallel1=proj_dict.get('lat_1'),
+            ProjStdParallel2=proj_dict.get('lat_2'),
+            ProjNatOriginLat=proj_dict.get('lat_0'),
+            ProjNatOriginLong=proj_dict.get('lon_0'),
+        )
+        if proj.coordinate_operation:
+            method_name = proj.coordinate_operation.method_name
+            transformation = GeoTiffTransformations.get(method_name.replace(' ', ''))
+            geokeys['GTCitation'] = method_name
+            geokeys['ProjCoordTrans'] = transformation
+        if proj_dict.get('units'):
+            linear_units = GeoTiffLinearUnits.get(proj_dict.get('units')).value
+            geokeys['ProjLinearUnits'] = linear_units
+
+    geokeys = {k: v for k, v in geokeys.items() if v is not None}
+
+    key_directory_version = 1
+    key_revision = 1
+    minor_revision = 0
+    number_of_keys = len(geokeys)
+    header = ' '.join(str(v) for v in [
+        key_directory_version,
+        key_revision,
+        minor_revision,
+        number_of_keys,
+    ])
+
+    geokey_tag = f'{header}'
+    doubles = []
+    asciis = []
+    for geokey, value in geokeys.items():
+        key_spec = GeoTiffGeoKey.get(geokey)
+        key_id = key_spec.value
+        data_type = key_spec.datatype
+        value_count = 1
+        if data_type == Datatype.SHORT:
+            tag_location = 0
+            value_offset = int(value)
+        elif data_type == Datatype.DOUBLE:
+            tag_location = Tag.get('GeoDoubleParamsTag').value
+            value_offset = len(doubles)
+            doubles.append(float(value))
+        else:
+            tag_location = Tag.get('GeoAsciiParamsTag').value
+            value_offset = len(asciis)
+            asciis.append(str(value))
+        geokey_tag += f' {key_id} {tag_location} {value_count} {value_offset}'
+    doubles_tag = ' '.join(str(v) for v in doubles)
path: tifftools/commands.py
user: login: manthey
body: This is a clever way to leverage the existing functions.  At first I thought that maybe we should try to avoid this conversion to string and back to float later, but modern python is numerically stable, so this won't introduce any variation or error.
created_at: 2025-04-16T14:19:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -548,14 +548,256 @@ def get_or_create_tag(key, tagSet=None, upperLimit=True, **tagOptions):
     3094: {'name': 'ProjAzimuthAngle', 'altnames': {'ProjAzimuthAngleGeoKey'}, 'datatype': Datatype.DOUBLE},
     3095: {'name': 'ProjStraightVertPoleLong', 'altnames': {'ProjStraightVertPoleLongGeoKey'}, 'datatype': Datatype.DOUBLE},
     3096: {'name': 'ProjRectifiedGridAngle', 'altnames': {'ProjRectifiedGridAngleGeoKey'}, 'datatype': Datatype.DOUBLE},
-    4096: {'name': 'VerticalCSType', 'altnames': {'VerticalCSTypeGeoKey'}, 'datatype': Datatype.DOUBLE},
+    4096: {'name': 'VerticalCSType', 'altnames': {'VerticalCSTypeGeoKey'}, 'datatype': Datatype.SHORT},
     4097: {'name': 'VerticalCitation', 'altnames': {'VerticalCitationGeoKey'}, 'datatype': Datatype.ASCII},
-    4098: {'name': 'VerticalDatum', 'altnames': {'VerticalDatumGeoKey'}, 'datatype': Datatype.DOUBLE},
-    4099: {'name': 'VerticalUnits', 'altnames': {'VerticalUnitsGeoKey'}, 'datatype': Datatype.DOUBLE},
+    4098: {'name': 'VerticalDatum', 'altnames': {'VerticalDatumGeoKey'}, 'datatype': Datatype.SHORT},
+    4099: {'name': 'VerticalUnits', 'altnames': {'VerticalUnitsGeoKey'}, 'datatype': Datatype.SHORT},
     5120: {'name': 'CoordinateEpoch', 'altnames': {'CoordinateEpochGeoKey'}, 'datatype': Datatype.DOUBLE},
 })
 
 
+GeoTiffCSTypes = TiffConstantSet('GeoTiffCSTypes', {
+    4201: {'name': 'GCS_Adindan', 'altnames': {'Adindan'}},
+    4202: {'name': 'GCS_AGD66', 'altnames': {'AGD66'}},
+    4203: {'name': 'GCS_AGD84', 'altnames': {'AGD84'}},
+    4204: {'name': 'GCS_Ain_el_Abd', 'altnames': {'Ain_el_Abd'}},
+    4205: {'name': 'GCS_Afgooye', 'altnames': {'Afgooye'}},
+    4206: {'name': 'GCS_Agadez', 'altnames': {'Agadez'}},
+    4207: {'name': 'GCS_Lisbon', 'altnames': {'Lisbon'}},
+    4208: {'name': 'GCS_Aratu', 'altnames': {'Aratu'}},
+    4209: {'name': 'GCS_Arc_1950', 'altnames': {'Arc_1950'}},
+    4210: {'name': 'GCS_Arc_1960', 'altnames': {'Arc_1960'}},
+    4211: {'name': 'GCS_Batavia', 'altnames': {'Batavia'}},
+    4212: {'name': 'GCS_Barbados', 'altnames': {'Barbados'}},
+    4213: {'name': 'GCS_Beduaram', 'altnames': {'Beduaram'}},
+    4214: {'name': 'GCS_Beijing_1954', 'altnames': {'Beijing_1954'}},
+    4215: {'name': 'GCS_Belge_1950', 'altnames': {'Belge_1950'}},
+    4216: {'name': 'GCS_Bermuda_1957', 'altnames': {'Bermuda_1957'}},
+    4217: {'name': 'GCS_Bern_1898', 'altnames': {'Bern_1898'}},
+    4218: {'name': 'GCS_Bogota', 'altnames': {'Bogota'}},
+    4219: {'name': 'GCS_Bukit_Rimpah', 'altnames': {'Bukit_Rimpah'}},
+    4220: {'name': 'GCS_Camacupa', 'altnames': {'Camacupa'}},
+    4221: {'name': 'GCS_Campo_Inchauspe', 'altnames': {'Campo_Inchauspe'}},
+    4222: {'name': 'GCS_Cape', 'altnames': {'Cape'}},
+    4223: {'name': 'GCS_Carthage', 'altnames': {'Carthage'}},
+    4224: {'name': 'GCS_Chua', 'altnames': {'Chua'}},
+    4225: {'name': 'GCS_Corrego_Alegre', 'altnames': {'Corrego_Alegre'}},
+    4226: {'name': 'GCS_Cote_d_Ivoire', 'altnames': {'Cote_d_Ivoire'}},
+    4227: {'name': 'GCS_Deir_ez_Zor', 'altnames': {'Deir_ez_Zor'}},
+    4228: {'name': 'GCS_Douala', 'altnames': {'Douala'}},
+    4229: {'name': 'GCS_Egypt_1907', 'altnames': {'Egypt_1907'}},
+    4230: {'name': 'GCS_ED50', 'altnames': {'ED50'}},
+    4231: {'name': 'GCS_ED87', 'altnames': {'ED87'}},
+    4232: {'name': 'GCS_Fahud', 'altnames': {'Fahud'}},
+    4233: {'name': 'GCS_Gandajika_1970', 'altnames': {'Gandajika_1970'}},
+    4234: {'name': 'GCS_Garoua', 'altnames': {'Garoua'}},
+    4235: {'name': 'GCS_Guyane_Francaise', 'altnames': {'Guyane_Francaise'}},
+    4236: {'name': 'GCS_Hu_Tzu_Shan', 'altnames': {'Hu_Tzu_Shan'}},
+    4237: {'name': 'GCS_HD72', 'altnames': {'HD72'}},
+    4238: {'name': 'GCS_ID74', 'altnames': {'ID74'}},
+    4239: {'name': 'GCS_Indian_1954', 'altnames': {'Indian_1954'}},
+    4240: {'name': 'GCS_Indian_1975', 'altnames': {'Indian_1975'}},
+    4241: {'name': 'GCS_Jamaica_1875', 'altnames': {'Jamaica_1875'}},
+    4242: {'name': 'GCS_JAD69', 'altnames': {'JAD69'}},
+    4243: {'name': 'GCS_Kalianpur', 'altnames': {'Kalianpur'}},
+    4244: {'name': 'GCS_Kandawala', 'altnames': {'Kandawala'}},
+    4245: {'name': 'GCS_Kertau', 'altnames': {'Kertau'}},
+    4246: {'name': 'GCS_KOC', 'altnames': {'KOC'}},
+    4247: {'name': 'GCS_La_Canoa', 'altnames': {'La_Canoa'}},
+    4248: {'name': 'GCS_PSAD56', 'altnames': {'PSAD56'}},
+    4249: {'name': 'GCS_Lake', 'altnames': {'Lake'}},
+    4250: {'name': 'GCS_Leigon', 'altnames': {'Leigon'}},
+    4251: {'name': 'GCS_Liberia_1964', 'altnames': {'Liberia_1964'}},
+    4252: {'name': 'GCS_Lome', 'altnames': {'Lome'}},
+    4253: {'name': 'GCS_Luzon_1911', 'altnames': {'Luzon_1911'}},
+    4254: {'name': 'GCS_Hito_XVIII_1963', 'altnames': {'Hito_XVIII_1963'}},
+    4255: {'name': 'GCS_Herat_North', 'altnames': {'Herat_North'}},
+    4256: {'name': 'GCS_Mahe_1971', 'altnames': {'Mahe_1971'}},
+    4257: {'name': 'GCS_Makassar', 'altnames': {'Makassar'}},
+    4258: {'name': 'GCS_EUREF89', 'altnames': {'EUREF89'}},
+    4259: {'name': 'GCS_Malongo_1987', 'altnames': {'Malongo_1987'}},
+    4260: {'name': 'GCS_Manoca', 'altnames': {'Manoca'}},
+    4261: {'name': 'GCS_Merchich', 'altnames': {'Merchich'}},
+    4262: {'name': 'GCS_Massawa', 'altnames': {'Massawa'}},
+    4263: {'name': 'GCS_Minna', 'altnames': {'Minna'}},
+    4264: {'name': 'GCS_Mhast', 'altnames': {'Mhast'}},
+    4265: {'name': 'GCS_Monte_Mario', 'altnames': {'Monte_Mario'}},
+    4266: {'name': 'GCS_M_poraloko', 'altnames': {'M_poraloko'}},
+    4267: {'name': 'GCS_NAD27', 'altnames': {'NAD27'}},
+    4268: {'name': 'GCS_NAD_Michigan', 'altnames': {'NAD_Michigan'}},
+    4269: {'name': 'GCS_NAD83', 'altnames': {'NAD83'}},
+    4270: {'name': 'GCS_Nahrwan_1967', 'altnames': {'Nahrwan_1967'}},
+    4271: {'name': 'GCS_Naparima_1972', 'altnames': {'Naparima_1972'}},
+    4272: {'name': 'GCS_GD49', 'altnames': {'GD49'}},
+    4273: {'name': 'GCS_NGO_1948', 'altnames': {'NGO_1948'}},
+    4274: {'name': 'GCS_Datum_73', 'altnames': {'Datum_73'}},
+    4275: {'name': 'GCS_NTF', 'altnames': {'NTF'}},
+    4276: {'name': 'GCS_NSWC_9Z_2', 'altnames': {'NSWC_9Z_2'}},
+    4277: {'name': 'GCS_OSGB_1936', 'altnames': {'OSGB_1936'}},
+    4278: {'name': 'GCS_OSGB70', 'altnames': {'OSGB70'}},
+    4279: {'name': 'GCS_OS_SN80', 'altnames': {'OS_SN80'}},
+    4280: {'name': 'GCS_Padang', 'altnames': {'Padang'}},
+    4281: {'name': 'GCS_Palestine_1923', 'altnames': {'Palestine_1923'}},
+    4282: {'name': 'GCS_Pointe_Noire', 'altnames': {'Pointe_Noire'}},
+    4283: {'name': 'GCS_GDA94', 'altnames': {'GDA94'}},
+    4284: {'name': 'GCS_Pulkovo_1942', 'altnames': {'Pulkovo_1942'}},
+    4285: {'name': 'GCS_Qatar', 'altnames': {'Qatar'}},
+    4286: {'name': 'GCS_Qatar_1948', 'altnames': {'Qatar_1948'}},
+    4287: {'name': 'GCS_Qornoq', 'altnames': {'Qornoq'}},
+    4288: {'name': 'GCS_Loma_Quintana', 'altnames': {'Loma_Quintana'}},
+    4289: {'name': 'GCS_Amersfoort', 'altnames': {'Amersfoort'}},
+    4290: {'name': 'GCS_RT38', 'altnames': {'RT38'}},
+    4291: {'name': 'GCS_SAD69', 'altnames': {'SAD69'}},
+    4292: {'name': 'GCS_Sapper_Hill_1943', 'altnames': {'Sapper_Hill_1943'}},
+    4293: {'name': 'GCS_Schwarzeck', 'altnames': {'Schwarzeck'}},
+    4294: {'name': 'GCS_Segora', 'altnames': {'Segora'}},
+    4295: {'name': 'GCS_Serindung', 'altnames': {'Serindung'}},
+    4296: {'name': 'GCS_Sudan', 'altnames': {'Sudan'}},
+    4297: {'name': 'GCS_Tananarive', 'altnames': {'Tananarive'}},
+    4298: {'name': 'GCS_Timbalai_1948', 'altnames': {'Timbalai_1948'}},
+    4299: {'name': 'GCS_TM65', 'altnames': {'TM65'}},
+    4300: {'name': 'GCS_TM75', 'altnames': {'TM75'}},
+    4301: {'name': 'GCS_Tokyo', 'altnames': {'Tokyo'}},
+    4302: {'name': 'GCS_Trinidad_1903', 'altnames': {'Trinidad_1903'}},
+    4303: {'name': 'GCS_TC_1948', 'altnames': {'TC_1948'}},
+    4304: {'name': 'GCS_Voirol_1875', 'altnames': {'Voirol_1875'}},
+    4305: {'name': 'GCS_Voirol_Unifie', 'altnames': {'Voirol_Unifie'}},
+    4306: {'name': 'GCS_Bern_1938', 'altnames': {'Bern_1938'}},
+    4307: {'name': 'GCS_Nord_Sahara_1959', 'altnames': {'Nord_Sahara_1959'}},
+    4308: {'name': 'GCS_Stockholm_1938', 'altnames': {'Stockholm_1938'}},
+    4309: {'name': 'GCS_Yacare', 'altnames': {'Yacare'}},
+    4310: {'name': 'GCS_Yoff', 'altnames': {'Yoff'}},
+    4311: {'name': 'GCS_Zanderij', 'altnames': {'Zanderij'}},
+    4312: {'name': 'GCS_MGI', 'altnames': {'MGI'}},
+    4313: {'name': 'GCS_Belge_1972', 'altnames': {'Belge_1972'}},
+    4314: {'name': 'GCS_DHDN', 'altnames': {'DHDN'}},
+    4315: {'name': 'GCS_Conakry_1905', 'altnames': {'Conakry_1905'}},
+    4322: {'name': 'GCS_WGS_72', 'altnames': {'WGS_72'}},
+    4324: {'name': 'GCS_WGS_72BE', 'altnames': {'WGS_72BE'}},
+    4326: {'name': 'GCS_WGS_84', 'altnames': {'WGS_84'}},
+    4801: {'name': 'GCS_Bern_1898_Bern', 'altnames': {'Bern_1898_Bern'}},
+    4802: {'name': 'GCS_Bogota_Bogota', 'altnames': {'Bogota_Bogota'}},
+    4803: {'name': 'GCS_Lisbon_Lisbon', 'altnames': {'Lisbon_Lisbon'}},
+    4804: {'name': 'GCS_Makassar_Jakarta', 'altnames': {'Makassar_Jakarta'}},
+    4805: {'name': 'GCS_MGI_Ferro', 'altnames': {'MGI_Ferro'}},
+    4806: {'name': 'GCS_Monte_Mario_Rome', 'altnames': {'Monte_Mario_Rome'}},
+    4807: {'name': 'GCS_NTF_Paris', 'altnames': {'NTF_Paris'}},
+    4808: {'name': 'GCS_Padang_Jakarta', 'altnames': {'Padang_Jakarta'}},
+    4809: {'name': 'GCS_Belge_1950_Brussels', 'altnames': {'Belge_1950_Brussels'}},
+    4810: {'name': 'GCS_Tananarive_Paris', 'altnames': {'Tananarive_Paris'}},
+    4811: {'name': 'GCS_Voirol_1875_Paris', 'altnames': {'Voirol_1875_Paris'}},
+    4812: {'name': 'GCS_Voirol_Unifie_Paris', 'altnames': {'Voirol_Unifie_Paris'}},
+    4813: {'name': 'GCS_Batavia_Jakarta', 'altnames': {'Batavia_Jakarta'}},
+    4901: {'name': 'GCS_ATF_Paris', 'altnames': {'ATF_Paris'}},
+    4902: {'name': 'GCS_NDG_Paris', 'altnames': {'NDG_Paris'}},
+    4001: {'name': 'GCSE_Airy1830', 'altnames': {'Airy1830'}},
+    4002: {'name': 'GCSE_AiryModified1849', 'altnames': {'AiryModified1849'}},
+    4003: {'name': 'GCSE_AustralianNationalSpheroid', 'altnames': {'AustralianNationalSpheroid'}},
+    4004: {'name': 'GCSE_Bessel1841', 'altnames': {'Bessel1841'}},
+    4005: {'name': 'GCSE_BesselModified', 'altnames': {'BesselModified'}},
+    4006: {'name': 'GCSE_BesselNamibia', 'altnames': {'BesselNamibia'}},
+    4007: {'name': 'GCSE_Clarke1858', 'altnames': {'Clarke1858'}},
+    4008: {'name': 'GCSE_Clarke1866', 'altnames': {'Clarke1866'}},
+    4009: {'name': 'GCSE_Clarke1866Michigan', 'altnames': {'Clarke1866Michigan'}},
+    4010: {'name': 'GCSE_Clarke1880_Benoit', 'altnames': {'Clarke1880_Benoit'}},
+    4011: {'name': 'GCSE_Clarke1880_IGN', 'altnames': {'Clarke1880_IGN'}},
+    4012: {'name': 'GCSE_Clarke1880_RGS', 'altnames': {'Clarke1880_RGS'}},
+    4013: {'name': 'GCSE_Clarke1880_Arc', 'altnames': {'Clarke1880_Arc'}},
+    4014: {'name': 'GCSE_Clarke1880_SGA1922', 'altnames': {'Clarke1880_SGA1922'}},
+    4015: {'name': 'GCSE_Everest1830_1937Adjustment', 'altnames': {'Everest1830_1937Adjustment'}},
+    4016: {'name': 'GCSE_Everest1830_1967Definition', 'altnames': {'Everest1830_1967Definition'}},
+    4017: {'name': 'GCSE_Everest1830_1975Definition', 'altnames': {'Everest1830_1975Definition'}},
+    4018: {'name': 'GCSE_Everest1830Modified', 'altnames': {'Everest1830Modified'}},
+    4019: {'name': 'GCSE_GRS1980', 'altnames': {'GRS1980'}},
+    4020: {'name': 'GCSE_Helmert1906', 'altnames': {'Helmert1906'}},
+    4021: {'name': 'GCSE_IndonesianNationalSpheroid', 'altnames': {'IndonesianNationalSpheroid'}},
+    4022: {'name': 'GCSE_International1924', 'altnames': {'International1924'}},
+    4023: {'name': 'GCSE_International1967', 'altnames': {'International1967'}},
+    4024: {'name': 'GCSE_Krassowsky1940', 'altnames': {'Krassowsky1940'}},
+    4025: {'name': 'GCSE_NWL9D', 'altnames': {'NWL9D'}},
+    4026: {'name': 'GCSE_NWL10D', 'altnames': {'NWL10D'}},
+    4027: {'name': 'GCSE_Plessis1817', 'altnames': {'Plessis1817'}},
+    4028: {'name': 'GCSE_Struve1860', 'altnames': {'Struve1860'}},
+    4029: {'name': 'GCSE_WarOffice', 'altnames': {'WarOffice'}},
+    4030: {'name': 'GCSE_WGS84', 'altnames': {'WGS84'}},
+    4031: {'name': 'GCSE_GEM10C', 'altnames': {'GEM10C'}},
+    4032: {'name': 'GCSE_OSU86F', 'altnames': {'OSU86F'}},
+    4033: {'name': 'GCSE_OSU91A', 'altnames': {'OSU91A'}},
+    4034: {'name': 'GCSE_Clarke1880', 'altnames': {'Clarke1880'}},
+    4035: {'name': 'GCSE_Sphere', 'altnames': {'Sphere'}},
+})
+
+
+GeoTiffAngularUnits = TiffConstantSet('GeoTiffAngularUnits', {
+    9101: {'name': 'Angular_Radian', 'altnames': {'radian', 'rad'}},
+    9102: {'name': 'Angular_Degree', 'altnames': {'degree', 'deg'}},
+    9103: {'name': 'Angular_Arc_Minute', 'altnames': {'arc_minute', 'arcminute'}},
+    9104: {'name': 'Angular_Arc_Second', 'altnames': {'arc_second', 'arcsecond'}},
+    9105: {'name': 'Angular_Grad', 'altnames': {'grad'}},
+    9106: {'name': 'Angular_Gon', 'altnames': {'gon'}},
+    9107: {'name': 'Angular_DMS', 'altnames': {'dms'}},
+    9108: {'name': 'Angular_DMS_Hemisphere', 'altnames': {'dms_hemisphere', 'dms_hemi'}},
+})
+
+
+GeoTiffLinearUnits = TiffConstantSet('GeoTiffLinearUnits', {
+    9001: {'name': 'Linear_Meter', 'altnames': {'m', 'meter', 'meters'}},
+    9002: {'name': 'Linear_Foot', 'altnames': {'ft', 'foot', 'feet'}},
+    9003: {'name': 'Linear_Foot_US_Survey', 'altnames': {'ft_us'}},
+    9004: {'name': 'Linear_Foot_Modified_American', 'altnames': {'ft_am'}},
+    9005: {'name': 'Linear_Foot_Clarke', 'altnames': {'ft_cl'}},
+    9006: {'name': 'Linear_Foot_Indian', 'altnames': {'ft_in'}},
+    9007: {'name': 'Linear_Link', 'altnames': {'link'}},
+    9008: {'name': 'Linear_Link_Benoit', 'altnames': {'link_bn'}},
+    9009: {'name': 'Linear_Link_Sears', 'altnames': {'link_sr'}},
+    9010: {'name': 'Linear_Chain_Benoit', 'altnames': {'chain_bn'}},
+    9011: {'name': 'Linear_Chain_Sears', 'altnames': {'chain_sr'}},
+    9012: {'name': 'Linear_Yard_Sears', 'altnames': {'yd_sr'}},
+    9013: {'name': 'Linear_Yard_Indian', 'altnames': {'yd_in'}},
+    9014: {'name': 'Linear_Fathom', 'altnames': {'fathom'}},
+    9015: {'name': 'Linear_Mile_International_Nautical', 'altnames': {'mi', 'mile', 'miles'}},
path: tifftools/constants.py
user: login: annehaley
body: Updated altnames for this unit in [10596f5](https://github.com/DigitalSlideArchive/tifftools/pull/103/commits/10596f53e3d88a4f55ecfa9d19fdeabe29431b37)
created_at: 2025-04-17T15:33:24Z
author_association: CONTRIBUTOR

PR comments:
user: login: manthey
created_at: 2025-04-16T14:32:07Z
body: With this addition, we really should add some example uses (issue #14)
author_association: CONTRIBUTOR
user: login: annehaley
created_at: 2025-04-17T15:35:46Z
body: > With this addition, we really should add some example uses

Ok, should I add them to the README?


author_association: CONTRIBUTOR
user: login: manthey
created_at: 2025-04-17T16:05:06Z
body: > > With this addition, we really should add some example uses
> 
> Ok, should I add them to the README?

Yes.  I'll comment on #14.
author_association: CONTRIBUTOR