WICG / local-font-access

Web API for enumerating fonts on the local system
https://wicg.github.io/local-font-access
Apache License 2.0
75 stars 16 forks source link
shipping-chromium

Local Font Access Explained

CI

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

Josh Bell <jsbell@google.com>

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:

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

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:

Possible/Future Goals

Non-goals

This API will not try to:

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:

// 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:

Detailed design discussion (enumeration)

Several aspects of this design need validation:

Other issues that feedback is needed on:

Privacy and Security Considerations

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.)