Created
October 16, 2014 22:29
-
-
Save JimBobSquarePants/cac72c4e7d9f05f13ac9 to your computer and use it in GitHub Desktop.
An animated gif encoder in c#
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// -------------------------------------------------------------------------------------------------------------------- | |
// <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 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To fix remove
at line 253