Rust's WebP story requires two crates: image for loading JPEG and webp for encoding. The webp crate wraps Google's libwebp C library, which produces byte-for-byte identical output to cwebp. For batch image processing in Rust, this combination gives excellent performance — Rayon can parallelize conversions across all cores trivially.
Method 1: image + webp crates (libwebp bindings)
The webp crate wraps Google's libwebp encoder. Combined with the image crate for JPEG loading, this is the idiomatic Rust path.
# Cargo.toml
[dependencies]
image = "0.25"
webp = "0.3"
# Note: webp crate links against libwebp — needs libwebp-dev on Linux
# Ubuntu: apt install libwebp-devuse image::GenericImageView;
use std::path::Path;
use std::fs;
fn jpg_to_webp(
src: &str,
dst: &str,
quality: f32, // 0.0 to 100.0; 80.0 is a good default
) -> Result<(), Box> {
let img = image::open(src)?;
let (width, height) = img.dimensions();
// Convert to RGBA for the webp encoder
let rgba = img.to_rgba8();
let encoder = webp::Encoder::from_rgba(rgba.as_raw(), width, height);
let webp_data = encoder.encode(quality);
fs::write(dst, &*webp_data)?;
println!("Converted: {} -> {} ({} bytes)", src, dst, webp_data.len());
Ok(())
}
fn jpg_to_webp_lossless(
src: &str,
dst: &str,
) -> Result<(), Box> {
let img = image::open(src)?;
let (width, height) = img.dimensions();
let rgba = img.to_rgba8();
let encoder = webp::Encoder::from_rgba(rgba.as_raw(), width, height);
let webp_data = encoder.encode_lossless();
fs::write(dst, &*webp_data)?;
Ok(())
}
fn main() -> Result<(), Box> {
jpg_to_webp("photo.jpg", "photo.webp", 80.0)?;
Ok(())
}
// Parallel batch conversion with Rayon
use rayon::prelude::*;
fn batch_jpg_to_webp(
src_dir: &str,
out_dir: &str,
quality: f32,
) -> Result> {
fs::create_dir_all(out_dir)?;
let entries: Vec<_> = fs::read_dir(src_dir)?
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name().to_string_lossy().to_lowercase();
name.ends_with(".jpg") || name.ends_with(".jpeg")
})
.collect();
let count = entries.len();
entries.par_iter().try_for_each(|entry| -> Result<(), Box> {
let src = entry.path();
let stem = src.file_stem().unwrap().to_string_lossy();
let dst = format!("{}/{}.webp", out_dir, stem);
let img = image::open(&src)?;
let (w, h) = img.dimensions();
let rgba = img.to_rgba8();
let encoder = webp::Encoder::from_rgba(rgba.as_raw(), w, h);
let data = encoder.encode(quality);
fs::write(&dst, &*data)?;
Ok(())
})?;
Ok(count)
}
Method 2: image crate with WebP feature (pure Rust, no libwebp)
The image crate includes a pure-Rust WebP encoder behind the webp feature flag. No system library needed — it compiles fully from source. The output quality is slightly lower than libwebp but good enough for most use cases.
# Cargo.toml
[dependencies]
image = { version = "0.25", features = ["webp"] }use std::fs;
fn jpg_to_webp_pure(
src: &str,
dst: &str,
) -> Result<(), Box> {
let img = image::open(src)?;
// image crate's WebP encoder — no quality control in current version,
// uses a fixed lossless-ish mode. For quality control, use the webp crate.
img.save(dst)?; // auto-detects WebP from .webp extension
println!("Saved: {}", dst);
Ok(())
}
fn main() -> Result<(), Box> {
jpg_to_webp_pure("photo.jpg", "photo.webp")?;
Ok(())
}
The image crate's built-in WebP encoder doesn't yet support quality control — it uses a default setting. For production use where you need quality tuning, use the webp crate. The advantage of the image crate's encoder is zero C dependencies — it compiles to WASM and cross-compiles cleanly to any Rust target.
Method 3: ChangeThisFile API (reqwest, no libwebp)
The API handles WebP encoding server-side. Source auto-detected from filename — pass target=webp. Free tier: 1,000 conversions/month, no card needed.
# Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["multipart", "blocking"] }use reqwest::blocking::{Client, multipart};
use std::fs;
use std::path::Path;
const API_KEY: &str = "ctf_sk_your_key_here";
fn jpg_to_webp_api(
src: &str,
dst: &str,
) -> Result<(), Box> {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(60))
.build()?;
let file_bytes = fs::read(src)?;
let filename = Path::new(src)
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
let form = multipart::Form::new()
.part(
"file",
multipart::Part::bytes(file_bytes)
.file_name(filename)
.mime_str("image/jpeg")?,
)
.text("target", "webp");
let response = client
.post("https://changethisfile.com/v1/convert")
.header("Authorization", format!("Bearer {}", API_KEY))
.multipart(form)
.send()?;
if !response.status().is_success() {
return Err(format!("API error: {}", response.status()).into());
}
fs::write(dst, response.bytes()?)?;
Ok(())
}
fn main() -> Result<(), Box> {
jpg_to_webp_api("photo.jpg", "photo.webp")?;
println!("Done");
Ok(())
}
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| image + webp crates | Production quality, quality control, batch with Rayon | Requires libwebp-dev system package |
| image crate (pure Rust) | Cross-compilation, WASM, zero C deps | No quality control in current version; slightly lower compression |
| ChangeThisFile API | No libwebp, minimal binary footprint | Network call per image; 25MB free tier limit |
Production tips
- Use Rayon for batch conversion. WebP encoding is CPU-bound and embarrassingly parallel. par_iter() across a directory of images gives near-linear speedup with core count.
- Build with --release for production. Debug builds are 10-20x slower for image processing. Always use cargo build --release for benchmarks and production deployments.
- Static link libwebp to avoid deployment issues. Add WEBP_SYS_STATIC=1 to your build environment to statically link libwebp. This creates a fully self-contained binary.
- Quality 80.0 is a good default. The webp crate's quality parameter is 0.0-100.0. Quality 80 produces files roughly comparable to JPEG quality 90 in visual quality.
- Handle large images with image::DynamicImage::resize_to_fill. For thumbnail generation, resize before encoding: img.resize(width, height, image::imageops::FilterType::Lanczos3).
The image + webp crates are the right choice for production Rust services that need quality control. For cross-compilation or WASM targets, the image crate's pure-Rust encoder avoids C dependencies. For services where minimizing native deps matters, the API works via reqwest. Free tier: 1,000 conversions/month.