Testing zlib compression in Delphi

Delphi has for a while contained two classes that simplify stream compression:

  • TZCompressionStream
  • TZDecompressionStream

I had not used them before and when I tried to use them now to compress and decompress some binary data that is stored as part of a large file, I got some inexplicable results. So I wrote this little test program to find the problem. It does not use TFileStreams like the example in the documentation, but TMemoryStreams instead. (Has anybody ever tested that example? If yes, why didn’t you fix the bug in it? ChangeFileExt requires the file extension to include the leading dot!)

As for my actual problem, it turned out that I didn’t set the position of the input stream to 0, so the TZDecompressionStream started to read from the end and of course returned an empty stream.

Another important remark on this:

The code uses …

    OutStream.CopyFrom(DecompressionStream, 0);

… to copy the content of the compressed input stream to a decompressed output stream. This works fine, but there is a performance penalty involved here. Passing 0 as the size makes CopyFrom get the size of the input stream (the DecompressionStream) and then uses the size to read everything from it. Unfortunately TZDecompressionStream.Size can only determine the size of the decompressed data by actually decompressing it completely (at least that’s how I understand the code). This means that the code above will decompress the data twice, first to determine the size and then the second time to fill OutStream.

Another remark on the example in the documentation: While zlib compression uses the same (deflate) compression algorithm as ZIP does, the result is just a binary stream of the compressed data. It lacks the the metadata, e.g. the additional directory of file names, sizes and offsets stored at the end of a ZIP archive. So naming the compressed stream something.zip is not a good idea. No program that is capable of handling ZIP archives will be able to open it.

So here goes my testing code, just in case I need it again at a later time. If you find it helpful, you are welcome to use it. Btw. It tests both, zlib (zip) compression / decompression and gzip compression / decompression.

program ZLibTest;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Classes,
  System.zlib;

procedure CompressTest(_UseGZip: Boolean; const _InputText: AnsiString; out _OutStream: TMemoryStream);
var
  InStream: TMemoryStream;
  CompressionStream: TZCompressionStream;
  WindowBits: Integer;
begin
  WindowBits := 15;
  if _UseGZip then
    WindowBits := WindowBits + 16;

  CompressionStream := nil;
  InStream := TMemoryStream.Create();
  try
    InStream.WriteBuffer(_InputText[1], Length(_InputText));
    InStream.Position := 0;

    _OutStream := TMemoryStream.Create();
    CompressionStream := TZCompressionStream.Create(_OutStream, zcDefault, WindowBits);

    CompressionStream.CopyFrom(InStream, InStream.Size);
  finally
    CompressionStream.Free;
    InStream.Free;
  end;
end;

procedure DecompressTest(_UseGZip: Boolean; _InStream: TMemoryStream; out _OutputText: AnsiString);
var
  OutStream: TMemoryStream;
  DecompressionStream: TZDecompressionStream;
  WindowBits: Integer;
begin
  WindowBits := 15;
  if _UseGZip then
    WindowBits := WindowBits + 16;

  DecompressionStream := nil;
  OutStream := TMemoryStream.Create();
  try
    _InStream.Position := 0;
    DecompressionStream := TZDecompressionStream.Create(_InStream, WindowBits);

    OutStream.CopyFrom(DecompressionStream, 0);

    OutStream.Position := 0;
    SetLength(_OutputText, OutStream.Size);
    OutStream.Read(_OutputText[1], OutStream.Size);
  finally
    DecompressionStream.Free;
    OutStream.Free;
  end;
end;

procedure RunTest(_UseGZip: Boolean; const _TestText: AnsiString);
var
  ms: TMemoryStream;
  OutputText: AnsiString;
begin
  CompressTest(_UseGZip, _TestText, ms);
  try
    WriteLn('Input text of lenght ', Length(_TestText), ' was compressed to ', ms.Size, ' bytes');
    DecompressTest(_UseGZip, ms, OutputText);
    if OutputText <> _TestText then
      WriteLn('Test failed, output does not match input')
    else
      WriteLn('Test succeeded. Output matches input');
  finally
    FreeAndNil(ms);
  end;
end;

procedure Test;
const
  TestText: AnsiString = 'this is a test text with some more input,'
    + ' so the compression actually compresses it'
    + ' and does not increase the size of it';
begin
  WriteLn('Testing zlib compression');
  RunTest(False, TestText);
  WriteLn('Testing gzip compression');
  RunTest(True, TestText);
end;

begin
  try
    Test;
  except
    on E: Exception do
      WriteLn(E.ClassName, ': ', E.Message);
  end;
  WriteLn('Press Enter');
  ReadLn;
end.

Discussion about this blog post in the international Delphi Praxis forum.