LiteGif 0.2.0
dotnet add package LiteGif --version 0.2.0
NuGet\Install-Package LiteGif -Version 0.2.0
<PackageReference Include="LiteGif" Version="0.2.0" />
<PackageVersion Include="LiteGif" Version="0.2.0" />
<PackageReference Include="LiteGif" />
paket add LiteGif --version 0.2.0
#r "nuget: LiteGif, 0.2.0"
#:package LiteGif@0.2.0
#addin nuget:?package=LiteGif&version=0.2.0
#tool nuget:?package=LiteGif&version=0.2.0
LiteGif
A lightweight, pure-managed GIF encoder for .NET 10. Zero production dependencies, MIT license.
LiteGif writes a streaming GIF89a file from either pre-indexed frames (a global Rgb palette plus byte indices) or full-color Rgba32 frames that are quantized with an octree color quantizer. It is intentionally small, has no external runtime dependencies, and is easy to drop into a project.
Features
- Targets
net10.0only; pure managed C# with zero productionPackageReferences. - Indexed animated GIF encoding from a caller-supplied palette and per-frame indices.
- Full-color animated GIF encoding from
Rgba32pixels (auto-quantized with an octree quantizer; by default the first frame's palette becomes the Global Color Table shared by all frames). - Optional per-frame Local Color Tables: pass a non-empty
localPaletteonAddFrame, or enable full-color auto multi-palette withUseGlobalColorTableFallback = false. Default (true) stays GCT-only; indexed mode never auto-generates an LCT. - Configurable loop count, default frame delay, transparent/background color index, and quantization settings.
- Standalone
LzwEncoderfor raw GIF image-data compression. - Valid GIF89a output: header, Logical Screen Descriptor, Global Color Table, optional NETSCAPE2.0 loop extension, per-frame Graphics Control Extension, full-frame Image Descriptor, LZW image data, and
0x3Btrailer. - Not thread-safe; create one
GifEncoderper stream.
Installation
Add the package to a .NET 10 project:
dotnet add package LiteGif
Or reference the LiteGif project directly in your solution.
Quick start — indexed animated GIF
using LiteGif;
using System.IO;
int width = 320;
int height = 240;
int frameCount = 30;
var palette = new Rgb[256];
for (int i = 0; i < palette.Length; i++)
palette[i] = new Rgb((byte)i, (byte)(i * 2), (byte)(i * 3));
var indices = new byte[width * height];
for (int i = 0; i < indices.Length; i++)
indices[i] = (byte)(i % 256);
using var encoder = new GifEncoder(
File.OpenWrite("indexed.gif"),
width,
height,
palette,
new GifEncoderOptions
{
LoopCount = 0, // 0/negative = infinite, 1 = once, N>1 = N plays
DefaultFrameDelayMs = 100, // used when AddFrame is called without delayMs
TransparentIndex = 0, // optional: index 0 is transparent
BackgroundIndex = 0
});
for (int f = 0; f < frameCount; f++)
encoder.AddFrame(indices, delayMs: 100);
// Dispose writes the GIF89a trailer.
Quick start — full-color animated GIF
using LiteGif;
using System.IO;
int width = 320;
int height = 240;
int frameCount = 30;
var pixels = new Rgba32[width * height];
for (int i = 0; i < pixels.Length; i++)
{
int x = i % width;
int y = i / width;
pixels[i] = new Rgba32(
(byte)(x * 255 / (width - 1)),
(byte)(y * 255 / (height - 1)),
128,
255);
}
using var encoder = new GifEncoder(
File.OpenWrite("fullcolor.gif"),
width,
height,
new GifEncoderOptions
{
LoopCount = 0,
DefaultFrameDelayMs = 100,
Quantize = new QuantizeOptions
{
MaxColors = 128, // 2–256
AlphaThreshold = 128, // pixels with A < this are treated as transparent
Dither = true // optional Floyd–Steinberg error diffusion (default false)
}
});
for (int f = 0; f < frameCount; f++)
encoder.AddFrame(pixels, delayMs: 100);
Multi-frame palette policy (default): with the default
UseGlobalColorTableFallback = true, the Global Color Table is built once from the first quantized frame (or fromGlobalPaletteif supplied) and later frames are remapped into that same palette — they are not re-quantized independently. This keeps multi-frame encoding fast and compact but can lose color fidelity when later frames introduce colors absent from the first frame. For per-frame palettes, setUseGlobalColorTableFallback = false(full-color frames then get a per-frame Local Color Table generated by the octree quantizer) or supply a betterGlobalPaletteup front.
Local Color Tables (optional)
A Global Color Table (GCT) is always written in the header. Each frame may additionally carry a Local Color Table (LCT) by passing a non-empty localPalette (2–256 Rgb entries) to AddFrame; that frame's indices are then interpreted against the local palette. With the default UseGlobalColorTableFallback = true, frames without a localPalette use the GCT and write no LCT — the existing quick-start behavior. A non-empty localPalette always writes an LCT (no equality elision).
Selective Local Color Table (indexed)
Mix GCT frames with per-frame LCT frames. localPalette is a trailing optional argument on every AddFrame overload.
using LiteGif;
using System.IO;
int width = 320;
int height = 240;
var globalPalette = new Rgb[256];
for (int i = 0; i < globalPalette.Length; i++)
globalPalette[i] = new Rgb((byte)i, (byte)(i * 2), (byte)(i * 3));
var indices0 = new byte[width * height];
var indices1 = new byte[width * height];
var framePalette = new Rgb[]
{
new(0, 0, 0), new(255, 255, 255), new(255, 0, 0), new(0, 255, 0)
};
using var encoder = new GifEncoder(
File.OpenWrite("selective.gif"),
width, height, globalPalette,
new GifEncoderOptions { LoopCount = 0, DefaultFrameDelayMs = 100 });
// Frame 0: uses the Global Color Table (no LCT on the wire).
encoder.AddFrame(indices0, delayMs: 100);
// Frame 1: uses a per-frame Local Color Table (always written; no equality elision).
encoder.AddFrame(indices1, delayMs: 100, localPalette: framePalette);
Auto multi-palette (full-color)
Set UseGlobalColorTableFallback = false so full-color frames without a localPalette get a per-frame LCT generated by the octree quantizer. The first auto-GCT materializing frame (no pre-supplied GlobalPalette, GCT not yet built) builds the GCT from that frame and is GCT-only; later such frames re-quantize and emit an LCT. This path is for full-color only — indexed mode with fallback false requires a non-empty localPalette on every frame and throws InvalidOperationException otherwise.
using LiteGif;
using System.IO;
int width = 320;
int height = 240;
var pixels0 = new Rgba32[width * height];
var pixels1 = new Rgba32[width * height];
// ...fill pixels0 / pixels1...
using var encoder = new GifEncoder(
File.OpenWrite("multipalette.gif"),
width, height,
new GifEncoderOptions
{
LoopCount = 0,
DefaultFrameDelayMs = 100,
UseGlobalColorTableFallback = false,
Quantize = new QuantizeOptions { MaxColors = 128, AlphaThreshold = 128 }
});
// Frame 0: builds the GCT, GCT-only.
encoder.AddFrame(pixels0, delayMs: 100);
// Later frames: re-quantized into a per-frame Local Color Table.
encoder.AddFrame(pixels1, delayMs: 100);
Size trade-off
Every LCT is padded to the next power of two ≥ its length (minimum 2) and costs 3 × padded entry count bytes after that frame's Image Descriptor — the same rule used for the GCT. Auto multi-palette re-quantizes each frame, so prefer it when frames differ enough to justify the extra tables.
Benchmarks
The benchmark harness lives in benchmarks/LiteGif.Benchmarks and is documented in benchmarks/LiteGif.Benchmarks/BenchmarksMethodology.md. It compares LiteGif against AnimatedGif 1.0.5 and ImageSharp 3.1.12 on pre-indexed 256-color frames written to Stream.Null. The comparison focuses on indexed framing + LZW cost; the ImageSharp lane uses a palette-locked Local encode but still performs palette matching (not raw index streaming), so its time includes quantization work LiteGif does not perform.
The harness has two lanes, both run from Program.cs:
- Per-frame LCT (
GifEncodeLctBenchmarks): a fair per-frame color-table lane where every frame carries its own Local Color Table on both encoders. This is the headline lane reported in the Results table below — it isolates per-frame color-table + LZW framing cost on both sides, so neither encoder benefits from a shared-palette shortcut. - GCT-only (
GifEncodeBenchmarks): shared 256-entry palette, no per-frame Local Color Table. A secondary lane; no result numbers are reported for it here. Run it with the filter below to measure on your own machine.
Run the full matrix (both lanes):
dotnet run -c Release --project benchmarks/LiteGif.Benchmarks -- --filter '*'
Run a single lane:
# Per-frame LCT lane (the Results table below).
dotnet run -c Release --project benchmarks/LiteGif.Benchmarks -- --filter '*GifEncodeLctBenchmarks*'
# GCT-only lane.
dotnet run -c Release --project benchmarks/LiteGif.Benchmarks -- --filter '*GifEncodeBenchmarks*'
Results
BenchmarkDotNet ShortRun (3 iterations, in-process), .NET 10.0.9, X64. Pre-indexed 256-color frames, each carrying its own 256-entry Local Color Table, written to Stream.Null; LiteGif is the baseline lane. The full curated matrix is shown below; run it on your own machine for comparable numbers.
| Method | Resolution | Frames | Mean | Ratio | Allocated |
|---|---|---|---|---|---|
| LiteGif | 320×240 | 30 | 8.41 ms | 1.00 | 32.39 KB |
| AnimatedGif | 320×240 | 30 | 37.89 ms | 4.51 | 28.3 MB |
| ImageSharp | 320×240 | 30 | 38.92 ms | 4.63 | 109.4 KB |
| LiteGif | 320×240 | 120 | 33.42 ms | 1.00 | 126.82 KB |
| AnimatedGif | 320×240 | 120 | 164.69 ms | 4.93 | 113.1 MB |
| ImageSharp | 320×240 | 120 | 155.33 ms | 4.65 | 432.17 KB |
| LiteGif | 1280×720 | 30 | 101.09 ms | 1.00 | 33.17 KB |
| AnimatedGif | 1280×720 | 30 | 964.06 ms | 9.54 | 321.0 MB |
| ImageSharp | 1280×720 | 30 | 385.96 ms | 3.82 | 113.31 KB |
| LiteGif | 1280×720 | 120 | 406.05 ms | 1.00 | 129.55 KB |
| AnimatedGif | 1280×720 | 120 | 3,798.39 ms | 9.35 | 1.28 GB |
| ImageSharp | 1280×720 | 120 | 1,548.05 ms | 3.81 | 435.34 KB |
| LiteGif | 1920×1080 | 30 | 226.64 ms | 1.00 | 33.73 KB |
| AnimatedGif | 1920×1080 | 30 | 11,755.17 ms | 51.87 | 668.5 MB |
| ImageSharp | 1920×1080 | 30 | 866.31 ms | 3.82 | 113.45 KB |
LiteGif's advantage grows with resolution. At 320×240 the ratio is ~4.5–4.9× against both competitors. At 1280×720 it widens to ~9.4× against AnimatedGif and ~3.8× against ImageSharp. At 1920×1080 it reaches ~52× against AnimatedGif (GDI+ per-frame Save overhead scales poorly with frame size) while staying ~3.8× against ImageSharp. LiteGif allocates a fixed ~33–130 KB per encode (scaling with frame count, not resolution); AnimatedGif allocates hundreds of MB to over 1 GB from per-frame GDI+ MemoryStream buffers. ImageSharp uses a palette-locked Local encode with no dither but still performs per-pixel palette matching (not raw index streaming); its time includes quantization work LiteGif does not perform. See the methodology doc for the full fairness caveat.
Notes
LoopCount:0or negative = infinite,1= play once,N>1= N plays, max 65536.DefaultFrameDelayMsis in milliseconds. GIF stores delays in centiseconds, so values are rounded half-up.TransparentIndexandBackgroundIndexare validated against the GCT / effective palette size in the constructor; the per-frametransparentIndexargument onAddFrameis validated against the frame's active table (LCT if present, else GCT).BackgroundIndexis written into the Logical Screen Descriptor and is not re-validated against per-frame LCTs.UseGlobalColorTableFallbackdefaults totrue(GCT-only multi-frame). Set it tofalsefor full-color auto per-frame LCTs; indexed mode then requires a non-emptylocalPaletteon every frame.QuantizeOptions.Dither = trueenables Floyd–Steinberg error-diffusion dithering (classic 7/16, 3/16, 5/16, 1/16 kernel, left-to-right, full strength) during full-color index assignment, reducing banding when mapping to a small palette. Default isfalse(nearest-only). Dithering applies only to index mapping after the palette is built; it does not affect palette construction and does not apply to indexed-modeAddFrameoverloads. The same 5-bit inverse color map is used for the per-pixel nearest lookup under dither.- Alpha values strictly less than
QuantizeOptions.AlphaThresholdare mapped to the transparent index when one is configured. - Full-color mapping uses a 5-bit-per-channel (32³) inverse color map for speed. A pixel that is not exactly a cell midpoint may, at bin boundaries, map to a different palette slot than an exhaustive 24-bit linear nearest search would pick. This is the accepted trade-off for the O(1) per-pixel lookup.
ValidateIndices(defaulttrue): whentrue, the indexedAddFrameoverloads scan every index byte and throw if any is>=the active palette size. Set it tofalseto skip that O(n) check for trusted indexed pipelines; the caller is then responsible for ensuring indices are in range (out-of-range indices produce undefined wire content). Length, delay, transparent-index, and LCT size validation still run regardless.Disposewrites the0x3Btrailer only after at least one successfulAddFrame; disposing an encoder that never received a frame writes nothing (no header, no trailer). It disposes the output stream unlessleaveOpen: trueis passed.
License
LiteGif is released under the MIT license. See the package metadata for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|