PSFile: Generate PostScript files with Python

By Jochen Voss, last updated 2012-02-18

Contents

[psfile test image]

Figure 1. Output of the script from example 1, below.

Introduction

The PSFile Python module helps you to create PostScript files from Python scripts. PSFile can creating the required wrappers to set up the page geometry; this allows you to only provide the PostScript code to draw the figure or page you want to create.

Download

psfile version 0.9, 2009-09-19

first public release

Installation instructions are in the file README of the source code archive. The source code archive also contains the example scripts from the documentation below (in the sub-directory examples).

Documentation

This manual explains the use of PSFile. In addition it includes a short PostScript primer, mainly by use of examples. For a more in-depth introduction to PostScript, I recommend Adobe's PostScript Language Tutorial and Cookbook, which can be downloaded from the Adobe web page (as a ZIP file, containing the book and PostScript example code).

Working with PostScript Files

Before we focus on how to create PostScript files using the PSFile module, here is a short summary about how PostScript files can be used.

Creating a New File

The PSFile Python module allows to generate two different types of PostScript files: stand-alone files describing a complete one-page document and Encapsulated PostScript files describing a figure for inclusion into other documents.

PostScript Files describing a Figure

An Encapsulated PostScript file describing an individual figure can be created as follows.

#! /usr/bin/env python

from psfile import EPSFile

fd = EPSFile("example.eps", 300, 300)
# ... write PostScript commands to `fd` ...
fd.close()

The EPSFile constructor has three required arguments: the name of the file to create (typically using the file extension .eps), followed by the width and height of the figure in units of 1/72th of an inch. The following optional keyword arguments are available.

margin
Set the width of the margin between the plotting region and the edge of the figure. Default is to use 3/72th of an inch (i.e. three PostScript points).
margin_top, margin_right, margin_bottom, margin_left
These parameters can be used to override the margin size for individual edges.
title
An optional figure title. This string will be stored in the header of the generated file.
creator
An optional document creator designation. This string will be stored in the header of the generated file and can be used to store the name of the Document composition software (i.e. of the program you wrote using PSFile).

PostScript Files describing a Full Page

A PostScript file describing a full page can be created as follows. Files created this way can be printed directly.

#! /usr/bin/env python

from psfile import PSFile

fd = PSFile("example.ps", paper="letter")
# ... write PostScript commands to `fd` ...
fd.close()

The only required argument to the PSFILE constructor is the output file name (typically using the file extension .ps). The following optional keyword arguments are available.

paper
The page size to use. This can either be one of the strings A4, A3 or letter to choose the corresponding pre-defined paper size (an appended * indicates landscape mode), or a pair `(width,height)` of integers to specify a custom paper size in units of 1/72th of an inch. The default is to use A4 paper.
margin
Set the size of the margins between the plotting region and the edge of the paper. Default is to use 1 inch.
margin_top, margin_right, margin_bottom, margin_left
These parameters can be used to override the margin size for individual edges.
title
An optional document title. This string will be stored in the header of the generated file and may, for example, be displayed in the title bar of a PostScript viewer.
creator
An optional document creator name. This string will be stored in the header of the generated file and can be used to store the name of the Document composition software.

Using the PostScript File Object

The psfile module constructs the required headers for the output file to set up page dimensions, margins, etc. The coordinate system is set up so that coordinates (0,0) correspond to the lower-left corner of the drawing area.

Instances of the PSFile and EPSFile classes, described above, are file-like objects which can be used to write the body of the PostScript file. The following methods are provided:

close()
Close the PostScript file. This method writes the PostScript code to the output file and then closes the PostScript file. A closed PostScript file cannot be written to any more.
append(text)
Append a block of text to the body of the PostScript file. This method sanitises white space in text (removes leading and trailing empty lines, expands tabs, removes indentation, and adds a trailing newline character as needed), and then appends the result to the body of the PostScript file.
write(text)
Append text to the body of the PostScript file without any change.
define(name, body)
Define a PostScript macro. This adds a PostScript macro to the header of the generated file, making name an abbreviation for body. The resulting macro can be used to prevent the generated PostScript from getting overly big.

In addition, there are two attributes which you can read to get the size of the drawing area:

width
The width of the drawing area (excluding margins) in units of 1/72th inch.
heigth
The height of the drawing area (excluding margins) in units of 1/72th inch.

Example 1 (colourful squares)

The following example script creates the image from figure 1 above.

#! /usr/bin/env python

from random import uniform
from psfile import EPSFile

fd = EPSFile("ex1.eps", 600, 100)

# dark gray background
fd.append("0.1 setgray")
fd.append("0 0 %d %d rectfill"%(fd.width, fd.height))

# a grid of dark orange lines
fd.append("1 .596 .118 setrgbcolor")
fd.append("1 setlinewidth")
for i in range(1,5):
    y = 100*i/5.0
    fd.append("5 %.1f moveto 595 %.1f lineto"%(y,y))
for i in range(1,30):
    x = 100*i/5.0
    fd.append("%.1f 5 moveto %.1f 95 lineto"%(x,x))
fd.append("stroke")

# randomly colored, filled squares
for i in range(0,30):
    x = i*20+3
    for j in range(0,5):
        y = j*20+3
        col = uniform(0,1)
        if 31*uniform(0,1) > i+1:
            fd.append("0 %.3f 0 setrgbcolor"%col)
        else:
            fd.append("%.3f 0 0 setrgbcolor"%col)
        fd.append("%.1f %.1f 14 14 rectfill"%(x,y))

fd.close()

Drawing Lines

To draw a polygonal line in PostScript you first have to move the PostScript point to the starting point of the line using the moveto PostScript operator, and then to move the pen along the segments of the polygon using the lineto operator. Finally, you have to use the stroke command to actually draw the line.

x y moveto
Move the current point to position (x,y). Don't change the current path.
dx dy rmoveto
Move the current point dx units to the right and dy units up. Don't change the current path.
x y lineto
Append a straight line segment, connecting the current point to the point (x,y), to the current path. Make the end of this line the new current point.
dx dy rlineto
Append a straight line from the current point to the point dx units to the right and dy units up to the current path. The end of this line segment is then the new current point.
closepath
Close the current path by appending a straight line from the current point to the starting point. This should be used for drawing closed curves, e.g. polygons.
stroke
Draw a line along the current path. The line uses the colour and line width (see below) current at the time of the stroke command. Then clear the current path.

If you have to draw many line segments, it may be a good idea to use commands like fd.define("l", "lineto") to define one-letter abbreviations for moveto and lineto.

Example 2 (basic lines)

The following code generates an Encapsulated PostScript file, containing two squares (one has a missing edge).

#! /usr/bin/env python

from psfile import EPSFile

fd = EPSFile("ex2.eps", 100, 100)
fd.append("""
  % outer square
  0 0 moveto
  100 0 lineto
  100 100 lineto
  0 100 lineto
  closepath

  % inner square, open to the left
  10 10 moveto
  90 10 lineto
  90 90 lineto
  10 90 lineto

  % draw the constructed path
  stroke
""")
fd.close()

The output looks as follows:

[psfile example 2]

Example 3 (line width)

You can use the setlinewidth command to set the width of a line. This command can be given any time before the corresponding stroke command.

w setlinewidth
The the current line width to w (in units of 1/72th of an inch). The default line width is 1.

The following example illustrates different choices of line widths.

#! /usr/bin/env python

from __future__ import division

from math import pow
from psfile import EPSFile

min_lw = .5
max_lw = 9
steps = 18

fd = EPSFile("ex3.eps", 350, 38,
             margin_left=10+.5*min_lw, margin_right=10+.5*max_lw)
fd.append("/Times-Roman 10 selectfont")
for i in range(0, steps+1):
    lw = min_lw * pow(max_lw/min_lw, i/steps)
    x = fd.width*i/steps
    fd.append("%.1f setlinewidth"%lw)
    fd.append("%f 0 moveto 0 27 rlineto"%x)
    fd.append("stroke")
    fd.append("%f 30 moveto"%(x-6))
    fd.append("(%.1f) show"%lw)
fd.close()

The output of this script looks as follows:

[psfile example 3]

Geometric Shapes

There are special commands to draw rectangles and circles.

x y w h rectstroke
Draw the outline of a rectangle with lower left corner (x,y) and upper right corner (x+w,y+h). This command already includes the final stroke command.
x y w h rectfill
Fill a rectangle with lower left corner (x,y) and upper right corner (x+w,y+h), using the current colour. This command already includes the final stroke command.
x y r 0 360 arc
Add a circle with centre (x,y) and radius r to the current path. You need to use a stroke or fill command after this to actually draw the circle.

Colours

x setgray
Set the current colour to gray level x. The value x must be between 0 (black) and 1 (white).
r g b setrgbcolor
Set the current colour to the RGB colour (r,g,b).

Example 4 (colours)

The following code creates a grid of 28 randomly coloured rectangles and labels them with the corresponding RGB intensities.

#! /usr/bin/env python

from __future__ import division

from random import uniform
from psfile import EPSFile

cols = 7
rows = 4
gap = 3

fd = EPSFile("ex4.eps", 350, 80)
fd.append("/Times-Roman 8 selectfont")
dx = (fd.width+gap)/cols
dy = (fd.height+gap)/rows
w = dx - gap
h = dy - gap
for j in range(0, rows):
    for i in range(0, cols):
        r = g = b = 0
        while r + g + b < 1:
            r, g, b = [ uniform(0,1), uniform(0,1), uniform(0,1) ]
        fd.append("%.1f %.1f %.1f setrgbcolor"%(r,g,b))
        fd.append("%f %f %f %f rectfill"%(i*dx, j*dy, w, h))
        fd.append("0 setgray")
        fd.append("%f %f moveto"%(i*dx+4, j*dy+3))
        fd.append("(%.1f, %.1f, %.1f) show"%(r,g,b))
fd.close()

The output of one run of the code above looks as follows. Since the choice of colours is random, you'll most likely get a different picture when you run the script yourself.

[psfile example 4]

Filling

x y w h rectfill
Draw a filled rectangle with lower left corner (x,y) and upper right corner (x+w,y+h).
fill
Fill the current path. This operator can be used to fill the region enclosed by the current path (instead of using stroke to draw the outline). This operation clears the current path.

Remark. The operators described in this section can also be used to set the background colour of a figure: just start the figure by drawing a coloured, filled rectangle which covers all of the drawing area, and then draw everything else on top of this.

Since fill clears the current path, some care needs to be taken when filling a region and drawing the outline of the same region in a different colour. The solution is to save the current graphics state (which includes the current path) with gsave before issuing the fill command and then to restore the state using grestore before stroking the outline of the region. This technique is illustrated in the following example.

Example 5 (filled circles)

The following code draws seven filled circles on a yellow background.

#! /usr/bin/env python

from psfile import EPSFile

fd = EPSFile("ex5.eps", 350, 50)
# draw the yellow background
fd.append("1 1 0 setrgbcolor")
fd.append("0 0 %d %d rectfill"%(fd.width, fd.height))
# draw black circles filled with red
fd.append("0 setgray")
for x in range(25, 375, 50):
    fd.append("""%f 25 20 0 360 arc
                 gsave
                 1 0 0 setrgbcolor
                 fill
                 grestore
                 stroke"""%x)
fd.close()

The output looks as follows:

[psfile example 5]

Text

PostScript provides a set of 13 standard fonts which can be scaled to arbitrary sizes. These are used with the following commands:

/font size selectfont
Make the given font, scaled to the given size, the active font. font should be one of Times-Roman, Times-Italic, Times-Bold, Times-BoldItalic, Helvetica, Helvetica-Oblique, Helvetica-Bold, Helvetica-BoldOblique, Courier, Courier-Oblique, Courier-Bold, Courier-BoldOblique or Symbol. See figure 6 for the shape of the different fonts.
(str) show
Print the string str. The lower left corner of the string will be placed at the current point. Without special preparations, the string is restricted to the ASCII character set. Unbalanced brackets and backslashes in the string need to be quoted with a backslash. There are various methods to print accented characters, too (see the PostScript Language Tutorial and Cookbook or google for ISOLatin1Encoding).

Example 6 (fonts)

The following code illustrates the 13 PostScript standard fonts.

#! /usr/bin/env python

from psfile import EPSFile

fonts = [
    "Times-Roman", "Times-Italic", "Times-Bold", "Times-BoldItalic",
    "Helvetica", "Helvetica-Oblique", "Helvetica-Bold", 
    "Helvetica-BoldOblique", "Courier", "Courier-Oblique",
    "Courier-Bold", "Courier-BoldOblique", "Symbol"
]
str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

fd = EPSFile("ex6.eps", 420, len(fonts)*12)
for i, name in enumerate(reversed(fonts)):
    fd.append("/Times-Roman 10 selectfont")
    fd.append("0 %d moveto"%(12*i+3))
    fd.append("(%s:) show"%name)
    fd.append("/%s 10 selectfont"%name)
    fd.append("100 %d moveto"%(12*i+3))
    fd.append("(%s) show"%str)
fd.close()

The output looks as follows:

[psfile example 6]

Figure 6. The PostScript standard fonts. This is the output of the script from example 6.

Coordinate Transformations

The following two PostScript commands allow to shift and rotate the picture. These functions can, for example, be used to create rotated text.

x y translate
Change the the current coordinate system so that the origin of the new coordinate system is where the point (x,y) was in the old system.
phi rotate
Change the the current coordinate system so that the new axes are rotated by phi degrees clockwise w.r.t. the axes of the old coordinate system.

You can use gsave to save the current coordinate system before using translate or rotate. This allows to restore the old coordinate system using grestore.

Example 7 (coordinate transforms)

#! /usr/bin/env python

from __future__ import division

from math import pi, sqrt
from random import vonmisesvariate
from psfile import EPSFile

# a sample from a von Mises distribution
sample = [ vonmisesvariate(pi/4, 1.0) for i in range(0,200) ]

# generate a histogram of the data
nhist = 4 * int(sqrt(len(sample))/4 + 0.5);
count = [ 0 ] * nhist
for x in sample:
    y = x/(2*pi) % 1
    count[int(y*nhist+.5)%nhist] += 1

# turn into a plot
radius = 72
fd = EPSFile("ex7.eps", 4*radius, 4*radius)
fd.append("/Times-Roman 10 selectfont")
fd.append("%f %f translate"%(2*radius, 2*radius))
fd.append("%f setlinewidth"%((2*pi*radius) / nhist - 1))
for k in count:
    r = radius * (1 + .9*k/max(count))
    fd.append("0 0 moveto %f 0 lineto"%r)
    fd.append("%f -3 moveto (%d) show"%(r+3, k))
    fd.append("%f rotate"%(360/nhist))
fd.append("stroke")
fd.append("""1 setlinewidth
             0 0 %f 0 360 arc
             gsave 1 setgray fill grestore stroke"""%radius)
fd.append("/Symbol 24 selectfont")
fd.append("-35 -10 moveto (k = 1.0) show")
fd.close()
[psfile example 7]

Figure 7. A circular histogram for the von Mises distribution with μ=π/4 and κ=1.

References

Copyright © 2012, Jochen Voss. All content on this website (including text, pictures, and any other original works), unless otherwise noted, is licensed under a Creative Commons Attribution-Share Alike 3.0 License.