Storing information in a .png

by Malte Skarupke

Spore did a neat thing where they stored the creature information in a .png file that was a picture taken of the creature. So to share the creature you had to share the image, which is pretty cool. And it turns out that it’s really easy to do. Well if you are already using libpng that is.

I implemented it for the game Leshy, but we only used the feature for the auto save functionality of the editor, because the game didn’t have loading and saving. (simply because it would have been too much effort to program a menu that allows you to load and save)

Autosave file for Leshy editor

Auto save file for the Leshy editor. This is the actual save file. I could load the level from this image.

People seem to really like this. I mean it is pretty cool that a screenshot is a save file. You could drag and drop a screenshot onto your game and you’d be there instantly.

Since all our level data is in a text format this also gave us compression for free, which I didn’t have implemented before. The save data for the finished game would only add something like 30kb to the image.

If we have saving and loading in our next game I’m simply going to put the level data into each screenshot. So that any screenshot taken with the built-in screenshot feature can be dragged (ideally directly out of the browser) into the game and you’d be there.

If you are a small indie developer whose save files are also just a couple kilobytes, I’d encourage you to do the same. It’d be really cool if this became a standard feature.

Below is the complete source code. There is “loadPng” which can read the text from the png file. Then there is “savePng” which takes a screenshot from the given buffer (usually GL_FRONT) and saves it. And then there is “isLevelPng” which checks whether the image contains save information.

The source code is pretty ugly, but that’s mostly because I’m using libpng. I know several ways to make this code slightly more pretty (like getting rid of copy+paste) but since I’m using libpng it could never be really good code, so I didn’t even bother. I’m using libpng version 1.2.44 simply because that’s a version that I was familiar with. I hear that the newer versions are much better but this works.

const string levelDataKey = "levelData";

ImgInfo loadPng(const string & fileName, string * levelData)
{
    string actualFileName = tryAllReadFolders(fileName);
    FILE * file = fopen(actualFileName.c_str(), "rb");
    assert(file, "Could not open file " << actualFileName.c_str() << ": " << strerror(errno));

    /* make sure that it is a png file */
    png_byte header[8];
    assert(fread(header, 1, 8, file) == 8, L"file " << actualFileName.c_str() << L" is not a PNG file");
    assert(png_check_sig(header, 8), L"File " << actualFileName.c_str() << L" is not a PNG file");

    /* initialize libPNG read and info struct */
    png_structp pngStruct = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    assert(pngStruct, L"libPNG doesn't work");
    png_infop pngInfo = png_create_info_struct(pngStruct);
    if (!pngInfo)
    {
        png_destroy_read_struct(&pngStruct, NULL, NULL);
        assert(pngInfo, L"libPNG doesn't work");
    }

    /* initialize libPNG error handling */
    if (setjmp(pngStruct->jmpbuf))
    {
        png_destroy_read_struct(&pngStruct, &pngInfo, NULL);
        assert(false, "something went wrong while reading file " << fileName.c_str());
    }

	/* start with input output */
	png_init_io(pngStruct, file);
	/* tell the library that we read the initial 8 bytes */
	png_set_sig_bytes(pngStruct, 8);

	/* make the library read everything */
	png_read_png(pngStruct, pngInfo, PNG_TRANSFORM_IDENTITY, NULL);

	/* get some info about the png */
	png_uint_32 width;
	png_uint_32 height;
	int bitDepth;
	int colorType;
	png_get_IHDR(pngStruct, pngInfo, &width, &height, &bitDepth,
	&colorType, NULL, NULL, NULL);

	/* assert that the info is correct */
	assert(bitDepth == 8, "image " << actualFileName.c_str() << " is not a png file with 8 bit color depth");
	GLenum format = GL_RGBA;
	int bytesPerPixel = 0;
	switch(colorType)
	{
	case PNG_COLOR_TYPE_RGB:
		bytesPerPixel = 3;
		format = GL_RGB;
		break;
	case PNG_COLOR_TYPE_RGB_ALPHA:
		bytesPerPixel = 4;
		format = GL_RGBA;
		break;
	case PNG_COLOR_TYPE_GRAY:
		bytesPerPixel = 1;
		format = GL_LUMINANCE;
		break;
	case PNG_COLOR_TYPE_GRAY_ALPHA:
		bytesPerPixel = 2;
		format = GL_LUMINANCE_ALPHA;
		break;
	default:
		assert(false, "image " << actualFileName.c_str() << " has an unrecognized color format. please make it an RGBA or grayscale png");
		break;
	}

	/* get the actual image data */
	png_bytepp data = png_get_rows(pngStruct, pngInfo);

	/* convert image data into the format used by glTexImage2D */
	png_uint_32 bytesPerRow = png_get_rowbytes(pngStruct, pngInfo);
	GLubyte * dataForGL = new GLubyte[bytesPerRow * height];
	assert(dataForGL, "could not allocate memory (" << bytesPerRow * height << " bytes)");
	for (unsigned i = 0; i < height; ++i)
	{
		memcpy(dataForGL + (i * bytesPerRow), data[(height - 1) - i], bytesPerRow);
	}

	if (levelData)
	{
		/* get the levelData, if it was stored in the png */
		png_text * png_levelData;
		int numTexts;
		png_get_text(pngStruct, pngInfo, &png_levelData, &numTexts);
		for (int i = 0; i < numTexts; ++i)
		{
			if (levelDataKey == png_levelData[i].key)
			{
				*levelData = png_levelData[i].text;
				break;
			}
		}
	}

	ImgInfo info(dataForGL, width, height, bytesPerPixel, format);

	/* clean up libPNGs data */
	png_destroy_read_struct(&pngStruct, &pngInfo, NULL);

	fclose(file);
	return info;
}

void savePng(GLenum readBuffer, const string & fileName, const string & levelData)
{
	FILE * file = fopen(fileName.c_str(), "wb");
	debugAssert(file, "could not open the file \"" << fileName.c_str() << "\" for writing.");
	if (!file)
	{
		return;
	}

	png_structp write_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
	assert(write_ptr, "could not create the png write pointer for some reason");
	png_infop info_ptr = png_create_info_struct(write_ptr);
	assert(info_ptr, "could not create the info struct for some reason");

	if (setjmp(png_jmpbuf(write_ptr)))
	{
		png_destroy_write_struct(&write_ptr, &info_ptr);
		fclose(file);
		assert(false, "libpng had an error");
	}

	png_init_io(write_ptr, file);

	int viewPort[4];
	glGetIntegerv(GL_VIEWPORT, viewPort);

	png_set_IHDR(write_ptr, info_ptr, viewPort[2], viewPort[3], 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_ADAM7, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);

	const int bytesPerRow = viewPort[2] * 3;
	glReadBuffer(readBuffer);
	GLubyte * glData = new GLubyte[bytesPerRow * viewPort[3]];
	glPixelTransferf(GL_RED_BIAS, 0.0f);
	glPixelTransferf(GL_GREEN_BIAS, 0.0f);
	glPixelTransferf(GL_BLUE_BIAS, 0.0f);
	glPixelTransferf(GL_ALPHA_BIAS, 0.0f);
	// disable the alignment for rows on 4 byte boundaries
	glPixelStorei(GL_PACK_ALIGNMENT, 1);
	glReadPixels(viewPort[0], viewPort[1], viewPort[2], viewPort[3], GL_RGB, GL_UNSIGNED_BYTE, glData);

	png_bytepp data = new png_bytep[viewPort[3]];
	for (int i = 0; i < viewPort[3]; ++i)
	{
		data[i] = new png_byte[bytesPerRow];
		memcpy(data[i], glData + bytesPerRow * (viewPort[3] - (i + 1)), bytesPerRow);
	}

	png_text png_levelData;
	png_levelData.key = const_cast<png_charp>(reinterpret_cast<const char *>(levelDataKey.c_str()));
	png_levelData.compression = PNG_TEXT_COMPRESSION_zTXt;
	png_levelData.text = const_cast<png_charp>(reinterpret_cast<const char *>(levelData.c_str()));
	png_levelData.text_length = levelData.length() * sizeof(levelData[0]);
	if (levelData.length() > 0)
	{
		png_set_text(write_ptr, info_ptr, &png_levelData, 1);
	}

	png_write_info(write_ptr, info_ptr);
	png_write_image(write_ptr, data);
	png_write_end(write_ptr, info_ptr);

	for (int i = 0; i < viewPort[3]; ++i)
	{
		delete[] data[i];
	}
	delete[] data;
	delete[] glData;
	png_destroy_write_struct(&write_ptr, &info_ptr);
	fclose(file);
}

bool isLevelPng(const string & fileName)
{
	string actualFileName = tryAllReadFolders(fileName);
	FILE * file = fopen(actualFileName.c_str(), "rb");
	if (!file) return false;

	/* check if it's a png file */
	png_byte header[8];
	bool isPng = fread(header, 1, 8, file) == 8 && png_check_sig(header, 8);
	if (!isPng)
	{
		fclose(file);
		return false;
	}

	/* initialize libPNG read and info struct */
	png_structp pngStruct = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
	assert(pngStruct, L"libPNG doesn't work");
	png_infop pngInfo = png_create_info_struct(pngStruct);
	if (!pngInfo)
	{
		png_destroy_read_struct(&pngStruct, NULL, NULL);
		assert(pngInfo, L"libPNG doesn't work");
	}

	/* initialize libPNG error handling */
	if (setjmp(pngStruct->jmpbuf))
	{
		png_destroy_read_struct(&pngStruct, &pngInfo, NULL);
		assert(false, "something went wrong while reading file " << fileName.c_str());
	}

	/* start with input output */
	png_init_io(pngStruct, file);
	/* tell the library that we read the initial 8 bytes */
	png_set_sig_bytes(pngStruct, 8);

	png_read_info(pngStruct, pngInfo);

	/* get the levelData, if it was stored in the png */
	png_text * png_levelData;
	int numTexts;
	png_get_text(pngStruct, pngInfo, &png_levelData, &numTexts);
	bool found = false;
	for (int i = 0; i < numTexts; ++i)
	{
		if (levelDataKey == png_levelData[i].key)
		{
			found = true;
			break;
		}
	}

	png_destroy_read_struct(&pngStruct, &pngInfo);
	fclose(file);
	return found;
}