creating EPS with Python

W

Will Stuyvesant

[Jamie]
Now all I need to do is create vector eps files. Can you tell me
where I should start for that?

If you don't know yet how to create .eps manually, search Google for
"Bluebook EPS". It has some nice examples. But from an earlier
remark by you, something like "the eps spec being to detailed" I guess
you are looking for a high level way of creating eps. I could not
find such a thing to my liking, so at the moment I am working on a
Python module for this, the idea is to use Python to create .eps
files. What I have now is below, it is a Python module that sends
test EPS to stdout when run standalone, if you call it pyps.py (like I
do) and then do

python pyps.py > test.eps

you get an eps file that shows some things it can do. I am not a
professional programmer so there are probably bugs etc., if a good
Python programmer is reading this then contact me if you'd like to
help with the project!

----- pyps.py -----

'''

This file (pyps.py) contains a Python module for generating .eps
files (Postscript with a BoundingBox) that display diagrams.


Programmer notes:
-----------------

The AutoPicture is a container class, you create an instance, add
figures to it and then you can call its epsText method that returns
the eps code: it calculates an EPS BoundingBox. See the global test
methods for example usage.

The basic classes to inherit from to create figures are StrokeFill
and TranslatedStrokeFill. Override their epsCode method with a
method that returns the epscode for the figure you want.

CLASSES
AutoPicture
StrokeFill
Box
GridBox
Line
TranslatedStrokeFill
Arrow
OpenArrow
Circle
Text

Classes in pyps and children of classes in pyps are expected to have
a method epsText() that returns Postscript code for a while figure,
including code for (translations and) rotations and scaling.
Classes should have a methods epsCode() that returns Postscript code
for a figure without (translation,) rotation or scale code. The
epsCode method is called by the epsText method in classes StrokeFill
and TranslatedStrokeFill.

Class StrokeFill does NOT do a Postscript translation. Used for
Line and Box as parent class. Class TranslatedStrokeFill does do a
Postscript translation to (self.x, self.y). Used for Circle and
OpenArrow as parent class.

'''

import math
import string
import sys

goldenratio = math.sqrt(5)/5.0 # about 0.45

##
# Mostly here for debugging purposes.
# Used in test() to show the size of the generated picture.
##
def log(msg):
sys.stderr.write('\n'+msg+'\n')



###
# The Text class does not inherit from StrokeFill because it is
# incompatible: sometimes text is printed with a Postscript "show"
# command instead of a "fill" or "stroke".
###
class Text:
def __init__(self, x=20, y=20, gray=0, linewidth=1, linegray=0,
angle=0.0, text='Spam spam spam', scalefont=6,
font='Times-Roman', xscale=1, yscale=1, charpath=0):
self.x = x
self.y = y
self.gray = gray
self.linegray = linegray
self.linewidth = linewidth
self.angle = angle
self.text = text
self.scalefont = scalefont
self.font = font
self.xscale = xscale
self.yscale = yscale
self.charpath = charpath
lw = scalefont * goldenratio # estimate of letterwidth
textlength = len(text) * lw
x2 = x + lw + math.cos(math.radians(
angle)) * textlength * xscale
y2 = y + scalefont + math.sin(math.radians(
angle)) * textlength * yscale
# elw for extra letterwidth (of first letter, because of
# rotation)
elw = lw * 2 * math.sin(math.radians(angle))
minX, minY = min(x, x2)-elw, min(y, y2)-lw*goldenratio
maxX, maxY = max(x, x2), max(y+lw*2, y2)
self.minx, self.miny = minX-linewidth, minY-linewidth\0
self.maxx, self.maxy = maxX+linewidth, maxY+linewidth
def epsText(self):
# content pattern
resultStr = '''
gsave
'''
resultStr = resultStr + '''\
/%(font)s findfont
%(scalefont)s scalefont
setfont
'''
if self.angle or (
int(self.xscale) != 1 or int(self.yscale) !=1 ):
resultStr = resultStr + '''\
%(x)s %(y)s translate
'''
if self.angle:
resultStr = resultStr + '''\
%(angle)s rotate
'''
if self.xscale != 1 or self.yscale !=1:
resultStr = resultStr + '''\
%(xscale)s %(yscale)s scale
'''
resultStr = resultStr + '''\
newpath
0 0 moveto
'''
else:
resultStr = resultStr + '''\
newpath
%(x)s %(y)s moveto
'''
if self.charpath:
resultStr = resultStr + '''\
(%(text)s) true charpath
'''
else:
resultStr = resultStr + '''\
(%(text)s) show
'''
if self.gray and self.linewidth:
resultStr = resultStr + '''\
gsave
%(gray)s setgray fill
grestore
'''
elif self.gray:
resultStr = resultStr + '''%(gray)s setgray fill\n'''
if self.linewidth:
# This should not be executed when charpath!=0, because
# then it will do a Postscript 'show' and it does not
# need a stroke anymore.
if self.charpath != 0:
resultStr = resultStr + '%(linewidth)s'
resultStr = resultStr + ' setlinewidth\n'
if self.linegray:
resultStr = resultStr + '%(linegray)s'
resultStr = resultStr + ' setgray\n'
resultStr = resultStr + 'stroke\n'
resultStr = resultStr + 'grestore\n'
return resultStr % self.__dict__



##
# This is an abstract class.
# It is expected that epsCode will be overriden.
# @param dash Tuple. Last element is offset. The other elements
# are used in a Postscript setdash command for defining the pattern.
##
class StrokeFill:
def __init__(self, x=0, y=0, gray=0, linewidth=1, linegray=0,
angle=0.0, xscale=1.0, yscale=1.0, dash=None):
self.x = x
self.y = y
self.gray = gray
self.linegray = linegray
self.linewidth = linewidth
self.angle = angle
self.xscale = xscale
self.yscale = yscale
self.dash = dash
# These should be overriden in children of StrokeFill!
self.minx, self.miny = 1000000, 1000000
self.maxx, self.maxy = -1000000, -1000000
##
# Override this: return a string of Postscript code.
# In that string you make use of attributes in self.__dict__
# like %(x)s
def epsCode(self): return '\n'
def epsText(self):
# content pattern
addSaveRestore = ( self.gray or self.angle or
int(self.xscale != 1) or int(self.yscale != 1) or
self.linegray
)
resultStr = '\n'
if addSaveRestore:
resultStr = '''
gsave
'''
if self.angle:
resultStr = resultStr + '''\
%(angle)s rotate
'''
if self.xscale != 1 or self.yscale !=1:
resultStr = resultStr + '''\
%(xscale)s %(yscale)s scale
'''
smallCode = self.epsCode()
if smallCode: resultStr = resultStr + smallCode
if self.gray:
resultStr = resultStr + '''\
gsave
%(gray)s setgray fill
grestore
'''
if self.linewidth:
resultStr = resultStr + '%(linewidth)s'
resultStr = resultStr + ' setlinewidth\n'
if self.linegray:
resultStr = resultStr + '%(linegray)s'
resultStr = resultStr + ' setgray\n'
if self.dash:
dashParams = '['
i = 0
patternList = self.dash[:-1]
for p in patternList:
dashParams = dashParams + repr(p)
if i < len(patternList)-1:
dashParams = dashParams + ' '
i = i + 1
dashParams = dashParams + '] '
dashParams = dashParams + repr(self.dash[-1])
resultStr = resultStr + dashParams + ' setdash\n'
resultStr = resultStr + 'stroke\n'
if self.dash:
resultStr = resultStr + '[] 0 setdash\n'
if addSaveRestore:
resultStr = resultStr + 'grestore\n'
return resultStr % self.__dict__






##
# __init__ constructor parameters:
# (x, y) where from
# (x2, y2) where to
# linegray color for lines
# linewidth width of lines
##
class Line(StrokeFill):
def __init__(self, x=20, y=20, gray=0, linewidth=1, linegray=0,
angle=0.0, x2=100, y2=100, dash=None):
StrokeFill.__init__(self, x=x, y=y, gray=gray,
linewidth=linewidth, linegray=linegray, angle=angle,
dash=dash)
self.x2, self.y2 = x2, y2
minX, minY = min(x, x2), min(y, y2)
maxX, maxY = max(x, x2), max(y, y2)
self.minx, self.miny = minX-linewidth, minY-linewidth
self.maxx, self.maxy = maxX+linewidth, maxY+linewidth
def epsCode(self):
# Starting with a newline because there will not be a gsave
# around a Line
return '''
newpath
%(x)s %(y)s moveto
%(x2)s %(y2)s lineto
'''



class Box(StrokeFill):
##
# (x,y) is the lower left corner of the box, w is width, h is
# height.
# Parameters w and h have to be positive numbers.
def __init__(self, x=0, y=0, linewidth=1, linegray=0, gray=0,
angle=0.0, w=50, h=None, dash=None):
StrokeFill.__init__(self, x=x, y=y, gray=gray,
linewidth=linewidth, linegray=linegray, angle=angle,
dash=dash)
self.x = x
self.y = y
if not h: h = w * goldenratio
self.w = w
self.h = h
self.x2 = x+w
self.y2 = y+h
# The -linewidth and +linewidth are sometimes a little too
# much when a box is in a corner, but tested effect looks
# good.
self.minx, self.miny = x-linewidth, y-linewidth
self.maxx, self.maxy = self.x2+linewidth, self.y2+linewidth
def epsCode(self):
return '''\
newpath
%(x)s %(y)s moveto
%(x2)s %(y)s lineto
%(x2)s %(y2)s lineto
%(x)s %(y2)s lineto
closepath
'''



class GridBox(Box):
def __init__(self, x=0, y=0, linewidth=1, linegray=0, gray=0,
angle=0.0, w=50, h=None, grid=10, glinewidth=1,
ggray0.9, dash=None):
StrokeFill.__init__(self, x=x, y=y, gray=gray,
linewidth=linewidth, linegray=linegray, angle=angle,
dash=dash)
if not h: h = w * goldenratio
self.w = w
self.h = h
self.gray = 0 # ! gray is for the self.box !
self.box = Box(x=x, y=x, linewidth=linewidth,
linegray=linegray, gray=gray, angle=angle, w=w, h=h)
self.grid = grid
self.glinewidth= glinewidth
self.ggray = ggray
self.x2 = x+w
self.y2 = y+h
self.minx, self.miny = x-linewidth, y-linewidth
self.maxx, self.maxy = self.x2+linewidth, self.y2+linewidth
def epsCode(self):
resultStr = self.box.epsText()
# Horizontal lines
vpoints = []
for y in range(int(math.floor(self.miny)),
int(math.ceil(self.maxy))):
if y % self.grid == 0: vpoints.append(y)
for y in vpoints:
resultStr = resultStr + Line(x=self.minx, y=y,
x2=self.maxx, y2=y, linegray=self.ggray,
linewidth=self.glinewidth).epsText()
# Vertical lines
hpoints = []
for x in range(int(math.floor(self.minx)),
int(math.ceil(self.maxx))):
if x % self.grid == 0: hpoints.append(x)
for x in hpoints:
resultStr = resultStr + Line(x=x, y=self.miny, x2=x,
y2=self.maxy, linegray=self.ggray,
linewidth=self.glinewidth).epsText()
return resultStr



###
# Same as StrokeFill but with a Postscript translate instruction as
# the first thing in the representation.
###
class TranslatedStrokeFill(StrokeFill):
def epsText(self):
resultStr = '''
gsave
%(x)s %(y)s translate
'''
if self.angle:
resultStr = resultStr + '''\
%(angle)s rotate
'''
if self.xscale != 1 or self.yscale !=1:
resultStr = resultStr + '''\
%(xscale)s %(yscale)s scale
'''
smallCode = self.epsCode()
if smallCode: resultStr = resultStr + smallCode
if self.gray:
resultStr = resultStr + '''\
gsave
%(gray)s setgray fill
grestore
'''
if self.linewidth:
resultStr = resultStr + '%(linewidth)s'
resultStr = resultStr + ' setlinewidth\n'
if self.linegray:
resultStr = resultStr + '%(linegray)s'
resultStr = resultStr + ' setgray\n'
if self.dash:
dashParams = '['
i = 0
patternList = self.dash[:-1]
for p in patternList:
dashParams = dashParams + repr(p)
if i < len(patternList)-1:
dashParams = dashParams + ' '
i = i + 1
dashParams = dashParams + '] '
dashParams = dashParams + repr(self.dash[-1])
resultStr = resultStr + dashParams + ' setdash\n'
resultStr = resultStr + 'stroke\n'
if self.dash:
resultStr = resultStr + '[] 0 setdash\n'
resultStr = resultStr + 'grestore\n'
return resultStr % self.__dict__



##
# __init__ constructor parameters:
# (x, y) centre of circle.
# r radius.
# start angle in degrees from where to start drawing
# counterclockwise.
# end angle where to stop drawing.
# (xscale, yscale) X and Y scaling. Useful for (part of) ellipses.
##
class Circle(TranslatedStrokeFill):
def __init__(self, x=20, y=20, gray=0, linewidth=1, linegray=0,
angle=0.0, r=40, start=0, end=360, xscale=1.0,
yscale=1.0, dash=None):
TranslatedStrokeFill.__init__(self, x=x, y=y, gray=gray,
linewidth=linewidth, linegray=linegray, angle=angle,
xscale=xscale, yscale=yscale, dash=dash)
self.r = r
self.start, self.end = start, end
minX, maxX = x - r, x + r
minY, maxY = y - r, y + r
self.minx, self.miny = minX-linewidth, minY-linewidth
self.maxx, self.maxy = maxX+linewidth, maxY+linewidth
def epsCode(self):
return '''\
newpath
0 0 %(r)s %(start)s %(end)s arc
'''



##
# __init__ constructor parameters:
# (x, y) where from
# (x2, y2) where to
# ahw width of the arrow head
# ahl length of the arrow head
# gray fill color for arrow head
##
class Arrow(TranslatedStrokeFill):
def __init__(self, x=20, y=20, gray=0, linewidth=1, linegray=0,
angle=0.0, x2=100, y2=100, sw=4, ahw=None, ahl=None,
dash=None, xscale=1.0, yscale=1.0):
TranslatedStrokeFill.__init__(self, x=x, y=y, gray=gray,
linewidth=linewidth, linegray=linegray, angle=angle,
xscale=xscale, yscale=yscale, dash=dash)
if not ahw: ahw = sw / goldenratio
if not ahl: ahl = sw / goldenratio
dx = (x2 - x) * 1.0
dy = (y2 - y) * 1.0
self.angle = math.degrees(math.atan2(dy, dx))
self.arrowlength = math.hypot(dx, dy)
self.base = self.arrowlength - ahl
self.halfthickness = sw / 2.0
self.halfheadthickness = ahw / 2.0
self.xvalues = [x, x2,
x - self.halfthickness,
x2 - self.halfthickness,
x + self.halfthickness,
x2 + self.halfthickness]
self.yvalues = [y, y2,
y - self.halfthickness,
y2 - self.halfthickness,
y + self.halfthickness,
y2 + self.halfthickness]
self.setminmax()
def setminmax(self):
self.minx = min(self.xvalues) - self.linewidth
self.miny = min(self.yvalues) - self.linewidth
self.maxx = max(self.xvalues) + self.linewidth
self.maxy = max(self.yvalues) + self.linewidth
def epsCode(self):
return '''\
newpath
0 0 moveto
%(arrowlength)s 0 lineto
%(base)s %(halfheadthickness)s neg moveto
%(arrowlength)s 0 lineto
%(base)s %(halfheadthickness)s lineto
'''



##
# __init__ constructor parameters:
# (x, y) where from
# (x2, y2) where to
# sw width of the stem
# ahw width of the arrow head
# ahl length of the arrow head
# gray fill color for whole arrow
# linegray color for arrow outline
# linewidth width of arrow outline
##
class OpenArrow(Arrow):
def __init__(self, x=20, y=20, gray=0, linewidth=1, linegray=0,
angle=0.0, x2=100, y2=100, sw=4, ahw=None, ahl=None,
dash=None, xscale=1.0, yscale=1.0):
Arrow.__init__(self, x=x, y=y, gray=gray,
linewidth=linewidth, linegray=linegray, angle=angle,
x2=x2, y2=y2, sw=sw, ahw=ahw, ahl=ahl,
xscale=xscale, yscale=yscale, dash=dash)
def epsCode(self):
return '''\
newpath
0 %(halfthickness)s neg moveto
%(base)s %(halfthickness)s neg lineto
%(base)s %(halfheadthickness)s neg lineto
%(arrowlength)s 0 lineto
%(base)s %(halfheadthickness)s lineto
%(base)s %(halfthickness)s lineto
0 %(halfthickness)s lineto
closepath
'''



##
# A class that can determine the BoundingBox itself, but only if all
# its elements have attributes minx, miny, maxx and maxy.
##
class AutoPicture:
def __init__(self):
self._intro = r'''%!
%%Creator: pyps.py
'''
self._introBoxPat = '''\
%%%%BoundingBox: %(minx)s %(miny)s %(maxx)s %(maxy)s

'''
self.elements = []
# silly inital min and max values
self.minx, self.miny = 1000000, 1000000
self.maxx, self.maxy = -1000000, -1000000
def add(self, what): self.elements.append(what)
def prepend(self, what):
self.elements = [what] + self.elements
##
# @return Tuple (minx, miny, maxx, maxy)
##
def dimensions(self):
for e in self.elements:
floor, ceil = math.floor, math.ceil
if e.minx < self.minx:
self.minx = int(min(floor(e.minx), ceil(e.minx)))
if e.miny < self.miny:
self.miny = int(min(floor(e.miny), ceil(e.miny)))
if e.maxx > self.maxx:
self.maxx = int(max(floor(e.maxx), ceil(e.maxx)))
if e.maxy > self.maxy:
self.maxy = int(max(floor(e.maxy), ceil(e.maxy)))
return (self.minx, self.miny, self.maxx, self.maxy)
def epsText(self):
self.minx, self.miny, self.maxx, self.maxy = self.dimensions()
cList = [self._intro + self._introBoxPat % self.__dict__]
for e in self.elements: cList.append(e.epsText())
return string.join(cList, '')




##
# Return a string: the contents of a test .eps file.
def test():
p = AutoPicture()
# A smal box, filled, default width=50 height=50*goldenratio
p.add(Box(x=20, y=40, gray=1))
# Lines
p.add(Line(x=30, y=20, x2=50, y2=60))
p.add(Line(x=40, y=20, x2=60, y2=60, linegray=0.2, dash=(3,3,0)))
p.add(Line(x=45, y=20, x2=65, y2=60, linegray=0.4))
p.add(Line(x=50, y=20, x2=70, y2=60, linegray=0.6, dash=(9,5,3)))
p.add(Line(x=55, y=20, x2=75, y2=60, linegray=0.8))
# An open arrow
p.add(OpenArrow(x=20, y=20, x2=40, y2=60, gray=0.4))
# A normal arrow
p.add(Arrow(x=10, y=20, x2=30, y2=60))
# Text
p.add(Text(x=4, y=5, font='Courier-New', text='Courier-New spam'))
p.add(Text(x=100, y=5, font='Arial', text='Arial spam'))
p.add(Text(x=65, y=20, angle=math.degrees(math.atan2(2,1)),
font='Verdana', text='Verdana spam'))
p.add(Text(x=24, y=80, angle=0, scalefont=32, charpath=1,
gray=0.7, text="Spam, ham and eggs"))
#
# Circles
p.add(Circle(x=14, y=30, r=6))
p.add(Circle(x=100, y=30, r=6, start=90, end=180, dash=(2,2,0)))
p.add(Circle(x=120, y=30, r=6, start=0, end=180, yscale=2.0,
linegray=0.5, gray=0.4))
p.add(Circle(x=160, y=30, r=6, start=0, end=180, xscale=1.5,
linewidth=0, gray=0.6))
# This one is rotated
p.add(Circle(x=140, y=30, r=6, start=0, end=180, xscale=0.5,
angle=30.0))
#
# Now calculate the dimensions of the picture
d = p.dimensions()
log('BoundingBox of the generated picture: '+ repr(d))
# PREPEND a background with the max dimensions
#p.prepend(Box(x=d[0], y=d[1], w=d[2]-d[0], h=d[3]-d[1],
# linewidth=0, gray=0.95))
p.prepend(GridBox(x=d[0], y=d[1], w=d[2]-d[0], h=d[3]-d[1],
linewidth=0, gray=0.95, grid=10, glinewidth=0.5,
ggray=0.8))
return p.epsText()



if __name__ == '__main__':
print test()
 
W

Will Stuyvesant

[John Hunter]
PyX is a very nice python postscript generator

http://pyx.sourceforge.net/

Their webpage has nice examples, and it looks promising, so I
downloaded it, but:

python.exe setup.py install
d:\python23\lib\distutils\dist.py:227: UserWarning: Unknown
distribution option: 'platform'
warnings.warn(msg)
running install
running build
running build_py
running build_ext
error: Python was built with version 6 of Visual Studio, and
extensions need to be built with the same version of the compiler, but
it isn't installed.

Bah!

Maybe there is something in the .py files I can use.
 
J

John J. Lee

error: Python was built with version 6 of Visual Studio, and
extensions need to be built with the same version of the compiler, but
it isn't installed.

Bah!

Maybe there is something in the .py files I can use.

You could rebuild Python with MSVC 7 (free download of non-optimising
compiler), or use cygwin Python (or mingw Python? Not sure if that
exists...).


John
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,009
Latest member
GidgetGamb

Latest Threads

Top