From f973d05d98110ffba264ccd238f49101b123ece1 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 29 Oct 2026 08:09:12 +0900 Subject: [PATCH] mcpplibs.mwrap.libwebp 1.5.0: libwebp-m (libwebp as C++23 modules) 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. --- .../docs/2026-10-29-add-libwebp-mwrap-plan.md | 56 +++++++ .github/workflows/validate.yml | 5 + CHANGELOG.md | 13 ++ mcpp.toml | 1 + pkgs/mwrap/libwebp.lua | 38 +++++ tests/mwrap/check_installed_template.py | 25 +++ tests/mwrap/libwebp-image/mcpp.toml | 11 ++ tests/mwrap/libwebp-image/tests/behavior.cpp | 143 ++++++++++++++++++ 8 files changed, 292 insertions(+) create mode 100644 .agents/docs/2026-10-29-add-libwebp-mwrap-plan.md create mode 100644 pkgs/mwrap/libwebp.lua create mode 100644 tests/mwrap/libwebp-image/mcpp.toml create mode 100644 tests/mwrap/libwebp-image/tests/behavior.cpp diff --git a/.agents/docs/2026-10-29-add-libwebp-mwrap-plan.md b/.agents/docs/2026-10-29-add-libwebp-mwrap-plan.md new file mode 100644 index 00000000..ee6a97b2 --- /dev/null +++ b/.agents/docs/2026-10-29-add-libwebp-mwrap-plan.md @@ -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/`); 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. diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6d8cd0bd..91b8709c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index f457006f..38070351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)。从固定版本的 `` 生成,以 `export using` 导出全部 29 个类型、91 个枚举值与 246 个函数(上游全局名不变);`mcpplibs.mwrap.pocketpy.macros` 把 82 个宏中的 65 个映射为常量与函数(`PK_VERSION`、 diff --git a/mcpp.toml b/mcpp.toml index c7051e88..2f35e8fc 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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", diff --git a/pkgs/mwrap/libwebp.lua b/pkgs/mwrap/libwebp.lua new file mode 100644 index 00000000..2cf27a43 --- /dev/null +++ b/pkgs/mwrap/libwebp.lua @@ -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", + }, + }, + }, +} diff --git a/tests/mwrap/check_installed_template.py b/tests/mwrap/check_installed_template.py index 312cd64a..810ebf8b 100644 --- a/tests/mwrap/check_installed_template.py +++ b/tests/mwrap/check_installed_template.py @@ -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. diff --git a/tests/mwrap/libwebp-image/mcpp.toml b/tests/mwrap/libwebp-image/mcpp.toml new file mode 100644 index 00000000..67742803 --- /dev/null +++ b/tests/mwrap/libwebp-image/mcpp.toml @@ -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"] } diff --git a/tests/mwrap/libwebp-image/tests/behavior.cpp b/tests/mwrap/libwebp-image/tests/behavior.cpp new file mode 100644 index 00000000..51317e59 --- /dev/null +++ b/tests/mwrap/libwebp-image/tests/behavior.cpp @@ -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 picture(int shift) { + std::vector rgba(static_cast(W) * H * 4); + for (int y = 0; y < H; ++y) + for (int x = 0; x < W; ++x) { + auto* p = &rgba[(static_cast(y) * W + x) * 4]; + p[0] = static_cast((x + shift) * 2); + p[1] = static_cast(y * 4); + p[2] = static_cast(255 - x * 2); + p[3] = static_cast(y < 8 ? 64 : 255); + } + return rgba; +} + +std::uint64_t fnv1a(std::span 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, ×tamp) && 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 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; +}