MagickNetでGIFアニメーションを作ろう

前回は9分割した画像を合成してもとに戻すというシナリオで画像合成について説明しました。

今回はGIFアニメーションを作成してみましょう。
シナリオとしては、前回まで使っていた
sample-mini
を9分割し、600ms間隔で各パーツを表示し
最後のパーツが表示されてから600ms後に合成されたものを表示するというGIFアニメーションを作成します。

今回は各フレームをMagickImageであらわし、それを束ねてアニメーションを行うためMagickImageCollectionというクラスを利用します。
MagickImageCollectionに複数のMagickImageを束ねて出力することでGIFアニメーションを作成することができます。

事前に今回3つのポイントについて説明します。

一つめは以下のコンストラクタを使ってまとめて登録することが可能です。

using(var collection in new MagickImageCollection(IEnumerable<MagickImage> images))
{
 //処理
}

追記:初出時、60msと誤記していましたが、600msの間違いでした。既に訂正済みです。

また、600msの間隔はcollectionで指定するのではなく、各フレーム用のMagickImage側で指定します。
以下のプロパティに値を設定します。
単位は1/100secで1です。つまり60と指定すると600msとなります。
前のフレームに対して600ms間隔をあけて表示されます。

MagickNet.AnimationDelay = 60;

最後に各フレーム間の差があるところだけを使うことでファイルを最適化するために以下のメソッドを使います。
MagickImageCollectionに用意されています。

MagickImageCollection.Optimize();

また、今回は分割した結果はファイルではなくバイト配列で持ちます。
上記を踏まえてサンプルコードは以下のようになります。

        private static void GIFアニメーションを作るよ()
        {
            var path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "sample.jpg");
            byte[] byteArray = null;
            var width = 0;
            var height = 0;
            using (var image = new MagickImage(path))
            {
                //リサイズ他にもスケールする%を渡したりなど、いろいろできるので今回は20%指定で
                image.Resize(0.2);

                //リサイズ後のサイズを取得
                width = image.Width;
                height = image.Height;

                //pngを指定するよ
                image.Format = MagickFormat.Png;

                //バイト配列に出力
                byteArray = image.ToByteArray();
            }

            //今回は分割した部分はバイト配列で持っておきます
            var images = Enumerable.Range(0, 9).Select(i =>
            {
                using (var divImage = new MagickImage(byteArray))
                {
                    var x = width / 3 * (i % 3);
                    var y = height / 3 * (i / 3);

                    divImage.Crop(new MagickGeometry(x, y, width / 3, height / 3));

                    return new { index = i, image = divImage.ToByteArray() };
                }
            }).ToDictionary(x => x.index, x => x.image);

            //MaigckImageを束ねるコレクションに9分割したそれぞれの部分をまとめて登録します
            using(var collection = new MagickImageCollection(
                Enumerable.Range(0, 9).Select(i =>
                {
                    var canvas = new MagickImage(MagickColor.Transparent, width, height);
                    using (var divImage = new MagickImage(images[i]))
                    {
                        var x = width / 3 * (i % 3);
                        var y = height / 3 * (i / 3);
                        //ここで合成しているよ。
                        canvas.Composite(divImage, x, y, CompositeOperator.Over);
                    }
                    //ここで600msずつ遅らせることを指定します
                    canvas.AnimationDelay = 60;
                    return canvas;
                })))
            {
                
                //すべての部分が表示されるコマを作成します
                var canvas = new MagickImage(MagickColor.Transparent, width, height);
                foreach(var i in Enumerable.Range(0, 9))
                {
                    using (var divImage = new MagickImage(images[i]))
                    {
                        var x = width / 3 * (i % 3);
                        var y = height / 3 * (i / 3);
                        //ここで合成しているよ。
                        canvas.Composite(divImage, x, y, CompositeOperator.Over);
                    }
                    //ここでも600ms遅らせることを指定します
                    canvas.AnimationDelay = 60;
                }
                collection.Add(canvas);

                //保存するまえにAnimationGifにする時各コマの差分だけがCropされてフレームになるように最適化を行います。
                collection.Optimize();
               
                var outputPngPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "animation.gif");
                collection.Write(outputPngPath);
            }

        }

そして結果は以下のようになります。

animation

このように、MagickNetでは少ないコード量でGIFアニメーションを作ることが可能です。

追記:
ちなみに、MagickImage側でAnimationDelayするならCollectionじゃなくても前回までのMagickImage.Compositeでいいのでは?と思うかも(思わないかも)しれませんが、その指定は無視されてただの合成画像になるだけです。

Share