0.前言
因?yàn)橹皹I(yè)務(wù)線上有一個字體預(yù)覽的需求惨远,所以經(jīng)歷了一次自己從頭開始實(shí)現(xiàn)一個字體文件格式的解析器庆尘,實(shí)現(xiàn)的過程中差點(diǎn)沒把我頭給撓禿,以至于成功實(shí)現(xiàn)后還能感覺到頭頂挺涼快的。
因?yàn)閷?shí)現(xiàn)的不容易茫陆,所以后面有時間慢慢的整理一些筆記仗岖,給后面對字體格式有解析需求或者有興趣的人保留一些頭發(fā)(禿頂了你們怎么找女朋友逃延,禿頂?shù)氖虑榫徒唤o我了!)轧拄。
字體操作相關(guān)的庫在其它語言中是相當(dāng)豐富和完善的揽祥,比如Google開發(fā)的庫sfntly(支持Java和C++),并且這個庫在Chromium瀏覽器(Google對Chrome瀏覽器的開源實(shí)現(xiàn))中也用作字體相關(guān)的處理檩电。而因?yàn)槲覀儤I(yè)務(wù)線上的所使用的語言是PHP拄丰,而在PHP的生態(tài)下字體操作相關(guān)的庫也有府树,但是數(shù)量相對稀少,并且大部分的庫已經(jīng)常年沒有更新和進(jìn)行BUG修復(fù)料按。
1.字體格式
了解一種格式必然是要先了解這個格式的規(guī)范定義奄侠,而目前市面上主流的字體格式為TrueType(.tff)和OpenType(.otf),而其中OpenType也可以叫做TrueType2.0载矿,除去OpenType所擴(kuò)展的一些特性外垄潮,基本上TrueType和OpenType的大部分定義是一樣的。
因?yàn)門rueType和OpenType這兩個格式都是由Apple和Micrososft一起開發(fā)闷盔,所以你能分別在Apple和Micrososft的網(wǎng)站上找到相關(guān)完整的格式說明文檔:
Apple關(guān)于TrueType格式的參考文檔
Micrososft關(guān)于OpenType格式的參考文檔
為什么要貼兩份文檔的地址呢弯洗,因?yàn)楫?dāng)你發(fā)現(xiàn)其中一個文檔描述不清楚或者看的不是太懂的時候,可以換另一份作為參考看看逢勾,是的牡整,我實(shí)現(xiàn)過程中就經(jīng)常這樣做(畢竟上學(xué)期間英文考試只能靠選擇題來得分)。
2.SFNT包裝格式概述
一般來說使用SFNT包裝格式這個文件就是otf或者ttf的字體格式溺拱,但是Apple的平臺上會有所區(qū)分逃贝,因?yàn)樵贗OS或者OS X上會使用這個格式來包裝其它類型的字體茸时,而關(guān)于這個的描述Apple的文檔內(nèi)也會有提到:
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字體實(shí)際上都是有很多張表所組成渣锦,每張表都會負(fù)責(zé)記錄一些和特定功能相關(guān)的數(shù)據(jù),比如cmap表記錄一種類型的字符編碼和字體形狀對應(yīng)的關(guān)系织鲸。而SFNT則是一種組織這些表數(shù)據(jù)以及可擴(kuò)展的格式攒菠。
我們可以以一個ttf格式的字體文件為例子迫皱,來展示一下SFNT包裝格式的大概樣子:
上圖就是SFNT的基本結(jié)構(gòu),而字體格式內(nèi)的數(shù)據(jù)都使用大端存儲辖众,所以上圖上所說的uint32實(shí)際上就是無符號大端32位卓起,現(xiàn)在我們首先來解釋一下頭部字段的作用:
類型 | 名稱 | 描述 |
---|---|---|
uint32 | sfntVersion | 字體格式類型和版本 |
uint16 | numTables | 這個字體文件內(nèi)有多少張表 |
uint16 | searchRange | 用于優(yōu)化搜索查找參考值 |
uint16 | entrySelector | 用于優(yōu)化搜索查找參考值 |
uint16 | rangeShift | 用于優(yōu)化搜索查找參考值 |
因?yàn)橐话銇碚f一個字體所包含的表數(shù)量不會特別夸張,所以我們依靠numTables這個值來線性遍歷讀取即可凹炸,所以后面的參數(shù)可以只解析出來但不用關(guān)心戏阅。而解析的流程實(shí)際上就是按照上圖列出的結(jié)構(gòu)順序讀取即可,而用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' => []
];
當(dāng)讀取完這個頭部后啤它,就得到了這個表結(jié)構(gòu)內(nèi)一共有多少張表奕筐,這個時候在遍歷所有表頭部結(jié)構(gòu),也就是這個結(jié)構(gòu)體:
類型 | 名稱 | 描述 |
---|---|---|
uint32 | tableTag | 表名稱变骡,不足4字節(jié)用空格補(bǔ)充离赫,可直接轉(zhuǎn)為ASCII得到表英文字符名稱 |
uint32 | checkSum | 表數(shù)據(jù)的校驗(yàn)和 |
uint32 | offset | 這個表數(shù)據(jù)位于這個文件內(nèi)的哪個位置 |
uint32 | length | 這個表數(shù)據(jù)的長度 |
這個結(jié)構(gòu)的讀取次數(shù)取決于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']);
}
我們先把所有表頭全部讀取出來塌碌,因?yàn)楸眍^包含了我們需要的每個表在文件內(nèi)的偏移位置以及長度渊胸,當(dāng)我們把表頭全部讀取出來以后,就可以用fseek函數(shù)設(shè)置表頭讀取出來的offset台妆,這樣下次fread讀取的時候就在相關(guān)表所在的位置了翎猛,然后我們再根據(jù)表頭的length字段讀取指定的長度的內(nèi)容胖翰,這樣就把每個表的數(shù)據(jù)都讀取了出來,方便我們后續(xù)針對各個表在進(jìn)行單獨(dú)的表內(nèi)容解析切厘,這樣就完成了SFNT包裝格式的解析萨咳。
這個時候你再回顧之前所看到的SFNT包裝結(jié)構(gòu)的圖例,結(jié)合代碼就應(yīng)該能大概理解這個格式了疫稿。
當(dāng)然培他,每個表都是有對應(yīng)的作用的,有一些表并不是必須的但有一些表是必須的遗座,下面列出TrueType(OpenType基本一致)所必須包含的表:
Tag | 描述 |
---|---|
cmap | 多種字符編碼對應(yīng)到字體形狀的映射表 |
glyf | 包含每個字體的形狀數(shù)據(jù) |
head | 字體頭部靶壮,包含一些設(shè)置參數(shù) |
hhea | horizontal header(不知道怎么翻譯,避免歧義直接用原文檔的術(shù)語) |
hmtx | horizontal metrics(不知道怎么翻譯员萍,避免歧義直接用原文檔的術(shù)語) |
loca | 記錄每個字體形狀存在于文件內(nèi)的哪個offset上 |
maxp | 記錄字體對于內(nèi)存上的一些需求參數(shù) |
name | 包含了人類可讀的相關(guān)名稱數(shù)據(jù),比如字體名稱等 |
post | PostScript相關(guān)數(shù)據(jù) |
以上就是必須包含的表拣度,其它表的種類因?yàn)樘嗔怂橐铮赃@里不一一列出來,感興趣的可以從開頭所貼出來的文檔中了解其它種類的表抗果。
3.總結(jié)
SFNT包裝結(jié)構(gòu)的解析相對來說還是特別簡單的筋帖,解析起來沒有太多的難度,而真正讓人頭禿的是對SFNT包裝格式里面包含的表數(shù)據(jù)進(jìn)行解析冤馏,比如CMAP表就有CMAP0 ~ CMAP14的規(guī)范定義(可參考文檔定義)日麸。而每個規(guī)范都是為一些特定的字符編碼提供支持,雖然常用的只有CMAP4逮光,但正確實(shí)現(xiàn)解析和生成CMAP4也夠你玩上一整天了代箭。
所以對于后面一些復(fù)雜的表的格式和解析,一篇文章不太容易一次性說明白涕刚,所以這里先整理一下SFNT包裝格式的解析嗡综,等后面有時間在慢慢的詳細(xì)整理字體內(nèi)一些關(guān)鍵表(CMAP、GLYF杜漠、LOCA等)的格式解析(懶癌晚期)极景。
因?yàn)樽煮w格式的一些相關(guān)細(xì)節(jié)還是特別多,所以如果有未提到或者說明不詳細(xì)的細(xì)節(jié)驾茴,我們可以多多交流盼樟!