米 Unisys 社が取得していた GIF の LZW 圧縮に関する特許が2004年6月20日に切れ、やっと Java SE 6 から GIF への出力がサポートされるようになりました。このページでは Java SE 6 の Image I/O を使用してアニメーション GIF を作成します。
サンプルプログラム
最初に Java でアニメーション GIF を作成するサンプルプログラムを載せておきますので、説明よりソースを見たほうが早いという人はこちらをご覧下さい。
このプログラムは右のような 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 フォーマットは同時に 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
リファレンス
*1を参照してください (DTD や Schema で記載されています)。
メタデータとして設定されたコメントやグラフィック制御の情報は画像ブロックの前に配置されます。従って一番最後のブロック
(終端ブロックの前) にコメントブロックなどを配置できないので注意して下さい。
*1 プレーンテキスト拡張ブロックに text 属性が抜けています。また characterCellWidth,
characterCellHeight が 2 バイトのようになっていますが実際は 1 バイトです。
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();