From 67c0495a6516b8c69d1db69469d37ba1ab9a2f7f Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 29 Oct 2026 07:51:27 +0900 Subject: [PATCH 1/3] libwebp-m: libwebp 1.5.0's decode, encode, sharpyuv, demux and mux API as modules, generated from the pinned headers (the static inline API adapted, the macros as constants and constexpr functions; demux and mux behind the features of the same names); API, macro, header-mixing, animation and pinned-hash tests; export audit --- .gitattributes | 2 + .gitignore | 7 + .xlings.json | 5 + NOTICE | 14 + mcpp.toml | 62 +++++ src/demux/demux.cppm | 70 +++++ src/demux/inline.cpp | 21 ++ src/inline.cpp | 36 +++ src/libwebp.cppm | 200 +++++++++++++ src/macros.cppm | 75 +++++ src/mux/inline.cpp | 21 ++ src/mux/mux.cppm | 83 ++++++ src/sharpyuv.cppm | 61 ++++ src/sharpyuv_inline.cpp | 20 ++ tests/anim/mcpp.toml | 12 + tests/anim/tests/anim_test.cpp | 302 ++++++++++++++++++++ tests/api_test.cpp | 494 +++++++++++++++++++++++++++++++++ tests/determinism_test.cpp | 113 ++++++++ tests/header_mix_test.cpp | 61 ++++ tests/macros.cpp | 33 +++ tools/check_exports.py | 91 ++++++ tools/gen_exports.py | 160 +++++++++++ tools/inventory.py | 155 +++++++++++ upstream/documents.json | 27 ++ upstream/libwebp/AUTHORS | 70 +++++ upstream/libwebp/COPYING | 30 ++ upstream/libwebp/PATENTS | 23 ++ upstream/libwebp/README.md | 54 ++++ 28 files changed, 2302 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .xlings.json create mode 100644 NOTICE create mode 100644 mcpp.toml create mode 100644 src/demux/demux.cppm create mode 100644 src/demux/inline.cpp create mode 100644 src/inline.cpp create mode 100644 src/libwebp.cppm create mode 100644 src/macros.cppm create mode 100644 src/mux/inline.cpp create mode 100644 src/mux/mux.cppm create mode 100644 src/sharpyuv.cppm create mode 100644 src/sharpyuv_inline.cpp create mode 100644 tests/anim/mcpp.toml create mode 100644 tests/anim/tests/anim_test.cpp create mode 100644 tests/api_test.cpp create mode 100644 tests/determinism_test.cpp create mode 100644 tests/header_mix_test.cpp create mode 100644 tests/macros.cpp create mode 100644 tools/check_exports.py create mode 100644 tools/gen_exports.py create mode 100644 tools/inventory.py create mode 100644 upstream/documents.json create mode 100644 upstream/libwebp/AUTHORS create mode 100644 upstream/libwebp/COPYING create mode 100644 upstream/libwebp/PATENTS create mode 100644 upstream/libwebp/README.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8a6c241 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Preserve upstream document bytes and original whitespace on every platform. +upstream/** -text -whitespace diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6dfbae --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +target +compile_commands.json +mcpp.lock +.mcpp/ +.xlings/ +doctor.log +__pycache__/ diff --git a/.xlings.json b/.xlings.json new file mode 100644 index 0000000..9a0e146 --- /dev/null +++ b/.xlings.json @@ -0,0 +1,5 @@ +{ + "workspace": { + "xim:mcpp": "2026.10.27.1" + } +} diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..ec664c3 --- /dev/null +++ b/NOTICE @@ -0,0 +1,14 @@ +libwebp-m: C++ module wrapper +Copyright (c) 2026 mcpp-community contributors + +The wrapper contributions are licensed under the Apache License, Version 2.0. +Upstream code, copied interfaces and dependencies retain their original +licenses and copyright notices. libwebp is distributed under the BSD +3-Clause license, with Google's additional patent grant: + + Copyright (c) 2010, Google Inc. All rights reserved. + +Its original README, license (COPYING), patent grant (PATENTS) and authors +are retained in upstream/libwebp/. The codec itself is built by +compat.libwebp from the same release. Provenance and checksums are recorded +in upstream/documents.json. diff --git a/mcpp.toml b/mcpp.toml new file mode 100644 index 0000000..e8e391a --- /dev/null +++ b/mcpp.toml @@ -0,0 +1,62 @@ +# Module wrapper; upstream API preserved. Policy: mcpp-index/docs/mwrap.md. +[package] +namespace = "mcpplibs.mwrap" +name = "libwebp" +version = "1.5.0" +description = "libwebp 1.5.0 C++ modules for mcpp (import mcpplibs.mwrap.libwebp;): the WebP codec's decode, encode and sharpyuv API over compat.libwebp, its static inline API adapted, its macros as constants and constexpr functions; demux (WebPAnimDecoder) and mux (WebPAnimEncoder) as features" +license = "Apache-2.0" +authors = ["mcpp-community"] +repo = "https://github.com/mcpplibs/libwebp-m" +platforms = ["linux", "macos", "windows"] + +[targets.libwebp] +kind = "lib" + +[build] +sources = ["src/*.cppm", "src/*.cpp"] + +[features] +default = [] +# libwebpdemux: the container parser and WebPAnimDecoder (compat.libwebp's +# src/demux), and the module that exports them. +demux = { sources = ["src/demux/*.cppm", "src/demux/*.cpp"], forward = ["compat.libwebp/demux"] } +# libwebpmux: the container editor and WebPAnimEncoder (compat.libwebp's +# src/mux), and the module that exports them. +mux = { sources = ["src/mux/*.cppm", "src/mux/*.cpp"], forward = ["compat.libwebp/mux"] } + +[dependencies] +compat.libwebp = "1.5.0" + +[dev-dependencies] +compat.gtest = "1.15.2" + +[test] +# tests/anim is its own project (it needs the demux and mux features). +discover = ["tests/*.cpp"] + +[package.metadata.mwrap] +upstream-version = "1.5.0" + +[[runtime.deploy]] +from = "LICENSE" +to = "licenses/libwebp" + +[[runtime.deploy]] +from = "NOTICE" +to = "licenses/libwebp" + +[[runtime.deploy]] +from = "upstream/libwebp/README.md" +to = "licenses/libwebp/upstream/libwebp" + +[[runtime.deploy]] +from = "upstream/libwebp/COPYING" +to = "licenses/libwebp/upstream/libwebp" + +[[runtime.deploy]] +from = "upstream/libwebp/PATENTS" +to = "licenses/libwebp/upstream/libwebp" + +[[runtime.deploy]] +from = "upstream/libwebp/AUTHORS" +to = "licenses/libwebp/upstream/libwebp" diff --git a/src/demux/demux.cppm b/src/demux/demux.cppm new file mode 100644 index 0000000..fb9b256 --- /dev/null +++ b/src/demux/demux.cppm @@ -0,0 +1,70 @@ +// Generated by tools/gen_exports.py from libwebp 1.5.0's headers — do not edit +// by hand. +// +// mcpplibs.mwrap.libwebp.demux — libwebpdemux (webp/demux.h): the container +// parser (WebPDemuxer, frames, chunks) and WebPAnimDecoder, which renders an +// animation's frames to full canvases. With the `demux` feature, which builds +// compat.libwebp's src/demux. Import it by name: the root does not re-export +// it (mcpp's scanner admits no import under a feature test); it re-exports the +// root, as demux.h includes decode.h and mux_types.h. +// +// The `static inline` API is adapted in mcpplibs::mwrap::libwebp +// (src/demux/inline.cpp). +module; +#include + +export module mcpplibs.mwrap.libwebp.demux; + +export import mcpplibs.mwrap.libwebp; + +// types (8) +export using ::WebPAnimDecoder; +export using ::WebPAnimDecoderOptions; +export using ::WebPAnimInfo; +export using ::WebPChunkIterator; +export using ::WebPDemuxState; +export using ::WebPDemuxer; +export using ::WebPFormatFeature; +export using ::WebPIterator; + +// enumerators (10) +export using ::WEBP_DEMUX_PARSE_ERROR; +export using ::WEBP_DEMUX_PARSING_HEADER; +export using ::WEBP_DEMUX_PARSED_HEADER; +export using ::WEBP_DEMUX_DONE; +export using ::WEBP_FF_FORMAT_FLAGS; +export using ::WEBP_FF_CANVAS_WIDTH; +export using ::WEBP_FF_CANVAS_HEIGHT; +export using ::WEBP_FF_LOOP_COUNT; +export using ::WEBP_FF_BACKGROUND_COLOR; +export using ::WEBP_FF_FRAME_COUNT; + +// functions (20) +export using ::WebPAnimDecoderDelete; +export using ::WebPAnimDecoderGetDemuxer; +export using ::WebPAnimDecoderGetInfo; +export using ::WebPAnimDecoderGetNext; +export using ::WebPAnimDecoderHasMoreFrames; +export using ::WebPAnimDecoderNewInternal; +export using ::WebPAnimDecoderOptionsInitInternal; +export using ::WebPAnimDecoderReset; +export using ::WebPDemuxDelete; +export using ::WebPDemuxGetChunk; +export using ::WebPDemuxGetFrame; +export using ::WebPDemuxGetI; +export using ::WebPDemuxInternal; +export using ::WebPDemuxNextChunk; +export using ::WebPDemuxNextFrame; +export using ::WebPDemuxPrevChunk; +export using ::WebPDemuxPrevFrame; +export using ::WebPDemuxReleaseChunkIterator; +export using ::WebPDemuxReleaseIterator; +export using ::WebPGetDemuxVersion; + +// the static inline API (4), adapted +export namespace mcpplibs::mwrap::libwebp { +[[nodiscard]] WebPDemuxer* WebPDemux(const WebPData* data); +[[nodiscard]] WebPDemuxer* WebPDemuxPartial(const WebPData* data, WebPDemuxState* state); +[[nodiscard]] int WebPAnimDecoderOptionsInit(WebPAnimDecoderOptions* dec_options); +[[nodiscard]] WebPAnimDecoder* WebPAnimDecoderNew(const WebPData* webp_data, const WebPAnimDecoderOptions* dec_options); +} // namespace mcpplibs::mwrap::libwebp diff --git a/src/demux/inline.cpp b/src/demux/inline.cpp new file mode 100644 index 0000000..79bc425 --- /dev/null +++ b/src/demux/inline.cpp @@ -0,0 +1,21 @@ +// Generated by tools/gen_exports.py from libwebp 1.5.0's headers — do not edit +// by hand. +// +// The adaptations of the static inline API of +// +// webp/demux.h +// +// each calling upstream's own inline body. +module; +#include + +module mcpplibs.mwrap.libwebp.demux; + +namespace mcpplibs::mwrap::libwebp { + +WebPDemuxer* WebPDemux(const WebPData* data) { return ::WebPDemux(data); } +WebPDemuxer* WebPDemuxPartial(const WebPData* data, WebPDemuxState* state) { return ::WebPDemuxPartial(data, state); } +int WebPAnimDecoderOptionsInit(WebPAnimDecoderOptions* dec_options) { return ::WebPAnimDecoderOptionsInit(dec_options); } +WebPAnimDecoder* WebPAnimDecoderNew(const WebPData* webp_data, const WebPAnimDecoderOptions* dec_options) { return ::WebPAnimDecoderNew(webp_data, dec_options); } + +} // namespace mcpplibs::mwrap::libwebp diff --git a/src/inline.cpp b/src/inline.cpp new file mode 100644 index 0000000..8f221c7 --- /dev/null +++ b/src/inline.cpp @@ -0,0 +1,36 @@ +// Generated by tools/gen_exports.py from libwebp 1.5.0's headers — do not edit +// by hand. +// +// The adaptations of the static inline API of +// +// webp/types.h +// webp/decode.h +// webp/encode.h +// webp/mux_types.h +// +// each calling upstream's own inline body. +module; +#include +#include +#include +#include + +module mcpplibs.mwrap.libwebp; + +namespace mcpplibs::mwrap::libwebp { + +int WebPIsPremultipliedMode(WEBP_CSP_MODE mode) { return ::WebPIsPremultipliedMode(mode); } +int WebPIsAlphaMode(WEBP_CSP_MODE mode) { return ::WebPIsAlphaMode(mode); } +int WebPIsRGBMode(WEBP_CSP_MODE mode) { return ::WebPIsRGBMode(mode); } +int WebPInitDecBuffer(WebPDecBuffer* buffer) { return ::WebPInitDecBuffer(buffer); } +uint8_t* WebPIDecGetYUV(const WebPIDecoder* idec, int* last_y, uint8_t** u, uint8_t** v, int* width, int* height, int* stride, int* uv_stride) { return ::WebPIDecGetYUV(idec, last_y, u, v, width, height, stride, uv_stride); } +VP8StatusCode WebPGetFeatures(const uint8_t* data, size_t data_size, WebPBitstreamFeatures* features) { return ::WebPGetFeatures(data, data_size, features); } +int WebPInitDecoderConfig(WebPDecoderConfig* config) { return ::WebPInitDecoderConfig(config); } +int WebPConfigInit(WebPConfig* config) { return ::WebPConfigInit(config); } +int WebPConfigPreset(WebPConfig* config, WebPPreset preset, float quality) { return ::WebPConfigPreset(config, preset, quality); } +int WebPPictureInit(WebPPicture* picture) { return ::WebPPictureInit(picture); } +void WebPDataInit(WebPData* webp_data) { return ::WebPDataInit(webp_data); } +void WebPDataClear(WebPData* webp_data) { return ::WebPDataClear(webp_data); } +int WebPDataCopy(const WebPData* src, WebPData* dst) { return ::WebPDataCopy(src, dst); } + +} // namespace mcpplibs::mwrap::libwebp diff --git a/src/libwebp.cppm b/src/libwebp.cppm new file mode 100644 index 0000000..1a0ca37 --- /dev/null +++ b/src/libwebp.cppm @@ -0,0 +1,200 @@ +// Generated by tools/gen_exports.py from libwebp 1.5.0's headers — do not edit +// by hand. +// +// mcpplibs.mwrap.libwebp — libwebp, the WebP image codec, as a C++ module: +// decoding (webp/decode.h), encoding (webp/encode.h), the common types +// (webp/types.h) and WebPData with the animation enums (webp/mux_types.h). +// Every exported name is the upstream global, unchanged; the root also +// re-exports the macro mappings and the sharpyuv module. The codec is +// compat.libwebp, built from the same release. +// +// The `static inline` API (internal linkage, not exportable) is adapted in +// namespace mcpplibs::mwrap::libwebp under the same names and signatures, each +// calling upstream's inline body (src/inline.cpp). +module; +#include +#include +#include +#include + +export module mcpplibs.mwrap.libwebp; + +export import mcpplibs.mwrap.libwebp.macros; +export import mcpplibs.mwrap.libwebp.sharpyuv; + +// types (23) +export using ::VP8StatusCode; +export using ::WEBP_CSP_MODE; +export using ::WebPAuxStats; +export using ::WebPBitstreamFeatures; +export using ::WebPConfig; +export using ::WebPData; +export using ::WebPDecBuffer; +export using ::WebPDecoderConfig; +export using ::WebPDecoderOptions; +export using ::WebPEncCSP; +export using ::WebPEncodingError; +export using ::WebPFeatureFlags; +export using ::WebPIDecoder; +export using ::WebPImageHint; +export using ::WebPMemoryWriter; +export using ::WebPMuxAnimBlend; +export using ::WebPMuxAnimDispose; +export using ::WebPPicture; +export using ::WebPPreset; +export using ::WebPProgressHook; +export using ::WebPRGBABuffer; +export using ::WebPWriterFunction; +export using ::WebPYUVABuffer; + +// enumerators (59) +export using ::MODE_RGB; +export using ::MODE_RGBA; +export using ::MODE_BGR; +export using ::MODE_BGRA; +export using ::MODE_ARGB; +export using ::MODE_RGBA_4444; +export using ::MODE_RGB_565; +export using ::MODE_rgbA; +export using ::MODE_bgrA; +export using ::MODE_Argb; +export using ::MODE_rgbA_4444; +export using ::MODE_YUV; +export using ::MODE_YUVA; +export using ::MODE_LAST; +export using ::VP8_STATUS_OK; +export using ::VP8_STATUS_OUT_OF_MEMORY; +export using ::VP8_STATUS_INVALID_PARAM; +export using ::VP8_STATUS_BITSTREAM_ERROR; +export using ::VP8_STATUS_UNSUPPORTED_FEATURE; +export using ::VP8_STATUS_SUSPENDED; +export using ::VP8_STATUS_USER_ABORT; +export using ::VP8_STATUS_NOT_ENOUGH_DATA; +export using ::WEBP_HINT_DEFAULT; +export using ::WEBP_HINT_PICTURE; +export using ::WEBP_HINT_PHOTO; +export using ::WEBP_HINT_GRAPH; +export using ::WEBP_HINT_LAST; +export using ::WEBP_PRESET_DEFAULT; +export using ::WEBP_PRESET_PICTURE; +export using ::WEBP_PRESET_PHOTO; +export using ::WEBP_PRESET_DRAWING; +export using ::WEBP_PRESET_ICON; +export using ::WEBP_PRESET_TEXT; +export using ::WEBP_YUV420; +export using ::WEBP_YUV420A; +export using ::WEBP_CSP_UV_MASK; +export using ::WEBP_CSP_ALPHA_BIT; +export using ::VP8_ENC_OK; +export using ::VP8_ENC_ERROR_OUT_OF_MEMORY; +export using ::VP8_ENC_ERROR_BITSTREAM_OUT_OF_MEMORY; +export using ::VP8_ENC_ERROR_NULL_PARAMETER; +export using ::VP8_ENC_ERROR_INVALID_CONFIGURATION; +export using ::VP8_ENC_ERROR_BAD_DIMENSION; +export using ::VP8_ENC_ERROR_PARTITION0_OVERFLOW; +export using ::VP8_ENC_ERROR_PARTITION_OVERFLOW; +export using ::VP8_ENC_ERROR_BAD_WRITE; +export using ::VP8_ENC_ERROR_FILE_TOO_BIG; +export using ::VP8_ENC_ERROR_USER_ABORT; +export using ::VP8_ENC_ERROR_LAST; +export using ::ANIMATION_FLAG; +export using ::XMP_FLAG; +export using ::EXIF_FLAG; +export using ::ALPHA_FLAG; +export using ::ICCP_FLAG; +export using ::ALL_VALID_FLAGS; +export using ::WEBP_MUX_DISPOSE_NONE; +export using ::WEBP_MUX_DISPOSE_BACKGROUND; +export using ::WEBP_MUX_BLEND; +export using ::WEBP_MUX_NO_BLEND; + +// functions (72) +export using ::WebPBlendAlpha; +export using ::WebPCleanupTransparentArea; +export using ::WebPConfigInitInternal; +export using ::WebPConfigLosslessPreset; +export using ::WebPDecode; +export using ::WebPDecodeARGB; +export using ::WebPDecodeARGBInto; +export using ::WebPDecodeBGR; +export using ::WebPDecodeBGRA; +export using ::WebPDecodeBGRAInto; +export using ::WebPDecodeBGRInto; +export using ::WebPDecodeRGB; +export using ::WebPDecodeRGBA; +export using ::WebPDecodeRGBAInto; +export using ::WebPDecodeRGBInto; +export using ::WebPDecodeYUV; +export using ::WebPDecodeYUVInto; +export using ::WebPEncode; +export using ::WebPEncodeBGR; +export using ::WebPEncodeBGRA; +export using ::WebPEncodeLosslessBGR; +export using ::WebPEncodeLosslessBGRA; +export using ::WebPEncodeLosslessRGB; +export using ::WebPEncodeLosslessRGBA; +export using ::WebPEncodeRGB; +export using ::WebPEncodeRGBA; +export using ::WebPFree; +export using ::WebPFreeDecBuffer; +export using ::WebPGetDecoderVersion; +export using ::WebPGetEncoderVersion; +export using ::WebPGetFeaturesInternal; +export using ::WebPGetInfo; +export using ::WebPIAppend; +export using ::WebPIDecGetRGB; +export using ::WebPIDecGetYUVA; +export using ::WebPIDecode; +export using ::WebPIDecodedArea; +export using ::WebPIDelete; +export using ::WebPINewDecoder; +export using ::WebPINewRGB; +export using ::WebPINewYUV; +export using ::WebPINewYUVA; +export using ::WebPIUpdate; +export using ::WebPInitDecBufferInternal; +export using ::WebPInitDecoderConfigInternal; +export using ::WebPMalloc; +export using ::WebPMemoryWrite; +export using ::WebPMemoryWriterClear; +export using ::WebPMemoryWriterInit; +export using ::WebPPictureARGBToYUVA; +export using ::WebPPictureARGBToYUVADithered; +export using ::WebPPictureAlloc; +export using ::WebPPictureCopy; +export using ::WebPPictureCrop; +export using ::WebPPictureDistortion; +export using ::WebPPictureFree; +export using ::WebPPictureHasTransparency; +export using ::WebPPictureImportBGR; +export using ::WebPPictureImportBGRA; +export using ::WebPPictureImportBGRX; +export using ::WebPPictureImportRGB; +export using ::WebPPictureImportRGBA; +export using ::WebPPictureImportRGBX; +export using ::WebPPictureInitInternal; +export using ::WebPPictureIsView; +export using ::WebPPictureRescale; +export using ::WebPPictureSharpARGBToYUVA; +export using ::WebPPictureSmartARGBToYUVA; +export using ::WebPPictureView; +export using ::WebPPictureYUVAToARGB; +export using ::WebPPlaneDistortion; +export using ::WebPValidateConfig; + +// the static inline API (13), adapted +export namespace mcpplibs::mwrap::libwebp { +int WebPIsPremultipliedMode(WEBP_CSP_MODE mode); +int WebPIsAlphaMode(WEBP_CSP_MODE mode); +int WebPIsRGBMode(WEBP_CSP_MODE mode); +[[nodiscard]] int WebPInitDecBuffer(WebPDecBuffer* buffer); +[[nodiscard]] uint8_t* WebPIDecGetYUV(const WebPIDecoder* idec, int* last_y, uint8_t** u, uint8_t** v, int* width, int* height, int* stride, int* uv_stride); +VP8StatusCode WebPGetFeatures(const uint8_t* data, size_t data_size, WebPBitstreamFeatures* features); +[[nodiscard]] int WebPInitDecoderConfig(WebPDecoderConfig* config); +[[nodiscard]] int WebPConfigInit(WebPConfig* config); +[[nodiscard]] int WebPConfigPreset(WebPConfig* config, WebPPreset preset, float quality); +[[nodiscard]] int WebPPictureInit(WebPPicture* picture); +void WebPDataInit(WebPData* webp_data); +void WebPDataClear(WebPData* webp_data); +[[nodiscard]] int WebPDataCopy(const WebPData* src, WebPData* dst); +} // namespace mcpplibs::mwrap::libwebp diff --git a/src/macros.cppm b/src/macros.cppm new file mode 100644 index 0000000..a8a2176 --- /dev/null +++ b/src/macros.cppm @@ -0,0 +1,75 @@ +// mcpplibs.mwrap.libwebp.macros — the macros of libwebp's public headers (a +// named module cannot export a macro), under their original names in +// mcpplibs::mwrap::libwebp::macros: +// +// - the ABI versions the headers were written for (WEBP_DECODER_ABI_VERSION, +// WEBP_ENCODER_ABI_VERSION, WEBP_DEMUX_ABI_VERSION, WEBP_MUX_ABI_VERSION), +// WEBP_MAX_DIMENSION and libsharpyuv's version (SHARPYUV_VERSION_MAJOR, +// _MINOR, _PATCH, SHARPYUV_VERSION) as `inline constexpr int`; +// - WEBP_ABI_IS_INCOMPATIBLE and SHARPYUV_MAKE_VERSION as constexpr +// functions. +// +// The demux and mux ABI versions are constants of their headers: they need +// neither feature. The rest are declaration annotations (WEBP_EXTERN, +// WEBP_INLINE, WEBP_NODISCARD, SHARPYUV_EXTERN, SHARPYUV_INLINE) and include +// guards; tools/check_exports.py accounts for every macro. +module; +#include +#include +#include +#include +#include + +namespace mwrap_libwebp_capture { +inline constexpr int value_WEBP_DECODER_ABI_VERSION = WEBP_DECODER_ABI_VERSION; +inline constexpr int value_WEBP_ENCODER_ABI_VERSION = WEBP_ENCODER_ABI_VERSION; +inline constexpr int value_WEBP_DEMUX_ABI_VERSION = WEBP_DEMUX_ABI_VERSION; +inline constexpr int value_WEBP_MUX_ABI_VERSION = WEBP_MUX_ABI_VERSION; +inline constexpr int value_WEBP_MAX_DIMENSION = WEBP_MAX_DIMENSION; +inline constexpr int value_SHARPYUV_VERSION_MAJOR = SHARPYUV_VERSION_MAJOR; +inline constexpr int value_SHARPYUV_VERSION_MINOR = SHARPYUV_VERSION_MINOR; +inline constexpr int value_SHARPYUV_VERSION_PATCH = SHARPYUV_VERSION_PATCH; +inline constexpr int value_SHARPYUV_VERSION = SHARPYUV_VERSION; +constexpr bool abi_is_incompatible(int a, int b) { return WEBP_ABI_IS_INCOMPATIBLE(a, b); } +constexpr int make_version(int major, int minor, int patch) { return SHARPYUV_MAKE_VERSION(major, minor, patch); } +} // namespace mwrap_libwebp_capture + +#undef WEBP_DECODER_ABI_VERSION +#undef WEBP_ENCODER_ABI_VERSION +#undef WEBP_DEMUX_ABI_VERSION +#undef WEBP_MUX_ABI_VERSION +#undef WEBP_MAX_DIMENSION +#undef WEBP_ABI_IS_INCOMPATIBLE +#undef SHARPYUV_VERSION_MAJOR +#undef SHARPYUV_VERSION_MINOR +#undef SHARPYUV_VERSION_PATCH +#undef SHARPYUV_VERSION +#undef SHARPYUV_MAKE_VERSION + +export module mcpplibs.mwrap.libwebp.macros; + +export namespace mcpplibs::mwrap::libwebp::macros { + +// The ABI versions (MAJOR(8b) + MINOR(8b)) passed to the *Internal functions. +inline constexpr int WEBP_DECODER_ABI_VERSION = mwrap_libwebp_capture::value_WEBP_DECODER_ABI_VERSION; +inline constexpr int WEBP_ENCODER_ABI_VERSION = mwrap_libwebp_capture::value_WEBP_ENCODER_ABI_VERSION; +inline constexpr int WEBP_DEMUX_ABI_VERSION = mwrap_libwebp_capture::value_WEBP_DEMUX_ABI_VERSION; +inline constexpr int WEBP_MUX_ABI_VERSION = mwrap_libwebp_capture::value_WEBP_MUX_ABI_VERSION; + +// The largest width or height the encoder accepts. +inline constexpr int WEBP_MAX_DIMENSION = mwrap_libwebp_capture::value_WEBP_MAX_DIMENSION; + +inline constexpr int SHARPYUV_VERSION_MAJOR = mwrap_libwebp_capture::value_SHARPYUV_VERSION_MAJOR; +inline constexpr int SHARPYUV_VERSION_MINOR = mwrap_libwebp_capture::value_SHARPYUV_VERSION_MINOR; +inline constexpr int SHARPYUV_VERSION_PATCH = mwrap_libwebp_capture::value_SHARPYUV_VERSION_PATCH; +inline constexpr int SHARPYUV_VERSION = mwrap_libwebp_capture::value_SHARPYUV_VERSION; + +// Whether two ABI versions differ in their major byte. +constexpr bool WEBP_ABI_IS_INCOMPATIBLE(int a, int b) { return mwrap_libwebp_capture::abi_is_incompatible(a, b); } + +// (MAJOR << 24) | (MINOR << 16) | PATCH, as SharpYuvGetVersion() answers. +constexpr int SHARPYUV_MAKE_VERSION(int major, int minor, int patch) { + return mwrap_libwebp_capture::make_version(major, minor, patch); +} + +} // namespace mcpplibs::mwrap::libwebp::macros diff --git a/src/mux/inline.cpp b/src/mux/inline.cpp new file mode 100644 index 0000000..9960d75 --- /dev/null +++ b/src/mux/inline.cpp @@ -0,0 +1,21 @@ +// Generated by tools/gen_exports.py from libwebp 1.5.0's headers — do not edit +// by hand. +// +// The adaptations of the static inline API of +// +// webp/mux.h +// +// each calling upstream's own inline body. +module; +#include + +module mcpplibs.mwrap.libwebp.mux; + +namespace mcpplibs::mwrap::libwebp { + +WebPMux* WebPMuxNew() { return ::WebPMuxNew(); } +WebPMux* WebPMuxCreate(const WebPData* bitstream, int copy_data) { return ::WebPMuxCreate(bitstream, copy_data); } +int WebPAnimEncoderOptionsInit(WebPAnimEncoderOptions* enc_options) { return ::WebPAnimEncoderOptionsInit(enc_options); } +WebPAnimEncoder* WebPAnimEncoderNew(int width, int height, const WebPAnimEncoderOptions* enc_options) { return ::WebPAnimEncoderNew(width, height, enc_options); } + +} // namespace mcpplibs::mwrap::libwebp diff --git a/src/mux/mux.cppm b/src/mux/mux.cppm new file mode 100644 index 0000000..87c7d64 --- /dev/null +++ b/src/mux/mux.cppm @@ -0,0 +1,83 @@ +// Generated by tools/gen_exports.py from libwebp 1.5.0's headers — do not edit +// by hand. +// +// mcpplibs.mwrap.libwebp.mux — libwebpmux (webp/mux.h): the container editor +// (WebPMux: frames, ICC/EXIF/XMP chunks, canvas and animation parameters) and +// WebPAnimEncoder, which encodes full-canvas frames into an animation. With +// the `mux` feature, which builds compat.libwebp's src/mux. Import it by name: +// the root does not re-export it; it re-exports the root, as mux.h includes +// mux_types.h and WebPAnimEncoder takes encode.h's WebPConfig and WebPPicture. +// +// The `static inline` API is adapted in mcpplibs::mwrap::libwebp +// (src/mux/inline.cpp). +module; +#include + +export module mcpplibs.mwrap.libwebp.mux; + +export import mcpplibs.mwrap.libwebp; + +// types (7) +export using ::WebPAnimEncoder; +export using ::WebPAnimEncoderOptions; +export using ::WebPChunkId; +export using ::WebPMux; +export using ::WebPMuxAnimParams; +export using ::WebPMuxError; +export using ::WebPMuxFrameInfo; + +// enumerators (17) +export using ::WEBP_MUX_OK; +export using ::WEBP_MUX_NOT_FOUND; +export using ::WEBP_MUX_INVALID_ARGUMENT; +export using ::WEBP_MUX_BAD_DATA; +export using ::WEBP_MUX_MEMORY_ERROR; +export using ::WEBP_MUX_NOT_ENOUGH_DATA; +export using ::WEBP_CHUNK_VP8X; +export using ::WEBP_CHUNK_ICCP; +export using ::WEBP_CHUNK_ANIM; +export using ::WEBP_CHUNK_ANMF; +export using ::WEBP_CHUNK_DEPRECATED; +export using ::WEBP_CHUNK_ALPHA; +export using ::WEBP_CHUNK_IMAGE; +export using ::WEBP_CHUNK_EXIF; +export using ::WEBP_CHUNK_XMP; +export using ::WEBP_CHUNK_UNKNOWN; +export using ::WEBP_CHUNK_NIL; + +// functions (27) +export using ::WebPAnimEncoderAdd; +export using ::WebPAnimEncoderAssemble; +export using ::WebPAnimEncoderDelete; +export using ::WebPAnimEncoderDeleteChunk; +export using ::WebPAnimEncoderGetChunk; +export using ::WebPAnimEncoderGetError; +export using ::WebPAnimEncoderNewInternal; +export using ::WebPAnimEncoderOptionsInitInternal; +export using ::WebPAnimEncoderSetChunk; +export using ::WebPGetMuxVersion; +export using ::WebPMuxAssemble; +export using ::WebPMuxCreateInternal; +export using ::WebPMuxDelete; +export using ::WebPMuxDeleteChunk; +export using ::WebPMuxDeleteFrame; +export using ::WebPMuxGetAnimationParams; +export using ::WebPMuxGetCanvasSize; +export using ::WebPMuxGetChunk; +export using ::WebPMuxGetFeatures; +export using ::WebPMuxGetFrame; +export using ::WebPMuxNumChunks; +export using ::WebPMuxPushFrame; +export using ::WebPMuxSetAnimationParams; +export using ::WebPMuxSetCanvasSize; +export using ::WebPMuxSetChunk; +export using ::WebPMuxSetImage; +export using ::WebPNewInternal; + +// the static inline API (4), adapted +export namespace mcpplibs::mwrap::libwebp { +[[nodiscard]] WebPMux* WebPMuxNew(void); +[[nodiscard]] WebPMux* WebPMuxCreate(const WebPData* bitstream, int copy_data); +[[nodiscard]] int WebPAnimEncoderOptionsInit(WebPAnimEncoderOptions* enc_options); +WebPAnimEncoder* WebPAnimEncoderNew(int width, int height, const WebPAnimEncoderOptions* enc_options); +} // namespace mcpplibs::mwrap::libwebp diff --git a/src/sharpyuv.cppm b/src/sharpyuv.cppm new file mode 100644 index 0000000..edaebeb --- /dev/null +++ b/src/sharpyuv.cppm @@ -0,0 +1,61 @@ +// Generated by tools/gen_exports.py from libwebp 1.5.0's headers — do not edit +// by hand. +// +// mcpplibs.mwrap.libwebp.sharpyuv — libsharpyuv, the sharp RGB to YUV +// conversion the encoder uses (sharpyuv/sharpyuv.h, sharpyuv/sharpyuv_csp.h), +// in compat.libwebp's archive. The root module re-exports it. +// +// SharpYuvOptionsInit (static inline) is adapted in mcpplibs::mwrap::libwebp. +module; +#include +#include + +export module mcpplibs.mwrap.libwebp.sharpyuv; + +// types (6) +export using ::SharpYuvColorSpace; +export using ::SharpYuvConversionMatrix; +export using ::SharpYuvMatrixType; +export using ::SharpYuvOptions; +export using ::SharpYuvRange; +export using ::SharpYuvTransferFunctionType; + +// enumerators (25) +export using ::kSharpYuvTransferFunctionBt709; +export using ::kSharpYuvTransferFunctionBt470M; +export using ::kSharpYuvTransferFunctionBt470Bg; +export using ::kSharpYuvTransferFunctionBt601; +export using ::kSharpYuvTransferFunctionSmpte240; +export using ::kSharpYuvTransferFunctionLinear; +export using ::kSharpYuvTransferFunctionLog100; +export using ::kSharpYuvTransferFunctionLog100_Sqrt10; +export using ::kSharpYuvTransferFunctionIec61966; +export using ::kSharpYuvTransferFunctionBt1361; +export using ::kSharpYuvTransferFunctionSrgb; +export using ::kSharpYuvTransferFunctionBt2020_10Bit; +export using ::kSharpYuvTransferFunctionBt2020_12Bit; +export using ::kSharpYuvTransferFunctionSmpte2084; +export using ::kSharpYuvTransferFunctionSmpte428; +export using ::kSharpYuvTransferFunctionHlg; +export using ::kSharpYuvTransferFunctionNum; +export using ::kSharpYuvRangeFull; +export using ::kSharpYuvRangeLimited; +export using ::kSharpYuvMatrixWebp; +export using ::kSharpYuvMatrixRec601Limited; +export using ::kSharpYuvMatrixRec601Full; +export using ::kSharpYuvMatrixRec709Limited; +export using ::kSharpYuvMatrixRec709Full; +export using ::kSharpYuvMatrixNum; + +// functions (6) +export using ::SharpYuvComputeConversionMatrix; +export using ::SharpYuvConvert; +export using ::SharpYuvConvertWithOptions; +export using ::SharpYuvGetConversionMatrix; +export using ::SharpYuvGetVersion; +export using ::SharpYuvOptionsInitInternal; + +// the static inline API (1), adapted +export namespace mcpplibs::mwrap::libwebp { +int SharpYuvOptionsInit(const SharpYuvConversionMatrix* yuv_matrix, SharpYuvOptions* options); +} // namespace mcpplibs::mwrap::libwebp diff --git a/src/sharpyuv_inline.cpp b/src/sharpyuv_inline.cpp new file mode 100644 index 0000000..4a4918b --- /dev/null +++ b/src/sharpyuv_inline.cpp @@ -0,0 +1,20 @@ +// Generated by tools/gen_exports.py from libwebp 1.5.0's headers — do not edit +// by hand. +// +// The adaptations of the static inline API of +// +// sharpyuv/sharpyuv.h +// sharpyuv/sharpyuv_csp.h +// +// each calling upstream's own inline body. +module; +#include +#include + +module mcpplibs.mwrap.libwebp.sharpyuv; + +namespace mcpplibs::mwrap::libwebp { + +int SharpYuvOptionsInit(const SharpYuvConversionMatrix* yuv_matrix, SharpYuvOptions* options) { return ::SharpYuvOptionsInit(yuv_matrix, options); } + +} // namespace mcpplibs::mwrap::libwebp diff --git a/tests/anim/mcpp.toml b/tests/anim/mcpp.toml new file mode 100644 index 0000000..defc8a3 --- /dev/null +++ b/tests/anim/mcpp.toml @@ -0,0 +1,12 @@ +# Animated WebP and the container through the modules: its own project, for +# the demux and mux features. +[package] +name = "libwebp-anim-tests" +version = "0.1.0" +description = "mcpplibs.mwrap.libwebp with the demux and mux features: WebPAnimEncoder, WebPAnimDecoder, WebPDemux and WebPMux" + +[dependencies] +"mcpplibs.mwrap.libwebp" = { path = "../..", features = ["demux", "mux"] } + +[dev-dependencies] +compat.gtest = "1.15.2" diff --git a/tests/anim/tests/anim_test.cpp b/tests/anim/tests/anim_test.cpp new file mode 100644 index 0000000..2a73e68 --- /dev/null +++ b/tests/anim/tests/anim_test.cpp @@ -0,0 +1,302 @@ +// Animated WebP and the container through the modules, with the demux and +// mux features: WebPAnimEncoder (lossless and lossy frames, timestamps, the +// loop count and background), WebPAnimDecoder (full canvases with their +// timestamps, reset, every supported colour mode), WebPDemux (frames, +// features, chunks), WebPMux (ICC and EXIF chunks on a still image, an +// animation assembled frame by frame), the error paths; and the assembled +// animation's bytes, pinned like tests/determinism_test.cpp's. +#include + +import std; +import mcpplibs.mwrap.libwebp.demux; // re-exports mcpplibs.mwrap.libwebp +import mcpplibs.mwrap.libwebp.mux; + +using namespace mcpplibs::mwrap::libwebp; // the adapted static inline API +namespace macros = mcpplibs::mwrap::libwebp::macros; + +namespace { + +constexpr int W = 64, H = 48, Frames = 4; + +// Frame f: a square moving over a gradient, opaque except a translucent band. +std::vector frame(int f) { + 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]; + const bool square = x >= 8 + f * 10 && x < 24 + f * 10 && y >= 12 && y < 28; + p[0] = static_cast(square ? 250 : x * 4); + p[1] = static_cast(square ? 30 : y * 5); + p[2] = static_cast(square ? 60 : 128); + p[3] = static_cast(y >= 40 ? 128 : 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; +} + +std::vector animate(bool lossless, float quality) { + WebPAnimEncoderOptions options; + if (!WebPAnimEncoderOptionsInit(&options)) return {}; + options.anim_params.loop_count = 3; + options.anim_params.bgcolor = 0xffffffffu; // opaque white (blue, green, red, alpha from the top byte) + WebPAnimEncoder* encoder = WebPAnimEncoderNew(W, H, &options); + if (!encoder) return {}; + WebPConfig config; + if (!WebPConfigInit(&config)) return {}; + config.lossless = lossless; + config.quality = quality; + config.exact = 1; + WebPPicture pic; + if (!WebPPictureInit(&pic)) return {}; + pic.use_argb = 1; + pic.width = W; + pic.height = H; + bool ok = true; + for (int f = 0; f < Frames && ok; ++f) { + const auto rgba = frame(f); + ok = WebPPictureImportRGBA(&pic, rgba.data(), W * 4) && WebPAnimEncoderAdd(encoder, &pic, f * 100, &config); + } + WebPPictureFree(&pic); + ok = ok && WebPAnimEncoderAdd(encoder, nullptr, Frames * 100, nullptr); // the last frame's end + WebPData out; + WebPDataInit(&out); + ok = ok && WebPAnimEncoderAssemble(encoder, &out); + WebPAnimEncoderDelete(encoder); + std::vector file; + if (ok) file.assign(out.bytes, out.bytes + out.size); + WebPDataClear(&out); + return file; +} + +} // namespace + +TEST(Anim, Versions) { + EXPECT_EQ(WebPGetDemuxVersion(), 0x010500); + EXPECT_EQ(WebPGetMuxVersion(), 0x010500); + EXPECT_EQ(macros::WEBP_DEMUX_ABI_VERSION, 0x0107); + EXPECT_EQ(macros::WEBP_MUX_ABI_VERSION, 0x0109); +} + +TEST(Anim, LosslessRoundTripsEveryFrame) { + const auto file = animate(true, 75); + ASSERT_FALSE(file.empty()); + WebPBitstreamFeatures features; + ASSERT_EQ(WebPGetFeatures(file.data(), file.size(), &features), VP8_STATUS_OK); + EXPECT_TRUE(features.has_animation); + + const WebPData data{file.data(), file.size()}; + WebPAnimDecoderOptions options; + ASSERT_TRUE(WebPAnimDecoderOptionsInit(&options)); + options.color_mode = MODE_RGBA; + WebPAnimDecoder* decoder = WebPAnimDecoderNew(&data, &options); + ASSERT_NE(decoder, nullptr); + WebPAnimInfo info; + ASSERT_TRUE(WebPAnimDecoderGetInfo(decoder, &info)); + EXPECT_EQ(info.canvas_width, static_cast(W)); + EXPECT_EQ(info.canvas_height, static_cast(H)); + EXPECT_EQ(info.frame_count, static_cast(Frames)); + EXPECT_EQ(info.loop_count, 3u); + + for (int pass = 0; pass < 2; ++pass) { // and again after a reset + int f = 0; + while (WebPAnimDecoderHasMoreFrames(decoder)) { + std::uint8_t* canvas = nullptr; + int timestamp = 0; + ASSERT_TRUE(WebPAnimDecoderGetNext(decoder, &canvas, ×tamp)); + EXPECT_EQ(timestamp, (f + 1) * 100); // the frame's end + const auto expected = frame(f); + EXPECT_TRUE(std::equal(expected.begin(), expected.end(), canvas)) << "frame " << f; + ++f; + } + EXPECT_EQ(f, Frames); + WebPAnimDecoderReset(decoder); + } + const WebPDemuxer* demux = WebPAnimDecoderGetDemuxer(decoder); + EXPECT_EQ(WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT), static_cast(Frames)); + WebPAnimDecoderDelete(decoder); +} + +TEST(Anim, LossyCloseAndEveryColourMode) { + const auto file = animate(false, 80); + ASSERT_FALSE(file.empty()); + const WebPData data{file.data(), file.size()}; + for (const auto mode : {MODE_RGBA, MODE_BGRA, MODE_rgbA, MODE_bgrA}) { + WebPAnimDecoderOptions options; + ASSERT_TRUE(WebPAnimDecoderOptionsInit(&options)); + options.color_mode = mode; + options.use_threads = 1; + WebPAnimDecoder* decoder = WebPAnimDecoderNew(&data, &options); + ASSERT_NE(decoder, nullptr) << mode; + std::uint8_t* canvas = nullptr; + int timestamp = 0; + ASSERT_TRUE(WebPAnimDecoderGetNext(decoder, &canvas, ×tamp)); + if (mode == MODE_RGBA) { + const auto expected = frame(0); + double error = 0; + for (std::size_t i = 0; i < expected.size(); ++i) error += std::abs(int(expected[i]) - int(canvas[i])); + EXPECT_LT(error / double(expected.size()), 4.0); // mean absolute error per channel + } + WebPAnimDecoderDelete(decoder); + } + // A colour mode the animation decoder does not support. + WebPAnimDecoderOptions options; + ASSERT_TRUE(WebPAnimDecoderOptionsInit(&options)); + options.color_mode = MODE_RGB; + EXPECT_EQ(WebPAnimDecoderNew(&data, &options), nullptr); +} + +TEST(Anim, DemuxFrames) { + const auto file = animate(true, 75); + ASSERT_FALSE(file.empty()); + const WebPData data{file.data(), file.size()}; + WebPDemuxer* demux = WebPDemux(&data); + ASSERT_NE(demux, nullptr); + EXPECT_EQ(WebPDemuxGetI(demux, WEBP_FF_CANVAS_WIDTH), static_cast(W)); + EXPECT_EQ(WebPDemuxGetI(demux, WEBP_FF_LOOP_COUNT), 3u); + EXPECT_TRUE(WebPDemuxGetI(demux, WEBP_FF_FORMAT_FLAGS) & ANIMATION_FLAG); + WebPIterator it; + ASSERT_TRUE(WebPDemuxGetFrame(demux, 1, &it)); + int frames = 0, total = 0; + do { + ++frames; + total += it.duration; + EXPECT_TRUE(it.complete); + EXPECT_GT(it.fragment.size, 0u); + } while (WebPDemuxNextFrame(&it)); + WebPDemuxReleaseIterator(&it); + EXPECT_EQ(frames, Frames); + EXPECT_EQ(total, Frames * 100); + WebPDemuxDelete(demux); + + // Partial data: the header parses, the frames are not all there yet. + const WebPData half{file.data(), file.size() / 2}; + WebPDemuxState state; + demux = WebPDemuxPartial(&half, &state); + ASSERT_NE(demux, nullptr); + EXPECT_TRUE(state == WEBP_DEMUX_PARSED_HEADER || state == WEBP_DEMUX_PARSING_HEADER); + WebPDemuxDelete(demux); +} + +TEST(Mux, ChunksOnAStillImage) { + const auto rgba = frame(0); + std::uint8_t* still = nullptr; + const std::size_t size = WebPEncodeLosslessRGBA(rgba.data(), W, H, W * 4, &still); + ASSERT_GT(size, 0u); + const WebPData image{still, size}; + WebPMux* mux = WebPMuxCreate(&image, 1); + ASSERT_NE(mux, nullptr); + WebPFree(still); + + using namespace std::literals; + const std::string icc = "not really an ICC profile", exif = "Exif\0\0MM"s; + const WebPData icc_data{reinterpret_cast(icc.data()), icc.size()}; + const WebPData exif_data{reinterpret_cast(exif.data()), exif.size()}; + ASSERT_EQ(WebPMuxSetChunk(mux, "ICCP", &icc_data, 1), WEBP_MUX_OK); + ASSERT_EQ(WebPMuxSetChunk(mux, "EXIF", &exif_data, 1), WEBP_MUX_OK); + WebPData assembled; + WebPDataInit(&assembled); + ASSERT_EQ(WebPMuxAssemble(mux, &assembled), WEBP_MUX_OK); + WebPMuxDelete(mux); + + // Read back through the demuxer, and the image still decodes. + WebPDemuxer* demux = WebPDemux(&assembled); + ASSERT_NE(demux, nullptr); + WebPChunkIterator chunk; + ASSERT_TRUE(WebPDemuxGetChunk(demux, "ICCP", 1, &chunk)); + EXPECT_EQ(std::string_view(reinterpret_cast(chunk.chunk.bytes), chunk.chunk.size), icc); + WebPDemuxReleaseChunkIterator(&chunk); + EXPECT_FALSE(WebPDemuxGetChunk(demux, "XMP ", 1, &chunk)); + WebPDemuxDelete(demux); + int width = 0, height = 0; + std::uint8_t* pixels = WebPDecodeRGBA(assembled.bytes, assembled.size, &width, &height); + ASSERT_NE(pixels, nullptr); + EXPECT_TRUE(std::equal(rgba.begin(), rgba.end(), pixels)); + WebPFree(pixels); + + mux = WebPMuxCreate(&assembled, 0); + ASSERT_NE(mux, nullptr); + std::uint32_t flags = 0; // from the VP8X chunk the assembly wrote + ASSERT_EQ(WebPMuxGetFeatures(mux, &flags), WEBP_MUX_OK); + EXPECT_TRUE(flags & ICCP_FLAG); + EXPECT_TRUE(flags & EXIF_FLAG); + EXPECT_TRUE(flags & ALPHA_FLAG); + WebPData got; + ASSERT_EQ(WebPMuxGetChunk(mux, "EXIF", &got), WEBP_MUX_OK); + EXPECT_EQ(got.size, exif.size()); + EXPECT_EQ(WebPMuxDeleteChunk(mux, "EXIF"), WEBP_MUX_OK); + EXPECT_EQ(WebPMuxGetChunk(mux, "EXIF", &got), WEBP_MUX_NOT_FOUND); + WebPMuxDelete(mux); + WebPDataClear(&assembled); +} + +TEST(Mux, AnAnimationFrameByFrame) { + WebPMux* mux = WebPMuxNew(); + ASSERT_NE(mux, nullptr); + const WebPMuxAnimParams params{0xffffffffu, 0}; + ASSERT_EQ(WebPMuxSetAnimationParams(mux, ¶ms), WEBP_MUX_OK); + std::vector stills; + for (int f = 0; f < 2; ++f) { + const auto rgba = frame(f); + std::uint8_t* still = nullptr; + const std::size_t size = WebPEncodeLosslessRGBA(rgba.data(), W, H, W * 4, &still); + ASSERT_GT(size, 0u); + stills.push_back(still); + WebPMuxFrameInfo info{}; + info.bitstream = {still, size}; + info.duration = 250; + info.id = WEBP_CHUNK_ANMF; + info.dispose_method = WEBP_MUX_DISPOSE_NONE; + info.blend_method = WEBP_MUX_NO_BLEND; + ASSERT_EQ(WebPMuxPushFrame(mux, &info, 1), WEBP_MUX_OK); + } + for (auto* still : stills) WebPFree(still); + int frames = 0; + ASSERT_EQ(WebPMuxNumChunks(mux, WEBP_CHUNK_ANMF, &frames), WEBP_MUX_OK); + EXPECT_EQ(frames, 2); + WebPData out; + WebPDataInit(&out); + ASSERT_EQ(WebPMuxAssemble(mux, &out), WEBP_MUX_OK); + WebPMuxDelete(mux); + WebPDemuxer* demux = WebPDemux(&out); + ASSERT_NE(demux, nullptr); + EXPECT_EQ(WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT), 2u); + EXPECT_EQ(WebPDemuxGetI(demux, WEBP_FF_BACKGROUND_COLOR), 0xffffffffu); + WebPDemuxDelete(demux); + WebPDataClear(&out); +} + +TEST(Anim, Errors) { + const std::array garbage{'R', 'I', 'F', 'F', 8, 0, 0, 0, 'W', 'E', 'B', 'P', 'V', 'P', '8', 'Q'}; + const WebPData data{garbage.data(), garbage.size()}; + EXPECT_EQ(WebPDemux(&data), nullptr); + EXPECT_EQ(WebPAnimDecoderNew(&data, nullptr), nullptr); + EXPECT_EQ(WebPMuxCreate(&data, 0), nullptr); + + WebPAnimEncoder* encoder = WebPAnimEncoderNew(W, H, nullptr); + ASSERT_NE(encoder, nullptr); + WebPPicture pic; + ASSERT_TRUE(WebPPictureInit(&pic)); + pic.width = W + 1; // not the canvas + pic.height = H; + ASSERT_TRUE(WebPPictureAlloc(&pic)); + EXPECT_FALSE(WebPAnimEncoderAdd(encoder, &pic, 0, nullptr)); + EXPECT_NE(std::string_view(WebPAnimEncoderGetError(encoder)), ""); + WebPPictureFree(&pic); + WebPAnimEncoderDelete(encoder); +} + +TEST(Anim, AssembledBytesArePinned) { + const auto lossless = animate(true, 75), lossy = animate(false, 75); + ASSERT_FALSE(lossless.empty()); + ASSERT_FALSE(lossy.empty()); + std::println("animation lossless: {} bytes, fnv1a 0x{:016x}", lossless.size(), fnv1a(lossless)); + std::println("animation lossy q75: {} bytes, fnv1a 0x{:016x}", lossy.size(), fnv1a(lossy)); + EXPECT_EQ(animate(true, 75), lossless); + EXPECT_EQ(fnv1a(lossless), 0x8acfc1377bc9e0c9ull) << std::hex << fnv1a(lossless); + EXPECT_EQ(fnv1a(lossy), 0x507c1a778261343cull) << std::hex << fnv1a(lossy); +} diff --git a/tests/api_test.cpp b/tests/api_test.cpp new file mode 100644 index 0000000..edc35c3 --- /dev/null +++ b/tests/api_test.cpp @@ -0,0 +1,494 @@ +// libwebp's API through the module: the simple decoding and encoding API +// (RGBA in and out, lossy at a quality and lossless, with alpha), the +// advanced API (WebPConfig and WebPPicture with a memory writer, presets, +// method and threads, WebPDecoderConfig with scaling, flipping and +// colourspaces), incremental decoding, the error paths, the adapted static +// inline API, WebPData and sharpyuv — spelled as the headers document them. +// And determinism: the same bytes for the same picture and settings, run +// after run, with thread_level set or not (compat.libwebp is built without +// WEBP_USE_THREAD: the work stays on the calling thread). The bytes +// themselves, across targets, are pinned in tests/determinism_test.cpp. +#include + +import std; +import mcpplibs.mwrap.libwebp; + +using namespace mcpplibs::mwrap::libwebp; // WebPConfigInit, WebPPictureInit … (the adapted static inline API) +namespace macros = mcpplibs::mwrap::libwebp::macros; + +namespace { + +// A picture with smooth gradients and alpha that is fully transparent, fully +// opaque and in between. (A saturated chroma checkerboard would do: VP8's +// 4:2:0 chroma at default settings smears it, as every libwebp does.) +std::vector picture(int width, int height) { + std::vector rgba(static_cast(width) * height * 4); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) { + auto* p = &rgba[(static_cast(y) * width + x) * 4]; + p[0] = static_cast(x * 255 / (width - 1)); + p[1] = static_cast(y * 255 / (height - 1)); + p[2] = static_cast(255 - (x + y) * 255 / (width + height - 2)); + p[3] = static_cast(x < width / 4 ? 0 : x < width / 2 ? (y * 255 / (height - 1)) : 255); + } + return rgba; +} + +struct Freed { + std::uint8_t* data = nullptr; + ~Freed() { WebPFree(data); } +}; + +std::span bytes(const Freed& f, std::size_t size) { return {f.data, size}; } + +// PSNR of the colour channels of the opaque pixels. (Where alpha is partial, +// the lossy encoder weights chroma by alpha, by design; where it is 0, the +// colour is not kept unless WebPConfig::exact is set.) +double psnr(std::span a, std::span b) { + double error = 0; + std::size_t n = 0; + for (std::size_t i = 0; i < a.size(); i += 4) { + if (a[i + 3] != 255) continue; + for (int c = 0; c < 3; ++c) { + const double d = double(a[i + c]) - double(b[i + c]); + error += d * d; + ++n; + } + } + return error == 0 ? 99.0 : 10.0 * std::log10(255.0 * 255.0 * double(n) / error); +} + +// Encodes with the advanced API into memory. +std::vector encode(const WebPConfig& config, std::span rgba, int width, int height) { + WebPPicture pic; + if (!WebPPictureInit(&pic)) return {}; + pic.use_argb = 1; + pic.width = width; + pic.height = height; + WebPMemoryWriter writer; + WebPMemoryWriterInit(&writer); + pic.writer = WebPMemoryWrite; + pic.custom_ptr = &writer; + std::vector out; + if (WebPPictureImportRGBA(&pic, rgba.data(), width * 4) && WebPEncode(&config, &pic)) + out.assign(writer.mem, writer.mem + writer.size); + WebPPictureFree(&pic); + WebPMemoryWriterClear(&writer); + return out; +} + +// Equal where it matters without WebPConfig::exact: alpha everywhere, and +// the colour wherever alpha is not 0. +bool visible_equal(std::span a, std::span b) { + if (a.size() != b.size()) return false; + for (std::size_t i = 0; i < a.size(); i += 4) + if (a[i + 3] != b[i + 3] || (a[i + 3] != 0 && !std::equal(a.data() + i, a.data() + i + 3, b.data() + i))) return false; + return true; +} + +// Lossless with WebPConfig::exact: every byte of every pixel survives. +std::vector encode_exact(std::span rgba, int width, int height) { + WebPConfig config; + if (!WebPConfigInit(&config)) return {}; + config.lossless = 1; + config.exact = 1; + return encode(config, rgba, width, height); +} + +constexpr int W = 96, H = 64; + +} // namespace + +TEST(Versions, AreTheRelease) { + EXPECT_EQ(WebPGetDecoderVersion(), 0x010500); + EXPECT_EQ(WebPGetEncoderVersion(), 0x010500); + EXPECT_EQ(SharpYuvGetVersion(), macros::SHARPYUV_VERSION); + EXPECT_FALSE(macros::WEBP_ABI_IS_INCOMPATIBLE(0x0200, macros::WEBP_DECODER_ABI_VERSION)); + EXPECT_TRUE(macros::WEBP_ABI_IS_INCOMPATIBLE(0x0300, macros::WEBP_DECODER_ABI_VERSION)); +} + +TEST(Simple, LosslessRgbaRoundTripsExactly) { + const auto rgba = picture(W, H); + Freed encoded; + const std::size_t size = WebPEncodeLosslessRGBA(rgba.data(), W, H, W * 4, &encoded.data); + ASSERT_GT(size, 0u); + const auto file = bytes(encoded, size); + EXPECT_TRUE(std::ranges::equal(file.first(4), std::string_view("RIFF"), {}, {}, [](char c) { return std::uint8_t(c); })); + + int width = 0, height = 0; + ASSERT_TRUE(WebPGetInfo(file.data(), file.size(), &width, &height)); + EXPECT_EQ(width, W); + EXPECT_EQ(height, H); + + WebPBitstreamFeatures features; + ASSERT_EQ(WebPGetFeatures(file.data(), file.size(), &features), VP8_STATUS_OK); + EXPECT_EQ(features.width, W); + EXPECT_TRUE(features.has_alpha); + EXPECT_FALSE(features.has_animation); + EXPECT_EQ(features.format, 2); // lossless + + // The simple API leaves WebPConfig::exact off: the colour under alpha 0 + // may change, every visible pixel is exact. + Freed decoded; + decoded.data = WebPDecodeRGBA(file.data(), file.size(), &width, &height); + ASSERT_NE(decoded.data, nullptr); + EXPECT_TRUE(visible_equal(bytes(decoded, rgba.size()), rgba)); +} + +TEST(Simple, LossyRgbaAtAQuality) { + const auto rgba = picture(W, H); + std::size_t previous = 0; + for (const float quality : {20.f, 60.f, 95.f}) { + Freed encoded; + const std::size_t size = WebPEncodeRGBA(rgba.data(), W, H, W * 4, quality, &encoded.data); + ASSERT_GT(size, 0u); + EXPECT_GT(size, previous) << "quality " << quality; // a higher quality spends more bytes + previous = size; + + WebPBitstreamFeatures features; + ASSERT_EQ(WebPGetFeatures(encoded.data, size, &features), VP8_STATUS_OK); + EXPECT_EQ(features.format, 1); // lossy (VP8 with an ALPH chunk) + EXPECT_TRUE(features.has_alpha); + + int width = 0, height = 0; + Freed decoded; + decoded.data = WebPDecodeRGBA(encoded.data, size, &width, &height); + ASSERT_NE(decoded.data, nullptr); + ASSERT_EQ(width, W); + const auto out = bytes(decoded, rgba.size()); + EXPECT_GT(psnr(rgba, out), quality < 50 ? 28.0 : 33.0) << "quality " << quality; + // Alpha is compressed losslessly by default (alpha_quality 100). + for (std::size_t i = 3; i < rgba.size(); i += 4) ASSERT_EQ(out[i], rgba[i]) << "pixel " << i / 4; + } +} + +TEST(Simple, OtherLayouts) { + const auto rgba = picture(W, H); + std::vector rgb, bgra(rgba.size()); + for (std::size_t i = 0; i < rgba.size(); i += 4) { + rgb.insert(rgb.end(), {rgba[i], rgba[i + 1], rgba[i + 2]}); + bgra[i] = rgba[i + 2], bgra[i + 1] = rgba[i + 1], bgra[i + 2] = rgba[i], bgra[i + 3] = rgba[i + 3]; + } + Freed a, b; + const std::size_t from_rgb = WebPEncodeLosslessRGB(rgb.data(), W, H, W * 3, &a.data); + const std::size_t from_bgra = WebPEncodeLosslessBGRA(bgra.data(), W, H, W * 4, &b.data); + ASSERT_GT(from_rgb, 0u); + ASSERT_GT(from_bgra, 0u); + int width, height; + Freed c, d; + c.data = WebPDecodeRGB(a.data, from_rgb, &width, &height); + d.data = WebPDecodeBGRA(b.data, from_bgra, &width, &height); + ASSERT_TRUE(c.data && d.data); + EXPECT_TRUE(std::ranges::equal(bytes(c, rgb.size()), rgb)); + for (std::size_t i = 0; i < bgra.size(); i += 4) // visible pixels exact + ASSERT_TRUE(bgra[i + 3] == 0 ? d.data[i + 3] == 0 : std::equal(bgra.data() + i, bgra.data() + i + 4, d.data + i)) << i / 4; + + // Into a caller's buffer, with a stride of its own. + const auto exact = encode_exact(rgba, W, H); + ASSERT_FALSE(exact.empty()); + std::vector into(static_cast(H) * (W * 4 + 16)); + ASSERT_NE(WebPDecodeRGBAInto(exact.data(), exact.size(), into.data(), into.size(), W * 4 + 16), nullptr); + for (int y = 0; y < H; ++y) + ASSERT_TRUE(std::equal(rgba.begin() + y * W * 4, rgba.begin() + (y + 1) * W * 4, into.begin() + y * (W * 4 + 16))); +} + +TEST(Advanced, ConfigPresetsAndValidation) { + WebPConfig config; + ASSERT_TRUE(WebPConfigInit(&config)); + EXPECT_EQ(config.quality, 75.f); + EXPECT_EQ(config.method, 4); + EXPECT_EQ(config.lossless, 0); + EXPECT_TRUE(WebPValidateConfig(&config)); + ASSERT_TRUE(WebPConfigPreset(&config, WEBP_PRESET_PHOTO, 90.f)); + EXPECT_EQ(config.quality, 90.f); + ASSERT_TRUE(WebPConfigLosslessPreset(&config, 9)); + EXPECT_EQ(config.lossless, 1); + EXPECT_EQ(config.method, 6); + config.method = 7; + EXPECT_FALSE(WebPValidateConfig(&config)); + // The *Internal entry the adaptation calls, with the ABI version the header names. + ASSERT_TRUE(WebPConfigInitInternal(&config, WEBP_PRESET_DEFAULT, 75.f, macros::WEBP_ENCODER_ABI_VERSION)); + EXPECT_TRUE(WebPValidateConfig(&config)); +} + +TEST(Advanced, EncodeLossyAndLosslessThroughAPicture) { + const auto rgba = picture(W, H); + WebPConfig config; + ASSERT_TRUE(WebPConfigInit(&config)); + config.quality = 80; + config.method = 6; + config.alpha_quality = 100; + const auto lossy = encode(config, rgba, W, H); + ASSERT_FALSE(lossy.empty()); + + config.lossless = 1; + config.exact = 1; + const auto lossless = encode(config, rgba, W, H); + ASSERT_FALSE(lossless.empty()); + + for (const auto* file : {&lossy, &lossless}) { + WebPDecoderConfig dc; + ASSERT_TRUE(WebPInitDecoderConfig(&dc)); + ASSERT_EQ(WebPGetFeatures(file->data(), file->size(), &dc.input), VP8_STATUS_OK); + EXPECT_EQ(dc.input.format, file == &lossy ? 1 : 2); + dc.output.colorspace = MODE_RGBA; + ASSERT_EQ(WebPDecode(file->data(), file->size(), &dc), VP8_STATUS_OK); + ASSERT_EQ(dc.output.width, W); + const std::span out(dc.output.u.RGBA.rgba, dc.output.u.RGBA.size); + ASSERT_EQ(out.size(), rgba.size()); + if (file == &lossless) EXPECT_TRUE(std::ranges::equal(out, rgba)); + else EXPECT_GT(psnr(rgba, out), 32.0); + WebPFreeDecBuffer(&dc.output); + } +} + +TEST(Advanced, PictureStatsProgressAndDistortion) { + const auto rgba = picture(W, H); + WebPConfig config; + ASSERT_TRUE(WebPConfigInit(&config)); + WebPPicture pic; + ASSERT_TRUE(WebPPictureInit(&pic)); + pic.width = W; + pic.height = H; + pic.use_argb = 1; + ASSERT_TRUE(WebPPictureImportRGBA(&pic, rgba.data(), W * 4)); + EXPECT_TRUE(WebPPictureHasTransparency(&pic)); + + WebPAuxStats stats; + pic.stats = &stats; + WebPMemoryWriter writer; + WebPMemoryWriterInit(&writer); + pic.writer = WebPMemoryWrite; + pic.custom_ptr = &writer; + static int last_percent; + last_percent = -1; + pic.progress_hook = [](int percent, const WebPPicture*) { + last_percent = percent; + return 1; + }; + ASSERT_TRUE(WebPEncode(&config, &pic)); + EXPECT_GT(last_percent, 50); + EXPECT_EQ(stats.coded_size, static_cast(writer.size)); + EXPECT_GT(stats.PSNR[4], 25.f); // the alpha plane's PSNR is stats.PSNR[4] + + // Distortion against a copy of itself is perfect. + WebPPicture copy; + ASSERT_TRUE(WebPPictureInit(©)); + ASSERT_TRUE(WebPPictureCopy(&pic, ©)); + float result[5]; + ASSERT_TRUE(WebPPictureDistortion(&pic, ©, 0, result)); + EXPECT_GE(result[4], 99.f); + // A crop and a rescale. + ASSERT_TRUE(WebPPictureCrop(©, 8, 8, 32, 16)); + EXPECT_EQ(copy.width, 32); + ASSERT_TRUE(WebPPictureRescale(©, 16, 8)); + EXPECT_EQ(copy.height, 8); + WebPPictureFree(©); + WebPPictureFree(&pic); + WebPMemoryWriterClear(&writer); +} + +TEST(Advanced, AbortedByTheProgressHook) { + const auto rgba = picture(W, H); + WebPConfig config; + ASSERT_TRUE(WebPConfigInit(&config)); + WebPPicture pic; + ASSERT_TRUE(WebPPictureInit(&pic)); + pic.width = W; + pic.height = H; + ASSERT_TRUE(WebPPictureImportRGBA(&pic, rgba.data(), W * 4)); + pic.progress_hook = [](int, const WebPPicture*) { return 0; }; + EXPECT_FALSE(WebPEncode(&config, &pic)); + EXPECT_EQ(pic.error_code, VP8_ENC_ERROR_USER_ABORT); + WebPPictureFree(&pic); +} + +TEST(Advanced, BadDimensionIsAnError) { + WebPConfig config; + ASSERT_TRUE(WebPConfigInit(&config)); + WebPPicture pic; + ASSERT_TRUE(WebPPictureInit(&pic)); + pic.width = macros::WEBP_MAX_DIMENSION + 1; + pic.height = 1; + ASSERT_TRUE(WebPPictureAlloc(&pic)); // the picture may be larger; the bitstream may not + EXPECT_FALSE(WebPEncode(&config, &pic)); + EXPECT_EQ(pic.error_code, VP8_ENC_ERROR_BAD_DIMENSION); + WebPPictureFree(&pic); + // The simple API answers 0 bytes. + std::uint8_t pixel[4] = {1, 2, 3, 4}; + std::uint8_t* out = nullptr; + EXPECT_EQ(WebPEncodeRGBA(pixel, macros::WEBP_MAX_DIMENSION + 1, 1, 4, 75.f, &out), 0u); + WebPFree(out); +} + +TEST(Advanced, DecodeScaledFlippedAndInOtherColourspaces) { + const auto rgba = picture(W, H); + const auto file = encode_exact(rgba, W, H); + ASSERT_FALSE(file.empty()); + const auto* const encoded_data = file.data(); + const std::size_t size = file.size(); + + WebPDecoderConfig dc; + ASSERT_TRUE(WebPInitDecoderConfig(&dc)); + dc.options.use_scaling = 1; + dc.options.scaled_width = W / 2; + dc.options.scaled_height = H / 2; + dc.output.colorspace = MODE_rgbA; // premultiplied + ASSERT_EQ(WebPDecode(encoded_data, size, &dc), VP8_STATUS_OK); + EXPECT_EQ(dc.output.width, W / 2); + EXPECT_EQ(dc.output.height, H / 2); + WebPFreeDecBuffer(&dc.output); + + ASSERT_TRUE(WebPInitDecoderConfig(&dc)); + dc.options.flip = 1; + dc.output.colorspace = MODE_RGBA; + ASSERT_EQ(WebPDecode(encoded_data, size, &dc), VP8_STATUS_OK); + const auto* top = dc.output.u.RGBA.rgba; + EXPECT_TRUE(std::equal(rgba.end() - W * 4, rgba.end(), top)); // the last row first + WebPFreeDecBuffer(&dc.output); + + ASSERT_TRUE(WebPInitDecoderConfig(&dc)); + dc.output.colorspace = MODE_YUVA; + ASSERT_EQ(WebPDecode(encoded_data, size, &dc), VP8_STATUS_OK); + EXPECT_NE(dc.output.u.YUVA.a, nullptr); + EXPECT_EQ(dc.output.u.YUVA.u_stride, (W + 1) / 2); + WebPFreeDecBuffer(&dc.output); + + // Into a caller's buffer (is_external_memory). + std::vector mine(rgba.size()); + ASSERT_TRUE(WebPInitDecoderConfig(&dc)); + dc.output.colorspace = MODE_RGBA; + dc.output.is_external_memory = 1; + dc.output.u.RGBA.rgba = mine.data(); + dc.output.u.RGBA.stride = W * 4; + dc.output.u.RGBA.size = mine.size(); + ASSERT_EQ(WebPDecode(encoded_data, size, &dc), VP8_STATUS_OK); + EXPECT_EQ(mine, rgba); + WebPFreeDecBuffer(&dc.output); +} + +TEST(Incremental, AppendInPieces) { + const auto rgba = picture(W, H); + Freed encoded; + const std::size_t size = WebPEncodeRGBA(rgba.data(), W, H, W * 4, 80.f, &encoded.data); + ASSERT_GT(size, 0u); + + WebPDecBuffer buffer; + ASSERT_TRUE(WebPInitDecBuffer(&buffer)); + buffer.colorspace = MODE_RGBA; + WebPIDecoder* idec = WebPINewDecoder(&buffer); + ASSERT_NE(idec, nullptr); + VP8StatusCode status = VP8_STATUS_SUSPENDED; + int suspended = 0; + for (std::size_t at = 0; at < size; at += 97) { + status = WebPIAppend(idec, encoded.data + at, std::min(97, size - at)); + if (status == VP8_STATUS_SUSPENDED) ++suspended; + else break; + } + EXPECT_EQ(status, VP8_STATUS_OK); + EXPECT_GT(suspended, 3); + int last_y = 0, width = 0, height = 0, stride = 0; + EXPECT_NE(WebPIDecGetRGB(idec, &last_y, &width, &height, &stride), nullptr); + EXPECT_EQ(last_y, H); + WebPIDelete(idec); + + // The one-shot decode gives the same pixels. + Freed whole; + whole.data = WebPDecodeRGBA(encoded.data, size, &width, &height); + EXPECT_TRUE(std::equal(buffer.u.RGBA.rgba, buffer.u.RGBA.rgba + rgba.size(), whole.data)); + WebPFreeDecBuffer(&buffer); + + // A YUVA decoder with its own buffers, read through the adapted WebPIDecGetYUV. + idec = WebPINewYUVA(nullptr, 0, 0, nullptr, 0, 0, nullptr, 0, 0, nullptr, 0, 0); + ASSERT_NE(idec, nullptr); + ASSERT_EQ(WebPIUpdate(idec, encoded.data, size), VP8_STATUS_OK); + std::uint8_t *u = nullptr, *v = nullptr; + int uv_stride = 0; + EXPECT_NE(WebPIDecGetYUV(idec, &last_y, &u, &v, &width, &height, &stride, &uv_stride), nullptr); + EXPECT_TRUE(u && v); + EXPECT_EQ(uv_stride, (W + 1) / 2); + WebPIDelete(idec); +} + +TEST(Errors, GarbageAndTruncation) { + const std::array garbage{'R', 'I', 'F', 'F', 9, 9, 9, 9, 'W', 'E', 'B', 'X'}; + int width = -1, height = -1; + EXPECT_FALSE(WebPGetInfo(garbage.data(), garbage.size(), &width, &height)); + EXPECT_EQ(WebPDecodeRGBA(garbage.data(), garbage.size(), &width, &height), nullptr); + WebPBitstreamFeatures features; + EXPECT_EQ(WebPGetFeatures(garbage.data(), garbage.size(), &features), VP8_STATUS_BITSTREAM_ERROR); + + const auto rgba = picture(W, H); + Freed encoded; + const std::size_t size = WebPEncodeRGBA(rgba.data(), W, H, W * 4, 50.f, &encoded.data); + ASSERT_GT(size, 40u); + EXPECT_EQ(WebPGetFeatures(encoded.data, 20, &features), VP8_STATUS_NOT_ENOUGH_DATA); + EXPECT_EQ(WebPDecodeRGBA(encoded.data, size / 2, &width, &height), nullptr); + WebPDecoderConfig dc; + ASSERT_TRUE(WebPInitDecoderConfig(&dc)); + EXPECT_EQ(WebPDecode(encoded.data, size / 2, &dc), VP8_STATUS_NOT_ENOUGH_DATA); + WebPFreeDecBuffer(&dc.output); +} + +TEST(Inline, ModesAndData) { + EXPECT_TRUE(WebPIsAlphaMode(MODE_RGBA)); + EXPECT_TRUE(WebPIsAlphaMode(MODE_rgbA)); + EXPECT_FALSE(WebPIsAlphaMode(MODE_RGB)); + EXPECT_TRUE(WebPIsPremultipliedMode(MODE_bgrA)); + EXPECT_FALSE(WebPIsPremultipliedMode(MODE_BGRA)); + EXPECT_TRUE(WebPIsRGBMode(MODE_ARGB)); + EXPECT_FALSE(WebPIsRGBMode(MODE_YUV)); + + const std::array five{1, 2, 3, 4, 5}; + WebPData from{five.data(), five.size()}, copy; + WebPDataInit(©); + EXPECT_EQ(copy.bytes, nullptr); + ASSERT_TRUE(WebPDataCopy(&from, ©)); + EXPECT_NE(copy.bytes, from.bytes); + EXPECT_TRUE(std::equal(copy.bytes, copy.bytes + copy.size, five.begin(), five.end())); + WebPDataClear(©); // WebPFree, and zeroed + EXPECT_EQ(copy.size, 0u); + + // WebPMalloc / WebPFree, the library's allocator. + void* block = WebPMalloc(64); + EXPECT_NE(block, nullptr); + WebPFree(block); + EXPECT_EQ(ALPHA_FLAG, 0x10); + EXPECT_EQ(ANIMATION_FLAG, 0x02); +} + +TEST(Determinism, SameBytesEveryRunWithOrWithoutThreads) { + const auto rgba = picture(W, H); + for (const int lossless : {0, 1}) { + WebPConfig config; + ASSERT_TRUE(WebPConfigInit(&config)); + config.lossless = lossless; + config.quality = 75; + config.method = 4; + const auto first = encode(config, rgba, W, H); + ASSERT_FALSE(first.empty()); + EXPECT_EQ(encode(config, rgba, W, H), first); + config.thread_level = 1; + EXPECT_EQ(encode(config, rgba, W, H), first) << "thread_level 1, lossless " << lossless; + } +} + +TEST(SharpYuv, ConvertsAPicture) { + const SharpYuvConversionMatrix* matrix = SharpYuvGetConversionMatrix(kSharpYuvMatrixWebp); + ASSERT_NE(matrix, nullptr); + SharpYuvOptions options; + ASSERT_TRUE(SharpYuvOptionsInit(matrix, &options)); + EXPECT_EQ(options.transfer_type, kSharpYuvTransferFunctionSrgb); + + const auto rgba = picture(16, 16); + std::vector y(16 * 16), u(8 * 8), v(8 * 8); + ASSERT_TRUE(SharpYuvConvertWithOptions(rgba.data(), rgba.data() + 1, rgba.data() + 2, 4, 16 * 4, 8, y.data(), 16, + u.data(), 8, v.data(), 8, 8, 16, 16, &options)); + EXPECT_LT(y[0], y[15]); // red rises to the right: so does luma + + SharpYuvConversionMatrix computed; + const SharpYuvColorSpace bt709{0.2126f, 0.0722f, 8, kSharpYuvRangeLimited}; + SharpYuvComputeConversionMatrix(&bt709, &computed); + const SharpYuvConversionMatrix* table = SharpYuvGetConversionMatrix(kSharpYuvMatrixRec709Limited); + EXPECT_TRUE(std::ranges::equal(computed.rgb_to_y, table->rgb_to_y)); +} diff --git a/tests/determinism_test.cpp b/tests/determinism_test.cpp new file mode 100644 index 0000000..d924f94 --- /dev/null +++ b/tests/determinism_test.cpp @@ -0,0 +1,113 @@ +// The same bytes on every target. One picture (a fixed pattern with alpha: +// gradients, edges, noise from a fixed LCG) encoded lossy at several +// qualities and methods, and lossless at several levels; then a lossy file +// decoded to RGBA. The FNV-1a hashes are pinned: CI checks them on Linux +// x86_64 (GCC, LLVM), Linux arm64, macOS arm64, Windows x86_64 and +// wasm32-emscripten, in the dev and release profiles. A failure prints the +// hash this target produced. +#include + +import std; +import mcpplibs.mwrap.libwebp; + +using namespace mcpplibs::mwrap::libwebp; + +namespace { + +constexpr int W = 128, H = 96; + +std::vector picture() { + std::vector rgba(static_cast(W) * H * 4); + std::uint32_t state = 12345; + for (int y = 0; y < H; ++y) + for (int x = 0; x < W; ++x) { + state = state * 1664525u + 1013904223u; + const int noise = static_cast(state >> 28); // 0 … 15 + auto* p = &rgba[(static_cast(y) * W + x) * 4]; + const bool disc = (x - 80) * (x - 80) + (y - 40) * (y - 40) < 24 * 24; + p[0] = static_cast(disc ? 230 : x * 2 + noise); + p[1] = static_cast(disc ? 40 : y * 2 + noise); + p[2] = static_cast((x ^ y) & 0x3f) + (x > 64 ? 128 : 0); + p[3] = static_cast(y < 16 ? 0 : y < 48 ? (y - 16) * 8 : 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; +} + +std::vector encode(const WebPConfig& config) { + const auto rgba = picture(); + WebPPicture pic; + if (!WebPPictureInit(&pic)) return {}; + pic.use_argb = 1; + pic.width = W; + pic.height = H; + WebPMemoryWriter writer; + WebPMemoryWriterInit(&writer); + pic.writer = WebPMemoryWrite; + pic.custom_ptr = &writer; + std::vector out; + if (WebPPictureImportRGBA(&pic, rgba.data(), W * 4) && WebPEncode(&config, &pic)) + out.assign(writer.mem, writer.mem + writer.size); + WebPPictureFree(&pic); + WebPMemoryWriterClear(&writer); + return out; +} + +struct Case { + const char* name; + int lossless; + float quality; + int method; // lossy: method; lossless: the WebPConfigLosslessPreset level + std::uint64_t expected; +}; + +} // namespace + +TEST(Determinism, EncodedBytesArePinned) { + const Case cases[] = { + {"lossy q50 m0", 0, 50, 0, 0x3fbae7dbd35518e5ull}, + {"lossy q75 m4", 0, 75, 4, 0x22522721157d9dd9ull}, + {"lossy q90 m6", 0, 90, 6, 0x8e8c480714b93d6aull}, + {"lossless level 0", 1, 0, 0, 0xb543b10b50599350ull}, + {"lossless level 6", 1, 0, 6, 0x67f05a16650abc6dull}, + {"lossless level 9", 1, 0, 9, 0x52a226853895b19eull}, + }; + for (const auto& c : cases) { + WebPConfig config; + ASSERT_TRUE(WebPConfigInit(&config)); + if (c.lossless) { + ASSERT_TRUE(WebPConfigLosslessPreset(&config, c.method)); + } else { + config.quality = c.quality; + config.method = c.method; + } + const auto file = encode(config); + ASSERT_FALSE(file.empty()) << c.name; + const auto hash = fnv1a(file); + std::println("{}: {} bytes, fnv1a 0x{:016x}", c.name, file.size(), hash); + EXPECT_EQ(hash, c.expected) << c.name << ": 0x" << std::hex << hash; + } +} + +TEST(Determinism, DecodedPixelsArePinned) { + WebPConfig config; + ASSERT_TRUE(WebPConfigInit(&config)); + const auto file = encode(config); + ASSERT_FALSE(file.empty()); + for (const int fancy : {1, 0}) { + WebPDecoderConfig dc; + ASSERT_TRUE(WebPInitDecoderConfig(&dc)); + dc.output.colorspace = MODE_RGBA; + dc.options.no_fancy_upsampling = !fancy; + ASSERT_EQ(WebPDecode(file.data(), file.size(), &dc), VP8_STATUS_OK); + const auto hash = fnv1a({dc.output.u.RGBA.rgba, dc.output.u.RGBA.size}); + std::println("decoded, fancy upsampling {}: fnv1a 0x{:016x}", fancy, hash); + EXPECT_EQ(hash, fancy ? 0xf4122fabaf7ed9abull : 0x9662fda975cec432ull) << "fancy " << fancy << ": 0x" << std::hex << hash; + WebPFreeDecBuffer(&dc.output); + } +} diff --git a/tests/header_mix_test.cpp b/tests/header_mix_test.cpp new file mode 100644 index 0000000..60fcadb --- /dev/null +++ b/tests/header_mix_test.cpp @@ -0,0 +1,61 @@ +// The raw headers and the import in one translation unit (the headers first): +// the module's names are the very same entities, the header's macros equal +// the mapped constants, and the header's static inline functions and the +// module's adaptations give the same results. While a header's macro is +// defined it expands even inside a qualified name, so +// `macros::WEBP_MAX_DIMENSION` is only reachable with the macro set aside; +// and both static inline forms are visible, so name them qualified. +#include +#include +#include + +import std; +import mcpplibs.mwrap.libwebp; + +namespace { +constexpr int raw_max = WEBP_MAX_DIMENSION, raw_decoder_abi = WEBP_DECODER_ABI_VERSION; +} + +#pragma push_macro("WEBP_MAX_DIMENSION") +#pragma push_macro("WEBP_DECODER_ABI_VERSION") +#undef WEBP_MAX_DIMENSION +#undef WEBP_DECODER_ABI_VERSION +static_assert(mcpplibs::mwrap::libwebp::macros::WEBP_MAX_DIMENSION == raw_max); +static_assert(mcpplibs::mwrap::libwebp::macros::WEBP_DECODER_ABI_VERSION == raw_decoder_abi); +#pragma pop_macro("WEBP_DECODER_ABI_VERSION") +#pragma pop_macro("WEBP_MAX_DIMENSION") + +TEST(HeaderMix, SameEntitiesAndValues) { + static_assert(std::is_same_v); + static_assert(std::is_same_v && std::is_same_v); + static_assert(MODE_RGBA == ::MODE_RGBA && VP8_ENC_ERROR_BAD_DIMENSION == ::VP8_ENC_ERROR_BAD_DIMENSION); + + // The header's WebPConfigInit and the module's: the same configuration + // (the reserved padding is left as it was, by both). + ::WebPConfig raw, adapted; + std::memset(&raw, 0x5a, sizeof raw); + std::memset(&adapted, 0x5a, sizeof adapted); + ASSERT_TRUE(::WebPConfigInit(&raw)); + ASSERT_TRUE(mcpplibs::mwrap::libwebp::WebPConfigInit(&adapted)); + EXPECT_EQ(std::memcmp(&raw, &adapted, sizeof raw), 0); + ASSERT_TRUE(::WebPConfigPreset(&raw, WEBP_PRESET_ICON, 33.f)); + ASSERT_TRUE(mcpplibs::mwrap::libwebp::WebPConfigPreset(&adapted, WEBP_PRESET_ICON, 33.f)); + EXPECT_EQ(std::memcmp(&raw, &adapted, sizeof raw), 0); + for (int mode = MODE_RGB; mode < MODE_LAST; ++mode) { + const auto m = static_cast(mode); + EXPECT_EQ(::WebPIsAlphaMode(m), mcpplibs::mwrap::libwebp::WebPIsAlphaMode(m)) << mode; + EXPECT_EQ(::WebPIsPremultipliedMode(m), mcpplibs::mwrap::libwebp::WebPIsPremultipliedMode(m)) << mode; + EXPECT_EQ(::WebPIsRGBMode(m), mcpplibs::mwrap::libwebp::WebPIsRGBMode(m)) << mode; + } + + // Encoded through the header's spelling, decoded through the module's. + const std::array pixels{255, 0, 0, 255, 0, 255, 0, 128, 0, 0, 255, 0, 9, 9, 9, 9}; + std::uint8_t* file = nullptr; + const std::size_t size = ::WebPEncodeLosslessRGBA(pixels.data(), 2, 2, 8, &file); + ASSERT_GT(size, 0u); + WebPBitstreamFeatures a, b; + ASSERT_EQ(::WebPGetFeatures(file, size, &a), VP8_STATUS_OK); + ASSERT_EQ(mcpplibs::mwrap::libwebp::WebPGetFeatures(file, size, &b), VP8_STATUS_OK); + EXPECT_EQ(std::memcmp(&a, &b, sizeof a), 0); + WebPFree(file); +} diff --git a/tests/macros.cpp b/tests/macros.cpp new file mode 100644 index 0000000..f075df7 --- /dev/null +++ b/tests/macros.cpp @@ -0,0 +1,33 @@ +// The macro mappings as constants: usable in constant expressions, with the +// values of the headers; and at run time, the same versions as the library +// reports them. +import std; +import mcpplibs.mwrap.libwebp; + +namespace m = mcpplibs::mwrap::libwebp::macros; + +static_assert(m::WEBP_DECODER_ABI_VERSION == 0x0209 && m::WEBP_ENCODER_ABI_VERSION == 0x0210); +static_assert(m::WEBP_DEMUX_ABI_VERSION == 0x0107 && m::WEBP_MUX_ABI_VERSION == 0x0109); +static_assert(m::WEBP_MAX_DIMENSION == 16383); +static_assert(std::is_same_v, int>); +static_assert(!m::WEBP_ABI_IS_INCOMPATIBLE(0x0209, 0x0201) && m::WEBP_ABI_IS_INCOMPATIBLE(0x0209, 0x0309)); +static_assert(m::SHARPYUV_VERSION_MAJOR == 0 && m::SHARPYUV_VERSION_MINOR == 4 && m::SHARPYUV_VERSION_PATCH == 1); +static_assert(m::SHARPYUV_VERSION == m::SHARPYUV_MAKE_VERSION(0, 4, 1) && m::SHARPYUV_VERSION == 0x00040001); + +int main() { + int failures = 0; + for (auto [name, value, expected] : {std::tuple{"WebPGetDecoderVersion", WebPGetDecoderVersion(), 0x010500}, + {"WebPGetEncoderVersion", WebPGetEncoderVersion(), 0x010500}, + {"SharpYuvGetVersion", SharpYuvGetVersion(), m::SHARPYUV_VERSION}}) { + const bool ok = value == expected; + std::println("{:<24} 0x{:06x} {}", name, value, ok ? "ok" : "FAILED"); + failures += !ok; + } + // The ABI version the module's adaptations pass is the one the library accepts. + WebPDecoderConfig config; + const bool init = WebPInitDecoderConfigInternal(&config, m::WEBP_DECODER_ABI_VERSION) != 0; + const bool refused = WebPInitDecoderConfigInternal(&config, m::WEBP_DECODER_ABI_VERSION + 0x100) == 0; + std::println("WEBP_DECODER_ABI_VERSION accepted {}, a newer major refused {}", init ? "ok" : "FAILED", + refused ? "ok" : "FAILED"); + return failures + !init + !refused; +} diff --git a/tools/check_exports.py b/tools/check_exports.py new file mode 100644 index 0000000..aa08e40 --- /dev/null +++ b/tools/check_exports.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Audit the modules against libwebp's public headers (the pinned release's). + +- The module interfaces and their implementation units are what + tools/gen_exports.py writes. +- Every type, enumerator and WEBP_EXTERN function of each header group is + exported by its module, and every static inline function adapted there. +- Every macro of the public headers is mapped in src/macros.cppm or excluded + below with a reason (the declaration annotations and the include guards). +""" +from pathlib import Path +import re +import subprocess +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from gen_exports import MODULES # noqa: E402 +from inventory import fetch, surface # noqa: E402 + +EXCLUDED = { + 'WEBP_EXTERN': 'a linkage annotation of declarations', + 'WEBP_INLINE': "the headers' inlining annotation of their static functions", + 'WEBP_NODISCARD': 'an attribute of declarations (kept as [[nodiscard]] on the adaptations)', + 'SHARPYUV_EXTERN': 'a linkage annotation of declarations', + 'SHARPYUV_INLINE': "the header's inlining annotation of its static function", +} + + +def is_guard(name: str) -> bool: + return re.fullmatch(r'WEBP_(?:WEBP|SHARPYUV)_\w+_H_', name) is not None + + +def mapped_names(text: str) -> set[str]: + text = text[text.index('export namespace'):] + names = set(re.findall(r'inline constexpr (?:[\w:&]+\s+)+?(\w+)\s*=', text)) + names |= set(re.findall(r'^constexpr [\w:\*\s]+?\b(\w+)\s*\(', text, flags=re.M)) + return names + + +def main(): + root = Path(__file__).resolve().parents[1] + problems = [] + generated = subprocess.run([sys.executable, str(root / 'tools/gen_exports.py'), '--check'], capture_output=True, + text=True) + if generated.returncode != 0: + problems.append(generated.stderr.strip() or generated.stdout.strip()) + sf = surface(fetch(root)) + macros = set() + counts = [] + for group, g in sf.items(): + text = (root / MODULES[group]['interface']).read_text(encoding='utf-8') + exported = set(re.findall(r'export using ::(\w+);', text)) + missing = (set(g.types) | set(g.enumerators) | set(g.functions)) - exported + if missing: + problems.append(f'{MODULES[group]["interface"]} lacks: ' + ', '.join(sorted(missing))) + adapted = text[text.index('export namespace mcpplibs::mwrap::libwebp {'):] + missing = {i.name for i in g.inline if not re.search(r'\b' + i.name + r'\(', adapted)} + if missing: + problems.append(f'{MODULES[group]["interface"]} does not adapt: ' + ', '.join(sorted(missing))) + macros |= set(g.macros) + counts.append(f'{group} {len(g.types)}/{len(g.enumerators)}/{len(g.functions)}/{len(g.inline)}') + owners = {} + for group, g in sf.items(): + for name in g.types + g.enumerators + g.functions: + owners.setdefault(name, []).append(group) + shared = {name: groups for name, groups in owners.items() if len(groups) > 1} + if shared: + problems.append('exported by more than one module: ' + ', '.join(sorted(shared))) + mapped = mapped_names((root / 'src/macros.cppm').read_text(encoding='utf-8')) + guards = {m for m in macros if is_guard(m)} + unaccounted = macros - mapped - EXCLUDED.keys() - guards + if unaccounted: + problems.append('macros neither mapped nor excluded: ' + ', '.join(sorted(unaccounted))) + both = EXCLUDED.keys() & mapped + if both: + problems.append('listed as excluded yet mapped: ' + ', '.join(sorted(both))) + stale = EXCLUDED.keys() - macros + if stale: + problems.append('excluded but not in the headers: ' + ', '.join(sorted(stale))) + extra = mapped - macros + if extra: + problems.append('mapped but not a macro of the headers: ' + ', '.join(sorted(extra))) + if problems: + sys.exit('\n'.join(problems)) + print('exported (types/enumerators/functions/adapted static inline): ' + ', '.join(counts) + + f'; {len(macros)} macros: {len(macros & mapped)} mapped, {len(EXCLUDED)} annotations and ' + f'{len(guards)} include guards excluded') + + +if __name__ == '__main__': + main() diff --git a/tools/gen_exports.py b/tools/gen_exports.py new file mode 100644 index 0000000..c113af4 --- /dev/null +++ b/tools/gen_exports.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Write the modules' export lists from libwebp's public headers. + +For each group of tools/inventory.py: + +- the module interface exports every type, enumerator and WEBP_EXTERN function + of the group's headers by `export using`: the upstream globals, unchanged; +- the `static WEBP_INLINE` functions (internal linkage, not exportable) are + declared in namespace mcpplibs::mwrap::libwebp under the same names and + signatures, and defined out of line in the group's implementation unit by + calling upstream's own inline body. + + core src/libwebp.cppm src/inline.cpp + sharpyuv src/sharpyuv.cppm src/sharpyuv_inline.cpp + demux src/demux/demux.cppm src/demux/inline.cpp + mux src/mux/mux.cppm src/mux/inline.cpp + +The macros are src/macros.cppm's, written by hand (tools/check_exports.py +accounts for every one). With --check, nothing is written and a difference +fails. +""" +import argparse +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from inventory import INCLUDES, fetch, surface # noqa: E402 + +GENERATED = '// Generated by tools/gen_exports.py from libwebp 1.5.0\'s headers — do not edit\n// by hand.\n' + +MODULES = { + 'core': { + 'interface': 'src/libwebp.cppm', 'implementation': 'src/inline.cpp', + 'module': 'mcpplibs.mwrap.libwebp', + 'about': '''// +// mcpplibs.mwrap.libwebp — libwebp, the WebP image codec, as a C++ module: +// decoding (webp/decode.h), encoding (webp/encode.h), the common types +// (webp/types.h) and WebPData with the animation enums (webp/mux_types.h). +// Every exported name is the upstream global, unchanged; the root also +// re-exports the macro mappings and the sharpyuv module. The codec is +// compat.libwebp, built from the same release. +// +// The `static inline` API (internal linkage, not exportable) is adapted in +// namespace mcpplibs::mwrap::libwebp under the same names and signatures, each +// calling upstream's inline body (src/inline.cpp). +''', + 'imports': ['mcpplibs.mwrap.libwebp.macros', 'mcpplibs.mwrap.libwebp.sharpyuv'], + }, + 'sharpyuv': { + 'interface': 'src/sharpyuv.cppm', 'implementation': 'src/sharpyuv_inline.cpp', + 'module': 'mcpplibs.mwrap.libwebp.sharpyuv', + 'about': '''// +// mcpplibs.mwrap.libwebp.sharpyuv — libsharpyuv, the sharp RGB to YUV +// conversion the encoder uses (sharpyuv/sharpyuv.h, sharpyuv/sharpyuv_csp.h), +// in compat.libwebp's archive. The root module re-exports it. +// +// SharpYuvOptionsInit (static inline) is adapted in mcpplibs::mwrap::libwebp. +''', + 'imports': [], + }, + 'demux': { + 'interface': 'src/demux/demux.cppm', 'implementation': 'src/demux/inline.cpp', + 'module': 'mcpplibs.mwrap.libwebp.demux', + 'about': '''// +// mcpplibs.mwrap.libwebp.demux — libwebpdemux (webp/demux.h): the container +// parser (WebPDemuxer, frames, chunks) and WebPAnimDecoder, which renders an +// animation's frames to full canvases. With the `demux` feature, which builds +// compat.libwebp's src/demux. Import it by name: the root does not re-export +// it (mcpp's scanner admits no import under a feature test); it re-exports the +// root, as demux.h includes decode.h and mux_types.h. +// +// The `static inline` API is adapted in mcpplibs::mwrap::libwebp +// (src/demux/inline.cpp). +''', + 'imports': ['mcpplibs.mwrap.libwebp'], + }, + 'mux': { + 'interface': 'src/mux/mux.cppm', 'implementation': 'src/mux/inline.cpp', + 'module': 'mcpplibs.mwrap.libwebp.mux', + 'about': '''// +// mcpplibs.mwrap.libwebp.mux — libwebpmux (webp/mux.h): the container editor +// (WebPMux: frames, ICC/EXIF/XMP chunks, canvas and animation parameters) and +// WebPAnimEncoder, which encodes full-canvas frames into an animation. With +// the `mux` feature, which builds compat.libwebp's src/mux. Import it by name: +// the root does not re-export it; it re-exports the root, as mux.h includes +// mux_types.h and WebPAnimEncoder takes encode.h's WebPConfig and WebPPicture. +// +// The `static inline` API is adapted in mcpplibs::mwrap::libwebp +// (src/mux/inline.cpp). +''', + 'imports': ['mcpplibs.mwrap.libwebp'], + }, +} + + +def includes(group: str) -> str: + return ''.join(f'#include <{name}>\n' for name in INCLUDES[group]) + + +def interface(group: str, g) -> str: + spec = MODULES[group] + out = [GENERATED + spec['about'] + 'module;\n' + includes(group) + f'\nexport module {spec["module"]};\n'] + if spec['imports']: + out.append(''.join(f'export import {name};\n' for name in spec['imports'])) + out.append(f'// types ({len(g.types)})') + out += [f'export using ::{n};' for n in g.types] + out.append(f'\n// enumerators ({len(g.enumerators)})') + out += [f'export using ::{n};' for n in g.enumerators] + out.append(f'\n// functions ({len(g.functions)})') + out += [f'export using ::{n};' for n in g.functions] + out.append(f'\n// the static inline API ({len(g.inline)}), adapted') + out.append('export namespace mcpplibs::mwrap::libwebp {') + out += [i.declaration() + ';' for i in g.inline] + out.append('} // namespace mcpplibs::mwrap::libwebp') + return '\n'.join(out) + '\n' + + +def implementation(group: str, g) -> str: + spec = MODULES[group] + out = [GENERATED + '//\n// The adaptations of the static inline API of\n//\n' + + ''.join(f'// {name}\n' for name in INCLUDES[group]) + + '//\n// each calling upstream\'s own inline body.\nmodule;\n' + includes(group) + + f'\nmodule {spec["module"]};\n\nnamespace mcpplibs::mwrap::libwebp {{\n'] + out += [i.definition() for i in g.inline] + out.append('\n} // namespace mcpplibs::mwrap::libwebp') + return '\n'.join(out) + '\n' + + +def outputs(root: Path) -> dict[str, str]: + sf = surface(fetch(root)) + files = {} + for group, g in sf.items(): + files[MODULES[group]['interface']] = interface(group, g) + files[MODULES[group]['implementation']] = implementation(group, g) + return files + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--check', action='store_true') + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + stale = [] + for name, text in outputs(root).items(): + path = root / name + current = path.read_text(encoding='utf-8') if path.exists() else None + if current == text: + continue # unchanged files keep their timestamps + if args.check: + stale.append(name) + continue + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding='utf-8', newline='\n') + print('wrote', name) + if stale: + sys.exit('not what tools/gen_exports.py writes: ' + ', '.join(stale)) + + +if __name__ == '__main__': + main() diff --git a/tools/inventory.py b/tools/inventory.py new file mode 100644 index 0000000..7f85e62 --- /dev/null +++ b/tools/inventory.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""The public surface of libwebp's headers, from the pinned release. + +Shared by tools/gen_exports.py (which writes the modules) and +tools/check_exports.py (which audits them against it). The headers come from +the upstream tarball, downloaded once into target/upstream and checked against +upstream/documents.json: the same bytes compat.libwebp builds. + +The public headers are the ones upstream installs, grouped by the library +that implements them and so by the module that exports them: + +- core (libwebp, always built): webp/types.h, webp/decode.h, webp/encode.h and + webp/mux_types.h (WebPData and the animation enums; header-only, shared by + demux and mux, so exported once, here); +- sharpyuv (in the same archive): sharpyuv/sharpyuv.h, sharpyuv/sharpyuv_csp.h; +- demux (the `demux` feature): webp/demux.h; +- mux (the `mux` feature): webp/mux.h. + +webp/format_constants.h is not installed by upstream: it is the library's own. +""" +from dataclasses import dataclass, field +import hashlib +import json +from pathlib import Path +import re +import sys +import tarfile +import urllib.request + +GROUPS = { + 'core': ['src/webp/types.h', 'src/webp/decode.h', 'src/webp/encode.h', 'src/webp/mux_types.h'], + 'sharpyuv': ['sharpyuv/sharpyuv.h', 'sharpyuv/sharpyuv_csp.h'], + 'demux': ['src/webp/demux.h'], + 'mux': ['src/webp/mux.h'], +} + +# What a consumer includes for each group (compat.libwebp's include roots are +# the archive root and its src/). +INCLUDES = { + 'core': ['webp/types.h', 'webp/decode.h', 'webp/encode.h', 'webp/mux_types.h'], + 'sharpyuv': ['sharpyuv/sharpyuv.h', 'sharpyuv/sharpyuv_csp.h'], + 'demux': ['webp/demux.h'], + 'mux': ['webp/mux.h'], +} + +# types.h declares the fixed-width integers itself under _MSC_VER: those are +# the C library's, not libwebp's. +STANDARD = {'int8_t', 'uint8_t', 'int16_t', 'uint16_t', 'int32_t', 'uint32_t', 'int64_t', 'uint64_t'} + + +def fetch(root: Path) -> Path: + pins = json.loads((root / 'upstream/documents.json').read_text(encoding='utf-8')) + cache = root / 'target/upstream' + cache.mkdir(parents=True, exist_ok=True) + archive = cache / 'libwebp-release.tar.gz' + if not archive.exists() or hashlib.sha256(archive.read_bytes()).hexdigest() != pins['upstream_sha256']: + print('+ fetch', pins['upstream'], flush=True) + with urllib.request.urlopen(pins['upstream'], timeout=120) as response: + archive.write_bytes(response.read()) + digest = hashlib.sha256(archive.read_bytes()).hexdigest() + if digest != pins['upstream_sha256']: + sys.exit(f'upstream archive checksum {digest} != {pins["upstream_sha256"]}') + with tarfile.open(archive) as tar: + top = tar.getmembers()[0].name.split('/')[0] + if not (cache / top).exists(): + tar.extractall(cache, filter='data') + return cache / top + + +@dataclass +class Inline: + """A `static WEBP_INLINE` function: internal linkage, adapted.""" + name: str + nodiscard: bool + result: str + params: list[tuple[str, str]] # (declaration, name) + + def declaration(self) -> str: + params = ', '.join(p for p, _ in self.params) or 'void' + return f'{"[[nodiscard]] " if self.nodiscard else ""}{self.result} {self.name}({params})' + + def definition(self) -> str: + params = ', '.join(p for p, _ in self.params) + args = ', '.join(n for _, n in self.params) + return f'{self.result} {self.name}({params}) {{ return ::{self.name}({args}); }}' + + +@dataclass +class Group: + types: list[str] = field(default_factory=list) + enumerators: list[str] = field(default_factory=list) + functions: list[str] = field(default_factory=list) + inline: list[Inline] = field(default_factory=list) + macros: list[str] = field(default_factory=list) + + +def strip(text: str) -> str: + # One pass, so a `/*` inside a line comment (encode.h has some) does not + # open a block comment. + return re.sub(r'//[^\n]*|/\*.*?\*/', lambda m: '' if m.group().startswith('//') else ' ', text, flags=re.S) + + +def declarations(text: str) -> str: + """The header without comments and preprocessor lines (and their continuations).""" + text = strip(text).replace('\\\n', ' ') + return re.sub(r'^\s*#[^\n]*', '', text, flags=re.M) + + +def parameter(text: str) -> tuple[str, str]: + text = ' '.join(text.split()) + m = re.fullmatch(r'(.*?[\s\*])(\w+)', text) + assert m, text + return text, m.group(2) + + +def group(upstream: Path, files: list[str]) -> Group: + raw = {name: (upstream / name).read_text(encoding='utf-8') for name in files} + decls = '\n'.join(declarations(text) for text in raw.values()) + g = Group() + names = set() + names |= set(re.findall(r'typedef\s+(?:struct|union)\s+(\w+)\s+\1\s*;', decls)) + names |= set(re.findall(r'typedef\s+(?:struct|enum)\s*\{[^{}]*\}\s*(\w+)\s*;', decls)) + names |= set(re.findall(r'typedef\s+[\w\s\*]+?\(\s*\*\s*(\w+)\s*\)\s*\(', decls)) + for m in re.finditer(r'\benum\s+(?:WEBP_NODISCARD\s+)?(\w*)\s*\{(.*?)\}\s*(\w*)\s*;', decls, flags=re.S): + names |= {n for n in (m.group(1), m.group(3)) if n} + g.enumerators += re.findall(r'\b([A-Za-z_]\w*)\b\s*(?:=[^,]*)?(?:,|$)', m.group(2).strip()) + names |= set(re.findall(r'typedef\s+(?!struct|enum|union)[\w\s\*]+?\b(\w+)\s*;', decls)) + g.types = sorted(names - STANDARD) + # Functions: WEBP_EXTERN / SHARPYUV_EXTERN declarations; the static inline + # ones are adapted. + for m in re.finditer(r'(?:WEBP|SHARPYUV)_EXTERN\s+[\w\s\*]+?\b(\w+)\s*\(', decls): + g.functions.append(m.group(1)) + for m in re.finditer(r'(WEBP_NODISCARD\s+)?static\s+(?:WEBP|SHARPYUV)_INLINE\s+([\w\s\*]+?)\b(\w+)\s*\(([^)]*)\)\s*\{', + decls): + params = [] if m.group(4).strip() in ('', 'void') else [parameter(p) for p in m.group(4).split(',')] + g.inline.append(Inline(m.group(3), bool(m.group(1)), ' '.join(m.group(2).split()), params)) + g.functions = sorted(set(g.functions)) + g.macros = sorted({m for text in raw.values() for m in re.findall(r'^\s*#\s*define\s+(\w+)', text, flags=re.M)}) + return g + + +def surface(upstream: Path) -> dict[str, Group]: + return {name: group(upstream, files) for name, files in GROUPS.items()} + + +if __name__ == '__main__': + root = Path(__file__).resolve().parents[1] + for name, g in surface(fetch(root)).items(): + print(f'== {name}: {len(g.types)} types, {len(g.enumerators)} enumerators, {len(g.functions)} functions, ' + f'{len(g.inline)} static inline, {len(g.macros)} macros') + print('types:', ' '.join(g.types)) + print('enumerators:', ' '.join(g.enumerators)) + print('functions:', ' '.join(g.functions)) + print('inline:', *(i.declaration() for i in g.inline), sep='\n ') + print('macros:', ' '.join(g.macros)) diff --git a/upstream/documents.json b/upstream/documents.json new file mode 100644 index 0000000..b6d0c7e --- /dev/null +++ b/upstream/documents.json @@ -0,0 +1,27 @@ +{ + "upstream": "https://github.com/webmproject/libwebp/archive/refs/tags/v1.5.0.tar.gz", + "upstream_sha256": "668c9aba45565e24c27e17f7aaf7060a399f7f31dba6c97a044e1feacb930f37", + "pruning": "Nothing of libwebp is vendored: the codec is compat.libwebp 1.5.0, built from the same tarball (dec, enc, dsp, utils and sharpyuv; demux and mux with the features of the same names). Its README, license, patent grant and authors are retained here.", + "documents": [ + { + "path": "upstream/libwebp/README.md", + "source": "https://github.com/webmproject/libwebp/blob/v1.5.0/README.md", + "sha256": "0e80b310df17ed453fdd843b853f4d0a713320f2156fb1763da6648d1ef017f5" + }, + { + "path": "upstream/libwebp/COPYING", + "source": "https://github.com/webmproject/libwebp/blob/v1.5.0/COPYING", + "sha256": "5aec868f669e384a22372a4e8a1a6cd7d44c64cd451f960ca69cc170d1e13acf" + }, + { + "path": "upstream/libwebp/PATENTS", + "source": "https://github.com/webmproject/libwebp/blob/v1.5.0/PATENTS", + "sha256": "cc3273e0694ea5896145e0677699b53471b03ea43021ddc50e7923fbb9f5023c" + }, + { + "path": "upstream/libwebp/AUTHORS", + "source": "https://github.com/webmproject/libwebp/blob/v1.5.0/AUTHORS", + "sha256": "b97b44ab6fc6f4b2e9c5b5ad84fe4f7bd198696fc6b43830598c3e1364b84df4" + } + ] +} diff --git a/upstream/libwebp/AUTHORS b/upstream/libwebp/AUTHORS new file mode 100644 index 0000000..6772bcd --- /dev/null +++ b/upstream/libwebp/AUTHORS @@ -0,0 +1,70 @@ +Contributors: +- Aidan O'Loan (aidanol at gmail dot com) +- Alan Browning (browning at google dot com) +- Alexandru Ardelean (ardeleanalex at gmail dot com) +- Anuraag Agrawal (anuraaga at gmail dot com) +- Arthur Eubanks (aeubanks at google dot com) +- Brian Ledger (brianpl at google dot com) +- Charles Munger (clm at google dot com) +- Cheng Yi (cyi at google dot com) +- Christian Duvivier (cduvivier at google dot com) +- Christopher Degawa (ccom at randomderp dot com) +- Clement Courbet (courbet at google dot com) +- Djordje Pesut (djordje dot pesut at imgtec dot com) +- Frank (1433351828 at qq dot com) +- Frank Barchard (fbarchard at google dot com) +- Hui Su (huisu at google dot com) +- H. Vetinari (h dot vetinari at gmx dot com) +- Ilya Kurdyukov (jpegqs at gmail dot com) +- Ingvar Stepanyan (rreverser at google dot com) +- Istvan Stefan (Istvan dot Stefan at arm dot com) +- James Zern (jzern at google dot com) +- Jan Engelhardt (jengelh at medozas dot de) +- Jehan (jehan at girinstud dot io) +- Jeremy Maitin-Shepard (jbms at google dot com) +- Johann Koenig (johann dot koenig at duck dot com) +- Jonathan Grant (jgrantinfotech at gmail dot com) +- Jonliu1993 (13720414433 at 163 dot com) +- Jovan Zelincevic (jovan dot zelincevic at imgtec dot com) +- Jyrki Alakuijala (jyrki at google dot com) +- Konstantin Ivlev (tomskside at gmail dot com) +- Lode Vandevenne (lode at google dot com) +- Lou Quillio (louquillio at google dot com) +- Mans Rullgard (mans at mansr dot com) +- Marcin Kowalczyk (qrczak at google dot com) +- Martin Olsson (mnemo at minimum dot se) +- Maryla Ustarroz-Calonge (maryla at google dot com) +- Masahiro Hanada (hanada at atmark-techno dot com) +- Mikołaj Zalewski (mikolajz at google dot com) +- Mislav Bradac (mislavm at google dot com) +- natewood (natewood at fb dot com) +- Nico Weber (thakis at chromium dot org) +- Noel Chromium (noel at chromium dot org) +- Nozomi Isozaki (nontan at pixiv dot co dot jp) +- Oliver Wolff (oliver dot wolff at qt dot io) +- Owen Rodley (orodley at google dot com) +- Ozkan Sezer (sezeroz at gmail dot com) +- Parag Salasakar (img dot mips1 at gmail dot com) +- Pascal Massimino (pascal dot massimino at gmail dot com) +- Paweł Hajdan, Jr (phajdan dot jr at chromium dot org) +- Pierre Joye (pierre dot php at gmail dot com) +- Roberto Alanis (alanisbaez at google dot com) +- Sam Clegg (sbc at chromium dot org) +- Scott Hancher (seh at google dot com) +- Scott LaVarnway (slavarnway at google dot com) +- Scott Talbot (s at chikachow dot org) +- Slobodan Prijic (slobodan dot prijic at imgtec dot com) +- Somnath Banerjee (somnath dot banerjee at gmail dot com) +- Sriraman Tallam (tmsriram at google dot com) +- Tamar Levy (tamar dot levy at intel dot com) +- Thiago Perrotta (tperrotta at google dot com) +- Timothy Gu (timothygu99 at gmail dot com) +- Urvang Joshi (urvang at google dot com) +- Vikas Arora (vikasa at google dot com) +- Vincent Rabaud (vrabaud at google dot com) +- Vlad Tsyrklevich (vtsyrklevich at chromium dot org) +- Wan-Teh Chang (wtc at google dot com) +- wrv (wrv at utexas dot edu) +- Yang Zhang (yang dot zhang at arm dot com) +- Yannis Guyon (yguyon at google dot com) +- Zhi An Ng (zhin at chromium dot org) diff --git a/upstream/libwebp/COPYING b/upstream/libwebp/COPYING new file mode 100644 index 0000000..7a6f995 --- /dev/null +++ b/upstream/libwebp/COPYING @@ -0,0 +1,30 @@ +Copyright (c) 2010, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/upstream/libwebp/PATENTS b/upstream/libwebp/PATENTS new file mode 100644 index 0000000..caedf60 --- /dev/null +++ b/upstream/libwebp/PATENTS @@ -0,0 +1,23 @@ +Additional IP Rights Grant (Patents) +------------------------------------ + +"These implementations" means the copyrightable works that implement the WebM +codecs distributed by Google as part of the WebM Project. + +Google hereby grants to you a perpetual, worldwide, non-exclusive, no-charge, +royalty-free, irrevocable (except as stated in this section) patent license to +make, have made, use, offer to sell, sell, import, transfer, and otherwise +run, modify and propagate the contents of these implementations of WebM, where +such license applies only to those patent claims, both currently owned by +Google and acquired in the future, licensable by Google that are necessarily +infringed by these implementations of WebM. This grant does not include claims +that would be infringed only as a consequence of further modification of these +implementations. If you or your agent or exclusive licensee institute or order +or agree to the institution of patent litigation or any other patent +enforcement activity against any entity (including a cross-claim or +counterclaim in a lawsuit) alleging that any of these implementations of WebM +or any code incorporated within any of these implementations of WebM +constitute direct or contributory patent infringement, or inducement of +patent infringement, then any patent rights granted to you under this License +for these implementations of WebM shall terminate as of the date such +litigation is filed. diff --git a/upstream/libwebp/README.md b/upstream/libwebp/README.md new file mode 100644 index 0000000..8ae1ab7 --- /dev/null +++ b/upstream/libwebp/README.md @@ -0,0 +1,54 @@ +# WebP Codec + +``` + __ __ ____ ____ ____ + / \\/ \/ _ \/ _ )/ _ \ + \ / __/ _ \ __/ + \__\__/\____/\_____/__/ ____ ___ + / _/ / \ \ / _ \/ _/ + / \_/ / / \ \ __/ \__ + \____/____/\_____/_____/____/v1.5.0 +``` + +WebP codec is a library to encode and decode images in WebP format. This package +contains the library that can be used in other programs to add WebP support, as +well as the command line tools 'cwebp' and 'dwebp' to compress and decompress +images respectively. + +See https://developers.google.com/speed/webp for details on the image format. + +The latest source tree is available at +https://chromium.googlesource.com/webm/libwebp + +It is released under the same license as the WebM project. See +https://www.webmproject.org/license/software/ or the "COPYING" file for details. +An additional intellectual property rights grant can be found in the file +PATENTS. + +## Building + +See the [building documentation](doc/building.md). + +## Encoding and Decoding Tools + +The examples/ directory contains tools to encode and decode images and +animations, view information about WebP images, and more. See the +[tools documentation](doc/tools.md). + +## APIs + +See the [APIs documentation](doc/api.md), and API usage examples in the +`examples/` directory. + +## Bugs + +Please report all bugs to the [issue tracker](https://issues.webmproject.org). +For security reports, select 'Security report' from the Template dropdown. + +Patches welcome! See [how to contribute](CONTRIBUTING.md). + +## Discuss + +Email: webp-discuss@webmproject.org + +Web: https://groups.google.com/a/webmproject.org/group/webp-discuss From 2c95c1ef3d07afb2346c301ef5f1446b4927ef9a Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 29 Oct 2026 07:51:27 +0900 Subject: [PATCH 2/3] conformance: examples/webpcodec, written against the module, compared byte for byte with upstream's cwebp and dwebp built by its own makefile.unix --- examples/webpcodec/mcpp.toml | 13 +++ examples/webpcodec/src/main.cpp | 179 ++++++++++++++++++++++++++++++++ tools/run_conformance.py | 143 +++++++++++++++++++++++++ 3 files changed, 335 insertions(+) create mode 100644 examples/webpcodec/mcpp.toml create mode 100644 examples/webpcodec/src/main.cpp create mode 100644 tools/run_conformance.py diff --git a/examples/webpcodec/mcpp.toml b/examples/webpcodec/mcpp.toml new file mode 100644 index 0000000..6fd2153 --- /dev/null +++ b/examples/webpcodec/mcpp.toml @@ -0,0 +1,13 @@ +# webpcodec: decode, encode and inspect WebP files, written against the module. +# tools/run_conformance.py runs upstream's reference decode on it. +[package] +name = "webpcodec" +version = "0.1.0" +description = "Decode WebP to PPM/PAM, encode PPM/PAM to WebP (lossy or lossless), print a file's features: libwebp through import mcpplibs.mwrap.libwebp" + +[targets.webpcodec] +kind = "bin" +main = "src/main.cpp" + +[dependencies] +"mcpplibs.mwrap.libwebp" = { path = "../.." } diff --git a/examples/webpcodec/src/main.cpp b/examples/webpcodec/src/main.cpp new file mode 100644 index 0000000..77e39cc --- /dev/null +++ b/examples/webpcodec/src/main.cpp @@ -0,0 +1,179 @@ +// webpcodec — WebP in and out, written against import mcpplibs.mwrap.libwebp. +// +// webpcodec info +// webpcodec decode [-nofancy] +// webpcodec encode [-q ] [-m ] [-lossless] [-exact] +// +// decode writes what upstream's `dwebp -ppm` / `dwebp -pam` write: a binary +// PPM (P6, RGB) or a PAM (P7, RGB_ALPHA). +import std; +import mcpplibs.mwrap.libwebp; + +using namespace mcpplibs::mwrap::libwebp; + +namespace { + +std::optional> read(const std::string& path) { + std::ifstream in(path, std::ios::binary); + if (!in) return std::nullopt; + return std::vector(std::istreambuf_iterator(in), {}); +} + +bool write(const std::string& path, std::string_view header, std::span body) { + std::ofstream out(path, std::ios::binary); + out.write(header.data(), static_cast(header.size())); + out.write(reinterpret_cast(body.data()), static_cast(body.size())); + return static_cast(out); +} + +struct Image { + int width = 0, height = 0; + bool alpha = false; + std::vector rgba; // always RGBA +}; + +// P6 (maxval 255) or P7 (RGB or RGB_ALPHA, maxval 255). +std::optional parse_pnm(const std::vector& file) { + std::size_t at = 0; + auto token = [&]() -> std::string { + while (at < file.size()) { + if (file[at] == '#') while (at < file.size() && file[at] != '\n') ++at; + else if (std::isspace(file[at])) ++at; + else break; + } + std::string t; + while (at < file.size() && !std::isspace(file[at])) t += static_cast(file[at++]); + return t; + }; + Image image; + int depth = 3, maxval = 0; + const std::string magic = token(); + if (magic == "P6") { + image.width = std::stoi(token()); + image.height = std::stoi(token()); + maxval = std::stoi(token()); + } else if (magic == "P7") { + for (std::string key = token(); key != "ENDHDR" && !key.empty(); key = token()) { + if (key == "WIDTH") image.width = std::stoi(token()); + else if (key == "HEIGHT") image.height = std::stoi(token()); + else if (key == "DEPTH") depth = std::stoi(token()); + else if (key == "MAXVAL") maxval = std::stoi(token()); + else if (key == "TUPLTYPE") token(); + } + } else { + return std::nullopt; + } + ++at; // the single whitespace after the header + if (maxval != 255 || (depth != 3 && depth != 4) || image.width <= 0 || image.height <= 0) return std::nullopt; + const std::size_t pixels = static_cast(image.width) * image.height; + if (file.size() - at < pixels * depth) return std::nullopt; + image.alpha = depth == 4; + image.rgba.resize(pixels * 4); + for (std::size_t i = 0; i < pixels; ++i) { + std::copy_n(&file[at + i * depth], 3, &image.rgba[i * 4]); + image.rgba[i * 4 + 3] = depth == 4 ? file[at + i * depth + 3] : 255; + } + return image; +} + +int info(const std::vector& file) { + WebPBitstreamFeatures features; + const VP8StatusCode status = WebPGetFeatures(file.data(), file.size(), &features); + if (status != VP8_STATUS_OK) { + std::println(std::cerr, "not a WebP file (status {})", static_cast(status)); + return 1; + } + std::println("{} x {}, {}, {}{}", features.width, features.height, + features.format == 1 ? "lossy" : features.format == 2 ? "lossless" : "mixed", + features.has_alpha ? "with alpha" : "opaque", features.has_animation ? ", animated" : ""); + return 0; +} + +int decode(const std::vector& file, const std::string& out, bool fancy) { + const bool pam = out.ends_with(".pam"); + WebPDecoderConfig config; + if (!WebPInitDecoderConfig(&config)) return 1; + config.output.colorspace = pam ? MODE_RGBA : MODE_RGB; + config.options.no_fancy_upsampling = !fancy; + const VP8StatusCode status = WebPDecode(file.data(), file.size(), &config); + if (status != VP8_STATUS_OK) { + std::println(std::cerr, "decoding failed (status {})", static_cast(status)); + return 1; + } + const auto& buffer = config.output; + const std::string header = pam ? std::format("P7\nWIDTH {}\nHEIGHT {}\nDEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n", + buffer.width, buffer.height) + : std::format("P6\n{} {}\n255\n", buffer.width, buffer.height); + const bool ok = write(out, header, {buffer.u.RGBA.rgba, buffer.u.RGBA.size}); + WebPFreeDecBuffer(&config.output); + return ok ? 0 : 1; +} + +int encode(const Image& image, const std::string& out, float quality, int method, bool lossless, bool exact) { + WebPConfig config; + if (!WebPConfigInit(&config)) return 1; + config.quality = quality; + config.method = method; + config.lossless = lossless; + config.exact = exact; + if (!WebPValidateConfig(&config)) { + std::println(std::cerr, "invalid settings"); + return 1; + } + WebPPicture picture; + if (!WebPPictureInit(&picture)) return 1; + picture.width = image.width; + picture.height = image.height; + // ARGB samples for lossless, YUVA for lossy: as upstream's cwebp chooses, + // so the same input and settings give cwebp's bytes. + picture.use_argb = lossless; + WebPMemoryWriter writer; + WebPMemoryWriterInit(&writer); + picture.writer = WebPMemoryWrite; + picture.custom_ptr = &writer; + const bool imported = image.alpha ? WebPPictureImportRGBA(&picture, image.rgba.data(), image.width * 4) + : WebPPictureImportRGBX(&picture, image.rgba.data(), image.width * 4); + const bool ok = imported && WebPEncode(&config, &picture); + if (!ok) std::println(std::cerr, "encoding failed (error {})", static_cast(picture.error_code)); + const bool written = ok && write(out, {}, {writer.mem, writer.size}); + WebPPictureFree(&picture); + WebPMemoryWriterClear(&writer); + return written ? 0 : 1; +} + +} // namespace + +int main(int argc, char** argv) { + const std::vector args(argv + 1, argv + argc); + if (args.size() < 2) { + std::println(std::cerr, "usage: webpcodec info | decode [-nofancy] | " + "encode [-q N] [-m N] [-lossless] [-exact]"); + return 2; + } + const auto file = read(args[1]); + if (!file) { + std::println(std::cerr, "cannot read {}", args[1]); + return 1; + } + if (args[0] == "info") return info(*file); + if (args.size() < 3) return 2; + if (args[0] == "decode") return decode(*file, args[2], std::ranges::find(args, "-nofancy") == args.end()); + if (args[0] == "encode") { + float quality = 75; + int method = 4; + bool lossless = false, exact = false; + for (std::size_t i = 3; i < args.size(); ++i) { + if (args[i] == "-q" && i + 1 < args.size()) quality = std::stof(args[++i]); + else if (args[i] == "-m" && i + 1 < args.size()) method = std::stoi(args[++i]); + else if (args[i] == "-lossless") lossless = true; + else if (args[i] == "-exact") exact = true; + } + const auto image = parse_pnm(*file); + if (!image) { + std::println(std::cerr, "{}: not a binary PPM or PAM with maxval 255", args[1]); + return 1; + } + return encode(*image, args[2], quality, method, lossless, exact); + } + return 2; +} diff --git a/tools/run_conformance.py b/tools/run_conformance.py new file mode 100644 index 0000000..21fe2e2 --- /dev/null +++ b/tools/run_conformance.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Compare examples/webpcodec, written against import mcpplibs.mwrap.libwebp, +with upstream's own cwebp and dwebp built from the same release. + +The release tarball is downloaded once into target/upstream and checked +against upstream/documents.json. Its cwebp and dwebp are built as upstream's +makefile.unix builds them (with the system C compiler, threads on, without the +optional PNG/JPEG/TIFF readers); webpcodec is built with mcpp over +compat.libwebp. Then, for each input (upstream's examples/test_ref.ppm, an +opaque picture and one with every kind of alpha, generated here) and each +setting (lossy at several qualities and methods, lossless at several levels, +lossless with -exact): + +- the two encoders must write the same bytes; +- the two decoders must write the same PPM or PAM from that file, with fancy + upsampling and without; + +and upstream's examples/test.webp must decode to the same pixels in both. +(Upstream's test_ref.ppm itself predates libwebp's current YUV to RGB +rounding: every libwebp release since decodes test.webp within 1 of it.) + +Needs `make` and a C compiler: CI runs it on Linux and macOS. +""" +import argparse +import os +from pathlib import Path +import shutil +import subprocess +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from inventory import fetch # noqa: E402 + +SETTINGS = [ + ['-q', '75', '-m', '4'], + ['-q', '30', '-m', '0'], + ['-q', '60', '-m', '2'], + ['-q', '95', '-m', '6'], + ['-lossless', '-q', '0', '-m', '0'], + ['-lossless', '-q', '75', '-m', '4'], + ['-lossless', '-q', '100', '-m', '6'], + ['-lossless', '-exact', '-q', '50', '-m', '2'], +] + + +def upstream_tools(upstream: Path, cc: str) -> tuple[Path, Path]: + build = upstream.parent / (upstream.name + '-build') + if not (build / 'examples/cwebp').exists() or not (build / 'examples/dwebp').exists(): + shutil.rmtree(build, ignore_errors=True) + shutil.copytree(upstream, build) + command = ['make', '-s', f'-j{os.cpu_count() or 2}', '-f', 'makefile.unix', f'CC={cc}', + 'EXTRA_FLAGS=-DWEBP_USE_THREAD', 'DWEBP_LIBS=', 'CWEBP_LIBS=', 'examples/cwebp', 'examples/dwebp'] + print('+', ' '.join(command), flush=True) + subprocess.run(command, cwd=build, check=True) + return build / 'examples/cwebp', build / 'examples/dwebp' + + +def webpcodec(example: Path, mcpp: str, toolchain: str | None, profile: str) -> Path: + command = [mcpp, 'build', '--profile', profile] + (['--toolchain', toolchain] if toolchain else []) + print('+', ' '.join(command), flush=True) + subprocess.run(command, cwd=example, check=True) + found = sorted((p for p in (example / 'target').rglob('webpcodec') if p.parent.name == 'bin' and p.is_file()), + key=lambda p: p.stat().st_mtime) + if not found: + sys.exit('examples/webpcodec was not built') + return found[-1] + + +def inputs(directory: Path, upstream: Path) -> list[Path]: + directory.mkdir(parents=True, exist_ok=True) + w, h = 160, 120 + opaque, alpha = bytearray(), bytearray() + state = 12345 + for y in range(h): + for x in range(w): + state = (state * 1664525 + 1013904223) & 0xffffffff + noise = state >> 28 + disc = (x - 100) ** 2 + (y - 50) ** 2 < 30 ** 2 + rgb = (230 if disc else (x * 2 + noise) & 255, 40 if disc else (y * 2 + noise) & 255, + ((x ^ y) & 63) + (128 if x > 64 else 0)) + opaque += bytes(rgb) + alpha += bytes(rgb) + bytes([0 if y < 16 else (y - 16) * 8 if y < 48 else 255]) + (directory / 'opaque.ppm').write_bytes(f'P6\n{w} {h}\n255\n'.encode() + opaque) + (directory / 'alpha.pam').write_bytes( + f'P7\nWIDTH {w}\nHEIGHT {h}\nDEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n'.encode() + alpha) + return [upstream / 'examples/test_ref.ppm', directory / 'opaque.ppm', directory / 'alpha.pam'] + + +def run(command: list) -> None: + result = subprocess.run([str(c) for c in command], capture_output=True, text=True) + if result.returncode != 0: + sys.exit(f'{" ".join(map(str, command))} failed:\n{result.stdout}{result.stderr}') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--mcpp', default='mcpp') + parser.add_argument('--toolchain') + parser.add_argument('--profile', default='release') + parser.add_argument('--cc', default=os.environ.get('CC', 'cc')) + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + upstream = fetch(root) + cwebp, dwebp = upstream_tools(upstream, args.cc) + ours = webpcodec(root / 'examples/webpcodec', args.mcpp, args.toolchain, args.profile) + work = root / 'target/conformance' + shutil.rmtree(work, ignore_errors=True) + failures, checks = [], 0 + + def compare(a: Path, b: Path, what: str) -> None: + nonlocal checks + checks += 1 + if a.read_bytes() != b.read_bytes(): + failures.append(what) + print('DIFFERENT', what, flush=True) + + def decoded(webp: Path, extension: str, label: str) -> None: + for flags in ([], ['-nofancy']): + theirs, mine = work / f'{label}.dwebp.{extension}', work / f'{label}.ours.{extension}' + run([dwebp, '-quiet', webp, *flags, f'-{extension}', '-o', theirs]) + run([ours, 'decode', webp, mine, *flags]) + compare(theirs, mine, f'decode {label} {" ".join(flags) or "(fancy)"}') + + for image in inputs(work, upstream): + extension = 'pam' if image.suffix == '.pam' else 'ppm' + for setting in SETTINGS: + label = f'{image.stem} {" ".join(setting)}' + theirs, mine = work / f'{label}.cwebp.webp', work / f'{label}.ours.webp' + run([cwebp, '-quiet', *setting, image, '-o', theirs]) + run([ours, 'encode', image, mine, *setting]) + compare(theirs, mine, f'encode {label}') + print(f'{label}: {theirs.stat().st_size} bytes', flush=True) + decoded(theirs, extension, label) + decoded(upstream / 'examples/test.webp', 'ppm', 'test.webp') + decoded(upstream / 'examples/test.webp', 'pam', 'test.webp') + if failures: + sys.exit(f'{len(failures)} of {checks} comparisons differ from upstream cwebp/dwebp') + print(f'{checks} comparisons: every encoded file and every decoded image is byte-identical to upstream ' + f'cwebp/dwebp 1.5.0 ({args.cc})') + + +if __name__ == '__main__': + main() From d494371d2d958af67df6e85b95f5b7585a92d268 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 29 Oct 2026 07:51:27 +0900 Subject: [PATCH 3/3] Default template (image) and an animation template, bilingual README, usage documentation, CI (Linux x86_64 GCC/LLVM, Linux arm64 GCC, macOS, Windows, the Web), release and template workflows --- .github/workflows/ci.yml | 152 ++++++++++++++++++++++++++++ .github/workflows/release.yml | 49 +++++++++ .github/workflows/templates.yml | 31 ++++++ README.md | 110 +++++++++++++++++++- README.zh.md | 97 ++++++++++++++++++ docs/usage.md | 94 +++++++++++++++++ templates/animation/mcpp.toml.in | 10 ++ templates/animation/src/main.cpp.in | 92 +++++++++++++++++ templates/animation/template.toml | 4 + templates/image/mcpp.toml.in | 10 ++ templates/image/src/main.cpp.in | 100 ++++++++++++++++++ templates/image/template.toml | 4 + tools/check_mwrap_release.py | 50 +++++++++ tools/test_template.py | 83 +++++++++++++++ 14 files changed, 884 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/templates.yml create mode 100644 README.zh.md create mode 100644 docs/usage.md create mode 100644 templates/animation/mcpp.toml.in create mode 100644 templates/animation/src/main.cpp.in create mode 100644 templates/animation/template.toml create mode 100644 templates/image/mcpp.toml.in create mode 100644 templates/image/src/main.cpp.in create mode 100644 templates/image/template.toml create mode 100644 tools/check_mwrap_release.py create mode 100644 tools/test_template.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8e21897 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,152 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + mcpp: + name: mcpp (${{ matrix.os }}, ${{ matrix.toolchain }}) + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-24.04, toolchain: 'gcc@16.1.0' } + - { os: ubuntu-24.04, toolchain: 'llvm@23.1.3' } + - { os: ubuntu-24.04-arm, toolchain: 'gcc@16.1.0' } + - { os: macos-15, toolchain: 'llvm@23.1.3' } + - { os: windows-2025, toolchain: 'llvm@23.1.3' } + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + defaults: + run: + shell: bash + env: + XLINGS_NON_INTERACTIVE: 1 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Mwrap identity and bilingual README contract + run: python3 tools/check_mwrap_release.py + + - name: Install Xlings (Linux/macOS) + if: runner.os != 'Windows' + run: | + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/refs/heads/main/tools/other/quick_install.sh | bash + echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + + - name: Install Xlings (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + irm https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 | iex + "$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Install project tools (.xlings.json) + run: | + for attempt in 1 2 3; do + xlings update > /dev/null 2>&1 || true + if xlings install; then break; fi + [ "$attempt" = 3 ] && exit 1 + sleep 30 + done + mcpp --version + + - name: Select the toolchain + run: | + name="${{ matrix.toolchain }}" + mcpp toolchain install "${name%@*}" "${name#*@}" + mcpp toolchain default "$name" + + - name: Build the modules (default, and with the demux and mux features) + run: | + mcpp build + mcpp build --features demux,mux + + - name: Tests (the decode and encode API, sharpyuv, the macros, header mixing, pinned bytes) + run: mcpp test + + - name: Tests, release profile + run: mcpp test --profile release + + - name: Animation tests (the demux and mux features) + working-directory: tests/anim + run: | + mcpp test + mcpp test --profile release + + - name: Export audit (the generated modules are fresh; every declaration and macro of the public headers) + run: python3 tools/check_exports.py + + # Upstream's cwebp and dwebp from the same release, built by its own + # makefile.unix with the system C compiler: needs make (not on Windows). + - name: Conformance (examples/webpcodec byte-identical to upstream cwebp and dwebp) + if: runner.os != 'Windows' + run: python3 tools/run_conformance.py + + - name: The example builds (Windows) + if: runner.os == 'Windows' + working-directory: examples/webpcodec + run: mcpp build + + # The Web: wasm32-emscripten (emsdk from xlings, through mcpp), the tests + # under node. + web: + name: mcpp (web, wasm32-emscripten) + runs-on: ubuntu-24.04 + timeout-minutes: 60 + defaults: + run: + shell: bash + env: + XLINGS_NON_INTERACTIVE: 1 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Xlings + run: | + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/refs/heads/main/tools/other/quick_install.sh | bash + echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + + - name: Install project tools (.xlings.json) + run: | + for attempt in 1 2 3; do + xlings update > /dev/null 2>&1 || true + if xlings install; then break; fi + [ "$attempt" = 3 ] && exit 1 + sleep 30 + done + mcpp --version + + - name: Install emsdk + run: mcpp toolchain install emsdk 6.0.9 + + - name: Tests under node + run: mcpp test --toolchain emsdk@6.0.9 --target wasm32-emscripten + + - name: Tests under node, release profile + run: mcpp test --toolchain emsdk@6.0.9 --target wasm32-emscripten --profile release + + - name: Animation tests under node (the demux and mux features) + working-directory: tests/anim + run: | + mcpp test --toolchain emsdk@6.0.9 --target wasm32-emscripten + mcpp test --toolchain emsdk@6.0.9 --target wasm32-emscripten --profile release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f0c6447 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,49 @@ +name: Release +on: + push: + tags: ["mwrap-*"] +permissions: + contents: read +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + actions: read + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Check tag, upstream version and bilingual entry pages + run: python3 tools/check_mwrap_release.py --tag "$GITHUB_REF_NAME" + - name: Require successful source CI at this exact commit + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + result=$(gh run list --repo "$GITHUB_REPOSITORY" --workflow ci.yml --commit "$GITHUB_SHA" --limit 1 --json status,conclusion --jq '.[0] | .status == "completed" and .conclusion == "success"') + test "$result" = true || { echo 'No successful source CI at the tag commit'; exit 1; } + - name: Build immutable source asset and publish + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + version="${GITHUB_REF_NAME#mwrap-}" + repo="${GITHUB_REPOSITORY#*/}" + asset="${repo}-${version}.tar.gz" + git archive --format=tar.gz --prefix="${repo}-${version}/" "$GITHUB_SHA" > "$RUNNER_TEMP/$asset" + (cd "$RUNNER_TEMP" && sha256sum "$asset" > "$asset.sha256") + if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo 'Release already exists; refusing to change immutable assets'; exit 1 + fi + extra=() + if [[ "$version" == *-rc* ]]; then extra+=(--prerelease); fi + gh release create "$GITHUB_REF_NAME" "$RUNNER_TEMP/$asset" "$RUNNER_TEMP/$asset.sha256" \ + --repo "$GITHUB_REPOSITORY" --verify-tag --title "$repo $version" \ + --notes "Module wrapper for upstream $version. New mwrap package identities become installable after mcpp-index publication; see the bilingual README for the tested support scope." "${extra[@]}" diff --git a/.github/workflows/templates.yml b/.github/workflows/templates.yml new file mode 100644 index 0000000..a453a27 --- /dev/null +++ b/.github/workflows/templates.yml @@ -0,0 +1,31 @@ +name: Actual project templates +on: + push: + branches: [main] + pull_request: + workflow_dispatch: +permissions: + contents: read +concurrency: + group: templates-${{ github.ref }} + cancel-in-progress: true +env: + MCPP_VERSION: "2026.10.27.1" +jobs: + templates: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - name: Install the pinned mcpp + shell: bash + run: | + archive="mcpp-${MCPP_VERSION}-linux-x86_64.tar.gz" + curl -L -fsS -o "$archive" "https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_VERSION}/$archive" + tar -xzf "$archive" + echo "$PWD/mcpp-${MCPP_VERSION}-linux-x86_64/bin" >> "$GITHUB_PATH" + - name: Actual scaffold, strict build and run of the default template (image) + run: python3 tools/test_template.py --toolchain gcc@16.1.0 + + - name: Actual scaffold, strict build and run of the animation template (the demux and mux features) + run: python3 tools/test_template.py --toolchain gcc@16.1.0 --template animation diff --git a/README.md b/README.md index a2c2346..59bf348 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,109 @@ -# libwebp-m +# libwebp -libwebp as C++23 modules for mcpp (`import mcpplibs.mwrap.libwebp;`). Work in progress. +> libwebp, the WebP image codec, through C++23 modules. + +**English** · [简体中文](README.zh.md) · [Upstream](https://github.com/webmproject/libwebp) · [Releases](https://github.com/mcpplibs/libwebp-m/releases) · [mcpp](https://github.com/mcpp-community/mcpp) · [Index](https://github.com/mcpplibs/mcpp-index) + +[![C++23](https://img.shields.io/badge/C%2B%2B-23-blue.svg)](https://en.cppreference.com/w/cpp/23) +[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) +[![libwebp](https://img.shields.io/badge/libwebp-BSD--3--Clause-blue.svg)](upstream/libwebp/COPYING) +[![mcpp-community](https://img.shields.io/badge/mcpp--community-mwrap-blue)](https://github.com/mcpp-community) +[![CI](https://github.com/mcpplibs/libwebp-m/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mcpplibs/libwebp-m/actions/workflows/ci.yml) + +libwebp 1.5.0 decodes and encodes WebP: lossy (VP8) and lossless (VP8L), +with alpha, still and animated. The upstream C API is unchanged: +`WebPGetInfo`, `WebPDecodeRGBA`, `WebPEncodeRGBA`, `WebPEncodeLosslessRGBA`, +`WebPEncode` with `WebPConfig` and `WebPPicture`, `WebPAnimEncoder` and +`WebPAnimDecoder` are the upstream globals of the headers. The codec is +`compat.libwebp` 1.5.0. The headers' `static inline` functions +(`WebPConfigInit`, `WebPPictureInit`, `WebPGetFeatures` …) are adapted under +the same names, and the macros are constants. On every CI run the bytes +written and the pixels read are those of upstream's own `cwebp` and `dwebp`, +and pinned hashes hold on every target, the Web included. + +## Modules + +- `import mcpplibs.mwrap.libwebp;`: decoding (`webp/decode.h`), encoding (`webp/encode.h`), `WebPData` and the animation enums (`webp/mux_types.h`), re-exporting the two below +- `import mcpplibs.mwrap.libwebp.sharpyuv;`: libsharpyuv, the sharp RGB to YUV conversion (`SharpYuvConvert` …) +- `import mcpplibs.mwrap.libwebp.macros;`: `WEBP_MAX_DIMENSION`, the ABI versions, `WEBP_ABI_IS_INCOMPATIBLE` … in `mcpplibs::mwrap::libwebp::macros` +- `import mcpplibs.mwrap.libwebp.demux;`: `WebPDemux`, `WebPAnimDecoder`, with the `demux` feature +- `import mcpplibs.mwrap.libwebp.mux;`: `WebPMux`, `WebPAnimEncoder`, with the `mux` feature + +> More Modules API -> [Usage and API](docs/usage.md) + +## Getting started + +```sh +mcpp new pictures --template mcpplibs.mwrap.libwebp@1.5.0 +cd pictures +mcpp run +``` + +The default template encodes a picture with alpha lossy at three qualities +and lossless, decodes each back to RGBA and prints its size and PSNR. The +`animation` template (`--template mcpplibs.mwrap.libwebp@1.5.0:animation`) +encodes frames with `WebPAnimEncoder` and plays them back with +`WebPAnimDecoder`. + +```toml +[dependencies] +"mcpplibs.mwrap.libwebp" = "1.5.0" +``` + +```cpp +import std; +import mcpplibs.mwrap.libwebp; + +int main() { + std::vector rgba(64 * 64 * 4, 200); // a light grey, opaque square + std::uint8_t* webp = nullptr; + const std::size_t size = WebPEncodeRGBA(rgba.data(), 64, 64, 64 * 4, 80.f, &webp); // lossy, quality 80 + int width = 0, height = 0; + std::uint8_t* pixels = WebPDecodeRGBA(webp, size, &width, &height); + std::println("{} bytes, {} x {}", size, width, height); + WebPFree(pixels); + WebPFree(webp); +} +``` + +| Feature | Effect | +|---|---| +| `demux` | libwebpdemux (compat.libwebp's `src/demux`) and the `demux` module: `WebPDemux*`, `WebPAnimDecoder*` | +| `mux` | libwebpmux (compat.libwebp's `src/mux`) and the `mux` module: `WebPMux*`, `WebPAnimEncoder*` | + +Neither is on by default. Animated WebP needs `demux` to read and `mux` to +write. + +## The codec + +`compat.libwebp` builds the release's `src/dec`, `src/enc`, `src/dsp`, +`src/utils` and `sharpyuv` (and `src/demux`, `src/mux` with the features) +with SSE2 on x86-64 and NEON on arm64, SSE4.1 off, and without +`WEBP_USE_THREAD`: `thread_level` and `use_threads` are accepted and run on +the calling thread. Its output is deterministic: the same picture and +settings give the same bytes on Linux, macOS, Windows and wasm32-emscripten, +with GCC or LLVM, dev or release, and the same bytes as upstream's `cwebp`. +Details and limits: [docs/usage.md](docs/usage.md). + +## Examples + +- [examples/webpcodec](examples/webpcodec): decodes WebP to PPM/PAM, encodes PPM/PAM to WebP (lossy or lossless) and + prints a file's features, written against the module; [tools/run_conformance.py](tools/run_conformance.py) compares + it with upstream's `cwebp` and `dwebp`. + +## Build and testing + +Linux (x86_64 GCC and LLVM, arm64 GCC), macOS, Windows and wasm32-emscripten +(under node) CI build the modules and run the tests in the dev and release +profiles, with and without the features; the native rows also run the export +audit ([tools/check_exports.py](tools/check_exports.py): the generated modules +are fresh, every declaration exported, every macro mapped or excluded with its +reason) and, but on Windows, the conformance run against upstream's tools. A +separate job scaffolds both templates as an installed package and runs them. + +## License + +[Apache-2.0](LICENSE) for the wrapper, by mcpp-community; libwebp remains +BSD-3-Clause with Google's patent grant +([upstream/libwebp/COPYING](upstream/libwebp/COPYING), +[upstream/libwebp/PATENTS](upstream/libwebp/PATENTS)). diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 0000000..59d90b4 --- /dev/null +++ b/README.zh.md @@ -0,0 +1,97 @@ +# libwebp + +> 通过 C++23 模块使用 WebP 图像编解码库 libwebp。 + +[English](README.md) · **简体中文** · [Upstream](https://github.com/webmproject/libwebp) · [Releases](https://github.com/mcpplibs/libwebp-m/releases) · [mcpp](https://github.com/mcpp-community/mcpp) · [Index](https://github.com/mcpplibs/mcpp-index) + +[![C++23](https://img.shields.io/badge/C%2B%2B-23-blue.svg)](https://en.cppreference.com/w/cpp/23) +[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) +[![libwebp](https://img.shields.io/badge/libwebp-BSD--3--Clause-blue.svg)](upstream/libwebp/COPYING) +[![mcpp-community](https://img.shields.io/badge/mcpp--community-mwrap-blue)](https://github.com/mcpp-community) +[![CI](https://github.com/mcpplibs/libwebp-m/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mcpplibs/libwebp-m/actions/workflows/ci.yml) + +libwebp 1.5.0 解码和编码 WebP:有损(VP8)与无损(VP8L),支持 alpha, +静态图与动画。上游 C API 保持不变:`WebPGetInfo`、`WebPDecodeRGBA`、 +`WebPEncodeRGBA`、`WebPEncodeLosslessRGBA`、配合 `WebPConfig` 与 +`WebPPicture` 的 `WebPEncode`、`WebPAnimEncoder` 和 `WebPAnimDecoder` +都是头文件中的上游全局名。编解码器由 `compat.libwebp` 1.5.0 提供。头文件中的 +`static inline` 函数(`WebPConfigInit`、`WebPPictureInit`、`WebPGetFeatures` …) +以同名同签名适配,宏映射为常量。每次 CI 都验证:写出的字节与读出的像素和上游自己的 +`cwebp`、`dwebp` 完全一致,固定的哈希在所有目标上(包括 Web)都成立。 + +## 模块 + +- `import mcpplibs.mwrap.libwebp;`:解码(`webp/decode.h`)、编码(`webp/encode.h`)、`WebPData` 与动画枚举(`webp/mux_types.h`),并重新导出下面两个模块 +- `import mcpplibs.mwrap.libwebp.sharpyuv;`:libsharpyuv,锐利的 RGB 到 YUV 转换(`SharpYuvConvert` …) +- `import mcpplibs.mwrap.libwebp.macros;`:`WEBP_MAX_DIMENSION`、各 ABI 版本、`WEBP_ABI_IS_INCOMPATIBLE` …,位于 `mcpplibs::mwrap::libwebp::macros` +- `import mcpplibs.mwrap.libwebp.demux;`:`WebPDemux`、`WebPAnimDecoder`,需要 `demux` 特性 +- `import mcpplibs.mwrap.libwebp.mux;`:`WebPMux`、`WebPAnimEncoder`,需要 `mux` 特性 + +> 更多模块 API -> [用法与 API](docs/usage.md) + +## 快速开始 + +```sh +mcpp new pictures --template mcpplibs.mwrap.libwebp@1.5.0 +cd pictures +mcpp run +``` + +默认模板把一张带 alpha 的图片以三种质量有损编码、再无损编码,逐一解码回 RGBA, +打印大小与 PSNR。`animation` 模板(`--template mcpplibs.mwrap.libwebp@1.5.0:animation`) +用 `WebPAnimEncoder` 编码多帧,再用 `WebPAnimDecoder` 回放。 + +```toml +[dependencies] +"mcpplibs.mwrap.libwebp" = "1.5.0" +``` + +```cpp +import std; +import mcpplibs.mwrap.libwebp; + +int main() { + std::vector rgba(64 * 64 * 4, 200); // 浅灰色、不透明的方块 + std::uint8_t* webp = nullptr; + const std::size_t size = WebPEncodeRGBA(rgba.data(), 64, 64, 64 * 4, 80.f, &webp); // 有损,质量 80 + int width = 0, height = 0; + std::uint8_t* pixels = WebPDecodeRGBA(webp, size, &width, &height); + std::println("{} bytes, {} x {}", size, width, height); + WebPFree(pixels); + WebPFree(webp); +} +``` + +| 特性 | 作用 | +|---|---| +| `demux` | libwebpdemux(compat.libwebp 的 `src/demux`)与 `demux` 模块:`WebPDemux*`、`WebPAnimDecoder*` | +| `mux` | libwebpmux(compat.libwebp 的 `src/mux`)与 `mux` 模块:`WebPMux*`、`WebPAnimEncoder*` | + +两者默认都不开启。读取动画 WebP 需要 `demux`,写出需要 `mux`。 + +## 编解码器 + +`compat.libwebp` 构建该版本的 `src/dec`、`src/enc`、`src/dsp`、`src/utils` 与 +`sharpyuv`(开启特性时还有 `src/demux`、`src/mux`):x86-64 上使用 SSE2,arm64 上使用 +NEON,关闭 SSE4.1,且未定义 `WEBP_USE_THREAD`:`thread_level` 与 `use_threads` +可以设置,但在调用线程上执行。输出是确定的:同一图片与设置在 Linux、macOS、Windows +和 wasm32-emscripten 上,用 GCC 或 LLVM、dev 或 release,都得到相同的字节,并与上游 +`cwebp` 的输出一致。细节与限制见 [docs/usage.md](docs/usage.md)。 + +## 示例 + +- [examples/webpcodec](examples/webpcodec):把 WebP 解码为 PPM/PAM,把 PPM/PAM 编码为 WebP(有损或无损), + 打印文件特征,基于本模块编写;[tools/run_conformance.py](tools/run_conformance.py) 将它与上游的 + `cwebp`、`dwebp` 对比。 + +## 构建与测试 + +Linux(x86_64 GCC 与 LLVM、arm64 GCC)、macOS、Windows 和 wasm32-emscripten(node 下运行) +的 CI 构建模块,在 dev 与 release 配置下运行测试,包括开启与不开启特性;原生平台还运行导出审计 +([tools/check_exports.py](tools/check_exports.py):生成的模块是最新的,每个声明都已导出,每个宏都已映射 +或附理由排除),除 Windows 外还运行与上游工具的一致性对比。另有一个任务把两个模板当作已安装的包生成并运行。 + +## 许可证 + +包装层采用 [Apache-2.0](LICENSE),作者 mcpp-community;libwebp 仍为 BSD-3-Clause,并附 Google 的专利授权 +([upstream/libwebp/COPYING](upstream/libwebp/COPYING)、[upstream/libwebp/PATENTS](upstream/libwebp/PATENTS))。 diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..d5030ec --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,94 @@ +# Using mcpplibs.mwrap.libwebp + +`import mcpplibs.mwrap.libwebp;` declares libwebp 1.5.0's API as its headers do. Types, enumerators and functions are the upstream globals, the very same entities as the headers', so a translation unit may include a header too. The codec is `compat.libwebp` 1.5.0, built from the same release. + +## Modules + +| Module | What it holds | +|---|---| +| `mcpplibs.mwrap.libwebp` | `webp/types.h`, `webp/decode.h`, `webp/encode.h`, `webp/mux_types.h`: 23 types (`WebPConfig`, `WebPPicture`, `WebPDecoderConfig`, `WebPBitstreamFeatures`, `WebPMemoryWriter`, `WebPData` …), 59 enumerators (`MODE_RGBA`, `VP8_STATUS_OK`, `WEBP_PRESET_PHOTO`, `VP8_ENC_ERROR_BAD_DIMENSION`, `ALPHA_FLAG` …) and 72 functions (`WebPGetInfo`, `WebPDecodeRGBA`, `WebPEncodeRGBA`, `WebPEncodeLosslessRGBA`, `WebPEncode`, `WebPPictureImportRGBA`, `WebPINewDecoder`, `WebPFree` …); 13 static inline functions adapted; re-exports the two below | +| `mcpplibs.mwrap.libwebp.sharpyuv` | `sharpyuv/sharpyuv.h`, `sharpyuv/sharpyuv_csp.h`: 6 types, 25 enumerators, 6 functions; `SharpYuvOptionsInit` adapted | +| `mcpplibs.mwrap.libwebp.macros` | the macros, in `mcpplibs::mwrap::libwebp::macros` | +| `mcpplibs.mwrap.libwebp.demux` | `webp/demux.h`, with the `demux` feature: 8 types, 10 enumerators, 20 functions (`WebPDemuxGetFrame`, `WebPAnimDecoderGetNext` …); `WebPDemux`, `WebPDemuxPartial`, `WebPAnimDecoderOptionsInit`, `WebPAnimDecoderNew` adapted; re-exports the root | +| `mcpplibs.mwrap.libwebp.mux` | `webp/mux.h`, with the `mux` feature: 7 types, 17 enumerators, 27 functions (`WebPMuxSetChunk`, `WebPAnimEncoderAdd` …); `WebPMuxNew`, `WebPMuxCreate`, `WebPAnimEncoderOptionsInit`, `WebPAnimEncoderNew` adapted; re-exports the root | + +The root cannot re-export `demux` and `mux` (mcpp's scanner admits no import under a feature test): import them by name. `webp/format_constants.h` is not public upstream (it is not installed) and is not exported. + +## The static inline API + +Twenty-two functions of the headers are `static inline`: the initialisers that pass the header's ABI version to an `*Internal` function (`WebPConfigInit`, `WebPConfigPreset`, `WebPPictureInit`, `WebPInitDecoderConfig`, `WebPInitDecBuffer`, `WebPGetFeatures`, `WebPAnimEncoderOptionsInit`, `WebPAnimEncoderNew`, `WebPAnimDecoderOptionsInit`, `WebPAnimDecoderNew`, `WebPDemux`, `WebPDemuxPartial`, `WebPMuxNew`, `WebPMuxCreate`, `SharpYuvOptionsInit`), the colourspace tests (`WebPIsAlphaMode`, `WebPIsPremultipliedMode`, `WebPIsRGBMode`), `WebPIDecGetYUV` and the `WebPData` helpers (`WebPDataInit`, `WebPDataClear`, `WebPDataCopy`). + +Internal linkage cannot be exported. Each is adapted as a function with the same name and signature (and `[[nodiscard]]` where the header has `WEBP_NODISCARD`) in `mcpplibs::mwrap::libwebp`, defined out of line by calling upstream's own inline body, so it passes the same ABI version and gives the same result. `using namespace mcpplibs::mwrap::libwebp;` makes code read as it does in C. A translation unit that also includes the header sees both: call the header's as `::WebPConfigInit`. The `*Internal` functions are exported too. + +## Decoding and encoding, in brief + +```cpp +import std; +import mcpplibs.mwrap.libwebp; +using namespace mcpplibs::mwrap::libwebp; // WebPConfigInit, WebPPictureInit, WebPGetFeatures … + +// RGBA (width * height * 4 bytes) to WebP: lossy at `quality` (0 … 100), or lossless. +std::vector encode(std::span rgba, int width, int height, bool lossless, float quality) { + WebPConfig config; + WebPPicture picture; + if (!WebPConfigInit(&config) || !WebPPictureInit(&picture)) return {}; + config.lossless = lossless; + config.quality = quality; // lossy: visual quality; lossless: effort + config.method = 4; // 0 (fast) … 6 (smallest) + config.exact = lossless; // keep the colour under alpha 0 too + picture.use_argb = 1; + picture.width = width; + picture.height = height; + WebPMemoryWriter writer; + WebPMemoryWriterInit(&writer); + picture.writer = WebPMemoryWrite; + picture.custom_ptr = &writer; + std::vector out; + if (WebPPictureImportRGBA(&picture, rgba.data(), width * 4) && WebPEncode(&config, &picture)) + out.assign(writer.mem, writer.mem + writer.size); // else picture.error_code says why + WebPPictureFree(&picture); + WebPMemoryWriterClear(&writer); + return out; +} + +// WebP to RGBA. +std::vector decode(std::span webp, int& width, int& height) { + std::uint8_t* pixels = WebPDecodeRGBA(webp.data(), webp.size(), &width, &height); + if (!pixels) return {}; + std::vector out(pixels, pixels + std::size_t(width) * height * 4); + WebPFree(pixels); + return out; +} +``` + +- **Inspecting.** `WebPGetInfo` gives the size from the first bytes; `WebPGetFeatures` adds `has_alpha`, `has_animation` and `format` (1 lossy, 2 lossless, 0 mixed or animated). An animated file decodes with `WebPAnimDecoder`, not `WebPDecode`. +- **The simple API.** `WebPDecodeRGBA` / `BGRA` / `ARGB` / `RGB` / `BGR` / `YUV` allocate the result (free it with `WebPFree`); the `…Into` forms write into the caller's buffer with its stride. `WebPEncodeRGBA` (lossy at a quality) and `WebPEncodeLosslessRGBA` allocate the file (free it with `WebPFree`) and answer its size, 0 on failure. They use the default settings: `exact` is off, so the colour under fully transparent pixels may change (alpha itself is kept). +- **The advanced API.** `WebPConfig` holds every setting (`quality`, `method`, `lossless`, `exact`, `alpha_quality`, `near_lossless`, `use_sharp_yuv`, `target_size` …); `WebPConfigPreset` and `WebPConfigLosslessPreset(config, 0 … 9)` start from upstream's presets, `WebPValidateConfig` checks one. `WebPPicture` takes RGBA, BGRA, RGB … (`WebPPictureImport*`), can be cropped, rescaled, copied and compared (`WebPPictureDistortion`); `stats` and `progress_hook` report on an encoding, and a hook answering 0 aborts it (`VP8_ENC_ERROR_USER_ABORT`). `WebPDecoderConfig` decodes with scaling, cropping, flipping, without fancy upsampling, into any `WEBP_CSP_MODE` (premultiplied `MODE_rgbA` …) or into the caller's memory. +- **Incremental decoding.** `WebPINewDecoder` / `WebPINewRGB` / `WebPINewYUVA`, then `WebPIAppend` (or `WebPIUpdate`) as data arrives: `VP8_STATUS_SUSPENDED` until the image is complete; `WebPIDecGetRGB` / `WebPIDecGetYUVA` give the rows decoded so far. +- **Animation** (both features). `WebPAnimEncoderNew(width, height, options)`, then `WebPAnimEncoderAdd(encoder, picture, timestamp_ms, config)` per frame and once with a null picture at the end time, `WebPAnimEncoderAssemble` into a `WebPData` (free with `WebPDataClear`). `WebPAnimDecoderNew(data, options)` with `color_mode` `MODE_RGBA`, `MODE_BGRA`, `MODE_rgbA` or `MODE_bgrA`; `WebPAnimDecoderGetNext` gives the whole canvas and the frame's end time, owned by the decoder until the next call. `WebPAnimDecoderGetInfo` has the canvas, frame and loop counts. +- **The container.** `WebPDemux` reads frames (`WebPDemuxGetFrame`), chunks (`WebPDemuxGetChunk(demux, "ICCP", 1, &it)`, `"EXIF"`, `"XMP "`) and features (`WebPDemuxGetI`); `WebPMux` adds or removes chunks and frames and `WebPMuxAssemble` writes the file. +- **Errors** never abort: functions answer 0, `nullptr`, a `VP8StatusCode`, a `WebPEncodingError` in `picture.error_code`, a `WebPMuxError`, or a message (`WebPAnimEncoderGetError`). + +## Macros + +A named module cannot export a macro. `mcpplibs::mwrap::libwebp::macros` holds them under their original names: + +- **Constants** as `inline constexpr int`: `WEBP_DECODER_ABI_VERSION` (0x0209), `WEBP_ENCODER_ABI_VERSION` (0x0210), `WEBP_DEMUX_ABI_VERSION` (0x0107), `WEBP_MUX_ABI_VERSION` (0x0109), `WEBP_MAX_DIMENSION` (16383), `SHARPYUV_VERSION_MAJOR` / `_MINOR` / `_PATCH`, `SHARPYUV_VERSION`. +- **Functions**: `WEBP_ABI_IS_INCOMPATIBLE(a, b)` and `SHARPYUV_MAKE_VERSION(major, minor, patch)`, constexpr. + +No macro has to stay a macro, so there is no side header. [tools/check_exports.py](../tools/check_exports.py) accounts for all 24 macros of the public headers: 11 mapped, and excluded with their reasons the declaration annotations (`WEBP_EXTERN`, `WEBP_INLINE`, `WEBP_NODISCARD`, `SHARPYUV_EXTERN`, `SHARPYUV_INLINE`) and 8 include guards. While a header's macro is defined it expands even inside a qualified name, so with `` included, `macros::WEBP_MAX_DIMENSION` is reachable only with the macro set aside (see [tests/header_mix_test.cpp](../tests/header_mix_test.cpp)). + +## The codec as built + +compat.libwebp compiles every source of `src/dec`, `src/enc`, `src/dsp`, `src/utils` and `sharpyuv` into one archive (`src/demux` and `src/mux` join it with the features), with a generated `config.h`: + +- **SIMD**: SSE2 on x86-64 and NEON on arm64, the baselines of their architectures, chosen at run time by libwebp's own CPU dispatch; SSE4.1 is off (its sources would need a per-file target flag), as are MIPS/MSA. wasm32-emscripten builds the portable C. +- **Threads**: `WEBP_USE_THREAD` is not defined. `WebPConfig::thread_level` and `WebPDecoderOptions::use_threads` are accepted and the work runs on the calling thread, with the same result. Without it, libwebp's one-time DSP initialisation is not guarded by a mutex: a program that starts encoding or decoding on several threads at once should first encode or decode once (any image) on one thread. +- **Determinism**: the same picture and settings give the same bytes everywhere: checked, not assumed. libwebp's SSE2 and NEON paths give the results of its C paths, and the few `pow` calls of the encoder (quality to quantizer, gamma tables) are rounded to integers. [tests/determinism_test.cpp](../tests/determinism_test.cpp) pins the FNV-1a hashes of lossy files (quality 50/75/90, methods 0/4/6), lossless files (levels 0/6/9) and decoded pixels (with and without fancy upsampling), and [tests/anim](../tests/anim) those of lossless and lossy animations; CI checks them on Linux x86_64 (GCC, LLVM) and arm64, macOS arm64, Windows x86_64 and wasm32-emscripten, in the dev and release profiles. [tools/run_conformance.py](../tools/run_conformance.py) checks that the bytes are those of upstream's `cwebp` and `dwebp` built by upstream's own makefile. +- **Not built**: upstream's tools and their image readers (`examples/`, `imageio/`, `extras/`), GIF conversion, the SWIG and JavaScript bindings. + +## Checks + +- `mcpp test`: the simple and advanced decoding and encoding API through the module (lossy at several qualities and lossless, with alpha; presets, validation, statistics, progress and abort, distortion, crop and rescale; scaled, flipped, premultiplied, YUVA and caller-memory decoding; incremental decoding; errors on garbage, truncation and oversized pictures), the adapted static inline API, `WebPData`, sharpyuv, the constants, the headers mixed with the import, and the pinned hashes. `tests/anim` (its own project, with both features): animations encoded and decoded frame by frame, every colour mode, the demuxer, chunks and frames through the muxer, the error paths, pinned hashes. CI runs both on Linux, macOS, Windows and under node, in the dev and release profiles. +- `python3 tools/check_exports.py`: the modules are what [tools/gen_exports.py](../tools/gen_exports.py) writes from the pinned headers; every declaration is exported by exactly one module, every static inline function adapted, every macro mapped or excluded with its reason. +- `python3 tools/run_conformance.py`: [examples/webpcodec](../examples/webpcodec) against upstream's `cwebp` and `dwebp` 1.5.0, built from the release by its `makefile.unix`: 76 comparisons (three inputs, eight lossy and lossless settings, each file decoded with and without fancy upsampling, and upstream's `examples/test.webp`), all byte-identical. Upstream's `examples/test_ref.ppm` predates libwebp's current YUV to RGB rounding: `dwebp` 1.5.0 itself decodes `test.webp` within 1 of it, and so does the module. diff --git a/templates/animation/mcpp.toml.in b/templates/animation/mcpp.toml.in new file mode 100644 index 0000000..7b12f44 --- /dev/null +++ b/templates/animation/mcpp.toml.in @@ -0,0 +1,10 @@ +[package] +name = "{{project.name}}" +version = "0.1.0" + +[targets.{{project.name}}] +kind = "bin" +main = "src/main.cpp" + +[dependencies] +"{{self.name}}" = { version = "{{self.version}}", features = ["demux", "mux"] } diff --git a/templates/animation/src/main.cpp.in b/templates/animation/src/main.cpp.in new file mode 100644 index 0000000..e4344e1 --- /dev/null +++ b/templates/animation/src/main.cpp.in @@ -0,0 +1,92 @@ +// {{project.name}} — animated WebP through mcpplibs.mwrap.libwebp's demux and +// mux features: a ball bouncing across 12 frames, encoded with +// WebPAnimEncoder (lossless, looping forever), then played back with +// WebPAnimDecoder, every frame checked against the one drawn. Give a file +// name to also write the animation. +import std; +import mcpplibs.mwrap.libwebp.demux; // WebPAnimDecoder; re-exports mcpplibs.mwrap.libwebp +import mcpplibs.mwrap.libwebp.mux; // WebPAnimEncoder + +using namespace mcpplibs::mwrap::libwebp; // the headers' static inline functions + +namespace { + +constexpr int W = 120, H = 90, Frames = 12, FrameMs = 80; + +std::vector draw(int f) { + std::vector rgba(static_cast(W) * H * 4); + const int cx = 15 + f * 8, cy = 70 - static_cast(50 * std::abs(std::sin(f * 3.14159265 / 6))); + for (int y = 0; y < H; ++y) + for (int x = 0; x < W; ++x) { + auto* p = &rgba[(static_cast(y) * W + x) * 4]; + const bool ball = (x - cx) * (x - cx) + (y - cy) * (y - cy) < 12 * 12; + p[0] = static_cast(ball ? 250 : 20); + p[1] = static_cast(ball ? 90 : 30 + y); + p[2] = static_cast(ball ? 40 : 60 + y); + p[3] = 255; + } + return rgba; +} + +} // namespace + +int main(int argc, char** argv) { + // Encode: one WebPPicture per frame, at its timestamp; the encoder finds + // what changed between frames. + WebPAnimEncoderOptions options; + WebPConfig config; + WebPPicture pic; + if (!WebPAnimEncoderOptionsInit(&options) || !WebPConfigInit(&config) || !WebPPictureInit(&pic)) return 1; + options.anim_params.loop_count = 0; // forever + config.lossless = 1; + WebPAnimEncoder* encoder = WebPAnimEncoderNew(W, H, &options); + if (!encoder) return 1; + pic.use_argb = 1; + pic.width = W; + pic.height = H; + for (int f = 0; f < Frames; ++f) { + const auto rgba = draw(f); + if (!WebPPictureImportRGBA(&pic, rgba.data(), W * 4) || !WebPAnimEncoderAdd(encoder, &pic, f * FrameMs, &config)) { + std::println(std::cerr, "frame {}: {}", f, WebPAnimEncoderGetError(encoder)); + return 1; + } + } + WebPPictureFree(&pic); + WebPData webp; + WebPDataInit(&webp); + if (!WebPAnimEncoderAdd(encoder, nullptr, Frames * FrameMs, nullptr) || !WebPAnimEncoderAssemble(encoder, &webp)) { + std::println(std::cerr, "assembly: {}", WebPAnimEncoderGetError(encoder)); + return 1; + } + WebPAnimEncoderDelete(encoder); + std::println("encoded {} frames of {} x {} into {} bytes ({} raw)", Frames, W, H, webp.size, + Frames * W * H * 4); + + // Decode: every call gives the whole canvas at the frame's end time. + WebPAnimDecoderOptions decoding; + if (!WebPAnimDecoderOptionsInit(&decoding)) return 1; + decoding.color_mode = MODE_RGBA; + WebPAnimDecoder* decoder = WebPAnimDecoderNew(&webp, &decoding); + WebPAnimInfo info; + if (!decoder || !WebPAnimDecoderGetInfo(decoder, &info)) return 1; + std::println("{} x {}, {} frames, loop count {}", info.canvas_width, info.canvas_height, info.frame_count, + info.loop_count); + int f = 0, failures = 0; + while (WebPAnimDecoderHasMoreFrames(decoder)) { + std::uint8_t* canvas = nullptr; + int timestamp = 0; + if (!WebPAnimDecoderGetNext(decoder, &canvas, ×tamp)) return 1; + const auto expected = draw(f); + const bool same = std::equal(expected.begin(), expected.end(), canvas); + failures += !same; + std::println("frame {:>2} until {:>4} ms {}", f++, timestamp, same ? "exact" : "DIFFERENT"); + } + WebPAnimDecoderDelete(decoder); + if (argc > 1) { + std::ofstream(argv[1], std::ios::binary).write(reinterpret_cast(webp.bytes), + static_cast(webp.size)); + std::println("wrote {}", argv[1]); + } + WebPDataClear(&webp); + return failures; +} diff --git a/templates/animation/template.toml b/templates/animation/template.toml new file mode 100644 index 0000000..9b59ce9 --- /dev/null +++ b/templates/animation/template.toml @@ -0,0 +1,4 @@ +[template] +description = "Animated WebP with mcpplibs.mwrap.libwebp's demux and mux features: frames encoded with WebPAnimEncoder, played back with WebPAnimDecoder" +default = false +post_message = "Run `mcpp run`, or `mcpp run -- ` to also write the animation." diff --git a/templates/image/mcpp.toml.in b/templates/image/mcpp.toml.in new file mode 100644 index 0000000..a637560 --- /dev/null +++ b/templates/image/mcpp.toml.in @@ -0,0 +1,10 @@ +[package] +name = "{{project.name}}" +version = "0.1.0" + +[targets.{{project.name}}] +kind = "bin" +main = "src/main.cpp" + +[dependencies] +"{{self.name}}" = "{{self.version}}" diff --git a/templates/image/src/main.cpp.in b/templates/image/src/main.cpp.in new file mode 100644 index 0000000..2b00c24 --- /dev/null +++ b/templates/image/src/main.cpp.in @@ -0,0 +1,100 @@ +// {{project.name}} — WebP through import mcpplibs.mwrap.libwebp: a picture +// with alpha (a gradient, a disc, a translucent band) encoded lossy at three +// qualities and lossless, each decoded back to RGBA and compared with the +// original. Give a directory to also write the files there. +import std; +import mcpplibs.mwrap.libwebp; + +using namespace mcpplibs::mwrap::libwebp; // WebPConfigInit, WebPPictureInit … (header static inline functions) + +namespace { + +constexpr int W = 256, H = 160; + +std::vector picture() { + 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]; + const bool disc = (x - 170) * (x - 170) + (y - 80) * (y - 80) < 50 * 50; + p[0] = static_cast(disc ? 240 : x); + p[1] = static_cast(disc ? 180 : y * 255 / (H - 1)); + p[2] = static_cast(disc ? 40 : 255 - x); + p[3] = static_cast(y >= H - 24 ? 96 : 255); // a translucent band at the bottom + } + return rgba; +} + +// WebPConfig + WebPPicture: every setting is reachable (quality, method, +// lossless, exact, alpha quality …); the bytes go to a memory writer. +std::vector encode(std::span rgba, bool lossless, float quality) { + WebPConfig config; + WebPPicture pic; + if (!WebPConfigInit(&config) || !WebPPictureInit(&pic)) return {}; + config.lossless = lossless; + config.quality = quality; // lossy: visual quality; lossless: effort + config.method = 4; // 0 fast … 6 smallest + pic.use_argb = 1; + pic.width = W; + pic.height = H; + WebPMemoryWriter writer; + WebPMemoryWriterInit(&writer); + pic.writer = WebPMemoryWrite; + pic.custom_ptr = &writer; + std::vector out; + if (WebPPictureImportRGBA(&pic, rgba.data(), W * 4) && WebPEncode(&config, &pic)) + out.assign(writer.mem, writer.mem + writer.size); + else + std::println(std::cerr, "encoding failed: error {}", static_cast(pic.error_code)); + WebPPictureFree(&pic); + WebPMemoryWriterClear(&writer); + return out; +} + +// PSNR over the colour of the opaque pixels, and whether alpha came back exactly. +std::pair compare(std::span a, const std::uint8_t* b) { + double error = 0; + std::size_t n = 0; + bool alpha = true; + for (std::size_t i = 0; i < a.size(); i += 4) { + alpha = alpha && a[i + 3] == b[i + 3]; + if (a[i + 3] != 255) continue; + for (int c = 0; c < 3; ++c, ++n) error += (double(a[i + c]) - b[i + c]) * (double(a[i + c]) - b[i + c]); + } + return {error == 0 ? std::numeric_limits::infinity() : 10 * std::log10(255.0 * 255.0 * n / error), alpha}; +} + +} // namespace + +int main(int argc, char** argv) { + const auto rgba = picture(); + std::println("libwebp {:x}.{:x}.{:x}: a {} x {} RGBA picture, {} bytes raw", WebPGetEncoderVersion() >> 16, + (WebPGetEncoderVersion() >> 8) & 0xff, WebPGetEncoderVersion() & 0xff, W, H, rgba.size()); + struct Setting { + const char* name; + bool lossless; + float quality; + }; + for (const auto& s : {Setting{"lossy q30", false, 30}, Setting{"lossy q75", false, 75}, + Setting{"lossy q95", false, 95}, Setting{"lossless", true, 75}}) { + const auto file = encode(rgba, s.lossless, s.quality); + if (file.empty()) return 1; + // Decode: the simple API answers a buffer the library allocated. + int width = 0, height = 0; + std::uint8_t* pixels = WebPDecodeRGBA(file.data(), file.size(), &width, &height); + if (!pixels || width != W || height != H) { + std::println(std::cerr, "{}: decoding failed", s.name); + return 1; + } + const auto [psnr, alpha] = compare(rgba, pixels); + WebPFree(pixels); + std::println("{:<10} {:>6} bytes PSNR {:>5.1f} dB alpha {}", s.name, file.size(), psnr, + alpha ? "exact" : "changed"); + if (argc > 1) { + const auto path = std::filesystem::path(argv[1]) / std::format("{}.webp", s.name); + std::ofstream(path, std::ios::binary).write(reinterpret_cast(file.data()), + static_cast(file.size())); + std::println(" wrote {}", path.string()); + } + } +} diff --git a/templates/image/template.toml b/templates/image/template.toml new file mode 100644 index 0000000..e951fef --- /dev/null +++ b/templates/image/template.toml @@ -0,0 +1,4 @@ +[template] +description = "WebP with import mcpplibs.mwrap.libwebp: a picture with alpha encoded lossy at three qualities and lossless, decoded back to RGBA and compared" +default = true +post_message = "Run `mcpp run`, or `mcpp run -- ` to also write the .webp files there." diff --git a/tools/check_mwrap_release.py b/tools/check_mwrap_release.py new file mode 100644 index 0000000..9bedddb --- /dev/null +++ b/tools/check_mwrap_release.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Check the immutable mwrap release coordinate and bilingual entry pages.""" +import argparse +import hashlib +import json +from pathlib import Path +import re +import tomllib + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--tag', help='release tag; omitted during PR checks') + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + package = tomllib.loads((root / 'mcpp.toml').read_text(encoding="utf-8"))['package'] + assert package['namespace'] == 'mcpplibs.mwrap', package + assert re.fullmatch(r'[A-Za-z_][A-Za-z_0-9]*', package['name']), package['name'] + upstream = package['metadata']['mwrap']['upstream-version'] + version = package['version'] + assert version == upstream or re.fullmatch(re.escape(upstream) + r'\.[1-9][0-9]*', version), (version, upstream) + if args.tag: + assert args.tag == 'mwrap-' + version, (args.tag, version) + assert package['license'] == 'Apache-2.0', package['license'] + assert package['authors'] == ['mcpp-community'], package.get('authors') + license_text = (root / 'LICENSE').read_text(encoding='utf-8') + assert 'Apache License' in license_text and 'Version 2.0' in license_text + assert 'mcpp-community' in (root / 'NOTICE').read_text(encoding='utf-8') + documents = json.loads((root / 'upstream/documents.json').read_text(encoding='utf-8'))['documents'] + assert any(Path(d['path']).name.startswith('README') for d in documents) + assert any(Path(d['path']).name.startswith(('LICENSE', 'COPYING')) for d in documents) + deployed = {entry['from'] for entry in tomllib.loads((root / 'mcpp.toml').read_text(encoding='utf-8')).get('runtime', {}).get('deploy', [])} + assert {'LICENSE', 'NOTICE'} <= deployed + for document in documents: + path = Path(document['path']) + assert not path.is_absolute() and '..' not in path.parts and path.parts[0] == 'upstream' + assert hashlib.sha256((root / path).read_bytes()).hexdigest() == document['sha256'], path + assert document['source'] and path.as_posix() in deployed, path + identity = package['namespace'] + '.' + package['name'] + for name, other in [('README.md', 'README.zh.md'), ('README.zh.md', 'README.md')]: + text = (root / name).read_text(encoding="utf-8") + assert len(text.splitlines()) <= 150, name + assert '](' + other + ')' in '\n'.join(text.splitlines()[:12]), name + assert identity in text and version in text, name + assert text.count('```') % 2 == 0, name + print(identity + '@' + version) + + +if __name__ == '__main__': + main() diff --git a/tools/test_template.py b/tools/test_template.py new file mode 100644 index 0000000..81ea805 --- /dev/null +++ b/tools/test_template.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Render an installed candidate's default mcpp template, then build and run it.""" +import argparse +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import tomllib + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--mcpp", default="mcpp") + parser.add_argument("--namespace", help="index namespace for legacy manifests without package.namespace") + parser.add_argument("--toolchain", default="llvm@23.1.3") + parser.add_argument("--template", help="named template; omit for the package default") + parser.add_argument("--run-arg", action="append", default=[]) + parser.add_argument("--index", type=Path, help="candidate SDK/wrapper index for unpublished feature dependencies") + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + metadata = tomllib.loads((root / "mcpp.toml").read_text())["package"] + namespace = metadata.get("namespace", args.namespace) + assert namespace, "Supply the published index namespace for this legacy manifest" + name, version = metadata["name"], metadata["version"] + identity = namespace + "." + name + with tempfile.TemporaryDirectory(prefix=name + "-template-") as directory: + temporary = Path(directory) + home = temporary / "home" + package = home / "registry/data/xpkgs" / (namespace + "-x-" + name) / version / (name + "-" + version) + shutil.copytree(root, package, ignore=shutil.ignore_patterns( + ".git", "target", ".mcpp", ".xlings", ".candidate-index", "__pycache__", "mcpp.lock", "compile_commands.json", "mcpp-2026.*")) + index = home / "indices/mcpplibs/pkgs/mwrap" + index.mkdir(parents=True) + entry = '{url="https://example.invalid/candidate.tar.gz",sha256="fixture"}' + descriptor = ('package={spec="1",namespace=' + json.dumps(namespace) + ',name=' + json.dumps(name) + + ',description="Installed template fixture",type="package",mcpp="mcpp.toml",xpm={') + descriptor += ",".join(platform + "={[" + json.dumps(version) + "]=" + entry + "}" + for platform in ("linux", "macosx", "windows")) + "}}\n" + (index / (identity + ".lua")).write_text(descriptor) + env = dict(os.environ, MCPP_HOME=str(home)) + selector = identity + "@" + version + for command in ([args.mcpp, "new", "--list-templates", selector], + [args.mcpp, "new", "quickstart", "--template", selector + (":" + args.template if args.template else "")]): + print("+", " ".join(command), flush=True) + subprocess.run(command, cwd=temporary, env=env, check=True) + project = temporary / "quickstart" + manifest = project / "mcpp.toml" + text = manifest.read_text() + dependency = tomllib.loads(text)["dependencies"][identity] + options = {"version": dependency} if isinstance(dependency, str) else dependency + assert options["version"] == version, options + replacement = '{ path = ' + json.dumps(package.as_posix()) + if options.get("features"): + replacement += ', features = ' + json.dumps(options["features"]) + replacement += ' }' + lines = text.splitlines() + matches = [i for i, line in enumerate(lines) if line.startswith(json.dumps(identity) + " =")] + assert len(matches) == 1, "Expected exactly one qualified dependency from self.name" + lines[matches[0]] = json.dumps(identity) + " = " + replacement + manifest.write_text("\n".join(lines) + "\n") + if args.index: + candidate_index = args.index.resolve() + subprocess.run([os.sys.executable, str(candidate_index / "tests/prepare_mwrap_indices.py")], + cwd=candidate_index, check=True) + with manifest.open("a") as out: + out.write("\n[indices]\n") + for namespace in ("compat", "mcpplibs.mwrap"): + view = candidate_index / "tests/mwrap/.indices" / namespace + out.write(json.dumps(namespace) + ' = { path = ' + json.dumps(view.as_posix()) + ' }\n') + command = [args.mcpp, "build", "--toolchain", args.toolchain, "--strict"] + print("+", " ".join(command), flush=True) + subprocess.run(command, cwd=project, check=True) + command = [args.mcpp, "run", "--toolchain", args.toolchain] + if args.run_arg: + command += ["--", *args.run_arg] + print("+", " ".join(command), flush=True) + subprocess.run(command, cwd=project, check=True) + + +if __name__ == "__main__": + main()