# Copyright (C) 2000 Greg Landrum
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""piddleSVG
This module implements an SVG PIDDLE canvas.
In other words, this is a PIDDLE backend that renders into a
SVG file.
Bits have been shamelessly cobbled from piddlePDF.py and/or
piddlePS.py
Greg Landrum (greglandrum@earthlink.net) 3/10/2000
"""
"""
Functionality implemented:
-drawLine
-drawPolygon
-drawEllipse
-drawArc
-drawCurve
-drawString (rotated text is, mostly, fine... see below)
-drawFigure
-drawImage
Known problems:
-Rotated text is right in either IBM's SVGView or Adobe's plugin. This
problem is explained in drawString()
-The font/string handling is not perfect. There are definite problems
with getting the widths of strings. Thus far heights seem to work okay
in the tests that I've done, but those could well be broken as well.
"""
from piddle import *
import pdfmetrics # for font info
import string, os, types
from math import *
SVG_HEADER = """<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 03December 1999//EN"
"SVG-19991203.dtd">
"""
def _ColorToSVG(color):
""" convenience function for converting a PIDDLE color to an SVG color
"""
if color == transparent:
return 'none'
else:
return 'rgb(%d,%d,%d)'%(int(color.red*255),int(color.green*255),
int(color.blue*255))
def _PointListToSVG(points,dupFirst=0):
""" convenience function for converting a list of points to a string
suitable for passing to SVG path operations
"""
outStr = ''
for i in xrange(len(points)):
outStr = outStr + '%.2f,%.2f '%(points[i][0],points[i][1])
# add back on the first point. This is not required in the spec,
# but Adobe's beta-quality viewer seems to not like it being skipped
if dupFirst == 1:
outStr = outStr + '%.2f,%.2f'%(points[0][0],points[0][1])
return outStr
class SVGCanvas( Canvas ):
def __init__(self, size=(300,300), name='SVGCanvas'):
self._nImages=1
# I'd rather do this as PNG, but IBM's SVGView doesn't support those
# yet. Adobe's plugin works just fine with them, however.
self._imageFormat='GIF'
self.size = size
self._initOutput()
Canvas.__init__(self, size, name)
def _initOutput(self):
self._txt = SVG_HEADER +\
'<svg xml:space="preserve" width="%dpx" height="%dpx">\n'%self.size
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 piddle_font_map[string.lower('sansserif')]
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 _FormFontStr(self,font):
""" form what we hope is a valid SVG font string.
Defaults to 'sansserif'
This should work when an array of font faces are passed in.
"""
fontStr = ''
if font.face is None:
font.face = 'sansserif'
if type(font.face) == StringType:
if len(string.split(font.face)) > 1:
familyStr = '\'%s\''%font.face
else:
familyStr = font.face
else:
face = font.face[0]
if len(string.split(face)) > 1:
familyStr = '\'%s\''%(face)
else:
familyStr = face
for i in xrange(1,len(font.face)):
face = font.face[i]
if len(string.split(face)) > 1:
familyStr = ', \'%s\''%(face)
else:
familyStr = familyStr + ', %s'%face
if font.italic:
styleStr = 'font-style:italic;'
else:
styleStr = ''
if font.bold:
weightStr = 'font-weight:bold;'
else:
weightStr = ''
if font.size:
sizeStr = 'font-size:%.2f;'%font.size
else:
sizeStr = ''
fontStr = 'font-family: %s; %s %s %s'%(familyStr,styleStr,weightStr,sizeStr)
return fontStr
def _FormArcStr(self,x1,y1,x2,y2,theta1,extent):
""" Forms an arc specification for SVG
"""
if abs(extent) > 360:
if extent < 0:
extent = -abs(extent)%360
else:
extent = extent%360
# deal with figuring out the various arc flags
# required by SVG.
if extent > 180: # this one is easy
arcFlag = 1
else:
arcFlag = 0
if extent >=0:
sweepFlag = 0
else:
sweepFlag = 1
# convert angles to radians (grn)
theta1 = pi * theta1 / 180.
extent = pi * extent / 180.
# center of the arc
cx = (x1+x2)/2.
cy = (y1+y2)/2.
# its radius
rx = abs(x2 - x1)/2.
ry = abs(y2 - y1)/2.
# final angle
theta2 = theta1 + extent
# SVG takes arcs as paths running from one point to another.
# figure out what those start and end points are now.
# the -thetas are required because of a difference in the handedness
# of angles in Piddle and SVG
startx = cx + rx*cos(-theta1)
starty = cy + ry*sin(-theta1)
endx = cx + rx*cos(-theta2)
endy = cy + ry*sin(-theta2)
arcStr = '%.2f %.2f A%.2f %.2f 0 %d %d %.2f %.2f'%(startx,starty,rx,ry,
arcFlag,sweepFlag,
endx,endy)
return arcStr
# public functions
def clear(self):
self._initOutput()
def flush(self):
self.save('svg')
def save(self, type=''):
if type == '':
if '.' not in self.name:
raise TypeError, 'no file type given to save()'
filename = self.name
else:
filename = self.name + '.' + type
outFile = open(filename,'w+')
outFile.write(self._txt+'</svg>')
outFile.close()
print filename, "saved"
#------------- drawing methods --------------
def drawLine(self, x1,y1, x2,y2, color=None, width=None):
"Draw a straight line between x1,y1 and x2,y2."
# set color...
if color:
if color == transparent: return
elif self.defaultLineColor == transparent:
return
else:
color = self.defaultLineColor
svgColor = _ColorToSVG(color)
if width:
w = width
else:
w = self.defaultLineWidth
styleStr = '"stroke:%s; stroke-width:%d"'%(svgColor,w)
outStr = '<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" style=%s />\n'%(x1,y1,x2,y2,styleStr)
self._txt = self._txt + outStr
def drawPolygon(self, pointlist,
edgeColor=None, edgeWidth=None, fillColor=transparent, closed=0):
"""drawPolygon(pointlist) -- draws a polygon
pointlist: a list of (x,y) tuples defining vertices
"""
# get the points into SVG format
pointStr = _PointListToSVG(pointlist,dupFirst=closed)
# set color for fill...
filling = 0
if fillColor:
if fillColor != transparent:
filling = 1
# do the fill
if filling:
fillStr = 'fill:%s;'%_ColorToSVG(fillColor)
else:
fillStr = 'fill:none;'
# set color for edge...
if not edgeColor:
edgeColor = self.defaultLineColor
# set edge width...
if edgeWidth == None: edgeWidth = self.defaultLineWidth
# SVG markers
edgeStr = 'stroke:%s; stroke-width:%d;'%(_ColorToSVG(edgeColor),int(edgeWidth))
# draw it
outStr = '<polygon style="%s %s" points="%s"/>\n'%(fillStr,edgeStr,pointStr)
self._txt = self._txt + outStr
def drawEllipse(self, x1,y1,x2,y2,
edgeColor=None, edgeWidth=None, fillColor=transparent):
# get the points into SVG format
cx = (x1+x2)/2.
cy = (y1+y2)/2.
rx = abs(x2 - x1)/2.
ry = abs(y2 - y1)/2.
ellipseStr = 'cx="%.2f" cy="%.2f" rx="%.2f" ry="%.2f"'%(cx,cy,rx,ry)
# set color for fill...
filling = 0
if fillColor:
if fillColor != transparent:
filling = 1
# do the fill
if filling:
fillStr = 'fill:%s;'%_ColorToSVG(fillColor)
else:
fillStr = 'fill:none;'
# set color for edge...
if not edgeColor:
edgeColor = self.defaultLineColor
# set edge width...
if edgeWidth == None: edgeWidth = self.defaultLineWidth
edgeStr = 'stroke:%s; stroke-width:%d;'%(_ColorToSVG(edgeColor),int(edgeWidth))
# draw it
outStr = '<ellipse style="%s %s" %s/>\n'%(fillStr,edgeStr,ellipseStr)
self._txt = self._txt + outStr
def drawArc(self, x1,y1,x2,y2,theta1=0,extent=360,
edgeColor=None, edgeWidth=None, fillColor=None):
# set color for fill...
filling = 0
if not fillColor:
fillColor = self.defaultFillColor
if fillColor != transparent:
filling = 1
# do the fill
if filling:
fillStr = 'fill:%s;'%_ColorToSVG(fillColor)
else:
fillStr = 'fill:none;'
arcStr = self._FormArcStr(x1,y1,x2,y2,theta1,extent)
if not filling:
pathStr = 'M' + arcStr
else:
# this is a bit trickier. Piddle requires filled arcs to stroke the
# arc bit and fill into the middle (like a piece of pie) without
# stroking the lines to the middle. So we need *two* paths here.
strokePathStr = 'M' + arcStr
cx = (x1 + x2)/2.
cy = (y1 + y2)/2.
fillPathStr = 'M%.2f %.2f L%sZ'%(cx,cy,arcStr)
# set color for edge...
if not edgeColor:
edgeColor = self.defaultLineColor
# set edge width...
if edgeWidth == None: edgeWidth = self.defaultLineWidth
# SVG markers
edgeStr = 'stroke:%s; stroke-width:%d;'%(_ColorToSVG(edgeColor),int(edgeWidth))
# draw it
if not filling:
outStr = '<path style="%s %s" d="%s"/>\n'%(fillStr,edgeStr,pathStr)
else:
outStr = '<path style="%s" d="%s"/>\n'%(fillStr,fillPathStr)
outStr = outStr+'<path style="fill:none; %s" d="%s"/>\n'%(edgeStr,strokePathStr)
self._txt = self._txt + outStr
def drawCurve(self, x1,y1,x2,y2,x3,y3,x4,y4,
edgeColor=None, edgeWidth=None, fillColor=transparent, closed=0):
# get the points into SVG format
curveStr = 'M%.2f %.2f C%.2f %.2f %.2f %.2f %.2f %.2f'%(x1,y1,x2,y2,x3,y3,x4,y4)
if closed:
curveStr = curveStr + 'Z'
# set color for fill...
filling = 0
if fillColor:
if fillColor != transparent:
filling = 1
# do the fill
if filling:
fillStr = 'fill:%s;'%_ColorToSVG(fillColor)
else:
fillStr = 'fill:none;'
# set color for edge...
if not edgeColor:
edgeColor = self.defaultLineColor
# set edge width...
if edgeWidth == None: edgeWidth = self.defaultLineWidth
# SVG markers
edgeStr = 'stroke:%s; stroke-width:%d;'%(_ColorToSVG(edgeColor),int(edgeWidth))
# draw it
outStr = '<path style="%s %s" d="%s"/>\n'%(fillStr,edgeStr,curveStr)
self._txt = self._txt + outStr
def drawString(self, s, x,y, font=None, color=None, angle=0):
# set color...
if color:
if color == transparent: return
elif self.defaultLineColor == transparent:
return
else:
color = self.defaultLineColor
if font is None:
font = self.defaultFont
if font:
fontStr = self._FormFontStr(font)
else:
fontStr = ''
svgColor = _ColorToSVG(color)
outStr = ''
if angle != 0:
# note: this is the correct order of the transforms according to my reading of
# the SVG spec and the behavior of Adobe's SVG plugin. If you want it to work
# in IBM's SVGView, you'll have to use the second (commented out) form.
# Ah, the joys of using mature technologies. ;-)
outStr = outStr + '<g transform="translate(%.2f,%.2f) rotate(%.2f)">\n'%(x,y,360-angle)
#outStr = outStr + '<g transform="rotate(%.2f) translate(%.2f,%.2f)">\n'%(360-angle,x,y)
xLoc = 0
yLoc = 0
else:
xLoc = x
yLoc = y
lines = string.split(s,'\n')
lineHeight = self.fontHeight(font)
yP = yLoc
for line in lines:
outStr = outStr + self._drawStringOneLine(line,xLoc,yP,fontStr,svgColor)
yP = yP + lineHeight
if angle != 0:
outStr = outStr + '</g>'
self._txt = self._txt + outStr
def _drawStringOneLine(self,line,x,y,fontStr,svgColor):
styleStr = 'style="%s stroke:%s"'%(fontStr,svgColor)
return ' <text %s x="%.2f" y="%.2f">%s</text>\n'%(styleStr,x,y,line)
def drawFigure(self, partList,
edgeColor=None, edgeWidth=None, fillColor=None, closed=0):
"""drawFigure(partList) -- draws a complex figure
partlist: a set of lines, curves, and arcs defined by a tuple whose
first element is one of figureLine, figureArc, figureCurve
and whose remaining 4, 6, or 8 elements are parameters."""
filling = 0
if fillColor:
if fillColor != transparent:
filling = 1
# do the fill
if filling:
fillStr = 'fill:%s;'%_ColorToSVG(fillColor)
else:
fillStr = 'fill:none;'
# set color for edge...
if not edgeColor:
edgeColor = self.defaultLineColor
# set edge width...
if edgeWidth == None: edgeWidth = self.defaultLineWidth
# SVG markers
edgeStr = 'stroke:%s; stroke-width:%d;'%(_ColorToSVG(edgeColor),int(edgeWidth))
pathStr = ''
for item in partList:
op = item[0]
args = list(item[1:])
if pathStr == '':
pathStr = pathStr + 'M'
else:
pathStr = pathStr + 'L'
if op == figureLine:
pathStr = pathStr + '%.2f %.2f L%.2f %.2f'%(tuple(args))
elif op == figureCurve:
pathStr = pathStr + '%.2f %.2f C%.2f %.2f %.2f %.2f %.2f %.2f'%(tuple(args))
elif op == figureArc:
x1,y1,x2,y2,theta1,extent=tuple(args)
pathStr = pathStr + self._FormArcStr(x1,y1,x2,y2,theta1,extent)
else:
raise TypeError, "unknown figure operator: "+op
if closed == 1:
pathStr = pathStr + 'Z'
outStr = '<path style="%s %s" d="%s"/>\n'%(edgeStr,fillStr,pathStr)
self._txt = self._txt + outStr
def drawImage(self, image, x1,y1, x2=None,y2=None):
"""
to the best of my knowledge, the only real way to get an image
into SVG is to read it from a file. So we'll save out to a PNG
file, then set a link to that in the SVG.
"""
imageFileName= '%s-%d.%s'%(self.name,self._nImages,string.lower(self._imageFormat))
self._nImages = self._nImages + 1
image.save(imageFileName,format=self._imageFormat)
im_width,im_height=image.size
if x2 is not None:
im_width = abs(x2-x1)
if y2 is not None:
im_height = abs(y2-y1)
outStr = '<image x="%.2f" y="%.2f" width="%.2f" height="%.2f" xlink:href="%s"/>\n'%\
(x1,y1,im_width,im_height,imageFileName)
self._txt = self._txt + outStr
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 pdfmetrics.stringwidth(s, fontname) * font.size * 0.001
def fontAscent(self, font=None):
if not font:
font = self.defaultFont
#return -font.size
fontname = self._findExternalFontName(font)
return pdfmetrics.ascent_descent[fontname][0] * 0.001 * font.size
def fontDescent(self, font=None):
if not font:
font = self.defaultFont
fontname = self._findExternalFontName(font)
return -pdfmetrics.ascent_descent[fontname][1] * 0.001 * font.size
def test():
#... for testing...
canvas = SVGCanvas(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 )
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, angle=-45)
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.flush()
def testit(canvas, s, x,y, font=None):
canvas.defaultLineColor = black
canvas.drawString(s, x,y, font=font)
canvas.defaultLineColor = blue
w = canvas.stringWidth(s, font=font)
canvas.drawLine(x,y, x+w,y)
canvas.drawLine(x,y-canvas.fontAscent(font=font), x+w,y-canvas.fontAscent(font=font))
canvas.drawLine(x,y+canvas.fontDescent(font=font), x+w,y+canvas.fontDescent(font=font))
def test2():
canvas = SVGCanvas(name="Foogar")
testit( canvas, "Foogar", 20, 30 )
testit( canvas, "Foogar", 20, 90, font=Font(size=24) )
global dammit
testit( canvas, "Foogar", 20, 150, font=Font(face='courier',size=24) )
testit( canvas, "Foogar", 20, 240, font=Font(face='courier') )
canvas.flush()
if __name__ == '__main__':
test()
test2()
|