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.