Sunday, December 9, 2012

Font Rendering with Freetype and Libpng (Part 1)

Namaste,

long time, no see. Not that anyone is reading this stuff, but I say it anyway: I'm still alive and this blog is definitely not dead :-)

So, without further ado, let's talk about some codez.
Today I want to show a simple example of rendering a character string using freetype and libpng. The codez are also available on github: https://github.com/sendtehcodez/font-rendering/tree/part1. Of course, it's free to use for any purpose and hereby released under the terms of the WTFPL license. Yay!

Note that I will use gcc for this exercise and I will compile this on (X)ubuntu linux. For bonus points I'm going to cross-compile it for Windows as well, but I'm going to cheat and use mingw-w64 which makes it so easy that even I can manage it. It's going to be glorious.
And, just so you know, because I'm apparently a crazy masochist I'm actually doing this in a virtual machine running on Windows 7. But that's just how I roll.

So, enough drivel, let's code the codez. My project directory structure is going to look like this:
font-render/         # the project root directory
    include/         # I will put include files from libs here
    libs/
        linux64/     # this will contain the libs for linux64
        win64/       # here we put the win64 libs
    render-text.c    # our codez
    arial.ttf        # just a random font I copied for testing so I 
                     # don't have to specify a long path to 
                     # /usr/share/fonts/... every time
To start off, let's create a simple stub for text-render.c:
#include <stdio.h>

int main(int argc, char **argv) {
  puts("booyah");
} 
Exciting, isn't it? I'm going to compile this:
$ gcc -Wall -pedantic -std=c99 render-text.c -o render-text
$ ./render-text
booyah
OK, so it works. I like repeatable builds and standalone executables and freetype and libpng don't ship with Windows anyway so I'm going to download the source and build a nice static library to link into my executable.
$ wget http://download.savannah.gnu.org/releases/freetype/freetype-2.4.10.tar.bz2
$ tar xjf freetype-2.4.10.tar.bz2
$ cd freetype-2.4.10
$ ./configure --disable-shared && make
$ cp objs/.libs/libfreetype.a ../font-render/libs/linux32/
$ cp -R include/* ../font-render/include/
So far so good. Let's actually load a font file now. To make life easier let's quickly implement some simple argument handling:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef struct {
  char *font_file;
  char *text;
  int size;
  int anti_alias;
} conf_t;

char *parse_args(int argc, char **argv, conf_t *conf) {
  memset(conf, 0, sizeof(conf_t));
  conf->size = 72;
  if (argc < 2) {
    return "missing font file argument";
  } else if (argc < 3) {
    return "missing text argument";
  }
  conf->font_file = argv[1];
  conf->text = argv[2];
  if (argc > 3) {
    conf->size = atoi(argv[3]);
  }
  if (argc > 4) {
    conf->anti_alias = strcmp(argv[4], "yes") == 0;
  }
  return NULL;
}

int main(int argc, char **argv) {
  conf_t conf;
  char *conf_err = parse_args(argc, argv, &conf);
  if (conf_err != NULL) {
    printf("Error: %s\n", conf_err);
    puts("Usage: render-text <font> <text> [<size> [<anti-alias>]]");
    puts("Example: render-text myfont.ttf foobar 24 yes");
    puts("         render-text myfont.ttf lorem");
    return 1;
  }

  printf("font: %s, text: %s, size: %d, anti-alias: %s\n",
         conf.font_file,
         conf.text,
         conf.size,
         conf.anti_alias ? "yes" : "no");

  return 0;
}
Recompiling this and testing it out it should work fine.
$ ./render-text 
Error: missing font file argument
Usage: render-text <font> <text> [<size> [<anti-alias>]]
Example: render-text myfont.ttf foobar 24 yes
         render-text myfont.ttf lorem
$ ./render-text foo.ttf blabla
font: foo.ttf, text: blabla, size: 72, anti-alias: no


Rendering a Glyph with Freetype

So let's add freetype into the mix. Add this to the includes:
#include <ft2build.h>
#include FT_FREETYPE_H
As a first step, let's just render the first character of the text. To keep it simple I put everything into a single function. This will be refactored when we render more than a single character. For error handling I simply return NULL for success and an error string on failure.
char *render_glyph(FT_Face *face, conf_t conf) {
  FT_Library ft;
  FT_Error err;

  err = FT_Init_FreeType(&ft);
  if (err) return "freetype init error";

  err = FT_New_Face(ft, conf.font_file, 0, face);
  if (err == FT_Err_Unknown_File_Format)
    return "unknown font file format";
  else if (err)
    return "error reading font file";

  err = FT_Set_Pixel_Sizes(*face, 0, conf.size);
  if (err) return "error setting font size";

  FT_UInt index = FT_Get_Char_Index(*face, *conf.text);
  if (index == 0) return "no glyph found for char";

  err = FT_Load_Glyph(*face, index, FT_LOAD_DEFAULT);
  if (err) return "error loading glyph";

  err = FT_Render_Glyph((*face)->glyph, conf.anti_alias ? 
                        FT_RENDER_MODE_NORMAL : 
                        FT_RENDER_MODE_MONO);
  if (err) return "error rendering glyph";

  return NULL;
}

Now we can use render_glyph in the main function, just add this at the end (before the return of course):
  FT_Face face;
  char *ft_err = render_glyph(&face, conf);
  if (ft_err != NULL) {
    printf("freetype error: %s\n", ft_err);
    return 2;
  }

  printf("bitmap rows: %d, width: %d\n", 
         face->glyph->bitmap.rows,
         face->glyph->bitmap.width);

When recompiling make sure to give it the correct paths for includes and libs. For example:
$ gcc -Wall -pedantic -std=c99 -Iinclude \
  render-text.c -Llibs/linux64 -lfreetype -o render-text

$ ./render-text arial.ttf blabla
font: arial.ttf, text: blabla, size: 72, anti-alias: no
bitmap rows: 53, width: 33

$ ./render-text arial.ttf blabla 200
font: arial.ttf, text: blabla, size: 200, anti-alias: no
bitmap rows: 145, width: 90

Alrighty. Looks good so far. Now we just need to write a PNG with the bitmap data.

Writing the PNG

Let's build zlib:
$ wget http://prdownloads.sourceforge.net/libpng/zlib-1.2.7.tar.gz?download -O zlib-1.2.7.tar.gz
$ tar xzf zlib-1.2.7.tar.gz
$ cd zlib-1.2.7
$ ./configure --static && make
$ cp libz.a ../font-render/libs/linux64/
$ cp zlib.h ../font-render/include/
And then libpng:
$ wget http://prdownloads.sourceforge.net/libpng/libpng-1.5.13.tar.gz?download -O libpng-1.5.13.tar.gz
$ tar xzf libpng-1.5.13.tar.gz
$ cd libpng-1.5.13
$ ./configure CFLAGS='-L../zlib-1.2.7' --disable-shared
$ make CFLAGS='-I../zlib-1.2.7 -L../zlib-1.2.7'
$ cp .libs/libpng15.a ../font-render/libs/linux64/
$ cp png*.h ../font-render/include/
Now we're ready to use libpng. Behold, the glorious render_png function:
char *render_png(FT_Face face, char *out, int aa) {
  FILE *f = fopen(out, "wb");
  if (!f) return "failed to open output file";

  png_structp png_out = png_create_write_struct(
    PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
  if (!png_out) return "failed to create png write struct";
  png_infop png_info = png_create_info_struct(png_out);
  if (!png_info)
    return "failed to create png info struct";

  if (setjmp(png_jmpbuf(png_out)))
    return "png init io error";
  png_init_io(png_out, f);

  if (setjmp(png_jmpbuf(png_out)))
    return "IHDR write error";
  png_set_IHDR(png_out,
               png_info,
               face->glyph->bitmap.width,
               face->glyph->bitmap.rows,
               aa ? 8 : 1,
               PNG_COLOR_TYPE_GRAY,
               PNG_INTERLACE_NONE,
               PNG_COMPRESSION_TYPE_DEFAULT,
               PNG_FILTER_TYPE_DEFAULT);
  png_write_info(png_out, png_info);

  if (setjmp(png_jmpbuf(png_out)))
    return "png write error";

  for (int i = 0; i < face->glyph->bitmap.rows; ++i) {
    const unsigned char *rowptr = face->glyph->bitmap.buffer +
      (face->glyph->bitmap.pitch * i);
    png_write_row(png_out, rowptr);
  }

  if (setjmp(png_jmpbuf(png_out)))
    return "png end error";
  png_write_end(png_out, NULL);

  fclose(f);
  return NULL;
}

Note that this is not properly cleaning up in case of errors because I was too lazy to do it The Right WayTM but for this program it doesn't really matter anyway. I'm also using the same lazy error handling technique as before. Oh, and don't forget to include the "png.h" header file which isn't shown here.

Now we just need to use that function in main. This is the complete main function at this point:
int main(int argc, char **argv) {
  conf_t conf;
  char *conf_err = parse_args(argc, argv, &conf);
  if (conf_err != NULL) {
    printf("Error: %s\n", conf_err);
    puts("Usage: render-text <font> <text> [<size> [<anti-alias>]]");
    puts("Example: render-text myfont.ttf foobar 24 yes");
    puts("         render-text myfont.ttf lorem");
    return 1;
  }

  printf("font: %s, text: %s, size: %d, anti-alias: %s\n",
         conf.font_file,
         conf.text,
         conf.size,
         conf.anti_alias ? "yes" : "no");

  FT_Face face;
  char *ft_err = render_glyph(&face, conf);
  if (ft_err != NULL) {
    printf("freetype error: %s\n", ft_err);
    return 2;
  }

  printf("bitmap rows: %d, width: %d\n",
         face->glyph->bitmap.rows,
         face->glyph->bitmap.width);

  char *png_err = render_png(face, "a.png", conf.anti_alias);
  if (png_err != NULL) {
    printf("png error: %s\n", png_err);
    return 3;
  }

  return 0;
}

To compile:
$ gcc -Wall -pedantic -std=c99 -Iinclude/ render-text.c -o render-text -Llibs/linux64 -lfreetype -lpng15 -lz -lm
If you execute the result with the proper arguments it should successfully create a file called "a.png" in the current directory. I leave it as an exercise for the reader to replace the hardcoded output file name with a command line argument.

So Long and Thanks for All the Codez

That's it for this first part. Next time (soon!) I'll show how to cross-compile this program for Windows with mingw-w64 and how to render multiple characters. If you spot any errors, have any suggestions or just want to yell at me, feel free to leave a comment below.

Here's the complete code (and again, you can find the complete project on github here: https://github.com/sendtehcodez/font-rendering/tree/part1):
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <ft2build.h>
#include FT_FREETYPE_H

#include <png.h>

typedef struct {
  char *font_file;
  char *text;
  int size;
  int anti_alias;
} conf_t;

char *parse_args(int argc, char **argv, conf_t *conf) {
  memset(conf, 0, sizeof(conf_t));
  conf->size = 72;
  if (argc < 2) {
    return "missing font file argument";
  } else if (argc < 3) {
    return "missing text argument";
  }
  conf->font_file = argv[1];
  conf->text = argv[2];
  if (argc > 3) {
    conf->size = atoi(argv[3]);
  }
  if (argc > 4) {
    conf->anti_alias = strcmp(argv[4], "yes") == 0;
  }
  return NULL;
}

char *render_glyph(FT_Face *face, conf_t conf) {
  FT_Library ft;
  FT_Error err;

  err = FT_Init_FreeType(&ft);
  if (err) return "freetype init error";

  err = FT_New_Face(ft, conf.font_file, 0, face);
  if (err == FT_Err_Unknown_File_Format)
    return "unknown font file format";
  else if (err)
    return "error reading font file";

  err = FT_Set_Pixel_Sizes(*face, 0, conf.size);
  if (err) return "error setting font size";

  FT_UInt index = FT_Get_Char_Index(*face, *conf.text);
  if (index == 0) return "no glyph found for char";

  err = FT_Load_Glyph(*face, index, FT_LOAD_DEFAULT);
  if (err) return "error loading glyph";

  err = FT_Render_Glyph((*face)->glyph, conf.anti_alias ?
                        FT_RENDER_MODE_NORMAL :
                        FT_RENDER_MODE_MONO);
  if (err) return "error rendering glyph";

  return NULL;
}

char *render_png(FT_Face face, char *out, int aa) {
  FILE *f = fopen(out, "wb");
  if (!f) return "failed to open output file";

  png_structp png_out = png_create_write_struct(
    PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
  if (!png_out) return "failed to create png write struct";
  png_infop png_info = png_create_info_struct(png_out);
  if (!png_info)
    return "failed to create png info struct";

  if (setjmp(png_jmpbuf(png_out)))
    return "png init io error";
  png_init_io(png_out, f);

  if (setjmp(png_jmpbuf(png_out)))
    return "IHDR write error";
  png_set_IHDR(png_out,
               png_info,
               face->glyph->bitmap.width,
               face->glyph->bitmap.rows,
               aa ? 8 : 1,
               PNG_COLOR_TYPE_GRAY,
               PNG_INTERLACE_NONE,
               PNG_COMPRESSION_TYPE_DEFAULT,
               PNG_FILTER_TYPE_DEFAULT);
  png_write_info(png_out, png_info);

  if (setjmp(png_jmpbuf(png_out)))
    return "png write error";

  for (int i = 0; i < face->glyph->bitmap.rows; ++i) {
    const unsigned char *rowptr = face->glyph->bitmap.buffer +
      (face->glyph->bitmap.pitch * i);
    png_write_row(png_out, rowptr);
  }

  if (setjmp(png_jmpbuf(png_out)))
    return "png end error";
  png_write_end(png_out, NULL);

  fclose(f);
  return NULL;
}

int main(int argc, char **argv) {
  conf_t conf;
  char *conf_err = parse_args(argc, argv, &conf);
  if (conf_err != NULL) {
    printf("Error: %sn", conf_err);
    puts("Usage: render-text <font> <text> [<size> [<anti-alias>]]");
    puts("Example: render-text myfont.ttf foobar 24 yes");
    puts("         render-text myfont.ttf lorem");
    return 1;
  }

  printf("font: %s, text: %s, size: %d, anti-alias: %sn",
         conf.font_file,
         conf.text,
         conf.size,
         conf.anti_alias ? "yes" : "no");

  FT_Face face;
  char *ft_err = render_glyph(&face, conf);
  if (ft_err != NULL) {
    printf("freetype error: %sn", ft_err);
    return 2;
  }

  printf("bitmap rows: %d, width: %dn",
         face->glyph->bitmap.rows,
         face->glyph->bitmap.width);

  char *png_err = render_png(face, "a.png", conf.anti_alias);
  if (png_err != NULL) {
    printf("png error: %sn", png_err);
    return 3;
  }

  return 0;
}

2 comments:

  1. i followed your tutorial...but i am getting this error

    In file included from /usr/include/png.h:510,
    from text-render.c:8:
    /usr/include/pngconf.h:371: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘.’ token
    /usr/include/pngconf.h:372: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘include’

    I am using Ubuntu 10.04 LTS.

    I tried to solve error by manually editing pngconf.h file but was not able to save changes to the file.

    can you tell me how to solve this problem?

    ReplyDelete
    Replies
    1. Hi,

      thanks for reading. That seems like an issue with your libpng and compiler. It's hard to say what's really wrong without more information. But it doesn't seem like you've followed the tutorial exactly because you're apparently using the system libpng headers. In any case you definitely should not edit the pngconf.h.
      Sorry I couldn't be of more help.

      cheers
      J.R.

      Delete