【改善版】C#でgifアニメを作る - ルパン三世のタイトルコール風

# まえがき
この前作った [C#でgifアニメを作る - ルパン三世のタイトルコール風](https://qiita.com/kob58im/items/3ebae01d624661c906ec) は、「繰り返せない」という問題があったので、改善してみました。
例によってWindows7以降[^1]であれば<b>インストール不要</b>で使えます。

タグに「Qiita」を入れているのは、Qiita投稿に活用できそうなので入れてみました。

# こんな感じ

![out2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/42d5cc98-f3fc-9d84-dfda-be4396da17a6.gif)

# 参考サイト

- [GIFアニメーションを作成する - dobon.net](https://dobon.net/vb/dotnet/graphics/createanimatedgif.html)

- [gif フォーマット - とほほのWWW入門]
(http://www.tohoho-web.com/wwwgif.htm)

# ソースコード - 各画像の生成

[こちら](https://qiita.com/kob58im/items/3ebae01d624661c906ec#%E6%96%87%E5%AD%97%E3%81%AE%E7%94%BB%E5%83%8F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB1%E6%9E%9A1%E6%9E%9A%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89)

# ソースコード - gifアニメファイルへの変換

gif生成部分(`MyGifEncorder`クラスの中身)は参考サイト(dobon.net)から拝借しました。
（そのままコピペするのもアレなので、少しだけいじってます。）

```csharp

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;


public static class MyGifEncorder
{
    public class BitmapAndDelayTime
    {
        public Bitmap bitmap;
        public ushort delayTime;
        public BitmapAndDelayTime(Bitmap _bitmap, ushort _delayTime){
            bitmap = _bitmap;
            delayTime = _delayTime;
        }
    }
    public static void SaveAnimatedGif(string fileName, List<BitmapAndDelayTime> baseImages, ushort loopCount)
    {
        if (baseImages.Count == 0) {
            return;
        }

        //書き込み先のファイルを開く
        var writerFs = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None);
        //BinaryWriterで書き込む
        var writer = new BinaryWriter(writerFs);
        var ms = new MemoryStream();

        try {
            bool hasGlobalColorTable = false;
            int colorTableSize = 0;
            int imagesCount = baseImages.Count;

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

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

                    // http://www.tohoho-web.com/wwwgif.htm#GIFHeader

                    //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] &= 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, (1<<(colorTableSize+1)) * 3);
                }

                //Graphics Control Extension
                writer.Write(GetGraphicControlExtension(baseImages[i].delayTime));

                {
                    byte[] tmp = PeekBytes(ms,2);
                    if (tmp[0] == 0x21 && tmp[1] == 0xF9) {
                        //基のGraphics Control Extensionをスキップ
                        ms.Position += 8;
                    }
                }

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

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

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

                ms.SetLength(0);                            //MemoryStreamをリセット
            }
        }
        finally {
            ms.Close();
            writer.Close();
            writerFs.Close();
        }
    }

    private static byte[] ReadBytes(MemoryStream ms, int count)
    {
        byte[] bs = new byte[count];
        int n = ms.Read(bs, 0, count);
        if ( n < count ) {
            throw new Exception("ReadBytes failed.");
        }
        return bs;
    }

    private static byte[] PeekBytes(MemoryStream ms, int count)
    {
        byte[] bs = new byte[count];
        long pos = ms.Position;
        int n = ms.Read(bs, 0, count);
        ms.Position = pos; // positionを戻す
        if ( n < count ) {
            throw new Exception("PeekBytes failed.");
        }
        return bs;
    }

    // loopCount: 繰り返し回数(0 = 無限)
    private static byte[] GetApplicationExtension(ushort loopCount)
    {
        byte[] bs = new byte[19] {
            0x21,               // [0] 拡張導入符 (Extension Introducer)
            0xFF,               // [1] アプリケーション拡張ラベル (Application Extension Label)
            0x0B,               // [2] ブロック寸法 (Block Size)
                                // [3..10] "NETSCAPE"  アプリケーション識別名 (Application Identifier)
            0x4E, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45,
            0x32, 0x2E, 0x30,   // [11..13] "2.0"  アプリケーション確証符号 (Application Authentication Code)
            0x03,               // [14] データ副ブロック寸法 (Data Sub-block Size)
            0x01,               // [15] 詰め込み欄 [ネットスケープ拡張コード (Netscape Extension Code)]
            0x00, 0x00,         // [16..17] ※以降の処理で代入  繰り返し回数 (Loop Count)
            0x00                // [18] ブロック終了符 (Block Terminator)
        };
        bs[16] = (byte)( loopCount     & 0xFF);
        bs[17] = (byte)((loopCount>>8) & 0xFF);
        return bs;
    }

    // delayTime: 遅延時間 (単位:10ms)
    private static byte[] GetGraphicControlExtension(ushort delayTime)
    {
        byte[] bs = new byte[8]{
            0x21,       // [0] 拡張導入符 (Extension Introducer)
            0xF9,       // [1] グラフィック制御ラベル (Graphic Control Label)
            0x04,       // [2] ブロック寸法 (Block Size, Byte Size)
        
            0x00,       // [3] 詰め込み欄 (Packed Field)
                        //     bit 0:            1=透過色指標を使う
                        //     bit 3-2: 消去方法: 1=そのまま残す  2=背景色でつぶす  3=直前の画像に戻す

            0x00, 0x00, // [4..5] ※以降の処理で代入   遅延時間 (Delay Time)
            0x00,       // [6] 透過色指標 (Transparency Index, Transparent Color Index)
            0x00        // [7] ブロック終了符 (Block Terminator)
        };
        bs[4] = (byte)( delayTime     & 0xFF);
        bs[5] = (byte)((delayTime>>8) & 0xFF);
        return bs;
    }
}

class GifEncoderTest2
{
    const int LoopCount = 0;

    [STAThread]
    static void Main(string[] args)
    {
        if (args.Length != 1) {
            Console.WriteLine("Argument error.");
            return;
        }

        ushort delayTime = 10; // unit:[10ms]
        ushort delayTimeEndFrame = 100; // unit:[10ms]

        const int MaxFrames = 1000;
        const int FileNameDigits = 3;

        string prefix = args[0];
        var bmps = new List<MyGifEncorder.BitmapAndDelayTime>();


        for (int i=0;i<MaxFrames;i++) {
            string s = prefix + i.ToString("D"+FileNameDigits.ToString()) + ".png";
            FileInfo fi = new FileInfo(s);

            if ( fi.Exists ) {
                Bitmap tmp = (Bitmap)Image.FromFile(fi.FullName);
                bmps.Add(new MyGifEncorder.BitmapAndDelayTime(tmp, delayTime));
            }
            else {
                if ( i==0 ) {
                    Console.WriteLine("File \"" + fi.FullName + "\" is not found.");
                    return;
                }
                break;
            }
        }

        // 最後のdelayを長くする
        bmps[bmps.Count-1].delayTime = delayTimeEndFrame;

        // 生成
        MyGifEncorder.SaveAnimatedGif("out.gif", bmps, LoopCount);
    }
}
```

[^1]: Windows10環境でしか試していないので、今回のプログラムがWindows7で動くかは未確認です。