C#: Attaching ZLIB header/trailer to DeflateStream

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
However, this doesn't matter too much - the stream can be decompressed even if you have completely misled the program about your compression level. Per spec, the compression level in header is more for "is this compressed / could this be compressed better?" estimates.

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
ZLIB trailer/footer is an Adler-32 checksum of the uncompressed data. There is not an Adler-32 implementation in .NET, but it is among the easier algorithms to implement.

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:
		switch (level) {
			case CompressionLevel.NoCompression:
			case CompressionLevel.Fastest:
			case CompressionLevel.Optimal:
		// 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();
	var dflStream = new cs.system.io.compression.DeflateStream(memStream, Compress);
	dflStream.Write(bytes.getData(), 0, bytes.length);
	var outData = memStream.ToArray();
	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.


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.

Related posts:

One thought on “C#: Attaching ZLIB header/trailer to DeflateStream

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.