《More Programming Pearls》在第九章介绍了一种名为pic的小型语言(little language),它和graphviz一样也是贝尔实验室的产物,作者是大名鼎鼎的K!所谓小型语言,就是你可以在一小时内学会并使用之,下面我们争取能达到这个目标。
pic也是troff的预处理器,Linux都会自带这个小工具。它的作用仅仅是把一段文本中标识符.PS和.PE之间的指令,即pic语言,解释成troff语言,最终实际是由troff绘图。另外还有两个类似pic的预处理工具是eqn和tbl,顾名思义分别是用作绘制公式和表格的。这里有一份关于troff家族的全家福。
pic语言的基本理念是:
- 想象在图纸上有一个绘图的光标;
- 逐行读入指令,光标按照指令从(from)当前位置向(to)某方向移动或绘图;
- 下一条指令如果不明确重置光标,则是在上一条指令的光标位置和方向的基础上继续执行,直到绘图结束。
先给一个十边形连通图的例子:

用如下代码绘制:
.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
使用 ljust、rjust、above 和 below 可以修改文字的默认位置。
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 表示以左下角的坐标绘制形状。
使用 1st、2nd、last 这样的标识可以用来引用相应的形状,包括block,甚至是引用形状的边角点。例如,last box.nw 表示获取上一个box形状的左上角坐标
from、to指令通常是搭配线条形状使用的,不过此时如果直接连接形状的话,默认使用的是形状的中心坐标,所以一般还会搭配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这本书里还有其他一些例子值得我们思考学习。
参考
- Brian W. Kernighan, PIC - A Graphics Language for Typesetting User Manual (1991)
- 很全的troff资源 Troff Resouces