A TTF file is orgonized into tables. The definition of these tables can be found in the Apple True Type Manual. Tables can appear in any order, they are 32 bit aligned, and padded with zeros if necesary.

The code presented on this page won't be used on later pages. The code is here to introduce parsing tables, but the final code for parsing tables is presented when those tables are first needed.

The code in this section demonstrates how to read tables, the structurs are listed to show memory layout and will not be used on other pages.

Font Directory

The first table in any file is the Font Directory. The font directory serves as an index to the contents of the font. It contains an offset subtable, and a list of table locations called the table directory.

struct FontDirectory {
    OffsetTable offsetTable = { 0 };
    vector< TableLocation > tableDirectory;

The offset subtable begins with a tag (scaler) that identifies the information in the font. This tag should have a value of "true" (0x74727565), or 0x00010000. Next is the number of tables in the font. Finally there are three values for speeding up searches in the font, we will ignore these.

struct OffsetTable {
    u32 scaler; // Should 0x74727565 or 0x00010000
    u16 numTables;
    u16 searchRange;
    u16 entrySelector;
    u16 rangeShift;

The offset subtable is followed by a list of table location. Each table location is made up of a tag (32 bit int), a checksum, the offset (from the begining of the file), and the length of the table in bytes. The table directory is sorted by ascending tag, there is no padding between elements of the table directory.

struct TableLocation {
    union {
        char ptag[4];
        u32 tag;
    u32 checkSum;
    u32 offset;
    u32 length;

Reading the font directory is trivial, the offset table makes up the first 96 bytes of the file, take endianess into account when reading the data. The offset table contains the number of tables in the font, which lets us know how much data to read for the table directory.

FontDirectory ReadFontDirectory(u8* p) {
    FontDirectory r;
    // Read offset table
    r.offsetTable.scaler = read_u32(&p);
    r.offsetTable.numTables = read_u16(&p);
    r.offsetTable.searchRange = read_u16(&p);
    r.offsetTable.entrySelector = read_u16(&p);
    r.offsetTable.rangeShift = read_u16(&p);

    // Read table directory
    u16 numTables = r.offsetTable.numTables;

    for (u16 table = 0; table < numTables; ++table) {
        r.tableDirectory[table].tag = read_u32(&p);
        r.tableDirectory[table].checkSum = read_u32(&p);
        r.tableDirectory[table].offset = read_u32(&p);
        r.tableDirectory[table].length = read_u32(&p);

    return r;

Finding tables

The FindTableByTag function below takes a pointer to the contents of a TTF file and a 4 letter c-string that specifies a tag. If the table is in the font, a pointer to it is returned, if the table is not in the font the function returns null. The function is provided for reference, we're not going to be using it later.

u8* FindTableByTag(u8* data, const char* sTag) {
    u8* p = data + 4; // + 4: skip 'u32 scaler'
    u16 numTables = read_u16(&p);

    // Because tags are read as a big endian u32, they are reversed.
    u32 intTag = (sTag[0] << 24) | (sTag[1] << 16) | (sTag[2] << 8) | sTag[3];
    u8* t = data + 12; // + 12: skip sizeof(OffsetTable)

    for (u16 i = 0; i < numTables; ++i) {
        u32 thisTag = (t[0] << 24) | (t[1] << 16) | (t[2] << 8) | t[3];
        if (thisTag == intTag) {
            p = t + 8; // + 8: skip 'u32 tag' and 'u32 checksum'
            u32 offset = (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];

            return data + offset;
        t += 16; // + 16: skip sizeof(TableLocation)

    return 0;

The FindTableByTag function can be used to find table pointers like this:

FILE* fontFile = fopen("arial.ttf", "rb");

fseek(fontFile, 0, SEEK_END);
u32 fontDataLength = ftell(fontFile);
fseek(fontFile, 0, SEEK_SET);

u8* data = (u8*)malloc(fontDataLength + 1);
fread(data, fontDataLength, 1, fontFile);
data[fontDataLength] = '\0';

u8* head = FindTableByTag(data, "head");
u8* maxp = FindTableByTag(data, "maxp");