Usando a biblioteca libJPEG para manipulação de imagens

Nesse artigo, iremos explanar sobre uma classe C++ que tem por objetivo abrir um arquivo JPEG, armazenando a imagem na memória para podermos manipulá-la, salvando o resultando em um novo arquivo JPEG.

O ponto de partida para nossa classe é bem básico: teremos apenas três atributos e dois métodos – read_JPEG_file e write_JPEG_file. A partir desse modelo básico, podemos ampliar nossa classe adicionando métodos para manipulação da imagem, ou construtores para podermos gerar um arquivo JPEG a partir de fontes diversas, como um array de pontos gerados pelo computador.
A estrutura básica de nossa classe seria a seguinte:

class JPEG
{
private:
JSAMPLE * image_buffer;
int image_height;
int image_width;
public:
void write_JPEG_file (char * filename, int quality);
int read_JPEG_file (char * filename);
};

Os atributos da classe tem funções bem óbvias: image_buffer armazena os pixels da imagem (sendo que cada pixel possui três componentes, correspondentes as componentes do padrão de cores RGB), image_height armazena a altura da imagem e image_width armazena a largura da imagem.
A função read_JPEG_image recebe como parâmetro o nome de um arquivo JPEG, e lê os dados desse arquivo para armazená-los nos atributos da classe. Isso em feito em sete passos:

1º passo: alocação e inicialização do objeto de descompressão JPEG

Aqui, iremos declarar as variáveis necessárias para leitura do arquivo, abrir o arquivo passado como parâmetro em modo leitura e criaremos uma estrutura para acessar os dados do arquivo JPEG. O código para isso é o seguinte:

  struct jpeg_decompress_struct cinfo;
  struct my_error_mgr jerr;
  FILE * infile;                /* source file */
  JSAMPARRAY buffer;            /* Output row buffer */
  int row_stride;               /* physical row width in output buffer */
  if ((infile = fopen(filename, "rb")) == NULL) {
    fprintf(stderr, "can't open %s\n", filename);
    return 0;
  }
  cinfo.err = jpeg_std_error(&jerr.pub);
  jerr.pub.error_exit = my_error_exit;
  if (setjmp(jerr.setjmp_buffer)) {
    jpeg_destroy_decompress(&cinfo);
    fclose(infile);
    return 0;
  }
  jpeg_create_decompress(&cinfo);

 2º passo: Definir a fonte dos dados

Aqui, informaremos que a fonte dos dados de nossa classe será o arquivo informado pelo programa. Isso é feito através da função jpeg_stdio_src da biblioteca libjpeg, dessa forma:

jpeg_stdio_src(&cinfo, infile);

3º passo: leitura dos parâmetros do arquivo

Agora, iremos ler o cabeçalho do arquivo JPEG para obter os dados necessários para finalizar a leitura de todos os pixels da imagem. Nesse passo, já teremos acesso à altura e largura da imagem. Isso é feito com a função jpeg_read_header da bibliotecas libjpeg, dessa forma:

(void) jpeg_read_header(&cinfo, TRUE);

4º passo: ajuste dos parâmetros da descompressão

Aqui, podemos definir alguns parâmetros que irão influenciar na descompressão da imagem. Normalmente, nenhum ajuste é preciso. Caso você tenha necessidade de algum ajuste diferente, pesquise por “JPEG decompression parameters” no Google.

5º e 6º passos: Inicio da descompressão e Leitura dos pixels do arquivo

Agora, percorremos o arquivo JPEG linha por linha e iremos preenchendo o vetor JSAMPLE (definido como um atributo da classe) com os pixels da imagem. Isso é feito dessa forma:

(void) jpeg_start_decompress(&cinfo);
row_stride = cinfo.output_width * cinfo.output_components;
buffer = (*cinfo.mem->alloc_sarray)
                ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
while (cinfo.output_scanline < cinfo.output_height) {
    (void) jpeg_read_scanlines(&cinfo, buffer, 1);
    /*Assuma que put_scanline_someplace precise de um ponteiro e um contador.*/
    put_scanline_someplace(buffer[0], row_stride);
  }

7º passo: Finalizando

Aqui, encerramos todos ponteiros usados, liberando a memória usada para a descompressão do arquivo.

(void) jpeg_finish_decompress(&cinfo);
 jpeg_destroy_decompress(&cinfo);
fclose(infile);

A função write_JPEG_image recebe como parâmetro o nome de um arquivo JPEG e a qualidade com que esse arquivo será saldo, e salva os dados armazenados nos atributos da classe em um arquivo. Isso em feito em seis passos:

1º passo: alocação e inicialização do objeto de compressão JPEG

Aqui, iremos declarar as variáveis necessárias para escrever o arquivo, abrir o arquivo passado como parâmetro em modo escrita e criaremos uma estrutura que será gravada no arquivo JPEG. O código para isso é o seguinte:

struct jpeg_compress_struct cinfo;
  struct jpeg_error_mgr jerr;
  FILE * outfile;               /* target file */
  JSAMPROW row_pointer[1];      /* pointer to JSAMPLE row[s] */
  int row_stride;               /* physical row width in image buffer */
  cinfo.err = jpeg_std_error(&jerr);
  jpeg_create_compress(&cinfo);

2º passo: Especificação do destino dos dados

Aqui, informaremos qual será o arquivo de destino dos dados de nossa classe. Isso é feito através da função jpeg_stdio_src da biblioteca libjpeg, dessa forma:

if ((outfile = fopen(filename, "wb")) == NULL) {
    fprintf(stderr, "can't open %s\n", filename);
    exit(1);
  }
  jpeg_stdio_dest(&cinfo, outfile);

3º passo: Ajustar parâmetros da compressão

Aqui, definiremos alguns parâmetros necessários à compressão da imagem. Basicamente, iremos associar os valores armazenados nos atributos da classe aos membros da estrutura de dados criada por meio da biblioteca libjpeg para criação do arquivo.

cinfo.image_width = image_width;        /* image width and height, in pixels */
  cinfo.image_height = image_height;
  cinfo.input_components = 3;           /* # of color components per pixel */
  cinfo.in_color_space = JCS_RGB;       /* colorspace of input image */
  jpeg_set_defaults(&cinfo);
  jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);

4º passo: Inicio da compressão

Aqui iniciaremos a compressão dos dados que irão ser salvos no arquivo indicado. O parâmetro TRUE no comando a seguir garante que será escrito um arquivo JPEG completo.

jpeg_start_compress(&cinfo, TRUE);

5º passo: Escrevendo os dados no arquivo

Nesse momento, iremos percorrer o vetor JSAMPLE salvando os pixels armazenados nele no arquivo, de acordo com os parâmetros fornecidos. O código para fazer isso é o seguinte:

row_stride = image_width * 3; /* JSAMPLEs per row in image_buffer */
while (cinfo.next_scanline < cinfo.image_height) {
row_pointer[0] = & image_buffer[cinfo.next_scanline * row_stride];
(void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
}

6º passo: Finalizando

Aqui, encerramos todos ponteiros usados, liberando a memória usada para a descompressão do arquivo.

jpeg_finish_compress(&cinfo);
fclose(outfile);
jpeg_destroy_compress(&cinfo);

Depois de implementar esses dois métodos, você pode adicionar outros métodos para manipular a imagem. Dois tipos de método se destacam aqui: o primeiro tipo são método para executar algum tipo de efeito na imagem, e o segundo tipo são construtores que recebem como argumento vetores de pixels de diversos tipos e armazenam esses vetores em image_buffer. Nesse ultimo caso, lembre que o tipo JSAMPLE é um alias para unsigned char*, então na hora de armazenar algum vetor cujo tipo de dado seja diferente, lembre-se de fazer a devida conversão entre tipos (cast).