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).