Files
rdkit/Python/sping/PDF/pidPDF.py
Greg Landrum 75a79b6327 initial import
2006-05-06 22:20:08 +00:00

714 lines
26 KiB
Python
Executable File

# piddlePDF.py - a PDF backend for PIDDLE
"""This is the PIDDLE back end for PDF. It acts as a wrapper
over the pdfgen.Canvas class, and translates between the
PIDDLE graphics state and the PDF/PostScript one. It only
exposes PIDDLE methods; however, it has an attribute
self.pdf which offers numerous lower-level drawing routines.
"""
# Copyright Robinson Analytics 1998-9
# 6/10/99 - near total rewrite. This is just a wrapper
# on top of a lower-level graphics canvas which speaks the
# Postscript metaphor. Offers native methods for everything
# except drawFigure, which doesn't behave like PostScript
# paths so I left it unchanged.
#standard python library modules
import string
import cStringIO
import pdfmetrics
import glob
import os
import types
from math import sin, cos, pi, ceil
# app specific
import sping.pagesizes
from sping.pid import *
import pdfgen
import pdfgeom
#edit this is the setting offends you, or set it in the constructor
DEFAULT_PAGE_SIZE = sping.pagesizes.A4
#DEFAULT_PAGE_SIZE = sping.pagesizes.letter
##########################################################################
#
# Font Mappings
# The brute force approach to finding the correct postscript font name;
# much safer than the rule-based ones we tried.
# preprocessor to reduce font face names to the shortest list
# possible. Add any aliases you wish; it keeps looking up
# until it finds no more translations to do. Any input
# will be lowercased before checking.
font_face_map = {
'serif':'times',
'sansserif':'helvetica',
'monospaced':'courier',
'arial':'helvetica'
}
#maps a piddle font to a postscript one.
ps_font_map = {
#face, bold, italic -> ps name
('times', 0, 0) :'Times-Roman',
('times', 1, 0) :'Times-Bold',
('times', 0, 1) :'Times-Italic',
('times', 1, 1) :'Times-BoldItalic',
('courier', 0, 0) :'Courier',
('courier', 1, 0) :'Courier-Bold',
('courier', 0, 1) :'Courier-Oblique',
('courier', 1, 1) :'Courier-BoldOblique',
('helvetica', 0, 0) :'Helvetica',
('helvetica', 1, 0) :'Helvetica-Bold',
('helvetica', 0, 1) :'Helvetica-Oblique',
('helvetica', 1, 1) :'Helvetica-BoldOblique',
# there is only one Symbol font
('symbol', 0, 0) :'Symbol',
('symbol', 1, 0) :'Symbol',
('symbol', 0, 1) :'Symbol',
('symbol', 1, 1) :'Symbol',
# ditto for dingbats
('zapfdingbats', 0, 0) :'ZapfDingbats',
('zapfdingbats', 1, 0) :'ZapfDingbats',
('zapfdingbats', 0, 1) :'ZapfDingbats',
('zapfdingbats', 1, 1) :'ZapfDingbats',
}
######################################################################
#
# Canvas class
#
######################################################################
class PDFCanvas(Canvas):
"""This works by accumulating a list of strings containing
PDF page marking operators, as you call its methods. We could
use a big string but this is more efficient - only concatenate
it once, with control over line ends. When
done, it hands off the stream to a PDFPage object."""
def __init__(self,
size=None,
name="pidPDF.pdf",
pagesize=DEFAULT_PAGE_SIZE
):
#if no extension, add .PDF
root, ext = os.path.splitext(name)
if ext == '':
name = root + '.pdf'
#create the underlying pdfgen canvas and set some attributes
self.pdf = pdfgen.Canvas(name, pagesize=pagesize, bottomup=0) # add pagesize to constructor
# by default do not use comrpression (mod by cwl, may not be necessary w/ newer pdfgen)
self.pdf.setPageCompression(0)
self.pdf.setLineCap(2)
#now call super init, which will trigger
#calls into self.pdf
Canvas.__init__(self, size=size,name=name)
#memorize stuff
self.pagesize = pagesize
self.filename = name
# self.pdf.setPageSize(pagesize) # This doesn't seem to work correctly -cwl
if size == None:
#take the page size, which might not be default
self.drawingsize = self.pagesize
else:
#convenience for other platformslike GUI views
#we let them centre a smaller drawing in a page
self.drawingsize = size
self.pageTransitionString = ''
self.pageNumber = 1 # keep a count
#if they specified a size smaller than page,
# be helpful and centre their diagram
if self.pagesize <> self.drawingsize:
dx = 0.5 * (self.pagesize[0] - self.drawingsize[0])
dy = 0.5 * (self.pagesize[1] - self.drawingsize[1])
self.pdf.translate(dx, dy)
def _resetDefaults(self):
"""Only used in setup - persist from page to page"""
self.defaultLineColor = black
self.defaultFillColor = transparent
self.defaultLineWidth = 1
self.defaultFont = Font()
self.pdf.setLineCap(2)
def showPage(self):
"""ensure basic settings are the same after a page break"""
self.pdf.showPage()
self.defaultFont = self.defaultFont
self.defaultLineColor = self.defaultLineColor
self.defaultFillColor = self.defaultFillColor
self.defaultLineWidth = self.defaultLineWidth
self.pdf.setLineCap(2)
#------------ canvas capabilities -------------
def isInteractive(self):
return 0
def canUpdate(self):
return 0
#------------ general management -------------
def clear(self):
"Not wll defined for file formats, use same as ShowPage"
self.showPage()
def flush(self):
pass
def save(self, file=None, format=None):
"""Saves the file. If holding data, do
a showPage() to save them having to."""
if self.pdf.pageHasData():
self.pdf.showPage()
if hasattr(file, 'write'):
self.pdf.save(fileobj=file)
elif isinstance(file, types.StringType):
self.pdf.save(filename=file)
else:
self.pdf.save()
def setInfoLine(self, s):
self.pdf.setTitle(s)
#-------------handle assignment to defaultXXX-------
def __setattr__(self, key, value):
#we let it happen...
self.__dict__[key] = value
#...but take action if needed
if key == "defaultLineColor":
self._updateLineColor(value)
elif key == "defaultLineWidth":
self._updateLineWidth(value)
elif key == "defaultFillColor":
self._updateFillColor(value)
elif key == "defaultFont":
self._updateFont(value)
def _updateLineColor(self, color):
"""Triggered when someone assigns to defaultLineColor"""
self.pdf.setStrokeColorRGB(color.red, color.green, color.blue)
def _updateFillColor(self, color):
"""Triggered when someone assigns to defaultFillColor"""
self.pdf.setFillColorRGB(color.red, color.green, color.blue)
def _updateLineWidth(self, width):
"""Triggered when someone assigns to defaultLineWidth"""
self.pdf.setLineWidth(width)
def _updateFont(self, font):
"""Triggered when someone assigns to defaultFont"""
psfont = self._findPostScriptFontName(font)
self.pdf.setFont(psfont, font.size)
def _findPostScriptFontName(self, font):
"""Attempts to return proper font name."""
#step 1 - no face ends up serif, others are lowercased
if not font.face:
face = 'serif'
else:
face = string.lower(font.face)
while font_face_map.has_key(face):
face = font_face_map[face]
#step 2, - resolve bold/italic to get the right PS font name
psname = ps_font_map[(face, font.bold, font.italic)]
return psname
def _escape(self, s):
"""PDF escapes are like Python ones, but brackets need slashes before them too.
Use Python's repr function and chop off the quotes first"""
s = repr(s)[1:-1]
s = string.replace(s, '(','\(')
s = string.replace(s, ')','\)')
return s
def resetDefaults(self):
"""If you drop down to a lower level, PIDDLE can lose
track of the current graphics state. Calling this after
wards ensures that the canvas is updated to the same
defaults as PIDDLE thinks they should be."""
self.defaultFont = self.defaultFont
self.defaultLineColor = self.defaultLineColor
self.defaultFillColor = self.defaultFillColor
self.defaultLineWidth = self.defaultLineWidth
#------------ string/font info ------------
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._findPostScriptFontName(font)
return pdfmetrics.stringwidth(s, fontname) * font.size * 0.001
def fontHeight(self, font=None):
if not font:
font = self.defaultFont
return font.size
def fontAscent(self, font=None):
if not font:
font = self.defaultFont
fontname = self._findPostScriptFontName(font)
return pdfmetrics.ascent_descent[fontname][0] * 0.001 * font.size
def fontDescent(self, font=None):
if not font:
font = self.defaultFont
fontname = self._findPostScriptFontName(font)
return -pdfmetrics.ascent_descent[fontname][1] * 0.001 * font.size
#------------- drawing helpers --------------
def _endPath(self, path, edgeColor, fillColor):
"""in PIDDLE, the edge and fil colors might be transparent,
and might also be None, in which case they should be taken
from the defaults. This leads to a standard 10 lines of code
when closing each shape, which are wrapped up here. Use
these if you implement new PIDDLE shapes."""
#allow for transparent fills and lines
fill = fillColor or self.defaultFillColor
edge = edgeColor or self.defaultLineColor
if (fill == transparent and edge == transparent):
pass
else:
self.pdf.drawPath(
path,
(edge <> transparent), #whether to stroke
(fill <> transparent) #whether to fill
)
#------------- drawing methods --------------
def drawLine(self, x1,y1, x2,y2, color=None, width=None,
dash=None, **kwargs):
"""Calls the underlying methods in pdfgen.canvas. For the
highest performance, use canvas.setDefaultFont and
canvas.setLineWidth, and draw batches of similar
lines together."""
#set the state if needed
if color:
self._updateLineColor(color)
if width:
self._updateLineWidth(width)
# now do the work
self.pdf.line(x1, y1, x2, y2)
#now reset state if needed
if color:
self._updateLineColor(self.defaultLineColor)
if width:
self._updateLineWidth(self.defaultLineWidth)
def drawLines(self, lineList, color=None, width=None,
dash=None, **kwargs):
"""Draws several distinct lines, all with same color
and width, efficiently"""
if color:
self._updateLineColor(color)
if width:
self._updateLineWidth(width)
self.pdf.lines(lineList)
if color:
self._updateLineColor(self.defaultLineColor)
if width:
self._updateLineWidth(self.defaultLineWidth)
def drawString(self, s, x, y, font=None, color=None, angle=0,
**kwargs):
"""As it says, but many options to process. It translates
user space rather than text space, in case underlining is
needed on rotated text. It cheats and does literals
for efficiency, avoiding changing the python graphics state."""
self.pdf.addLiteral('%begin drawString')
col = color or self.defaultLineColor
if col != transparent:
if '\n' in s or '\r' in s:
#normalize line ends
s = string.replace(s, '\r\n','\n')
s = string.replace(s, '\n\r','\n')
lines = string.split(s, '\n')
else:
lines = [s]
fnt = font or self.defaultFont
self._updateFont(fnt)
text = self.pdf._escape(s)
# start of Chris's hacking
# inserting basic commands here to see if can get working
textobj = self.pdf.beginText()
if col <> self.defaultFillColor:
textobj.setFillColorRGB(col.red,col.green, col.blue)
if angle != 0 :
co = cos(angle * pi / 180.0)
si = sin(angle * pi / 180.0)
textobj.setTextTransform(co, -si, si, co, x, y) #top down coords so reverse angle
else :
textobj.setTextOrigin(x,y)
for line in lines:
#keep underlining separate - it is slow and unusual anyway
if fnt.underline:
#breaks on angled text - FIXME
ycursor = textobj.getY() # returns offset from last set origin
dy = 0.5 * self.fontDescent(fnt)
width = self.stringWidth(line, fnt)
linewidth = fnt.size * 0.1
self.pdf.saveState()
self.pdf.setLineWidth(linewidth)
self.pdf.translate(x,y) # need to translate first before rotate
if angle != 0 :
self.pdf.rotate(-angle)
self.pdf.translate(0, ycursor-y) #move down to start of current text line
self.pdf.line(0,dy, width, dy)
self.pdf.restoreState()
lasty = ycursor
textobj.textLine(line) # adds text to textobj, advances getY's cursor
# finally actually send text object to the page
self.pdf.drawText(textobj) # draw all the text afterwards? Doesn't seem right
self.pdf.addLiteral('%end drawString')
# done wth drawString()
def drawCurve(self, x1, y1, x2, y2, x3, y3, x4, y4,
edgeColor=None, edgeWidth=None, fillColor=None, closed=0,
dash=None, **kwargs):
"""This could do two totally different things. If not closed,
just does a bezier curve so fill is irrelevant. If closed,
it is actually a filled shape."""
if closed:
if edgeColor:
self._updateLineColor(edgeColor)
if edgeWidth:
self._updateLineWidth(edgeWidth)
if fillColor:
self._updateFillColor(fillColor)
p = self.pdf.beginPath()
p.moveTo(x1, y1)
p.curveTo(x2, y2, x3, y3, x4, y4)
p.close()
self._endPath(p, edgeColor, fillColor) #handles case of transparency
if edgeColor:
self._updateLineColor(self.defaultLineColor)
if edgeWidth:
self._updateLineWidth(self.defaultLineWidth)
if fillColor:
self._updateFillColor(self.defaultFillColor)
else:
#just a plain old line segment
if edgeColor:
self._updateLineColor(edgeColor)
if edgeWidth:
self._updateLineWidth(edgeWidth)
self.pdf.bezier(x1, y1, x2, y2, x3, y3, x4, y4)
if edgeColor:
self._updateLineColor(self.defaultLineColor)
if edgeWidth:
self._updateLineWidth(self.defaultLineWidth)
def drawRect(self, x1, y1, x2, y2, edgeColor=None,
edgeWidth=None, fillColor=None,dash=None,**kwargs):
if edgeColor:
self._updateLineColor(edgeColor)
if edgeWidth:
self._updateLineWidth(edgeWidth)
if fillColor:
self._updateFillColor(fillColor)
p = self.pdf.beginPath()
p.rect(x1, y1, x2-x1, y2-y1)
self._endPath(p, edgeColor, fillColor) #handles case of transparency
if edgeColor:
self._updateLineColor(self.defaultLineColor)
if edgeWidth:
self._updateLineWidth(self.defaultLineWidth)
if fillColor:
self._updateFillColor(self.defaultFillColor)
#drawRoundRect is inherited - cannot really improve on that one,
#and figures are quite efficient now.
def drawEllipse(self, x1,y1, x2,y2, edgeColor=None, edgeWidth=None,
fillColor=None, dash=None, **kwargs):
if edgeColor:
self._updateLineColor(edgeColor)
if edgeWidth:
self._updateLineWidth(edgeWidth)
if fillColor:
self._updateFillColor(fillColor)
p = self.pdf.beginPath()
p.ellipse(x1, y1, x2-x1, y2-y1)
self._endPath(p, edgeColor, fillColor) #handles case of transparency
if edgeColor:
self._updateLineColor(self.defaultLineColor)
if edgeWidth:
self._updateLineWidth(self.defaultLineWidth)
if fillColor:
self._updateFillColor(self.defaultFillColor)
def drawArc(self, x1,y1, x2,y2, startAng=0, extent=90, edgeColor=None,
edgeWidth=None, fillColor=None, dash=None, **kwargs):
"""This draws a PacMan-type shape connected to the centre. One
idiosyncracy - if you specify an edge color, it apples to the
outer curved rim but not the radial edges."""
if edgeColor:
self._updateLineColor(edgeColor)
if edgeWidth:
self._updateLineWidth(edgeWidth)
if fillColor:
self._updateFillColor(fillColor)
# I need to do some more work on flipping the coordinate system -
# in pdfgen - note the angle reversal needed when drawing top-down.
pointList = pdfgeom.bezierArc(x1,y1, x2,y2, -startAng, -extent)
start = pointList[0]
end = pointList[-1]
x_cen = 0.5 * (x1 + x2)
y_cen = 0.5 * (y1 + y2)
#first the fill
p = self.pdf.beginPath()
p.moveTo(x_cen, y_cen)
p.lineTo(start[0], start[1])
for curve in pointList:
p.curveTo(curve[2], curve[3], curve[4], curve[5], curve[6], curve[7])
p.close() #back to centre
self._endPath(p, transparent, fillColor) #handles case of transparency
#now the outer rim
p2 = self.pdf.beginPath()
p2.moveTo(start[0], start[1])
for curve in pointList:
p2.curveTo(curve[2], curve[3], curve[4], curve[5], curve[6], curve[7])
self._endPath(p2, edgeColor, transparent) #handles case of transparency
if edgeColor:
self._updateLineColor(self.defaultLineColor)
if edgeWidth:
self._updateLineWidth(self.defaultLineWidth)
if fillColor:
self._updateFillColor(self.defaultFillColor)
def drawPolygon(self, pointlist, edgeColor=None,
edgeWidth=None, fillColor=None, closed=0, dash=None, **kwargs):
"""As it says. Easy with paths!"""
if edgeColor:
self._updateLineColor(edgeColor)
if edgeWidth:
self._updateLineWidth(edgeWidth)
if fillColor:
self._updateFillColor(fillColor)
p = self.pdf.beginPath()
p.moveTo(pointlist[0][0], pointlist[0][1])
for point in pointlist[1:]:
p.lineTo(point[0], point[1])
if closed:
p.close()
self._endPath(p, edgeColor, fillColor) #handles case of transparency
if edgeColor:
self._updateLineColor(self.defaultLineColor)
if edgeWidth:
self._updateLineWidth(self.defaultLineWidth)
if fillColor:
self._updateFillColor(self.defaultFillColor)
## def drawFigure(self, partList,
## edgeColor=None, edgeWidth=None, fillColor=None, closed=0):
## """This is PIDDLE's attempt at Postscript paths. Due to
## necessary limitations in the algorithm, if the start and end
## points are not connected but closed=1, then you get the full shape
## filled but the final line segment does not join up. I have to
## do extra work to simulate this."""
##
## if edgeColor:
## self._updateLineColor(edgeColor)
## if edgeWidth:
## self._updateLineWidth(edgeWidth)
## if fillColor:
## self._updateFillColor(fillColor)
##
## p1 = self.pdf.beginPath() #use for the fill (i.e. closed)
## p2 = self.pdf.beginPath() #use for the edge (may not be closed)
##
## #move to first point
## start = (partList[0][1:3])
## end = None
## p1.moveTo(start[0], start[1])
## p2.moveTo(start[0], start[1])
##
## for tuple in partList:
## op = tuple[0]
## args = list(tuple[1:])
## start = args[0:2]
## # lineTo the start if not coincident with end of last segment
## if start <> end:
## p1.lineTo(start[0], start[1])
## p2.lineTo(start[0], start[1])
##
## #now draw appropriate segment
## if op == figureLine:
## p1.lineTo(args[2], args[3])
## p2.lineTo(args[2], args[3])
## end = args[2:4]
## elif op == figureArc:
## #p1.arcTo(args[0], args[1], args[2], args[3], args[4], args[5])
## #p2.arcTo(args[0], args[1], args[2], args[3], args[4], args[5])
## p1.arc(args[0], args[1], args[2], args[3], args[4], args[5])
## p2.arc(args[0], args[1], args[2], args[3], args[4], args[5])
## end = args[2:4]
## elif op == figureCurve:
## p1.curveTo(args[2], args[3], args[4], args[5], args[6], args[7])
## p2.curveTo(args[2], args[3], args[4], args[5], args[6], args[7])
## end = args[6:8]
## else:
## raise TypeError, "unknown figure operator: " + op
##
## #now for the weirdness
## p1.close()
## if closed:
## p2.close()
## print 'closed edge path'
## print 'inner path p1:' + p1.getCode()
## print 'outer path p2:' + p2.getCode()
##
## self._endPath(p1, transparent, fillColor)
## self._endPath(p2, edgeColor, transparent)
##
## if edgeColor:
## self._updateLineColor(self.defaultLineColor)
## if edgeWidth:
## self._updateLineWidth(self.defaultLineWidth)
## if fillColor:
## self._updateFillColor(self.defaultFillColor)
def drawImage(self, image, x1,y1, x2=None,y2=None,**kwargs):
"""Draw a PIL Image or image filename into the specified rectangle.
If x2 and y2 are omitted, they are calculated from the image size.
"""
# chris starts meddling here -cwl
# piddle only takes PIL images
im_width, im_height = image.size
if not x2 :
x2 = x1 + im_width
if not y2 :
y2 = y1 + im_height
self.pdf.saveState() # I'm changing coordinates to isolate the problem -cwl
self.pdf.translate(x1,y1)
self.pdf.drawInlineImage(image, 0, 0, abs(x1-x2), abs(y1-y2))
self.pdf.restoreState()
### original code below -cwl
#the underlying canvas uses a bott-up coord system, so flips things
#if x2:
#width = abs(x2 - x1)
#x = min(x1, x2)
#if y2:
#height = abs(y2 - y1)
#y = min(y1, y2)
#self.pdf.drawInlineImage(image, x, y, width, height)
##########################################################
#
# non-standard extensions outside Piddle API
#
##########################################################
def drawLiteral(self, literal):
#adds a chunk of raw stuff to the PDF contents stream
self.code.append(literal)
def test():
#... for testing...
canvas = PDFCanvas(name="test")
canvas.defaultLineColor = Color(0.7,0.7,1.0) # light blue
canvas.drawLines( map(lambda i:(i*10,0,i*10,300), range(30)) )
canvas.drawLines( map(lambda i:(0,i*10,300,i*10), range(30)) )
canvas.defaultLineColor = black
canvas.drawLine(10,200, 20,190, color=red)
canvas.drawEllipse( 130,30, 200,100, fillColor=yellow, edgeWidth=4 )
canvas.drawArc( 130,30, 200,100, 45,50, fillColor=blue, edgeColor=navy, edgeWidth=4 )
canvas.defaultLineWidth = 4
canvas.drawRoundRect( 30,30, 100,100, fillColor=blue, edgeColor=maroon,dash=(3,3) )
canvas.drawCurve( 20,20, 100,50, 50,100, 160,160 )
canvas.drawString("This is a test!", 30,130, Font(face="times",size=16,bold=1),
color=green, angle=-45)
canvas.drawString("This is a test!", 30,130, color=red)
polypoints = [ (160,120), (130,190), (210,145), (110,145), (190,190) ]
canvas.drawPolygon(polypoints, fillColor=lime, edgeColor=red, edgeWidth=3, closed=1)
canvas.drawRect( 200,200,260,260, edgeColor=yellow, edgeWidth=5 )
canvas.drawLine( 200,260,260,260, color=green, width=5 )
canvas.drawLine( 260,200,260,260, color=red, width=5 )
canvas.save('test.pdf')
if __name__ == '__main__':
test()
#dashtest()
#test2()