Updated 2023-07-21
PostScript
is primarily thought of as a document format (you may have come across .ps
files),
but it is, in-fact, a genuine Turing-complete programming language.
And a fun one to learn and use.
This page presents a small PostScript program to generate the Chicago flag shown above. And as a learning exercise, I’ve written a line-by-line translation of the PostScript file into a Python script that does the same thing.
I won’t attempt to write full tutorial of the PostScript language here, but with a few notes, I think someone with some programming experience can understand the following program pretty well. Over-simplyfying things, of course,
PostScript is a minimal language with very little
syntax. You can view a .ps
file as basically a long sequence of tokens
that the interpreter will step through and handle one at a time (with a few exceptions).
PostScript is a stack-based language. The interpreter maintains a stack data structure which acts like a global shared memory for the arguments and return values of functions (called operators in PostScript).
For example, when the interpreter encounters the
three tokens, 1 2 add
. It handles each in succession,
Encounter a number literal, 1
. Push it onto the stack.
Encounter a number literal, 2
. Push it onto the stack.
Encounter a name, add
. Since this is
a known operator (in this case, it’s a built-in),
execute it. Internally, add
pops two values off
the stack, performs its operation, and pushes
its result onto the stack.
If the stack was originally empty ([]
), then after 1 2 add
, the stack will have a size of one ([3]
). So, 1 2 add
is similar to add(1,2)
in many other languages. This "backwards-looking"
syntax (operands before operators) is called
Reverse_Polish or postfix notation.
A named operator can be defined with something like /foo { ... } def
,
equivalent to def foo(): ...
in Python.
{ ... }
defines a procedure or executable array. Its
objects (the tokens inside the brackets) aren’t executed
immediately, but stored in a new object. These procedures
are first-class variables. They are also used in looping constructs, for example, 10 {...} repeat
.
def
is an operator that takes two argments (key
and
value
) and stores that pair in an internal dictionary
of named objects.
Since operators are first-class objects, you will see the
same syntax for operators, numbers, arrays, etc:
/name value def
. /
followed by a name
is a name literal
and is used for dictionary keys.
While you can use def
to define local variables as you
need, more idiomatic PostScript will store temporary
values on the stack. Doing this effectively (and
reading programs that do this) is the main shift in
mindset you need to start thinking in PostScript.
Warning: this is not "idiomatic" Python. You would not want to write a python script organized in this way. I've done it to match the PostScript code as closely as possible. There are a few weird hacks (see flag_utils.py) that allow it to line-up with the PostScript and still run and generate the same output.
PostScript | Python |
---|---|
%
%
/width 720 def
/height 480 def
/numstar 4 def
/pw 16 def % pw,ph control size of points
/ph 60 def
/npoint 6 def % num points on a star
/barheight 80 def
/starRGB [ 1 0 0] def % red
/barRGB [.702 .867 .949] def % light-blue
/backgroundRGB [ 1 1 1] def % white
/setcolor {
% this allows color to be specified as
% an array [r,g,b] rather than 3 separate vars
aload pop setrgbcolor
} def
/background {
backgroundRGB setcolor
0 0 width height rectfill
} def
/bars {
0 120 moveto
width 0 rlineto
0 360 moveto
width 0 rlineto
barheight setlinewidth
barRGB setcolor
stroke
} def
% draw one point for in a star (a triangle)
/point {
pw neg 0 moveto
pw ph rlineto
pw ph neg rlineto
} def
% draw one star
/star {
/degrees 360 npoint div def
npoint {
point
degrees rotate
} repeat
} def
/stars {
% compute horiz distance between stars
% (equally spaced with pad 10 on both sides)
% ie sdist = (width - 20)/(numstar+1)
/sdist width 20 sub numstar 1 add div def
% move to first:
10 sdist add height 2 div translate
numstar {
star
sdist 0 translate
} repeat
starRGB setcolor
fill
} def
%---main---
background
bars
stars
showpage
| from flag_utils import *
width = 720
height = 480
numstar = 4
pw = 16 # pw,ph control size of points
ph = 60
npoint = 6 # num points on a star
barheight = 80
starRGB = [ 1, 0, 0] # red
barRGB = [.702, .867, .949] # light-blue
backgroundRGB = [ 1, 1, 0] # white
def setcolor(rgb):
setrgbcolor(*rgb)
def background():
setcolor(backgroundRGB)
rectfill(0,0,width,height)
def bars():
moveto(0, 120)
rlineto(width, 0)
moveto(0, 360)
rlineto(width, 0)
setlinewidth(barheight)
setcolor(barRGB)
stroke()
def point():
"draw one point in a star (a triangle)"
moveto(-pw, 0)
rlineto(pw, ph)
rlineto(pw, -ph)
def star():
"draw one star"
degrees = 360 / float(npoint)
for i in range(npoint):
point()
rotate(degrees)
def stars():
# compute horiz distance between stars
# (equally spaced with pad 10 on both sides)
#
sdist = (width - 20)/(numstar+1)
# move to first:
translate(10+sdist, height/2)
for i in range(numstar):
star()
translate(sdist,0)
setcolor(starRGB)
fill()
if __name__ == "__main__":
background()
bars()
stars()
write_png("/tmp/py-flag.png", globals())
|
One way to generate an image from a PostScript file
is using the common utility convert
(ImageMagick),
$ convert -page 720x480 flag.ps flag.png
Note, we set the page size to the same width and height defined in the file.
A popular PostScript interpreter is
Ghostscript
(convert
uses Ghostscript internally).
If you have it installed, you can similarly run,
$ gs -sDEVICE=png16m -g720x480 -o flag.png flag.ps
Note that flag.ps
is written in a
very parametric way: nearly all the constants
are defined by variables at the top of file.
You can try changing a few,
/numstar 2 def
/barheight 20 def
/npoint 10 def
/ph 100 def
/starRGB [ 1 1 0] def
/barRGB [ 1 .5 .5] def
/backgroundRGB [.5 .7 1] def
and you'll get something like: