Converting Fonts to WOFF2 for the web

While redoing my website from scratch, I was looking to ditch the Google Fonts API for privacy reasons. I ended up downloading the full font from Google Fonts. By full font, I mean a TTF file—which is not appropriate for the web. My goal was to make a web friendly font, or woff2 which is practically supported on all modern browsers since 2018.

Font Faces

Before I share the commands to make woff2s, I'd like to cover what we have to put in our CSS file. A font family "font-family" is the name of the font we will be using. In CSS, the font-family is like a variable name that points to a collection of font faces. Each font face "@font-face" specifies what characteristics that configuration supports. Is the font-style normal or italic? Is the font-weight bold, normal, or some some weight number? What characters are supported "unicode-range" in this @font-face?

Each @font-face configuration has a source "src" which is where the actual woff2 file can be found if the browser determines it needs that @font-face.

There are more properties like size-adjust which modify how the font is drawn after it is loaded, however these won't be covered. At least be aware that there are variable fonts which can be tuned with font-variation-settings on most modern browsers. Most fonts are "static" fonts rather than "variable" fonts, what I share below will be helpful for converting static fonts for your website.

A concrete example

Here's what the Google Font API for Roboto:italic and Roboto returns for a given web request. @font-face sections for other character sets have been omitted.

/* latin */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/​s/​roboto/​v30/​KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin */
@font-face {
  font-family: 'Roboto';
  font-style: italic;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/​s/​roboto/​v30/​KFOkCnqEu92Fr1Mu51xIIzIXKMny.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

As you can see, we have two related @font-faces identified by the font-family 'Roboto'. They happen to share the same unicode-range and font-weight, but otherwise have different font-styles. Together, the above four properties form the lookup key to which font src should be downloaded and used to render a character on screen.

In the above example, there is no bold @font-face. Will you get bold text if you used <strong>? The answer is: it depends on the font. If it does use the same font-family, it might not have the visual properties you would expect. For that reason, I choose to generate the following variants:

  • Regular
  • Italic
  • Bold
  • Bold + Italic

If the font comes with a lighter variant, you are of course able to make a lighter @font-face for it and its italic form.

Building our own web font

For this example, I will be using the Gloria Hallelujah Handwriting font. Inside, we only have one file, GloriaHallelujah-Regular.ttf. We will not be getting any intentionally italic or bold web fonts from this file.

a picture of the font family Gloria Hallelujah

There's a handy tool for fonts called fonttools. It runs on python 3. If you do not python 3 installed, then install python. Next, because woff2 requires a compression technology called Brotli, you will need to install brotli with pip3 install brotli. And last, install fonttools with pip install fonttools. This will install an important utility called pyftsubset, which we will use.

The following demonstration includes first: converting from TTF to woff2, and second: restricting the character set to have only what we need. I specifically will use the unicode codepoints for 'H', 'e', 'l', and 'o', such that I can say "Hello" in this font. That comes out to be U+0048,U+0065,U+006C,U+006F, which I wrote while referencing an ASCII table. If you want all of the ASCII range, then the unicodes parameter could be set to U+0000-U00FF.

pyftsubset "GloriaHallelujah-Regular.ttf" \
  --output-file="custom-font-example.woff2" \
  --flavor=woff2 \
  --layout-features="*" \
  --unicodes="U+0048,U+0065,U+006C,U+006F"

The file that comes out "custom-font-example.woff2" is only a mere 2068 bytes! That is quite small! Overall, this tool is easy to use, if you use it in exactly this way. Come up with a unicode range list, provide the input file and output file, and keep the flavor (that is, the format) set to woff2 and layout-features sot te *.

There are a lot of options to the pyftsubset tool for more advanced use cases. This should be enough for most people.

Making our CSS

To use this new woff2 font on the web, we need two files: CSS that describes the @font-face and the font file we made earlier. Below is what that CSS could look like.

@font-face {
  font-family: 'Example Font';
  font-style: normal;
  font-weight: normal;
  font-display: swap;
  src: url(./example-font.woff2) format('woff2');
  unicode-range: U+0048,U+0065,U+006C,U+006F;
}

.example-font {
  font-family: 'Example Font', monospace;
  font-size: 400%;
  text-align: center;
}

I keep the font file in the same folder as the CSS file so it can load easily with a relative URL. While I use a relative URL, in Google's example, src is set to a full URL. This is fine too, though it requires more connections to be made and can be a hassle if you're using Content-Security-Policy.

Using the new font

Tada! Here's "Hello world" in our new font!

Hello World

Looks a little off, right? The characters 'W', 'r', and 'd' are being rendered with a monospace font. The 'Example Font' font-family literally does not have the characters 'W', 'r', or 'd' available! In this case, the browser falls back to the monospace font-family. Plan for what contexts your font will be used in.

Unicode range support

I actually do not recommend encoding only the characters you will display. Instead, select ranges of characters that you plan to support in general. You can always reference Google's Roboto font-family for unicode-ranges. For latin characters, the list appears to be: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD. Although, your font might not support all the unicode codepoints you ask for.

To find out what codepoints were missing, use the option --no-ignore-missing-unicodes. You may get an output like:

fontTools.subset.Subsetter.MissingUnicodesSubsettingError: [ 'U+0000', 'U+0001', 'U+0002', 'U+0003', ...

If you are concerned, make note of what ranges are not available and adjust your @font-face accordingly. With trial and error, I find that Gloria Hallelujah only supports a unicode range set of U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+20AC, U+2122, U+2212. I started with the Roboto latin range list, so this may be incomplete. There are better methods to find out the available unicode codepoints. However, to keep the toolbox small in this article, I simply used --no-ignore-missing-unicodes to demonstrate this.

Wrap up

To support fonts on the web, create a woff2 file using pyftsubset on a TTF file with a command like the one above. You may want to create multiple woff2 files for each variant (bold, italic, or a combo thereof) and a corresponding @font-face. Each @font-face must link to the woff2 file with its src and label it appropriately with font-family, font-style, font-weight, and unicode-range. And finally, set the font-family on the content you wish to style.