読者です 読者をやめる 読者になる 読者になる

自由研究ノート(仮)

とかいう名前の備忘録

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^{31}-1」までの値を収められる

 

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 TypeBit 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イメージは、以下の図に示す通りに処理される。

f:id:DarkCrowCorvus:20170120234812g:plain

これによる利点は、画像データを読み始めた早い段階で、おおざっぱな画像の全体像がわかるということ。初めにモザイク調で表示させ、読み込みが進み次第、徐々に鮮明になるように画像を表示させることができるようになる。


なお、今回は残念ながら 実装が面倒だったので インタレースに対応していない。
気が向いたらそのうち..

 


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イメージから、画像を構成するピクセルデータを引っ張りだすことができる

  1. シグネチャ一致チェック
  2. チャンク読み出し
  3. IDATチャンクの zlib圧縮データを解凍
  4. 解凍したデータのフィルタリングを解く

 

1. シグネチャ一致チェック

ファイル先頭の 8バイト を読み出し、前述したシグネチャの並びと一致するかどうかをチェックする。

一致しなかった場合、そのファイルが PNGイメージではない、もしくは、ファイルを正しく読み出すことができないため、エラーとみなして、テキトウに処理を終了する。

 

2. チャンク読み出し 

以下を IEND チャンクが出現するまで繰り返す

  1.  チャンクのLength (4バイト) を読み出す
  2.  Chunk Typeの4文字 (4バイト) を読み出す
  3.  Lengthが 0 でなければ、そのバイト数分だけ Chunk Dataを読み出す
  4.  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ビット 付加する以外には、水平ラインに対して特になにも行わない。

f:id:DarkCrowCorvus:20170209000242j:plain


ちなみに、ビット深度が 8 に満たないとき (BitDepth=1,2,4) 
各水平ラインの末端がバイト境界になっていなかった場合は、次に出現するバイト境界の地点までの間にパディングが詰められるみたい

次の水平ラインの先頭ビットは必ず、その直近のバイト境界の位置から始まる

f:id:DarkCrowCorvus:20170207233748j:plain

 

フィルタタイプ 1:Sub

該当する画像の水平ラインの先頭に「1」を表す数値を 8ビット長で付加する 。
PNGイメージ中に収められるこの水平ライン上の各ピクセルの値は、左隣のピクセルの値との差分値になる。

f:id:DarkCrowCorvus:20170209000256j:plain

なお、差の結果は 非負の 0~255(00~FF)の範囲に収まるよう丸められる。
 ※例えば差の結果が負数の場合 [-1, -2, -3] は [255, 254, 253] のような通りになる。


また先頭のピクセルについては、左隣に参照できるピクセルがないため、左隣のピクセル値 = 0 として扱う

 

ここまでを見たとおり、フィルタリング処理自身は、データを圧縮するような作用を持っていない。しかも各水平ラインの先頭に、フィルタを識別するための 8ビットを付与するため、このままでは むしろデータサイズを増やしてしまうだけのように見える。

ただしこのフィルタリング処理には、その後に続くDeflate の、圧縮効率を底上げするという機能がある。

実際には、Noneを除く 4種類 のフィルタリングを使って、PNGイメージ中の値に偏りや同じ値並びを出現させることを行う。

このようにデータを変換することで、Deflateによるハフマン符号化、およびLZ77符号化がうまく働く機会が増えるため、データをより小さく圧縮できるようになる。

f:id:DarkCrowCorvus:20170210084353j:plain

うち「フィルタタイプ 1:Sub」は、横方向にピクセル値の変化が小さい 水平ラインに対して効果を発揮する

 

ここで、フィルタリング処理は、必ず 8ビット単位 で行われるということに注意。
このルールは他のフィルタタイプにも共通する

ビット深度が16であるPNGイメージにおいては、上位8ビット、下位8ビットの2つに分割し、別々にフィルタリング処理を施す

ビット深度が8未満のPNGイメージにNone以外のフィルタタイプを適用する場合は、事前に8ビットに伸長されるらしい...? サンプルを見つけられなかったため、ちょっと確かではないのだけど..


なお、Subフィルタリングされたピクセルは、
ピクセル値 + 左隣のピクセル の 256による剰余
によって元の値に復元できる

 

フィルタタイプ 2:Up

該当する画像の水平ラインの先頭に「2」を表す数値を 8ビット長で付加する 。
PNGイメージ中に収められるこの水平ライン上の各ピクセルの値は、真上のピクセルの値との差分値になる。f:id:DarkCrowCorvus:20170211232608j:plain

差の結果は 非負の 0~255(00~FF)の範囲に収まるよう丸められる。
このルールについても、差を扱うすべてのフィルタタイプに共通する


「フィルタタイプ 2:Up」は縦方向にピクセルの値の変化が小さい水平ラインに対して効果を発揮する

なお、Upフィルタリングされたピクセルは、
ピクセル値 + 真上のピクセル 256による剰余
によって元の値に復元できる

 

フィルタタイプ 3:Average

該当する画像の水平ラインの先頭に「3」を表す数値を 8ビット長で付加する 。
PNGイメージ中に収められるこの水平ライン上の各ピクセルの値は、左隣のピクセル値と、真上のピクセル値との平均をとった値との差分値になる。

 filtered value = current - ((left + up) ÷ 2)

f:id:DarkCrowCorvus:20170212174810p:plain

差の結果は 非負の 0~255(00~FF)の範囲に収まるよう丸められる。
また先頭のピクセルについては、左隣に参照できるピクセルがないため、
左隣のピクセル値を 0 として扱い、平均を求める

平均の計算には整数徐算を利用する。
徐算の結果は、値の小さいほうの整数に丸められる(小数点以下切り捨て)


「フィルタタイプ 3:Average」は似たピクセル値が密集しているあたりの水平ラインに対して効果を発揮する

なお、Averageフィルタリングされたピクセルは、
ピクセル値 + 左隣と真上のピクセル値の平均 256による剰余
によって元の値に復元できる

 

フィルタタイプ 4:Paeth

該当する画像の水平ラインの先頭に「4」を表す数値を 8ビット長で付加する 。
PNGイメージ中に収められるこの水平ライン上の各ピクセルの値は、「Paeth」というアルゴリズムから得られる結果の値との差分値になる。

 filtered value = current -Paeth (left, up, upleft)

f:id:DarkCrowCorvus:20170212230424p:plain

差の結果は 非負の 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でのみ動作確認済み。

 


参考にしたサイトさん

わかりやすい PNG の話 for Web

PNG を自力で読んで表示しよう その1 - 雑念日記

PNG ファイルフォーマット

PNG画像形式の概要 - ウェブで用いられる画像形式。

Portable Network Graphics (PNG) Specification (Second Edition)

PNG Specification: File Structure

Adam7 algorithm - Wikipedia

Convert, Edit, Or Compose Bitmap Images @ ImageMagick

PNG イメージを自力でパースしてみる ~4/6 非圧縮とzlib編~

ここまでのあらすじ

PNG イメージを自力でパースしてみる ~1/6 予備知識編~
PNG イメージを自力でパースしてみる ~2/6 Deflateの基本と固定ハフマン編~ 
PNG イメージを自力でパースしてみる ~3/6 カスタムハフマン編~


前回、前々回でDeflateの「固定ハフマン」と「カスタムハフマン」について触れた。ここでは残るブロックタイプの「非圧縮」と、Deflateを利用した圧縮フォーマットの「zlib」についてを紹介。

 

 


予備知識

※2017/02/14更新
追加で予備知識が必要かも

 

バイトオーダー

あたりまえとは存ずるものの...
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 で定義されている圧縮タイプのうちの一つ。

 

このブロックに含まれるデータは圧縮されない。圧縮がむしろ逆効果になってしまうような 小さなデータに対しては、このタイプを使う。

 

非圧縮ブロック中には、以下の要素が順番に整列している。

 

  1. ヘッダー情報 (前々回の記事で紹介済み)
  2. パディング
  3. LEN:非圧縮データのバイト数 (2バイト)
  4. NLEN:LENの補数 (2バイト)
  5. 非圧縮データ (~65,535バイト?)

f:id:DarkCrowCorvus:20170108132252j:plain

 

2. パディング

メモリ上に展開されたとき、LENより以降のデータは、必ずバイト境界から始まるようになっている。パディングは、ヘッダー情報 (BFINAL、BTYPE) を読み出してから、それより後に現れる直近のバイト境界位置までの間に詰められる。

このデータにそれ以上の役割はないので、読み飛ばすか、読んでそのまま捨ててしまって構わない。

 

3. LEN:非圧縮データのバイト数

このブロックに含まれている非圧縮データの、バイト数を表す値がこの中に収められる。最大で 65,535 (0xffff) まで設定可能。
 
このデータは リトルエンディアン の並びで記録されている

4. NLEN:LENの補数

ここには LENの全ビットの0,1を反転した"補数"の値が設定される。


データが壊れていないかをチェックするためのものなんだろうか...?

  1. NLENの全ビットの0,1を反転させたものが LENと一致するか、
  2. LEN + NLEN = 0xffff になるか、

一応、このどちらかで、バイト長の値に誤りがないかをチェックできる。
同じく、リトルエンディアンの並びで格納される


このデータも リトルエンディアン の並びで記録されている

 

5. 非圧縮データ

ここに非圧縮状態のデータがそのまま収められる。データサイズはLENに設定されている通り。

 


zlib

 

 

PNGの圧縮データ部には、Deflateではなく、zlib というのを使っているそうだった。ここにきてようやく気付く。

 

実際にはそのzlibの中で、Deflateが扱われているので、「PNGはDeflate圧縮を使っている」といっても、あながち間違いではないとは思うのだけど...

 


構成

 

zlib 中には以下の要素が順番に整列している

 

  1. CMF (Compression Method and flags)
    1.1. CM:圧縮方式 (4ビット)
    1.2. CINFO :圧縮情報 (4ビット)

  2. FLG (FLaGs)
    2.1. FCHECK:CMF/FLGチェックビット (5ビット)
    2.2. FDICT: プリセット辞書の利用有無 (1ビット)
    2.3. FLEVEL: このデータの圧縮レベル (2ビット)

  3. DICTID:プリセット辞書識別ID (存在しない場合あり 4バイト)
  4. 圧縮データ
  5. Adler-32 (4バイト)

f:id:DarkCrowCorvus:20170109143630j:plain

なお、データ中の各々の要素は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 圧縮で使用したスライド窓のサイズを表す値が設定される。

実際にはスライド窓のサイズを n としたとき、 
log_2(n)-8 によって求まる値を、この 4ビット長 の中に収める。 

 

なお、ここに設定できる値は 0~7 の値のうちいずれかで、利用できるスライド窓のサイズは 最小で256バイト、最大で32,768バイトというのが決められている。

 

ちなみに、符号化時にどのサイズのスライド窓を利用したとしても、復号時には最大サイズ (32,768) のスライド窓が用意できれば 困ることはない。細かな実装が面倒なら CINFO ≤ 7 であるかだけをチェックすれば、後は考慮しなくてもかまわない。 

 

2.1. FCHECK:CMF/FLGチェックビット
FCHECK:Flag check bits?

1. CMF2. 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) と呼ぶ。

 

f:id:DarkCrowCorvus:20170108215754j:plain

 

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

Improving compression with a preset DEFLATE dictionary

Adler-32 - Wikipedia

PNG イメージを自力でパースしてみる ~3/6 カスタムハフマン編~

ここまでのあらすじ

PNG イメージを自力でパースしてみる ~1/6 予備知識編~
PNG イメージを自力でパースしてみる ~2/6 Deflateの基本と固定ハフマン編~ 

 

ここでは Deflateの カスタムハフマン についてを解説

 

 


カスタムハフマン符号化 (BTYPE:10)


Deflate で定義されている圧縮タイプのうちの一つ

 

個人的に ここのブロックの圧縮ルールが結構ややこしかった。
まずはどんなふうに圧縮されているかを見てから 復号の解説を行いたいと思う 

 


圧縮

 

カスタムハフマンのブロックは、以下の手順で作成される (たぶん)

 

  1. Deflateの LZ77 でデータ圧縮
  2. カノニカル・ハフマン符号化
  3. 符号長表を符号化
  4. 配置

 

符号長表を符号化 とかいう パッと見なんかよくわからないことをしていらっしゃるけど、まずは一番上から 順を追って解説してみる

 


1. Deflateの LZ77 でデータ圧縮

 

Deflate仕様の LZ77 で元データを圧縮する。
実際の圧縮方法については、これのひとつ前の記事で解説した

 

この結果、"文字" または "一致した長さ" (それと終端符号) は 0 ~ 285  (+拡張ビット)
"距離" は 0 ~ 29  (+拡張ビット) のかたちになって出力される

 

f:id:DarkCrowCorvus:20170105224856j:plain

 


2. カノニカル・ハフマン符号化

 

  1. LZ77で置換された各値の出現回数を調べる
  2. 各値に対する 符号の長さを求め、符号長表を作成する
  3. 得られた符号長表から、カノニカル・ハフマンを使って符号表を作成する。
  4. 得られた符号表を使って、LZ77符号化されたデータをさらに符号化する

 

カノニカル・ハフマンについては、これのふたつ前の記事で解説した。これによって、圧縮後のデータに 符号長表 だけを収めれば、符号表と元データを間違いなく復元できる。

 

符号の長さを求めるときに ひとつ注意するべきことがある。Deflateのカスタムハフマンで扱うハフマン符号は、仕様により その符号長が 15 以下にならなければならない


一応、長さの制限されたハフマン符号を作成する方法が きちんとあるらしい。長くなるため こっちの記事で解説することにする
 

 

なお、文字/一致した長さの値 (0~285) の出現回数と、距離の値 (0~29) の出現回数の 2つは、それぞれ別々に統計され、それぞれ別々の符号表として作成される。

 

また、LZ77符号化されたデータは、その中に現れる値が 文字/一致した長さの値か、距離の値かによって、その都度2つの異なる符号表を使って符号化される。

 

f:id:DarkCrowCorvus:20170105224953j:plain

 


3. 符号長表を符号化

 

直前の手続きによって得られた 2つの符号長表は、表中の "符号長" を表す値の偏り具合によって、それ自体、一度ハフマン符号化される

 

まず、2つの符号長表は、値の昇順に連続して連なるリスト構造に展開されているものとする。このうち、本来出現しない値に対しては 符号長:0 が充てられる。

 

f:id:DarkCrowCorvus:20170106201614j:plain

 

符号長表中の 圧縮データ化される範囲は「可変」である。

文字/一致した長さの符号長表 の場合、利用されている一番大きな 一致した長さ の値に合わせて、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 )

 

f:id:DarkCrowCorvus:20170107163645j:plain

拡張ビットには、"どれだけ連続するか" を調整する値が設定され、
ベース値 + 拡張ビット によって本来の連続した長さを求められるようになっている

 

3.2 カノニカル・ハフマン符号化

ランレングス符号化が終わった2つの符号長表に対して、その表中の値の出現回数を調べ、符号長表を作成し、それから符号表を作成し、それから符号化を行う。

f:id:DarkCrowCorvus:20170107163725j:plain

 

値の出現回数は、表2つを合わせて集計される。

 

各値の出現回数が分かれば、次にその各値に対する符号長を求める。
ここでも求める符号長の、その長さに注意すること。LZ77符号化済みデータに対する符号長制限が 15以下 だったのに対し、こちらでは 7以下 になっていなければならない

 

得られた各値に対する符号長から 符号長表の符号長表 を作成すれば、あとはそれから、符号表を作成し、2つの符号長表を圧縮する。

 

f:id:DarkCrowCorvus:20170107163737j:plain

 

ここまでの手続きで、ひとまずカスタムハフマンブロックに含む 各データの符号化が完了する

 

4. 配置

 

最終的に カスタムハフマンブロック中には、以下の要素が順番に整列することになる

 

  1.  ヘッダー情報 (前回の記事で紹介済み)
  2.  HLIT:文字/一致長符号の個数 (5ビット)
  3.  HDIST:距離符号の個数 (5ビット)
  4.  HCLEN:符号長表の符号長表のサイズ (4ビット)
  5.  符号長表の符号長表
  6.  符号化された文字/一致長の符号長表
  7.  符号化された距離の符号長表
  8.  圧縮データ

 

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未満にはならない

 

f:id:DarkCrowCorvus:20170107144210j:plain

 

この結果、並びの中に残った符号長の個数 -4 した値が、
前述の HCLEN に 実際に設定される数になる。

 

それ以降にようやく

6. 符号化された文字/一致長の符号長表
7. 符号化された距離の符号長表
8. 圧縮データ

のデータが順番に現れるようになっている

 


解凍

 

上で説明した構築の手順を逆にたどれば、カスタムハフマンブロックの解凍が行える

 

  1. ヘッダー情報を読み出す(前回の記事で紹介済み) 

  2. HLIT (5ビット) 、HDIST (5ビット) 、HCLEN (4ビット) をそれぞれ読み出す。

  3. HCLEN + 4 によって 並びの中の符号長の数 がわかれば、後に続く符号長(3ビット) をその数だけ読み出し、本来の 符号長表の符号長表 を復元する

  4. 符号長表の符号長表から、符号長表の符号表 を作成する。

  5. HLIT + 257 によって、このブロックに含まれている 文字/一致長の符号長値の数 がわかれば、先ほど作成した符号長表の符号表を使って、その数だけの符号長を読み出して復号し、本来の 文字/一致長の符号長表 を復元する

  6. HDIST + 1 によって、このブロックに含まれている 距離の符号長値の数 がわかれば、同じく符号長表の符号表を使って、その数だけの符号長を読み出して復号し、本来の距離の符号長表 を復元する

  7. 復元した2つの符号長表から 文字/一致長の符号表 と、距離の符号表 をそれぞれ作成する

  8. 作成した2つの符号表を使って、残りの圧縮データを読み出して復号する

 

なお、HLIT、HDIST、HCLEN、符号長表の符号長表、拡張ビットは "値のデータ" なので、読み出すときには その値がそれぞれ、最下位ビットから順番にパックされたビット並びになっていることに注意すること。

 


サンプル 

 

ここまでのサンプルプログラム
VC2015でのみ動作確認済み。

 


参考にしたサイトさん 

 
RFC 1951 DEFLATE Compressed Data Format Specification version 1.3 日本語訳 - futomi's CGI Cafe

カスタムハフマン符号 - 七誌の開発日記(旧)

デフレート圧縮(LZ77圧縮)処理の概要 - ウェブで用いられる画像形式。

 

package-merge algorithmを勉強してみる

最大符号長の制限されたハフマン符号表を生成する方法として package-merge algorithm っていうのがあるらしい。気が向いたのでちょっと調べてみる

 

今回は以下の資料さんを参考にさせていただいた

"A Fast and Space-Economical Algorithm for Length-Limited Coding Jyrki Katajainen, Alistair Moffat, Andrew Turpin".

 

自身の理解できた範囲でそれぞれ順番に紹介してみる

 

 


package-merge algorithm

 

前述のとおり package-merge algorithm は、最大符号長の制限されたハフマン符号表を生成するために利用できる

 

ただし、符号表中の符号を ある長さまでに制限するためには、
符号表を生成する元として使う、シンボルの種類の数に制限がかかる

 

たとえば最大符号長を「2」に制限して符号表を作成した場合、
その中で同時に定義ができる接頭符号の種類は、多くて4つまで

00 01 10 11

 

最大符号長を「3」に制限した場合では、
その中で同時に定義できる接頭符号の種類は、多くて8つまでになる

000 001 010 011 100 101 110 111

 

この通り、最大符号長を L と定義した時、
表現できる符号の種類の最大は、 2^L のように表すことができる

 

シンボルの種類の数を n とするとき、

  n ≤ 2^L

を満たさなければ、最大符号長を L に制限した中で、それぞれのシンボルに対して、きちんと接頭符号を割り当てることができない。

 

package-merge algorithm  は上の制約を満たす各々のシンボルに対して、指定した符号長以下になるよう符号を割り当てて、符号表を作成するのを手助けしてくれる アルゴリズムである。

 

生成手順

 

はじめに断っておくと、package-merge algorithm 単体ではハフマン符号を生成することはできない

 

package-merge algorithm が出力できるのは、各々のシンボルを符号化したときの 符号長 までにとどまる

 

そこから実際に符号を割り当てて、符号表を求めるには、 カノニカル・ハフマン符号化 を利用する。カノニカル・ハフマン符号化については、以前別の記事で紹介した

 

 

符号化対象のデータに含まれる、各々の文字を シンボルデータ中で各々の文字が出現する回数を シンボルの重み として扱い、

 

[シンボル, 重み] のペアからなるリストと、制限したい符号の長さ情報が与えられたとき、package-merge algorithmを使って符号表を生成する手順を以下に示す

 

f:id:DarkCrowCorvus:20161224172840j:plain

 

1. ステージの初期化

制限したい符号の長さの数だけのステージを用意し、シンボルのリストの内容物によってそれぞれ初期化する。

ここであらかじめ、一番上のステージのリストはシンボルの重みの昇順にソートしておく。

 

f:id:DarkCrowCorvus:20161224191534j:plain

 

2. パッケージマージ

一番上のステージにある要素を、先頭から順番に2つずつ選び出し、そのペアからなる パッケージ をそれぞれ作成する。

その後、作成したパッケージを一つ下のステージにマージする

 

f:id:DarkCrowCorvus:20161224191941j:plain

 

ここで パッケージ はペアとなる要素それぞれへの参照と、その2つの要素の重みの合計値を自身の重みとして持つ。なお、ペアを作れなかった要素に対しては何も行わず無視する

 

マージ後、そのステージの要素を昇順に並べ替える。

またその後、そのステージで2つずつのペアを作ったとき、ペアを組めず無視されるであろう要素があれば、あらかじめそのステージから除外しておく

 

f:id:DarkCrowCorvus:20161225003416j:plain

 

ここまでのパッケージマージの流れを、今度は2番目のステージから、その下の3番目のステージに対して行い、最終的に一番下のステージにたどり着くまでこれを繰り返す

 

f:id:DarkCrowCorvus:20161225021548j:plain

 

3. 符号長を抽出

最終的に、一番下のステージに出揃った各々の要素を一つずつピックアップし、各々の要素が持つシンボル、またはその要素が参照しているシンボルの数だけ該当のシンボルの符号長をインクリメントする

 

f:id:DarkCrowCorvus:20161225115930g:plain

 

各々のシンボルの符号長は、多くてステージの数までとなり、パッケージマージの過程で、無視され除外された回数の多いシンボルほど、最終的な符号長が小さくなるという仕組みになっている

 

4. 符号表を作成

後はカノニカル・ハフマン符号化の手順に従って、各々のシンボルの符号長から 符号表を作成する

 

  1. 符号長別にグループ分けした後、その中で文字番号の小さい順に並べかえる 
  2. 符号長が短く文字番号の小さなシンボルほど小さな符号になるよう符号を割り当てる

 

f:id:DarkCrowCorvus:20161225165246j:plain

 

これで、package-merge algorithm によって最大符号長の制限された符号表が完成する

 


遅延 package-merge

 

package-merge algorithm は 一番下のステージに出揃った要素をピックアップして、各々のシンボルの符号長を求める

 

この一番下に出揃う要素について、その数に着目すると、
ステージの数にかかわらず、必ず  2n - 2 だけになる。
このあたりなんかうまくそうできてるらしい

 

f:id:DarkCrowCorvus:20161225184410j:plain

 

これから察するに、最後のステージに対して、2n-2 回だけ要素の作成を要求して、その過程で必要になり次第、上のステージの要素を作成してパッケージを貰ってくる...というアルゴリズムが組めそうだ。

 

実際に必要になるまで上のステージの要素の作成を遅らせる様子から、
このアルゴリズムのことを、
遅延 package-merge (Lazy package-merge) と呼ぶことにする。

 

生成手順

各々のステージは それぞれ以下の情報を保有する

 

  • 要素1 (先読みツリー)
  • 要素2 (先読みツリー)
  • シンボルカウンタ 

 

このうち、2つの "要素" は 自身の一つ下のステージで、次にパッケージを作成する元として使われるであろう候補の要素を事前に知っておくために保有しておく

 

冒頭で紹介した資料さんによれば、
この要素のことを 先読みツリー (lookahead tree) と呼ぶそうだ

 

なお最下段のステージは、自身の下にパッケージを提供するステージが存在しないため、先読みツリーを持たなくても構わない。

 

"シンボルカウンタ" は自身のステージで、[シンボル,重み] のリストを いくつだけ読んだかの情報を覚えておくために、その数を保持する。 

 

遅延 package-mergeを使った符号表の生成手順を以下に示す

  

1. シンボルリストをソート

[シンボル,重み] のリストを、あらかじめ重みの昇順になるように並べ替えておく

 

2. ステージの初期化

一番下のステージを除いて、各ステージ上の先読みツリー2つを、重みの昇順に並んだ [シンボル,重み] リストの上二つの要素で初期化する。

また、シンボルカウンタには、リストを "2つ" 読み出したことを記録しておく

 

f:id:DarkCrowCorvus:20161227232710j:plain

 

3. 要素を作成

これから、最下段ステージに要素を作成する次の手順を、2n-2 回だけ繰り返す

 

まず、[シンボル,重み] のリストから次に読み出せる要素の重みと、自身の一つ上の先読みツリー2つの重みの合計のどちらが小さいかを比較する。

 

リストの要素の重みのほうが小さければ 3.1 の処理、そうではない場合、またはシンボルのリストから読み出せる要素がもうない場合は  3.2 の処理へ進む

 

f:id:DarkCrowCorvus:20161229200142j:plain

 

3.1 [シンボル,重み] リストの要素を追加

[シンボル,重み] のリストから読み出した要素を自身のステージに追加する。
その後、シンボルカウンタを1つ進める。

 

f:id:DarkCrowCorvus:20161229204603j:plain

 

なお、1番目、2番目に追加される要素は必ず、[シンボル,重み] のリストの1番目、2番目の要素になるため、2. ステージの初期化 で各ステージを初期化する際に、この要素2つをあらかじめ 最下段ステージに作成しておいてもかまわない。 

 

f:id:DarkCrowCorvus:20161229205426j:plain

 

3.2 先読みツリーのパッケージを追加

一つ上のステージの先読みツリー2つからパッケージを作成し、自身のステージへ追加する。 

 

f:id:DarkCrowCorvus:20161229213605j:plain

 

一つ上の先読みツリーからパッケージが作成されるたび、ここまでの 3.要素を作成 の手順を、今度は一つ上のステージに対して2回行うことで 先読みツリー2つを更新する。

 

この過程でさらに上のステージの先読みツリーからもパッケージが作られれば、そのステージでも同じ通り、新しい要素を作成する手順を2回行う。

 

それよりさらに上のステージでも同様、最終的に更新が必要な先読みツリーすべてに対して、新しい要素が補填されるまでこの処理は繰り返される

 

なお最上段のステージは、上にパッケージを貰ってこれるステージが存在しないため、常に [シンボル,重み] リストの まだ読んでいない新しい要素 2つによって先読みツリーを更新する

 

f:id:DarkCrowCorvus:20161230172854g:plain

 

最下段のステージに 2n-2 個の要素がそろえば、あとは通常の package-merge algorithm の時と同様の手順で、各シンボルの符号長を導出し、符号表を作成できる

 

利点

早いうちに最下段ステージに要素が作成されるというのが、このアルゴリズムの特徴になる。これによる利点は、メモリ空間の再利用ができるということ

 

符号長を導出する手続きは、なにも最下段のステージに要素がすべて出揃うのを待つ必要はない。 要素が一つ新しく作成されるたびに、その要素がもつシンボル、またはその要素が参照するシンボルの符号長を更新することができる。

 

符号長の更新後、その要素とそれが参照している要素は、もう扱われることがない。そのため、それらの要素を保持するために使ったメモリ空間は、これより以降に要素を作成するために使いまわすことができる。

 

f:id:DarkCrowCorvus:20170101011731j:plain

 

要素のプールを作成してこれを実現する場合、
資料さんによれば、プールの "空き要素" は nL つだけ必要とのこと。
(資料さんに載ってるここの証明がうまく理解できなかった。悔しい)

 

※ 2017/01/07追記
たまに nL で空き要素が足りないことがあるっぽい。一番下のサンプルプログラムのほうは、ステージの数を減らしたりして ひとまず対処してみてるけど、理由はよくわかってない

 

なお、 n はシンボルの数、 L は制限したい符号の長さを表す。

 


境界 package-merge

 

package-merge algorithm で 生成した各ステージ上の要素のうち、実際にシンボルの符号長を求めるために使われた要素に対して色を付けてみる。

 

f:id:DarkCrowCorvus:20170102163345j:plain

 

最下段のステージを除いて、それより上にある各ステージはともに、ある要素を境にして、実際に符号長の導出のために使われた要素(左側) と使われなかった要素(右側)に分かれるのがわかる。

 

この色のついた左側にある要素を、以降は "アクティブな要素" と呼んで扱うことにする

 

さらにこのアクティブな要素のうち、シンボル単体からなる要素 (パッケージではない要素) に着目し、その数を数えてみる

 

f:id:DarkCrowCorvus:20170102163357j:plain

 

シンボル単体からなる要素は、各ステージともに、その重みの小さい順にもれなく出現している

 

そのため、アクティブなシンボル単体の要素の数が、その中で  であるとするならば、そこには重みの一番小さなシンボルと、2番目に小さなシンボルの 2つ が必ず現れる

 

アクティブなシンボル単体の要素の出現回数を c と定義するならば、重みの昇順に並んだ 1~ c 番目までのシンボル単体からなる要素が、そこには出現することになる

 

この通り、アクティブなシンボル単体の要素の出現回数だけを ステージごとに覚えておきさえすれば、後から実際に出現したシンボル単体の要素が何であるかを推測できる

 

出現回数から、実際に出現するシンボルが何であるかを割り出せれば、あとはそのシンボルの符号長をインクリメントする...という方法で同じ通り符号長の導出ができそうだ。

 

f:id:DarkCrowCorvus:20170103144355g:plain

 

アクティブな要素とアクティブでない要素との境界を ステージごとに形成し、それよりも左側に出現した シンボル単体の要素の数から、各シンボルの符号長を求めるこのアルゴリズム

境界 package-merge (Boundary package-merge) と呼ぶことにする。

 

生成手順

各々のステージは それぞれ以下の情報を保有する

 

  • 要素1 (先読みチェーン)
  • 要素2 (先読みチェーン)

 

また、各要素は以下の情報を保有する

 

  • 重み
  • シンボルカウンタ
  • 要素への参照

 

このうち "要素への参照" は、自身の要素が作成されたときに、一つ上のステージで、一番最後にパッケージを作成する元として使われていた要素を覚えておくために利用される。以降はこの参照を チェーン と呼ぶことにする

 

境界 package-mergeを使った符号表の生成手順を以下に示す

 

1. シンボルリストをソート

[シンボル,重み] のリストを、あらかじめ重みの昇順になるように並べ替えておく

 

2. ステージの初期化

一番下のステージを除いて、各ステージ上の先読みチェーン 2つを、重みの昇順に並んだ [シンボル,重み] リストの上二つの要素で初期化する。

 

うち、先読みチェーンの右側 (重みの大きいほう) の要素は、

  • シンボルカウンタ ⇒ 2
  • チェーン ⇒ なし  

の状態でそれぞれ設定しておく。

 

f:id:DarkCrowCorvus:20170103191631j:plain

 

なお、一番初めの先読みチェーンの左側 (重みの小さいほう) の要素については、重み以外の情報を扱うことがないため、重み以外はテキトウな設定にしておいて構わない。

 

3. 要素を作成

これから、最下段ステージに要素を作成する次の手順を、2n-2 回だけ繰り返す

 

[シンボル,重み] のリストから次に読み出せる要素の重みと、自身の一つ上の先読みチェーン2つの重みの合計のどちらが小さいかを比較する。

 

リストの要素の重みのほうが小さければ 3.1 の処理、そうではない場合、またはシンボルのリストから読み出せる要素がもうない場合は  3.2 の処理へ進む

 

3.1 [シンボル,重み] リストの要素を追加

[シンボル,重み] のリストから読み出した要素を自身のステージに追加する


要素のシンボルカウンタには、ここまでに同じステージ上で [シンボル,重み] のリストからシンボルを読んだ回数の値を設定する

要素のチェーンは、自身の一つ前の要素がチェーンを持っていれば、そのチェーンをそのまま引き継ぐ

 

f:id:DarkCrowCorvus:20170103191654j:plain

 

遅延 package-merge の時と同様、1番目、2番目に追加される要素は必ず、[シンボル,重み] のリストの1番目、2番目の要素になるため、2. ステージの初期化 で各ステージを初期化する際に、この要素2つをあらかじめ 最下段ステージに作成しておいてもかまわない。 

 

3.2 先読みチェーンのパッケージを追加

一つ上のステージの先読みチェーン2つからパッケージを作成し、自身のステージへ追加する。 

 

要素のシンボルカウンタは、自身の一つ前の要素のシンボルカウンタの値を引き継ぐ

要素のチェーンには、パッケージ作成元の先読みチェーン2つのうち、右側 (重みの大きいほう) の要素の参照を設定する。

 

f:id:DarkCrowCorvus:20170103191736j:plain

 

この後は、遅延 package-merge の時と同様。3.要素を作成 の手順を、更新が必要な先読みチェーンすべてに、新しい要素が補填されきれるまで繰り返す。

 

4. 符号長を抽出

最下段のステージに 2n-2 回、要素を作成する手続きを行ったあと、2n-2 番目に作成される要素が、最下段のステージの 境界の要素 になる。

 

また、その要素がチェーンを持っていれば、そのチェーン先の要素が 一段上のステージの境界の要素となり、それがまたチェーンを持っていれば、そのチェーン先の要素が さらに一段上のステージの境界の要素となる。

 

それぞれの境界の要素が持つ "シンボルカウンタ" には、自身を含めて 自身の左側に出現したシンボル単体の要素の数を保有するため、この数から 前述した方法で、各シンボルの符号長を求めることができる。

 

f:id:DarkCrowCorvus:20170103204520j:plain

 

各シンボルの符号長が求まれば、あとは package-merge algorithm と時と同様、カノニカル・ハフマン符号化の手順を利用して符号表を作成できる

 

利点

境界の要素 よりも左側にある各ステージ上の要素は、それが作成されても、以降扱われることはない。最下段ステージに 2n-2 の要素を作成する過程で、境界の要素が更新されるたびに、それよりも左側にある要素のメモリ空間は、ほかの要素を作成するために使いまわすことができる。

 

この通り、境界 package-merge でも 要素に使ったメモリ空間の再利用が行える。
遅延 package-merge との違いは、プールにあらかじめ確保しておくべき "空き要素" の数が L(L+1) であるということ。 遅延 package-mergeの nL よりも、その数が少なくなることを期待できる

 

※Deflateのカスタムハフマン符号 (シンボル数n=286, 制限符号長L=15) の場合を例とするならば、遅延 package-merge を利用したとき、最大で nL = 4,290 の空き要素が必要であるのに対し、境界パッケージマージを利用した時は、最大でも L(L+1) = 240 の空き要素だけ用意すれば事足りる。

 

遅延 package-merge のパッケージ要素が持つ要素への参照が2つであるのに対し、境界 package-merge の要素が持つ要素への参照 (チェーン) は1つだけ

 

これによって、要素の利用がピークになるとき、同時に生存していないといけない要素の 全体的な数を減らすことができるため、遅延 package-merge と比べて、はじめに準備しておくべき空き要素の数をその分だけ抑えられる

 


サンプル

 

 

ここまでのサンプルプログラム。VC2015でのみ動作確認済み。

 


参考にしたサイトさん

Package-merge algorithm - Wikipedia

"A Fast and Space-Economical Algorithm for Length-Limited Coding Jyrki Katajainen, Alistair Moffat, Andrew Turpin".

sfnt2woff-zopfli/katajainen.c at master · bramstein/sfnt2woff-zopfli · GitHub

PNG イメージを自力でパースしてみる ~2/6 Deflateの基本と固定ハフマン編~

ここまでのあらすじ

PNG イメージを自力でパースしてみる ~1/6 予備知識編~

 

周辺知識に おおかた整理がついてきたところで、いよいよDeflateに手を出してみる

 

なお、今回はパースが目的なので、圧縮を実装するうえで必要になる、詳細なところにまでは首を突っ込まない

 

 


Deflate圧縮

 

改めておさらい

 

Deflate(デフレート)とはLZ77とハフマン符号化を組み合わせた可逆データ圧縮アルゴリズム

Deflate - Wikipedia  

 

ほんの少しだけ具体的には、つぎのとおりに利用している。

 

  1. 圧縮元データを LZ77 を使って圧縮
  2. LZ77で圧縮したデータを、ハフマン符号化 を使ってさらに圧縮

 

Deflateの ”LZ77” の実装は、

どちらかといえば、LZSS のほうが それに近い。

 

"近い" とはどういうことなのか、

圧縮の考え方は、前の記事で紹介したLZSSとそのまま同じなのだけど、圧縮の結果、出力される各々の要素のフォーマットが、元のLZSSとは異なる。

  

元のLZSSでは、それが "距離" と ”一致した長さ” に置換されたか、識別できるようにするために、要素の先頭に1ビットを付加する方法を使っていた

 

Deflateではこの仕様が変更されている。
各々の要素は以下のいずれかの 値 で出力される。

 

0 ~ 255  : 置換されなかった 0x00 ~0xff までの文字そのまま

256    : ブロックの終端 (後で説明)

257 ~ 285   :  ”一致した長さ” & "距離"

 

仮に 0~285の数値を、固定長のビット並びで取り扱うならば、一応、9ビットだけあれば事足りる ( 0 ~511 まで表現可能)

  

ただしそのままを出力するわけではなく、それをさらにハフマン符号化によって可変長な符号へ置換するため、0~285に置換した結果、出現する値に偏りがあれば、ハフマン符号化の特性によって、普通にLZSS圧縮を行ったときよりも、データが小さくなることを期待できる。

 

Deflate の LZSSでは、スライド窓に同じ文字列を見つけた場合、置換された結果は "一致した長さ" → "距離" の順番になる。

※ 前の記事では "距離" → ”一致した長さ” の順番だった

 

257 ~ 285 の数値は "一致した長さ" を表し、その後ろに "距離" の情報が、連なって記録される。

 


拡張ビット

 

さて、"一致した長さ" は 257 ~ 285 の数字で表される。

 

一方で、Deflateでは ”一致した長さ” には 最小で3、最大で258まで使えると定められている。

 

いや...どう見ても 257 ~ 285 までの 28パターンだけでは 3 から 258 までの数値を表現することはできない。

 

ではどうしているのか...というと、
拡張ビット(extra bit) という方法を使っている。

 

257 ~ 285 までの値にはあらかじめ、それ単体が表す "一致した長さ" の数値と、

拡張ビットの数が定められている。

 

復号時、 257 ~ 285 の値を見つけたら、そのすぐ後ろに付随する拡張ビットを読み出し、

 

値が表す "一致した長さ" のベース値

+ 拡張ビット内の値

この要素があらわす本来の ”一致した長さ”

 

の通りに 本来の "一致した長さ" を求める。これも元の LZSS にはない Deflate独自の仕様っぽい。 

 

f:id:DarkCrowCorvus:20160927220214j:plain

 

257 ~ 285 までの値と、
"一致した長さ" のベース値、拡張ビット数 の対応は 以下の表のとおり 

 

一致した長さ拡張ビット数表現できる範囲
257 3 0 3
258 4 0 4
259 5 0 5
260 6 0 6
261 7 0 7
262 8 0 8
263 9 0 9
264 10 0 10
265 11 1 11 - 12
266 13 1 13 - 14
267 15 1 15 - 16
268 17 1 17 - 18
269 19 2 19 - 22
270 23 2 23 - 27
271 27 2 27 - 30
272 31 2 31 - 34
273 35 3 35 - 42
274 43 3 43 - 50
275 51 3 51 - 58
276 59 3 59 - 66
277 67 4 67 - 82
278 83 4 83 - 98
279 99 4 99 - 114
280 115 4 115 - 130
281 131 5 131 - 162
282 163 5 163 - 194
283 195 5 195 - 226
284 227 5 227 - 257
285 258 0 258

 

最大で258までの "一致した長さ" を扱えるものの、そんな長さで一致する文字列を見つけられる機会は、そう頻繁にはない。

 

一般的によく出現する 小さな "一致した長さ" は、”一致した長さ” のベース値のみによって表現し、大きな "一致した長さ" の表現が必要な場合に、拡張ビットを使うようにすることで、圧縮後のサイズをできる限りコンパクトにすることができる。

 

”一致した長さ” 情報の直後には "距離" の情報が来るのだけど、"距離" の情報も、同じ通り 拡張ビット を利用して記録される。

 

"距離" を表す値には 0 ~ 29 までが扱われる。この値もハフマン符号化されているため、元の値に復号したのち、それに必要な拡張ビットを読み込んで、本来の "距離" を求める。

 

0 ~ 29 の値と、
"距離" のベース値、拡張ビット数 の対応は 以下の表のとおり

 

距離

拡張ビット数表現できる範囲
0 1 0 1
1 2 0 2
2 3 0 3
3 4 0 4
4 5 1 5 - 6
5 7 1 7 - 8
6 9 2 9 - 12
7 13 2 13 - 16
8 17 3 17 - 24
9 25 3 25 - 32
10 33 4 33 - 48
11 49 4 49 - 64
12 65 5 65 - 96
13 97 5 97 - 128
14 129 6 129 - 192
15 193 6 193 - 256
16 257 7 257 - 384
17 385 7 385 - 512
18 513 8 513 - 768
19 769 8 769 - 1024
20 1025 9 1025 - 1536
21 1537 9 1537 - 2048
22 2049 10 2049 - 3072
23 3073 10 3073 - 4096
24 4097 11 4097 - 6144
25 6145 11 6145 - 8192
26 8193 12 8193 - 12288
27 12289 12 12289 - 16384
28 16385 13 16385 - 24576
29 24577 13 24577 - 32768

 

Deflateの "距離" の情報には 最大で 32,768 までが扱える。
これは前の記事の LZ77の項でも紹介した

 

まとめると、257 ~ 285 の値に遭遇したとき、
"一致した長さ" と "距離" の本来の値を復元する手順は次の通り

 

  1.  257 ~ 285 の値に遭遇
  2.  必要な追加ビットを読み出し、本来の "一致した長さ" を算出する
  3.  0 ~ 29 を表す符号を読み出す
  4.  必要な追加ビットを読み出し、本来の "距離" を算出する

 


読み書き方向

 

コンピュータのメモリは バイト(Byte) という単位で区切られている。

1バイト(Byte) は 8ビット(bit) から成るのが一般的。

 

メモリ上の各バイトには、それぞれを識別するためのアドレス という番号が割り振られている。また、バイト内のビットにもそれぞれ番号が振られている。

 

f:id:DarkCrowCorvus:20161001163300j:plain

 

これを踏まえて、Deflateで圧縮されたデータは、メモリに置かれるとき、先頭から、アドレスが小さい順の、ビット番号が小さい順 に並べられる。

 

f:id:DarkCrowCorvus:20161003230036j:plain

 

また、Deflate圧縮データの中で複数ビットを使って表されるような要素は、それが何であるかによって、各々のビットの並びかたが異なる。

 

ハフマン符号

ハフマン符号は、先頭から1ビットずつ順番に読み出す必要があるため、符号の先頭(最上位ビット)から順番にパックされる。メモリ上に配置された場合、次の通りに並ぶ

 

f:id:DarkCrowCorvus:20161001175759j:plain

 

それ以外

一方で、符号ではない、それそのままが 何らかの "数" を表すものは一番小さい桁のビット(最下位ビット)から順番にパックされる。メモリ上に配置された場合、次の通りに並ぶ 

 

f:id:DarkCrowCorvus:20161001180934j:plain

 

"符号ではないそれ以外" というのは例えば、拡張ビットや ブロックタイプ(後で説明)などの要素がこれにあたる。

 

Deflateの解凍のプログラムを作るとき、要素のビットが並ぶ方向 に注意すること。さもなければ、

 

なんか入っている数がおかしい ! 読み出すビット数はあっているはずなのに!

 

なんてことが起こる(起こった)

 


ブロック

 

実際に Deflate圧縮されたデータは、ひとつ、もしくは複数の ブロック の集合で構成されている。

 

複数ブロックから構成されている場合は、解凍時、各々のブロックを復号した結果を、先頭から順番に連結することで、圧縮前のデータを復元できる

 

ブロック別に、圧縮のルールが定められている。主には次の3パターン

 

  • 無圧縮
  • 固定ハフマン
  • カスタムハフマン

 

このうち、上で説明した LZ77とハフマン符号を使って圧縮 を実際に行うのは、固定ハフマンブロックと、カスタムハフマンブロック になる。これらについては、この次の項からひとつずつ解説してみる。 

 

各ブロックの先頭には、3ビットからなる ヘッダー情報が付加されている。

 

  • 最初の1ビット: BFINAL (最終ブロック判定ビット)
  • 次の2ビット:  BTYPE(ブロックタイプ)

 

BFINAL には 自身のブロックがDeflate圧縮データ中で

一番最後のブロックである場合には 1
ほかのブロックが後に続く場合には 0 が設定される

 

BTYPE は 自身のブロックの圧縮のルールを示す。
設定されるパターンとそれに対応する圧縮のルールは以下の通り

 

  • 00 無圧縮
  • 01 固定ハフマン
  • 10 カスタムハフマン
  • 11 エラー (予約域)

 

ここまでを踏まえて、Deflate圧縮データの解凍は、大まかには次の流れで行う

 

  1. ヘッダー情報を読み出す
  2. BTYPE の圧縮のルールを確認し、その後ろに続く圧縮データを復号
  3. BFINAL の値が 0なら繰り返し。1 なら終了。 

 

ブロックの終端

 

固定ハフマンブロック と カスタムハフマンブロックでは 256 を表す符号を読み出せた地点が、そのブロックの終端になる。

 

無圧縮ブロックの場合は、ブロックの先頭に無圧縮データの長さ情報が、追加で付加されるため、その分のデータを最後まで読み切った地点が、そのブロックの終端になる。

 

符号表とスライド窓 

 

ハフマン符号の符号表は、各々のブロックごとに用意される。

 

一方、LZ77のスライド窓は、前のブロックで出現した文字列を、次のブロックにも引き継ぐことができる。

 


固定ハフマン符号化 (BTYPE:01)

 

ここから、各ブロックの圧縮ルールについて紹介

 

固定ハフマン では、あらかじめ用意された符号表に従って、データの符号化と復号を行う。符号表が事前に定められているため、圧縮データの中に符号表は埋め込まれない。

 

文字 または 一致した長さ を表す、0 ~285 までを収める符号表と、距離 を表す、0 ~ 29 までを収める符号表は、それぞれ別に定義される。

 

文字と一致した長さの符号表は、次の通りに定義される

 

ビット数符号
0 - 143 8 00110000 - 10111111
144 - 255 9 110010000 - 111111111
256 - 279 7 0000000 - 0010111
280 - 287 8 11000000 - 11000111

 

表の符号間の値は連番になっている。

 

よく見ると 286 と 287 が符号表に含まれることになっている。ただし、この値が圧縮データ中に出現することはない

 

あくまで使用されるのは 0 ~ 285まで。符号のキリがいいから、符号表には含まれたのかもしれない

 

距離  の符号表は、次の通りに定義される

 

ビット数符号
0 - 31 5 00000 - 11111

 

すべての符号が 5ビットの固定長で、0からの連番になっているため、5ビットの長さで読み出した符号そのままを 0 ~ 29 を表す値としてみなすことができる。

 

ただし、あくまで扱いは "数値" ではなく "符号" なので、5ビットいっぺんに読み出す場合は、それが 符号のビット並びに なっていることに注意すること。

 

ここでも符号のキリがいいからか、 30 と 31 が符号表に含まれる。例によってこの値も、圧縮データ中に出現することはない

 

固定ハフマンブロックは、 符号表を圧縮データ中に含まないため、その分だけ 圧縮データのサイズを節約できる。

 

ASCII文字からなるテキストなど、主に144 - 255 の範囲の文字が出現しにくいデータの圧縮に向く。ただし圧縮効果は、それほどよろしくはないとのこと

 

そもそもデータ中の文字の出現率によらず、あらかじめ用意された符号表を使うこのアルゴリズムを"ハフマン符号" と呼んでもいいのかどうかが 自分の中でちょっと疑問だったりする...

  


サンプル 

 

ここまでのサンプルプログラム
VC2015でのみ動作確認済み。

 


参考にしたサイトさん 

 
RFC 1951 DEFLATE Compressed Data Format Specification version 1.3 日本語訳 - futomi's CGI Cafe

Deflate

デフレート圧縮(LZ77圧縮)処理の概要 - ウェブで用いられる画像形式。

SWFバイナリ編集のススメ番外編 (zlib 伸張) 前編 | GREE Engineers' Blog

A guide to PNG optimization

PNG イメージを自力でパースしてみる ~1/6 予備知識編~

  

DirectX12でなにか作りたいと思って、じゃあとりあえず、まずはその辺のサンプルを読み漁ろうと、上のサイトさんからサンプルプログラムを拝借していろいろ見ていたとき、

 

f:id:DarkCrowCorvus:20160919105439j:plain

"HelloTexture" 

テクスチャを画面に表示させるサンプルを見つけて、たぶんここで画像ファイルをロードする機能とか、紹介してるんじゃないかな.. と思ってたのだけど、どうやら違った。

 

 

代わりにテキトウなイメージを実行中に作っていた様子。画像ファイルをロードするためには、自身でそのまわり、なにかしらの準備をしないといけないみたい。

 

どこかのライブラリを拝借してきてもよかったものの、自身の勉強になるかと思い、じゃあ自作しましょうと、

 

自身が比較的よく使ってたフォーマットがPNGだから、じゃあPNGのパーサーを作ってみようかなと―――..

 

ということで、タイトルの通りに至る。

 

 


泥沼のはじまり 

  

 

解説してるサイトさんとか探せばすぐに終わるかな...

なんて思ってたけど、そうも甘くなかった。 PNGが圧縮を使用していて..ということまでは知ってたけど 、実際にその正体が何でどんな圧縮方法なのかまでは知らない。

 

C++の標準で ”デフレート” なんて機能は提供されていない様子。後で調べたら linuxだとそれに相当するライブラリがデフォルトで使えるらしい、ただ自分のPCはwinだしなぁ..

 

ないなら作ればいいじゃない

いや...標準とか、デフォルトで提供されていないだけで、
google先生を使えば、そこらへんにライブラリが転がってたりはするのだけど...

 

中途半端に他人のライブラリを使いたくないだとかなんだとか...いつもの自前主義精神(?) がはたらいてしまって、結局自身で作ってみるに至る。

 

じゃあとりあえず ”デフレート” に関して調べてみる。よく見た回答は、wiki先生から言葉を拝借すると次の通り

 

Deflate(デフレート)とはLZ77とハフマン符号化を組み合わせた可逆データ圧縮アルゴリズム

Deflate - Wikipedia  

 

"可逆圧縮" なら知ってる、でもそのほかがよくわからない。"ハフマン符号化" は言葉だけ、どこかで聞いたことあるなぁ...ってレベル。

 

まずはその辺、調べてみるところから始めた。
調べたことを自分なりに整理して、以下に書き並べてみる。

 

以上。前置き終わり。 

 


ハフマン符号化

 

データ圧縮アルゴリズムの一つ。

 

ハフマン符号化では、圧縮したいデータに含まれている各々の文字を、それぞれ別の ビット並び に置き換える、ということを行う

 

以降、この置き換えたビット並びのことを 符号 とよぶ。

 

この符号化のとき 元のデータの中で出現回数の多い文字にほど
短い符号を割り当てることで、全体的なデータサイズを減らそう
というのがハフマン符号化の基本的な圧縮の仕組み

 

f:id:DarkCrowCorvus:20160919193814j:plain

  

同じ考え方の圧縮方法は、これ以外にもいくつかの種類があるのだけど、ハフマン符号化はその中でも、符号化後のデータに含まれる符号の長さの平均が最小になるのだとか。

 

ハフマン符号化によって割り当てられた符号は、そのビット並びが、他のどの符号の頭の部分とも一致しないという特性を持つ。

 

この特性のことを 語頭条件 とよび、
語頭条件の特性を持つ符号は 接頭符号(Prefix code) とよばれる。

 

この通りに符号化することで、元のデータに戻すとき、その符号と同じビット並びが読み出せれば、それより後ろのビット並びに依存することなく、元の文字へ一意に復号することができるようになる。

 

この特性は 瞬時復号可能 と呼ばれる。

 

f:id:DarkCrowCorvus:20160920135531j:plain

 

ハフマン符号は 接頭符号 であり、瞬時復号可能 な特性を持つ。
ただ単純に短い符号を割り当てればいい、っていうわけではないみたい。 

 


カノニカル・ハフマン符号化(ハフマン符号の正規化)

 

ハフマン符号化でデータサイズを小さくできても、元の文字との対応がわからなければ、送った先でデータを復元することができない。

そのため圧縮したデータに、符号表を埋め込む必要があるのだけど、ここでの 符号 という存在が わりと厄介。

 

符号 はそれと対応する文字ごとに、長さの異なる ”可変長” なデータなのだけど、データを復元する側では、各々の 符号 の情報を抽出するとき、何ビット分だけデータを読めばいいのか、わかっていないといけない。

 

f:id:DarkCrowCorvus:20160922140110j:plain

 

パッと思いつく方法は 例えば上の通り、いっしょに 符号の長さ情報を持たせるとか、なのだけど、長さの情報を含むことによって、符号表全体のサイズが膨らんでしまう。

 

また、符号自体のサイズが大きくなりうる、というのも問題。
0x00 ~ 0xFF まで 256種類すべての文字に符号を割り当てる場合、一番長い符号は、最悪で255ビットにもなる(圧縮しないままの文字32コ分ぐらい)

 

※ここでは本来、アルファベットなどの文字として表すことのできない バイナリデータ中の8ビット並びも "文字" として扱うことにする。

 

そんなに大きな符号が生成されることはめったにないらしいのだけど、万が一、そんな符号があると、符号表はその分だけ大きくなる、

 

符号表のサイズが膨らむと、それだけ全体のサイズも膨らんでしまうため、

符号化したら、前よりもデータサイズが増えたんだけど!?

なんてことが起こりうる。

 

こんなことじゃハフマン符号化を利用する意味がなくなってしまう。データの圧縮効果を上げるためには、この符号表をできる限り小さく保つ必要がある。

 

そこで カノニカル・ハフマン という手法を利用する。

 

これを利用すると、符号表のサイズを小さくするうえで悩みの種である 符号 を、

そのまま符号表の中に納めなくても済むようになる。

代わりに符号表には、各文字に対応する符号の 長さの情報だけ を納める。

 カノニカル・ハフマン符号化 による 符号化の手順は以下の通り。

 

f:id:DarkCrowCorvus:20160921213600j:plain

  1. 一度ふつうにハフマン符号化された符号を、
    符号の長さ別 にグループ分けして、
    そのグループ内で 文字番号の昇順 になるように並べる

  2. 符号の長さが短く、文字番号の小さい文字ほど、
    小さな符号になるように符号を割り当てなおす

  3. 割り当てなおした符号表を使ってデータを符号化(圧縮)

  4. 符号の長さだけを納めた表を、符号化したデータの前に付加する

 

ポイントは、ある符号長グループへの 符号の再割り当てが終わって、次の符号長のグループに移るとき。

 

ひとつ前の符号長グループで一番最後に割り当てた符号に +1 したあと、その後ろに 0 ビットを付加したものを、次の符号長グループの再割り当てに利用する、ということ

 

この通りにすることで、再割り当てされた符号は 接頭符号 になり、瞬時復号可能な特性を持つようになる。

 

f:id:DarkCrowCorvus:20160922140126j:plain

 

一方で、復号の手順は以下の通り。

 

f:id:DarkCrowCorvus:20160922143103j:plain

  1. 符号の長さ表を読み出した後、
    それを 符号の長さ別 にグループ分けして、
    そのグループ内で 文字番号の昇順 になるように並べる

  2. 符号の長さが短く、文字番号の小さい文字ほど、
    小さな符号になるように符号を割り当て、符号表を復元する

  3. 得られた符号表を使ってデータを復号する

 


LZ77符号化

 

データ圧縮アルゴリズムの一つ
Lempel さんと Ziv さんが 1977年に作ったからこんな名前

 

データ中の ある文字列が、それよりも以前に同じ形で現れているならば、文字列をその "位置" と "一致した長さ" という情報に置き換えてしまうことで、全体的なデータサイズを減らそう、というのが LZ77の大まかな圧縮の仕組み

 

"位置" と "一致した長さ" に置き換えるために、”以前に同じ形があるか” を検索できる範囲は有限で、直前に読みだせた文字から、何文字前まで...というのが事前に決められている。

 

これは 検索にかかる時間や 使用メモリ量、置換後の "位置" を表す数値が、巨大になり過ぎないように制限するため。

 

たとえばDeflateなら、直前に読み出した文字から、最大で 32,768文字前までが検索範囲。

 

新しい文字を読み出すたびに、検索できる範囲がスライドするため、この範囲のことを スライド窓(Sliding Window) と呼んだりする。

 

LZ77では、圧縮対象のデータを 先頭から順番に読み込み、

そのときに現れた文字、または文字列を

(位置 , 一致した長さ , 直近の不一致文字)

というフォーマットに置き換えることでデータの圧縮を行う。

 

f:id:DarkCrowCorvus:20160923103358j:plain

 

圧縮元のデータが大きいほど、スライド窓の中に同じ文字列があるという機会が増えるため、圧縮効率が良くなるのだけど、

 

上の例だと、対象のデータが短すぎるせいか、逆にサイズが増えてしまってる。

なんてこというと、ハフマン符号の時に提示した例も、圧縮後のデータに符号表を含めると、元のデータよりもサイズが増えるのだけど...

 

LZ77では何が何でも(位置, 一致した長さ, 直近の不一致文字)というフォーマットへ置き換えようとするため、スライド窓に同じ文字列が見つからなかった場合は、むしろ置換前よりもデータが大きくなってしまう という欠点がある。

 

LZ77には、それを基本形とするさまざまな亜種が存在する。 

 


LZSS符号化 

 

データ圧縮アルゴリズムでLZ77の亜種のうちの一つ。
StorerさんとSzymanskiさんが改良を行ったからこんな名前。

 

現在 普及している圧縮プログラムで LZ77 符号と呼ばれているもののほとんどは、じつは LZSS 符号になっている。とのこと ※1

 

実際、”LZ77を利用している” と言っていたDeflateも、その実装は、どちらかといえばLZSSのものに近い。

 

LZSS ではスライド窓の中を検索して、同じ文字列が存在し、さらに置換することで圧縮効果が得られる場合に限ってそれを "位置" と "一致した長さ" の情報に置き換る。それ以外の場合は置き換えを行わない。

 

この通りに作られた圧縮データは、復号のとき先頭から順番に読み出されたそれが  "位置"と"一致した長さ"  を表すのか、置換されなかった文字そのままを表すのかを判断できなければ、データを正しく復元することができない。

 

これの解決には、記録する各々の要素の先頭に 1ビットを付加する、という方法を用いる。

 

これによって 復号時には、各々の要素ごとに先頭の 1ビットを読んで、
それが "1" ならばその直後に続くデータは "位置" と "一致した長さ"
"0" ならば1文字そのまま..という具合に識別することができる。

  

f:id:DarkCrowCorvus:20160923183340j:plain

 

識別のため、各々の要素に それぞれ 1ビットずつ付与されるため、"位置" と ”一致した長さ” の情報に 置換が行われなかった文字は、圧縮前よりも 1ビット分だけ大きくなるのだけど、

 

それでもLZ77と比べると、スライド窓に同じ文字列が見つからなかったときにデータが膨らんでしまう規模を、だいぶん抑えることができる。

 


参考にしたサイトさん

 

全般

Deflate

 

ハフマン符号

データ圧縮の基礎『ハフマン符号化』の仕組みを見てみよう - 道すがら講堂

圧縮アルゴリズム (3) ハフマン符号化 - 静的ハフマン圧縮

圧縮アルゴリズム(ハフマン符号) - UUUM攻殻機動隊

瞬時復号可能な符号 — Computer Science Textbook (under construction)

 

カノニカル・ハフマン符号化

圧縮アルゴリズム (4) ハフマン符号化 - 適応型ハフマン圧縮

Canonical Huffman code - Wikipedia, the free encyclopedia

 

LZ77

デフレート圧縮(LZ77圧縮)処理の概要 - ウェブで用いられる画像形式。 

Data Compression/data differencing - Wikibooks, open books for an open world

Lempel-Ziv-77 (LZ77)

 

LZSS

Algorithms with Python / LZ77 符号 (LZSS 符号) ※1

Lempel-Ziv-Storer-Szymanski (LZSS)