Draw Text with Bitmap Fonts
This tutorial is also available as a video. Download the code here. It's bundled as a complete project which opens a graphical window and draws text to the screen, compilable for Windows and Linux. In this tutorial I’ll show you how to draw text to a graphical window using simple bitmap fonts, where each character is pre-drawn in a bitmap. Compared to TrueType fonts, bitmap fonts are simpler to program, and make it easier to create unique text graphics, like Rayman 1. Forgive my lack of artistry! Note: I’m compiling this code with GCC. Note: The bitmap_font.h header #includes "sprite.h" which is my own simple sprite library, including loading .bmp and .tga files and drawing them to the screen. You can use that library, or substitute your own image loading and drawing code.Code Walkthrough
The code provided above loads a bitmap containing the font characters, trims the blank pixels, and loads a properties.txt file containing information about the font. Before we get to that, though, I'll start with a simpler version of bitmap fonts. We'll just load in the font bitmap and draw all the characters with the same width and height, like so: Before we get to the code, a bit of background knowledge. In C, strings are a series of bytes each representing 1 ASCII character, and ending with a 0 byte, or the '\0' or NULL character. Here's an ASCII character table: As you can see, only the characters from 33 through 126 are visible, so we need 94 character sprites. We could store all 94 sprites as individual images to be loaded, but I prefer to store them all in a single image, which will be a 10x10 grid of equally sized character cells, with 6 left unused.#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "sprite.h"
#define BITMAP_FONT_FIRST_VISIBLE_CHAR 33
#define BITMAP_FONT_LAST_VISIBLE_CHAR 126
#define BITMAP_FONT_NUM_VISIBLE_CHARS (BITMAP_FONT_LAST_VISIBLE_CHAR - BITMAP_FONT_FIRST_VISIBLE_CHAR + 1)
typedef struct {
int character_width, character_height;
pixel_t *pixels;
sprite_t bitmaps[BITMAP_FONT_NUM_VISIBLE_CHARS];
} font_t;
// Pass in the font image file, either a targa or 32-bit bitmap with alpha channel
// Returns false on failure and true on success
bool font_Load (font_t *font, char *bitmap_or_targa) {
bool return_value = false;
sprite_t sprite;
if (!SpriteLoad (&sprite, bitmap_or_targa)) {
PRINT_ERROR ("Failed to load font: %s", bitmap_or_targa);
goto LoadReturn;
}
// Convert bitmap to individual character bitmaps
sprite_t *bmp;
int character_width, character_height;
character_width = sprite.width / 10;
character_height = sprite.height / 10;
font->character_width = character_width;
font->character_height = character_height;
font->pixels = (pixel_t*)malloc (character_width * character_height * BITMAP_FONT_NUM_VISIBLE_CHARS * sizeof(pixel_t));
if (!font->pixels) {
PRINT_ERROR ("Failed to allocate %dx%d font pixels", sprite.w, sprite.h);
goto LoadFreeSprite;
}
int x, y;
for (int i = 0; i < BITMAP_FONT_NUM_VISIBLE_CHARS; ++i) {
y = i / 10;
x = i % 10;
bmp = &font->bitmaps[i];
bmp->w = character_width;
bmp->h = character_height;
bmp->p = &font->pixels[i * character_width * character_height];
for (int sy = 0; sy < character_height; ++sy) {
memcpy (&bmp->p[sy * character_width], &sprite.pixels[x * character_width + (y * character_height + sy) * sprite.width], character_width * sizeof(pixel_t));
}
}
return_value = true;
LoadFreeSprite: free (sprite.p);
LoadReturn: return return_value;
}
static inline void font_Free (font_t *font) {
free (font->pixels);
}
void font_Write (font_t *font, sprite_t *destination, int left, int top, char *text) {
char c = *(text++);
int i;
int x = left, y = top - font->character_height; // Current x and y, updated as we draw each character
while (c != '\0') {
switch (c) {
case ' ': x += font->character_width; break;
case '\n': {
x = left;
y -= font->character_height;
} break;
default: {
i = c - BITMAP_FONT_FIRST_VISIBLE_CHAR;
if (i >= 0 && i < BITMAP_FONT_NUM_VISIBLE_CHARS) {
BlitAlpha10 (&font->bitmaps[i], destination, x, y);
x += font->character_width;
} // If character wasn't in the range, then we ignore it.
} break;
}
c = *(text++);
}
}
Let's get into the code.
typedef struct {
int character_width, character_height;
pixel_t *pixels;
sprite_t bitmaps[BITMAP_FONT_NUM_VISIBLE_CHARS];
} font_t;
The font_t type contains:
The width and height of the characters, which will be the same for all characters.
A pointer to the memory which will be allocated to hold the characters' pixel data.
An array of sprite_t structures, one for each character, which can be used for drawing them to the window. Their pixel pointers will point to a section of the font.pixels memory.
// Pass in the font image file, either a targa or 32-bit bitmap with alpha channel
// Returns false on failure and true on success
bool font_Load (font_t *font, char *bitmap_or_targa) {
bool return_value = false;
sprite_t sprite;
if (!SpriteLoad (&sprite, bitmap_or_targa)) {
PRINT_ERROR ("Failed to load font: %s", bitmap_or_targa);
goto LoadReturn;
}
This is the beginning of the font_Load function. You pass in the font structure to be filled out and the bitmap directory/filename. Here I'm using my sprite.h library to load the image. You can use it too, or substitute your own.
Here is the font bitmap I'm using in this tutorial:
// Convert bitmap to individual character bitmaps
sprite_t *bmp;
int character_width, character_height;
character_width = sprite.width / 10;
character_height = sprite.height / 10;
font->character_width = character_width;
font->character_height = character_height;
font->pixels = (pixel_t*)malloc (character_width * character_height * BITMAP_FONT_NUM_VISIBLE_CHARS * sizeof(pixel_t));
if (!font->pixels) {
PRINT_ERROR ("Failed to allocate %dx%d font pixels", sprite.w, sprite.h);
goto LoadFreeSprite;
}
We set the font's character width/height to 1/10th of the font image's width/height, since it's a 10x10 grid of characters. Then we allocate the amount of memory needed to hold all 94 characters.
int x, y;
for (int i = 0; i < BITMAP_FONT_NUM_VISIBLE_CHARS; ++i) {
y = i / 10;
x = i % 10;
bmp = &font->bitmaps[i];
bmp->w = character_width;
bmp->h = character_height;
bmp->p = &font->pixels[i * character_width * character_height];
for (int sy = 0; sy < character_height; ++sy) {
memcpy (&bmp->p[sy * character_width], &sprite.pixels[x * character_width + (y * character_height + sy) * sprite.width], character_width * sizeof(pixel_t));
}
}
return_value = true;
LoadFreeSprite: free (sprite.p);
LoadReturn: return return_value;
}
We loop through the characters, setting up each character sprite's width and height, and pointing to the next section of the font.pixels memory. Then we copy the character from the loaded image, row-by-row, into the font.pixels memory.
Lastly we exit the function. Notice the error handling style, where the initial return value was set to false to signify failure, then only set to true at the end when everything is completed. We also have two goto labels which are used to skip to the end when errors happen.
With that, our font is loaded and just needs to be drawn to the screen.
void font_Write (font_t *font, sprite_t *destination, int left, int top, char *text) {
char c = *(text++);
int i;
int x = left, y = top - font->character_height; // Current x and y, updated as we draw each character
So we pass in the font, the destination sprite, the top-left corner of our drawing area and a C string. The variable, c, will represent the current character, so we dereference the text pointer and post-increment it so that next time we check it we'll get the next character. We also start x at the left, and y at the bottom of the top line. My sprites draw from bottom to top - if yours draw the other way around you might keep y at top.
while (c != '\0') {
switch (c) {
case ' ': x += font->character_width; break;
case '\n': {
x = left;
y -= font->character_height;
} break;
default: {
i = c - BITMAP_FONT_FIRST_VISIBLE_CHAR;
if (i >= 0 && i < BITMAP_FONT_NUM_VISIBLE_CHARS) {
BlitAlpha10 (&font->bitmaps[i], destination, x, y);
x += font->character_width;
} // If character wasn't in the range, then we ignore it.
} break;
}
c = *(text++);
}
}
We loop until we reach the NULL character, represented by '\0'. For every character we switch, checking for the special characters, space and newline. On space we just add a character's width to x, and on newline we reset x to left and subtract the character height from y.
For other characters, we subtract the first visible character from it and see if it lands in the range of 0 to 93. If so, we draw the corresponding character sprite and add the width to x.
At the end of the loop we get the character currently pointed to by text and increment the pointer.
Once we hit the NULL character, the loop will terminate and we will have drawn out all the text.
That's a simple way to draw text in a graphical window. It doesn't look pretty but it gets the job done.
Adding features
Now let's spruce things up by giving the characters their own individual sizes and descent and customizing the font's space width and line height. For reference, we're now exploring the code of bitmap_font.h inside the zip file downloadable at the top of the page.typedef struct {
int line_height;
int space_width;
pixel_t *pixels;
sprite_t bitmaps[BITMAP_FONT_NUM_VISIBLE_CHARS];
int8_t descent[BITMAP_FONT_NUM_VISIBLE_CHARS];
} font_t;
To the font type, I've added line_height and space_width, and an array of descent values, one for each character.
This new font format contains two files inside a folder, so to load the font you now call font_Load() passing the name of the folder, and inside the function we'll figure out the filenames of the font files inside.
// Pass in the folder which contains the font files: font.bmp/tga and properties.txt
// Returns false on failure and true on success
bool font_Load (font_t *font, char *directory) {
bool return_value = false;
int directory_length = strlen (directory);
if (directory_length > 512) {
PRINT_ERROR ("Directory exceeds 512 characters: %s", directory);
goto LoadFontReturn;
}
char folder_name[512];
char last_char = directory[directory_length-1]; // Go back from the NULL
bool ending_slash = false;
if (last_char == '\'' || last_char == '/') {
ending_slash = true;
}
// Copy the directory, adding a slash at the end if it's missing.
char directory_fixed[513];
sprintf (directory_fixed, "%s%c", directory, ending_slash ? '\0' : '/');
So we start at the end of the passed directory string, moving back 1 from the NULL character to the last real character. We check if it's a slash, then copy the directory to a new string buffer, directory_fixed, adding the slash if it was missing. Doing things like this allows you to be less strict about the format of a string passed into the function and makes the code easier to use, so as long as it's not performance-critical I think it's usually worth doing.
char filename[600];
sprintf (filename, "%sfont.bmp", directory_fixed);
sprite_t bitmap;
if (!SpriteLoad (&bitmap, filename)) {
sprintf (filename, "%sfont.tga", directory_fixed);
if (!SpriteLoad (&bitmap, filename)) {
PRINT_ERROR ("Failed to load font bitmap: %s. Attempted both .bmp and .tga.", filename);
goto LoadFontReturn;
}
}
We create a new string buffer a bit longer than the previous ones to make room for the entire directory, plus an extra filename at the end. sprintf allows us to write to a string buffer with the same style and formatting as printf or fprintf. So we use the directory_fixed string from before and add "font.bmp". Then we load font.bmp with a standard image loading function. I decided to also handle targa files, so if we fail to load font.bmp, we try again with font.tga.
// Convert bitmap to individual character bitmaps
sprite_t *bmp;
int width, height;
width = bitmap.width / 10;
height = bitmap.height / 10;
font->space_width = width/2; font->line_height = height + 4; // Just defaults in case they aren't set inside properties.txt
font->pixels = (pixel_t*) malloc (width * height * BITMAP_FONT_NUM_VISIBLE_CHARS * sizeof (pixel_t));
if (!font->pixels) {
PRINT_ERROR ("Failed to allocate font pixels: %s", directory);
free (bitmap.p);
goto LoadFontReturn;
}
Here we get set up for extracting the individual character sprites from the font image. Each character is inside a cell 1/10th the width and height of the image. We allocate the maximum amount of memory the font might need, which is 94 times that 1/10th width and height.
int x, y;
int cumulative_pixels = 0;
for (int i = 0; i < BITMAP_FONT_NUM_VISIBLE_CHARS; ++i) {
y = i / 10;
x = i % 10;
struct {
int left, right, bottom, top, width, height;
} source;
for (int xx = 0; xx < width; ++xx) {
for (int yy = 0; yy < height; ++yy) {
if (bitmap.pixels[(yy + y * height) * bitmap.width + x * width + xx].a != 0) {
source.left = xx;
yy = height;
xx = width;
}
}
}
for (int xx = width - 1; xx >= 0; --xx) {
for (int yy = 0; yy < height; ++yy) {
if (bitmap.pixels[(yy + y * height) * bitmap.width + x * width + xx].a != 0) {
source.right = xx;
yy = height;
xx = -1;
}
}
}
for (int yy = 0; yy < height; ++yy) {
for (int xx = 0; xx < width; ++xx) {
if (bitmap.pixels[(yy + y * height) * bitmap.width + x * width + xx].a != 0) {
source.bottom = yy;
xx = width;
yy = height;
}
}
}
for (int yy = height-1; yy >= 0; --yy) {
for (int xx = 0; xx < width; ++xx) {
if (bitmap.pixels[(yy + y * height) * bitmap.width + x * width + xx].a != 0) {
source.top = yy;
xx = width;
yy = -1;
}
}
}
We loop through all 94 characters, finding the smallest rectangle which contains each character sprite. These four for loops each check a different side: left, right, bottom and top. Taking the first one as an example, we go column by column, starting at the leftmost column of this character's cell, checking each pixel. If we encounter an opaque pixel, we've found the leftmost column and can set source.left to the current column, then set xx and yy such that both for loops will break. The same is done for the other sides.
source.width = source.right - source.left + 1;
source.height = source.top - source.bottom + 1;
bmp = &font->bitmaps[i];
bmp->w = source.width;
bmp->h = source.height;
font->descent[i] = 0;
bmp->p = font->pixels + cumulative_pixels;
for (int sy = 0; sy < source.height; ++sy) {
memcpy (&bmp->p[sy * bmp->w], &bitmap.pixels[(sy + source.bottom + y * height) * bitmap.width + x * width + source.left], bmp->w * sizeof(pixel_t));
}
cumulative_pixels += bmp->width * bmp->height;
}
We calculate the width and height of this character's sprite by subtracting left from right, and bottom from top, and adding one. Then we set the bmp sprite pointer to point to this character's sprite in the font data structure, and assign its width and height. We also set this character's descent to 0 by default.
The character's sprite will point to the next section of font pixel memory, which will initially be the very beginning of said memory. Then we loop through a number of pixel rows equal to the character's height, copying each row from the font bitmap to this character's section of the font pixels. Finally we increase the cumulative_pixels value by the width x height of this character sprite, to index the next blank section of font pixel memory for the next character.
free (bitmap.p);
font->pixels = (pixel_t*)realloc (font->pixels, cumulative_pixels * sizeof (pixel_t));
int pixel_offset = 0;
for (int i = 0; i < BITMAP_FONT_NUM_VISIBLE_CHARS; ++i) {
font->bitmaps[i].pixels = font->pixels + pixel_offset;
pixel_offset += font->bitmaps[i].w * font->bitmaps[i].h;
}
We've copied out the character sprites so we no longer need the font bitmap. Now we reallocate the font pixel memory to be only large enough to fit the cumulative number of pixels of all the character sprites combined. Even when guaranteed to reallocate a smaller - or the same - amount of memory, realloc may still return a pointer to a completely different section of memory, so we loop through all of the character sprites, setting their pixel pointers to point to the correct part of the new font pixel memory.
At this point we've got all of our character sprites trimmed of excess transparent pixels. Now let's load the font properties. These properties will be stored in a text file, "properties.txt" inside the font folder. It will be laid out line-by-line, where most lines begin with a visible character whose properties will then be listed. So for the character j with a descent of 9 pixels, the line will be:
j d9
There will also be one line beginning with a space, followed by the font properties of space width and line height, like so:
w12 h24
You may not be able to see the space there, but you get the point aye? Alright, back to the code.
sprintf (filename, "%sproperties.txt", directory_fixed);
char *contents;
contents = ReadEntireFile (filename, NULL);
if (contents == NULL) {
PRINT_ERROR ("Failed to read file: %s", filename);
goto LoadFontReturn;
}
Just like with font.bmp, we sprintf the directory and add "properties.txt". This file will contain the font's space width and line height, and the descent of any characters that need it to be other than 0.
char *c = contents;
int i;
char property;
int value;
while (*c != '\0') {
We start at the beginning of the file contents, and loop until we hit the null character.
i = *c;
if (i >= BITMAP_FONT_FIRST_VISIBLE_CHAR && i <= BITMAP_FONT_LAST_VISIBLE_CHAR) {
i -= BITMAP_FONT_FIRST_VISIBLE_CHAR;
++c; // Skip the letter to get to the space before first property
// Now we're at the list of properties for this letter [i]
do {
++c;
property = *c;
value = atoi (c+1);
switch (property) {
case 'd': font->descent[i] = value; break;
default: {
PRINT_ERROR ("Reading font \'%s\' encountered invalid property \'%c\' in line of character \'%c\'", directory, property, (char)(i + BITMAP_FONT_FIRST_VISIBLE_CHAR));
goto LoadFontFreeContents;
}
}
c = StringSkipNonWhiteSpace (c); // Skip the property value
} while (*c == ' '); // If it's a space, there's another property
c = StringSkipWhiteSpace (c); // Otherwise we need to get to the next line and the next letter
}
If the present character is one of our visible characters, then this line contains its properties. Here's an example line:
j d9
So we increment c to get past the first character, then loop, reading any listed properties. I've only created one property but I wrote the code in a way that makes it easy to add more. Each iteration we increment past the space, read the character which represents a property, and the next character(s) we convert to a number with atoi. Then we switch on the character. Currently we only have descent, represented by 'd', then the number of pixels to descend.
After reading a property, the c character pointer is still pointing to the property letter (d). So we use this StringSkipNonWhiteSpace function to skip all non-white-space characters. Now c will either be at the end of the line, pointing to a '\n', or there'll be a space followed by another property, so the while checks if we should repeat for another property. If we're done with the line, we skip the whitespace (\n). At this point we proceed to the end of our if else chain and go back to the top of the while (*c != '\0') loop.
If the character was not a visible character, then...
else if (*c == ' ') { // Font properties
do {
++c;
property = *c;
value = atoi (c+1);
switch (property) {
case 'w': font->space_width = value; break;
case 'h': font->line_height = value; break;
default: {
PRINT_ERROR ("Reading font \'%s\' encountered invalid property \'%c\' in line of font properties (line which starts with space).", directory, property);
goto LoadFontFreeContents;
}
}
c = StringSkipNonWhiteSpace (c); // Skip the property value
} while (*c == ' '); // If it's a space, there's another property
c = StringSkipWhiteSpace (c); // Otherwise we need to get to the next line and the next letter
}
If the first character on the line was a space, then this line has our font properties. Very similar to our character property code, we get a property and value, handling 'w' for space width and 'h' for line height.
else {
PRINT_ERROR ("Invalid font file. Format for each line should always be: \"<ascii character> p<property number>\". Error encountered here: %s", c);
goto LoadFontFreeContents;
}
}
Finally, a line starting with any other character is invalid.
return_value = true;
LoadFontFreeContents: free (contents);
LoadFontReturn: return return_value;
}
If we made it all the way through everything, then we can set return_value to true. Happy days! Otherwise, we have these goto labels for early exits when errors are encountered. If you notice that we don't have one for freeing the font image, that's because we can only error in one place between loading and freeing that image, so I just added the free() code in that spot before gotoing. Anyway, we free the properties file contents and return.
So the font bitmap and properties files have been loaded from a folder! Loading was the hard part, and now we just need a couple of updates to the drawing code.
void font_Write (font_t *font, sprite_t *destination, int left, int top, char *text) {
char c = *(text++);
int i;
int x = left, y = top - font->line_height; // Current x and y, updated as we draw each character
while (c != '\0') {
switch (c) {
case ' ': x += font->space_width; break;
case '\n': {
x = left;
y -= font->line_height;
} break;
default: {
i = c - BITMAP_FONT_FIRST_VISIBLE_CHAR;
if (i >= 0 && i < BITMAP_FONT_NUM_VISIBLE_CHARS) {
BlitAlpha10 (&font->bitmaps[i], destination, x, y - font->descent[i]);
x += font->bitmaps[i].width + 1;
} // If character wasn't in the range, then we ignore it.
} break;
}
c = *(text++);
}
}
When calling this function we pass in the top-left corner where the text will be drawn, so x is set to left, but y is top - line_height. In my sprite drawing library, sprites are drawn bottom-up with Cartesian coordinates. If yours are drawn top-down you can just set y to top.
We start going through the text string character-by-character. We handle space and newline with our new font properties, and when drawing a visibile character we increment x by its width + 1.
See? It was all in the loading. The drawing barely needed an update after all that! At this point we've got some pretty natural-looking text drawing to the screen:
One last thing. I created this extra drawing function to automatically handle word wrapping - moving to the next line automatically whenever a word would be too wide to fit a set area.
void font_WriteWrap (font_t *font, sprite_t *destination, int left, int top, int right, char *text) {
char c = *text;
int i;
int x = left, y = top - font->line_height; // Current x and y, updated as we draw each character
bool new_word = true;
while (c != '\0') {
i = c - BITMAP_FONT_FIRST_VISIBLE_CHAR;
if (i >= 0 && i < BITMAP_FONT_NUM_VISIBLE_CHARS) {
if (new_word) {
new_word = false;
int word_width = 0;
char *word = text;
do {
word_width += font->bitmaps[i].width + 1;
i = *(++word) - BITMAP_FONT_FIRST_VISIBLE_CHAR;
} while (i >= 0 && i < BITMAP_FONT_NUM_VISIBLE_CHARS);
i = c - BITMAP_FONT_FIRST_VISIBLE_CHAR;
word_width -= 1; // Don't need the extra pixel on the right of the word
if (x + word_width > right) { // The word is too wide so we need to start the next line
x = left;
y -= font->line_height;
}
}
if (!new_word) {
BlitAlpha10 (&font->bitmaps[i], destination, x, y - font->descent[i]);
x += font->bitmaps[i].width + 1;
}
}
else { // Character is not in the visible set
new_word = true;
switch (c) {
case ' ': {
x += font->space_width;
} break;
case '\n': {
x = left;
y -= font->line_height;
} break;
default: break;
}
}
c = *++text;
}
}
This looks quite a bit more complicated, but it's not so bad. All we're doing is, whenever we hit a new word, we loop through characters until we hit a non-visible character, adding each character's width and finally checking if the current x position + word width exceeds the right edge passed into the function. If it does, we basically do a newline right there before continuing to draw the characters from the start of the word. new_word is initially set to true and is set to false whenever we evaluate the width of a word, then set to true again whenever we hit non-visible characters. That includes ' ' and '\n', but also any other non-visible ASCII character that we don't specifically handle.
Unlike my other tutorials, I won't say that's all there is to rendering text! But with this relatively simple code, you can draw your own very cool fonts and print good-looking text to the screen. If you make a font and use this code to render text in your program I'd love to see it, so feel free to send me an e-mail with the contact link at the bottom of the page!
If you've got questions about any of the code feel free to e-mail me or comment on the youtube video. I'll try to answer them, or someone else might come along and help you out. If you've got any extra tips about how this code can be better or just more useful info about the code, let me know so I can update the tutorial. Cheers.