[C#] zlib形式圧縮の実装

  • このエントリーをはてなブックマークに追加

先日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 」∠)

それにしてもDeflateStreamのコンストラクタにCompressionLevel指定しなかったらどうなるのか謎…

  • このエントリーをはてなブックマークに追加

SNSでもご購読できます。




コメント

  1. たかおファン(surface0) より:

    おや、何かのお役に立てましたかな( ^ω^)

  2. 匿名 より:

    ありがとう。
    ありがとう…!

コメントを残す