Fun with fonts and Sharp in Lambda

A man, looking desperate and sad, face in his palms. Two dark thought bubble emerge
from the head, each showing the letters ABC in a different font. Lightnings in the background.

The last post showed how to generate images in AWS Lambda using JavaScript and Sharp. We can create an SVG image, we can convert it to PNG. We can do that in AWS Lambda or in local development. Now we want to embed some text. And we want to make sure that the correct font is used in all those environments. This is going to be fun...

This article is part of the "Terraform - placeruler.knappi.org" series, you may also want to read the other articles, especially the older ones:

In the last post, we’ve implemented an AWS Lambda that creates an SVG in a specified size. We also implemented on-the-fly conversion to PNG using Sharp.

Now let’s add some text. This should be easy, right?

But first, since we want to focus on fonts and text, I changed the code a bit:

  1. It will look at the extension of the requested URL. If the URL ends on .png, it will use Sharp to generate a PNG image. If it ends on .svg, it will return the SVG directly.

  2. It will always use the following SVG, no matter what size was requested:

<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300">
    <!-- black background-->
    <rect fill="black" x="0" y="0" width="100%" height="100%"/>
    <!-- One line from top-left to bottom-right, and from bottom-left to top-right -->
    <path d="M 0 0 L 300 300 M 300 0 L 0 300" stroke="#ff7" opacity="0.7"/>
    <!-- The letter "x" centered in the image, in Roboto Black -->
    <text
            x="150"
            y="150"
            font-family="Roboto Black"
            font-size="200"
            text-anchor="middle"
            alignment-baseline="middle"
            fill="#77f"
            opacity="0.7"
    >x
    </text>
</svg>

So what does the image look like?

SVGPNG
an X centered in the imagean X centered horizontally in the image, its bottom centered vertically\

The SVG looks as expected: An X in the correct font, Roboto Black, centered both horizontally and vertically.

The PNG, on the other hand, is a bit off. This is due to this issue: Sharp doesn’t support alignment-baseline (nor dominant-baseline) at the time of this writing.

We can probably live with that, and in the following examples, we adjust the position programmatically.

Missing fonts in the Lambda

Strange things happen when we let the Lambda generate the PNG, though.

SVGPNG
an X centered in the imagean small box slightly below the center of the image

The X is just a small box. It looks like the font is not available in the Lambda.

In a browser, on a regular web-page, we would specify a fallback font, like that:

<text font-family="Roboto Black,sans">X</text>

This is not what we want here. We don’t want to use a different font, we want to correct font, no matter on which platform. The Sharp documentation states that “fontconfig is used to find the relevant fonts.” So let’s create a font-configuration.

To do that, we create a folder fonts in the lambda directory and the file for roboto-latin-900-normal.ttf there, along with a font-config file fonts.conf containing this:

<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
    <dir prefix="relative">./</dir>
    <cachedir>/tmp/fonts-cache/</cachedir>
    <config></config>
</fontconfig>

The magic lies in the line <dir prefix="relative">./</dir>. It tells Sharp the fonts are found in the current folder, relative to the fonts.conf file.

Then, we need to tell Sharp to use that font-config.

Warning: Feature confusion

When I tried this I was a bit confused because Sharp supports a font and fontfile property in the constructor. I tried to set it and nothing happened. Then I noticed that there is another feature in Sharp: The following configuration will explicitly render an image containing text, and nothing else, in the specified font.

sharp({
  text: {
    text: "X",
    font: "Roboto Black",
    fontfile: "/absolute/path/to/font-file.ttf",
    height: 50,
    width: 50,
  },
});

However, this option doesn’t have any effect on SVG rendering.

If we want Sharp to use the font configuration, we need to set the environment variable FONTCONFIG_PATH to point to the fonts.conf file. The following function does that.

import { fileURLToPath } from "node:url";

export function initFontConfig() {
  // Resolve font-config via a relative path from the current file.
  const fontsConf = fileURLToPath(import.meta.resolve("../fonts/fonts.conf"));
  process.env.FONTCONFIG_PATH = path.dirname(fontsConf);
}

We can now call this function from our Lambda and from our dev-environment. Even if we don’t have the font installed globally in the system, the image is going to be correct.

Or is it…

Missing fonts on the local machine

SVG and PNG were using the correct on my local machine, but only because it was already installed. This is a pitfall that can easily happen.

Imagine, we’re on a different machine now. We start the dev-server and look at the PNG and SVG that is generated now.

SVGPNG
an X in a serif font centered in the imagea bold sans-serif (Roboto Black) X centered in the image

What’s the problem now? The browser dev-tools show the problem:

screenshot of a browser showing an X in serif font. Dev tools are open, showing the text-element inspected, with font-family being \

While the font-family specified in the computed styles is indeed “Roboto Black”, the rendered font is “Liberation Serif.” This happens because if “Roboto Black” is not installed on this machine, the browser will use a default font as fallback.

This is not what we want. What can we do about it?

I can see two ways to do this:

  1. We can convert the letters to SVG paths. I’ve seen this in the example of the fontkit package. This might be something worth investigating, but doing this correctly seems complicated to me.

  2. We can embed the font inside the SVG. This looks pretty easy at the first glance: We just need to add a style-element to the SVG. The font needs to be inlined as data-url, external URLs aren’t allowed in SVG images.

The second option is what I chose.


<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300">
    <defs>
        <style>
            @font-face {
            font-family: 'Roboto Black';
            src: url(data:application/font-woff2;charset=utf-8;base64,[...lots of base64 digits...]) format('woff2');
            font-weight: 900;
            font-style: normal;
            }
        </style>
    </defs>
    <!-- Rest of the SVG... -->
</svg>

There is one issue, though: Font files can be big. So far, our SVG was less than 1 KB in size. With the embedded font, get more than 25 KB, and other fonts may be even larger.

But there is a solution to this problem. We can create a subset font, that only contains the glyphs that are actually used in the SVG.

There are public tools like FontSquirrels Webfont Generator and npm packages like subset-font that do this for us. In the example repository I’ve created a small script using subset-font that creates a woff2 file containing only the glyphs 0123456789xX of Roboto Black. The base64 representation of this file is about 5 KB large, which I declare as acceptable here and now.

Legal implications

If you want to embed fonts into your project, your graphics, or your websites, you should always check the copyright and license agreements of those fonts. You may not be allowed to do so, especially if you’re modifying them, and creating a subset font is definitely a modification.

Even if you’re using one of the many free fonts, like Roboto, you might need to include the license agreement in the SVG.

I can’t give you legal advice on this, so don’t complain if you get sued. If you want to be sure, consult a lawyer.

I personally chose to do this by including the link to the license and the copyright notice, but this might not be enough.

Conclusion

  1. Just because an image is displayed correctly on your machine doesn’t mean that it also works on another machine. If we want to create the same image on different machines, we need to include the fonts.

  2. With Sharp, we need to specify font-config, for SVGs, we need to embed the fonts as data-url. Sadly, Sharp doesn’t support the embedded fonts, so if we want to produce SVGs and PNGs, we need to use both techniques.

  3. Fonts are often under copyright, which means you might not be allowed to modify them or even include them in your SVG. Even free fonts may require you to include the license. Consult a lawyer if you want to be sure.

The examples in this post are available in the “0033-sharp-lambda-and-fonts” branch of my example repository.