OpenType 特性(四):位置变体

教程
作者:Rainer Erich Scheichelbauer
en fr zh

9 一月 2017 发布日期: 23 四月 2014

手写字体最棒的一点在于,它会根据不同的位置出现不同的替换字符形,以及触发这一机制的正确 OpenType 特性。在这篇教程中了解如何实现。

对于手写字体或类似的设计而言,一个主要的问题就是,一个字母会根据它在单词中的相对位置而改变造型。总的来说,我们可以分出四种位置:

  • .init:词首形式(initial form),字母位于单词之首
  • .medi:词中形式(medial form),字母位于单词中间
  • .fina:词尾形式(final form),字母位于单词末尾
  • .isol:独立形式(isolated form),字母独立存在

四字母代码 initmedifinaisol 是相应 OpenType 特性的名称。你可能已经猜到了:你可以通过将这些名称作为后缀名添加给字符形,从而触发这四种特性的自动生成。例如,如果你的字体中存在 adieresisadieresis.init,那么 Glyphs 就可以自动为你生成 init 特性。你只需要在 “文件 > 字体信息 > 特性” 中点击窗口左下角的刷新按钮(圆形箭头标志),再重新导出字体即可。

只有一个问题:几乎没有哪个应用程序会默认使用这一特性。比如,在 InDesign 里,你需要在 OpenType 选项中启用 “自动位置变体”。许多用户仍然不知道如何处理 OpenType 字体,就会给你发送求助邮件。毕竟,对于他们而言,他们花了辛苦钱购买的字体却似乎并不好用。只要一句 “启用自动位置变体” 就能将恐惧带入某些人的内心。

注意:四种位置变体的 OpenType 特性 init, medi, fina, isol 仅用于内含 “连写行为” 的书写系统,例如阿拉伯文或类似的文字。这些特性无法用于拉丁字母、希腊字母、西里尔字母、希伯来字母等,即便是你想制作一款连笔字体。换句话说,如果 Glyphs 在 “文件 > 字体信息 > 特性” 中为你自动生成了这些特性,请考虑勾选相应的 “停用” 复选框来禁用它们,或在左侧边栏中选中后,点击左下角的减号按钮将其删除。

所以,为了帮你节省大量时间和精力,我们需要找到一种能让位置变体自动出现的办法。说来也巧,正有这样一种办法。请继续吧。

默认使用词中形式

现在,对于本篇教程,我将预设两个假定:首先假设你的默认字符形为词中形式,其次假设每个字母都有四种造型。这意味着,字体中有像 abc 这样的默认字符形,它们都是词中形式,各自又有三种带着 .isol.init.fina 后缀的变体。如果这不符合你的设计,你可能需要根据情况调整代码,但是读过本文后,这应该会很简单。

创建 OpenType 类

我认为这样就很清楚了。我们需要四个类:

  • Isolated 用于带有 .isol 后缀的独立字符形
  • Initial 用于带有 .init 的词首形式
  • Medial 用于不带后缀名的默认中间形字母
  • Final 用于带有 .fina 后缀名的词尾形式

我们从 Isolated 开始:这一个很简单我们只需要在字体视图右下角的搜索框中输入 .isol。Glyphs 会将选择范围缩小为仅在名称中包含 .isol 的所有字母。我们现在需要做的,就是全选所显示的字符形(Cmd-A),在右键菜单中选择 “复制字符形名称 > 空格分隔”。

现在,我们的剪贴板里就已经储存了全部 .isol 字符形的名称。所以,前往“文件 > 字体信息 > 特性”,点击左下角的加号按钮,在弹出的菜单中选择 “类”。左侧边栏中会出现一个新的 OpenType 类,我们只要把它的名称改为 Isolated 即可。然后,我们将字符形名称粘贴进代码区域。

你的类代码看上去应该如下图所示:

A.isol B.isol C.isol D.isol E.isol F.isol G.isol H.isol I.isol J.isol K.isol L.isol M.isol N.isol O.isol P.isol Q.isol R.isol S.isol T.isol U.isol V.isol W.isol X.isol Y.isol Z.isol a.isol b.isol c.isol d.isol e.isol f.isol g.isol h.isol i.isol j.isol k.isol l.isol m.isol n.isol o.isol p.isol q.isol r.isol s.isol t.isol u.isol v.isol w.isol x.isol y.isol z.isol

好多了。对于其余的类,我们的任务也清晰而简单:点击左侧边栏中的类名称 Isolated 并按下 Cmd-C,然后再按三次 Cmd-V,将 Isolated 类别复制粘贴三次。你会看到四个名为 Isolated 的类。将其中三个的名称改为 InitialMedialFinal。然后,点进每个新类,将 .isol 改为相应的后缀名,在 Medial 中则删去后缀名。你可以使用 Cmd-Shift-F 方便地完成这一操作,按下这一快捷键后会唤出 “查找和替换” 功能。将 .isol 后缀名替换为相应的后缀名称,或在 Medial 中,替换为空。

通过这种方式,使所有类处于同步。即,其字符形的数目和顺序都相同。这一点对于我们之后要写的替换代码而言至关重要。

顺便一提,你只需拖拽侧边栏中的类名称,即可调整它们的顺序。顺序对于特性的生效而言并不重要,但可以便于你的管理。

还有一个类需要设置。我们需要一个名为 AllLetters 的类,通过 “文件 > 字体信息 > 特性” 中的加号菜单添加,并确保 “自动生成特性” 复选框被勾选。这样,我们便获得了字体中所有字母名称的完整列表,也包括位置变体。为什么要这样?因为,比如说,词首形式的特性可能会写成类似这样:“将一个默认形式的字母替换成词首形式,除非它的前面有任何一个字母”。

显然,AllLetters 类别会比我们之前创建的位置变体类别都要大。所以,其中的字符形数量和顺序都不能和其他类别相一致。

作为上下文替代字形的位置变体

我们现在需要做的,就是创建一个 “上下文替代字” 的特性,即 calt,因为我们想要的是根据其所处的上下文来替换字符形。所以,我们的第一步会是创建这一特性。同样,在 “文件 > 字体信息 > 特性” 中,点击左下角的加号按钮。不过这次,在弹出菜单中选择 “特性”。左侧边栏中会出现一个名为 xxxx 的新特性,名称已处于选中状态,所以我们可以方便地将其改为 calt,全小些字母:

按下回车键来确认。如果你的字体中已经有了其他特性,你也可以再点击 “更新” 按钮,这样 Glyphs 可以将特性重排为合适的顺序。和类不同,特性的顺序有所影响。除非你有充足的理由,我建议使用 Glyphs 提供的默认排序。

编写特性代码

还记得我是如何表述词首形式特性的吗:“将一个默认形式的字母替换成词首形式,除非它的前面有任何一个字母”。

好的,那我们来一点一点地构建这一特性。“将一个默认形式的字母替换成词首形式” 翻译过来,就是将 Medial 类替换为 Initial 类。所以,我们可以先写下这一行:

sub @Medial by @Initial;

请记住,在特性代码中,sub...by 意为 “将……替换为”,且类别名称需要在开头标注 @ 符号,才能被识别为类。每条规则都需要以分号结尾,否则就不算数。

到目前为止都不错。不过问题在于:这样会将所有默认字母(Medial)都替换为词首形式。而我们想要的是,只有在前面没有字母的时候才转换,换句话说,“除非前面有任何字母”。或者,再换句话说,“忽略其前面有字母的情况”。听上去不错,因为我们有一个类就叫做 “所有字母” AllLetters,并且 Adobe OpenType 字体开发套件(AFDKO)的特性文件语法中有一条 “忽略替换” ignore sub声明。我们就这样写:

ignore sub @AllLetters @Medial';
sub @Medial' by @Initial;

点击 “编译”,看一下是不是都写对了。

这里我需要重点说两件事:首先,ignore sub 声明优先于其后所跟的 sub 声明。这意味着后面的所有 sub 声明仅在 ignore sub 条件不满足时才生效。有点难办。现在还好,之后会让我们有点头疼的。继续读下去吧。

第二件事,看一下直单引号  '。在 ignore sub 规则中,我们声明例外的是 Medial 组中的字符形。在其后的 sub 规则中,我们需要再标记它一次。嗯,我想这里应该是比较清楚的,因为也没有别的东西被替换。但是我们还是要标记上它,因为这里的类别不需要和上面 ignore sub 声明中的一样。它们可以是两个不同的类,其中几个字符形相同。另外,上下文替换可以更复杂。

让我再强调一遍:你需要给 ignore subsub 声明中各自的字符形类添加单引号标记。如果你的 ignore sub 没有效果,你可能是漏掉了某个标记,或两者都没有添加。不要担心,我们之中最优秀的人也会遇到这种状况。

我们来看一下它是否已经有效果了。在新的 “编辑” 标签页中,只需键入几个 n,然后在窗口左下角的 “特性” 弹出菜单中选择 “上下文替代字” 即可。

好!它有效果了!所以理论上,我们只需要在 IsolatedFinal 中也做同样的事情就可以了,是不是?是不是?!

不,不完全是。我们都是最聪明的少年少女,都还记得 ignore sub 声明会对后面的所有内容生效。那么我们的三个 ignore sub 声明肯定会互相影响。那么我们怎么办?

Lookup 来救场!

事实上,我之前没有告诉你全部真相。一个 ignore sub 并不会对后面所跟的所有内容生效,而只限于在同一个 lookup 中的后面所有内容。

哈?lookup?lookup 是什么?嗯,在一个 lookup 中,你可以将一系列规则汇总到同一个名称下,并且如果你想,还可以在之后再次唤出它们。所以,我们需要把到目前为止所写的一切放在一个 lookup 中,并取个名字。我们这样做:

lookup INITIAL {
    ignore sub @AllLetters @Medial';
    sub @Medial' by @Initial;
} INITIAL;

我选择将这个 lookup 称作 INITIAL,原因显而易见。语法很简单:lookup,后接一个名称,再接一个前半花括号。然后,是该在这个 lookup 中的全部规则。你不需要做缩进,但如果你觉得这样更好看的话可以使用 Cmd-] 添加缩进。如果你又改变主意了,就用 Cmd-[ 来取消缩进。最后,我们还需要一个合上花括号,再重复一遍 lookup 名称,然后使用分号结束 lookup。

现在,我需要告诉你另一件重要的事情:在 lookup 段落之间,顺序是有意义的。就像特性的顺序一样。字体会遍览整个特性,直到找到和当前字符形相关的内容,然后就不会再查找别的东西了。

这就是为什么我们需要在 lookup 中首先出现最大的 ignore sub。本例中,这是用于独立形式的 ignore sub,因为我们需要略过该字母之前有东西的情况,以及在它之后又东西的情况。这就需要两个 ignore sub 规则。所以,我们要写下两个 ignore sub 声明吗?

lookup ISOLATED {
  ignore sub @Medial' @AllLetters;
  ignore sub @AllLetters @Medial';
  sub @Medial' by @Isolated;
} ISOLATED;

当然了,我们可以这么做,它也可以正确运行。不过,有些人会想要将略过的替换形式通过逗号分隔,像这样写在一行里:

lookup ISOLATED {
   ignore sub @Medial' @AllLetters, @AllLetters @Medial';
   sub @Medial' by @Isolated;
} ISOLATED;

我就是 “这些人” 之一。所以,我就让代码保持这样,嘿嘿。

既然这样,我们就可以添加 Final 替换了。这次,把这个 lookup 放到最后:

lookup ISOLATED {
   ignore sub @Medial' @AllLetters, @AllLetters @Medial';
   sub @Medial' by @Isolated;
} ISOLATED;

lookup INITIAL {
   ignore sub @AllLetters @Medial';
   sub @Medial' by @Initial;
} INITIAL;

lookup FINAL {
   ignore sub @Medial' @AllLetters;
   sub @Medial' by @Final;
} FINAL;

再提醒一下,千万确保你的单引号标记都是对的。我们都是标记在 Medial 类上的,ignore subsub 规则中都是这样。毕竟,Medial 类中包含我们的默认字符形,也就是它们需要根据所在位置被替换成别的形式。

好了,点击 “编译” 按钮。如果出现报错,仔细阅读报错信息。是不是漏掉了哪个分号?写错了一个类名称?忘记了 @ 标记?如果你无法找出原因, 在 Glyphs 使用手册中有一段冗长的错误排查内容:在 “处理错误”(Error Handling)章中,找到 “OpenType 特性代码”(OpenType Feature Code)一节。

如果你的特性完成编译,我们就可以在编辑选项卡中进行测试了。当我输入几次 n 时,就会出现:

看看左边独立的 n,以及右边连续的三个 n,每个造型都不一样:词首形式,词中形式,词尾形式。很酷,来击个掌,为了胜利!

可以用一个脚本来完成!

所以你已经读完了全部内容,也明白了 lookup 段落和 ignore sub 声明的本质。我猜你现在需要休息一会儿了。

那么就这样:我写了一个脚本,可以将位置变体的代码直接写入 calt。它会分析你的字符形结构,查找 .init.medi.fina.isol 的后缀名。最棒的一点是:它不需要你的四组位置变体保持同步。所以,类别和代码的结构看上去会有些微的不同,但基本想法是一致的。如果你重复运行脚本,它会更新类别和特性代码。

mekkablue 的 GitHub 页面下载 Glyphs 脚本库。将脚本放在 Glyphs 的脚本文件夹中(“脚本 > 打开脚本文件夹”,Cmd-Shift-Y),并重新载入脚本(按下 Opt 键:“脚本 > 重新载入脚本”,Cmd-Opt-Shift-Y)。然后你就能在“脚本”菜单中的某处找到 “Features > Build positional calt feature”(创建位置变体特性)命令。祝玩得开心。


2015-06-22 更新:更新到 Glyphs 2。
2017-01-09 更新:更正录入错误,更新截图,并明确 “AllLetters” 类(感谢 @Raseone)。
2019-10-30 更新:添加关于位置变体的 OT 特性的备注。

Chinese translation by Willie Liu (刘育黎) from 3type (三言).