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

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

网络安全字体

如果您从事 Web 开发已有相当长的时间,可能还记得所谓的Web 安全字体。这些字体已知可在最常用的操作系统(即 Windows、macOS、最常见的 Linux 发行版、Android 和 iOS)的几乎所有实例中使用。在 2000 年代初,Microsoft 甚至还发起了名为 TrueType core fonts for the Web计划,提供这些字体供免费下载,目标是“每当您访问指定这些字体的网站时,您看到的网页都会与网站设计师的预期完全一致”。是的,这包括使用 Comic Sans MS 设置的网站。下面是一个传统的网络安全字体堆栈(最终的后备字体是任何 sans-serif 字体),可能如下所示:

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

网页字体

网页安全字体的重要性早已不复存在。如今,我们有网络字体,其中一些甚至是可变字体,我们可以通过更改各种公开轴的值来进一步调整。您可以在 CSS 开头声明 @font-face 块,以指定要下载的字体文件,从而使用 Web 字体:

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

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

body {
  font-family: 'FlamboyantSansSerif';
}

将本地字体用作指纹矢量

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

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

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

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

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

除了公司字体之外,仅安装的字体列表也可能具有识别性。这种攻击向量的形势已经变得非常严重,因此 WebKit 团队最近决定“仅在可用字体列表中包含 Web 字体和操作系统附带的字体,但不包含用户在本地安装的字体”。(我现在就为您准备了一篇介绍如何授予对本地字体的访问权限的文章。)

Local Font Access API

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

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

过去,在 Web 上提供专业品质的设计和图形工具一直很难。其中一个障碍是无法访问和使用设计师在本地安装的各种专业构建和提示的字体。网页字体支持某些发布用例,但无法以编程方式访问光栅化程序用于渲染字形轮廓的矢量字形形状和字体表。同样,您也无法访问 Web 字体的二进制数据。

  • 设计工具需要访问字体字节才能实现自己的 OpenType 布局,并允许设计工具在更低级别钩入,以执行对字形形状执行矢量滤镜或转换等操作。
  • 开发者可能有要移植到 Web 的应用的旧版字体堆栈。如需使用这些堆栈,通常需要直接访问字体数据,而 Web 字体无法提供此类数据。
  • 某些字体可能未获许可,无法通过网络传送。例如,Linotype 拥有的某些字体的许可仅涵盖桌面使用

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

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

浏览器支持

浏览器支持

  • Chrome:103。
  • Edge:103.
  • Firefox:不受支持。
  • Safari:不受支持。

来源

如何使用 Local Font Access API

功能检测

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

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

枚举本地字体

如需获取本地安装的字体的列表,您需要调用 window.queryLocalFonts()。首次执行此操作时,系统会触发权限提示,用户可以批准或拒绝。如果用户批准查询其本地字体,浏览器将返回一个包含字体数据的数组,您可以对其进行循环处理。每个字体都表示为具有 family(例如 "Comic Sans MS")、fullName(例如 "Comic Sans MS")、postscriptName(例如 "ComicSansMS")和 style(例如 "Regular")属性的 FontData 对象。

// 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 团队使用控制对强大 Web 平台功能的访问权限中定义的核心原则(包括用户控制、透明度和人体工学)设计和实现了 Local Font Access API。

用户控制

用户对其字体的访问权限完全由其控制,除非授予 权限注册表中列出的 "local-fonts" 权限,否则系统将不允许访问。

透明度

网站是否已获准访问用户的本地字体,会显示在网站信息页面中。

权限持久性

"local-fonts" 权限将在网页重新加载期间保留。您可以通过网站信息表格撤消此权限。

反馈

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

请向我们说明 API 设计

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

报告实现方面的问题

您是否发现了 Chrome 实现中的 bug?或者实现方式是否与规范不同? 请访问 new.crbug.com 提交 bug。请务必提供尽可能详细的信息、简单的重现说明,并在 Components 框中输入 Blink>Storage>FontAccess故障非常适合分享快速简便的重现步骤。

显示对该 API 的支持

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

使用 #LocalFontAccess 标签向 @ChromiumDev 发推文,告诉我们您在哪里以及如何使用该工具。

致谢

本地字体访问权限 API 规范由 Emil A. EklundAlex RussellJoshua BellOlivier Yiptong。本文由 Joe MedleyDominik RöttschesOlivier Yiptong 审核。主打图片:Unsplash 用户 Brett Jordan 提供。