crossbind
GitHub

WebP for Android

v1.6.0Android

WebP 1.6.0 for React Native apps on Android, precompiled for arm64-v8a devices and the x86_64 emulator as @crossbind/port-webp-android.

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

Install

shell
npm install @crossbind/plugin-react-native@beta @crossbind/plugin-react-native-ios-helper@beta @crossbind/port-webp-android@beta
npm install --save-dev @crossbind/plugin-metro@beta
crossbind.config.mjs
import webpAndroid from '@crossbind/port-webp-android/crossbind.config.js';
 
export default {
dependencies: [webpAndroid],
paths: { config: import.meta.url },
};
metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const CrossbindMetroPlugin = require('@crossbind/plugin-metro');
 
const defaultConfig = getDefaultConfig(__dirname);
 
const config = {
...CrossbindMetroPlugin(defaultConfig),
};
 
module.exports = mergeConfig(defaultConfig, config);

The whole flow, including Expo, is in the React Native playbook.

Usage

The examples the WebAssembly page runs, as Android compiles them: the same headers and the same calls. They are checked on the WebAssembly build.

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);
PRINTS
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)}`);
PRINTS
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}`);
}
PRINTS
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)})`);
PRINTS
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)

What is different on Android

  • The React Native plugin compiles your headers with the library inside Gradle's native build, so npm run android builds everything.
  • Named imports from ./native/<header>.h work as on the web: await initNative() once, then call the classes.
  • There is no m.FS and no /memfs: files live in the app's own storage, and your C++ takes their paths.
  • No Worker and no COOP or COEP: runtime: 'mt' uses pthreads directly.

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