C/C++ Users Journal
TrueType Font Secrets
by Michael Bertrand and Dave Grundgeiger
Figure 1: Using the CGlyph class in an SDK application.
  // Declare pointer to main glyph object.
  static CGlyph* pGlyph;
          .
          .
  // Create glyph object (once only, at start).
  pGlyph = new CGlyph();
          .
          .
  // Realize 'B' (assumes font defined by LOGFONT lf).
  pGlyph->Realize('B', hDC, &lf);
          .
          .
  // Draw the glyph with (100,100) as top left.
  pGlyph->Draw(hDC, 100, 100);
          .
          .
  // Delete glyph object (once only, at end).
  delete pGlyph;

  - End of Figure -
Return to Article
Figure 2: Using the CGlyph class in an MFC SDI (Single Document Interface) application, where m_glyph is a member variable in the document class and pDoc is a pointer to the document.
  // m_glyph is a member variable in the document class.
  CGlyph m_glyph;
          .
          .
  // Realize 'B' (assumes font defined by LOGFONT lf).
  pDoc->m_glyph.Realize('B', *pDC, &lf);
          .
          .
  // Draw the glyph with (100,100) as top left.
  pDoc->m_glyph.Draw(*pDC, 100, 100);

  - End of Figure -
Return to Article
Figure 3: The Draw method of class CGlyph.
  void CGlyph::Draw(HDC hDC, const int xOffset, const int yOffset) const
  {
    LPTTPOLYGONHEADER lpttph;  // pointer to current polygon in glyph
    LPTTPOLYCURVE lpttpc;      // pointer to current polycurve in polygon
    DWORD dwHeaderOffset;      // byte offset of current polygon header
                               //     from start of buffer
    DWORD dwCurveOffset;       // byte offset of current polycurve header
                               //     from start of polygon header
    DWORD dwStructSize;        // size of current polycurve (depends on
                               //     the number of points in the polycurve)
    POINT ptPolyStart;         // polygon start point
    POINT ptCurveLast;         // last point on previous curve
    POINT pt;                  // temporary holder of point coordinates
    int i;                     // to loop thru lines or splines
    POINTFX p1, p2, p3;        // three points defining spline

    if (!IsRealized())
      return;

    assert(m_lpvBuffer != NULL);

    // Outer while loops for all polygon headers (can be more than one).
    dwHeaderOffset = 0;
    while (m_cbBuffer >= (dwHeaderOffset + sizeof(TTPOLYGONHEADER)))
    {
      // Get pointer to start of the polygon.
      lpttph = (LPTTPOLYGONHEADER)(((char *)m_lpvBuffer) + dwHeaderOffset);
      assert(lpttph->dwType == TT_POLYGON_TYPE);
      // Convert polygon's start point to pixels.
      FromGGOPoint(&ptPolyStart, lpttph->pfxStart, xOffset, yOffset);
      // Init last point of last record to polygon start point.
      ptCurveLast = ptPolyStart;
      // Position graphics current pointer to start of polygon.
      MoveToEx(hDC, ptPolyStart.x, ptPolyStart.y, NULL);

      // Inner while loops for all polycurves in one polygon.
      // (A polycurve is one or more polyline and/or QSpline records.)
      dwCurveOffset = sizeof(TTPOLYGONHEADER);
      while (lpttph->cb >= (dwCurveOffset + sizeof(TTPOLYCURVE)))
      {
        // Get pointer to start of polycurve.
        lpttpc = (LPTTPOLYCURVE)(((char *)m_lpvBuffer) + dwHeaderOffset
                 + dwCurveOffset);

        // Test record type, draw polyline or series of Beziers accordingly.
        switch (lpttpc->wType)
        {
          case TT_PRIM_LINE:
            // Draw polyline connecting GGO points (there are lpttpc->cpfx).
            for (i = 0; i < lpttpc->cpfx; i++)
            {
              FromGGOPoint(&pt, lpttpc->apfx[i], xOffset, yOffset);
              LineTo(hDC, pt.x, pt.y);
            }
            ptCurveLast = pt;
            break;
          case TT_PRIM_QSPLINE:
            // Draw series of Beziers connecting GGO points.
            // But for initial Bezier, grab last point on previous curve.
            p3.x.value = (short)ptCurveLast.x;
            p3.x.fract = 0;
            p3.y.value = (short)ptCurveLast.y;
            p3.y.fract = 0;
            // Draw QSplines based on GGO points (there are lpttpc->cpfx-1).
            for (i = 0; i < lpttpc->cpfx - 1; i++)
            {
              // p1 is 1st control -- last point in this or previous contour.
              p1 = p3;
              // p2 is handle -- a point in the record.
              FromGGOPoint(&pt, lpttpc->apfx[i], xOffset, yOffset);
              p2.x.value = (short)pt.x;
              p2.x.fract = 0;
              p2.y.value = (short)pt.y;
              p2.y.fract = 0;
              // p3 is 2nd control -- the midpoint of 2 spline points
              // except for the last spline, when it is the second to
              // the last point in the spline record.
              p3 = (i == (lpttpc->cpfx-2)) ? lpttpc->apfx[i+1] :
                AvgPointsFX(lpttpc->apfx[i], lpttpc->apfx[i+1]);
              FromGGOPoint(&pt, p3, xOffset, yOffset);
              p3.x.value = (short)pt.x;
              p3.x.fract = 0;
              p3.y.value = (short)pt.y;
              p3.y.fract = 0;
              // Call deCasteljau to draw quadratic Bezier curve.
              DrawQSpline(hDC, p1, p2, p3);
              } // for
            ptCurveLast = pt;
            break;
          default:
            assert(false); // must be one of the two cases above
            break;
          } // switch

        // Increment curve offset so point to next polycurve.
        dwStructSize = sizeof(TTPOLYCURVE) + ((lpttpc->cpfx - 1)
                       * sizeof(POINTFX));
        dwCurveOffset += dwStructSize;
        } // inner while (polycurve)

      // Close polygon by drawing a line back to the start point.
      LineTo(hDC, ptPolyStart.x, ptPolyStart.y);

      // Increment polygon offset so point to next polygon.
      dwHeaderOffset += lpttph->cb;
      } // outer while (polygon)

  } // Draw()

  - End of Figure -
Return to Article (towards start)
Return to Article (towards end)
Figure 4: The signature of GetGlyphOutline.
  DWORD GetGlyphOutline(
    HDC hdc,             // handle of device context
    UINT uChar,          // character to query
    UINT uFormat,        // format of data to return
    LPGLYPHMETRICS lpgm, // pointer to metrics struct
    DWORD cbBuffer,      // size of buffer for data
    LPVOID lpvBuffer,    // address of buffer for data
    CONST MAT2 *lpmat2   // pointer to transform matrix
  );

  - End of Figure -
Return to Article
Figure 5: Transformation matrix for a rotation through angle A (counter-clockwise rotation for a positive angle), where the eMs are the fields of GetGlyphOutline's lpmat2 parameter.
  *-  -*     *-                         -*     *- -*
  | x' |     | eM11=cos(A)  eM21=-sin(A) |     | x |
  |    |  =  |                           |  *  |   |
  | y' |     | eM12=sin(A)  eM22= cos(A) |     | y |
  *-  -*     *-                         -*     *- -*

  - End of Figure -
Return to Article
Figure 6: Quadratic Bezier curve determined by control points p1 and p3 and handle point p2. Points q1, q2, and r1 are the deCasteljau construction points, where r1 is the midpoint of the curve.

 Quadratic Bezier Curve

Return to Article
Figure 7: Stitching two quadratic Bezier curves together smoothly in a TrueType QSpline record. Points p2 and p2' in the record are handles off the curve, but their midpoint (average) is a common control.

 Stitching Bezier Curves

Return to Article
Figure 8: Outer contour of a TrueType Times New Roman 'A' with intermingled Line (blue) and QSpline (red) records.

 Glyph for A

Return to Article
Figure 9: Detail of left foot of 'A' showing two adjacent QSpline records. QSpline3 consists of three points defining two smoothly joined Bezier curves.

 Foot of A

Return to Article
Figure 10: Steps to create an MFC Single Document Interface (SDI) application using class CGlyph to put character outlines on the screen.
  1) Use MFC AppWizard to create SDI app 'GlyphSDI'.

  2) Copy glyph.cpp and glyph.h into the GlyphSDI directory and add
     glyph.cpp to the project.

  3) Towards the top of GlyphSDIDoc.h, just before the definition of
     class CGlyphSDIDoc, add:

     #include "glyph.h"

     Also add a public CGlyph member variable inside the class:

     // Attributes
     public:
       CGlyph m_glyph;

  4) Add a public LOGFONT member variable inside the definition of
     class CGlyphSDIView in GlyphSDIView.h:

     // Attributes
     public:
       LOGFONT m_lf;          // keeps track of current font properties

  5) Add this code to the CGlyphSDIView constructor in GlyphSDIView.cpp:

     CGlyphSDIView::CGlyphSDIView()
     {
       // TODO: add construction code here
       // Initial font information.
       memset(&m_lf, 0, sizeof(m_lf));
       m_lf.lfPitchAndFamily = FF_ROMAN;
       m_lf.lfHeight         = 576;
       m_lf.lfCharSet        = ANSI_CHARSET;
       m_lf.lfOutPrecision   = OUT_TT_PRECIS;
       strcpy(m_lf.lfFaceName, "Times New Roman");
     }

  6) Add this code to CGlyphSDIView's OnDraw() in GlyphSDIView.cpp:

     void CGlyphSDIView::OnDraw(CDC* pDC)
     {
       CGlyphSDIDoc* pDoc = GetDocument();
       ASSERT_VALID(pDoc);

       // TODO: add draw code for native data here
       // First time through, realize 'B'.
       if (!pDoc->m_glyph.IsRealized())
         pDoc->m_glyph.Realize('B', *pDC, &m_lf);

       // Draw the glyph.
       pDoc->m_glyph.Draw(*pDC, 20, 20);
     }

  7) Use the class wizard to add an event handler to CGlyphSDIView
     for the WM_CHAR message (call it OnChar()):

     void CGlyphSDIView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
     {
       // TODO: Add your message handler code here and/or call default
       CGlyphSDIDoc* pDoc;
       pDoc = GetDocument();
       ASSERT_VALID(pDoc);
       CDC *pDC = GetDC();        // need a DC to realize the glyph

       // Must realize the glyph for change to take effect.
       pDoc->m_glyph.Realize(nChar, *pDC, &m_lf);

       // Redraw.
       Invalidate();
     }

  - End of Figure -
Return to Article