Categories
Game Development Geek / Technical General

Integrating LodePNG with an SDL Project

In efforts to port Stop That Hero! to the Mac, I ran into a strange issue involving PNG image data.

See, the level layout in “Stop That Hero!” is defined by a 50×33 PNG. The colors of pixels in the PNG correspond to tiles and structures in the game. Grass tiles are represented in the PNG as green pixels, water is blue-green, mountains are gray, and so on.

This PNG (blown up since 50×33 is so tiny):

Level 4 PNG

results in this level layout:

Stop That Hero! Bringing evil back...

I use libSDL_image in order to load image formats other than BMP, and on Windows and GNU/Linux, everything works as expected.

On the Mac, however, I was seeing a problem. It was as if most of the tile data was not getting loaded correctly. Instead of seeing grass, fields, forests, and mountains, I was only seeing mountains. And structure data was also not loading correctly. The player’s castle wasn’t appearing either, so the game always ends in defeat.

After ruling out endian issues (Intel-based Macs aren’t going to require any data-parsing changes from Intel-based Windows or GNU/Linux), I found that the pixel colors being returned from a loaded PNG weren’t what I expected.

I expect red-green-blue(RGB) data to be (0, 255, 0) for grass tiles, but the color that I was seeing was slightly different.

And it turned out that I wasn’t alone. A thread on the libSDL mailing list discussed a similar pixel bug on Mac OS X, and it turned out to be related to libSDL_image’s use of Apple’s ImageIO for the backend. I’m still not quite 100% clear on what the actual problem is, but the best I can figure out is that ImageIO tries to helpfully convert the image you’re loading so that it is more optimized for rendering on the specific Mac running the code. It’s not a problem if all you want to do is render images to the screen, but is a problem if you’re depending on the pixel data to be accurately decoded.

Last week a fix was introduced to solve this issue, but as it isn’t in the release version yet, and I didn’t want to convert my data or change how I was doing things, I decided a better way would be to replace libSDL_image in my own code. Thanks to a conversation on Google+, I was introduced to stb_image and LodePNG, both of which are liberally licensed, comprehensive, PNG-handling modules of code. By comprehensive, I mean that unlike libSDL_image, I don’t also have to require zlib. You just drop in a couple of files into your project, and you’re done.

I opted for LodePNG because unlike stb_image, it not only loads PNGs but also saves them, and I want to make sure I don’t have to switch libraries again when I get around to creating a level editor. Also, quite frankly, it was less intimidating than stb_image being a .c file that leaves the production of the associated .h as an exercise for the programmer.

LodePNG had some examples associated with it, and while one example uses libSDL, it wasn’t clear how to load a PNG into an SDL_Surface. The example simply rendered the PNG to the screen. It was not what I wanted, and I could not find any code out on the Internet that used LodePNG and libSDL together.

So, in the interest of filling this gap, here’s how to load a PNG with LodePNG and store it into an SDL_Surface:

SDL_Surface * loadImage(const char * filename)
{
    //Using LodePNG instead of SDL_image because of bug with Mac OS X
    //that prevents PNGs from being loaded without corruption.

    std::vector<unsigned char> image;
    unsigned width, height;
    unsigned error = LodePNG::decode(image, width, height, filename); //load the image file with given filename

    SDL_Surface * surface = 0;
    if (error == 0)
    {
        Uint32 rmask, gmask, bmask, amask;

#if SDL_BYTEORDER == SDL_BIG_ENDIAN
        rmask = 0xff000000;
        gmask = 0x00ff0000;
        bmask = 0x0000ff00;
        amask = 0x000000ff;
#else
        rmask = 0x000000ff;
        gmask = 0x0000ff00;
        bmask = 0x00ff0000;
        amask = 0xff000000;
#endif
        int depth = 32;
        surface = SDL_CreateRGBSurface(SDL_SWSURFACE, width, height, depth, rmask, gmask, bmask, amask);

        // Lock the surface, then store the pixel data.
        SDL_LockSurface(surface);

        unsigned char * pixelPointer = static_cast<unsigned char *>(surface->pixels);
        for (std::vector<unsigned char>::iterator iter = image.begin();
                    iter != image.end();
                    ++iter)
        {
            *pixelPointer = *iter;
            ++pixelPointer;
        }

        SDL_UnlockSurface(surface);

        SDL_Surface * convertedSurface = SDL_DisplayFormatAlpha(surface);
        if (convertedSurface != NULL)
        {
            SDL_FreeSurface(surface);
            surface = convertedSurface;
        }
    }

    return surface;
}

Technically, the piece of code related to convertedSurface isn’t necessary, but SDL_DisplayFormat and SDL_DisplayFormatAlpha convert the surface to one that is optimized for rendering. And it doesn’t modify the pixel data, which means that if you depend on it for map layout or for doing interesting effects at run-time, it just works, like you expected.

2 replies on “Integrating LodePNG with an SDL Project”

I would opt for the C version of LodePNG (or stb_image), since it would probably interface with SDL better. There would likely be no need to loop through the pixels if you used an array instead of a vector, because one would use SDL_CreateRGBSurfaceFrom(). Your code works fine, of course. Another thing is that this converts all PNGs to 32-bit RGBA, which might be undesired. Anyhow, a new release of SDL 1.2 and affiliated libs is coming very soon.

By the way, did you forget the SDL_ prefix on CreateRGBSurface()?

You might be right, although SDL_CreateRGBSurfaceFrom seems to require that you store the pixel data yourself according to the docs. Since the rest of my code is C++, and this was meant to be wrapper code anyway, I used vectors.

As for the SDL_ prefix, that was a mistake when copying the code from my wrapper to here. I’ll fix it.

Comments are closed.