Working with fonts using opentype.js

Introduction

There are many problems when working with fonts in the web. Starting from the lack of built in font loading detection, to problem with measuring text dimensions when rendering it on the canvas. There are many polyfills that try to fill in that hole, but in this article we will focus on a little bit different approach. We will draw text on the canvas using the opentype.js library. The library instead of rendering text directly to the canvas, first loads the font file and parses it. Then it reads information about the font and each glyph (letter). Thanks to this information, the library is able to precisely measure text dimensions and render the font letters correctly into the canvas. It uses generated paths instead of fonts loaded by the browser in the standard way.

You can download the opentype.js library directly from this github repository and include it into the document's head section

<head>
    <script src="js/opentype.js"></script>
</head>

or before the closing BODY tag.

<body>
    <!-- HTML code goes here -->
    
    <script src="js/opentype.js"></script>
    <script src="js/main.js"></script>
</body>

Make opentype.js working on Tizen

When trying to use opentype.js on Tizen, you can encounter a problem with the loading of the fonts. There is an error in Tizen that doesn't set the proper status code of the XMLHttpRequest object, so we have to modify the loadFromUrl() function of the opentype.js library. In the listing below you can see the fixed version of this function. You should just open opentype.js file, look for the loadFromUrl() function and replace it with the version below.

function loadFromUrl(url, callback) {
  var request = new XMLHttpRequest();
  request.open('GET', url, true);
  request.responseType = 'arraybuffer';
  request.onload = function() {
    return callback(null, request.response);
  };
  request.send();
}

Usage

The library is very easy to use. First, you have to load the font into the memory. You can do it by using the opentype.load() function that takes the font URL as a first argument and a callback function as the second.

opentype.load('fonts/font-name.ttf', function (err, font) {
    if (err) {
        alert('Can not load font');
        return;
    }
    
    /* Do something with the font */
});

You can also parse the font from an ArrayBuffer if the font has already been loaded. Thanks to that it can also be used in Node.js.

var font = opentype.parse(buffer);

Let's move on to displaying some text on the canvas. There are two ways of rendering the font using opentype.js. The first one is to use the draw() function of the font object returned from the opentype.load() or the opentype.parse() function.

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

font.draw(
    ctx,
    'Hello World', // Text to render
    100, // X position
    200, // Y position
    100, // Font size
    {
        kerning: true // Take kerning into account when rendering font
    } // Options
);

The second option is taking the path object of the font and rendering it on the canvas.

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

var path = font.getPath(
    'Hello World', // Text to render
    100, // X position
    200, // Y position
    100, // Font size
    {
        kerning: true // Take kerning into account when rendering font
    } // Options
);

path.draw(ctx); // Draw path on the canvas

It's just that simple.

Measuring text/font dimensions

As mentioned before there are many problems in measuring text/font dimensions. To measure text using the standard approach we have to use the measureText() function of the context object. It returns the TextMetrics object containing information about the text/font. Unfortunately there is only information about text width available, even though there is a standard that contains far more information. Fortunately we can get rid of this problem by using opentype.js. We can calculate text metrics by ourselves.

Here is the sample code that calculates widthactualBoundingBoxAscentactualBoundingBoxDescentfontBoundingBoxAscent and fontBoundingBoxDescent.

var measureText = function(text) {
    var ascent = 0;
    var descent = 0;
    var width = 0;
    var scale = 1 / font.unitsPerEm * fontSize;
    var glyphs = font.stringToGlyphs(text);

    for (var i = 0; i < glyphs.length; i++) {
        var glyph = glyphs[i];
        if (glyph.advanceWidth) {
            width += glyph.advanceWidth * scale;
        }
        if (i < glyphs.length - 1) {
            kerningValue = font.getKerningValue(glyph, glyphs[i + 1]);
            width += kerningValue * scale;
        }
        ascent = Math.max(ascent, glyph.yMax);
        descent = Math.min(descent, glyph.yMin);
    }

    return {
        width: width,
        actualBoundingBoxAscent: ascent * scale,
        actualBoundingBoxDescent: descent * scale,
        fontBoundingBoxAscent: font.ascender * scale,
        fontBoundingBoxDescent: font.descender * scale
    };
};

Firstly, we have to calculate a scale by which we will multiple all paths' points, to get the actual point in space for the given font size.

var scale = 1 / font.unitsPerEm * fontSize;

Later, we have to get paths for each letter/glyph using the font.stringToGlyphs() function.

var glyphs = font.stringToGlyphs(text);

Looping through each letter we can measure the whole text width by summing glyph.advancedWidth, which is the letter width without the kerning. Next, to calculate the number, we have to add kerning between each pair of letters. Kerning is usually a negative number. You can see at the image below what is the difference between letters' distance with and without kerning.

While looping through glyphs we also calculate the max and min y coordinates for each letter. Those values are needed to calculate the actualBoundingBoxAscent and the actualBoundingBoxDescent.

There are two more crucial properties for measuring a font. The fontBoundingBoxAscent and fontBoundingBoxDescent. They are easy to calculate because the font object delivers them in the ascender and the descender fields.

Sample application

We've prepared a sample application that uses opentype.js to render the text and display font/text bounding box. You can see how the application looks like at the screenshot below.

There are two input fields at the bottom of the screen, where you can set the font size and the displayed text.

Summary

Using opentype.js can help you get rid of all problems with displaying and measureing font/text on the web canvas object.

File attachments: