从活字到字体源码(11):不会字体设计,也能看懂字体改动
看到一张新连字截图时,先确定它证明了什么。手画的概念图可以说明设计意图,从字体提取的轮廓可以检查字形,而真实编辑器截图还能暴露应用加载和排版的差异。
开发者不必一开始就学会设计整套字体,但可以先检查字符、字形输出、总宽度与上下文,再把不能由数据决定的视觉问题留给实际阅读判断。下面用 Ligconsolata Next 的开发字体举例。
先确认样例与实际字体
!== 的输入应保留三个 ASCII 字符。用 ≠ 替代 != 的图,展示的是另一个字符,无法说明编程连字是否实现。字体文件也要明确来源,避免截图使用系统里另一个同名版本。
项目的浏览器 demo 由 scripts/build-demo-assets.py 准备资源,入口为 documentation/demo/index.html。它允许修改文本并切换 feature。静态 specimen 则应记录对应字体与样例,不能在源文件变化后继续把旧图当作当前构建结果。
检查真正命中的 glyph
在已安装 HarfBuzz 命令行工具、并生成 demo 字体的环境里,从字体仓库根目录运行:
hb-shape "documentation/demo/fonts/LigconsolataNext[wdth,wght].ttf" "a != b a !== b a == b a === b x <==== y i--" --features="liga=1,calt=1,dlig=0"
命令写成一行,便于在常见 shell 中使用。显式关闭 dlig,是为了验证核心效果确实来自默认的 liga / calt 路径。再改成 liga=1,calt=0,dlig=0,可以区分固定替换与上下文行为。
当前采样字体中,a == b 命中 equal_equal.dlig,a === b 命中 equal_equal_equal.dlig;i-- 的两个减号仍命中 hyphen_hyphen.dlig。glyph 的 .dlig 后缀属于内部名称,并不意味着该 glyph 只能通过 dlig 调用。
本系列从当时源码临时构建字体,再用 HarfBuzz 的 Python 绑定采样,保留了修复前的字形采样弹窗。记录包含字体哈希、feature 设置和输出 glyph 的 cluster、advance,重新构建后应重新采样。它与读者本机已有的 demo 字体可能属于不同构建。
这次采样发现,孤立 === 在 calt=1 时会被等号长线规则接管,输出 equal_start.seq、equal_middle.seq、equal_end.seq,视觉上只剩两条连续长线。旧规则看到三个等号就启动替换,已有的避让条件又依赖后续字符,因此漏掉了 shaping run 结束的位置。a === b 能命中三横线固定字形,并不能证明独立输入也正确。
修复时把启动条件缩小为:三个等号后还必须有 =、<、>、|、/、: 或 !,才进入长线序列。这些字符用于延长横线、形成端点或加入中间标记。孤立 === 留给固定连字,===> 和更长箭头仍走原来的片段拼接。
两侧都开启 liga / calt,关闭 dlig。修复后,孤立 === 命中 equal_equal_equal.dlig,总 advance 仍为 1500;==、!==、图中长箭头和 i-- 保持原有输出。修复前后采样弹窗保存了两份字体的哈希与各行结果。关闭 calt 可以帮助定位问题,但会失去长箭头等上下文效果;缩小启动条件才同时保留了这些功能。
用实际实例核对推进宽度
下面的 Python 示例在仓库根目录执行,前提是已安装 fontTools,且 demo 字体存在。它读取默认轴位置的宽度,不代替全部可变位置检查:
from pathlib import Path
from fontTools.ttLib import TTFont
path = Path("documentation/demo/fonts/LigconsolataNext[wdth,wght].ttf")
font = TTFont(path)
cmap = font.getBestCmap()
hmtx = font["hmtx"].metrics
raw = sum(hmtx[cmap[ord(ch)]][0] for ch in "==")
shaped = hmtx["equal_equal.dlig"][0]
print(raw, shaped)
assert raw == shaped
当前默认位置两个值均为 1000。检查其他轴位置时,先通过 fontTools 实例化,再读取该实例的指标;不能仅在默认 hmtx 中查一次,就声称整个可变空间宽度正确。
零宽 spacer 加多格结果 glyph 的实现,还需要核对整个输出序列的总 advance。每个 glyph 逐一等于一格并非必要条件,原始输入与最终输出的总宽度一致才是这里的要求。
反例与成功样例共同检查
连字规则会与其他规则共享字符,新增成功样例之外,要检查原有组合与不应触发的输入:
a-b
A:B
0xFF
0xG
a---->b
a====>b
i--
https://example.com
// comment
//TODO
/* block */
**/*.{ts,tsx}
/\d/
\/tmp
!== === == !=
\/tmp 与逻辑连字 \/ 共享字符,检查的是没有空白条件时能否避开合并。对 Markdown hash badge,要分别输入行首的 ## title、##title、a ## b 与长 hash run,不能把它们拼到同一个 shaping run 后再要求每一段都有行首行为。
现有上下文回归测试覆盖注释、glob、逻辑 slash、numeric x、hash badge、孤立相等操作符与长箭头避让。孤立 === 之外,还要检查 a===、"==="、===. 等边界。通过这些样例,只说明相应条件的结果成立,无法证明所有语言与所有文本都不会误触。
真实字号里检查相似形态
两横线和三横线要能区分,-- 要保留间隔,比较符要与箭头保持区别。还要观察笔画、重心和旁边字母是否协调,字形有没有贴边或粘连。
当前项目已有字号与字重矩阵,覆盖 11、13、16、20px,字重 100、400、700、900,以及深浅背景。它是字体轮廓生成的静态 QA,截图里的像素尺寸、操作系统的 hinting 和目标编辑器仍需另行验证。
图中的小字号适合检查整体黑度,局部细节建议点开原图看。当前源码只有 upright,Italic 没有被这份矩阵覆盖;终端、语法 token 分段、多光标与选区也都属于应用验收范围。
总结
字体 review 可以先用数据排除结构、替换和宽度错误,再用真实字号与目标应用检查视觉和交互。成功样例证明功能命中,反例约束误触范围,两者共同决定当前可用边界。AI 能参与生成与检查,最终仍需确认:这组符号有没有让代码更容易判断,还是引入了新的歧义。
修复前的 HarfBuzz 字形与推进宽度采样
孤立三等号修复前的 upright 字体快照,包含字体 SHA-256、轴范围、HarfBuzz 版本、输入和 feature 条件。
{
"fontFile": "LigconsolataNext.ttf",
"fontSha256": "9afcbff67cb4f9889816d4244cc02142c64d19286a486b70214c88798ce889dd",
"upem": 1000,
"uharfbuzzVersion": "0.56.4",
"harfbuzzVersion": "15.0.0",
"axes": [
{
"tag": "wght",
"minimum": 100,
"default": 400,
"maximum": 900
},
{
"tag": "wdth",
"minimum": 50,
"default": 100,
"maximum": 200
}
],
"samples": [
{
"text": "==",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "equal_equal.dlig",
"cluster": 0,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1000
},
{
"text": "===",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "equal_start.seq",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal_middle.seq",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal_end.seq",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1500
},
{
"text": "!=",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "exclam_equal.dlig",
"cluster": 0,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1000
},
{
"text": "!==",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "exclam_equal_equal.dlig",
"cluster": 0,
"advance": 1500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1500
},
{
"text": "a == b",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal_equal.dlig",
"cluster": 2,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 3000
},
{
"text": "a === b",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal_equal_equal.dlig",
"cluster": 2,
"advance": 1500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 6,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 3500
},
{
"text": "a != b",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "exclam_equal.dlig",
"cluster": 2,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 3000
},
{
"text": "a !== b",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "exclam_equal_equal.dlig",
"cluster": 2,
"advance": 1500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 6,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 3500
},
{
"text": "=>",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "equal_greater.dlig",
"cluster": 0,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1000
},
{
"text": "<====",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "less_equal_start.seq",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal_middle.seq",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal_middle.seq",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal_middle.seq",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal_end.seq",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 2500
},
{
"text": "i--",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "i",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "hyphen_hyphen.dlig",
"cluster": 1,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1500
},
{
"text": "// comment",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "slash_comment_spacer",
"cluster": 0,
"advance": 0,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash_slash.dlig",
"cluster": 1,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "c",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "o",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "m",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "m",
"cluster": 6,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "e",
"cluster": 7,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "n",
"cluster": 8,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "t",
"cluster": 9,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 5000
},
{
"text": "/// reference",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "slash_comment_spacer",
"cluster": 0,
"advance": 0,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash_comment_spacer",
"cluster": 1,
"advance": 0,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash_slash_slash.dlig",
"cluster": 2,
"advance": 1500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "r",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "e",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "f",
"cluster": 6,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "e",
"cluster": 7,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "r",
"cluster": 8,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "e",
"cluster": 9,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "n",
"cluster": 10,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "c",
"cluster": 11,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "e",
"cluster": 12,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 6500
},
{
"text": "https://example.com",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "h",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "t",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "t",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "p",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "s",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "colon",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash",
"cluster": 6,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash",
"cluster": 7,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "e",
"cluster": 8,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "x",
"cluster": 9,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "a",
"cluster": 10,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "m",
"cluster": 11,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "p",
"cluster": 12,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "l",
"cluster": 13,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "e",
"cluster": 14,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "period",
"cluster": 15,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "c",
"cluster": 16,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "o",
"cluster": 17,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "m",
"cluster": 18,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 9500
},
{
"text": "//TODO",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "slash",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "T",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "O",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "D",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "O",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 3000
},
{
"text": "/* block */",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "slash_comment_spacer",
"cluster": 0,
"advance": 0,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash_asterisk.dlig",
"cluster": 1,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "l",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "o",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "c",
"cluster": 6,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "k",
"cluster": 7,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 8,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash_comment_spacer",
"cluster": 9,
"advance": 0,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "asterisk_slash.dlig",
"cluster": 10,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 5500
},
{
"text": "**/*.{ts,tsx}",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "asterisk_asterisk.dlig",
"cluster": 0,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "asterisk",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "period",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "braceleft",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "t",
"cluster": 6,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "s",
"cluster": 7,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "comma",
"cluster": 8,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "t",
"cluster": 9,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "s",
"cluster": 10,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "x",
"cluster": 11,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "braceright",
"cluster": 12,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 6500
},
{
"text": "src/*.ts",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "s",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "r",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "c",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "asterisk",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "period",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "t",
"cluster": 6,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "s",
"cluster": 7,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 4000
},
{
"text": "a /\\ b",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash_logic_spacer",
"cluster": 2,
"advance": 0,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash_backslash.dlig",
"cluster": 3,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 3000
},
{
"text": "x \\/ y",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "x",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "backslash_logic_spacer",
"cluster": 2,
"advance": 0,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "backslash_slash.dlig",
"cluster": 3,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "y",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 3000
},
{
"text": "/\\d/",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "slash",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "backslash.thin",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "d",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 2000
},
{
"text": "\\/tmp",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "backslash.thin",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "slash",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "t",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "m",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "p",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 2500
},
{
"text": "0xFF",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "zero",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "x.multiply",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "F",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "F",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 2000
},
{
"text": "0xG",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "zero",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "x",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "G",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1500
},
{
"text": "## title",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "numbersign_badge_spacer",
"cluster": 0,
"advance": 0,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "numbersign_run2.dlig",
"cluster": 1,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "t",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "i",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "t",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "l",
"cluster": 6,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "e",
"cluster": 7,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 4000
},
{
"text": "##title",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "numbersign",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "numbersign",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "t",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "i",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "t",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "l",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "e",
"cluster": 6,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 3500
},
{
"text": "a ## b",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "numbersign",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "numbersign",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 3000
},
{
"text": "####### title",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "numbersign_start.seq",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "numbersign_middle.seq",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "numbersign_middle.seq",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "numbersign_middle.seq",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "numbersign_middle.seq",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "numbersign_middle.seq",
"cluster": 5,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "numbersign_end.seq",
"cluster": 6,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 7,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "t",
"cluster": 8,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "i",
"cluster": 9,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "t",
"cluster": 10,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "l",
"cluster": 11,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "e",
"cluster": 12,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 6500
},
{
"text": "==",
"features": {
"liga": 1,
"calt": 0,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "equal_equal.dlig",
"cluster": 0,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1000
},
{
"text": "===",
"features": {
"liga": 1,
"calt": 0,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "equal_equal_equal.dlig",
"cluster": 0,
"advance": 1500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1500
},
{
"text": "!=",
"features": {
"liga": 1,
"calt": 0,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "exclam_equal.dlig",
"cluster": 0,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1000
},
{
"text": "!==",
"features": {
"liga": 1,
"calt": 0,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "exclam_equal_equal.dlig",
"cluster": 0,
"advance": 1500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1500
},
{
"text": "a = b",
"location": {
"wght": 100,
"wdth": 50
},
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 250,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 250,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal",
"cluster": 2,
"advance": 250,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 3,
"advance": 250,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 4,
"advance": 250,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1250
},
{
"text": "a = b",
"location": {
"wght": 100,
"wdth": 100
},
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 2500
},
{
"text": "a = b",
"location": {
"wght": 100,
"wdth": 200
},
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal",
"cluster": 2,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 3,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 4,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 5000
},
{
"text": "a = b",
"location": {
"wght": 400,
"wdth": 50
},
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 250,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 250,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal",
"cluster": 2,
"advance": 250,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 3,
"advance": 250,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 4,
"advance": 250,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1250
},
{
"text": "a = b",
"location": {
"wght": 400,
"wdth": 100
},
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 2500
},
{
"text": "a = b",
"location": {
"wght": 400,
"wdth": 200
},
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal",
"cluster": 2,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 3,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 4,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 5000
},
{
"text": "a = b",
"location": {
"wght": 900,
"wdth": 50
},
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 250,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 250,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal",
"cluster": 2,
"advance": 250,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 3,
"advance": 250,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 4,
"advance": 250,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 1250
},
{
"text": "a = b",
"location": {
"wght": 900,
"wdth": 100
},
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal",
"cluster": 2,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 3,
"advance": 500,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 4,
"advance": 500,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 2500
},
{
"text": "a = b",
"location": {
"wght": 900,
"wdth": 200
},
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"glyphs": [
{
"glyph": "a",
"cluster": 0,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 1,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "equal",
"cluster": 2,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "space",
"cluster": 3,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
},
{
"glyph": "b",
"cluster": 4,
"advance": 1000,
"xOffset": 0,
"yOffset": 0
}
],
"totalAdvance": 5000
}
]
}孤立三等号修复前后采样
同一组 HarfBuzz 设置下的六组输入,记录两份字体的 SHA-256、glyph、cluster 与总 advance。
打开原文件 equality-guard-regression.json
{
"harfbuzzVersion": "15.0.0",
"features": {
"liga": 1,
"calt": 1,
"dlig": 0,
"kern": 0
},
"beforeFontSha256": "9afcbff67cb4f9889816d4244cc02142c64d19286a486b70214c88798ce889dd",
"afterFontSha256": "e6c3a110f07cc34890ab466e1497e6b531b44a6d2b39bb2584c037c20d774447",
"samples": [
{
"text": "==",
"before": {
"glyphs": [
{
"glyph": "equal_equal.dlig",
"cluster": 0,
"advance": 1000
}
],
"totalAdvance": 1000
},
"after": {
"glyphs": [
{
"glyph": "equal_equal.dlig",
"cluster": 0,
"advance": 1000
}
],
"totalAdvance": 1000
}
},
{
"text": "===",
"before": {
"glyphs": [
{
"glyph": "equal_start.seq",
"cluster": 0,
"advance": 500
},
{
"glyph": "equal_middle.seq",
"cluster": 1,
"advance": 500
},
{
"glyph": "equal_end.seq",
"cluster": 2,
"advance": 500
}
],
"totalAdvance": 1500
},
"after": {
"glyphs": [
{
"glyph": "equal_equal_equal.dlig",
"cluster": 0,
"advance": 1500
}
],
"totalAdvance": 1500
}
},
{
"text": "!==",
"before": {
"glyphs": [
{
"glyph": "exclam_equal_equal.dlig",
"cluster": 0,
"advance": 1500
}
],
"totalAdvance": 1500
},
"after": {
"glyphs": [
{
"glyph": "exclam_equal_equal.dlig",
"cluster": 0,
"advance": 1500
}
],
"totalAdvance": 1500
}
},
{
"text": "===>",
"before": {
"glyphs": [
{
"glyph": "equal_start.seq",
"cluster": 0,
"advance": 500
},
{
"glyph": "equal_middle.seq",
"cluster": 1,
"advance": 500
},
{
"glyph": "equal_middle.seq",
"cluster": 2,
"advance": 500
},
{
"glyph": "greater_equal_end.seq",
"cluster": 3,
"advance": 500
}
],
"totalAdvance": 2000
},
"after": {
"glyphs": [
{
"glyph": "equal_start.seq",
"cluster": 0,
"advance": 500
},
{
"glyph": "equal_middle.seq",
"cluster": 1,
"advance": 500
},
{
"glyph": "equal_middle.seq",
"cluster": 2,
"advance": 500
},
{
"glyph": "greater_equal_end.seq",
"cluster": 3,
"advance": 500
}
],
"totalAdvance": 2000
}
},
{
"text": "<====",
"before": {
"glyphs": [
{
"glyph": "less_equal_start.seq",
"cluster": 0,
"advance": 500
},
{
"glyph": "equal_middle.seq",
"cluster": 1,
"advance": 500
},
{
"glyph": "equal_middle.seq",
"cluster": 2,
"advance": 500
},
{
"glyph": "equal_middle.seq",
"cluster": 3,
"advance": 500
},
{
"glyph": "equal_end.seq",
"cluster": 4,
"advance": 500
}
],
"totalAdvance": 2500
},
"after": {
"glyphs": [
{
"glyph": "less_equal_start.seq",
"cluster": 0,
"advance": 500
},
{
"glyph": "equal_middle.seq",
"cluster": 1,
"advance": 500
},
{
"glyph": "equal_middle.seq",
"cluster": 2,
"advance": 500
},
{
"glyph": "equal_middle.seq",
"cluster": 3,
"advance": 500
},
{
"glyph": "equal_end.seq",
"cluster": 4,
"advance": 500
}
],
"totalAdvance": 2500
}
},
{
"text": "i--",
"before": {
"glyphs": [
{
"glyph": "i",
"cluster": 0,
"advance": 500
},
{
"glyph": "hyphen_hyphen.dlig",
"cluster": 1,
"advance": 1000
}
],
"totalAdvance": 1500
},
"after": {
"glyphs": [
{
"glyph": "i",
"cluster": 0,
"advance": 500
},
{
"glyph": "hyphen_hyphen.dlig",
"cluster": 1,
"advance": 1000
}
],
"totalAdvance": 1500
}
}
]
}