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