crossbind
GitHub

WebP for WASI

v1.6.0WASI

WebP 1.6.0 for command-line programs under wasmtime, precompiled for wasm32-wasip3, single-threaded as @crossbind/port-webp-wasi.

npm install @crossbind/port-webp-wasi@beta

Install

shell
npm install @crossbind/port-webp-wasi@beta crossbind@beta

A WebP command-line tool

WASI has no JavaScript bindings: the program is a main() that links libwebp, built into one .wasm and run with wasmtime. This one encodes a PPM to WebP at a quality and reads a WebP header back.

crossbind.config.js
import webpWasi from '@crossbind/port-webp-wasi/crossbind.config.js';
 
export default {
general: { name: 'webp-tool' },
dependencies: [webpWasi],
paths: { config: import.meta.url },
};
src/native/main.cpp
// A command-line WebP tool for WASI:
// webp-tool encode <input.ppm> <output.webp> [quality]
// webp-tool info <input.webp>
#include <webp/decode.h>
#include <webp/encode.h>
 
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
 
namespace {
 
using File = std::unique_ptr<FILE, int (*)(FILE*)>;
 
std::string version() {
const int packed = WebPGetEncoderVersion();
return std::to_string(packed >> 16) + "." + std::to_string((packed >> 8) & 0xff) + "." + std::to_string(packed & 0xff);
}
 
bool readFile(const char* path, std::vector<uint8_t>& data) {
File file(std::fopen(path, "rb"), std::fclose);
if (!file) {
std::fprintf(stderr, "cannot open %s\n", path);
return false;
}
std::vector<uint8_t> chunk(1 << 16);
size_t got = 0;
while ((got = std::fread(chunk.data(), 1, chunk.size(), file.get())) > 0) data.insert(data.end(), chunk.begin(), chunk.begin() + got);
return true;
}
 
// Binary PPM: "P6", width, height and 255, separated by whitespace or # comments, one whitespace, then RGB.
bool parsePpm(const std::vector<uint8_t>& data, int& width, int& height, size_t& pixels) {
size_t at = 0;
const auto token = [&]() {
for (;;) {
while (at < data.size() && std::isspace(data[at])) ++at;
if (at >= data.size() || data[at] != '#') break;
while (at < data.size() && data[at] != '\n') ++at;
}
std::string text;
while (at < data.size() && !std::isspace(data[at])) text += static_cast<char>(data[at++]);
return text;
};
if (token() != "P6") return false;
width = std::atoi(token().c_str());
height = std::atoi(token().c_str());
const int maxval = std::atoi(token().c_str());
pixels = at + 1;
return width > 0 && height > 0 && maxval == 255 && pixels <= data.size() && data.size() - pixels >= static_cast<size_t>(width) * height * 3;
}
 
int encode(const char* input, const char* output, float quality) {
std::vector<uint8_t> data;
int width = 0;
int height = 0;
size_t pixels = 0;
if (!readFile(input, data)) return 1;
if (!parsePpm(data, width, height, pixels)) {
std::fprintf(stderr, "%s is not a binary PPM (P6, maxval 255)\n", input);
return 1;
}
uint8_t* webp = nullptr;
const size_t size = WebPEncodeRGB(data.data() + pixels, width, height, width * 3, quality, &webp);
File out(std::fopen(output, "wb"), std::fclose);
const bool written = size && out && std::fwrite(webp, 1, size, out.get()) == size;
WebPFree(webp);
if (!written) {
std::fprintf(stderr, "cannot encode %s to %s\n", input, output);
return 1;
}
std::printf("webp %s encode: %s %dx%d -> %s, %zu B at quality %g\n", version().c_str(), input, width, height, output, size, quality);
return 0;
}
 
int info(const char* input) {
std::vector<uint8_t> data;
if (!readFile(input, data)) return 1;
WebPBitstreamFeatures features;
if (WebPGetFeatures(data.data(), data.size(), &features) != VP8_STATUS_OK) {
std::fprintf(stderr, "%s is not a WebP file\n", input);
return 1;
}
const char* format = features.format == 1 ? "lossy" : features.format == 2 ? "lossless" : "mixed";
std::printf("webp %s info: %s %dx%d %s, %zu B, alpha: %s, animated: %s\n", version().c_str(), input, features.width, features.height, format, data.size(),
features.has_alpha ? "yes" : "no", features.has_animation ? "yes" : "no");
return 0;
}
 
} // namespace
 
int main(int argc, char** argv) {
if (argc >= 4 && std::strcmp(argv[1], "encode") == 0) return encode(argv[2], argv[3], argc >= 5 ? std::strtof(argv[4], nullptr) : 75.0f);
if (argc == 3 && std::strcmp(argv[1], "info") == 0) return info(argv[2]);
std::fprintf(stderr, "usage: webp-tool encode <input.ppm> <output.webp> [quality]\n webp-tool info <input.webp>\n");
return 2;
}
shell
npx crossbind build -p wasi -b release
wasmtime run --dir=. .crossbind/build/webp-tool-wasi-wasm32-st-release.wasm encode landscape.ppm landscape.webp 80
wasmtime run --dir=. .crossbind/build/webp-tool-wasi-wasm32-st-release.wasm info landscape.webp
output
webp 1.6.0 encode: landscape.ppm 256x256 -> landscape.webp, 1966 B at quality 80
webp 1.6.0 info: landscape.webp 256x256 lossy, 1966 B, alpha: no, animated: no

Command-line tools

3 upstream commands ship as npm executables built for wasm32-wasip3. They need wasmtime on PATH and no compiler - see WASI commands.

shell
npm install --global @crossbind/port-webp-bin-wasi@beta
cwebp-wasi --help
  • cwebp-wasi
  • dwebp-wasi
  • webpinfo-wasi

What is different on WASI

  • There are no JavaScript bindings: src/native provides main(int, char**), and the build is a single .wasm.
  • Files come from the host through --dir preopens; --dir=. gives the program the current directory.
  • crossbind build -p wasi -b release writes the program to .crossbind/build/<name>-wasi-wasm32-st-release.wasm.
  • WASI 0.3's wasi:cli/exit carries success or failure only, so any non-zero return from main reaches the shell as exit code 1.
  • The build needs wasi-sdk 34 or newer (WASI_SDK_PATH) or the crossbind Docker image; running needs wasmtime 47 or newer. See WASI.

Other platforms

Facts on this page come from the port manifests in the repository and from what npm served on beta when the site was built. See the Libraries guide for the full consumer flow.

MORE LIBRARIES
cURLExpatGDALGEOSGeoTIFFiconvLERClibjpeg-turbolibTIFFOpenSSLPROJSpatiaLiteSQLitezlibZstandard
Type to search every guide page and section.
↑↓ navigate↵ openesc close