Learn how the Local Font Access API allows you to access the user's locally installed fonts and obtain low-level details about them
Web safe fonts
If you have been doing web development long enough, you may remember the so-called
web safe fonts.
These fonts are known to be available on nearly all instances of the most used operating systems
(namely Windows, macOS, the most common Linux distributions, Android, and iOS). In the early 2000s,
Microsoft even spearheaded an
initiative
called TrueType core fonts for the Web that provided these fonts for free download with the
objective that "whenever you visit a Web site that specifies them, you'll see pages exactly as the
site designer intended". Yes, this included sites set in
Comic Sans MS. Here is a
classic web safe font stack (with the ultimate fallback of whatever
sans-serif
font) might look like this:
body {
font-family: Helvetica, Arial, sans-serif;
}
Web fonts
The days where web safe fonts really mattered are long gone. Today, we have
web fonts, some of which are
even variable fonts that we can tweak further by changing the values for the
various exposed axes. You can use web fonts by declaring an
@font-face
block at the start of the CSS,
which specifies the font file(s) to download:
@font-face {
font-family: 'FlamboyantSansSerif';
src: url('flamboyant.woff2');
}
After this, you can then use the custom web font by specifying the
font-family
, as normal:
body {
font-family: 'FlamboyantSansSerif';
}
Local fonts as fingerprint vector
Most web fonts come from, well, the web. An interesting fact, though, is that the
src
property in the @font-face
declaration, apart from the
url()
function, also accepts a
local()
function. This allows custom fonts to be loaded (surprise!) locally. If the user happens to have
FlamboyantSansSerif installed on their operating system, the local copy will be used rather than
it being downloaded:
@font-face {
font-family: 'FlamboyantSansSerif';
src: local('FlamboyantSansSerif'), url('flamboyant.woff2');
}
This approach provides a nice fallback mechanism that potentially saves bandwidth. On the Internet,
unfortunately, we cannot have nice things. The problem with the local()
function is that it can be
abused for browser fingerprinting. Turns out, the list of fonts a user has installed can be pretty
identifying. A lot of companies have their own corporate fonts that are installed on employees'
laptops. For example, Google has a corporate font called Google Sans.
An attacker can try to determine what company someone works for by testing for the existence of a large number of known corporate fonts like Google Sans. The attacker would attempt rendering text set in these fonts on a canvas and measure the glyphs. If the glyphs match the known shape of the corporate font, the attacker has a hit. If the glyphs do not match, the attacker knows that a default replacement font was used since the corporate font was not installed. For full details on this and other browser fingerprinting attacks, read the survey paper by Laperdix et al.
Company fonts apart, even just the list of installed fonts can be identifying. The situation with this attack vector has become so bad that recently the WebKit team decided to "only include [in the list available fonts] web fonts and fonts that come with the operating system, but not locally user-installed fonts". (And here I am, with an article on granting access to local fonts.)
The Local Font Access API
The beginning of this article may have put you in a negative mood. Can we really not have nice things? Fret not. We think we can, and maybe everything is not hopeless. But first, let me answer a question that you might be asking yourself.
Why do we need the Local Font Access API when there are web fonts?
Professional-quality design and graphics tools have historically been difficult to deliver on the web. One stumbling block has been an inability to access and use the full variety of professionally constructed and hinted fonts that designers have locally installed. Web fonts enable some publishing use-cases, but fail to enable programmatic access to the vector glyph shapes and font tables used by rasterizers to render the glyph outlines. There is likewise no way to access a web font's binary data.
- Design tools need access to font bytes to do their own OpenType layout implementation and allow design tools to hook in at lower levels, for actions such as performing vector filters or transforms on the glyph shapes.
- Developers may have legacy font stacks for their applications that they are bringing to the web. To use these stacks, they usually require direct access to font data, something web fonts do not provide.
- Some fonts may not be licensed for delivery over the web. For example, Linotype has a license for some fonts that only includes desktop use.
The Local Font Access API is an attempt at solving these challenges. It consists of two parts:
- A font enumeration API, which allows users to grant access to the full set of available system fonts.
- From each enumeration result, the ability to request low-level (byte-oriented) SFNT container access that includes the full font data.
Browser support
How to use the Local Font Access API
Feature detection
To check if the Local Font Access API is supported, use:
if ('queryLocalFonts' in window) {
// The Local Font Access API is supported
}
Enumerating local fonts
To obtain a list of the locally installed fonts, you need to call window.queryLocalFonts()
. The
first time, this will trigger a permission prompt, which the user can approve or deny. If the user
approves their local fonts to be queried, the browser will return an array with fonts data
that you can loop over. Each font is represented as a FontData
object with the properties family
(for example, "Comic Sans MS"
), fullName
(for example, "Comic Sans MS"
), postscriptName
(for
example, "ComicSansMS"
), and style
(for example, "Regular"
).
// Query for all available fonts and log metadata.
try {
const availableFonts = await window.queryLocalFonts();
for (const fontData of availableFonts) {
console.log(fontData.postscriptName);
console.log(fontData.fullName);
console.log(fontData.family);
console.log(fontData.style);
}
} catch (err) {
console.error(err.name, err.message);
}
If you are only interested in a subset of fonts, you can also filter them based on the PostScript
names by adding a postscriptNames
parameter.
const availableFonts = await window.queryLocalFonts({
postscriptNames: ['Verdana', 'Verdana-Bold', 'Verdana-Italic'],
});
Accessing SFNT data
Full SFNT access is available via the blob()
method of the
FontData
object. SFNT is a font file format which can contain other fonts, such as PostScript,
TrueType, OpenType, Web Open Font Format (WOFF) fonts and others.
try {
const availableFonts = await window.queryLocalFonts({
postscriptNames: ['ComicSansMS'],
});
for (const fontData of availableFonts) {
// `blob()` returns a Blob containing valid and complete
// SFNT-wrapped font data.
const sfnt = await fontData.blob();
// Slice out only the bytes we need: the first 4 bytes are the SFNT
// version info.
// Spec: https://docs.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font
const sfntVersion = await sfnt.slice(0, 4).text();
let outlineFormat = 'UNKNOWN';
switch (sfntVersion) {
case '\x00\x01\x00\x00':
case 'true':
case 'typ1':
outlineFormat = 'truetype';
break;
case 'OTTO':
outlineFormat = 'cff';
break;
}
console.log('Outline format:', outlineFormat);
}
} catch (err) {
console.error(err.name, err.message);
}
Demo
You can see the Local Font Access API in action in the
demo below. Be sure to also check out the
source code. The demo
showcases a custom element called <font-select>
that
implements a local font picker.
Privacy considerations
The "local-fonts"
permission appears to provide a highly fingerprintable surface. However,
browsers are free to return anything they like. For example, anonymity-focused browsers may choose
to only provide a set of default fonts built into the browser. Similarly, browsers are not required
to provide table data exactly as it appears on disk.
Wherever possible, the Local Font Access API is designed to only expose exactly the information needed to enable the mentioned use cases. System APIs may produce a list of installed fonts not in a random or a sorted order, but in the order of font installation. Returning exactly the list of installed fonts given by such a system API can expose additional data that may be used for fingerprinting, and use cases we want to enable are not assisted by retaining this ordering. As a result, this API requires that the returned data be sorted before being returned.
Security and permissions
The Chrome team has designed and implemented the Local Font Access API using the core principles defined in Controlling Access to Powerful Web Platform Features, including user control, transparency, and ergonomics.
User control
Access to a user's fonts is fully under their control and will not be allowed unless the
"local-fonts"
permission, as listed in the
permission registry, is granted.
Transparency
Whether a site has been granted access to the user's local fonts will be visible in the site information sheet.
Permission persistence
The "local-fonts"
permission will be persisted between page reloads. It can be revoked via the
site information sheet.
Feedback
The Chrome team wants to hear about your experiences with the Local Font Access API.
Tell us about the API design
Is there something about the API that does not work like you expected? Or are there missing methods or properties that you need to implement your idea? Have a question or comment on the security model? File a spec issue on the corresponding GitHub repo, or add your thoughts to an existing issue.
Report a problem with the implementation
Did you find a bug with Chrome's implementation? Or is the implementation different from the spec?
File a bug at new.crbug.com. Be sure to include as much detail as you can,
simple instructions for reproducing, and enter Blink>Storage>FontAccess
in the Components box.
Glitch works great for sharing quick and easy repros.
Show support for the API
Are you planning to use the Local Font Access API? Your public support helps the Chrome team to prioritize features and shows other browser vendors how critical it is to support them.
Send a tweet to @ChromiumDev using the hashtag
#LocalFontAccess
and let
us know where and how you're using it.
Helpful links
- Explainer
- Spec draft
- Chromium bug for font enumeration
- Chromium bug for font table access
- ChromeStatus entry
- GitHub repo
- TAG review
- Mozilla standards position
Acknowledgements
The Local Font Access API spec was edited by Emil A. Eklund, Alex Russell, Joshua Bell, and Olivier Yiptong. This article was reviewed by Joe Medley, Dominik Röttsches, and Olivier Yiptong. Hero image by Brett Jordan on Unsplash.