0.前言
因为之前业务线上有一个字体预览的需求,所以经历了一次自己从头开始实现一个字体文件格式的解析器,实现的过程中差点没把我头给挠秃,以至于成功实现后还能感觉到头顶挺凉快的。
因为实现的不容易,所以后面有时间慢慢的整理一些笔记,给后面对字体格式有解析需求或者有兴趣的人保留一些头发(秃顶了你们怎么找女朋友,秃顶的事情就交给我了!)。
字体操作相关的库在其它语言中是相当丰富和完善的,比如Google开发的库sfntly(支持Java和C++),并且这个库在Chromium浏览器(Google对Chrome浏览器的开源实现)中也用作字体相关的处理。而因为我们业务线上的所使用的语言是PHP,而在PHP的生态下字体操作相关的库也有,但是数量相对稀少,并且大部分的库已经常年没有更新和进行BUG修复。
1.字体格式
了解一种格式必然是要先了解这个格式的规范定义,而目前市面上主流的字体格式为TrueType(.tff)和OpenType(.otf),而其中OpenType也可以叫做TrueType2.0,除去OpenType所扩展的一些特性外,基本上TrueType和OpenType的大部分定义是一样的。
因为TrueType和OpenType这两个格式都是由Apple和Micrososft一起开发,所以你能分别在Apple和Micrososft的网站上找到相关完整的格式说明文档:
Apple关于TrueType格式的参考文档
Micrososft关于OpenType格式的参考文档
为什么要贴两份文档的地址呢,因为当你发现其中一个文档描述不清楚或者看的不是太懂的时候,可以换另一份作为参考看看,是的,我实现过程中就经常这样做(毕竟上学期间英文考试只能靠选择题来得分)。
2.SFNT包装格式概述
一般来说使用SFNT包装格式这个文件就是otf或者ttf的字体格式,但是Apple的平台上会有所区分,因为在IOS或者OS X上会使用这个格式来包装其它类型的字体,而关于这个的描述Apple的文档内也会有提到:
Apple makes a distinction between a "TrueType font" (which refers to a particular font outline definition technology) and an "sfnt-housed font," which refers to any font which uses the same packaging format as a TrueType font: that is, it uses the same directory structure and the same table format and meaning for any tables present
This is an important distinction, because Apple supports other varieties of sfnt-housed font on OS X and iOS, most notably bitmap only fonts and OpenType fonts. Informally, people often to any sfnt-housed font as a "TrueType font," but this is strictly speaking inaccurate.
OpenType和TrueType字体实际上都是有很多张表所组成,每张表都会负责记录一些和特定功能相关的数据,比如cmap表记录一种类型的字符编码和字体形状对应的关系。而SFNT则是一种组织这些表数据以及可扩展的格式。
我们可以以一个ttf格式的字体文件为例子,来展示一下SFNT包装格式的大概样子:
上图就是SFNT的基本结构,而字体格式内的数据都使用大端存储,所以上图上所说的uint32实际上就是无符号大端32位,现在我们首先来解释一下头部字段的作用:
类型 | 名称 | 描述 |
---|---|---|
uint32 | sfntVersion | 字体格式类型和版本 |
uint16 | numTables | 这个字体文件内有多少张表 |
uint16 | searchRange | 用于优化搜索查找参考值 |
uint16 | entrySelector | 用于优化搜索查找参考值 |
uint16 | rangeShift | 用于优化搜索查找参考值 |
因为一般来说一个字体所包含的表数量不会特别夸张,所以我们依靠numTables这个值来线性遍历读取即可,所以后面的参数可以只解析出来但不用关心。而解析的流程实际上就是按照上图列出的结构顺序读取即可,而用PHP代码解析这个SFNT头部部分就如下:
function read_uint32($fd)
{
$data = fread($fd, 4);
return unpack('NN', $data)['N'];
}
function read_uint16($fd)
{
$data = fread($fd, 2);
return unpack('nn', $data)['n'];
}
$fd = fopen('微软雅黑.ttf', 'r');
$sfnt = [
'sfntVersion' => read_uint32($fd),
'numTables' => read_uint16($fd),
'searchRange' => read_uint16($fd),
'entrySelector' => read_uint16($fd),
'rangeShift' => read_uint16($fd),
'tableHeaders' => [],
'tableData' => []
];
当读取完这个头部后,就得到了这个表结构内一共有多少张表,这个时候在遍历所有表头部结构,也就是这个结构体:
类型 | 名称 | 描述 |
---|---|---|
uint32 | tableTag | 表名称,不足4字节用空格补充,可直接转为ASCII得到表英文字符名称 |
uint32 | checkSum | 表数据的校验和 |
uint32 | offset | 这个表数据位于这个文件内的哪个位置 |
uint32 | length | 这个表数据的长度 |
这个结构的读取次数取决于SFNT头部中的numTables字段,PHP的解析代码如下:
for ($i = 0; $i < $sfnt['numTables']; $i++) {
$tableHeader = [
'tag' => read_uint32($fd),
'checkSum' => read_uint32($fd),
'offset' => read_uint32($fd),
'length' => read_uint32($fd),
];
$sfnt['tableHeaders'][$tableHeader['tag']] = $tableHeader;
}
foreach ($sfnt['tableHeaders'] as $tableTag => $tableHeader) {
fseek($fd, $tableHeader['offset'], SEEK_SET);
$sfnt[$tableTag] = fread($fd, $tableHeader['length']);
}
我们先把所有表头全部读取出来,因为表头包含了我们需要的每个表在文件内的偏移位置以及长度,当我们把表头全部读取出来以后,就可以用fseek函数设置表头读取出来的offset,这样下次fread读取的时候就在相关表所在的位置了,然后我们再根据表头的length字段读取指定的长度的内容,这样就把每个表的数据都读取了出来,方便我们后续针对各个表在进行单独的表内容解析,这样就完成了SFNT包装格式的解析。
这个时候你再回顾之前所看到的SFNT包装结构的图例,结合代码就应该能大概理解这个格式了。
当然,每个表都是有对应的作用的,有一些表并不是必须的但有一些表是必须的,下面列出TrueType(OpenType基本一致)所必须包含的表:
Tag | 描述 |
---|---|
cmap | 多种字符编码对应到字体形状的映射表 |
glyf | 包含每个字体的形状数据 |
head | 字体头部,包含一些设置参数 |
hhea | horizontal header(不知道怎么翻译,避免歧义直接用原文档的术语) |
hmtx | horizontal metrics(不知道怎么翻译,避免歧义直接用原文档的术语) |
loca | 记录每个字体形状存在于文件内的哪个offset上 |
maxp | 记录字体对于内存上的一些需求参数 |
name | 包含了人类可读的相关名称数据,比如字体名称等 |
post | PostScript相关数据 |
以上就是必须包含的表,其它表的种类因为太多了,所以这里不一一列出来,感兴趣的可以从开头所贴出来的文档中了解其它种类的表。
3.总结
SFNT包装结构的解析相对来说还是特别简单的,解析起来没有太多的难度,而真正让人头秃的是对SFNT包装格式里面包含的表数据进行解析,比如CMAP表就有CMAP0 ~ CMAP14的规范定义(可参考文档定义)。而每个规范都是为一些特定的字符编码提供支持,虽然常用的只有CMAP4,但正确实现解析和生成CMAP4也够你玩上一整天了。
所以对于后面一些复杂的表的格式和解析,一篇文章不太容易一次性说明白,所以这里先整理一下SFNT包装格式的解析,等后面有时间在慢慢的详细整理字体内一些关键表(CMAP、GLYF、LOCA等)的格式解析(懒癌晚期)。
因为字体格式的一些相关细节还是特别多,所以如果有未提到或者说明不详细的细节,我们可以多多交流!