Skip to content

imazen/webpx

Repository files navigation

webpx

CI Crates.io Docs.rs codecov License

Ergonomic FFI bindings to Google's libwebp, with support for static images, animations, ICC profiles, streaming, and no_std.

Use zenwebp instead

For any new project, reach for zenwebp. It is equally or more capable than webpx on every axis that matters:

  • Full feature parity with libwebp: lossy and lossless encode and decode, animation, alpha, ICC / EXIF / XMP metadata, streaming, content presets, resource limits.
  • Native wasm32-unknown-unknown support — pure Rust, no C compiler, no emscripten. webpx requires emscripten because libwebp is C.
  • #![forbid(unsafe_code)] — pure Rust top to bottom. Zero FFI surface, zero unsafe blocks.

Performance and compression are essentially a wash:

  • libwebp can be up to 35 % faster on specific photos, but it can also be up to 2.5× slower on others. Net wash unless you're tuned to specific content types.
  • Encoded-size difference is at most ~0.02 % — noise, not meaningful.

The security argument is concrete, not theoretical:

  • libwebp has a documented history of high-severity vulnerabilities. CVE-2023-4863 was a heap buffer overflow in BuildHuffmanTable actively exploited in the wild against Chrome, Safari, Firefox, and Electron apps via a 0-click attack chain — patched out of band on every major platform. That is the failure mode an FFI wrapper inherits, not a hypothetical.
  • Every libwebp wrapper that has been audited has shipped soundness bugs, webpx included. Versions 0.1.0–0.1.4 are yanked, and 0.2.0 + 0.2.1 fixed multiple stride-overflow / use-after-free / aliasing issues found across two parallel audit passes. If you adopt a libwebp wrapper, you are taking on that exposure.

zenwebp's #![forbid(unsafe_code)] makes that whole class of bug structurally impossible. Use it.

webpx is maintained for users whose application already links libwebp through another path (existing C / C++ code, system package) and would prefer to share that codebase, or who specifically need libwebp's MIPS DSP code paths. If that's not you, switch.

Why use webpx anyway?

  • Ergonomic Rust API — Builder patterns, strong types, comprehensive error handling, Limits policy for untrusted-input decoding
  • Shares an existing libwebp link — If your application already links libwebp via another path (C / C++ code, system package), webpx reuses that codebase rather than pulling in a second WebP implementation
  • MIPS DSP — libwebp ships hand-written MIPS DSP-R2 / DSP-ASE assembly paths. If you target that hardware, webpx inherits them; zenwebp does not.
  • Up to 35 % faster on specific photos — libwebp's hand-tuned VP8 path beats pure-Rust on some content. Note that it can also be up to 2.5× slower on other content; benchmark your actual workload.

Quick Start

[dependencies]
webpx = "0.2"
use webpx::{Encoder, decode_rgba, Unstoppable};

// Encode RGBA pixels to WebP
let webp = Encoder::new_rgba(&pixels, width, height)
    .quality(85.0)
    .encode(Unstoppable)?;

// Decode WebP back to RGBA
let (pixels, w, h) = decode_rgba(&webp)?;

Decoding untrusted input

Limits::default() applies opinionated production caps suited to typical web / image-server use, so the default DecoderConfig and AnimationDecoder paths are already bounded. Defaults: 64 MP per frame, 256 MP cumulative, 16383×16383 (libwebp's intrinsic limit), 64 MiB input, 4096 frames, 5 min animation, 4 MiB metadata, 256 MiB output. Override individual fields via the with_* builders on top of Limits::default(), or use Limits::none() to opt out entirely (only when you fully trust the input).

use webpx::{Decoder, DecoderConfig, Limits};

// Tighter than default: 16 MP per frame for a thumbnail decoder.
let limits = Limits::default().with_max_pixels(16 * 1024 * 1024);

let img = Decoder::new(webp_data)?
    .config(DecoderConfig::new().limits(limits))
    .decode_rgba()?;

The same Limits value also wires into AnimationDecoder::with_options_limits and mux::get_icc_profile_with_limits (and the _with_limits variants for EXIF / XMP). Field naming matches zencodec::ResourceLimits so a single shared policy carries cleanly between Imazen codecs.

Features at a Glance

Feature Description
Lossy Encoding VP8-based compression with quality 0-100
Lossless Encoding Exact pixel preservation
Alpha Channel Full transparency support with separate quality control
Animation Multi-frame WebP with timing control
ICC Profiles Embed/extract color profiles
EXIF/XMP Preserve camera metadata
Streaming Decode as data arrives
Cropping/Scaling Decode to any size
YUV Support Direct YUV420 input/output
Content Presets Optimized settings for photos, drawings, icons, text
Resource Limits Limits policy: per-frame & cumulative pixel caps, frame count, metadata size, ...
Cancellation Cooperative cancellation via enough crate

Examples

Basic Encoding

use webpx::{Encoder, Unstoppable};

// Lossy encoding (quality 0-100)
let webp = Encoder::new_rgba(&rgba_data, 640, 480)
    .quality(85.0)
    .encode(Unstoppable)?;

// Lossless encoding (exact pixels)
let webp = Encoder::new_rgba(&rgba_data, 640, 480)
    .lossless(true)
    .encode(Unstoppable)?;

// RGB without alpha
let webp = Encoder::new_rgb(&rgb_data, 640, 480)
    .quality(85.0)
    .encode(Unstoppable)?;

Builder API with Options

use webpx::{Encoder, Preset, Unstoppable};

let webp = Encoder::new_rgba(&rgba_data, 640, 480)
    .preset(Preset::Photo)    // Content-aware optimization
    .quality(90.0)            // Higher quality
    .method(5)                // Better compression (slower)
    .alpha_quality(95)        // High-quality alpha
    .sharp_yuv(true)          // Better color accuracy
    .encode(Unstoppable)?;

Advanced Configuration

use webpx::EncoderConfig;

// Maximum compression (slow but smallest files)
let config = EncoderConfig::max_compression();
let webp = config.encode_rgba(&data, width, height)?;

// Maximum quality lossless
let config = EncoderConfig::max_compression_lossless();
let webp = config.encode_rgba(&data, width, height)?;

// Fine-grained control
let config = EncoderConfig::new()
    .quality(85.0)
    .method(6)
    .filter_strength(60)
    .sns_strength(80)
    .segments(4)
    .pass(6)
    .preprocessing(4);
let (webp, stats) = config.encode_rgba_with_stats(&data, width, height)?;
println!("PSNR: {:.2} dB, size: {} bytes", stats.psnr[4], stats.coded_size);

Decoding with Processing

use webpx::Decoder;

let decoder = Decoder::new(&webp_data)?;

// Get image info without decoding
let info = decoder.info();
println!("{}x{}, alpha: {}", info.width, info.height, info.has_alpha);

// Decode with cropping and scaling
let (pixels, w, h) = decoder
    .crop(100, 100, 400, 300)  // Extract region
    .scale(200, 150)           // Resize
    .decode_rgba_raw()?;

Animation

use webpx::{AnimationEncoder, AnimationDecoder, ColorMode, Limits};

// Create animated WebP
let mut encoder = AnimationEncoder::new(320, 240)?;
encoder.set_quality(80.0);
encoder.set_lossless(false);

encoder.add_frame_rgba(&frame1_rgba, 0)?;     // Start at 0ms
encoder.add_frame_rgba(&frame2_rgba, 100)?;   // Show at 100ms
encoder.add_frame_rgba(&frame3_rgba, 200)?;   // Show at 200ms
let webp = encoder.finish(300)?;              // End timestamp

// Decode animation. Use `with_options_limits` if the input is
// untrusted — `max_total_pixels` covers the W × H × frame_count
// case (a 1000×1000 × 200-frame animation has 200 MP cumulative
// even when each frame fits a per-frame `max_pixels` cap).
let limits = Limits::none()
    .with_max_pixels(64 * 1024 * 1024)
    .with_max_total_pixels(256 * 1024 * 1024)
    .with_max_frames(1024)
    .with_max_animation_ms(60_000)            // 60 s of animation
    .with_max_input_bytes(64 * 1024 * 1024);  // 64 MB bitstream
let mut decoder = AnimationDecoder::with_options_limits(
    &webp, ColorMode::Rgba, true, &limits,
)?;
let info = decoder.info();
println!("{} frames, {}x{}", info.frame_count, info.width, info.height);

// Iterate frames
while let Some(frame) = decoder.next_frame()? {
    render(&frame.data, frame.timestamp_ms);
}

// Or get all at once (this also enforces `max_animation_ms` against
// the cumulative timestamp).
decoder.reset();
let frames = decoder.decode_all()?;

ICC Profiles & Metadata

use webpx::{embed_icc, get_icc_profile_with_limits, embed_exif, get_exif_with_limits, Limits};

// Embed ICC profile
let webp_with_icc = embed_icc(&webp_data, &srgb_profile)?;

// Extract — apply `max_metadata_bytes` to bound the ICCP/EXIF/XMP
// chunk size even if the bitstream declares it as huge. Without
// limits, an internal 256 MiB hard cap still applies.
let limits = Limits::none().with_max_metadata_bytes(4 * 1024 * 1024);
if let Some(icc) = get_icc_profile_with_limits(&webp_data, &limits)? {
    println!("ICC profile: {} bytes", icc.len());
}

// EXIF data
let webp_with_exif = embed_exif(&webp_data, &exif_bytes)?;
if let Some(exif) = get_exif_with_limits(&webp_data, &limits)? {
    // Parse EXIF...
}

Streaming Decode

use webpx::{StreamingDecoder, DecodeStatus, ColorMode};

let mut decoder = StreamingDecoder::new(ColorMode::Rgba)?;

// Feed data as it arrives
for chunk in network_stream {
    match decoder.append(&chunk)? {
        DecodeStatus::Complete => break,
        DecodeStatus::NeedMoreData => continue,
        DecodeStatus::Partial(rows) => {
            // Progressive display
            if let Some((data, w, h)) = decoder.get_partial() {
                display_partial(data, w, h);
            }
        }
        _ => {} // Handle future variants
    }
}

let (pixels, width, height) = decoder.finish()?;

Cooperative Cancellation

Encoding can be cancelled cooperatively using the enough crate:

use webpx::{Encoder, Error, StopReason};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

// Create a cancellation flag
let cancelled = Arc::new(AtomicBool::new(false));
let flag = cancelled.clone();

// Custom Stop implementation
struct MyCanceller(Arc<AtomicBool>);
impl enough::Stop for MyCanceller {
    fn check(&self) -> Result<(), enough::StopReason> {
        if self.0.load(Ordering::Relaxed) {
            Err(enough::StopReason::Cancelled)
        } else {
            Ok(())
        }
    }
}

// In another thread: flag.store(true, Ordering::Relaxed);

match Encoder::new_rgba(&data, width, height)
    .quality(85.0)
    .encode(MyCanceller(cancelled))
{
    Ok(webp) => { /* success */ },
    Err(Error::Stopped(StopReason::Cancelled)) => { /* cancelled */ },
    Err(e) => { /* other error */ },
}

For ready-to-use cancellation primitives (timeouts, channels, etc.), see the almost-enough crate.

Feature Flags

Feature Default Description
decode Yes WebP decoding
encode Yes WebP encoding
std Yes Use std (disable for no_std + alloc)
animation No Animated WebP support
icc No ICC/EXIF/XMP metadata
streaming No Incremental decode/encode
# All features
webpx = { version = "0.1", features = ["animation", "icc", "streaming"] }

# no_std
webpx = { version = "0.1", default-features = false, features = ["decode", "encode"] }

Content Presets

Choose a preset to optimize for your content type:

Preset Best For Characteristics
Default General use Balanced settings
Photo Photographs Better color, outdoor scenes
Picture Indoor/portraits Skin tone optimization
Drawing Line art High contrast, sharp edges
Icon Small images Color preservation
Text Screenshots Crisp text rendering
use webpx::{Encoder, Preset, Unstoppable};

let webp = Encoder::new_rgba(&data, w, h)
    .preset(Preset::Photo)
    .encode(Unstoppable)?;

Platform Support

Platform Status
Linux x64/ARM64 ✅ Full support
macOS x64/ARM64 ✅ Full support
Windows x64/ARM64 ✅ Full support
WebAssembly (emscripten) ✅ Supported
WebAssembly (wasm32-unknown-unknown) ❌ Not supported (use zenwebp, which is native to this target)
MIPS / MIPS DSP ✅ Inherits libwebp's hand-tuned DSP-R2 paths

Building for WebAssembly

# Install emscripten
git clone https://github.com/emscripten-core/emsdk.git ~/emsdk
cd ~/emsdk && ./emsdk install latest && ./emsdk activate latest

# Add target and build
rustup target add wasm32-unknown-emscripten
source ~/emsdk/emsdk_env.sh
cargo build --target wasm32-unknown-emscripten --release

Migration from Other Crates

From webp crate

// Before
use webp::{Encoder, Decoder};

// After - use compat shim
use webpx::compat::webp::{Encoder, Decoder};
// API is compatible, just change the import

From webp-animation crate

// Before
use webp_animation::{Encoder, Decoder};

// After - use compat shim
use webpx::compat::webp_animation::{Encoder, Decoder};
// Uses finalize() instead of finish() to match original API

Performance Tips

  1. Use appropriate method - Higher values (4-6) give better compression but are slower
  2. Choose the right preset - Presets tune internal parameters for content type
  3. Consider sharp_yuv - Better color accuracy at slight speed cost
  4. Batch frames - For animations, encode multiple frames before finalizing
  5. Pre-allocate buffers - Use StreamingDecoder::with_buffer() to avoid allocations

Minimum Supported Rust Version

Rust 1.80 or later.

License

Licensed under either of:

at your option.

Contributing

Contributions welcome! Please open issues and pull requests on GitHub.

AI-Generated Code Notice

This crate was developed with assistance from Claude (Anthropic). Not all code has been manually reviewed. Please review critical paths before production use.

About

Complete WebP encoding/decoding for Rust - safe bindings to libwebp

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Packages

 
 
 

Contributors