【C#】Gif画像を分解してpngファイルで保存する

ゲーム開発で買ったアセットの 2D アニメーションが gif しかなかったので gif を分解して個別の png に保存する + 全て結合して SpriteSheet 化(Unityなどのゲーム開発環境の SpriteEditor などで利用可能な形式)するプログラムを作成してみました。

特にゲームに限定しなくても gif 画像を分解できるのツールとしても使えます。

確認環境

この記事は以下の環境で作成・確認を行っています

  • .NET8 + C#12
  • Visual Studio 2022 (17.14.18)
  • Windows11

構文が C#12 以前のバージョンでは一部コンパイルエラーになる構文が含まれます。

この実装は System.Drawing.Common を使用しているため Windows のみ動作します。

コード例

GifUnpackクラス

gif 画像を処理するクラスです。

以下の2つの機能があります。

  • Unpack: gif を分解して連番を付けて保存
  • CreateSpriteSheet: gif を分解して各画像を横一列に並べてSpriteSheetを作成
// GifUnpack.cs

using System.Drawing;
using System.Drawing.Imaging;

/// <summary>
/// Gifアニメーションを分解します。
/// </summary>
public class GifUnpack
{
    /// <summary>
    /// Gifの画像を分解して _001.png, 002.png のように連番ファイルを作成します。
    /// </summary>
    public void Unpack(string gifPath)
    {
        // 保存先ディレクトリ(GIFと同じディレクトリ)
        string outputDir = Path.GetDirectoryName(gifPath);
        string baseFileName = Path.GetFileNameWithoutExtension(gifPath);

        using Image gif = Image.FromFile(gifPath);

        // フレームの次元(時間ベースのアニメーションフレーム)を指定
        FrameDimension frameDim = new FrameDimension(gif.FrameDimensionsList[0]);
        int frameCount = gif.GetFrameCount(frameDim);

        for (int i = 0; i < frameCount; i++)
        {
            gif.SelectActiveFrame(frameDim, i);

            // ファイル名を生成、元の名前_001.png
            string frameFileName = $"{baseFileName}_{i:D3}.png";
            string outputPath = Path.Combine(outputDir, frameFileName);

            gif.Save(outputPath, ImageFormat.Png);
        }
    }

    /// <summary>
    /// Gifの画像を分解してアニメーションをSpriteSheet形式で横に並べて保存します。
    /// </summary>
    public void CreateSpriteSheet(string gifPath)
    {
        // 出力先の情報を先に作っておく
        string outDir = Path.GetDirectoryName(gifPath);
        string fileName = Path.GetFileNameWithoutExtension(gifPath);
        string sheetPath = Path.Combine(outDir, $"{fileName}_SpriteSheet.png");

        using Image gif = Image.FromFile(gifPath);

        // フレームの次元(時間ベースのアニメーションフレーム)を指定
        FrameDimension frameDim = new FrameDimension(gif.FrameDimensionsList[0]);
        int frameCount = gif.GetFrameCount(frameDim);

        // フレームのサイズを取得(すべてのフレームが同じサイズと仮定)
        gif.SelectActiveFrame(frameDim, 0);
        int frameWidth = gif.Width;
        int frameHeight = gif.Height;

        // スプライトシートのサイズ: 幅 = フレーム幅, 高さ = フレーム高さ * フレーム数
        int sheetWidth = frameWidth * frameCount;
        int sheetHeight = frameHeight;

        using Bitmap spriteSheet = new Bitmap(sheetWidth, sheetHeight);
        using Graphics g = Graphics.FromImage(spriteSheet);

        // 背景を透明に設定(PNG対応)
        g.Clear(Color.Transparent);

        // 各フレームを横方向にコピー
        for (int i = 0; i < frameCount; i++)
        {
            gif.SelectActiveFrame(frameDim, i);
            Rectangle destRect = new Rectangle(i * frameWidth, 0, frameWidth, frameHeight);
            g.DrawImage(gif, destRect, 0, 0, frameWidth, frameHeight, GraphicsUnit.Pixel);
        }

        // スプライトシートを保存
        spriteSheet.Save(sheetPath, ImageFormat.Png);
    }
}

使い方

上記の GifUnpack をコンソールから使えるように Main メソッドを以下の通り実装します。

// Program.cs

internal class Program
{
    static readonly GifUnpack _gifUnpack = new();

    // - - - - - - - - - -
    // 引数の説明:
    // [0] モードの指定
    //   - /both   : 個別に分解 + シートの作成
    //   - /unpack : 個別に分解のみ
    //   - /sheet  : シートの作成のみ
    // [1...] 対象の画像もしくは画像が入ってるフォルダパスのリスト(混在OK)
    // - - - - - - - - - -
    static void Main(string[] args)
    {
        // 引数チェック: オプションが指定されているか
        if (args.Length < 1)
        {
            Console.WriteLine("エラー: オプションを指定してください。" +
                "使用法: program.exe <オプション> <GIFファイルパス1> [パス2...]");
            Console.WriteLine("オプション: /both, /unpack, /sheet");
            return;
        }

        string option = args[0].ToLower();
        // オプションの有効性チェック
        if (option != "/both" && option != "/unpack" && option != "/sheet")
        {
            Console.WriteLine("エラー: 無効なオプションです。" +
                "有効: /both, /unpack, /sheet");
            return;
        }

        // ファイルパスのリストを取得(オプションをスキップ)
        string[] pathList = [.. args.Skip(1)];
        if (pathList.Length == 0)
        {
            Console.WriteLine("エラー: 処理対象のGIFファイルを指定してください。");
            return;
        }

        foreach (string path in pathList)
        {
            if (Directory.Exists(path))
            {
                // 直下のファイルのみサポート
                string[] files = Directory.GetFiles(path);
                Proc(files, option);
            }
            else if (File.Exists(path))
            {
                Proc([path], option);
            }
        }
    }

    static void Proc(IEnumerable<string> files, string option)
    {
        foreach (string filePath in files)
        {
            // 各ファイルの存在と拡張子チェック
            if (!File.Exists(filePath))
            {
                Console.WriteLine($"エラー: ファイルが見つかりません: {filePath}");
                continue;
            }

            if (option == "/both" || option == "/unpack")
            {
                _gifUnpack.Unpack(filePath);
            }
            if (option == "/both" || option == "/sheet")
            {
                _gifUnpack.CreateSpriteSheet(filePath);
            }
        }
    }
}


// コマンドライン例:
GifUnpacker.exe /both sample.gif

// 出力例
C:\Temp
  |
  +  sample.gif // 入力:内容が3枚のアニメーションの場合
  |
  + sample_001.png // 出力:各フレームの画像
  + sample_002.png
  + sample_003.png
  + sample_SpriteSheet.png // 各フレームを結合したもの

最後に

ツール化しているので大量に画像を処理する場合、ひとつひとつ Web のツールなどで手動で変換するより時間が節約できると思います。

フリーの Web サービスの場合広告が表示されたり変換に上限があると思うのでローカルでツール化するのは悪くないと思います。

ツールの想定として、アニメーションは小さいサイズの画像が10フレーム程度なので1列に横に並べていますが、大きい画像やフレーム数が多いなどの場合、複数列に分割するなど用途によって少し改変する必要があるかもしれません。