GIF 画像出力

2008年01月27日
米 Unisys 社が取得していた GIF の LZW 圧縮に関する特許が2004年6月20日に切れ、 やっと Java SE 6 から GIF への出力がサポートされるようになりました。 このページでは Java SE 6 の Image I/O を使用してアニメーション GIF を作成します。

サンプルプログラム

最初に Java でアニメーション GIF を作成するサンプルプログラムを載せておきます ので、説明よりソースを見たほうが早いという人はこちらをご覧下さい。

Sample Animation このプログラムは右のような 0~9 までの数字を切り替える簡単なアニメーション GIF ファイルを作成します。

また GIF フォーマットの出力が行いたいだけでアニメーションや詳細なフォーマット 指定は不要という場合は ImageIO#write()Java™ API リファレンス を使用してください。

BufferedImage image = ...;
File file = new File("foo.gif");
ImageIO.write(image, "GIF", file);

GIF フォーマット

ビットシフト LZW 圧縮の複雑さを別にすれば、GIF は非常にシンプルで拡張しやすい 設計がなされたバイナリフォーマットです。また低速なネットワークの時代に少しでも データサイズを小さくして画像を効率的に使用するための工夫があちこちに見られます。 ここでの解説は Image I/O が使える程度にとどめますが、勉強が目的であれば フォーマット設計の意図などを考えながら少々突っ込んで調べてみるのも面白い かもしれません。

概要

GIF フォーマットは GIF ヘッダ、終端ブロック以外の1 つ以上のブロック (GIF87a は一つの画像ブロックのみ)、終端ブロックで構成されています。 ブロックは画像ブロック、コメントブロック、アプリケーション拡張ブロック、 グラフィック制御ブロック、プレーンテキストブロックの 5 種類が存在し、種類や 順序関係なく出現して良いことになっています。

GIFフォーマット図解

カラーテーブル

カラーテーブルの説明図 GIF フォーマットは同時に 2~256 個の色を扱う事の出来るカラーテーブル (Color Table) 形式の画像です。

赤、青、緑 3 つのカラーチャネルそれぞれに 8 ビット (256) の諧調があれば 人間の目で識別できる全ての色が表現できます (24ビット色)。しかし少ない 色数しか使用していない画像に 1 ピクセル 3 バイトもの幅を持たせる のは効率が悪いため、カラーテーブルに定義した色のインデックスのみを ピクセル情報に持たせることでデータサイズを小さくすることが出来ます。 つまりカラーテーブルとは RGB 色データを定義した配列の事です。

GIF フォーマットには、ヘッダに定義するグローバルカラーテーブル (Global Color Table) と、個別の画像ブロックに定義するローカルカラー テーブル (Local Color Table) の 2 種類が存在します。 全ての画像ブロックが同じカラーテーブルを共有できるならグローバルカラー テーブルを使用するとデータ量を削減することが出来ます。

サブブロック

サブブロック (Sub-block) とは任意の長さのデータを小分けした一塊です。 データ長を表す 1 バイトとそれに続くデータで 1 つのサブブロックが構成されて おり、データ長 0 のサブブロックがデータの終わりを示しています (HTTP/1.1 の chunked メッセージに似ています)。つまり GIF のサブブロックは

サブブロックのサイズは最大でも 256 バイトです。ただし長さを示す 1 バイト があるので 1 サブブロックに格納できるデータの最大サイズは 255 バイトです。 例えば 833 バイトのデータは {255, 255, 255, 68, 0} というデータ長の 5 個 のサブブロックに分割できます。またサブブロックのデータ長は 0 以外なら 問題ないため {1, 1, ..., 1, 0} という 834 個のサブブロックにも分割できます。

コメントブロックやグラフィック制御ブロックなど、GIF89a で追加された拡張 ブロックには識別子の後がサブブロック形式になっています。つまり、アプリケー ションが認識できない識別子のブロックを検出したとしてもサブブロックとして 読み飛ばす事が出来るよう設計されています。

サブブロックに関しては Image I/O が処理してくれているため、GIF のバイナリを 直接扱うのでなければ意識する必要はありません。

データモデル・マッピング

GIF フォーマットの透過色やインターレースなどの出力形式固有の情報は、 Image I/O では画像に付属するメタデータ (Metadata) とみなされます。

Image I/O は TIFF などのような自由度が高く複雑な内部構造を持つ画像を サポートするためにメタデータを階層構造で表現しています。そしてこれは Image I/O 独自の XML DOM 実装で行われています (DOM で実装する利点が あまり見受けられないのですが…)。

メタデータにはストリーム (ファイル) 全体を表すストリームメタデータと、画像 それぞれを表す画像メタデータの 2 種類が存在します。GIF フォーマットでは ストリームメタデータが GIF ヘッダに、グラフィック制御ブロックなどが画像 メタデータになります。それぞれのメタデータは ImageWriter から参照 するメソッドが違うので注意してください。

Image I/O がサポートする画像形式でどのようなメタデータが利用できるかは Image I/O の API リファレンスJava™ API リファレンス*1を参照してください (DTD や Schema で記載されています)。

メタデータとして設定されたコメントやグラフィック制御の情報は 画像ブロックの前に配置されます。従って一番最後のブロック (終端ブロックの前) にコメントブロックなどを配置できないので 注意して下さい。

*1 プレーンテキスト拡張 ブロックに text 属性が抜けています。また characterCellWidth, characterCellHeight が 2 バイトのようになっていますが実際は 1 バイト です。

GIF 画像出力

出力先の取得

まず ImageIOJava™ API リファレンス クラスを使用して GIF フォーマット用の ImageWriterJava™ API リファレンス を参照します (Writer という名前は付いていますが java.io パッケージのそれ とは関係ありません)。下記では省略していますが、 ImageOutputStreamJava™ API リファレンス を取得後は try-finally で囲って画像出力処理を終了する時に確実にクローズする必要が あります。

Iterator<ImageWriter> it = ImageIO.getImageWritersByFormatName("GIF");
if(! it.hasNext()){
    throw new IllegalStateException();
}
ImageWriter writer = it.next();
ImageOutputStream out = ImageIO.createImageOutputStream(file);
writer.setOutput(out);

GIF ヘッダの設定

次に GIF ヘッダを設定します。サイズやカラーテーブルなどをすべて Image I/O お任せでラスタ画像から算出させるのであれば単純に null を指定する だけでかまいません (この呼び出しは省略できません)。

writer.prepareWriteSequence(null);

GIF バージョンや論理画面記述子を明示的に指定したい場合はストリームメタ データを取得して必要な要素と属性を設定します。以下の例は論理画面を 200×200 に指定しています。

IIOMetadata meta = writer.getDefaultStreamMetadata(null);
String format = meta.getNativeMetadataFormatName();
IIOMetadataNode root = (IIOMetadataNode)meta.getAsTree(format);
IIOMetadataNode node = new IIOMetadataNode("LogicalScreenDescriptor");
node.setAttribute("logicalScreenWidth", "200");
node.setAttribute("logicalScreenHeight", "200");
node.setAttribute("colorResolution", "8");
node.setAttribute("pixelAspectRatio", "0");
root.appendChild(node);
meta.setFromTree(format, root);
writer.prepareWriteSequence(meta);

ヘッダの情報は最初の 1 枚目の画像に基づいて決定されるようなので、 2 枚目以降が色化けがしてしまうような場合は画像個別に指定してください。

画像の設定

続いて画像やその他のブロックを追加します。画像以外のブロックは画像に付随する メタデータとして指定しないといけないことに注意してください。以下の例は 1 秒の待ち時間を設定したグラフィック制御ブロックを付けた画像ブロックを保存 します。

meta = writer.getDefaultImageMetadata(
    ImageTypeSpecifier.createFromRenderedImage(image), null);
format = meta.getNativeMetadataFormatName();
root = (IIOMetadataNode)meta.getAsTree(format);
// ※1
node = new IIOMetadataNode("GraphicControlExtension");
node.setAttribute("disposalMethod", "none");
node.setAttribute("userInputFlag", "FALSE");
node.setAttribute("transparentColorFlag", "FALSE");
node.setAttribute("delayTime", "100");
node.setAttribute("transparentColorIndex", "0");
root.appendChild(node);
meta.setFromTree(format, root);
writer.writeToSequence(new IIOImage(image, null, meta), null);

この処理を必要な画像の枚数分だけ繰り返せば GIF アニメーションを作成する ことが出来ます。ただしアニメーションをループさせたい場合は、最初の画像 を追加するときに以下のコードを上記※1の位置に記述してください。変数 count はループ回数であり 0 は無限にループすることを意味します。

int count = 0;
byte[] data = {
  0x01,
  (byte)((count >> 0) & 0xFF),
  (byte)((count >> 8) & 0xFF)
};
IIOMetadataNode list = new IIOMetadataNode("ApplicationExtensions");
node = new IIOMetadataNode("ApplicationExtension");
node.setAttribute("applicationID", "NETSCAPE");
node.setAttribute("authenticationCode", "2.0");
node.setUserObject(data);
list.appendChild(node);
root.appendChild(list);

他、コメントブロックに簡単な著作権等を入れておくのも良いかも知れません。

終了処理

全ての画像を書き込み終わったら endWriteSequence() を呼び出します。これで 終端ブロックが書き込まれます。

writer.endWriteSequence();
out.close();

CVS 2008/01/29