工作记录

字体原理笔记

开发 阅读 3 分钟

目录

上一篇《博客字体的选择》记的是换字体的结果,现在继续说一下背后的原理。

三大字体族

  1. 衬线体(serif):笔画末端带小装饰角。拉丁的 Times、Georgia;中文对应宋体/明体——横细竖粗、横画末端有顿角。装饰不是审美添头,是工艺化石:拉丁衬线来自古罗马石刻收刀,宋体顿角来自雕版刀口与毛笔顿笔。气质天然联想「纸、书、正式」。

  2. 黑体(sans-serif):去掉一切装饰,笔画等粗。拉丁的 Helvetica、Inter,中文的微软雅黑、苹方、思源黑体。工业时代产物,气质「现代、中性、屏幕」,是当代互联网的默认审美。

  3. 等宽体(monospace):每字符占宽相同(Consolas、JetBrains Mono),为代码对齐而生。中文单字在任何字体里本来就是等宽方块,「中文等宽字体」的真正含义是拉丁字符恰好半个汉字宽(2:1),中西混排的代码列才对得齐——代表是更纱黑体(Sarasa Mono SC)、思源等宽。

屏幕上的衬线兴衰

低分辨率时代衬线细横渲染不出来,黑体统治屏幕。

Windows「宋体发虚」的根源即中易宋体是为小字号点阵优化的,大字号经不起 ClearType。

高分屏普及后衬线才在屏幕翻身——Noto Serif SC 就是绕开中易宋体的历史包袱。

两个细节

  • 真斜体 vs 假斜体:Georgia 的 italic 是单独设计的字形;中文无斜体传统,浏览器只能机械倾斜(假斜体)。「斜体日期」只有拉丁字符才好看。

  • 老式数字:Georgia 的数字高低错落如小写字母,混入正文不打眼——它比 Noto 拉丁部分「有温度」的原因之一。

font-family

font-family: Georgia, "Noto Serif SC", SimSun

font-family 栈是「逐字」路由表,不是「三选一」,而是每个字符独立地从左到右查询,第一个拥有该字形的字体中标:

"Docker 容器" 的渲染:
D-o-c-k-e-r → Georgia 有 ✓ → 用 Georgia
容-器       → Georgia 无汉字 ✗ → Noto Serif SC ✓ → 用 Noto

推论:

  1. 同一行中英文分属两个字体是机制而非问题。

  2. 栈序即设计决策:Georgia 在前,英文数字全走 Georgia;若中文字体在前,它自带的拉丁字形会拦截英文,Georgia 永远轮不上(本站踩过:一行之差决定味道)。

  3. 栈的末尾永远放系统兜底(SimSun/serif),保证任何字符都有着落。

子集化

字量问题:拉丁字体几百个字形、几十 KB,西方网站随手全量引入 webfont;中文字体两万多字形,一个字重完整文件 8–12MB,没法上网页。

子集化(subsetting):把字体裁到「本站实际用到的字符」。博客约 1500 个唯一字符,裁后一个字重约 400KB。

代价:子集是快照。文件里只有裁剪那一刻存在的字。新文章用了子集外的字(如「吝啬」)时:吝 → Noto 子集无 ✗ → 沿栈下落 → SimSun ✓ → 仅这个字用宋体渲染

自定义字体构建

1. 构建时比对:每次构建把 dist 产物的全部字符与基线 charset.txt 对比,缺字警告。

have  = set(open("static/fonts/charset.txt", encoding="utf-8").read())
chars = set()
for dp, _, fs in os.walk("dist"):                 # 以产物为准:
    for f in fs:                                  # 已发布内容+UI 文案,
        if f.endswith((".html", ".js", ".css")):  # 草稿不计入
            chars.update(open(os.path.join(dp, f), encoding="utf-8").read())
miss = {c for c in chars - have if c.isprintable()}
if miss:
    print(f"字体子集缺 {len(miss)} 个字符(这些字将回退宋体): {''.join(sorted(miss))}")

2. 重切:先重新提取字符集写回基线,再用 fonttools 从源字体切出新子集:

python3 -m fontTools.subset /tmp/NotoSerifSC-Regular.otf \
  --text-file=static/fonts/charset.txt \
  --flavor=woff2 \
  --layout-features='*' \
  --output-file=static/fonts/NotoSerifSC-400.woff2

参数:

  • --text-file 留哪些字
  • --flavor 出什么格式(woff2,brotli 压缩)
  • --layout-features='*' 保留全部 OpenType 特性,标点挤压和 kern 都在里面,省那几十 KB 不值得

3. 缓存级联失效:字体 URL 的 ?v=哈希 写在 style.css 内,css 自身也带哈希——字体变 → css 哈希变 → 页面引用变,任何缓存层都不会卡旧字体。