Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

General update to drop getTable() in favor of blob() #47

Merged
merged 10 commits into from
Oct 12, 2020
129 changes: 69 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Local Font Access Explained

> August 14th, 2018<br>
> Last Update: September 1st, 2020
> Last Update: October 7th, 2020
>
> Alex Russell `<slightlyoff@google.com>`<br>
> Josh Bell `<jsbell@google.com>`<br>
Expand All @@ -15,20 +15,21 @@
## 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](https://developer.mozilla.org/en-US/docs/Learn/CSS/Styling_text/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 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.
* 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 fonts.
* From each enumeration result, the ability to request low-level (byte-oriented) SFNT container access that includes the full font data.

Taken together, these provide high-end 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](https://docs.microsoft.com/en-us/typography/opentype/spec/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).
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](https://docs.microsoft.com/en-us/typography/opentype/spec/glyf) table for glyph vector data, the [GPOS](https://docs.microsoft.com/en-us/typography/opentype/spec/gpos) table for glyph placement, and the [GSUB](https://docs.microsoft.com/en-us/typography/opentype/spec/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.

Expand All @@ -44,11 +45,10 @@ A successful API should:
* 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](https://developer.mozilla.org/en-US/docs/Web/API/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 access to all [browser-allowed font tables](https://chromium.googlesource.com/external/ots/+/master/docs/DesignDoc.md) (may vary per browser)
* Enable a memory efficient implementation, avoiding leaks and copies by design
* Shield applications from unnecessary complexity by requiring that browser implementations produce valid OpenType 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 [Feature Policy](https://wicg.github.io/feature-policy) spec
* Sort any result list by font name to reduce possible fingerprinting entropy bits; e.g. .query() returns an iterator which will be sorted by Unicode code point given font names
* 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](https://w3c.github.io/webappsec-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](https://infra.spec.whatwg.org/#list-sort-in-ascending-order) by given font names

#### Possible/Future Goals

Expand All @@ -72,7 +72,7 @@ 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 `FontFaceSource` and the other the fully-asynchronous version that survives in this doc. While attractive from a re-use perspective, [`FontFaceSource`](https://drafts.csswg.org/css-font-loading/#font-face-source) (and the implied global `window.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 `FontFaceSource` caused us to abandon this approach.
> 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`](https://drafts.csswg.org/css-font-loading/#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

Expand All @@ -87,20 +87,25 @@ Font enumeration can help by enabling:
```js
// Asynchronous Query and Iteration
(async () => { // Async block
// May prompt the user:
const status = await navigator.permissions.request({ name: "local-fonts" });
if (status.state !== "granted")
const status = await navigator.permissions.query({ name: "font-access" });
if (status.state === "denied")
throw new Error("Cannot enumerate local fonts");

// 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.
const fonts_iterator = navigator.fonts.query();

for await (const metadata of fonts_iterator) {
console.log(metadata.postscriptName);
console.log(metadata.fullName);
console.log(metadata.family);
const iterable = navigator.fonts.query();

try {
// May prompt the user:
for await (const metadata of iterable) {
console.log(metadata.postscriptName);
console.log(metadata.fullName);
console.log(metadata.family);
}
} catch(e) {
// Handle error. It could be a permission error.
throw new Error(e);
}
})();
```
Expand All @@ -110,26 +115,30 @@ Font enumeration can help by enabling:
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:

```js
const font_select = document.createElement("select");
font_select.onchange = e => {
console.log("selected:", font_select.value);
const fontSelect = document.createElement("select");
fontSelect.onchange = e => {
console.log("selected:", fontSelect.value);
// Use the selected font to style something here.
};

document.body.appendChild(font_select);
document.body.appendChild(fontSelect);

(async () => { // Async block
// May prompt the user:
const status = await navigator.permissions.request({ name: "local-fonts" });
if (status.state !== "granted")
const status = await navigator.permissions.query({ name: "font-access" });
if (status.state === "denied")
throw new Error("Cannot continue to style with local fonts");

for await (const metadata of navigator.fonts.query()) {
const option = document.createElement("option");
option.text = metadata.fullName;
option.value = metadata.fullName;
option.setAttribute("postscriptName", metadata.postscriptName);
font_select.append(option);
try {
// May prompt the user:
for await (const metadata of navigator.fonts.query()) {
const option = document.createElement("option");
option.text = metadata.fullName;
option.value = metadata.postscriptName;
fontSelect.append(option);
}
} catch(e) {
// Handle error. It could be a permission error.
throw new Error(e);
}
})();
```
Expand All @@ -140,38 +149,38 @@ Here we use enumeration and new APIs on `FontMetadata` to access a full and vali

```js
(async () => { // Async block
// May prompt the user
const status = await navigator.permissions.request({ name: "local-fonts" });
if (status.state !== "granted")
const status = await navigator.permissions.query({ name: "font-access" });
if (status.state === "denied")
throw new Error("Cannot continue to style with local fonts");

for await (const metadata of navigator.fonts.query()) {
// Looking for a specific font:
if (metadata.postscriptName !== "Consolas")
continue;

// blob()' returns a Blob containing valid and complete SFNT
// wrapped font data.
const sfnt = await metadata.blob();

const sfntVersion = (new TextDecoder).decode(
// 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
await sfnt.slice(0, 4).arrayBuffer());

let outlineFormat = "UNKNOWN";
switch (sfntVersion) {
case '\x00\x01\x00\x00':
case 'true':
case 'typ1':
outlineFormat = "truetype";
break;
case 'OTTO':
outlineFormat = "cff";
break;
try {
// May prompt the user
for await (const metadata of navigator.fonts.query()) {
// 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}`);
}
console.log("Consolas outline format:", outlineFormat);
} catch(e) {
// Handle error. It could be a permission error.
throw new Error(e);
}
})();
```
Expand Down Expand Up @@ -274,5 +283,5 @@ We'd like to acknowledge the contributions of:
* Daniel Nishi, Owen Campbell-Moore, and Mike Tsao who helped pioneer the previous local font access proposal
* Evan Wallace, Biru, Leah Cassidy, Katie Gregorio, Morgan Kennedy, and Noah Levin of Figma who have patiently enumerated the needs of their ambitious web product.
* Tab Atkins and the CSS Working Group who have provided usable base-classes which only need slight extension to enable these cases
* Dominik Röttsches and Igor Kopylov for their thoughtful feedback
* Domenic Denicola, Dominik Röttsches, Igor Kopylov and Jake Archibald for their thoughtful feedback
* Lastly, we would like to express our gratitude to former editor Emil A. Eklund, who passed away in 2020. Emil was instrumental in getting this proposal underway, providing technical guidance, and championing the needs of users and developers
Loading