#gif #image-codec #gif-animation #animation #image

no-std zengif

Server-side GIF codec with zero-trust design, memory bounds, streaming, and full animation transparency support

7 releases (breaking)

new 0.6.0 Feb 8, 2026
0.5.0 Feb 5, 2026
0.4.0 Feb 4, 2026
0.3.0 Jan 24, 2026
0.1.0 Jan 18, 2026

#575 in Images

MIT/Apache and maybe GPL-3.0-or-later

310KB
6K SLoC

zengif

CI Crates.io Documentation codecov License

A GIF codec built for servers: streaming, memory-safe, and production-ready.

Getting Started

cargo add zengif

Decode a GIF

use zengif::{Decoder, Limits, Unstoppable};
use std::fs::File;
use std::io::BufReader;

fn main() -> zengif::Result<()> {
    let file = File::open("animation.gif")?;
    let reader = BufReader::new(file);

    let mut decoder = Decoder::new(reader, Limits::default(), &Unstoppable)?;

    println!("{}x{}, {} frames",
        decoder.metadata().width,
        decoder.metadata().height,
        decoder.metadata().frame_count_hint.unwrap_or(0));

    while let Some(frame) = decoder.next_frame()? {
        // frame.pixels: Vec<Rgba> - fully composited with transparency
        // frame.delay: u16 - delay in centiseconds (100ths of a second)
        println!("Frame {}: {}ms delay", frame.index, frame.delay as u32 * 10);
    }

    Ok(())
}

Encode a GIF

use zengif::{EncodeRequest, EncoderConfig, FrameInput, Limits, Repeat, Rgba, Unstoppable};

fn main() -> zengif::Result<()> {
    let width = 100;
    let height = 100;

    // Create 3 frames of solid colors
    let red: Vec<Rgba> = (0..width*height).map(|_| Rgba::rgb(255, 0, 0)).collect();
    let green: Vec<Rgba> = (0..width*height).map(|_| Rgba::rgb(0, 255, 0)).collect();
    let blue: Vec<Rgba> = (0..width*height).map(|_| Rgba::rgb(0, 0, 255)).collect();

    let config = EncoderConfig::new().repeat(Repeat::Infinite);
    let limits = Limits::default();

    let mut encoder = EncodeRequest::new(&config, width, height)
        .limits(&limits)
        .stop(&Unstoppable)
        .build()?;

    encoder.add_frame(FrameInput::new(width, height, 50, red))?;   // 500ms
    encoder.add_frame(FrameInput::new(width, height, 50, green))?; // 500ms
    encoder.add_frame(FrameInput::new(width, height, 50, blue))?;  // 500ms

    let output = encoder.finish()?;

    std::fs::write("output.gif", &output)?;
    Ok(())
}

Why zengif?

If you're building a server that handles untrusted GIF uploads, you need:

  • Memory limits - Reject oversized images before allocating
  • Cancellation - Stop processing if the request is cancelled
  • Error context - Know exactly where parsing failed
  • Correct compositing - Handle all disposal methods and transparency

zengif builds on the excellent gif crate, adding these production features:

Feature gif crate zengif
Streaming decode
Memory limits
Frame compositing ❌ (use gif-dispose) ✅ built-in
Cooperative cancellation
Error tracing (file:line)
High-quality encoding ✅ (optional)

Memory Protection

Protect your server from malicious inputs:

use zengif::Limits;

let limits = Limits::default()
    .max_dimensions(4096, 4096)       // Reject huge canvases
    .max_frame_count(1000)            // Limit animation length
    .max_memory(256 * 1024 * 1024);   // 256 MB peak memory

The decoder will return an error before allocating if limits would be exceeded.

Cancellation

For web servers, you often need to stop processing if the client disconnects:

use almost_enough::Stopper;
use zengif::{Decoder, Limits};

let stop = Stopper::new();
let stop_for_handler = stop.clone();

// In your request handler, if client disconnects:
stop_for_handler.cancel();

// The decoder will return GifError::Cancelled at the next check point
let mut decoder = Decoder::new(reader, Limits::default(), stop)?;

Error Diagnostics

When something goes wrong, you get the full story:

Error: InvalidFrameBounds { frame_left: 0, frame_top: 0, frame_width: 5000,
                            frame_height: 5000, canvas_width: 100, canvas_height: 100 }
   at src/decode/frame.rs:142:9
      ╰─ validating frame 3
   at src/decode/mod.rs:89:5
      ╰─ in decode_frame

High-Quality Encoding

For the smallest file sizes, enable the imagequant feature:

cargo add zengif --features imagequant
use zengif::{EncoderConfig, Quantizer};

let config = EncoderConfig::new()
    .quantizer(Quantizer::imagequant());  // Best quality, smallest files

Quantizer Options

Feature License Quality Speed
imagequant GPL-3.0* Best Medium
quantizr MIT Good Fast
color_quant MIT Good Fastest

*imagequant is GPL-3.0-or-later. Commercial license available from upstream.

Why imagequant produces smaller files: imagequant's superior quality comes from more compressible dithering and quantization patterns. The resulting palettes compress dramatically better in the LZW stage, producing smaller final GIF files compared to other quantizers at the same visual quality.

Without any quantizer feature, zengif is MIT/Apache-2.0 licensed.

no_std / WASM

For WASM or embedded, disable the default std feature:

zengif = { version = "0.6", default-features = false }

You get core types (Rgba, Limits, GifError, etc.) but not the codec. Useful when you need to share types between WASM and native code.

Performance

On AMD Ryzen 9 5900X:

Operation Throughput
Decode (composited) ~150 MB/s
Encode (quantized) ~40 MB/s
Encode (pre-indexed) ~200 MB/s

License

MIT or Apache-2.0, at your option.

The optional imagequant feature uses libimagequant (GPL-3.0-or-later). A commercial license is available from the upstream author for closed-source use. Alternatively, use quantizr or color_quant for fully permissive licensing.


100% safe Rust - #![forbid(unsafe_code)]

Dependencies

~0.9–1.4MB
~28K SLoC