crossbind
GitHub
← All libraries

WebP

WebP image codec.

Install

Runs in browsers; Node.js and edge runtimes use the same package. Built for wasm32, single-threaded and multi-threaded.

shell
npm install @crossbind/port-webp-wasm@beta
crossbind.config.js
import webpWasm from '@crossbind/port-webp-wasm/crossbind.config.js';
 
export default {
dependencies: [webpWasm],
paths: { config: import.meta.url },
};
main.js
import { initNative, WebPGetEncoderVersion } from '@crossbind/port-webp/webp/encode.h';
 
await initNative();

crossbind itself comes with your bundler plugin; Browser has Vite, Webpack, Rspack and Rollup.

WebP encoding with the controls a canvas does not have

Every current browser displays WebP, but a canvas can only encode it at a quality, and Safari's canvas cannot encode it at all. These apps run libwebp 1.6.0 itself, compiled by crossbind: they show what each quality costs and gives away, fit a sticker under a byte budget, and decode a file as its bytes arrive. The first run downloads 0.8 MB of WebAssembly once; every app on this page shares it, and nothing is uploaded.

APP 01

See what each WebP quality costs, and what it gives away

Encode the test card or your own image with libwebp 1.6.0, then compare the result with the original next to its size, PSNR and SSIM. A canvas can only be given a type and a quality, and Safari cannot encode WebP from a canvas at all (caniuse). Here you also get lossless, near-lossless, content presets, effort and sharp YUV.

The test card is generated in the module, so its numbers are the same in every browser. Your own image stays in this tab; one longer than 1,600 px on a side is scaled down first. After the first run, every change encodes again.

Encode the test card or open an image to compare it with its WebP, side by side.
SHOW THE CODE
src/native/webp_studio.h
// src/native/webp_studio.h (excerpt): one encode, then measure what it decodes to
WebPConfig config;
WebPConfigPreset(&config, lossy ? presetNamed(preset) : WEBP_PRESET_DEFAULT, quality);
config.method = method;
if (lossy) {
config.use_sharp_yuv = sharpYuv ? 1 : 0;
} else {
config.lossless = 1;
config.near_lossless = mode == "near-lossless" ? nearLossless : 100;
}
const std::vector<uint8_t> webp = imaging::encode(config, rgba, width, height);
const imaging::Fidelity fidelity = imaging::measure(rgba, imaging::decode(webp).rgba, width, height);
main.js
const m = await initNative();
const pixels = canvas.getContext('2d').getImageData(0, 0, 512, 384).data;
// m.FS.writeFile appends to an existing file here: unlink it first when you replace one
await m.FS.writeFile('/memfs/webpapps/studio/source.rgba', new Uint8Array(pixels.buffer));
 
const report = JSON.parse(await m.WebpStudio.encode('/memfs/webpapps/studio/source.rgba', 512, 384,
'/memfs/webpapps/studio/out.webp', 'lossy', 75, 100, 'default', 4, false));
// the test card: report.bytes 14852, report.psnr 29.37, report.ssim 0.9561
const webp = await m.getFileBytes('/memfs/webpapps/studio/out.webp');
APP 02

Turn any picture into a sticker that fits the limit

WhatsApp asks for stickers that are WebP, exactly 512 × 512 and at most 100 KB, and recommends an 8 px white outline (its sticker guide). libwebp crops the empty border, fits the rest into the square, keeps the transparency exact and finds the highest quality that stays under the budget.

The sample is a smiley drawn in the module on a transparent background. A PNG with transparency makes the best sticker; a photo becomes a framed square. Nothing is uploaded.

Make the sample sticker, or bring a PNG with transparency, to see it on light and dark chats.
SHOW THE CODE
src/native/sticker_maker.h
// src/native/sticker_maker.h (excerpt): trim, fit, then the best quality under the budget
WebPPictureCrop(&picture, box.left, box.top, box.width, box.height); // drop empty borders
WebPPictureRescale(&picture, fitWidth, fitHeight); // aspect kept
// ... centre it on a transparent 512x512 canvas, draw the 8 px white outline ...
if (!attempt(100)) {
int low = 0;
int high = 99;
while (low <= high) {
const int quality = (low + high) / 2;
if (attempt(quality)) low = quality + 1;
else high = quality - 1;
}
}
main.js
const m = await initNative();
// any size, with alpha; m.FS.writeFile appends to an existing file here, so unlink it first when you replace one
await m.FS.writeFile('/memfs/webpapps/sticker/source.rgba', rgba);
 
const report = JSON.parse(await m.StickerMaker.make('/memfs/webpapps/sticker/source.rgba', 600, 400,
true, 100000, '/memfs/webpapps/sticker/sticker.webp', '/memfs/webpapps/sticker/canvas.rgba'));
// the sample: 33,610 B at quality 100; under a 12,000 B budget, 11,952 B at quality 12
const sticker = await m.getFileBytes('/memfs/webpapps/sticker/sticker.webp');
APP 03

Watch a WebP arrive, row by row

On a slow connection an image fills in from the top. libwebp's incremental decoder does this for WebP: give it the bytes that have arrived and it returns every row it can already finish. Play the download, or drag through the file byte by byte.

The sample is the WebP Studio test card at quality 75. Your own file is mounted in memory and stays in this tab. Animated files are not decoded incrementally.

Stream the test card, or open a .webp of your own, to watch it decode as the bytes arrive.
SHOW THE CODE
src/native/webp_stream.h
// src/native/webp_stream.h (excerpt): decode whatever has arrived
std::unique_ptr<WebPIDecoder, void (*)(WebPIDecoder*)> decoder(WebPINewRGB(MODE_RGBA, nullptr, 0, 0), WebPIDelete);
const VP8StatusCode status = WebPIUpdate(decoder.get(), data.data(), available); // SUSPENDED: wants more
int rows = 0, width = 0, height = 0, stride = 0;
const uint8_t* rgba = WebPIDecGetRGB(decoder.get(), &rows, &width, &height, &stride);
// rows 0 to rows - 1 are final: copy them into the frame, leave the rest transparent
main.js
const m = await initNative();
const report = JSON.parse(await m.WebpStream.decodePrefix('/memfs/webpapps/stream/card.webp', 7426,
'/memfs/webpapps/stream/frame.rgba'));
// the test card at quality 75 is 14,852 B: half of it gives 135 of its 384 rows
const frame = await m.getFileBytes('/memfs/webpapps/stream/frame.rgba');
context.putImageData(new ImageData(new Uint8ClampedArray(frame.buffer), report.width, report.height), 0, 0);

Examples

Each one is a small C++ header and the JavaScript that calls it, with what it printed when the site was built. The JavaScript only tab does the same with WebP's own headers and no C++ file. The last is a whole C++ program, built for WASI.

Encode pixels to WebP and decode them back

The simple API most code starts with: WebPEncodeRGBA at a quality, WebPEncodeLosslessRGBA, WebPGetInfo for the size in the header and WebPDecodeRGBA for the pixels. The input is a generated 256×256 landscape with a little noise, standing in for a photo.

src/native/webp_codec.h
#pragma once
 
#include <webp/decode.h>
#include <webp/encode.h>
 
#include <cstdint>
#include <stdexcept>
#include <string>
 
// libwebp's simple API. Bytes cross the binding as a byte string: one UTF-16 code unit (0-255) per byte.
class WebpCodec {
public:
// WebPGetEncoderVersion() packs the version into one integer, 0xMMmmpp.
static std::string version() {
const int packed = WebPGetEncoderVersion();
return std::to_string(packed >> 16) + "." + std::to_string((packed >> 8) & 0xff) + "." + std::to_string(packed & 0xff);
}
 
// `rgba` is 4 bytes per pixel, row after row. Quality runs from 0 (smallest file) to 100 (best).
static std::u16string encode(const std::u16string& rgba, int width, int height, float quality) {
const std::string pixels = checkedPixels(rgba, width, height);
uint8_t* output = nullptr;
const size_t size = WebPEncodeRGBA(reinterpret_cast<const uint8_t*>(pixels.data()), width, height, width * 4, quality, &output);
return take(output, size);
}
 
static std::u16string encodeLossless(const std::u16string& rgba, int width, int height) {
const std::string pixels = checkedPixels(rgba, width, height);
uint8_t* output = nullptr;
const size_t size = WebPEncodeLosslessRGBA(reinterpret_cast<const uint8_t*>(pixels.data()), width, height, width * 4, &output);
return take(output, size);
}
 
// "<width>x<height>", read from the file header without decoding the image.
static std::string dimensions(const std::u16string& webp) {
const std::string data = fromUnits(webp);
int width = 0;
int height = 0;
if (!WebPGetInfo(reinterpret_cast<const uint8_t*>(data.data()), data.size(), &width, &height)) throw std::runtime_error("not a WebP image");
return std::to_string(width) + "x" + std::to_string(height);
}
 
static std::u16string decode(const std::u16string& webp) {
const std::string data = fromUnits(webp);
int width = 0;
int height = 0;
uint8_t* rgba = WebPDecodeRGBA(reinterpret_cast<const uint8_t*>(data.data()), data.size(), &width, &height);
if (!rgba) throw std::runtime_error("not a decodable WebP image");
return take(rgba, static_cast<size_t>(width) * height * 4);
}
 
private:
static std::string checkedPixels(const std::u16string& rgba, int width, int height) {
if (width <= 0 || height <= 0 || rgba.size() != static_cast<size_t>(width) * height * 4) {
throw std::invalid_argument("expected width * height * 4 bytes of RGBA");
}
return fromUnits(rgba);
}
 
// Copies memory libwebp allocated into a byte string, then releases it with WebPFree.
static std::u16string take(uint8_t* data, size_t size) {
if (!data || size == 0) {
WebPFree(data);
throw std::runtime_error("WebP encoding failed");
}
std::u16string units(data, data + size);
WebPFree(data);
return units;
}
 
static std::string fromUnits(const std::u16string& units) {
std::string data(units.size(), '\0');
for (size_t i = 0; i < units.size(); ++i) {
if (units[i] > 0xFF) throw std::invalid_argument("not a byte string");
data[i] = static_cast<char>(units[i]);
}
return data;
}
};
main.js
import { initNative, WebpCodec } from './native/webp_codec.h';
 
await initNative();
let seed = 1;
const random = (n) => (seed = (seed * 48271) % 2147483647) % n;
let rgba = '';
for (let y = 0; y < 256; y += 1) {
for (let x = 0; x < 256; x += 1) {
const sun = (x - 180) ** 2 + (y - 70) ** 2 < 900;
const hill = y > 170 + (((x - 128) ** 2) >> 8);
const [r, g, b] = sun ? [255, 214, 90] : hill ? [40 + (y >> 2), 120 + (x >> 3), 50] : [90 + (y >> 1), 150 + (y >> 2), 235];
rgba += String.fromCharCode(r ^ random(8), g ^ random(8), b ^ random(8), 255);
}
}
 
const lossy = await WebpCodec.encode(rgba, 256, 256, 80);
const lossless = await WebpCodec.encodeLossless(rgba, 256, 256);
console.log(await WebpCodec.version(), await WebpCodec.dimensions(lossy));
console.log(`RGBA ${rgba.length} B -> lossy q80 ${lossy.length} B, lossless ${lossless.length} B`);
console.log((await WebpCodec.decode(lossless)) === rgba);
PRINTSfirst run downloads 0.8 MB
1.6.0 256x256
RGBA 262144 B -> lossy q80 1966 B, lossless 87058 B
true

Tune the encoder and measure what it gave away

The advanced API: WebPConfigPreset picks settings for the kind of image, then method, sharp YUV and a target size adjust them before WebPEncode. WebPPlaneDistortion measures PSNR and SSIM against the original, so a quality can be chosen by numbers.

src/native/webp_encoder.h
#pragma once
 
#include <webp/decode.h>
#include <webp/encode.h>
 
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <stdexcept>
#include <string>
 
// libwebp's advanced API: a WebPConfig started from a content preset and adjusted field by field,
// then WebPEncode on a WebPPicture. Bytes cross the binding as one UTF-16 code unit (0-255) per byte.
class WebpEncoder {
public:
// preset: "default", "photo", "picture" (indoor and portrait shots), "drawing", "icon" or "text".
WebpEncoder(const std::string& preset, float quality) {
if (!WebPConfigPreset(&config, presetNamed(preset), quality)) throw std::runtime_error("libwebp version mismatch");
}
 
// 0 is the fastest; 6 is the slowest and usually the smallest. The default is 4.
void setMethod(int method) { config.method = method; }
 
// A slower RGB to YUV conversion that keeps colour edges sharper.
void setSharpYuv(bool enabled) { config.use_sharp_yuv = enabled ? 1 : 0; }
 
// The encoder searches for the quality that lands closest to this many bytes, over up to 6 passes.
void setTargetSize(int bytes) {
config.target_size = bytes;
config.pass = 6;
}
 
std::u16string encode(const std::u16string& rgba, int width, int height) const {
if (!WebPValidateConfig(&config)) throw std::invalid_argument("invalid encoder settings");
if (width <= 0 || height <= 0 || rgba.size() != static_cast<size_t>(width) * height * 4) {
throw std::invalid_argument("expected width * height * 4 bytes of RGBA");
}
const std::string pixels = fromUnits(rgba);
WebPPicture picture;
if (!WebPPictureInit(&picture)) throw std::runtime_error("libwebp version mismatch");
picture.width = width;
picture.height = height;
// ARGB input lets WebPEncode do the YUV conversion itself, which is where sharp YUV applies.
picture.use_argb = 1;
WebPMemoryWriter writer;
WebPMemoryWriterInit(&writer);
picture.writer = WebPMemoryWrite;
picture.custom_ptr = &writer;
const bool ok = WebPPictureImportRGBA(&picture, reinterpret_cast<const uint8_t*>(pixels.data()), width * 4) && WebPEncode(&config, &picture);
const WebPEncodingError error = picture.error_code;
WebPPictureFree(&picture);
if (!ok) {
WebPMemoryWriterClear(&writer);
throw std::runtime_error("WebP encoding failed with error " + std::to_string(error));
}
std::u16string units(writer.mem, writer.mem + writer.size);
WebPMemoryWriterClear(&writer);
return units;
}
 
// What the encoder gave away: PSNR over red, green and blue, and libwebp's SSIM over the same
// channels (1 means identical), between `rgba` and the pixels `webp` decodes to.
static std::string compare(const std::u16string& rgba, const std::u16string& webp) {
const std::string original = fromUnits(rgba);
const std::string data = fromUnits(webp);
int width = 0;
int height = 0;
uint8_t* decoded = WebPDecodeRGBA(reinterpret_cast<const uint8_t*>(data.data()), data.size(), &width, &height);
if (!decoded) throw std::runtime_error("not a decodable WebP image");
if (original.size() != static_cast<size_t>(width) * height * 4) {
WebPFree(decoded);
throw std::invalid_argument("the RGBA and the WebP image differ in size");
}
const uint8_t* source = reinterpret_cast<const uint8_t*>(original.data());
double squaredError = 0;
double similarity = 0;
for (int channel = 0; channel < 3; ++channel) {
float sum = 0;
float perChannel = 0;
WebPPlaneDistortion(source + channel, width * 4, decoded + channel, width * 4, width, height, 4, 0, &sum, &perChannel);
squaredError += sum;
WebPPlaneDistortion(source + channel, width * 4, decoded + channel, width * 4, width, height, 4, 1, &sum, &perChannel);
similarity += sum;
}
WebPFree(decoded);
const double samples = 3.0 * width * height;
const double psnr = squaredError > 0 ? 10 * std::log10(255.0 * 255.0 * samples / squaredError) : 99.0;
char text[64];
std::snprintf(text, sizeof text, "PSNR %.2f dB, SSIM %.4f", psnr, similarity / samples);
return text;
}
 
private:
static WebPPreset presetNamed(const std::string& name) {
if (name == "default") return WEBP_PRESET_DEFAULT;
if (name == "photo") return WEBP_PRESET_PHOTO;
if (name == "picture") return WEBP_PRESET_PICTURE;
if (name == "drawing") return WEBP_PRESET_DRAWING;
if (name == "icon") return WEBP_PRESET_ICON;
if (name == "text") return WEBP_PRESET_TEXT;
throw std::invalid_argument("unknown preset " + name);
}
 
static std::string fromUnits(const std::u16string& units) {
std::string data(units.size(), '\0');
for (size_t i = 0; i < units.size(); ++i) {
if (units[i] > 0xFF) throw std::invalid_argument("not a byte string");
data[i] = static_cast<char>(units[i]);
}
return data;
}
 
WebPConfig config;
};
main.js
import { initNative, WebpEncoder } from './native/webp_encoder.h';
 
await initNative();
let seed = 1;
const random = (n) => (seed = (seed * 48271) % 2147483647) % n;
let rgba = '';
for (let y = 0; y < 256; y += 1) {
for (let x = 0; x < 256; x += 1) {
const sun = (x - 180) ** 2 + (y - 70) ** 2 < 900;
const hill = y > 170 + (((x - 128) ** 2) >> 8);
const [r, g, b] = sun ? [255, 214, 90] : hill ? [40 + (y >> 2), 120 + (x >> 3), 50] : [90 + (y >> 1), 150 + (y >> 2), 235];
rgba += String.fromCharCode(r ^ random(8), g ^ random(8), b ^ random(8), 255);
}
}
 
for (const quality of [50, 75, 90]) {
const encoder = await new WebpEncoder('photo', quality);
const webp = await encoder.encode(rgba, 256, 256);
console.log(`photo q${quality}: ${webp.length} B, ${await WebpEncoder.compare(rgba, webp)}`);
}
 
const slow = await new WebpEncoder('photo', 75);
await slow.setMethod(6);
await slow.setSharpYuv(true);
const tuned = await slow.encode(rgba, 256, 256);
console.log(`photo q75, method 6, sharp YUV: ${tuned.length} B, ${await WebpEncoder.compare(rgba, tuned)}`);
 
const budget = await new WebpEncoder('photo', 75);
await budget.setTargetSize(3000);
const fitted = await budget.encode(rgba, 256, 256);
console.log(`photo, 3000 B target: ${fitted.length} B, ${await WebpEncoder.compare(rgba, fitted)}`);
PRINTSfirst run downloads 0.8 MB
photo q50: 1300 B, PSNR 33.64 dB, SSIM 0.8993
photo q75: 1658 B, PSNR 34.26 dB, SSIM 0.9047
photo q90: 4388 B, PSNR 35.02 dB, SSIM 0.9133
photo q75, method 6, sharp YUV: 1708 B, PSNR 34.58 dB, SSIM 0.9050
photo, 3000 B target: 3000 B, PSNR 34.83 dB, SSIM 0.9097

Keep transparency, and decide what happens under it

A lossy WebP stores alpha in a plane of its own: alpha_quality 100 keeps it exact, lower values trade it for bytes. Lossless keeps every visible pixel, but may change the colour under fully transparent pixels to compress better unless exact is set.

src/native/webp_alpha.h
#pragma once
 
#include <webp/decode.h>
#include <webp/encode.h>
 
#include <cstdint>
#include <stdexcept>
#include <string>
 
// Transparency in WebP. Lossy files keep alpha in a plane of its own with its own quality; lossless
// files keep it exactly. Bytes cross the binding as one UTF-16 code unit (0-255) per byte.
class WebpAlpha {
public:
// Lossy colour at `quality`, alpha at `alphaQuality` (100 keeps it lossless, lower values shrink it).
static std::u16string encodeLossy(const std::u16string& rgba, int width, int height, float quality, int alphaQuality) {
WebPConfig config;
if (!WebPConfigInit(&config)) throw std::runtime_error("libwebp version mismatch");
config.quality = quality;
config.alpha_quality = alphaQuality;
return encode(config, rgba, width, height);
}
 
// Lossless. Without `exact`, libwebp may change the colour under fully transparent pixels,
// which nobody sees, to compress better.
static std::u16string encodeLossless(const std::u16string& rgba, int width, int height, bool exact) {
WebPConfig config;
if (!WebPConfigInit(&config)) throw std::runtime_error("libwebp version mismatch");
config.lossless = 1;
config.exact = exact ? 1 : 0;
return encode(config, rgba, width, height);
}
 
static std::u16string decode(const std::u16string& webp) {
const std::string data = fromUnits(webp);
int width = 0;
int height = 0;
uint8_t* rgba = WebPDecodeRGBA(reinterpret_cast<const uint8_t*>(data.data()), data.size(), &width, &height);
if (!rgba) throw std::runtime_error("not a decodable WebP image");
std::u16string units(rgba, rgba + static_cast<size_t>(width) * height * 4);
WebPFree(rgba);
return units;
}
 
private:
static std::u16string encode(const WebPConfig& config, const std::u16string& rgba, int width, int height) {
if (!WebPValidateConfig(&config)) throw std::invalid_argument("invalid encoder settings");
if (width <= 0 || height <= 0 || rgba.size() != static_cast<size_t>(width) * height * 4) {
throw std::invalid_argument("expected width * height * 4 bytes of RGBA");
}
const std::string pixels = fromUnits(rgba);
WebPPicture picture;
if (!WebPPictureInit(&picture)) throw std::runtime_error("libwebp version mismatch");
picture.width = width;
picture.height = height;
picture.use_argb = 1;
WebPMemoryWriter writer;
WebPMemoryWriterInit(&writer);
picture.writer = WebPMemoryWrite;
picture.custom_ptr = &writer;
const bool ok = WebPPictureImportRGBA(&picture, reinterpret_cast<const uint8_t*>(pixels.data()), width * 4) && WebPEncode(&config, &picture);
WebPPictureFree(&picture);
if (!ok) {
WebPMemoryWriterClear(&writer);
throw std::runtime_error("WebP encoding failed");
}
std::u16string units(writer.mem, writer.mem + writer.size);
WebPMemoryWriterClear(&writer);
return units;
}
 
static std::string fromUnits(const std::u16string& units) {
std::string data(units.size(), '\0');
for (size_t i = 0; i < units.size(); ++i) {
if (units[i] > 0xFF) throw std::invalid_argument("not a byte string");
data[i] = static_cast<char>(units[i]);
}
return data;
}
};
main.js
import { initNative, WebpAlpha } from './native/webp_alpha.h';
 
await initNative();
let rgba = '';
for (let y = 0; y < 128; y += 1) {
for (let x = 0; x < 128; x += 1) {
const d = (x - 64) ** 2 + (y - 64) ** 2;
const alpha = d < 1600 ? 255 : d < 2304 ? Math.floor(((2304 - d) * 255) / 704) : 0;
rgba += alpha ? String.fromCharCode(255 - x, 2 * y, 40, alpha) : String.fromCharCode(2 * x, 2 * y, 77, 0);
}
}
const alphaOf = (pixels) => Array.from({ length: pixels.length / 4 }, (_, i) => pixels[i * 4 + 3]).join('');
 
for (const alphaQuality of [100, 50]) {
const webp = await WebpAlpha.encodeLossy(rgba, 128, 128, 80, alphaQuality);
const alphaKept = alphaOf(await WebpAlpha.decode(webp)) === alphaOf(rgba);
console.log(`lossy q80, alpha quality ${alphaQuality}: ${webp.length} B, alpha unchanged: ${alphaKept}`);
}
for (const exact of [false, true]) {
const webp = await WebpAlpha.encodeLossless(rgba, 128, 128, exact);
console.log(`lossless${exact ? ', exact' : ''}: ${webp.length} B, identical: ${(await WebpAlpha.decode(webp)) === rgba}`);
}
PRINTSfirst run downloads 0.8 MB
lossy q80, alpha quality 100: 1890 B, alpha unchanged: true
lossy q80, alpha quality 50: 1472 B, alpha unchanged: false
lossless: 1710 B, identical: false
lossless, exact: 1982 B, identical: true

Inspect a WebP, then decode only what you need

WebPGetFeatures reads size, alpha, animation and lossy or lossless from the header. WebPDecode with a WebPDecoderConfig scales to a thumbnail or crops a region while it decodes. The file comes from the first example's encoder.

src/native/webp_reader.h
#pragma once
 
#include <webp/decode.h>
 
#include <cstdint>
#include <stdexcept>
#include <string>
 
// Reading WebP files: what the header says, and decoding with WebPDecoderConfig, which can scale or
// crop while it decodes. Bytes cross the binding as one UTF-16 code unit (0-255) per byte.
class WebpReader {
public:
// {"width","height","hasAlpha","hasAnimation","format"} from the header; format is "lossy", "lossless" or "mixed".
static std::string features(const std::u16string& webp) {
const std::string data = fromUnits(webp);
WebPBitstreamFeatures features;
if (WebPGetFeatures(reinterpret_cast<const uint8_t*>(data.data()), data.size(), &features) != VP8_STATUS_OK) {
throw std::runtime_error("not a WebP image");
}
const char* format = features.format == 1 ? "lossy" : features.format == 2 ? "lossless" : "mixed";
return "{\"width\":" + std::to_string(features.width) + ",\"height\":" + std::to_string(features.height) +
",\"hasAlpha\":" + (features.has_alpha ? "true" : "false") + ",\"hasAnimation\":" + (features.has_animation ? "true" : "false") +
",\"format\":\"" + format + "\"}";
}
 
// The whole image, resized to width x height while it decodes.
static std::u16string decodeScaled(const std::u16string& webp, int width, int height) {
WebPDecoderConfig config;
if (!WebPInitDecoderConfig(&config)) throw std::runtime_error("libwebp version mismatch");
config.options.use_scaling = 1;
config.options.scaled_width = width;
config.options.scaled_height = height;
return decode(webp, config);
}
 
// Only the width x height rectangle at (left, top) comes out as RGBA.
static std::u16string decodeRegion(const std::u16string& webp, int left, int top, int width, int height) {
WebPDecoderConfig config;
if (!WebPInitDecoderConfig(&config)) throw std::runtime_error("libwebp version mismatch");
config.options.use_cropping = 1;
config.options.crop_left = left;
config.options.crop_top = top;
config.options.crop_width = width;
config.options.crop_height = height;
return decode(webp, config);
}
 
private:
static std::u16string decode(const std::u16string& webp, WebPDecoderConfig& config) {
const std::string data = fromUnits(webp);
config.output.colorspace = MODE_RGBA;
const VP8StatusCode status = WebPDecode(reinterpret_cast<const uint8_t*>(data.data()), data.size(), &config);
if (status != VP8_STATUS_OK) throw std::runtime_error("WebP decoding failed with status " + std::to_string(status));
const WebPRGBABuffer& buffer = config.output.u.RGBA;
std::u16string units(buffer.rgba, buffer.rgba + buffer.size);
WebPFreeDecBuffer(&config.output);
return units;
}
 
static std::string fromUnits(const std::u16string& units) {
std::string data(units.size(), '\0');
for (size_t i = 0; i < units.size(); ++i) {
if (units[i] > 0xFF) throw std::invalid_argument("not a byte string");
data[i] = static_cast<char>(units[i]);
}
return data;
}
};
main.js
import { initNative, WebpReader } from './native/webp_reader.h';
import { WebpCodec } from './native/webp_codec.h';
 
await initNative();
let seed = 1;
const random = (n) => (seed = (seed * 48271) % 2147483647) % n;
let rgba = '';
for (let y = 0; y < 256; y += 1) {
for (let x = 0; x < 256; x += 1) {
const sun = (x - 180) ** 2 + (y - 70) ** 2 < 900;
const hill = y > 170 + (((x - 128) ** 2) >> 8);
const [r, g, b] = sun ? [255, 214, 90] : hill ? [40 + (y >> 2), 120 + (x >> 3), 50] : [90 + (y >> 1), 150 + (y >> 2), 235];
rgba += String.fromCharCode(r ^ random(8), g ^ random(8), b ^ random(8), 255);
}
}
const lossy = await WebpCodec.encode(rgba, 256, 256, 80);
const lossless = await WebpCodec.encodeLossless(rgba, 256, 256);
 
for (const webp of [lossy, lossless]) {
const info = JSON.parse(await WebpReader.features(webp));
console.log(`${info.width}x${info.height} ${info.format}, alpha: ${info.hasAlpha}, animated: ${info.hasAnimation}`);
}
const pixel = (pixels, width, x, y) => [...pixels.slice((y * width + x) * 4, (y * width + x) * 4 + 4)].map((c) => c.charCodeAt(0)).join(', ');
const thumbnail = await WebpReader.decodeScaled(lossy, 64, 64);
console.log(`thumbnail 64x64: ${thumbnail.length} B, the sun at (45, 17) is rgba(${pixel(thumbnail, 64, 45, 17)})`);
const region = await WebpReader.decodeRegion(lossy, 150, 40, 60, 60);
console.log(`region 60x60 from (150, 40): ${region.length} B, its centre is rgba(${pixel(region, 60, 30, 30)})`);
PRINTSfirst run downloads 0.8 MB
256x256 lossy, alpha: false, animated: false
256x256 lossless, alpha: false, animated: false
thumbnail 64x64: 16384 B, the sun at (45, 17) is rgba(251, 211, 93, 255)
region 60x60 from (150, 40): 14400 B, its centre is rgba(251, 211, 93, 255)

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 -e 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
Type to search every guide page and section.
↑↓ navigate↵ openesc close