Ever wonder how all these glyphs get drawn on your monitor or printer? Here's the inside scoop.
TrueType is a common vector font standard used by the Microsoft Windows and Apple operating systems, among others. In a vector font, a series of coordinates define a character's contour, so simple scaling transformations effectively shrink or enlarge the character. Multiplying all the coordinates by two doubles the character's size, for example, and it looks just as good at both resolutions. Operating systems typically allow users to access TrueType font handling without having to know all the details. But font manipulations beyond those supplied by the operating system require a deeper understanding of the TrueType format. Understanding the format, and having the coordinates to each character's contour, opens the door to a world of special text effects like gradient-filling the character's interior, extruding it, placing it realistically on a sphere, and so on.
Microsoft Windows furnishes direct access to TrueType coordinates through the GetGlyphOutline API. GetGlyphOutline supplies the vector points for straight lines and Bezier curves in an abstract coordinate system. Rendering the character then requires deciphering the vector points and drawing the lines and curves with MoveTos and LineTos. The Bezier curves in particular must be decomposed into straight lines and patched together end-to-end to produce the final smooth contour. We provide a class, CGlyph, to handle this task. In this article we explain the mechanics of drawing a TrueType font, and show how you can use the CGlyph class to create your own special effects with TrueType characters.
The CGlyph Class
CGlyph is a C++ wrapper class that taps the basic functionality of GetGlyphOutline. CGlyph can be used in either an SDK context (see Figure 1) or in an MFC Single Document Interface (SDI) application (see Figure 2). We have provided several driver programs of each type for downloading.
CGlyph's main methods are Realize and Draw. Realize allocates the buffer needed to hold the vector points, then calls GetGlyphOutline to load them. A subsequent Draw traverses the buffer, drawing the lines and Bezier curves making up that character, or glyph. The Draw method is shown in Figure 3.
The GetGlyphOutline API
The signature of GetGlyphOutline is shown in Figure 4. The handle to device context, hdc, must be valid at the time GetGlyphOutline is called, and it must have the TrueType font of interest selected into it. uChar is the character being interrogated for an outline. uFormat determines whether the data returned is in bitmap (GGO_BITMAP) or vector (GGO_NATIVE) form, the latter being appropriate here. GetGlyphOutline fills in the fields of the GLYPHMETRICS structure pointed to by lpgm with information about the glyph's size and placement; fields gmBlackBoxX and gmBlackBoxY, for example, hold the size of the glyph's bounding box. (See MSDN for a description of the GLYPHMETRICS structure.)
Using GetGlyphOutline always requires two calls. In the first call, parameter cbBuffer is set to 0 and lpvBuffer is set to NULL. This tells GetGlyphOutline to return the size of the buffer needed to hold the glyph data. After the program has allocated a buffer of that size, it calls GetGlyphOutline again, passing the buffer size in cbBuffer and the buffer address in lpvBuffer. When called with these argument values, GetGlyphOutline copies the vector data into the buffer.
Parameter lpmat2 is a pointer to a transformation matrix, which GetGlyphOutline will apply to all points in the glyph before writing them to the buffer. The transformation is applied through matrix multiplication, thus making GetGlyphOutline capable of linear effects such as shearing and rotating. Figure 5 shows a transformation matrix for rotation through an angle A.
GetGlyphOutline returns numbers in a fixed point format, in which two integers (fract, value) represent a real number. value represents the part of the real number to the left of the decimal point; fract represents the part to the right of the decimal point, considered as a fraction of 65536. For example, 0.5 becomes (fract, value) = (32768, 0); 2.25 is equivalent to (fract, value) = (16384, 2); and so on. Numbers of this format are stored in structures of type FIXED. The same structure must be used for matrix entries as well.
Polyline and QSpline Records
GetGlyphOutline fills the buffer with a sequence of structures describing the glyph. A glyph consists of one or more "contours". Each contour is described by a TTPOLYGONHEADER structure followed by as many TTPOLYCURVE structures as required to describe it. (See MSDN for a description of these structures.) Each TTPOLYCURVE structure can be either a polyline record or a spline record.
Two contours make up a capital 'A', for example: one for the outer contour and one for the triangular hole. Each contour consists of one or more curves, a series of connected, intermingled polyline and QSpline records. A polyline is a series of connected straight lines, while a QSpline record is a series of connected three-point (quadratic) Bezier curves. A contour is closed, ending where it started. Curve data consists of a series of points, which are represented as POINTFX structures consisting of a FIXED x and a FIXED y.
Polyline records consist of a short (2 byte) integer n followed by n points. The last point of the previous record connects by a straight line to the first point, then straight lines connect subsequent points.
QSpline records also consist of a short integer n followed by n points, but only the last point lies on the glyph itself. These points define a connected series of n-1 Bezier curves.
Figure 6 shows a quadratic Bezier curve. A quadratic Bezier curve is defined by three points: controls p1 and p3, and handle p2. The curve begins at p1 in the direction of handle p2, eventually veering back towards p3, where it ends. The handle vectors connecting p1 and p3 to p2 in Figure 6 are construction lines -- they're shown only to illustrate how the curve runs tangent to one of these vectors before breaking off towards the other control point. Although Windows 95 has built-in Bezier drawing support with functions PolyBezier and PolyBezierTo, these functions draw four-point (cubic) Bezier curves, not three-point (quadratic) curves. (Cubic Bezier curves have two handles; quadratic Beziers convert to cubic Beziers by choosing the cubic handles to be two-thirds of the way from the quadratic control points to the quadratic handle.)
Instead of trying to use the Windows 95 functions, we elected to implement the elegant recursive deCasteljau algorithm, which calculates a series of points along the Bezier curve which are then connected as a polyline. DeCasteljau works by calculating point q1 midway between p1 and p2, and point q2 midway between p2 and p3. Then point r1, the midpoint of segment q1q2, is a point on the curve, and one that partitions the original Bezier curve into left sub-Bezier p1q1r1 and right sub-Bezier r1q2p3 (see Figure 6). The subdivision process continues recursively to generate as many evenly spaced points on the original Bezier curve as are desired. (See Mike's article, "Fast Bezier Curves in Windows" [1], for details on the deCasteljau algorithm).
TrueType adds an extra twist in the way Bezier curves are stitched together in a single QSpline record. If n = 2 in a QSpline record, there will be a single Bezier with p1 being the last point on the previous record and p2 and p3 the given points. If n = 3, however, there will be two Beziers joined end to end. Following the notation in CGlyph's Draw method, denote the three points by apfx[0], apfx[1], and apfx[2]. The first Bezier curve is defined by:
p1 = last point in previous record p2 = apfx[0] p3 = (apfx[0] + apfx[1]) / 2
The second Bezier curve has points:
p1' = p3 on last Bezier p2' = apfx[1] p3' = apfx[2]
With the exception of the last point, the spline points returned by GetGlyphOutline are the Bezier handles. The curve does not pass through any of the points returned by GetGlyphOutline (since they are handles) except the last point. The control points can be reconstructed from the handles: each control point is the average of two adjacent handles. Since the average of two points is the point exactly midway between them, this ingenious scheme insures that the joined Bezier curves are smooth at the point of juncture. This is because the first Bezier is tangent to the vector connecting p3 to p2, while the second one is tangent to the vector connecting p1' = p3 to p2'. The two vectors point in diametrically opposite directions, by construction (compare with Figure 7).
The same averaging scheme applies if there are more than two Bezier curves in one QSpline record -- they patch together continuously at each juncture, insuring smoothness at any resolution.
Representing an 'A'
Consider the TrueType representation of the Times New Roman capital 'A' in Figure 8, for example. First comes the outer contour, pictured here, then a second contour for the hole (not shown). The outer contour's start point, marked by a green cross, is on the right underside of the horizontal arm. The contour begins to trace straight to the left, then turns down and to the left to start down the left foot, proceeding all the way around in a clockwise fashion. The blue crosses mark points in polyline records, the red crosses points in QSpline records.
If Line2 denotes a polyline record with two points, QSpline3 a QSpline record with three points, and so on, then this contour consists of the following records:
Line2 QSpline2 QSpline3 Line3 QSpline2 QSpline2 Line3 QSpline3 Line3 QSpline3 QSpline2
A detail view of the left foot in Figure 9, helps illustrate how the QSplines work. The first QSpline record in the contour begins to define the right edge of the left foot. It has two points (QSpline2) and determines a single Bezier curve. The two points are the handle and second control, the first control being the last point on the previous polyline record. The next QSpline record has three points (QSpline3), where the first two handles are not on the contour and the third is the final control. Compare to Figure 7, which shows two quadratic Beziers joined at point p3, essentially the same diagram in a different orientation. Here too the construction lines are drawn, showing that the midpoint of the two interior handles is a control point common to both Beziers.
Hinting
Even vector fonts ultimately must be displayed on raster devices. When a filled glyph is displayed, its contours determine which pixels are turned on and off. Boundary pixels present a problem, especially at small font sizes, where the decision to include a pixel can considerably affect the character's legibility and aesthetic appeal. To address this problem, TrueType provides "hinting" to customize glyphs at smaller sizes. While producing the best results in general, hinting means that glyphs produced at different resolutions may not be scaled versions of each other. Our GlyphDemo application uses a very large font size (576) for retrieving glyph contours. Scaling these contours down produces different glyphs than the hinted versions, which are produced at a small font size to begin with. (The hinted versions look better.)
Figure 10 explains the mechanics of using class CGlyph in a Microsoft Visual C++ SDI application. When the application has been built, the user can display Times New Roman characters on the display by pressing keys on the keyboard. GlyphDemo enables the user to choose any TrueType font and any character to render. Then through menu items or hot keys, the character can be rotated clockwise or counter-clockwise or zoomed in (to make the character larger) or zoomed out (to make the character smaller).
Special Effects
Programming special text effects would involve amending CGlyph's Draw method (Figure 3), which traverses the TrueType vector points and sends them directly to MoveTo / LineTo. Coloring a character's interior, for example, would require using the traversal algorithm to collect the points into a buffer, then sending them from there to the Windows Polygon API to draw the character and fill its interior.
Nonlinear point transformations beyond those built into GetGlyphOutline become possible as well. Think of a character as being painted on a hemisphere, for example, with the center of the character at the top of the hemisphere. Then project the character down onto the plane base of the hemisphere, much as maps of the earth are produced. Projected points on the contour are distorted in proportion to their distance from the center, but non-linearly according to trigonometric calculations. The transformation is applied to every point produced by GetGlyphOutline and only then is the character rendered as before to produce the special effect.¤
References:
[1] Michael Bertrand. "Fast Bezier Curves in Windows", PC Techniques, February/March 1992, pp. 25-30. (Reprinted in the book PC Techniques C/C++ Power Tools, 1992, pp. 213-225.)