59

Does anyone know how to create an animated gif using c#? Ideally I would have some control over the color reduction used.

Is using imagemagick (as an external started process) the best choice?

Kiquenet
  • 14,494
  • 35
  • 148
  • 243
  • imagemagick still seems to have the best options (dithering, color reduction, etc). The suggested lib and other creation method are of quite poor quality –  Jul 29 '09 at 17:30
  • did you tried any final solution? not marked answer... – Kiquenet Dec 26 '16 at 11:58

8 Answers8

34

This Gif Animation Creater code from https://github.com/DataDink/Bumpkit can set Delay foreach Frame:

Uses .Net standard Gif Encoding and adds Animation headers.

EDIT: Made the code similar to a typical file writer.

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Threading.Tasks;

/// <summary>
/// Creates a GIF using .Net GIF encoding and additional animation headers.
/// </summary>
public class GifWriter : IDisposable
{
    #region Fields
    const long SourceGlobalColorInfoPosition = 10,
        SourceImageBlockPosition = 789;

    readonly BinaryWriter _writer;
    bool _firstFrame = true;
    readonly object _syncLock = new object();
    #endregion

    /// <summary>
    /// Creates a new instance of GifWriter.
    /// </summary>
    /// <param name="OutStream">The <see cref="Stream"/> to output the Gif to.</param>
    /// <param name="DefaultFrameDelay">Default Delay between consecutive frames... FrameRate = 1000 / DefaultFrameDelay.</param>
    /// <param name="Repeat">No of times the Gif should repeat... -1 not to repeat, 0 to repeat indefinitely.</param>
    public GifWriter(Stream OutStream, int DefaultFrameDelay = 500, int Repeat = -1)
    {
        if (OutStream == null)
            throw new ArgumentNullException(nameof(OutStream));

        if (DefaultFrameDelay <= 0)
            throw new ArgumentOutOfRangeException(nameof(DefaultFrameDelay));

        if (Repeat < -1)
            throw new ArgumentOutOfRangeException(nameof(Repeat));

        _writer = new BinaryWriter(OutStream);
        this.DefaultFrameDelay = DefaultFrameDelay;
        this.Repeat = Repeat;
    }

    /// <summary>
    /// Creates a new instance of GifWriter.
    /// </summary>
    /// <param name="FileName">The path to the file to output the Gif to.</param>
    /// <param name="DefaultFrameDelay">Default Delay between consecutive frames... FrameRate = 1000 / DefaultFrameDelay.</param>
    /// <param name="Repeat">No of times the Gif should repeat... -1 not to repeat, 0 to repeat indefinitely.</param>
    public GifWriter(string FileName, int DefaultFrameDelay = 500, int Repeat = -1)
        : this(new FileStream(FileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read), DefaultFrameDelay, Repeat) { }

    #region Properties
    /// <summary>
    /// Gets or Sets the Default Width of a Frame. Used when unspecified.
    /// </summary>
    public int DefaultWidth { get; set; }

    /// <summary>
    /// Gets or Sets the Default Height of a Frame. Used when unspecified.
    /// </summary>
    public int DefaultHeight { get; set; }

    /// <summary>
    /// Gets or Sets the Default Delay in Milliseconds.
    /// </summary>
    public int DefaultFrameDelay { get; set; }

    /// <summary>
    /// The Number of Times the Animation must repeat.
    /// -1 indicates no repeat. 0 indicates repeat indefinitely
    /// </summary>
    public int Repeat { get; }
    #endregion

    /// <summary>
    /// Adds a frame to this animation.
    /// </summary>
    /// <param name="Image">The image to add</param>
    /// <param name="Delay">Delay in Milliseconds between this and last frame... 0 = <see cref="DefaultFrameDelay"/></param>
    public void WriteFrame(Image Image, int Delay = 0)
    {
        lock (_syncLock)
            using (var gifStream = new MemoryStream())
            {
                Image.Save(gifStream, ImageFormat.Gif);

                // Steal the global color table info
                if (_firstFrame)
                    InitHeader(gifStream, _writer, Image.Width, Image.Height);

                WriteGraphicControlBlock(gifStream, _writer, Delay == 0 ? DefaultFrameDelay : Delay);
                WriteImageBlock(gifStream, _writer, !_firstFrame, 0, 0, Image.Width, Image.Height);
            }

        if (_firstFrame)
            _firstFrame = false;
    }

    #region Write
    void InitHeader(Stream SourceGif, BinaryWriter Writer, int Width, int Height)
    {
        // File Header
        Writer.Write("GIF".ToCharArray()); // File type
        Writer.Write("89a".ToCharArray()); // File Version

        Writer.Write((short)(DefaultWidth == 0 ? Width : DefaultWidth)); // Initial Logical Width
        Writer.Write((short)(DefaultHeight == 0 ? Height : DefaultHeight)); // Initial Logical Height

        SourceGif.Position = SourceGlobalColorInfoPosition;
        Writer.Write((byte)SourceGif.ReadByte()); // Global Color Table Info
        Writer.Write((byte)0); // Background Color Index
        Writer.Write((byte)0); // Pixel aspect ratio
        WriteColorTable(SourceGif, Writer);

        // App Extension Header for Repeating
        if (Repeat == -1)
            return;

        Writer.Write(unchecked((short)0xff21)); // Application Extension Block Identifier
        Writer.Write((byte)0x0b); // Application Block Size
        Writer.Write("NETSCAPE2.0".ToCharArray()); // Application Identifier
        Writer.Write((byte)3); // Application block length
        Writer.Write((byte)1);
        Writer.Write((short)Repeat); // Repeat count for images.
        Writer.Write((byte)0); // terminator
    }

    static void WriteColorTable(Stream SourceGif, BinaryWriter Writer)
    {
        SourceGif.Position = 13; // Locating the image color table
        var colorTable = new byte[768];
        SourceGif.Read(colorTable, 0, colorTable.Length);
        Writer.Write(colorTable, 0, colorTable.Length);
    }

    static void WriteGraphicControlBlock(Stream SourceGif, BinaryWriter Writer, int FrameDelay)
    {
        SourceGif.Position = 781; // Locating the source GCE
        var blockhead = new byte[8];
        SourceGif.Read(blockhead, 0, blockhead.Length); // Reading source GCE

        Writer.Write(unchecked((short)0xf921)); // Identifier
        Writer.Write((byte)0x04); // Block Size
        Writer.Write((byte)(blockhead[3] & 0xf7 | 0x08)); // Setting disposal flag
        Writer.Write((short)(FrameDelay / 10)); // Setting frame delay
        Writer.Write(blockhead[6]); // Transparent color index
        Writer.Write((byte)0); // Terminator
    }

    static void WriteImageBlock(Stream SourceGif, BinaryWriter Writer, bool IncludeColorTable, int X, int Y, int Width, int Height)
    {
        SourceGif.Position = SourceImageBlockPosition; // Locating the image block
        var header = new byte[11];
        SourceGif.Read(header, 0, header.Length);
        Writer.Write(header[0]); // Separator
        Writer.Write((short)X); // Position X
        Writer.Write((short)Y); // Position Y
        Writer.Write((short)Width); // Width
        Writer.Write((short)Height); // Height

        if (IncludeColorTable) // If first frame, use global color table - else use local
        {
            SourceGif.Position = SourceGlobalColorInfoPosition;
            Writer.Write((byte)(SourceGif.ReadByte() & 0x3f | 0x80)); // Enabling local color table
            WriteColorTable(SourceGif, Writer);
        }
        else Writer.Write((byte)(header[9] & 0x07 | 0x07)); // Disabling local color table

        Writer.Write(header[10]); // LZW Min Code Size

        // Read/Write image data
        SourceGif.Position = SourceImageBlockPosition + header.Length;

        var dataLength = SourceGif.ReadByte();
        while (dataLength > 0)
        {
            var imgData = new byte[dataLength];
            SourceGif.Read(imgData, 0, dataLength);

            Writer.Write((byte)dataLength);
            Writer.Write(imgData, 0, dataLength);
            dataLength = SourceGif.ReadByte();
        }

        Writer.Write((byte)0); // Terminator
    }
    #endregion

    /// <summary>
    /// Frees all resources used by this object.
    /// </summary>
    public void Dispose()
    {
        // Complete File
        _writer.Write((byte)0x3b); // File Trailer

        _writer.BaseStream.Dispose();
        _writer.Dispose();
    }
}
Paul A Jungwirth
  • 23,504
  • 14
  • 74
  • 93
Mathew Sachin
  • 1,429
  • 1
  • 15
  • 20
26

There is a built in .NET class which will encode GIF files. GifBitmapEncode MSDN

System.Windows.Media.Imaging.GifBitmapEncoder gEnc = new GifBitmapEncoder();

foreach (System.Drawing.Bitmap bmpImage in images)
{
    var bmp = bmpImage.GetHbitmap();
    var src = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
        bmp,
        IntPtr.Zero,
        Int32Rect.Empty,
        BitmapSizeOptions.FromEmptyOptions());
    gEnc.Frames.Add(BitmapFrame.Create(src));
    DeleteObject(bmp); // recommended, handle memory leak
}
using(FileStream fs = new FileStream(path, FileMode.Create))
{
    gEnc.Save(fs);
}
fireydude
  • 1,181
  • 10
  • 23
  • 3
    is there a way using this code to have a certain delay between each frame or set a frame rate? – uSeRnAmEhAhAhAhAhA Oct 14 '13 at 03:49
  • 9
    Um. This code fails massively. All it does is place a bunch of images ontop of one another and saves it as a single image. – uSeRnAmEhAhAhAhAhA Oct 14 '13 at 04:32
  • 2
    Something useful to note in the solution above is that you need to handle the possible memory leak caused by the GDI Bitmap object - this is the `bmpImage.GetHbitmap()` in the given code. As [MSDN notes, this has to be cleared manually](https://msdn.microsoft.com/en-us/library/1dz311e4.aspx). [Here](https://msdn.microsoft.com/en-us/library/1dz311e4.aspx#Anchor_3) is a code example on how to do that to avoid the GDI Bitmap object taking up too much memory. – vburca Oct 14 '16 at 22:28
  • 1
    interesting, I have added additional code for the memory leak – fireydude Oct 17 '16 at 12:03
  • Anyone getting a black rectangle after the gif is created and displayed? – inoabrian Jan 27 '17 at 18:19
25

You might also consider using the ImageMagick library.

There are two .net wrappers for the library listed at http://www.imagemagick.org/script/api.php

Here is an example on how to do it using the Magick.net wrapper:

using (MagickImageCollection collection = new MagickImageCollection())
{
  // Add first image and set the animation delay to 100ms
  collection.Add("Snakeware.png");
  collection[0].AnimationDelay = 100;

  // Add second image, set the animation delay to 100ms and flip the image
  collection.Add("Snakeware.png");
  collection[1].AnimationDelay = 100;
  collection[1].Flip();

  // Optionally reduce colors
  QuantizeSettings settings = new QuantizeSettings();
  settings.Colors = 256;
  collection.Quantize(settings);

  // Optionally optimize the images (images should have the same size).
  collection.Optimize();

  // Save gif
  collection.Write("Snakeware.Animated.gif");
}
Bryan Legend
  • 6,790
  • 1
  • 59
  • 60
18

Whether or not calling imagemagick is the best choice is kind of hard to awnser without knowing the quality parameters that are important. Some other options would be:

these have the advantage that you don't have a dependency on a third partly library which might or might not be available on all systems executing your code.

This article at MS Support explains how to save a gif with a custom color table (this does require full trust). A animated gif is just a set of gifs for each image with some additional information in the header. So combining these two articles should get you what you need.

eshs
  • 194
  • 7
olle
  • 4,597
  • 24
  • 28
1

To use the sample from a Windows Forms app, add references to these assemblies:

C:\Program Files\Reference Assemblies\Microsoft\Framework.NETFramework\v4.0\PresentationCore.dll C:\Program Files\Reference Assemblies\Microsoft\Framework.NETFramework\v4.0\System.Xaml.dll C:\Program Files\Reference Assemblies\Microsoft\Framework.NETFramework\v4.0\WindowsBase.dll

Then

  • Int32Rect is in the System.Windows namespace

  • BitmapSizeOptions is in the System.Windows.Media.Imaging namespace

  • BitmapFrame is in the System.Windows.Media.Imaging namespace

Also, don't forget to close the file stream (something like this):

using(FileStream targetFile = new FileStream(path, FileMode.Create))
{
   gEnc.Save(targetFile);
}
BalintN
  • 141
  • 1
  • 7
1

The AnimatedGif package can make animated gifs.

using (var gif = AnimatedGif.AnimatedGif.Create(Path.Combine(outputPath, "output.gif"), 20))
{
    for (var i = 0; i < 10; i++)
    {
        Image img = Image.FromFile(Path.Combine(outputPath, $"{i.ToString().PadLeft(3, '0')}.png"));
        gif.AddFrame(img, delay: -1, quality: GifQuality.Bit8);
    }
}
Gabriel
  • 587
  • 5
  • 17
jaycer
  • 2,941
  • 2
  • 26
  • 36
0

I noticed that one more great alternative to ImageMagic and NGif is not listed in answers yet.

FFMpeg can be used for creating animated GIFs from:

  • sequence of images (files)
  • existing video clip (say, mp4 or avi)
  • from C# bitmap objects by providing input data as "ravvideo" through stdin (without using any temp files)

You can start ffmpeg.exe directly from C# code (with System.Diagnostics.Process) or use one of the existing .NET ffmpeg wrappers:

var ffmpeg = new NReco.VideoConverter.FFMpegConverter();
ffmpeg.ConvertMedia("your_clip.mp4", null, "result.gif", null, new ConvertSettings() );

(this code example uses free NReco VideoConverter - I'm an author of this component, feel free to ask any questions about its usage).

GIF size can be easily reduced by decreasing frame rate and/or frame size. Also it is possible to get fine-looking animated GIFs with 2-pass approach that generates optimal GIF palette.

Vitaliy Fedorchenko
  • 8,447
  • 3
  • 37
  • 34
  • Hey, I was looking for a code to convert multiple Images to an MP4 (h.264) format with NReco. I couldn't find any working example out there. Would you please help me through this? Thanks! – Moji Jan 20 '16 at 06:56
  • hi do you know how i can merge image with a video. Merging videos is working fine. But when i try to merge and image in between two videos it is not working with nreco. – Kanishka Jul 28 '16 at 10:06
  • @Kanishka if you know how to do that from the command line with ffmpeg.exe, the same is possible to do with VideoConverter wrapper too. – Vitaliy Fedorchenko Jul 28 '16 at 11:02
  • @VitaliyFedorchenko any idea about this question i have posted http://stackoverflow.com/questions/38613330/nreco-vedio-cut – Kanishka Jul 28 '16 at 11:25
  • more full real world samples using ***FFMpeg*** ? – PreguntonCojoneroCabrón Feb 28 '18 at 22:42
  • @PreguntonCojoneroCabrón take a look to the ffmpeg docs on this topic: https://trac.ffmpeg.org/wiki/Slideshow – Vitaliy Fedorchenko Mar 01 '18 at 06:56
0

Quote from fireydude answer: https://stackoverflow.com/a/16598294/8917485

This method is not complete, cause the .gif can't repeat. I found some additional code on other question, make .gif repeat.

.NET - Creating a looping .gif using GifBitmapEncoder http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp

The complete code should looks like the code below:

System.Windows.Media.Imaging.GifBitmapEncoder gEnc = new GifBitmapEncoder();
foreach (System.Drawing.Bitmap bmpImage in bitMaps)
{
    var bmp = bmpImage.GetHbitmap();
    var src = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
        bmp,
        IntPtr.Zero,
        Int32Rect.Empty,
        BitmapSizeOptions.FromEmptyOptions());
    gEnc.Frames.Add(BitmapFrame.Create(src));
    DeleteObject(bmp); // recommended, handle memory leak
    bmpImage.Dispose(); // recommended, handle memory leak
}

// After adding all frames to gifEncoder (the GifBitmapEncoder)...
using (var ms_ = new MemoryStream())
{
    gEnc.Save(ms_);
    var fileBytes = ms_.ToArray();
    // This is the NETSCAPE2.0 Application Extension.
    var applicationExtension = new byte[] { 33, 255, 11, 78, 69, 84, 83, 67, 65, 80, 69, 50, 46, 48, 3, 1, 0, 0, 0 };
    var newBytes = new List<byte>();
    newBytes.AddRange(fileBytes.Take(13));
    newBytes.AddRange(applicationExtension);
    newBytes.AddRange(fileBytes.Skip(13));
    File.WriteAllBytes("abc.gif", newBytes.ToArray());
}
江明哲
  • 27
  • 5