For some prior and upcoming blog posts, I have added animated GIFs. The images in question were diagrams, which were easy to generate using a C# function, given a Graphics context. I foresaw the need to do this more often, and so I encapsulated the functionality into a useful abstract class:
using System; using System.Drawing; using System.IO; using System.Linq; using System.Windows; using System.Windows.Interop; using System.Windows.Media.Imaging; /// <summary> /// Encapsulates an algorithm for generating an animated GIF image stream from a /// collection of dynamically-generated image frames. /// </summary> public abstract class GifGenerator { #region Abstractions /// <summary> /// The width, in pixels, of the output image. /// </summary> public abstract int Width { get; } /// <summary> /// The height, in pixels, of the output image. /// </summary> public abstract int Height { get; } /// <summary> /// The numer of frames in the animation. /// </summary> public abstract int FrameCount { get; } /// <summary> /// Callback to render a frame of animation. /// </summary> /// <param name="g">The target graphics context upon which to render the frame.</param> /// <param name="frameIndex">The frame index of the animation. Ranges from 0 to FrameCount - 1.</param> protected abstract void RenderFrame(Graphics g, int frameIndex); #endregion #region Public methods /// <summary> /// Writes the GIF image stream of this GifGenerator to the specified filename. /// </summary> /// <param name="outputFilename">The filename of the resulting file.</param> public void Generate(string outputFilename) { using (var file = File.Create(outputFilename)) Generate(file); } /// <summary> /// Writes the GIF image stream of this GifGenerator to the specified stream. /// </summary> /// <param name="output">The stream into which the result will be written.</param> public void Generate(Stream output) { ProduceBitmapEncoder().Save(output); } #endregion #region Helper methods GifBitmapEncoder ProduceBitmapEncoder() { var result = new GifBitmapEncoder(); using (var buffer = new Bitmap(Width, Height)) foreach (var frameIndex in Enumerable.Range(0, FrameCount)) { RenderFrameToBuffer(buffer, frameIndex); AppendBufferToEncoder(buffer, result); } return result; } void RenderFrameToBuffer(Bitmap buffer, int frameIndex) { using (var g = Graphics.FromImage(buffer)) RenderFrame(g, frameIndex); } static void AppendBufferToEncoder(Bitmap buffer, BitmapEncoder encoder) { var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(buffer.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); var bitmapFrame = BitmapFrame.Create(bitmapSource); encoder.Frames.Add(bitmapFrame); } #endregion }
The class itself is easy to use. You need only inherit from it, implement its abstract properties to specify the output GIF size, and implement its RenderFrame method to draw each image that will make up the GIF. As an example, I wrote a program that animates a bouncing ball:
using System.Drawing; using System.Drawing.Drawing2D; public static class Program { class BouncyBallGifGenerator : GifGenerator { const int BallDiameter = 60; public override int Width { get { return BallDiameter; } } public override int Height { get { return 100 + BallDiameter; } } public override int FrameCount { get { return 20; } } protected override void RenderFrame(Graphics g, int frameIndex) { g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Color.CornflowerBlue); g.FillEllipse(Brushes.Orange, new Rectangle(0, BallHeightForFrame(frameIndex), BallDiameter-1, BallDiameter-1)); g.DrawEllipse(Pens.Black, new Rectangle(0, BallHeightForFrame(frameIndex), BallDiameter-1, BallDiameter-1)); } static int BallHeightForFrame(int frameIndex) { return (frameIndex - 10) * (frameIndex - 10); } } public static void Main() { new BouncyBallGifGenerator().Generate(@"C:\output.gif"); } }
There is one problem, however: the resulting GIF does not have its animation properties specified. If you open it with a web browser in its current form, you will probably find it to render with a high frame duration, and no looping. This is unacceptable, because the only reason anyone would animate a GIF is to loop the animation.
I tried, and failed, to find an answer to this problem using Google. The WPF GifBitmapEncoder isn't configured out-of-the-box to allow you to set the animation properties of the output GIF. The WPF team at Microsoft really only created it to open GIF images for display in WPF applications, not as a general-purpose GIF toolkit. There are probably libraries available which will write your GIF with animation properties, but I wanted to stick with the core libraries that would be installed anywhere on earth.
Fortunately, I found a way to hand-edit the resulting images in Gimp. Take the output from the above program and open it in Gimp. Run the 'File → Export...' menu command, and choose an output filename (I just overwrite the original). On the subsequent "Export Image as GIF" dialog, check the 'As animation', 'Loop forever' and 'Use delay entered above for all frames' checkboxes. Set the 'Delay between frames where unspecified' value as desired; your resulting framerate will be (1000/delay) frames per second. Hit the export button, and you will get the output you desire:
One other important point: the acronym is pronounced GIF (with a hard G), not JIF. I don't care what Steve Wilhite says. If he wants me to pronounce it as JIF, he must rename the format to Giraffics Interchange Format, complete with long-necked images roaming the African savannah, eating choice leaves from the top of the tallest trees.
It is also surprisingly important to me for "gif" to be pronounced with a hard G.
ReplyDelete