Fun with fonts and Sharp in Lambda
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...
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:
-
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. -
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?
SVG | PNG |
---|---|
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.
SVG | PNG |
---|---|
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.
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.
SVG | PNG |
---|---|
What’s the problem now? The browser dev-tools show the problem:
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:
-
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.
-
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.
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
-
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.
-
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.
-
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.