ZLib: Compactando dados em tempo real…

ZLIB é uma biblioteca de compressão e decompressão de dados, de forma lossless (sem perdas) padronizada. Está, inclusive, listada nas RFCs 1950 e 1951. É a biblioteca usada no padrão ZIP e GZIP (duas estruturas de arquivos diferentes, que usam o mesmo método de compressão!). O legal de ZLIB é que ele pode compactar qualquer tipo de dados, inclusive dados localizados em memória… e é bem simples de usar.

Essencialmente, para compactar, temos que informar o ponteiro do buffer de entrada e seu tamanho e chamar a função deflate() tantas vezes quantas forem necessárias para obter todos os bytes compactados num buffer de saída… Para decompactar o processo é inverso: Fornecemos o buffer de entrada, com os dados compactados, e chamamos inflate() tantas vezes quantas forem necessárias para obtermos, no buffer de saída, os dados descompactados.

Repare que os dois processos falam por sí: Para compactar temos que “desinflar” os dados. Para descompactar, temos que “inflá-los”. Abaixo temos um código de exemplo de um compactador:

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <zlib.h>

/* Vamos usar dois buffers de 8 kB aqui. */
#define MAX_CHUNK_SIZE  16384

int main(int argc, char *argv[])
{
  jmp_buf jb;
  FILE *fin, *fout;
  z_stream zstrm;
  static char chunk[MAX_CHUNK_SIZE];
  static char zbuff[MAX_CHUNK_SIZE];
  int flush, size;

  if (argc != 3)
  {
    fprintf(stderr, "Uso: compress <infile> <outfile>\n");
    return 1;
  }

  if ((fin = fopen(argv[1], "rb")) == NULL)
  {
    fprintf(stderr, "Erro abrindo arquivo de entrada.\n");
    return 1;
  }

  if ((fout = fopen(argv[2], "wb")) == NULL)
  {
    fclose(fin);
    fprintf(stderr, "Erro criando arquivo de saída.\n");
    return 1;
  }

  /* Tratamento de erro. */
  if (setjmp(jb))
  {
    /* Libera os recursos internos da zlib. */
    deflateEnd(&zstrm);

    /* Fecha os arquivos e livra-se do arquivo de saída. */
    fclose(fin);
    fclose(fout);
    unlink(argv[2]);
    return 1;
  }

  /* Usa os algorítmos internos para alocação, liberação e 
     buffers adicionais. */
  zstrm.zalloc = Z_NULL;
  zstrm.zfree = Z_NULL;
  zstrm.opaque = Z_NULL;

  /* Inicializa zlib para compressão (deflate) */
  if (deflateInit(&zstrm, Z_BEST_COMPRESSION) != Z_OK)
  {
    fprintf(stderr, "Erro inicializando zlib.\n");
    longjmp(jb, 1);
  }

  do
  {
    /* 'avail_in' é a quantidade de bytes a serem compactados.
       'next_in' é o ponteiro para o buffer contendo os bytes a 
         serem comprimidos. */
    zstrm.avail_in = fread(chunk, 1, MAX_CHUNK_SIZE, fin);

    if (ferror(fin))
    {
      fprintf(stderr, "Erro lendo arquivo de entrada.\n");
      longjmp(jb, 1);
    }

    /* Deflate precisa fazer 'flushing' no bloco final. */
    flush = feof(fin) ? Z_FINISH : Z_NO_FLUSH;
    zstrm.next_in = chunk;

    do
    {
      /* 'avail_out' é o tamanho do buffer de saída ainda
         disponível (atualizado por deflate()).
         'next_out' é o ponteiro para esse buffer. */
      zstrm.avail_out = MAX_CHUNK_SIZE;
      zstrm.next_out = zbuff;

      /* Compacta, retorna Z_OK em caso de sucesso. */
      if (deflate(&zstrm, flush) == Z_STREAM_ERROR)
      {
        fprintf(stderr, "Erro compactaando dados.\n");
        longjmp(jb, 1);
      }

      /* O buffer de saída pode ainda não estar cheio! */
      size = MAX_CHUNK_SIZE - zstrm.avail_out;

      if (fwrite(zbuff, 1, size, fout) != size || ferror(fout))
      {
        fprintf(stderr, "Erro gravando stream compactado.\n");
        longjmp(jb, 1);
      }

      /* Se 'avail_out' for zero, deflate() ainda tem
         dados para colocar no buffer de saída... */
    } while (zstrm.avail_out == 0);

    /* Continua, se ainda não acabou... */
  } while (flush != Z_FINISH);

  deflateEnd(&zstrm);
  fclose(fin);
  fclose(fout);

  return 0;
}

Note que, no código acima, lemos pedaços (chunks) do arquivo de entrada, compactando o arquivo pedaço por pedaço. O loop interno (o que chama deflate()) é executado enquanto a função não diz que o buffer de saída está cheio (avail_out == 0)… Essa é uma requisição de deflate(): Se ela retorna Z_OK (ou zero) e avail_out for zero, quer dizer que ainda existem dados no stream para gravar…

Para testar o programinha, podemos compilá-lo e executá-lo contra o próprio código fonte:

$ gcc -O3 -o compress compress.c -lz
$ ./compress compress.c conpress.c.z
$ ls -go compress.c*
-rw-r--r-- 1 2777 Jun 30 16:00 compress.c
-rw-r--r-- 1 1135 Jun 30 16:00 compress.c.z
$ hd compress.c.z | head -n 10
00000000  78 da 95 56 ed 6e da 48  14 fd cf 53 dc 24 d2 c6  |x..V.n.H...S.$..|
00000010  8e 28 69 da 6a 55 95 d0  55 1b 11 05 95 84 15 84  |.(i.jU..U.......|
00000020  d5 2a dd c8 1a ec 71 98  c4 9e a1 33 36 d9 25 ed  |.*....q....36.%.|
00000030  d3 f4 c7 6a 7f e4 29 78  b1 bd f3 61 1b 03 4d 76  |...j..)x...a..Mv|
00000040  a3 08 cc 65 e6 ce b9 e7  9c 7b 87 3d c6 c3 24 8f  |...e.....{.=..$.|
00000050  28 1c ab 2c 62 a2 35 7d  df d8 5b 0d 25 6c b2 16  |(..,b.5}..[.%l..|
00000060  a3 d9 6d 3a ab c7 16 6e  55 e3 f0 00 7e 23 a9 50  |..m:...nU...~#.P|
00000070  90 2b 22 21 12 4c c1 24  8f 63 2a 15 e0 ba b7 70  |.+"!.L.$.c*....p|
00000080  f7 11 c8 97 9c b5 e0 e0  b0 b1 17 d1 98 71 0a e7  |.............q..|
00000090  1f 7e 0f 4e ce c6 17 9f  82 51 ef aa 0b 70 f4 f3  |.~.N.....Q...p..|

O arquivo de saída não tem quaisquer informações sobre o nome e outros atributos do arquivo original. A saída não é um arquivo .zip ou .gz. É o stream compactado PURO. Acima listei os resultados… O arquivo compress.c.z tem 40.9% do tamanho do arquivo original e, como pode ser visto, o seu conteúdo é totalmente “maluco” (listei apenas os 160 primeiros bytes).

De forma similar, para decompactar, podemos usar um programinha assim:

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <zlib.h>

/* Vamos usar dois buffers de 8 kB aqui. */
#define MAX_CHUNK_SIZE  16384

int main(int argc, char *argv[])
{
  jmp_buf jb;
  FILE *fin, *fout;
  z_stream zstrm;
  static char chunk[MAX_CHUNK_SIZE];
  static char zbuff[MAX_CHUNK_SIZE];
  int size, ret;

  if (argc != 3)
  {
    fprintf(stderr, "Uso: compress <infile> <outfile>\n");
    return 1;
  }

  if ((fin = fopen(argv[1], "rb")) == NULL)
  {
    fprintf(stderr, "Erro abrindo arquivo de entrada.\n");
    return 1;
  }

  if ((fout = fopen(argv[2], "wb")) == NULL)
  {
    fclose(fin);
    fprintf(stderr, "Erro criando arquivo de saída.\n");
    return 1;
  }

  /* Tratamento de erro. */
  if (setjmp(jb))
  {
    /* Libera os recursos internos da zlib. */
    inflateEnd(&zstrm);

    /* Fecha os arquivos e livra-se do arquivo de saída. */
    fclose(fin);
    fclose(fout);
    unlink(argv[2]);
    return 1;
  }

  /* Usa as rotinas de alocação padrão. */
  zstrm.zalloc = Z_NULL;
  zstrm.zfree = Z_NULL;
  zstrm.opaque = Z_NULL;

  /* Inicializa 'inflate' */
  if (inflateInit(&zstrm) != Z_OK)
  {
    fprintf(stderr, "Erro inicializando zlib para 'inflate'.\n");
    longjmp(jb, 1);
  }

  do
  {
    /* Lê um bloco do arquivo compactado. */
    zstrm.avail_in = fread(zbuff, 1, MAX_CHUNK_SIZE, fin);

    /* Em caso de erro, sai */
    if (ferror(fin))
    {
      fprintf(stderr, "Erro lendo arquivo compactado.\n");
      longjmp(jb, 1);
    }

    /* Se não há mais nada para ler, sai do loop. */
    if (zstrm.avail_in == 0)
      break;

    /* Aponta para o buffer de entrada. */
    zstrm.next_in = zbuff;

    do
    {
      /* Ajusta o buffer de saída e decompacta o bloco. */
      zstrm.avail_out = MAX_CHUNK_SIZE;
      zstrm.next_out = chunk;
      ret = inflate(&zstrm, Z_NO_FLUSH);

      /* Se houve qualquer erro, sai e apaga o arquivo 
         de saída. */
      switch (ret)
      {
      case Z_NEED_DICT:
      case Z_DATA_ERROR:
      case Z_MEM_ERROR:
        fprintf(stderr, "Erro decompactando.\n");
        longjmp(jb, 1);
      }

      /* Grava o bloco descompactado. */
      size = MAX_CHUNK_SIZE - zstrm.avail_out;

      if (fwrite(chunk, 1, size, fout) != size || ferror(fout))
      {
        fprintf(stderr, "Erro ao gravar arquivo descompactado.\n");
        longjmp(jb, 1);
      }

      /* Pode ser que ainda existam dados para descompactar... */
    } while (zstrm.avail_out == 0);

    /* Se a última chamada a inflate() informou que chegamos ao fim,
       então chegamos ao fim! */
  } while (ret != Z_STREAM_END);

  /* fecha tudo e limpa o contexto da zlib. */
  fclose(fin);
  fclose(fout);
  inflateEnd(&zstrm);

  return 0;
}

E, para provar que a coisa funciona, eis a decompressão em ação:

$ gcc -O3 -o decompress decompress.c -lz
$ ./decompress compress.c.z compress.c.orig
$ diff compress.c compress.c.orig

De novo, estamos fazendo a decompressão pedaço por pedaço, mas poderíamos fazer de um buffer inteiro de uma só vez (de fato, as duas rotinas fazem exatamente isso, já que nossos buffers têm 16 kB de tamanho e o arquivo original só tem 2 kB).

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s