1290 lines
45 KiB
Python
1290 lines
45 KiB
Python
#Copyright ReportLab Europe Ltd. 2000-2004
|
|
#see license.txt for license details
|
|
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/shapes.py
|
|
"""
|
|
core of the graphics library - defines Drawing and Shapes
|
|
"""
|
|
__version__=''' $Id: shapes.py 2845 2006-05-03 12:24:35Z rgbecker $ '''
|
|
|
|
import string, os, sys
|
|
from math import pi, cos, sin, tan
|
|
from types import FloatType, IntType, ListType, TupleType, StringType, InstanceType
|
|
from pprint import pprint
|
|
|
|
from reportlab.platypus import Flowable
|
|
from reportlab.rl_config import shapeChecking, verbose, defaultGraphicsFontName, _unset_
|
|
from reportlab.lib import logger
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.validators import *
|
|
from reportlab.lib.attrmap import *
|
|
from reportlab.lib.utils import fp_str
|
|
from reportlab.pdfbase.pdfmetrics import stringWidth
|
|
|
|
class NotImplementedError(Exception):
|
|
pass
|
|
|
|
# two constants for filling rules
|
|
NON_ZERO_WINDING = 'Non-Zero Winding'
|
|
EVEN_ODD = 'Even-Odd'
|
|
|
|
## these can be overridden at module level before you start
|
|
#creating shapes. So, if using a special color model,
|
|
#this provides support for the rendering mechanism.
|
|
#you can change defaults globally before you start
|
|
#making shapes; one use is to substitute another
|
|
#color model cleanly throughout the drawing.
|
|
|
|
STATE_DEFAULTS = { # sensible defaults for all
|
|
'transform': (1,0,0,1,0,0),
|
|
|
|
# styles follow SVG naming
|
|
'strokeColor': colors.black,
|
|
'strokeWidth': 1,
|
|
'strokeLineCap': 0,
|
|
'strokeLineJoin': 0,
|
|
'strokeMiterLimit' : 'TBA', # don't know yet so let bomb here
|
|
'strokeDashArray': None,
|
|
'strokeOpacity': 1.0, #100%
|
|
|
|
'fillColor': colors.black, #...or text will be invisible
|
|
#'fillRule': NON_ZERO_WINDING, - these can be done later
|
|
#'fillOpacity': 1.0, #100% - can be done later
|
|
|
|
'fontSize': 10,
|
|
'fontName': defaultGraphicsFontName,
|
|
'textAnchor': 'start' # can be start, middle, end, inherited
|
|
}
|
|
|
|
|
|
####################################################################
|
|
# math utilities. These could probably be moved into lib
|
|
# somewhere.
|
|
####################################################################
|
|
|
|
# constructors for matrices:
|
|
def nullTransform():
|
|
return (1, 0, 0, 1, 0, 0)
|
|
|
|
def translate(dx, dy):
|
|
return (1, 0, 0, 1, dx, dy)
|
|
|
|
def scale(sx, sy):
|
|
return (sx, 0, 0, sy, 0, 0)
|
|
|
|
def rotate(angle):
|
|
a = angle * pi/180
|
|
return (cos(a), sin(a), -sin(a), cos(a), 0, 0)
|
|
|
|
def skewX(angle):
|
|
a = angle * pi/180
|
|
return (1, 0, tan(a), 1, 0, 0)
|
|
|
|
def skewY(angle):
|
|
a = angle * pi/180
|
|
return (1, tan(a), 0, 1, 0, 0)
|
|
|
|
def mmult(A, B):
|
|
"A postmultiplied by B"
|
|
# I checked this RGB
|
|
# [a0 a2 a4] [b0 b2 b4]
|
|
# [a1 a3 a5] * [b1 b3 b5]
|
|
# [ 1 ] [ 1 ]
|
|
#
|
|
return (A[0]*B[0] + A[2]*B[1],
|
|
A[1]*B[0] + A[3]*B[1],
|
|
A[0]*B[2] + A[2]*B[3],
|
|
A[1]*B[2] + A[3]*B[3],
|
|
A[0]*B[4] + A[2]*B[5] + A[4],
|
|
A[1]*B[4] + A[3]*B[5] + A[5])
|
|
|
|
def inverse(A):
|
|
"For A affine 2D represented as 6vec return 6vec version of A**(-1)"
|
|
# I checked this RGB
|
|
det = float(A[0]*A[3] - A[2]*A[1])
|
|
R = [A[3]/det, -A[1]/det, -A[2]/det, A[0]/det]
|
|
return tuple(R+[-R[0]*A[4]-R[2]*A[5],-R[1]*A[4]-R[3]*A[5]])
|
|
|
|
def zTransformPoint(A,v):
|
|
"Apply the homogenous part of atransformation a to vector v --> A*v"
|
|
return (A[0]*v[0]+A[2]*v[1],A[1]*v[0]+A[3]*v[1])
|
|
|
|
def transformPoint(A,v):
|
|
"Apply transformation a to vector v --> A*v"
|
|
return (A[0]*v[0]+A[2]*v[1]+A[4],A[1]*v[0]+A[3]*v[1]+A[5])
|
|
|
|
def transformPoints(matrix, V):
|
|
return map(transformPoint, V)
|
|
|
|
def zTransformPoints(matrix, V):
|
|
return map(lambda x,matrix=matrix: zTransformPoint(matrix,x), V)
|
|
|
|
def _textBoxLimits(text, font, fontSize, leading, textAnchor, boxAnchor):
|
|
w = 0
|
|
for t in text:
|
|
w = max(w,stringWidth(t,font, fontSize))
|
|
|
|
h = len(text)*leading
|
|
yt = fontSize
|
|
if boxAnchor[0]=='s':
|
|
yb = -h
|
|
yt = yt - h
|
|
elif boxAnchor[0]=='n':
|
|
yb = 0
|
|
else:
|
|
yb = -h/2.0
|
|
yt = yt + yb
|
|
|
|
if boxAnchor[-1]=='e':
|
|
xb = -w
|
|
if textAnchor=='end': xt = 0
|
|
elif textAnchor=='start': xt = -w
|
|
else: xt = -w/2.0
|
|
elif boxAnchor[-1]=='w':
|
|
xb = 0
|
|
if textAnchor=='end': xt = w
|
|
elif textAnchor=='start': xt = 0
|
|
else: xt = w/2.0
|
|
else:
|
|
xb = -w/2.0
|
|
if textAnchor=='end': xt = -xb
|
|
elif textAnchor=='start': xt = xb
|
|
else: xt = 0
|
|
|
|
return xb, yb, w, h, xt, yt
|
|
|
|
def _rotatedBoxLimits( x, y, w, h, angle):
|
|
'''
|
|
Find the corner points of the rotated w x h sized box at x,y
|
|
return the corner points and the min max points in the original space
|
|
'''
|
|
C = zTransformPoints(rotate(angle),((x,y),(x+w,y),(x+w,y+h),(x,y+h)))
|
|
X = map(lambda x: x[0], C)
|
|
Y = map(lambda x: x[1], C)
|
|
return min(X), max(X), min(Y), max(Y), C
|
|
|
|
|
|
class _DrawTimeResizeable:
|
|
'''Addin class to provide the horribleness of _drawTimeResize'''
|
|
def _drawTimeResize(self,w,h):
|
|
if hasattr(self,'_canvas'):
|
|
canvas = self._canvas
|
|
drawing = canvas._drawing
|
|
drawing.width, drawing.height = w, h
|
|
if hasattr(canvas,'_drawTimeResize'):
|
|
canvas._drawTimeResize(w,h)
|
|
|
|
class _SetKeyWordArgs:
|
|
def __init__(self, keywords={}):
|
|
"""In general properties may be supplied to the constructor."""
|
|
for key, value in keywords.items():
|
|
setattr(self, key, value)
|
|
|
|
|
|
#################################################################
|
|
#
|
|
# Helper functions for working out bounds
|
|
#
|
|
#################################################################
|
|
|
|
def getRectsBounds(rectList):
|
|
# filter out any None objects, e.g. empty groups
|
|
L = filter(lambda x: x is not None, rectList)
|
|
if not L: return None
|
|
|
|
xMin, yMin, xMax, yMax = L[0]
|
|
for (x1, y1, x2, y2) in L[1:]:
|
|
if x1 < xMin:
|
|
xMin = x1
|
|
if x2 > xMax:
|
|
xMax = x2
|
|
if y1 < yMin:
|
|
yMin = y1
|
|
if y2 > yMax:
|
|
yMax = y2
|
|
return (xMin, yMin, xMax, yMax)
|
|
|
|
def getPathBounds(points):
|
|
n = len(points)
|
|
f = lambda i,p = points: p[i]
|
|
xs = map(f,xrange(0,n,2))
|
|
ys = map(f,xrange(1,n,2))
|
|
return (min(xs), min(ys), max(xs), max(ys))
|
|
|
|
def getPointsBounds(pointList):
|
|
"Helper function for list of points"
|
|
first = pointList[0]
|
|
if type(first) in (ListType, TupleType):
|
|
xs = map(lambda xy: xy[0],pointList)
|
|
ys = map(lambda xy: xy[1],pointList)
|
|
return (min(xs), min(ys), max(xs), max(ys))
|
|
else:
|
|
return getPathBounds(pointList)
|
|
|
|
#################################################################
|
|
#
|
|
# And now the shapes themselves....
|
|
#
|
|
#################################################################
|
|
class Shape(_SetKeyWordArgs,_DrawTimeResizeable):
|
|
"""Base class for all nodes in the tree. Nodes are simply
|
|
packets of data to be created, stored, and ultimately
|
|
rendered - they don't do anything active. They provide
|
|
convenience methods for verification but do not
|
|
check attribiute assignments or use any clever setattr
|
|
tricks this time."""
|
|
_attrMap = AttrMap()
|
|
|
|
def copy(self):
|
|
"""Return a clone of this shape."""
|
|
|
|
# implement this in the descendants as they need the right init methods.
|
|
raise NotImplementedError, "No copy method implemented for %s" % self.__class__.__name__
|
|
|
|
def getProperties(self,recur=1):
|
|
"""Interface to make it easy to extract automatic
|
|
documentation"""
|
|
|
|
#basic nodes have no children so this is easy.
|
|
#for more complex objects like widgets you
|
|
#may need to override this.
|
|
props = {}
|
|
for key, value in self.__dict__.items():
|
|
if key[0:1] <> '_':
|
|
props[key] = value
|
|
return props
|
|
|
|
def setProperties(self, props):
|
|
"""Supports the bulk setting if properties from,
|
|
for example, a GUI application or a config file."""
|
|
|
|
self.__dict__.update(props)
|
|
#self.verify()
|
|
|
|
def dumpProperties(self, prefix=""):
|
|
"""Convenience. Lists them on standard output. You
|
|
may provide a prefix - mostly helps to generate code
|
|
samples for documentation."""
|
|
|
|
propList = self.getProperties().items()
|
|
propList.sort()
|
|
if prefix:
|
|
prefix = prefix + '.'
|
|
for (name, value) in propList:
|
|
print '%s%s = %s' % (prefix, name, value)
|
|
|
|
def verify(self):
|
|
"""If the programmer has provided the optional
|
|
_attrMap attribute, this checks all expected
|
|
attributes are present; no unwanted attributes
|
|
are present; and (if a checking function is found)
|
|
checks each attribute. Either succeeds or raises
|
|
an informative exception."""
|
|
|
|
if self._attrMap is not None:
|
|
for key in self.__dict__.keys():
|
|
if key[0] <> '_':
|
|
assert self._attrMap.has_key(key), "Unexpected attribute %s found in %s" % (key, self)
|
|
for (attr, metavalue) in self._attrMap.items():
|
|
assert hasattr(self, attr), "Missing attribute %s from %s" % (attr, self)
|
|
value = getattr(self, attr)
|
|
assert metavalue.validate(value), "Invalid value %s for attribute %s in class %s" % (value, attr, self.__class__.__name__)
|
|
|
|
if shapeChecking:
|
|
"""This adds the ability to check every attribute assignment as it is made.
|
|
It slows down shapes but is a big help when developing. It does not
|
|
get defined if rl_config.shapeChecking = 0"""
|
|
def __setattr__(self, attr, value):
|
|
"""By default we verify. This could be off
|
|
in some parallel base classes."""
|
|
validateSetattr(self,attr,value) #from reportlab.lib.attrmap
|
|
|
|
def getBounds(self):
|
|
"Returns bounding rectangle of object as (x1,y1,x2,y2)"
|
|
raise NotImplementedError("Shapes and widgets must implement getBounds")
|
|
|
|
class Group(Shape):
|
|
"""Groups elements together. May apply a transform
|
|
to its contents. Has a publicly accessible property
|
|
'contents' which may be used to iterate over contents.
|
|
In addition, child nodes may be given a name in which
|
|
case they are subsequently accessible as properties."""
|
|
|
|
_attrMap = AttrMap(
|
|
transform = AttrMapValue(isTransform,desc="Coordinate transformation to apply"),
|
|
contents = AttrMapValue(isListOfShapes,desc="Contained drawable elements"),
|
|
)
|
|
|
|
def __init__(self, *elements, **keywords):
|
|
"""Initial lists of elements may be provided to allow
|
|
compact definitions in literal Python code. May or
|
|
may not be useful."""
|
|
|
|
# Groups need _attrMap to be an instance rather than
|
|
# a class attribute, as it may be extended at run time.
|
|
self._attrMap = self._attrMap.clone()
|
|
self.contents = []
|
|
self.transform = (1,0,0,1,0,0)
|
|
for elt in elements:
|
|
self.add(elt)
|
|
# this just applies keywords; do it at the end so they
|
|
#don;t get overwritten
|
|
_SetKeyWordArgs.__init__(self, keywords)
|
|
|
|
def _addNamedNode(self,name,node):
|
|
'if name is not None add an attribute pointing to node and add to the attrMap'
|
|
if name:
|
|
if name not in self._attrMap.keys():
|
|
self._attrMap[name] = AttrMapValue(isValidChild)
|
|
setattr(self, name, node)
|
|
|
|
def add(self, node, name=None):
|
|
"""Appends non-None child node to the 'contents' attribute. In addition,
|
|
if a name is provided, it is subsequently accessible by name
|
|
"""
|
|
# propagates properties down
|
|
if node is not None:
|
|
assert isValidChild(node), "Can only add Shape or UserNode objects to a Group"
|
|
self.contents.append(node)
|
|
self._addNamedNode(name,node)
|
|
|
|
def _nn(self,node):
|
|
self.add(node)
|
|
return self.contents[-1]
|
|
|
|
def insert(self, i, n, name=None):
|
|
'Inserts sub-node n in contents at specified location'
|
|
if n is not None:
|
|
assert isValidChild(n), "Can only insert Shape or UserNode objects in a Group"
|
|
if i<0:
|
|
self.contents[i:i] =[n]
|
|
else:
|
|
self.contents.insert(i,n)
|
|
self._addNamedNode(name,n)
|
|
|
|
def expandUserNodes(self):
|
|
"""Return a new object which only contains primitive shapes."""
|
|
|
|
# many limitations - shared nodes become multiple ones,
|
|
obj = isinstance(self,Drawing) and Drawing(self.width,self.height) or Group()
|
|
obj._attrMap = self._attrMap.clone()
|
|
if hasattr(obj,'transform'): obj.transform = self.transform[:]
|
|
|
|
self_contents = self.contents
|
|
a = obj.contents.append
|
|
for child in self_contents:
|
|
if isinstance(child, UserNode):
|
|
newChild = child.provideNode()
|
|
elif isinstance(child, Group):
|
|
newChild = child.expandUserNodes()
|
|
else:
|
|
newChild = child.copy()
|
|
a(newChild)
|
|
|
|
self._copyNamedContents(obj)
|
|
return obj
|
|
|
|
def _explode(self):
|
|
''' return a fully expanded object'''
|
|
from reportlab.graphics.widgetbase import Widget
|
|
obj = Group()
|
|
if hasattr(obj,'transform'): obj.transform = self.transform[:]
|
|
P = self.contents[:] # pending nodes
|
|
while P:
|
|
n = P.pop(0)
|
|
if isinstance(n, UserNode):
|
|
P.append(n.provideNode())
|
|
elif isinstance(n, Group):
|
|
n = n._explode()
|
|
if n.transform==(1,0,0,1,0,0):
|
|
obj.contents.extend(n.contents)
|
|
else:
|
|
obj.add(n)
|
|
else:
|
|
obj.add(n)
|
|
return obj
|
|
|
|
def _copyContents(self,obj):
|
|
for child in self.contents:
|
|
obj.contents.append(child)
|
|
|
|
def _copyNamedContents(self,obj,aKeys=None,noCopy=('contents',)):
|
|
from copy import copy
|
|
self_contents = self.contents
|
|
if not aKeys: aKeys = self._attrMap.keys()
|
|
for (k, v) in self.__dict__.items():
|
|
if v in self_contents:
|
|
pos = self_contents.index(v)
|
|
setattr(obj, k, obj.contents[pos])
|
|
elif k in aKeys and k not in noCopy:
|
|
setattr(obj, k, copy(v))
|
|
|
|
def _copy(self,obj):
|
|
"""copies to obj"""
|
|
obj._attrMap = self._attrMap.clone()
|
|
self._copyContents(obj)
|
|
self._copyNamedContents(obj)
|
|
return obj
|
|
|
|
def copy(self):
|
|
"""returns a copy"""
|
|
return self._copy(self.__class__())
|
|
|
|
def rotate(self, theta):
|
|
"""Convenience to help you set transforms"""
|
|
self.transform = mmult(self.transform, rotate(theta))
|
|
|
|
def translate(self, dx, dy):
|
|
"""Convenience to help you set transforms"""
|
|
self.transform = mmult(self.transform, translate(dx, dy))
|
|
|
|
def scale(self, sx, sy):
|
|
"""Convenience to help you set transforms"""
|
|
self.transform = mmult(self.transform, scale(sx, sy))
|
|
|
|
|
|
def skew(self, kx, ky):
|
|
"""Convenience to help you set transforms"""
|
|
self.transform = mmult(mmult(self.transform, skewX(kx)),skewY(ky))
|
|
|
|
def shift(self, x, y):
|
|
'''Convenience function to set the origin arbitrarily'''
|
|
self.transform = self.transform[:-2]+(x,y)
|
|
|
|
def asDrawing(self, width, height):
|
|
""" Convenience function to make a drawing from a group
|
|
After calling this the instance will be a drawing!
|
|
"""
|
|
self.__class__ = Drawing
|
|
self._attrMap.update(self._xtraAttrMap)
|
|
self.width = width
|
|
self.height = height
|
|
|
|
def getContents(self):
|
|
'''Return the list of things to be rendered
|
|
override to get more complicated behaviour'''
|
|
b = getattr(self,'background',None)
|
|
C = self.contents
|
|
if b and b not in C: C = [b]+C
|
|
return C
|
|
|
|
def getBounds(self):
|
|
if self.contents:
|
|
b = []
|
|
for elem in self.contents:
|
|
b.append(elem.getBounds())
|
|
x1 = getRectsBounds(b)
|
|
if x1 is None: return None
|
|
x1, y1, x2, y2 = x1
|
|
trans = self.transform
|
|
corners = [[x1,y1], [x1, y2], [x2, y1], [x2,y2]]
|
|
newCorners = []
|
|
for corner in corners:
|
|
newCorners.append(transformPoint(trans, corner))
|
|
return getPointsBounds(newCorners)
|
|
else:
|
|
#empty group needs a sane default; this
|
|
#will happen when interactively creating a group
|
|
#nothing has been added to yet. The alternative is
|
|
#to handle None as an allowed return value everywhere.
|
|
return None
|
|
|
|
def _addObjImport(obj,I,n=None):
|
|
'''add an import of obj's class to a dictionary of imports''' #'
|
|
from inspect import getmodule
|
|
c = obj.__class__
|
|
m = getmodule(c).__name__
|
|
n = n or c.__name__
|
|
if not I.has_key(m):
|
|
I[m] = [n]
|
|
elif n not in I[m]:
|
|
I[m].append(n)
|
|
|
|
def _repr(self,I=None):
|
|
'''return a repr style string with named fixed args first, then keywords'''
|
|
if type(self) is InstanceType:
|
|
if self is EmptyClipPath:
|
|
_addObjImport(self,I,'EmptyClipPath')
|
|
return 'EmptyClipPath'
|
|
if I: _addObjImport(self,I)
|
|
if isinstance(self,Shape):
|
|
from inspect import getargs
|
|
args, varargs, varkw = getargs(self.__init__.im_func.func_code)
|
|
P = self.getProperties()
|
|
s = self.__class__.__name__+'('
|
|
for n in args[1:]:
|
|
v = P[n]
|
|
del P[n]
|
|
s = s + '%s,' % _repr(v,I)
|
|
for n,v in P.items():
|
|
v = P[n]
|
|
s = s + '%s=%s,' % (n, _repr(v,I))
|
|
return s[:-1]+')'
|
|
else:
|
|
return repr(self)
|
|
elif type(self) is FloatType:
|
|
return fp_str(self)
|
|
elif type(self) in (ListType,TupleType):
|
|
s = ''
|
|
for v in self:
|
|
s = s + '%s,' % _repr(v,I)
|
|
if type(self) is ListType:
|
|
return '[%s]' % s[:-1]
|
|
else:
|
|
return '(%s%s)' % (s[:-1],len(self)==1 and ',' or '')
|
|
else:
|
|
return repr(self)
|
|
|
|
def _renderGroupPy(G,pfx,I,i=0,indent='\t\t'):
|
|
s = ''
|
|
C = getattr(G,'transform',None)
|
|
if C: s = s + ('%s%s.transform = %s\n' % (indent,pfx,_repr(C)))
|
|
C = G.contents
|
|
for n in C:
|
|
if isinstance(n, Group):
|
|
npfx = 'v%d' % i
|
|
i = i + 1
|
|
s = s + '%s%s=%s._nn(Group())\n' % (indent,npfx,pfx)
|
|
s = s + _renderGroupPy(n,npfx,I,i,indent)
|
|
i = i - 1
|
|
else:
|
|
s = s + '%s%s.add(%s)\n' % (indent,pfx,_repr(n,I))
|
|
return s
|
|
|
|
def _extraKW(self,pfx,**kw):
|
|
kw.update(self.__dict__)
|
|
R = {}
|
|
n = len(pfx)
|
|
for k in kw.keys():
|
|
if k.startswith(pfx):
|
|
R[k[n:]] = kw[k]
|
|
return R
|
|
|
|
class Drawing(Group, Flowable):
|
|
"""Outermost container; the thing a renderer works on.
|
|
This has no properties except a height, width and list
|
|
of contents."""
|
|
|
|
_xtraAttrMap = AttrMap(
|
|
width = AttrMapValue(isNumber,desc="Drawing width in points."),
|
|
height = AttrMapValue(isNumber,desc="Drawing height in points."),
|
|
canv = AttrMapValue(None),
|
|
background = AttrMapValue(isValidChildOrNone,desc="Background widget for the drawing"),
|
|
hAlign = AttrMapValue(OneOf("LEFT", "RIGHT", "CENTER", "CENTRE"), desc="Horizontal alignment within parent document"),
|
|
vAlign = AttrMapValue(OneOf("TOP", "BOTTOM", "CENTER", "CENTRE"), desc="Vertical alignment within parent document"),
|
|
#AR temporary hack to track back up.
|
|
#fontName = AttrMapValue(isStringOrNone),
|
|
renderScale = AttrMapValue(isNumber,desc="Global scaling for rendering"),
|
|
)
|
|
|
|
_attrMap = AttrMap(BASE=Group)
|
|
_attrMap.update(_xtraAttrMap)
|
|
|
|
def __init__(self, width=400, height=200, *nodes, **keywords):
|
|
self.background = None
|
|
apply(Group.__init__,(self,)+nodes,keywords)
|
|
self.width = width
|
|
self.height = height
|
|
self.hAlign = 'LEFT'
|
|
self.vAlign = 'BOTTOM'
|
|
self.renderScale = 1.0
|
|
|
|
def _renderPy(self):
|
|
I = {'reportlab.graphics.shapes': ['_DrawingEditorMixin','Drawing','Group']}
|
|
G = _renderGroupPy(self._explode(),'self',I)
|
|
n = 'ExplodedDrawing_' + self.__class__.__name__
|
|
s = '#Autogenerated by ReportLab guiedit do not edit\n'
|
|
for m, o in I.items():
|
|
s = s + 'from %s import %s\n' % (m,string.replace(str(o)[1:-1],"'",""))
|
|
s = s + '\nclass %s(_DrawingEditorMixin,Drawing):\n' % n
|
|
s = s + '\tdef __init__(self,width=%s,height=%s,*args,**kw):\n' % (self.width,self.height)
|
|
s = s + '\t\tapply(Drawing.__init__,(self,width,height)+args,kw)\n'
|
|
s = s + G
|
|
s = s + '\n\nif __name__=="__main__": #NORUNTESTS\n\t%s().save(formats=[\'pdf\'],outDir=\'.\',fnRoot=None)\n' % n
|
|
return s
|
|
|
|
def draw(self,showBoundary=_unset_):
|
|
"""This is used by the Platypus framework to let the document
|
|
draw itself in a story. It is specific to PDF and should not
|
|
be used directly."""
|
|
import renderPDF
|
|
renderPDF.draw(self, self.canv, 0, 0, showBoundary=showBoundary)
|
|
|
|
def wrap(self, availWidth, availHeight):
|
|
width = self.width
|
|
height = self.height
|
|
renderScale = self.renderScale
|
|
if renderScale!=1.0:
|
|
width *= renderScale
|
|
height *= renderScale
|
|
return width, height
|
|
|
|
def expandUserNodes(self):
|
|
"""Return a new drawing which only contains primitive shapes."""
|
|
obj = Group.expandUserNodes(self)
|
|
obj.width = self.width
|
|
obj.height = self.height
|
|
return obj
|
|
|
|
def copy(self):
|
|
"""Returns a copy"""
|
|
return self._copy(self.__class__(self.width, self.height))
|
|
|
|
def asGroup(self,*args,**kw):
|
|
return self._copy(apply(Group,args,kw))
|
|
|
|
def save(self, formats=None, verbose=None, fnRoot=None, outDir=None, title='', **kw):
|
|
"""Saves copies of self in desired location and formats.
|
|
Multiple formats can be supported in one call
|
|
|
|
the extra keywords can be of the form
|
|
_renderPM_dpi=96 (which passes dpi=96 to renderPM)
|
|
"""
|
|
from reportlab import rl_config
|
|
ext = ''
|
|
if not fnRoot:
|
|
fnRoot = getattr(self,'fileNamePattern',(self.__class__.__name__+'%03d'))
|
|
chartId = getattr(self,'chartId',0)
|
|
if callable(fnRoot):
|
|
fnRoot = fnRoot(chartId)
|
|
else:
|
|
try:
|
|
fnRoot = fnRoot % getattr(self,'chartId',0)
|
|
except TypeError, err:
|
|
#the exact error message changed from 2.2 to 2.3 so we need to
|
|
#check a substring
|
|
if str(err).find('not all arguments converted') < 0: raise
|
|
|
|
if os.path.isabs(fnRoot):
|
|
outDir, fnRoot = os.path.split(fnRoot)
|
|
else:
|
|
outDir = outDir or getattr(self,'outDir','.')
|
|
outDir = outDir.rstrip().rstrip(os.sep)
|
|
if not outDir: outDir = '.'
|
|
if not os.path.isabs(outDir): outDir = os.path.join(getattr(self,'_override_CWD',os.path.dirname(sys.argv[0])),outDir)
|
|
if not os.path.isdir(outDir): os.makedirs(outDir)
|
|
fnroot = os.path.normpath(os.path.join(outDir,fnRoot))
|
|
plotMode = os.path.splitext(fnroot)
|
|
if string.lower(plotMode[1][1:]) in ['pdf','ps','eps','gif','png','jpg','jpeg','pct','pict','tiff','tif','py','bmp','svg']:
|
|
fnroot = plotMode[0]
|
|
|
|
plotMode, verbose = formats or getattr(self,'formats',['pdf']), (verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),))[0]
|
|
_saved = logger.warnOnce.enabled, logger.infoOnce.enabled
|
|
logger.warnOnce.enabled = logger.infoOnce.enabled = verbose
|
|
if 'pdf' in plotMode:
|
|
from reportlab.graphics import renderPDF
|
|
filename = fnroot+'.pdf'
|
|
if verbose: print "generating PDF file %s" % filename
|
|
renderPDF.drawToFile(self, filename, title, showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPDF_',**kw))
|
|
ext = ext + '/.pdf'
|
|
if sys.platform=='mac':
|
|
import macfs, macostools
|
|
macfs.FSSpec(filename).SetCreatorType("CARO", "PDF ")
|
|
macostools.touched(filename)
|
|
|
|
for bmFmt in ['gif','png','tif','jpg','tiff','pct','pict', 'bmp']:
|
|
if bmFmt in plotMode:
|
|
from reportlab.graphics import renderPM
|
|
filename = '%s.%s' % (fnroot,bmFmt)
|
|
if verbose: print "generating %s file %s" % (bmFmt,filename)
|
|
renderPM.drawToFile(self, filename,fmt=bmFmt,showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPM_',**kw))
|
|
ext = ext + '/.' + bmFmt
|
|
|
|
if 'eps' in plotMode:
|
|
from rlextra.graphics import renderPS_SEP
|
|
filename = fnroot+'.eps'
|
|
if verbose: print "generating EPS file %s" % filename
|
|
renderPS_SEP.drawToFile(self,
|
|
filename,
|
|
title = fnroot,
|
|
dept = getattr(self,'EPS_info',['Testing'])[0],
|
|
company = getattr(self,'EPS_info',['','ReportLab'])[1],
|
|
preview = getattr(self,'preview',1),
|
|
showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPS_',**kw))
|
|
ext = ext + '/.eps'
|
|
|
|
if 'svg' in plotMode:
|
|
from reportlab.graphics import renderSVG
|
|
filename = fnroot+'.svg'
|
|
if verbose: print "generating EPS file %s" % filename
|
|
renderSVG.drawToFile(self,
|
|
filename,
|
|
showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderSVG_',**kw))
|
|
ext = ext + '/.svg'
|
|
|
|
if 'ps' in plotMode:
|
|
from reportlab.graphics import renderPS
|
|
filename = fnroot+'.ps'
|
|
if verbose: print "generating EPS file %s" % filename
|
|
renderPS.drawToFile(self, filename, showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPS_',**kw))
|
|
ext = ext + '/.ps'
|
|
|
|
if 'py' in plotMode:
|
|
filename = fnroot+'.py'
|
|
if verbose: print "generating py file %s" % filename
|
|
open(filename,'w').write(self._renderPy())
|
|
ext = ext + '/.py'
|
|
|
|
logger.warnOnce.enabled, logger.infoOnce.enabled = _saved
|
|
if hasattr(self,'saveLogger'):
|
|
self.saveLogger(fnroot,ext)
|
|
return ext and fnroot+ext[1:] or ''
|
|
|
|
|
|
def asString(self, format, verbose=None, preview=0):
|
|
"""Converts to an 8 bit string in given format."""
|
|
assert format in ['pdf','ps','eps','gif','png','jpg','jpeg','bmp','ppm','tiff','tif','py','pict','pct'], 'Unknown file format "%s"' % format
|
|
from reportlab import rl_config
|
|
#verbose = verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),)[0]
|
|
if format == 'pdf':
|
|
from reportlab.graphics import renderPDF
|
|
return renderPDF.drawToString(self)
|
|
elif format in ['gif','png','tif','jpg','pct','pict','bmp','ppm']:
|
|
from reportlab.graphics import renderPM
|
|
return renderPM.drawToString(self, fmt=format)
|
|
elif format == 'eps':
|
|
from rlextra.graphics import renderPS_SEP
|
|
return renderPS_SEP.drawToString(self,
|
|
preview = preview,
|
|
showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
|
|
elif format == 'ps':
|
|
from reportlab.graphics import renderPS
|
|
return renderPS.drawToString(self, showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
|
|
elif format == 'py':
|
|
return self._renderPy()
|
|
|
|
class _DrawingEditorMixin:
|
|
'''This is a mixin to provide functionality for edited drawings'''
|
|
def _add(self,obj,value,name=None,validate=None,desc=None,pos=None):
|
|
'''
|
|
effectively setattr(obj,name,value), but takes care of things with _attrMaps etc
|
|
'''
|
|
ivc = isValidChild(value)
|
|
if name and hasattr(obj,'_attrMap'):
|
|
if not obj.__dict__.has_key('_attrMap'):
|
|
obj._attrMap = obj._attrMap.clone()
|
|
if ivc and validate is None: validate = isValidChild
|
|
obj._attrMap[name] = AttrMapValue(validate,desc)
|
|
if hasattr(obj,'add') and ivc:
|
|
if pos:
|
|
obj.insert(pos,value,name)
|
|
else:
|
|
obj.add(value,name)
|
|
elif name:
|
|
setattr(obj,name,value)
|
|
else:
|
|
raise ValueError, "Can't add, need name"
|
|
|
|
class LineShape(Shape):
|
|
# base for types of lines
|
|
|
|
_attrMap = AttrMap(
|
|
strokeColor = AttrMapValue(isColorOrNone),
|
|
strokeWidth = AttrMapValue(isNumber),
|
|
strokeLineCap = AttrMapValue(None),
|
|
strokeLineJoin = AttrMapValue(None),
|
|
strokeMiterLimit = AttrMapValue(isNumber),
|
|
strokeDashArray = AttrMapValue(isListOfNumbersOrNone),
|
|
)
|
|
|
|
def __init__(self, kw):
|
|
self.strokeColor = STATE_DEFAULTS['strokeColor']
|
|
self.strokeWidth = 1
|
|
self.strokeLineCap = 0
|
|
self.strokeLineJoin = 0
|
|
self.strokeMiterLimit = 0
|
|
self.strokeDashArray = None
|
|
self.setProperties(kw)
|
|
|
|
|
|
class Line(LineShape):
|
|
_attrMap = AttrMap(BASE=LineShape,
|
|
x1 = AttrMapValue(isNumber),
|
|
y1 = AttrMapValue(isNumber),
|
|
x2 = AttrMapValue(isNumber),
|
|
y2 = AttrMapValue(isNumber),
|
|
)
|
|
|
|
def __init__(self, x1, y1, x2, y2, **kw):
|
|
LineShape.__init__(self, kw)
|
|
self.x1 = x1
|
|
self.y1 = y1
|
|
self.x2 = x2
|
|
self.y2 = y2
|
|
|
|
def getBounds(self):
|
|
"Returns bounding rectangle of object as (x1,y1,x2,y2)"
|
|
return (self.x1, self.y1, self.x2, self.y2)
|
|
|
|
|
|
class SolidShape(LineShape):
|
|
# base for anything with outline and content
|
|
|
|
_attrMap = AttrMap(BASE=LineShape,
|
|
fillColor = AttrMapValue(isColorOrNone),
|
|
)
|
|
|
|
def __init__(self, kw):
|
|
self.fillColor = STATE_DEFAULTS['fillColor']
|
|
# do this at the end so keywords overwrite
|
|
#the above settings
|
|
LineShape.__init__(self, kw)
|
|
|
|
|
|
# path operator constants
|
|
_MOVETO, _LINETO, _CURVETO, _CLOSEPATH = range(4)
|
|
_PATH_OP_ARG_COUNT = (2, 2, 6, 0) # [moveTo, lineTo, curveTo, closePath]
|
|
_PATH_OP_NAMES=['moveTo','lineTo','curveTo','closePath']
|
|
|
|
def _renderPath(path, drawFuncs):
|
|
"""Helper function for renderers."""
|
|
# this could be a method of Path...
|
|
points = path.points
|
|
i = 0
|
|
hadClosePath = 0
|
|
hadMoveTo = 0
|
|
for op in path.operators:
|
|
nArgs = _PATH_OP_ARG_COUNT[op]
|
|
func = drawFuncs[op]
|
|
j = i + nArgs
|
|
apply(func, points[i:j])
|
|
i = j
|
|
if op == _CLOSEPATH:
|
|
hadClosePath = hadClosePath + 1
|
|
if op == _MOVETO:
|
|
hadMoveTo = hadMoveTo + 1
|
|
return hadMoveTo == hadClosePath
|
|
|
|
class Path(SolidShape):
|
|
"""Path, made up of straight lines and bezier curves."""
|
|
|
|
_attrMap = AttrMap(BASE=SolidShape,
|
|
points = AttrMapValue(isListOfNumbers),
|
|
operators = AttrMapValue(isListOfNumbers),
|
|
isClipPath = AttrMapValue(isBoolean),
|
|
)
|
|
|
|
def __init__(self, points=None, operators=None, isClipPath=0, **kw):
|
|
SolidShape.__init__(self, kw)
|
|
if points is None:
|
|
points = []
|
|
if operators is None:
|
|
operators = []
|
|
assert len(points) % 2 == 0, 'Point list must have even number of elements!'
|
|
self.points = points
|
|
self.operators = operators
|
|
self.isClipPath = isClipPath
|
|
|
|
def copy(self):
|
|
new = self.__class__(self.points[:], self.operators[:])
|
|
new.setProperties(self.getProperties())
|
|
return new
|
|
|
|
def moveTo(self, x, y):
|
|
self.points.extend([x, y])
|
|
self.operators.append(_MOVETO)
|
|
|
|
def lineTo(self, x, y):
|
|
self.points.extend([x, y])
|
|
self.operators.append(_LINETO)
|
|
|
|
def curveTo(self, x1, y1, x2, y2, x3, y3):
|
|
self.points.extend([x1, y1, x2, y2, x3, y3])
|
|
self.operators.append(_CURVETO)
|
|
|
|
def closePath(self):
|
|
self.operators.append(_CLOSEPATH)
|
|
|
|
def getBounds(self):
|
|
return getPathBounds(self.points)
|
|
|
|
EmptyClipPath=Path() #special path
|
|
|
|
def getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, reverse=None):
|
|
if yradius is None: yradius = radius
|
|
points = []
|
|
from math import sin, cos, pi
|
|
degreestoradians = pi/180.0
|
|
startangle = startangledegrees*degreestoradians
|
|
endangle = endangledegrees*degreestoradians
|
|
while endangle<startangle:
|
|
endangle = endangle+2*pi
|
|
angle = float(endangle - startangle)
|
|
a = points.append
|
|
if angle>.001:
|
|
degreedelta = min(angle,degreedelta or 1.)
|
|
radiansdelta = degreedelta*degreestoradians
|
|
n = max(int(angle/radiansdelta+0.5),1)
|
|
radiansdelta = angle/n
|
|
n += 1
|
|
else:
|
|
n = 1
|
|
radiansdelta = 0
|
|
|
|
for angle in xrange(n):
|
|
angle = startangle+angle*radiansdelta
|
|
a((centerx+radius*cos(angle),centery+yradius*sin(angle)))
|
|
|
|
if reverse: points.reverse()
|
|
return points
|
|
|
|
class ArcPath(Path):
|
|
'''Path with an addArc method'''
|
|
def addArc(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, moveTo=None, reverse=None):
|
|
P = getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=yradius, degreedelta=degreedelta, reverse=reverse)
|
|
if moveTo or not len(self.operators):
|
|
self.moveTo(P[0][0],P[0][1])
|
|
del P[0]
|
|
for x, y in P: self.lineTo(x,y)
|
|
|
|
def definePath(pathSegs=[],isClipPath=0, dx=0, dy=0, **kw):
|
|
O = []
|
|
P = []
|
|
for seg in pathSegs:
|
|
if type(seg) not in [ListType,TupleType]:
|
|
opName = seg
|
|
args = []
|
|
else:
|
|
opName = seg[0]
|
|
args = seg[1:]
|
|
if opName not in _PATH_OP_NAMES:
|
|
raise ValueError, 'bad operator name %s' % opName
|
|
op = _PATH_OP_NAMES.index(opName)
|
|
if len(args)!=_PATH_OP_ARG_COUNT[op]:
|
|
raise ValueError, '%s bad arguments %s' % (opName,str(args))
|
|
O.append(op)
|
|
P.extend(list(args))
|
|
for d,o in (dx,0), (dy,1):
|
|
for i in xrange(o,len(P),2):
|
|
P[i] = P[i]+d
|
|
return apply(Path,(P,O,isClipPath),kw)
|
|
|
|
class Rect(SolidShape):
|
|
"""Rectangle, possibly with rounded corners."""
|
|
|
|
_attrMap = AttrMap(BASE=SolidShape,
|
|
x = AttrMapValue(isNumber),
|
|
y = AttrMapValue(isNumber),
|
|
width = AttrMapValue(isNumber),
|
|
height = AttrMapValue(isNumber),
|
|
rx = AttrMapValue(isNumber),
|
|
ry = AttrMapValue(isNumber),
|
|
)
|
|
|
|
def __init__(self, x, y, width, height, rx=0, ry=0, **kw):
|
|
SolidShape.__init__(self, kw)
|
|
self.x = x
|
|
self.y = y
|
|
self.width = width
|
|
self.height = height
|
|
self.rx = rx
|
|
self.ry = ry
|
|
|
|
def copy(self):
|
|
new = self.__class__(self.x, self.y, self.width, self.height)
|
|
new.setProperties(self.getProperties())
|
|
return new
|
|
|
|
def getBounds(self):
|
|
return (self.x, self.y, self.x + self.width, self.y + self.height)
|
|
|
|
|
|
class Image(SolidShape):
|
|
"""Bitmap image."""
|
|
|
|
_attrMap = AttrMap(BASE=SolidShape,
|
|
x = AttrMapValue(isNumber),
|
|
y = AttrMapValue(isNumber),
|
|
width = AttrMapValue(isNumberOrNone),
|
|
height = AttrMapValue(isNumberOrNone),
|
|
path = AttrMapValue(None),
|
|
)
|
|
|
|
def __init__(self, x, y, width, height, path, **kw):
|
|
SolidShape.__init__(self, kw)
|
|
self.x = x
|
|
self.y = y
|
|
self.width = width
|
|
self.height = height
|
|
self.path = path
|
|
|
|
def copy(self):
|
|
new = self.__class__(self.x, self.y, self.width, self.height, self.path)
|
|
new.setProperties(self.getProperties())
|
|
return new
|
|
|
|
def getBounds(self):
|
|
return (self.x, self.y, self.x + width, self.y + width)
|
|
|
|
class Circle(SolidShape):
|
|
|
|
_attrMap = AttrMap(BASE=SolidShape,
|
|
cx = AttrMapValue(isNumber),
|
|
cy = AttrMapValue(isNumber),
|
|
r = AttrMapValue(isNumber),
|
|
)
|
|
|
|
def __init__(self, cx, cy, r, **kw):
|
|
SolidShape.__init__(self, kw)
|
|
self.cx = cx
|
|
self.cy = cy
|
|
self.r = r
|
|
|
|
def copy(self):
|
|
new = self.__class__(self.cx, self.cy, self.r)
|
|
new.setProperties(self.getProperties())
|
|
return new
|
|
|
|
def getBounds(self):
|
|
return (self.cx - self.r, self.cy - self.r, self.cx + self.r, self.cy + self.r)
|
|
|
|
class Ellipse(SolidShape):
|
|
_attrMap = AttrMap(BASE=SolidShape,
|
|
cx = AttrMapValue(isNumber),
|
|
cy = AttrMapValue(isNumber),
|
|
rx = AttrMapValue(isNumber),
|
|
ry = AttrMapValue(isNumber),
|
|
)
|
|
|
|
def __init__(self, cx, cy, rx, ry, **kw):
|
|
SolidShape.__init__(self, kw)
|
|
self.cx = cx
|
|
self.cy = cy
|
|
self.rx = rx
|
|
self.ry = ry
|
|
|
|
def copy(self):
|
|
new = self.__class__(self.cx, self.cy, self.rx, self.ry)
|
|
new.setProperties(self.getProperties())
|
|
return new
|
|
|
|
def getBounds(self):
|
|
return (self.cx - self.rx, self.cy - self.ry, self.cx + self.rx, self.cy + self.ry)
|
|
|
|
class Wedge(SolidShape):
|
|
"""A "slice of a pie" by default translates to a polygon moves anticlockwise
|
|
from start angle to end angle"""
|
|
|
|
_attrMap = AttrMap(BASE=SolidShape,
|
|
centerx = AttrMapValue(isNumber),
|
|
centery = AttrMapValue(isNumber),
|
|
radius = AttrMapValue(isNumber),
|
|
startangledegrees = AttrMapValue(isNumber),
|
|
endangledegrees = AttrMapValue(isNumber),
|
|
yradius = AttrMapValue(isNumberOrNone),
|
|
radius1 = AttrMapValue(isNumberOrNone),
|
|
yradius1 = AttrMapValue(isNumberOrNone),
|
|
)
|
|
|
|
degreedelta = 1 # jump every 1 degrees
|
|
|
|
def __init__(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, **kw):
|
|
SolidShape.__init__(self, kw)
|
|
while endangledegrees<startangledegrees:
|
|
endangledegrees = endangledegrees+360
|
|
#print "__init__"
|
|
self.centerx, self.centery, self.radius, self.startangledegrees, self.endangledegrees = \
|
|
centerx, centery, radius, startangledegrees, endangledegrees
|
|
self.yradius = yradius
|
|
|
|
def _xtraRadii(self):
|
|
yradius = getattr(self, 'yradius', None)
|
|
if yradius is None: yradius = self.radius
|
|
radius1 = getattr(self,'radius1', None)
|
|
yradius1 = getattr(self,'yradius1',radius1)
|
|
if radius1 is None: radius1 = yradius1
|
|
return yradius, radius1, yradius1
|
|
|
|
#def __repr__(self):
|
|
# return "Wedge"+repr((self.centerx, self.centery, self.radius, self.startangledegrees, self.endangledegrees ))
|
|
#__str__ = __repr__
|
|
|
|
def asPolygon(self):
|
|
#print "asPolygon"
|
|
centerx= self.centerx
|
|
centery = self.centery
|
|
radius = self.radius
|
|
yradius, radius1, yradius1 = self._xtraRadii()
|
|
startangledegrees = self.startangledegrees
|
|
endangledegrees = self.endangledegrees
|
|
from math import sin, cos, pi
|
|
degreestoradians = pi/180.0
|
|
startangle = startangledegrees*degreestoradians
|
|
endangle = endangledegrees*degreestoradians
|
|
while endangle<startangle:
|
|
endangle = endangle+2*pi
|
|
angle = float(endangle-startangle)
|
|
points = []
|
|
if angle>0.001:
|
|
degreedelta = min(self.degreedelta or 1.,angle)
|
|
radiansdelta = degreedelta*degreestoradians
|
|
n = max(1,int(angle/radiansdelta+0.5))
|
|
radiansdelta = angle/n
|
|
n += 1
|
|
else:
|
|
n = 1
|
|
radiansdelta = 0
|
|
CA = []
|
|
CAA = CA.append
|
|
a = points.append
|
|
for angle in xrange(n):
|
|
angle = startangle+angle*radiansdelta
|
|
CAA((cos(angle),sin(angle)))
|
|
for c,s in CA:
|
|
a(centerx+radius*c)
|
|
a(centery+yradius*s)
|
|
if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None):
|
|
a(centerx); a(centery)
|
|
else:
|
|
CA.reverse()
|
|
for c,s in CA:
|
|
a(centerx+radius1*c)
|
|
a(centery+yradius1*s)
|
|
return Polygon(points)
|
|
|
|
def copy(self):
|
|
new = self.__class__(self.centerx,
|
|
self.centery,
|
|
self.radius,
|
|
self.startangledegrees,
|
|
self.endangledegrees)
|
|
new.setProperties(self.getProperties())
|
|
return new
|
|
|
|
def getBounds(self):
|
|
return self.asPolygon().getBounds()
|
|
|
|
class Polygon(SolidShape):
|
|
"""Defines a closed shape; Is implicitly
|
|
joined back to the start for you."""
|
|
|
|
_attrMap = AttrMap(BASE=SolidShape,
|
|
points = AttrMapValue(isListOfNumbers),
|
|
)
|
|
|
|
def __init__(self, points=[], **kw):
|
|
SolidShape.__init__(self, kw)
|
|
assert len(points) % 2 == 0, 'Point list must have even number of elements!'
|
|
self.points = points
|
|
|
|
def copy(self):
|
|
new = self.__class__(self.points)
|
|
new.setProperties(self.getProperties())
|
|
return new
|
|
|
|
def getBounds(self):
|
|
return getPointsBounds(self.points)
|
|
|
|
class PolyLine(LineShape):
|
|
"""Series of line segments. Does not define a
|
|
closed shape; never filled even if apparently joined.
|
|
Put the numbers in the list, not two-tuples."""
|
|
|
|
_attrMap = AttrMap(BASE=LineShape,
|
|
points = AttrMapValue(isListOfNumbers),
|
|
)
|
|
|
|
def __init__(self, points=[], **kw):
|
|
LineShape.__init__(self, kw)
|
|
lenPoints = len(points)
|
|
if lenPoints:
|
|
if type(points[0]) in (ListType,TupleType):
|
|
L = []
|
|
for (x,y) in points:
|
|
L.append(x)
|
|
L.append(y)
|
|
points = L
|
|
else:
|
|
assert len(points) % 2 == 0, 'Point list must have even number of elements!'
|
|
self.points = points
|
|
|
|
def copy(self):
|
|
new = self.__class__(self.points)
|
|
new.setProperties(self.getProperties())
|
|
return new
|
|
|
|
def getBounds(self):
|
|
return getPointsBounds(self.points)
|
|
|
|
class String(Shape):
|
|
"""Not checked against the spec, just a way to make something work.
|
|
Can be anchored left, middle or end."""
|
|
|
|
# to do.
|
|
_attrMap = AttrMap(
|
|
x = AttrMapValue(isNumber),
|
|
y = AttrMapValue(isNumber),
|
|
text = AttrMapValue(isString),
|
|
fontName = AttrMapValue(None),
|
|
fontSize = AttrMapValue(isNumber),
|
|
fillColor = AttrMapValue(isColorOrNone),
|
|
textAnchor = AttrMapValue(isTextAnchor),
|
|
encoding = AttrMapValue(isString),
|
|
)
|
|
|
|
def __init__(self, x, y, text, **kw):
|
|
self.x = x
|
|
self.y = y
|
|
self.text = text
|
|
self.textAnchor = 'start'
|
|
self.fontName = STATE_DEFAULTS['fontName']
|
|
self.fontSize = STATE_DEFAULTS['fontSize']
|
|
self.fillColor = STATE_DEFAULTS['fillColor']
|
|
self.setProperties(kw)
|
|
self.encoding = 'cp1252' #matches only fonts we have!
|
|
|
|
def getEast(self):
|
|
return self.x + stringWidth(self.text,self.fontName,self.fontSize, self.encoding)
|
|
|
|
def copy(self):
|
|
new = self.__class__(self.x, self.y, self.text)
|
|
new.setProperties(self.getProperties())
|
|
return new
|
|
|
|
def getBounds(self):
|
|
# assumes constant drop of 0.2*size to baseline
|
|
w = stringWidth(self.text,self.fontName,self.fontSize, self.encoding)
|
|
if self.textAnchor == 'start':
|
|
x = self.x
|
|
elif self.textAnchor == 'middle':
|
|
x = self.x - 0.5*w
|
|
elif self.textAnchor == 'end':
|
|
x = self.x - w
|
|
return (x, self.y - 0.2 * self.fontSize, x+w, self.y + self.fontSize)
|
|
|
|
class UserNode(_DrawTimeResizeable):
|
|
"""A simple template for creating a new node. The user (Python
|
|
programmer) may subclasses this. provideNode() must be defined to
|
|
provide a Shape primitive when called by a renderer. It does
|
|
NOT inherit from Shape, as the renderer always replaces it, and
|
|
your own classes can safely inherit from it without getting
|
|
lots of unintended behaviour."""
|
|
|
|
def provideNode(self):
|
|
"""Override this to create your own node. This lets widgets be
|
|
added to drawings; they must create a shape (typically a group)
|
|
so that the renderer can draw the custom node."""
|
|
|
|
raise NotImplementedError, "this method must be redefined by the user/programmer"
|
|
|
|
|
|
def test():
|
|
r = Rect(10,10,200,50)
|
|
import pprint
|
|
pp = pprint.pprint
|
|
print 'a Rectangle:'
|
|
pp(r.getProperties())
|
|
print
|
|
print 'verifying...',
|
|
r.verify()
|
|
print 'OK'
|
|
#print 'setting rect.z = "spam"'
|
|
#r.z = 'spam'
|
|
print 'deleting rect.width'
|
|
del r.width
|
|
print 'verifying...',
|
|
r.verify()
|
|
|
|
|
|
if __name__=='__main__':
|
|
test()
|