APNGの構造とRubyでの読み書き

Posted by Morohoshi Yuki on December 08, 2016 · 6 mins read

(注) 本稿執筆時点の2016年12月現在、APNGはFirefoxやSafariなどの一部ブラウザでのみアニメーションが再生されます。

NaCl松江本社の諸星です。絵を描くのが趣味です。
様々な都合があることは理解しつつも、SNSにアップロードしたPNG画像が自動でJPEGに変換されると悲しい気持ちになります。

ところでみなさんはAPNGについてご存知でしょうか。

Animated PNG、すなわちアニメーションするPNG画像のことで、要は動画を扱うことを目的としたファイルフォーマットです。みんな大好きGIFアニメみたいなものですね。

APNGを巡る比較的最近の動向として、2016年6月にAPNGを使ったアニメーションLINEスタンプ作成が一般クリエイターにも開放されるという出来事がありました。 これを受けてか、グラフィックソフトCLIP STUDIO PAINTでも2016年10月末に公開されたVer.1.6.3からAPNG形式での書き出しに対応しています。

このようなかたちで今後じわじわと利用が広がっていくことを期待しつつAPNGの構造について調べてみましたので、以下の構成でまとめたいと思います。

  • APNGとは?
    • GIFとは何が違うの?
  • PNGとの違い
    • PNGの構造
    • APNGの構造
  • RubyでAPNGを読み書きしてみよう
    • ChunkyPNGを拡張してAPNGを読み書き

APNGとは?

APNGは従来のPNGを拡張し、アニメーションする機能を備えたフォーマットです。 拡張子は.pngまたは.apngとなります。 記事先頭にも書きましたが、本稿執筆時点の2016年12月ではFirefoxやSafariなどの一部のブラウザが最新バージョンでサポートしています。

従来のPNGと後方互換があり、APNGに対応していないビューア/ブラウザで表示を試みると通常のPNG画像と同様に静止画として表示できるという特徴があります。 例えば以下のページを表示するとサポートされているブラウザなら全てのデモが動いて見えるのですが、サポートしていないブラウザだと一部の画像(GIF)のみが動いて見える、という状態になります。

Animated PNG demos

GIFとは何が違うの?

わざわざGIF画像と同じようなフォーマットを後発で作る必要があるのか、と思われる方もおられるかもしれません。 そもそもPNGがGIFの代替としてスタートしたんだよ的な歴史の話はさておき、機能的な話に絞ると例えば次のような違いがあります。

項目 GIF APNG
扱える色の数 256色 24bitフルカラー(1677万色)
透過(アルファ) 完全な透明か完全な不透明のみ可能 8bit(0-255)の透過度を指定可能

色数もさることながら、透過度(アルファ値)を扱えることで背景などと重ねて表示する際にアンチエイリアスを効かせることができるなど、GIFと比較して表現の幅を広げることができる利点があることが伺えます。 上に載せたリンク先のデモをご覧になった方は、APNGのアニメーションがGIFと比較してどうかというのは既にご理解いただけたのではないでしょうか。

PNGとの違い

先述の通りAPNGは従来のPNGと後方互換性があるので、「違いがある」という表現よりはアニメーションに必要な「補助的なデータが追加されている」と表現した方が適切なのかもしれません。

いずれにせよその違いを述べるには従来のPNGの構造を知っていただく必要があるので、まずは簡単にPNGの構造について記述します。

PNGの構造

PNGファイルは、それがPNGであることを示す先頭8byteのシグネチャと、チャンクと呼ばれるデータのまとまりで構成されます。 チャンクはいくつかのタイプに分かれていて、その中でも必ず無くてはならない必須チャンクと、無くても構わない補助チャンクとに大別できます。

必須チャンクは以下の3つです。

タイプ サイズ 説明
IHDR 固定長(25byte) (Image Header) 画像の幅、高さ、色深度などのヘッダ情報を含む。
IDAT 可変長 (Image Data) 各ピクセルの色データ(画像の実データ)を含む。複数可。
IEND 固定長(12byte) (Image End) 終端を示す。

IDATチャンクは複数存在することができ、その場合は全てのIDATチャンクのデータ部を結合したものが1枚の画像データになります。 補助チャンクにはテキスト情報や画像の最終更新時間など、無くても表示に問題ないような情報を含んでいます。 ここで抑えておきたいことは、IHDRから得た情報をもとにIDATのデータ部を展開すると画像が得られる、という点です。

下は最もシンプルなPNGの構成イメージを図にしたものです。

単純な構造のPNG

各チャンクの詳細なデータ構造を知りたい方はW3Cの仕様や「png フォーマット」などでググって出てくる情報などをご参照ください。

APNGの構造

APNGにはアニメーションを表現するために上記のPNG構造に加えて3つの補助チャンクが追加されています。 補助チャンクなので非サポートのビューア/ブラウザには単純に無視されます。そのため上で述べたような後方互換があるんですね。

追加されている補助チャンクとは、acTLチャンク、fcTLチャンク、fdATチャンクです。
それぞれについて以下に詳細を記述します。

acTLチャンク

acTLはAnimation Controlの略字です。 必ずIDATチャンクよりも前に位置しています。
チャンクのデータ部の構造は次のようになっています。

byte 名前 説明
0 num_frames (フレーム数) unsigned int アニメーションのフレーム数。0以上でなくてはならない。
4 num_plays (再生回数) unsigned int ループ再生する回数。0を指定すると無限ループする。

fcTLチャンク

fcTLはFrame Controlの略字です。 シーケンス番号やディレイ(どのくらいの長さそのフレームを表示するのか)など、フレームごとのレンダリング情報を持っています。 fcTLチャンクの数がアニメーションのフレーム数に対応するため、acTLチャンクが持つフレーム数の値とfcTLチャンクの数は一致します。

各フレームの実際の画像データは後述のfdATチャンクが持っているため、基本的にfcTLチャンクとfdATチャンクはセットで扱われます。 例外として、IDATチャンクよりも前にfcTLチャンク単体で配置すると、IDATチャンクの内容がアニメーションの最初のフレームとなります。 (本来fcTLとfdATがセットとなるところが、fcTLとIDATでセットになるイメージ。)

byte 名前 説明
0 sequence_number (シーケンス番号) unsigned int アニメーション用のチャンクのシーケンス番号。
4 width (幅) unsigned int フレームの画像の幅。
8 height (高さ) unsigned int フレームの画像の高さ。
12 x_offset (X方向のオフセット) unsigned int フレームの画像をレンダリングするX座標。
16 y_offset (Y方向のオフセット) unsigned int フレームの画像をレンダリングするY座標
20 delay_num (ディレイの分子) unsigned short フレームの遅延分数の分子。
22 delay_den (ディレイの分母) unsigned short フレームの遅延分数の分母。
24 dispose_op (後処理のタイプ) byte フレームレンダリング後に実行されるフレーム領域処理のタイプ。
25 blend_op (レンダリングのタイプ) byte フレーム領域のレンダリングのタイプ。

ヘッダ(IHDR)に幅や高さの情報を持っているのに、各フレームのレンダリング情報としても幅や高さを持っているのが少し不思議な感じがします。 これはfdATチャンクが持っている画像データのサイズが、必ずしもIDATチャンクが持っている画像データのサイズと一致しなくてもいいようにこのような構造になってるようです。 つまり以下の図のように、オフセットを考慮した各フレーム画像が、ヘッダで定義された幅と高さの中に内包されていればOKという感じです。

idat-and-fdat.png

fdATチャンク

fdATはFrame Dataの略字です。 先頭4byteにシーケンス番号(後述)を持つ、という点以外はIDATチャンクと同じ構造を持っています。 1フレームに必ず1つ以上必要で、1フレームに複数のfdATチャンクが存在する場合はIDATと同様に結合したものが1フレーム分の画像データになります。

byte 名前 説明
0 sequence_number (シーケンス番号) unsigned int アニメーション用のチャンクのシーケンス番号。
4 frame_data (画像データ) X bytes (可変長) IDATチャンクのデータ部と同じ構造の画像データ。

チャンクのシーケンス番号について

fcTLチャンクとfdATチャンクはシーケンス番号を持っていますが、これらのチャンクはシーケンス番号を共有しています。 すなわち、同じシーケンス番号を持つfcTLチャンクとfdATチャンクが同居してはなりません。

また、最初のfcTLチャンクのシーケンス番号は0でなくてはいけません。 そして残りのfcTLチャンクとfdATチャンクのシーケンス番号は抜けや重複なく順序通りになっている必要があります。

APNGの構造イメージ図

ここまでを簡単にまとめると、APNGのチャンク構造のイメージは以下のような感じです。

apng-overview.png

(図中のseq_numはsequence_numberを図のスペースの都合で省略したものです。)

RubyでAPNGを読み書きしてみよう

APNGの構造がある程度わかったので、Rubyで読んだり書いたりしてみます。
以降、サンプル画像を含むのでAPNG対応のブラウザで読んでいただくのが良いかもしれません。

今回はChunkyPNGというライブラリを拡張してAPNGを読み書きできるようにしたいと思います。 ChunkyPNGはPure RubyなPNGライブラリで、画素毎に手を加えたりはもちろんのこと、チャンクレベルでもいろいろ操作できちゃうすごいやつです。

ChunkyPNGの本家リポジトリをforkして、わりと雑な拡張ですがAPNGサポート版を以下に作成しました。

https://github.com/hoshi-sano/chunky_png/tree/apng_support

Gemfileに次のように書いてbundle installすればAPNG拡張版ChunkyPNGが使えるようになるかと思います。

gem 'chunky_png', git: 'git://github.com/hoshi-sano/chunky_png.git', branch: 'apng_support'

簡単にやったことを記述しますと、APNGを扱う上で主に以下のクラスを追加しました。

  • ChunkyPNG::Animation => APNG全体を管理するためのクラス。saveメソッドでファイルに書き出せます。
  • ChunkyPNG::Frame => 各フレームを管理するためのクラス。fcTLチャンクとfdATチャンクに対応します。
  • ChunkyPNG::AnimationDatastream => IO(File)とAnimationクラスの橋渡しをするクラス。

試しにCLIP STUDIOで作成したこのAPNGアニメを読んでみます。

サンプルPNG

require 'chunky_png'

animation = ChunkyPNG::Animation.from_file('apng-sample.png')
puts "width: #{animation.width}px"         #=> width: 200px
puts "height: #{animation.height}px"       #=> height: 200px
puts "num_frames: #{animation.num_frames}" #=> num_frames: 8
puts "num_plays: #{animation.num_plays}"   #=> num_plays: 0

読めました。わーい。
続いて、fcTLチャンクに手を入れて再生速度を変えてみたいと思います。

require 'chunky_png'

animation = ChunkyPNG::Animation.from_file('apng-sample.png')
ads = animation.to_datastream
ads.each_chunk do |c|
  next unless c.is_a?(ChunkyPNG::Chunk::FrameControl)
  c.delay_den = c.delay_den / 2
end
ads.save('out_1.png')

out_1.png

ちゃんと再生速度が遅くなりましたね。
最後に、複数の画像から1枚のAPNG画像を生成してみます。

連番画像

require 'chunky_png'

frame_attrs = { delay_num: 1, delay_den: 10 }
animation = nil
(0..7).each do |i|
  name = '%03d' % i
  frame = ChunkyPNG::Frame.from_file("#{name}.png", frame_attrs)
  if i == 0
    animation = ChunkyPNG::Animation.new(frame.width, frame.height, frame)
  else
    animation.frames << frame
  end
end
animation.save('out_2.png')

out_2.png

ちゃんとアニメーションしてます。

APNGの構造の特性上、画像がたくさんなくても同じ画像をスライドさせるだけなら表現できます。

2つの画像

require 'chunky_png'

base_attrs = { delay_num: 1, delay_den: 10, dispose_op: 1 }
first_frame = ChunkyPNG::Frame.from_file("000.png", base_attrs)
animation = ChunkyPNG::Animation.new(first_frame.width, first_frame.height, first_frame)
[[ 0, 60],
 [20, 60],
 [40, 60],
 [60, 60],
 [80, 60]].each do |x_offset, y_offset|
  each_frame_attrs = base_attrs.merge(x_offset: x_offset, y_offset: y_offset)
  frame = ChunkyPNG::Frame.from_file("face.png", each_frame_attrs)
  animation.frames << frame
end
animation.save('out_3.png')

out_2.png

まとめ

APNGの構造を把握することで、RubyでAPNGを読み書きすることができました。
Rubyで作ったアニメーションスタンプがストアに並ぶ日も近い気がするようなしないような!
以上です。お粗末さまでした。

参考リンク