crossbind
GitHub

WebP

v1.6.0Imaging

WebP 1.6.0, webP image codec, packaged by crossbind as @crossbind/port-webp and one package per target. Only a variant that is actually on npm beta is listed as published.

npm install @crossbind/port-webp-wasm@beta
LIVE · 3 APPS · RUNS IN THIS TAB

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);

Usage

The calls most WebP code makes, each a small C++ header crossbind binds and the JavaScript that uses it. Every example runs here in WebAssembly and prints what the site build checked; the same headers and calls work on Android and iOS.

Each example also has a JavaScript only tab: the same task with no C++ file, calling WebP's own headers from @crossbind/port-webp directly. 2 of 4 work that way; the others say what stops them.

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)

Add it to your project

One package per platform: install the ones you build for and list each in crossbind.config.js; crossbind compiles only the one that matches the build target. Your C++ goes in src/native, next to the headers it binds. Libraries explains the whole flow.

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 },
};

Platforms

PlatformRuns inBuildsPage
WebAssemblybrowsers, Node.js and edge runtimeswasm32, single-threaded and multi-threadedWebP for WebAssembly
AndroidReact Native apps on Androidarm64-v8a devices and the x86_64 emulatorWebP for Android
iOSReact Native apps on iOSarm64 devices and simulatorsWebP for iOS
macOSnative Node.js addons and Electron on macOSarm64 and x64, macOS 11 or laterWebP for macOS
Linuxnative Node.js addons on Linuxx64 and arm64, glibc 2.28 or laterWebP for Linux
Windowsnative Node.js addons on Windowsx64 and arm64, Windows 10 or laterWebP for Windows
WASIcommand-line programs under wasmtimewasm32-wasip3, single-threadedWebP for WASI

Packages

TargetPackagenpm `beta`
Meta package@crossbind/port-webp2.0.0-beta.62
Web and Node.js@crossbind/port-webp-wasm2.0.0-beta.62
WASI library@crossbind/port-webp-wasi2.0.0-beta.62
WASI commands@crossbind/port-webp-standalone-wasinot published
Android@crossbind/port-webp-android2.0.0-beta.62
iOS@crossbind/port-webp-ios2.0.0-beta.62
macOS@crossbind/port-webp-darwin2.0.0-beta.62
Linux@crossbind/port-webp-linux2.0.0-beta.62
Linux (musl)@crossbind/port-webp-linuxmuslnot published
Windows@crossbind/port-webp-win322.0.0-beta.62

Licence

  • npm license field of @crossbind/port-webp: BSD-3-Clause.
  • The licence files that ship with the package, and the port recipe, are in the port directory.

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