mirror of
https://github.com/rdkit/rdkit.git
synced 2026-06-06 22:39:55 +08:00
1034 lines
38 KiB
Python
Executable File
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
|
|
|
|
|
|
|
|
|
|
|
|
|