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:
2026-06-14 03:28:11 +00:00
commit 4da9a48d7c
10 changed files with 22045 additions and 0 deletions
+209
View File
@@ -0,0 +1,209 @@
#!/usr/bin/env python3
"""
amalgamate.py -- generate webpdec.h, a single-file WebP decoder.
Collapses libwebp's *decode + demux* path (lossy VP8, lossless VP8L, the VP8X
extended container, the ALPH alpha chunk, and the WebPDemux / WebPAnimDecoder
animation+metadata API) into one stb-style header, built as portable C99 with
all SIMD, threads and host file I/O disabled.
Self-contained: with no arguments it clones libwebp at the pinned commit (into
./libwebp, git-ignored), writes the empty config.h that forces the generic-C
path, applies the few single-translation-unit symbol renames itself, and writes
./webpdec.h. A fresh checkout + `python3 amalgamate.py` just works.
python3 amalgamate.py [OUTPUT.h]
Output layout:
#ifndef WEBPDEC_H <- public API (types/decode/demux)
...declarations...
#endif
#ifdef WEBPDEC_IMPLEMENTATION
#define HAVE_CONFIG_H <- forces the portable C path (SIMD off)
...all decode+demux .c files + their internal headers, inlined once each...
#endif
"""
import os
import re
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "libwebp")
UPSTREAM_URL = "https://chromium.googlesource.com/webm/libwebp"
PINNED_COMMIT = "b43b2caa710c0c997c066cb32c7fea1391fad70a" # v1.6.0
VERSION = "1.6.0"
# ---- the decoder-only, portable-C object set (LIBWEBPDECODER_OBJS + DEMUX_OBJS,
# minus every *_sse2/_sse41/_neon/_msa/_mips* SIMD translation unit) -------
DEC = [
"src/dec/alpha_dec.c", "src/dec/buffer_dec.c", "src/dec/frame_dec.c",
"src/dec/idec_dec.c", "src/dec/io_dec.c", "src/dec/quant_dec.c",
"src/dec/tree_dec.c", "src/dec/vp8_dec.c", "src/dec/vp8l_dec.c",
"src/dec/webp_dec.c",
]
DSP = [
"src/dsp/alpha_processing.c", "src/dsp/cpu.c", "src/dsp/dec.c",
"src/dsp/dec_clip_tables.c", "src/dsp/filters.c", "src/dsp/lossless.c",
"src/dsp/rescaler.c", "src/dsp/upsampling.c", "src/dsp/yuv.c",
]
UTILS = [
"src/utils/bit_reader_utils.c", "src/utils/color_cache_utils.c",
"src/utils/filters_utils.c", "src/utils/huffman_utils.c",
"src/utils/palette.c", "src/utils/quant_levels_dec_utils.c",
"src/utils/random_utils.c", "src/utils/rescaler_utils.c",
"src/utils/thread_utils.c", "src/utils/utils.c",
]
# Demux + animation: pure RIFF chunk parser (demux.c) and the frame compositor
# WebPAnimDecoder (anim_decode.c). anim_decode reuses WebPDecode and calls no
# WebPMux* function, so no muxer/encoder TU is needed.
DEMUX = ["src/demux/demux.c", "src/demux/anim_decode.c"]
SOURCES = DEC + DSP + UTILS + DEMUX
# demux.h declares both the WebPDemux and WebPAnimDecoder public APIs and pulls
# in decode.h + mux_types.h, so it covers the whole public surface.
PUBLIC = ["src/webp/types.h", "src/webp/decode.h", "src/webp/demux.h"]
# A handful of file-local symbols share a name across these TUs, which only
# matters once everything is one translation unit. We give the colliding copy
# a private prefix by wrapping just that source file's body in #define/#undef --
# leaving the upstream tree pristine (so a fresh clone regenerates identically).
RENAME_PREFIX = "webpdec_priv_"
RENAMES = {
"src/utils/thread_utils.c": ["ChangeState"],
"src/utils/quant_levels_dec_utils.c": ["clip_8b"],
"src/demux/demux.c": [
"MemBuffer", "InitMemBuffer", "MemDataSize", "RemapMemBuffer", "ParseVP8X",
],
}
INCLUDE_RE = re.compile(r'^\s*#\s*include\s+"([^"]+)"\s*(?://.*|/\*.*)?$')
def ensure_libwebp():
"""Clone libwebp at the pinned commit if it isn't already present."""
if os.path.isfile(os.path.join(SRC, "src/dec/webp_dec.c")):
return
sys.stderr.write("libwebp not found; cloning %s @ %s ...\n"
% (UPSTREAM_URL, PINNED_COMMIT[:12]))
try:
subprocess.check_call(["git", "clone", "--depth", "1", UPSTREAM_URL, SRC])
head = subprocess.check_output(
["git", "-C", SRC, "rev-parse", "HEAD"]).decode().strip()
if head != PINNED_COMMIT:
subprocess.check_call(
["git", "-C", SRC, "fetch", "--depth", "1", "origin", PINNED_COMMIT])
subprocess.check_call(
["git", "-C", SRC, "checkout", "--detach", PINNED_COMMIT])
except Exception as e:
sys.exit("error: could not fetch libwebp (%s).\nClone it manually:\n"
" git clone %s %s\n git -C %s checkout %s"
% (e, UPSTREAM_URL, SRC, SRC, PINNED_COMMIT))
def write_config_h():
"""Empty config.h: with HAVE_CONFIG_H defined this forces libwebp's
generic-C path (no WEBP_HAVE_* => no SIMD), even on SIMD-capable hosts."""
with open(os.path.join(SRC, "src/webp/config.h"), "w") as f:
f.write("/* intentionally empty: forces libwebp's generic-C path */\n")
emitted = set()
def resolve(inc, curfile):
cands = []
if inc.startswith("src/"):
cands.append(os.path.join(SRC, inc))
cands.append(os.path.join(os.path.dirname(curfile), inc))
cands.append(os.path.join(SRC, inc))
for c in cands:
c = os.path.normpath(c)
if os.path.isfile(c):
return c
return None
def inline(path, out):
path = os.path.normpath(path)
if path in emitted:
return
emitted.add(path)
rel = os.path.relpath(path, SRC)
names = RENAMES.get(rel.replace(os.sep, "/"))
out.append("/* >>> %s */\n" % rel)
if names:
for nm in names:
out.append("#define %s %s%s\n" % (nm, RENAME_PREFIX, nm))
with open(path, "r") as f:
for line in f:
m = INCLUDE_RE.match(line)
if m:
tgt = resolve(m.group(1), path)
if tgt is not None:
inline(tgt, out)
continue
out.append(line)
if names:
for nm in names:
out.append("#undef %s\n" % nm)
def main(out_path):
ensure_libwebp()
write_config_h()
out = []
out.append("""\
/*
* webpdec.h -- single-file WebP decoder (stb-style).
*
* libwebp's decode + demux path -- lossy VP8, lossless VP8L, the VP8X extended
* container, the ALPH alpha chunk, plus the demux/animation API (WebPDemux,
* WebPAnimDecoder) -- amalgamated into one file and built as portable C99 with
* all SIMD, threads and host file I/O disabled. The public API is the standard
* libwebp one (WebPDecodeRGBA, WebPDemux*, WebPAnimDecoder*).
*
* Usage: in exactly ONE translation unit,
* #define WEBPDEC_IMPLEMENTATION
* #include "webpdec.h"
* Everywhere else, just #include "webpdec.h". Decode-only (no encoder/muxer).
* Link with -lm.
*
* Generated by amalgamate.py from libwebp %s (see UPSTREAM_COMMIT).
* BSD-style license (see LICENSE) + PATENTS grant. Copyright Google LLC.
* DO NOT EDIT BY HAND -- rerun `python3 amalgamate.py`.
*/
""" % VERSION)
out.append("#ifndef WEBPDEC_H\n#define WEBPDEC_H\n")
for h in PUBLIC:
inline(os.path.join(SRC, h), out)
out.append("#endif /* WEBPDEC_H */\n\n")
out.append("#ifdef WEBPDEC_IMPLEMENTATION\n")
out.append("#define HAVE_CONFIG_H\n")
for c in SOURCES:
inline(os.path.join(SRC, c), out)
out.append("#endif /* WEBPDEC_IMPLEMENTATION */\n")
text = "".join(out)
with open(out_path, "w") as f:
f.write(text)
# refresh the recorded upstream revision
with open(os.path.join(HERE, "UPSTREAM_COMMIT"), "w") as f:
f.write("libwebp %s (v%s)\nupstream: %s\n"
"decode + demux amalgamation (SIMD/threads/file-IO disabled) "
"via amalgamate.py\n" % (PINNED_COMMIT, VERSION, UPSTREAM_URL))
print("wrote %s (%d lines, %d KB) from libwebp %s"
% (os.path.relpath(out_path, HERE), text.count("\n"),
len(text) // 1024, PINNED_COMMIT[:12]))
if __name__ == "__main__":
out = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "webpdec.h")
main(out)