GIF 画像出力

Takami Torao Java 6
  • このエントリーをはてなブックマークに追加

米 Unisys 社が取得していた GIF の LZW 圧縮に関する特許が2004年6月20日に切れ Java SE 6 から GIF への出力がサポートされた。このページでは Java SE 6 の Image I/O を使用してアニメーション GIF を作成する方法について述べる。

サンプルプログラム

最初に Java からアニメーション GIF を作成するサンプルプログラムを載せる。説明より実装を見たほうが早いという人はこの Gist のソースを参照すると良いだろう。このプログラムは下のような 0~9 までの数字を切り替える簡単なアニメーション GIF ファイルを作成する。

また単に GIF フォーマットでの出力を行うことを目的としていてアニメーションや詳細なフォーマット指定が不要であれば ImageIO#write() を使用すれば良いだろう。

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 のバイナリを直接扱うのでなければ意識する必要はない。

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

Image I/O において透過色やインターレースなどの出力形式固有の情報は画像に付属するメタデータ (metadata) とみなされる。

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

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

Image I/O がサポートする画像形式でどのようなメタデータが利用できるかは Image I/O の API リファレンスを参照 プレーンテキスト拡張ブロックに text 属性が抜けている。また characterCellWidth, characterCellHeight が 2 バイトのようになっているが実際は 1 バイト。(DTD や Schema で記載されている)。

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

GIF 画像出力

出力先の取得

まず ImageIO クラスを使用して GIF フォーマット用の ImageWriter を参照する (Writer という名前だが java.io パッケージのそれとは無関係)。下記では省略しているが、 ImageOutputStream を取得後は 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();