A small post about using System.IO.Compression.DeflateStream
for compressing data in C# (and Haxe-C#).
The idea
Per RFC 1950, a ZLIB stream has a two-byte header.
The first byte (CMF) defines compression method and flags (in case of "deflate", the window size).
The second byte (FLG) contains a 5-bit checksum of the two bytes, a bit indicating whether to use a preset dictionary (which would then follow the header), and two bits to indicate the compression level.
The ten API-side compression levels map to 4 header-side compression levels as following (assuming the default ):
Compression level | CMF | FLG |
---|---|---|
0 | 78 | 01 |
1 | 78 | 01 |
2 | 78 | 5E |
3 | 78 | 5E |
4 | 78 | 5E |
5 | 78 | 5E |
6 | 78 | 9C |
7 | 78 | DA |
8 | 78 | DA |
9 | 78 | DA |
DeflateStream
has just 3 (default/unspecified likely being same as Optimal) modes total so that's easier to map:
CompressionLevel | CMF | FLG |
---|---|---|
NoCompression | 78 | 01 |
Fastest | 78 | 01 |
(default) | 78 | 9C |
Optimal | 78 | DA |
The code
There's not a lot to it:
// uses System.IO, System.IO.Compression public static byte[] Deflate(byte[] data, CompressionLevel? level = null) { byte[] newData; using (var memStream = new MemoryStream()) { // write header: memStream.WriteByte(0x78); switch (level) { case CompressionLevel.NoCompression: case CompressionLevel.Fastest: memStream.WriteByte(0x01); break; case CompressionLevel.Optimal: memStream.WriteByte(0xDA); break; default: memStream.WriteByte(0x9C); break; } // write compressed data (with Deflate headers): using (var dflStream = level.HasValue ? new DeflateStream(memStream, level.Value) : new DeflateStream(memStream, CompressionMode.Compress )) dflStream.Write(data, 0, data.Length); // newData = memStream.ToArray(); } // compute Adler-32: uint a1 = 1, a2 = 0; foreach (byte b in data) { a1 = (a1 + b) % 65521; a2 = (a2 + a1) % 65521; } // append the checksum-trailer: var adlerPos = newData.Length; Array.Resize(ref newData, adlerPos + 4); newData[adlerPos] = (byte)(a2 >> 8); newData[adlerPos + 1] = (byte)a2; newData[adlerPos + 2] = (byte)(a1 >> 8); newData[adlerPos + 3] = (byte)a1; return newData; }
A slight wiggle with using
here is due to fact that you need to close a DeflateStream
for it to
write bytes to the associated stream, but doing so also closes the associated stream, meaning that
you can't just write the extra 4 bytes there afterwards.
There's probably a better way of doing this.
Bonus: same code but for Haxe-C# (format.tools.Deflate
is not supported as of writing this):
static function deflate(bytes:Bytes):Bytes { var memStream = new cs.system.io.MemoryStream(); memStream.WriteByte(0x78); memStream.WriteByte(0x9C); // var dflStream = new cs.system.io.compression.DeflateStream(memStream, Compress); dflStream.Write(bytes.getData(), 0, bytes.length); dflStream.Dispose(); // var outData = memStream.ToArray(); memStream.Dispose(); var adlerPos = outData.length; cs.system.Array.Resize(outData, adlerPos + 4); // var outBytes = Bytes.ofData(outData); var a32 = haxe.crypto.Adler32.make(bytes); outBytes.set(adlerPos + 3, a32); outBytes.set(adlerPos + 2, a32 >> 8); outBytes.set(adlerPos + 1, a32 >> 16); outBytes.set(adlerPos, a32 >>> 24); // return outBytes; }
There is not a CompressionLevel
in this because the enum is mysteriously amiss from auto-generated externs as of hxcs
4.2.
Example
The following takes test.json
, compresses it with default compression level, and saves it as test.zlib
:
var sourceBytes = File.ReadAllBytes("test.json"); var compressedBytes = Deflate(sourceBytes); File.WriteAllBytes("test.zlib", compressedBytes);
test.zlib
can be subsequently decompressed with compliant ZLIB implementations,
such as inflateInit
..inflateEnd
in C API or buffer_decompress
in GameMaker.
That is all.
Thank you, it helped me a lot. In Asp.net there is no zlib compression function. I had to find a way out.