4da9a48d7c
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>
210 lines
7.8 KiB
Python
210 lines
7.8 KiB
Python
#!/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)
|