1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-02-27

はじめに

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

環境

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

ソースコード

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ファイルが1つの場合、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に保持します。

コンパイル、実行

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
i2.png
i2.png
i3.png
i3.png
i4.png
i4.png
i5.png
i5.png
ani.png
※QiitaにアニメーションPNGをアップロードできないので、GitHub Pageにアップロード

参考情報

APNGの構造メモ
Animated PNG graphics
PNG ファイルフォーマット

更新履歴

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?