1
#Copyright ReportLab Europe Ltd. 2000-2004
2
#see license.txt for license details
3
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/shapes.py
5
core of the graphics library - defines Drawing and Shapes
7
__version__=''' $Id$ '''
10
from math import pi, cos, sin, tan
11
from types import FloatType, IntType, ListType, TupleType, StringType, InstanceType
12
from pprint import pprint
14
from reportlab.platypus import Flowable
15
from reportlab.rl_config import shapeChecking, verbose
16
from reportlab.lib import logger
17
from reportlab.lib import colors
18
from reportlab.lib.validators import *
19
from reportlab.lib.attrmap import *
20
from reportlab.lib.utils import fp_str
21
from reportlab.pdfbase.pdfmetrics import stringWidth
23
class NotImplementedError(Exception):
26
# two constants for filling rules
27
NON_ZERO_WINDING = 'Non-Zero Winding'
30
## these can be overridden at module level before you start
31
#creating shapes. So, if using a special color model,
32
#this provides support for the rendering mechanism.
33
#you can change defaults globally before you start
34
#making shapes; one use is to substitute another
35
#color model cleanly throughout the drawing.
37
STATE_DEFAULTS = { # sensible defaults for all
38
'transform': (1,0,0,1,0,0),
40
# styles follow SVG naming
41
'strokeColor': colors.black,
45
'strokeMiterLimit' : 'TBA', # don't know yet so let bomb here
46
'strokeDashArray': None,
47
'strokeOpacity': 1.0, #100%
49
'fillColor': colors.black, #...or text will be invisible
50
#'fillRule': NON_ZERO_WINDING, - these can be done later
51
#'fillOpacity': 1.0, #100% - can be done later
54
'fontName': 'Times-Roman',
55
'textAnchor': 'start' # can be start, middle, end, inherited
59
####################################################################
60
# math utilities. These could probably be moved into lib
62
####################################################################
64
# constructors for matrices:
66
return (1, 0, 0, 1, 0, 0)
68
def translate(dx, dy):
69
return (1, 0, 0, 1, dx, dy)
72
return (sx, 0, 0, sy, 0, 0)
76
return (cos(a), sin(a), -sin(a), cos(a), 0, 0)
80
return (1, 0, tan(a), 1, 0, 0)
84
return (1, tan(a), 0, 1, 0, 0)
87
"A postmultiplied by B"
89
# [a0 a2 a4] [b0 b2 b4]
90
# [a1 a3 a5] * [b1 b3 b5]
93
return (A[0]*B[0] + A[2]*B[1],
94
A[1]*B[0] + A[3]*B[1],
95
A[0]*B[2] + A[2]*B[3],
96
A[1]*B[2] + A[3]*B[3],
97
A[0]*B[4] + A[2]*B[5] + A[4],
98
A[1]*B[4] + A[3]*B[5] + A[5])
101
"For A affine 2D represented as 6vec return 6vec version of A**(-1)"
103
det = float(A[0]*A[3] - A[2]*A[1])
104
R = [A[3]/det, -A[1]/det, -A[2]/det, A[0]/det]
105
return tuple(R+[-R[0]*A[4]-R[2]*A[5],-R[1]*A[4]-R[3]*A[5]])
107
def zTransformPoint(A,v):
108
"Apply the homogenous part of atransformation a to vector v --> A*v"
109
return (A[0]*v[0]+A[2]*v[1],A[1]*v[0]+A[3]*v[1])
111
def transformPoint(A,v):
112
"Apply transformation a to vector v --> A*v"
113
return (A[0]*v[0]+A[2]*v[1]+A[4],A[1]*v[0]+A[3]*v[1]+A[5])
115
def transformPoints(matrix, V):
116
return map(transformPoint, V)
118
def zTransformPoints(matrix, V):
119
return map(lambda x,matrix=matrix: zTransformPoint(matrix,x), V)
121
def _textBoxLimits(text, font, fontSize, leading, textAnchor, boxAnchor):
124
w = max(w,stringWidth(t,font, fontSize))
126
h = len(text)*leading
128
if boxAnchor[0]=='s':
131
elif boxAnchor[0]=='n':
137
if boxAnchor[-1]=='e':
139
if textAnchor=='end': xt = 0
140
elif textAnchor=='start': xt = -w
142
elif boxAnchor[-1]=='w':
144
if textAnchor=='end': xt = w
145
elif textAnchor=='start': xt = 0
149
if textAnchor=='end': xt = -xb
150
elif textAnchor=='start': xt = xb
153
return xb, yb, w, h, xt, yt
155
def _rotatedBoxLimits( x, y, w, h, angle):
157
Find the corner points of the rotated w x h sized box at x,y
158
return the corner points and the min max points in the original space
160
C = zTransformPoints(rotate(angle),((x,y),(x+w,y),(x+w,y+h),(x,y+h)))
161
X = map(lambda x: x[0], C)
162
Y = map(lambda x: x[1], C)
163
return min(X), max(X), min(Y), max(Y), C
166
class _DrawTimeResizeable:
167
'''Addin class to provide the horribleness of _drawTimeResize'''
168
def _drawTimeResize(self,w,h):
169
if hasattr(self,'_canvas'):
170
canvas = self._canvas
171
drawing = canvas._drawing
172
drawing.width, drawing.height = w, h
173
if hasattr(canvas,'_drawTimeResize'):
174
canvas._drawTimeResize(w,h)
176
class _SetKeyWordArgs:
177
def __init__(self, keywords={}):
178
"""In general properties may be supplied to the constructor."""
179
for key, value in keywords.items():
180
setattr(self, key, value)
183
#################################################################
185
# Helper functions for working out bounds
187
#################################################################
189
def getRectsBounds(rectList):
190
# filter out any None objects, e.g. empty groups
191
L = filter(lambda x: x is not None, rectList)
192
if not L: return None
194
xMin, yMin, xMax, yMax = L[0]
195
for (x1, y1, x2, y2) in L[1:]:
204
return (xMin, yMin, xMax, yMax)
206
def getPathBounds(points):
208
f = lambda i,p = points: p[i]
209
xs = map(f,xrange(0,n,2))
210
ys = map(f,xrange(1,n,2))
211
return (min(xs), min(ys), max(xs), max(ys))
213
def getPointsBounds(pointList):
214
"Helper function for list of points"
216
if type(first) in (ListType, TupleType):
217
xs = map(lambda xy: xy[0],pointList)
218
ys = map(lambda xy: xy[1],pointList)
219
return (min(xs), min(ys), max(xs), max(ys))
221
return getPathBounds(pointList)
223
#################################################################
225
# And now the shapes themselves....
227
#################################################################
228
class Shape(_SetKeyWordArgs,_DrawTimeResizeable):
229
"""Base class for all nodes in the tree. Nodes are simply
230
packets of data to be created, stored, and ultimately
231
rendered - they don't do anything active. They provide
232
convenience methods for verification but do not
233
check attribiute assignments or use any clever setattr
238
"""Return a clone of this shape."""
240
# implement this in the descendants as they need the right init methods.
241
raise NotImplementedError, "No copy method implemented for %s" % self.__class__.__name__
243
def getProperties(self,recur=1):
244
"""Interface to make it easy to extract automatic
247
#basic nodes have no children so this is easy.
248
#for more complex objects like widgets you
249
#may need to override this.
251
for key, value in self.__dict__.items():
256
def setProperties(self, props):
257
"""Supports the bulk setting if properties from,
258
for example, a GUI application or a config file."""
260
self.__dict__.update(props)
263
def dumpProperties(self, prefix=""):
264
"""Convenience. Lists them on standard output. You
265
may provide a prefix - mostly helps to generate code
266
samples for documentation."""
268
propList = self.getProperties().items()
271
prefix = prefix + '.'
272
for (name, value) in propList:
273
print '%s%s = %s' % (prefix, name, value)
276
"""If the programmer has provided the optional
277
_attrMap attribute, this checks all expected
278
attributes are present; no unwanted attributes
279
are present; and (if a checking function is found)
280
checks each attribute. Either succeeds or raises
281
an informative exception."""
283
if self._attrMap is not None:
284
for key in self.__dict__.keys():
286
assert self._attrMap.has_key(key), "Unexpected attribute %s found in %s" % (key, self)
287
for (attr, metavalue) in self._attrMap.items():
288
assert hasattr(self, attr), "Missing attribute %s from %s" % (attr, self)
289
value = getattr(self, attr)
290
assert metavalue.validate(value), "Invalid value %s for attribute %s in class %s" % (value, attr, self.__class__.__name__)
293
"""This adds the ability to check every attribute assignment as it is made.
294
It slows down shapes but is a big help when developing. It does not
295
get defined if rl_config.shapeChecking = 0"""
296
def __setattr__(self, attr, value):
297
"""By default we verify. This could be off
298
in some parallel base classes."""
299
validateSetattr(self,attr,value) #from reportlab.lib.attrmap
302
"Returns bounding rectangle of object as (x1,y1,x2,y2)"
303
raise NotImplementedError("Shapes and widgets must implement getBounds")
306
"""Groups elements together. May apply a transform
307
to its contents. Has a publicly accessible property
308
'contents' which may be used to iterate over contents.
309
In addition, child nodes may be given a name in which
310
case they are subsequently accessible as properties."""
313
transform = AttrMapValue(isTransform,desc="Coordinate transformation to apply"),
314
contents = AttrMapValue(isListOfShapes,desc="Contained drawable elements"),
317
def __init__(self, *elements, **keywords):
318
"""Initial lists of elements may be provided to allow
319
compact definitions in literal Python code. May or
320
may not be useful."""
322
# Groups need _attrMap to be an instance rather than
323
# a class attribute, as it may be extended at run time.
324
self._attrMap = self._attrMap.clone()
326
self.transform = (1,0,0,1,0,0)
329
# this just applies keywords; do it at the end so they
330
#don;t get overwritten
331
_SetKeyWordArgs.__init__(self, keywords)
333
def _addNamedNode(self,name,node):
334
'if name is not None add an attribute pointing to node and add to the attrMap'
336
if name not in self._attrMap.keys():
337
self._attrMap[name] = AttrMapValue(isValidChild)
338
setattr(self, name, node)
340
def add(self, node, name=None):
341
"""Appends non-None child node to the 'contents' attribute. In addition,
342
if a name is provided, it is subsequently accessible by name
344
# propagates properties down
346
assert isValidChild(node), "Can only add Shape or UserNode objects to a Group"
347
self.contents.append(node)
348
self._addNamedNode(name,node)
352
return self.contents[-1]
354
def insert(self, i, n, name=None):
355
'Inserts sub-node n in contents at specified location'
357
assert isValidChild(n), "Can only insert Shape or UserNode objects in a Group"
359
self.contents[i:i] =[n]
361
self.contents.insert(i,n)
362
self._addNamedNode(name,n)
364
def expandUserNodes(self):
365
"""Return a new object which only contains primitive shapes."""
367
# many limitations - shared nodes become multiple ones,
368
obj = isinstance(self,Drawing) and Drawing(self.width,self.height) or Group()
369
obj._attrMap = self._attrMap.clone()
370
if hasattr(obj,'transform'): obj.transform = self.transform[:]
372
self_contents = self.contents
373
a = obj.contents.append
374
for child in self_contents:
375
if isinstance(child, UserNode):
376
newChild = child.provideNode()
377
elif isinstance(child, Group):
378
newChild = child.expandUserNodes()
380
newChild = child.copy()
383
self._copyNamedContents(obj)
387
''' return a fully expanded object'''
388
from reportlab.graphics.widgetbase import Widget
390
if hasattr(obj,'transform'): obj.transform = self.transform[:]
391
P = self.contents[:] # pending nodes
394
if isinstance(n, UserNode):
395
P.append(n.provideNode())
396
elif isinstance(n, Group):
398
if n.transform==(1,0,0,1,0,0):
399
obj.contents.extend(n.contents)
406
def _copyContents(self,obj):
407
for child in self.contents:
408
obj.contents.append(child)
410
def _copyNamedContents(self,obj,aKeys=None,noCopy=('contents',)):
411
from copy import copy
412
self_contents = self.contents
413
if not aKeys: aKeys = self._attrMap.keys()
414
for (k, v) in self.__dict__.items():
415
if v in self_contents:
416
pos = self_contents.index(v)
417
setattr(obj, k, obj.contents[pos])
418
elif k in aKeys and k not in noCopy:
419
setattr(obj, k, copy(v))
423
obj._attrMap = self._attrMap.clone()
424
self._copyContents(obj)
425
self._copyNamedContents(obj)
430
return self._copy(self.__class__())
432
def rotate(self, theta):
433
"""Convenience to help you set transforms"""
434
self.transform = mmult(self.transform, rotate(theta))
436
def translate(self, dx, dy):
437
"""Convenience to help you set transforms"""
438
self.transform = mmult(self.transform, translate(dx, dy))
440
def scale(self, sx, sy):
441
"""Convenience to help you set transforms"""
442
self.transform = mmult(self.transform, scale(sx, sy))
445
def skew(self, kx, ky):
446
"""Convenience to help you set transforms"""
447
self.transform = mmult(mmult(self.transform, skewX(kx)),skewY(ky))
449
def shift(self, x, y):
450
'''Convenience function to set the origin arbitrarily'''
451
self.transform = self.transform[:-2]+(x,y)
453
def asDrawing(self, width, height):
454
""" Convenience function to make a drawing from a group
455
After calling this the instance will be a drawing!
457
self.__class__ = Drawing
458
self._attrMap.update(self._xtraAttrMap)
462
def getContents(self):
463
'''Return the list of things to be rendered
464
override to get more complicated behaviour'''
465
b = getattr(self,'background',None)
467
if b and b not in C: C = [b]+C
473
for elem in self.contents:
474
b.append(elem.getBounds())
475
x1 = getRectsBounds(b)
476
if x1 is None: return None
478
trans = self.transform
479
corners = [[x1,y1], [x1, y2], [x2, y1], [x2,y2]]
481
for corner in corners:
482
newCorners.append(transformPoint(trans, corner))
483
return getPointsBounds(newCorners)
485
#empty group needs a sane default; this
486
#will happen when interactively creating a group
487
#nothing has been added to yet. The alternative is
488
#to handle None as an allowed return value everywhere.
491
def _addObjImport(obj,I,n=None):
492
'''add an import of obj's class to a dictionary of imports''' #'
493
from inspect import getmodule
495
m = getmodule(c).__name__
502
def _repr(self,I=None):
503
'''return a repr style string with named fixed args first, then keywords'''
504
if type(self) is InstanceType:
505
if self is EmptyClipPath:
506
_addObjImport(self,I,'EmptyClipPath')
507
return 'EmptyClipPath'
508
if I: _addObjImport(self,I)
509
if isinstance(self,Shape):
510
from inspect import getargs
511
args, varargs, varkw = getargs(self.__init__.im_func.func_code)
512
P = self.getProperties()
513
s = self.__class__.__name__+'('
517
s = s + '%s,' % _repr(v,I)
518
for n,v in P.items():
520
s = s + '%s=%s,' % (n, _repr(v,I))
524
elif type(self) is FloatType:
526
elif type(self) in (ListType,TupleType):
529
s = s + '%s,' % _repr(v,I)
530
if type(self) is ListType:
531
return '[%s]' % s[:-1]
533
return '(%s%s)' % (s[:-1],len(self)==1 and ',' or '')
537
def _renderGroupPy(G,pfx,I,i=0,indent='\t\t'):
539
C = getattr(G,'transform',None)
540
if C: s = s + ('%s%s.transform = %s\n' % (indent,pfx,_repr(C)))
543
if isinstance(n, Group):
546
s = s + '%s%s=%s._nn(Group())\n' % (indent,npfx,pfx)
547
s = s + _renderGroupPy(n,npfx,I,i,indent)
550
s = s + '%s%s.add(%s)\n' % (indent,pfx,_repr(n,I))
553
class Drawing(Group, Flowable):
554
"""Outermost container; the thing a renderer works on.
555
This has no properties except a height, width and list
558
_xtraAttrMap = AttrMap(
559
width = AttrMapValue(isNumber,desc="Drawing width in points."),
560
height = AttrMapValue(isNumber,desc="Drawing height in points."),
561
canv = AttrMapValue(None),
562
background = AttrMapValue(isValidChildOrNone,desc="Background widget for the drawing"),
563
hAlign = AttrMapValue(OneOf("LEFT", "RIGHT", "CENTER", "CENTRE"), desc="Alignment within parent document"),
564
vAlign = AttrMapValue(OneOf("TOP", "BOTTOM", "CENTER", "CENTRE"), desc="Alignment within parent document")
567
_attrMap = AttrMap(BASE=Group)
568
_attrMap.update(_xtraAttrMap)
570
def __init__(self, width=400, height=200, *nodes, **keywords):
571
self.background = None
572
apply(Group.__init__,(self,)+nodes,keywords)
576
self.vAlign = 'BOTTOM'
579
I = {'reportlab.graphics.shapes': ['_DrawingEditorMixin','Drawing','Group']}
580
G = _renderGroupPy(self._explode(),'self',I)
581
n = 'ExplodedDrawing_' + self.__class__.__name__
582
s = '#Autogenerated by ReportLab guiedit do not edit\n'
583
for m, o in I.items():
584
s = s + 'from %s import %s\n' % (m,string.replace(str(o)[1:-1],"'",""))
585
s = s + '\nclass %s(_DrawingEditorMixin,Drawing):\n' % n
586
s = s + '\tdef __init__(self,width=%s,height=%s,*args,**kw):\n' % (self.width,self.height)
587
s = s + '\t\tapply(Drawing.__init__,(self,width,height)+args,kw)\n'
589
s = s + '\n\nif __name__=="__main__": #NORUNTESTS\n\t%s().save(formats=[\'pdf\'],outDir=\'.\',fnRoot=None)\n' % n
593
"""This is used by the Platypus framework to let the document
594
draw itself in a story. It is specific to PDF and should not
598
R = renderPDF._PDFRenderer()
599
R.draw(self, self.canv, 0, 0)
601
def expandUserNodes(self):
602
"""Return a new drawing which only contains primitive shapes."""
603
obj = Group.expandUserNodes(self)
604
obj.width = self.width
605
obj.height = self.height
610
return self._copy(Drawing(self.width, self.height))
612
def asGroup(self,*args,**kw):
613
return self._copy(apply(Group,args,kw))
615
def save(self, formats=None, verbose=None, fnRoot=None, outDir=None, title=''):
616
"""Saves copies of self in desired location and formats.
618
Multiple formats can be supported in one call"""
619
from reportlab import rl_config
622
fnRoot = getattr(self,'fileNamePattern',(self.__class__.__name__+'%03d'))
623
chartId = getattr(self,'chartId',0)
625
fnRoot = fnRoot(chartId)
628
fnRoot = fnRoot % getattr(self,'chartId',0)
629
except TypeError, err:
630
if str(err) != 'not all arguments converted': raise
632
if os.path.isabs(fnRoot):
633
outDir, fnRoot = os.path.split(fnRoot)
635
outDir = outDir or getattr(self,'outDir','.')
636
if not os.path.isabs(outDir): outDir = os.path.join(os.path.dirname(sys.argv[0]),outDir)
637
if not os.path.isdir(outDir): os.makedirs(outDir)
638
fnroot = os.path.normpath(os.path.join(outDir,fnRoot))
639
plotMode = os.path.splitext(fnroot)
640
if string.lower(plotMode[1][1:]) in ['pdf','ps','eps','gif','png','jpg','jpeg','pct','pict','tiff','tif','py','bmp']:
643
plotMode, verbose = formats or getattr(self,'formats',['pdf']), (verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),))[0]
644
_saved = logger.warnOnce.enabled, logger.infoOnce.enabled
645
logger.warnOnce.enabled = logger.infoOnce.enabled = verbose
646
if 'pdf' in plotMode:
647
from reportlab.graphics import renderPDF
648
filename = fnroot+'.pdf'
649
if verbose: print "generating PDF file %s" % filename
650
renderPDF.drawToFile(self, filename, title, showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
652
if sys.platform=='mac':
653
import macfs, macostools
654
macfs.FSSpec(filename).SetCreatorType("CARO", "PDF ")
655
macostools.touched(filename)
657
for bmFmt in ['gif','png','tif','jpg','tiff','pct','pict', 'bmp']:
658
if bmFmt in plotMode:
659
from reportlab.graphics import renderPM
660
filename = '%s.%s' % (fnroot,bmFmt)
661
if verbose: print "generating %s file %s" % (bmFmt,filename)
662
renderPM.drawToFile(self, filename,fmt=bmFmt,showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
663
ext = ext + '/.' + bmFmt
665
if 'eps' in plotMode:
666
from rlextra.graphics import renderPS_SEP
667
filename = fnroot+'.eps'
668
if verbose: print "generating EPS file %s" % filename
669
renderPS_SEP.drawToFile(self,
672
dept = getattr(self,'EPS_info',['Testing'])[0],
673
company = getattr(self,'EPS_info',['','ReportLab'])[1],
674
preview = getattr(self,'preview',1),
675
showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
679
from reportlab.graphics import renderPS
680
filename = fnroot+'.ps'
681
if verbose: print "generating EPS file %s" % filename
682
renderPS.drawToFile(self, filename, showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
686
filename = fnroot+'.py'
687
if verbose: print "generating py file %s" % filename
688
open(filename,'w').write(self._renderPy())
691
logger.warnOnce.enabled, logger.infoOnce.enabled = _saved
692
if hasattr(self,'saveLogger'):
693
self.saveLogger(fnroot,ext)
694
return ext and fnroot+ext[1:] or ''
697
def asString(self, format, verbose=None, preview=0):
698
"""Converts to an 8 bit string in given format."""
699
assert format in ['pdf','ps','eps','gif','png','jpg','jpeg','bmp','ppm','tiff','tif','py','pict','pct'], 'Unknown file format "%s"' % format
700
from reportlab import rl_config
701
#verbose = verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),)[0]
703
from reportlab.graphics import renderPDF
704
return renderPDF.drawToString(self)
705
elif format in ['gif','png','tif','jpg','pct','pict','bmp','ppm']:
706
from reportlab.graphics import renderPM
707
return renderPM.drawToString(self, fmt=format)
708
elif format == 'eps':
709
from rlextra.graphics import renderPS_SEP
710
return renderPS_SEP.drawToString(self,
712
showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
714
from reportlab.graphics import renderPS
715
return renderPS.drawToString(self, showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
717
return self._renderPy()
719
class _DrawingEditorMixin:
720
'''This is a mixin to provide functionality for edited drawings'''
721
def _add(self,obj,value,name=None,validate=None,desc=None,pos=None):
723
effectively setattr(obj,name,value), but takes care of things with _attrMaps etc
725
ivc = isValidChild(value)
726
if name and hasattr(obj,'_attrMap'):
727
if not obj.__dict__.has_key('_attrMap'):
728
obj._attrMap = obj._attrMap.clone()
729
if ivc and validate is None: validate = isValidChild
730
obj._attrMap[name] = AttrMapValue(validate,desc)
731
if hasattr(obj,'add') and ivc:
733
obj.insert(pos,value,name)
737
setattr(obj,name,value)
739
raise ValueError, "Can't add, need name"
741
class LineShape(Shape):
742
# base for types of lines
745
strokeColor = AttrMapValue(isColorOrNone),
746
strokeWidth = AttrMapValue(isNumber),
747
strokeLineCap = AttrMapValue(None),
748
strokeLineJoin = AttrMapValue(None),
749
strokeMiterLimit = AttrMapValue(isNumber),
750
strokeDashArray = AttrMapValue(isListOfNumbersOrNone),
753
def __init__(self, kw):
754
self.strokeColor = STATE_DEFAULTS['strokeColor']
756
self.strokeLineCap = 0
757
self.strokeLineJoin = 0
758
self.strokeMiterLimit = 0
759
self.strokeDashArray = None
760
self.setProperties(kw)
763
class Line(LineShape):
764
_attrMap = AttrMap(BASE=LineShape,
765
x1 = AttrMapValue(isNumber),
766
y1 = AttrMapValue(isNumber),
767
x2 = AttrMapValue(isNumber),
768
y2 = AttrMapValue(isNumber),
771
def __init__(self, x1, y1, x2, y2, **kw):
772
LineShape.__init__(self, kw)
779
"Returns bounding rectangle of object as (x1,y1,x2,y2)"
780
return (self.x1, self.y1, self.x2, self.y2)
783
class SolidShape(LineShape):
784
# base for anything with outline and content
786
_attrMap = AttrMap(BASE=LineShape,
787
fillColor = AttrMapValue(isColorOrNone),
790
def __init__(self, kw):
791
self.fillColor = STATE_DEFAULTS['fillColor']
792
# do this at the end so keywords overwrite
794
LineShape.__init__(self, kw)
797
# path operator constants
798
_MOVETO, _LINETO, _CURVETO, _CLOSEPATH = range(4)
799
_PATH_OP_ARG_COUNT = (2, 2, 6, 0) # [moveTo, lineTo, curveTo, closePath]
800
_PATH_OP_NAMES=['moveTo','lineTo','curveTo','closePath']
802
def _renderPath(path, drawFuncs):
803
"""Helper function for renderers."""
804
# this could be a method of Path...
809
for op in path.operators:
810
nArgs = _PATH_OP_ARG_COUNT[op]
813
apply(func, points[i:j])
816
hadClosePath = hadClosePath + 1
818
hadMoveTo = hadMoveTo + 1
819
return hadMoveTo == hadClosePath
821
class Path(SolidShape):
822
"""Path, made up of straight lines and bezier curves."""
824
_attrMap = AttrMap(BASE=SolidShape,
825
points = AttrMapValue(isListOfNumbers),
826
operators = AttrMapValue(isListOfNumbers),
827
isClipPath = AttrMapValue(isBoolean),
830
def __init__(self, points=None, operators=None, isClipPath=0, **kw):
831
SolidShape.__init__(self, kw)
834
if operators is None:
836
assert len(points) % 2 == 0, 'Point list must have even number of elements!'
838
self.operators = operators
839
self.isClipPath = isClipPath
842
new = Path(self.points[:], self.operators[:])
843
new.setProperties(self.getProperties())
846
def moveTo(self, x, y):
847
self.points.extend([x, y])
848
self.operators.append(_MOVETO)
850
def lineTo(self, x, y):
851
self.points.extend([x, y])
852
self.operators.append(_LINETO)
854
def curveTo(self, x1, y1, x2, y2, x3, y3):
855
self.points.extend([x1, y1, x2, y2, x3, y3])
856
self.operators.append(_CURVETO)
859
self.operators.append(_CLOSEPATH)
862
return getPathBounds(self.points)
864
EmptyClipPath=Path() #special path
866
def getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, reverse=None):
867
degreedelta = degreedelta or 1
868
if yradius is None: yradius = radius
870
from math import sin, cos, pi
871
degreestoradians = pi/180.0
872
radiansdelta = degreedelta*degreestoradians
873
startangle = startangledegrees*degreestoradians
874
endangle = endangledegrees*degreestoradians
875
while endangle<startangle:
876
endangle = endangle+2*pi
877
angle = float(endangle - startangle)
879
n = max(int(angle/radiansdelta+0.5),1)
880
radiansdelta = angle/n
882
for angle in xrange(n+1):
883
angle = startangle+angle*radiansdelta
884
a((centerx+radius*cos(angle),centery+yradius*sin(angle)))
885
#a((centerx+radius*cos(endangle),centery+yradius*sin(endangle)))
886
if reverse: points.reverse()
890
'''Path with an addArc method'''
891
def addArc(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, moveTo=None, reverse=None):
892
P = getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=yradius, degreedelta=degreedelta, reverse=reverse)
893
if moveTo or not len(self.operators):
894
self.moveTo(P[0][0],P[0][1])
896
for x, y in P: self.lineTo(x,y)
898
def definePath(pathSegs=[],isClipPath=0, dx=0, dy=0, **kw):
902
if type(seg) not in [ListType,TupleType]:
908
if opName not in _PATH_OP_NAMES:
909
raise ValueError, 'bad operator name %s' % opName
910
op = _PATH_OP_NAMES.index(opName)
911
if len(args)!=_PATH_OP_ARG_COUNT[op]:
912
raise ValueError, '%s bad arguments %s' % (opName,str(args))
915
for d,o in (dx,0), (dy,1):
916
for i in xrange(o,len(P),2):
918
return apply(Path,(P,O,isClipPath),kw)
920
class Rect(SolidShape):
921
"""Rectangle, possibly with rounded corners."""
923
_attrMap = AttrMap(BASE=SolidShape,
924
x = AttrMapValue(isNumber),
925
y = AttrMapValue(isNumber),
926
width = AttrMapValue(isNumber),
927
height = AttrMapValue(isNumber),
928
rx = AttrMapValue(isNumber),
929
ry = AttrMapValue(isNumber),
932
def __init__(self, x, y, width, height, rx=0, ry=0, **kw):
933
SolidShape.__init__(self, kw)
942
new = Rect(self.x, self.y, self.width, self.height)
943
new.setProperties(self.getProperties())
947
return (self.x, self.y, self.x + self.width, self.y + self.height)
950
class Image(SolidShape):
953
_attrMap = AttrMap(BASE=SolidShape,
954
x = AttrMapValue(isNumber),
955
y = AttrMapValue(isNumber),
956
width = AttrMapValue(isNumberOrNone),
957
height = AttrMapValue(isNumberOrNone),
958
path = AttrMapValue(None),
961
def __init__(self, x, y, width, height, path, **kw):
962
SolidShape.__init__(self, kw)
970
new = Image(self.x, self.y, self.width, self.height, self.path)
971
new.setProperties(self.getProperties())
975
return (self.x, self.y, self.x + width, self.y + width)
977
class Circle(SolidShape):
979
_attrMap = AttrMap(BASE=SolidShape,
980
cx = AttrMapValue(isNumber),
981
cy = AttrMapValue(isNumber),
982
r = AttrMapValue(isNumber),
985
def __init__(self, cx, cy, r, **kw):
986
SolidShape.__init__(self, kw)
992
new = Circle(self.cx, self.cy, self.r)
993
new.setProperties(self.getProperties())
997
return (self.cx - self.r, self.cy - self.r, self.cx + self.r, self.cy + self.r)
999
class Ellipse(SolidShape):
1001
_attrMap = AttrMap(BASE=SolidShape,
1002
cx = AttrMapValue(isNumber),
1003
cy = AttrMapValue(isNumber),
1004
rx = AttrMapValue(isNumber),
1005
ry = AttrMapValue(isNumber),
1008
def __init__(self, cx, cy, rx, ry, **kw):
1009
SolidShape.__init__(self, kw)
1016
new = Ellipse(self.cx, self.cy, self.rx, self.ry)
1017
new.setProperties(self.getProperties())
1020
def getBounds(self):
1021
return (self.cx - self.rx, self.cy - self.ry, self.cx + self.rx, self.cy + self.ry)
1023
class Wedge(SolidShape):
1024
"""A "slice of a pie" by default translates to a polygon moves anticlockwise
1025
from start angle to end angle"""
1027
_attrMap = AttrMap(BASE=SolidShape,
1028
centerx = AttrMapValue(isNumber),
1029
centery = AttrMapValue(isNumber),
1030
radius = AttrMapValue(isNumber),
1031
startangledegrees = AttrMapValue(isNumber),
1032
endangledegrees = AttrMapValue(isNumber),
1033
yradius = AttrMapValue(isNumberOrNone),
1034
radius1 = AttrMapValue(isNumberOrNone),
1035
yradius1 = AttrMapValue(isNumberOrNone),
1038
degreedelta = 1 # jump every 1 degrees
1040
def __init__(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, **kw):
1041
SolidShape.__init__(self, kw)
1042
while endangledegrees<startangledegrees:
1043
endangledegrees = endangledegrees+360
1045
self.centerx, self.centery, self.radius, self.startangledegrees, self.endangledegrees = \
1046
centerx, centery, radius, startangledegrees, endangledegrees
1047
self.yradius = yradius
1049
def _xtraRadii(self):
1050
yradius = getattr(self, 'yradius', None)
1051
if yradius is None: yradius = self.radius
1052
radius1 = getattr(self,'radius1', None)
1053
yradius1 = getattr(self,'yradius1',radius1)
1054
if radius1 is None: radius1 = yradius1
1055
return yradius, radius1, yradius1
1057
#def __repr__(self):
1058
# return "Wedge"+repr((self.centerx, self.centery, self.radius, self.startangledegrees, self.endangledegrees ))
1061
def asPolygon(self):
1063
centerx= self.centerx
1064
centery = self.centery
1065
radius = self.radius
1066
yradius, radius1, yradius1 = self._xtraRadii()
1067
startangledegrees = self.startangledegrees
1068
endangledegrees = self.endangledegrees
1069
degreedelta = self.degreedelta
1070
from math import sin, cos, pi
1071
degreestoradians = pi/180.0
1072
radiansdelta = degreedelta*degreestoradians
1073
startangle = startangledegrees*degreestoradians
1074
endangle = endangledegrees*degreestoradians
1075
while endangle<startangle:
1076
endangle = endangle+2*pi
1077
angle = float(endangle-startangle)
1080
n = max(1,int(angle/radiansdelta+0.5))
1081
radiansdelta = angle/n
1085
for angle in xrange(n+1):
1086
angle = startangle+angle*radiansdelta
1087
CAA((cos(angle),sin(angle)))
1088
#CAA((cos(endangle),sin(endangle)))
1091
a(centery+yradius*s)
1092
if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None):
1093
a(centerx); a(centery)
1097
a(centerx+radius1*c)
1098
a(centery+yradius1*s)
1099
return Polygon(points)
1102
new = Wedge(self.centerx,
1105
self.startangledegrees,
1106
self.endangledegrees)
1107
new.setProperties(self.getProperties())
1110
def getBounds(self):
1111
return self.asPolygon().getBounds()
1113
class Polygon(SolidShape):
1114
"""Defines a closed shape; Is implicitly
1115
joined back to the start for you."""
1117
_attrMap = AttrMap(BASE=SolidShape,
1118
points = AttrMapValue(isListOfNumbers),
1121
def __init__(self, points=[], **kw):
1122
SolidShape.__init__(self, kw)
1123
assert len(points) % 2 == 0, 'Point list must have even number of elements!'
1124
self.points = points
1127
new = Polygon(self.points)
1128
new.setProperties(self.getProperties())
1131
def getBounds(self):
1132
return getPointsBounds(self.points)
1134
class PolyLine(LineShape):
1135
"""Series of line segments. Does not define a
1136
closed shape; never filled even if apparently joined.
1137
Put the numbers in the list, not two-tuples."""
1139
_attrMap = AttrMap(BASE=LineShape,
1140
points = AttrMapValue(isListOfNumbers),
1143
def __init__(self, points=[], **kw):
1144
LineShape.__init__(self, kw)
1145
lenPoints = len(points)
1147
if type(points[0]) in (ListType,TupleType):
1149
for (x,y) in points:
1154
assert len(points) % 2 == 0, 'Point list must have even number of elements!'
1155
self.points = points
1158
new = PolyLine(self.points)
1159
new.setProperties(self.getProperties())
1162
def getBounds(self):
1163
return getPointsBounds(self.points)
1165
class String(Shape):
1166
"""Not checked against the spec, just a way to make something work.
1167
Can be anchored left, middle or end."""
1171
x = AttrMapValue(isNumber),
1172
y = AttrMapValue(isNumber),
1173
text = AttrMapValue(isString),
1174
fontName = AttrMapValue(None),
1175
fontSize = AttrMapValue(isNumber),
1176
fillColor = AttrMapValue(isColorOrNone),
1177
textAnchor = AttrMapValue(isTextAnchor),
1180
def __init__(self, x, y, text, **kw):
1184
self.textAnchor = 'start'
1185
self.fontName = STATE_DEFAULTS['fontName']
1186
self.fontSize = STATE_DEFAULTS['fontSize']
1187
self.fillColor = STATE_DEFAULTS['fillColor']
1188
self.setProperties(kw)
1191
return self.x + stringWidth(self.text,self.fontName,self.fontSize)
1194
new = String(self.x, self.y, self.text)
1195
new.setProperties(self.getProperties())
1198
def getBounds(self):
1199
# assumes constant drop of 0.2*size to baseline
1200
w = stringWidth(self.text,self.fontName,self.fontSize)
1201
if self.textAnchor == 'start':
1203
elif self.textAnchor == 'middle':
1205
elif self.textAnchor == 'end':
1207
return (x, self.y - 0.2 * self.fontSize, x+w, self.y + self.fontSize)
1209
class UserNode(_DrawTimeResizeable):
1210
"""A simple template for creating a new node. The user (Python
1211
programmer) may subclasses this. provideNode() must be defined to
1212
provide a Shape primitive when called by a renderer. It does
1213
NOT inherit from Shape, as the renderer always replaces it, and
1214
your own classes can safely inherit from it without getting
1215
lots of unintended behaviour."""
1217
def provideNode(self):
1218
"""Override this to create your own node. This lets widgets be
1219
added to drawings; they must create a shape (typically a group)
1220
so that the renderer can draw the custom node."""
1222
raise NotImplementedError, "this method must be redefined by the user/programmer"
1226
r = Rect(10,10,200,50)
1229
print 'a Rectangle:'
1230
pp(r.getProperties())
1232
print 'verifying...',
1235
#print 'setting rect.z = "spam"'
1237
print 'deleting rect.width'
1239
print 'verifying...',
1243
if __name__=='__main__':