webp-anim 0.1.1

Sequential, bounded-memory animated WebP processing for Rust
Documentation

webp-anim

CI Documentation

webp-anim is a reusable foundation for sequential, bounded-memory processing of animated WebP frame sequences.

It preserves animation semantics while frames are inspected, decoded, resized, transformed, and re-encoded: frame order, source durations, loop count, and ANIM background color remain explicit parts of the processing contract.

The input is a complete WebP byte slice; decoded frames are processed one at a time instead of being retained as a complete RGBA sequence.

Unlike a general-purpose WebP codec wrapper, this crate focuses on the animation-aware processing pipeline above the codec layer. It keeps other application concerns out of the crate: archive handling, GUI frameworks, playback queues, and application-specific output policies are not included.

Positioning

The crate is intended to sit between a WebP codec implementation and an application-level viewer, converter, or optimizer:

WebP codec
    ↓
webp-anim
    inspect → sequential decode → bounded frame transform → ordered encode
    ↓
consuming application

API at a glance

  • inspect classifies a complete WebP byte slice as static or animated and returns canvas and animation metadata without decoding every frame.
  • AnimationDecoder reads one stored animated sequence frame by frame as composited, full-canvas RGBA buffers, can inspect source durations without pixel decoding, and can reset the sequence without recreating the decoder.
  • ResizePlan derives one aspect-ratio-preserving resize operation; ResizeWorkspace reuses its destination allocation across frames.
  • AnimationEncoder accepts full-canvas RGBA frames in presentation order and writes an animated WebP byte vector.
  • transcode_animated_webp composes those stages for the common sequential decode-resize-encode path.

The modules under codec, inspect, model, resize, and transcode are public for applications that need the lower-level types. The same primary types are re-exported at the crate root for the common case.

Features

  • Inspect static and animated WebP containers.
  • Inspect stored animation frame durations without decoding RGBA pixels.
  • Decode one stored animation sequence frame by frame.
  • Reset a decoder to the stored sequence start for reuse.
  • Return composited full-canvas RGBA frames.
  • Preserve source frame durations without playback-time normalization.
  • Resize animation frames with a reusable plan and workspace.
  • Encode animated WebP frames in presentation order.
  • Preserve animation loop count and ANIM background color when transcoding.
  • Apply configurable input, canvas, frame-count, and RGBA buffer limits.
  • Avoid retaining the complete decoded RGBA frame sequence in the processing path.

The decoder processes one stored animation sequence and reports its loop count, but does not replay frames according to that count. Playback and repetition policy belong to the consuming application.

Input and output contract

All processing APIs accept a complete WebP byte slice. inspect accepts both static and animated WebP images. AnimationDecoder, AnimationEncoder, and transcode_animated_webp operate on exactly one stored animated sequence; they do not implement playback, loop replay, or application-level frame queues.

Decoded frames are owned, tightly packed RGBA8 buffers in row-major, full-canvas order. A frame for a width × height canvas therefore contains width * height * 4 bytes. The decoder returns the source frame duration without applying a playback minimum or other normalization.

The encoder requires durations that are an exact whole number of milliseconds. It converts each duration to the cumulative WebP timestamp and flushes the last frame duration when AnimationEncoder::finish is called. Every frame must use the encoder canvas and the exact RGBA buffer length.

AnimationDecoder::frame_durations returns one Duration per stored frame in source order. It reads animation-container metadata only: it does not decode RGBA pixels or advance the decoder's sequential frame state. The durations are subject to the decoder's configured total-duration limit.

AnimationDecoder::reset returns the existing decoder to the first stored frame without cloning the input or creating a new native decoder. It is a sequence reset, not a random-seek operation; the next next_frame call starts again at the first frame.

AnimationInfo::loop_count and AnimationInfo::background_color describe the stored WebP animation metadata. transcode_animated_webp copies both values to the output. BackgroundColor::raw is preserved verbatim; this crate does not assign a channel order or color-space interpretation to it.

Usage

Add the dependency:

[dependencies]
webp-anim = "0.1.1"

Inspect an input:

use webp_anim::{InspectLimits, WebpKind, inspect};

fn classify(input: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
    match inspect(input, InspectLimits::default())? {
        WebpKind::Static(info) => {
            println!("static WebP: {}x{}", info.canvas.width, info.canvas.height);
        }
        WebpKind::Animated(info) => {
            println!(
                "animated WebP: {}x{}, {} frames",
                info.canvas.width, info.canvas.height, info.frame_count
            );
        }
    }
    Ok(())
}

Process an animation one frame at a time:

use webp_anim::{AnimationDecoder, DecodeLimits};

fn process_frames(input: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
    let mut decoder = AnimationDecoder::new(input, DecodeLimits::default())?;
    let info = *decoder.info();

    println!("{} frames on a {}x{} canvas", info.frame_count, info.canvas.width, info.canvas.height);

    while let Some(frame) = decoder.next_frame()? {
        // Transform or consume this frame before requesting the next one.
        let _rgba = frame.rgba;
        let _duration = frame.duration;
    }

    Ok(())
}

Inspect durations and reuse the same decoder:

use webp_anim::{AnimationDecoder, DecodeLimits};

fn inspect_and_reuse(input: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
    let mut decoder = AnimationDecoder::new(input, DecodeLimits::default())?;
    let durations = decoder.frame_durations()?;
    println!("{} stored frame durations", durations.len());

    let _first = decoder.next_frame()?;
    decoder.reset();
    let _first_again = decoder.next_frame()?;
    Ok(())
}

The decoder owns only the input bytes and the current decoding state; callers can consume or hand off each decoded frame without collecting the complete animation in memory.

Resize and transcode one animation sequence:

use webp_anim::{
    AnimationTranscodeOptions, CanvasSize, ResizeOptions, transcode_animated_webp,
};

fn transcode(input: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    let options = AnimationTranscodeOptions::new(ResizeOptions::contain(CanvasSize {
        width: 1600,
        height: 2560,
    }));
    Ok(transcode_animated_webp(input, options)?.bytes)
}

transcode_animated_webp always produces an encoded result, including when the resize operation is a no-op. The caller decides whether the encoded bytes should replace the original input, for example by comparing output sizes.

For a keep-or-replace policy, the consuming application can make that decision after the transcode:

let result = transcode_animated_webp(input, options)?;
let output = if result.bytes.len() < input.len() {
    result.bytes
} else {
    input.to_vec()
};

Semantics and limits

Frames are returned as composited, full-canvas RGBA buffers. Frame durations are the source durations and are not adjusted for an application's minimum playback delay. The source loop count and ANIM background color are represented as explicit values and are retained by the transcode path.

AnimationDecoder::frame_durations returns source durations in stored frame order without advancing the RGBA decoder or decoding frame pixels. AnimationDecoder::reset reuses the decoder and restarts sequential decoding at the first stored frame; it does not provide random seeking.

The decoder applies configurable resource limits before and during processing. Applications should choose limits appropriate for their own input trust model and process-wide memory budget. The limits do not claim to bound allocations inside libwebp itself.

The default decoder and inspection limits are:

Limit Default
Input bytes 256 MiB
Canvas pixels 100,000,000
Stored frame count 10,000
Total source duration 1 hour (decoder only)
RGBA bytes per frame 400 MiB

ResizeOptions::max_output_rgba_bytes adds a destination-buffer limit for a resize plan. DecodeLimits::for_trusted_input and InspectLimits::for_trusted_input relax the crate-level checks, but do not bypass libwebp or platform allocation limits. Do not use them as a substitute for an application-wide resource policy when processing untrusted input.

Each fallible stage returns a typed error: InspectError, DecodeError, ResizeError, EncodeError, or TranscodeError. The fast classifier is_animated_webp_fast is intended only for routing and returns false for malformed input; use inspect when the reason for rejection matters.

Low-level sequential pipeline

Use a reusable ResizePlan and ResizeWorkspace when processing many frames. The workspace retains its resize state and destination buffer, while the caller can hand each output frame directly to an AnimationEncoder:

use std::time::Duration;

use webp_anim::{
    AnimationEncoder, AnimationEncoderOptions, CanvasSize, LoopCount,
    BackgroundColor, ResizeOptions, ResizePlan,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let source = CanvasSize { width: 320, height: 240 };
    let plan = ResizePlan::new(
        source,
        ResizeOptions::contain(CanvasSize { width: 160, height: 120 }),
    )?;
    let mut workspace = plan.workspace()?;
    let mut encoder = AnimationEncoder::new(
        plan.destination(),
        AnimationEncoderOptions::new(LoopCount::Infinite, BackgroundColor { raw: 0 }),
    )?;

    // For each decoded full-canvas frame:
    let mut rgba = vec![0; source.rgba_bytes().unwrap()];
    let duration = Duration::from_millis(100);
    workspace.transform_rgba(&mut rgba)?;
    encoder.add_rgba(workspace.pixels(), duration)?;

    let _encoded_webp = encoder.finish()?;
    Ok(())
}

The example shows the ownership and buffer contract; a real application would replace the placeholder rgba buffer with each frame returned by AnimationDecoder::next_frame.

Scope

This crate provides an animation-aware processing foundation. Playback control, GUI integration, worker lifecycle, archive orchestration, product presets, and application-wide concurrency or memory policies remain in the consuming application.

transcode_animated_webp is one convenience composition of the lower-level primitives, not the only processing pipeline. Applications with different policies can combine AnimationDecoder, ResizePlan, ResizeWorkspace, and AnimationEncoder directly.

Reference integrations

The following public applications provide real-world integration examples:

These projects show application-level playback and optimization policies while webp-anim remains independent of their archive, GUI, and product-specific code.

Requirements

  • Rust 1.85 or newer
  • Rust 2024 Edition

The crate uses libwebp-sys for WebP decoding and encoding.

License

Licensed under the MIT License.