Posted by: Morten Nobel-Jørgensen | November 7, 2010

Loading a PNG as texture in OpenGL using libPNG


When learning texture mapping OpenGL most example uses targa (.tga) or PPM (.ppm). Both of these formats are easy to parse, but they are not well supported in modern image editors and they do not compress the data well. An attractive alternative is PNG images. PNG provides lossless image compression and is well supported in all image editors. In this post I’ll show how to load PNG images as OpenGL textures using libPNG.

03-04-2013 Update: I have updated the original post to libPNG 1.5.15 instead of the 1.2.44.

libPNG is the official PNG reference library you can find the sourcecode here: (http://www.libpng.org/pub/png/libpng.html). For my example I use version 1.5.15 (was 1.2.44). libPNG depends on zlib verion 1.2.7 (was 1.0.4) (http://www.zlib.net/).

The PNG file format supports of different modes; roughly speaking the models are: palette modes and RGB modes. In palette mode each pixel contains a pointer (a byte) to a palette table with the RGB color. In RGB mode each pixel contains a RGB (A) value. When loading the texture we are only interested in the RGB mode, and PNG_TRANSFORM_EXPAND flag convert the image to RGB(A).

PNG also supports different bit depths. We are usually only interested in 8 bit and we can use the flags PNG_TRANSFORM_STRIP_16 and PNG_TRANSFORM_PACKING to convert into 8 bit per channel when loading.

Png texture loaded

A last but important thing. The PNG format stores the pixels left-to-right-top-to-bottom (first pixel is in upper left corner), but the OpenGL expects it left-to-right-bottom-to-top (first pixel is in lower left corner). So we must reorder rows.

The program simply loads a texture and render it as a quad. This quad can be rotated using mouse drag. The texture width and height must be a power of 2.

Note: most of the comments in the loadPngImage function is copied from the example.c file from the libPNG source code.

Gist: https://gist.github.com/mortennobel/5299151

libPNG 1.5.15

#ifdef _WIN32
#include <GL/glut.h>
#else
#include <GLUT/glut.h>
#endif
#include <stdlib.h>
#include <stdio.h>
#include <png.h>
#include <iostream>

GLubyte *textureImage;
float rotateX = 0;
float rotateY = 0;

int mouseX;
int mouseY;

bool loadPngImage(char *name, int &outWidth, int &outHeight, bool &outHasAlpha, GLubyte **outData) {
    png_structp png_ptr;
    png_infop info_ptr;
    unsigned int sig_read = 0;
    int color_type, interlace_type;
    FILE *fp;

    if ((fp = fopen(name, "rb")) == NULL)
        return false;

    /* Create and initialize the png_struct
     * with the desired error handler
     * functions.  If you want to use the
     * default stderr and longjump method,
     * you can supply NULL for the last
     * three parameters.  We also supply the
     * the compiler header file version, so
     * that we know if the application
     * was compiled with a compatible version
     * of the library.  REQUIRED
     */
    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
                                     NULL, NULL, NULL);

    if (png_ptr == NULL) {
        fclose(fp);
        return false;
    }

    /* Allocate/initialize the memory
     * for image information.  REQUIRED. */
    info_ptr = png_create_info_struct(png_ptr);
    if (info_ptr == NULL) {
        fclose(fp);
        png_destroy_read_struct(&png_ptr, NULL, NULL);
        return false;
    }

    /* Set error handling if you are
     * using the setjmp/longjmp method
     * (this is the normal method of
     * doing things with libpng).
     * REQUIRED unless you  set up
     * your own error handlers in
     * the png_create_read_struct()
     * earlier.
     */
    if (setjmp(png_jmpbuf(png_ptr))) {
        /* Free all of the memory associated
         * with the png_ptr and info_ptr */
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        fclose(fp);
        /* If we get here, we had a
         * problem reading the file */
        return false;
    }

    /* Set up the output control if
     * you are using standard C streams */
    png_init_io(png_ptr, fp);

    /* If we have already
     * read some of the signature */
    png_set_sig_bytes(png_ptr, sig_read);

    /*
     * If you have enough memory to read
     * in the entire image at once, and
     * you need to specify only
     * transforms that can be controlled
     * with one of the PNG_TRANSFORM_*
     * bits (this presently excludes
     * dithering, filling, setting
     * background, and doing gamma
     * adjustment), then you can read the
     * entire image (including pixels)
     * into the info structure with this
     * call
     *
     * PNG_TRANSFORM_STRIP_16 |
     * PNG_TRANSFORM_PACKING  forces 8 bit
     * PNG_TRANSFORM_EXPAND forces to
     *  expand a palette into RGB
     */
    png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_16 | PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND, NULL);

    png_uint_32 width, height;
    int bit_depth;
    png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type,
                 &interlace_type, NULL, NULL);
    outWidth = width;
    outHeight = height;

    unsigned int row_bytes = png_get_rowbytes(png_ptr, info_ptr);
    *outData = (unsigned char*) malloc(row_bytes * outHeight);

    png_bytepp row_pointers = png_get_rows(png_ptr, info_ptr);

    for (int i = 0; i < outHeight; i++) {
        // note that png is ordered top to
        // bottom, but OpenGL expect it bottom to top
        // so the order or swapped
        memcpy(*outData+(row_bytes * (outHeight-1-i)), row_pointers[i], row_bytes);
    }

    /* Clean up after the read,
     * and free any memory allocated */
    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);

    /* Close the file */
    fclose(fp);

    /* That's it */
    return true;
}

void init(void) {
    glClearColor(0.0, 0.0, 0.0, 0.0);
    glEnable(GL_DEPTH_TEST);
    // The following two lines enable semi transparent
    glEnable(GL_BLEND);
    glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    int width, height;
    bool hasAlpha;
    char filename[] = "logo.png";
    bool success = loadPngImage(filename, width, height, hasAlpha, &textureImage);
    if (!success) {
        std::cout << "Unable to load png file" << std::endl;
        return;
    }
    std::cout << "Image loaded " << width << " " << height << " alpha " << hasAlpha << std::endl;
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexImage2D(GL_TEXTURE_2D, 0, hasAlpha ? 4 : 3, width,
                 height, 0, hasAlpha ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE,
                 textureImage);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glEnable(GL_TEXTURE_2D);
    glShadeModel(GL_FLAT);
}

void display(void) {
    glLoadIdentity();
    glTranslatef(0.0, 0.0, -3.6);
    glRotatef(rotateX, 0,1,0);
    glRotatef(rotateY, 1,0,0);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glBegin(GL_QUADS);
    glTexCoord2f(0.0, 0.0);
    glVertex3f(-2.0, -1.0, 0.0);
    glTexCoord2f(0.0, 1.0);
    glVertex3f(-2.0, 1.0, 0.0);
    glTexCoord2f(1.0, 1.0);
    glVertex3f(0.0, 1.0, 0.0);
    glTexCoord2f(1.0, 0.0);
    glVertex3f(0.0, -1.0, 0.0);

    glEnd();
    glutSwapBuffers();
}

void myReshape(int w, int h) {
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60.0, 1.0 * (GLfloat) w / (GLfloat) h, 1.0, 30.0);
    glMatrixMode(GL_MODELVIEW);
}

void mousePassive(int x, int y){
    mouseX = x;
    mouseY = y;
}

void mouseMotion(int x, int y){
    const float SPEED = 2;

    rotateX += (mouseX-x)/SPEED;
    rotateY += (mouseY-y)/SPEED;
    mousePassive(x, y);
    glutPostRedisplay();
}

int
main(int argc, char** argv) {
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH);
    glutCreateWindow("PNG texture");
    glutMotionFunc(mouseMotion);
    glutPassiveMotionFunc(mousePassive);
    init();
    glutReshapeFunc(myReshape);
    glutDisplayFunc(display);
    std::cout << "Use mouse drag to rotate." << std::endl;
    glutMainLoop();
    return 0;
}

libPNG 1.2.44

#ifdef _WIN32
#include <GL/glut.h>
#else
#include <GLUT/glut.h>
#endif
#include <stdlib.h>
#include <stdio.h>
#include <png.h>
#include <iostream>

GLubyte *textureImage;
float rotateX = 0;
float rotateY = 0;

int mouseX;
int mouseY;

bool loadPngImage(char *name, int &outWidth, int &outHeight, bool &outHasAlpha, GLubyte **outData) {
    png_structp png_ptr;
    png_infop info_ptr;
    unsigned int sig_read = 0;
    int color_type, interlace_type;
    FILE *fp;

    if ((fp = fopen(name, "rb")) == NULL)
        return false;

    /* Create and initialize the png_struct
     * with the desired error handler
     * functions.  If you want to use the
     * default stderr and longjump method,
     * you can supply NULL for the last
     * three parameters.  We also supply the
     * the compiler header file version, so
     * that we know if the application
     * was compiled with a compatible version
     * of the library.  REQUIRED
     */
    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
            NULL, NULL, NULL);

    if (png_ptr == NULL) {
        fclose(fp);
        return false;
    }

    /* Allocate/initialize the memory
     * for image information.  REQUIRED. */
    info_ptr = png_create_info_struct(png_ptr);
    if (info_ptr == NULL) {
        fclose(fp);
        png_destroy_read_struct(&png_ptr, png_infopp_NULL, png_infopp_NULL);
        return false;
    }

    /* Set error handling if you are
     * using the setjmp/longjmp method
     * (this is the normal method of
     * doing things with libpng).
     * REQUIRED unless you  set up
     * your own error handlers in
     * the png_create_read_struct()
     * earlier.
     */
    if (setjmp(png_jmpbuf(png_ptr))) {
        /* Free all of the memory associated
         * with the png_ptr and info_ptr */
        png_destroy_read_struct(&png_ptr, &info_ptr, png_infopp_NULL);
        fclose(fp);
        /* If we get here, we had a
         * problem reading the file */
        return false;
    }

    /* Set up the output control if
     * you are using standard C streams */
    png_init_io(png_ptr, fp);

    /* If we have already
     * read some of the signature */
    png_set_sig_bytes(png_ptr, sig_read);

    /*
     * If you have enough memory to read
     * in the entire image at once, and
     * you need to specify only
     * transforms that can be controlled
     * with one of the PNG_TRANSFORM_*
     * bits (this presently excludes
     * dithering, filling, setting
     * background, and doing gamma
     * adjustment), then you can read the
     * entire image (including pixels)
     * into the info structure with this
     * call
     *
     * PNG_TRANSFORM_STRIP_16 |
     * PNG_TRANSFORM_PACKING  forces 8 bit
     * PNG_TRANSFORM_EXPAND forces to
     *  expand a palette into RGB
     */
    png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_16 | PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND, png_voidp_NULL);

    outWidth = info_ptr->width;
    outHeight = info_ptr->height;
    switch (info_ptr->color_type) {
        case PNG_COLOR_TYPE_RGBA:
            outHasAlpha = true;
            break;
        case PNG_COLOR_TYPE_RGB:
            outHasAlpha = false;
            break;
        default:
            std::cout << "Color type " << info_ptr->color_type << " not supported" << std::endl;
            png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
            fclose(fp);
            return false;
    }
    unsigned int row_bytes = png_get_rowbytes(png_ptr, info_ptr);
    *outData = (unsigned char*) malloc(row_bytes * outHeight);

    png_bytepp row_pointers = png_get_rows(png_ptr, info_ptr);

    for (int i = 0; i < outHeight; i++) {
        // note that png is ordered top to
        // bottom, but OpenGL expect it bottom to top
        // so the order or swapped
        memcpy(*outData+(row_bytes * (outHeight-1-i)), row_pointers[i], row_bytes);
    }

    /* Clean up after the read,
     * and free any memory allocated */
    png_destroy_read_struct(&png_ptr, &info_ptr, png_infopp_NULL);

    /* Close the file */
    fclose(fp);

    /* That's it */
    return true;
}

void init(void) {
    glClearColor(0.0, 0.0, 0.0, 0.0);
    glEnable(GL_DEPTH_TEST);
    // The following two lines enable semi transparent
    glEnable(GL_BLEND);
    glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    int width, height;
    bool hasAlpha;
    char filename[] = "logo.png";
    bool success = loadPngImage(filename, width, height, hasAlpha, &textureImage);
    if (!success) {
        std::cout << "Unable to load png file" << std::endl;
        return;
    }
    std::cout << "Image loaded " << width << " " << height << " alpha " << hasAlpha << std::endl;
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexImage2D(GL_TEXTURE_2D, 0, hasAlpha ? 4 : 3, width,
            height, 0, hasAlpha ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE,
            textureImage);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glEnable(GL_TEXTURE_2D);
    glShadeModel(GL_FLAT);
}

void display(void) {
    glLoadIdentity();
    glTranslatef(0.0, 0.0, -3.6);
    glRotatef(rotateX, 0,1,0);
    glRotatef(rotateY, 1,0,0);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glBegin(GL_QUADS);
    glTexCoord2f(0.0, 0.0);
    glVertex3f(-2.0, -1.0, 0.0);
    glTexCoord2f(0.0, 1.0);
    glVertex3f(-2.0, 1.0, 0.0);
    glTexCoord2f(1.0, 1.0);
    glVertex3f(0.0, 1.0, 0.0);
    glTexCoord2f(1.0, 0.0);
    glVertex3f(0.0, -1.0, 0.0);

    glEnd();
    glutSwapBuffers();
}

void myReshape(int w, int h) {
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60.0, 1.0 * (GLfloat) w / (GLfloat) h, 1.0, 30.0);
    glMatrixMode(GL_MODELVIEW);
}

void mousePassive(int x, int y){
    mouseX = x;
    mouseY = y;
}

void mouseMotion(int x, int y){
    const float SPEED = 2;

    rotateX += (mouseX-x)/SPEED;
    rotateY += (mouseY-y)/SPEED;
    mousePassive(x, y);
    glutPostRedisplay();
}

int
main(int argc, char** argv) {
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH);
    glutCreateWindow("PNG texture");
    glutMotionFunc(mouseMotion);
    glutPassiveMotionFunc(mousePassive);
    init();
    glutReshapeFunc(myReshape);
    glutDisplayFunc(display);
    std::cout << "Use mouse drag to rotate." << std::endl;
    glutMainLoop();
    return 0;
}
Advertisement

Responses

  1. Thanks mate, this solve my problem.

  2. Thanks! Nice and easy. You made my day 🙂

  3. Thanks!

  4. Great! It took a little bit to get working though. Had to change ‘png_infopp_NULL’ (and others like it) to just NULL. Also, changed:
    outWidth = info_ptr->width;
    outHeight = info_ptr->height;
    switch (info_ptr->color_type) …
    to this:
    png_uint_32 width, height;
    int bit_depth;
    png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type,
    &interlace_type, NULL, NULL);
    outWidth = width;
    outHeight = height;

    libpng I guess doesn’t like you access the data in the pointer directly anymore.

    Thanks again!

  5. Hi,
    you’re code worked great until i tried indexed color png’s. The only thing that happens to my oprngl textures is lines showing up on my models. Any thoughts on how to fix it

  6. Thank you for sharing this.

    Is it possible to find a complete package anywhere to get libpng to work with Visual Studio without having to dig up dependencies like zlib?

    I’ve tried to compile a half dozen different “projects” and keep running into missing files, “can’t load zlib.h” one error after another. I cannot find a VS 2010 working project anywhere?

    I don’t understand why, in 2012, such simple operations are still not headache free – can’t we write programs using standard graphics libraries and standard file formats without hours of headaches fighting with dependencies?!?

  7. Ok, sorry I was a bit hasty, adding zlib was easy. Putting your code into a project was a little harder – do you know of simple instructions how to use pnglib with Visual Studio C++ 2010 anywhere?

    Is there some way to set it up such that you only have to add #include in order to use the library in your program? I seemed to have had to add all the various .c files in my project “source files” to get your example code to work – I must be doing more than I really have to?

  8. After lots more googling, I think both LodePNG and SOIL look a lot simpler to use…

  9. This post was awesome to help me finish my own PNG loading routine for OpenGL. I am confused though on why you need:

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    I’ve tried it both with and without this line and it doesn’t look like it has any effect. I figured it covers a case I’m not testing against. Could you explain why that’s needed and what it does?

  10. Thank you,

    I was looking for exactly this. At school they only provided a windows only solution.
    I found a few more png image loaders, but yours is definitely the best.
    It does also not require SDL and uses just libpng, the basic png image decoding library.

    Kind regards,
    Tijs

  11. Great tutorial.

    I would like to say, using something like SDL, SFML and to some extent Qt would be much much simpler for loading PNG’s. Those libraries have a built in image load function which simplifies this process.

  12. how can I get remove this
    error LNK2019: unresolved external symbol _png_create_read_struct referenced in function “bool __cdecl loadPngImage(char *,int &,int &,bool &,unsigned char * *)” (?loadPngImage@@YA_NPADAAH1AA_NPAPAE@Z)
    as I am using visual studio 2012,

    • Hi Morten Nobel-Jørgensen, I have the same problem as Ushna above. Do you know how to fix it? I’m using Visual Studio 2013, and it seems like I have to add:
      #include
      #include
      ..and potentially other .h files. Can you help?

  13. […] to extract 32 bit RGBA data from a PNG file without losing the alpha channel reference. Therefore, some people have been using libpng to extract their OpenGL textures. However, all the examples have required […]

  14. […] to extract 32 bit RGBA data from a PNG file without losing the alpha channel reference. Therefore, some people have been using libpng to extract their OpenGL textures. However, all the examples have required […]

  15. Thanks a lot for the great tutorial, it was really helpful! I have a problem tho, when i load the png it does not load it correctly, it looks like there is a problem with the alpha? The image is all messed up when rendered. Help please? 🙂

    • The author seems to have forgotten to set the value of hasAlpha.

      To know if the PNG has alpha or not, add the following line after phg_get_IHDR:

      outHasAlpha = (color_type == PNG_COLOR_TYPE_RGBA);

      Hopefully this will solve your problems.

      • I think using:

        outHasAlpha = (color_type & PNG_COLOR_MASK_ALPHA);

        is better, as it includes the PNG_COLOR_TYPE_GRAY_ALPHA case.

  16. How to link libpng library because i am getting following errors at compile time

    /tmp/cctwIIT3.o: In function `loadPngImage(char*, int&, int&, bool&, unsigned char**)’:
    pngimport.cpp:(.text+0x58): undefined reference to `png_create_read_struct’
    pngimport.cpp:(.text+0x82): undefined reference to `png_create_info_struct’
    pngimport.cpp:(.text+0xb2): undefined reference to `png_destroy_read_struct’
    pngimport.cpp:(.text+0xea): undefined reference to `png_destroy_read_struct’
    pngimport.cpp:(.text+0x111): undefined reference to `png_init_io’
    pngimport.cpp:(.text+0x123): undefined reference to `png_set_sig_bytes’
    pngimport.cpp:(.text+0x145): undefined reference to `png_read_png’
    pngimport.cpp:(.text+0x18a): undefined reference to `png_get_IHDR’
    pngimport.cpp:(.text+0x1b0): undefined reference to `png_get_rowbytes’
    pngimport.cpp:(.text+0x1dd): undefined reference to `png_get_rows’
    pngimport.cpp:(.text+0x24b): undefined reference to `png_destroy_read_struct’
    collect2: error: ld returned 1 exit status

  17. Great Man.. Thanks..good solution..Thanks a lot

  18. Hi Morten Nobel-Jørgensen, I’m having the same problem as Ushna and Praveen above. I’m using Visual Studio 2013, ported your code as is, and linked the libpng (1.6.16) and zlib (1.2.8) and it gives the following 10 linking errors:

    unresolved external symbol _png_set_sig_bytes
    unresolved external symbol _png_set_longjmp_fn
    unresolved external symbol _png_read_png
    unresolved external symbol _png_init_io
    unresolved external symbol _png_get_rows
    unresolved external symbol _png_get_rowbytes
    unresolved external symbol _png_get_IHDR
    unresolved external symbol _png_destroy_read_struct
    unresolved external symbol _png_create_read_struct
    unresolved external symbol _png_create_info_struct

    All of these unresolved externals are referenced in the function loadPngImage(…). I don’t know what’s going wrong. Please help ASAP.


Leave a Reply to Praveen Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Categories

%d bloggers like this: