结合使用高级排版与本地字体

了解如何使用本地字体访问 API 访问用户本地安装的字体并获取有关这些字体的低级详细信息

网络安全字体

如果您从事 Web 开发的时间足够长,可能还记得所谓的网页安全字体。这些字体已知可在几乎所有最常用的操作系统(即 Windows、macOS、最常见的 Linux 发行版、Android 和 iOS)上使用。在 21 世纪初,Microsoft 甚至率先推出了一项名为 TrueType core fonts for the Web计划,免费提供这些字体供下载,目的是“无论何时,只要您访问指定了这些字体的网站,您看到的网页都将与网站设计者的预期完全一致”。是的,这包括以 Comic Sans MS 设置的网站。以下是一个经典的网络安全字体堆栈(最终回退到任何 sans-serif 字体)示例:

body {
  font-family: Helvetica, Arial, sans-serif;
}

网络字体

网络安全字体真正重要的时代早已过去。如今,我们有了网络字体,其中一些甚至是可变字体,我们可以通过更改各种公开轴的值来进一步调整这些字体。您可以在 CSS 开头声明一个 @font-face 块来使用网络字体,该块用于指定要下载的字体文件:

@font-face {
  font-family: 'FlamboyantSansSerif';
  src: url('flamboyant.woff2');
}

之后,您就可以像往常一样通过指定 font-family 来使用自定义 Web 字体了:

body {
  font-family: 'FlamboyantSansSerif';
}

将本地字体作为指纹向量

大多数 Web 字体都来自网络。不过,一个有趣的事实是,@font-face 声明中的 src 属性除了 url() 函数之外,还接受 local() 函数。这样就可以在本地加载自定义字体了(惊喜!)。如果用户恰好在其操作系统上安装了 FlamboyantSansSerif,则系统会使用本地副本,而不是下载该字体:

@font-face {
  font-family: 'FlamboyantSansSerif';
  src: local('FlamboyantSansSerif'), url('flamboyant.woff2');
}

这种方法提供了一种不错的回退机制,可节省带宽。很遗憾,在互联网上,我们无法拥有美好的事物。local() 函数的问题在于,它可能会被滥用于浏览器指纹识别。事实证明,用户已安装的字体列表具有相当的识别性。许多公司都有自己的公司字体,这些字体安装在员工的笔记本电脑上。例如,Google 有一种名为 Google Sans 的公司专用字体。

macOS“字体册”应用,其中显示了 Google Sans 字体的预览。
安装在 Google 员工笔记本电脑上的 Google Sans 字体。

攻击者可以尝试通过测试大量已知的公司字体(例如 Google Sans)是否存在来确定某人所在的公司。攻击者会尝试在画布上呈现以这些字体设置的文本,并测量字形。如果字形与公司字体的已知形状相符,则攻击者命中。如果字形不匹配,攻击者就会知道,由于公司字体未安装,系统使用了默认替换字体。如需详细了解此攻击和其他浏览器指纹识别攻击,请参阅 Laperdix 等人调查论文

除了公司字体之外,即使只是已安装字体的列表也可能具有识别性。这种攻击媒介的情况已经变得非常糟糕,以至于最近 WebKit 团队决定“仅包含 [在可用字体列表中] Web 字体和操作系统自带的字体,而不包含本地用户安装的字体”。(而我正在撰写一篇关于授予本地字体访问权限的文章。)

Local Font Access API

本文的开头可能让您感到沮丧。难道我们真的不能拥有美好的事物吗?别担心。我们认为可以,也许一切并非毫无希望。 不过,首先让我回答一个您可能正在问自己的问题。

既然有 Web 字体,为什么还需要 Local Font Access API?

从历史上来看,在 Web 上提供专业级设计和图形工具一直是一项艰巨的任务。一个障碍是无法访问和使用设计师在本地安装的各种专业构建和提示字体。网络字体可实现一些发布用例,但无法实现对光栅化程序用于渲染字形轮廓的矢量字形形状和字体表的程序化访问。同样,也无法访问网络字体的二进制数据。

  • 设计工具需要访问字体字节,以便实现自己的 OpenType 布局,并允许设计工具在较低级别进行钩连,以执行诸如对字形形状应用矢量滤镜或转换等操作。
  • 开发者可能为其要迁移到 Web 的应用设置了旧版字体堆栈。 若要使用这些堆栈,通常需要直接访问字体数据,而 Web 字体不提供此功能。
  • 某些字体可能未获得通过网络交付的许可。例如,Linotype 拥有某些字体的许可,但这些许可仅包含桌面使用权

本地字体访问 API 旨在解决这些难题。它由两部分组成:

  • 字体枚举 API,允许用户授予对整套可用系统字体的访问权限。
  • 从每个枚举结果中,能够请求低级(面向字节)SFNT 容器访问权限,其中包括完整的字体数据。

浏览器支持

Browser Support

  • Chrome: 103.
  • Edge: 103.
  • Firefox: not supported.
  • Safari: not supported.

Source

如何使用 Local Font Access API

功能检测

如需检查是否支持 Local Font Access API,请使用:

if ('queryLocalFonts' in window) {
  // The Local Font Access API is supported
}

枚举本地字体

如需获取本地安装的字体列表,您需要调用 window.queryLocalFonts()。首次调用时,系统会触发权限提示,用户可以批准或拒绝该权限。如果用户批准查询其本地字体,浏览器将返回一个包含字体数据的数组,您可以循环访问该数组。每种字体都表示为一个 FontData 对象,该对象具有 family(例如 "Comic Sans MS")、fullName(例如 "Comic Sans MS")、postscriptName(例如 "ComicSansMS")和 style(例如 "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);
}

如果您只对部分字体感兴趣,还可以通过添加 postscriptNames 参数,根据 PostScript 名称过滤字体。

const availableFonts = await window.queryLocalFonts({
  postscriptNames: ['Verdana', 'Verdana-Bold', 'Verdana-Italic'],
});

访问 SFNT 数据

通过 FontData 对象的 blob() 方法可获得完整的 SFNT 访问权限。SFNT 是一种字体文件格式,可以包含其他字体,例如 PostScript、TrueType、OpenType、Web Open Font Format (WOFF) 字体等。

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);
}

演示

您可以在演示中查看 Local Font Access API 的实际应用。请务必同时查看源代码。此演示展示了一个名为 <font-select> 的自定义元素,该元素实现了本地字体选择器。

隐私注意事项

"local-fonts" 权限似乎提供了一个高度可指纹识别的表面。不过,浏览器可以自由返回任何内容。例如,注重匿名性的浏览器可能会选择仅提供浏览器内置的一组默认字体。同样,浏览器也不需要提供与磁盘上完全相同的表格数据。

Local Font Access API 旨在尽可能仅公开实现上述用例所需的确切信息。系统 API 可能会生成已安装字体的列表,但该列表并非按随机顺序或排序顺序排列,而是按字体安装顺序排列。准确返回此类系统 API 给出的已安装字体列表可能会暴露可用于指纹识别的其他数据,并且保留此顺序并不能帮助实现我们想要启用的用例。因此,此 API 要求返回的数据在返回之前进行排序。

安全与权限

Chrome 团队在设计和实现本地字体访问 API 时,遵循了控制对强大的 Web 平台功能的访问权限中定义的核心原则,包括用户控制、透明度和人机工程学。

用户控制

用户对自己的字体拥有完全控制权,除非授予 权限注册表中列出的 "local-fonts" 权限,否则不允许访问用户的字体。

透明度

网站是否已获得用户本地字体的访问权限将显示在网站信息表单中。

权限持久性

"local-fonts" 权限将在页面重新加载之间保持不变。您可以通过网站信息工作表撤消此权限。

反馈

Chrome 团队希望了解您在使用 Local Font Access API 方面的体验。

介绍 API 设计

API 是否存在未按预期运行的情况?或者,是否有缺少的方法或属性需要您来实现自己的想法?对安全模型有疑问或意见?在相应的 GitHub 代码库中提交规范问题,或在现有问题中添加您的想法。

报告实现方面的问题

您是否发现 Chrome 的实现存在 bug?或者实现是否与规范不同? 请前往 new.crbug.com 提交 bug。请务必尽可能详细地说明问题,提供简单的重现说明,并在组件框中输入 Blink>Storage>FontAccess

显示对 API 的支持

您是否打算使用 Local Font Access API?您的公开支持有助于 Chrome 团队确定功能优先级,并向其他浏览器供应商展示支持这些功能的重要性。

使用 #LocalFontAccess 主题标签向 @ChromiumDev 发送一条推文,告诉我们您在何处以及如何使用它。

致谢

Local Font Access API 规范已由 Emil A. EklundAlex RussellJoshua BellOlivier Yiptong。本文由 Joe MedleyDominik RöttschesOlivier Yiptong 审核。主打图片由 Brett JordanUnsplash 上发布。