Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .agents/docs/2026-10-29-add-libwebp-mwrap-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Add mcpplibs.mwrap.libwebp (libwebp-m 1.5.0)

## Form

- Third-party upstream [webmproject/libwebp](https://github.com/webmproject/libwebp), tag `v1.5.0`, BSD-3-Clause
with Google's additional patent grant. The tarball (`668c9aba…7f37`) is the one compat.libwebp 1.5.0 pins.
- Form A external mwrap module repository [mcpplibs/libwebp-m](https://github.com/mcpplibs/libwebp-m), package
`mcpplibs.mwrap.libwebp`, version 1.5.0 (first release of the identity, no revision suffix), tag series
`mwrap-${version}` on the merged main commit (`0252b65`); release asset `libwebp-m-1.5.0.tar.gz` (`2a085e5e…f6a0`).
No CN mirror yet (docs/cn-mirror.md fallback).
- Implementation ownership: the codec is compat.libwebp's (dec, enc, dsp, utils and sharpyuv in one archive; src/demux
and src/mux with its `demux` and `mux` features; SSE2/NEON, SSE4.1 off, no `WEBP_USE_THREAD`). The wrapper vendors no
upstream code. It retains upstream's README.md, COPYING, PATENTS and AUTHORS under `upstream/libwebp/`, with checksums
in `upstream/documents.json`, and deploys them with its own LICENSE/NOTICE (`runtime.deploy`).

## Modules

- `mcpplibs.mwrap.libwebp`: by `export using`, generated from the pinned public headers (`webp/types.h`, `decode.h`,
`encode.h`, `mux_types.h`): 23 types, 59 enumerators, 72 functions; re-exports `.sharpyuv` (6 types, 25 enumerators,
6 functions) and `.macros`.
- `mcpplibs.mwrap.libwebp.demux` (`webp/demux.h`: 8 / 10 / 20) and `mcpplibs.mwrap.libwebp.mux` (`webp/mux.h`:
7 / 17 / 27), with the `demux` and `mux` features (each adds its module's sources and forwards
`compat.libwebp/<feature>`); each re-exports the root, as its header includes the root's. Each entity has one export
owner.
- The 22 `static inline` functions (internal linkage) are adapted in `mcpplibs::mwrap::libwebp` under the same names
and signatures (`[[nodiscard]]` where the header has `WEBP_NODISCARD`), defined out of line by calling upstream's body.
- `mcpplibs.mwrap.libwebp.macros`: 11 of the 24 macros (the ABI versions, `WEBP_MAX_DIMENSION`, sharpyuv's version as
`inline constexpr int`; `WEBP_ABI_IS_INCOMPATIBLE` and `SHARPYUV_MAKE_VERSION` as constexpr functions); the rest are
declaration annotations and include guards, excluded with reasons. No side header is needed.

`tools/check_exports.py` fails when a generated module is stale, a declaration is not exported or exported twice, a
static inline function is not adapted, or a macro is neither mapped nor excluded.

## Verification

Wrapper CI (Linux x86_64 GCC 16.1 and LLVM 23.1.3, Linux arm64 GCC 16.1, macOS LLVM, Windows LLVM, wasm32-emscripten
under node; dev and release profiles; default features and `demux`+`mux`): the API through the modules, the constants,
the headers beside the import, the animation tests, the export audit; FNV-1a hashes of lossy and lossless files, of
decoded pixels and of animations, the same on every row; and, but on Windows, `tools/run_conformance.py`:
`examples/webpcodec` (written against the module) against upstream's `cwebp`/`dwebp` 1.5.0 built by its makefile.unix,
76 comparisons, all byte-identical.

Here, with the CI-pinned mcpp 2026.10.27.1:

- `bash tests/run_members.sh libwebp-image`: the real release archive through the candidate index, with both 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.
- `python3 tests/mwrap/check_installed_template.py --package mcpplibs.mwrap.libwebp@1.5.0 --deploy`: the unmodified
default `image` template from the real archive, then a relocated bundle whose `licenses/libwebp/` holds the wrapper's
LICENSE/NOTICE and libwebp's README, COPYING, PATENTS and AUTHORS byte for byte.

## Motivation

openmui decodes and encodes images through C++ module imports (libpng so far); libwebp adds WebP with the same shape:
RGBA in and out from memory, lossy at a quality and lossless, alpha, animated WebP through WebPAnimEncoder and
WebPAnimDecoder, and the same bytes for the same input on every platform, the Web included.
5 changes: 5 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1658,6 +1658,11 @@ jobs:
shell: bash
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.box2d@3.2.0 --deploy

- name: Installed libwebp wrapper archive, actual default image template and its relocated notices
if: contains(format(' {0} ', env.MEMBERS), ' libwebp-image ') && matrix.toolchain == 'default'
shell: bash
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.libwebp@1.5.0 --deploy

- name: Installed PocketPy wrapper archive, actual default script template and its relocated notices
if: contains(format(' {0} ', env.MEMBERS), ' pocketpy-embed ') && matrix.toolchain == 'default'
shell: bash
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@

### Added

- **mcpplibs.mwrap.libwebp 1.5.0(新包)。** libwebp-m:以 C++23 模块(`import mcpplibs.mwrap.libwebp;`)使用
compat.libwebp 1.5.0(BSD-3-Clause 与 Google 专利授权)。从固定版本的公开头文件生成,以 `export using` 导出:根模块
`webp/types.h`、`decode.h`、`encode.h`、`mux_types.h` 的 23 个类型、59 个枚举值与 72 个函数,`mcpplibs.mwrap.libwebp.sharpyuv`
(根模块重新导出),以及 `demux`/`mux` 特性下的 `mcpplibs.mwrap.libwebp.demux`(`WebPDemux*`、`WebPAnimDecoder*`)与
`mcpplibs.mwrap.libwebp.mux`(`WebPMux*`、`WebPAnimEncoder*`),特性转发到 compat.libwebp 的同名特性。22 个
`static inline` 函数(`WebPConfigInit`、`WebPPictureInit`、`WebPGetFeatures`、`WebPAnimEncoderNew`…)在
`mcpplibs::mwrap::libwebp` 中以同名同签名适配;`mcpplibs.mwrap.libwebp.macros` 映射 ABI 版本、`WEBP_MAX_DIMENSION`、
sharpyuv 版本与 `WEBP_ABI_IS_INCOMPATIBLE`/`SHARPYUV_MAKE_VERSION`。包装仓库 CI(Linux x86_64 GCC/LLVM、Linux arm64 GCC、
macOS、Windows、wasm32-emscripten,dev 与 release)固定编码字节与解码像素的哈希,在所有目标上一致;一致性检查确认
`examples/webpcodec` 的编码与解码结果与上游 makefile.unix 构建的 `cwebp`/`dwebp` 逐字节相同(76 项)。描述文件
`pkgs/mwrap/libwebp.lua` 指向不可变的 `mwrap-1.5.0` 发布归档(暂无 CN 镜像)。新增测试成员 `tests/mwrap/libwebp-image`
(开启 demux 与 mux:有损与无损 RGBA 往返含 alpha、高级 API 及固定哈希、动画逐帧编码与回放、错误路径),validate.yml 中
验证已安装归档的默认 `image` 模板及其迁移后的许可文件。
- **mcpplibs.mwrap.pocketpy 2.2.0(新包)。** pocketpy-m:以 C++23 模块(`import mcpplibs.mwrap.pocketpy;`)使用
compat.pocketpy 2.2.0(MIT)。从固定版本的 `<pocketpy.h>` 生成,以 `export using` 导出全部 29 个类型、91 个枚举值与
246 个函数(上游全局名不变);`mcpplibs.mwrap.pocketpy.macros` 把 82 个宏中的 65 个映射为常量与函数(`PK_VERSION`、
Expand Down
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ members = [
"tests/mwrap/quickjs-embed",
"tests/mwrap/pocketpy-embed",
"tests/mwrap/libpng-image",
"tests/mwrap/libwebp-image",
"tests/mwrap/concurrentqueue-mpmc",
"tests/mwrap/fribidi-bidi",
"tests/mwrap/sheenbidi-bidi",
Expand Down
38 changes: 38 additions & 0 deletions pkgs/mwrap/libwebp.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-- Immutable upstream-aligned mwrap source release (libwebp-m mwrap-1.5.0).
-- The CN mirror is added by a maintainer with mcpp-res access; until then CN
-- users fall back to the GitHub release (docs/cn-mirror.md).
package = {
spec = "1",
namespace = "mcpplibs.mwrap",
name = "libwebp",
description = "libwebp-m: libwebp (the WebP image codec: lossy, lossless, alpha, animation) as C++23 modules",
licenses = { "Apache-2.0" },
repo = "https://github.com/mcpplibs/libwebp-m",
type = "package",
extensions = {
autoupdate = {
tag = "mwrap-${version}",
url = "https://github.com/mcpplibs/libwebp-m/releases/download/${tag}/libwebp-m-${version}.tar.gz",
},
},
xpm = {
linux = {
["1.5.0"] = {
url = "https://github.com/mcpplibs/libwebp-m/releases/download/mwrap-1.5.0/libwebp-m-1.5.0.tar.gz",
sha256 = "2a085e5e795f0dbc110f26ae8f423dcd2e517f7f6d7dd6f06e8877dea4dbf6a0",
},
},
macosx = {
["1.5.0"] = {
url = "https://github.com/mcpplibs/libwebp-m/releases/download/mwrap-1.5.0/libwebp-m-1.5.0.tar.gz",
sha256 = "2a085e5e795f0dbc110f26ae8f423dcd2e517f7f6d7dd6f06e8877dea4dbf6a0",
},
},
windows = {
["1.5.0"] = {
url = "https://github.com/mcpplibs/libwebp-m/releases/download/mwrap-1.5.0/libwebp-m-1.5.0.tar.gz",
sha256 = "2a085e5e795f0dbc110f26ae8f423dcd2e517f7f6d7dd6f06e8877dea4dbf6a0",
},
},
},
}
25 changes: 25 additions & 0 deletions tests/mwrap/check_installed_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,31 @@ def main():
notice = (roots[0] / "NOTICE").read_bytes()
assert b"mcpp-community" in notice and b"Erin Catto" in notice
print(f"Verified {len(declared)} byte-identical wrapper/Box2D license files", flush=True)
elif identity == "mcpplibs.mwrap.libwebp":
# libwebp (BSD-3-Clause with Google's patent grant) is linked
# statically: the wrapper's LICENSE/NOTICE and libwebp's README,
# COPYING, PATENTS and AUTHORS travel with the program, beside
# the COPYING/PATENTS/AUTHORS compat.libwebp deploys itself.
sources = [path for store in (home / "xpkgs", home / "registry/data/xpkgs")
for path in store.glob(f"mcpplibs.mwrap-x-libwebp/{version}/*/src/libwebp.cppm")]
assert len(sources) == 1, sources
source = sources[0].parent.parent
declared = tomllib.loads((source / "mcpp.toml").read_text(encoding="utf-8"))["runtime"]["deploy"]
roots = list(relocated.rglob("licenses/libwebp"))
assert len(roots) == 1, f"Missing libwebp runtime license tree: {roots}"
for entry in declared:
notice = source / entry["from"]
copied = roots[0].parent.parent / entry["to"] / notice.name
assert copied.is_file() and copied.read_bytes() == notice.read_bytes(), copied
upstream = roots[0] / "upstream/libwebp"
assert b"Google Inc." in (upstream / "COPYING").read_bytes()
assert b"patent" in (upstream / "PATENTS").read_bytes()
for name in ("COPYING", "PATENTS", "AUTHORS"): # compat.libwebp's own deployment
assert (roots[0] / name).read_bytes() == (upstream / name).read_bytes(), name
assert b"Apache License" in (roots[0] / "LICENSE").read_bytes()
notice = (roots[0] / "NOTICE").read_bytes()
assert b"mcpp-community" in notice and b"Google" in notice
print(f"Verified {len(declared)} byte-identical wrapper/libwebp license files", flush=True)
elif identity == "mcpplibs.mwrap.pocketpy":
# PocketPy is MIT and linked statically: its LICENSE and README
# travel with the program beside the wrapper's LICENSE/NOTICE.
Expand Down
11 changes: 11 additions & 0 deletions tests/mwrap/libwebp-image/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "libwebp-image"
version = "1.5.0"
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"

[indices]
# Use the complete candidate built-in index prepared by tests/run_members.sh.
# An explicit empty table overrides the legacy workspace redirect.

[dependencies]
"mcpplibs.mwrap.libwebp" = { version = "1.5.0", features = ["demux", "mux"] }
143 changes: 143 additions & 0 deletions tests/mwrap/libwebp-image/tests/behavior.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// libwebp through the published module, with the demux and mux features:
// RGBA with alpha encoded lossy at a quality and lossless and decoded back;
// the advanced API (WebPConfig, WebPPicture, a memory writer) with its pinned
// bytes; an animation encoded with WebPAnimEncoder and played back with
// WebPAnimDecoder; and the error paths.
import std;
import mcpplibs.mwrap.libwebp.demux; // re-exports mcpplibs.mwrap.libwebp
import mcpplibs.mwrap.libwebp.mux;

using namespace mcpplibs::mwrap::libwebp; // the headers' static inline functions, adapted

namespace {

int failures = 0;

void check(bool ok, std::string_view what) {
std::println("{:<64} {}", what, ok ? "ok" : "FAILED");
failures += !ok;
}

constexpr int W = 96, H = 64;

std::vector<std::uint8_t> picture(int shift) {
std::vector<std::uint8_t> rgba(static_cast<std::size_t>(W) * H * 4);
for (int y = 0; y < H; ++y)
for (int x = 0; x < W; ++x) {
auto* p = &rgba[(static_cast<std::size_t>(y) * W + x) * 4];
p[0] = static_cast<std::uint8_t>((x + shift) * 2);
p[1] = static_cast<std::uint8_t>(y * 4);
p[2] = static_cast<std::uint8_t>(255 - x * 2);
p[3] = static_cast<std::uint8_t>(y < 8 ? 64 : 255);
}
return rgba;
}

std::uint64_t fnv1a(std::span<const std::uint8_t> data) {
std::uint64_t h = 0xcbf29ce484222325ull;
for (const auto b : data) h = (h ^ b) * 0x100000001b3ull;
return h;
}

} // namespace

int main() {
const auto rgba = picture(0);

// The simple API, lossy at quality 80 and lossless.
std::uint8_t *lossy = nullptr, *lossless = nullptr;
const std::size_t lossy_size = WebPEncodeRGBA(rgba.data(), W, H, W * 4, 80.f, &lossy);
const std::size_t lossless_size = WebPEncodeLosslessRGBA(rgba.data(), W, H, W * 4, &lossless);
check(lossy_size > 0 && lossless_size > 0, "WebPEncodeRGBA / WebPEncodeLosslessRGBA");
WebPBitstreamFeatures features;
check(WebPGetFeatures(lossy, lossy_size, &features) == VP8_STATUS_OK && features.format == 1 && features.has_alpha,
"WebPGetFeatures: lossy, with alpha");
int width = 0, height = 0;
check(WebPGetInfo(lossless, lossless_size, &width, &height) && width == W && height == H, "WebPGetInfo");
std::uint8_t* back = WebPDecodeRGBA(lossless, lossless_size, &width, &height);
check(back && std::equal(rgba.begin(), rgba.end(), back), "lossless round trip, every pixel");
WebPFree(back);
back = WebPDecodeRGBA(lossy, lossy_size, &width, &height);
int worst = 0;
bool alpha = back != nullptr;
for (std::size_t i = 0; back && i < rgba.size(); ++i)
if (i % 4 == 3) alpha = alpha && back[i] == rgba[i];
else if (rgba[i - i % 4 + 3] == 255) worst = std::max(worst, std::abs(int(back[i]) - int(rgba[i])));
check(back && alpha && worst < 24, std::format("lossy round trip: alpha exact, colour within {}", worst));
WebPFree(back);
WebPFree(lossy);
WebPFree(lossless);

// The advanced API: the same settings give the same bytes everywhere.
WebPConfig config;
WebPPicture pic;
WebPMemoryWriter writer;
check(WebPConfigInit(&config) && WebPPictureInit(&pic), "WebPConfigInit / WebPPictureInit");
config.quality = 75;
config.method = 4;
pic.use_argb = 1;
pic.width = W;
pic.height = H;
WebPMemoryWriterInit(&writer);
pic.writer = WebPMemoryWrite;
pic.custom_ptr = &writer;
const bool encoded = WebPPictureImportRGBA(&pic, rgba.data(), W * 4) && WebPEncode(&config, &pic);
const auto hash = encoded ? fnv1a({writer.mem, writer.size}) : 0;
check(encoded && hash == 0xf128a79c2ff683a2ull, std::format("WebPEncode q75 m4: {} bytes, fnv1a 0x{:016x}", writer.size, hash));
WebPPictureFree(&pic);
WebPMemoryWriterClear(&writer);

// An animation: three frames in, three full canvases out.
WebPAnimEncoderOptions options;
check(WebPAnimEncoderOptionsInit(&options) != 0, "WebPAnimEncoderOptionsInit");
WebPAnimEncoder* encoder = WebPAnimEncoderNew(W, H, &options);
config.lossless = 1;
config.exact = 1;
bool added = encoder && WebPPictureInit(&pic);
pic.use_argb = 1;
pic.width = W;
pic.height = H;
for (int f = 0; f < 3 && added; ++f) {
const auto frame = picture(f * 10);
added = WebPPictureImportRGBA(&pic, frame.data(), W * 4) && WebPAnimEncoderAdd(encoder, &pic, f * 100, &config);
}
WebPPictureFree(&pic);
WebPData animation;
WebPDataInit(&animation);
added = added && WebPAnimEncoderAdd(encoder, nullptr, 300, nullptr) && WebPAnimEncoderAssemble(encoder, &animation);
WebPAnimEncoderDelete(encoder);
check(added, std::format("WebPAnimEncoder: 3 frames, {} bytes", animation.size));
WebPAnimDecoderOptions decoding;
check(WebPAnimDecoderOptionsInit(&decoding) != 0, "WebPAnimDecoderOptionsInit");
decoding.color_mode = MODE_RGBA;
WebPAnimDecoder* decoder = WebPAnimDecoderNew(&animation, &decoding);
WebPAnimInfo info{};
check(decoder && WebPAnimDecoderGetInfo(decoder, &info) && info.frame_count == 3, "WebPAnimDecoder: 3 frames");
int f = 0;
bool frames = decoder != nullptr;
while (decoder && WebPAnimDecoderHasMoreFrames(decoder)) {
std::uint8_t* canvas = nullptr;
int timestamp = 0;
const auto expected = picture(f * 10);
frames = frames && WebPAnimDecoderGetNext(decoder, &canvas, &timestamp) && timestamp == (f + 1) * 100 &&
std::equal(expected.begin(), expected.end(), canvas);
++f;
}
check(frames && f == 3, "every frame exact, at its time");
WebPAnimDecoderDelete(decoder);
WebPDemuxer* demux = WebPDemux(&animation);
check(demux && WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT) == 3, "WebPDemux: frame count");
WebPDemuxDelete(demux);
WebPDataClear(&animation);

// Errors answer, never abort.
const std::array<std::uint8_t, 12> garbage{'R', 'I', 'F', 'F', 4, 0, 0, 0, 'W', 'E', 'B', 'P'};
check(WebPDecodeRGBA(garbage.data(), garbage.size(), &width, &height) == nullptr, "garbage: WebPDecodeRGBA is null");
const WebPData bad{garbage.data(), garbage.size()};
check(WebPAnimDecoderNew(&bad, nullptr) == nullptr, "garbage: WebPAnimDecoderNew is null");
std::uint8_t* none = nullptr;
check(WebPEncodeRGBA(rgba.data(), mcpplibs::mwrap::libwebp::macros::WEBP_MAX_DIMENSION + 1, 1, W * 4, 75.f, &none) == 0,
"beyond WEBP_MAX_DIMENSION: 0 bytes");
WebPFree(none);
return failures;
}
Loading