// -------------------------------------------------------------------------------------------------------------------- // <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 } }