Server-side GIF codec with zero-trust design, memory bounds, streaming, and full animation transparency support.
zengif combines streaming GIF codec capabilities with server-side production features:
| Feature | gif + gif-dispose | zengif |
|---|---|---|
| Streaming decode | ✅ | ✅ |
| Disposal + transparency | ✅ | ✅ |
| Memory limits | ✅ | ✅ |
| Cooperative cancellation | ❌ | ✅ |
| Error tracing (file:line) | ❌ | ✅ |
| Round-trip metadata | ❌ | ✅ |
| High-quality encode | ❌ | ✅ |
The gif crate provides excellent low-level GIF parsing with memory limits. gif-dispose adds disposal method handling. zengif wraps these and adds: cancellation support, error tracing with file:line info, and a unified API for encode/decode with metadata preservation.
- Streaming decode/encode - Process GIFs without loading entire file into memory
- Complete animation support - All disposal methods (Keep, Background, Previous) + transparency working together
- Memory bounded - Configurable limits, reject oversized inputs before allocating
- Production ready - Error tracing via
whereat, cancellation viaenough - Zero-trust design - Validate headers, bounds-check frames, limit decompression ratio
- Round-trip fidelity - Frame timing, loop count, palette, and disposal methods preserved
- High-quality encoding - Optional imagequant integration for small file sizes
use zengif::{Decoder, Limits, Stats};
use enough::Unstoppable;
let limits = Limits::default()
.max_dimensions(4096, 4096)
.max_frame_count(1000);
let stats = Stats::new();
let cursor = std::io::Cursor::new(gif_data);
let mut decoder = Decoder::new(cursor, limits, &stats, Unstoppable)?;
while let Some(frame) = decoder.next_frame()? {
// frame.pixels: Vec<Rgba> - composited RGBA with disposal applied
// frame.delay: u16 - delay in centiseconds
// frame.index: usize - frame number
}
println!("Peak buffer usage: {} bytes", stats.peak());use zengif::{Encoder, EncoderConfig, FrameInput, Limits, Repeat, Rgba};
use enough::Unstoppable;
let config = EncoderConfig::new(width, height).repeat(Repeat::Infinite);
let limits = Limits::default();
let mut output = Vec::new();
let mut encoder = Encoder::new(&mut output, config, limits, Unstoppable)?;
for frame in source_frames {
encoder.add_frame(frame)?;
}
encoder.finish()?;use zengif::{Decoder, Encoder, Limits, Stats};
use enough::Unstoppable;
let stats = Stats::new();
let mut decoder = Decoder::new(reader, Limits::default(), &stats, Unstoppable)?;
let metadata = decoder.metadata().clone();
let mut encoder = Encoder::from_metadata(writer, &metadata, Limits::default(), Unstoppable)?;
while let Some(frame) = decoder.next_frame()? {
// Re-encode the composed frame
let input = FrameInput::new(frame.width, frame.height, frame.delay, frame.pixels);
encoder.add_frame(input)?;
}
encoder.finish()?;use almost_enough::Stopper;
use zengif::{Decoder, Limits, Stats};
let stop = Stopper::new();
let stop_clone = stop.clone();
// In another thread/task:
stop_clone.cancel();
// Decoder will return GifError::Cancelled
let stats = Stats::new();
let decoder = Decoder::new(reader, Limits::default(), &stats, stop)?;Protect against malicious inputs:
use zengif::Limits;
let limits = Limits::default()
.max_dimensions(8192, 8192) // Max canvas size
.max_total_pixels(67_108_864) // Max 64M pixels per frame
.max_frame_count(10_000) // Max frames
.max_file_size(500 * 1024 * 1024) // 500 MB
.max_memory(1024 * 1024 * 1024); // 1 GB peak memoryErrors include location and context for debugging:
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
| Feature | Default | Description |
|---|---|---|
std |
✅ | Enable std library (disable for no_std+alloc) |
simd |
❌ | SIMD acceleration via wide/multiversed |
rgb-interop |
❌ | Interop with the rgb crate |
imgref-interop |
❌ | Interop with the imgref crate |
zengif supports no_std environments with alloc. Disable the default std feature:
[dependencies]
zengif = { version = "0.3", default-features = false }With std (default): Full codec - decoder, encoder, quantizers, heuristics.
Without std: Core types only - GifError, Limits, Stats, Rgba, Palette, ComposedFrame, Screen, Disposal. Useful for WASM or embedded where you process frames but use a different codec.
Verified targets: wasm32-unknown-unknown (144KB release build with decoder).
Choose one or more quantization backends for high-quality GIF encoding:
| Feature | License | Quality | Speed | Notes |
|---|---|---|---|---|
imagequant |
AGPL-3.0 | Best | Medium | Recommended - best quality AND smallest files (LZW-aware dithering), commercial license |
quantizr |
MIT | Good | Fast | Best MIT-licensed option |
color_quant |
MIT | Good | Fastest | High-throughput servers |
exoquant-deprecated |
MIT | Good | Slow | Deprecated - use quantizr instead |
Without any quantization feature, zengif is purely MIT/Apache-2.0 licensed.
Benchmarks on AMD Ryzen 9 5900X:
| Operation | Throughput |
|---|---|
| Decode (composited) | ~150 MB/s |
| Encode (quantized) | ~40 MB/s |
| Encode (pre-indexed) | ~200 MB/s |
Developed with Claude (Anthropic). Not all code manually reviewed. Review critical paths before production use.
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
The optional imagequant feature uses libimagequant by Kornel Lesiński, which is licensed under AGPL-3.0. For closed-source projects, please purchase a commercial license — it's reasonably priced and supports continued development of excellent image optimization tools.
Alternative MIT-licensed quantizers: quantizr or color_quant for fully permissive licensing (slightly lower quality).
Without any quantization feature (the default), zengif has no AGPL dependencies and is purely MIT/Apache-2.0 licensed.