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:
+209
@@ -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)
|
||||
Reference in New Issue
Block a user