MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


A Simple Mac OS X libpng Example with OpenGL

by Michael J. Norton
10/14/2005

From my previous article, "Developing in OpenGL Using Makefiles," I left you with the means to develop OpenGL code in a cross-platform environment, such as with Mac OS X and Solaris. Well, as my endeavors progressed, I quickly outgrew simple line-drawn polygon examples and wanted to play with more challenging concepts. "What could possibly be more interesting than line-drawn polygons?" you ask. Why, putting texture maps onto them, of course!

My first experiments involved writing a DIB reader. I thought I was being clever by recycling old C code I wrote some time ago for the Microsoft DirectX environment, but there was that whole Endian thing to deal with. On various mailing lists, others recommended image file libraries to use--but these were Mac-OS-dependent. In order to even raise my brow in interest, I needed a library that, one, was absolutely free and, two, had to work cross-platform on the Macintosh and UNIX.

My curiosity was finally satisfied when a fellow programmer recommend I use the open source library libpng, from libpng.org. This library supports the Portable Network Graphics (PNG) format. Long story short, the PNG format is the open source workaround to the patented LZW-compressed GIF format. The GIF format incurs some royalty fees in commercial usage, where, with the PNG format, there are no patent restrictions. PNG is a supported format on the World Wide Web. Commercial applications, such as Adobe Photoshop, also support the PNG file format.

OK, venturing this far into the research, I was sold on the PNG file format. What I needed was to find a way to implement it on my Macintosh and use it with OpenGL.

What You'll Need

As mentioned previously, you can download libpng from its home site, libpng.org. The easiest way to install the libpng library is to use the fink package installer. The fink package installer will install libpng in the directory /sw/lib.

You'll also need the Apple Xcode development system installed, of course, if you are going to compile and build the demonstration program. The last item you will need is a sample 256 by 256, 72dpi texture map PNG file, which I have provided for you.

figure 1
Figure 1. Sample PNG file to use as a texture map

Now you'll need some source code to load the PNG file. Source code listing 1, pngLoad.h, is the header file for the C function pngLoad. Source code listing 2, pngLoad.c, is the C code for the function pngLoad, which is exported to your source code via the header file. As a little disclaimer, I pieced this code together by surfing the internet for PNG coding examples. A good portion of this code comes directly from the O'Reilly book PNG: The Definitive Guide, which is available online in HTML form . Chapter 13 of this book has the meat of what we're after in order to read a PNG file using C code.

Mac OS X Tiger in a Nutshell

Related Reading

Mac OS X Tiger in a Nutshell
A Desktop Quick Reference
By Andy Lester, Chris Stone, Chuck Toporek, Jason McIntosh

Reading a PNG File Using libpng

The following code is a no-frills approach to reading a PNG file using the libpng library. I want the code to work cross-platform, so we'll stick with the simple approach of reading the file and verifying the PNG file format signature. If you have ever worked with different graphic formats, then you're aware of embedded byte headers in the files that identify the type of file being read. The PNG file format is no different. Here's how you open and read the PNG file.

/* Open the PNG file. */
infile = fopen(file, "rb");
if (!infile) {
  return 0;
}

/* Check for the 8-byte signature */
fread(sig, 1, 8, infile);
if (!png_check_sig((unsigned char *) sig, 8)) {
  fclose(infile);
  return 0;
}

Once the file is verified to be a PNG file, you'll begin setting up the libpng data structures.

/* 
 * Set up the PNG structs 
 */
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr) {
  fclose(infile);
  return 4; /* out of memory */
}

info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
  png_destroy_read_struct(&png_ptr, (png_infopp) NULL, (png_infopp) NULL);
  fclose(infile);
  return 4; /* out of memory */
}

end_ptr = png_create_info_struct(png_ptr);
if (!end_ptr) {
  png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) NULL);
  fclose(infile);
  return 4; /* out of memory */
}

We have created three pointers to libpng data structures. These are:

png_structp png_ptr; 
png_infop info_ptr; 
png_infop end_ptr; 

Our png_ptr is used by the libpng library to maintain, basically, state information about the PNG file as it is being read in by the library. It's used for housekeeping by the libpng library. The other two pointers, info_ptr and end_ptr, are used to help us extract data from the PNG file. You'll see png_ptr and info_ptr passed in together as arguments consistently throughout the code once the png_ptr and info_ptr data structures are initialized. The last pointer, end_ptr, is merely a placeholder pointer to data or chunks that may exist after the image data in our file. This is what we have for initializing the pointers and reading the image file data.

png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr) {
  fclose(infile);
  return 4; /* out of memory */
}

info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
  png_destroy_read_struct(&png_ptr, (png_infopp) NULL, (png_infopp) NULL);
  fclose(infile);
  return 4; /* out of memory */
}

end_ptr = png_create_info_struct(png_ptr);
if (!end_ptr) {
  png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) NULL);
  fclose(infile);
  return 4; /* out of memory */
}

/*
 * block to handle libpng errors, 
 * then check whether the PNG file had a bKGD chunk
 */
if (setjmp(png_jmpbuf(png_ptr))) {
  png_destroy_read_struct(&png_ptr, &info_ptr, &end_ptr);
  fclose(infile);
  return 0;
}

/*
 * takes our file stream pointer (infile) and 
 * stores it in the png_ptr struct for later use.
 */
png_ptr->io_ptr = (png_voidp)infile;

/*
 * lets libpng know that we already checked the 8 
 * signature bytes, so it should not expect to find 
 * them at the current file pointer location
 */
png_set_sig_bytes(png_ptr, 8);

That's our code for the data structure initialization. Again, I cloned the lines from Chapter 13.3 of the PNG book, which uses the return 4 /* out of memory */ code.

Let's read in the image info. The code looks like this.

png_read_info(png_ptr, info_ptr);
png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, 
&color_type, NULL, NULL, NULL);
*pwidth = width;
*pheight = height;

/* snipped out the color type code, see source pngLoad.c */
/* Update the png info struct.*/
png_read_update_info(png_ptr, info_ptr);

/* Rowsize in bytes. */
rowbytes = png_get_rowbytes(png_ptr, info_ptr);

/* Allocate the image_data buffer. */
if ((image_data = (unsigned char *) malloc(rowbytes * height))==NULL) {
  png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
  return 4;
}

if ((row_pointers = (png_bytepp)malloc(height*sizeof(png_bytep))) == NULL) {
  png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
  free(image_data);
  image_data = NULL;
   return 4;
}

Now, the most important point of all, this code is to be used for loading PNG files into OpenGL. The code as it's presented will load the PNG file inverted in OpenGL. The OpenGL coordinate system has its origin in the lower-left corner of the texture, where the PNG image has its origin in the upper-left corner. We need to account for this in our code.

/* set the individual row_pointers to point at the correct offsets */
    for (i = 0;  i < height;  ++i)
        row_pointers[height - 1 - i] = image_data + i*rowbytes;
}

That's it for the PNG file loader. Now, let's display this PNG texture using OpenGL.

I tend to be a bit impatient when I experiment with code. Once more, I returned to the internet and looked for readily available source code I could snap my pngLoad function code into. It's the Dr. Frankenstein method of coding; find a dead piece of code you can use and bring it back to life. (Also known as code leveraging, in professional circles.) I stumbled across an excellent tutorial on the internet: "OpenGL Texture Mapping: An Introduction," written by Nate Miller. The source code example is over six years old and uses TGA files (the TARGA file format is used by paint programs) for the original texture map source.

It was time to pimp up this source code and give it some new life with a PNG file loader. Essentially, Nate's source code is a hybrid of the checkerboard-texture-mapping example 9-1 from the book OpenGL Programming. I borrowed a snippet of Nate's code, which sets up the textures and texture vertices, and re-integrated it back into the original example 9-1 source code. Source listing 3, pngDrvr.c, shows the program that demonstrates loading a PNG file.

The C function setupGLTexture is where I leveraged Nate's code to wedge in my PNG loader function. This is what the setupGLTexture code looks like:

int setupGLTexture(char *image, int width, int height, int texName) 
{
  if (image == NULL)
  {
    return 0;
  }
  printf("(loadTexture) width: %d height: %d\n", width, height); 

  /* create a new texture object
   * and bind it to texname (unsigned integer < 0)
   */
  glBindTexture(GL_TEXTURE_2D, texName);
  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, 
         GL_RGB, GL_UNSIGNED_BYTE, image);
  return 1;
}

This C function is called from the OpenGL callback function display() in the pngDrvr.c code.

Building the Example Program

If you have been following along the previous articles, in "Developing in OpenGL Using Makefiles," I develop software in two distinct UNIX environments, Mac OS X (Darwin) and Solaris. To work on OpenGL programming between the two environments, I use makefiles and try to keep the source code as platform-independent as possible.

Here is the makefile for Mac OS X in the UNIX environment.

LIBDIR += -L"/System/Library/Frameworks/OpenGL.framework/Libraries" 

FRAMEWORK = -framework GLUT
FRAMEWORK += -framework OpenGL

COMPILERFLAGS = -Wall 
CC = g++
CFLAGS = $(COMPILERFLAGS)
LIBRARIES = -lGL -lGLU -lm -lobjc -lstdc++ 
LIBRARIES += -L/sw/lib -lpng -lz

SRCS = pngDrvr.c pngLoad.c
OBJECTS = $(SRCS:.c=.o)
All: pngDrvr
 
pngDrvr: $(OBJECTS)
        $(CC) $(FRAMEWORK) $(CFLAGS) -o $@ $(LIBDIR) $(OBJECTS) $(LIBRARIES) 

I need to point out that the last line of the makefile, with <TAB>$CC, indicates an actual physical tab from the text editor. Make that correction before attempting to compile.

There You Have it!

There are pretty much two types of OpenGL programmers: the gurus, who earn a living from their incredible skill sets, and the rest of us. When I started down this road, I noticed that on various forums, there were postings by people like you and me who were looking for source code on how to do this. I know the guru is out there waving a hand: "Blah. It is all so simple." And now, we all know how easy it is. There just wasn't too much info out there demonstrating how simple this really is. Now that you've been informed, go put some textures on those naked polygons you've been drawing, will ya?

Michael J. Norton is a software engineer at Cisco Systems.


Return to the Mac DevCenter

Copyright © 2009 O'Reilly Media, Inc.