【ゲーム用】gif画像をフレームに分解して、HTML5のcanvas上で再生してみた


<h2>概要</h2>
一般的にgif画像をcanvas上に貼り付けるとアニメーション機能が停止され、静止gif画像のように取り扱われます。そこで、私はgif画像をフォームから受けとり、それをフレームに分解して、HTML5のCanvas上で再生するWebアプリを作ってみました。。JavaScript上で動くゲームアプリを作るときに、gifアニメ画像をそのままゲーム素材として使えたら便利だと思います。

##技術

バックエンドはJavaサーブレットを使っています。
フロントエンドは、flashの技術をJavaScriptに応用して使えると言われているadobe社のCREATEJSを使用しています。

<h2>使用するJavaのクラスのうち重要なもの</h2>

:one: org.apache.commons.fileupload.FileItem
このクラスはクライアントから受け取る multipart/form-data のPOSTリクエスト内のファイルまたはアイテムを表現したものです。利用にはApacheCommonsのjarファイルをダウンロードする必要があります。

:two: javax.imageio.stream.ImageInputStream
ImageReader で使用されるシーク可能な入力ストリームインタフェースです。InputStream や File などのさまざまな入力ソース、および将来の高速な入出力ソースを、このインタフェースの適切な実装で「ラップ」することで、イメージ入出力 API から使用可能にできます。

:three: javax.imageio.ImageReader
イメージを解析して復号化する抽象スーパークラスです。

:four: java.awt.image.BufferedImage
イメージデータのアクセス可能なバッファーを備えたImageオブジェクトを記述します。

:five: javax.imageio.metadata.IIOMetadata
イメージとストリームに関連付けられたメタデータ (イメージ以外のデータ) を表すオブジェクトにより拡張される abstract クラスです。

:six: javax.imageio.metadata.IIOMetadataNode
メタデータツリーのノードを表すクラスです。

:seven: org.w3c.dom.NodeList
順序付けられたノードのコレクションの抽象を提供します

:eight: org.w3c.dom.NamedNodeMap
名前を指定してアクセスできるノードのコレクションを表すインターフェースです。


　<h2>手順</h2>

:one: gifファイルには、メタ情報が存在します。ファイル名、画像の高さ、幅、フレーム数、アニメ待機時間、画像データなど、画像についての情報がメタデータとして画像ファイルには付与されています。HTMLのPOSTリクエストから受け取ったgifファイルからメタデータを取得します。

```java
//fileItem:FileItemクラスのオブジェクト
ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(readAll(fileItem)));
ImageReader reader = ImageIO.getImageReadersByFormatName("gif").next();
reader.setInput(iis);
// i番目の画像データを取得する
BufferedImage bi = reader.read(i);
// i番目のフレームのメタデータを取得する
IIOMetadata meta = reader.getImageMetadata(i);
```

:two: メタデータツリーのノードの中から、画像記述子を探し出す。

```java
Node tree = metadata.getAsTree("javax_imageio_gif_image_1.0");
NodeList children = tree.getChildNodes();
for (int j = 0; j < children.getLength(); j++) {
　　　　　Node nodeItem = children.item(j);
　　　　　//もし画像記述子ならば
　　　　　if (nodeItem.getNodeName().equals("ImageDescriptor")) {
　　　　　//nodeItemを使用した次の処理
　　　　　｝
}
```

:three: 画像の位置とサイズをノードから取得して、後で、いつでも使えるようにMapに格納しておく。

```java
//最終的にデータを格納する用のMap
Map<String, Integer> imageAttrMap = new HashMap<>();
//左位置、上位置、幅、高さを表す文字列を配列している
String[] imageAttArray = new String[] { "imageLeftPosition", "imageTopPosition", "imageWidth", "imageHeight" };
for (int k = 0; k < imageAttArray.length; k++) {
　　　　　NamedNodeMap attr = nodeItem.getAttributes();
　　　　　Node attnode = attr.getNamedItem(imageatt[k]);
　　　　　//Mapに追加
　　　　　imageAttrMap.put(imageatt[k], Integer.valueOf(attnode.getNodeValue()));
}
```

:four: 今まで取得した情報に基づいてイメージオブジェクトを書き出す。


```java
BufferedImage master = new BufferedImage(imageAttr.get("imageWidth"),imageAttr.get("imageHeight"),BufferedImage.TYPE_INT_ARGB);
master.getGraphics().drawImage(bi,imageAttr.get("imageLeftPosition"),imageAttr.get("imageTopPosition"), null);
```

:five: イメージオブジェクトをファイルに書き出してみる。

```java
ImageIO.write(master, "gif", new File("hoge.gif"));
```


##ソースコード

https://github.com/fruitwater/gif-splitter


##サンプルアプリ

サンプルアプリ
https://rocky-sands-26639.herokuapp.com/top

サンプルアプリを実行する際には、以下のようなサイトからgifアニメ画像をダウンロードするなどして準備してください。
http://www.civillink.net/sozai/move.html
https://studio.beatnix.co.jp/kids-it/kids-programming/scratch/free-gif-animation-site/

##参考URL
以下のスタックオーバーフローの質問を参考にしています。
https://stackoverflow.com/questions/20077913/read-delay-between-frames-in-animated-gif