gen_heif_testdata.sh (7116B)
1 #!/bin/sh 2 # This file is part of libextractor. 3 # Copyright (C) 2026 Vidyut Samanta and Christian Grothoff 4 # 5 # Generate the test data for the "heif" plugin: 6 # 7 # src/plugins/testdata/heif_test.heic 8 # src/plugins/testdata/heif_test.avif 9 # 10 # Both files are hand-constructed ISO base media file format containers. 11 # They are structurally conformant HEIF/AVIF files -- the box tree, the 12 # item structures and the property associations are exactly what the 13 # specifications call for -- but the 'mdat' payload is a placeholder 14 # rather than a real HEVC or AV1 bitstream, so they identify but do not 15 # decode. That is deliberate: the plugin is a container parser, the 16 # values it must report are known exactly, and no encoder is needed. 17 # 18 # Needs: python3 (no third party modules). Deterministic: no 19 # timestamps, no random data. 20 # 21 # Written to CC0; there is no third party content in these files. 22 # 23 # Usage: contrib/gen_heif_testdata.sh [output-directory] 24 25 set -e 26 27 OUT="${1:-src/plugins/testdata}" 28 mkdir -p "$OUT" 29 30 python3 - "$OUT" <<'EOF' 31 import struct 32 import sys 33 34 out = sys.argv[1] 35 36 37 def box(btype, payload): 38 """A plain ISO-BMFF box: 32-bit size, 4CC type, payload.""" 39 assert len(btype) == 4 40 return struct.pack('>I', 8 + len(payload)) + btype.encode('ascii') + payload 41 42 43 def fullbox(btype, version, flags, payload): 44 """A FullBox: a box whose payload starts with version and flags.""" 45 return box(btype, bytes([version]) + struct.pack('>I', flags)[1:] + payload) 46 47 48 def ftyp(major, brands): 49 return box('ftyp', 50 major.encode('ascii') 51 + struct.pack('>I', 0) 52 + b''.join(b.encode('ascii') for b in brands)) 53 54 55 def hdlr(handler): 56 return fullbox('hdlr', 0, 0, 57 struct.pack('>I', 0) 58 + handler.encode('ascii') 59 + b'\0' * 12 60 + b'libextractor test\0') 61 62 63 def pitm(item_id): 64 return fullbox('pitm', 0, 0, struct.pack('>H', item_id)) 65 66 67 def infe(item_id, item_type, name): 68 """ItemInfoEntry version 2: 16-bit item_ID, then the item_type.""" 69 return fullbox('infe', 2, 0, 70 struct.pack('>HH', item_id, 0) 71 + item_type.encode('ascii') 72 + name.encode('ascii') + b'\0') 73 74 75 def iinf(entries): 76 return fullbox('iinf', 0, 0, 77 struct.pack('>H', len(entries)) + b''.join(entries)) 78 79 80 def iref_thmb(from_id, to_id): 81 return fullbox('iref', 0, 0, 82 box('thmb', struct.pack('>HHH', from_id, 1, to_id))) 83 84 85 def ispe(width, height): 86 return fullbox('ispe', 0, 0, struct.pack('>II', width, height)) 87 88 89 def pixi(depths): 90 return fullbox('pixi', 0, 0, bytes([len(depths)]) + bytes(depths)) 91 92 93 def irot(quarter_turns): 94 """ImageRotation: anti-clockwise quarter turns, 0..3.""" 95 return box('irot', bytes([quarter_turns & 0x03])) 96 97 98 def imir(axis): 99 """ImageMirror: 0 mirrors about a vertical axis, 1 about a horizontal one.""" 100 return box('imir', bytes([axis & 0x01])) 101 102 103 def colr_nclx(primaries, transfer, matrix, full_range): 104 return box('colr', 105 b'nclx' 106 + struct.pack('>HHH', primaries, transfer, matrix) 107 + bytes([0x80 if full_range else 0x00])) 108 109 110 def av1c(high_bitdepth, twelve_bit, monochrome): 111 """AV1CodecConfigurationBox, the four byte fixed part.""" 112 flags = (high_bitdepth << 6) | (twelve_bit << 5) | (monochrome << 4) | 0x0C 113 return box('av1C', bytes([0x81, 0x00, flags, 0x00])) 114 115 116 def hvcc(): 117 """A minimal HEVCDecoderConfigurationRecord shell (no parameter sets).""" 118 return box('hvcC', 119 bytes([0x01, 0x01, 0x60, 0x00, 0x00, 0x00, 120 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 121 0x3C, 0xF0, 0x00, 0xFC, 0xFD, 0xF8, 122 0xF8, 0x00, 0x00, 0x0F, 0x00])) 123 124 125 def ipma(entries): 126 """ItemPropertyAssociation with 16-bit item IDs and 7-bit indices.""" 127 payload = struct.pack('>I', len(entries)) 128 for item_id, props in entries: 129 payload += struct.pack('>HB', item_id, len(props)) 130 for essential, index in props: 131 assert 0 < index < 0x80 132 payload += bytes([(0x80 if essential else 0x00) | index]) 133 return fullbox('ipma', 0, 0, payload) 134 135 136 def iprp(props, assoc): 137 return box('iprp', box('ipco', b''.join(props)) + ipma(assoc)) 138 139 140 def meta(children): 141 return fullbox('meta', 0, 0, b''.join(children)) 142 143 144 def mdat(nbytes): 145 """A placeholder for the coded data; never read by the plugin.""" 146 return box('mdat', bytes((i * 7 + 3) & 0xFF for i in range(nbytes))) 147 148 149 # ---------------------------------------------------------------- HEIC 150 # 151 # Major brand 'heic', so the MIME type and the HEVC codec follow from 152 # the brand alone. Three items: the primary image (id 1), a thumbnail 153 # (id 2, tied to the primary by an 'iref' of type 'thmb') and an Exif 154 # metadata item (id 3). 155 # 156 # The property container deliberately holds the *thumbnail's* 'ispe' 157 # first: a parser that simply reports the first 'ispe' it finds says 158 # 16x12, and only one that follows the 'ipma' association for the 159 # primary item reports the true 96x64. 160 heic_props = [ 161 ispe(16, 12), # 1: the thumbnail's extent 162 ispe(96, 64), # 2: the primary item's extent 163 pixi([10, 10, 10]), # 3: 10 bit, i.e. an HDR capture 164 irot(1), # 4: 90 degrees anti-clockwise 165 colr_nclx(9, 16, 9, True), # 5: BT.2020 with the PQ curve 166 imir(0), # 6: mirrored left to right 167 hvcc(), # 7 168 ] 169 heic_assoc = [ 170 (1, [(False, 2), (False, 3), (True, 4), (False, 5), (True, 6), 171 (True, 7)]), 172 (2, [(False, 1)]), 173 ] 174 heic = (ftyp('heic', ['mif1', 'heic', 'miaf', 'MiHB']) 175 + meta([hdlr('pict'), 176 pitm(1), 177 iinf([infe(1, 'hvc1', 'primary'), 178 infe(2, 'hvc1', 'thumb'), 179 infe(3, 'Exif', 'exif')]), 180 iref_thmb(2, 1), 181 iprp(heic_props, heic_assoc)]) 182 + mdat(512)) 183 184 # ---------------------------------------------------------------- AVIF 185 # 186 # Major brand 'mif1' with 'avif' among the compatible brands, which is 187 # what several AVIF writers emit and what forces a reader to look past 188 # the major brand. One image item, no 'pixi', so the bit depth has to 189 # come from the 'av1C' configuration record. 190 avif_props = [ 191 ispe(128, 72), # 1 192 av1c(0, 0, 0), # 2: 8 bit, colour 193 colr_nclx(1, 13, 6, True), # 3: BT.709 primaries, sRGB curve 194 ] 195 avif_assoc = [ 196 (1, [(False, 1), (True, 2), (False, 3)]), 197 ] 198 avif = (ftyp('mif1', ['mif1', 'miaf', 'MA1B', 'avif']) 199 + meta([hdlr('pict'), 200 pitm(1), 201 iinf([infe(1, 'av01', 'primary')]), 202 iprp(avif_props, avif_assoc)]) 203 + mdat(384)) 204 205 for name, data in (('heif_test.heic', heic), ('heif_test.avif', avif)): 206 path = out + '/' + name 207 with open(path, 'wb') as f: 208 f.write(data) 209 print('%s: %d bytes' % (path, len(data))) 210 EOF