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