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

了解 Local Font Access API 如何让您访问用户本地安装的字体,并获取有关这些字体的底层详细信息

网络安全字体

如果您从事 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 代码块,该代码块用于指定要下载的字体文件:

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

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

Local Font Access 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 数据

完整的 SFNT 访问权限可通过 FontData 对象的 blob() 方法获得。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 的实现过程中是否发现了错误?或者实现方式是否与规范不同? 请访问 new.crbug.com 提交 bug。请务必提供尽可能详细的信息、简单的重现说明,并在 Components 框中输入 Blink>Storage>FontAccessGlitch 非常适用于分享轻松快速的重现问题。

显示对该 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 用户发出。