PNG イメージを自力でパースしてみる ~5/6 PNGフォーマット編~
ここまでのあらすじ
PNG イメージを自力でパースしてみる ~1/6 予備知識編~
PNG イメージを自力でパースしてみる ~2/6 Deflateの基本と固定ハフマン編~
PNG イメージを自力でパースしてみる ~3/6 カスタムハフマン編~
PNG イメージを自力でパースしてみる ~4/6 非圧縮とzlib編~
zlibを解凍するプログラムが一通り完成した。いよいよPNGファイルのパースに挑戦してみる。
PNGフォーマット
PNGファイルの中身は、以下のような構成になっている
- シグネチャ
- チャンク (IHDR)
- チャンク
⋮ - チャンク (IEND)
チャンクは 1ファイルにつき、必要な数だけ作られる。
このチャンクのかたまりの中で、IHDRのチャンク (後述) は 必ず先頭に出現し、
IENDのチャンク (後述) は 必ず末尾に出現する。
シグネチャ
ファイルの先頭に必ず出現し、このファイルがPNGファイルであることを示す 8バイト長のデータ。すべてのPNGファイルは共通して、以下の値の並びを保有する。
10進 (16進):
137 (89) | 80 (50) | 78 (4e) | 71 (47) | 13 (0d) | 10 (0a) | 26 (1a) | 10 (0a) |
PNGファイルをロードするプログラムは、まず初めにファイルの先頭 8バイトを読み出し、このシグネチャの並びと一致するかどうかで、対象が確かに PNGファイルであるかどうかをチェックする必要がある
ちなみに この並びの中に現れる各値は、それぞれ以下のような役割を持つ
10進 | 16進 | ASCII文字 | 役割 |
---|---|---|---|
137 | 89 | \221 | ASCII文字として表現できない値。ファイルに必ず非ASCII文字を含むことで、テキストファイルとして間違えてロードされてしまう可能性を減らす。 |
80 | 50 | P | 自身のフォーマット名を表す |
78 | 4e | N | |
71 | 47 | G | |
13 | 0d | \r |
"改行文字"として解釈されてしまい、システムによって勝手に「\r」や「\n」に置き換えられたりしないかを検出するために利用される |
10 | 0a | \n | |
26 | 1a | \032 | Control-z文字。ファイルの中身をテキストとして読み出し表示させようとしたDOSコマンドを停止させる |
10 | 0a | \n | "改行文字"として解釈されてしまい、システムによって勝手に「\r」や「\r\n」に置き換えられたりしないかを検出するために利用される |
チャンク
PNGファイル中に現れる各チャンクは、共通して以下のような構造になっている
Length | 4バイト |
Chunk Type | 4バイト |
Chunk Data | Length バイト |
CRC | 4バイト |
1. Length(データのサイズ)
このチャンクの Chunk Data 部のサイズ (バイト数) を表す値を ここに4バイト長で格納する。最小で「0」 最大で「」までの値を収められる
2. Chunk Type(チャンクの種類)
このチャンクの種類を表す、アルファベット4文字のデータがここに格納される
3. Chunk Data(データ)
Length が「0」ではない場合、このチャンクの種類に関連したデータが、この場所に格納される。
4. CRC(crc32チェックサム)
CRC32チェックサムアルゴリズムを使って算出された値を、ここに4バイト長で格納する。ここの詳しい解説は、次回の記事で行うことにする。
なお、PNGファイル中に現れる これら多バイト長の数値データはすべて ビッグエンディアン の並びになっている。 これらの数値データを読み出す際には、その読み出したデータのバイト並びに気を付けること。
チャンクの種類
PNGで利用できるチャンクの種類 (Chunk Type) を以下にざっと書き並べてみる
Chunk Type | 説明 | 必須 |
---|---|---|
IHDR | ヘッダ情報 | 〇 |
PLTE | パレット | 〇 |
IDAT | イメージデータ | 〇 |
IEND | 終端 | 〇 |
tRNS | 透過 | |
cHRM | 色度と白色点 | |
gAMA | イメージガンマ | |
iCCP | 埋め込みICCプロファイル | |
sBIT | 主要ビット | |
sRGB | 標準的なRGB色空間 | |
tEXt | テキストデータ | |
zTXt | 圧縮テキストデータ | |
iTXt | 国際テキストデータ | |
bKGD | 背景色 | |
hIST | 画像のヒストグラム | |
pHYs | ピクセル寸法 | |
sPLT | 推奨パレット | |
tIME | 最終更新時刻 |
これすべてに対応しようとすると、さすがに途中で心が折れそう..
今回は必須チャンクに分類されるもののうち、
- IHDR(ヘッダ情報)
- IDAT(イメージデータ)
- IEND(終端)
の3つのみに対応することにしてみる。
IHDR (ヘッダ情報)
Length | 4バイト |
Chunk Type : "IHDR" | 4バイト |
Chunk Data | 計 13バイト |
・Width | 4バイト |
・Height | 4バイト |
・Bit Depth | 1バイト |
・Color Type | 1バイト |
・Compression Method | 1バイト |
・Filter Method | 1バイト |
・Interlace method | 1バイト |
CRC | 4バイト |
シグネチャを読み出した後、チャンクのかたまりの先頭に必ず現れるチャンク。
データ部は、以下の要素で構成される
1. Width (イメージの横幅)
2. Height(イメージの縦幅)
画像の縦横のピクセル数の値が それぞれ4バイト長で 収められる。
3. Bit Depth(ビット深度)
ピクセルの色要素「R」「 G」「 B」のそれぞれ、またはパレット番号 (今回は取り扱わない) が何ビット長で記録されているかを表す値が、ここに 1バイト長で収められる。
なお、後の Color Type の値によって、この場所に設定できる値が異なってくる
4. Color Type(カラータイプ)
PNGイメージのカラータイプを表す値が、ここに1バイト長で収められる。
ここに設定される値は、以下のビットフラグの組み合わせによって形成される
- 1(001): パレット使用 (0なら不使用)
- 2(010): カラー使用 (0ならグレースケール)
- 4(100): αチャンネル使用 (0なら不使用)
ただし、このうち実際に使用することのできるフラグの組み合わせは、いくつかに限られている。使用が可能な Color Type と 対応する使用が可能な Bit Depth との一覧を以下に示す
Color Type | Bit Depth | 説明 |
---|---|---|
0(000) | 1, 2, 4, 8, 16 | グレースケール |
2(010) | 8, 16 | RGBカラー |
3(011) | 1, 2, 4, 8 | インデックスカラー(要PLTEチャンク) |
4(100) | 8, 16 | グレースケール + αチャンネル |
6(110) | 8, 16 | RGBカラー + αチャンネル |
5. Compression Method(圧縮方法)
画像データの圧縮のために、どのようなアルゴリズムを利用したかを表す値を、ここに 1バイト長で収める。「0」が設定されている場合、自身の画像データが、Deflate によって圧縮されたことを表す。
なお、 0以外の値は、現状扱われることがない。とりあえず今は「0を読み出せれば正しい」と覚えていれば問題ないはず
6. Filter Method(フィルタ方法)
圧縮前の画像データに対して、どのような事前処理を行ったかを表す値を、ここに 1バイト長で収める。「0」が設定されている場合、画像データに対して、圧縮前にフィルタリング処理 (後述) が施されることを表す。
なお、0以外の値は、現状扱われることがない。こちらもとりあえずは「0を読み出せれば正しい」と覚えておけば問題ないはず
7. Interlace Method(インタレース)
画像データのピクセル要素の出現規則を表す値を、ここに 1バイト長で収める。
「0」ならインタレースなし、 「1」ならインタレースあり となる。
画像データにおける インタレース とは、画像データ中のピクセル要素を そのままの順で出現させるのではなく、飛び飛びで出現させるようにする手法のこと。
うち、インタレースありの PNGイメージは、以下の図に示す通りに処理される。
これによる利点は、画像データを読み始めた早い段階で、おおざっぱな画像の全体像がわかるということ。初めにモザイク調で表示させ、読み込みが進み次第、徐々に鮮明になるように画像を表示させることができるようになる。
なお、今回は残念ながら 実装が面倒だったので インタレースに対応していない。
気が向いたらそのうち..
IDAT (イメージデータ)
Length | 4バイト |
Chunk Type : "IDAT" | 4バイト |
Chunk Data (画像データ) | Length バイト |
CRC | 4バイト |
実際に画像を構成するピクセル要素 または パレット番号 の連なりからなるデータが、このチャンクの中に収められる。
このとき、収められる画像データには、フィルタリング処理 (後述) と zlibによる圧縮が施される。zlibについては、前回の記事で紹介済み。
なお、このIDATチャンクは、ファイル中にいくつでも出現してかまわないことになっている。その場合、それらのIDATチャンクは必ず1か所に連続して出現する。
IEND (終端)
Length | 4バイト |
Chunk Type : "IEND" | 4バイト |
CRC | 4バイト |
チャンクのかたまりの末尾に、必ず現れるチャンク。
自身を読み出した地点がファイルが終わりであることを示す。
なお、IENDチャンクはデータ部を持たない。
Lengthには常に0に設定される。
PNGイメージのパース
対応できるチャンクを IHDR, IDAT, IEND に限った場合、これから以下に示す手順で、PNGイメージから、画像を構成するピクセルデータを引っ張りだすことができる
- シグネチャ一致チェック
- チャンク読み出し
- IDATチャンクの zlib圧縮データを解凍
- 解凍したデータのフィルタリングを解く
1. シグネチャ一致チェック
ファイル先頭の 8バイト を読み出し、前述したシグネチャの並びと一致するかどうかをチェックする。
一致しなかった場合、そのファイルが PNGイメージではない、もしくは、ファイルを正しく読み出すことができないため、エラーとみなして、テキトウに処理を終了する。
2. チャンク読み出し
以下を IEND チャンクが出現するまで繰り返す
- チャンクのLength (4バイト) を読み出す
- Chunk Typeの4文字 (4バイト) を読み出す
- Lengthが 0 でなければ、そのバイト数分だけ Chunk Dataを読み出す
- CRC (4バイト) を読み出す
くどいようではあるけれど、今回は IHDR, IDAT, IEND のチャンクのみに対応する。それ以外のチャンクに遭遇しても、中身を考慮せずそのまま読み飛ばすことにする。
2.1. IHDRチャンク読み出し
シグネチャ並びの直後に出現。この場所に記録されている情報は、後に画像データを復元したり、ピクセル要素を抽出したりするために必要になる
なお、Color Typeが 「3」である場合、ファイル中に PLTEチャンクが出現し、画像データはパレット番号によって構成される。
対応チャンク 3つ縛りの制限下では、このフォーマットに対応することができない。今回に限っては、これをエラーとみなしてテキトウに処理を終了することにする。
実装はたぶん またそのうち...
2.2. IDATチャンクの読み出し
前述した通り、圧縮された実画像データがここに格納されている。
なお、このチャンクが複数出現する場合は、チャンクが出現した順番にChunk Data部のデータを連結する
3. IDATチャンクのzlib圧縮データを解凍
IDATチャンクから得られた Chunk Data 部
またはそれら複数を結合したデータに対して、zlibによる解凍を行う。
フィルタリング処理
PNGファイルに収められる画像データは、zlibによって圧縮される前に、その圧縮効率を上げる目的で フィルタリング という事前処理が施される
PNGイメージをパースする際、zlib解凍を行った後 それを本来の画像データに戻すために、そのデータのフィルタリングを解く必要がある
現在、フィルタリングには、以下の 5種類 のアルゴリズムが利用されている
番号 | フィルタ名 | 説明 |
---|---|---|
0 | None | フィルタなし |
1 | Sub | 隣接する左ピクセル色との差分 |
2 | Up | 隣接する上ピクセル色との差分 |
3 | Average | 左と上のピクセルの平均色との差分 |
4 | Paeth | 左、上、左上のピクセルのうち次回出現しそうな色との差分 |
これらの フィルタリングアルゴリズムは、画像の各水平ラインごとに行われる
フィルタタイプ 0:None
該当する画像の水平ラインの先頭に 「0」を表す数値を 8ビット長で付加する
先頭に 8ビット 付加する以外には、水平ラインに対して特になにも行わない。
ちなみに、ビット深度が 8 に満たないとき (BitDepth=1,2,4)
各水平ラインの末端がバイト境界になっていなかった場合は、次に出現するバイト境界の地点までの間にパディングが詰められるみたい
次の水平ラインの先頭ビットは必ず、その直近のバイト境界の位置から始まる
フィルタタイプ 1:Sub
該当する画像の水平ラインの先頭に「1」を表す数値を 8ビット長で付加する 。
PNGイメージ中に収められるこの水平ライン上の各ピクセルの値は、左隣のピクセルの値との差分値になる。
なお、差の結果は 非負の 0~255(00~FF)の範囲に収まるよう丸められる。
※例えば差の結果が負数の場合 [-1, -2, -3] は [255, 254, 253] のような通りになる。
また先頭のピクセルについては、左隣に参照できるピクセルがないため、左隣のピクセル値 = 0 として扱う
ここまでを見たとおり、フィルタリング処理自身は、データを圧縮するような作用を持っていない。しかも各水平ラインの先頭に、フィルタを識別するための 8ビットを付与するため、このままでは むしろデータサイズを増やしてしまうだけのように見える。
ただしこのフィルタリング処理には、その後に続くDeflate の、圧縮効率を底上げするという機能がある。
実際には、Noneを除く 4種類 のフィルタリングを使って、PNGイメージ中の値に偏りや同じ値並びを出現させることを行う。
このようにデータを変換することで、Deflateによるハフマン符号化、およびLZ77符号化がうまく働く機会が増えるため、データをより小さく圧縮できるようになる。
うち「フィルタタイプ 1:Sub」は、横方向にピクセル値の変化が小さい 水平ラインに対して効果を発揮する
ここで、フィルタリング処理は、必ず 8ビット単位 で行われるということに注意。
このルールは他のフィルタタイプにも共通する
ビット深度が16であるPNGイメージにおいては、上位8ビット、下位8ビットの2つに分割し、別々にフィルタリング処理を施す
ビット深度が8未満のPNGイメージにNone以外のフィルタタイプを適用する場合は、事前に8ビットに伸長されるらしい...? サンプルを見つけられなかったため、ちょっと確かではないのだけど..
なお、Subフィルタリングされたピクセルは、
ピクセル値 + 左隣のピクセル値 の 256による剰余
によって元の値に復元できる
フィルタタイプ 2:Up
該当する画像の水平ラインの先頭に「2」を表す数値を 8ビット長で付加する 。
PNGイメージ中に収められるこの水平ライン上の各ピクセルの値は、真上のピクセルの値との差分値になる。
差の結果は 非負の 0~255(00~FF)の範囲に収まるよう丸められる。
このルールについても、差を扱うすべてのフィルタタイプに共通する
「フィルタタイプ 2:Up」は縦方向にピクセルの値の変化が小さい水平ラインに対して効果を発揮する
なお、Upフィルタリングされたピクセルは、
ピクセル値 + 真上のピクセル値 の 256による剰余
によって元の値に復元できる
フィルタタイプ 3:Average
該当する画像の水平ラインの先頭に「3」を表す数値を 8ビット長で付加する 。
PNGイメージ中に収められるこの水平ライン上の各ピクセルの値は、左隣のピクセル値と、真上のピクセル値との平均をとった値との差分値になる。
filtered value = current - ((left + up) ÷ 2)
差の結果は 非負の 0~255(00~FF)の範囲に収まるよう丸められる。
また先頭のピクセルについては、左隣に参照できるピクセルがないため、
左隣のピクセル値を 0 として扱い、平均を求める
平均の計算には整数徐算を利用する。
徐算の結果は、値の小さいほうの整数に丸められる(小数点以下切り捨て)
「フィルタタイプ 3:Average」は似たピクセル値が密集しているあたりの水平ラインに対して効果を発揮する
なお、Averageフィルタリングされたピクセルは、
ピクセル値 + 左隣と真上のピクセル値の平均 の 256による剰余
によって元の値に復元できる
フィルタタイプ 4:Paeth
該当する画像の水平ラインの先頭に「4」を表す数値を 8ビット長で付加する 。
PNGイメージ中に収められるこの水平ライン上の各ピクセルの値は、「Paeth」というアルゴリズムから得られる結果の値との差分値になる。
filtered value = current - (left, up, upleft)
差の結果は 非負の 0~255(00~FF)の範囲に収まるよう丸められる
Paethアルゴリズムは、左、上、左上の 3つの隣接するピクセル値から、「この位置に来るであろうピクセル値が、上記 3つのピクセル値のうち、どれと一番近くなりそうか」を予測するために利用される。Alan W. Paethさんが考案した
実際には以下のように求める
int PaethPredictor(int a, int b, int c) { // ┏━━┳━━┓ // ┃ c ┃ b ┃ // ┣━━╋━━┫ // ┃ a ┃ ?? ┃ // ┗━━┻━━┛ int p = a + b - c; // pa = |b - c| 横向きの値の変わり具合 // pb = |a - c| 縦向きの値の変わり具合 // pc = |b-c + a-c| ↑ふたつの合計 int pa = abs(p - a); int pb = abs(p - b); int pc = abs(p - c); // 横向きのほうがなだらかな値の変化 → 左 if (pa <= pb && pa <= pc) return a; // 縦向きのほうがなだらかな値の変化 → 上 if (pb <= pc) return b; // 縦横それぞれ正反対に値が変化するため中間色を選択 → 左上 return c; }
なお、先頭のピクセルについては、左隣、左上の双方ともに参照できるピクセルがないため、それぞれピクセルの値を 0 として扱い 計算を行う
「フィルタタイプ 4:Paeth」はSub, Up, Averageのフィルタが有効ではない、ピクセル値がなだらかに並ぶ水平ラインに対して効果を発揮する
Paethフィルタリングされたピクセルは、
ピクセル値 + Paethアルゴリズムの結果の値 の 256による剰余
によって元の値に復元できる
サンプル
ここまでのサンプルプログラム
VC2015でのみ動作確認済み。
参考にしたサイトさん
Portable Network Graphics (PNG) Specification (Second Edition)
PNG イメージを自力でパースしてみる ~4/6 非圧縮とzlib編~
ここまでのあらすじ
PNG イメージを自力でパースしてみる ~1/6 予備知識編~
PNG イメージを自力でパースしてみる ~2/6 Deflateの基本と固定ハフマン編~
PNG イメージを自力でパースしてみる ~3/6 カスタムハフマン編~
前回、前々回でDeflateの「固定ハフマン」と「カスタムハフマン」について触れた。ここでは残るブロックタイプの「非圧縮」と、Deflateを利用した圧縮フォーマットの「zlib」についてを紹介。
予備知識
これからの説明を読む前に 予備知識が必要かも
バイトオーダー
あたりまえとは存ずるものの...
1バイト (8ビット) 単体では、256種類までの数値を表現できる。それより広い範囲の数値を扱いたい場合には、一般的に連続した複数のバイト並びを利用することで、それを実現する
ここで、表現したい値を その連続したバイト並びに対して、
どのような順で収めるのかを、ルールとして定めたものを
バイトオーダー または エンディアン と呼ぶ。
うち、実際によく利用されるのは以下の2つ
リトルエンディアン
0x1234ABCD (305,441,741) | |||
0x12 | 0x34 | 0xAB | 0xCD |
↓ | ↓ | ↓ | ↓ |
0xCD | 0xAB | 0x34 | 0x12 |
この並びの規則に従う場合、数値の下位バイトほど、先頭に出現するよう収められる。
Deflate 圧縮データ中に含まれる 多バイト長データは、このリトルエンディアンの並びの規則に従って出現する。
ビッグエンディアン
0x1234ABCD (305,441,741) | |||
0x12 | 0x34 | 0xAB | 0xCD |
↓ | ↓ | ↓ | ↓ |
0x12 | 0x34 | 0xAB | 0xCD |
ネットワークバイトオーダー とも呼ばれる 。
この並びの規則に従う場合、数値の上位バイトほど、先頭に出現するよう収められる。
後に解説する zlibデータ中に含まれる多バイト長のデータは、このビッグエンディアンの並びの規則に従って出現する。
非圧縮 (BTYPE:00)
Deflate で定義されている圧縮タイプのうちの一つ。
このブロックに含まれるデータは圧縮されない。圧縮がむしろ逆効果になってしまうような 小さなデータに対しては、このタイプを使う。
非圧縮ブロック中には、以下の要素が順番に整列している。
- ヘッダー情報 (前々回の記事で紹介済み)
- パディング
- LEN:非圧縮データのバイト数 (2バイト)
- NLEN:LENの補数 (2バイト)
- 非圧縮データ (~65,535バイト?)
2. パディング
メモリ上に展開されたとき、LENより以降のデータは、必ずバイト境界から始まるようになっている。パディングは、ヘッダー情報 (BFINAL、BTYPE) を読み出してから、それより後に現れる直近のバイト境界位置までの間に詰められる。
このデータにそれ以上の役割はないので、読み飛ばすか、読んでそのまま捨ててしまって構わない。
3. LEN:非圧縮データのバイト数
このブロックに含まれている非圧縮データの、バイト数を表す値がこの中に収められる。最大で 65,535 (0xffff) まで設定可能。
このデータは リトルエンディアン の並びで記録されている
4. NLEN:LENの補数
ここには LENの全ビットの0,1を反転した"補数"の値が設定される。
データが壊れていないかをチェックするためのものなんだろうか...?
- NLENの全ビットの0,1を反転させたものが LENと一致するか、
- LEN + NLEN = 0xffff になるか、
一応、このどちらかで、バイト長の値に誤りがないかをチェックできる。
このデータも リトルエンディアン の並びで記録されている
5. 非圧縮データ
ここに非圧縮状態のデータがそのまま収められる。データサイズはLENに設定されている通り。
zlib
あぁ、なんかうまくいかないと思った... pngのIDATの中身って、Deflate圧縮のデータがそんまま入ってるわけじゃないのか、zlibのヘッダとフッタを取り除かなければならぬ、
— ロヴスタング=パシフィケイナス (@DarkCrowCorvus) 2016年7月9日
PNGの圧縮データ部には、Deflateではなく、zlib というのを使っているそうだった。ここにきてようやく気付く。
実際にはそのzlibの中で、Deflateが扱われているので、「PNGはDeflate圧縮を使っている」といっても、あながち間違いではないとは思うのだけど...
構成
zlib 中には以下の要素が順番に整列している
- CMF (Compression Method and flags)
1.1. CM:圧縮方式 (4ビット)
1.2. CINFO :圧縮情報 (4ビット) - FLG (FLaGs)
2.1. FCHECK:CMF/FLGチェックビット (5ビット)
2.2. FDICT: プリセット辞書の利用有無 (1ビット)
2.3. FLEVEL: このデータの圧縮レベル (2ビット) - DICTID:プリセット辞書識別ID (存在しない場合あり 4バイト)
- 圧縮データ
- Adler-32 (4バイト)
なお、データ中の各々の要素はDeflateと同様、
アドレスの小さい順のビット番号の小さい順に並ぶ。
また、前述していたとおり、圧縮データ部を除く、zlib中の多バイトで表現される数値はすべて ビッグエンディアン の並びで記録されているということに注意すること。
1.1. CM:圧縮方式
CM:Compression method
ここには、zlibがどの圧縮方式を利用しているかの情報が 4ビット長 で収められる。
CM = 8 だった場合、それがDeflate圧縮を利用したzlibであるということを示す。
なお、CM = 8 以外の値は、現状扱われていない。とりあえず今は「8を読み出せれば正しい」とだけ覚えておけば問題ないはず..
1.2. CINFO:圧縮情報
CINFO:Compression info
CM = 8 のとき、ここには Deflate 圧縮で使用したスライド窓のサイズを表す値が設定される。
実際にはスライド窓のサイズを としたとき、
によって求まる値を、この 4ビット長 の中に収める。
なお、ここに設定できる値は 0~7 の値のうちいずれかで、利用できるスライド窓のサイズは 最小で256バイト、最大で32,768バイトというのが決められている。
ちなみに、符号化時にどのサイズのスライド窓を利用したとしても、復号時には最大サイズ (32,768) のスライド窓が用意できれば 困ることはない。細かな実装が面倒なら CINFO ≤ 7 であるかだけをチェックすれば、後は考慮しなくてもかまわない。
2.1. FCHECK:CMF/FLGチェックビット
FCHECK:Flag check bits?
1. CMF と 2. FRG を連結した 計16ビット長からなるデータの表す値が、31の倍数 になるよう、調節するために設定される。復号するとき、この結果が31の倍数にならない場合は、データが壊れていることを示唆する。
2.2. FDICT:プリセット辞書の利用有無
FDICT:Flag dictionaly?
このビットに「1」が設定されている場合は、プリセット辞書 (後述) を利用して解凍を行うべきであることを示す。またこれが「1」に設定されている場合は、後に 3. DICTID が出現する。
2.3. FLEVEL: このデータの圧縮レベル
FLEVEL:Flag level?
データの圧縮がどの程度の精度で行われたかを表す値が、2ビット長 で収められる。
CM = 8 の場合、以下のいずれかの値が設定される。
基本的に圧縮アルゴリズムは、圧縮が完了するまでの速さを重視すると圧縮率が落ち、逆に圧縮率を重視すると、圧縮が完了するまでに時間がかかる。
ここに設定される値は例えば、あるzlibのデータの圧縮率を上げるために、そのデータへの再圧縮を試みたいとき、それの結果、圧縮率が上がるかどうかを評価するための指標となる。
ちなみに データを解凍する上でこのデータは役に立たないため、読み飛ばしてしまっても構わない。
3. DICTID:プリセット辞書識別ID
DICTID:Dictionaly identifier
前述の FDICT に「1」が設定されている場合に限り、4バイトの長さで出現する。
ここには、プリセット辞書 (後述) に対してAdler-32 (後述) を施した結果、得られた値が設定される。解凍するプログラムはこれを見て、圧縮に使われたプリセット辞書を、自身が持っているかをチェックする。
4. 圧縮データ
CM = 8 であるとき、ここには Deflate 圧縮されたデータが収められる。解凍方法は、これまでの記事で紹介してきた通りだ。
プリセット辞書
LZ77 はその仕様により、圧縮のはじめはスライド窓の中身が空の状態である。そのため通常は、圧縮元のデータが小さかったり、または大きくても、はじめのほうに出現する羅列の圧縮には、あまり期待ができない。
しかし、特定のアプリケーションにおいて、どのような羅列が出現しやすいか 事前にわかっていたならば、あらかじめその羅列をスライド窓にセットしておけば、それらのデータを圧縮できる機会が生まれ、結果的に全体的なデータサイズを減らすことができそうだ。
この通り、圧縮前に事前にスライド窓へ渡されるこの羅列のことを プリセット辞書 (Preset dictionary) と呼ぶ。
zlibの圧縮プログラムと解凍プログラムはそれぞれ、自身のアプリケーションに合った任意の数のプリセット辞書を保有する。
圧縮時にプリセット辞書を使ったならば、FDICTに「1」を設定し、DICTIDに使ったプリセット辞書を識別するためのIDを設定する。通常ここには、プリセット辞書に対して、後述のAdler-32を施した結果の値を使用する。
解凍時には、FDICTを見て「1」であるならば、後に現れるDICTIDから、使われたプリセット辞書を特定し、そのプリセット辞書を使って解凍を行う。
Adler-32
任意のデータ列を 「数値の並び」 であるとみなして これの和を求め、結果をデータ列の末尾などに付与する。そのデータを受け取った側では同じ通り、データ列から和を求め、付与されていた和の値と一致するかをチェックする
これによって受け取るまでの途中でデータが壊れたりしていないかを、簡易的にチェックすることができる。
この考え方をベースとしたアルゴリズム、またはそれにより求まるチェック用の値のことを チェックサム (check sum) と呼ぶ。Adler-32 はそのチェックサムアルゴリズムの一種。 Mark Adlerさんが考案した
Adler-32は 2つの和の値 s1(2バイト)、s2(2バイト) からなる。
s1 は最初に「1」で初期化され、データ列の各値を先頭から順番に加算した結果を保持する。
s2 は最初に「0」で初期化され、s1にデータ列の値が加算されるたびに、そのs1の値を自身に加算した結果を保持する。
また双方ともに、加算されるたびに 65,521 との剰余によって上書きされ、計算の途中で桁あふれが起こらないようにされる。
結果的に求まる s1、s2を [s2, s1] の順番に連結することで、本来のAdler-32によるチェックサムが求まる。
zlibの解凍においては、4. 圧縮データ 部を使って、Adler-32 チェックサムを算出する。その直後に現れる、圧縮データに含まれる 5. Adler-32 のチェックサムと比較し、値が一致していれば、"圧縮データが壊れていない" ということを確認することができる。
サンプル
ここまでのサンプルプログラム
VC2015でのみ動作確認済み。
残念ながらプリセット辞書には対応できてない。
参考にしたサイトさん
RFC 1950 ZLIB Compressed Data Format Specification version 3.3 日本語訳 - futomi's CGI Cafe
RFC 1951 DEFLATE Compressed Data Format Specification version 1.3 日本語訳 - futomi's CGI Cafe
SWFバイナリ編集のススメ番外編 (zlib 伸張) 前編 | GREE Engineers' Blog
PNG イメージを自力でパースしてみる ~3/6 カスタムハフマン編~
ここまでのあらすじ
PNG イメージを自力でパースしてみる ~1/6 予備知識編~
PNG イメージを自力でパースしてみる ~2/6 Deflateの基本と固定ハフマン編~
ここでは Deflateの カスタムハフマン についてを解説
カスタムハフマン符号化 (BTYPE:10)
Deflate で定義されている圧縮タイプのうちの一つ
個人的に ここのブロックの圧縮ルールが結構ややこしかった。
まずはどんなふうに圧縮されているかを見てから 復号の解説を行いたいと思う
圧縮
カスタムハフマンのブロックは、以下の手順で作成される (たぶん)
- Deflateの LZ77 でデータ圧縮
- カノニカル・ハフマン符号化
- 符号長表を符号化
- 配置
符号長表を符号化 とかいう パッと見なんかよくわからないことをしていらっしゃるけど、まずは一番上から 順を追って解説してみる
1. Deflateの LZ77 でデータ圧縮
Deflate仕様の LZ77 で元データを圧縮する。
実際の圧縮方法については、これのひとつ前の記事で解説した
この結果、"文字" または "一致した長さ" (それと終端符号) は 0 ~ 285 (+拡張ビット)
"距離" は 0 ~ 29 (+拡張ビット) のかたちになって出力される
2. カノニカル・ハフマン符号化
- LZ77で置換された各値の出現回数を調べる
- 各値に対する 符号の長さを求め、符号長表を作成する
- 得られた符号長表から、カノニカル・ハフマンを使って符号表を作成する。
- 得られた符号表を使って、LZ77符号化されたデータをさらに符号化する
カノニカル・ハフマンについては、これのふたつ前の記事で解説した。これによって、圧縮後のデータに 符号長表 だけを収めれば、符号表と元データを間違いなく復元できる。
符号の長さを求めるときに ひとつ注意するべきことがある。Deflateのカスタムハフマンで扱うハフマン符号は、仕様により その符号長が 15 以下にならなければならない
一応、長さの制限されたハフマン符号を作成する方法が きちんとあるらしい。長くなるため こっちの記事で解説することにする
なお、文字/一致した長さの値 (0~285) の出現回数と、距離の値 (0~29) の出現回数の 2つは、それぞれ別々に統計され、それぞれ別々の符号表として作成される。
また、LZ77符号化されたデータは、その中に現れる値が 文字/一致した長さの値か、距離の値かによって、その都度2つの異なる符号表を使って符号化される。
3. 符号長表を符号化
直前の手続きによって得られた 2つの符号長表は、表中の "符号長" を表す値の偏り具合によって、それ自体、一度ハフマン符号化される
まず、2つの符号長表は、値の昇順に連続して連なるリスト構造に展開されているものとする。このうち、本来出現しない値に対しては 符号長:0 が充てられる。
符号長表中の 圧縮データ化される範囲は「可変」である。
文字/一致した長さの符号長表 の場合、利用されている一番大きな 一致した長さ の値に合わせて、0から 最小で256(257個)、最大で285(286個) までの範囲の符号長を対象とする。
距離の符号長表 については、最小で0のみ(1個)、最大で0~31(32個) までの範囲の符号長を対象とする。こちらも 利用されている 距離 の最大値によって、圧縮データ中に含める範囲が変化する。
この2つの表中の圧縮範囲に対しては、次の2つの圧縮が施される
3.1 ランレングス符号化
表中の符号長の値を連ねたとき、 同じ値が連続する箇所があれば、それを いくつ連続したか を表す数のデータに置き換えることで圧縮を行う。
このアプローチは通常、ランレングス符号化 と呼ばれる
Deflate仕様のランレングス符号化では、符号長表の圧縮範囲の値が連続する場所に対して、以下の通りに置き換えを行うものとしている
・ひとつ前に出現した値が 3 ~ 6 回繰り返される | 16 ( 拡張ビット:2 ) |
・"0" が 3 ~10 回繰り返される | 17 ( 拡張ビット:3 ) |
・"0" が 11 ~ 138 回繰り返される | 18 ( 拡張ビット:7 ) |
拡張ビットには、"どれだけ連続するか" を調整する値が設定され、
ベース値 + 拡張ビット によって本来の連続した長さを求められるようになっている
3.2 カノニカル・ハフマン符号化
ランレングス符号化が終わった2つの符号長表に対して、その表中の値の出現回数を調べ、符号長表を作成し、それから符号表を作成し、それから符号化を行う。
値の出現回数は、表2つを合わせて集計される。
各値の出現回数が分かれば、次にその各値に対する符号長を求める。
ここでも求める符号長の、その長さに注意すること。LZ77符号化済みデータに対する符号長制限が 15以下 だったのに対し、こちらでは 7以下 になっていなければならない
得られた各値に対する符号長から 符号長表の符号長表 を作成すれば、あとはそれから、符号表を作成し、2つの符号長表を圧縮する。
ここまでの手続きで、ひとまずカスタムハフマンブロックに含む 各データの符号化が完了する
4. 配置
最終的に カスタムハフマンブロック中には、以下の要素が順番に整列することになる
- ヘッダー情報 (前回の記事で紹介済み)
- HLIT:文字/一致長符号の個数 (5ビット)
- HDIST:距離符号の個数 (5ビット)
- HCLEN:符号長表の符号長表のサイズ (4ビット)
- 符号長表の符号長表
- 符号化された文字/一致長の符号長表
- 符号化された距離の符号長表
- 圧縮データ
2. HLIT:文字/一致長符号の個数
HLIT ...header literalの略?
ここには、文字/一致長の符号長表に含めた符号長の個数 (257 ~ 286) を表す値が設定される。実際には 個数 -257 した 0 ~ 29 までの範囲の値が 5ビット長の中に収められる
3. HDIST:距離符号の個数
HDIST ...header distanceの略?
ここには、距離符号長表に含めた符号長の個数 (1 ~ 32) を表す値が設定される。実際には 個数 -1 した 0 ~ 31 までの範囲の値が 5ビット長の中に収められる
4. HCLEN:符号長表の符号長表のサイズ
HCLEN ...header code lengthの略?
ここには カスタムハフマンブロックに収められる 符号長表の符号長表に、符号長がいくつ含まれているか (~19) を表す値が設定される。実際には個数 -4 した 0 ~ 15 までの範囲の値が 4ビット長の中に収められる
5. 符号長表の符号長表
符号長表の符号長表 は、カスタムハフマンブロックに対して、以下のような変則的な並びになった状態で記録される。
16 | 17 | 18 | 0 | 8 | 7 | 9 | 6 | 10 | 5 | 11 | 4 | 12 | 3 | 13 | 2 | 14 | 1 | 15 |
各符号長値 (0~7) は、それぞれ3ビット長の中に収められる。
また、このうち本来出現しない値に対しては、符号長:0が充てられる
この並びの中に各符号長を収めた時、最後尾「15」の符号長が0で、なおかつそこから先頭方向に連続して0が続く場合、カスタムハフマンブロックに並びを収めるとき、その0が続く部分を省略することができる。
ただし省略できる符号長は15個まで。
並びの中の符号長の個数は 4未満にはならない
この結果、並びの中に残った符号長の個数 -4 した値が、
前述の HCLEN に 実際に設定される数になる。
それ以降にようやく
6. 符号化された文字/一致長の符号長表
7. 符号化された距離の符号長表
8. 圧縮データ
のデータが順番に現れるようになっている
解凍
上で説明した構築の手順を逆にたどれば、カスタムハフマンブロックの解凍が行える
- ヘッダー情報を読み出す(前回の記事で紹介済み)
- HLIT (5ビット) 、HDIST (5ビット) 、HCLEN (4ビット) をそれぞれ読み出す。
- HCLEN + 4 によって 並びの中の符号長の数 がわかれば、後に続く符号長(3ビット) をその数だけ読み出し、本来の 符号長表の符号長表 を復元する
- 符号長表の符号長表から、符号長表の符号表 を作成する。
- HLIT + 257 によって、このブロックに含まれている 文字/一致長の符号長値の数 がわかれば、先ほど作成した符号長表の符号表を使って、その数だけの符号長を読み出して復号し、本来の 文字/一致長の符号長表 を復元する
- HDIST + 1 によって、このブロックに含まれている 距離の符号長値の数 がわかれば、同じく符号長表の符号表を使って、その数だけの符号長を読み出して復号し、本来の距離の符号長表 を復元する
- 復元した2つの符号長表から 文字/一致長の符号表 と、距離の符号表 をそれぞれ作成する
- 作成した2つの符号表を使って、残りの圧縮データを読み出して復号する
なお、HLIT、HDIST、HCLEN、符号長表の符号長表、拡張ビットは "値のデータ" なので、読み出すときには その値がそれぞれ、最下位ビットから順番にパックされたビット並びになっていることに注意すること。
サンプル
ここまでのサンプルプログラム
VC2015でのみ動作確認済み。
参考にしたサイトさん
RFC 1951 DEFLATE Compressed Data Format Specification version 1.3 日本語訳 - futomi's CGI Cafe
デフレート圧縮(LZ77圧縮)処理の概要 - ウェブで用いられる画像形式。