Usando a biblioteca libPNG para manipulação de imagens

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

O ponto de partida para nossa classe é bem básico: temos alguns atributos (que armazenarão os dados lidos do arquivo) e dois métodos – read_png_file e write_png_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 PNG a partir de fontes diversas, como um array de pontos gerados pelo computador.

A estrutura básica de nossa classe seria a seguinte:

class PNG

{

private:

int width, height, rowbytes;

png_byte color_type;

png_byte bit_depth;

png_structp png_ptr;

png_infop info_ptr;

int number_of_passes;

png_bytep * row_pointers;

public:

void write_png_file (char * filename);

int read_png_file (char * filename);

};

Os atributos da classe tem as funções que seus nomes descrevem: width e height armazenam a largura e altura da imagem, respectivamente; rowbytes armazena o tamanho da linha, que é a largura multiplicada pelo numero de bytes por pixel (que varia de imagem para imagem, e é armazenado em bit_depth); color_type, png_str, info_ptr e number_of_passes são estruturas de dados que armazenam dados especificos do formato PNG (esses dados não são necessários para o propósito desse arqtigo, mas você pode se aprofundar sobre o assunto se deseja manipulações mais complexas); finalmente, row_pointers é uma matriz que armazena os pixels da imagem (apenar da declaração do atributo indicar ser um vetor de png_bytep, esse tipo é um alias para para unsigned char*, o que torna esse tipo uma matriz de valores unsigned char).

A função read_png_image recebe como parâmetro o nome de um arquivo PNG, e lê os dados desse arquivo para armazená-los nos atributos da classe. Isso em feito da seguinte forma:

1º passo:

Primeiro, precisamos abrir o arquivo que iremos ler e associá-lo a um stream de dados de onde serão lidos os dados:

/* open file and test for it being a png */

FILE *fp = fopen(file_name, "rb");

if (!fp)

abort_("[read_png_file] File %s could not be opened for reading", file_name);

fread(header, 1, 8, fp);

if (png_sig_cmp(header, 0, 8))

abort_("[read_png_file] File %s is not recognized as a PNG file", file_name);

2º passo:

Em seguida, inicializaremos as estruturas de dados especificas do formato PNG – png_str e info_ptr – que foram declaradas como atributos da classe; essas estruturas guardarão dados necessários à manipulação da imagem e leitura dos dados restantes da imagem:

png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

if (!png_ptr)

abort_("[read_png_file] png_create_read_struct failed");

info_ptr = png_create_info_struct(png_ptr);

if (!info_ptr)

abort_("[read_png_file] png_create_info_struct failed");

if (setjmp(png_jmpbuf(png_ptr)))

abort_("[read_png_file] Error during init_io");

png_init_io(png_ptr, fp);

png_set_sig_bytes(png_ptr, 8);

png_read_info(png_ptr, info_ptr);

 3º passo:

Após termos inicializado as estruturas, agora iremos ler do arquivo os demais dados da imagem, como por exemplo a largura e a altura, dentre outros:

width = png_get_image_width(png_ptr, info_ptr);

height = png_get_image_height(png_ptr, info_ptr);

color_type = png_get_color_type(png_ptr, info_ptr);

bit_depth = png_get_bit_depth(png_ptr, info_ptr);

number_of_passes = png_set_interlace_handling(png_ptr);

png_read_update_info(png_ptr, info_ptr);

/* read file */

if (setjmp(png_jmpbuf(png_ptr)))

abort_("[read_png_file] Error during read_image");

4º passo:

Iremos agora alocar espaço na memória para row_pointers e ler do arquivo os dados relativos aos pixels da imagem. Isso é feito da seguinte forma:

row_pointers = (png_bytep*) malloc(sizeof(png_bytep) * height);

if (bit_depth == 16)

rowbytes = width*8;

else

rowbytes = width*4;

for (y=0; y<height; y++)

row_pointers[y] = (png_byte*) malloc(rowbytes);

png_read_image(png_ptr, row_pointers);

fclose(fp);

A função write_png_image recebe como parâmetro o nome de um arquivo PNG, e salva os dados armazenados nos atributos da classe em um arquivo. Isso em feito da seguinte forma:

1º passo:

Para começar, precisamos criar um arquivo onde serão salvos os dados armazenados na memória, como o nome fornecido à função como parâmetro:

/* create file */

FILE *fp = fopen(file_name, "wb");

if (!fp)

abort_("[write_png_file] File %s could not be opened for writing", file_name);

2º passo:

Em seguida, inicializamos as estruturas necessárias à formatação dos dados da imagem, png_ptr e info_ptr, da seguinte forma:

/* initialize stuff */

png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

if (!png_ptr)

abort_("[write_png_file] png_create_write_struct failed");

info_ptr = png_create_info_struct(png_ptr);

if (!info_ptr)

abort_("[write_png_file] png_create_info_struct failed");

if (setjmp(png_jmpbuf(png_ptr)))

abort_("[write_png_file] Error during init_io");

png_init_io(png_ptr, fp);

3º e 4º passos:

Nesse momento, iremos escrever no arquivo os dados do cabeçalho da imagem PNG e os pontos da imagem. Isso é feito dessa forma:

/* write header */

if (setjmp(png_jmpbuf(png_ptr)))

abort_("[write_png_file] Error during writing header");

png_set_IHDR(png_ptr, info_ptr, width, height,

8, 6, PNG_INTERLACE_NONE,

PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);

png_write_info(png_ptr, info_ptr);

/* write bytes */

if (setjmp(png_jmpbuf(png_ptr)))

abort_("[write_png_file] Error during writing bytes");

png_write_image(png_ptr, row_pointers);

4º passo:

Por fim, precisamos finalizar todos os ponteiros usados e liberar os recursos utilizados pela função.

/* end write */

if (setjmp(png_jmpbuf(png_ptr)))

abort_("[write_png_file] Error during end of write");

png_write_end(png_ptr, NULL);

/* cleanup heap allocation */

for (y=0; y<height; y++)

free(row_pointers[y]);

free(row_pointers);

fclose(fp);

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étodos 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 row_pointer. Nesse ultimo caso, lembre que o tipo png_bytep é 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).