Font Rendering in OpenGL with Pango and Cairo

August 17, 2013
Tags:

I am working towards a 0.1 release of my game development framework for GNU Guile, guile-2d. One of the few remaining blockers on my to-do list is font rendering. A reddit user, Madsy9, pointed me in the right direction with this comment. There are two libraries needed to perform nice font rendering with proper internationalization support: Pango, “a library for laying out and rendering of text, with an emphasis on internationalization,” and Cairo, “a 2D graphics library with support for multiple output devices.”

It took me awhile to put together all of the pieces and build a working sample program. The goal of this post is to help others that may be trying to accomplish a similar task that have no prior knowledge of Pango and Cairo. I will assume basic knowledge of C, SDL, and OpenGL throughout this post.

Let’s get the basic SDL and OpenGL initialization out of the way:

#include <pango/pangocairo.h>
#include <SDL.h>
#include <SDL_opengl.h>

#define WINDOW_WIDTH 800
#define WINDOW_HEIGHT 600
#define FONT "Sans Bold 18"
#define TEXT "The quick brown fox is so かわいい!"

void
init_sdl ()
{
    SDL_Init (SDL_INIT_EVERYTHING);
    SDL_SetVideoMode (WINDOW_WIDTH, WINDOW_HEIGHT, 0, SDL_OPENGL);
}

void
init_gl ()
{
    glClearColor (0.0f, 0.0f, 0.0f, 0.0f);
    glDisable (GL_DEPTH_TEST);
    glEnable (GL_BLEND);
    glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable (GL_TEXTURE_2D);
    glViewport (0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
    glMatrixMode (GL_PROJECTION);
    glLoadIdentity ();
    glOrtho (0, WINDOW_WIDTH, WINDOW_HEIGHT, 0, -1, 1);
    glMatrixMode (GL_MODELVIEW);
    glLoadIdentity ();
}

create_texture simply creates an OpenGL texture given an array of pixel data and the texture dimensions. Our Cairo surface will use BGRA color.

unsigned int
create_texture (unsigned int width,
                unsigned int height,
                unsigned char *pixels)
{
    unsigned int texture_id;

    glGenTextures (1, &texture_id);
    glBindTexture (GL_TEXTURE_2D, texture_id);
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexImage2D (GL_TEXTURE_2D,
                  0,
                  GL_RGBA,
                  width,
                  height,
                  0,
                  GL_BGRA,
                  GL_UNSIGNED_BYTE,
                  pixels);

    return texture_id;
}

draw_texture clears the screen, renders a simple textured quad using OpenGL’s immediate mode, and then swaps buffers.

void
draw_texture (int width,
              int height,
              unsigned int texture_id)
{
    /* Render a texture in immediate mode. */
    glMatrixMode (GL_MODELVIEW);
    glLoadIdentity ();
    glClear (GL_COLOR_BUFFER_BIT);
    glPushMatrix ();
    glBindTexture (GL_TEXTURE_2D, texture_id);
    glColor3f (1.f, 1.0f, 1.0f);

    glBegin (GL_QUADS);
    glTexCoord2f (0.0f, 0.0f);
    glVertex2f (0.0f, 0.0f);
    glTexCoord2f (1.0f, 0.0f);
    glVertex2f (width, 0.0f);
    glTexCoord2f (1.0f, 1.0f);
    glVertex2f (width , height);
    glTexCoord2f (0.0f, 1.0f);
    glVertex2f (0.0f, height);
    glEnd ();

    glPopMatrix ();
    SDL_GL_SwapBuffers();
}

create_cairo_context is used to make a new Cairo context that draws to a raw data surface. The return value, a cairo_t, is the main object in Cairo. All drawing is done via a cairo_t object. A context needs a surface to draw on. cairo_image_surface_create_for_data creates a raw data surface for us. We will be translating the surface into a texture later on.

cairo_t*
create_cairo_context (int width,
                      int height,
                      int channels,
                      cairo_surface_t** surf,
                      unsigned char** buffer)
{
    *buffer = calloc (channels * width * height, sizeof (unsigned char));
    *surf = cairo_image_surface_create_for_data (*buffer,
                                                 CAIRO_FORMAT_ARGB32,
                                                 width,
                                                 height,
                                                 channels * width);
    return cairo_create (*surf);
}

create_layout_context also makes a new Cairo context, but this context is for PangoLayout objects. In Pango, a layout describes the style of a paragraph of text. The layout needs a context in order to function. We use cairo_image_surface_create with dimensions of 0x0 because we won’t actually be rendering to this surface. Instead, we will layout our text and use create_cairo_context to build a context with a surface that is the size of the rendered text. Cairo uses reference counting for dynamically allocated objects, so we need to call cairo_surface_destroy when we’re done with the temporary surface. The context still maintains a reference to the surface, so the memory for the surface will not be freed until the context is.

cairo_t*
create_layout_context ()
{
    cairo_surface_t *temp_surface;
    cairo_t *context;

    temp_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 0, 0);
    context = cairo_create (temp_surface);
    cairo_surface_destroy (temp_surface);

    return context;
}

get_text_size tells us the size of the text that’s in the layout, in pixels. Pango’s units are not in pixels, so we must divide by PANGO_SCALE in order to get pixel units.


void
get_text_size (PangoLayout *layout,
                unsigned int *width,
                unsigned int *height)
{
    pango_layout_get_size (layout, width, height);
    /* Divide by pango scale to get dimensions in pixels. */
    *width /= PANGO_SCALE;
    *height /= PANGO_SCALE;
}

render_text is where all of the magic happens. First, we create a layout with a layout context and set the text that we will render with this layout. TEXT is defined earlier in the program as "The quick brown fox is so かわいい!"

Then we create a PangoFontDescription object. This object represents the font that we want to render. Earlier in the program, FONT is defined as "Sans Bold 18". Pango is able to figure out how to load a font from a string in this format. Your system must be able to recognize the font family and font face, though. I haven’t yet figured out how to have Pango render an arbitrary font from a *.ttf file.

Next, we create a rendering context by getting the layout’s size and creating a context with a surface big enough to show all of the rendered text.

Finally, we set the font color to white, render the text to the surface with pango_cairo_show_layout, and create an OpenGL texture from the surface. We also clean up all the objects that we no longer need before returning.

unsigned int
render_text (const char *text,
             unsigned int *text_width,
             unsigned int *text_height,
             unsigned int *texture_id)
{
    cairo_t *layout_context;
    cairo_t *render_context;
    cairo_surface_t *temp_surface;
    cairo_surface_t *surface;
    unsigned char* surface_data = NULL;
    PangoFontDescription *desc;
    PangoLayout *layout;

    layout_context = create_layout_context ();

    /* Create a PangoLayout, set the font and text */
    layout = pango_cairo_create_layout (layout_context);
    pango_layout_set_text (layout, text, -1);

    /* Load the font */
    desc = pango_font_description_from_string (FONT);
    pango_layout_set_font_description (layout, desc);
    pango_font_description_free (desc);

    /* Get text dimensions and create a context to render to */
    get_text_size (layout, text_width, text_height);
    render_context = create_cairo_context (*text_width,
                                           *text_height,
                                           4,
                                           &surface,
                                           &surface_data);

    /* Render */
    cairo_set_source_rgba (render_context, 1, 1, 1, 1);
    pango_cairo_show_layout (render_context, layout);
    *texture_id = create_texture(*text_width, *text_height, surface_data);

    /* Clean up */
    free (surface_data);
    g_object_unref (layout);
    cairo_destroy (layout_context);
    cairo_destroy (render_context);
    cairo_surface_destroy (surface);
}

main is pretty simple. We initialize SDL and OpenGL, render text to a texture, and enter the rendering loop. The program will run until you click the close button, press "enter", or press "q".

int main (int argc, char **argv)
{
    SDL_Event event;
    int keep_running = 1;
    unsigned int texture_id;
    unsigned int text_width = 0;
    unsigned int text_height = 0;

    init_sdl ();
    init_gl ();
    render_text(TEXT,
                &texture_id,
                &text_width,
                &text_height);

    /* Update/render loop */
    while (keep_running) {
        SDL_PollEvent (&event);

        switch (event.type) {
        case SDL_QUIT :
            keep_running = 0;
            break;

        case SDL_KEYDOWN :
            if (event.key.keysym.sym == SDLK_ESCAPE)
                keep_running = 0;
            if (event.key.keysym.sym == SDLK_q)
                keep_running = 0;
            break;
        }

        draw_texture (texture_id, text_width, text_height);
        SDL_Delay (16);
    }

    /* Clean up */
    glDeleteTextures (1, &texture_id);

    SDL_Quit();

    return 0;
}

And we’re done! You should now be able to render some text in an OpenGL context. I hope this brief tutorial was helpful. Font rendering isn’t easy, and it’s not really my area of interest. I’m glad that Pango exists to do all of the real work for me so that I can more quickly move on to the parts of graphics programming that I actually enjoy.

You can download the full source code here.