fast-webp 0.1.1

Fast WebP encoder for Rust image buffers and proxy image payloads.
Documentation

fast-webp

fast-webp is a small WebP encoder crate for Rust applications that need the ergonomics of the webp crate and a faster hot path.

It is intentionally narrow:

  • RgbImage, RgbaImage, DynamicImage -> WebP bytes
  • RGB/RGBA byte buffers -> WebP bytes
  • optional JPEG/PNG response bytes -> WebP bytes via image codecs

It does not try to be an image processing framework. Resize, crop, filters, metadata editing, animation, and WebP decoding are out of scope for the first version.

Why this exists

The upstream webp crate has a convenient Encoder::from_image(&image).encode(quality) API. Internally it is organized as a small wrapper around libwebp-sys: src/lib.rs re-exports modules, src/encoder.rs builds an encoder/picture pipeline, and src/shared.rs owns the WebP memory wrappers.

That shape is good for a general wrapper, but for reverse proxies and HTTP image conversion we want a shorter path:

DynamicImage/RGB/RGBA/JPEG/PNG -> RGB/RGBA bytes -> WebPEncodeRGB/RGBA -> Vec<u8>

So fast-webp keeps the public API simple and pushes the encoding hot path through libwebp-sys with a fast encoder configuration:

  • WebPConfig with method = 0
  • pass = 1
  • thread_level = 1
  • WebPPictureImportRGB
  • WebPPictureImportRGBA
  • WebPEncode
  • WebPFree

Install

[dependencies]
fast-webp = "0.1"

Runtime dependencies are deliberately close to webp:

[dependencies]
libwebp-sys = "0.9.3"
image = { version = "^0.25.0", default-features = false, optional = true }

Minimal encoder-only build:

[dependencies]
fast-webp = { version = "0.1", default-features = false }

Only image buffer integration:

[dependencies]
fast-webp = { version = "0.1", default-features = false, features = ["image"] }

JPEG/PNG byte conversion through image codecs:

[dependencies]
fast-webp = { version = "0.1", features = ["image-codecs"] }

If your proxy already decodes PNG/JPEG with another fast decoder, prefer feeding the resulting RGB/RGBA buffer into encode_rgb or encode_rgba. That is the fast path this crate optimizes.

Public API

Raw Buffers

Use this when you already have tightly packed RGB/RGBA pixels.

use fast_webp::{encode_rgb, encode_rgba, WebpOptions};

let rgb = vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255];
let webp = encode_rgb(&rgb, 2, 2, WebpOptions::default())?;

let rgba = vec![
    255, 0, 0, 255,
    0, 255, 0, 128,
    0, 0, 255, 255,
    255, 255, 255, 0,
];
let webp_with_alpha = encode_rgba(&rgba, 2, 2, WebpOptions::default())?;

The encoder validates:

  • quality is finite and in 0.0..=100.0
  • width and height are non-zero
  • dimensions are within WEBP_MAX_DIMENSION
  • width * height * channels does not overflow
  • input length exactly matches the expected packed buffer length

image crate

Enable the image feature. It is enabled by default.

use fast_webp::{encode_dynamic_image, WebpOptions};

let image = image::open("input.jpg")?;
let webp = encode_dynamic_image(
    &image,
    WebpOptions {
        quality: 75.0,
        lossless: false,
    },
)?;

std::fs::write("output.webp", webp)?;

Direct RgbImage / RgbaImage entry points avoid unnecessary conversion:

use fast_webp::{encode_rgb_image, encode_rgba_image, WebpOptions};

let rgb = image::open("photo.jpg")?.to_rgb8();
let webp = encode_rgb_image(&rgb, WebpOptions::default())?;

let rgba = image::open("icon.png")?.to_rgba8();
let webp = encode_rgba_image(&rgba, WebpOptions::default())?;

DynamicImage handling is deliberately conservative:

  • ImageRgb8 -> RGB encode
  • ImageRgba8 -> RGBA encode
  • grayscale -> RGB
  • grayscale + alpha -> RGBA
  • higher-depth RGB -> to_rgb8
  • everything else -> to_rgba8

JPEG/PNG bytes

Enable image-codecs for this convenience API. This keeps the default crate small while still allowing a single-call proxy workflow when you want it.

use fast_webp::{convert_bytes_to_webp, WebpOptions};

let webp = convert_bytes_to_webp(
    &response_body,
    WebpOptions {
        quality: 75.0,
        lossless: false,
    },
)?;

Format detection is magic-byte based:

use fast_webp::{detect_format, InputFormat};

assert_eq!(detect_format(b"\xff\xd8\xff..."), Some(InputFormat::Jpeg));

Decoder path:

  • JPEG -> image JPEG decoder -> DynamicImage -> RGB/RGBA encode
  • PNG -> image PNG decoder -> DynamicImage -> RGB/RGBA encode

The 800 ms target for a 4 MB PNG depends mostly on PNG decode time and CPU. The part controlled by this crate is the WebP encode path after decode: it avoids the heavier WebPPicture setup used by generic wrappers and calls the simple WebPEncode* functions directly. For the fastest proxy path, decode with the fastest decoder available in your application and call encode_rgb/encode_rgba.

  • PNG grayscale -> RGB
  • PNG grayscale + alpha -> RGBA

Options

use fast_webp::WebpOptions;

let lossy = WebpOptions {
    quality: 82.0,
    lossless: false,
};

let lossless = WebpOptions {
    quality: 100.0,
    lossless: true,
};

For lossless mode, quality is still validated for API consistency, but the simple libwebp lossless functions do not use it.

File Layout

src/lib.rs          public facade and crate docs
src/options.rs      WebpOptions
src/error.rs        WebpError
src/encode.rs       hot path: validation + fast WebPConfig + libwebp-sys FFI
src/bytes.rs        detect_format + optional image-codecs bytes API
src/image_api.rs    image crate integration
src/pixel.rs        internal pixel layout and grayscale helpers
src/tests.rs        unit tests for public behavior
benches/convert.rs  Criterion comparison with webp crate
examples/           runnable API examples

Examples

cargo run --example dynamic_image -- input.jpg output.webp
cargo run --features image-codecs --example proxy_bytes -- input.jpg output.webp
cargo run --example raw_buffers -- output.webp

Benchmarks

cargo bench --features image-codecs

Current benchmark groups:

  • webp crate DynamicImage -> WebP
  • fast-webp DynamicImage -> WebP
  • fast-webp JPEG bytes -> WebP
  • fast-webp PNG bytes -> WebP

The benchmark generates images in memory so it is reproducible in CI. For proxy tuning, add your production JPEG/PNG corpus and run the same groups against those files.

Release

cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo package --allow-dirty
cargo publish --dry-run

Tag the release from the clean commit:

git tag -a v0.1.0 -m "fast-webp 0.1.0"
git push origin main --tags

After crates.io accepts the package:

cargo publish