libextractor

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

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