Tutorial 9 : Text & Fonts

Several classes of applications were considered in order to determining the set of features supported by the OpenVG text rendering API: e-book readers, scalable user interfaces with text-driven menus, SVG viewers and games. These application requirements made it clear that OpenVG must provide a fast, low-level API that is capable of supporting vector glyph outlines, as well as glyphs represented as bitmaps.

The process of text rendering involves the following steps:

Font and glyph scaling is usually done once for each selected text size; however, the rendering of glyph outlines and blitting of bitmaps is repeated routinely. Let’s have a look at how we can handle fonts and text rendering in OpenVG.


Glyph positioning and text layout

Scalable fonts define glyphs using vector outlines and additional set of data, such as hinting instructions, font and glyph metrics, etc. A typical glyph would be defined by the following information:

Some basic properties of glyphs: origin, outline, bounding box, escapement
Some basic properties of glyphs: origin, outline, bounding box, escapement

The complexity of text rendering and composition depends on language scripts. In many simple scripts (such as western and eastern European languages) text is composed by simply planking glyphs next to each other along the horizontal baseline. The next glyph origin must be calculated using the escapement for the current glyph. Note that vector defined by two points glyph origin and escapement must be subjected to the same matrix transformation that is applied to a glyph outline when the glyph is scaled.

In some cases, the text composition requires that glyph layout and positioning be adjusted along the baseline (using kerning) to account for the difference in appearance of different glyphs and to achieve uniform typographic color (optical density) of the text.

Glyph positioning with kerning
Glyph positioning with kerning

Some complex scripts require glyph positioning be adjusted in both directions; therefore, when a text composition involves support for complex scripts, the inter-character spacing between each pair of glyphs in a text string may have to be defined using the escapement for the current glyph (i), and the additional adjustment vector for the pair of glyphs (i, i + 1).


OpenVG text

OpenVG provides a mechanism to allow applications to define a VGFont object as a collection of glyphs, where each glyph can be represented as either a VGPath representing either an original unhinted outline that can be scaled and rendered, or a scaled and hinted outline; or a VGImage representing a scaled, optionally hinted, and rendered image of a glyph. Use of a VGImage provides the opportunity to satisfy rendering quality requirements that cannot be achieved by generic outline rendering. No further hinting is applied to image glyphs.

VGFont objects are created by an application, and can contain either a full set of glyphs or a subset of glyphs of an original font. VGFont objects do not contain any metric or layout information; instead, applications are responsible for all text layout operations using the information provided by the original fonts. In other words, OpenVG can assist applications in text composition by providing glyph positioning calculations; however, the text layout is the responsibility of the application.

Glyphs in a VGFont are identified by a glyph index, which is an arbitrary number assigned to a glyph when it is created. This mapping mechanism is similar to the glyph mapping used in standard font formats, such as TrueType or OpenType fonts, where each glyph is assigned an index that is mapped to a particular character code using a separate mapping table. The semantics of the mapping are application-dependent, possible mappings include:

VGFont objects are created and destroyed using the vgCreateFont and vgDestroyFont functions. Font glyphs may be added (vgSetGlyphToPath, vgSetGlyphToImage), replaced or deleted (vgClearGlyph) after the font has been created. Please refer to the “Text and font” section for the detailed API description. The following example (adapted from the tutorial code) shows a possible vector glyph definition structure and a classic OpenVG font creation and initialization function.

typedef struct {
    // glyph index within the OpenVG font object
    VGuint glyphIndex;
    // glyph origin
    VGfloat glyphOrigin[2];
    // the advance for this glyph
    VGfloat escapement[2];
    // OpenVG path commands defining glyph outline
    VGint commandsCount;
    VGubyte* commands;
    // OpenVG path coordinates defining glyph outline
    VGint coordinatesCount;
    VGfloat* coordinates;
} Glyph;

typedef struct {
    // OpenVG font object
    VGFont openvgHandle;
    // number of glyphs
    VGuint glyphsCount;
    // glyphs array
    Glyph* glyphs;
} Font;

// initialize a given font
void fontInit(Font* font) {

    VGuint i;

    // create OpenVG font object
    font->openvgHandle = vgCreateFont(font->glyphsCount);
    // create OpenVG glyphs
    for (i = 0; i < font->glyphsCount; ++i) {
        // specify segments and coordinates capacity hint, so the OpenVG driver
        // could try to optimize memory allocation
        VGPath path = vgCreatePath(VG_PATH_FORMAT_STANDARD, VG_PATH_DATATYPE_F,
                                   1.0f, 0.0f, font->glyphs[i].commandsCount,
                                   font->glyphs[i].coordinatesCount,
                                   VG_PATH_CAPABILITY_ALL);
        // upload vector data (i.e. glyph outlines)
        vgAppendPathData(path, font->glyphs[i].commandsCount, font->glyphs[i].commands,
                         font->glyphs[i].coordinates);
        // remove "editing" capabilities, so that OpenVG driver
        // can try to free some memory
        vgRemovePathCapabilities(path, VG_PATH_CAPABILITY_APPEND_FROM |
                                       VG_PATH_CAPABILITY_APPEND_TO |
                                       VG_PATH_CAPABILITY_MODIFY | 
                                       VG_PATH_CAPABILITY_TRANSFORM_FROM |
                                       VG_PATH_CAPABILITY_TRANSFORM_TO | 
                                       VG_PATH_CAPABILITY_INTERPOLATE_FROM |
                                       VG_PATH_CAPABILITY_INTERPOLATE_TO);
        // associate the created path to the given glyph index
        vgSetGlyphToPath(font->openvgHandle, font->glyphs[i].glyphIndex, path, VG_FALSE,
                         font->glyphs[i].glyphOrigin, font->glyphs[i].escapement);
        // since path objects are reference counted, destroying the object will mark its
        // handle as invalid while leaving the resource available to the VGFont object
        vgDestroyPath(path);
    }
}

// destroy a given font
void fontDestroy(Font* font) {
    // destroying the VGFont object will destroy/release all previously attached
    // paths, because within the fontInit function we already dereferenced them
    // through the vgDestroyPath call
    vgDestroyFont(font->openvgHandle);
}

The tutorial code

The tutorial draws two text strings along two different trajectories: one straight line and a curve trait. The straight line trajectory is defined by two control points, that can be moved using the mouse/touch. The wavy trajectory is defined by a single cubic Bézier curve, so four control points are needed.

The text routes: a line and a cubic Bézier curve
The text routes: a line and a cubic Bézier curve

In both cases, the text string must cover the whole trajectory, so we must implement:

  1. a function that calculates the text string length in glyph/font units, taking care of escapements and kerning

  2. a function that calculates the trajectory length in drawing surface space (control points are expressed in the drawing surface reference system)

  3. a function that calculates a font size so that the text string will span the whole route

  4. a function that draws the text string following the trajectory baseline

Lets start with the first function. In order to calculate a text string length in glyph/font units, we take advantage of the OpenVG vgDrawGlyphs function: such function is normally used to draw a set of glyphs, but under some conditions can be used to extract the metric we are looking for.

void vgDrawGlyphs(VGFont font,
                  VGint glyphCount,
                  const VGuint* glyphIndices,
                  const VGfloat* adjustments_x,
                  const VGfloat* adjustments_y,
                  VGbitfield paintModes,
                  VGboolean allowAutoHinting)

The paintModes parameter controls how glyphs are rendered. If paintModes is 0, neither glyphs are drawn: this mode is useful for determining the metrics of the glyph sequence. The user space position of the glyph (i.e. the point where the glyph origin will be placed) is determined by value of VG_GLYPH_ORIGIN; such OpenVG context parameter is updated after drawing each glyph of sequence of glyphs. So, the function (1) will look like the following one (see font_common.c file):

// temporary arrays, used by the textLineBuild function
VGuint* glyphIndices;
VGfloat* adjustmentsX;
VGfloat* adjustmentsY;

void textLineBuild(Font* font,
                   char* str,
                   size_t strLen) {

    size_t i;
    VGint leftGlyphIndex = glyphIndexFromCharCode(font, str[0]);

    // first glyph index
    glyphIndices[0] = leftGlyphIndex;
    for (i = 1; i < strLen; ++i) {
        VGint rightGlyphIndex = glyphIndexFromCharCode(font, str[i]);
        // get kerning info relative to the (leftGlyphIndex, rightGlyphIndex) couple
        KerningEntry* krn = kerningFromGlyphIndices(font,
                                                    leftGlyphIndex, rightGlyphIndex);
        // append glyph index
        glyphIndices[i] = rightGlyphIndex;
        // initialize adjustment
        adjustmentsX[i - 1] = 0.0f;
        adjustmentsY[i - 1] = 0.0f;
        // add kerning info
        if (krn != NULL) {
            adjustmentsX[i - 1] += krn->x;
            adjustmentsY[i - 1] += krn->y;
        }
        leftGlyphIndex = rightGlyphIndex;
    }
    // last adjustment entry
    adjustmentsX[strLen - 1] = 0.0f;
    adjustmentsY[strLen - 1] = 0.0f;
}

// get text string width, in glyph/font space
VGfloat textLineWidth(Font* font,
                      char* str) {

    // get number of characters
    size_t strLen = strlen(str);
    // start at origin (just a convention)
    VGfloat glyphOrigin[2] = { 0.0f, 0.0f };

    // build the sequence of glyph indices and kerning data
    textLineBuild(font, str, strLen);
    // calculate the metrics of the glyph sequence
    vgSetfv(VG_GLYPH_ORIGIN, 2, glyphOrigin);
    vgSeti(VG_MATRIX_MODE, VG_MATRIX_GLYPH_USER_TO_SURFACE);
    vgLoadIdentity();
    vgDrawGlyphs(font->openvgHandle, strLen, glyphIndices,
    	         adjustmentsX, adjustmentsY, 0, VG_FALSE);
    vgGetfv(VG_GLYPH_ORIGIN, 2, glyphOrigin);
    return glyphOrigin[0];
}

Now we take a look at the function (2), the calculation of trajectories length (surface space). For the straight line trajectory is really simple, it’s a matter to calculate a distance between two 2D points. For the curved trajectory it is simple too, we just need to invoke the OpenVG vgPathLength function, that calculates the lenght of a given path (in our case the wavy path). Function (3) is really simple: in order to calculate a font size so that a text string will span a whole trajectory, we simply divide the trajectory length by the text string length.

// straight text parameters
VGfloat straightTextFontSize;
char* straightText = "AmanithVG engine";

// wavy text parameters
VGfloat wavyTextFontSize;
char* wavyText = "OpenVG text example";

void fontSizeCalculate(void) {

    VGfloat objSpaceWidth, srfSpaceLen;

    // calculate the text width, in glyph/font space
    objSpaceWidth = textLineWidth(&font, straightText);
    // calculate the text width, in surface space
    srfSpaceLen = distance(controlPoints[0], controlPoints[1]);
    // calculate font size
    straightTextFontSize = srfSpaceLen / objSpaceWidth;
    
    // calculate the text width, in glyph/font space
    objSpaceWidth = textLineWidth(&font, wavyText);
    // calculate the text width, in surface space
    srfSpaceLen = vgPathLength(textCurve, 0,
    	                       vgGetParameteri(textCurve, VG_PATH_NUM_SEGMENTS));
    // calculate font size
    wavyTextFontSize = srfSpaceLen / objSpaceWidth;
}

Function (4) implements the drawing of a text string along a trajectory. The function has been actually split in two functions, one for the straight line trajectory, and one for the curved trajectory. The first one makes use of the vgDrawGlyphs function, in order to draw a set of given glyph indices and kerning adjustments:

// draw a line of text
void textLineDraw(Font* font,
                  char* str,
                  VGbitfield paintModes) {

    size_t strLen = strlen(str);
    // build the sequence of glyph indices and kerning data
    textLineBuild(font, str, strLen);
    // draw glyphs
    vgDrawGlyphs(font->openvgHandle, strLen, glyphIndices,
    	         adjustmentsX, adjustmentsY, paintModes, VG_FALSE);
}

void straightTextDraw(void) {

    VGfloat glyphOrigin[2] = { 0.0f, 0.0f };
    // calculate text rotation (radians)
    VGfloat rotation = atan2(controlPoints[1].y - controlPoints[0].y,
                             controlPoints[1].x - controlPoints[0].x);

    vgSetfv(VG_GLYPH_ORIGIN, 2, glyphOrigin);
    vgSeti(VG_MATRIX_MODE, VG_MATRIX_GLYPH_USER_TO_SURFACE);
    vgLoadIdentity();
    vgTranslate(controlPoints[0].x, controlPoints[0].y);
    // rotation in degrees
    vgRotate(rotation * 57.2957795f);
    // set font size
    vgScale(straightTextFontSize, straightTextFontSize);
    // draw straight text
    textLineDraw(&font, straightText, VG_FILL_PATH);
}
Text follows the straight line route
Text follows the straight line route

In order to draw a text along a wavy path, we cannot use the same vgDrawGlyphs function that we have used before, because we must change the VG_MATRIX_GLYPH_USER_TO_SURFACE matrix for every glyph, in order to match path position and slope. So we must stick to the vgDrawGlyph function (that draws a single glyph) with the additional help of vgPointAlongPath function in order to calculate, for each glyph, path position and slope.

// draw a text along the given path
void textAlongPathDraw(Font* font,
                       VGPath path,
                       char* str,
                       VGfloat fontSize,
                       VGbitfield paintModes) {

    size_t i, strLen = strlen(str);
    VGfloat cursor = 0.0f;
    // set glypth origin to (0, 0)
    VGfloat glyphOrigin[2] = { 0.0f, 0.0f };
    VGint pathSegmentsCount = vgGetParameteri(path, VG_PATH_NUM_SEGMENTS);

    // build the sequence of glyph indices and kerning data
    textLineBuild(font, str, strLen);

    // loop over all characters
    for (i = 0; i < strLen; ++i) {
        VGfloat x, y, tgx, tgy;
        // get glyph
        Glyph* glyph = glyphFromGlyphIndex(font, glyphIndices[i]);
        VGfloat halfEscapement = glyph->escapement[0] * 0.5f;

        // evaluate path
        cursor += halfEscapement;
        vgPointAlongPath(path, 0, pathSegmentsCount, cursor * fontSize,
                         &x, &y, &tgx, &tgy);
        // set matrix in order to match path position and slope
        vgSetfv(VG_GLYPH_ORIGIN, 2, glyphOrigin);
        vgSeti(VG_MATRIX_MODE, VG_MATRIX_GLYPH_USER_TO_SURFACE);
        vgLoadIdentity();
        vgTranslate(x, y);
        vgRotate(atan2(tgy, tgx) * 57.2957795f);
        // set font size
        vgScale(fontSize, fontSize);
        vgTranslate(-halfEscapement, 0.0f);
        // draw glyph
        vgDrawGlyph(font->openvgHandle, glyphIndices[i], paintModes, VG_FALSE);
        // advance cursor
        cursor += halfEscapement + adjustmentsX[i];
    }
}
Text along a wavy path
Text along a wavy path

A note about the actual Font structure defined within the code. The real structure contains two important tables: character codes map and kerning table. The first is a simple array of the following structure (see font_common.h file):

typedef struct {
    VGuint charCode;
    VGuint glyphIndex;
} MappedChar;

Each entry maps a character code to a glyph index; the map is sorted by ascending character codes, so a binary search could be used to accelerate queries like “given a character code, plese return me its glyph index”. If the font does not include a character codes map, it means that glyph indices coincide with character codes (see font_common.c file):

// compare two mapped characters
VGint mappedCharsCompare(void* arg0,
                         void* arg1) {

    MappedChar* ch0 = (MappedChar*)arg0;
    MappedChar* ch1 = (MappedChar*)arg1;
    return (VGint)ch0->charCode - (VGint)ch1->charCode;
}

// given a character code, return its glyph index
VGint glyphIndexFromCharCode(Font* font,
                             VGint charCode) {

    VGint glyphIndex = -1;

    if (font->charCodesMap != NULL) {
        MappedChar ch = { charCode, 0 };
        // "character code" to "glyph index" map is sorted by ascending character
        // codes, so we can perform a binary search in order to speedup things
        MappedChar* found = (MappedChar*)bsearch(&ch, font->charCodesMap, 
                                                 font->charCodesMapSize,
                                                 sizeof(MappedChar),
                                                 mappedCharsCompare);
        if (found) {
            glyphIndex = found->glyphIndex;
        }
    }
    else {
        // in this case glyph indices coincide with character codes
        glyphIndex = charCode;
    }
 
    return glyphIndex;
}

The kerning table is a simple array of the following structure (see font_common.h file):

typedef struct {
    // the key representing two glyph indices ((leftGlyphIndex << 16) + rightGlyphIndex)
    VGuint key;
    // the kerning amount relative to the glyphs couple
    VGfloat x;
    VGfloat y;
} KerningEntry;

Each entry maps a glyph indices couple to the corresponding (non-zero) kerning value; table is sorted by ascending glyph indices, so a binary search could be used to accelerate queries like “given two glyph indices (representing two characters), plese return me the kerning value”. If the font does not include a kerning table, it means that kerning is always 0, regardless of the glyph indices:

// compare two kerning entries
VGint kerningsCompare(void* arg0,
                      void* arg1) {

    KerningEntry* krn0 = (KerningEntry*)arg0;
    KerningEntry* krn1 = (KerningEntry*)arg1;
    return (VGint)krn0->key - (VGint)krn1->key;
}

// given a couple of glyph indices, return the relative
// kerning data (NULL if kerning is zero)
KerningEntry* kerningFromGlyphIndices(Font* font,
                                      VGint leftGlyphIndex,
                                      VGint rightGlyphIndex) {

    KerningEntry krn = {
        // the key
        ((VGuint)leftGlyphIndex << 16) + (VGuint)rightGlyphIndex,
        0.0f, 0.0f
    };

    return (KerningEntry*)bsearch(&krn, font->kerningTable,
    	                          font->kerningTableSize, sizeof(KerningEntry),
    	                          kerningsCompare);
}
The full tutorial example
The full tutorial example