2017年9月13日水曜日

C#でDeflateをMemoryStreamで

 C#でMemoryStreamを使って(ファイル操作をせずに)Deflateの圧縮をしようと思ったら、結構穴にハマったのでメモ。

byte[] src = new byte[1024],dst;

for(int i = 0; i < src.Length; i++)
{
 src[i] = (byte)(i % 256);
}

using (MemoryStream ms = new MemoryStream())
using (DeflateStream ds = new DeflateStream(ms, CompressionMode.Compress, true))
{
 ds.Write(src, 0, src.Length);
 ds.Dispose();
 ms.Position = 0;

 dst = new byte[ms.Length];
 ms.Read(dst, 0, dst.Length);
}

src = dst;

using (MemoryStream msSrc = new MemoryStream())
using (MemoryStream msDst = new MemoryStream())
using (DeflateStream ds = new DeflateStream(msSrc, CompressionMode.Decompress))
{
 msSrc.Write(src, 0, src.Length);
 msSrc.Position = 0;

 ds.CopyTo(msDst);
 msDst.Position = 0;

 dst = new byte[msDst.Length];
 msDst.Read(dst, 0, dst.Length);
}

 圧縮と展開、両方のソース。

 圧縮はCompressionMode.Compressで行う。
 DeflateStreamのコンストラクタで第3引数を省略するとfalseになるが、この場合は圧縮が終わった時点で第1引数に指定したStreamをCloseする。この動作は、FileStreamを使ったときには問題にならないが、MemoryStreamを使った場合は、あとから読めなくなる。なので、trueを指定してStreamがCloseされないようにする。
 また、DeflateStreamは、StreamがCloseされた際に圧縮を行うらしい。ということで、DeflateStream.Dispose()で強制的に閉じておく。
 そして、StreamのPositionはStreamの最後に移動しているので、あらかじめ0に戻しておく。
 最後に、Stream.Lengthを使って圧縮後の容量を確認し、Stream.Readでメモリに戻す。

 展開はCompressionMode.Decompressで行う。
 この際は読み込みStreamは閉じられてもかまわないので、第3引数は省略可能。
 まずはSrcStreamにデータをコピーする。DeflateStreamは入力StreamのPositionを戻さないので、自分でSrcStream.Position=0にしておく。
 そしてDeflateStream.CopyToでDstStreamにデータを移動する。CopyToやReadでDeflateStreamを読み出した際に、SrcStreamからの展開を行うらしい。
 またCopyToで読み出すと、Positionも移動するので、ここでも0にしておく。
 最後に、DstStreamのLengthで容量を確認し、Readでメモリに戻す。


 とりあえず、上記のサンプルデータだとちゃんと展開はできることを確認している。

 最初、入力に乱数を使っていて、全く圧縮されなく悩んだ。Deflateはハフマン符号圧縮と違い、過去のメッセージと同じデータを探してくるので、乱数のような同じメッセージが複数回出てこないようなデータに対しては圧縮率が極めて低い。
 今回のサンプルのような、同じメッセージが複数回出てくるようなデータに対しては、かなり圧縮率が良い。例えば、今回のサンプルデータは0x00から0xFFまでの256バイトのメッセージが4回出てくる。最初の1回は圧縮できないが、残りの3回は「nバイト前から長さlバイトと同じ」という情報があれば良いので、その分を圧縮できる。今回は1024バイトが280バイトまで圧縮された。

/*
 そう考えると、PNGの圧縮効率ってあんまり高くない気がする。ピクセルデータを直接ハフマン符号圧縮したほうが効率良さそう。当時のコンピュータの処理能力とか、そういう制限だったんだろうなぁ。
 PNGは浮動小数点演算とかいらない分、JPEGよりは組み込み向けといえるのか? どうだろう。JPEGはブロック単体で展開できるはずだけど、PNGを展開するには最大で圧縮窓全体をカバーするメモリ(=32KiB)が必要になりそう。PNGはフォーマットや圧縮方法にバリエーションが多いので、自前でライブラリ書くのは面倒だな。
 最近扱った組み込みの画像を扱う処理だと、マイコン内の処理が8bitIndexedだったので、BMP8bppIndexed限定にした。コレなら読み込みプログラムも凄まじく簡単に書ける。JPEGだとマイコン内で減色アルゴリズムを走らせる必要がある。PNGは8bppIndexedも保存できるけど、大抵のドローソフトでは8bppPNGでは保存できない気がする。結局、リソースの制限された組み込み系では8bppIndexedBMPが楽だと思う。読み込み早いし。プログラム楽だし。大抵のドローソフトで保存できるし。
*/

0 件のコメント:

コメントを投稿