前段时间回家和 ewind 大神吹了吹水。交谈过程中了解到他最近在研究使用 JavaScript 渲染文字黑科技,发现在计算机中根据字体渲染文字也是一门有趣的学问。于是我也去了解了一些跟字体渲染有关的内容。

计算机字体标准的历史

在上古时期的计算机中,字体都是使用点阵 (bitmap) 的方式来描述的。点阵字体描述了每一个文字的各个字号下像素点的排列组合方式。 但是它存在一些无法避免的问题。例如,你需要的字号没有被描述,那么系统只能根据相近的被描述的字号推算出应有的点阵排列,这会使文字看起来相当丑陋;或者是当你需要把文字旋转到一个微妙的角度时,很可能它的显示效果也会不尽人意。

在 1980 年代后期,Adobe 推出了矢量字体标准Type1,使用矢量的图形来描述字母。矢量字体在放大缩小的时候不会遇到像点阵字体那样失真的问题。同时,Adobe 还推出了用于印刷的语言Postscript

当时 MS 和 Apple 都想在自己的操作系统中引入矢量字体,但是因为 Adobe 的东西太贵了,因此这两家公司联手开发了自己的矢量字体和打印技术。但是最终落地的只有 Apple 开发的TrueType,而 Microsoft 开发的打印引擎TrueImage只是放了个鸽子。

bitmap_font_and_true_type_font

点阵字体和矢量的TrueType字体的对比

microsoft_true_type

TrueType字体的一些特性

到了后面 Adobe 发现自己的游戏玩不下去了,于是也和微软一起,开发了OpenType字体标准,也叫Type2,OpenType 兼容 TrueType 标准(也可以看做是 TrueType 的升级版),但有一些更强大的功能。

除此之外,还有为 web 而生的字体标准woffwoff2,它们的优势在于压缩率高。当然这些都是后话了。

由于 TrueType 是计算机行业中比较基础、应用广泛的字体格式,因此本文的字体介绍中都使用 TrueType 来举例。

如何描述一个字形

在矢量字体中,描述一个字形方式由原本的点阵变成了使用数学符号记录的它的轮廓。

控制点和轮廓

在 TrueType 中,使用直线和曲线的组合以构建复杂的字形。实际上,在最底层,TrueType 字体中的每一个字形都是由网格上的点来描述的。

通过两个点可以得到一条直线。通过三个点可以得到一个二次贝塞尔曲线 (Bezier quadratic curves)。在这种情况下,有两个点作为曲线的起点和终点,另一个点为在曲线外的控制点。这三个点可以确定一条唯一的曲线。

它的完整定义如下:

给定三个点 p0,p1,p2。控制点 p1 位于点 p0 和 p2 处的切线与曲线的交点处。 因此,p0,p1 与点 p0 处的曲线相切。 类似地,p2,p1 与点 p2 处的曲线相切。 由这三个点指定的曲线由参数方程定义。 对于从 0 到 1 的 t,p(t)的位置可以用这个方程式表示:

p(t) = (1-t)2p0 + 2t(1-t)p1 + t2p2
由此可以得出这条曲线。

figure_1

更复杂的曲线则可以使用多段二次曲线连接得到。两段曲线的切线相互连接,可以产生二次样条(quadratic spline)。 如果每个曲线上的点都位于连接其两侧的两个侧翼控制点的线上,则连接的二次曲线具有一阶连续性和切线连续性。

例如下图,由p0,p1,p2三个点定义了一条曲线,再添加一个曲线上的终点p4,并在p1p2延长线上添加控制点p3,这样就得到了由p2,p3,p4定义的一条新的曲线。这条曲线与第一条在p2处相连,并与第一条具有一阶连续性。

figure_2

在这五个点中,由于p2是可以由p1,p3和其他数据推算出来的,因此可以去掉点p2来简化这条曲线的表述。由此我们可以得到一条这样的曲线:

figure_3

最后,通过线段之间的相互连接,可以得到一个封闭的字形轮廓。构成轮廓的线段不一定只有一条,也可能一条都没有(例如空格)。例如下面的两张图就是由线段围成的两个字母的轮廓。在创建一个字体时,还需要每个字形中轮廓的方向,浅显的讲就是控制点要按顺序排列。轮廓的方向同时也定义了一个字形中哪些地方是实心哪些地方是空心的。

figure_4figure_5

在TrueType字体中,也支持轮廓相交,多个字形组合成为一个新的字形等功能。

网格

设计好一个字形的形状后,就要把这个字形放到网格中。在TrueType中,主网格是一个二维坐标系,横纵向都分别 至多有32768个单位(正负各16384个单位),这个单位被叫做FUnit。 在创建字形轮廓时,字体创建者会使用一个虚构的正方形区域,这个区域常被称为"em square"。在数字字体中,字形是可以延伸到em square之外的。Em square中高和宽的单位数量(也就是横纵坐标上单位的数量)反映了网格的密度。每个正方形区域中网格的密度越高,表现出的字体形状就越精细。但是字体设计者也必须考虑到输出设备的最大分辨率,以达到理想的效果。

打个比方——就像你手工制作并打磨了一个手机模型,只要0.01厘米的偏差都能让你感觉到不自在——然而实际要拿去生产的时候却发现机器的加工精度只能做到0.1厘米,因此生产出来的手机怎么摸都会让你感觉不自在。只有当你设计的精度和实际生产时能达到的精度达到平衡时,才能拥有最佳的效果。例如下图中的字母A,设计出的轮廓是左上方的样子,但如果放到一个较低密度的网格中,就不能较好的表现字体设计的细节,而放在较高密度的网格中,则可以更好地还原字体的设计。

figure_6

在罗马字体中,横坐标(y = 0)通常对应字体的基线。在纵坐标(x = 0)上,大多数字体制作者会选择将字体的左边缘对齐中纵坐标,或将字体水平居中于坐标系。对于非罗马字体来说,会尽量选择把字体水平居中于坐标系,而垂直方向则大多根据实际情况而定。

figure_7figure_8

在将文字的轮廓置入到网格中后,还需要对文字缩放、网格拟合轮廓、设备优化等进行调整和声明。至此,如何描述一个字体就介绍完了。关于 网格拟合轮廓 的内容和如何渲染一个字体会在下一篇文章中介绍。

参考