アニメーションPNG(APNG)の作り方

# はじめに
PNGファイルは、静止画像形式だけでなく、動画アニメーション形式にも対応しています。
この記事では、複数の静止画像PNGファイルを元にAPNG(アニメーションPNG、Animated PNG)を作成する方法を紹介します。
PNGファイルの構造については、記事最後にある参考情報に詳しく書かれています。

# 環境
* OS : Windows 10 64ビット版 (バージョン 21H1)
* D言語コンパイラ : DMD v2.099.0

# ソースコード
```d:apng1.d
import std.format;
import std.stdio;

ubyte[] loadPngFile(string ifile)
{
	auto file = File(ifile, "rb");
	scope(exit) file.close();
	auto size = file.size();
	ubyte[] buf = file.rawRead(new ubyte[size]);
	return ( buf );
}

void savePngFile(string ifile, string ofile)
{
	ubyte[] buf = loadPngFile(ifile);
	auto file = File(ofile, "wb");
	scope(exit) file.close();
	file.rawWrite(buf);
}

ubyte[] calcCRC(ubyte[] buf)
{
	import std.algorithm.mutation;
	import std.conv;
	import std.digest.crc;
	
	CRC32 crc;
	crc.put(buf);
	ubyte[4] hash = crc.finish();
	return ( hash.to!(ubyte[]).reverse.dup );
}

void setDword(ubyte* p, uint u)
{
	*(p++) = cast(ubyte)(u >> 24 & 0xFF);
	*(p++) = cast(ubyte)(u >> 16 & 0xFF);
	*(p++) = cast(ubyte)(u >>  8 & 0xFF);
	*(p  ) = cast(ubyte)(u       & 0xFF);
}

ubyte[] getACTL(uint num, uint loop)
{
	ubyte[] buf = [
		0, 0, 0, 0x08,
		cast(ubyte)'a',
		cast(ubyte)'c',
		cast(ubyte)'T',
		cast(ubyte)'L',
		0, 0, 0, 0, 0, 0, 0, 0
	];
	setDword(&buf[ 8], num);
	setDword(&buf[12], loop);
	return ( buf ~ calcCRC(buf[4 .. $]) );
}

ubyte[] getFCTL(uint seq, uint w, uint h, ushort delayTime)
{
	ubyte[] buf = [
		0, 0, 0, 0x1A,
		cast(ubyte)'f',
		cast(ubyte)'c',
		cast(ubyte)'T',
		cast(ubyte)'L',
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0
	];
	setDword(&buf[ 8], seq);
	setDword(&buf[12], w);
	setDword(&buf[16], h);
	buf[28] = cast(ubyte)(delayTime >> 8 & 0xFF);
	buf[29] = cast(ubyte)(delayTime      & 0xFF);
	return ( buf ~ calcCRC(buf[4 .. $]) );
}

ubyte[] getFDAT(ubyte[] buf, uint seq)
{
	int size = (buf[0]<<24) + (buf[1]<<16) + (buf[2]<<8) + buf[3] + 4;
	buf = new ubyte[4] ~ buf[0 .. $-4];
	setDword(&buf[ 0], size);
	buf[ 4] = cast(ubyte)'f';
	buf[ 5] = cast(ubyte)'d';
	buf[ 6] = cast(ubyte)'A';
	buf[ 7] = cast(ubyte)'T';
	setDword(&buf[ 8], seq);
	return ( buf ~ calcCRC(buf[4 .. $]) );
}

void saveAPngFile(string[] ifiles, string ofile, uint loopCount, ushort delayTime)
{
	auto file = File(ofile, "wb");
	scope(exit) file.close();
	uint seq, width, height;
	ubyte[] iend;
	foreach ( i, ifile; ifiles ){
		ubyte[] buf = loadPngFile(ifile);
		ubyte[] chunk = buf[0 .. 8];
		if ( i == 0 ){
			file.rawWrite(chunk);
		}
		buf = buf[8 .. $];
		while ( buf.length > 0 ){
			int size = (buf[0]<<24) + (buf[1]<<16) + (buf[2]<<8) + buf[3] + 12;
			chunk = buf[0 .. size];
			buf = buf[size .. $];
			string tag = format("%c%c%c%c",
				cast(char)chunk[4], cast(char)chunk[5], cast(char)chunk[6], cast(char)chunk[7]);
			if ( tag == "IHDR" ){
				if ( i == 0 ){
					width  = (chunk[ 8]<<24) + (chunk[ 9]<<16) + (chunk[10]<<8) + chunk[11];
					height = (chunk[12]<<24) + (chunk[13]<<16) + (chunk[14]<<8) + chunk[15];
					file.rawWrite(chunk);
					file.rawWrite(getACTL(cast(uint)ifiles.length, loopCount));
				}
				file.rawWrite(getFCTL(seq, width, height, delayTime));
				seq++;
				continue;
			} else if ( tag == "IDAT" ){
				if ( i > 0 ){
					chunk = getFDAT(chunk, seq);
					seq++;
				}
			} else if ( tag == "IEND" ){
				if ( i == 0 ){
					iend = chunk;
				}
				continue;
			}
			file.rawWrite(chunk);
		}
	}
	file.rawWrite(iend);
}

void main(string[] args)
{
	if ( args.length == 3 ){
		savePngFile(args[1], args[2]);
	} else {
		uint   loopCount = 0;	// 0:infinite
		ushort delayTime = 50;	// delayTime * 0.01s
		saveAPngFile(args[1 .. $-1], args[$-1], loopCount, delayTime);
	}
}
```

# ソースコード補足
## loadPngFile
入力PNGファイル`ifile`をメモリに読み込みます。

## savePngFile
入力PNGファイルが１つの場合、APNGを作成せずそのまま出力PNGファイル`ofile`を作成します。

## getACTL
`acTL`チャンクを生成します。
APNGに必要な情報である全体のフレーム数とループ回数をセットします。

## getFCTL
`fcTL`チャンクを生成します。
APNGに必要な情報であるフレームの幅、高さ、フレーム間の表示間隔をセットします。

## getFDAT
`fdAT`チャンクを生成します。
入力PNGファイルの`IDAT`チャンクにシーケンス番号を加えて再構成します。
CRC32の再計算が必要になりますが、D言語の標準ライブラリを使えば簡単です。

## saveAPngFile
APNG作成の本処理です。

* シグネチャ、`IHDR`チャンクや`IEND`チャンクは、1つ目の入力PNGファイルのものを使います。その他の入力PNGファイルにあるシグネチャ、`IHDR`チャンクや`IEND`チャンクは除きます。
* フレームの幅、高さの情報は`IHDR`チャンクに保持しているため、すべての入力PNGファイルはフレームの幅、高さが同じであることが前提です。
* `acTL`チャンク、`fcTL`チャンクを新たに作成します。
* 各入力PNGファイル`IDAT`チャンクを元に`fdAT`チャンクを生成します。
* 各入力PNGファイルのその他のチャンクはそのままAPNGに保持します。

# コンパイル、実行
```text:
D:\Dev\> dmd -m64 apng1.d

D:\Dev\> apng1 i1.png i2.png i3.png i4.png i5.png ani.png
```

# 実行結果
i1.png
![i1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675901/eacdc553-cad6-dd18-a6c4-6a8b32a8b090.png)
i2.png
![i2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675901/7016a8b3-6916-5486-4b8c-69651268cc2e.png)
i3.png
![i3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675901/e9f13a93-c43d-2850-859b-124bfd1c6ddf.png)
i4.png
![i4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675901/7c471ade-4a45-8ded-8199-eae73d40b888.png)
i5.png
![i5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675901/fce33620-a7af-83e3-41e6-9c7224aef256.png)
ani.png
※QiitaにアニメーションPNGをアップロードできないので、[GitHub Page](https://devmynote.github.io/ImageSample/image/ani.png)にアップロード

# 参考情報
[APNGの構造メモ](https://gist.github.com/cia-rana/71275c7db98c2887e8ab0fad365add4a)
[Animated PNG graphics](https://developer.mozilla.org/ja/docs/Animated_PNG_graphics)
[PNG ファイルフォーマット](https://www.setsuki.com/hsp/ext/png.htm)

# 更新履歴

* 2021.2.27 初回投稿
* 2021.3.6 `apng1.d` 修正
  * APNGがchromeで表示できるように対応（修正前はfirefox、safariのみに対応） → ハッシュ値の修正（`Chunk Type`、`Chunk Data`が算出範囲）
  * `IDAT`チャンクが分割されているPNGファイルに対応 → シーケンス番号の割り当て修正
* 2022.3.13 `apng1.d` 修正
  * `calcCRC`の不具合修正
  * `setDword`を追加