#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.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 endangledegrees0.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()