summaryrefslogtreecommitdiff
path: root/posts/2013-08-17-font-rendering-pango-cairo.skr
blob: 59e0f966aa7889833863599e63054a6adee85678 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
(define guile-2d-url "https://github.com/davexunit/guile-2d")
(define madsy9-url "http://www.reddit.com/user/Madsy9")
(define madsy9-comment-url
  "http://www.reddit.com/r/scheme/comments/1k739l/guile_2d_game_programming_lib_for_scheme/cbmnyuk")
(define pango-url "http://www.pango.org/")
(define cairo-url "http://cairographics.org/")
;; TODO: Move to files.dthompson.us
(define tarball-url "/src/pangocairo.tar.gz")

(post
 :title "Font Rendering in OpenGL with Pango and Cairo"
 :date (make-date* 2013 08 17)
 :tags '("opengl" "pango" "cairo" "font" "wsu")
 :summary "A brief tutorial for rendering fonts in OpenGL with libpangocairo"

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

 (p [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.])

 (p [Let's get the basic SDL and OpenGL initialization out of the way:])

 (source-code
  (c-source
   "#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 ();
}"))

 (p [,(code "create_texture") simply creates an OpenGL texture given an array of
     pixel data and the texture dimensions. Our Cairo surface will use BGRA
     color.])

 (source-code
  (c-source
   "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;
}"))

 (p [,(code "draw_texture") clears the screen, renders a simple textured quad
     using OpenGL’s immediate mode, and then swaps buffers.])

 (source-code
  (c-source
   "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();
}"))

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

 (source-code
  (c-source
   "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);
}"))

 (p [,(code [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 ,(code [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 ,(code [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 ,(code [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.])

 (source-code
  (c-source
   "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;
}"))

 (p [,(code [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
,(code [PANGO_SCALE]) in order to get pixel units.])

 (source-code
  (c-source
   "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;
}"))

 (p [,(code [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.  ,(code [TEXT]) is defined earlier in the program as “The quick
brown fox is so かわいい!”])

 (p [Then we create a ,(code [PangoFontDescription]) object.  This object
represents the font that we want to render.  Earlier in the program,
,(code [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
,(code [*.ttf]) file.])

 (p [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.])

 (p [Finally, we set the font color to white, render the text to the
surface with ,(code [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.])

 (source-code
  (c-source
   "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);
}"))

 (p [,(code [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”.])

 (source-code
  (c-source
   "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;
}"))

 (p [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.])

 (p [You can download the full source code ,(anchor [here] tarball-url).]))