TrueType Font Secrets
by
Michael Bertrand and Dave Grundgeiger
|
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.)
About the Authors
Mike Bertrand teaches Mathematics and programming at Madison Area Technical
College, Madison, WI 53704. He can be reached at
mikeber@execpc.com
(home page http://www.execpc.com/~mikeber).
Dave Grundgeiger is a Microsoft Certified Solution Developer, and is the
author of .INI Master - The .INI File Difference Utility for Windows.
Dave lives in Madison, Wisconsin, where he is a senior developer at
Tara Software, Inc. Dave can be reached at
daveg@tarasoftware.com
(home page http://www.execpc.com/~dg/articles).
Home | Top
This article and accompanying illustration first appeared in the
August 1999 issue of C/C++ Users Journal
(www.cuj.com) and are
copyright 1999 Miller Freeman Inc., 1601 W. 23rd St., Ste 200,
Lawrence, KS 66046. They are used here with permission of the
publisher.
|
|