GIF

edit

GIF is the animation format that every viewer plays. It stores at most 256 colors per frame and one fully transparent color, so writing a GIF is lossy by design: choosing GifEncoder is the consent to reduce the colors, the alpha values and 16-bit samples, and the encoder needs no other setting to do it.

What is read and written

ReadingWriting
FilesGIF87a and GIF89a, global and local palettes, interlacingGIF89a, one local palette per frame, optional interlacing
AnimationsComposited into full frames with exact durations (hundredths of a second) and the play countFull frames, durations to the hundredth of a second, play count
TransparencyThe transparent color; cleared areas are transparent blackOne transparent color: alpha is thresholded or flattened, never partial
MetadataCommentsComments only

Every GIF loads as Rgba32. A frame whose colors fit its palette is written exactly; the others are reduced with the deterministic quantizer of Quantize.

Save a GIF

AlphaMode decides what happens to the pixels that are not fully opaque, and Quantization how many colors each frame keeps and how they are dithered:

public static void SaveGif(string inputPath, string gifPath, string flattenedGifPath)
{
    using var image = Image.Load(inputPath);

    // GIF has one fully transparent palette entry and no partial alpha: pixels whose alpha is below the threshold
    // become transparent, the others opaque. 64 colors per frame (the transparent entry included), dithered.
    image.Save(gifPath, new GifEncoder
    {
        AlphaThreshold = 64,
        Quantization = new QuantizeOptions { MaxColors = 64, Dithering = DitheringMode.FloydSteinberg },
        MetadataHandling = MetadataHandling.DiscardUnsupported, // GIF only stores comments: drop the ICC profile, EXIF...
    });

    // Soft edges look better composited over the color of the page that shows the image
    image.Save(flattenedGifPath, new GifEncoder
    {
        AlphaMode = GifAlphaMode.Flatten,
        BackgroundColor = new Rgba32(255, 255, 255),
        Interlaced = true,
        MetadataHandling = MetadataHandling.DiscardUnsupported,
    });
}
  • With GifAlphaMode.Threshold (the default), a pixel whose alpha is below AlphaThreshold becomes transparent and the others become opaque. Antialiased edges then look jagged on a background of another color.
  • With GifAlphaMode.Flatten, every pixel is composited over BackgroundColor, and the GIF is opaque. Without BackgroundColor, saving throws an ArgumentException.
  • GIF only stores Comment text entries. An ICC profile, EXIF, XMP or a resolution follow MetadataHandling, which rejects them by default: the example discards them.

Options

OptionWhat it doesDefault
AlphaModeThreshold makes each pixel transparent or opaque; Flatten composites the image over BackgroundColorThreshold
AlphaThresholdThe alpha (8-bit scale) below which a pixel becomes transparent128
BackgroundColorThe opaque color Flatten composites overnull
QuantizationThe number of colors per frame (2 to 256, the transparent entry included), the dithering, or a fixed palette for every frame256 colors, no dithering
InterlacedWrites interlaced framesfalse
DurationRoundingRoundToNearest rounds durations to the nearest hundredth of a second (ties up); RequireExact throwsRoundToNearest
MetadataHandlingWhat to do with metadata GIF cannot storeStrict

Dithering and MaxColors are obsolete aliases of Quantization.Dithering and Quantization.MaxColors.

Share one palette between frames

Each frame gets its own palette by default, so the colors of a gradient can change slightly from one frame to the next and flicker. A palette built from every frame and given to every frame avoids it:

public static void SaveGifWithSharedPalette(string inputPath, string outputPath)
{
    using var animation = Image.Load(inputPath);

    // The palette of the pixels of every frame and of the poster: 255 colors leave room for the transparent entry
    ImagePalette palette;
    using (var builder = new PaletteBuilder())
    {
        builder.Add(animation);
        palette = builder.Build(maxColors: 255);
    }

    // Every frame maps to the same colors, so they do not flicker from one frame to the next
    animation.Save(outputPath, new GifEncoder
    {
        Quantization = new QuantizeOptions { Palette = palette, Dithering = DitheringMode.FloydSteinberg },
    });
}

The palette leaves room for the transparent entry: a palette of 256 colors with Threshold and a pixel format with alpha throws an ArgumentException. The palette and the dithering modes are described in Color reduction.

Store only what changes

With OptimizeFrames, each frame after the first one stores the rectangle of the pixels that differ from the canvas it is drawn on, and its unchanged pixels take the transparent entry. The disposal of the previous frame ("do not dispose", "restore to background" or "restore to previous") is the one that gives the smallest rectangle; a GIF frame cannot make a pixel transparent, so the rectangle of the previous frame grows to cover the pixels that become transparent, and its disposal clears them. A frame equal to the previous one is a 1x1 image. The animation looks the same, with the same number of frames, durations and play count:

public static void SaveOptimizedAnimation(string inputPath, string gifPath, string apngPath, string webpPath)
{
    using var animation = Image.Load(inputPath);

    // Each frame after the first one stores the region that differs from the canvas it is drawn on, with the
    // blending and the disposal of the previous frame that reproduce it: the displayed frames, their durations and
    // the play count are the same as with full-canvas frames
    animation.Save(gifPath, new GifEncoder { OptimizeFrames = true });
    animation.Save(apngPath, new PngEncoder { AnimationMode = PngAnimationMode.Animated, OptimizeFrames = true });
    animation.Save(webpPath, new WebPEncoder { OptimizeFrames = true });
}

The colors are compared before they are reduced: when every frame fits its palette, the displayed frames are exactly those of full frames. A frame with more colors is reduced from the colors of its rectangle only, and a pixel that does not change keeps the color the frame that drew it was reduced to. The encoder keeps two copies of the canvas (4 bytes per pixel each) and writes each frame when the next one is given, or when the output completes.

Pitfalls

  • Durations are hundredths of a second: 1/30 s is rounded to 0.03 s by default. A duration above 655.35 s is rejected, never clamped, and so is a play count above 65,536.
  • A frame is at most 65,535 pixels per side.
  • Every frame is written as a full frame by default, so an animation that changes a small area stays large. Set OptimizeFrames to store only the rectangle that changes in each frame (see below).
  • The plain-text extension (text rendered by the viewer) is rejected with an UnsupportedImageFeatureException. Other unknown extensions, such as XMP or ICC application extensions, are skipped.
  • A GIF without its trailer, or with 0x00 bytes before its end, is rejected. The lenient mode reads these files, and skips plain-text extensions without drawing their text, as browsers and the reference decoders do. A file that ends inside an image is still rejected.
  • The background color of the file is never painted: areas a frame clears are transparent black.
  • A writer created with Image.CreateWriter streams frames as they come and does not need to know the frame count.