为 Glyphs 编写脚本(二)

教程
作者:Rafał Buchner & Rainer Scheichelbauer
en zh

 

第一篇中,我们学习了如何输出字体和字符形的一些信息。现在我们想要更进一步,实际地操作字体。所以,给你最喜欢的字体作品留个备份,再热热身准备开始 Python。

本教程默认你已经事先读过为 Glyphs 编写脚本(一)了。

字符形和图层

我们首先备忘一下 Glyphs 是如何安排字母的。所有的路径、锚点、辅助线等等,任何你在绘制和编辑字母时所处理的东西,所有这些都不仅仅是一个字符形中所包含的属性,而是属于字符形所拥有的某个图层。既可以是手动插入的图层,也可以是母版层(用于插值的那些)。

所选图层

现在,在我们做什么之前,我们需要确定当前所选的是哪个图层。如下操作。打开宏面板并输入:

print( Glyphs.font.selectedLayers )

你会得到类似这样的内容:

(
    "GSLayer <0x7fe471526320>: [Bold Italic] (A)",
    "GSLayer <0x7fe47152fbb0>: [Bold Italic] (B)",
    "GSLayer <0x7fe471530d30>: [Bold Italic] (C)"
)

所以我们在这里做了什么?首先,我们取出 Glyphs 对象,找到它的 font 属性,这样会获得当前字体(窗口最前的)。如果没有字体打开,那么它会报错。确实,结果(‘GSLayer…’)不太好看懂,不过这就是 Glyphs 记录图层的方式:一个十六进制的 ID 编码,后接图层名称,以及所在的字符形名称。

幸运的是,我们不需要直接处理这些东西。我们可以让 Python 帮我们做这项工作。所以,我们的第一行会是:

myLayers = Glyphs.font.selectedLayers

获取路径

我们来稍微延展一下:

myLayers = Glyphs.font.selectedLayers
for thisLayer in myLayers:
    print( thisLayer.parent.name )
    print( thisLayer.paths )

在宏面板中运行这段代码,你会在结果区得到像这样的内容:

A
(<GSPath 57 nodes and 29 segments>, <GSPath 10 nodes and 4 segments>)
B
(<GSPath 53 nodes and 23 segments>, <GSPath 42 nodes and 16 segments>)
C
(<GSPath 57 nodes and 23 segments>)

那么发生了什么呢?我们从 font 中获取了 selectedLayers,所以我们得到了用户选择的图层,把交给了变量 myLayers

第二行里,我们在 myLayers 中的所有图层间循环。对于每一个图层,我们这样做:首先,将其称作 thisLayer,然后我们让 thisLayer 告诉我们它的父级 parent 的名称 name 并输出 print 到宏面板中,最后,我们再让 thisLayer 告知它所拥有的路径 path 并将结果也输出到宏面板中。然后循环在 myLayers 中的下一个图层继续,所有工作重复一遍,直到 myLayers 中的最后一个图层完成了这一程序。

这里我略过了一件事:“父级” parent。还记得我们怎样获得一个对象的子对象吗?在后面添加句点,再接子对象名称。如果我们想知道 Glyphs 中当前打开的所有字体,我们会输入 Glyphs.fonts。如果我们想知道当前打开的第一个字体的家族名称,我们输入 Glyphs.fonts[0].familyName,等等。这样,我们可以沿着对象树向下钻。不过有时,我们已经有了一个对象,需要在对象级别中向上钻一层。这种情况下,我们指的就是这个对象的父级 parent

本例中,我们有一个图层,它被保存在 thisLayer 中,我们想要知道这个图层所属于的字符形,所以我们让它告知其父级:thisLayer.parent。最后,我们让父级字符形告知其名称,thisLayer.parent.name,并将结果传递给 print 命令。

最后一行对我们同样重要,因为这里我们就拿到了终极目标——构成我们字体的那些路径。我希望你们确实在字体的副本中做这些事,因为我们在下一步中就会对这些路径做出很可怕的事情。我已经警告过你了!

路径和节点

好,我们想在所选图层上做点疯狂的事情。我们来让其中的节点随机地向左右偏移。那么我们要做的就是:让 Python 随机产生一些正负数,加在第一个所选图层的第一个路径的第一个节点的 x 坐标上。然后我们循环这一程序,在所有其他节点、路径、图层上。

Python 有一个随机数生成器,但是它需要一份特殊的 “邀请函” 才能参加我们这场派对。邀请函被称作 “引入一个模组” 或 “引入一个库”。所以我们首先引入随机数生成器:

import random

再告诉你一个秘密,每次你引入随机数生成器 import random 时,最好调用随机数生成器的“seed”函数:

random.seed()

注意到 seed 后面的括号了吗?圆括号标志着所谓的方法(method)。对,对象可以拥有其他对象,也可以拥有方法(或称 “函数” function)。方法会对它所跟随的对象做出一些事情,比如 seed() 函数会告诉随机数生成器不要有所顾虑,要产生真正的随机数。如果不这样做,随机数生成器可能会重复给你相同的数值,我们当然不希望这样。

好,现在该遍历全部所选图层中所有路径的所有节点,让它们完成所需的程序:

import random
random.seed()

myLayers = Glyphs.font.selectedLayers
for thisLayer in myLayers:
    for thisPath in thisLayer.paths:
        for thisNode in thisPath.nodes:
            thisNode.x += random.randint( -50, 50 )

除了最后一行,其他内容应该都很清楚了。变量 thisNode 控制某个路径上的节点或手柄。在上面几行中,我们已经循环了所有的节点。当然了,属性 x 便是这一点的 x 坐标。

运算符 += 表示 “在左边的变量中加上跟在右边的内容”,例如 x+=5 会为 x 增加 5。所以 += 也被称作增量运算符(increment operator)。这种方式更简短也更高效,等价于 x=x+5

现在,来看随机部分。首先,我们使用 random——在前几行中引入的模组。然后我们调用其中的一个方法,名为 randint(),即随机整数(random integer)的简称。和其他方法一样,randint() 后面也有一对圆括号。你可以在圆括号中给这个方法一些数值,这样就可以根据输入的内容获得相应的结果。方法 randint() 接受逗号分隔的两个数值,最大值最小值,并返回两者之间的随机整数。我们使用 -50 作为最小值、50 作为最大值交给 randint(),希望它返回一个区间内的随机数。

所以简而言之,最后一行做了这件事:取每个点的 x 坐标,为其加上 -50 和 50 之间的一个随机数。没错吧?

到此为止都很清楚吧?那么下一个是什么?让我们把脚本变成 “脚本” 菜单下的一个条目,这样我们就可以随时调用它了。怎么做呢?阅读为 Glyphs 编写脚本(三)来继续了解。


2014-10-04 更新:添加(一)和(三)的链接。
2016-12-08 更新:修正了一些格式,删掉了一个过时的警告,添加了关于 help() 的专业提示。
2019-02-06 更新:细小的文字和格式调整。
2019-02-12 更新:修正录入错误。
2020-12-02 更新:更新到 Python3.

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