Freetype 字体引擎分析与指南
Cathy.zheng
1.FreeType 字形约定
1.1 基本印刷概念
1.1.1 字体文件、格式和信息
字体是一组可以被显示和打印的多样的字符映像,在单个字体中共享一些共有的特性,包括外表、风格、
衬线等。按印刷领域的说法,它必须区别一个字体家族和多种字体外观,后者通常是从同样的模板而来,但
是风格不同。例如,Palatino Regular 和 Palatino Italic 是两种不同的外观,但是属于同样的家族 Palatino。
单个字体术语根据上下文既可以指家族也可指外观。例如,大多文字处理器的用户用字体指不同的字体
家族,然而,大多这些家族根据它们的格式会通过多个数据文件实现。对于 TrueType 来讲,通常是每个外
观一个文件(arial.ttf 对应 Arial Regular 外观,ariali.ttf 对应 Arial Italic 外观)这个文件也叫字体,但是实际上
只是一个字体外观。
数字字体是一个可以包含一个和多个字体外观的数据文件,它们每个都包含字符映像、字符度量,以及
其他各种有关文本布局和特定字符编码的重要信息。对有些难用的格式,像 Adobe 的 Type1,一个字体外观
由几个文件描述(一个包含字符映象,一个包含字符度量等)。在这里我们忽略这种情况,只考虑一个外观
一个文件的情况,不过在 FT2.0 中,能够处理多文件字体。
为了方便说明,一个包含多个外观的字体文件我们叫做字体集合,这种情况不多见,但是多数亚洲字体
都是如此,它们会包含两种或多种表现形式的映像,例如横向和纵向布局。
1.1.2 字符映象和图
字符映象叫做字形,根据书写、用法和上下文,单个字符能够有多个不同的映象,即多个字形。多个字
符也可以有一个字形(例如 Roman)。字符和字形之间的关系可能是非常复杂,本文不多述。而且,多数字
体格式都使用不太难用的方案存储和访问字形。为了清晰的原因,当说明 FT 时,保持下面的观念
一个字体文件包含一组字形,每个字形可以存成位图、向量表示或其他结构(更可缩放的格式使用一种
数学表示和控制数据/程序的结合方式)。这些字形可以以任意顺序存在字体文件中,通常通过一个简单
的字形索引访问。
字体文件包含一个或多个表,叫做字符图,用来为某种字符编码将字符码转换成字形索引,例如 ASCII、
Unicode、Big5 等等。单个字体文件可能包含多个字符图,例如大多 TrueType 字体文件都会包含一个 Apple
特定的字符图和 Unicode 字符图,使它在 Mac 和 Windows 平台都可以使用。
1.1.3 字符和字体度量
每个字符映象都关联多种度量,被用来在渲染文本时,描述如何放置和管理它们。在后面会有详述,它们
和字形位置、光标步进和文本布局有关。它们在渲染一个文本串时计算文本流时非常重要。
每个可缩放的字体格式也包含一些全局的度量,用概念单位表示,描述同一种外观的所有字形的一些特
性,例如最大字形外框,字体的上行字符、下行字符和文本高度等。
虽然这些度量也会存在于一些不可缩放格式,但它们只应用于一组指定字符维度和分辨率,并且通常用
象素表示。
1.2 字形轮廓
1.2.1 象素、点和设备解析度
当处理计算机图形程序时,指定象素的物理尺寸不是正方的。通常,输出设备是屏幕或打印机,在水平
和垂直方向都有多种分辨率,当渲染文本是要注意这些情况。
定义设备的分辨率通常使用用 dpi(每英寸点(dot)数)表示的两个数,例如,一个打印机的分辨率为
300x600dpi 表示在水平方向,每英寸有 300 个象素,在垂直方向有 600 个象素。一个典型的计算机显示器根
据它的大小,分辨率不同(15’’和 17’’显示器对 640x480 象素大小不同),当然图形模式分辨率也不一样。
所以,文本的大小通常用点(point)表示,而不是用设备特定的象素。点是一种简单的物理单位,在数
字印刷中,一点等于 1/72 英寸。例如,大多罗马书籍使用 10 到 14 点大小印刷文字内容。
可以用点数大小来计算象素数,公式如下: 象素数 = 点数*分辨率/72
分辨率用 dpi 表示,因为水平和垂直分辨率可以不同,单个点数通常定义不同象素文本宽度和高度。
1.2.2 向量表示
字体轮廓的源格式是一组封闭的路径,叫做轮廓线。每个轮廓线划定字形的外部或内部区域,它们可以
是线段或是 Bezier 曲线。
曲线通过控制点定义,根据字体格式,可以是二次(conic Beziers)或三次(cubic Beziers)多项式。在文献中,
conic Bezier 通常称为 quadratic Beziers。因此,轮廓中每个点都有一个标志表示它的类型是一般还是控制点,
缩放这些点将缩放整个轮廓。
每个字形最初的轮廓点放置在一个不可分割单元的网格中,点通常在字体文件中以 16 位整型网格坐标
存储,网格的原点在(0,0),它的范围是-16384 到-16383(虽然有的格式如 Type1 使用浮点型,但为简便起见,
我们约定用整型分析)。
网格的方向和传统数学二维平面一致,x 轴从左到右,y 轴从下到上。
在创建字形轮廓时,一个字体设计者使用一个假想的正方形,叫做 EM 正方形。他可以想象成一个画字
符的平面。正方形的大小,即它边长的网格单元是很重要的,原因是 :
它是用来将轮廓缩放到指定文本尺寸的参考,例如在 300x300dpi 中的 12pt 大小对应 12*300/72=50 象素。
从网格单元缩放到象素可以使用下面的公式
象素数 = 点数 × 分辨率/72
象素坐标= 网格坐标*象素数/EM 大小
EM 尺寸越大,可以达到更大的分辨率,例如一个极端的例子,一个 4 单元的 EM,只有 25 个点位置,
显然不够,通常 TrueType 字体之用 2048 单元的 EM;Type1 PostScript 字体有一个固定 1000 网格单元的
EM,但是点坐标可以用浮点值表示。
注意,字形可以自由超出 EM 正方形。网格单元通常交错字体单元或 EM 单元。上边的象素数并不是指
实际字符的大小,而是 EM 正方形显示的大小,所以不同字体,虽然同样大小,但是它们的高度可能不同。
1.2.3Hinting 和位图渲染
存储在一个字体文件中的轮廓叫“主”轮廓,它的点坐标用字体单元表示,在它被转换成一个位图时,
它必须缩放至指定大小。这通过一个简单的转换完成,但是总会产生一些不想要的副作用,例如像字母 E 和
H,它们主干的宽度和高度会不相同。
所以,优秀的字形渲染过程在缩放“点”是,需要通过一个网格对齐(grid-fitting)的操作(通常叫 hinting),
将它们对齐到目标设备的象素网格。这主要目的之一是为了确保整个字体中,重要的宽度和高度能够一致。
例如对于字符 I 和 T 来说,它们那个垂直笔划要保持同样象素宽度。另外,它的目的还有管理如 stem 和
overshoot 的特性,这在小象素字体会引起一些问题。
有若干种方式来处理网格对齐,多数可缩放格式中,每种字形轮廓都有一些控制数据和程序。
显式网格对齐
TrueType 格式定义了一个基于栈的虚拟机(VM),可以借助多于 200 中操作码(大多是几何操作)来编
写程序,每个字形都由一个轮廓和一个控制程序组成,后者可以处理实际的网格对齐,他由字体设计者定义。
采用显式的方式,质量上--对小字体有很好的结果,这对屏幕显示非常重要;速度上--如果程序很复杂,解释
字节码很慢;一致性上--所有渲染器产生同样的字形位图; 大小上--字形程序会很长;技术难度上--编写优
秀的 hinting 程序非常难,没有好的工具支持。
隐式网格对齐(也叫 hinting)
Type1 格式有一个更简单的方式,每个字形由一个轮廓以及若干叫 hints 的片断组成,后者用来描述字形
的某些重要特性,例如主干的存在、某些宽度匀称性等诸如此类。没有多少种 hint,要看渲染器如何解释 hint
来产生一个对齐的轮廓。大小:Hint 通常比显式字形程序小的多;质量:小字体不好,最后结合反走样;速
度:网格对齐会非常快 不一致:不同渲染器结果不同,甚至同一引擎不同版本也不同。
自动网格对齐
有些格式很简单,没有包括控制信息,将字体度量如步进、宽度和高度分开。要靠渲染器来猜测轮廓的
一些特性来实现得体的网格对齐。大小:不需要控制信息,导致更小的字体文件;质量:小字体不好,最后
结合反走样;速度:依赖对齐算法,通常比显式对齐快。 速度:依赖算法;不一致:不同渲染器结果不同,
甚至同一引擎不同版本也不同。
1.3 字形度量
1.3.1 基线(baseline)、笔(pen)和布局(layout)
基线是一个假想的线,用来在渲染文本时知道字形,它可以是水平(如 Roman)和是垂直的(如中文)。
而且,为了渲染文本,在基线上有一个虚拟的点,叫做笔位置(pen position)或原点(origin),他用来定位
字形。每种布局使用不同的规约来放置字形:
对水平布局,字形简单地搁在基线上,通过增加笔位置来渲染文本,既可以向右也可以向左增加。 两个
相邻笔位置之间的距离是根据字形不同的,叫做步进宽度(advance width)。注意这个值总是正数,即使
是从右往左的方向排列字符,如 Arabic。这和文本渲染的方式有些不同。 笔位置总是放置在基线上。
对垂直布局,字形在基线上居中放置:
1.3.2 印刷度量和边界框
在指定字体中,定义了多种外观度量。
上行高度(ascent)。从基线到放置轮廓点最高/上的网格坐标,因为 Y 轴方向是向上的,所以它是一个正
值。
下行高度(descent)。从基线到放置轮廓点最低/下的网格坐标,因为 Y 轴方向是向上的,所以它是一个
负值。
行距(linegap)。两行文本间必须的距离,基线到基线的距离应该计算成:
上行高度 - 下行高度 + 行距
边界框(bounding box,bbox)。这是一个假想的框子,他尽可能紧密的装入字形。通过四个值来表示,
叫做 xMin、yMin、xMax、yMax,对任何轮廓都可以计算,它们可以是字体单元(测量原始轮廓)或者
整型象素单元(测量已缩放的轮廓)。注意,如果不是为了网格对齐,你无需知道这个框子的这个值,只
需知道它的大小即可。但为了正确渲染一个对齐的字形,需要保存每个字形在基线上转换、放置的重要
对齐。
内部 leading。这个概念从传统印刷业而来,他表示字形出了 EM 正方形空间数量,通常计算如下:internal
leading = ascent – descent – EM_size
外部 leading。行距的别名。
1.3.3 跨距(bearing)和步进
每个字形都有叫跨距和步进的距离,它们的定义是常量,但是它们的值依赖布局,同样的字形可以用来
渲染横向或纵向文字。
左跨距或 bearingX。从当前笔位置到字形左 bbox 边界的水平距离,对水平布局是正数,对垂直布局大多
是负值。
上跨距或 bearingY。从基线到 bbox 上边界的垂直距离,对水平布局是正值,对垂直布局是负值。
步进宽度或 advanceX。当处理文本渲染一个字形后,笔位置必须增加(从左向右)或减少(从右向左)
的水平距离。对水平布局总是正值,垂直布局为 null。
步进高度或 advanceY。当每个字形渲染后,笔位置必须减少的垂直距离。对水平布局为 null,对垂直布
局总是正值。
字形宽度。字形的水平长度。对未缩放的字体坐标,它是 bbox.xMax-bbox.xMin,对已缩放字形,它的计
算要看特定情况,视乎不同的网格对齐而定。
字形高度。字形的垂直长度。对未缩放的字体坐标,它是 bbox.yMax-bbox.yMin,对已缩放字形,它的计
算要看特定情况,视乎不同的网格对齐而定。
右跨距。只用于水平布局,描述从 bbox 右边到步进宽度的距离,通常是一个非负值。
advance_width – left_side_bearing – (xMax-xMin)
下图是水平布局所有的度量
下图是垂直布局的度量:
1.3.4 网格对齐的效果
因为 hinting 将字形的控制点对齐到象素网格,这个过程将稍稍修改字符映象的尺寸,和简单的缩放有所
区别。例如,小写字母 m 的映象在主网格中有时是一个正方形,但是为了使它在小象素大小情况下可以辨别,
hinting 试图扩大它已缩放轮廓,以让它三条腿区分开来,这将导致一个更大的字符位图。
字形度量也会受网格对齐过程的影响:
映象的宽度和高度改变了,即使只是一个象素,对于小象素大小字形区别都很大;
映象的边界框改变了,也改变了跨距;
步进必须更改,例如如果被 hint 的位图比缩放的位图大时,必须增加步进宽度,来反映扩大的字形宽度。
这有一些含义如下,
因为 hinting,简单缩放字体上行或下行高度可能不会有正确的结果,一个可能的方法时保持被缩放上行
高度的顶和被缩放下行高度的底。
没有容易的方法去 hint 一个范围内字形并步进它们宽度,因为 hinting 对每个轮廓工作都不一样。唯一的
方法时单独 hint 每个字形,并记录返回值。有些格式,如 TrueType,包含一些表对一些通用字符预先计
算出它们的象素大小。
hinting 依赖最终字符宽度和高度的象素值,意味着它非常依赖分辨率,这个特性使得正确的所见即所得
布局非常难以实现。
在 FT 中,对字形轮廓处理 2D 变换很简单,但是对一个已 hint 的轮廓,需要注意专有地使用整型象素
距离(意味着 FT_Outline_Translate() 函数的参数应该都乘以 64,因为点坐标都是 26.6 固定浮点格式),否则,
变换将破坏 hinter 的工作,导致非常难看的位图。
1.3.5 文本宽度和边界框
如上所示,指定字形的原点对应基线上笔的位置,没有必要定位字形边界框的某个角,这不像多数典型
的位图字体格式。有些情况,原点可以在边界框的外边,有时,也可以在里边,这要看给定的字形外形了。
同样,字形的步进宽度是在布局时应用于笔位置的增量,而不是字形的宽度,那是字形边界的宽度。对
文本串,具有相同的规约,这意味着:
指定文本串的边界框没有必要包含文本光标,也不需要后边的字形放置在它的角上。
字符串的步进宽度和它的边界框大小无关,特别时它在开始和最后包含空格或 tab。
最后,附加的处理如间距调整能够创建文本串,它的大小不直接依赖单独字形度量并列排列。例如,VA
的步进宽度不是 V 和 A 各自的步进之和。
1.4 字距调整
字距调整这个术语指用来在一个文本串中调整重合字形的相对位置的特定信息。
1.4.1 字距调整对
字距调整包括根据相邻字形的轮廓修改它们之间的距离。例如 T 和 y 可以贴得更近一点,因为 y 的上缘
正好在 T 的右上角一横的下边。
当仅仅根据字形的标准宽度来布局文本,一些连续的字符看上去有点太挤和太松,有的字体外观包含一
个表,它包含文本布局所需的指定字形对的字距距离。
这个对是顺序的,AV 对的距离和 VA 对不一定一致;
依据布局或书写,字距可以表示水平或垂直方向。
字距表示成网格单元,它们通常是 X 轴方向的,意味着负值表示两个字形需要在水平方向放的更近一点。
1.4.2 应用字距调整
在渲染文本时应用字据调整是一个比较简单的过程,只需要在写下一个字形时,将缩放的字距加到笔位
置即可。然而,正确的渲染器要考虑的更细一点。
“滑动点”问题是一个很好的例子:很多字体外观包括一个大写字符(如 T、F)和一个点.之间的字距
调整,以将点正好放置在前者的主腿的右侧。
根据字符的外形,有时候需要在点和随后的字符间作附加的调整。一个方案是,只在需要时滑动点,当然这
需要对文本的意思有了解。如果当我们在渲染特定段落的最后一个点时,上面的调整就不适合了。这只是一
个例子,还有很多其他例子显示一个真正的印刷工人需要恰当地布局文本。
有一个很简单地算法,可以避免滑动点问题。
1. 在基线上放置第一个字形;
2. 将笔位置保存到 pen1;
3. 根据第一个和第二个字形的字距距离调整笔位置;
4. 放置第二个字形,并计算下个笔位置,放到 pen2;
5. 如果 pen1 大于 pen2,使用 pen1 作为下个笔位置,否则使用 pen2。
1.5 文本处理
1.5.1 书写简单文本串
在第一个例子中,我们将生成一个简单的 Roman 文字串,即采用水平的自左向右布局,使用专有的象素度量,
这个过程如下:
1. 将字符串转换成一系列字形索引;
2. 将笔放置在光标位置;
3. 获得或装入字形映象;
4. 平移字形以使它的原点匹配笔位置;
5. 将字形渲染到目标设备;
6. 根据字形的步进象素增加笔位置;
7. 对剩余的字形进行第三步;
8. 当所有字形都处理了,在新的笔位置设置文本光标。
注意字距调整不在这个算法中。
1.5.2 子象素定位
在渲染文本时使用子象素定位有时很有用。这非常重要,例如为了提供半所见即所得的文本布局,文本渲
染的算法和上一节很相似,但是有些区别:
笔位置表示成小数形式的象素;
因为将一个已经 hint 过的轮廓平移一个非整型距离将破坏网格对齐,字形原点的位置在渲染字符映象前
必须取整;
步进宽度表示成小数形式的象素,没有必要是整型。
这里是算法的改进版本:
1. 将字符串转换成一系列字形索引;
2. 将笔放置在光标位置,这可以是一个非整型点;
3. 获得或装入字形映象;
4. 平移字形以使它的原点匹配取整后的笔位置;
5. 将字形渲染到目标设备;
6. 根据字形的步进象素宽度增加笔位置,这个宽度可以是小数形式;
7. 对剩余的字形进行第三步;
8. 当所有字形都处理了,在新的笔位置设置文本光标。
注意使用小数象素定位后,两个指定字符间的空间将不是固定的,它右先前的取整操作堆积的数决定。
1.5.3 简单字距调整
在基本文本渲染算法上增加字距调整非常简单,当一个字距调整对发现了,简单地在第 4 步前,将缩放后的
调整距离增加到笔位置即可。淡然,这个距离在算法 1 需要被取整,算法 2 不必要。
1.5.4 自右向左布局
布局 Arabic 或 Heberw 文字的过程非常相似,区别只是在字形渲染前,笔位置需要减少(记住步进宽度总是
正值)
1.5.5 垂直布局
布局垂直文字也是同样的过程,重要的区别如下:
基线是垂直的,使用垂直的度量而不是水平度量;
左跨距通常是负的,但字形原点必须在基线上;
步进高度总是正值,所以笔位置必须减少以从上至下书写;
1.6 FT 轮廓
1.6.1 FT 轮廓描述和结构
a. 轮廓曲线分解
一个轮廓是 2D 平面上一系列封闭的轮廓线。每个轮廓线由一系列线段和 Bezier 弧组成,根据文件格式不同,
曲线可以是二次和三次多项式,前者叫 quadratic 或 conic 弧,它们在 TrueType 格式中用到,后者叫 cubic 弧,
多数用于 Type1 格式。
每条弧由一系列起点、终点和控制点描述,轮廓的每个点有一个特定的标记,表示它用来描述一个线段
还是一条弧。这个标记可以有以下值:
FT_Curve_Tag_On 当点在曲线上,这对应线段和弧的起点和终点。其他标记叫做“Off”点,即它不在轮廓
线上,但是作为 Bezier 弧的控制点。
FT_Curve_Tag_Conic 一个 Off 点,控制一个 conic Bezier 弧
FT_Curve_Tag_Cubic 一个 Off 点,控制一个 cubic Bezier 弧
下面的规则应用于将轮廓点分解成线段和弧
两个相邻的“on”点表示一条线段;
一个 conic Off 点在两个 on 点之间表示一个 conic Bezier 弧,off 点是控制点,on 点是起点和终点;
两个相邻的 cubic off 点在两个 on 点之间表示一个 cubic Bezier 弧,它必须有两个 cubic 控制点和两个 on
点。
最后,两个相邻的 conic off 点强制??在它们正中间创建一个虚拟的 on 点。这大大方便定义连续的 conic
弧。TrueType 规范就是这么定义的。
注意,在单个轮廓线中可以混合使用 conic 和 cubic 弧,不过现在没有那种字体驱动产生这样的轮廓。
b. 轮廓描述符
FT 轮廓通过一个简单的结构描述
FT_Outline
n_points 轮廓中的点数
n_contours 轮廓中轮廓线数
points 点坐标数组
contours 轮廓线端点索引数组
tags 点标记数组
这里,points 是一个 FT_Vector 记录数组的指针,用来存储每个轮廓点的向量坐标。它表示为一个象素 1/64,
也叫做 26.6 固定浮点格式。
contours 是一组点索引,用来划定轮廓的轮廓线。例如,第一个轮廓线总是从 0 点开始,以 contours[0]点结
束。第二个轮廓线从 contours[0]+1 点开始,以 contours[1]结束,等等。
注意,每条轮廓线都是封闭的,n_points 应该和 contours[n_controus-1]+1 相同。最后,tags 是一组字节,用
来存放每个轮廓的点标记。
1.6.2 边界和控制框计算
边界框(bbox)是一个完全包含指定轮廓的矩形,所要的是最小的边界框。因为弧的定义,bezier 的控
制点无需包含在轮廓的边界框中。例如轮廓的上缘是一个 Bezier 弧,一个 off 点就位于 bbox 的上面。不过这
在字符轮廓中很少出现,因为大多字体设计者和创建工具都会在每个曲线拐点处放一个 on 点,这会使 hinting
更加容易。于是我们定义了控制框(cbox),它是一个包含轮廓所有点的最小矩形,很明显,它包含 bbox,
通常它们是一样的。不想 bbox,cbox 计算起来非常快。
控制框和边界框可以通过函数 FT_Outline_Get_CBox()和 FT_Outline_Get_BBox()自动计算,前者总是非
常快,后者在有外界控制点的情况下会慢一点,因为需要找到 conic 和 cubic 弧的末端,如果不是这种情况,
它和计算控制框一样快。