Chapter 5: Building the Importer

In the last chapter, we thought through the font sheet import process, and put together a specification for our font sheet graphic. In this chapter, we will construct the EDFontImporter class and walk through the code.

First, let’s look at our new header file, EDFontImporter.h.

#import <Foundation/Foundation.h>

#import <OpenGLES/ES2/gl.h>
#import <OpenGLES/ES2/glext.h>

#define NUM_CHARACTERS 92

#define ALLOWABLE_IN_CHARACTER_WHITESPACE 3

@interface EDFontImporter : NSObject {
    // Name of the graphics file with character set

    NSString *myCharacterPageName;
   
    // Array of character coordinates from named font character sheet.
    // Each character will have four coordinates: upper left (X, Y) and
    // lower right (X, Y).
   
    int myCharacterCoords[NUM_CHARACTERS * 4];
   
    int characterPageWidth;     // Width of character page graphic
    int characterPageHeight;    // Height of character page graphic
}

@property (nonatomic, readonly) int characterPageWidth;
@property (nonatomic, readonly) int characterPageHeight;

- (EDFontImporter *)init;
- (void)loadCharacterPage:(NSString *)characterPageName;
- (int *)getCoordinateArray;
- (char *)getCharacterIndex;
- (NSString *)getCharacterPageName;

@end

I’ve actually removed a block of comments from the top that describes the font sheet format. It’s still in the source code included in the project download, but it was too large and redundant for this text, since we spent the whole last chapter talking about it.

Let’s take a closer look at this header file, starting at the top.

#import <Foundation/Foundation.h>

#import <OpenGLES/ES2/gl.h>
#import <OpenGLES/ES2/glext.h>

#define NUM_CHARACTERS 92

#define ALLOWABLE_IN_CHARACTER_WHITESPACE 3

After a pretty standard set of #import statements, we define a couple of control values. The NUM_CHARACTERS define specifies the number of characters we will be expecting on the graphics file (not including the first two ‘spacing’ characters on each line). It’s also the number of characters that will be referenced by our index. This index will be used to look up coordinate pairs by the text string management classes, and we’ll look at those in the next chapter.

The other define, ALLOWABLE_IN_CHARACTER_WHITESPACE, is used by the horizontal scanning logic, and specifies how much whitespace we can encounter while in a character before we decide we’ve gone past the end of it. For example, look at the character “w”, it’s basically one line that bends a few times. If I scan this character from left to right, there will always be a dark pixel somewhere. Now think about a double quote, which can have a gap between the two single quote marks in some fonts. If I’m scanning a double quote and hit all whitespace after the first single quote, I’ll end the character unless I look ahead and find the second single quote.

While I can’t actually “look ahead” in the scanning code, I can tell the code how far to scan before closing the character. Allowing some whitespace inside of the characters will make scanning double quotes in all fonts possible, but it will also require that I have plenty of whitespace between characters to avoid accidentally registering two characters as one.

@interface EDFontImporter : NSObject {
    // Name of the graphics file with character set

    NSString *myCharacterPageName;
   
    // Array of character coordinates from named font character sheet.
    // Each character will have four coordinates: upper left (X, Y) and
    // lower right (X, Y).
   
    int myCharacterCoords[NUM_CHARACTERS * 4];
   
    int characterPageWidth;     // Width of character page graphic
    int characterPageHeight;    // Height of character page graphic
}

We don’t need too many instance variables for this class, just the name of the graphics file that contains our characters, the array of coordinates that results from the scan, and the size of the page in width and height.

Keep in mind that since the same graphic we’re using here to scan characters will be used as a texture within OpenGL, it’s dimensions should be power of 2. My Cooper Std font sheet fit into a 512 x 256 sheet, but the Chalkduster font required a 512 x 512.

@property (nonatomic, readonly) int characterPageWidth;
@property (nonatomic, readonly) int characterPageHeight;

We’ll need this class to expose the character page width and height for accurately calculating texture coordinates from the pixel coordinates that will be stored after the scan. The texture coordinates will be determined by dividing 1 by the width and height of this sheet. We’ll cover this in much more detail later.

- (EDFontImporter *)init;
- (void)loadCharacterPage:(NSString *)characterPageName;
- (int *)getCoordinateArray;
- (char *)getCharacterIndex;
- (NSString *)getCharacterPageName;

@end

Our new importer class doesn’t have to do too much, but it must do it well. All we’ll be doing is loading in a font character sheet, scanning it at a pixel level to build an array of character coordinates, and making that information available to callers.

The best way to see how we’ll do all of this is by looking at the class source code.

#import "EDFontImporter.h"

// The character sheet index is used to find the correct set of character
// coordinates in the character coordinate array. Any characters (including
// a space) not in the index will be rendered as a space by collapsing the
// texture coordinates for that character.

char myCharacterSheetIndex[] = {
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=!@#$%^&*()_+[]\\;',./{}|:\"<>?"
};

@implementation EDFontImporter

@synthesize characterPageWidth, characterPageHeight;

The only thing to note at the top of this file is the myCharacterSheetIndex array. While we can use pixel data to detect where on the font sheet our characters exist, we don’t want to have to actually figure out which characters they are. There are countless different fonts out there, and writing a routing to accurately identify all letters and symbols in all possible fonts is a wee bit outside of the scope of this exercise.

Instead, we will make them positional. This importer class will require that the letters and symbols we want to import be in a specific order on the font sheets. Our myCharacterSheetIndex array will reflect the order of the letters and symbols, and give us a way to look up their coordinate data in the generated coordinate data array.

Compare the myCharacterSheetIndex array to the actual font sheet, and you’ll see that they match.

If it seems that there are too many characters in the myCharacterSheetIndex array, keep in mind that we need to escape the backslash character and the double quote.

// Standard initialization

- (EDFontImporter *)init {
    if((self = [super init])) {
        myCharacterPageName = nil;
    }
   
    return self;
}

The init method is pretty standard, and initializes the myCharacterPageName array to nil.

The next method is the heart, soul, and guts of the importer class. I’m going to list the entire method source, chock full of comments, and then go over the working code bit by bit.

// The loadCharacterPage: method will read in the graphics file containing the
// characters and create an array of locations in the graphic for each character.
// This information can be used by an OpenGL program to utilize the graphics file
// as a texture and the index information as texture coordinates to display the
// individual characters from the sheet.

- (void)loadCharacterPage:(NSString *)characterPageName {
    myCharacterPageName = characterPageName;
   
    // The first pass of the importer will scan down the character
    // sheet and determine the top and bottom limits of each row
    // of characters. This is why we prefix each row with the
    // "{ |" characters, so that the top and bottom limits will
    // be consistent across all rows of text. If they weren't,
    // different characters from different rows wouldn't line up.
   
    int characterLineBounds[16];    // 8 lines, 2 boundaries per line
    int currentCharacterLine = 0;   // Current line boundary
    BOOL inCharacterLine = FALSE;   // Are we inside a row of characters?
   
    // After reading in the graphics file with characters, scan from top to
    // bottom and record top and bottom boundaries for each line
   
    UIImage *image = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:myCharacterPageName ofType:nil]];
    CGImageRef imageRef = [image CGImage];
   
    // If the graphics file was read in successfully, we'll create a memory
    // area into which we can copy the data in a known format. At present,
    // we can't be sure exactly how the pixel data is formatted, so we'll
    // tell the OS what kind of format we want it in and make it do all
    // of the hard work. After the copy, we can iterate through the bytes
    // and read the character pixel data.
   
    if(imageRef) {
        size_t imageWidth = CGImageGetWidth(imageRef);
        size_t imageHeight = CGImageGetHeight(imageRef);
       
        // Save the width and height
       
        characterPageWidth = imageWidth;
        characterPageHeight = imageHeight;
       
        // Well allocate and clear an area that can accomodate our
        // image in an RGBA format. It'll we the width * height *
        // 4 bytes per bixel (RGBA).
       
        GLubyte *imageData = (GLubyte *)malloc(imageWidth * imageHeight * 4);
        memset(imageData, 0, (imageWidth * imageHeight * 4));
       
        // Now we'll create a context using that memory area and tell
        // the OS what kind of image format we want it to be.
       
        CGContextRef imageContextRef = CGBitmapContextCreate(imageData, imageWidth, imageHeight, 8, imageWidth * 4, CGImageGetColorSpace(imageRef), kCGImageAlphaPremultipliedLast);
       
        // Next, we'll draw the image we read in (in whatever format it
        // was originally created) into our new memory area in a known
        // format.
       
        CGContextDrawImage(imageContextRef, CGRectMake(0.0, 0.0, (CGFloat)imageWidth, (CGFloat)imageHeight), imageRef);
       
        // Finally, with the image data copied safely into our new RGBA
        // memory area, we can free the image context reference.
       
        CGContextRelease(imageContextRef);
       
        // Now we can start scanning down the rows of our character sheet,
        // creating the top and bottom boundaries for each row.
       
        for(int i = 0; i < imageHeight; i++) {
            BOOL cleanLine = TRUE; // No character pixels detected.
           
            for(int j = 0; j < (imageWidth * 4); j += 4) {
                // Each pixel horizontally will have 4 bytes of data for RGBA (Red, Green
                // Blue, and Alpha). If we detect a pixel that's not white, we consider
                // that character pixel data, and will mark a boundary if needed.
                // Don't forget that the first two characters of every new row will
                // be use for boundary detection, but not added into the character
                // array.
               
                int pixelPos = (i * imageWidth * 4) + j;
               
                if(imageData[pixelPos] < 250 || imageData[pixelPos + 1] < 250 || imageData[pixelPos + 2] < 250) {
                    cleanLine = FALSE;
                    break;
                }
            }
           
            // Either the entire line has been scanned cleanly, or character pixel data
            // was detected.
           
            if(cleanLine == FALSE) {
                if(inCharacterLine == FALSE) {
                    // We found character pixel data AND we weren't already in
                    // a character row, so record this Y position, post-incrementing
                    // our currentCharacterLine variable at the same time
                   
                    characterLineBounds[currentCharacterLine++] = i + 3;
                    inCharacterLine = TRUE;
                }
            } else {
                // If this was a clean line, let's make sure we didn't just
                // finish scanning a row of characters. If we did, record the
                // bottom boundary and reset scanning variables. We'll record
                // the line above this one, since the previous line was really
                // the end of the row character data.
               
                if(inCharacterLine == TRUE) {
                    characterLineBounds[currentCharacterLine++] = (i - 1) + 3;
                    inCharacterLine = FALSE;
                }
            }
        }
       
        // Now that all of the top and bottom Y boundaries have been detected,
        // we can work on finding the individual characters within those rows.
       
        int currentCharacter = 0;
       
        // Step through all of the known character rows, using each pair of
        // top and bottom boundaries as a guide.
       
        for(int i = 0; i < 16; i += 2) {
            BOOL inCharacter = FALSE;   // Are we inside of a character?
            int inCharacterNumber = 0;  // Which character in the row?
            BOOL cleanColumn = TRUE;    // All vertical whitespace within row?
            int spacingCounter = 0;     // Allow for vertical whitespace within character
           
            for(int j = 0; j < (imageWidth * 4); j += 4) {
                cleanColumn = TRUE;     // Reset indicator at the top of each scan
               
                // Now we are going to increment from this row's top pixel line boundary
                // to this row's bottom pixel line boundary. For each step, we will calculate
                // the position of the pixels in that column. We will scan the entire row
                // of pixel columns between the top and bottom boundaries, recording the
                // positions of the characters we find in each row (except the first two,
                // of course).
               
                for(int k = characterLineBounds[i]; k < characterLineBounds[i + 1]; k++) {
                    int pixelInColumn = k * (imageWidth * 4) + j;
                   
                    // Now look at the color data for this pixel to see if it's
                    // clean or not.
                   
                    if(imageData[pixelInColumn] < 250 || imageData[pixelInColumn + 1] < 250 || imageData[pixelInColumn + 2] < 250) {
                        cleanColumn = FALSE;
                        break;
                    }
                }
               
                // If the column was not clean, and we were not already in a character, this
                // is the start of a new character.
               
                if(cleanColumn == FALSE) {
                    if(inCharacter == FALSE) {
                        // Make sure we're not in one of the first two character
                       
                        if(inCharacterNumber++ > 1) {
                            // Not in first two characters, so process positional data
                           
                            myCharacterCoords[currentCharacter++] = (j / 4) - 1; // The "- 1" will create a little whitespace before the character
                            myCharacterCoords[currentCharacter++] = characterLineBounds[i] - 1;
                           
                            // Reset spacing counter, which will allow some whitespace inside of the character, if needed
                           
                            spacingCounter = 0;
                        }
                       
                        inCharacter = TRUE;
                    }
                } else {
                    if(inCharacter == TRUE) {
                        // We got a clean line, and we were in a character - this may be the end of
                        // character data. Don't process the character data if we're in the first
                        // two characters, or we're under our permittable whitespace count
                        // per character.
                       
                        if(spacingCounter++ > ALLOWABLE_IN_CHARACTER_WHITESPACE) {
                            if(inCharacterNumber > 2) {
                                myCharacterCoords[currentCharacter++] = (j / 4) - 1 + 2 - spacingCounter; // The "+ 2" will create a little whitespace after the character
                                myCharacterCoords[currentCharacter++] = characterLineBounds[i + 1] + 2 - spacingCounter;
                            }
                           
                            spacingCounter = 0;
                            inCharacter = FALSE;
                        }
                    }
                }
            }
        }
        // All finished, free memory used to store image
       
        free(imageRef);
    }
}

Whew, let’s start at the top of the method, and we’ll remove many of the comments to make the working code easier to follow.

- (void)loadCharacterPage:(NSString *)characterPageName {
    myCharacterPageName = characterPageName;
   
    int characterLineBounds[16];    // 8 lines, 2 boundaries per line
    int currentCharacterLine = 0;   // Current line boundary
    BOOL inCharacterLine = FALSE;   // Are we inside a row of characters?

Our loadCharacterPage: method will take the name of the font sheet to import from, and set our myCharacterPageName instance name to that value.

We know we are going to have eight lines of characters because of the format we’re requiring for the font sheet, so that’s eight top boundaries and eight bottom boundaries, meaning we need to store 16 values. The characterLineBounds array exists for this purpose.

The currentCharacterLine will be incremented each time we hit a new line top or bottom boundary. It is basically the index for our characterLineBounds array.

The inCharacterLine switch will allow us to keep track of whether or not we are currently within a line, or row, of characters.

   UIImage *image = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:myCharacterPageName ofType:nil]];
    CGImageRef imageRef = [image CGImage];
       
    if(imageRef) {
        size_t imageWidth = CGImageGetWidth(imageRef);
        size_t imageHeight = CGImageGetHeight(imageRef);
       
        // Save the width and height
       
        characterPageWidth = imageWidth;
        characterPageHeight = imageHeight;
       
        GLubyte *imageData = (GLubyte *)malloc(imageWidth * imageHeight * 4);
        memset(imageData, 0, (imageWidth * imageHeight * 4));
       
        CGContextRef imageContextRef = CGBitmapContextCreate(imageData, imageWidth, imageHeight, 8, imageWidth * 4, CGImageGetColorSpace(imageRef), kCGImageAlphaPremultipliedLast);
       
        CGContextDrawImage(imageContextRef, CGRectMake(0.0, 0.0, (CGFloat)imageWidth, (CGFloat)imageHeight), imageRef);
       
        CGContextRelease(imageContextRef);

This code should look very familiar to you. It’s copied directly from the loadTexture code from the previous tutorial’s Part Thirteen: Rendering Textures. We first use the UIImage class to load our image, then allocate space to copy the image into the format we require for the scanning process.

The CGBitmapContextCreate() function will create a context of the image format we want using the memory we allocated to the imageData variable. Once the memory space has been formatted, we use the CGContextDrawImage() function to copy the image data into our new memory area in the format we specified.

Once the image data has been copied over, we can use CGContextRelease() to clean up the context reference.

        for(int i = 0; i < imageHeight; i++) {
            BOOL cleanLine = TRUE; // No character pixels detected.
           
            for(int j = 0; j < (imageWidth * 4); j += 4) {
               
                int pixelPos = (i * imageWidth * 4) + j;
               
                if(imageData[pixelPos] < 250 || imageData[pixelPos + 1] < 250 || imageData[pixelPos + 2] < 250) {
                    cleanLine = FALSE;
                    break;
                }
            }
           
            if(cleanLine == FALSE) {
                if(inCharacterLine == FALSE) {
                   
                    characterLineBounds[currentCharacterLine++] = i + 3;
                    inCharacterLine = TRUE;
                }
            } else {
               
                if(inCharacterLine == TRUE) {
                    characterLineBounds[currentCharacterLine++] = (i - 1) + 3;
                    inCharacterLine = FALSE;
                }
            }
        }

The first loop is the vertical scan, and will look at each complete row of pixels from top to bottom. If the entire row of pixels is white, there is no character pixel data on that row, so we go on to the next one.

If, however, darker pixels are found, they are considered character pixels, and the cleanLine switch is set to FALSE and we break out of our loop.

The conditional logic below the inner loop works like this: if we detected character pixel data and we are NOT currently in a row of characters (inCharacterLine == FALSE), we make a note of this location in the characterLineBounds array and set our inCharacterLine switch to TRUE. If we detected character pixel data and inCharacterLine is TRUE, then we don’t need to do anything, since we would expect to find character pixel data if we know we’re currently in a row of characters.

On the other hand, if cleanLine was TRUE, meaning we did not detect any character pixel data, we need to make sure that we don’t have inCharacterLine set to TRUE. If we do, it means that we just left a row of characters, so we need to make a note of this pixel location in our characterLineBounds array and set the inCharacterLine switch to FALSE.

What we’ll end up at the end of the scan is a set of upper and lower pixel positions for each of our eight rows of characters. These boundaries will be used by the next loop to scan horizontally and detect each of the individual characters in each row.

By the way, if you’re new to C, you might be thrown by pre and post increment logic. In C, there are shortcuts for common mathematical operations. If I need to increment a variable, I could type:

var = var + 1;

In C, I can write the same thing in two more different ways.

var += 1;

and

var++;

If I use that variable in an expression, I can also apply a pre or post increment as a part of processing that expression. For example, look at the following statement:

arrayVar[var++] = 100;

This will set the value in the ‘arrayVar’ array at the ‘var’ index to 100. It will then increment the ‘var’ variable because the ‘++’ comes after the variable. This is a post increment.

Now look at this statement:

arrayVar[++var] = 100;

This will first increment the ‘var’ variable, because the ‘++’ comes before it, and then set the value in the ‘arrayVar’ array at the ‘var’ index to 100. This is a pre increment.

If ‘var’ was 5 before executing each one of these lines, the post increment version would set arrayVal[5] to 100, but the pre increment version would have set arrayVal[6] to 100.

Right, back to the code.

       int currentCharacter = 0;
       
        for(int i = 0; i < 16; i += 2) {
            BOOL inCharacter = FALSE;   // Are we inside of a character?
            int inCharacterNumber = 0;  // Which character in the row?
            BOOL cleanColumn = TRUE;    // All vertical whitespace within row?
            int spacingCounter = 0;     // Allow for vertical whitespace within character
           
            for(int j = 0; j < (imageWidth * 4); j += 4) {
                cleanColumn = TRUE;     // Reset indicator at the top of each scan
                               
                for(int k = characterLineBounds[i]; k < characterLineBounds[i + 1]; k++) {
                    int pixelInColumn = k * (imageWidth * 4) + j;
                   
                    if(imageData[pixelInColumn] < 250 || imageData[pixelInColumn + 1] < 250 || imageData[pixelInColumn + 2] < 250) {
                        cleanColumn = FALSE;
                        break;
                    }
                }
               
                if(cleanColumn == FALSE) {
                    if(inCharacter == FALSE) {
                       
                        if(inCharacterNumber++ > 1) {
                           
                            myCharacterCoords[currentCharacter++] = (j / 4) - 1; // The "- 1" will create a little whitespace before the character
                            myCharacterCoords[currentCharacter++] = characterLineBounds[i] - 1;
                           
                            spacingCounter = 0;
                        }
                       
                        inCharacter = TRUE;
                    }
                } else {
                    if(inCharacter == TRUE) {
                       
                        if(spacingCounter++ > ALLOWABLE_IN_CHARACTER_WHITESPACE) {
                            if(inCharacterNumber > 2) {
                                myCharacterCoords[currentCharacter++] = (j / 4) - 1 + 2 - spacingCounter; // The "+ 2" will create a little whitespace after the character
                                myCharacterCoords[currentCharacter++] = characterLineBounds[i + 1] + 2 - spacingCounter;
                            }
                           
                            spacingCounter = 0;
                            inCharacter = FALSE;
                        }
                    }
                }
            }
        }
        // All finished, free memory used to store image
       
        free(imageRef);
    }
}

This is the complete second loop that will find individual characters in each row. Notice that the outermost loop is coded to increment i to 16 in steps of two? That’s eight iterations, one for each row of letters on the font sheet.

The next loop using the j variable will iterate all the way across the font sheet, horizontally, and the loop inside of that, using the k variable, will iterate down each column of pixels between the top and bottom boundaries of the current character row.

What we’re doing now is similar to what we did in the first pass, except instead of scanning down the sheet and noting the top and bottom boundaries of rows of letters, we’re scanning left to right within each of those rows and noting the left and right boundaries of each letter.

The thing that makes this tricky is that each pixel is represented by four bytes of data, in RGBA format. This is why the loop using the j variable goes from zero to (imageWidth * 4), and increments by four.

The innermost loop is evaluating the pixels down a straight line from the top row boundary to the bottom row boundary. If the innermost loop detects any pixel data, it sets the cleanColumn switch to FALSE and breaks out.

Much like our previous processing, the conditionals after the innermost loop check to see if cleanColumn is FALSE, and if so, if we are in a character. If we’re not in a character, this must be the start of a new one, so we mark the location in our myCharacterCoords array, incrementing the currentCharacter index variable as we go.

Before the logic to note the location of this new character, there’s a check to make sure we’re not in one of the first two characters. Remember, the first two characters of each line are only used for spacing, and are not actually used as part of the data set.

Alternately, if cleanLine was TRUE, we check to see if we were in a character. We also check to see if we’re past the allowable whitespace within a character, and that we’re not in one of the first two characters. If everything checks out, we must have just gone past the right side of a character, so we note it’s location in our myCharacterCoords array.

Also notice in the code that records the locations of the character boundaries that there are slight adjustments here and there. For example, when recording the start of a character at the left side.

                            myCharacterCoords[currentCharacter++] = (j / 4) - 1; // The "- 1" will create a little whitespace before the character
                            myCharacterCoords[currentCharacter++] = characterLineBounds[i] - 1;

We divide by four because the size of an image is based on a single pixel (512 x 512, for example), and not each individual RGBA element that makes up that pixel. Since we’re iterating through based on the individual RGBA elements, we need to convert back to plain pixels before recording the image locations. Subtracting one from that result gives us a little padding on the left. Subtracting one from the Y coordinate on the next line gives us a little padding on the top, too.

We have a few adjustments when recording the right side, too.

                                myCharacterCoords[currentCharacter++] = (j / 4) - 1 + 2 - spacingCounter; // The "+ 2" will create a little whitespace after the character
                                myCharacterCoords[currentCharacter++] = characterLineBounds[i + 1] + 2 - spacingCounter;

We divide j by four for the same reason we did above, and we subtract one because we had to pass the end of the character to detect we were finished with it. We then add two for some additional padding, and subtract the spacingCounter because we allowed the scanner to be some distance past the end of the character before confirming we were actually finished with the character. Did anyone else just think of the scene in Raiders of the Lost Ark where the old man deciphers the instructions on the headpiece?

After all of this processing, we need a way to expose all of our work somehow, and that’s what the last few methods in this class will do.

// Return the array of character coordinate information

- (int *)getCoordinateArray {
    return myCharacterCoords;
}

This method will return all of the coordinates in the array we just populated by scanning the font sheet.

// return the supported character index string

- (char *)getCharacterIndex {
    return myCharacterSheetIndex;
}

This method returns the character index array we declared at the top of our class.

char myCharacterSheetIndex[] = {
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=!@#$%^&*()_+[]\\;',./{}|:\"<>?"
};

We could force the caller to maintain this index, since it’s static, but because it ties in so closely with the font sheet format we’re enforcing, it makes more sense for us to own it.

// Return the name of the graphics file used (needed by OpenGL programs for texture)

- (NSString *)getCharacterPageName {
    return myCharacterPageName;
}

@end

The final method in our importer class returns the name of the font sheet we stored earlier, during the initialization call.

Now that we have an array of character coordinates and an accompanying image file that can be used as a texture, we need to create a class that can use all of this to draw text into OpenGL. We’ll create new EDTextString and EDTextStringManager classes in the next chapters that will make use of our new importer, and we’ll also talk about coordinate systems.

Chapter 4 | Index | Chapter 6