oxideav-webp
Pure-Rust WebP image codec and container — RIFF/WEBP simple
(lossy VP8 + lossless VP8L) + extended (VP8X with ALPH, ICCP,
EXIF, XMP ) + animated (ANIM/ANMF) decode, plus single-frame
encode on both the VP8 lossy and VP8L lossless paths. Zero C
dependencies.
VP8 lossy decoding and encoding both go through
oxideav-vp8 (also pure-Rust).
VP8L lossless is a self-contained implementation of Google's Huffman +
LZ77 + colour-cache + four-transform bitstream in this crate.
Part of the oxideav framework but usable standalone.
Installation
[]
= "0.1"
= "0.1"
= "0.1"
= "0.0"
Standalone use (no oxideav-core)
Image-library consumers that just want to turn a .webp byte buffer
into RGBA pixels — no framework, no codec registry, no trait
objects — can depend on this crate with the default registry
feature off:
[]
= { = "0.0", = false }
That drops the oxideav-core dependency entirely (and cascades the
same off-switch through to oxideav-vp8) and exposes the
free-standing decode/encode entry points:
use ;
let img: WebpImage = decode_webp?;
for frame in &img.frames
# Ok::
WebpImage / WebpFrame / WebpFileMetadata already use
std-primitive fields (Vec<u8> RGBA, u32 dimensions). WebpError
covers InvalidData / Unsupported / Eof / NeedMore and
From-converts from oxideav_vp8::Vp8Error so the VP8 lossy path
composes through cleanly. Encoder entry points
(encode_vp8l_argb / encode_vp8l_argb_with,
build_animated_webp) likewise stay available without
oxideav-core. Turning the registry feature back on adds the
Decoder / Encoder / Demuxer trait implementations + the
register helpers + the WebpDecoder streaming type so the crate
plugs into the framework registry as before.
Quick use
.webp files carry one or many frames, so the typical path is: open
the file as a container, pull packets, decode them. Output is always
PixelFormat::Rgba regardless of whether the source chunk was VP8
(lossy YUV → RGB) or VP8L (native RGBA).
use ;
let mut ctx = new;
register;
let codecs = &ctx.codecs;
let containers = &ctx.containers;
let input: = Boxnew;
let mut dmx = containers.open?;
let stream = &dmx.streams;
let mut dec = new;
loop
# Ok::
For a one-shot decode of an in-memory buffer, skip the registry dance:
let bytes = read?;
let img = decode_webp?;
for frame in &img.frames
# Ok::
Encoder — VP8L (lossless, RGBA / RGB in)
use ;
let mut params = video;
params.width = Some;
params.height = Some;
params.pixel_format = Some; // or PixelFormat::Rgb24
let mut enc = codecs.make_encoder?;
enc.send_frame?;
let pkt = enc.receive_packet?; // complete .webp file
The registered webp_vp8l encoder accepts two input pixel formats:
Rgba— the historical default. Fully-opaque frames use the simpleRIFF/WEBP/VP8Llayout; frames with any transparent pixel switch to the extendedRIFF/WEBP/VP8X + VP8Llayout so the VP8X header advertises the alpha flag (required by any spec-compliant reader).Rgb24— three bytes per pixel, no alpha. Useful when the upstream is a JPEG decode or a PNG-without-alpha decode (the common case on theimagecrate side). The conversion to the encoder's internal ARGB pixel buffer streams through the input three bytes at a time — no intermediateRgbabyte buffer is materialised, so re-encoding an RGB image to WebP costs only the encoder's own working memory, not a full 4-byte expansion. Always emits the simple layout (Rgb24 is implicitly opaque). Closes #7.
If you need a bare VP8L bitstream (for embedding in another container,
say), call oxideav_webp::encode_vp8l_argb directly — that entry
point still returns the header-to-data bytes with no RIFF wrapper.
Encoder — VP8 (lossy)
let mut params = video;
params.width = Some;
params.height = Some;
// One of: Yuv420P, Yuva420P, Rgba, Rgb24
params.pixel_format = Some;
let mut enc = codecs.make_encoder?;
enc.send_frame?;
let pkt = enc.receive_packet?; // complete .webp file
Four input pixel formats are accepted:
Yuv420P— the native VP8 input. Emits the simpleRIFF/WEBP/VP8layout.Yuva420P— Yuv420P with a side full-resolution alpha plane. The YUV planes feed straight into the keyframe (no RGB roundtrip) and the alpha plane goes straight into a VP8L-compressedALPHsidecar. Emits the extendedRIFF/WEBP/VP8X + ALPH + VP8layout with the VP8XALPHAflag set.Rgba— converts RGB to YUV 4:2:0 (BT.601 limited range) for the VP8 keyframe and compresses the alpha plane into anALPHsidecar chunk. Emits the extendedRIFF/WEBP/VP8X + ALPH + VP8layout with the VP8XALPHAflag set.Rgb24— RGB without alpha. Streams the RGB→YUV conversion three bytes at a time without ever building a Rgba byte buffer (issue #7), and emits the simpleRIFF/WEBP/VP8layout.
Yuva420P is the natural input shape if you already have a
YUV-with-alpha frame from a video decoder. It avoids the YUV→RGB→YUV
roundtrip the Rgba path goes through.
Quality control: the VP8 lossy encoder exposes two equivalent factory entry points for picking a target compression level —
encoder_vp8::make_encoder_with_quality(¶ms, quality)— takes a libwebp-stylequality: f32in0.0..=100.0(higher = better quality / larger file; the libwebp default is75.0).encoder_vp8::make_encoder_with_qindex(¶ms, qindex)— takes the underlying VP8 qindex in0..=127(lower = better) for callers that already speak the libvpx scale.
The quality → qindex mapping is the linear inversion
qindex = round((100 - quality) * 1.27). As of #465 the per-quality
knob also drives the per-segment QP / LF deltas (§10 / §15.2) and
the per-frequency AC/DC quant deltas (§6.6 / §9.6) — at high quality
every delta collapses to zero, at low quality the high-frequency Y2
AC and chroma AC bins land on a coarser step while the macroblock-
mean (Y2 DC) bin holds finer to suppress visible block-mean banding.
File size is byte-strictly monotone with quality on AC-rich content
and bitstreams stay spec-compliant under libwebp's dwebp. Callers
that have already done their own perceptual tuning should reach for
the explicit *_and_freq_deltas factories, which pass the supplied
Vp8FreqDeltas through verbatim (no preset added on top).
Scope
Encoder scope (current):
- VP8L lossless from
RgbaorRgb24(single frame). Emits subtract-green + colour (G↔R/B decorrelation) + tile-based predictor- colour-indexing (palette) transforms plus a tunable colour cache.
The default
encode_vp8l_argbentry point runs a per-image RDO sweep over every combination of the four optional transforms × eight colour-cache widths ({off, 4, 6, 7, 8, 9, 10, 11} bits) × three predictor tile sizes ({8, 16, 32} px) and keeps the smallest encoded variant. Each trial also tries meta-Huffman per-tile grouping at K = 1 / 2 / 4 / 8 / 16 (gated by image pixel count: K=4 ≥ 4096 px, K=8 ≥ 16384 px, K=16 ≥ 65536 px) and picks the byte-smallest. Predictor pool covers all 14 RFC 9649 §4.1 modes per tile. LZ77 backreference search uses a 16384-pixel sliding window with up to 256 hash-chain candidates per starting position; the matcher runs a two-pass cost-modelled scan on the main image — pass 1 is greedy first-match, pass 2 re-walks the chain with a per-symbol-log2(p) × 16bit-cost model derived from the pass-1 histogram and picks each match by lowest bit-cost-per-pixel (plus a one-step lazy lookahead that defers a match if literal- here + match-at-i+1 bills fewer model bits). Optional near- lossless preprocessing (libwebp-compatible0..=100knob) collapses near-identical pixels into longer LZ77 runs / richer cache hits. Callers that want a fixed configuration callencode_vp8l_argb_withdirectly. Encoder ≈ 93 % libwebp parity on natural fixtures (≤ 1.13× cwebp on a 1024×768 photo, ≤ 1.06× on a 512×512 still, beats cwebp by 7.0 % on the in-tree 128×128 natural fixture and by 25.6 % on the 64×64 cache-stress fixture); residual gap is the entropy-image transform (per-tile entropy clustering driving meta-Huffman group assignment) and full Viterbi-style optimal LZ77.
- colour-indexing (palette) transforms plus a tunable colour cache.
The default
- VP8 lossy from
Yuv420P,Yuva420P,Rgba, orRgb24(single frame). ForYuva420PandRgbathe alpha plane is emitted as a VP8L-compressedALPHchunk inside the extended (VP8X) container;Yuva420Pskips the YUV→RGB→YUV roundtrip theRgbapath forces.Rgb24streams the RGB→YUV conversion without a Rgba alloc (issue #7). Per-segment quantiser deltas (RFC 6386 §10)- per-segment loop-filter deltas (§15.2) are wired in based on a
source-luma variance classifier, so smooth / textured regions get
finer / coarser quant + softer / stronger deblocking respectively.
Per-frequency AC/DC quantiser deltas (
y_dc_delta/y2_dc_delta/y2_ac_delta/uv_dc_delta/uv_ac_delta) are wired throughencoder_vp8::Vp8FreqDeltasand driven by the libwebp-stylequalityknob viafreq_deltas_for_qindex: zero at qindex=0, widening to[0, -2, +4, 0, +4]at qindex=127 so high-frequency bins compress harder and the macroblock-mean bin holds finer to suppress block-mean banding. Default qindex fromoxideav-vp8is used unless the caller selects one viaencoder_vp8::make_encoder_with_qindex(VP8 qindex0..=127, lower = better) or the libwebp-styleencoder_vp8::make_encoder_with_quality(0.0..=100.0, higher = better). Explicit*_and_freq_deltasfactories pass user freq-deltas through verbatim (zero argument reproduces the pre-#465 bitstream byte-for-byte). Encoder ≈ 90 % libwebp parity on natural fixtures; residual gap is psy-RDO + per-MB rate control.
- per-segment loop-filter deltas (§15.2) are wired in based on a
source-luma variance classifier, so smooth / textured regions get
finer / coarser quant + softer / stronger deblocking respectively.
Per-frequency AC/DC quantiser deltas (
VP8Xextended header is emitted automatically whenever the output carries anALPHsidecar or optional ICC / EXIF / XMP metadata via theriff::WebpMetadatahelper.- Animated WebP encode via [
build_animated_webp] / [build_animated_webp_with_options] — emits aVP8X + ANIM + ANMF...ANMFfile from a slice ofAnimFrames with per-frame durations, x/y offsets, blend, and disposal flags. Per-frameAnimFrameMode::Autoruns both VP8L and VP8+ALPH encoders and picks whichever sub-chunk is byte-smaller — animations can mix lossless and lossy frames, matching libwebp'sWebPAnimEncoderAddbehaviour. All four blend × dispose-to-background combinations round-trip through the in-crate decoder.
Decoder scope:
VP8simple lossy (throughoxideav-vp8),VP8Llossless,VP8Xextended withALPH(raw / filtered / VP8L-compressed alpha plane), andANIM/ANMFanimation with per-frame disposal + blend modes composited onto an internal RGBA canvas.ICCP/EXIF/XMPchunks are surfaced onWebpImage::metadata(aWebpFileMetadatastruct with optionalicc/exif/xmpbyte vectors). For metadata-only access without decoding any pixels, calloxideav_webp::extract_metadataon the file bytes directly.- Default output pixel format is
Rgba. For single-frame VP8+ALPH input,WebpDecoder::new_yuva420p(w, h)(orset_prefer_yuva420p(true)after construction) flips the output to a 4-planeYuva420Pframe — skipping the YUV→RGB conversion- alpha overlay the default path runs. VP8L and animated files always stay on the RGBA path (cross-frame composite needs a unified pixel format).
Codec / container IDs
- Codec:
"webp_vp8l"(VP8L encoder + standalone VP8L decoder); accepted input pixel formatsRgba,Rgb24. Decoded output is alwaysRgba. - Codec:
"webp_vp8"(VP8 lossy encoder path); accepted input pixel formatsYuv420P,Yuva420P,Rgba,Rgb24. - Container:
"webp", matches.webpby extension +RIFF/WEBPmagic.
Single-image WebPs decode to one VideoFrame; animated WebPs produce
N frames with PTS in milliseconds (the ANMF native unit).
For VP8+ALPH inputs the WebpDecoder defaults to Rgba output (the
historical behaviour). To opt into a 4-plane Yuva420P frame
straight from the VP8 + ALPH decoders — skipping the YUV→RGB
conversion + alpha overlay — construct the decoder with
WebpDecoder::new_yuva420p(w, h) (or set
set_prefer_yuva420p(true) after construction). VP8L and animated
files always go through the RGBA canvas because cross-frame
disposal/blend semantics need a unified pixel format.
image crate interop
If you already have a RgbImage (image::ImageBuffer<Rgb<u8>, _>)
from a JPEG decode or a PNG-without-alpha decode, you can feed its
backing Vec<u8> straight to the webp_vp8l or webp_vp8 encoder
with pixel_format = Some(PixelFormat::Rgb24) and a single-plane
VideoFrame { stride: w * 3, data: ... } — no Rgba allocation
required.
License
MIT — see LICENSE.