一文讀懂字體文件
這篇文章是從0到1自定義富文本渲染的原理篇之一,此外你還可能感興趣:
更多內容歡迎關注公眾號:非專業程序員Ping
- 一文讀懂字符與編碼
- 一文讀懂字符、字形、字體
- 一文讀懂字體文件
- 從0到1自定義文字排版引擎:原理篇
- 逆向分析CoreText中的字體級聯/Font Fallback機制
- 新手小白也能看懂的LLDB技巧/逆向技巧
- 深入理解iOS CoreText API
一、引言
在開始閱讀本文之前,推薦先閱讀字符(Character)、字形(Glyph)、字體的區別理解基本概念。
如果你對字符與Unicode的相關概念還不理解,推薦閱讀字符與編碼
前文,我們介紹了字符(Character)、字形(Glyph)、字體的區別,這里我們再來實際分析一個字體文件中到底有什么,這有利于我們后續理解文字排版引擎的工作原理和流程。
macOS上系統字體路徑一般為/System/Library/Fonts/,可以看到有文件后綴有ttc和ttf,二者有什么區別呢?
1).ttf (TrueType Font)
ttf表示這是一個單字體文件,每個 .ttf 文件通常只對應一個字體樣式(例如 Microsoft YaHei Regular)
2).ttc (TrueType Collection)
ttc表示這是一個字體集合文件,內部可以包含多個 TrueType 字體(多個 .ttf 打包在一起),這些字體通常共享某些表(比如 glyph 輪廓、cmap),減少冗余,提高存儲效率,常用于一個 Typeface 的多個變體(Regular, Bold, Italic, Light…)
上面提到了TrueType,與之對應的還有OpenType,二者其實都是字體類型標準,簡單理解就是OpenType是TrueType的擴展,OpenType支持更多的特性,比如:連字、RTL、上下標等。
OpenType一般以otf為后綴,但也不能簡單的根據文件名后綴區分二者,文件擴展名只是習慣,并不能完全說明內部格式,真正的區別還是要看字體表結構,比如OpenType有GSUB、GPOS、GDEF等擴展表。
下面,我們來真正解析一個字體文件,看里面有什么,可以通過如下命令行將字體解析成XML。
# 對于ttf文件
ttx NewYork.ttf
# ttc文件是個字體集合,需要明確指明要提取哪個index的字體
ttx -y 0 Times.ttc
二、Font文件解析
我們以NewYork.ttf文件為例,如下是NewYork.ttf中的表

2.1 GlyphOrder
<GlyphOrder>
<GlyphID id="0" name=".notdef"/>
<GlyphID id="1" name=".null"/>
<GlyphID id="2" name="nonmarkingreturn"/>
<GlyphID id="3" name="space"/>
<GlyphID id="4" name="A"/>
...
</GlyphOrder>
GlyphOrder定義glyphID與glyphName的映射。
2.2 head
Font Header,存儲一些全局信息;關注幾個值:
<head>
<unitsPerEm value="2048"/>
...
</head>
1)unitsPerEm
字體表里的數值一般都很大(見后文),其單位并不是像素值,而是 em unit,<unitsPerEm value="2048"/>表示2048 units = 1 em = 設計的字高,比如當字體在屏幕上以 16px 渲染時,1 em = 16px,其他數值可按比例換算
2.3 hhea
Horizontal Typesetting Header,橫向排版信息,關注幾個值
<hhea>
<!-- MacOS一般使用hhea里的ascent、descent;OS_2表里還有幾個ascent、descent,一般在Windows或專業設計上使用 -->
<ascent value="1950"/>
<descent value="-494"/>
<lineGap value="0"/>
<advanceWidthMax value="2818"/>
<minLeftSideBearing value="-693"/>
<minRightSideBearing value="-693"/>
...
</hhea>
1)ascent & descent
假設字體大小16,unitsPerEm如上為2048,則按比例換算:ascent = 1950/2048 * 16 ≈ 15.2,descent ≈ 494/2048 * 16 ≈ 3.8。
需要注意,OS_2表中也有ascent、descent的定義,這是因為不同平臺會讀取不同表中的ascent、descent,比如macOS、iOS一般使用hhea中的值,Windows一般使用OS_2表中的usWinAscent、usWinDescent,專業排版軟件(如InDesign)一般用OS_2表中的sTypoAscender、sTypoDescender。
Q:對于同一個Font,ascent、descent的值是固定的嗎?
這個問題的答案需要加定語,對于同一個Font,在同一個平臺上,ascent、descent是固定的。
Q:為什么descent值是負數?
可以理解成規范,TrueType/OpenType的規范里,descent是負數,表示基線(baseline)以下延伸的高度。
2.4 maxp
<maxp>
<numGlyphs value="1811"/>
...
</maxp>
定義字體里 glyph 的數量,以及一些最大值參數。
2.5 OS_2
<OS_2>
<!-- 下標的大小和偏移 -->
<ySubscriptXSize value="650"/>
<ySubscriptYSize value="600"/>
<ySubscriptXOffset value="0"/>
<ySubscriptYOffset value="75"/>
<!-- 上標的大小和偏移 -->
<ySuperscriptXSize value="650"/>
<ySuperscriptYSize value="600"/>
<ySuperscriptXOffset value="0"/>
<ySuperscriptYOffset value="350"/>
<!-- 刪除線的粗細和垂直位置 -->
<yStrikeoutSize value="12"/>
<yStrikeoutPosition value="620"/>
<!--
ulUnicodeRange表示字體支持的Unicode范圍,用ulUnicodeRange1 … ulUnicodeRange4 這 4 個 32 位字段來表示,總共 128 個 bit,對應 128 個 Unicode Block,如果某 bit = 1,表示字體支持該區塊中的至少一些字符,
映射表見:https://learn.microsoft.com/en-us/typography/opentype/spec/os2#ur
-->
<ulUnicodeRange1 value="10100001 00000000 00000010 11111111"/>
<ulUnicodeRange2 value="00000010 00000000 00100000 01011110"/>
<ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
<ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
<!-- 專業排版(比如 InDesign)一般使用sTypoAscender、sTypoDescender -->
<sTypoAscender value="1950"/>
<sTypoDescender value="-494"/>
<sTypoLineGap value="0"/>
<!-- Windows一般使用sTypoAscender、sTypoDescender -->
<usWinAscent value="1950"/>
<usWinDescent value="494"/>
...
</OS_2>
參見Apple文檔,關注幾個值:
1)ySubscriptXSize & ySubscriptYSize & ySubscriptXOffset & ySubscriptYOffset
下標的大小和偏移
2)ySuperscriptXSize & ySuperscriptYSize & ySuperscriptXOffset & ySuperscriptYOffset
上標的大小和偏移
3)yStrikeoutSize & yStrikeoutPosition
刪除線的粗細和垂直位置
4)ulUnicodeRange1 & ulUnicodeRange2 & ulUnicodeRange3 & ulUnicodeRange4
ulUnicodeRange表示該字體支持的Unicode范圍,用ulUnicodeRange1 … ulUnicodeRange4 這 4 個 32 位字段來表示,總共 128 個 bit,對應 128 個 Unicode Block,如果某 bit = 1,表示字體支持該區塊中的至少一些字符,映射表見:https://learn.microsoft.com/en-us/typography/opentype/spec/os2#ur 。
Windows系統通常用 ulUnicodeRange 來看一個字體是否支持某Unicode;macOS/iOS系統一般用 cmap 表(精確的字符映射),ulUnicodeRange只作為輔助信息;瀏覽器排版一般直接查 cmap,但 ulUnicodeRange 有時也用于字體 fallback 策略。
5)sTypoAscender & sTypoDescender & usWinAscent & usWinDescent
如前文所述,不同系統會取不同的值作為ascent、descent
2.6 hmtx
<hmtx>
<mtx name=".notdef" width="2048" lsb="199"/>
<mtx name=".null" width="0" lsb="0"/>
<mtx name="A" width="1244" lsb="-16"/>
...
</hmtx>
Horizontal Metrics,記錄每個 glyph 的 advance width 和left side bearing。
簡單理解排版引擎繪制字形的流程是:將字形放在當前點 + lsb 偏移位置進行繪制,畫完后,將光標向右移動 advanceWidth,準備繪制下一個字形。
2.7 cmap
<cmap>
<tableVersion version="0"/>
<cmap_format_4 platformID="0" platEncID="3" language="0">
<!-- A的Unicode code point是0x41 -->
<map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
<map code="0x42" name="B"/><!-- LATIN CAPITAL LETTER B -->
<map code="0x43" name="C"/><!-- LATIN CAPITAL LETTER C -->
<map code="0x44" name="D"/><!-- LATIN CAPITAL LETTER D -->
<map code="0x45" name="E"/><!-- LATIN CAPITAL LETTER E -->
<map code="0x46" name="F"/><!-- LATIN CAPITAL LETTER F -->
<map code="0x47" name="G"/><!-- LATIN CAPITAL LETTER G -->
<map code="0x48" name="H"/><!-- LATIN CAPITAL LETTER H -->
...
</cmap_format_4>
...
</cmap>
Character to Glyph Mapping,定義 Unicode code point → glyph ID 的映射,cmap表中能精確的查到該Font支持哪些Unicode。
2.8 glyf
<glyf>
<TTGlyph name="A" xMin="-16" yMin="0" xMax="1260" yMax="1444">
<contour>
<pt x="1086" y="213" on="1"/>
<pt x="1113" y="137" on="0"/>
<pt x="1161" y="50" on="0"/>
<pt x="1219" y="9" on="0"/>
<pt x="1260" y="1" on="1"/>
<pt x="1260" y="0" on="1"/>
<pt x="793" y="0" on="1"/>
<pt x="793" y="1" on="1"/>
<pt x="845" y="7" on="0"/>
<pt x="897" y="54" on="0"/>
<pt x="899" y="143" on="0"/>
<pt x="874" y="213" on="1"/>
<pt x="528" y="1200" on="1"/>
<pt x="528" y="1200" on="1"/>
<pt x="220" y="292" on="1"/>
<pt x="184" y="186" on="0"/>
<pt x="170" y="66" on="0"/>
<pt x="224" y="11" on="0"/>
<pt x="290" y="1" on="1"/>
<pt x="290" y="0" on="1"/>
<pt x="-16" y="0" on="1"/>
<pt x="-16" y="1" on="1"/>
<pt x="27" y="9" on="0"/>
<pt x="89" y="59" on="0"/>
<pt x="151" y="181" on="0"/>
<pt x="193" y="297" on="1"/>
<pt x="614" y="1444" on="1"/>
<pt x="648" y="1444" on="1"/>
</contour>
<contour>
<pt x="290" y="532" on="1"/>
<pt x="294" y="544" on="1"/>
<pt x="859" y="544" on="1"/>
<pt x="860" y="532" on="1"/>
</contour>
<instructions/>
</TTGlyph>
...
</glyf>
Glyph Data,真正的字形輪廓(矢量點、輪廓、控制點);cmap 表負責把 Unicode 字符映射到 glyphID,而 glyf 表告訴渲染系統該 glyph 的具體形狀。
2.9 name
<name>
<namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
? 2017-2024 Apple Inc. All rights reserved.
</namerecord>
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
.New York
</namerecord>
<namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
Regular
</namerecord>
<namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
.New York; 20.0d1e1; 2024-05-06
</namerecord>
<namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
.New York
</namerecord>
<namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
20.0d1e1
</namerecord>
<namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
.NewYork-Regular
</namerecord>
...
</name>
name表中定義的是字體名稱、字體家族、PostScript Name、廠商信息等。
nameID對應的含義如下:
| nameID | 含義 |
|---|---|
| 0 | Copyright notice |
| 1 | Font Family name(字體家族名,比如 New York) |
| 2 | Font Subfamily name(字重/樣式,比如 Regular、Bold) |
| 3 | Unique font identifier(唯一ID,通常包含廠商名+版本號) |
| 4 | Full font name(family + subfamily,比如 New York Regular) |
| 5 | Version string |
| 6 | PostScript name(唯一的、無空格的名字) |
| ... | ... |
這里需要重點介紹下PostScript Name:PostScript Name是字體在一個系統里的唯一標識,是單個字符串,不允許有空格,一般是 FamilyName-StyleName 形式,比如:.NewYork-Regular,Helvetica-Bold,NotoSansCJKsc-Regular等。
在CoreText的API里,一般都要求傳PostScript Name,比如:CTFontCreateWithName
2.10 GDEF
Glyph Definition Table,簡單理解GDEF表就是是GPOS / GSUB的輔助表,比如GPOS和 GSUB需要知道「哪些字形是 mark、哪些能連接、哪些有變體」等信息,這些元數據就是放在GDEF 表里的。
2.11 GPOS
Glyph Positioning Table,控制字形的相對位置(如kerning、上下標等),比如「A + V」之間要減少間距,或者音標放在元音正上方等。
2.12 GSUB
Glyph Substitution Table,控制字形替換(連字、、阿拉伯文變體、直角引號換彎引號等),比如f + i → ?,'quoteleft' → ‘等。
2.13 HVAR & MVAR & avar & fvar & gvar...
這幾個表是用于轉換可變字體的,可變字體不在本文范圍內,不再詳述。
更多精彩內容歡迎關注??公眾號:非專業程序員Ping
posted on 2025-10-21 23:17 非專業程序員Ping 閱讀(211) 評論(0) 收藏 舉報
浙公網安備 33010602011771號