PostScript,被遗忘的编程艺术
代尔夫特理工大学的 Alparon 研究小组旨在改进用于信息检索和信息存储对话的自动化语音处理系统。目前的重点是开放交通旅行信息研究项目的对话管理。该公司提供有关荷兰公共交通系统的信息,范围从当地巴士服务到长途火车。他们能够提供从荷兰任何地址到任何其他地址的最新旅行建议。去年,他们接到了超过 1200 万个信息咨询电话。
由于我们使用基于语料库的方法,我们分析了大量数据。由于数据量庞大,我们几乎以 Unix 方式完成所有操作:我们只使用 stdin 和 stdout,并且我们像 sed 一样运行我们的脚本(那些不会编程的人,编写 C/C++ 程序;那些会编程的人,尽可能坚持使用脚本。另请参阅参考文献中的白皮书)。基本上,我们用 Perl(及其小伙伴,如 awk、sed、tr、grep、find 等)来折磨我们的数据,直到它变成简单的形式,例如,在每一行你都有一个 x 值和一个 y 值。
虽然我们可以将其导入到一些花哨的演示程序中,但我们发现这些程序生成的 PostScript 文件通常很大。如果您只有几个图形,那可能还可以,但如果您有很多图形,您就会开始怀疑是否有更好的方法。当然有;您可以自己编写 PostScript,就像我经常做的那样。在一个 Perl 脚本中,我将 x-y 表格转换为 PostScript。由于 LATeX 需要边界框,我总是使 PostScript 符合 level-1 标准。
在本文中,我将为您提供一个关于如何编写符合 level-1 标准的 PostScript 的速成课程——足以让您制作自己的简单图形。我将从基本运算符开始,然后我们可以开始绘制线条、填充形状和绘制文本。之后,我将介绍符合标准的 PostScript 的描述和一个示例。我将向您展示如何绘制直方图,因为直方图具有所有方面:线条、形状和文本。
通常,当您希望学习 PostScript 时,您会阅读蓝皮书(参见参考文献)。如果您只是想了解足够满足您大部分需求的 PostScript,请继续阅读。PostScript 是一种图灵完备的堆栈语言。图灵完备部分(嗯,我是理论计算机科学家)意味着它与其他任何编程语言一样强大。堆栈部分意味着所有计算都在堆栈上进行。
例如,通过键入 gs 运行 Ghostscript(不是 Ghostview)。命令 pstack,基本调试工具,将显示当前堆栈。在提示符下输入 1 2 3 4 pstack,将显示一个新的堆栈。
当您键入堆栈运算符 pop 时,4 将从堆栈中弹出。接下来,键入 exch,2 和 3 将交换位置。另一个方便的堆栈运算符是 dup,它复制顶部元素。最后一个重要的堆栈运算符是 roll,它接受两个参数,比如 n 和 j。命令 n j roll(其中 n 和 j 由数字替换,当然)将堆栈顶部的 n 个元素旋转 j 次。因此,如果堆栈显示 1 3 2 2,命令 4 1 roll 输出 2 1 3 2。
PostScript 也具有所有正常的算术运算符,但由于它是一种堆栈语言,因此您以逆波兰表示法进行算术运算;即,运算符始终跟随参数。标准算术运算符是 add、sub、mul、div、idiv(整数除法)和 mod。PostScript 也具有几何、对数和指数函数。
如果您在堆栈上完成所有操作,PostScript 的效果最佳,但在某些情况下,这并不是特别方便。PostScript 也有变量,但它们比堆栈慢一点。当您开始编写自己的 PostScript 程序时,您通常会尝试使用变量来完成所有操作——这被认为是 坏事。通过一些练习,您将使用越来越少的变量。要给变量赋值,您需要键入
/PointsPerInch 72 def
这会将 72 赋值给名为 PointsPerInch 的变量。如果您使用 PointsPerInch,PostScript 会将其替换为 72。
在 PostScript 中,您还可以定义子程序。基本上,这与赋值变量相同,只是在这种情况下,值是用花括号括起来的代码块。例如
/Inch { PointsPerInch mul } def
PostScript 还具有超出本入门教程范围的流程控制命令。
是时候做一些更有趣的事情了。要画一条线,请输入
newpath 100 400 moveto 300 200 lineto 500 300 lineto stroke
此指令通过移动到点 (100, 400) 开始一条新路径,绘制一条线到点 (300, 200),然后到 (500, 300),最后绘制当前路径。如果没有 stroke,您将看不到任何东西。除了绝对位置,您还可以使用 rmoveto 和 rlineto 进行相对移动。线条的粗细可以通过 setlinewidth 选项控制,例如,要绘制一条发际线
0.01 setlinewidth填充形状也很容易。将 stroke 替换为 closepath fill;命令 closepath 将最后一个点与第一个点连接起来形成形状,fill 用当前颜色或灰度填充形状。在这种情况下,我们得到一个黑色三角形。如果您执行 0.9 setgray,填充颜色将是浅灰色(0 是黑色,1 是白色)。您还可以使用 sethsbcolor 或 setrgbcolor 选择颜色,但这些选项有点复杂。(有关更多信息,请参阅参考文献中的红皮书。)
放置文本需要一些初步准备:首先选择合适的字体
/Times-Roman findfont 10 scalefont setfont
选择 10 磅 Times-Roman 字体。其他著名的字体有 Helvetica 和 Courier。放置文本很容易;您移动到您想要文本的位置,添加括号以括起文本,并添加命令 show。如果文本包含括号或反斜杠,您必须通过在要转义的字符前插入反斜杠来“转义”它们。听起来很熟悉,对吧?所以这行
400 400 moveto (Hello World) show打印了您见过很多次的问候语。
如果您想在打印机上看到任何内容,还有一个最终命令至关重要:showpage。此命令将您的打印机的 PostScript 解释器制作的图片传输到纸上,然后清除内存,以便您可以开始新的一页。命令 run 可用于加载您的文件:(file.ps) run 执行名为 file.ps 的文件。
符合标准的 PostScript 无非是带有用于 PostScript 程序的特殊注释和布局指令的 PostScript,以便其他程序可以读取您的 PostScript 程序并对其执行某些操作。有许多可用操作的示例:反转页面、缩放页面、旋转页面、将两页或四页放在一页上等等。甚至程序 Ghostview 也使用符合标准的 PostScript,您看到的标题和页码是从 PostScript 注释中检索出来的。
由于这一切都是通过注释完成的,因此 PostScript 是否符合标准对您的打印机来说无关紧要——打印机跳过所有注释。但是,在 Unix 环境中,通常使用一个程序的输出作为另一个程序的输入。因此,很自然地不将 PostScript 文件视为终点站。在 Unix 环境中,符合标准的 PostScript 文件是必须的;否则,打印过滤器将无法处理这些文件。
在使用微软著名的文字处理器的微软环境中,PostScript 是一个终点站。生成的输出表明它是符合 level-2 标准的 PostScript,但不幸的是,它不是。使您的文档符合 level-1 标准(参见红皮书)甚至 level-2 标准(参见参考文献中的 DSC)的规则非常少,以至于您可能会怀疑它怎么会不符合标准。
我将坚持使用 level-1 标准(它比 level-2 更容易,因为 level-2 有更多的开销)。在红皮书中,level-1 标准的 PostScript 仅在八页中进行了描述;因此,如果您想了解所有的来龙去脉,它是 正确 的来源。
有三种类型的注释
标头注释位于任何 PostScript 代码之前。
正文注释主要用于标记页面的边界。
尾部注释在所有 PostScript 代码之后,通常用于描述某些从标头注释中尚不清楚的特性,因此它们被推迟了(例如,页数、要使用的字体和边界框)。
通常,PostScript 中的注释以 %(百分号)开头,但这些结构化注释以 %%(或 %!,如果是第一行)开头。%% 注释直接后跟一个关键字,表示其结构化注释的类型,如果需要参数,它们会跟在 :(冒号)后面,并用空格分隔。作为一个例子,让我们看一下以下模板(每行前面的数字仅供参考)
1 %!PS-Adobe-1.0 2 %%DocumentFonts: 3 %%Title: 4 %%Creator: 5 %%CreationDate: 6 %%For: 7 %%Pages: 8 %%BoundingBox: 9 %%EndComments第一行表明这些文件将符合 level-1 标准。一个常见的误解是人们认为 1.0 中的 1 表示符合标准级别,但事实并非如此。仅存在 level-1 和 level-2 标准,因此即使您看到 %!PS-Adobe-3.2,它也不符合 level-3 标准(它应该是 level-2 标准)。
第二行包含此文件中要使用的字体。一些程序发现在开始时知道它们应该加载哪些字体很方便。但是,当您创建一个生成 PostScript 的程序时,您通常此时并不知道这一点。此标头注释可以推迟到尾部注释。在这种情况下,您将必须将标头中行的 font1 font2... 部分替换为 (atend)。
第三行很容易;text 表示文档的标题。通常这是文件名,但不必如此。text 中的空格没有问题。第四行同样容易;这里的 text 应该替换为作者或创建此文件的应用程序。
在第五行中,text 应该是人类可以理解的日期和时间。第 6 行是可选的,text 应替换为预期收件人。如果不存在,则预期收件人为 Creator。
在第 7 行中,number 应反映文档中的页数。由于此数字通常事先未知,因此经常推迟到尾部注释。同样,在这种情况下替换为 (atend)。
第 8 行包含四个参数:左下角和右上角的 x 和 y 坐标。在多页的情况下,您应该使用边界框,以便所有页面都位于边界框中。要填写正确的值,您会发现 Ghostview 非常方便。边界框也可以推迟到末尾;再次,您将指定 (atend)。
最后,第 9 行结束标头部分。除了第 1 行和第 9 行之外,其他行的顺序可以随意选择。
在标头之后,您通常会看到一些变量和子程序的 PostScript 定义。这些变量旨在在 PostScript 程序的其余部分中保持不变。
接下来,是正文注释的时间。第一个正文注释是 %%EndProlog,它结束了程序的“不变”部分。大多数打印过滤器都保持到此行之前的所有内容不变。
每个页面前面都有(同样,行号仅供参考)
1 %%Page: 2 %%PageFonts: elm
第一行包含 labell 和 ordinal。label 应替换为不包含空格的字符串,以指示它是哪个页面。听起来有点奇怪,但这表示罗马数字是可以的,如果您在一页上有两页,那么 1,2 或 3,4 也是有效的页码。在我们的例子中,它只是一个普通的数字。ordinal 部分表示页码的值。具有两种类型的页码只是为了您的方便。
第二行是可选的,描述了输出中要使用的字体。如果不存在,则使用标头中为 DocumentFonts 指定的字体。
虽然不是必需的,但如果您只想创建一个页面,则下一个 PostScript 命令是 save,它会复制您到目前为止构建的环境。在页面末尾,您将找到以下行
restore showpage
此命令检索原始情况并打印页面。这意味着您在 save 和 restore 之间做的任何事情都是公平的,即,如果过滤器重新排序页面,您不会搞砸其他页面。
在最后一页之后,您有 %%Trailer。大多数过滤器都保持此行以及之后的所有内容不变。在某些 PostScript 程序中,这里会发生一些清理工作,但在大多数 PostScript 程序中,尾部注释直接跟随(同样,行号仅供参考)
1 %%DocumentFonts: 2 %%Pages: 3 %%BoundingBox:
在这三行中,只有那些在标头注释中推迟的行才应包含在此处的尾部中。
如果您想使您的 PostScript 符合 level-2 标准,您需要阅读 DSC(参见参考文献)。
第一步是折磨您的原始数据,直到您得到一个简单的表格。在实践中,您为此步骤使用 Perl 和朋友。为了演示,我将使用一个小表格
1993 9.0 8.6 1994 5.7 7.8 1995 6.4 7.1 1996 7.5 6.1 1997 8.4 5.9
此表具有 x、y 和 z。我想绘制的是 x 和 y 的浅灰色直方图,以及 x 和 z 的深灰色直方图。通常,您知道表格中的最小值和最大值,或者您只是使用 awk 单行命令来确定这些值。
下一步是开始使用 Perl 脚本 histogram.pl 的模板,它将生成 PostScript 文件。此模板在 清单 1 中显示。
关于最后一行边界框的说明。这是 A4 的大小(欧洲标准页面尺寸);对于 letter 尺寸,您需要 0 0 612 792。在稍后阶段,我们将更改此行,以便边界框更紧密地贴合。
运行脚本并将输出保存在 histogram.ps 中。启动 Ghostview 以查看此文件。没什么好看的,对吧?是时候编辑 histogram.ps 了。使用此文件进行一些实验比直接更改 Perl 文件更容易(尤其是在稍后阶段,当您实际处理数据时)。我们将尝试使用坐标轴;我们的更改在 清单 2 中显示。
当您对结果感到满意时,将其复制到 save 命令之后的 histogram.pl 中,并添加行 1 setlinewidth 以恢复原始线宽。现在是时候做艰苦的工作了:定义两个子程序 Histo-y 和 Histo-z。同样,这通常需要一些实验,因此创建 PostScript 文件并对其进行编辑。我们将假设每个子程序分别在堆栈上获得 x,y 和 x,z。我们将给两个直方图都加上边框线。通常将几个数据点放在堆栈上作为实验会有所帮助。
您可以将您的子程序复制到您的 Perl 脚本的 EndPrologue 行之前,如 清单 3 所示。
只需说几句话:我警告您避免使用变量,但我没有实践我所宣讲的内容。好吧,只有在大型表格的情况下,您才会在堆栈上完成所有操作。这样做要困难得多,并且通常不值得付出努力——我的时间比我在速度上获得的收益更昂贵。此外,我在 PostScript 中进行了一些计算;通常,在您的 Perl 脚本中执行此操作是值得的。最后,您通常不想重新计算路径;您保存它。我只是想保持示例简单。
现在是时候完成您的 Perl 脚本并通过添加以下行来处理您的数据
while (<>) { chomp; ($x, $y, $z) = split; print "$x $y Histo-y $x $z Histo-z\n"; }
现在您可以运行您的脚本,并将数据作为 stdin 来创建一个新的 histogram.ps。最后一步是确定一个更好的边界框。这就是 Ghostview 发挥作用的地方。转到您图片的左侧和最右侧像素,并写下 x 坐标。现在对图片的顶部和底部执行相同的操作,写下 y 值。使用这些坐标,您可以确定边界框(不必像素级精确)83 85 400 405,您可以在 PostScript 文件中更改它。(或在您的 Perl 脚本中;但是,如果您有一个巨大的数据文件要处理,重新创建 PostScript 文件可能需要一段时间。)
现在您有了一个完全符合 level-1 标准的 PostScript 文件,大小小于 2KB,您实际上可以理解它。我见过 MS-DOS 下的应用程序生成的 PostScript 文件,同一张图片需要 2MB。完整的 Perl 脚本和输出 PostScript 包含在 ftp 站点上的 gzipped tar 文件中,如清单 4 和 5 所示。输出直方图如图 1 所示。
所以从现在开始我们都用 PostScript 做所有事情,对吗?错了。如果使用另一个应用程序更快,并且生成的 PostScript 文件不太大,请使用该应用程序。对于许多图片,我仍然使用 xfig 或类似的东西。如果您的数据集很大,并且将数据导入应用程序已经需要大量工作,请直接使用 PostScript。如果您是 PostScript 的新手,请专注于 x-y 图形和直方图。如果您获得了一些经验,请阅读蓝皮书和红皮书。最重要的是,玩得开心。
Hans de Vreught (J.P.M.deVreught@cs.tudelft.nl) 是代尔夫特理工大学的计算机科学研究员。他自 1982 年以来一直使用 Unix(自 0.99.13 以来使用 Linux),并且是一个深刻的 MS 仇恨者(他们所有的产品都是坏东西)。他喜欢非虚拟的比利时啤酒,并且是一个真正的环球旅行家(已经环游世界两次)。