先日PNGエンコーダを自作してみたのですが、なんでかC#もとい.NET FrameworkにはPNGフォーマットに必須のzlib圧縮(RFC1950)が見当たりませんでした。
無いものは作るべし、ということで自作してみました。
正直なところサードパーティ製のライブラリ使えば終わりですが、PNG圧縮のためだけに逆に大げさすぎるのではと思ったので、自力でなんとかしたいと思います。
必要なもの
データ圧縮のコアとなるDeflate(RFC1951)については幸いにも.NET Frameworkに盛り込まれているので、それを使います。
zlib形式ではDeflate圧縮したデータに独自のヘッダーとフッターが必要になります。
+---+---+=====================+---+---+---+---+
|CMF|FLG|...compressed data...| ADLER32 |
+---+---+=====================+---+---+---+---+
ヘッダーは内容を説明する単純なものですが、フッターはAdler-32という32bitのチェックサムになります。
なおプリセット辞書を使う場合はヘッダーと圧縮データの間に辞書情報が挟まるようです。(まず使わないので無視)
ということでAdler-32アルゴリズムを使いたいのですが、これもまた.NET Frameworkにはありませんので自作することにします。
ヘッダーの組み立て
ヘッダーはCM、CINFOを含むCMF(1バイト)とFLEVEL、FDICT、FCHECKを含むFLG(1バイト)の合計2バイトから成ります。
+---------------+---------------+
| C M F | F L G |
+-------+-------+---+-+---------+
| CINFO | CM |Flv|D| FCHECK |
+-------+-------+---+-+---------+
D = FDICT
Flv = FLEVEL
以下Wikipediaからまんま引用。
1バイト目
上位4ビットは圧縮情報であり LZ77 のウィンドウサイズ。7なら32KBのウィンドウサイズ。
下位4ビットが圧縮方式。通常は数値の8。
2バイト目は
上位2ビットは圧縮レベル。デフォルトは2。
6ビット目はプリセット辞書があるかどうか。
下位5ビットがヘッダー2バイト分のチェックビット。
RFCを読む限り、PNGやgzipでは圧縮方式(CM)は8、ウィンドウサイズ(CINFO)は32KBが使われるそうで、最初の1バイトは0x78で良さそうです。
.NETのDeflateはその辺は調整効かないみたいですがそれで大丈夫そうです。
次にFLG。FLEVELはCM=8の場合は以下の定義になっています。
0 – compressor used fastest algorithm
1 – compressor used fast algorithm
2 – compressor used default algorithm
3 – compressor used maximum compression, slowest algorithm
しかし.NETのCompressionLevel
列挙体にはFastest,NoCompression,Optimalという大雑把なものしかありません。。。
というか、FLEVELは展開時には必要なく、ぶっちゃけどうでもいい値らしいです。とりあえずデフォルトの2にしておきましょう。
FDICTは単一ビットのフラグ値ですが、辞書はまず必要ありませんので0にします。
最後のFCHECKはヘッダー内容が可変の場合は都度生成する必要がありますが、今回のケースでは不変な為、固定値で対応します。
ヘッダー2バイトをビッグエンディアンの16ビットの符号なし整数とみなし、それが31の倍数となるように調整します。
つまり以下の式が成り立つようにします。
(CMF * 256 + FLG) mod 31 == 0
これからFCHECKを計算すると。
FCHECK = 0x1F - 0x7890 mod 0x1F = 0x0C
ということで最終的なヘッダーは『78 9C』となります。
Adler-32の実装
アルゴリズムの説明はRFC1950にプログラムコード付きで載っているので割愛します。
細かい説明もWikipediaを参照すれば良いと思います。
今回はHashAlgorithm
を継承して実装しました。
using System.Security.Cryptography;
namespace Sample
{
public class Adler32 : HashAlgorithm
{
private const uint BASE = 65521; /* largest prime smaller than 65536 */
private uint s1;
private uint s2;
public Adler32()
{
base.HashSizeValue = 32;
Initialize();
}
protected override void HashCore(byte[] array, int ibStart, int cbSize)
{
while (--cbSize >= 0) {
s1 = (s1 + array[ibStart++]) % BASE;
s2 = (s2 + s1) % BASE;
}
}
protected override byte[] HashFinal()
{
HashValue = new byte[] {
(byte)((s2 >> 8) & 0xFF),
(byte)( s2 & 0xFF),
(byte)((s1 >> 8) & 0xFF),
(byte)( s1 & 0xFF)
};
return HashValue;
}
public override void Initialize()
{
s1 = 1;
s2 = 0;
}
}
}
zCompress関数作成
PHPだとなぜかRFC1950相当の圧縮関数がgzcompress
とかいう名前になっているんですが、それを真似するのはなんかぱっとしないので(gzipとは別だし!)、zCompress
と名付けることにしました。
ということでDeflateStream
クラスを使ってさくっと実装。
public static byte[] zCompress(byte[] buf)
{
byte[] cbuf;
using (var ms = new MemoryStream())
{
using (var ds = new DeflateStream(ms, CompressionLevel.Optimal))
{
ds.Write(buf, 0, buf.Length);
}
ms.Close();
cbuf = ms.ToArray();
}
using (var ms = new MemoryStream())
{
var zlibHeader = new byte[] { 0x78, 0x9C }; // CMF(CM, CINFO), FLG
ms.Write(zlibHeader, 0, zlibHeader.Length);
ms.Write(cbuf, 0, cbuf.Length);
var adler32 = new Adler32();
var checksum = adler32.ComputeHash(buf);
ms.Write(checksum, 0, checksum.Length);
ms.Close();
return ms.ToArray();
}
}
以上です!
ご意見ありましたらどうぞ。
クラス2個も作って、こっちの方が大げさじゃねーか!って突っ込みはなしでどうぞ(:3 」∠)
コメント
おや、何かのお役に立てましたかな( ^ω^)
ありがとう。
ありがとう…!