Skip to content

Instantly share code, notes, and snippets.

@JimBobSquarePants
Created October 16, 2014 22:29
Show Gist options
  • Save JimBobSquarePants/cac72c4e7d9f05f13ac9 to your computer and use it in GitHub Desktop.
Save JimBobSquarePants/cac72c4e7d9f05f13ac9 to your computer and use it in GitHub Desktop.
An animated gif encoder in c#
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="GifEncoder.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Encodes multiple images as an animated gif to a stream.
// <remarks>
// Always wire this up in a using block.
// Disposing the encoder will complete the file.
// Uses default .NET GIF encoding and adds animation headers.
// Adapted from <see href="http://github.com/DataDink/Bumpkit/blob/master/BumpKit/BumpKit/GifEncoder.cs"/>
// </remarks>
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Imaging.Formats
{
using System;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
/// <summary>
/// Encodes multiple images as an animated gif to a stream.
/// <remarks>
/// Always wire this up in a using block.
/// Disposing the encoder will complete the file.
/// Uses default .NET GIF encoding and adds animation headers.
/// Adapted from <see href="http://github.com/DataDink/Bumpkit/blob/master/BumpKit/BumpKit/GifEncoder.cs"/>
/// </remarks>
/// </summary>
public class GifEncoder : IDisposable
{
#region Constants
/// <summary>
/// The application block size.
/// </summary>
private const byte ApplicationBlockSize = 0x0b;
/// <summary>
/// The application extension block identifier.
/// </summary>
private const int ApplicationExtensionBlockIdentifier = 0xff21;
/// <summary>
/// The application identification.
/// </summary>
private const string ApplicationIdentification = "NETSCAPE2.0";
/// <summary>
/// The file trailer.
/// </summary>
private const byte FileTrailer = 0x3b;
/// <summary>
/// The file type.
/// </summary>
private const string FileType = "GIF";
/// <summary>
/// The file version.
/// </summary>
private const string FileVersion = "89a";
/// <summary>
/// The graphic control extension block identifier.
/// </summary>
private const int GraphicControlExtensionBlockIdentifier = 0xf921;
/// <summary>
/// The graphic control extension block size.
/// </summary>
private const byte GraphicControlExtensionBlockSize = 0x04;
/// <summary>
/// The source color block length.
/// </summary>
private const long SourceColorBlockLength = 768;
/// <summary>
/// The source color block position.
/// </summary>
private const long SourceColorBlockPosition = 13;
/// <summary>
/// The source global color info position.
/// </summary>
private const long SourceGlobalColorInfoPosition = 10;
/// <summary>
/// The source graphic control extension length.
/// </summary>
private const long SourceGraphicControlExtensionLength = 8;
/// <summary>
/// The source graphic control extension position.
/// </summary>
private const long SourceGraphicControlExtensionPosition = 781;
/// <summary>
/// The source image block header length.
/// </summary>
private const long SourceImageBlockHeaderLength = 11;
/// <summary>
/// The source image block position.
/// </summary>
private const long SourceImageBlockPosition = 789;
#endregion
#region Fields
/// <summary>
/// The stream.
/// </summary>
// ReSharper disable once FieldCanBeMadeReadOnly.Local
private MemoryStream inputStream;
/// <summary>
/// The height.
/// </summary>
private int? height;
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// If the entity is disposed, it must not be disposed a second
/// time. The isDisposed field is set the first time the entity
/// is disposed. If the isDisposed field is true, then the Dispose()
/// method will not dispose again. This help not to prolong the entity's
/// life in the Garbage Collector.
/// </remarks>
private bool isDisposed;
/// <summary>
/// The is first image.
/// </summary>
private bool isFirstImage = true;
/// <summary>
/// The repeat count.
/// </summary>
private int? repeatCount;
/// <summary>
/// The width.
/// </summary>
private int? width;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="GifEncoder"/> class.
/// </summary>
/// <param name="stream">
/// The stream that will be written to.
/// </param>
/// <param name="width">
/// Sets the width for this gif or null to use the first frame's width.
/// </param>
/// <param name="height">
/// Sets the height for this gif or null to use the first frame's height.
/// </param>
/// <param name="repeatCount">
/// The number of times to repeat the animation.
/// </param>
public GifEncoder(MemoryStream stream, int? width = null, int? height = null, int? repeatCount = null)
{
this.inputStream = stream;
this.width = width;
this.height = height;
this.repeatCount = repeatCount;
}
/// <summary>
/// Finalizes an instance of the <see cref="GifEncoder"/> class.
/// </summary>
/// <remarks>
/// Use C# destructor syntax for finalization code.
/// This destructor will run only if the Dispose method
/// does not get called.
/// It gives your base class the opportunity to finalize.
/// Do not provide destructors in types derived from this class.
/// </remarks>
~GifEncoder()
{
// Do not re-create Dispose clean-up code here.
// Calling Dispose(false) is optimal in terms of
// readability and maintainability.
this.Dispose(false);
}
#endregion
#region Public Methods and Operators
/// <summary>
/// Adds a frame to the gif.
/// </summary>
/// <param name="frame">
/// The <see cref="GifFrame"/> containing the image.
/// </param>
public void AddFrame(GifFrame frame)
{
using (MemoryStream gifStream = new MemoryStream())
{
frame.Image.Save(gifStream, ImageFormat.Gif);
if (this.isFirstImage)
{
// Steal the global color table info
this.WriteHeaderBlock(gifStream, frame.Image.Width, frame.Image.Height);
}
this.WriteGraphicControlBlock(gifStream, Convert.ToInt32(frame.Delay.TotalMilliseconds / 10F));
this.WriteImageBlock(gifStream, !this.isFirstImage, frame.X, frame.Y, frame.Image.Width, frame.Image.Height);
}
this.isFirstImage = false;
}
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.
/// </summary>
public void Dispose()
{
this.Dispose(true);
// This object will be cleaned up by the Dispose method.
// Therefore, you should call GC.SupressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
#endregion
#region Methods
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.
/// </summary>
/// <param name="disposing">
/// If true, the object gets disposed.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
if (disposing)
{
// Complete Application Block
this.WriteByte(0);
// Complete File
this.WriteByte(FileTrailer);
// Push the data
this.inputStream.Flush();
}
// Call the appropriate methods to clean up
// unmanaged resources here.
// Note disposing is done.
this.isDisposed = true;
}
/// <summary>
/// Writes the header block of the animated gif to the stream.
/// </summary>
/// <param name="sourceGif">
/// The source gif.
/// </param>
/// <param name="w">
/// The width of the image.
/// </param>
/// <param name="h">
/// The height of the image.
/// </param>
private void WriteHeaderBlock(Stream sourceGif, int w, int h)
{
int count = this.repeatCount.GetValueOrDefault(0);
// File Header signature and version.
this.WriteString(FileType);
this.WriteString(FileVersion);
// Write the logical screen descriptor.
this.WriteShort(this.width.GetValueOrDefault(w)); // Initial Logical Width
this.WriteShort(this.height.GetValueOrDefault(h)); // Initial Logical Height
// Read the global color table info.
sourceGif.Position = SourceGlobalColorInfoPosition;
this.WriteByte(sourceGif.ReadByte());
this.WriteByte(0); // Background Color Index
this.WriteByte(0); // Pixel aspect ratio
this.WriteColorTable(sourceGif);
// Application Extension Header
this.WriteShort(ApplicationExtensionBlockIdentifier);
this.WriteByte(ApplicationBlockSize);
this.WriteString(ApplicationIdentification);
this.WriteByte(3); // Application block length
this.WriteByte(1);
this.WriteShort(count); // Repeat count for images.
this.WriteByte(0); // Terminator
}
/// <summary>
/// The write byte.
/// </summary>
/// <param name="value">
/// The value.
/// </param>
private void WriteByte(int value)
{
this.inputStream.WriteByte(Convert.ToByte(value));
}
/// <summary>
/// The write color table.
/// </summary>
/// <param name="sourceGif">
/// The source gif.
/// </param>
private void WriteColorTable(Stream sourceGif)
{
sourceGif.Position = SourceColorBlockPosition; // Locating the image color table
byte[] colorTable = new byte[SourceColorBlockLength];
sourceGif.Read(colorTable, 0, colorTable.Length);
this.inputStream.Write(colorTable, 0, colorTable.Length);
}
/// <summary>
/// The write graphic control block.
/// </summary>
/// <param name="sourceGif">
/// The source gif.
/// </param>
/// <param name="frameDelay">
/// The frame delay.
/// </param>
private void WriteGraphicControlBlock(Stream sourceGif, int frameDelay)
{
sourceGif.Position = SourceGraphicControlExtensionPosition; // Locating the source GCE
byte[] blockhead = new byte[SourceGraphicControlExtensionLength];
sourceGif.Read(blockhead, 0, blockhead.Length); // Reading source GCE
this.WriteShort(GraphicControlExtensionBlockIdentifier); // Identifier
this.WriteByte(GraphicControlExtensionBlockSize); // Block Size
this.WriteByte(blockhead[3] & 0xf7 | 0x08); // Setting disposal flag
this.WriteShort(frameDelay); // Setting frame delay
this.WriteByte(blockhead[6]); // Transparent color index
this.WriteByte(0); // Terminator
}
/// <summary>
/// The write image block.
/// </summary>
/// <param name="sourceGif">
/// The source gif.
/// </param>
/// <param name="includeColorTable">
/// The include color table.
/// </param>
/// <param name="x">
/// The x position to write the image block.
/// </param>
/// <param name="y">
/// The y position to write the image block.
/// </param>
/// <param name="h">
/// The height of the image block.
/// </param>
/// <param name="w">
/// The width of the image block.
/// </param>
private void WriteImageBlock(Stream sourceGif, bool includeColorTable, int x, int y, int h, int w)
{
// Local Image Descriptor
sourceGif.Position = SourceImageBlockPosition; // Locating the image block
byte[] header = new byte[SourceImageBlockHeaderLength];
sourceGif.Read(header, 0, header.Length);
this.WriteByte(header[0]); // Separator
this.WriteShort(x); // Position X
this.WriteShort(y); // Position Y
this.WriteShort(h); // Height
this.WriteShort(w); // Width
if (includeColorTable)
{
// If first frame, use global color table - else use local
sourceGif.Position = SourceGlobalColorInfoPosition;
this.WriteByte(sourceGif.ReadByte() & 0x3f | 0x80); // Enabling local color table
this.WriteColorTable(sourceGif);
}
else
{
this.WriteByte(header[9] & 0x07 | 0x07); // Disabling local color table
}
this.WriteByte(header[10]); // LZW Min Code Size
// Read/Write image data
sourceGif.Position = SourceImageBlockPosition + SourceImageBlockHeaderLength;
int dataLength = sourceGif.ReadByte();
while (dataLength > 0)
{
byte[] imgData = new byte[dataLength];
sourceGif.Read(imgData, 0, dataLength);
this.inputStream.WriteByte(Convert.ToByte(dataLength));
this.inputStream.Write(imgData, 0, dataLength);
dataLength = sourceGif.ReadByte();
}
this.inputStream.WriteByte(0); // Terminator
}
/// <summary>
/// The write short.
/// </summary>
/// <param name="value">
/// The value.
/// </param>
private void WriteShort(int value)
{
// Leave only one significant byte.
this.inputStream.WriteByte(Convert.ToByte(value & 0xff));
this.inputStream.WriteByte(Convert.ToByte((value >> 8) & 0xff));
}
/// <summary>
/// The write string.
/// </summary>
/// <param name="value">
/// The value.
/// </param>
private void WriteString(string value)
{
this.inputStream.Write(value.ToArray().Select(c => (byte)c).ToArray(), 0, value.Length);
}
#endregion
}
}
@JimBobSquarePants
Copy link
Author

To fix remove

            // Complete Application Block
            this.WriteByte(0);

at line 253

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment