As far as I can tell most everyone is interested in reading PNG image data from file (internally done in libpng via fread()
). I thought: “Wow, that’s quite unfortunate.” and figured I’d write about it after I figured it out. The article below is the result.
NOTE: the intent of this article is to help you get something up and running quickly. By design, it does not provide detailed explanations about the functions or interface of libpng — that’s what the documentation is for.
Motivation
There are several reasons why one might want to have data read from memory rather than directly from a file.
Testing – This was the first reason that came to my mind. In the Pulsar engine codebase, I want to test my format data loaders, which includes anything from textures and models to config files and scripts. More on this later.
Archives – If you pack a “file” into a custom archive, do you expect you want to hand a third-party library your file pointer? …
IO Performance – File IO has never been instant, and it can be worse depending upon the platform. As a result, you’re usually better off if you read in a file in its entirety as opposed to only reading a small chunk at a time, since the device may have seeked to somewhere else in between your reads.
Scenario in Pulsar
I have abstracted the operation of reading into an InputStream
interface so that the source of the data (file, buffer in memory, network, or whatever) can be swapped out without affecting the behavior of the loader classes.
The function signature looks like this:
size_t InputStream::Read(byte* dest, const size_t byteCount);
You will see this used as the main data pipe in the next section.
Implementation
I will assume you have already set the include and library dependencies for libpng. The libpng documentation is pretty clear about this topic, so I won’t delve into it here.
I should also note that the code provided below is practical and is taken from a module in my codebase. It includes a basic amount of error checking, but I stripped the rest of it out for clarity sake. As such, I recommend that you use this as a starting point.
You will undoubtedly have to switch out the calls to pulsar::InputStream::Read()
and replace with them your own data source’s read interface.
Lastly, I have typedefed a few basic data types in my engine. For example: byte
is typedefed as an unsigned char
.
Setup
First, we check if the data has the PNG signature, which is held in the first 8 bytes of the image data. If libpng determines that the signature is invalid, we bail.
enum {kPngSignatureLength = 8};
byte pngSignature[kPngSignatureLength];
// call to pulsar::InputStream::Read()
// -> replace with your own data source's read interface
inputStream.Read(pngSignature, kPngSignatureLength);
if(!png_check_sig(pngSignature, kPngSignatureLength))
return false;
Next, we need to create a pair of file info structures. For future compatibility reasons, libpng must allocate and free the memory for these structures. If either one of these calls fail, we perform the necessary cleanup and bail.
// get PNG file info struct (memory is allocated by libpng)
png_structp png_ptr = NULL;
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if(png_ptr == NULL)
return false;
// get PNG image data info struct (memory is allocated by libpng)
png_infop info_ptr = NULL;
info_ptr = png_create_info_struct(png_ptr);
if(info_ptr == NULL)
{
// libpng must free file info struct memory before we bail
png_destroy_read_struct(&png_ptr, NULL, NULL);
return false;
}
This is where the magic happens. Now we must tell libpng to use a our own custom routine for retrieving the image data. To do this, we use the png_set_read_fn()
routine:
png_set_read_fn(png_ptr, &inputStream, ReadDataFromInputStream);
The first parameter is the pointer to the first struct we asked libpng to create for us: png_ptr
.
The second parameter is a void pointer to the data source object. In this case, the data source my InputStream
object.
The final parameter is a pointer to a function that will handle copying the data from the data source object into a given byte buffer. Here, my reader function is called ReadDataFromInputStream()
. This routine must have the following function signature:
void ReadDataFromInputStream(png_structp png_ptr, png_bytep outBytes,
png_size_t byteCountToRead);
Now that we have told libpng where to go when it needs to read bytes, we need to implement that routine. Mine looks like this:
void ReadDataFromInputStream(png_structp png_ptr, png_bytep outBytes,
png_size_t byteCountToRead)
{
png_voidp io_ptr = png_get_io_ptr(png_ptr);
if(io_ptr == NULL)
return; // add custom error handling here
// using pulsar::InputStream
// -> replace with your own data source interface
InputStream& inputStream = *(InputStream*)io_ptr;
const size_t bytesRead = inputStream.Read(
(byte*)outBytes,
(size_t)byteCountToRead);
if((png_size_t)bytesRead != byteCount)
return; // add custom error handling here
} // end ReadDataFromInputStream()
The last piece of setup we need to do is tell libpng that we have already read the first 8 bytes when we checked the signature at the beginning.
// tell libpng we already read the signature
png_set_sig_bytes(png_ptr, kPngSignatureLength);
PNG Header
Now we are ready to read the PNG header info from the data stream. There is a lot of data one could extract from this, but here I’m only going to grab a few important values. Refer to the libpng documentation for more details on this function.
png_read_info(png_ptr, info_ptr);
png_uint_32 width = 0;
png_uint_32 height = 0;
int bitDepth = 0;
int colorType = -1;
png_uint_32 retval = png_get_IHDR(png_ptr, info_ptr,
&width,
&height,
&bitDepth,
&colorType,
NULL, NULL, NULL);
if(retval != 1)
return false; // add error handling and cleanup
The width
and height
parameters are the image dimensions in pixels.
The bitDepth
is the number of bits per channel. It is important to note that this is not per pixel (which is what is normally the value stored in a TGA or BMP).
The final parameter we care about is colorType
which is an integer that maps to one of the predefined values in libpng that describe the image data (RGB, RGBA, etc). You will need this to determine how many channels you need to read from the bytes into your pixel color.
PNG Image Data
We have reached the point where we can now read in the image data into our internal image pixels.
outImage.Init(width, height);
switch(colorType)
{
case PNG_COLOR_TYPE_RGB:
ParseRGB(outImage, png_ptr, info_ptr);
break;
case PNG_COLOR_TYPE_RGB_ALPHA:
ParseRGBA(outImage, png_ptr, info_ptr);
break;
default:
PULSAR_ASSERT_MSG(false, "Invalid PNG ColorType enum value given.\n");
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
return false;
}
First, I initialize my internal image format, which allocates the appropriate memory for the pixels — each of which is a Color32 (red,green,blue,alpha).
The switch is to differentiate my parsing routines (one of which is listed below) based on the colorType
value we retrieved from the header.
To parse the data into pixel colors all we have to do is grab each row from the image and proceed to read each tuple of bytes that represents a pixel. The number of bytes we read is dependent upon the data stored, which is why we differentiate between parsing a 24-bit (RGB) color value and a 32-bit (RGBA) color value via colorType
.
I have provided the ParseRGBA()
routine below. The ParseRGB()
is very similar except no bytes are read for the alpha channel.
void ParseRGBA(Image& outImage, const png_structp& png_ptr,
const png_infop& info_ptr)
{
const u32 width = outImage.GetWidth();
const u32 height = outImage.GetHeight();
const png_uint_32 bytesPerRow = png_get_rowbytes(png_ptr, info_ptr);
byte* rowData = new byte[bytesPerRow];
// read single row at a time
for(u32 rowIdx = 0; rowIdx < height; ++rowIdx)
{
png_read_row(png_ptr, (png_bytep)rowData, NULL);
const u32 rowOffset = rowIdx * width;
u32 byteIndex = 0;
for(u32 colIdx = 0; colIdx < width; ++colIdx)
{
const u8 red = rowData[byteIndex++];
const u8 green = rowData[byteIndex++];
const u8 blue = rowData[byteIndex++];
const u8 alpha = rowData[byteIndex++];
const u32 targetPixelIndex = rowOffset + colIdx;
outImage.SetPixel(targetPixelIndex, Color32(red, green, blue, alpha));
}
PULSAR_ASSERT(byteIndex == bytesPerRow);
}
delete [] rowData;
} // end ParseRGBA()
Cleanup
Finally, now that the image data has been read we should clean up the libpng structures, then we can be on our merry way!
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
That's it! Now we can go do whatever we need to with the image data we just read in!
Conclusion
Here I've only touched on the basics. This seemingly simple task was practically nowhere to be found when I searched for it, resulting in me having to piece it together. Hopefully this will same someone else some time.
Resources