// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) James South. // Licensed under the Apache License, Version 2.0. // // // Encodes multiple images as an animated gif to a stream. // // 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 // // // -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor.Imaging.Formats { using System; using System.Drawing.Imaging; using System.IO; using System.Linq; /// /// Encodes multiple images as an animated gif to a stream. /// /// 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 /// /// public class GifEncoder : IDisposable { #region Constants /// /// The application block size. /// private const byte ApplicationBlockSize = 0x0b; /// /// The application extension block identifier. /// private const int ApplicationExtensionBlockIdentifier = 0xff21; /// /// The application identification. /// private const string ApplicationIdentification = "NETSCAPE2.0"; /// /// The file trailer. /// private const byte FileTrailer = 0x3b; /// /// The file type. /// private const string FileType = "GIF"; /// /// The file version. /// private const string FileVersion = "89a"; /// /// The graphic control extension block identifier. /// private const int GraphicControlExtensionBlockIdentifier = 0xf921; /// /// The graphic control extension block size. /// private const byte GraphicControlExtensionBlockSize = 0x04; /// /// The source color block length. /// private const long SourceColorBlockLength = 768; /// /// The source color block position. /// private const long SourceColorBlockPosition = 13; /// /// The source global color info position. /// private const long SourceGlobalColorInfoPosition = 10; /// /// The source graphic control extension length. /// private const long SourceGraphicControlExtensionLength = 8; /// /// The source graphic control extension position. /// private const long SourceGraphicControlExtensionPosition = 781; /// /// The source image block header length. /// private const long SourceImageBlockHeaderLength = 11; /// /// The source image block position. /// private const long SourceImageBlockPosition = 789; #endregion #region Fields /// /// The stream. /// // ReSharper disable once FieldCanBeMadeReadOnly.Local private MemoryStream inputStream; /// /// The height. /// private int? height; /// /// A value indicating whether this instance of the given entity has been disposed. /// /// if this instance has been disposed; otherwise, . /// /// 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. /// private bool isDisposed; /// /// The is first image. /// private bool isFirstImage = true; /// /// The repeat count. /// private int? repeatCount; /// /// The width. /// private int? width; #endregion #region Constructors /// /// Initializes a new instance of the class. /// /// /// The stream that will be written to. /// /// /// Sets the width for this gif or null to use the first frame's width. /// /// /// Sets the height for this gif or null to use the first frame's height. /// /// /// The number of times to repeat the animation. /// 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; } /// /// Finalizes an instance of the class. /// /// /// 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. /// ~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 /// /// Adds a frame to the gif. /// /// /// The containing the image. /// 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, frame.Delay); this.WriteImageBlock(gifStream, !this.isFirstImage, frame.X, frame.Y, frame.Image.Width, frame.Image.Height); } this.isFirstImage = false; } /// /// Disposes the object and frees resources for the Garbage Collector. /// 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 /// /// Disposes the object and frees resources for the Garbage Collector. /// /// /// If true, the object gets disposed. /// 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; } /// /// Writes the header block of the animated gif to the stream. /// /// /// The source gif. /// /// /// The width of the image. /// /// /// The height of the image. /// 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); // The different browsers interpret the spec differently when adding a loop. // If the loop count is one IE and FF < 3 (incorrectly) loop an extra number of times. // Removing the Netscape header should fix this. if (count > -1 && count != 1) { // 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 } } /// /// The write byte. /// /// /// The value. /// private void WriteByte(int value) { this.inputStream.WriteByte(Convert.ToByte(value)); } /// /// The write color table. /// /// /// The source gif. /// 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); } /// /// The write graphic control block. /// /// /// The source gif. /// /// /// The frame delay. /// 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(Convert.ToInt32(frameDelay / 10.0f)); // Setting frame delay this.WriteByte(blockhead[6]); // Transparent color index this.WriteByte(0); // Terminator } /// /// The write image block. /// /// /// The source gif. /// /// /// The include color table. /// /// /// The x position to write the image block. /// /// /// The y position to write the image block. /// /// /// The height of the image block. /// /// /// The width of the image block. /// 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 } /// /// The write short. /// /// /// The value. /// 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)); } /// /// The write string. /// /// /// The value. /// private void WriteString(string value) { this.inputStream.Write(value.ToArray().Select(c => (byte)c).ToArray(), 0, value.Length); } #endregion } }