Skip to content

Commit 7859f2c

Browse files
authored
mcpplibs.mwrap.libwebp 1.5.0: libwebp-m (libwebp as C++23 modules) (#622)
Descriptor pkgs/mwrap/libwebp.lua (form A, the immutable mwrap-1.5.0 release asset, tagged on libwebp-m's merged main; no CN mirror yet), over compat.libwebp 1.5.0. Consumer tests/mwrap/libwebp-image, with the demux and mux features: lossy and lossless RGBA round trips with alpha, the advanced API with its pinned bytes, an animation encoded and played back frame by frame, the error paths. The installed-template check in validate.yml with its deployment contract (the wrapper's LICENSE/NOTICE, libwebp's README, COPYING, PATENTS and AUTHORS, byte for byte in a relocated bundle), and the plan.
1 parent 1ee06bf commit 7859f2c

8 files changed

Lines changed: 292 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Add mcpplibs.mwrap.libwebp (libwebp-m 1.5.0)
2+
3+
## Form
4+
5+
- Third-party upstream [webmproject/libwebp](https://github.com/webmproject/libwebp), tag `v1.5.0`, BSD-3-Clause
6+
with Google's additional patent grant. The tarball (`668c9aba…7f37`) is the one compat.libwebp 1.5.0 pins.
7+
- Form A external mwrap module repository [mcpplibs/libwebp-m](https://github.com/mcpplibs/libwebp-m), package
8+
`mcpplibs.mwrap.libwebp`, version 1.5.0 (first release of the identity, no revision suffix), tag series
9+
`mwrap-${version}` on the merged main commit (`0252b65`); release asset `libwebp-m-1.5.0.tar.gz` (`2a085e5e…f6a0`).
10+
No CN mirror yet (docs/cn-mirror.md fallback).
11+
- Implementation ownership: the codec is compat.libwebp's (dec, enc, dsp, utils and sharpyuv in one archive; src/demux
12+
and src/mux with its `demux` and `mux` features; SSE2/NEON, SSE4.1 off, no `WEBP_USE_THREAD`). The wrapper vendors no
13+
upstream code. It retains upstream's README.md, COPYING, PATENTS and AUTHORS under `upstream/libwebp/`, with checksums
14+
in `upstream/documents.json`, and deploys them with its own LICENSE/NOTICE (`runtime.deploy`).
15+
16+
## Modules
17+
18+
- `mcpplibs.mwrap.libwebp`: by `export using`, generated from the pinned public headers (`webp/types.h`, `decode.h`,
19+
`encode.h`, `mux_types.h`): 23 types, 59 enumerators, 72 functions; re-exports `.sharpyuv` (6 types, 25 enumerators,
20+
6 functions) and `.macros`.
21+
- `mcpplibs.mwrap.libwebp.demux` (`webp/demux.h`: 8 / 10 / 20) and `mcpplibs.mwrap.libwebp.mux` (`webp/mux.h`:
22+
7 / 17 / 27), with the `demux` and `mux` features (each adds its module's sources and forwards
23+
`compat.libwebp/<feature>`); each re-exports the root, as its header includes the root's. Each entity has one export
24+
owner.
25+
- The 22 `static inline` functions (internal linkage) are adapted in `mcpplibs::mwrap::libwebp` under the same names
26+
and signatures (`[[nodiscard]]` where the header has `WEBP_NODISCARD`), defined out of line by calling upstream's body.
27+
- `mcpplibs.mwrap.libwebp.macros`: 11 of the 24 macros (the ABI versions, `WEBP_MAX_DIMENSION`, sharpyuv's version as
28+
`inline constexpr int`; `WEBP_ABI_IS_INCOMPATIBLE` and `SHARPYUV_MAKE_VERSION` as constexpr functions); the rest are
29+
declaration annotations and include guards, excluded with reasons. No side header is needed.
30+
31+
`tools/check_exports.py` fails when a generated module is stale, a declaration is not exported or exported twice, a
32+
static inline function is not adapted, or a macro is neither mapped nor excluded.
33+
34+
## Verification
35+
36+
Wrapper CI (Linux x86_64 GCC 16.1 and LLVM 23.1.3, Linux arm64 GCC 16.1, macOS LLVM, Windows LLVM, wasm32-emscripten
37+
under node; dev and release profiles; default features and `demux`+`mux`): the API through the modules, the constants,
38+
the headers beside the import, the animation tests, the export audit; FNV-1a hashes of lossy and lossless files, of
39+
decoded pixels and of animations, the same on every row; and, but on Windows, `tools/run_conformance.py`:
40+
`examples/webpcodec` (written against the module) against upstream's `cwebp`/`dwebp` 1.5.0 built by its makefile.unix,
41+
76 comparisons, all byte-identical.
42+
43+
Here, with the CI-pinned mcpp 2026.10.27.1:
44+
45+
- `bash tests/run_members.sh libwebp-image`: the real release archive through the candidate index, with both features:
46+
lossy and lossless RGBA round trips with alpha, the advanced API with its pinned bytes, an animation encoded and
47+
played back frame by frame, the error paths.
48+
- `python3 tests/mwrap/check_installed_template.py --package mcpplibs.mwrap.libwebp@1.5.0 --deploy`: the unmodified
49+
default `image` template from the real archive, then a relocated bundle whose `licenses/libwebp/` holds the wrapper's
50+
LICENSE/NOTICE and libwebp's README, COPYING, PATENTS and AUTHORS byte for byte.
51+
52+
## Motivation
53+
54+
openmui decodes and encodes images through C++ module imports (libpng so far); libwebp adds WebP with the same shape:
55+
RGBA in and out from memory, lossy at a quality and lossless, alpha, animated WebP through WebPAnimEncoder and
56+
WebPAnimDecoder, and the same bytes for the same input on every platform, the Web included.

‎.github/workflows/validate.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1658,6 +1658,11 @@ jobs:
16581658
shell: bash
16591659
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.box2d@3.2.0 --deploy
16601660

1661+
- name: Installed libwebp wrapper archive, actual default image template and its relocated notices
1662+
if: contains(format(' {0} ', env.MEMBERS), ' libwebp-image ') && matrix.toolchain == 'default'
1663+
shell: bash
1664+
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.libwebp@1.5.0 --deploy
1665+
16611666
- name: Installed PocketPy wrapper archive, actual default script template and its relocated notices
16621667
if: contains(format(' {0} ', env.MEMBERS), ' pocketpy-embed ') && matrix.toolchain == 'default'
16631668
shell: bash

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@
99

1010
### Added
1111

12+
- **mcpplibs.mwrap.libwebp 1.5.0(新包)。** libwebp-m:以 C++23 模块(`import mcpplibs.mwrap.libwebp;`)使用
13+
compat.libwebp 1.5.0(BSD-3-Clause 与 Google 专利授权)。从固定版本的公开头文件生成,以 `export using` 导出:根模块
14+
`webp/types.h`、`decode.h`、`encode.h`、`mux_types.h` 的 23 个类型、59 个枚举值与 72 个函数,`mcpplibs.mwrap.libwebp.sharpyuv`
15+
(根模块重新导出),以及 `demux`/`mux` 特性下的 `mcpplibs.mwrap.libwebp.demux`(`WebPDemux*`、`WebPAnimDecoder*`)与
16+
`mcpplibs.mwrap.libwebp.mux`(`WebPMux*`、`WebPAnimEncoder*`),特性转发到 compat.libwebp 的同名特性。22 个
17+
`static inline` 函数(`WebPConfigInit`、`WebPPictureInit`、`WebPGetFeatures`、`WebPAnimEncoderNew`…)在
18+
`mcpplibs::mwrap::libwebp` 中以同名同签名适配;`mcpplibs.mwrap.libwebp.macros` 映射 ABI 版本、`WEBP_MAX_DIMENSION`、
19+
sharpyuv 版本与 `WEBP_ABI_IS_INCOMPATIBLE`/`SHARPYUV_MAKE_VERSION`。包装仓库 CI(Linux x86_64 GCC/LLVM、Linux arm64 GCC、
20+
macOS、Windows、wasm32-emscripten,dev 与 release)固定编码字节与解码像素的哈希,在所有目标上一致;一致性检查确认
21+
`examples/webpcodec` 的编码与解码结果与上游 makefile.unix 构建的 `cwebp`/`dwebp` 逐字节相同(76 项)。描述文件
22+
`pkgs/mwrap/libwebp.lua` 指向不可变的 `mwrap-1.5.0` 发布归档(暂无 CN 镜像)。新增测试成员 `tests/mwrap/libwebp-image`
23+
(开启 demux 与 mux:有损与无损 RGBA 往返含 alpha、高级 API 及固定哈希、动画逐帧编码与回放、错误路径),validate.yml 中
24+
验证已安装归档的默认 `image` 模板及其迁移后的许可文件。
1225
- **mcpplibs.mwrap.pocketpy 2.2.0(新包)。** pocketpy-m:以 C++23 模块(`import mcpplibs.mwrap.pocketpy;`)使用
1326
compat.pocketpy 2.2.0(MIT)。从固定版本的 `<pocketpy.h>` 生成,以 `export using` 导出全部 29 个类型、91 个枚举值与
1427
246 个函数(上游全局名不变);`mcpplibs.mwrap.pocketpy.macros` 把 82 个宏中的 65 个映射为常量与函数(`PK_VERSION`、

‎mcpp.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ members = [
2121
"tests/mwrap/quickjs-embed",
2222
"tests/mwrap/pocketpy-embed",
2323
"tests/mwrap/libpng-image",
24+
"tests/mwrap/libwebp-image",
2425
"tests/mwrap/concurrentqueue-mpmc",
2526
"tests/mwrap/fribidi-bidi",
2627
"tests/mwrap/sheenbidi-bidi",

‎pkgs/mwrap/libwebp.lua‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
-- Immutable upstream-aligned mwrap source release (libwebp-m mwrap-1.5.0).
2+
-- The CN mirror is added by a maintainer with mcpp-res access; until then CN
3+
-- users fall back to the GitHub release (docs/cn-mirror.md).
4+
package = {
5+
spec = "1",
6+
namespace = "mcpplibs.mwrap",
7+
name = "libwebp",
8+
description = "libwebp-m: libwebp (the WebP image codec: lossy, lossless, alpha, animation) as C++23 modules",
9+
licenses = { "Apache-2.0" },
10+
repo = "https://github.com/mcpplibs/libwebp-m",
11+
type = "package",
12+
extensions = {
13+
autoupdate = {
14+
tag = "mwrap-${version}",
15+
url = "https://github.com/mcpplibs/libwebp-m/releases/download/${tag}/libwebp-m-${version}.tar.gz",
16+
},
17+
},
18+
xpm = {
19+
linux = {
20+
["1.5.0"] = {
21+
url = "https://github.com/mcpplibs/libwebp-m/releases/download/mwrap-1.5.0/libwebp-m-1.5.0.tar.gz",
22+
sha256 = "2a085e5e795f0dbc110f26ae8f423dcd2e517f7f6d7dd6f06e8877dea4dbf6a0",
23+
},
24+
},
25+
macosx = {
26+
["1.5.0"] = {
27+
url = "https://github.com/mcpplibs/libwebp-m/releases/download/mwrap-1.5.0/libwebp-m-1.5.0.tar.gz",
28+
sha256 = "2a085e5e795f0dbc110f26ae8f423dcd2e517f7f6d7dd6f06e8877dea4dbf6a0",
29+
},
30+
},
31+
windows = {
32+
["1.5.0"] = {
33+
url = "https://github.com/mcpplibs/libwebp-m/releases/download/mwrap-1.5.0/libwebp-m-1.5.0.tar.gz",
34+
sha256 = "2a085e5e795f0dbc110f26ae8f423dcd2e517f7f6d7dd6f06e8877dea4dbf6a0",
35+
},
36+
},
37+
},
38+
}

‎tests/mwrap/check_installed_template.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,31 @@ def main():
247247
notice = (roots[0] / "NOTICE").read_bytes()
248248
assert b"mcpp-community" in notice and b"Erin Catto" in notice
249249
print(f"Verified {len(declared)} byte-identical wrapper/Box2D license files", flush=True)
250+
elif identity == "mcpplibs.mwrap.libwebp":
251+
# libwebp (BSD-3-Clause with Google's patent grant) is linked
252+
# statically: the wrapper's LICENSE/NOTICE and libwebp's README,
253+
# COPYING, PATENTS and AUTHORS travel with the program, beside
254+
# the COPYING/PATENTS/AUTHORS compat.libwebp deploys itself.
255+
sources = [path for store in (home / "xpkgs", home / "registry/data/xpkgs")
256+
for path in store.glob(f"mcpplibs.mwrap-x-libwebp/{version}/*/src/libwebp.cppm")]
257+
assert len(sources) == 1, sources
258+
source = sources[0].parent.parent
259+
declared = tomllib.loads((source / "mcpp.toml").read_text(encoding="utf-8"))["runtime"]["deploy"]
260+
roots = list(relocated.rglob("licenses/libwebp"))
261+
assert len(roots) == 1, f"Missing libwebp runtime license tree: {roots}"
262+
for entry in declared:
263+
notice = source / entry["from"]
264+
copied = roots[0].parent.parent / entry["to"] / notice.name
265+
assert copied.is_file() and copied.read_bytes() == notice.read_bytes(), copied
266+
upstream = roots[0] / "upstream/libwebp"
267+
assert b"Google Inc." in (upstream / "COPYING").read_bytes()
268+
assert b"patent" in (upstream / "PATENTS").read_bytes()
269+
for name in ("COPYING", "PATENTS", "AUTHORS"): # compat.libwebp's own deployment
270+
assert (roots[0] / name).read_bytes() == (upstream / name).read_bytes(), name
271+
assert b"Apache License" in (roots[0] / "LICENSE").read_bytes()
272+
notice = (roots[0] / "NOTICE").read_bytes()
273+
assert b"mcpp-community" in notice and b"Google" in notice
274+
print(f"Verified {len(declared)} byte-identical wrapper/libwebp license files", flush=True)
250275
elif identity == "mcpplibs.mwrap.pocketpy":
251276
# PocketPy is MIT and linked statically: its LICENSE and README
252277
# travel with the program beside the wrapper's LICENSE/NOTICE.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "libwebp-image"
3+
version = "1.5.0"
4+
description = "Consume the immutable libwebp wrapper with the demux and mux features: lossy and lossless RGBA round trips with alpha, the advanced API, an animation encoded and decoded frame by frame, the error paths, pinned bytes"
5+
6+
[indices]
7+
# Use the complete candidate built-in index prepared by tests/run_members.sh.
8+
# An explicit empty table overrides the legacy workspace redirect.
9+
10+
[dependencies]
11+
"mcpplibs.mwrap.libwebp" = { version = "1.5.0", features = ["demux", "mux"] }
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// libwebp through the published module, with the demux and mux features:
2+
// RGBA with alpha encoded lossy at a quality and lossless and decoded back;
3+
// the advanced API (WebPConfig, WebPPicture, a memory writer) with its pinned
4+
// bytes; an animation encoded with WebPAnimEncoder and played back with
5+
// WebPAnimDecoder; and the error paths.
6+
import std;
7+
import mcpplibs.mwrap.libwebp.demux; // re-exports mcpplibs.mwrap.libwebp
8+
import mcpplibs.mwrap.libwebp.mux;
9+
10+
using namespace mcpplibs::mwrap::libwebp; // the headers' static inline functions, adapted
11+
12+
namespace {
13+
14+
int failures = 0;
15+
16+
void check(bool ok, std::string_view what) {
17+
std::println("{:<64} {}", what, ok ? "ok" : "FAILED");
18+
failures += !ok;
19+
}
20+
21+
constexpr int W = 96, H = 64;
22+
23+
std::vector<std::uint8_t> picture(int shift) {
24+
std::vector<std::uint8_t> rgba(static_cast<std::size_t>(W) * H * 4);
25+
for (int y = 0; y < H; ++y)
26+
for (int x = 0; x < W; ++x) {
27+
auto* p = &rgba[(static_cast<std::size_t>(y) * W + x) * 4];
28+
p[0] = static_cast<std::uint8_t>((x + shift) * 2);
29+
p[1] = static_cast<std::uint8_t>(y * 4);
30+
p[2] = static_cast<std::uint8_t>(255 - x * 2);
31+
p[3] = static_cast<std::uint8_t>(y < 8 ? 64 : 255);
32+
}
33+
return rgba;
34+
}
35+
36+
std::uint64_t fnv1a(std::span<const std::uint8_t> data) {
37+
std::uint64_t h = 0xcbf29ce484222325ull;
38+
for (const auto b : data) h = (h ^ b) * 0x100000001b3ull;
39+
return h;
40+
}
41+
42+
} // namespace
43+
44+
int main() {
45+
const auto rgba = picture(0);
46+
47+
// The simple API, lossy at quality 80 and lossless.
48+
std::uint8_t *lossy = nullptr, *lossless = nullptr;
49+
const std::size_t lossy_size = WebPEncodeRGBA(rgba.data(), W, H, W * 4, 80.f, &lossy);
50+
const std::size_t lossless_size = WebPEncodeLosslessRGBA(rgba.data(), W, H, W * 4, &lossless);
51+
check(lossy_size > 0 && lossless_size > 0, "WebPEncodeRGBA / WebPEncodeLosslessRGBA");
52+
WebPBitstreamFeatures features;
53+
check(WebPGetFeatures(lossy, lossy_size, &features) == VP8_STATUS_OK && features.format == 1 && features.has_alpha,
54+
"WebPGetFeatures: lossy, with alpha");
55+
int width = 0, height = 0;
56+
check(WebPGetInfo(lossless, lossless_size, &width, &height) && width == W && height == H, "WebPGetInfo");
57+
std::uint8_t* back = WebPDecodeRGBA(lossless, lossless_size, &width, &height);
58+
check(back && std::equal(rgba.begin(), rgba.end(), back), "lossless round trip, every pixel");
59+
WebPFree(back);
60+
back = WebPDecodeRGBA(lossy, lossy_size, &width, &height);
61+
int worst = 0;
62+
bool alpha = back != nullptr;
63+
for (std::size_t i = 0; back && i < rgba.size(); ++i)
64+
if (i % 4 == 3) alpha = alpha && back[i] == rgba[i];
65+
else if (rgba[i - i % 4 + 3] == 255) worst = std::max(worst, std::abs(int(back[i]) - int(rgba[i])));
66+
check(back && alpha && worst < 24, std::format("lossy round trip: alpha exact, colour within {}", worst));
67+
WebPFree(back);
68+
WebPFree(lossy);
69+
WebPFree(lossless);
70+
71+
// The advanced API: the same settings give the same bytes everywhere.
72+
WebPConfig config;
73+
WebPPicture pic;
74+
WebPMemoryWriter writer;
75+
check(WebPConfigInit(&config) && WebPPictureInit(&pic), "WebPConfigInit / WebPPictureInit");
76+
config.quality = 75;
77+
config.method = 4;
78+
pic.use_argb = 1;
79+
pic.width = W;
80+
pic.height = H;
81+
WebPMemoryWriterInit(&writer);
82+
pic.writer = WebPMemoryWrite;
83+
pic.custom_ptr = &writer;
84+
const bool encoded = WebPPictureImportRGBA(&pic, rgba.data(), W * 4) && WebPEncode(&config, &pic);
85+
const auto hash = encoded ? fnv1a({writer.mem, writer.size}) : 0;
86+
check(encoded && hash == 0xf128a79c2ff683a2ull, std::format("WebPEncode q75 m4: {} bytes, fnv1a 0x{:016x}", writer.size, hash));
87+
WebPPictureFree(&pic);
88+
WebPMemoryWriterClear(&writer);
89+
90+
// An animation: three frames in, three full canvases out.
91+
WebPAnimEncoderOptions options;
92+
check(WebPAnimEncoderOptionsInit(&options) != 0, "WebPAnimEncoderOptionsInit");
93+
WebPAnimEncoder* encoder = WebPAnimEncoderNew(W, H, &options);
94+
config.lossless = 1;
95+
config.exact = 1;
96+
bool added = encoder && WebPPictureInit(&pic);
97+
pic.use_argb = 1;
98+
pic.width = W;
99+
pic.height = H;
100+
for (int f = 0; f < 3 && added; ++f) {
101+
const auto frame = picture(f * 10);
102+
added = WebPPictureImportRGBA(&pic, frame.data(), W * 4) && WebPAnimEncoderAdd(encoder, &pic, f * 100, &config);
103+
}
104+
WebPPictureFree(&pic);
105+
WebPData animation;
106+
WebPDataInit(&animation);
107+
added = added && WebPAnimEncoderAdd(encoder, nullptr, 300, nullptr) && WebPAnimEncoderAssemble(encoder, &animation);
108+
WebPAnimEncoderDelete(encoder);
109+
check(added, std::format("WebPAnimEncoder: 3 frames, {} bytes", animation.size));
110+
WebPAnimDecoderOptions decoding;
111+
check(WebPAnimDecoderOptionsInit(&decoding) != 0, "WebPAnimDecoderOptionsInit");
112+
decoding.color_mode = MODE_RGBA;
113+
WebPAnimDecoder* decoder = WebPAnimDecoderNew(&animation, &decoding);
114+
WebPAnimInfo info{};
115+
check(decoder && WebPAnimDecoderGetInfo(decoder, &info) && info.frame_count == 3, "WebPAnimDecoder: 3 frames");
116+
int f = 0;
117+
bool frames = decoder != nullptr;
118+
while (decoder && WebPAnimDecoderHasMoreFrames(decoder)) {
119+
std::uint8_t* canvas = nullptr;
120+
int timestamp = 0;
121+
const auto expected = picture(f * 10);
122+
frames = frames && WebPAnimDecoderGetNext(decoder, &canvas, &timestamp) && timestamp == (f + 1) * 100 &&
123+
std::equal(expected.begin(), expected.end(), canvas);
124+
++f;
125+
}
126+
check(frames && f == 3, "every frame exact, at its time");
127+
WebPAnimDecoderDelete(decoder);
128+
WebPDemuxer* demux = WebPDemux(&animation);
129+
check(demux && WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT) == 3, "WebPDemux: frame count");
130+
WebPDemuxDelete(demux);
131+
WebPDataClear(&animation);
132+
133+
// Errors answer, never abort.
134+
const std::array<std::uint8_t, 12> garbage{'R', 'I', 'F', 'F', 4, 0, 0, 0, 'W', 'E', 'B', 'P'};
135+
check(WebPDecodeRGBA(garbage.data(), garbage.size(), &width, &height) == nullptr, "garbage: WebPDecodeRGBA is null");
136+
const WebPData bad{garbage.data(), garbage.size()};
137+
check(WebPAnimDecoderNew(&bad, nullptr) == nullptr, "garbage: WebPAnimDecoderNew is null");
138+
std::uint8_t* none = nullptr;
139+
check(WebPEncodeRGBA(rgba.data(), mcpplibs::mwrap::libwebp::macros::WEBP_MAX_DIMENSION + 1, 1, W * 4, 75.f, &none) == 0,
140+
"beyond WEBP_MAX_DIMENSION: 0 bytes");
141+
WebPFree(none);
142+
return failures;
143+
}

0 commit comments

Comments
 (0)