libextractor

GNU libextractor
Log | Files | Refs | Submodules | README | LICENSE

gen_geotiff_testdata.sh (6520B)


      1 #!/bin/sh
      2 # This file is part of libextractor.
      3 # Copyright (C) 2026 Vidyut Samanta and Christian Grothoff
      4 #
      5 # Regenerate src/plugins/testdata/geotiff_test.tif.
      6 #
      7 # The fixture is hand-built rather than produced by gdal_translate: that
      8 # keeps it tiny (about 1 KB instead of the several KB GDAL emits), makes
      9 # it byte-for-byte reproducible without pulling in gdal-bin, and -- most
     10 # importantly -- lets us pick round numbers for the tiepoint and the
     11 # pixel scale so that the bounding box the plugin computes from them is
     12 # an exact, checkable constant.
     13 #
     14 # Requires: python3 (no third-party modules).
     15 #
     16 # The file it writes is a classic little-endian TIFF 6.0 baseline image,
     17 # 16x16, 8 bit greyscale, uncompressed, carrying the GeoTIFF 1.1 tags
     18 # for EPSG:32633 (WGS 84 / UTM zone 33N) plus GDAL's two private tags.
     19 # It is dedicated to the public domain under CC0 1.0.
     20 set -e
     21 
     22 out="$(dirname "$0")/../src/plugins/testdata/geotiff_test.tif"
     23 
     24 python3 - "$out" <<'EOF'
     25 import struct
     26 import sys
     27 
     28 # --- TIFF field types -------------------------------------------------
     29 ASCII = 2
     30 SHORT = 3
     31 LONG = 4
     32 DOUBLE = 12
     33 
     34 TYPE_SIZE = {ASCII: 1, SHORT: 2, LONG: 4, DOUBLE: 8}
     35 
     36 WIDTH = 16
     37 HEIGHT = 16
     38 
     39 # --- the GeoTIFF key directory ---------------------------------------
     40 # GeoAsciiParamsTag holds all ASCII-valued keys concatenated, each one
     41 # terminated by '|' rather than NUL.
     42 GEO_ASCII = "WGS 84 / UTM zone 33N|" "WGS 84|" "WGS 84 / UTM zone 33N|"
     43 GT_CITATION_OFF = 0
     44 GT_CITATION_LEN = 22
     45 GEOG_CITATION_OFF = 22
     46 GEOG_CITATION_LEN = 7
     47 PCS_CITATION_OFF = 29
     48 PCS_CITATION_LEN = 22
     49 assert len(GEO_ASCII) == 51
     50 
     51 # (KeyID, TIFFTagLocation, Count, Value_Offset), ascending by KeyID as
     52 # the specification requires.
     53 GEO_KEYS = [
     54     (1024, 0, 1, 1),          # GTModelTypeGeoKey = ModelTypeProjected
     55     (1025, 0, 1, 1),          # GTRasterTypeGeoKey = RasterPixelIsArea
     56     (1026, 34737, GT_CITATION_LEN, GT_CITATION_OFF),   # GTCitationGeoKey
     57     (2048, 0, 1, 4326),       # GeographicTypeGeoKey = WGS 84
     58     (2049, 34737, GEOG_CITATION_LEN, GEOG_CITATION_OFF),  # GeogCitation
     59     (3072, 0, 1, 32633),      # ProjectedCSTypeGeoKey = UTM zone 33N
     60     (3073, 34737, PCS_CITATION_LEN, PCS_CITATION_OFF),    # PCSCitation
     61     (3076, 0, 1, 9001),       # ProjLinearUnitsGeoKey = metre
     62 ]
     63 
     64 geo_dir = [1, 1, 1, len(GEO_KEYS)]        # version 1.1.1, NumberOfKeys
     65 for k in GEO_KEYS:
     66     geo_dir.extend(k)
     67 
     68 GDAL_METADATA = (
     69     "<GDALMetadata>"
     70     "<Item name=\"ACQUISITIONDATETIME\">2026-03-14T09:15:00Z</Item>"
     71     "<Item name=\"SENSOR_NAME\">libextractor synthetic sensor</Item>"
     72     "<Item name=\"PROCESSING_LEVEL\">L1C</Item>"
     73     "</GDALMetadata>"
     74 )
     75 
     76 
     77 def ascii_field(text):
     78     """NUL-terminated ASCII, as TIFF wants it."""
     79     return (text + "\0").encode("ascii")
     80 
     81 
     82 # --- the tags, ascending by tag number (TIFF requires sorted IFDs) ----
     83 # StripOffsets (273) is patched once the layout is known; it is a LONG,
     84 # so its size never changes and the layout is stable.
     85 tags = [
     86     (256, SHORT, 1, struct.pack("<H", WIDTH)),            # ImageWidth
     87     (257, SHORT, 1, struct.pack("<H", HEIGHT)),           # ImageLength
     88     (258, SHORT, 1, struct.pack("<H", 8)),                # BitsPerSample
     89     (259, SHORT, 1, struct.pack("<H", 1)),                # Compression=none
     90     (262, SHORT, 1, struct.pack("<H", 1)),                # BlackIsZero
     91     (270, ASCII, 0, ascii_field("libextractor GeoTIFF test fixture")),
     92     (271, ASCII, 0, ascii_field("libextractor")),         # Make
     93     (272, ASCII, 0, ascii_field("synthetic raster")),     # Model
     94     (273, LONG, 1, struct.pack("<I", 0)),                 # StripOffsets
     95     (277, SHORT, 1, struct.pack("<H", 1)),                # SamplesPerPixel
     96     (278, SHORT, 1, struct.pack("<H", HEIGHT)),           # RowsPerStrip
     97     (279, LONG, 1, struct.pack("<I", WIDTH * HEIGHT)),    # StripByteCounts
     98     (305, ASCII, 0, ascii_field("libextractor gen_geotiff_testdata 1.0")),
     99     (306, ASCII, 0, ascii_field("2026:03:14 09:15:00")),  # DateTime
    100     (315, ASCII, 0, ascii_field("Christian Grothoff")),   # Artist
    101     (33432, ASCII, 0, ascii_field("CC0 1.0 Universal (public domain)")),
    102     # ModelPixelScaleTag: 10 m per pixel in x and y, no z scale.
    103     (33550, DOUBLE, 3, struct.pack("<3d", 10.0, 10.0, 0.0)),
    104     # ModelTiepointTag: raster (0,0,0) is model (500000, 5400000, 0),
    105     # i.e. the upper-left corner of the upper-left pixel.
    106     (33922, DOUBLE, 6,
    107      struct.pack("<6d", 0.0, 0.0, 0.0, 500000.0, 5400000.0, 0.0)),
    108     (34735, SHORT, len(geo_dir),
    109      struct.pack("<%dH" % len(geo_dir), *geo_dir)),       # GeoKeyDirectory
    110     (34737, ASCII, len(GEO_ASCII), GEO_ASCII.encode("ascii")),
    111     (42112, ASCII, 0, ascii_field(GDAL_METADATA)),        # GDAL_METADATA
    112     (42113, ASCII, 0, ascii_field("0")),                  # GDAL_NODATA
    113 ]
    114 
    115 # Fill in the counts we left at 0 (ASCII counts include the NUL).
    116 tags = [(t, ty, (len(v) if 0 == c else c), v) for (t, ty, c, v) in tags]
    117 for (t, ty, c, v) in tags:
    118     assert c * TYPE_SIZE[ty] == len(v), t
    119 assert tags == sorted(tags, key=lambda e: e[0])
    120 
    121 # --- lay the file out -------------------------------------------------
    122 header_len = 8
    123 ifd_len = 2 + 12 * len(tags) + 4
    124 value_base = header_len + ifd_len
    125 
    126 values = bytearray()
    127 for (t, ty, c, v) in tags:
    128     if len(v) > 4:
    129         if 0 != len(values) % 2:
    130             values.append(0)       # TIFF offsets must be word-aligned
    131         values.extend(v)
    132 
    133 image_off = value_base + len(values)
    134 
    135 # Patch StripOffsets now that we know where the image data lands.
    136 tags = [(t, ty, c, struct.pack("<I", image_off) if 273 == t else v)
    137         for (t, ty, c, v) in tags]
    138 
    139 ifd = bytearray(struct.pack("<H", len(tags)))
    140 values = bytearray()
    141 for (t, ty, c, v) in tags:
    142     if len(v) <= 4:
    143         field = v + b"\0" * (4 - len(v))
    144     else:
    145         if 0 != len(values) % 2:
    146             values.append(0)
    147         field = struct.pack("<I", value_base + len(values))
    148         values.extend(v)
    149     ifd.extend(struct.pack("<HHI", t, ty, c))
    150     ifd.extend(field)
    151 ifd.extend(struct.pack("<I", 0))   # no further IFD
    152 assert len(ifd) == ifd_len
    153 assert value_base + len(values) == image_off
    154 
    155 # A fixed, non-random pattern so the file is byte-for-byte reproducible.
    156 image = bytes(((x * 16 + y) & 0xFF) for y in range(HEIGHT)
    157               for x in range(WIDTH))
    158 
    159 blob = struct.pack("<2sHI", b"II", 42, header_len) + bytes(ifd) \
    160     + bytes(values) + image
    161 assert len(blob) == image_off + len(image)
    162 
    163 with open(sys.argv[1], "wb") as f:
    164     f.write(blob)
    165 print("wrote %s (%d bytes)" % (sys.argv[1], len(blob)))
    166 EOF