Skip to content

Web API for enumerating fonts on the local system

License

Apache-2.0, Unknown licenses found

Licenses found

Apache-2.0
LICENSE
Unknown
LICENSE.md
Notifications You must be signed in to change notification settings

WICG/local-font-access

Repository files navigation

Local Font Access Explained

CI

August 14th, 2018
Last Update: April 6th, 2022

Josh Bell <[email protected]>

Table of Contents generated with DocToc

What’s all this then?

Professional-quality design and graphics tools have historically been difficult to deliver on the web. These tools provide extensive typographic features and controls as core capabilities.

One stumbling block has been an inability to access and use the full variety of professionally constructed and hinted fonts which designers have locally installed. The web's answer to this situation has been the introduction of Web Fonts which are loaded dynamically by browsers and are subsequently available to use via CSS. This level of flexibility enables some publishing use-cases but fails to fully enable high-fidelity, platform independent vector-based design tools for several reasons:

  • System font engines (and browser stacks) may display certain glyphs differently. These differences are necessary, in general, to create fidelity with the underlying OS (so web content doesn't "look wrong"). These differences reduce consistency for applications that span across multiple platforms, e.g. when pixel-accurate layout and rendering is required.
  • 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 custom font handling strategies for their applications they are bringing to the web. To use these strategies, 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.

We propose a two-part API to help address this gap:

  • A font enumeration API, which allows users to grant access to the full set of available system font metadata.
  • From each enumeration result, the ability to request low-level (byte-oriented) SFNT container access that includes the full font data.

The API provides the aforementioned tools access to the same underlying data tables that browser layout and rasterization engines use for drawing text. Examples of these data tables include the glyf table for glyph vector data, the GPOS table for glyph placement, and the GSUB table for ligatures and other glyph substitution. This information is necessary for these tools in order to guarantee both platform-independence of the resulting output (by embedding vector descriptions rather than codepoints) and to enable font-based art (treating fonts as the basis for manipulated shapes).

Note that this implies that the web application provides its own shaper and libraries for Unicode, bidirectional text, text segmentation, and so on, duplicating the user agent and/or operating system's text stack. See the "Considered alternatives" section below.

NOTE: Long term, we expect that this proposal would merge into an existing CSS-related spec rather than stand on its own.

Goals

A successful API should:

  • Where allowed, provide efficient enumeration of all local fonts without blocking the main thread
  • Ensure UAs are free to return anything they like. If a browser implementation prefers, for security and privacy, they may choose to only provide a set of default fonts built into the browser.
  • Be available from Workers
  • Allow multiple levels of privacy preservation; e.g. full access for "trusted" sites and degraded access for untrusted scenarios
  • Reflect local font access state in the Permissions API
  • Provide the ability to uniquely identify a specific font in the case of conflicting names (e.g. Web Font aliases vs. local PostScript font names)
  • Enable a memory efficient implementation, avoiding leaks and copies by design
  • Shield applications from unnecessary complexity by requiring that browser implementations produce valid SFNT data in the returned data
  • Restrict access to local font data to Secure Contexts and to only the top-most frame by default via the Permissions Policy spec
  • Sort any result list by font name to reduce possible fingerprinting entropy bits; e.g. .query() returns an iterable which will be sorted by given font names
  • Provide access to commonly used properties of fonts (for example, metrics used in CSS selectors, or when building font selection UI) without requiring parsing every font.

Possible/Future Goals

  • Direct access to localized font names (can be done via data API)
  • Access to font table data for web (network-loaded) fonts
  • Registration of new font families (extensibility)
  • Additional metadata available during enumeration (ascender, descender, baseline, x-height, etc.). Will require feedback from developers; can be determined using data access, even if not exposed during enumeration.
  • Signals when system font configuration changes (fonts added/removed); some designers work with tools that swap font portfolios at the system level
  • Provide access to named instances and subfamilies (e.g. "semibold", "light")

Non-goals

This API will not try to:

  • Fully describe how font loading works within the web platform. Fonts are a complex topic and Web Font loading implicates aspects of layout and style recalculation which are not at this time pluggable. As this design isn't addressing those aspects, we will not describe font application or CSS recalculation semantics
  • Standardize font family detection or grouping
  • Describe or provide full access to an existing WOFF/TTF/PS parser.
  • Provide access to the underlying WOFF/TTF/PS font files or describe their locations on disk.
  • Provide a guarantee that the set of available font data matches the font on disk byte to byte.
  • Normalize differences in processed font data across browser implementations. The font data that will be exposed will have been processed by browser-provided parsers, but we will not describe or constrain them except to say that their output will continue to be in a valid OpenType format. For instance, if a library like OTS reduces the available information for a font, this spec will not require implementations to do more than they already would or provide alternative ways of getting such information back from the source font files.

Key scenarios

Note: Earlier versions of this document attempted to sketch out two versions of each API; one based on FontFaceSet and the other the fully-asynchronous version that survives in this doc. While attractive from a re-use perspective, FontFaceSet (and the implied global document.fonts) implies synchronous iteration over a potentially unbounded (and perhaps slow) set of files, and each item may require synchronous IPCs and I/O. This, combined with the lack of implementations of FontFaceSet caused us to abandon this approach.

Enumerating Local Fonts

Web developers historically lack anything more than heuristic information about which local fonts are available for use in styling page content. Web developers often include complex lists of font-family values in their CSS to control font fallback in a heuristic way. Generating good fallbacks is such a complex task for designers that tools have been built to help "eyeball" likely-available local matches.

At the same time, when creating font-based content, specific fonts need to be identified and used.

Font enumeration can help by enabling:

  • Logging of likely-available fonts to improve server-side font rule generation.
  • Scripts to generate style rules based on "similar" local fonts, perhaps saving a download
  • Improving styling options for user-generated content, allowing the generation of style rules via more expressive font selection menus
// Asynchronous Query and Iteration

// User activation is required.
showLocalFontsButton.onclick = async function() {
  // This sketch returns individual FontMetadata instances rather than families:
  // In the future, query() could take filters e.g. family name, and/or options
  // e.g. locale.
  try {
    const array = await self.queryLocalFonts();

    array.forEach(metadata => {
      console.log(metadata.postscriptName);
      console.log(` full name: ${metadata.fullName}`);
      console.log(` family: ${metadata.family}`);
      console.log(` style: ${metadata.style}`);

      console.log(` italic: ${metadata.italic}`);
      console.log(` stretch: ${metadata.stretch}`);
      console.log(` weight: ${metadata.weight}`);
    });
   } catch(e) {
    // Handle error, e.g. user cancelled the operation.
    console.warn(`Local font access not available: ${e.message}`);
  }
};

Styling with Local Fonts

Advanced creative tools may wish to use CSS to style text using all available local fonts. In this case, getting access to the local font name can allow the user to select from a richer set of choices:

// User activation is required.
useLocalFontsButton.onclick = async function() {

  try {
    // Query for allowed local fonts.
    const array = await self.queryLocalFonts();

    // Create an element to style.
    const exampleText = document.createElement("p");
    exampleText.id = "exampleText";
    exampleText.innerText = "The quick brown fox jumps over the lazy dog";
    exampleText.style.fontFamily = "dynamic-font";

    // Create a list of fonts to select from, and a selection handler.
    const textStyle = document.createElement("style");
    const fontSelect = document.createElement("select");
    fontSelect.onchange = e => {
      console.log("selected:", fontSelect.value);
      // An example of styling using @font-face src: local matching.
      textStyle.textContent = `
        @font-face {
          font-family: "dynamic-font";
          src: local("${postscriptName}");
        }`;
    };

    // Populate the list with the available fonts.
    array.forEach(metadata => {
      const option = document.createElement("option");
      option.text = metadata.fullName;
      // postscriptName works well as an identifier of sorts.
      // It is unique as returned by the API, the OpenType spec expects
      // it to be in ASCII, and it can be used by @font-face src: local
      // matching to be used to style elements.
      option.value = metadata.postscriptName;
      fontSelect.append(option);
    });

    // Add all of the elements to the page.
    document.body.appendChild(textStyle);
    document.body.appendChild(exampleText);
    document.body.appendChild(fontSelect);
  } catch(e) {
    // Handle error, e.g. user cancelled the operation.
    console.warn(`Local font access not available: ${e.message}`);
  }
};

Accessing Full Font Data

Here we use the FontMetadata blob() method to access a full and valid SFNT font data payload; we can use this to parse out specific data or feed it into, e.g., WASM version of HarfBuzz or Freetype:

// User activation is required.
useLocalFontsButton.onclick = async function() {
  // This sketch returns individual FontMetadata instances rather than families:
  // In the future, query() could take filters e.g. family name, and/or options
  // e.g. locale. A user agent may return all fonts, or show UI allowing selection
  // of a subset of fonts.
  try {
    const array = await self.queryLocalFonts();

    array.forEach(metadata => {
      // blob() returns a Blob containing valid and complete SFNT
      // wrapped font data.
      const sfnt = await metadata.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(`${metadata.fullName} outline format: ${outlineFormat}`);
    }
  } catch(e) {
    // Handle error. It could be a permission error.
    console.warn(`Local font access not available: ${e.message}`);
  }
};

Requesting specific fonts

In some cases, a web application may wish to request access to specific fonts. For example, it may be presenting previously authored content that embeds font names. The query() call takes a postscriptNames option that scopes the request to fonts identified by PostScript names. Only matching fonts will be returned.

User agents may provide a different user interface to support this. For example, if the fingerprinting risk is deemed minimal, the request may be satisfied without prompting the user for permission. Alternately, a picker could be shown with only the requested fonts included.

// User activation is required.
requestFontsButton.onclick = async function() {
  try {
    const array = await self.queryLocalFonts({postscriptNames: ['Verdana', 'Verdana-Bold', 'Verdana-Italic']});

    array.forEach(metadata => {
      console.log(`Access granted for ${metadata.postscriptName}`);
    });

  } catch(e) {
    // Handle error. It could be a permission error.
    console.warn(`Local font access not available: ${e.message}`);
  }
};

Detailed design discussion (data)

Several aspects of this design need validation:

  • This design tries to address concerns with FontFaceSet and friends at the cost of introducing a new API surface.

Detailed design discussion (enumeration)

Several aspects of this design need validation:

  • What precisely is being iterated over needs to be identified. Is it over files on disk, families, or other groupings that a system level enumeration API provides? There is not a 1:1 relationship between files and named instances.
  • Grouping of related fonts and variants into a parent object is difficult. Some families can be represented by one file or many, and the definition of a "family" is heuristic to start with. Is grouping needed? Design currently leaves this open to future additions.
  • This design tries to address concerns with FontFace, FontFaceSet and friends at the cost of introducing a new API surface.

Other issues that feedback is needed on:

  • Font "name" propertes in OpenType are quite logically a map of (language tag → string) rather than just a string. The sketch just provides a single name (the "en" variant or first?) - should we introduce a map? Or have query() take a language tag? Or defer for now?

Privacy and Security Considerations

  • The local-fonts permission appears to provide a highly fingerprintable surface. However, UAs are free to return anything they like. For example, the Tor Browser or Brave may choose to only provide a set of default fonts built into the browser. Similarly, UAs are not required to provide table data exactly as it appears on disk. Browsers, e.g., may choose to only provide access to table data after sanitization via OTS and would fail to reflect certain tables entirely.

  • Another fingerprinting vector could be the font version. Providing access to the raw font data could provide further bits of entropy, due to developers being able to discern between users having certain versions of a given font file.

  • Some users (mostly in big organizations) have custom fonts installed on their system. Listing these could provide highly identifying information about the user's company.

  • Wherever possible, these APIs are 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 entropy bits, and use cases we want to enable aren't assisted by retaining this ordering. As a result, this API requires that the returned data be sorted before being returned.

Considered alternatives

FontFaceSource

FontFaceSource is specified in the CSS 3 Font Loading draft. At first glance, this is the most appropriate interface from which to hang something like the proposed query() method. It is, however, a synchronous iterator. In conversation with implemeners, this contract may be problematic from a performance perspective across OSes. Instead of providing a potentially unbounded way for developers to naively lock up the main thread, we've chosen to introduce a different root object from which to hang asynchronous iteratation and query methods.

This might be the wrong thing to do! Hopefully vendors can weigh in more thoroughly on this point.

Add a browser/OS-provided font chooser

The proposed API exposes some more bits about the user via the web that could improve fingerprinting efforts. The bits are based on the presence or lack of presence of certain fonts in the enumeration-returned list.

An alternative to the API that only exposes a single user-selected font was considered. This alternative enumeration API would trigger a browser/OS-provided font chooser and, from that chooser, the user would select a single font. This would reduce the bits exposed to help mitigate fingerprinting at the cost of significant new functionality.

We've heard interest from partners in a full-fledged enumeration API to get access to the list of available fonts on the system, and haven't heard interest in a font-chooser approach to the enumeration API. However, we're keeping the alternative in mind as we balance the need for new functionality with privacy concerns.

Exposing Font Tables as a map

The proposed API exposes font data as Blob containing a complete and valid SFNT data payload, itself containing valid OpenType font data.

An alternative to the API is to expose the data as a map of the tables contained in the SFNT wrapper. This alternative would provide a higher level API whereby font table data could be parsed individually instead of the font data as a whole.

We've heard from partners that this alternative does not provide a lot of value, and may in fact be counter-productive, because intended use-cases of this API subsume font data parsing tasks and require re-assembling the tables into a whole.

Metadata Properties

Including a subset of useful font metrics (ascender, descender, xheight, baseline) in the metadata was considered. Some are complicated (baseline), others more straightforward but may not be of practical use, especially if the full pipeline involves passing tables into Harfbuzz/FreeType for rendering. They are not included in the latest version of the sketch.

Additional metadata properties such whether the font uses color (SBIX, CBDT, SVG etc), or is a variable font could be provided, but may not be of use.

Exposing Building Blocks

To be of use, font table data must be consumed by a shaping engine such as HarfBuzz, in conjunction with Unicode libraries such as ICU for bidirectional text support, text segmentation, and so on. Web applications could include these existing libraries, for example compiled via WASM, or equivalents. Necessarily, user agents and operating systems already provide this functionality, so requiring web applications to include their own copies leads to additional download and memory cost. In some cases, this may be required by the web application to ensure identical behavior across browsers, but in other cases exposing some of these libraries directly to script as additional web APIs could be beneficial.

We are not considering these options right now for the API, but we'll keep them in mind in case there's demand for them.

(Parts of ICU are being incrementally exposed to the web via the ECMA-402 effort.)

About

Web API for enumerating fonts on the local system

Topics

Resources

License

Apache-2.0, Unknown licenses found

Licenses found

Apache-2.0
LICENSE
Unknown
LICENSE.md

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published