了解 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
块,以指定要下载的字体文件,从而使用 Web 字体:
@font-face {
font-family: 'FlamboyantSansSerif';
src: url('flamboyant.woff2');
}
之后,您可以像往常一样指定 font-family
来使用自定义 Web 字体:
body {
font-family: 'FlamboyantSansSerif';
}
将本地字体用作指纹矢量
大多数网络字体都来自网络。但有趣的是,@font-face
声明中的 src
属性除了 url()
函数外,还接受 local()
函数。这样一来,您就可以在本地加载自定义字体了(真是太棒了!)。如果用户恰好在其操作系统中安装了 FlamboyantSansSerif,将使用本地副本,而不会下载该副本:
@font-face {
font-family: 'FlamboyantSansSerif';
src: local('FlamboyantSansSerif'), url('flamboyant.woff2');
}
这种方法提供了一种不错的后备机制,可能会节省带宽。很遗憾,在互联网上,我们无法拥有美好的事物。local()
函数的问题在于,它可能会被滥用于浏览器指纹识别。事实证明,用户安装的字体列表可以提供非常有用的身份信息。许多公司都有自己的企业字体,这些字体安装在员工的笔记本电脑上。例如,Google 有一个名为 Google Sans 的公司字体。
攻击者可以通过测试是否存在大量已知的企业字体(例如 Google Sans)来尝试确定某人所在的公司。攻击者会尝试在画布上渲染使用这些字体设置的文本,并测量字形。如果字形与公司字体的已知形状匹配,则攻击者会获得命中。如果字形不匹配,攻击者就会知道由于未安装公司字体,因此系统使用了默认替换字体。如需详细了解此攻击和其他浏览器指纹攻击,请参阅 Laperdix 等撰写的调查论文。
除了公司字体之外,仅安装的字体列表也可能具有识别性。这种攻击向量的形势已经变得非常严重,最近 WebKit 团队决定“仅在可用字体列表中包含 Web 字体和操作系统附带的字体,但不包含用户在本地安装的字体”。(我现在就为您准备了一篇介绍如何授予本地字体访问权限的文章。)
Local Font Access API
本文开头的内容可能让您感到沮丧。我们真的不能拥有美好的事物吗?别担心。我们认为可以,也许一切并非毫无希望。不过,我先回答一个您可能在问自己的问题。
既然有 Web 字体,为什么还需要 Local Font Access API?
过去,在 Web 上提供专业品质的设计和图形工具一直很难。一个障碍是,无法访问和使用设计师在本地安装的各种专业构建和提示的字体。网页字体支持某些发布用例,但无法以编程方式访问光栅化程序用于渲染字形轮廓的矢量字形形状和字体表。同样,您也无法访问 Web 字体的二进制数据。
- 设计工具需要访问字体字节才能实现自己的 OpenType 布局,并允许设计工具在更低级别钩入,以执行对字形形状执行矢量滤镜或转换等操作。
- 开发者可能有要移植到 Web 的应用的旧版字体堆栈。如需使用这些堆栈,通常需要直接访问字体数据,而 Web 字体无法提供此类数据。
- 某些字体可能未获许可,无法通过网络传送。例如,Linotype 拥有的某些字体的许可仅涵盖桌面使用。
Local Font Access API 就是为了解决这些问题而推出的。它由两部分组成:
- 字体枚举 API,可让用户授予对所有可用系统字体的访问权限。
- 从每个枚举结果中,能够请求包含完整字体数据的低级(以字节为导向)SFNT 容器访问权限。
浏览器支持
如何使用 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 团队使用控制对强大 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
。Glitch 非常适用于分享轻松快速的重现问题。
表示对 API 的支持
您打算使用 Local Font Access API 吗?您的公开支持可帮助 Chrome 团队确定功能的优先级,并向其他浏览器供应商展示支持这些功能的重要性。
使用 # 标签 #LocalFontAccess
向 @ChromiumDev 发送一条推文,告诉我们您在何处以及如何使用它。
实用链接
致谢
Local Font Access API 规范由 Emil A. Eklund、Alex Russell、Joshua Bell 和 Olivier Yiptong。本文由 Joe Medley、Dominik Röttsches 和 Olivier Yiptong 审核。主打图片,由 Brett Jordan 在 Unsplash 用户发出。