Skip to content

OpenType 特性编程

2025-12-16 · 1.1万字 · 36分钟

参考文献:

  1. https://simoncozens.github.io/fonts-and-layout/features.html
  2. https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html

OpenType 相较于其他字体格式真正突出的是其可编程性。“智能字体”(例如通过 OpenType 特性实现的字体)能够基于字体内部数据执行一系列排版优化操作,包括字距调整 kerning、连字替换 ligature substitution、向用户提供替代字形、提供特定文字系统和语言的字形变体,乃至完全替换、重新排序和重新定位字形。

具体而言,字体内的两个表—— GPOS 表和 GSUB 表——提供了广泛的上下文敏感字体转换功能。 GPOS 包含调整字形位置的指令。上下文敏感重定位的典型示例是 kerning,它根据字形特性动态修改字符间距;而 GPOS 则支持更丰富的重定位指令。 GSUB 包含根据特定条件替换字形的指令。最浅显的例子是连字,它用一对(或多对)字形替换另一对字形:用户输入“f”后再输入“i”,字体却不会显示这两个独立字形,而是指示造型引擎调用单个字形“fi”。 GSUB 则允许更多有趣的替换——不仅包括排版上的精妙设计,更是复杂文字系统设计字体的不可或缺的技术。

Adobe 特性语言 Adobe feature language

OpenType 指令(或“规则”)——通常用一种无正式名称的语言编写;习惯上称为“AFDKO”(源自“Adobe OpenType 字体开发工具包”,该软件工具集包含可读取此语法并将规则添加至二进制字体文件的组件)、“Adobe 特性语言”、“fea”或“特性格式”等等。虽然存在其他规则表示方式(且字体内部采用截然不同的存储格式),但这是编写字体规则最常用的编程方式。尽管还有许多其他工具用于指定 OpenType 布局特性,但 Adobe 特性语言几乎是通用的,所以必须精通。

基本特性编码:替换与连字

下面是一则最短的 OpenType 程序

afdko
feature liga {
  sub f f by f_f;
} liga;

这条规则读做“在特性 liga 中,将字形 f f 替换为 f_f ”。这假设字体中至少包含两个字形,其中一个名为 f ,另一个名为 f_f 。 后续我们将详细阐释特性的具体含义,以及为何创建名为 liga 的特征。当前您只需理解:我们已制定一条规则,当文本使用本字体排版时,字形调整器将自动应用该规则。

特性语言的语法简单,详见 http://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html 。定义一条特性大致如下:

afdko
feature <feature tag> { ... } <feature tag>;

由此我们创建了一条特性,标签 tag 为 <feature tag> ,它必须是四字母标识符。 {} 中存放这条特性的所有规则。每条规则以一个规则名 sub 或者 pos 开头,以分号 ; 结尾,中间的内容取决于规则的性质。上述的规则为 sub ,表示用一组字形替换另一组字形。pos 规则用于调整字形位置; sub 规则置于 GSUB 表中, pos 规则置于 GPOS 表。

字形类和具名类

现在让我们编写一组规则,将小写元音转换为大写元音:

afdko
feature liga {
  sub a by A;
  sub e by E;
  sub i by I;
  sub o by O;
  sub u by U;
} liga;

更紧凑的写法是

afdko
feature liga {
    sub [a e i o u] by [A E I O U];
}  liga;

这里,方括号 [] 定义了一个字形类,它使得规则可以逐个作用在类中的每个字形中。

还可以只在匹配部分写,例如

afdko
feature liga {
    sub f [a e i o u] by f_f;
}  liga;

这相当于

afdko
feature liga {
    sub f a by f_f;
    sub f e by f_f;
    ...
}  liga;

即把规则分配给类内每个字形。

如果需要在不同地方使用同一类,可以借助具名类来快速引用,以 @ 开头

afdko
@lower_vowels = [a e i o u];
@upper_vowels = [A E I O U];

使用时

afdko
@vowels = [@lower_vowels @upper_vowels];

feature liga {
  sub @lower_vowels by @upper_vowels;
} liga;

具名类的语法相当于宏

特性和查找

我们将规则放入特性中。特性是用于指示塑形器 shaper 在何种情况下应用哪些规则的一种方式。 例如,名为 liga 的特性即“连字”特性,在处理拉丁字母文本时始终生效,除非用户关闭;另一特性 rlig 用于强制连字,即便用户不希望显式应用连字。某些特性作为排版处理的基础环节始终生效——尤其在处理非拉丁文字系统时——而其他特性则属于可选的美学调整。

实际上在规则和特性之间,还有一层结构称为查找 lookup。特性实际上是由一系列的查找组成的,而查找中存放的是规则。上述的最简案例实际上相当于隐含了一层查找:

afdko
feature liga {
	lookup lookup_1{
	  sub f f by f_f;
	} lookup_1;
} liga;

查找是规则的集合,用于区分特性在不同字形类中的规则,也方便在不同特性和规则链中复用,例如

afdko
lookup myAlternates {
  sub A by A.001; # Alternate form
  ...
} myAlternates;

feature salt { lookup myAlternates; } salt;
feature ss01 { lookup myAlternates; } ss01;

文字系统 script 与语言 language

文字系统指某种文本,例如 arab 文字系统。而语言使用文字系统,例如 ARA 阿拉伯语和 URD 乌尔都语。如果没有显式说明,以上所有规则均视为适用于默认文字系统和默认语言。文字系统和语言通过四字符代码(称为标签 tag)描述。

在字体文件中,GSUB 和 GPOS 表按照文字系统、语言、特性的顺序排列,但这可能对设计师而言造成不变,因此 AFDKO 语法允许先定义特性,再在其内部编写特定于文字系统和语言的代码。为此须现在特性文件开头定义 language system。例如,以下字体将支持阿拉伯语和乌尔都语,同时提供土耳其语及拉丁文字系统的"默认"(非语言特定)处理方式,以及阿拉伯文字系统的非特定语言处理方式(例如当字体用于波斯语文档时),并包含若干通用规则:

afdko
# languagesystem <script tag> <language tag>;
languagesystem DFLT dflt;
languagesystem arab dflt;
languagesystem arab ARA;
languagesystem arab URD;
languagesystem latn dflt;
languagesystem latn TRK;

在声明将使用的系统后,我们可以指定特定的查找规则适用于特定的语言系统。例如,乌尔都语数字 4、5 和 7 的形态与阿拉伯数字不同。若字体需同时支持阿拉伯语和乌尔都语,当输入文本为乌尔都语时,应确保替换为预期形态的数字。

我们将使用 locl (本地化)功能实现此操作,该功能仅适用于乌尔都语:

afdko
feature locl {
    script arab;
    language URD;
    # All lookups here apply to Urdu until the next script/language
    lookup urdu_digits {
      sub four-ar by four-ar.urd;
      sub five-ar by five-ar.urd;
      sub seven-ar by seven-ar.urd;
    } urdu_digits;
} locl;

如上所述,设置中出现在首个 script 关键字之前的任何查找操作,均被视为适用于所有文字系统和语言。若需指定这些操作不应出现在特定语言环境中,则需使用 exclude_dflt 声明,例如:

afdko
feature liga {
    script latn;
    lookup fi_ligature {
      sub f i by fi; # All Latin-based languages get this ligature
      } fi_ligature;

    language TRK exclude_dflt; # Except Turkish. Turkish doesn't.
} locl;

OpenType 字形处理原理

在继续介绍 OT 特性语法并给出规则示例前,有必要更深入地理解这些规则的本质及其在计算机中的处理机制,以便我们处理更复杂的文字系统。

映射与重排

塑形器首先将输入中的 Unicode 字符映射为字体内部的一系列字形 ID。(我将这个结果序列称为“字形流”,但这并非常用术语。塑形器实现者可能称其为缓冲区。)对某些文字系统而言,只需使用字符映射表( cmap 表)将 Unicode 字符转换为字体内部的字形 ID 即可。然而大多数文字系统的转换需要复杂塑形器的辅助;OpenType 字形引擎内置多个复杂塑形器,各自处理不同文字系统或文字家族的文本。

例如当文本为阿拉伯语时,它将以一系列不包含任何“拓扑”信息的码点形式呈现:字符串 ججج 由相同的 Unicode 码点重复三次构成(U+062C 阿拉伯字母吉姆)。但输出时需呈现为三个不同字形:初始吉姆、中间吉姆和终止吉姆。此时,字形引擎中的特定组件会识别阿拉伯语处理规则,对 Unicode 输入进行标注,标明各字形所需的位置。 该引擎理解阿拉伯语的运作机制:例如在 جاج(JEEM ALIF JEEM)中,首个 JEEM 因与 ALIF 连接而采用初始形态,而第二个 JEEM 保持原状,因为 ALIF 字母不与左侧字符连接。完成标注后,引擎仅对标记为初始形态的字符流部分应用指定的初始形态替换规则,中位形态、终止形态及孤立形态的处理亦遵循相同逻辑。

其他文字系统需要不同辅助才能实现转换。 Unicode 定义字符编码的方式有时与字符书写顺序略有差异。以梵文为例,字符序列कि(ki)的编码顺序是辅音क(ka)在前,元音ि(i)在后。但视觉上元音必须置于首位,因此字形引擎需要重排字形流:将所有视觉上需置于首位的元音——即"基元前元音"——置于基元辅音之前。这纯粹出于便利性考虑:对于工程师而言,处理字形序列 iMatra-deva ka-deva 远比直接接收 ka-deva iMatra-deva 这类 Unicode 转字形转换结果更轻松,后者会迫使开发者在字体的 OpenType 规则中手动调整字形顺序。

请注意,当我单独展示元音字母 ि 它显示为带点圆圈的形态,这是因为元音符号通常不能独立存在,必须附着于辅音字母,因此我输入的内容是违反正字法的。为标示缺失的辅音并使显示效果合理,字形引擎插入了点状圆圈;这正是复杂字形引擎的另一项功能。它能识别有效音节与无效音节,并通过添加点状圆圈提示音节断裂处。(因此若在印刷品中见到点状圆圈,说明输入文本存在错误。)

规则选取

接下来处理 GSUB 表中的替换规则,最后处理 GPOS 表中的定位规则。(这很合理,因为在定位字形之前,你需要知道要绘制哪些字形)

处理表格的第一步是确定应用哪些规则和应用顺序。塑形器通过定义其关注的特征集来实现这一目标。

处理顺序一般如下:

  1. 首先是将字符转化为字形的特性(如 ccmprvrnlocl
  2. 其次是特定文本系统的塑形特性,例如重排音节群(印度文本系统)或实现连写行为(阿拉伯文、恩科文等)
  3. 接着是必需的排版优化,如强制连字(阿拉伯文)、可选排版优化(小型大写字母、日语居中标点等)
  4. 最后是定位特征(如字距调整和标记定位)

比方说 Uniscribe 为拉丁字符集收集了以下特征: ccmpligacligdistkernmarkmkmk 。 Harfbuzz 的处理顺序为: rvrn ,接着是 ltraltrm (适用于从左到右的语境)或 rtlartlm (适用于从右到左的语境),随后是 fracnumrdnomrandtrak 、私有功能字符 HARFBUZZ ,接着是 abvmblwmccmploclmarkmkmkliga ,接着是 caltcligcursdistkernliga 以及 rclt (用于横排排版)或 vert (用于竖排排版)。

对于其他字符集,其特性处理顺序(至少在 Uniscribe 中如此,尽管 Harfbuzz 通常遵循 Uniscribe 的处理逻辑)可参见微软的《文本系统开发规范》文档。例如,可查阅阿拉伯语规范;其他字符集的处理顺序可通过侧边菜单访问。

在文本系统所需的默认特性列表之后,我们会添加排版引擎请求的所有特性——例如: 用户可能启用了小型大写字母按钮,触发排版引擎向字体请求 smcp 特性;或者排版引擎检测到分数符号后,会为斜杠前的数字启用 numr 特性,为斜杠后的数字启用 dnom 特性。

在确定一组需要查找的特性后,接下来我们需要将其转化为查找列表。根据目标语言和文本系统,检查是否存在该语言/文本系统组合的特性;若存在,则将该特性中的查找项加入列表,否则检查输入文本系统及其默认语言的特征。若仍未找到,则转而检查默认文本系统 dflt 的特性。

例如,若文本使用阿拉伯文本系统(标签 arab )乌尔都语(语言标签 URD ),则塑形器将首先检查阿拉伯语是否包含在文本系统表中。 若包含,则继续检查阿拉伯文本系统规则中是否定义了乌尔都语规则;若有则采用。若无,则使用阿拉伯语脚本的默认规则。若文本系统表中没有阿拉伯文规则,则会将文本系统视为 DFLT ,并采用其定义的特性列表。

查找应用

现在我们得到了一串查找,其中含有各种规则。这些规则将逐个应用于字形流上。

塑形过程类似于一台图灵机。用户输入的字形被写入磁带,随后读取头逐格扫描磁带,检查匹配当前位置的查找。

若查找匹配成功,塑形器将执行相应操作(如替换),随后移至下一格。当它遍历整条磁带并执行所有操作后,就轮到下一个查找(我们称之为应用查找)。

应用按照查找顺序依次进行的,而属于同一查找的所有规则同时应用。例如:

afdko
feature liga {
    sub a by b;
    sub b by c;
} liga;

应用在字形流 cabbage 上得到 cbccbge。而

afdko
feature liga {
    lookup l1 { sub a by b; } l1;
    lookup l2 { sub b by c; } l2;
} liga;

应用在字形流 cabbage 上则得到 cccccge,这是因为首先应用 l1 得到 cbbbbge,然后应用 l2 得到 cccccge。

因此要注意规则的相互作用关系,制订合适的查找。

在 OpenType 中有 16 种规则,而单个查找只能包含同类型的规则。如果同一特性中存在不同类型的规则时,它们会被字体编译器暗中拆分到不同的查找中。因此有必要将规则显式放入查找,避免规则拆分到不同查找中产生意外效果,妨碍调试。

查找标记 lookup flags

在应用查找的过程中,每个查找都可以设置一组标记,用于控制全局文本系统的塑形。

例如,在阿拉伯语中,字母 lam 和 alef 之间存在强制连字符。我们可以尝试使用简单的连字符来实现,就像我们的 f_f 连字符那样:

afdko
feature liga {
  lookup lamalef-ligature {
      sub lam-ar alef-ar by lam_alef-ar;
  } lamalef-ligature;
} liga;

但字母之间可能存在变音符号:输入字形流可能是 lam-ar kasra-ar alef-ar ,无法由上述规则匹配。自然想到可以添加一条规则:

afdko
feature liga {
  lookup lamalef-ligature {
      sub lam-ar alef-ar by lam_alef-ar;
      sub lam-ar kasra-ar alef-ar by lam_alef-ar kasra-ar;
  } lamalef-ligature;
} liga;

可是这段代码无法编译;它不符合 AFDKO 语法规范。 正如下一章将要阐述的,OpenType 虽支持多个匹配字形与单个替换字形(连字)及单个匹配字形与多个替换字形(多重替换)的组合,但不支持多个匹配字形与多个替换字形(多对多替换)的模式。

不过在这种情况下,我们可以让字形生成器在应用查找时跳过变音符号,

afdko
feature liga {
  lookup lamalef-ligature {
      lookupFlag IgnoreMarks;
      sub lam-ar alef-ar by lam_alef-ar;
  } lamalef-ligature;
} liga;

当应用此查找规则时,塑形器仅关注字形流中包含基础字符的部分—— lam-ar alef-ar ——而 kasra 字形被 IgnoreMarks 标记屏蔽掉,于是上述规则对含有变音符号的字符流仍然生效。

塑形器如何知晓标记与非标记字形?这是在 GDEF 表中定义的,它包含字形定义、字体中字形属性的元数据,其中一项便是字形类别。每个字形可定义为:基础字形(普通字形)、标记字形(无间距 non-spacing 字形)、连字字形(多个基础字形的组合),或组件字形(因用途不明而无人使用)。未明确归类的字形将进入零类,且永远不会被忽略。字符类别通常在字体编辑器中设置或通过特性代码定义 GDEF 表。

其他有用的标记包括:

  1. RightToLeft 仅用于 Nastaliq 字体,罕用
  2. IgnoreBaseGlyphs
  3. IgnoreLigatures
  4. IgnoreMarks
  5. UseMarkFilteringSet @class 忽略除指定类别外的所有标记

定位规则

在处理完 GSUB 表中所有替换规则后,对 GPOS 表执行相同操作:通过特性选取和语言/脚本选择收集一组查找规则,随后依次处理这些规则。

字形定位通过值记录 value record 和锚点 anchor 指定,跟随在作用字形之后。

值记录有三种形式:

  1. 单个数字 <metric>,默认表示 X 进距 advance width,竖排环境则表示 Y 进距 advance height。
  2. 四元组 <<metric> <metric> <metric> <metric>>,分别表示 X 偏移(左负右正),Y 偏移(上正下负),X 进距,Y 进距。
  3. 具名引用 <name>,通过 valueRecordRef 定义,例如
    afdko
    valueRecordDef -10 FIRST_KERN
    valueRecordDef <0 0 20 0> SECOND_KERN
    具名引用在使用时必须用 <> 包围名称,即便它是单个数字的引用。
  4. <NULL>,表示空值记录(不修改字形位置)。实际规则中直接省略值记录而非使用该占位符。

其实还有一种没实现的形式,省略

作为设计师,我们可能将 X 进距视为字形宽度,但在进行 OpenType 编程时,将其理解为下一个字形的绘制位置更为合理(尤其是前进量为负数时,二者差异立现)

试举一例:

afdko
pos o f' <100 100 1237 0>;

它意味着在绘制 o 之后 f 时,首先绘制在 o 后的第 1237 个单位处,然后相对默认位置向上偏移 100 个单位、向右偏移 100 个单位。

锚点有五种形式:

形式 A

afdko
<anchor <metric> <metric>>  # x 坐标,y 坐标

例如

afdko
<anchor 120 -20>

形式 B

afdko
<anchor <metric> <metric> <contour point>>  # x 坐标,y 坐标,轮廓点

罕用

形式 C

afdko
<anchor <metric> <metric>   # x coordinate, y coordinate
        <device> <device>>  # x coordinate device, y coordinate device

形式 D,空锚点

afdko
<anchor NULL> # Anchor not defined

形式 E,具名锚点

afdko
<anchor <name>>

具名锚点通过 anchorDef 关键字定义

afdko
anchorDef <coordinates> <name>;
```,例如
```afdko
anchorDef 300 0 ANCHOR_1;
anchorDef 120 -20 contourpoint 5 ANCHOR_2;

使用时,

afdko
<anchor ANCHOR_2>

替换和定位规则深入

为用 OT 特性实现种种复杂排版,我们需要先熟悉现有的各种可能性。下面介绍替换规则和定位规则的不同类型

替换规则

单字替换

语法

afdko
sub <glyph> by <glyph>;            # format A
sub <glyphclass> by <glyph>;       # format B
sub <glyphclass> by <glyphclass>;  # format C
  1. 用一个字形代替另一个字形
  2. 用一个字形代替一个字形类中任一字形
  3. 用一个字形类中的字形代替另一个字形类中的相应字形(前后两个字形类中字形数目一致)

用法

afdko
sub a by A.sc;                                          # format A
sub [one.fitted one.oldstyle one.tab.oldstyle] by one;  # format B
sub [a - z] by [A.sc - Z.sc];                           # format C
sub @Capitals by @CapSwashes;                           # format C

若替换字形为保留字符 NULL ,则该规则将直接从字形序列中移除输入字形:

afdko
sub a by NULL;

省略 by 子句等同于添加 by NULL

多字替换

afdko
sub <glyph> by <glyph sequence>;

<glyph sequence> 包含两个或多个字形。它不能包含字形类,否则规则将无法明确指定所需的替换序列。例如:

afdko
sub f_f_i by f f i;            # Ligature decomposition

可选替换

afdko
sub <glyph> from <glyphclass>;

塑形器提供可选替换的字形类给用户替换对应字形。

连字替换

afdko
sub <glyph sequence> by <glyph>;

<glyph sequence> 必须包含两个及以上的 <glyph|glyphclass> 。例如:

afdko
sub [one one.oldstyle] [slash fraction] [two two.oldstyle] by onehalf;

由于 OpenType 规范不允许在包含字形类的目标序列上指定连字替换,当在 <glyph sequence> 中检测到字形类时,实现软件将枚举所有特定字形序列。上述示例会展开为下列规则:

afdko
sub  one           slash     two           by  onehalf;
sub  one.oldstyle  slash     two           by  onehalf;
sub  one           fraction  two           by  onehalf;
sub  one.oldstyle  fraction  two           by  onehalf;
sub  one           slash     two.oldstyle  by  onehalf;
sub  one.oldstyle  slash     two.oldstyle  by  onehalf;
sub  one           fraction  two.oldstyle  by  onehalf;
sub  one.oldstyle  fraction  two.oldstyle  by  onehalf;

编辑器无需对连续的连字规则集进行特定排序;实现软件必须执行适当的排序。因此:

afdko
sub f f     by f_f;
sub f i     by f_i;
sub f f i   by f_f_i;
sub o f f i by o_f_f_i;

将等价于

afdko
sub o f f i by o_f_f_i;
sub f f i   by f_f_i;
sub f f     by f_f;
sub f i     by f_i;

也即在目标字形流有重合时,优先应用最长匹配。

上下文替换

此查找类型是 GSUB 查找类型 6 的功能子集,采用链式上下文替换机制。因此,该查找类型的所有所需规则均可通过链式上下文替换规则来表达。

链式上下文替换

链式替换规则的目标序列包含三个部分:前缀序列(上文)、输入序列、后缀序列(下文)。每个序列由一个或多个字形或字形类构成。

最重要的输入字形序列。这是应用替换操作的字形及其类别的序列。可选地,可指定前缀(也称为回溯)字形序列以及后缀(也称为前瞻)字形序列。整个字形序列——前缀+输入+后缀——必须与当前上下文完全匹配才能应用规则。匹配序列通过将输入序列的首个字形与当前处理文本的字形对齐来与上下文关联。若规则匹配成功,则当前上下文将原文本中的字形指针向前移动输入序列的长度。

需注意在 FEA 语法中,整个上下文字符串(回溯序列+输入序列+前瞻序列)均按文本字符串顺序书写。这一点值得强调,因为在查找规则内部,回溯序列的字形书写顺序与待匹配文本相反。了解此特性的字体编辑工具开发者有时会因 FEA 语法而产生困惑。

输入序列的定义是在输入序列内的所有字形名称和类名称后附加标记字符 '

上下文替换规则最通用的形式是在规则中显式引用具名查找。例如:定义两个独立的查找,然后在上下文替换规则的输入序列中使用关键字 lookup 和查找名称来引用它们。

afdko
lookup CNTXT_LIGS {
    sub f i by f_i;
    sub c t by c_t;
} CNTXT_LIGS;

lookup CNTXT_SUB {
    sub n by n.end;
    sub s by s.end;
} CNTXT_SUB;

feature test {
    sub [ a e i o u] f' lookup CNTXT_LIGS i' n' lookup CNTXT_SUB;
    sub [ a e i o u] c' lookup CNTXT_LIGS t' s' lookup CNTXT_SUB;
} test;

请注意,两个上下文替换规则都使用相同的查找表。这是因为每个被引用的查找表中包含多条规则,而不同规则会在不同上下文中匹配。 在第一条上下文替换规则中,查找表 CNTXT_LIGS 将在输入序列字形“f”处应用,字形“f”和“i”将被替换为“f_i”。 查找表 CNTXT_SUB 将在输入序列字形“n”处应用,此时字形“n”将被替换为“n.end”。但此替换仅在序列“f i n”前接字形“a e i o u”中的任意一个时发生。同样地,在第二条上下文替换规则中,字形“c”和“t”将被替换为“c_t”,字形“s”将被替换为“s.end”。此替换仅在字符序列“c t s”前出现字形“a e i o u”中的任意一个时生效。

要注意的是, CNTXT_LIGS 在上下文替换规则中插入的位置在 f 和 i 之间,而不是二者之后,这是因为其匹配锚点从首字母位置开始,并需要紧随其后。

这种上下文替换规则的形式最为灵活。你可以为多个输入序列字形或字形类指定替换查找,被引用的查找可以是不同类型,且被引用的查找可以具有与父级上下文查找不同的查找标记。其缺点在于难以理解将应用何种替换规则,且当被引用的查找中不包含与上下文匹配的规则时,实现可能不会发出警告。

若仅含一次单字替换(一换一)或连字替换(一换多),则可采用内联方式指定该操作。其类型将通过输入序列与替换序列自动检测确定,检测方式与对应的独立(非上下文)语句完全一致。

试举几例:

  1. 单字替换,输入序列中仅包含一个字形:在序列“a d”或“e d”或“n d”中,将“d”替换为“d.alt”

    afdko
    sub [a e n] d' by d.alt;
  2. 单字替换,输入序列中仅包含一个字形类:如果大写字母后跟一个小写大写字母,则将该小型大写字母替换为对应的小写字母。

    afdko
    sub [A-Z] [A.sc-Z.sc]' by [a-z];
  3. 连字替换,输入序列是由待连字字形构成的序列:在字符序列“e t c”或“e.begin t c”中,将前两个字形替换为连字符。

    afdko
    sub [e e.begin]' t' c by ampersand;

    这个规则是下面等价规则的内联形式

    afdko
    lookup CNTXT_LIGS {
        sub e t by ampersand;
        sub e.begin t by ampersand;
    } CNTXT_LIGS;
    feature test {
        sub e' lookup CNTXT_LIGS t' c;
        sub e.begin' lookup CNTXT_LIGS t' c;
    } test;
  4. 对同一输入进行多重替换:在查找 REORDER_CHAIN 中,字符串“ka ka.pas_cakra.ns”首先被替换为“ka”,随后第二个查找将剩余的“ka”替换为字符串“ka.pas_cakra ka”。

    afdko
    lookup REMOVE_CAKRA {
        sub ka ka.pas_cakra.ns by ka;
    } REMOVE_CAKRA;
    
    lookup REORDER_CAKRA {
        sub ka by ka.pas_cakra ka;
    } REORDER_CAKRA;
    
    lookup REORDER_CHAIN {
        sub ka' lookup REMOVE_CAKRA lookup REORDER_CAKRA ka.pas_cakra.ns' ;
    } REORDER_CHAIN;

链式替换规则的排除情况

链式上下文替换规则的例外情况,可通过在链式上下文规则之前任意位置(且在同一查找表中)插入以下形式的语句来表达:

afdko
ignore sub <backtrack glyph sequence>?<marked glyph sequence><lookahead glyph sequence>?;

这里前缀和后缀序列可省略,但至少包含一个标记的字形或字形类。为方便起见,多条语句可以用逗号合并,即

afdko
ignore sub <match sequence1>;
ignore sub <match sequence2>;
ignore sub <match sequence3>;

等价于

afdko
ignore sub <match sequence1>, <match sequence2>, <match sequence3>;

忽略语句的原理是在同一个 lookup 下创建子表 subtable,并指示排版引擎仅匹配指定序列但不进行任何替换操作,由于例外规则位于正常规则之前,因此排版引擎会优先匹配例外规则,并跳过后面的正常替换规则(同一个 lookup 只会匹配一次 subtable)。

subtable 可以用关键字 subtable 在一个 lookup 中分隔。

举例:

  1. 忽略特定序列:下面的 ignore sub 规则将阻止任何后续规则对“d”进行替换,当“d”周围的上下文匹配“f a d”、“f e d”或“a d d”序列时。例外序列中标记的字形标记了替换本应发生的位置,因为 OT 引擎实际上是执行了一次无替换匹配。

    afdko
    ignore sub f [a e] d';
    ignore sub a d' d;
    sub [a e n] d' by d.alt;
  2. 匹配单词开头边界:

    afdko
    ignore sub @LETTER f' i';
    sub f' i' by f_i.begin;

    这里 @LETTER 定义为包含所有被视为单词组成部分的字形。替换语句仅在序列不匹配 @LETTER f i 时生效,即仅在单词开头处应用。

  3. 匹配整个单词边界:

    afdko
    ignore sub @LETTER a' n' d', a' n' d' @LETTER;
    sub a' n' d' by a_n_d;

    在此示例中,a_n_d 连字符仅在“a n d”序列前后均不存在 @LETTER 时生效。另请注意 ignore 语句中逗号的使用:这等同于书写:

    afdko
    ignore sub @LETTER a' n' d';
    ignore sub a' n' d' @LETTER;
    sub a' n' d' by a_n_d;

    注意,这种写法并不等同于

    afdko
    ignore sub @LETTER a' n' d' @LETTER;
    sub a' n' d' by a_n_d;

    以上写法意味着仅忽略 and 前后同时存在 @LETTER 的情况。

  4. 这展示了上下文装饰笔画功能的规范:

    afdko
    feature cswh {
    
        # --- Glyph classes used in this feature:
        @BEGINNINGS = [A-N P-Z Th m];
        @BEGINNINGS_SWASH = [A.swash-N.swash P.swash-Z.swash T_h.swash m.begin];
        @ENDINGS = [a e z];
        @ENDINGS_SWASH = [a.end e.end z.end];
    
        # --- Beginning-of-word swashes:
        ignore sub @LETTER @BEGINNINGS';
        sub @BEGINNINGS' by @BEGINNINGS_SWASH;
    
        # --- End-of-word swashes:
        ignore sub @ENDINGS' @LETTER;
        sub @ENDINGS' by @ENDINGS_SWASH;
    
    } cswh;

    若某特性仅针对单词开头或结尾处的字形(如 initfina 功能),则可由应用负责检测单词边界;特性本身仅需定义为相应的替换操作,无需考虑单词边界。

    这就是说,因为 initfina 的特性标签已经在注册表中,因此只需要写

    afdko
    feature init {
        sub a by a.init;
        sub b by b.init;
    } init;

    而不需要

    afdko
    feature init {
        sub @LETTER a' by a.init;
        sub a' @LETTER by a.init;
    } init;

扩展替换

罕用。现代字体编译工具(如 fontmake、AFDKO)通常会自动决定是否需要使用扩展格式。

反向链式单字替换

关键字为 rsub ,罕用。

定位规则

字形定位通过值记录 value record 和锚点 anchor 指定,跟随在作用字形之后。

单字调整定位

afdko
pos <glyph|glyphclass> <valuerecord>;

此处 <glyph|glyphclass><valuerecord> 进行调整。例如,要将字形左右侧距各减少 80 个设计单位:

afdko
pos one <-80 0 -160 0>;

字偶调整定位

特定字符与字符对的字距调整

形式 A

afdko
pos <glyph1|glyphclass1> <valuerecord1> <glyph2|glyphclass2> <valuerecord2>;

其中 <valuerecord1> 是第一种形式值记录。

例如:要调整 Ta 组合的 kern 为 -100(相较于默认字距缩短 100),可以使用下面这种特殊形式

afdko
pos T -60 a <-40 0 -40 0>;

这条规则将 T的 X 进距左移了 60 个单位(T 与书写下一个字母的光标减少 60),同时又把 a 的字形位置左移了 40 个单位(a 与书写光标位置减少 40),这就意味着Ta的字距较默认情况缩短了 100 单位。

形式 B

afdko
pos <glyph|glyphclass1> <glyph|glyphclass2>
         <valuerecord1>;

是对形式 A

afdko
pos <glyph1|glyphclass1> <valuerecord1> <glyph2|glyphclass2> <valuerecord2>;

的简写,其中 <valuerecord2> 为空。例如,

afdko
pos T a -100;        # specific pair (no glyph class present)
pos [T] a -100;      # class pair (singleton glyph class present)
pos T @a -100;       # class pair (glyph class present, even if singleton)
pos @T [a o u] -80;  # class pair

在 kern 特性中,特定字形对应该置于字形类对之前,这与它们在字体中的存储顺序一致。否则,根据具体实现方式,任何类对之后的特定字形对可能永远不会被应用。

枚举对

枚举对允许把类对展开为特定对,避免将其存储到 class kerning 子表中。

pos 前添加 enum 关键字实现枚举对,例如:

afdko
@Y_LC = [y yacute ydieresis];
@SMALL_PUNC = [comma semicolon period];

enum pos @Y_LC semicolon -80;     # class pair expanded to specific pairs
pos      f quoteright 30;         # specific pair
pos      @Y_LC @SMALL_PUNC -100;  # class pair

其中 enum pos @Y_LC semicolon -80 规则等价于

afdko
pos y semicolon -80;
pos yacute semicolon -80;
pos ydieresis semicolon -80;

注意,第一条规则产生了 pos y semicolon -80; 而第三条类对规则包含pos y semicolon -100;,按照规则应用顺序,后者会被忽略。

子表分隔符

类对规则的重叠会造成某些问题。这里太复杂不展开讨论。

曲线连接定位

略,主要应用于字形连笔。连笔和连写 liga 的区别在于,前者仍然是两个字形通过定位上的叠合实现视觉的连笔形状,而后者是被替换成一个连写字形。

标记-基底连接定位

略,主要应用于带标记的字母系统中标记符与主字符之间的连接。

标记-连写连接定位

标记-标记连接定位

上下文定位

是链式上下文定位的功能子集,见下文

链式上下文定位

链式上下文定位规则和链式上下文替换规则完全一致,唯一的差异是关键字由 sub 改为 pos

指定链式定位规则和标记子序列

链式定位规则的目标序列包含三个部分:前缀序列(上文)、输入序列、后缀序列(下文)。每个序列由一个或多个字形或字形类构成。

最重要的输入字形序列。这是应用替换操作的字形及其类别的序列。可选地,可指定前缀(也称为回溯)字形序列以及后缀(也称为前瞻)字形序列。整个字形序列——前缀+输入+后缀——必须与当前上下文完全匹配才能应用规则。匹配序列通过将输入序列的首个字形与当前处理文本的字形对齐来与上下文关联。若规则匹配成功,则当前上下文将原文本中的字形指针向前移动输入序列的长度。

需注意在 FEA 语法中,整个上下文字符串(回溯序列+输入序列+前瞻序列)均按文本字符串顺序书写。这一点值得强调,因为在查找规则内部,回溯序列的字形书写顺序与待匹配文本相反。了解此特性的字体编辑工具开发者有时会因 FEA 语法而产生困惑。

对于输入序列中的每个字形或字形类,上下文规则可以指定一个或多个要在该位置应用的查找。注意,指定的查找可能包含许多规则;实现必须确保在输入序列的该位置被引用的查找中的只有一个规则会匹配。不能为回溯和前瞻序列中的字形或字形类指定查找。

输入序列的定义是在输入序列内的所有字形名称和类名称后附加标记字符 ' 。将标记置于其他标识符后会导致语法错误。

使用显式查找引用指定上下文定位

上下文替换规则的最通用形式是在规则中显式引用命名查找。

例:

定义两个独立查找,然后在上下文定位规则的输入序列中使用关键字 lookup 和查找名称来引用它们。

afdko
```arkClass [acute grave] <anchor 150 -10> @ALL_MARKS;

lookup CNTXT_PAIR_POS {
     position T o -10;
     position T c -12;
 } CNTXT_PAIR_POS;

lookup CNTXT_MARK_TO_BASE {
     position base o <anchor 250 450> mark @ALL_MARKS;
     position base c <anchor 250 450> mark @ALL_MARKS;
 } CNTXT_MARK_TO_BASE;

feature test {
     position T' lookup CNTXT_PAIR_POS [o c]' @ALL_MARKS' lookup CNTXT_MARK_TO_BASE;
 } test;

这条规则只有输入序列,没有回溯或前瞻序列。它将匹配当前字形是 'T',后跟 'o' 或 'c',再后跟任何标记字形。查找 CNTXT_PAIR_POS 将应用于 'T',查找 CNTXT_MARK_TO_BASE 将应用于 @ALL_MARKS 类中的字形。

这种上下文定位规则的形式最灵活。你可以为输入序列中的多个字形或字形类指定定位查找,引用的查找可以是不同类型,引用的查找可以具有与父上下文查找不同的查找标志。缺点是很难理解将应用什么定位规则,并且如果引用的查找不包含匹配上下文的规则,实现可能不会发出警告。

当仅需为输入序列中的单个字形或字形类指定定位规则,且所引用的查找项与父级上下文查找项具有相同查找标志时,可采用内联定位规则的方式指定上下文规则。此种方式更易于理解。


使用内联单字定位规则指定上下文定位

例 1:

afdko
```osition [quoteleft quotedblleft ][Y T]' <0 0 20 0> [quoteright quotedblright];
position [quoteleft quotedblleft ][Y T]' 20 [quoteright quotedblright];

这两条规则的输入序列指定的都是单个字形类 [Y T] 的定位,后跟一个值记录。第一种形式显示四元组值记录,允许你更改原点的 (x,y) 坐标和前进距离的 (x,y) 坐标。第二条规则显示值记录的简单形式,仅指定 x 进距。这两条规则都将 Y 或 T 的前进宽度增加 20,它介于 quoteleft 或 quotedblleft 和 quoteright 或 quotedblright 之间时。注意,输入序列中并非所有标记的字形或字形类都必须后跟值记录;如果省略,则说明其定位不变(空值记录)。

例 2:

afdko
```osition s f' 10 t;
position s f' 10 t' -5 period;

第一条规则指定了若 ft 的前缀为 s,则将 f 的 x 进距增加 10。第二条则指定了若 ft. 的前缀为 s 则 f 的 x 进距增加 10 且字母 t 的 x 进距减少 5。整组标记字形将被规则完全处理:第一种情况匹配规则后,当前查找规则集将从字形"t"开始继续应用;第二种情况则从字形"句号"开始应用后续规则。

上下文字偶距调整的特别说明

上下文定位规则必须与字偶定位规则位于不同的查找中,因为规则属于不同的查找类型。由于每个查找独立于其他查找应用在整个文本流上,因此当两条规则匹配文本流中的同一字偶对时,字偶距调整规则中指定的定位更改将添加到上下文字偶距调整规则中指定的定位更改。可以通过指定上下文字偶距调整规则值来管理这种效果,使字偶定位规则值和上下文定位规则值的总和等于所需值,如例 3A 所示。

例 3A:

afdko
position L quoteright -150;
position quoteright A -120;
position L' 50 quoteright' 70 A;

期望的最终调整:L' -100 quoteright' -50 A;

在此示例中,三元组 "L quoteright A" 的预期调整是:当 L 后跟 quoteright 时,L 的前进宽度调整 -100;当 quoteright 后跟 A 时,quoteright 的前进宽度调整 -50。但是,由于字偶定位规则将字偶 "L quoteright" 调整 -150,字偶 "quoteright A" 调整 -120,因此三元组的上下文规则中的调整值必须如所示设置。这种方法是可行的,但难以理解。

为了避免出现这种困惑,可以通过标记每个字偶定位规则的第一个字形或字形类,将所有字偶定位调整转换成上下文定位。这样使得所有规则均属于同一个查找,因此在任何上下文中只会匹配一个规则,无需在意规则相加的复杂关系。此解决方案在例 3B 中使用上下文定位的特性文件语法显示。但是,由于同一查找的规则应用优先级,必须在其他两条规则之前定义三元组规则。否则,字偶定位规则将提前匹配从而妨碍三元组的匹配与定位调整。

例 3B:

afdko
position L' -100 quoteright' -50 A;
position L' -150 quoteright;
position quoteright' -120 A;
position s f' 10 t period;

特性文件语法提供一种特殊的上下文规则,它包含一个输入字形或字形类,后跟后缀字形或字形类,以及一个值记录。这将被视为上下文字偶定位语句,也是值记录可以跟随未标记字形的唯一情况。因此,例 3B 可以写成例 3C。两个示例完全等价。

例 3C:

afdko
position L' -100 quoteright' -50 A;
position L' quoteright -150;  # 上下文定位的特殊情况
position quoteright' A -120;  # 值记录跟随未标记字形,
position s f' t 10 period;    # 使它们与 3B 完全等价。

注意,例 3D 不是字偶距调整语句,且报错:

示例 3D

afdko
position L' quoteright' -150;

它有两个问题。首先,它会减少 quoteright 的 X 进距,而非 L。其次,它会将当前字形指针向前移动 2 个字形,跳过 quoteright,使 quoteright 不会被检查是否匹配调整规则。

FEA 语法不允许在一个上下文规则中应用不同类型的定位查找。例如,如果你想在 lam_meem_jeem 后跟 alef 时将 sukun 定位在 lam_meem_jeem 上方,并在此上下文中调整 lam_meem_jeem 与 alef 的字偶距,则需要将 mark 和 kern 规则放在不同的查找中。

示例 4

afdko
markClass sukun <anchor 0 0> @TOP_CLASS;

lookup MARK_POS {
    position base lam_meem_jeem' <anchor 625 1800> mark @TOP_CLASS alef;
} MARK_POS;

lookup MARK_KERN {
    position lam_meem_jeem' 5 @TOP_CLASS alef;
} MARK_KERN;

查找 MARK_POS 中的规则将在 lam_meem_jeem 后跟 alef 时将 sukun 定位在 lam_meem_jeem 上方。第二条规则将在 lam_meem_jeem 后跟 sukun 然后 alef 时,将 lam_meem_jeem 的前进宽度增加 5。

该示例没看懂,略

例 6:

afdko
lookup a_reduce_sb {
    pos a <-80 0 -160 0>;
} a_reduce_sb;

lookup a_raise {
    pos a <0 100 0 0>;
} a_raise;

feature kern {
    pos a' lookup a_reduce_sb lookup a_raise b;
} test;

在此示例中,kern 特性中的规则将匹配序列 "a b" 并将多个查找应用于输入 "a"。第一个查找将从 "a" 的 x placement 减去 80 单位,从 x advance 减去 160 单位。第二个查找将 "a" 的 y placement 调整 100 单位。

例 7:

afdko
lookup REPHA_SPACE {
    pos ka-gran <0 0 644 0>;
} REPHA_SPACE;

lookup ANUSVARA_SPACE {
    pos ka-gran <0 0 510 0>;
} ANUSVARA_SPACE;

lookup ADD70 {
    pos ka-gran <0 0 70 0>;
} ADD70;

lookup ADD_ADVANCE_WIDTH {
    pos ka-gran' lookup REPHA_SPACE lookup ANUSVARA_SPACE lookup ADD70 repha-gran anusvara-gran;
    pos ka-gran' lookup REPHA_SPACE lookup ADD70 repha-gran;
    pos ka-gran' lookup ANUSVARA_SPACE lookup ADD70 anusvara-gran;
} ADD_ADVANCE_WIDTH;

feature dist {
    lookup ADD_ADVANCE_WIDTH;
} dist;

在此示例中,ADD_ADVANCE_WIDTH 中的规则将匹配序列 "ka-gran repha-gran anusvara-gran"、"ka-gran repha-gran" 或 "ka-gran anusvara-gran" 并将多个查找应用于输入。这里每个查找根据后续字形向 "ka-gran" 添加前进宽度。

使用内联连写定位规则指定上下文定位

使用内联标记附着定位规则指定上下文定位

指定链式定位规则的例外

链式上下文定位规则的例外通过在链式上下文规则之前的任何位置插入以下形式的语句来表达,并且在同一查找中:

afdko
ignore position <marked glyph sequence> (, <marked glyph sequence>)*;

此规则的工作方式与指定链式上下文替换规则的例外完全相同。

返回

人同此心,心同此理;如风沐面,若水润心