第 18 篇 | LINSHIYI
上周发过这一篇,但是今天重读的时候发现里面有一些英文翻译的错误 加上之前和朋友聊到这篇文章的时候得到了一些建议,所以干脆重修一遍,顺便更新了一下模板下面是修改后的内容:日常我们在电脑、手机这类输出设备上看到的字符其实是字形,也就是字的形状。但是计算机是无法识别这些形状的,它只能识别由0和1组成的二进制信息。那么我们在屏幕上见到的各类字体是如何显示出来的?字形又是如何存储在计算机中的?这就是本文所讲的问题简单来说,本文旨在说明我们在屏幕上看到的字符是如何在计算机中进行存储和显示的。字体文件有很多,本文只针对TrueType Font文件(后缀为.ttf的字体文件)进行解析。共有6个部分,前两部分是背景简介,主要介绍了矢量字体和位图字体的概念以及Type1,TrueType和OpenType字体的发展史,第三和第四部分对字体文件中轮廓线和字形度量的概念进行了详细介绍。第五和第六部分以标准楷体(simkai.ttf)中的字符“马”为例,对TTF的文件结构和字形存储方式进行了解析。在看这篇之前,可以先点击查看姐妹文SVG里都有什么,该文第一部分“矢量图和位图”的概念和本文第一部分“矢量字体和位图字体”的概念很相似,推荐搭配食用 首先,介绍一下字体(Font)和字形(Glyph)的概念。字形是指字符的外观形态,字体是具有同样外观样式和尺寸的字形集合。举例来说,黑体、宋体、楷体属于不同的字体,同一种字体中的“你”、“我”、“A”等字符具有不同的字形。在微软官方文档中,字体技术被分为了四种:Raster, Vector, TrueType和Microsoft OpenType。这些不同字体之间的差异反映了字形的不同存储方式:In raster fonts, a glyph is a bitmap that the system uses to draw a single character or symbol in the font.
栅格字体(Raster Font)也被称为位图字体(Bitmap Font)或点阵字体。在这种字体中,字形都以一组二维像素信息来表示,我们可以把栅格字体看作一幅由许多像素点组成的图片。由于字形以像素点构成,所以栅格字体只有在特定的分辨率下才能被清晰地输出,当字体被强行放大时会出现“锯齿”,类似于位图的失真现象。In vector fonts, a glyph is a collection of line endpoints that define the line segments that the system uses to draw a character or symbol in the font.
In TrueType and OpenType fonts, a glyph is a collection of line and curve commands as well as a collection of hints.
矢量字体(Vector Font)也叫轮廓字体(Outline Font),它的字形由一系列端点来定义,这些端点可以将字形分割为若干条线段,有些是直线,有些是曲线。目前主流的矢量字体有3种:Type1,TrueType和OpenType,这三种字体使用一系列直线和曲线命令来存储字形,具体示例在第二节展示。由于矢量字体不使用像素描述字形,所以它不受设备分辨率的影响。矢量字体在屏幕或打印机上输出时总会和设备的分辨率保持一致,所以无论字体缩小或放大都不会出现变形或变色的现象,这一点和矢量图形的原理相同。由于矢量字体在缩放方面的优越性,目前大多数系统采用的字体都为矢量字体。下图分别是矢量字体和栅格字体的“马”,从图中我们能很明显地看出两种字体之间的不同:先介绍两个概念,第一个是页面描述语言(Page Description Language, PDL),它是一种用面向输出效应的语言,主要用于描述版面内容。页面描述语言既可以处理文字、也可以处理图形和图像。第二个是PostScript,它是一种可以表示出各类矢量图形的页面描述语言和编程语言,多应用于电子产业和桌面出版领域。PostScript是目前最著名、应用最广的页面描述语言,我们常见的EPS(Encapsulated PostScript)文件就是PostScript的一种延展类型。
1985年,Adobe公司在推出PostScript的同时推出了Type1字体,Type1字体使用图形描述的方法来描述字形,每一个字形都由一系列PostScript语句组成。举例来说,如果我们要画一条直线,就需要使用以下代码:
1newpath
20 0 moveto
30 5 lineto
4stroke
5showpage
其中,newpath命令代表初始化当前画笔,x y moveto是将画笔移动到(x,y)的命令,x y lineto是以(x,y)为终点进行画线的命令,stroke代表将构建的路径绘制到当前页面,showpage可以将当前页面打印出来。1991年,Apple公司和Microsoft公司共同推出了另一种字体:TTF(TrueType Font),它也使用了图形描述的方法描述字形。一直以来,Type1和TrueType两种字体互不兼容,直到1997年OpenType1.0的出现才打破了这种状态。OTF(OpenType Font)是Adobe和Microsoft联合研发的字体格式,它兼容Type1和TrueType,可以被认为是这两种字体的超集。OpenType支持Unicode字体,还支持更高级的印刷功能。OpenType的出现逐渐淘汰了Type1字体,Adobe已经宣布在今年(也就是2021年)取消Photoshop中对Type1的支持,并在2023年1月结束在所有Adobe产品中对该字体格式的支持。在Windows系统中的C:/Windows/Fonts/路径下能找到电脑中安装的字体,其中扩展名为.ttf的是TrueType字体,拓展名为.otf的是OpenType字体,有的字体扩展名为.ttc,全称TrueType Collection,它是TrueType字体的集成文件,也就是几个TTF字体文件合成的字库。TrueType和OpenType的更多介绍见微软官网:https://docs.microsoft.com/zh-cn/typography/opentype/spec/ttch01和https://docs.microsoft.com/zh-cn/typography/opentype/轮廓线,简单来说就是组成字形的闭合曲线。一个字符的字形由一条或多条轮廓线构成,每一条轮廓线都划定了字形的外部或内部区域。来看一个例子:使用标准楷体画出“马”的轮廓线,可以发现一共有两条:这里需要注意区分轮廓线和笔画的概念,矢量字体中的字形是按照闭合路径而非笔画来存储的,单看汉字的轮廓线非常像我们常见的空心字。构成轮廓线的曲线是贝塞尔曲线,贝塞尔曲线(Bézier Curve)又被称为贝兹曲线或贝济埃曲线,它是一种由两个定点和零至无数个控制点绘制的曲线,其中最常用的二次贝塞尔曲线有2个端点和1个控制点,三次贝塞尔曲线有2个端点和2个控制点。TrueType字体使用二次贝塞尔曲线(Quadratic Bézier Curve),Type1字体使用三次贝塞尔曲线(Cubic Bézier Curve)。上图从左至右分别是绘制一次、二次、三次和四次贝塞尔曲线的动画,来自 https://www.jasondavies.com/animated-bezier 这一节内容参考了苹果官网的TrueType参考手册 https://developer.apple.com/fonts/TrueType-Reference-Manual/和FreeType官网中的字形约定:https://www.freetype.org/freetype2/docs/glyphs/index.html字形度量(Glyph Metrics),顾名思义,指的是描述字形的一系列参数或指标。FreeType官网中这样描述字形度量:Each glyph image is associated with various metrics which describe how to place and manage it when rendering text. Metrics relate to glyph placement, cursor advances, as well as text layout.
简单来说,字形度量描述了字形的布局、长宽、边界坐标等指标,这些指标决定了计算机在呈现字符时如何放置和管理这些字形图像。这一节介绍常用的几种字形度量,这部分内容在第五节和第六节也会涉及到。4.1 Baseline, Origin和Layout基线(Baseline)是一条假想的线,用于在呈现文本时“引导”字形。它可以是水平的(例如拉丁文、阿拉伯文)或垂直的(例如中文、日文)。位于基线上的一个虚拟点被称为笔位置(Pen Position)或原点(Origin),用来定位字形。这样说可能有点抽象,来看一张FreeType官网给出的水平布局示例图片:可以看到,对于水平布局(Horizontal Layout)来说,字形只是被放置在基线上,通过从左至右或从右至左地增加笔位置来呈现文本。相邻两个笔位置之间水平距离被称为步进宽度(Advance Width),步进宽度由具体的字形来决定,从图中也可以发现不同字母的步进宽度是不同的。有关步进宽度的概念在4.3中会提到。下面是来自官网的一张垂直布局的示意图,对于垂直布局(Vertical Layout)来说,字形以基线为中心,基线两侧字形等宽:4.2 Ascent,Descent 和Bounding Box从基线到字体轮廓最高点的距离叫做Ascent,中文貌似最贴合的翻译叫做“上坡度”,其他合适的翻译实在是没有找到。从基线到字体轮廓最低点的距离叫做Descent,也可以叫做“下坡度”,一般来说Ascent是正值,Descent是负值,我画了一张示意图:Ascent,Descent和Baseline示例上图粉色的线就是基线,水平布局中基线沿着水平方向延伸。两条灰色的线分别是字体轮廓最高点和最低点的水平线。这两条水平线和基线的距离就是Ascent和Descent。边界框(Bounding Box)是一个虚拟框,它被用来尽可能紧密地围住字形,边界框有四个参数:xMin,yMin,xMax和yMax。在FreeType API中边界框也被简称为bbox。给刚刚的示例图做一个扩充,图中的蓝框就是每个字形的边界框:另外还有3个轴承(Bearings)的概念:首先是左侧轴承(Left Side Bearing),它指的是从当前笔位置到字形边界框左侧边缘的水平距离。大多情况下水平布局字形的左侧轴承是正数,垂直布局字形的左侧轴承是负数。左侧轴承也被称为X轴承(bearingX)或被简写为lsb。顶部轴承(Top Side Bearing)也被称为Y轴承(bearingY),它是指当前笔位置到字形边界框顶部的垂直距离,水平布局字形的顶部轴承通常是正数,垂直布局通常是负数。右侧轴承(Right Side Bearing)简称rsb,它仅用于水平布局,用于描述字形边界框右侧和步进宽度之间的距离,大多情况下为正数。我们在讲基线的时候就提到过,步进宽度(Advance Width)指的是两个相邻笔位置(Pen Position)之间的水平距离,对于水平布局来说,步进宽度总是正数,对于垂直布局而言总是零。步进宽度在FreeType API中也被称为advanceX。除了步进宽度,还有步进高度(Advance Height)的概念,它指的是两个相邻笔位置之间的垂直距离,也常被称为advanceY。对水平布局来说它总为零,对垂直布局来说总为正数。最后还有两个简单的度量:字形宽度(Glyph Width)和字形高度(Glyph Height)。我们用字母“y”做例子,看一下这几个概念:水平布局中的字形度量先从水平方向上来看,粉色的线是基线,基线上的两个实心小黑块是笔位置。相邻两个笔位置之间的水平距离就是步进宽度advanceX,从图中可以看出:左侧轴承lsb + 字符宽度Width + 右侧轴承rsb = 步进宽度advanceX。再从纵向上来看,Ascent + |Descent| = 字符高度Height,对于单个字形而言,顶部轴承和Ascent是一样的。图中的蓝色方框就是字形的边界框,边界框的四个参数xMin,xMax,yMin和yMax也用蓝色标注出来了。另外,图中箭头的方向也有含义,字体布局中默认向右和向上的方向为正,这和我们常见的坐标轴方向是一样的。所以上图中除去Descent为负值,其他度量都为正值,这也是|Descent|要加绝对值的原因前文说过,垂直布局的字形以基线为中心,基线两侧字形等宽。和水平布局相同,粉色的线是基线,黑色方块是笔位置。相邻两个笔位置之间的垂直距离是步进高度advanceY。
左侧轴承是当前笔位置到边界框左侧的距离,顶部轴承是到边界框最顶部的距离,而垂直布局中没有右侧轴承的概念。上图中的箭头方向也指示了度量值的正负,除了高度Height和宽度Width为正数,其他的度量都为负数。这一节将会介绍TrueType字体文件的结构。TrueType字体文件由一系列串联的表组成。第一个表是特殊表:字体目录( Font Directory),字体目录是字体文件内容的指南,它提供访问其他表中数据所需要的信息,通过字体目录可以访问字体中的其他表。字体目录后面是一系列包含字体数据的表--字体表(Font Table),字体表可以以任意顺序出现,其中有些表是必须表(Required Table),有些表是可选表(Optional Table)。字体表的表名(Tag)必须包含4个字符,并用引号括起来,如果表名不足4个字符需要用空格填补。下面是一个示例图,分别展示了simkai.ttf(标准楷体)和simhei.ttf(黑体)两种字体文件的结构。从图中可以看出,两个字体文件的第一个表都是字体目录(粉色框),接着是随机排列的表,包含必须表(绿色框)和可选表(蓝色框)。我们还可以发现'cvt '的表名由3个字母和1个空格组成,这是为了满足表名必须包含4个字符的要求。字体目录由两部分组成:偏移子表(Offset Subtable)和表目录(Table Directory)。偏移子表记录了字体中表的数量和偏移信息,这里的偏移信息指的是表在文件中起始位置相对于文件头的偏移量,偏移子表是为了提升访问目录表的速度。表目录跟在偏移子表之后,由一系列条目组成,这些条目必须按标签升序排列,字体文件中的每个表都必须有自己的表目录条目。我把上一张图中的标准楷体文件扩充了一下,增加了字体目录包含的两个文件,便于大家更清晰地理解字体文件的内部结构:介绍完了字体目录,接下来介绍字体表。由于字体表数量繁多,这里只对必须表进行简要介绍,想了解更多内容可以在苹果官网https://developer.apple.com/fonts/TrueType-Reference-Manual/查看。TrueType字体文件中的必须表有9张,我们分4部分进行介绍:5.1 'head'
'head'表全称Font Header,即字体头部,这个表包含了有关字体的全局信息。它记录了字体版本号,创建和修改日期,修订号以及适用于整个字体的基本印刷数据等内容。另外,’head’表中还存储了所有字形的总边界框数据,即xMin,xMax,yMin和yMax。5.2 'hhea'和'hmtx'
'hhea'全称是Horizontal Header(水平头部),它记录了水平布局字体所需的信息。'hmtx'全称是Horizontal Metrics(水平度量),它包含了字体中每个字符的水平布局度量信息。二者的区别在于,’hhea’表中存储的是字体整体的通用信息,'hmtx'表存储的是特定字符的信息。'hhea'以版本号(version)开头,包含ascent,descent,advanceWidthMax(最大步进宽度),minLeftSideBearing(最小左轴承),minRightSideBearing(最小右轴承)等信息。'hmtx'以hMetrics数组开始,数组中包含了每一个字符的度量,度量包括两个参数:advanceWidth(步进宽度)和leftSideBearing(左侧轴承)。5.3 'cmap', 'loca'和'glyf'
'cmap','loca'和'glyf'三张表之间有着映射关系,'glyf'表(Glyph Outline)是最大的一张表,包括了每个字形的轮廓、指令等数据,'cmap'表(Character Code Mapping)存储的是字符到字形的映射,'loca'表(Glyph Location)是位置索引表,它存储了各个字符相对于’glyf’表头的偏移量,'loca'表的存在是为了提升特定字符数据的快速访问速度。通常的字符映射方法为:由'cmap'表中的字符定位到'loca'表中,再由'loca'表中存储的字形偏移量定位到'glyf'表中的字形中。我画了一张“马”的字符映射示意图:上图使用标准楷体simkai.ttf为例,在'cmap'表中存储了字符“马”的Unicode编码”uni9A6C”和字形编号20642,'glyf'表中存储了“马”的字形数据,例如轮廓线条数,边界框坐标等等。'loca'表负责通过字形编号20642查找到字形“马”在'glyf'表中的具体位置。5.4 'maxp', 'name'和'post'
'maxp'全称Maximum Profile,包含字体中所需内存的分配情况。它以表格版本号(version)开头,紧接着是字体中的字形数量(numGlyphs),剩下的条目都是一些参数的最大值,例如简单字形的最大轮廓数(maxContours),复杂字形中的最大端点数(maxComponentPoints)等等。'name'包括版权说明、字体名、风格名等内容,全称就是Name。'post'表包含在PostScript打印机上使用TrueType字体时所需要的信息,全称为Glyph Name and PostScript Compatibility。这几张表和字形数据的存储没有太大关系,更多的是在存储整个字体文件的信息。最后,我们介绍'glyf'表的结构和内容。'glyf'表是最大的表,它定义了字体中的字形数据,包括字形轮廓的轮廓点、边界框以及一些字形指令。我们用XML的格式来看一下标准楷体文件是如何存储“马”的字形数据的:如何将TTF文件转换成XML文件会在下一篇文中讲到。在上面的图中,蓝色框代表'glyf'表,里面包含了所有字形的条目,一个字形对应一个绿色框。每一条字形条目中包括了字形名称,边界框坐标,轮廓线等参数。一个红色框代表一条轮廓线,轮廓线中记录了生成轮廓线的点的坐标x,y与控制命令on(或称为flag)。在第三节中我们提到过贝塞尔曲线的概念,二次贝塞尔曲线需要2个端点和1个控制点来定义,TrueType字体使用直线和二次贝塞尔曲线来绘制字形。这里的on/flag参数就是控制端点是否在曲线上的命令,如果flag=0,说明点在曲线上,如果flag=1,说明点不在曲线上。最后,我们把之前的所有内容串起来,解答一下最开始的问题:我们在屏幕上看到的字符是如何在计算机中进行存储和显示的?用一张图来解释:TTF文件以串联表的形式存储了字体的所有数据,例如版本号、字形轮廓坐标、指令等等,所有数据都以二进制的方式存储在电脑之中,例如'head'表在计算机中就是一串二进制数:
1from fontTools.ttLib import TTFont
2#载入字体文件
3font = TTFont("simkai.ttf")
4#'head'表的二进制形式
5bin(int(font.getTableData('head').hex(), 16))
6#'0b100000000000000000000000000000101000000101000111100001010110010011110100001011011010111110000111100111100111101010000000000001011000000010000000000000000000000000000000000000000101111000100011001100000101110000000000000000000000000000000000011000001011000110111010101010111111111111111010011111111110100010000000100001000000000001101110000000000000000000000000000001100000000000000001000000000000000010000000000000000'
所有的字形数据都被存放在了'glyf'表之中,以轮廓线上的点坐标和绘制指令进行存储,每个字符都以Unicode编码命名,例如“马”被命名为“uni9A6C”。计算机提取出'glyf'表中的点坐标并按照命令进行绘制,就成为了我们在屏幕上见到的各种字符。有关TTF的内容就介绍到这里,由于这一篇文侧重于介绍,所以不展示过多的代码。下一篇是“如何使用Python解析TTF文件”,主要通过代码解析TTF文件结构,并从'glyf'表中提取出字形进行复现,欢迎继续收看~END~