iOSアプリでGIFアニメーションを再生してみたところ、PCのブラウザ上でのループ再生回数と異なるケースがあることがわかりました。PCのブラウザ上で動作するWebアプリケーションと同等のGIFループ再生機能を提供したかったので、なぜループ再生回数が違うのか調査し、その違いを吸収することを試みました。
具体的な事象としては、ブラウザで表示すると1回アニメーションするGIFのループ回数が、kCGImagePropertyGIFLoopCount を使ってGIFファイル中のループ回数を取得すると0が返ってきていました。 UIImageViewのanimationRepeatCountではrepeat count == 0は無限ループを意味し、このまま解釈するとiOS上では無限ループしてしまい、ブラウザと同様に再生できません。
GIFのフォーマットについて調べてみると、GIF内にはNetscape Looping Application Extension (GIF Unofficial Specification)という領域があり、ここにGIFのループ回数が記されていることがわかりました。
更に調べてみると、このNetscape Looping Application ExtensionはApplication Extensionを利用してNetscape社が入れた仕様で、明確なGIFの仕様ではなさそうでした。
明確なGIFの仕様ではないので、GIFプレイヤーの実装で解釈が異なることがありえそうです。GIF内にNetscape Looping Application Extensionが存在する場合の2つの解釈方法を考えてみます:
- A: 記録されている画像フレームたちをとりあえず展開(再生)し、更にloop回数分ループする派
- (ここでいう「画像フレーム」とは、Animation用の各フレームのことです)
- B: loop回数分だけループする派
このAとBの解釈では、GIF上に同じループ回数が記されていても再生回数が1回分ズレることになります。
GIF内にNetscape Looping Application Extensionが存在しない場合の2つの解釈方法は次の通りです:
- A: 記録されている画像フレームたちをとりあえず展開(再生)し、更にloop回数分ループする派
- => loop回数(今回はextensionブロック無しなので0) + 1回ループしてるように見える
- = 1
- B: loop回数分だけループする派
- => loop回数(今回はextensionブロック無しなので0)
- = 0
macos上で動作するメジャーなブラウザでのループ回数を調べてみました(2017-06-07, コヤチ調べ)
GIF内のNetscape Looping Application Extensionブロック有無、画像フレームの有無、Extension内loop回数 | Chrome | Safari | Firefox |
---|---|---|---|
ブロックなし、複数フレームなし | 静止画 | 静止画 | 静止画 |
ブロックなし、複数フレームあり | 1回再生 | 1回再生 | 1回再生 |
ブロックあり、loop回数=0 | 無限ループ | 無限ループ | 無限ループ |
ブロックあり、loop回数=1 | 2回再生 | 1回再生 | 1回再生 |
ブロックあり、loop回数=2 | 3回再生 | 2回再生 | 2回再生 |
現在のブラウザではB派に近い(loop回数分だけループする派 + 0の場合だけ無限ループ)解釈が多いようです。
ここで、Netscape Looping Application Extensionが存在しない場合のiOSアプリ上でのループ回数を確認してみます。 ブラウザで見ると1回再生されるGIFのループ回数をkCGImagePropertyGIFLoopCountで参照すると0ということは、kCGImagePropertyGIFLoopCountでの解釈はB派のように見えます。これは想像ですがNetscape Looping Application Extension内に記されているループ回数を取得する、という用途に使うためのものだからだと思われます。ただ、ブラウザの挙動に似せたい場合は困ります。
そもそも、Netscape Looping Application Extensionが存在しなく、複数画像フレームのみが存在するGIF画像はありえるのでしょうか?結果から言うとありえます。あるWebサービスにアップロードしたループ回数1回のGIFをダウンロードするとそうなっていました。あってもなくてもブラウザの解釈が同じならファイルから削ってしまって、ファイルサイズ圧縮手法の一つとして使われているのではないかと思われます。
Netscape Looping Application Extensionが存在しなく複数画像フレームのみが存在する画像のループ回数を、ブラウザと同様に解釈したいので、以下のSwiftライブラリを作りました。
このライブラリではループ回数以外の要素は真面目に解析していません。実装にあたってはいろんな言語のGIF解析ライブラリを参考にしました。GIFMetadataを使うことでUIImageView等に適切なループ回数を設定することができるようになりました。
GIFMetadataを作るにあたって初めてSwiftでバイナリ解析のコードを書きましたが、他の言語でバイナリ解析するのと遜色ない感じで書けました。このライブラリ実装時にはKaitai Structも気になりましたが、decode対象部分も限定されているので素直な実装としました。
- kCGImagePropertyGIFLoopCount - Image I/O | Apple Developer Documentation
- animationRepeatCount - UIImageView | Apple Developer Documentation
- Netscape Looping Application Extension (GIF Unofficial Specification) - vurdalakov.net
- 592735 - GIF animation loops once more than it should - chromium - Monorail
- koyachi/swift-GIFMetadata: Wrokaround for getting preferred loop count of Animation GIFs without Application Extension Blocks.
- Kaitai Struct
- Goodpatch Advent Calendar 2018 - Qiita
- ちょうど10年前にもバイナリのこと書いてました