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