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アニメーションを作成するコードを書いてみました。
'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
//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以降では、WPFの機能であるGifBitmapEncoderクラスを使って作成することもできます。ただし、繰り返し回数や遅延時間は指定できません。
以下に例を示します。PresentationCoreとWindowsBase、さらにVB.NETの場合はSystem.Xamlを参照設定に追加する必要があります。
'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
//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アニメを作成するクラスが公開されています。
注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。