diff options
Diffstat (limited to 'posts/pangocairo.md')
-rw-r--r-- | posts/pangocairo.md | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/posts/pangocairo.md b/posts/pangocairo.md new file mode 100644 index 0000000..fd651b5 --- /dev/null +++ b/posts/pangocairo.md @@ -0,0 +1,321 @@ +title: Font Rendering in OpenGL with Pango and Cairo +date: 2013-08-17 16:00:00 +tags: opengl, pango, cairo, font, wsu +summary: A brief tutorial for rendering fonts in OpenGL with libpangocairo +--- + +I am working towards a 0.1 release of my game development framework +for GNU Guile, [guile-2d](https://github.com/davexunit/guile-2d). One +of the few remaining blockers on my to-do list is font rendering. A +reddit user, [Madsy9](http://www.reddit.com/user/Madsy9), pointed me +in the right direction with this +[comment](http://www.reddit.com/r/scheme/comments/1k739l/guile_2d_game_programming_lib_for_scheme/cbmnyuk). There +are two libraries needed to perform nice font rendering with proper +internationalization support: [Pango](http://www.pango.org/), “a +library for laying out and rendering of text, with an emphasis on +internationalization,” and [Cairo](http://cairographics.org/), “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: + +```c +#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. + +```c +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. + +```c +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. + +```c +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. + +```c +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. + +```c + +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. + +```c +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". + +```c +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](/src/pangocairo.tar.gz). |