Abrindo imagem PNG com libpng 1.5.12 ou mais recente na memória
Autor: Santos | Publicado: 2012-08-28T12:10:00-03:00

Eu fiz este tutorial porque vi que não existe nenhuma ou pouca documentação sobre o uso da libpng nas novas versões.
Uma vez que existem algumas grandes mudanças no código da libpng, muitas informações podem estar inválidas.
Bem, antes de qualquer coisa, você precisa baixar a biblioteca, compilar e deixá-la pronta para uso. Eu não irei cobrir esses passos
nessa tutorial.

Com tudo no lugar, abra seu editor favorito e crie um arquivo de cabeçalho:

PngLoader.h:


#ifndef __PNGLOADER_H__
#define __PNGLOADER_H__

#include &lt;string&gt;
#include &lt;png.h&gt;

class PngLoader
{
public:
    PngLoader();
    ~PngLoader();

    bool load( std::string file );
    const char *getData() const { return data; }
    int getWidth() const { return width; }
    int getHeight() const { return height; }
    int getDepth() const { return depth; }
    int getColorType() const { return color_type; }

private:
    void release();

    char *data;
    int width;
    int height;
    int depth;
    int color_type;
};

#endif


Esta é uma classe simples, que irá apenas garantir acesso para ler os dados na memória. Você pode, certamente,
fazer as alterações que desejar para mudar os dados, por exemplo (se você possui o devido conhecimento para isso).
Note que existe algumas outras informações que você pode obter pela libpng sobre sua imagem PNG. Se você precisar de
alguma dessas informações você terá que adicionar os atributos e métodos correspondentes na sua classe.

Vamos agora criar o aquivo fonte.

PngLoader.cpp:


#include "PngLoader.h"

#include &lt;fstream&gt;
#include &lt;string&gt;
#include &lt;sstream&gt;

void readFileCallback( png_structp png_ptr, png_bytep out, png_size_t count );

PngLoader::PngLoader(void)
          :data(0)
{
}

bool PngLoader::load( std::string file )
{
    png_structp png_ptr;
    png_infop info_ptr;
 
    std::ifstream ifile( file.c_str(), std::ios::binary );

    if( !ifile.is_open() )
        return false;

    char assinatura[8];
    ifile.read( &assinatura[0], 8 * sizeof(char) );
 
    if(!png_check_sig( (png_bytep)assinatura, 8) )
    {
        return false;
    }

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

    if(png_ptr == 0)
    {
        return false;
    }

    info_ptr = png_create_info_struct( png_ptr );

    if( info_ptr == 0 )
    {
        png_destroy_read_struct( &png_ptr, 0, 0 );
        return false;
    }

    png_set_read_fn( png_ptr, (void*)&ifile, readFileCallback );

    png_set_sig_bytes( png_ptr, 8 );

    png_read_info( png_ptr, info_ptr ); 

    png_get_IHDR( png_ptr, info_ptr, (png_uint_32*)&width, (png_uint_32*)&height,
                  &depth, &color_type, 0, 0, 0 );

    png_size_t cols = png_get_rowbytes(png_ptr, info_ptr);

    png_bytepp row_pp = new png_bytep[height];
    data = new char[ cols * height ];

    for( int i = 0; i < height; ++i )
    {
        row_pp[height - i - 1] = (png_bytep)&data[ i * cols ];
    }

    png_read_image( png_ptr, row_pp );
    png_read_end( png_ptr, info_ptr );

    png_destroy_read_struct( &png_ptr, &info_ptr, 0 );

    delete[] row_pp;
    return true;
}

void PngLoader::release()
{
    if( data )
        delete[] data;
    data = 0;
}

PngLoader::~PngLoader(void)
{
    release();
}

void readFileCallback( png_structp png_ptr, png_bytep out, png_size_t count )
{
    png_voidp io_ptr = png_get_io_ptr( png_ptr );
 
    if( io_ptr == 0 )
    {
        return;
    }

    std::ifstream &ifs = *(std::ifstream*)io_ptr;

    ifs.read( (char*)out, count );
}


Aqui o código pode parecer um pouco confuso. Os caras da libpng escolheram nomes grandes para suas funções e estruturas de dados. Mas
dessa forma, se você possui algumas habilidades de programação em C puro, você pode entender que as coisas ficam mais fáceis de entender.
No construtor não existe nenhum código especial.
O método load é o maior, então vamos iniciar vendo outras partes do código.
O método release é responsável por liberar o atributo data da memória. O usuário poderá chamá-lo logo após usar os dados da imagem. Não
há nada complicado de se entender aqui, há apenas uma verificação para certificarmos que não tentaremos deletar de um ponteiro nulo.
O destrutor faz apenas uma chamada para o método release, assim, se o usuário esquecer de chamar o método release, a memória para data será
liberada quando o objeto já não for mais necessário e for destruído.

Bem, chegou a vez do método load. Nós começamos chamando algumas variáveis locais. As variáveis mais importantes são png_ptr e info_ptr.
A variável png_ptr é usada pela libpng para decodificar a imagem, enquanto info_ptr é usada para manter as informações sobre a imagem. A partir
da versão 1.5.0 da libpng o acesso direto aos membros das estruturas de dados internos ficou depreciado. Então você precisará usar as funções png_get_*
e png_set_*, onde o asterisco significa o nome do membro que você deseja acessar ou alterar.
O próximo passo é abrir o arquivo de imagem, o que é feito pelo objeto ifstream ifile. Assim, são passado, na instanciação do objeto, o nome do arquivo
e o modo de acesso para binário.
Com o arquivo aberto com sucesso, nós verificamos a assinatura do arquivo PNG. Isso é feito pela função png_check_sig.
Para isso, nós passamos um pequeno buffer de oito bytes, que é o tamanho para assinatura PNG.
Uma vez que a imagem está pronta para ser lida, nós alocamos memória para png_ptr com a função png_create_read_struct.
Após isso, info_ptr é inicializada com a função png_create_info_struct. Se tudo deu certo, é hora para o próximo passo.
Agora nós informamos a libpng que temos um callback para leitura da imagem. Dessa forma, nós podemos ler a imagem da maneira que desejarmos. Nesse
tutorial nós iremos usar a classe ifstream da STL. Assim nós passamos o ponteiro para nosso objeto. E a chamada final para a função é:

png_set_read_fn( png_ptr, (void*)ifile, readFileCallback );

Onde png_ptr é o nosso ponteiro para decodificar a imagem; ifile é o objeto leitor da ifstream e readFileCallback é a função que irá ler cada porção ou
chunk do arquivo de imagem.

Para melhor entender essa função, vamos vê-la em detalhes.


void readFileCallback( png_structp png_ptr, png_bytep out, png_size_t count )
{
    png_voidp io_ptr = png_get_io_ptr( png_ptr );
 
    if( io_ptr == 0 )
    {
        return;
    }

    std::ifstream &ifs = *(std::ifstream*)io_ptr;

    ifs.read( (char*)out, count );
}


A função precisa ter exatamente a mesma assinatura acima. Ela aceita três argumentos. O primeiro é o ponteiro para png_struct, png_ptr. O ponteiro png_byte, out, é o
ponteiro para ler o conteúdo dentro da função. E o número de bytes para ler é dado pelo argumento count do tipo png_size_t.
A primeira coisa a ser feita é recuperar o ponteiro para nosso objeto ifstream. Então nós usamos a função png_get_io_ptr para obter o membro io_ptr da estrutura png_ptr.
Depois de verificar se o ponteiro está setado, é criado um objeto ifstream que aponta para io_ptr. Dessa forma, um cast é usado.
Nesse ponto, nós temos apenas que passar para o buffer out o conteúdo lido. Nós lemos, assim, count bytes do arquivo usando o método read da classe ifstream e salvamos
seu conteúdo dentro do buffer.
Uma coisa importante para saber é que o objeto ifs aponta para ifile, criado anteriormente. E que o método read irá ler e setar a posição lida no objeto ifstream. Assim,
na próxima leitura, ele continua a leitura de onde parou, e assim por diante até o fim do arquivo.

No próximo passo, dentro do método load, nós comunicamos a libpng que a assinatura do arquivo PNG já foi lida. Para isso usamos a função:

png_set_sig_bytes( png_ptr, 8 );

Está função irá setar o membro sig_bytes de png_ptr para 8. E é como a png sabe que oito bytes já foram lidos.

É hora de ler as informações da imagem. E este é um trabalho para a função png_read_info.

png_read_info( png_ptr, info_ptr );

Nada de especial, mas, como dito anteriormente, o conteúdo lido estará dentro da estrutura info_ptr.
Nós vamos obter algumas informações do arquivo. E essa informação está dentro do chunk IHDR. Então chamamos:

png_get_IHDR( png_ptr, info_ptr, (png_uint_32*)&width, (png_uint_32*)&height, &depth, &color_type, 0, 0, 0 );

O membro IHDR é um membro de info_ptr, então nós passamos primeiro o ponteiro para png_struct, depois o ponteiro para info_struct. Todos os outros
argumentos são para obtermos as informações desejadas. Precisamos apenas da largura da imagem, altura, profundidade de cores e o tipo de cor para a
maioria dos trabalhos em imagem (Criar uma textura com OpenGL ou desenhar a imagem com alguma API gráfica com suporte a leitura a partir da memória,
por exemplo).

Agora precisamos obter o número de bytes de cada linha na matriz de dados da imagem.

png_size_t cols = png_get_rowbytes(png_ptr, info_ptr);

O número retornado irá depender dos canais de cores que a imagem possui. Por exemplo, se sua imagem possui 512 pixels de largura e quatro canais
de cores (RGBA), o número de bytes por linha será 512 * 4 = 2048 bytes.

Com essa informação nos podemos alocar memória para os dados da imagem. Mas antes, nós criamos uma lista de ponteiros para bytes com tamanho de height.


png_bytepp row_pp = new png_bytep[height];


O png_bytep é o mesmo que png_byte* e png_bytepp é o mesmo que png_byte**.

Então, nós alocamos o espaço em que estarão os dados da imagem.

data = new char[ cols * height ];

Dessa vez nós iremos setar os ponteiros de row_pp de cada linha para setar para uma localização no atributo data. Uma vez que os dados da libpng vem de uma matriz,
é necessário converter os ponteiros da forma bidimensional para a forma unidimensional.


for( int i = 0; i < height; ++i )
{
    row_pp[height - i - 1] = (png_bytep)&data[ i * cols ];
}


O que é feito nesse laço é ir em cada linha, na ordem reversa, e apontar para o endereço correspondente no array data. O cast é usado porque libpng precisa de
uma matriz de png_byte para por os dados dentro e eu preciso de um array de chars. Por alguma razão a libpng retorna os dados da imagem com as linhas na ordem
inversa, então row_pp é mapeado na ordem inversa o que faz com que ele fique da forma correta no final.

Veja a imagem:


Finalmente, nós podemos obter os dados da imagem e armazená-las dentro do atributo data através de row_pp:

png_read_image( png_ptr, row_pp );

A função png_read_end, chamada após png_read_image, irá apenas ler os dados remanescentes no arquivo.

Após todos esses passos nós temos os dados da imagem. E o atributo data agora tem o conteúdo da imagem.

É hora de limpar tudo que não precisamos mais. Primeiro, nós limpamos png_ptr e info_ptr:


png_destroy_read_struct( &png_ptr, &info_ptr, 0 );


O terceiro argumento é para informação do final do arquivo, como nós não a usamos, simplesmente passamos 0 ou nulo pra função.

A próxima coisa a limpar é row_pp, uma vez que não vamos mais precisar dele. Note que row_pp é apenas um array de ponteiros usados para apontar para as
posições do atributo data.


delete[] row_pp;