FontForge Spline Font Database(SFD)格式完全指南:从文件头到逐字形数据

原创2026-09-26 23:59:231,021 阅读
文章标签:桌面应用图形学

FontForge Spline Font Database(SFD)格式完全指南:从文件头到逐字形数据

SFD(Spline Font Database)是 FontForge 的原生字体工程文件格式,以纯 ASCII 文本完整描述字体的轮廓、提示、排印布局(GPOS/GSUB)、位图点阵与元数据。本指南以 doc/sphinx/techref/sfdformat.rst 为骨架,结合 fontforge/sfd.cpp 的读写实现与 tests/fonts 中的真实测试字体,逐段拆解 SFD 的完整语法:文件头关键字、轮廓字符数据、位图字体、CID/MM 变体、.sfdir 目录式存储与自动保存(.asfd)格式。读完本文,你将能够读懂并手工编写 SFD 文件、排查损坏格式,并理解 FontForge 内部如何序列化与解析这一格式。

SFD 格式概览:设计目标与文件结构

SFD 格式随 FontForge 的功能演进不断变化(其变更历史见 doc/sphinx/techref/sfdchangelog.rst),但当前解析器(fontforge/sfd.cpp)尽量兼容所有旧版本文件。格式设计有几个关键特征:

  • 纯 ASCII 文本:便于在互联网上直接复制传输,且 diff 结果有实际意义(适合版本控制)。
  • 有 IANA 注册的 MIME 类型:application/vnd.font-fontforge-sfd(自 2008 年 5 月 14 日起注册)。
  • 结构模仿 BDF 文件:开头几行是字体级属性,接着是每个字符一个区块,最后是每个位图字体的区块。

一个完整 SFD 文件的顶层骨架如下:

SplineFontDB: 3.0          # 版本标识,必须是文件第一行
FontName: Ambrosia         # ……字体级属性(Font Header)……
...
BeginChars: 285 253        # 开始轮廓字符数据
StartChar: exclam
...
EndChar
EndChars                   # 轮廓字符数据结束
BitmapFont: 12 285 10 2 1  # 位图字体区段
...
EndBitmapFont
EndSplineFont              # 整个文件结束

第一个版本行必须位于文件第一行。其后文件基本上是"关键字 + 值"的键值对集合;在给定区段内,键值对的顺序大体无关紧要。

版本号:兼容性边界

SFD 版本号发生过两次关键提升(详情见 SFD 格式变更日志):

  • 版本 2(2007 年 3 月):FontForge 对 lookups 与 features 的处理发生了根本性重构,首次提升了 SFD 版本号;此前生成的文件最好参考旧版文档理解。
  • 版本 3(2008 年 2 月):允许字体拥有多个图层,这是一次不兼容的变更,因此版本号提升到 3。

从源码看,当前版本号的生成逻辑位于 fontforge/sfd.cpp 的 SFDDump:默认写出 3.2(若未启用 WritePNGInSFD 则降为 3.1,若未启用 UndoRedoLimitToSave 则进一步降为 3.0)。而解析端 SFDStartsCorrectly(fontforge/sfd.cpp)会严格校验版本号,只接受 0、1、2.0、3.0~3.21 区间以及 4.0(4.x 尚未生成,但解析代码已预留对超长行反斜杠续行的支持),否则报 Bad SFD Version number 并拒绝加载。这意味着你手工编写 SFD 时应使用 3.0~3.2 之间的版本号。

跨文件粘贴的警告

文档特别警告:把某个 SFD 文件的片段剪切粘贴到另一个文件通常可行,但有三类内容必须避免:

  1. 带 TrueType 指令的字形:指令可能调用旧字体中定义的子程序,而在新字体中不存在,栅格化(grid fit)时可能发生意外,甚至导致系统崩溃。
  2. 替换类条目(连字、字距、上下文链式等):替换条目内部引用 lookup-subtable 的名称,若目标字体缺少该 subtable,大概率出问题。
  3. 带引用(references)的字形:引用通过字形索引(glyph index)实现,而索引在不同字体间通常不同,可能引用到错误的字形。

Font Header:字体级属性

以下是文档给出的文件头示例(原样继承):

SplineFontDB: 3.0
FontName: Ambrosia
FullName: Ambrosia
FamilyName: Ambrosia
DefaultBaseFilename: Ambrosia-1.0
Weight: Medium
Copyright: Copyright (C) 1995-2000 by George Williams
Comments: This is a funny font.
UComments: "This is a funny font."
FontLog: "Create Jan 2008"
Version: 001.000
ItalicAngle: 0
UnderlinePosition: -133
UnderlineWidth: 20
Ascent: 800
Descent: 200
sfntRevision: 0x00078106
WidthSeparation: 140
LayerCount: 4
Layer: 0 0 "Back" 1
Layer: 1 1 "Fore" 0
Layer: 2 0 "Cubic_Fore" 0
Layer: 3 0 "Test" 1
DisplaySize: -24
DisplayLayer: 1
AntiAlias: 1
WinInfo: 64 16 4
FitToEm: 1
UseUniqueID: 0
UseXUID: 1
XUID: 3 18 21
Encoding: unicode
Order2: 1
OnlyBitmaps: 0
MacStyle: 0
TeXData: 1 10485760 0 269484 134742 89828 526385 1048576 89828
CreationTime: 1151539072
ModificationTime: 11516487392
GaspTable 3 8 2 16 1 65535 3 0
DEI: 91125
ExtremaBound: 30

其中大部分条目一目了然(不明白时可对照 字体信息对话框),少数条目需要专门解释。

注释类关键字

  • Comments:已弃用。一串 ASCII 字符。
  • UComments:字体注释的新格式,UTF-7 编码的字符串。
  • FontLog:UTF-7 编码的字符串。

度量与版本相关

  • TeXData:TeX 字体参数(及若干相近信息)。第一个数字为 1、2 或 3,分别表示 text、math、math ext 字体;第二个数字是设计点尺寸(乘以 (1<<20));其后是字体参数。这些值通常采用 TeX fix_word 格式——二进制小数点位于第 20 个二进制位之后,即数值需除以 (1<<20) 才是真实值。
  • sfntRevision:sfnt 的 head 表修订号字段,以十六进制存储为 16.16 定点数(32 位数字,二进制小数点在第 16 位二进制位之后)。
  • WidthSeparation:用户不可见的内部信息,记录 AutoWidth 命令最近一次使用的字形间距值;也用作 AutoKern 中间距的默认值。
  • CreationTime / ModificationTime:自 1970 年 1 月 1 日 00:00:00 起的秒数(标准 Unix 时间戳)。若文件中没有这两项,读取时会回退为文件自身的修改时间(见 fontforge/sfd.cpp 的 SFTimesFromFile)。

图层相关

  • LayerCount:字体的图层数量,至少为 2。

  • Layer:每层一条记录,用于命名并描述其样条类型。语法为:

    Layer <layer-number> <quadratic-flag> <name> [<background-flag>]
    

    例如 Layer: 1 1 "Fore" 0 表示:第 1 层、二次(quadratic)样条、名为 "Fore"、不是背景层;Layer: 2 0 "Cubic_Fore" 0 表示:第 2 层、非二次样条(即三次 cubic)、名为 "Cubic_Fore"、也不是背景层。行尾可选的背景标志是 2008 年 8 月新增的(见 变更日志),旧版本 FontForge 会忽略它。注意旧关键字 Order2 在多层时代已被移除。

  • DisplaySize:fontview 中默认显示字体所用的每 em 像素数(可被用户更改)。负数表示从轮廓栅格化显示,正数表示使用该尺寸的预构建位图字体。

  • DisplayLayer:打开字体时默认显示的图层。

  • AntiAlias:fontview 是否以抗锯齿方式(而非黑白)显示字体;抗锯齿观感更好但更慢。

  • FitToEm:控制显示该字体的 fontview 中 "Fit to Em" 是否默认勾选。

  • WinInfo:包含三个数据,描述含该字体的窗口的默认显示:第一个数据表示窗口应滚动到编码 64 处的字形可见;第二个表示窗口水平方向应有 16 个字符列;第三个表示垂直方向应有 4 行字符。

编码与 CID

  • Encoding:普通字体取值为 Encoding 下拉列表中出现的名字(或其近似)。CID 键控字体没有编码,取而代之的是:

    Registry: Adobe
    Ordering: japan1
    Supplement: 4
    CIDVersion: 1.2
    

其它文件头条目

  • GaspTable:关键字后的第一个数字给出该行中 ppem/flag 对的数目;接下来两个数字是第一个 ppem 与第一个 flag;最后一个数字是 gasp 表版本(2010 年 9 月新增,见 变更日志)。
  • UseXUID:Adobe 现已声明 XUID 已弃用;若该标志置位,FontForge 仍会为 PostScript 字体生成 XUID 条目。
  • DEI:文档原文称"太难解释,可安全忽略"(涉及 CalTech OddHack 委员会 1980 年 1 月 15 日的会议纪要)。
  • ExtremaBound:Adobe 规定短样条允许存在内部极值,大样条则不允许,但并未定义"多大算大"。该值让用户指定这个阈值:端点间距离超过该数字的样条才会被检查极值。

WOFF 文件专用条目

woffMajor: 7
woffMinor: 504
woffMetadata: "<?xml version+AD0AIgAA-1.0+ACIA encoding ..."
  • woffMajor / woffMinor:写入 woff 文件的主/次版本号。
  • woffMetadata:woff 文件的元数据,以 UTF-7 存储(未解析的 XML)。

UFO 文件专用条目

UFOAscent: 697
UFODescent: -154
  • UFOAscent:UFO 字体 fontinfo.plist 中 ascender 字段的值。
  • UFODescent:descender 字段的值。

TrueType 信息

部分字体还包含 TrueType 信息(这些字段通常位于 OS/2、hhea 或 vhea 表中,含义参见 TrueType 规范):

FSType: 4
PFMFamily: 17
TTFWeight: 400
TTFWidth: 5
Panose: 2 0 5 3 0 0 0 0 0 0
LineGap: 252
OS2LineGap: 252
VLineGap: 0
OS2Vendor: 'PfEd'
OS2FamilyClass: 2050
OS2Version: 4
OS2_WeightWidthSlopeOnly: 1
OS2_UseTypoMetrics: 1
OS2CodePages: 6000009f.9fd70000
OS2UnicodeRanges: 800002ef.50002049.00000000.00000000

来自 OS/2 与 hhea 表、但稍复杂的条目采用配对关键字方式存储:HheadAscent 与 HheadAOffset 成对工作。若 offset 关键字为 1(true),则另一关键字被视为相对值——FontForge 先计算它认为正确的值,再加上指定值;若 offset 为 0(false),则该值被原样使用。

HheadAscent: 0
HheadAOffset: 1
HheadDescent: 0
HheadDOffset: 1
OS2TypoAscent: 0
OS2TypoAOffset: 1
OS2TypoDescent: 0
OS2TypoDOffset: 1
OS2WinAscent: 0
OS2WinAOffset: 1
OS2WinDescent: 0
OS2WinDOffset: 1

这些成对条目代表了存储在 TrueType 文件不同位置(Horizontal header 与 OS/2 表)的多种 ascent/descent 定义。OS/2 表还包含上下标与删除线的位置信息:

OS2SubXSize: 1351
OS2SubYSize: 1228
OS2SubXOff: 0
OS2SubYOff: -446
OS2SupXSize: 1351
OS2SupYSize: 1228
OS2SupXOff: 0
OS2SupYOff: 595
OS2StrikeYSize: 143
OS2StrikeYPos: 614

MacStyle 字段(若存在)指示字体是否为粗体、斜体、压缩、扩展等。PostScript 字体还可能包含 Private 字典中的信息(值前有表示字符串表示所需字符数的整数,能略微加快读取,但不够美观):

BeginPrivate: 1
BlueValues 23 [-19 0 502 517 750 768]
EndPrivate

附加数据:Python 与 Lookup

某些字体可能包含 Python 数据:

PickledData: "I3
."

这是任意 Python pickle 数据(protocol=0),由 Python 脚本设置,FontForge 将其作为字符串存储;若字符串内含双引号或反斜杠,则它们前面会被加上反斜杠转义。

若字体包含任何 lookups:

Lookup: 6 0 0 "calt"  {"calt-1"  } ['calt' ('DFLT' <'dflt' > 'latn' <'dflt' > ) ]
Lookup: 1 0 0 "'smcp' Lowercase to Small Capitals in Latin lookup 0"  {"'smcp' Lowercase to Small Capitals in Latin lookup 0"  } ['smcp' ('latn' <'dflt' > ) ]
Lookup: 4 0 1 "'liga' Standard Ligatures in Latin lookup 1"  {"'liga' Standard Ligatures in Latin lookup 1"  } ['liga' ('latn' <'dflt' > ) ]
Lookup: 258 0 0 "'kern' Horizontal Kerning in Latin lookup 0"  {"'kern' Horizontal Kerning in Latin lookup 0" [150,0,0]  } ['kern' ('latn' <'dflt' > ) ]

Lookup 列表的每个条目都以 Lookup: 关键字开头,后跟 lookup 类型、flags 与 save-in-afm 标志;花括号内是该 lookup 的所有 subtable 名称列表;方括号内是所有 feature 的列表,每个 feature 后跟(圆括号内)所有 script 的列表,每个 script 后跟(尖括号内)所有 language 的列表。lookup flags 现在是 32 位数字,低 16 位是传统 flags,高 16 位是 mark attachment set 索引(如有)。

  • GSUB 单个替换 subtable 名称后可能跟一对圆括号,内含该 subtable 默认后缀的 UTF-7 字符串。
  • 字距 subtable 名称后可跟 (1) 表示垂直字距,或跟一对内含三个数字的方括号——这三个数字表示该 subtable 中自动字距的默认值:第一个是字形间期望间距,第二个是生成字距对所需的最小(绝对值)值(按 1 em 单位字距没有意义,若 autokern 得出该值则浪费时间),第三个是一组位标志:若为奇数表示间距基于最近接近(touching);若 bit 2 置位,则 autokerning 只生成负(更近)的字距值;若 bit 4 置位,则完全不进行自动字距。

lookup 的应用顺序即文件中列出的顺序,subtable 的应用顺序即其中列出的顺序。

Kerning 类(KernClass2)

若字体包含字距类:

KernClass2: 31 64 2 "'kern' Horizontal Kerning in Latin lookup 0"
 1 F
 41 L Lacute glyph78 Lcommaaccent Ldot Lslash
 1 P
 ...
 6 hyphen
 5 space
 ...
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -152 -195 -152 -225 0 0 0 0 0 0 0 0 0 0 0 0 0 -145 -145 -130
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -130 0 0 0 0 0 0 0 0 0 0 -145 0 -115 0 0 0 0 -65 0 -140 -120 -120
 ...
  • 第一行:第一个字符有 31 个类,第二个字符有 64 个类;它位于名为 'kern' Horizontal Kerning in Latin lookup 0 的 lookup subtable 中。
  • 下一行:第一个字符的类 1(类编号从 1 开始,类 0 保留且通常不定义)只含一个字符 F(行首数字是该行字符串长度,仅用于加速 SFD 处理,无语义内容)。
  • 类 2 含更多字符、字符串长度更长。第一字符的 30 个类条目之后,开始第二字符的类,格式与第一字符完全相同。
  • 所有第二字符类定义完后,是一个 <char1 class cnt> * <char2 class cnt> 个数字的数组,指定左右字符各类别之间应放置的字距量(例如 char1 在左类 2、char2 在右类 4,则用 3*<char2 class cnt> + 4 索引该数组)。

某些情况下可指定第一个字形的类 0(但不能指定第二个字形的类 0),此时第一字形类计数后会出现一个加号,随后第一组名称就是类 0。字距偏移数组中还可能散布着设备表(见下文 设备表):

...
0 {} 0 {} 0 {} ...
-145 {12-13 -1,1} -145 {} -130 {8-9 -1,-1} ...

Mark 附加类与集合

若字体包含 GDEF Mark attachment 类或集合:

MarkAttachClasses: 2
"ABClass" 3 A B
MarkAttachSets: 2
"ABSet" 3 A B
"ASet" 1 A

上例中有(大约)2 个 mark attachment 类,但类 0 恒为空且不列出,所以实际上只有一个类。它名为 ABClass(名称是 FontForge 概念,不导出到 OpenType),长度为 3 个字符,内容为 A B。同理,这里有 2 个 mark attachment 集合:集合 0 被使用且必须指定;集合 0 名为 ABSet、3 个字符、内容 A B;集合 1 名为 ASet、1 个字符、内容 A。

Baseline(BASE 表)数据

BaseHoriz: 3 'hang' 'ideo' 'romn'
BaseScript: 'cyrl' 2  1405 -288 0
BaseScript: 'grek' 2  1405 -288 0
BaseScript: 'latn' 2  1405 -288 0 { 'dflt' -576 1913} { 'ENG ' -576 1482} { 'VIT ' -578 2150}

BaseHoriz(或 BaseVert)行指出该轴(水平或垂直)激活了多少条及哪些基线。每个 script 有一条 BaseScript 行:其后的第一个数字指出该 script 的默认基线;后续数字指示其它基线相对于默认基线如何配置;语言特定信息放在 {} 对中(feature 特定信息会放在语言特定花括号内层)。

JSTF(justification)数据

Justify: 'arab'
JstfExtender: afii57440 afii5739
Justify: 'latn'
JstfLang: 'dflt' 5
JstfPrio:
JstfMaxShrink: "JSTF shrinkage max at priority 0 #0 for dflt in latn"
JstfMaxExtend: "JSTF extension max at priority 0 #1 for dflt in latn"
JstfPrio:
JstfEnableShrink: "'mark' Mark Positioning in Latin lookup 5"  "'kern' Horizontal Kerning in Latin lookup 6"  "'kern' Horizontal Kerning in Cyrillic lookup 7"
JstfPrio:
JstfEnableShrink: "'liga' Standard Ligatures in Latin lookup 10"  "'alig' Ancient Ligatures in Latin lookup 11"  "'liga' Standard Ligatures in Latin lookup 12"
JstfDisableExtend: "'liga' Standard Ligatures in Latin lookup 10"  "'alig' Ancient Ligatures in Latin lookup 11"  "'liga' Standard Ligatures in Latin lookup 12"
JstfPrio:
JstfMaxShrink: "JSTF shrinkage max at priority 3 #2 for dflt in latn"
JstfMaxExtend: "JSTF extension max at priority 3 #3 for dflt in latn"
JstfPrio:
JstfMaxShrink: "JSTF shrinkage max at priority 4 #4 for dflt in latn"
JstfMaxExtend: "JSTF extension max at priority 4 #5 for dflt in latn"
Justify: 'cyrl'
JstfLang: 'dflt' 1
JstfPrio:
JstfMaxShrink: "JSTF shrinkage max at priority 0 #6 for dflt in cyrl"
JstfMaxExtend: "JSTF extension max at priority 0 #7 for dflt in cyrl"
EndJustify

一块 justification 信息以 Justify: 关键字开始,后跟 script 标签;可为多个 script 提供多条 Justify:,但最后一块必须用 EndJustify 关键字终止。块内可用 JstfExtender: 关键字后跟字形名列表来指定扩展字形(kashidas)。script 内的每个语言以 JstfLang: 关键字开始,后跟语言标签与优先级级别计数。每个优先级级别以 JstfPrio: 开始,其后可出现以下关键字中的任意一个:JstfEnableShrink、JstfDisableShrink、JstfMaxShrink、JstfEnableExtend、JstfDisableExtend、JstfMaxExtend,每个关键字后跟一个 lookup 名称列表。

TrueType 表(fpgm/prep、cvt/maxp、未知表)

若字体包含 ttf hinting,文件中可能含有 TrueType 表,具体存储格式取决于表类型。含 TrueType 指令的表(fpgm、prep):

TtTable: prep
PUSHW_1
 640
NPUSHB
 255
 251
 254
 3
 ...

第一行指出这是 prep 表,后续行提供该表的指令,采用 FontForge 使用的格式存储;字节码与指令名之间的转换表见 fontforge/ttfinstrs.c。

cvt 与 maxp 表则采用短整数列表格式:

ShortTable: cvt  255
  309 "Big stem width, vertical"
  184 "Small stem width, vertical"
  203 "Stem width, horizontal"
  203
 ...

第一行将其标识为 cvt 表并指出将有 255 个数字(注意:不是 255 字节,而是 2*255 字节)。在 cvt 表(而非 maxp)中,每个数字后还可带注释,提醒人们每个 cvt 条目的用途,完全可选。

FontForge 也会存储它不理解的表,以未解释的二进制形式用 ASCII85Encode 编码打包:

TtfTable: LILY 4360
5S;o3()It?eJ8r@H[HSJH[H^@!b&BQ*?Vcm@'XSh+1MACZ>Up/\,o1+Ca't2!<ocH+Wn2p"@,t&
+Wo+[()Is6G8:u7D/^7,*,KO/(E=N5!=s)LCMjn,:Mp3:DSL&j05dG#cY`hLCN"!<CBO$@s(_ZX
...

第一行说明 LILY 表长 4360 字节;后续行提供这 4360 字节的 ASCII85Encode 编码数据(打包方式描述见下文 ASCII85Encode 编码 一节)。

名称表:LangName 与 OtfFeatName

LangName 条目表示 TrueType name 表:数字代表语言,后跟一组 UTF-7 编码的字符串。第一个字符串对应 ID=0(Copyright),第二个对应 ID=1(Family),依此类推;末尾的空字符串会被省略。在美国英语(1033)区段中,若某个名字与等价的 PostScript 条目完全一致,则该名字会被省略(便于处理更新,用户只需在一处修改版权):

LangName: 1033 "" "" "Regular" "GWW:Caliban Regular: Version 1.0" "" "Version 1.0"
LangName: 1032 "" "" "+A5oDsQ09A78DvQ05A7oDrAAA"

OpenType 1.6 起,允许为 feature 名 ss01~ss20 提供 name 表条目:

OtfFeatName: 'ss01'  1036 "Riable"  1033 "Risible"

它将 feature ss01 绑定到法语(语言 1036)名称 "Riable" 与英语(语言 1033)名称 "Risible"。

Anchor 类

AnchorClass2: "top" "Latin marks-1" "bottom" "Latin marks-1" "Anchor-2" "Latin marks-1" "Anchor-3" "Latin marks-2" "Anchor-4" "Latin marks-2" "Anchor-5" "Latin marks-2" "Anchor-6" "Latin marks-2"

存在一个名为 top 的 Anchor 类,位于 lookup subtable Latin marks-1 中;下一个类是 bottom,然后是 Anchor-2,依此类推(Anchor 类名以 UTF-7 输出)。

上下文与上下文链式(contextual chaining)lookups

上下文/链式 lookups 也存储在字体头部,由以下关键字之一引入:ContextPos2、ContextSub2、ChainPos2、ChainSub2、ReverseChain2,并以 EndFPST 结束。链式(chaining)lookup 可检查前序字形(称为 backtracking)、当前字形与后视字形(lookahead);而 Contextual lookup 只检查一串当前字形。共有四种格式:

1. 按 coverage 表(By coverage tables)

ChainSub2: coverage "calt-1" 0 0 0 1
 1 1 0
  Coverage: 7 uni0C40
  BCoverage: 8 glyph388
 1
  SeqLookup: 0 "high"
EndFPST

它定义了一个位于 lookup subtable calt-1 的简单上下文链式替换:未定义类、未定义 backtracking 类、未定义 lookahead 类(按 coverage 表),有 1 条规则。规则的第一行 1 1 0 表示 normal 列表有 1 张 coverage 表、backtrack 列表有 1 张、lookahead 列表有 0 张。随后是一个 normal coverage 表(描述单个字形 uni0C40)、一个 backtracking coverage 表(定义 glyph388)。最后是一个序列 lookup:在 normal 位置 0,应应用名为 high 的替换。即:若发现字形流 "glyph388 uni0C40",则该规则匹配,应将 lookup "high" 应用于 uni0C40。

2. 按类(By classes)

ChainSub2: class "calt-1" 3 3 0 1
  Class: 52 b o v w b.high o.high v.high w.high r.alt.high r.alt
  Class: 43 a c d e f g h i j k l m n p q r s t u x y z
  BClass: 52 b o v w b.high o.high v.high w.high r.alt.high r.alt
  BClass: 43 a c d e f g h i j k l m n p q r s t u x y z
 1 1 0
  ClsList: 2
  BClsList: 1
  FClsList:
 1
  SeqLookup: 0 "high"
EndFPST

第一行格式同上:normal 匹配有 3 个类、backtracking 匹配有 3 个类、1 条规则。接下来 4 行定义类。与按类字距一样,类 0 无需显式定义——它隐式定义为"未在其它类中定义的任何字形"。这里定义类 1 为 b,o,v,...,类 2 为 a,c,d,e,...,然后定义 backtracking 类(此处恰好与 normal 类相同,但并非总是如此)。这条规则说:若 normal 类 2 中的字形跟在 backtracking 类 1 中的字形之后(即出现类似 "ba" 或 "oc" 的序列),则应用 lookup "high"。

3. 按字形(By glyphs)

ChainSub2: glyph "calt-1" 0 0 0 1
 String: 1 D
 BString: 3 c b
 FString: 1 e
 1
 SeqLookup: 0 "high"
EndFPST

同样只有一条规则:若得到字形序列 "c b D e",则对字形 "D" 应用替换 "high"。

4. 按反向 coverage 表(By reverse coverage tables)

ChainSub2: revcov "calt-1" 0 0 0 1
 1 1 0
  Coverage: 7 uni0C40
  BCoverage: 8 glyph388
 1
  Replace: 11 uni0C40.alt
EndFPST

表示当 glyph388 位于 uni0C40 之前时,uni0C40 应被替换为 uni0C40.alt。

Apple 状态机与 Mac 特性名

可能存在 Apple 状态机,由 MacIndic2、MacContext2、MacInsert2、MacKern2 等关键字引入,并以 EndASM 终止:

MacContext2: "calt-1" 16384 9 5
  Class: 320 yehhamzaabovearabic beharabic teharabic theharabic jeemarabic haharabic khaharabic seenarabic sheenarabic sadarabic dadarabic taharabic zaharabic ainarabic ghainarabic feharabic qafarabic kafarabic lamarabic meemarabic noonarabic heharabic alefmaksuraarabic yeharabic peharabic tteharabic tcheharabic veharabic gafarabic
  Class: 227 noonghunnaarabic alefmaddaabovearabic alefhamzaabovearabic wawhamzaabovearabic alefhamzabelowarabic alefarabic tehmarbutaarabic dalarabic thalarabic reharabic zainarabic wawarabic ddalarabic rreharabic jeharabic yehbarreearabic
  Class: 201 shaddakasraarabic shaddakasratanarabic shaddafathaarabic shaddadammaarabic shaddadammatanarabic fathatanarabic dammatanarabic kasratanarabic fathaarabic dammaarabic kasraarabic shaddaarabic sukunarabic
  Class: 13 tatweelarabic
  Class: 17 ttehinitialarabic
 0 0 ~ ~
 0 0 ~ ~
 0 0 ~ ~
 0 0 ~ ~
 2 32768 ~ ~
 ...
 3 32768 "high" "low"
 ...
EndASM

状态机以一行开始,定义调用它的 lookup subtable、一些 mac flags、类数目与状态数。Mac 上前四个状态是预定义的,所以从类 4(yehhamzaabovearabic...)开始。最后有 <类数>*<状态数> 行描述转移:从状态 0、类 0 的转移开始,然后是状态 0、类 1,依此类推。每个转移包含下一状态与一组 flags;根据状态机类型还可能有其它参数。

  • MacIndic2:该格式没有额外参数。

  • MacContext2:该格式可能包含两个 lookup 替换的名称,一个应用于标记字形,一个应用于当前字形(特殊替换 ~ 表示"什么都不做",用作占位符)。

  • MacInsert2:包含两个字形列表,每个前面有表示其后字节数的数字:

    2 0 0 3 a b
    0 32768 4 fi q 0
    

    第一行表示不应在标记字形处插入字符,但应在当前字形处插入 "a" 与 "b";第二行表示在标记字形处插入 "fi" 与 "q",当前字形处不插入。flags 决定字符是在字形前还是后插入。

  • MacKern2:包含字距偏移列表。先是计数域说明后跟多少数字,然后是一列数字,调整此前已压入字距栈的字形的字距。

还可能存在 Mac Feature/Setting 名称列表:

MacFeat: 0 0 0
MacName: 0 0 24 "All Typographic Features"
MacSetting: 0
MacName: 0 0 12 "All Features"
MacFeat: 1 0 0
MacName: 0 0 9 "Ligatures"
MacSetting: 0
MacName: 0 0 18 "Required Ligatures"
MacSetting: 2
MacName: 0 0 16 "Common Ligatures"
MacFeat: 2 1 2

Grid 层与 MATH 表

字体顶部附近可能有 Grid 条目,指定在字体的网格层绘制的样条(格式见下文 样条集格式):

Grid
678 -168 m 5
 -40 -168 l 5
-678 729 m 1
 1452 729 l 1
-678 525 m 1
 1452 525 l 1
EndSplineSet

若字体包含 MATH 表,会看到类似行:

MATH:ScriptPercentScaleDown: 80
MATH:ScriptScriptPercentScaleDown: 60

每个存在于 math 表中的常量都有一条对应条目,名称与(英文)MATH Info 对话框中的名称一致。大多数 math 常量还可以指定设备表:

MATH:SubscriptShiftDown: 483 {12-17 1,0,0,0,1,1}

轮廓字符数据:BeginChars 与字形结构

对非 CID 字体,接下来应看到:

BeginChars: 285 253

表示该字体编码可容纳 285 个字符,共定义了 253 个字形(通常控制字符未定义)。文档特别强调:大多数编码对编码槽的数量与使用方式有特定约束(哪个字形放哪里、什么顺序)——这正是"编码"的含义。例如 UnicodeBmp 编码必须有至少 65536 个槽(编号 0~65535,对应 U+0000~U+FFFF);有 Unicode 码点的字形必须按码点顺序编入这些槽;没有码点的字形必须从 65536 开始连续编号编入附加槽。与编码不一致、或与实际字形数不一致的 BeginChars: 行就不是一个合法的 SFD 文件——FontForge 可能(但不保证)将非法文件视为致命错误,也可能重排/重编号字形使其符合编码要求。若其它编码的要求不适用,可考虑使用约束较少的 "Custom" 或 "Original" 编码。从源码看,写入端在 fontforge/sfd.cpp 处输出 BeginChars: <编码容量> <字形数> 与 EndChars,读取端 SFD_GetFont 中 BeginChars: 分支(fontforge/sfd.cpp)负责据此校验与分配字形槽位。

单个字符的结构

一个字符(glyph)的典型样子:

StartChar: exclam
Encoding: 33 33 3
Width: 258
Flags:
HStem: 736 13<39 155>  -14 88<162 180>
VStem: 71 84<49 396>
DStem2: 510 435 225 423 0.568682 -0.822558<0 124.816>
Fore
SplineSet
195 742 m 0
 195 738 193 736 189 736 c 0
 175 736 155 743 155 682 c 0
 155 661 130 249 130 131 c 0
 130 100 96 99 96 131 c 0
 96 149 71 662 71 682 c 0
 71 731 51 736 37 736 c 0
 33 736 31 738 31 742 c 0
 31 748 36 747 38 749 c 1
 188 749 l 1
 190 747 195 748 195 742 c 0
80 32 m 0
 81 53 95 75 116 74 c 0
 137 73 150 53 150 32 c 0
 150 10 137 -14 115 -14 c 0
 93 -14 79 10 80 32 c 0
EndSplineSet
EndChar
  • StartChar:命名该字符。若使用带 UTF-8 名称(而非 ASCII 名称)的非标准字形名称表,则名称以 UTF-7 编码并放入引号。
  • Encoding:依次给出当前字体中的编码、Unicode 编码、原始位置(GID)。
  • Width:前进宽度(advance width)。
  • Flags:目前有五个标志:H(该字符自上次加提示以来被修改过)、M(提示被人为调整过)、W(宽度被显式设置)、O(上次保存时字符处于打开状态)、I(字符指令已过期)。
  • HStem / VStem:水平与垂直(PostScript)茎干提示:若干组两个数字,组内第一个数字是茎干位置、第二个是茎干宽度;尖括号 <> 内的数字指示提示生效范围。
  • DStem2(对角茎干):更复杂,共 6 个数字、3 组两两配对。第一对表示提示左侧的一个点,第二对表示右侧的一个点,第三对是提示方向的单位向量;同样有尖括号内的生效范围。

带垂直度量的字体还可能有:

VWidth: 1000

指定垂直前进宽度。

SplineSet 样条集格式

Fore 条目开始前景样条(Back 则开始背景样条),它们以 PostScript 命令编码:moveto 缩写为 m、curveto 缩写为 c、lineto 缩写为 l(小写 el)。字母后的数字是一组标志位,其位含义如下:

  • 0x3:指示点是曲线点(0)、角点(1)还是切点(2)。
  • 0x4:点被选中。
  • 0x8:点有默认的下一控制点。
  • 0x10:点有默认的上一控制点。
  • 0x20:点在 x 方向应取整(TrueType 提示,实际不太有效)。
  • 0x40:点在 y 方向应取整(TrueType 提示,实际不太有效)。
  • 0x80:点是从两个控制点间插值得到的(从 ttf 文件读取时),因此没有自己的点号。
  • 0x100:点绝不应被插值。
  • 0x200:前一条样条上的任何极值都被标记为对验证器可接受。

TrueType 字体的样条在标志后还会有两个额外数字,即该点本身及其后续控制点的 TrueType 点号;若值为 -1 则表示该点没有编号。

进行提示替换(hint substitution)的字体,在发生提示替换的任意点后会有提示掩码(hint mask):

459 422 m 1xc0
 339 442 l 1xa0
 312 243 l 1

前两个点有提示掩码 xc0 与 xa0;掩码可以(几乎)任意长,取决于字形中的提示数量。xc0 表示前两个提示激活(0x80 & 0x40);xa0 表示第一个和第三个激活(0x80 和 0x20)。

Spiro 控制点

若使用过 Spiro 包(Raph Levien 的贝塞尔样条替代方案),可能还有一组 Spiro 控制点,出现在 SplineSet 列表中每个轮廓之后;可能有些轮廓有 spiros,有些没有。下面是 Inconsolata 字体的 'a' 字形转换成的 SFD 中的相关片段:

Fore
SplineSet
115 467 m 1
 134.212 486.845 157.062 503.125 182 515 c 0
 ...
 115 467 l 1
  Spiro
    115 467 v
    182 515 o
    310 541 o
    514 328 [
    514 0 v
    435 0 v
    435 58 v
    246 -13 o
    68 119 o
    274 307 o
    417 312 [
    434 312 v
    434 331 ]
    418 420 o
    304 473 o
    211 453 o
    156 414 v
    0 0 z
  EndSpiro
437 248 m 1
 ...
  Spiro
    437 248 v
    418 248 ]
    283 246 o
    153 123 o
    254 51 o
    366 84 ]
    437 213 [
    0 0 z
  EndSpiro
EndSplineSet

Spiro 数据遵循 Raph 的 "Plate file" 惯例。每个控制点有一个位置 (x,y) 和一个点类型,点类型为:

  • { —— 只能出现在第一个控制点,表示轮廓是开放的;
  • v —— 角点(corner point);
  • o —— G4 曲线点;
  • c —— G2 曲线点;
  • [ —— 左点(left point);
  • ] —— 右点(right point)。

最后一个 spiro 应为 z 类型;它不是轮廓的一部分,仅标记轮廓结束。(与 Raph 惯例略有出入:他的 plate 文件坐标系不同,且最后的 z 不带坐标。)若字形应在 spiro 模式下打开(显示 spiro 控制点而非贝塞尔控制点),会有 InSpiro 条目:

InSpiro: 1
Flags: HO
Fore

只有引用的字符

字符可以不包含任何样条:

StartChar: semicolon
Encoding: 59 59
Width: 264
Flags:
HStem:
VStem:
Fore
Refer: 33 44 N 1 0 0 1 0 0 1
Refer: 35 46 N 1 0 0 1 0 414 2
EndChar

上面这个分号只是画一个逗号再在其上方叠加一个句点。Refer: 行中第一个数字是被引用字符的字形索引(当然指当前字体中),第二个数字是 Unicode 码点,N 表示该引用未被选中(S 表示被选中),其后 6 个数字是 PostScript 变换矩阵——逗号(unicode 44)的是单位矩阵,句点(unicode 46)的只是垂直平移 414 单位。最后一个数字是 TrueType 标志集合:

  • 1 => 使用我的度量(Use My Metrics)
  • 2 => 对齐到网格(Round to Grid)
  • 4 => 通过点匹配定位引用(而非偏移量);若设置此标志,会有两个额外数字:第一个指示基础字形中的点号,第二个指示当前引用中的点号:
Ref: 33 44 N 1 0 0 1 0 0 1
Ref: 35 46 N 1 0 0 1 0 414 6 3 7 O

第二行表示点 3 与点 7 将被放在一起。可选结尾的 O 是标志,表示该信息已过期。

背景样条集类似,由 Back 条目引入,也可有 spiros:

Back
SplineSet
195 742 m 0
 195 738 193 736 189 736 c 0
 175 736 155 743 155 682 c 0
 ...
Refer 33 44 N 1 0 0 1 0 0 1

背景图像与 RLE 压缩

背景图像以如下"可怕"的格式存储:

StartChar: A
...
Back
Image: 167 301 0 21 2 1 23 753 2.53892 2.53892
J:N0SYd"0-qu?]szzz!!#7`s7cQozzz!!!!(s8Viozzzz"98E!zzzz!!3-"rVuouzzz!!!'"
s8N'!zzz!!!!$s8W,7zzzz"98E$huE`WzJ+s!Dz!"],0s6p!g!!!!"s8W-!n,NFg!!!Q0s8Vio
z5QCc`s82is!!!!`s8W,gz!WW3"s8W&uzJ,fQKp](9o!!iQ(s8W-!z!<<*!s7cQo!!",@s8W-!
...
EndImage
EndChar

Image: 行上的数字依次表示:图像宽度(像素)、高度、图像类型(0=>单色、1=>索引色、2=>rgb 真彩、3=>rgba)、每行字节数、颜色表颜色项数、颜色表中透明色的索引(真彩图像则为透明色本身)、图像左上角的 x 与 y 坐标、将图像像素转换为字符单位的 x 与 y 缩放因子。随后是一批用 Adobe Encode85 过滤器编码的二进制数据,包含颜色表全部颜色及图像像素数据转储。位图数据会经过游程编码(RLE)压缩——文档建议查看 fontforge/sfd.cpp 中的 image2rle 理解具体做法;RLE 压缩后的图像输出时,Image: 行上会多出一个参数,给出需从数据流读取的字节数。多图层字体的前景层也可以包含图像,存储方式相同。

额外图层

若字形有超出前景/背景的额外图层,则用以下方式引入:

Layer: 2
SplineSet
...

CounterMasks 计数器掩码

PostScript 字形还可能包含 countermasks:

StartChar: m
Encoding: 109 109 77
Width: 785
HStem: 0 18<28 224 292 488 566 752> 445 27<280 296 542 558>
VStem: 98 56<45 376> 362 56<45 378> 626 56<45 378>
CounterMasks: 1 38
...
EndChar

CounterMasks 行第一个参数声明一个 counter group;"38"(及其后任何值)是一个十六进制位掩码,描述一个组。掩码大小始终是 8 的倍数(即十六进制位数总为偶数)。掩码的最高位指定第一个茎干提示在组中是否存在(1)或缺失(0),次高位对应第二个提示,依此类推;不对应任何提示的多余低位被忽略。(从高位而非低位开始是 Adobe 的设计决定。)本例中第三(0x20)、第四(0x10)与第五(0x08)个茎干提示(即三个垂直茎干)在组中,得到掩码 0x38。若再给字形加四个 VStem 提示使提示总数达到 9,则掩码需写为 0x3800(需要两个字节容纳 9 位)。

TrueType 指令

二次(TrueType)字体中的字形可包含 TrueType 指令,有两种输出格式——文本格式或旧二进制格式:

TtInstrs:
NPUSHB
 4
 251
 0
 6
 251
MDAP[rnd]
MDRP[rnd,grey]
MDRP[rp0,rnd,grey]
...

与前述 prep 表一样,这就是一列 FontForge 使用的 TrueType 指令名(转换表见 fontforge/ttfinstrs.c)。旧格式则是 ASCII85 编码的二进制转储(注意:两者通过初始关键字区分:TtInstrs 与 TtfInstrs):

TtfInstrs: 107
5Xtqo&gTLA(_S)TQj!Kq"UP8<!<rr:&$QcW!"K,K&kWe?(^pl]#mUY<!s\f7"U>G:!%\s-3WRec
$pP.r$uZOWNsl$t"H>'?EW%CM&Cer:&f3P>eEnad5<Qq=rQYuk3AE2g>q7E*
EndTtf

这是 107 字节的 ASCII85Encode 编码二进制数据。

Anchor Points(锚点)

字符若包含锚点,则如下所示:

AnchorPoint: "bottom" 780 -60 basechar 0
AnchorPoint: "top" 803 1487 basechar 0

点名称(UTF-7)指明它所属的锚点类,随后是位置、点类型(basechar、mark、baselig、basemark、entry、exit),对连字还有一个数字指明它涉及哪个连字组件。也可能看到:

AnchorPoint: "bottom" 780 -60 basechar 0 {12-13 -1,-1} {8-14 1,0,-1,-1,-2,-2,-2}

其中花括号内的项是水平/垂直设备表:第一个表示(水平)设备表应用于像素尺寸 12~13,像素调整均为 -1 像素;第二个表示垂直设备表应用于像素尺寸 8~14,像素调整为 1、0、-1、-1、-2、-2、-2。最后,TrueType 字体可能基于字形内一个普通点定位锚点(若如此则不能存在设备表):

AnchorPoint: "bottom" 780 -60 basechar 0 23

表示该锚点将定位于(通常为 780,-60 但可能被指令移动的)TrueType 点 23 的同一位置。

字形类与替代 Unicode

若用户设置了字形类:

GlyphClass: 2

该数字比其表示的 'GDEF' 字形类大 1(上面的 2 表示 base 字形(类 1))。

若字形有多个 Unicode 编码(拉丁 "A" 可能用于希腊 "Alpha"),或由变体选择器指定,则提供替代 Unicode 信息:

AltUni2: 000061.00fe01.0

点分三元组中第一个数字是替代 Unicode 码点(十六进制),第二个是变体选择器(或 ffffffff),最后一个保留给将来使用(当前必须为 0)。一行可有多个三元组。

非类字距对(Kerns2)

若字符是任何字距对的第一个字形(而非类定义的字距对):

Kerns2: 60 -100 "Kern Latin" {12-13 -1,-1} 63 -92 "Kern Latin" 70 -123 "Kern Latin" 45 -107 "Kern Latin" 76 -107 "Kern Latin"

每个字距对由 2 个数字和一个 lookup subtable 表示(以及可选的设备表):第一个是第二个字符的原始位置(在当前字体中),下一个是水平字距量,然后是 lookup subtable 名称;然后从下一个字距对重新开始。

其它 GPOS / GSUB / GDEF 数据

要放入其它 GPOS、GSUB 或 GDEF sub-table 的数据存储如下:

Position2: "Inferiors" dx=0 dy=-900 dh=0 dv=0
PairPos2: "Distances" B dx=0 dy=0 dh=0 dv=0  dx=-10 dy=0 dh=0 dv=0
Ligature2: "Latin Ligatures" one slash four
Substitution2: "Latin Smallcaps" agrave.sc
AlternateSubs2: "Latin Swash" glyph490 A.swash
MultipleSubs2: "Latin Decomposition" a grave
LCarets2: 1 650

这些行中绝大多数第一个字符串是 lookup subtable 名称(LCarets 除外,它没有名称)。简单的位移由字形位移量及字形前进宽度位移量表达;双字形定位控制两个相邻字形的定位(字距是它的特例);连字包含构成它的字符名称;简单替换包含它将成为的字符名称;alternate sub 包含用户可从中选择的字符列表;多重替换包含当前字形将被分解成的字符;连字插入符(ligature caret)包含已定义插入符数量的计数,及这些插入符位置的列表。

字形级附加数据

字形也可以有:

Comment: Hi
Colour: ff0000
Validated: 1
UnlinkRmOvrlpSave: 1
  • Comment:任意注释,以 UTF-7 输出;
  • Colour:颜色(6 位十六进制 RGB 值);
  • Validated:缓存的验证状态(一个位掩码,每位含义见 Python 文档;每个图层可有一条);
  • UnlinkRmOvrlpSave:标志,指示保存字体前应解除该字形引用并运行移除重叠(remove overlap)。

以及 Python 数据(与字体级格式相同):

PickledData: "I3
."

任意 Python pickle 数据(protocol=0),双引号与反斜杠前会加反斜杠转义。

TeX 与 MATH 字形数据

TEX: 0 425

指定字形的 tfm 高度与深度。

ItalicCorrection: 50
TopAccentHorizontal: 400
IsExtendedShape: 1
GlyphVariantsVertical: parenleft.big parenleft.bigger parenleft.biggest
GlyphConstructionVertical: 3  uni239D%0,0,300,4733 uni239C%1,2500,2500,2501 uni239B%0,300,0,4733
TopRightVertex: 2 0,0{13-15 1,0,1} 100,10
  • ItalicCorrection:斜体校正(来自 tfm 文件或 MATH 表),也可包含设备表。
  • TopAccentHorizontal:数学排版中顶部重音的水平放置,也可包含设备表。
  • IsExtendedShape:当前字形是扩展形状(因此可能需要特殊的上标定位)。
  • 对字形变体与构造,单词 "Vertical" 可替换为 "Horizontal",用于水平生长的字形。
  • GlyphConstruction 的第一个数字是组件数;每个组件由字形名、%、是否为延伸件(extender)的指示、起始重叠长度、结束重叠长度与全长表示。
  • 数学字距信息可为字形每个顶点(TopRight、TopLeft、BottomRight、BottomLeft)指定:先是点数计数,然后每点的高度与字距对。设备表允许出现,可直接跟在高度或字距值之后。

Type3 多层字体的轮廓字符扩展

与单一 "Fore" 图层不同,多层 Type3 字体有(可能)多个图层,每层由 Layer 行引入并指定填充与描边信息;每层内可有 splineset、引用列表、图像列表(任意或全部可省略),语法与常规情形相同:

LayerCount: 3
Layer: 1  1 1 1  #fffffffe 1  #fffffffe 1 40 round round [1 0 0 1] []
FillGradient: 220;260 490;450 0 pad 2 {0 #808080 1} {1 #000000 1}
SplineSet
...
EndSplineSet
Layer: 2  1 0 1  #00ff00 1  #0000ff 1 19 miter butt [0.5 0 0 1] [10 5]
FillGradient: 400;400 400;400 400 repeat 2 {0 #ffffff 1} {1 #808080 1}
SplineSet
...
EndSplineSet
Layer: 3  1 0 1  #00ff00 1  #0000ff 1 19 miter butt [0.5 0 0 1] [10 5]
FillPattern: E 200;200 [0.707 0.707 -0.707 0.707 0 0]
SplineSet
...
EndSplineSet
  • LayerCount 给出字形中的图层数(背景层是层 0,所以此数比实际指定的层数多 1——背景层仍按之前方式指定,但计入该数)。此行非必需;若省略(或错误)FontForge 会自行推断。
  • Layer 行信息较多:第一个数字说明是哪一层;接下来三个布尔值,第一个指定是否填充 splineset(或对图像使用 imagemask),第二个是否描边 splineset,第三个目前基本无意义。之后依次是:填充色的 RGB 值(特殊值 #fffffffe 表示颜色继承)、不透明度(特殊值 -1 表示继承)、描边色与不透明度、描边宽度(-1 表示继承)、linejoin 类型(miter、round、bevel、inherited)、linecap 类型(butt、round、square、inherited)、应用于画笔的变换矩阵,最后是虚线数组(dash array)。此 Layer 与下一个 Layer(或字符结束)之间的任何 SplineSet、Ref、Images 都属于本层。
  • FillGradient(或 StrokeGradient)指定线性或径向渐变填充(描边)。其后是两个点:线性渐变的起点与终点(径向渐变则为焦点与圆心)、一个半径(0 表示线性渐变,否则为径向)、一个指定渐变在起止点之外区域行为的关键词,最后是一个给出 "stop-points" 数量的数字。每个 stop point 在花括号内,含三个数字:0~1 的位置(从渐变起点到终点的比例)、十六进制颜色、0~1 的不透明度。
  • FillPattern(或 StrokePattern)指定用平铺图案代替传统单色填充:后跟图案字形名、图案在当前坐标系中的尺寸(宽、高)(图案字形会被缩放至该尺寸),以及平铺后应用于平面的变换矩阵(上例将瓦片旋转 45 度)。

若字形被用作瓦片(tile),可能有以下附加关键字之一:

TileMargin: 20
TileBounds: -20 -220 1020 820

两者都未指定时,瓦片尺寸取其包围盒。若指定 TileBounds,尺寸取自其中给出的盒信息(minx、miny、maxx、maxy)。若指定 TileMargin,包围盒向外扩展该量(为瓦片四周提供空白边距)。

SplineSet 内,轮廓完成后可用 PathFlags 关键字修饰(目前只有标志 1,指示该轮廓是字形的裁剪路径(clipping path)的一部分):

SplineSet
102 699 m 5
 1176 699 l 5
 1176 -120 l 5
 102 -120 l 5
 102 699 l 5
  PathFlags: 1
EndSplineSet

位图字体(BitmapFont)

所有轮廓字符描述完后是 EndChars 条目,随后是位图字体:

EndChars
BitmapFont: 12 285 10 2 1
BDFStartProperties: 2
COMMENT 0 "This is a bdf comment property"
FONT_DESCENT 18 2
BDFEndProperties
Resolution: 75
BDFChar: 32 3 0 0 0 0
z
BDFChar: 3 33 3 0 1 0 9
^d(.M5X7S"!'gMa
BDFRefChar: 302 488 7 1 N
BDFRefChar: 302 58 0 0 N
  • BitmapFont: 行包含以下数字:字体的像素尺寸、字体中潜在字符数、字体的 ascent、descent,以及字体深度(每像素位数)。

  • 可选地有一组 BDF 属性。若存在属性,会有 BDFStartProperties: 行并带属性计数;应有那么多属性行,然后是一行 BDFEndProperties。每个属性形式为:

    <NAME> <TYPE> <VALUE>
    

    其中 <NAME> 是属性名;<TYPE> 为 0(字符串)、1(原子 atom)、2(整数)、3(无符号整数);另外 <TYPE> 可加上 16 表示该行是真正的属性(而非 BDF 头部其它地方的信息)。<VALUE> 是带引号的字符串或整数。

  • 可选地有分辨率行,指定字体的设计分辨率。

  • 之后是一列位图字符。位图字符行包含以下数字:原始位置(字形 ID)、编码(本地)、宽度、最小 x 值、最小 y 值、最大 x 值、最大 y 值;若位图字体有垂直度量还会有 vwidth。之后是又一批如上编码的二进制数据。共有 (ymax-ymin+1) * ((xmax-xmin)/(8/depth)+1) 个(未编码)字节;此处无颜色表(图像中高位在前,置位字节应着色为黑色,清除位透明)。

  • 若存在带位图引用的位图字形,它们出现在位图 strike 的末尾。每条 BDFRefChar 行定义一个引用:第一个数字是复合字形的字形 ID,第二个是被引用字形的字形 ID,最后两个是引用在复合字形中的水平与垂直平移。

位图字体以如下方式结束,随后可有下一个位图字体,最后整个文件以 EndSplineFont 收尾:

EndBitmapFont
BitmapFont: 17 285 14 3 1
BDFChar: 0 17 0 0 0 0
z
...
EndBitmapFont
EndSplineFont

CID 键控字体与 Multiple Master 字体

CID 键控字体

CID 字体的保存方式略有不同。它以普通字体头部开始,包含顶层 CID 字体字典中的信息——如前所述,其中会包括指定 CID 字符集的特殊键(registry、ordering、supplement),此外还包括:

CIDVersion: 2.0
BeginSubFonts: 5 8318

CIDVersion 不言自明;BeginSubFonts 行表示有 5 个子字体,其中最大者包含 8318 个字符槽(其中部分可能未定义)。之后是子字体列表(像普通字体一样转储)及其字符。只有顶层字体会包含任何位图字符、锚点类等。

Multiple Master 字体

Multiple master(MM)字体以不同风格的文件头开始,后跟一组子字体。若 mm 字体有 4 个实例,则会有 5 个子字体(每个实例一个,外加一个混合字体)。文件头形如:

SplineFontDB: 1.0
MMCounts: 4 2 0 0
MMAxis: Weight Width
MMPositions: 0 0 1 0 0 1 1 1
MMWeights: 0.31502 0.13499 0.38499 0.16499
MMAxisMap: 0 2 0=>50 1=>1450
MMAxisMap: 1 2 0=>50 1=>1450
MMCDV:
{
1 index 1 2 index sub mul 3 1 roll
1 2 index sub 1 index mul 3 1 roll
1 index 1 index mul 3 1 roll
pop pop
0 1 1 3 {index add} for 1 exch sub 4 1 roll
}
EndMMSubroutine
MMNDV:
{
exch 50 sub 1400 div
exch 50 sub 1400 div
}
EndMMSubroutine
BeginMMFonts: 5 0
  • MMCounts 行给出实例数(4)与轴数(2),还指出它是否是 apple distortable 字体(0)及若是则该字体的命名样式数(0)。

  • MMAxis 行给出各轴的名称。

  • MMPositions 行是实数组(有 instance_count * axis_count 个元素),描述每个实例沿每个轴的坐标。

  • MMWeights 行提供实例字体的权重(混合比),插值这些实例即得到默认字体。

  • 每个轴有一条 MMAxisMap 行,提供从设计空间到归一化空间的映射。例如:

    MMAxisMap: 0 2 0=>50 1=>1450
    

    表示轴 0 有两个映射点:一个在归一化位置 0、对应设计位置 50;一个在归一化位置 1、对应设计位置 1450。

  • 这里存储了两个子程序,都是简单 PostScript:第一个是 /NormalizeDesignVector 例程,第二个是 /ConvertDesignVector 例程。

最后是实际的实例字体。

Apple 'fvar' 系列变体

包含 Apple *var 表(fvar、gvar、avar、cvar)信息的字体格式略有不同:

MMCounts: 8 2 1 10
MMAxis: Weight Width
MMPositions: 1 0 -1 0 0 1 0 -1 -1 -1 1 -1 1 1 -1 1
MMWeights: 0 0 0 0 0 0 0 0
MMAxisMap: 0 3 -1=>0.479996 0=>1 1=>3.2
MacName: 0 0 6 "Weight"
MacName: 0 1 15 "Type de graisse"
...
MMAxisMap: 1 3 -1=>0.619995 0=>1 1=>1.3
MacName: 0 0 5 "Width"
MacName: 0 1 7 "Largeur"
...
MMNamedInstance: 0  3.2 1
MacName: 0 0 5 "Black"
MacName: 0 1 9 "Tr\217s gras"
...

Adobe 格式的坐标范围是 [0,1],Apple 格式则是 [-1,1]。Adobe 通常每轴指定 2 个实例(在极值处),Apple 则期望 3 个(极值与 0 处的默认设置)。因此双轴字体的 Adobe 版通常含 4 个实例加一个默认版本,Apple 版含 8 个实例加一个默认版本。MMWeights 字段对 Apple 无关紧要。每个轴有一组名称,翻译成各种语言。最后,Apple 允许设计空间中的某些点被命名——这里 Weight=3.2、Width=1 的点被命名为 "Black"(或 "Trés gras")。

SplineFont 目录(.sfdir):把 SFD 拆成文件树

2006 年底,一些人希望有一种每个字形存为单个文件的字体格式(为版本控制系统提供更细粒度、减少变更后的下载量)。FontForge 据此扩展出 SplineFont Directories:本质上是把 SFD 文件拆成目录中的小片段,目录扩展名为 .sfdir。目录包含:

  • font.props:包含文件头(Font Header),涵盖到(但不含)BeginChars 行之前的所有内容。
  • 非 CID 键控字体:目录中每个字形一个文件,文件名为 <字形名>.glyph,格式按上文 轮廓字符数据 指定。
  • 若字体含位图 strike:会有名为 <像素尺寸>.strike 的子目录,内含 strike.props 文件与 strike 中每字形一个的 <字形名>.bitmap 文件。strike.props 包含位图头部与 BDF 属性,*.bitmap 文件包含每字形位图数据,格式见上文 位图字体 一节。
  • CID 键控字体:会有名为 <子字体名>.subfont 的子目录,每个子字体含自己的 font.props 文件与自己的字形文件集。
  • Multiple Master 字体:会有名为 <实例名>.instance 的子目录。

在源码中,.sfdir 相关文件与扩展名常量定义于 fontforge/sfd.cpp 附近,读写路径分别为 SFDWrite/SFDirClean(写)与 SFDirRead(读,fontforge/sfd.cpp),且清理时会刻意保留不认识的文件(可能包含版本控制信息)。

自动保存格式(.asfd)

错误恢复文件保存在 ~/.FontForge/autosave 目录下(参见 错误恢复文档),文件名相当随机,以 .asfd 结尾,外观与 .sfd 文件非常相似。

若 asfd 文件以如下行开头:

Base: /home/gww/myfonts/fontforge/Ambrosia.sfd

则它被认为是应用到该文件(可以是 sfd 文件或字体文件)的变更列表。若不以此 Base: 行开头,则被认为是新字体。下一行包含编码(同前);再下一行是 BeginChars 行——该行给出的数字不是文件中的字符数,而是字体中可能出现的最多字符数。随后是所有已变更字符的列表(格式见上文 轮廓字符数据](#outline-char) 一节)。文档明确说明:位图不被保留,Grid 变更不被保留(对应写出端 SFDDumpASFD 中只转储 BeginChars + 变更字形的逻辑,见 [fontforge/sfd.cpp)。

ASCII85Encode 编码

SFD 中所有内嵌二进制数据(未知表、旧式 TTF 指令、图像像素等)都用 ASCII85Encode 过滤器打包。它是一种把 4 个二进制字节转换为 5 个介于 ! 与 u 之间的 ASCII 字符的编码。设有 4 个二进制字节 (b1 b2 b3 b4),要求出 (c1 c2 c3 c4 c5) 使得:

b1 * 2563  +  b2 * 2562  +  b3 * 256  +  b4 =
   c1 * 854  +  c2 * 853  +  c3 * 852  +  c4 * 85  +  c5

最终输出是在每个 ci 上加 33(即 !)得到。若四个字节全为 0,则可不写 !!!!! 而用字母 z 代替。

不过并非所有二进制数据流的长度都能被 4 整除。因此:在数据流末尾,设剩余 n 个字节;向数据流追加 4-n 个零字节,应用上述变换(全零时不用 z 特例),并输出 n+1 个 ci 字节。例如编码单个 0 字节:

  • 追加 3 个额外 0 字节(n==1 => 加 4-1=3 字节);
  • 发现所有 ci 也为零;
  • 每个都加 !(现在全为 !);
  • 输出两个 !(n+1 = 2)。

如何在仓库中验证与实践

  • 读写实现:SFD 的完整序列化与解析逻辑集中在 fontforge/sfd.cpp:SFDDump(L2940 起)负责写出并决定版本号;SFDStartsCorrectly(L9011-L9039)校验首行版本;SFD_Read(L9041 起)执行加载流程。savefont.c 中的 SplineFontDB: 标识(fontforge/savefont.c)也印证了该头部的关键地位。
  • 测试字体样本:仓库 tests/fonts 下收录了大量真实 .sfd 文件,覆盖从早期 SplineFontDB: 1.0(如 tests/fonts/AHBugs.sfd)到当前 3.x(如 tests/fonts/AddExtremaTest2.sfd)的各类版本,以及配套的回归测试脚本(tests 下的 test*.pe / test*.py)。阅读这些样本是把本指南中的语法与真实文件对应起来的最快途径。
  • 版本演进:想了解某关键字何时加入、为何改动,可查阅 SFD 格式变更日志,其中逐条记录了 gasp 版本号、woff 关键字、WidthSeparation、MarkAttachmentSet、BDFRefChar、JSTF、ExtremaBound、Layer 背景标志、图像格式扩展、BASE 表、渐变/图案填充、PathFlags、Layers 重构等里程碑。

掌握 SFD 格式后,你可以直接阅读或手工修补字体工程文件、在版本控制系统中进行有意义的 diff 审查,也可以基于 fontforge/sfd.cpp 的解析逻辑编写自己的转换工具,让 SFD 成为字体数据交换与自动化的可靠中间格式。

登录后查看全文
fontforge