《Making Software》是 Dan Hollick 正在写的一本讲软件底层原理的图解书,How to make a font. 这一章用六千词把数字字体从矢量轮廓一路讲到 hinting。表面上看,字体不过是每个字符的一组矢量,做字体不过是把这些矢量一笔笔画出来;但画字形反而是容易的部分,难的是让几百个字形在不同字号、大小写、字重下彼此协调。
这一章的内容天然分成三层:机器怎么存储和渲染字体,设计师用什么语言描述字母,以及真正的功夫花在哪。笔记按这三层展开。
机器一侧:字体文件是个数据库
先厘清术语。typeface(字体家族)指整个家族——Garamond 连同它所有的字重与样式;font(字体)是一个具体实例,比如 12pt 的 Garamond Roman。日常混用没什么问题,但 type 设计师很在意这个区分。
技术上更颠覆直觉的一点:字体文件本质是由若干张表(table)组成的结构化数据库。我们以为的“字体”——字形轮廓——只占其中一张表;其余的表装着让文字正确渲染的一切:每个字符占多大空间的间距度量、为特定字母对微调间距的 kerning(字距)表、小字号下保清晰的 hinting(微调)指令,以及名称、授权、支持语言等元数据。
四十年格式演化
- PostScript Type 1(Adobe,1984):用三次(cubic)Bézier 曲线描述轮廓,成为专业印刷标准。局限是单文件只能装 256 个 glyph(字形),对很多语言远远不够,而且要两个文件配合使用。
- TrueType(Apple,1980 年代末):改用二次(quadratic)Bézier——控制点少一个(三个而非四个)、更简单,但同样的形状需要更多的点。最大卖点是内建 hinting,让设计者精确控制小字号下字形如何贴合像素网格。微软在 Windows 上采用后迅速普及。
- OpenType(Microsoft + Adobe,1990 年代末):统一容器,TrueType 轮廓和 PostScript 轮廓都能装。单文件最多 65,536 个 glyph,足以覆盖几十种语言的完整 Unicode,还引入了 layout 表,把 ligature(连字)、small caps(小型大写)、风格替代字、上下文替换等排版特性直接放进字体文件。
- Variable fonts(可变字体):最新一步,把整个设计空间压进单个文件。不再为 Light、Regular、Bold 各发一份文件,而是定义若干条 axis(轴)——weight、width、slant、optical size——运行时在 master(母版)之间做 interpolation(插值),轴上任意一点都能取。对 web 尤其划算:五个字重原本意味着五次网络请求。
打开一个 OpenType 文件
轮廓表的格式取决于字体的“口味”(flavor)。TrueType 口味(.ttf)用 glyf 和 loca 两张表。glyf 是一串点,用二次 Bézier 描述字形:每个点有 x,y 坐标和一个标志位,标记它在曲线上(on-curve)还是控制点(off-curve)。规则相当巧妙——两个连续 on-curve 点之间是直线;on-curve → off-curve → on-curve 构成一段二次 Bézier;出现两个连续 off-curve 点时,rasterizer(栅格化器)在中点自动插入一个虚拟 on-curve 点,于是不必显式存储每个锚点。loca 则只存每个 glyph 的字节偏移,标出各字形数据的起止。
OpenType 口味(.otf)用 CFF 或 CFF2 表。它不存点,而存一串绘制操作符(rmoveto、rlineto、rrcurveto),从栈里取数字移动画笔,和 SVG 的 path 数据非常像;因为数字是相对值,体积更小、压缩更好。CFF2 是为 variable fonts 设计的现代版:额外存储基准字形随 weight、width、optical size 变化的偏移量,渲染时先处理基准点,再按当前设置叠加偏移。
其余几张关键表:
cmap:Unicode 码位到字形的映射。敲一个 A,计算机认成U+0041,再查表找到对应 glyph。head:文件头,装版本、全局包围盒和以 UPM(Units Per em)表示的网格尺度。UPM 定义多少字体单位对应一个 em square,CFF 通常取 1000,TrueType 通常取 1024。hhea:横排文字的全局度量——最大 ascender/descender、推荐 leading(行距)、最宽字形的 Max Advance Width;竖排另有可选的vhea。hmtx(竖排为vmtx):逐字形的间距数据,为每个 glyph 定义 Advance Width(步进宽度)、LSB(左侧留白)与 RSB(右侧留白)。Advance Width 相当于字母的占位盒,排文本行时靠它分配空间;RSB 通常由宽度和 LSB 算出。- 另有存内存需求的
maxp、存名称等文本信息的name,以及OS/2表——名字来自 1980 年代末的同名操作系统,如今存字重与字宽范围、家族样式(Serif、Sans-Serif、Script、Monospace)、x-height、cap height,以及一批把 Windows、macOS、web 各自渲染差异抹平的跨平台度量。
Text shaping:从字符串到定位好的字形
计算机排文字要经过一连串步骤,统称 text shaping(文本塑形)。画字之前,文本先被切成一个个 run:字体、字号、颜色、语言完全相同的片段,任何一项变化(比如句子里一个斜体词)都会结束当前 run、另起一个。字符分 strong 和 weak 两类:strong 字符强属于某个字母表(拉丁 “A”、希腊 “Δ”),为渲染器指明语言归属;weak 字符是空格、数字、标点,不属于任何语言,也不中断 run。run 之间完全隔离——kerning 不跨 run 边界,希腊字母和拉丁字母若分处两个 run,就不会互相调距。
切好之后,每个 run 走四步:
- 查表:把字符的 Unicode 值(A 是
U+0041)经cmap换成 Glyph ID。 - 替换(
GSUB):按上下文替换字形,比如把相邻的 “f” 和 “i” 换成合并的 “fi” ligature。 - 定位(
GPOS):对字母对施加间距调整,比如让 “o” 左移、钻到大写 T 的横画下面——上下文感知的 kerning。 - 渲染:前三步挪动的都是看不见的空盒子,这一步才按 Glyph ID 取出轮廓数据、按尺度画出来,每个字形靠自己的 sidebearing 在盒子里落位。
用 variable font 时,无非是在切 run 之前先把字重定下来。整个流程和浏览器排 HTML/CSS 高度同构:Unicode 文本是内容,字体与颜色是 CSS,四步流水线就是搭布局块再绘制上屏。
Hinting:向像素网格的妥协
字形画在约 1000 单位的网格上,小字号渲染时整个坐标系被压进几个像素。11px 下,一根 100 单位宽的竖干只合一个多像素,几乎不可能干净地落在像素网格上:H 的一根竖干栅格化成一列清爽的像素,另一根却糊成两列灰的,整个字又虚又歪。
Hinting 是存在字体里的指令,在特定字号下扭曲轮廓,让它在栅格化之前对齐像素网格。目标不是保住字母的真实形状,而是稍稍出卖它以换取清晰——竖干凑成整数像素宽,强制 H 的两根竖干一致,保证一行字的 x-height 齐平。
两种口味的处理方式迥异。TrueType 的 hint 本质是小程序:以字节码形式存在 fpgm、prep、glyf 表里,由 rasterizer 内置的小虚拟机执行,给设计者逐像素、逐字号的控制。低分辨率 CRT 时代这种控制至关重要——Verdana 和 Georgia 都是一个像素一个像素手工 hint 出来的,这是它们当年在屏幕上格外清晰的重要原因。CFF 走声明式路线:不编排具体结果,只标出竖干的位置与宽度,外加一组叫 blue zone 的水平对齐带(对应 baseline、x-height、cap height),把网格贴合的决策留给 rasterizer。如今 hinting 远不如从前重要:高分屏让竖干天然有好几个像素宽,anti-aliasing 抹平其余,macOS 干脆忽略大部分 hinting、直接渲染忠实轮廓。今天多数字体由字体编辑器或 ttfautohint 自动 hint,手工 hinting 已近失传。
设计一侧:字母的语言
机器的部分到此为止。type 设计是门很老的手艺,它有自己的一整套语言,而且大半来自铅字时代的物理排版:font 源自法语 *fondre*(熔铸),本指一套特定字号的完整铸字;每个字母叫 sort,收在字盘(case)里——大写放上层(upper case),更常用的其余字母放下层(lower case),这就是大小写英文名的出处。排字工用薄铅条隔开行,行距因此叫 leading(源自 lead,铅);铅字上伸出字身、用来补偿间距的部分叫 kern,于是字母间距调整叫 kerning。
尺寸与比例
每个字形画在一个叫 em 的方框里,单位尺度就是前面说的 UPM(通常 1000),之后的一切度量都基于这套坐标系。字号设为 14px 时,是这个 em 方框去占那 14 像素的高度;放大字号只是放大方框,坐标本身不变,因为它是相对的。坐标原点也不在角上,而是由 baseline(基线)决定——它锚定整套字体的对齐,西文一般放在 em 网格约四分之三高的位置。
在 baseline 之上,一套字体要依次定出几条高度线:
- cap height(大写高度):用平顶的 H 来定(E、I 也行)。它不是硬上限——圆形字母(O、C、G、Q、S)和尖头字母(A、V)常常略微冲出 cap height 和 baseline,这叫 overshoot(过冲),为的是视觉对齐而非数学对齐。
- x-height(小写高度):用 x(偶尔 o、n)来定,可能是做字体最有分量的一个决定——它决定字体的“视觉大小”,一般取 cap height 的 60–75%。x-height 越高越易读、同字号下显得越大:Zuzana Licko 为 Emigre 设计的 Mrs. Eaves 只有 57%,后来的 Mrs. Eaves XL 提到 72%,同字号对比相当惊人。但收益有边际——太高了 ascender 和 descender 就难以分辨。有研究给出的最佳值约为 0.3° 视角(视角同时取决于大小和距离,实验取 40cm),超出这个范围阅读速度开始下降。
- ascender / descender(上伸部 / 下伸部):小写字母伸出 x-height 之上、baseline 之下的部分对应的对齐线。定 ascender 用带干净竖干的 h、d、b、l,定 descender 用 p、q;f、g 这类带曲线的字母照例会过冲。
- 其余还有 figure height(数字高度,通常等于 cap height)、small cap height(介于 cap height 和 x-height 之间)、accent 高度,以及上标、下标的基线偏移与缩放比例。
宽度方面:proportional(比例)字体每个字母宽度不同,monospaced(等宽)字体则一律相同。一个字母占的总宽叫 advance width = LSB + 字形宽度 + RSB。sidebearing(边距)像内建在每个字母上的页边距,用来和邻字保持距离——别和 kerning 混淆。x-height 越小,小写通常越需要更大的 sidebearing 来保证可辨认;condensed(窄体)字体则整体收窄。
字重、对比度与 stress
weight(字重)直觉上就是笔画多粗,但 regular、semi-bold、bold 到底多粗、甚至该不该叫这些名字,都没有标准。最接近标准的是 web:CSS 把 regular 定为 400、bold 定为 700,除此之外各玩各的。
不过字重可以量化。设计师 Charles Bigelow 用 x-height 与竖干粗细的比值来度量:典型 regular 约在 1:5–1:6。放到 em 网格上,x-height 若取 500 单位(0.5em),regular 的竖干就约 100 单位;相邻字重的笔画粗细应差 1.3–1.5 倍才足够区分——上一档约 130 单位,下一档约 70。这是线性刻度;另一位设计师 Luc(as) de Groot 主张非线性递进,像一条缓动曲线(原文配图即以 Inter 的字重序列为例)。当然,这些都不是硬规矩。
竖干和横干通常不一样粗,两者之差叫 contrast(对比度):差大是高对比,差小是低对比,全部等粗叫 monolinear(单线)。但即便单线字体,笔画也会因方向和曲率略有粗细差别,原因是 thickness illusion(粗细错觉):横线看起来比等粗的竖线、斜线更粗,所以横干通常要削细一点来找平。同样的补偿也用在 bowl(字碗,封闭的圆弧笔画)上:曲线最厚处要比竖干更厚、最薄处要比横干更薄,看起来才一致。contrast 说的是差多大,stress(重心轴)说的是差发生在哪个方向——它源于书法里笔尖随压力和速度改变粗细的方式,同一套字里的 stress 轴应当一致,像出自同一支笔。
视觉修正无处不在。double-story(双层)字母(E、B、S)的横画如果放在数学正中会显得偏低,所以要整体上移;而 E、B 上移之后痕迹太明显,就再把下半层做宽一点找平。
一个字母的解剖
字母的每个部位都有专名。serif(衬线)是笔画末端的小脚:横段向内凹以抵消视错觉的叫 cupped serif(EB Garamond 就是);按形态又分 bracketed(带弧形支撑连接主干)、wedged(末端渐细)、slab(等粗)、hairline(极细)等。不以衬线收尾的笔画末端统称 terminal,可以带 teardrop(泪滴)、ball(圆球)、flare(喇叭)等修饰;只出现在字母顶部的竖衬线叫 beak,竖干一侧的横衬线(数字 1 那种)叫 flag;伸到 baseline 以下的笔画叫 tail,装饰性的花笔叫 swash。
字母内部封闭或半封闭的空间叫 counter(字怀)——小写 e 的那一块特称 eye,双层 g 的下环特称 loop。此外还有一长串:i、j 的点叫 tittle,f 的弯头叫 hook,n、m 的弧形连接叫 shoulder,k、R 的斜笔叫 leg,E 的横笔叫 arm,s、8 的中轴曲线叫 spine;连接竖干的横笔叫 crossbar,斜笔在底部相交叫 vertex,在顶部相交叫 apex。原文还有更长的清单,这里只留最常见的一批。
工艺一侧:负空间与起笔
间距:负空间才是主角
做字体的很大一部分功夫不在画字形,而在打磨字形之间的负空间。间距对可读性的影响超乎直觉:太松,词里出现“空白河流”,难以成形;太紧,字母彼此糊住。理想状态是每对字母之间的视觉留白相同,一行字在一臂之外看是均匀的灰度、近看有可预期的条纹节奏。负空间大的字母(往往和 counter 大小相关)需要收得更紧,行话叫 “spacing matches counters”(间距随字怀)。
工具只有两个:sidebearing 和 kerning,前者内建在每个字母里,后者是对特定字母对(比如 AV)的额外修正,理想情况下只在 sidebearing 无能为力时才动用。逐个字母定 sidebearing 太苦,Walter Tracy 留下了一套方法:按形状归类、共用取值。先从 H 和 O 起手——一个极方、一个极圆——拿 “HHHOOHHH”“OOOHHOOO” 这样的组合反复调整,直到竖干的节奏看起来均匀统一;再对小写的 o 和 n 做同样的事。其余字母按最相似的形状套用:B 的左 sidebearing 取自 H(平边),右 sidebearing 取自 O(圆边);A、w 这类斜边字母公式失效,只能手调。
验证方式是把每个字母放到所有其他字母中间,找视觉节奏被打断的地方。总会有一些字母对光靠 sidebearing 解决不了,比如 Ti——i 得钻到 T 的悬挑底下去。这就轮到 kerning 出场:一条存进 GPOS 表、针对特定字母对的间距偏移,排 run 的时候取用。逐对手工 kern 工作量惊人,好在 OpenType 支持 class-based kerning:把形状相似的字母按“边”分组——D、E、F 的左边都是直的,对它们前面的字母就可以共用同一套 kerning 规则。
怎么开始画一套字
真到了画字形这一步,原文自嘲进入了“把猫头鹰剩下的部分画完”环节,只能快讲。设计师不会从 A 顺着画到 Z,而是先做几个 control letter(控制字母),逼自己把 stress、contrast、比例、性格这些会传导给所有字母的决定先定下来。大写通常用 O 和 H/E。
O 格外重要:它确立 stress(斜轴还是竖轴)、contrast(粗细比)和比例(宽圆还是窄椭圆),所有圆形字母都得跟着它走——C 大致是 O 切掉一块,G 加根横画,Q 加条尾巴,D 用竖干封住右侧。O 画错,整套字都会垮。方形基准有人用 H,但 E 更有用:它自带 terminal,又比 O 和 H 窄,能把比例系统立起来。E 的三条横臂常常故意不等长——底臂最长、顶臂居中、中臂最短,而且中臂要稍稍上移。大写字母按结构分组推进:圆形(O、Q、C、G、S)、方形(E、F、H、I、L、T)、斜形(V、A、W、X),以及圆方组合(D、B、P、R)和斜方组合(M、N、K、Z、Y)。
小写常从 a、e、g、n、o 起手,道理和 O、E 相同。经典测试词 “hamburgefontsiv” 几乎囊括高频小写字母,能暴露不同形状之间的相互作用。o 是所有圆碗字母(a、c、e、p、d、b、q、g)的基础,contrast、stress、碗的比例都要保持一致:o 的碗最宽,c、e 因为开口、负空间更大,要做窄一点;碗和竖干相交处通常留一个小凹口,叫 relief。n 是 m、h、u 的基础,比 o 略窄;r 像 n,但凹口要压得更低。最难归类的是双层的小写 a:拱可以借 n 的 shoulder,但收笔方式不同,下层碗的高度通常取 x-height 的 55–65%。
值得记住的几点
- 字体文件是数据库,不是一堆图形。轮廓只占一张表,
cmap、hmtx、GSUB/GPOS、OS/2这些表才让文字能被查找、被排布、被跨平台一致地渲染。 - 两条曲线,两条技术路线。TrueType 用二次 Bézier(
glyf/loca),OpenType/CFF 用三次 Bézier 加类 SVG 的绘制指令;variable fonts 在 master 之间插值,把整个设计空间装进一个文件。 - 排版是条流水线。text shaping 先切 run,再走
cmap查表 →GSUB替换 →GPOS定位 → 渲染四步,和浏览器排 HTML/CSS 高度同构。 - type 设计是视觉欺骗的艺术。overshoot、thickness illusion 补偿、上移的横画——数学对齐几乎处处让位于视觉对齐。
- 间距比字形更费功夫。负空间决定可读性:先用 sidebearing 按形状分组定基调,再用 kerning 收拾特例。hinting 曾是小字号清晰的命脉,如今在高分屏和抗锯齿面前基本退场。