Working with PNGs with libpng
I get to work with the PNG format all the time. I was very pleased when I discovered the libpng C library makes it easy to programmatically work with it too.
I enjoy the decent compression and lossless quality offered by the PNG image format, plus the fact it supports transparency. A word of confession, here, though: libpng is easy to work with only because of how the API is organised, not because it comes with good documentation. All the same, here's a few notes to get the newcomer started. I enjoyed using libpng when I wrote my remote LeCroy WaveJet series oscilloscope client.
1 Foreword
As far as I understood, libpng is the one official PNG library. As a consequence it's widely spread. On various Linux distributions, it comes in packages whose names start with libpng
:
libpng12-0
– the runtime library;libpng12-dev
– the development files;libpng3
– the runtime library of an older version. Forget it.
2 Getting Started
2.1 Header Files
The /usr/include/png.h
header file is all you'll ever need:
#include <png.h>
int main(void)
{
2.2 Open a File
No surprises here:
FILE *fp = fopen("file.png", "r");
2.3 Initialise the Library
You'll be dealing with two structures most of the time:
png_structp
, which contains notably the pixel data;png_infop
, which contains various information.
png_structp pngptr =
png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
png_infop pnginfo = png_create_info_struct(pngptr);
You'll see in the documentation there's an extra call to png_create_info_struct()
to create something called end_info
. Since it's neither told nor clear what it's meant for, just forget it. Libpng doesn't miss it as far as I could experience.
2.4 RGB
Sometimes, you come across colour-indexed PNGs which you might want to convert to RGB. Just call:
png_set_palette_to_rgb(pngptr);
... to do this.
2.5 Error Handling
It's unclear in the doc whether or not there's anything to set up to later handle errors, especially dealing with twisted things such as setjmp
/longjmp
. But libpng doesn't seem to miss it, so let's not bother with it. A message will show up anyway when something turns ill.
2.6 Set Up I/O
This is basically useful to say where we read the data from:
png_init_io(pngptr, fp);
2.7 High-Level Reading
You'll find I've skipped a number of sections in the doc, notably about callbacks called after each row has been read. Forget about it, I didn't need it. Let's move on to actually reading the PNG file to put the data into an intelligible array:
png_bytepp rows;
png_read_png(pngptr, pnginfo, PNG_TRANSFORM_IDENTITY, NULL);
rows = png_get_rows(pngptr, pnginfo);
The doc simply uses an array of png_bytep
for the rows. But the compiler will never buy it. So I used a png_bytepp
as the API intended in the first place. (I don't understand why the doc allocates HEIGHT
lines for this array as function png_get_rows()
handles the allocation anyway.) The PNG_TRANSFORM_IDENTITY
constant means you apply no transformation.
2.8 Read Pixel Values
You have to read the image on a per-row basis. For each row, there's a series of continuous RGB values for each value:
for (i = 0; i < HEIGHT; i++) {
for (j = 0; j < WIDTH * 3; j += 3) {
printf("%d %d %d ", rows[i][j], rows[i][j + 1], rows[i][j + 2]);
}
printf("\n");
}
}
If you're dealing with indexed colour images, you'll have only one value per pixel (a palette index).
3 Changing the I/O Method
So far, we've seen how to read PNG data from a file. How to do the same from, say, an array? Take a look at section V. Modifying/Customizing libpng from the official doc. The idea is to call png_set_read_fn()
instead of png_init_io()
:
png_set_read_fn(pngptr, input, readpng);
The readpng()
home-made function is called to write the PNG data. The input
variable is actually a pointer to whatever you may need within readpng()
. Here, we'll pass an array where the input PNG data is waiting. It may be retrieved calling png_get_io_ptr()
:
void readpng(png_structp _pngptr, png_bytep _data, png_size_t _len)
{
static int offset = 0;
char *input;
/* Get input */
input = png_get_io_ptr(_pngptr);
/* Copy data from input */
memcpy(_data, input + offset, _len);
offset += _len;
}
The _data
argument is where you're supposed to copy the PNG input data. Note that this function is called multiple times before everything has been read so you'll need to keep track of how many bytes have already been copied and offset the readout or you'll keep reading the same data.
4 Checking the Signature
You should read the first 8 bytes of the file. The doc says that the more you read, the better the signature check is accurate:
char header[HEADERLEN];
fread(header, 1, HEADERLEN, fp); /* #define HEADER 8 */
int sig = png_sig_cmp(header, 0, HEADERLEN);
if (sig == 0) {
/* It's a fine PNG file */
} else {
/* It's no valid PNG file */
}
You have to tell libpng how many bytes you read for this signature check. You may traditionally do so just before a call to png_read_png()
, or something:
png_set_sig_bytes(pngptr, HEADERLEN);