了解如何使用 Local Font Access API 访问用户本地安装的字体,并获取有关这些字体的低级别详细信息
发布时间:2020 年 8 月 24 日
网络安全字体
如果您从事 Web 开发的时间足够长,可能还记得所谓的
网络安全字体。
众所周知,这些字体几乎可在最常用的操作系统
(即 Windows、macOS、最常见的 Linux 发行版、Android 和 iOS)的所有实例中使用。在 21 世纪初,
Microsoft 甚至率先推出了一项
名为
“TrueType 核心网络字体”的计划,免费提供这些字体供下载,
目的是“无论您访问哪个网站,只要该网站指定了这些字体,您看到的页面都与
网站设计者的预期完全一致”。是的,这包括使用
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来使用自定义网络字体:
body {
font-family: 'FlamboyantSansSerif';
}
本地字体作为指纹向量
大多数网络字体都来自网络。不过,一个有趣的事实是,
src 声明中的 @font-face
属性,除了
url()
函数之外,还接受
local()
函数。这样一来,自定义字体就可以在本地加载(惊喜!)。如果用户恰好在其操作系统上安装了
FlamboyantSansSerif,则会使用本地副本,而不是
下载副本:
@font-face {
font-family: 'FlamboyantSansSerif';
src: local('FlamboyantSansSerif'), url('flamboyant.woff2');
}
这种方法提供了一种不错的后备机制,可以节省带宽。遗憾的是,在互联网上,
我们无法拥有美好的事物。local() 函数的问题在于,它可能会被
滥用于浏览器指纹识别。事实证明,用户安装的字体列表可以相当具有
识别性。许多公司都有自己的公司字体,这些字体安装在员工的
笔记本电脑上。例如,Google 有一种名为 Google Sans 的公司字体。
攻击者可以尝试通过测试是否存在大量已知的公司字体(如 Google Sans)来确定某人所工作的公司。攻击者会尝试在画布上呈现以这些字体 设置的文本,并测量字形。如果字形与 公司字体的已知形状匹配,则攻击者会成功。如果字形不匹配,则攻击者知道使用了 默认替换字体,因为公司字体未安装。如需详细了解 此攻击和其他浏览器指纹识别攻击,请参阅 Laperdix 等人的 调查论文。
除了公司字体之外,即使只是安装的字体列表也具有识别性。这种攻击媒介的情况变得非常糟糕,以至于 WebKit 团队最近 决定 “仅在 [可用字体列表] 中包含网络字体和操作系统附带的字体,但不包含本地用户安装的字体”。(而我在这里写了一篇关于授予本地字体访问权限的文章。)
Local Font Access API
本文的开头可能会让您心情不好。我们真的不能拥有美好的 事物吗?别担心。我们认为可以,也许 一切并非毫无希望。 但首先,让我回答您可能在问自己的一个问题。
既然有网络字体,为什么还需要 Local Font Access API?
从历史上看,专业品质的设计和图形工具很难在 网络上提供。一个障碍是无法访问和使用设计师在本地安装的各种专业 构建和提示字体。网络字体支持一些发布 用例,但无法实现对光栅化程序用于呈现字形轮廓的矢量字形形状和字体表的程序化访问。同样,也无法访问网络字体的二进制 数据。
- 设计工具需要访问字体字节才能执行自己的 OpenType 布局实现,并允许 设计工具在较低级别挂钩,以执行对字形形状执行矢量过滤或 转换等操作。
- 开发者可能为其应用提供了旧版字体堆栈,他们正在将这些堆栈引入网络。 如需使用这些堆栈,他们通常需要直接访问字体数据,而网络字体不 提供此功能。
- 某些字体可能未获得通过网络交付的许可。例如,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 团队在设计和实现 Local Font Access 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. 编辑Eklund、 Alex Russell、 Joshua Bell 和 Olivier Yiptong。本文由 Joe Medley、 Dominik Röttsches和 Olivier Yiptong审核。