Load a PNG texture

The bbutil API provides a bbutil_load_texture() function to load .png files into a texture to display to the active surface. We'll use the libpng library to load the .png file. In this section, we'll go over this process, which can be modified to suit the image needs.

Read the PNG file

Since we're going to write this in a function, we'll specify some parameters we'll require. We need the filename path of the .png file to load, and a bunch of pointers for what we'll return: texture width, texture height, modified texture coordinates, and a handle to the texture.

int bbutil_load_texture(const char* filename, int* width, int* height, float* tex_x, float* tex_y, unsigned int *tex) {

First, we need to declare a eight byte array for the .png header. We also need to ensure that the texture handle is NULL, so that we're not overwriting an existing texture. Then, we open the .png file as a binary read and read the header.

    int i;
    GLuint format;
    //header for testing if it is a png
    png_byte header[8];

    if (!tex) {
        return EXIT_FAILURE;
    }

    //open file as binary
    FILE *fp = fopen(filename, "rb");
    if (!fp) {
        return EXIT_FAILURE;
    }

    //read the header
    fread(header, 1, 8, fp);

    //test if png
    int is_png = !png_sig_cmp(header, 0, 8);
    if (!is_png) {
        fclose(fp);
        return EXIT_FAILURE;
    }

Now, we need to create the .png structures that will hold our data. We also perform error handling each time we setup the structure. But, if we didn't perform error handling, we need to call the setjmp() function to setup error handling. If we have a direct invocation, setjmp() returns 0, so we can continue on.

    //create png struct
    png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (!png_ptr) {
        fclose(fp);
        return EXIT_FAILURE;
    }

    //create png info struct
    png_infop info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr) {
        png_destroy_read_struct(&png_ptr, (png_infopp) NULL, (png_infopp) NULL);
        fclose(fp);
        return EXIT_FAILURE;
    }

    //create png info struct
    png_infop end_info = png_create_info_struct(png_ptr);
    if (!end_info) {
        png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) NULL);
        fclose(fp);
        return EXIT_FAILURE;
    }

    //setup error handling (required without using custom error handlers above)
    if (setjmp(png_jmpbuf(png_ptr))) {
        png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
        fclose(fp);
        return EXIT_FAILURE;
    }

Next, we initialize the .png read with a call to png_init_io(). We also specify that we've already read the first eight bytes. Then, we read all the information up to the image data. This information contains file details that we need. So, we declare the variables to extract and call the png_get_IHDR() function.

    png_init_io(png_ptr, fp);

    png_set_sig_bytes(png_ptr, 8);

    png_read_info(png_ptr, info_ptr);

    int bit_depth, color_type;
    png_uint_32 image_width, image_height;

    png_get_IHDR(png_ptr, info_ptr, &image_width, &image_height, &bit_depth, &color_type, NULL, NULL, NULL);

We set the format type of the texture based on the color type of the .png file and update the information structure for the transformations that we've requested. We need the row size in bytes to determine the buffer size for the .png, so we obtain it via the png_get_rowbytes() function. With that, we allocate the image buffer.

    switch (color_type)
    {
        case PNG_COLOR_TYPE_RGBA:
            format = GL_RGBA;
            break;
        case PNG_COLOR_TYPE_RGB:
            format = GL_RGB;
            break;
        default:
            fprintf(stderr,"Unsupported PNG color type (%d) for texture: %s", (int)color_type, filename);
            fclose(fp);
            png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
            return NULL;
    }

    png_read_update_info(png_ptr, info_ptr);

    int rowbytes = png_get_rowbytes(png_ptr, info_ptr);

    png_byte *image_data = (png_byte*) malloc(sizeof(png_byte) * rowbytes * image_height);

    if (!image_data) {
        png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
        fclose(fp);
        return EXIT_FAILURE;
    }

We'll use the memory addresses of the image data rows to read the .png. So, we allocate enough space for the row pointers, then set the individual pointers to the correct offset in the image data rows. We call the png_read_image() function, passing the row pointers which will store the entire image data based on those rows.

    png_bytep *row_pointers = (png_bytep*) malloc(sizeof(png_bytep) * image_height);
    if (!row_pointers) {
        //clean up memory and close stuff
        png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
        free(image_data);
        fclose(fp);
        return EXIT_FAILURE;
    }

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

    png_read_image(png_ptr, row_pointers);

Bind the image data to the texture

With the image data ready, we can create our texture. We set the texture width and height to the next power of two from the original image dimensions, so the image data will have a border. We generate and bind the texture to the handle that was passed in.

    int tex_width, tex_height;

    tex_width = nextp2(image_width);
    tex_height = nextp2(image_height);

    glGenTextures(1, tex);
    glBindTexture(GL_TEXTURE_2D, (*tex));

Next, we specify the texture parameters we're going to use. For magnification or minification, we'll use a weighted average of the surrounding pixels. And, specify that the texture wrap parameters to clamp to the edge. We also set the pixel storage mode to unpack data in a byte alignment.

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

Now, we can store the image data in the texture. If the texture width or height don't match the image, we'll set as a subimage. Otherwise, we'll set it as the main texture image.

    if ((tex_width != image_width) || (tex_height != image_height) ) {
        glTexImage2D(GL_TEXTURE_2D, 0, format, tex_width, tex_height, 0, format, GL_UNSIGNED_BYTE, NULL);
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image_width, image_height, format, GL_UNSIGNED_BYTE, image_data);
    } else {
        glTexImage2D(GL_TEXTURE_2D, 0, format, tex_width, tex_height, 0, format, GL_UNSIGNED_BYTE, image_data);
    }

We start cleaning things up now. We'll check for any errors with OpenGL ES, free the memory no longer needed, and destroy any handles we no longer need. If we don't have any errors, we can return the other variables that were passed in: width, height, and modified texture coordinates.

    GLint err = glGetError();

    //clean up memory and close stuff
    png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
    free(image_data);
    free(row_pointers);
    fclose(fp);

    if (err == 0) {
        //Return physical width and height of texture if pointers are not null
        if(width) {
            *width = image_width;
        }
        if (height) {
            *height = image_height;
        }
        //Return modified texture coordinates if pointers are not null
        if(tex_x) {
            *tex_x = ((float) image_width - 0.5f) / ((float)tex_width);
        }
        if(tex_y) {
            *tex_y = ((float) image_height - 0.5f) / ((float)tex_height);
        }
        return EXIT_SUCCESS;
    } else {
        fprintf(stderr, "GL error %i \n", err);
        return EXIT_FAILURE;
    }
}