七种武器之pic

《More Programming Pearls》在第九章介绍了一种名为pic的小型语言(little language),它和graphviz一样也是贝尔实验室的产物,作者是大名鼎鼎的K!所谓小型语言,就是你可以在一小时内学会并使用之,下面我们争取能达到这个目标。

pic也是troff的预处理器,Linux都会自带这个小工具。它的作用仅仅是把一段文本中标识符.PS.PE之间的指令,即pic语言,解释成troff语言,最终实际是由troff绘图。另外还有两个类似pic的预处理工具是eqn和tbl,顾名思义分别是用作绘制公式和表格的。这里有一份关于troff家族的全家福

pic语言的基本理念是:

  1. 想象在图纸上有一个绘图的光标;
  2. 逐行读入指令,光标按照指令从(from)当前位置向(to)某方向移动或绘图;
  3. 下一条指令如果不明确重置光标,则是在上一条指令的光标位置和方向的基础上继续执行,直到绘图结束。

先给一个十边形连通图的例子:

pic example

用如下代码绘制:

.PS
pi = 3.1415926; n = 10; r = 1; s = 2*pi/n
for i = 1 to n-1 do {
    for j = i+1 to n do {
        line from r*cos(s*i), r*sin(s*i) to r*cos(s*j), r*sin(s*j)
    }
}
r1 = r+0.1
for i = 1 to n do {
    sprintf("%g", i) at r1*cos(s*i), r1*sin(s*i)
}
.PE

pic是以英寸为绘图单位的,下面是各种形状的默认大小:

形状 大小 形状参数
box 3/4”宽 x 1/2”高 width, height
circle 1/2”直径 diameter
ellipse 3/4”宽 x 1/2”高 width, height
arc 1/2”半径 radius
line/arrow 1/2”长 up, down, left, right
move 1/2”平移距离 up, down, left, right

记住每绘制完一个形状后光标所处的位置,有助于更好的使用pic绘图。

下边是一些预定义变量的默认尺寸值:

boxwid = 0.75;         boxht= 0.5
linewid = 0.75;        lineht= 0.5
circlerad = 0.25;      arcrad= 0.25
ellipsewid = 0.75;     ellipseht= 0.5
movewid = 0.75;        moveht= 0.5
textwid = 0;           textht= 0
arrowwid = 0.05;       arrowht= 0.1 (These refer to the arrowhead.)
dashwid = 0.05;        arrowhead= 2 (Arrowhead fill style)
maxpsht = 8.5;         maxpswid= 11 (Maximum picture dimensions)
fillval = 0.3;         scale= 1

pic默认每行是一条指令,也可以使用分号分隔各条指令。如果使用大括号{},则表示其中指令执行完毕后,不改变光标的位置和方向;如果使用中括号[],则表示其中的形状是一个整体,称作block。

一条指令可以用于定义一个形状,例如:

line up 1 right 2
arrow "on top of" above
box invis "input"
box dotted height 0.2 width 0.2 at 0,0
box same
arc -> from 0.5,0 to 0,0.5
arc -> cw from 0,0 to 2,0 radius 15

使用 ljustrjustabovebelow 可以修改文字的默认位置。

pic中也可以使用标记(label)来引用一个形状,不过为了变量区分,标记需要大写字母开头。例如:

Box1: box + 1,1

pic中形状有边角点(corner)的概念,分为8个方向点,外加中心点,分布是东 .e、南 .s、西 .w、北 .n、东南 .se、东北 .ne、西南 .sw、西北 .nw 和中心 .c。边角点一般搭配标记使用以获取坐标,例如,Box1.se 表示该形状右下角那个点的坐标。边角点也可以搭配with属性使用以绘制形状,例如,box with .sw at 1,1 表示以左下角的坐标绘制形状。

使用 1st2ndlast 这样的标识可以用来引用相应的形状,包括block,甚至是引用形状的边角点。例如,last box.nw 表示获取上一个box形状的左上角坐标

fromto指令通常是搭配线条形状使用的,不过此时如果直接连接形状的话,默认使用的是形状的中心坐标,所以一般还会搭配chop属性使用,这样会从形状的边界开始连线。比如:

arrow from 1st circle to 2nd circle chop

pic可以说是面向对象的,每个形状都是一个对象,且有各种成员属性。例如:

Box1.x              # the x coordinate of the center of Box1
Box1.ne.y           # the y coordinate of the northeast corner of Box1
Box1.wid            # the width of Box1
Box1.ht             # and its height
2nd last circle.rad # the radius of the 2nd last circle
last [].A           # label A of last block

同时提供如下内置函数:

sin(expr), cos(expr), atan2(y,x)   # angle in radians
log(expr), exp(expr)               # Beware: both base 10
sqrt(expr),max(e1,e2 ),min(e1,e2)
int(expr)                          # integer part of expr
rand()                             # random number between 0 and 1

pic中甚至还有宏,主要用于替换文本,而且宏可以有可选参数。例如:

define square { box ht $1 wid $1 $2}

宏还可以搭配文件操作使用。例如,文件里逐行保存着一些坐标点,可以用如下语句绘制这些坐标点:

copy /path/to/file thru { "." at $1, $2 }

pic还支持for循环和if判断。例如,绘制一条分段函数:

pi = atan2(0,-1)
for i = 0 to pi by 0.1 do {
    if (s = sin(i)) > 0.8 then { s = 0.8 }
    "." at i/2, s/2
}

不过pic里不处理各种字体、字号等内容,而是交由troff处理,所以,如果对文本格式有要求,需要进一步参考troff的语法。

troff里处理文本格式常用的一些标记有:

  • \f 表示字体。比如,\fB表示粗体,\f(BI表示粗体+斜体,\fR表示标准的Times Roman。
  • \d\u 分别表示文字下沉和上升,可以用来表示上下缀。比如,X\d1会显示为 X1
  • \s 表示字号。比如,\s12表示12pt,而\s0表示恢复到之前的字号,也可以用\s+2表示增大字号。

由于eqn是可以预处理公式的,因此,也可以在pic里使用公式。语法和TeX类似,但命令无需反斜杠开头,具体可参考eqn手册。示例:

box "$space 0 {H( omega )} over {1 - H( omega )}$"

不过由于eqn只处理.EQ.EN之间的文本,所以,需要利用所谓的内嵌公式功能,需要在文本开头声明内嵌文本标记,如下:

.EQ
delim $$
.EN

运行如下命令即可画出带公式的图形:

$ cat foo | pic | eqn | groff > foo.ps

更详尽的内容可以参考manual,一共才20来页,囊括了pic的所有内容。此外,别忘了我们同样也可以开发pic的预处理器,比如chem这个小工具就是可以解释一段化学公式描述,然后再交由pic处理,MPP这本书里还有其他一些例子值得我们思考学习。

参考