libextractor

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

gen_webp_testdata.sh (5367B)


      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 "webp" plugin:
      6 #
      7 #   src/plugins/testdata/webp_test.webp
      8 #
      9 # An extended (VP8X) animated WebP with an alpha channel, an ICC
     10 # profile, three animation frames and EXIF and XMP chunks -- that is,
     11 # every optional feature the container defines, so that one file
     12 # exercises the whole chunk walk.
     13 #
     14 # The three frames carry a genuine lossless VP8L bitstream, encoded here
     15 # by ffmpeg from a solid colour generated by lavfi (so nothing is read
     16 # from disk and the result does not depend on a sample image).  The
     17 # container around it is assembled by hand because no encoder emits all
     18 # of these chunks at once.  The ICC, EXIF and XMP payloads are
     19 # placeholders: the plugin only reports that they are present, and
     20 # parsing them is the exiv2 plugin's job.
     21 #
     22 # Needs: ffmpeg built with libwebp (Debian package "ffmpeg") and
     23 # python3.  Deterministic: no timestamps, no random data.
     24 #
     25 # Written to CC0; there is no third party content in this file.
     26 #
     27 # Usage: contrib/gen_webp_testdata.sh [output-directory]
     28 
     29 set -e
     30 
     31 OUT="${1:-src/plugins/testdata}"
     32 mkdir -p "$OUT"
     33 
     34 TMP=$(mktemp -d)
     35 trap 'rm -rf "$TMP"' EXIT
     36 
     37 # A 40x24 solid colour, encoded losslessly.  libwebp emits a bare
     38 # "RIFF....WEBPVP8L" file, whose VP8L chunk we lift out below.
     39 ffmpeg -hide_banner -loglevel error -y \
     40        -f lavfi -i "color=c=0x1EB45A:s=40x24" \
     41        -frames:v 1 -c:v libwebp -lossless 1 -pix_fmt bgra \
     42        "$TMP/frame.webp"
     43 
     44 python3 - "$OUT" "$TMP/frame.webp" <<'EOF'
     45 import struct
     46 import sys
     47 
     48 out = sys.argv[1]
     49 frame_path = sys.argv[2]
     50 
     51 WIDTH = 40
     52 HEIGHT = 24
     53 
     54 # VP8X feature flags, in the single flag byte at the start of the
     55 # "VP8X" payload: Rsv(2) ICC(1) Alpha(1) EXIF(1) XMP(1) Anim(1) Rsv(1)
     56 ICC_FLAG = 0x20
     57 ALPHA_FLAG = 0x10
     58 EXIF_FLAG = 0x08
     59 XMP_FLAG = 0x04
     60 ANIM_FLAG = 0x02
     61 
     62 
     63 def chunk(fourcc, payload):
     64     """A RIFF chunk: 4CC, 32-bit little-endian size, payload, pad byte."""
     65     assert len(fourcc) == 4
     66     data = fourcc.encode('ascii') + struct.pack('<I', len(payload)) + payload
     67     if len(payload) & 1:
     68         data += b'\0'
     69     return data
     70 
     71 
     72 def le24(value):
     73     return struct.pack('<I', value)[:3]
     74 
     75 
     76 def extract_vp8l(path):
     77     """Lift the VP8L chunk (header included) out of a simple WebP file."""
     78     with open(path, 'rb') as f:
     79         data = f.read()
     80     assert data[0:4] == b'RIFF' and data[8:12] == b'WEBP', 'not a WebP file'
     81     pos = 12
     82     while pos + 8 <= len(data):
     83         fourcc = data[pos:pos + 4]
     84         size = struct.unpack('<I', data[pos + 4:pos + 8])[0]
     85         if fourcc == b'VP8L':
     86             end = pos + 8 + size + (size & 1)
     87             return data[pos:end]
     88         pos += 8 + size + (size & 1)
     89     raise SystemExit('ffmpeg did not produce a VP8L chunk')
     90 
     91 
     92 vp8l = extract_vp8l(frame_path)
     93 
     94 # A placeholder ICC profile: a 128 byte header saying "RGB display
     95 # profile, no tags".  Small, self-describing, and never parsed by us.
     96 icc = (struct.pack('>I', 132)
     97        + b'lcms' + struct.pack('>I', 0x04300000)
     98        + b'mntr' + b'RGB ' + b'XYZ '
     99        + b'\0' * 12                      # creation date, zeroed
    100        + b'acsp' + b'APPL' + b'\0' * 4
    101        + b'\0' * 12                      # device manufacturer/model
    102        + b'\0' * 8                       # device attributes
    103        + b'\0' * 4                       # rendering intent: perceptual
    104        + struct.pack('>III', 0xF6D6, 0x10000, 0xD32D)   # D50 white point
    105        + b'none' + b'\0' * 44
    106        + struct.pack('>I', 0))           # tag count: zero
    107 
    108 # "Exif\0\0" is not part of the WebP EXIF chunk; the payload is the raw
    109 # TIFF structure.  A little-endian TIFF header with an empty IFD.
    110 exif = b'II' + struct.pack('<HI', 42, 8) + struct.pack('<H', 0) \
    111     + struct.pack('<I', 0)
    112 
    113 xmp = (b'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>'
    114        b'<x:xmpmeta xmlns:x="adobe:ns:meta/">'
    115        b'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">'
    116        b'<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">'
    117        b'<dc:title><rdf:Alt><rdf:li xml:lang="x-default">'
    118        b'libextractor webp test</rdf:li></rdf:Alt></dc:title>'
    119        b'</rdf:Description></rdf:RDF></x:xmpmeta>'
    120        b'<?xpacket end="w"?>')
    121 
    122 vp8x = chunk('VP8X',
    123              bytes([ICC_FLAG | ALPHA_FLAG | EXIF_FLAG | XMP_FLAG | ANIM_FLAG])
    124              + b'\0\0\0'
    125              + le24(WIDTH - 1)
    126              + le24(HEIGHT - 1))
    127 
    128 # Background colour is BGRA; a loop count of zero means "forever".
    129 anim = chunk('ANIM', struct.pack('<IH', 0xFF204080, 0))
    130 
    131 # Frame durations of 100, 150 and 250 ms sum to a round 500 ms.
    132 frames = b''
    133 for duration in (100, 150, 250):
    134     frames += chunk('ANMF',
    135                     le24(0) + le24(0)                    # frame_x, frame_y
    136                     + le24(WIDTH - 1) + le24(HEIGHT - 1)
    137                     + le24(duration)
    138                     + bytes([0x02])                      # dispose, no blend
    139                     + vp8l)
    140 
    141 body = (b'WEBP'
    142         + vp8x
    143         + chunk('ICCP', icc)
    144         + anim
    145         + frames
    146         + chunk('EXIF', exif)
    147         + chunk('XMP ', xmp))
    148 
    149 data = b'RIFF' + struct.pack('<I', len(body)) + body
    150 
    151 path = out + '/webp_test.webp'
    152 with open(path, 'wb') as f:
    153     f.write(data)
    154 print('%s: %d bytes, ICC %d bytes' % (path, len(data), len(icc)))
    155 EOF