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

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

网络安全字体

如果您从事 Web 开发的时间足够长,可能还记得网络安全字体。众所周知,这些字体几乎可以在最常用的操作系统(即 Windows、macOS、最常见的 Linux 发行版、Android 和 iOS)的所有实例中使用。在 21 世纪初,Microsoft 甚至发起了一项名为 TrueType 网络核心字体计划。该计划提供免费下载这些字体,其目标是“每当您访问指定了这些字体的网站时,都能看到与网站设计人员完全一样的页面。是的,这包括在 Comic Sans MS 中设置的网站。以下是经典的 Web 安全字体堆栈(具有任何 sans-serif 字体的最终回退方式),可能如下所示:

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

网络字体

网络安全字体至关重要的时代已一去不复返。现在,我们有网页字体,其中一些甚至是可变字体,我们可以通过更改各个公开轴的值来进一步微调这些字体。您可以通过在 CSS 开头声明 @font-face 代码块来指定要下载的字体文件,使用网页字体:

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

之后,您便可以照常通过指定 font-family 来使用自定义网页字体:

body {
  font-family: 'FlamboyantSansSerif';
}

本地字体(作为指纹矢量)

大多数网络字体都来自网络。但有趣的事实是,除了 url() 函数之外,@font-face 声明中的 src 属性还接受 local() 函数。这允许在本地加载自定义字体(令人惊讶!)如果用户恰好在其操作系统上安装了 FlamboyantSansSerif,系统将使用本地副本,而不是下载:

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

这种方法提供了一种很好的回退机制,有可能节省带宽。遗憾的是,在互联网上 我们不能拥有美好的事物local() 函数的问题在于,它可以被滥用于浏览器数字“指纹”收集。事实证明,用户安装的字体列表可能非常具有辨识度。许多公司在员工的笔记本电脑上安装了自己的企业字体。例如,Google 有一种名为 Google Sans 的公司字体。

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

攻击者可通过测试是否存在大量已知的企业字体(如 Google Sans),试图确定某人为哪家公司工作。攻击者会尝试在画布上渲染以这些字体设置的文本,并测量字形。如果字形与公司字体的已知形状相匹配,攻击者就能够攻击。如果字形不匹配,攻击者就会知道,由于未安装公司字体,因此使用了默认替换字体。如需了解此攻击及其他浏览器数字“指纹”收集攻击的完整详情,请参阅 Laperdix 等人撰写的调查论文

除了公司字体之外,即使仅列出了已安装的字体,也能够识别出来。这种攻击媒介的情况变得非常糟糕,以至于 WebKit 团队最近决定“仅包含 [在列表中可用的字体中] 网页字体和字体,不包括操作系统自带的字体,而不是本地用户安装的字体”。(我现在是一篇介绍如何授予对本地字体的访问权限的文章。)

Local Font Access API

这篇文章的开头部分可能让您感到不快。难道就不能有好点子吗?别担心。我们认为我们可以,但或许一切并非没有希望。但我先回答一个您可能会问自己的问题。

为什么在有网页字体时还需要 Local Font Access API?

以往,很难在网络上提供专业品质的设计和图形工具。其中一个阻碍是,他们无法访问和使用设计人员在本地安装的各种专业构造的微调字体。网络字体支持某些发布用例,但无法以编程方式访问光栅化工具用于渲染字形轮廓的矢量字形形状和字体表。同样,您也无法访问网络字体的二进制数据。

  • 设计工具需要访问字体字节才能执行自己的 OpenType 布局实现,并允许设计工具在较低级别执行相关操作,例如对字形形状执行矢量滤镜或转换。
  • 开发者可能为其应用引入到 Web 中的旧版字体堆栈。如需使用这些堆栈,它们通常需要直接访问字体数据,而网页字体不提供某些访问权限。
  • 某些字体可能未获得通过网络分发的许可。例如,Linotype 拥有某些字体的许可,只允许桌面使用

Local Font Access API 是为解决这些挑战的尝试。它由两部分组成:

  • 字体枚举 API,可让用户授予对所有可用系统字体的访问权限。
  • 根据每个枚举结果,能够请求包含完整字体数据的低级别(面向字节)SFNT 容器访问

浏览器支持

浏览器支持

  • 103
  • 103
  • x
  • x

来源

如何使用 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 数据

完整的 SFNT 访问权限可通过 FontData 对象的 blob() 方法获得。SFNT 是一种字体文件格式,它可以包含其他字体,例如 PostScript、TrueType、OpenType、网络开放字体格式 (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。请务必提供尽可能多的详情和简单的重现说明,并在组件框中输入 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 用户发布。