Games usually don't have static sprites. Usually, they are animated, and some other information are also linked with the sprites such as the animation speed or the texture origin. I'm sure most of you have hard coded those extra values or put them in extra files. If you are using PNG images, you don't need to!
The PNG file is chunk based and designed to have so called private chunks besides the public chunks which contain the actual image information. So how to use your own chunks?
PNG chunks always have a 4-letter-identifer, some of the public ones are IHDR with header information or IDAT with the actual image. Those are both always needed, hence all letters are capital. Some ancillary are for example bKGD or pHYs.
The case of each letter significates something:
- critical (X) or not (x): those chunks are needed to read the image
- public (X) or not (x): we must use lower case letters for out private chunks
- always capital (X): might be used in the future of PNG
- save to copy (x) or not (X): indicates whether the chunk depends on other information (such as the image itself) or not
Typical image editors don't allow you to write your own custom chunks, neither there is a way to read the custom chunks with the SFML library itself. We need some library so read and write such chunks, and whick library should be better than the original PNG library libpng? It's quite complicated and the documentation poor (in my opinion), but I tried it and finally have a solution. To spare you the same trouble I had I tell you how to use private chunks. This assumes you having installed libpng already, I'm not going to help with that because I had my own trouble there too and I'm unsure about the "correct" installation.
Writing Private Chunks
I assume you already having the original PNG files which you want to "tag" with your custom chunk(s). First of all you should have planned which information you want to store. I created my own struct for that:
struct isPR_struct
{
sf::Uint16 nFrames; // only positive values are allowed. 16-bit can store a number up to 65535
sf::Int16 xOrigin; // might be positive or negative. values from -32768 - 32767 are allowed
sf::Int16 yOrigin;
} __attribute__((packed));
// isPR: i = not critical; s = private; P must be capital; R = depends on image data
Most systems pad variables so that they fit to 16 or 32 bit, no matter what the actual type size is. If one system writes 32 bit for even just a byte value (char) and another one pad it to 16 bit, there will be incompatibilities if you are reading your chunks from a different system. Hence you should add __attribute__(packed)). But I'm not sure whether it works for any compiler or not, I'm using gcc.
Now I'm going to write a program that reads the necessary data from the source file and copies it to the destination along with our custom chunk.
#include <png.h>
...
png_bytepp loadPng(const char *fname, unsigned &width, unsigned &height)
{
png_structp png_ptr = 0;
png_infop info_ptr = 0;
png_bytepp data = 0; // arrays of rows containing the image data
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
0, 0, 0);
if(!png_ptr) return 0; // return NULL-pointer: error
FILE *fp = fopen(fname, "rb");
try
{
info_ptr = png_create_info_struct(png_ptr);
if(!info_ptr) throw 1; // just throw anything, you might use your own exception class
png_init_io(png_ptr, fp); // connect libpng with our fp
png_read_info(png_ptr, info_ptr); // fill info_ptr
width = png_get_image_width(png_ptr, info_ptr);
height = png_get_image_height(png_ptr, info_ptr);
png_uint_32 bitdepth = png_get_bit_depth(png_ptr, info_ptr); // depth per channel, not pixel!
png_uint_32 channels = png_get_channels(png_ptr, info_ptr);
//png_uint_32 color_type = png_get_color_type(png_ptr, info_ptr);
printf("Image: %dx%d\n", width, height); // you can discard this if it works
printf("Bitdepth: %d\nChannels: %d\n", bitdepth, channels); // this too
// allocate the data
data = new png_bytep[height];
for(size_t i=0;i<height;i++) data[i] = new png_byte[width*4];
// fill the data
png_read_image(png_ptr, data);
png_destroy_read_struct(&png_ptr, &info_ptr, 0); // don't forget to free the pointers
fclose(fp); // and to close the file
}
catch(int &e)
{
fclose(fp);
png_destroy_read_struct(&png_ptr, &info_ptr, 0);
return 0; // return NULL-pointer: error
}
return data; // finally return the filled data
}
The code assumes 32-bit RGBA images. If you are going to use any format or are unsure about the actual format of your images, do some checking with the given values (bitdepth, etc.).
Note that we have to care about deleting the returned data later with delete.
loadPng() is only needed where you are going to tag your png. You might have a separated tool for that, you don't need it in your actual game later.
// overwrites your file!
bool saveAttributes(const isPR_struct &isPR, const char *filename)
{
unsigned width, height;
png_bytepp data = loadPng(filename, width, height); // width and height are filled
png_structp png_ptr;
png_infop info_ptr;
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,
0, 0, 0);
if(!png_ptr) return false;
FILE *fp = fopen(filename.c_str(), "wb");
try
{
info_ptr = png_create_info_struct(png_ptr);
if(!info_ptr) throw 1; // like in loadPng()
png_init_io(png_ptr, fp);
png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB_ALPHA,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT); // set the header information
png_write_info(png_ptr, info_ptr);
png_write_image(png_ptr, (png_bytepp)data); // write the actual image data
// tell libpng about our custom chunks
png_unknown_chunk chunks[1]; // 1 custom chunk
strcpy((char*)chunks[0].name, "isPR"); // fill in the name
chunks[0].data = (png_byte*)&isPR; // data points to the chunk data
chunks[0].size = sizeof(isPR);
// it doesn't write your chunks without the following lines
png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS, 0, 0);
png_set_unknown_chunks(png_ptr, info_ptr, chunks, 1);
png_set_unknown_chunk_location(png_ptr, info_ptr, 0, PNG_AFTER_IDAT);
// this might actually write your chunks
png_write_end(png_ptr, info_ptr);
png_destroy_write_struct(&png_ptr, &info_ptr);
fclose(fp);
// don't forget to delete our data which was allocated in loadPng()!
// you might provide another function named unloadPng() instead...
for(size_t i=0;i<height;i++) delete data[i];
delete data;
}
catch(int &e)
{
png_destroy_write_struct(&png_ptr, 0);
fclose(fp);
for(size_t i=0;i<height;i++) delete data[i];
delete data;
return false;
}
return true;
}
Note that the following code will discard any additional information such as the author or comments. You need to change the code if you want to keep additional information.
Reading Private Chunks
I think loading your custom chunks is the easiest step of them. First some code again:
bool loadAttributes(isPR_struct &isPR, const char *filename)
{
DataStream ds(filename); // I use my own sf::InputStream
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
0, 0, 0);
if(!png_ptr) return false;
try
{
png_infop info_ptr = png_create_info_struct(png_ptr);
if(!info_ptr) throw 1;
png_infop end_ptr = png_create_info_struct(png_ptr);
if(!end_ptr) throw 1; // don't know why it is needed...
// set our user chunk handler and tell libpng not to discard them
png_set_read_user_chunk_fn(png_ptr, &isPR, &chunkReader);
png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS,
(png_const_bytep)"isPR", 1);
// instead of png_init_io() if using custom file stream
png_set_read_fn(png_ptr, &ds, &readFunction);
png_read_info(png_ptr, info_ptr);
png_read_update_info(png_ptr, info_ptr);
// you need to read the rows to avoid CRC error... strange
for(unsigned i=0;i<png_get_image_height(png_ptr, info_ptr);i++)
{
png_read_row(png_ptr, 0, 0);
}
// this might actually read your chunks
png_read_end(png_ptr, end_ptr);
png_destroy_read_struct(&png_ptr, &info_ptr, &end_ptr);
}
catch(int &e)
{
png_destroy_read_struct(&png_ptr, &info_ptr, &end_ptr);
return false;
}
return true;
}
void readFunction(png_structp png_ptr, png_bytep data, png_size_t size)
{
DataStream *ds = (DataStream*)png_get_io_ptr(png_ptr);
// some operations on our data stream... (read size bytes into data)
// in standard C it would be: fread(data, 1, size, fp);
}
int chunkReader(png_structp png_ptr, png_unknown_chunkp chunk)
{
// todo: you should first check whether:
// - chunk->name=="isPR"
// - chunk->size==sizeof(isPR_struct)
isPR_struct *isPR_input = (isPR_struct*)chunk->data;
isPR_struct *isPR_output = (isPR_struct*)png_get_user_chunk_ptr(png_ptr);
*isPR_output = *isPR_input;
// return value < 0 if the chunk had an error (e.g. mismatching size),
// return 0 if the chunk is unknown and
// value > 0 on success
return 1;
}
I hope this helps you. I don't guarantee that the could would really works and wouldn't crash. I had a lot of crashes myself before arriving the final code you see above.
Please give me comments and error reports. I'm going to make the code better if something doesn't work well. Maybe I will write a small library which makes the tasks much easier.
Hope you like it :)