PNG ファイルはなぜ 89 50 4E 47 で始まるのか:シグネチャとチャンク構造
PNG ファイルが必ず 89 50 4E 47 0D 0A 1A 0A で始まる理由、長さ・タイプ・データ・CRC からなるチャンク構造、大文字・小文字で性質を表すチャンク名の規則と必須チャンクの順序を、W3C PNG 第3版と RFC 2083 をもとに整理します。
PNG ファイルを16進数で開くと、どの画像でも先頭の8バイトは同じです。
$ xxd red1x1.png | head -2
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR
00000010: 0000 0001 0000 0001 0802 0000 0090 7753 ..............wS
先頭の 89 50 4E 47 0D 0A 1A 0A が PNG シグネチャで、その後ろにはチャンクと呼ばれる同じ形のブロックが並びます。
8バイトのシグネチャ
現行の W3C PNG 第3版は、この8バイトが PNG をほかのデータと区別し、一部の転送エラーを早く見つけるためのものだと説明しています。1バイトごとの理由は、PNG 1.0 を収めた RFC 2083 にあります。
| バイト | RFC 2083 が挙げる理由 |
|---|---|
89 |
ASCII ではない値なので、テキストファイルが PNG と誤認されにくい。第7ビットを落とす転送も検出できる |
50 4E 47 |
ASCII で PNG |
0D 0A |
CR-LF。改行コードを書き換える転送を検出する |
1A |
Control-Z。MS-DOS でファイルの表示を止める |
0A |
LF。CR-LF 変換の逆方向の問題を検出する |
テキストモードの転送で CR や LF のバイトが追加・変換されたファイルは、画像を展開する前にこの8バイトで食い違います。シグネチャにバージョン番号はありません。RFC 2083 によれば、これは意図的なもので、形式の拡張はチャンクを使って行います。
チャンクの構造
| フィールド | サイズ | 内容 |
|---|---|---|
| Length | 4バイト | データ部分のバイト数だけを数える。最大 2³¹−1 |
| Chunk Type | 4バイト | IHDR や IDAT のような英字4文字 |
| Chunk Data | Length バイト | 0バイトでもよい |
| CRC | 4バイト | タイプとデータから計算。Length は含まない |
複数バイトの整数は、上位バイトが先に来るネットワークバイトオーダーです。上のダンプの 00 00 00 0D は長さ13、49 48 44 52 は IHDR です。続く13バイトには幅・高さ、ビット深度、カラータイプなどの基本情報が入っています。
長さの上限が 2³¹−1 なのは、符号なし4バイト整数を扱いにくい言語に配慮した制限です。
チャンク名の大文字と小文字
チャンクタイプの4文字は、各バイトのビット5、つまり大文字か小文字かで性質を表します。
- 1文字目:大文字なら必須(critical)、小文字なら補助(ancillary)
- 2文字目:大文字なら公開(public、W3C が定義するために予約)、小文字なら非公開(private)
- 3文字目:予約。現行の仕様では必ず大文字
- 4文字目:小文字ならコピー可(safe-to-copy)
たとえば pHYs は、補助・公開・コピー可のチャンクです。現行仕様では、画像を取り出すデコーダーにとって未知の必須チャンクは致命的な状況です。未知の補助チャンクはエラーではなく、無視できます。
必須チャンクの順序
すべての PNG に共通する必須の骨格は、シグネチャ、IHDR、1つ以上の IDAT、IEND です。IHDR は先頭、IEND は末尾に1つずつだけ置きます。
ただし、インデックスカラー、つまりカラータイプ3の画像には PLTE も必要です。PLTE は最初の IDAT より前に置きます。IDAT が複数ある場合は間にほかのチャンクを挟まずに連続させます。そのデータをつなげたものが1つの zlib 圧縮データになります。
CRC を確かめる
import struct, sys, zlib
data = open(sys.argv[1], "rb").read()
assert data[:8] == b"\x89PNG\r\n\x1a\n", "not a PNG"
pos = 8
while pos < len(data):
length, ctype = struct.unpack(">I4s", data[pos:pos + 8])
body = data[pos + 8:pos + 8 + length]
crc, = struct.unpack(">I", data[pos + 8 + length:pos + 12 + length])
ok = zlib.crc32(ctype + body) == crc
print(ctype.decode("ascii"), length, "CRC ok" if ok else "CRC mismatch")
pos += 12 + length
1×1 の赤い点の PNG で実行した結果です。環境は Python 3.14.7、macOS 15.6.1 です。
IHDR 13 CRC ok
IDAT 12 CRC ok
IEND 0 CRC ok
PNG の CRC は gzip と同じ多項式を使います。この実行では、3つのチャンクすべてで zlib.crc32 の値がファイルに記録された CRC と一致しました。コピーしたファイルで IDAT データの先頭バイト、オフセット41を書き換えると、そのチャンクだけが IDAT 12 CRC mismatch になりました。仕様は CRC の不一致を壊れたデータストリームの兆候とみなし、致命的なエラーとして扱ってよいとしています。
このコードは、完全で正しい形式の入力を前提にした確認用です。長さの範囲、チャンクの順序、IEND の有無、IEND の後に残るデータは検査しません。途中で切れたファイルや形式の誤ったファイルでは例外が発生することがあります。