Files
rdkit/Python/sping/PS/pidPS.py

1034 lines
38 KiB
Python
Executable File

"""
piddlePS - a PostScript backend for the PIDDLE drawing module
Magnus Lie Hetland
1999
"""
# some fixups by Chris Lee (cwlee@artsci.wustl.edu)
# help from Paul S. and Bernhard Herzog and others
# $Id$
#
# For each page, the coordinate system is intialized with "0 canvasHeight translate" so
# that coordiante (0,0) is at the top left of the page
# Therefore all y coordinates must be entered in opposite sign to go down
# Also, remember that angles are reversed relative to postscript standard -cwl
# TODO for version 1.0:
# X add greyscale image support for smaller images (added to level 2)
# X add Level2 postscript options
# X add underline implementation from piddlePDF
# X ** fix \n breaking in drawString
# ? Bezier curve thing (Robert Kern's fix is in piddlePDF)
# Hmm..drawCurve(..) already uses postscript's bezier curveto-- is there anything else
# to be done?
# X drawArc stuff from Eric
# In the Future:
# _ Base85 ecooding just use hex encoding involves 1:2 expansion of image data vs 4:5
# _ Don't see a flate/deflate filter for Postscript, jpeg DCTEncode could be added.
# PIL may have a LZW encoder
# _ check Adobe Document struturing conventions (half done)
# _ look at symbol's font metrics--they appear to be a little off
# X postscript native implementation of drawRoundRect
# _ improve underlining placement...doesn't look good for courier and symbol
# DSC: plan uses flags for keeping track of BeginX/EndX pairs.
# convention: use flag _inXFlag
from sping.pid import *
import string, cStringIO
import psmetrics # for font info
import exceptions
import math
class PostScriptLevelException(exceptions.ValueError):
pass
linesep = '\n'
### constants for fonts ###
# This is actually a mapping between legal font names and PSFontMapXXX keys
# note: all piddle font names are lower-cased before running against this mapping)
PiddleLegalFonts = {"helvetica": "helvetica", # note: keys are lowercased
"times": "times",
"courier": "courier",
"serif": "times",
"sansserif": "helvetica",
"monospaced": "courier",
"symbol" : "symbol"} # Could add more...
Roman="Roman"; Bold="Bold"; Italic="Italic"
# This is starting to look like a class
PSFontMapStdEnc = { ("helvetica", Roman): "Helvetica-Roman",
("helvetica", Bold): "Helvetica-Bold",
("helvetica", Italic): "Helvetica-Oblique",
("times", Roman) : "Times-Roman",
("times", Bold) : "Times-Bold",
("times", Italic) : "Times-Italic",
("courier", Roman) : "Courier-Roman",
("courier", Bold) : "Courier-Bold",
("courier", Italic) : "Courier-Oblique",
("symbol", Roman) : "Symbol",
("symbol", Bold) : "Symbol",
("symbol", Italic) : "Symbol",
"EncodingName" : 'StandardEncoding' }
PSFontMapLatin1Enc = { ("helvetica", Roman): "Helvetica-Roman-ISOLatin1",
("helvetica", Bold): "Helvetica-Bold-ISOLatin1",
("helvetica", Italic): "Helvetica-Oblique-ISOLatin1",
("times", Roman) : "Times-Roman-ISOLatin1",
("times", Bold) : "Times-Bold-ISOLatin1",
("times", Italic) : "Times-Italic-ISOLatin1",
("courier", Roman) : "Courier-Roman-ISOLatin1",
("courier", Bold) : "Courier-Bold-ISOLatin1",
("courier", Italic) : "Courier-Oblique-ISOLatin1",
("symbol", Roman) : "Symbol",
("symbol", Bold) : "Symbol",
("symbol", Italic) : "Symbol",
"EncodingName" : 'Latin1Encoding' }
###############################################################
def latin1FontEncoding(fontname):
"""use this to generating PS code for re-encoding a font as ISOLatin1
from font with name 'fontname' defines reencoded font, 'fontname-ISOLatin1'"""
latin1FontTemplate = """/%s findfont
dup length dict begin
{1 index /FID ne
{def}
{pop pop}
ifelse
} forall
/Encoding ISOLatin1Encoding def
currentdict
end
/%s-ISOLatin1 exch definefont pop
"""
#
return latin1FontTemplate % (fontname, fontname)
def dashLineDefinition():
res = r"""
%% This is hacked straight out of the Blue book (from Adobe)
/centerdash
{ /pattern exch def
/pathlen pathlength def
/patternlength 0 def
pattern
{ patternlength add /patternlength exch def
} forall
pattern length 2 mod 0 ne
{ /patternlength patternlength 2 mul def } if
/first pattern 0 get def
/last patternlength first sub def
/n pathlen last sub cvi patternlength idiv def
/endpart pathlen patternlength n mul sub
last sub 2 div def
/offset first endpart sub def
pattern offset setdash
} def
/pathlength
{ flattenpath
/dist 0 def
{ /yfirst exch def /xfirst exch def
/ymoveto yfirst def /xmoveto xfirst def }
{ /ynext exch def /xnext exch def
/dist dist ynext yfirst sub dup mul
xnext xfirst sub dup mul add sqrt add def
/yfirst ynext def /xfirst xnext def }
{}
{ /ynext ymoveto def /xnext xmoveto def
/dist dist ynext yfirst sub dup mul
xnext xfirst sub dup mul add sqrt add def
/yfirst ynext def /xfirst xnext def }
pathforall
dist
} def
"""
return res
class PsDSC:
# remeber %% will be reduced to % when using string substitution
# returned strings do not end with \n
def __init__(self):
pass
## Genral DSC conventions
def documentHeader(self):
return "%!PS-Adobe-3.0"
def boundingBoxStr(self, x0, y0, x1,y1):
"coordinates of bbox in default PS coordinates"
return "%%BoundingBox: " + "%s %s %s %s" % (x0, y0, x1,y1)
def BeginPageStr(self, pageSetupStr, pageName=None):
"""Use this at the beginning of each page, feed it your setup code
in the form of a string of postscript. pageName is the "number" of the
page. By default it will be 0."""
self.inPageFlag = 1 # keep track
if not pageName:
pageDeclaration = r"%%Page: %d %d"%(1,1) # default 1
else:
pageDeclaration = "%%Page: " + pageName
ret = pageDeclaration + "\n" + \
"""%%BeginPageSetup
/pgsave save def
"""
# print pageSetupStr ???
return ret + pageSetupStr + "\n%%EndPageSetup"
def EndPageStr(self):
self.inPageFlag = 0
return ""
class EpsDSC(PsDSC):
def __init__(self):
PsDSC.__init__(self)
## Genral DSC conventions
def documentHeader(self):
return "%!PS-Adobe-3.0 EPSF-3.0"
##########################################################################
class PSCanvas(Canvas):
"""This canvas is meant for generating encapsulated PostScript files
(EPS) used for inclusion in other documents; thus really only
single-page documents are supported. For historical reasons and
because they can be printed (a showpage is included), the files are
given a .ps extension by default, and a primitive sort of multipage
document can be generated using nextPage() or clear(). Use at your own
risk! Future versions of piddlePS will include an EPSCanvas and a
PSCanvas which will clearly delineate between single and multipage
documents.
Note: All font encodings must be taken care in __init__, you can't add
more after this"""
def __init__(self,size=(300,300),name='piddlePS',
PostScriptLevel=2,
fontMapEncoding=PSFontMapLatin1Enc):
Canvas.__init__(self,size,name)
width, height = self.size = size
self.filename = name
if len(name) < 3 or string.lower(name[-3:]) != '.ps':
self.filename = name + ".ps"
# select between postscript level 1 or level 2
if PostScriptLevel == 1 :
self.drawImage = self._drawImageLevel1
elif PostScriptLevel == 2 :
self.drawImage = self._drawImageLevel2
else :
raise PostScriptLevelException
self.code = []
self.dsc = PsDSC() # handle document structing conventions
c = self._currentColor = self.defaultLineColor
r,g,b = c.red, c.green, c.blue
w = self._currentWidth = self.defaultLineWidth
self.defaultFont = Font(face='serif')
self.fontMapEncoding = fontMapEncoding
self._currentFont = self.defaultFont
f = self._findFont(self._currentFont)
s = self._currentFont.size
# Page Structure State
#----------------------
self._inDocumentFlag = 0 # this is set in psBeginDocument
self._inPageFlag = 0 # we have't started a page
self.pageNum = 1 # User is free to reset this or even make this a string
self.psBeginDocument()
## finally begin the first page ##
self.psBeginPage() # each psBeginPage() needs to be closed w/ a psEndPage()
def psBeginDocument(self):
# General DSC Prolog := <header> [<defaults>] <procedures>
self.code.append(self.dsc.documentHeader())
self.code.append(self.dsc.boundingBoxStr(0,0,self.size[0],self.size[1]))
self.code.append("%%Pages: (atend)")
self._inDocumentFlag = 1 # we need a Trailer to fix this up
###### Defaults Procedures Prolog Setup ??? ######
# are these procedures??? check this chris
# Now create Latin1ISO font encodings for non-symbol fonts (need to add pdf fonts too)
shapes = {"Helvetica": ["Roman", "Bold", "Oblique"],
"Times": ["Roman", "Bold", "Italic"],
"Courier": ["Roman", "Bold", "Oblique"] }
fntnames = []
for basename in ['Helvetica', 'Times', 'Courier']:
for mys in shapes[basename]:
fntnames.append(basename + '-' + mys)
# assign the default FontMapping (which also determines the encoding)
for fontname in fntnames:
self.code.append(latin1FontEncoding(fontname))
self.code.append(dashLineDefinition())
def psEndDocument(self):
if self._inDocumentFlag:
# Take care of Trailer
self.code.append("%%Trailer")
self.code.append("%%%%Pages: %d" % self.pageNum)
# signal end of file
# check on need for %%EOF
self.code.append("%%EOF") # remove \n at end of EOF
def psBeginPage(self, pageName=None):
# call this function when beginning a new page but before any piddle drawing commands
# pagesetup contains code to insert into DSC page 'header'
if not pageName:
pageName = "%s %s" %(self.pageNum,self.pageNum)
pagesetup = self._psPageSetupStr(self.size[1], self.defaultLineColor,
self._findFont(self.defaultFont),
self.defaultFont.size,
self.defaultLineWidth)
self.code.append(self.dsc.BeginPageStr(pageSetupStr= pagesetup, pageName=pageName ))
self._inPageFlag = 1
def _psPageSetupStr(self,pageheight, initialColor, font_family, font_size, line_width):
"ps code for settin up coordinate system for page in accords w/ piddle standards"
r,g,b = initialColor.red, initialColor.green, initialColor.blue
return '''
%% initialize
2 setlinecap
0 %d
translate
%s %s %s setrgbcolor
(%s) findfont %s scalefont setfont
%s setlinewidth''' % (pageheight, r, g, b, font_family, font_size, line_width)
def psEndPage(self):
self.code.append("pgsave restore")
self.code.append("showpage")
self._inPageFlag = 0
def _findFont(self,font):
requested = font.face or "Serif" # Serif is the default
if type(requested) == StringType:
requested = [requested]
# once again, fall back to default, redundant, no?
face = string.lower(PiddleLegalFonts["serif"])
for reqFace in requested:
if PiddleLegalFonts.has_key(string.lower(reqFace)):
face = string.lower(PiddleLegalFonts[string.lower(reqFace)])
break
if font.bold:
shape = Bold
elif font.italic:
shape = Italic
else:
shape = Roman
return self.fontMapEncoding[(face, shape)]
def _findExternalFontName(self, font): #copied from piddlePDF by cwl- hack away!
"""Attempts to return proper font name.
PDF uses a standard 14 fonts referred to
by name. Default to self.defaultFont('Helvetica').
The dictionary allows a layer of indirection to
support a standard set of PIDDLE font names."""
piddle_font_map = {
'Times':'Times',
'times':'Times',
'Courier':'Courier',
'courier':'Courier',
'helvetica':'Helvetica',
'Helvetica':'Helvetica',
'symbol':'Symbol',
'Symbol':'Symbol',
'monospaced':'Courier',
'serif':'Times',
'sansserif':'Helvetica',
'ZapfDingbats':'ZapfDingbats',
'zapfdingbats':'ZapfDingbats',
'arial':'Helvetica'
}
try:
face = piddle_font_map[string.lower(font.face)]
except:
return 'Helvetica'
name = face + '-'
if font.bold and face in ['Courier','Helvetica','Times']:
name = name + 'Bold'
if font.italic and face in ['Courier', 'Helvetica']:
name = name + 'Oblique'
elif font.italic and face == 'Times':
name = name + 'Italic'
if name == 'Times-':
name = name + 'Roman'
# symbol and ZapfDingbats cannot be modified!
#trim and return
if name[-1] == '-':
name = name[0:-1]
return name
def _psNextPage(self):
"advance to next page of document. "
self.psEndPage()
self.pageNum = self.pageNum + 1
self.psBeginPage()
def nextPage(self):
self.clear()
def clear(self):
"""clear resets the canvas to it's default state. Though this
canvas is really only meant to be an EPS canvas, i.e., single page,
for historical reasons we will allow multipage documents. Thus
clear will end the page, clear the canvas state back to default,
and start a new page. In the future, this PSCanvas will become
EPSCanvas and will not support multipage documents. In that case,
the canvas will be reset to its default state and the file will be
emptied of all previous drawing commands"""
self.resetToDefaults()
self._psNextPage()
def resetToDefaults(self):
self._currentColor = self.defaultLineColor
self._currentWidth = self.defaultLineWidth
self._currentFont = self.defaultFont
def flush(self):
pass
# Comment the following out to make flush() a null function
# file = open(self.filename,'w')
# from string import join
# file.write(join(self.code,linesep))
# file.write('\nshowpage\n')
# file.close()
def save(self, file=None, format=None):
"""Write the current document to a file or stream and close the file
Computes any final trailers, etc. that need to be done in order to
produce a well formed postscript file. At least for now though,
it still allows you to add to the file after a save by not actually
inserting the finalization code into self.code
the format argument is not used"""
# save() will now become part of the spec.
file = file or self.filename
fileobj = getFileObject(file)
fileobj.write(string.join(self.code, linesep))
# here's a hack. we might want to be able to add more after saving so
# preserve the current code ???
preserveCode = self.code
self.code = finalizationCode = [""]
# might be able to move these things into DSC class & use a save state call
preserve_inPageFlag = self._inPageFlag
preserve_inDocumentFlag = self._inDocumentFlag
# now do finalization code in via self.code
# first question: are we in the middle of a page?
if self._inPageFlag:
self.psEndPage()
self.psEndDocument() # depends on _inDocumentFlag :(
fileobj.write(string.join(finalizationCode, linesep))
# fileobj.close() ### avoid this for now
## clean up my mess: This is not a good way to do things FIXME!!! ???
self.code = preserveCode
self._inPageFlag = preserve_inPageFlag
self._inDocumentFlag = preserve_inDocumentFlag
def stringWidth(self, s, font=None):
"Return the logical width of the string if it were drawn \
in the current font (defaults to self.font)."
if not font:
font = self.defaultFont
fontname = self._findExternalFontName(font)
return psmetrics.stringwidth(s, fontname,
self.fontMapEncoding["EncodingName"]) * font.size * 0.001
### def fontHeight(self, font=None) ### use default piddle method
# distance between baseline of two lines of text 1.2 * font.size for piddle.py
# Thus is 1.2 times larger than postscript minimum baseline to baseline separation
# for single-spaced text. fontHeight() used internally for drawString() multi-line text
def fontAscent(self, font=None):
if not font:
font = self.defaultFont
fontname = self._findExternalFontName(font)
return psmetrics.ascent_descent[fontname][0] * 0.001 * font.size
def fontDescent(self, font=None):
if not font:
font = self.defaultFont
fontname = self._findExternalFontName(font)
return -psmetrics.ascent_descent[fontname][1] * 0.001 * font.size
def _updateLineColor(self, color):
color = color or self.defaultLineColor
if color != self._currentColor:
self._currentColor = color
r,g,b = color.red, color.green, color.blue
self.code.append('%s %s %s setrgbcolor' % (r,g,b))
def _updateFillColor(self, color):
color = color or self.defaultFillColor
if color != self._currentColor:
self._currentColor = color
r,g,b = color.red, color.green, color.blue
self.code.append('%s %s %s setrgbcolor' % (r,g,b))
def _updateLineWidth(self, width):
if width == None: width = self.defaultLineWidth
if width != self._currentWidth:
self._currentWidth = width
self.code.append('%s setlinewidth' % width)
def _updateFont(self, font):
font = font or self.defaultFont
if font != self._currentFont:
self._currentFont = font
f = self._findFont(font)
s = font.size
self.code.append('(%s) findfont %s scalefont setfont' % (f, s))
def drawLine(self, x1, y1, x2, y2, color=None, width=None,
dash=None,**kwargs):
self._updateLineColor(color)
self._updateLineWidth(width)
if dash is not None:
dashTxt = '['+'%d '*len(dash)+']'
dashTxt = dashTxt%dash
self.code.append('%s centerdash'%dashTxt)
if self._currentColor != transparent:
self.code.append('%s %s neg moveto %s %s neg lineto stroke' %
(x1, y1, x2, y2))
if dash is not None:
self.code.append('[1 0] centerdash')
def drawLines(self, lineList, color=None, width=None,dash=None,**kwargs):
self._updateLineColor(color)
self._updateLineWidth(width)
codeline = '%s %s neg moveto %s %s neg lineto stroke'
if self._currentColor != transparent:
for line in lineList:
self.code.append(codeline % line)
def _escape(self, s):
# return a copy of string s with special characters in postscript strings
# escaped" with backslashes."""
# Have not handled characters that are converted normally in python strings
# i.e. \n -> newline
str = string.replace(s, chr(0x5C), r'\\' )
str = string.replace(str, '(', '\(' )
str = string.replace(str, ')', '\)')
return str
# ??? check to see if \n response is handled correctly (should move cursor down)
def _drawStringOneLineNoRot(self, s, x, y, font=None,**kwargs) :
# PRE: x and y and position at which to place text
# PRE: helper function, only called from drawString(..)
text = self._escape(s)
self.code.append('%s %s neg moveto (%s) show' % (x,y,text))
if self._currentFont.underline:
swidth = self.stringWidth(s, self._currentFont)
ypos = (0.5 * self.fontDescent(self._currentFont))
thickness = 0.08 * self._currentFont.size # relate to font.descent?
self.code.extend(['%s setlinewidth' % thickness,
'0 %s neg rmoveto' % (ypos),
'%s 0 rlineto stroke' % -swidth])
def _drawStringOneLine(self, s, x, y, font=None, color=None, angle=0,
**kwargs):
# PRE: assumes that coordinate system has already been set for rotated strings
# PRE: only meant to be called by drawString(..)
text = self._escape(s)
self.code.extend(['%f %f neg moveto (%s) show' % (x, y, text)])
if self._currentFont.underline:
swidth = self.stringWidth(s, self._currentFont)
dy = (0.5 * self.fontDescent(self._currentFont))
thickness = 0.08 * self._currentFont.size # relate to font.descent?
self.code.extend(['%s setlinewidth' % thickness,
'%f %f neg moveto' % (x, dy+y),
'%f 0 rlineto stroke' % swidth])
def drawString(self, s, x, y, font=None, color=None, angle=0,**kwargs):
"""drawString(self, s, x, y, font=None, color=None, angle=0)
draw a string s at position x,y"""
self._updateLineColor(color)
self._updateFont(font)
if self._currentColor != transparent:
lines = string.split(s, '\n')
lineHeight = self.fontHeight(font)
if angle == 0 : # do special case of angle = 0 first. Avoids a bunch of gsave/grestore ops
for line in lines:
self._drawStringOneLineNoRot(line, x, y, font=font,**kwargs)
else : # general case, rotated text
self.code.extend([
'gsave',
'%s %s neg translate' % (x,y),
`angle`+' rotate'])
down = 0
for line in lines :
self._drawStringOneLine(line, 0, 0+down, font, color, angle,
**kwargs)
down = down + lineHeight
self.code.extend(['grestore'])
def drawCurve(self, x1, y1, x2, y2, x3, y3, x4, y4,
edgeColor=None, edgeWidth=None, fillColor=None, closed=0,
dash=None,**kwargs):
codeline = '%s %s neg moveto %s %s neg %s %s neg %s %s neg curveto'
data = (x1, y1, x2, y2, x3, y3, x4, y4)
self._updateFillColor(fillColor)
if self._currentColor != transparent:
self.code.append((codeline % data) + ' eofill')
self._updateLineWidth(edgeWidth)
self._updateLineColor(edgeColor)
if self._currentColor != transparent:
self.code.append((codeline % data)
+ ((closed and ' closepath') or '')
+ ' stroke')
########################################################################################
def drawRoundRect(self, x1,y1, x2,y2, rx=8, ry=8,
edgeColor=None, edgeWidth=None, fillColor=None,
dash=None,**kwargs):
"Draw a rounded rectangle between x1,y1, and x2,y2, \
with corners inset as ellipses with x radius rx and y radius ry. \
These should have x1<x2, y1<y2, rx>0, and ry>0."
# Path is drawn in counter-clockwise direction"
x1, x2 = min(x1,x2), max(x1, x2) # from piddle.py
y1, y2 = min(y1,y2), max(y1, y2)
# Note: arcto command draws a line from current point to beginning of arc
# save current matrix, translate to center of ellipse, scale by rx ry, and draw
# a circle of unit radius in counterclockwise dir, return to original matrix
# arguments are (cx, cy, rx, ry, startAngle, endAngle)
ellipsePath = 'matrix currentmatrix %s %s neg translate %s %s scale 0 0 1 %s %s arc setmatrix'
# choice between newpath and moveto beginning of arc
# go with newpath for precision, does this violate any assumptions in code???
# rrcode = ['%s %s neg moveto' % (x1+rx, y1)] # this also works
rrcode = ['newpath'] # Round Rect code path
# upper left corner ellipse is first
rrcode.append(ellipsePath % (x1+rx, y1+ry, rx, ry, 90, 180))
rrcode.append(ellipsePath % (x1+rx, y2-ry, rx, ry, 180, 270))
rrcode.append(ellipsePath % (x2-rx, y2-ry, rx, ry, 270, 360))
rrcode.append(ellipsePath % (x2-rx, y1+ry, rx, ry, 0, 90) )
rrcode.append('closepath')
# This is what you are required to do to take care of all color cases
# should fix this so it doesn't define path twice, just use gsave if need
# to fill and stroke path-need to figure out this system
self._updateFillColor(fillColor)
if self._currentColor != transparent:
self.code.extend(rrcode)
self.code.append("eofill")
self._updateLineWidth(edgeWidth)
self._updateLineColor(edgeColor)
if self._currentColor != transparent:
self.code.extend(rrcode)
self.code.append("stroke")
def drawEllipse(self, x1,y1, x2,y2, edgeColor=None, edgeWidth=None,
fillColor=None,dash=None,**kwargs):
"Draw an orthogonal ellipse inscribed within the rectangle x1,y1,x2,y2. \
These should have x1<x2 and y1<y2."
#Just invoke drawArc to actually draw the ellipse
self.drawArc(x1,y1, x2,y2, edgeColor=edgeColor, edgeWidth=edgeWidth,
fillColor=fillColor)
def drawArc(self, x1,y1, x2,y2, startAng=0, extent=360, edgeColor=None,
edgeWidth=None, fillColor=None, dash=None, **kwargs):
"Draw a partial ellipse inscribed within the rectangle x1,y1,x2,y2, \
starting at startAng degrees and covering extent degrees. Angles \
start with 0 to the right (+x) and increase counter-clockwise. \
These should have x1<x2 and y1<y2."
#calculate centre of ellipse
cx, cy = (x1+x2)/2.0, (y1+y2)/2.0
rx, ry = (x2-x1)/2.0, (y2-y1)/2.0
codeline = self._genArcCode(x1, y1, x2, y2, startAng, extent)
# fill portion
self._updateFillColor(fillColor)
if self._currentColor != transparent:
self.code.append('%s %s neg moveto' % (cx,cy)) # move to center of circle
self.code.append(codeline + ' eofill')
# stroke portion
self._updateLineWidth(edgeWidth)
self._updateLineColor(edgeColor)
if self._currentColor != transparent:
# move current point to start of arc, note negative angle because y increases down
self.code.append('%s %s neg moveto' % (cx+rx*math.cos(-startAng),
cy+ry*math.sin(-startAng)))
self.code.append(codeline + ' stroke')
def _genArcCode(self, x1, y1, x2, y2, startAng, extent):
"Calculate the path for an arc inscribed in rectangle defined by (x1,y1),(x2,y2)"
#calculate semi-minor and semi-major axes of ellipse
xScale = abs((x2-x1)/2.0)
yScale = abs((y2-y1)/2.0)
#calculate centre of ellipse
x, y = (x1+x2)/2.0, (y1+y2)/2.0
codeline = 'matrix currentmatrix '+\
'%s %s neg translate %s %s scale 0 0 1 %s %s %s '+\
'setmatrix'
if extent >= 0:
arc='arc'
else:
arc='arcn'
data = (x,y, xScale, yScale, startAng, startAng+extent, arc)
return codeline % data
def drawPolygon(self, pointlist,
edgeColor=None, edgeWidth=None, fillColor=None, closed=0,
dash=None, **kwargs):
start = pointlist[0]
pointlist = pointlist[1:]
polyCode = []
polyCode.append("%s %s neg moveto" % start)
for point in pointlist:
polyCode.append("%s %s neg lineto" % point)
if closed:
polyCode.append("closepath")
self._updateFillColor(fillColor)
if self._currentColor != transparent:
self.code.extend(polyCode)
self.code.append("eofill")
self._updateLineWidth(edgeWidth)
self._updateLineColor(edgeColor)
if self._currentColor != transparent:
self.code.extend(polyCode)
self.code.append("stroke")
def drawFigure(self, partList,
edgeColor=None, edgeWidth=None, fillColor=None, closed=0,
dash=None,**kwargs):
figureCode = []
first = 1
for part in partList:
op = part[0]
args = list(part[1:])
if op == figureLine:
if first:
first = 0
figureCode.append("%s %s neg moveto" % tuple(args[:2]))
else:
figureCode.append("%s %s neg lineto" % tuple(args[:2]))
figureCode.append("%s %s neg lineto" % tuple(args[2:]))
elif op == figureArc:
first = 0
x1,y1,x2,y2,startAngle,extent = args[:6]
figureCode.append(self._genArcCode(x1,y1,x2,y2,startAngle,extent))
elif op == figureCurve:
if first:
first = 0
figureCode.append("%s %s neg moveto" % tuple(args[:2]))
else:
figureCode.append("%s %s neg lineto" % tuple(args[:2]))
figureCode.append("%s %s neg %s %s neg %s %s neg curveto" % tuple(args[2:]))
else:
raise TypeError, "unknown figure operator: "+op
if closed:
figureCode.append("closepath")
self._updateFillColor(fillColor)
if self._currentColor != transparent:
self.code.extend(figureCode)
self.code.append("eofill")
self._updateLineWidth(edgeWidth)
self._updateLineColor(edgeColor)
if self._currentColor != transparent:
self.code.extend(figureCode)
self.code.append("stroke")
############################################################################################
# drawImage(self. image, x1, y1, x2=None, y2=None) is now defined by either _drawImageLevel1
# ._drawImageLevel2, the choice is made in .__init__ depending on option
def _drawImageLevel1(self, image, x1, y1, x2=None,y2=None,**kwargs):
# Postscript Level1 version available for fallback mode when Level2 doesn't work
"""drawImage(self,image,x1,y1,x2=None,y2=None) : If x2 and y2 are ommitted, they are
calculated from image size. (x1,y1) is upper left of image, (x2,y2) is lower right of
image in piddle coordinates."""
try:
from PIL import Image
except ImportError:
print 'Python Imaging Library not available'
return
# For now let's start with 24 bit RGB images (following piddlePDF again)
print "Trying to drawImage in piddlePS"
component_depth = 8
myimage = image.convert('RGB')
imgwidth, imgheight = myimage.size
if not x2:
x2 = imgwidth + x1
if not y2:
y2 = y1 + imgheight
drawwidth = x2 - x1
drawheight = y2 - y1
print 'Image size (%d, %d); Draw size (%d, %d)' % (imgwidth, imgheight, drawwidth, drawheight)
# now I need to tell postscript how big image is
# "image operators assume that they receive sample data from
# their data source in x-axis major index order. The coordinate
# of the lower-left corner of the first sample is (0,0), of the
# second (1,0) and so on" -PS2 ref manual p. 215
#
# The ImageMatrix maps unit squre of user space to boundary of the source image
#
# The CurrentTransformationMatrix (CTM) maps the unit square of
# user space to the rect...on the page that is to receive the
# image. A common ImageMatrix is [width 0 0 -height 0 height]
# (for a left to right, top to bottom image )
# first let's map the user coordinates start at offset x1,y1 on page
self.code.extend([
'gsave',
'%s %s translate' % (x1,-y1 - drawheight), # need to start are lower left of image
'%s %s scale' % (drawwidth,drawheight),
'/scanline %d 3 mul string def' % imgwidth # scanline by multiples of image width
])
# now push the dimensions and depth info onto the stack
# and push the ImageMatrix to map the source to the target rectangle (see above)
# finally specify source (PS2 pp. 225 ) and by exmample
self.code.extend([
'%s %s %s' % (imgwidth, imgheight, component_depth),
'[%s %s %s %s %s %s]' % (imgwidth, 0, 0, -imgheight, 0, imgheight),
'{ currentfile scanline readhexstring pop } false 3',
'colorimage '
])
# data source output--now we just need to deliver a hex encode
# series of lines of the right overall size can follow
# piddlePDF again
rawimage = myimage.tostring()
assert(len(rawimage) == imgwidth*imgheight, 'Wrong amount of data for image')
#compressed = zlib.compress(rawimage) # no zlib at moment
hex_encoded = self._AsciiHexEncode(rawimage)
# write in blocks of 78 chars per line
outstream = cStringIO.StringIO(hex_encoded)
dataline = outstream.read(78)
while dataline <> "":
self.code.append(dataline)
dataline= outstream.read(78)
self.code.append('% end of image data') # for clarity
self.code.append('grestore') # return coordinates to normal
# end of drawImage
def _AsciiHexEncode(self, input): # also based on piddlePDF
"Helper function used by images"
output = cStringIO.StringIO()
for char in input:
output.write('%02x' % ord(char))
output.reset()
return output.read()
def _drawImageLevel2(self, image, x1,y1, x2=None,y2=None): # Postscript Level2 version
try:
from PIL import Image
except ImportError:
print 'Python Imaging Library not available'
return
# I don't have zlib -cwl
# try:
# import zlib
# except ImportError:
# print 'zlib not available'
# return
### what sort of image are we to draw
if image.mode=='L' :
print 'found image.mode= L'
imBitsPerComponent = 8
imNumComponents = 1
myimage = image
elif image.mode == '1':
print 'found image.mode= 1'
myimage = image.convert('L')
imNumComponents = 1
myimage = image
else :
myimage = image.convert('RGB')
imNumComponents = 3
imBitsPerComponent = 8
imwidth, imheight = myimage.size
# print 'imwidth = %s, imheight = %s' % myimage.size
if not x2:
x2 = imwidth + x1
if not y2:
y2 = y1 + imheight
drawwidth = x2 - x1
drawheight = y2 - y1
self.code.extend([
'gsave',
'%s %s translate' % (x1,-y1 - drawheight), # need to start are lower left of image
'%s %s scale' % (drawwidth,drawheight)])
if imNumComponents == 3 :
self.code.append('/DeviceRGB setcolorspace')
elif imNumComponents == 1 :
self.code.append('/DeviceGray setcolorspace')
print 'setting colorspace gray'
# create the image dictionary
self.code.append("""
<<
/ImageType 1
/Width %d /Height %d %% dimensions of source image
/BitsPerComponent %d""" % (imwidth, imheight, imBitsPerComponent) )
if imNumComponents == 1:
self.code.append('/Decode [0 1]')
if imNumComponents == 3:
self.code.append('/Decode [0 1 0 1 0 1] %% decode color values normally')
self.code.extend(['/ImageMatrix [%s 0 0 %s 0 %s]' % (imwidth, -imheight, imheight),
'/DataSource currentfile /ASCIIHexDecode filter',
'>> % End image dictionary',
'image'])
# after image operator just need to dump image dat to file as hexstring
rawimage = myimage.tostring()
assert(len(rawimage) == imwidth*imheight, 'Wrong amount of data for image')
#compressed = zlib.compress(rawimage) # no zlib at moment
hex_encoded = self._AsciiHexEncode(rawimage)
# write in blocks of 78 chars per line
outstream = cStringIO.StringIO(hex_encoded)
dataline = outstream.read(78)
while dataline <> "":
self.code.append(dataline)
dataline= outstream.read(78)
self.code.append('> % end of image data') # > is EOD for hex encoded filterfor clarity
self.code.append('grestore') # return coordinates to normal