webpdec.h: single-file WebP decoder
libwebp 1.6.0's decode + demux path (lossy VP8, lossless VP8L, VP8X container, ALPH alpha, and the WebPDemux/WebPAnimDecoder animation API) amalgamated into one stb-style header, built as portable C99 with all SIMD, threads and host file I/O disabled. amalgamate.py regenerates webpdec.h from a pinned, auto-cloned libwebp checkout; tests/run.sh checks decode output bit-exactly against reference libwebp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/bin/sh
|
||||
# Build and run the webpdec.h tests. If Pillow is available, samples are
|
||||
# generated and decode output is checked bit-exactly against reference libwebp;
|
||||
# otherwise the tests just decode whatever .webp files are passed/present.
|
||||
#
|
||||
# sh tests/run.sh
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.." # repo root
|
||||
CC=${CC:-cc}
|
||||
CFLAGS="-O2 -std=c99 -Wall -I."
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# Generate samples with Pillow if present (covers lossy, lossless, alpha,
|
||||
# odd dimensions, and a multi-frame animation).
|
||||
HAVE_PIL=0
|
||||
if python3 - "$TMP" >/dev/null 2>&1 <<'PY'
|
||||
import sys
|
||||
from PIL import Image
|
||||
d = sys.argv[1]
|
||||
w, h = 137, 83
|
||||
img = Image.new("RGBA", (w, h))
|
||||
px = img.load()
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
px[x, y] = ((x*255)//w, (y*255)//h, (x*y) % 256, (x+y) % 256)
|
||||
img.save(d+"/lossless.webp", lossless=True)
|
||||
img.save(d+"/lossy.webp", quality=90)
|
||||
frames = [Image.new("RGBA", (48, 32), (i*50 % 256, 80, 200, 255)) for i in range(5)]
|
||||
frames[0].save(d+"/anim.webp", save_all=True, append_images=frames[1:],
|
||||
duration=[100, 120, 140, 160, 180], loop=0, lossless=True)
|
||||
PY
|
||||
then HAVE_PIL=1; fi
|
||||
|
||||
echo "== build =="
|
||||
$CC $CFLAGS -o "$TMP/test_decode" tests/test_decode.c -lm
|
||||
$CC $CFLAGS -o "$TMP/test_demux" tests/test_demux.c -lm
|
||||
|
||||
if [ "$HAVE_PIL" = 1 ]; then
|
||||
echo "== decode (still) =="
|
||||
for k in lossless lossy; do
|
||||
"$TMP/test_decode" "$TMP/$k.webp" "$TMP/$k.raw"
|
||||
python3 - "$TMP/$k.webp" "$TMP/$k.raw" <<'PY'
|
||||
import sys
|
||||
from PIL import Image
|
||||
ref = Image.open(sys.argv[1]).convert("RGBA").tobytes()
|
||||
mine = open(sys.argv[2], "rb").read()
|
||||
assert ref == mine, "%s: pixels differ from reference libwebp!" % sys.argv[1]
|
||||
print(" %s: bit-exact vs reference libwebp" % sys.argv[1].split('/')[-1])
|
||||
PY
|
||||
done
|
||||
echo "== demux + animation =="
|
||||
"$TMP/test_demux" "$TMP/anim.webp"
|
||||
else
|
||||
echo "(Pillow not found -- decoding any .webp passed as args)"
|
||||
for f in "$@"; do "$TMP/test_decode" "$f"; done
|
||||
fi
|
||||
echo "ALL TESTS PASSED"
|
||||
@@ -0,0 +1,54 @@
|
||||
/* test harness: decode a .webp to RGBA, report, dump a PPM for eyeballing */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define WEBPDEC_IMPLEMENTATION
|
||||
#include "webpdec.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) { fprintf(stderr, "usage: %s file.webp [out.ppm]\n", argv[0]); return 2; }
|
||||
FILE *f = fopen(argv[1], "rb");
|
||||
if (!f) { perror("open"); return 1; }
|
||||
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
|
||||
unsigned char *buf = malloc(n);
|
||||
if (fread(buf, 1, n, f) != (size_t)n) { perror("read"); return 1; }
|
||||
fclose(f);
|
||||
|
||||
int w = 0, h = 0;
|
||||
if (!WebPGetInfo(buf, n, &w, &h)) { fprintf(stderr, "not a webp / bad header\n"); return 1; }
|
||||
printf("WebPGetInfo: %dx%d\n", w, h);
|
||||
|
||||
uint8_t *rgba = WebPDecodeRGBA(buf, n, &w, &h);
|
||||
if (!rgba) { fprintf(stderr, "decode FAILED\n"); return 1; }
|
||||
printf("decoded: %dx%d, RGBA (R,G,B,A top-to-bottom)\n", w, h);
|
||||
|
||||
/* sample a few pixels */
|
||||
long center = ((long)h/2 * w + w/2) * 4;
|
||||
printf("center px RGBA = %d %d %d %d\n",
|
||||
rgba[center], rgba[center+1], rgba[center+2], rgba[center+3]);
|
||||
|
||||
/* checksum + alpha presence */
|
||||
unsigned long sum = 0; int has_alpha = 0;
|
||||
for (long i = 0; i < (long)w*h; i++) {
|
||||
sum += rgba[i*4] + rgba[i*4+1] + rgba[i*4+2];
|
||||
if (rgba[i*4+3] != 255) has_alpha = 1;
|
||||
}
|
||||
printf("rgb checksum=%lu non-opaque-alpha=%s\n", sum, has_alpha ? "yes" : "no");
|
||||
|
||||
if (argc >= 3) { /* .raw => raw RGBA dump; else opaque PPM */
|
||||
const char *p = argv[2];
|
||||
size_t L = 0; while (p[L]) L++;
|
||||
FILE *o = fopen(argv[2], "wb");
|
||||
if (L >= 4 && p[L-4]=='.' && p[L-3]=='r' && p[L-2]=='a' && p[L-1]=='w') {
|
||||
fwrite(rgba, 4, (size_t)w*h, o);
|
||||
} else {
|
||||
fprintf(o, "P6\n%d %d\n255\n", w, h);
|
||||
for (long i = 0; i < (long)w*h; i++) fwrite(&rgba[i*4], 1, 3, o);
|
||||
}
|
||||
fclose(o);
|
||||
printf("wrote %s\n", argv[2]);
|
||||
}
|
||||
WebPFree(rgba);
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/* exercises the demux + animation API folded into the single header */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define WEBPDEC_IMPLEMENTATION
|
||||
#include "webpdec.h"
|
||||
|
||||
static unsigned char* slurp(const char* p, size_t* n){
|
||||
FILE* f=fopen(p,"rb"); if(!f) return 0;
|
||||
fseek(f,0,SEEK_END); long L=ftell(f); fseek(f,0,SEEK_SET);
|
||||
unsigned char* b=malloc(L); if(fread(b,1,L,f)!=(size_t)L){} fclose(f); *n=L; return b;
|
||||
}
|
||||
|
||||
int main(int argc,char**argv){
|
||||
size_t n; unsigned char* buf=slurp(argv[1],&n);
|
||||
if(!buf){ printf("no file\n"); return 1; }
|
||||
|
||||
WebPData data = { buf, n };
|
||||
|
||||
/* --- demux: report canvas + frame/loop count --- */
|
||||
WebPDemuxer* dmux = WebPDemux(&data);
|
||||
if(!dmux){ printf("WebPDemux failed\n"); return 1; }
|
||||
unsigned cw = WebPDemuxGetI(dmux, WEBP_FF_CANVAS_WIDTH);
|
||||
unsigned ch = WebPDemuxGetI(dmux, WEBP_FF_CANVAS_HEIGHT);
|
||||
unsigned nf = WebPDemuxGetI(dmux, WEBP_FF_FRAME_COUNT);
|
||||
unsigned lp = WebPDemuxGetI(dmux, WEBP_FF_LOOP_COUNT);
|
||||
unsigned fl = WebPDemuxGetI(dmux, WEBP_FF_FORMAT_FLAGS);
|
||||
printf("demux: canvas=%ux%u frames=%u loop=%u flags=0x%x\n", cw,ch,nf,lp,fl);
|
||||
WebPDemuxDelete(dmux);
|
||||
|
||||
/* --- animation: decode every frame via WebPAnimDecoder --- */
|
||||
WebPAnimDecoderOptions opt;
|
||||
WebPAnimDecoderOptionsInit(&opt);
|
||||
opt.color_mode = MODE_RGBA;
|
||||
WebPAnimDecoder* adec = WebPAnimDecoderNew(&data, &opt);
|
||||
if(!adec){ printf("WebPAnimDecoderNew failed\n"); return 1; }
|
||||
WebPAnimInfo info;
|
||||
WebPAnimDecoderGetInfo(adec, &info);
|
||||
printf("anim: %ux%u frames=%u loops=%u bgcolor=0x%08x\n",
|
||||
info.canvas_width, info.canvas_height, info.frame_count,
|
||||
info.loop_count, info.bgcolor);
|
||||
int prev = 0, got = 0;
|
||||
while (WebPAnimDecoderHasMoreFrames(adec)) {
|
||||
uint8_t* frgba; int ts;
|
||||
if (!WebPAnimDecoderGetNext(adec, &frgba, &ts)) { printf("GetNext failed\n"); break; }
|
||||
long c = ((long)info.canvas_height/2*info.canvas_width + info.canvas_width/2)*4;
|
||||
printf(" frame %d ts=%dms (+%dms) centerRGBA=%d,%d,%d,%d\n",
|
||||
got, ts, ts-prev, frgba[c],frgba[c+1],frgba[c+2],frgba[c+3]);
|
||||
prev = ts; got++;
|
||||
}
|
||||
printf("decoded %d/%u frames -> %s\n", got, info.frame_count,
|
||||
(got==(int)info.frame_count)?"OK":"MISMATCH");
|
||||
WebPAnimDecoderDelete(adec);
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user