CJK 字体渲染如何在浏览器指纹中暴露真实系统
中文、日文和韩文的字体家族、回退链和文本度量会暴露宿主操作系统。了解差异所在,以及一致的跨平台输出需要什么。
BotBrowser Team
BotBrowser 可以为授权的 CJK 测试同步配置文件字体包和枚举,但不能保证网站接受该身份,也不能控制页面 CSS、字体加载或服务器行为。
TL;DR
TL;DR: CJK 字体的可用性、回退和文本度量都可能暴露宿主系统。BotBrowser 可以在授权测试中让配置文件字体包与枚举保持一致,但不能保证网站接受该身份,也不能控制页面 CSS 或服务器行为。
比较不同宿主上的 CJK 渲染,可以识别不在 BotBrowser 控制范围内的限制。
内容导航
为什么 CJK 文本会暴露宿主平台
中文、日文和韩文文本是页面判断浏览器真实运行在哪个操作系统上的最快途径之一。每个平台都自带一套 CJK 字体家族,通过自己的回退链处理缺失的字体,并用自己的文本渲染栈绘制字形。一个在拉丁文本上看起来一致的配置文件,只要页面测量或绘制东亚字符,仍然可能显露宿主平台。
原因很简单。Arial 或 Helvetica 这类拉丁字体存在于多个系统上,所以包含它们的字体列表说明不了多少问题。CJK 字体体积大、与厂商绑定,并且按系统的语言包安装。列表里有微软雅黑,指向 Windows。列表里有苹方-简,指向 macOS。列表里有 Noto Sans CJK 和文泉驿,则指向典型的 Linux 桌面或服务器。
以下各节讨论 CJK 渲染会暴露什么,为什么常见的宿主侧修补会留下可测量的差距,以及一致的跨平台输出需要什么。它们还说明如何为 CJK 工作流选择 --bot-fonts 模式,并用简短的 Canvas 宽度对比检查结果。关于字体信号的整体情况,请参阅字体指纹与跨平台文本一致性。
各平台的 CJK 字体家族
下面的列表是具有代表性的示例,并非完整清单。实际安装情况会随操作系统版本、已安装的语言包以及用户或管理员额外添加的字体而变化。请用它们来理解哪些字体家族通常属于哪个平台,而不要当作每台机器都必须满足的检查表。
Windows 通常提供以下 CJK 字体家族。
- 简体中文:微软雅黑、宋体、黑体、仿宋、楷体、新宋体
- 繁体中文:微软正黑体、细明体、新细明体
- 日文:Yu Gothic、Meiryo、MS Gothic、MS Mincho、MS PGothic
- 韩文:Malgun Gothic、Batang、Dotum、Gulim、Gungsuh
macOS 通常提供以下 CJK 字体家族。
- 简体中文:苹方-简、华文黑体、华文宋体、华文楷体、华文仿宋
- 繁体中文:苹方-繁、苹方-港、丽宋 Pro
- 日文:Hiragino Sans、Hiragino Kaku Gothic、Hiragino Mincho
- 韩文:Apple SD Gothic Neo、AppleMyungjo、Nanum Gothic
Linux 发行版通常提供以下 CJK 字体家族,具体取决于安装了哪些软件包。
- 三种语言通用:Noto Sans CJK(SC、TC、JP 和 KR 变体)以及 Noto Serif CJK
- 中文:文泉驿微米黑、文泉驿正黑
- 日文:IPAGothic、IPAMincho、Takao Gothic
- 韩文:UnBatang、UnDotum
由此得到一组读者一眼就能识别的不一致情形。声称是 Windows 并使用中文区域设置的配置文件,如果没有报告微软雅黑或宋体,就是不一致的,因为中文版 Windows 默认就带有它们。面向日本的 macOS 配置文件缺少 Hiragino Sans,同样不一致。一台 Linux 机器同时报告 Windows 字体和苹果字体,则与任何真实安装都不相符,因为这些字体家族从来不会来自同一个厂商。
中文内部也存在地区差异。简体中文是中国大陆的通行写法,繁体中文则用于台湾和香港,每个平台都为它们提供了各自的字体家族,例如 macOS 上的苹方-简与苹方-繁、苹方-港,或 Windows 上的微软雅黑与微软正黑体。因此,声称台湾区域设置的配置文件应当带有繁体中文字体家族,而不只是简体中文字体家族。
语言与平台同样重要。为英文配置的 Windows 机器可能只报告很少的 CJK 字体,而为中文配置的机器会报告很多。因此,CJK 区域设置、CJK 字体列表和所声称的操作系统必须一致。三者中只要有一项缺失或指向别处,即使每个值单独看都合理,组合起来也会显得异常。
回退链与文本度量
当页面请求的字体家族没有安装时,浏览器不会停下来。它会沿着回退链查找,也就是按顺序尝试若干字体家族,直到有一个覆盖文本中的字符。CSS Fonts 规范描述了通用字体家族和逐字符回退的工作方式,而每个平台用自己的默认值来填充这些机制。结果是,同一段 CSS 在不同系统上可能解析为不同的字体。
典型的中文回退链如下所示。
- 在 Windows 上,中文文本通常解析为微软雅黑,其次是宋体。
- 在 macOS 上,通常解析为苹方-简,其次是华文黑体。
- 在 Linux 上,通常解析为 Noto Sans CJK 或文泉驿家族,取决于安装了什么。
页面作者通常会写一个字体栈,例如先写 macOS 字体家族,再写 Windows 字体家族,最后写通用字体家族。浏览器对每个字符从左到右评估这个字体栈,并使用第一个已安装且覆盖该字符的字体家族。因此,页面自己的 CSS 顺序决定了哪个已安装的字体家族会显现出来,为多个平台编写的字体栈在每个平台上自然会得到不同的结果。
假设页面请求 font-family: "Nonexistent Font", sans-serif 并显示中文文本。页面中没有任何内容指明平台,但最终的字形在每个系统上来自不同的字体家族,可见的形状和间距也随之变化。页面不需要已安装字体的列表就能察觉这种差异,只需要渲染文本并观察结果。
文本的语言带来另一层影响。中文、日文和韩文共享许多汉字,但各语言偏好的字形不同。浏览器会根据内容声明的语言和系统的语言设置,在不同地区的字形变体之间做选择。区域设置声称是日文,而回退结果却是中文字形,这就是又一处需要避免的不一致。
除了可用性之外,CJK 字体还会产生可测量的差异。
- 字形宽度:单个字符,尤其是拉丁字母与 CJK 混排的字符串,在不同字体家族中的步进宽度不同。
- 行高:行与行之间的默认间距取决于所选字体家族的垂直度量。
- 间距规则:标点、字距和字符间距在中文、日文和韩文排版中遵循不同的惯例。
- 字体微调与光栅化:小字号文本的像素级外观取决于字体的微调数据,以及 Windows 上的 DirectWrite、macOS 上的 Core Text 或 Linux 上的 FreeType。
这些值会进入布局计算。getBoundingClientRect() 和 getComputedStyle() 之类的调用返回的数字取决于实际使用的字体,而 Canvas 的 measureText 返回当前字体下字符串的宽度。MDN 将 measureText 记录为获取文本度量的方法,这也是它适合做简单检查的原因:如果指定字体家族后字符串宽度发生变化,说明找到了该字体家族;如果没有变化,说明浏览器进行了回退。
为什么宿主侧修补会留下可测量的差距
当运营者想要与宿主不同的 CJK 输出时,常见的做法有三种。每一种都解决了问题的一部分,同时留下另一部分。理解这个差距,就能明白一致的输出为什么取决于让字体列表和渲染结果一起对齐。
第一种做法是在宿主上安装目标平台的 CJK 字体。在 Linux 服务器上,这可能意味着添加微软核心字体、CJK 软件包或单独的字体文件。它对某些页面有效,但也有代价。
- 它改变的是操作系统,所以机器上的每个浏览器实例都会看到新字体,而不仅是需要它们的那个实例。
- 同时安装 Windows 和 macOS 的字体家族,会得到一个与任何真实平台都不匹配的列表。
- 渲染仍然经过宿主的文本栈,例如 Linux 上的 FreeType,而不是被代表平台的文本栈。
- 在多台服务器上保持已安装字体一致,会增加运维工作和授权方面的问题。
第二种做法是 Web 字体注入。页面或脚本通过 CSS 添加字体,使文本以选定的字体家族绘制。这能控制部分文本的外观,但不会改变系统报告的内容。检测字体的常见方式是在指定与不指定该字体家族的情况下测量文本并比较宽度。注入的 Web 字体可以被渲染,而系统字体列表仍然显示宿主的真实字体,于是两种视角互相矛盾。
第三种做法是使用拦截字体相关 API 调用并修改所报告列表的扩展。列表可以被编辑,但像素不能。如果列表声称有微软雅黑,而文本实际上由宿主的 FreeType 字体绘制,Canvas 测量得到的宽度就不属于微软雅黑。报告的值与观察到的值不一致,这种不一致是可测量的。
这三种做法有一个共同的弱点:它们只改变一个层面,要么是安装,要么是页面样式,要么是报告的列表,而其他层面保持原样。一致的 CJK 输出需要枚举、回退行为和渲染一起变化,因为页面可以比较其中任意两项。
为 CJK 工作选择字体模式
BotBrowser 通过 --bot-fonts 提供字体处理,共有三种模式。配置文件携带目标平台的字体包,模式决定该字体包与宿主字体之间的关系。BotBrowser 字体文档将配置文件字体包描述为目标平台的完整标准字体集,并说明枚举结果与该平台会报告的内容一致。
--bot-fonts=profile使用配置文件内嵌的字体包,并且是默认值。当配置文件的字体清单和回退行为应当保持权威时使用它。--bot-fonts=expand使用配置文件的字体,并允许工作流对字体包未覆盖的字符回退到宿主字体。只有在工作流有意允许这种回退时才使用它。--bot-fonts=real使用宿主的真实字体。用于对比或测试,并且由于不应用配置文件字体包,输出会依赖宿主。
对大多数 CJK 工作流来说,profile 是合适的起点。它让字体可用性和回退与配置文件的平台绑定,因此即使宿主是没有这些字体的 Linux 服务器,使用中文区域设置的 Windows 配置文件也会报告微软雅黑和宋体。这同时消除了为配置文件而在宿主上安装 CJK 字体的需要。
字体模式只是连贯的地区设置的一部分。请将 CJK 配置文件与同一地区的区域设置、语言列表和时区搭配使用,详见时区、区域设置和语言一致性。这些参数列在 CLI 参数参考中,并标明了每个参数所属的方案层级。下面是三个示例。
chrome --bot-profile="profiles/win11-zh-cn.enc" \
--bot-fonts=profile \
--bot-locale=zh-CN \
--bot-languages=zh-CN,zh,en \
--bot-timezone=Asia/Shanghai
chrome --bot-profile="profiles/win11-ja.enc" \
--bot-fonts=profile \
--bot-locale=ja-JP \
--bot-languages=ja,en \
--bot-timezone=Asia/Tokyo
chrome --bot-profile="profiles/win11-ko.enc" \
--bot-fonts=profile \
--bot-locale=ko-KR \
--bot-languages=ko,en \
--bot-timezone=Asia/Seoul
expand 模式需要谨慎决定。当页面包含配置文件字体包未覆盖的生僻汉字、符号或文字时,它很有用,因为宿主可以提供字形,而不是留下方框。代价是这些字符现在来自宿主的字体,所以它们的输出取决于机器。只有当完整覆盖比与配置文件平台严格一致更重要时才选择 expand,而以一致性为目标的工作流应保留 profile。
配置文件本身也很重要。从已使用目标区域设置的安装中捕获的配置文件,带有该配置所附带的 CJK 字体家族,因此应优先选择从匹配区域设置的安装中捕获的配置文件。从仅有英文的机器上捕获的配置文件不会包含完整的中文或日文字体集,而且没有任何参数能添加配置文件没有的字体包。当工作流同时需要日文和中文字体时,请使用从同时装有两种语言包的系统上捕获的配置文件。关于在不同操作系统上运行同一配置文件的一般情况,请参阅跨平台浏览器配置文件。
检查结果并了解限制
简单的宽度对比可以确认基本的字体可用性。用你期望的字体测量一个混合了拉丁字母和 CJK 字符的字符串,然后用 monospace 这样的回退字体测量同一个字符串。如果两个宽度不同,说明找到了指定的字体家族;如果相同,说明浏览器进行了回退。之所以使用混合字符串,是因为许多 CJK 字体家族给每个表意字符相同的步进,所以仅由表意字符组成的字符串即使在不同字体下也可能得到相同的宽度。下面的代码片段可以粘贴到测试页面的开发者控制台中。
const ctx = document.createElement('canvas').getContext('2d');
const sample = 'Hello 你好 mmmlli';
ctx.font = '16px monospace';
const baseline = ctx.measureText(sample).width;
ctx.font = '16px "Microsoft YaHei", monospace';
const candidate = ctx.measureText(sample).width;
console.log({ baseline, candidate, found: candidate !== baseline });
对属于配置文件目标平台的几个字体家族,以及不属于该平台的几个字体家族,运行同样的检查。Windows 中文配置文件应当找到微软雅黑和宋体,而不应找到苹方-简。macOS 日文配置文件应当找到 Hiragino Sans,而不应找到 Meiryo。使用同一个配置文件,在至少两种宿主操作系统上重复检查。如果答案随宿主而变,说明宿主的字体泄漏到了结果中,首先要确认已设置 --bot-fonts=profile。
请正确看待结果的含义。宽度对比显示的是可用性,并不能证明每个字形、每个行高和每个光栅化细节都与源平台的真实机器一致。把它当作第一步检查,并用你关心的语言,对一个 CJK 内容密集的页面做目视检查作为补充。一套简短的检查流程会有帮助。
- 确认配置文件是从你想要代表的区域设置和平台上捕获的。
- 确认区域设置、语言列表和时区与配置文件指向同一地区。
- 对属于目标平台的字体家族和不属于目标平台的字体家族分别运行宽度对比。
- 打开一个包含大量中文、日文或韩文的页面,查找缺失的字形或方框。
- 在第二个宿主上记录宽度差异,并注明是字体列表变化还是渲染栈变化。
出现意外结果通常只有几个原因。字符显示为方框,说明配置文件没有包含该语言的 CJK 字体包,请选择包含它的配置文件版本。两台宿主之间文本宽度不同,说明 BotBrowser 版本或配置文件不同,因为版本之间的字体更新可能改变度量。CJK 与拉丁混排时间距不一致,通常指向回退差异,请确认配置文件的字体配置覆盖了两种文字。渲染结果不同的生僻字符可能落到了宿主字体上。你可以报告这些字符,以便将来的字体包更新收录它们;如果工作流有意允许宿主回退,也可以使用 expand。
BotBrowser 支持 --bot-fonts=profile,它使用配置文件内嵌的字体包,因此所报告的 CJK 字体可用性、枚举和回退跟随配置文件的目标平台,而不是宿主上安装的字体。对于 CJK 工作流,你可以借此检查中文、日文或韩文区域设置的配置文件在不同宿主操作系统上是否保持一致。BotBrowser 不能保证站点自身的检查会接受该身份,不能替代从匹配区域设置的安装中捕获的配置文件,也不能控制页面自己的字体、CSS 回退选择或平台的服务端行为。
实践结论
为目标 CJK 区域设置使用相应捕获的配置文件和 --bot-fonts=profile,再在多个宿主上验证字体可用性与中西文混排宽度。应把结果视为一致性证据,而不是每个字形或光栅化细节都与实体机器相同的证明。
公开来源
- https://botbrowser.io/docs/fingerprint/font/ 介绍了字体模式、内嵌字体包和跨平台字体行为。
- https://www.w3.org/TR/css-fonts-4/ 是 CSS Fonts Module Level 4 规范,涵盖字体匹配和回退。
- https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/measureText 记录了用于宽度对比的 Canvas 文本测量 API。
本文属于指南: Web 平台能力面:一致性复核地图