DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

GIFアニメーションを作成する

GIFアニメーションを作る方法は、「HOWTO: create an animated GIF using .Net (C#)」や「How could i create an animated .gif file from several other .jpg files in c# express?」で紹介されています。前者の記事はグローバルパレットだけを使っていますが、後者(および、前者に投稿されているコメント)ではローカルパレットだけを使っているようです。

これらの記事と、GIFのフォーマットについて書かれた「GIF89a仕様書抜粋」や「3MF Project: What's In A GIF - Animation and Transparency」を参考にして、ローカルパレットのみを使うGIFアニメーションを作成するコードを書いてみました。

VB.NET
コードを隠すコードを選択
'Imports System.Drawing
'Imports System.IO

''' <summary>
''' 複数の画像をGIFアニメーションとして保存する
''' </summary>
''' <param name="fileName">保存先のファイルのパス。</param>
''' <param name="baseImages">GIFアニメにする画像。</param>
''' <param name="delayTime">遅延時間(100分の1秒単位)。</param>
''' <param name="loopCount">繰り返す回数。0で無限。</param>
Public Shared Sub SaveAnimatedGif(ByVal fileName As String, _
                                  ByVal baseImages As Bitmap(), _
                                  ByVal delayTime As UInt16, _
                                  ByVal loopCount As UInt16)
    '書き込み先のファイルを開く
    Dim writerFs As New FileStream(fileName, _
        FileMode.Create, FileAccess.Write, FileShare.None)
    'BinaryWriterで書き込む
    Dim writer As New BinaryWriter(writerFs)

    Dim ms As New MemoryStream()
    Dim hasGlobalColorTable As Boolean = False
    Dim colorTableSize As Integer = 0

    Dim imagesCount As Integer = baseImages.Length
    For i As Integer = 0 To imagesCount - 1
        '画像をGIFに変換して、MemoryStreamに入れる
        Dim bmp As Bitmap = baseImages(i)
        bmp.Save(ms, ImageFormat.Gif)
        ms.Position = 0

        If i = 0 Then
            'ヘッダを書き込む
            'Header
            writer.Write(ReadBytes(ms, 6))

            'Logical Screen Descriptor
            Dim screenDescriptor As Byte() = ReadBytes(ms, 7)
            'Global Color Tableがあるか確認
            If (screenDescriptor(4) And &H80) <> 0 Then
                'Color Tableのサイズを取得
                colorTableSize = screenDescriptor(4) And &H7
                hasGlobalColorTable = True
            Else
                hasGlobalColorTable = False
            End If
            'Global Color Tableを使わない
            '広域配色表フラグと広域配色表の寸法を消す
            screenDescriptor(4) = CByte(screenDescriptor(4) And &H78)
            writer.Write(screenDescriptor)

            'Application Extension
            writer.Write(GetApplicationExtension(loopCount))
        Else
            'HeaderとLogical Screen Descriptorをスキップ
            ms.Position += 6 + 7
        End If

        Dim colorTable As Byte() = Nothing
        If hasGlobalColorTable Then
            'Color Tableを取得
            colorTable = ReadBytes(ms, CInt(Math.Pow(2, colorTableSize + 1)) * 3)
        End If

        'Graphics Control Extension
        writer.Write(GetGraphicControlExtension(delayTime))
        '基のGraphics Control Extensionをスキップ
        If ms.GetBuffer()(ms.Position) = &H21 Then
            ms.Position += 8
        End If

        'Image Descriptor
        Dim imageDescriptor As Byte() = ReadBytes(ms, 10)
        If Not hasGlobalColorTable Then
            'Local Color Tableを持っているか確認
            If (imageDescriptor(9) And &H80) = 0 Then
                Throw New Exception("Not found color table.")
            End If
            'Color Tableのサイズを取得
            colorTableSize = imageDescriptor(9) And 7
            'Color Tableを取得
            colorTable = ReadBytes(ms, CInt(Math.Pow(2, colorTableSize + 1)) * 3)
        End If
        '狭域配色表フラグ (Local Color Table Flag) と狭域配色表の寸法を追加
        imageDescriptor(9) = CByte(imageDescriptor(9) Or &H80 Or colorTableSize)
        writer.Write(imageDescriptor)

        'Local Color Tableを書き込む
        writer.Write(colorTable)

        'Image Dataを書き込む (終了部は書き込まない)
        writer.Write(ReadBytes(ms, CInt(ms.Length - ms.Position - 1)))

        If i = imagesCount - 1 Then
            '終了部 (Trailer)
            writer.Write(CByte(&H3B))
        End If

        'MemoryStreamをリセット
        ms.SetLength(0)
    Next

    '後始末
    ms.Close()
    writer.Close()
    writerFs.Close()
End Sub

''' <summary>
''' MemoryStreamの現在の位置から指定されたサイズのバイト配列を読み取る
''' </summary>
''' <param name="ms">読み取るMemoryStream</param>
''' <param name="count">読み取るバイトのサイズ</param>
''' <returns>読み取れたバイト配列</returns>
Private Shared Function ReadBytes(ByVal ms As MemoryStream, _
                                  ByVal count As Integer) As Byte()
    Dim bs As Byte() = New Byte(count - 1) {}
    ms.Read(bs, 0, count)
    Return bs
End Function

''' <summary>
''' Netscape Application Extensionブロックを返す。
''' </summary>
''' <param name="loopCount">繰り返す回数。0で無限。</param>
''' <returns>Netscape Application Extensionブロックのbyte配列。</returns>
Private Shared Function GetApplicationExtension(ByVal loopCount As UInt16) _
        As Byte()
    Dim bs As Byte() = New Byte(18) {}

    '拡張導入符 (Extension Introducer)
    bs(0) = &H21
    'アプリケーション拡張ラベル (Application Extension Label)
    bs(1) = &HFF
    'ブロック寸法 (Block Size)
    bs(2) = &HB
    'アプリケーション識別名 (Application Identifier)
    bs(3) = CByte(AscW("N"c))
    bs(4) = CByte(AscW("E"c))
    bs(5) = CByte(AscW("T"c))
    bs(6) = CByte(AscW("S"c))
    bs(7) = CByte(AscW("C"c))
    bs(8) = CByte(AscW("A"c))
    bs(9) = CByte(AscW("P"c))
    bs(10) = CByte(AscW("E"c))
    'アプリケーション確証符号 (Application Authentication Code)
    bs(11) = CByte(AscW("2"c))
    bs(12) = CByte(AscW("."c))
    bs(13) = CByte(AscW("0"c))
    'データ副ブロック寸法 (Data Sub-block Size)
    bs(14) = &H3
    '詰め込み欄 [ネットスケープ拡張コード (Netscape Extension Code)]
    bs(15) = &H1
    '繰り返し回数 (Loop Count)
    Dim loopCountBytes As Byte() = BitConverter.GetBytes(loopCount)
    bs(16) = loopCountBytes(0)
    bs(17) = loopCountBytes(1)
    'ブロック終了符 (Block Terminator)
    bs(18) = &H0

    Return bs
End Function

''' <summary>
''' Graphic Control Extensionブロックを返す。
''' </summary>
''' <param name="delayTime">遅延時間(100分の1秒単位)。</param>
''' <returns>Graphic Control Extensionブロックのbyte配列。</returns>
Private Shared Function GetGraphicControlExtension(ByVal delayTime As UInt16) _
        As Byte()
    Dim bs As Byte() = New Byte(7) {}

    '拡張導入符 (Extension Introducer)
    bs(0) = &H21
    'グラフィック制御ラベル (Graphic Control Label)
    bs(1) = &HF9
    'ブロック寸法 (Block Size, Byte Size)
    bs(2) = &H4
    '詰め込み欄 (Packed Field)
    '透過色指標を使う時は+1
    '消去方法:そのまま残す+4、背景色でつぶす+8、直前の画像に戻す+12
    bs(3) = &H0
    '遅延時間 (Delay Time)
    Dim delayTimeBytes As Byte() = BitConverter.GetBytes(delayTime)
    bs(4) = delayTimeBytes(0)
    bs(5) = delayTimeBytes(1)
    '透過色指標 (Transparency Index, Transparent Color Index)
    bs(6) = &H0
    'ブロック終了符 (Block Terminator)
    bs(7) = &H0

    Return bs
End Function
C#
コードを隠すコードを選択
//using System.Drawing;
//using System.IO;

/// <summary>
/// 複数の画像をGIFアニメーションとして保存する
/// </summary>
/// <param name="fileName">保存先のファイルのパス。</param>
/// <param name="baseImages">GIFアニメにする画像。</param>
/// <param name="delayTime">遅延時間(100分の1秒単位)。</param>
/// <param name="loopCount">繰り返す回数。0で無限。</param>
public static void SaveAnimatedGif(string fileName,
    Bitmap[] baseImages, UInt16 delayTime, UInt16 loopCount)
{
    //書き込み先のファイルを開く
    FileStream writerFs = new FileStream(fileName,
        FileMode.Create, FileAccess.Write, FileShare.None);
    //BinaryWriterで書き込む
    BinaryWriter writer = new BinaryWriter(writerFs);

    MemoryStream ms = new MemoryStream();
    bool hasGlobalColorTable = false;
    int colorTableSize = 0;

    int imagesCount = baseImages.Length;
    for (int i = 0; i < imagesCount; i++)
    {
        //画像をGIFに変換して、MemoryStreamに入れる
        Bitmap bmp = baseImages[i];
        bmp.Save(ms, ImageFormat.Gif);
        ms.Position = 0;

        if (i == 0)
        {
            //ヘッダを書き込む
            //Header
            writer.Write(ReadBytes(ms, 6));

            //Logical Screen Descriptor
            byte[] screenDescriptor = ReadBytes(ms, 7);
            //Global Color Tableがあるか確認
            if ((screenDescriptor[4] & 0x80) != 0)
            {
                //Color Tableのサイズを取得
                colorTableSize = screenDescriptor[4] & 0x07;
                hasGlobalColorTable = true;
            }
            else
            {
                hasGlobalColorTable = false;
            }
            //Global Color Tableを使わない
            //広域配色表フラグと広域配色表の寸法を消す
            screenDescriptor[4] = (byte)(screenDescriptor[4] & 0x78);
            writer.Write(screenDescriptor);

            //Application Extension
            writer.Write(GetApplicationExtension(loopCount));
        }
        else
        {
            //HeaderとLogical Screen Descriptorをスキップ
            ms.Position += 6 + 7;
        }

        byte[] colorTable = null;
        if (hasGlobalColorTable)
        {
            //Color Tableを取得
            colorTable = ReadBytes(ms, (int)Math.Pow(2, colorTableSize + 1) * 3);
        }

        //Graphics Control Extension
        writer.Write(GetGraphicControlExtension(delayTime));
        //基のGraphics Control Extensionをスキップ
        if (ms.GetBuffer()[ms.Position] == 0x21)
        {
            ms.Position += 8;
        }

        //Image Descriptor
        byte[] imageDescriptor = ReadBytes(ms, 10);
        if (!hasGlobalColorTable)
        {
            //Local Color Tableを持っているか確認
            if ((imageDescriptor[9] & 0x80) == 0)
                throw new Exception("Not found color table.");
            //Color Tableのサイズを取得
            colorTableSize = imageDescriptor[9] & 7;
            //Color Tableを取得
            colorTable = ReadBytes(ms, (int)Math.Pow(2, colorTableSize + 1) * 3);
        }
        //狭域配色表フラグ (Local Color Table Flag) と狭域配色表の寸法を追加
        imageDescriptor[9] = (byte)(imageDescriptor[9] | 0x80 | colorTableSize);
        writer.Write(imageDescriptor);

        //Local Color Tableを書き込む
        writer.Write(colorTable);

        //Image Dataを書き込む (終了部は書き込まない)
        writer.Write(ReadBytes(ms, (int)(ms.Length - ms.Position - 1)));

        if (i == imagesCount - 1)
        {
            //終了部 (Trailer)
            writer.Write((byte)0x3B);
        }

        //MemoryStreamをリセット
        ms.SetLength(0);
    }

    //後始末
    ms.Close();
    writer.Close();
    writerFs.Close();
}

/// <summary>
/// MemoryStreamの現在の位置から指定されたサイズのバイト配列を読み取る
/// </summary>
/// <param name="ms">読み取るMemoryStream</param>
/// <param name="count">読み取るバイトのサイズ</param>
/// <returns>読み取れたバイト配列</returns>
private static byte[] ReadBytes(MemoryStream ms, int count)
{
    byte[] bs = new byte[count];
    ms.Read(bs, 0, count);
    return bs;
}

/// <summary>
/// Netscape Application Extensionブロックを返す。
/// </summary>
/// <param name="loopCount">繰り返す回数。0で無限。</param>
/// <returns>Netscape Application Extensionブロックのbyte配列。</returns>
private static byte[] GetApplicationExtension(UInt16 loopCount)
{
    byte[] bs = new byte[19];

    //拡張導入符 (Extension Introducer)
    bs[0] = 0x21;
    //アプリケーション拡張ラベル (Application Extension Label)
    bs[1] = 0xFF;
    //ブロック寸法 (Block Size)
    bs[2] = 0x0B;
    //アプリケーション識別名 (Application Identifier)
    bs[3] = (byte)'N';
    bs[4] = (byte)'E';
    bs[5] = (byte)'T';
    bs[6] = (byte)'S';
    bs[7] = (byte)'C';
    bs[8] = (byte)'A';
    bs[9] = (byte)'P';
    bs[10] = (byte)'E';
    //アプリケーション確証符号 (Application Authentication Code)
    bs[11] = (byte)'2';
    bs[12] = (byte)'.';
    bs[13] = (byte)'0';
    //データ副ブロック寸法 (Data Sub-block Size)
    bs[14] = 0x03;
    //詰め込み欄 [ネットスケープ拡張コード (Netscape Extension Code)]
    bs[15] = 0x01;
    //繰り返し回数 (Loop Count)
    byte[] loopCountBytes = BitConverter.GetBytes(loopCount);
    bs[16] = loopCountBytes[0];
    bs[17] = loopCountBytes[1];
    //ブロック終了符 (Block Terminator)
    bs[18] = 0x00;

    return bs;
}

/// <summary>
/// Graphic Control Extensionブロックを返す。
/// </summary>
/// <param name="delayTime">遅延時間(100分の1秒単位)。</param>
/// <returns>Graphic Control Extensionブロックのbyte配列。</returns>
private static byte[] GetGraphicControlExtension(UInt16 delayTime)
{
    byte[] bs = new byte[8];

    //拡張導入符 (Extension Introducer)
    bs[0] = 0x21;
    //グラフィック制御ラベル (Graphic Control Label)
    bs[1] = 0xF9;
    //ブロック寸法 (Block Size, Byte Size)
    bs[2] = 0x04;
    //詰め込み欄 (Packed Field)
    //透過色指標を使う時は+1
    //消去方法:そのまま残す+4、背景色でつぶす+8、直前の画像に戻す+12
    bs[3] = 0x00;
    //遅延時間 (Delay Time)
    byte[] delayTimeBytes = BitConverter.GetBytes(delayTime);
    bs[4] = delayTimeBytes[0];
    bs[5] = delayTimeBytes[1];
    //透過色指標 (Transparency Index, Transparent Color Index)
    bs[6] = 0x00;
    //ブロック終了符 (Block Terminator)
    bs[7] = 0x00;

    return bs;
}

.NET Framework 3.0以降で、GifBitmapEncoderを使う方法

.NET Framework 3.0以降では、WPFの機能であるGifBitmapEncoderクラスを使って作成することもできます。ただし、繰り返し回数や遅延時間は指定できません。

以下に例を示します。PresentationCoreとWindowsBase、さらにVB.NETの場合はSystem.Xamlを参照設定に追加する必要があります。

VB.NET
コードを隠すコードを選択
'Imports System.Windows.Media.Imaging
'Imports System.IO

''' <summary>
''' 複数の画像をGIFアニメーションとして保存する
''' </summary>
''' <param name="savePath">保存先のファイルのパス</param>
''' <param name="imageFiles">GIFに追加する画像ファイルのパス</param>
Public Shared Sub CreateAnimatedGif(ByVal savePath As String, _
                                    ByVal imageFiles As String())
    'GifBitmapEncoderを作成する
    Dim encoder As New GifBitmapEncoder()

    For Each f As String In imageFiles
        '画像ファイルからBitmapFrameを作成する
        Dim bmpFrame As BitmapFrame = _
            BitmapFrame.Create(New Uri(f, UriKind.RelativeOrAbsolute))
        'フレームに追加する
        encoder.Frames.Add(bmpFrame)
    Next

    '書き込むファイルを開く
    Dim outputFileStrm As New FileStream(savePath, _
        FileMode.Create, FileAccess.Write, FileShare.None)
    '保存する
    encoder.Save(outputFileStrm)
    '閉じる
    outputFileStrm.Close()
End Sub
C#
コードを隠すコードを選択
//using System.Windows.Media.Imaging;
//using System.IO;

/// <summary>
/// 複数の画像をGIFアニメーションとして保存する
/// </summary>
/// <param name="savePath">保存先のファイルのパス</param>
/// <param name="imageFiles">GIFに追加する画像ファイルのパス</param>
public static void CreateAnimatedGif(string savePath, string[] imageFiles)
{
    //GifBitmapEncoderを作成する
    GifBitmapEncoder encoder = new GifBitmapEncoder();

    foreach (string f in imageFiles)
    {
        //画像ファイルからBitmapFrameを作成する
        BitmapFrame bmpFrame =
            BitmapFrame.Create(new Uri(f, UriKind.RelativeOrAbsolute));
        //フレームに追加する
        encoder.Frames.Add(bmpFrame);
    }

    //書き込むファイルを開く
    FileStream outputFileStrm = new FileStream(savePath,
        FileMode.Create, FileAccess.Write, FileShare.None);
    //保存する
    encoder.Save(outputFileStrm);
    //閉じる
    outputFileStrm.Close();
}

出来合いのライブラリを使う方法

ImageMagick」という定評のあるフリーの画像処理ソフトウェアがありますが、これの.NETラッパーとして「MagickNet」があります。これを使ってGIFアニメを作成することができます。

また、CodeProjectには「NGif」というGIFアニメを作成するクラスが公開されています。

  • 履歴:
  • 2014/5/12 誤字修正。
  • 2016/9/4 リンク切れを修正。Graphic Control Extensionの詰め込み欄の説明を増やす。

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • このサイトで紹介されているコードの多くは、例外処理が省略されています。例外処理については、こちらをご覧ください。
  • コードの先頭に記述されている「Imports ??? がソースファイルの一番上に書かれているものとする」(C#では、「using ???; がソースファイルの一番上に書かれているものとする」)の意味が分からないという方は、こちらをご覧ください。
  • 「???を参照に追加します」の意味が分からないという方は、こちらをご覧ください。
  • .NET Tipsをご利用いただく際は、注意事項をお守りください。