#Copyright ReportLab Europe Ltd. 2000-2017 #see license.txt for license details #history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/shapes.py __version__='3.5.60' __doc__='''Core of the graphics library - defines Drawing and Shapes''' import os, sys from math import pi, cos, sin, sqrt, radians, floor from pprint import pprint from reportlab.platypus import Flowable from reportlab.rl_config import shapeChecking, verbose, defaultGraphicsFontName as _baseGFontName, _unset_, decimalSymbol from reportlab.lib import logger from reportlab.lib import colors from reportlab.lib.validators import * from reportlab.lib.utils import isSeq, asBytes isOpacity = NoneOr(isNumberInRange(0,1)) from reportlab.lib.attrmap import * from reportlab.lib.rl_accel import fp_str from reportlab.pdfbase.pdfmetrics import stringWidth from reportlab.lib.fonts import tt2ps from reportlab.pdfgen.canvas import FILL_EVEN_ODD, FILL_NON_ZERO _baseGFontNameB = tt2ps(_baseGFontName,1,0) _baseGFontNameI = tt2ps(_baseGFontName,0,1) _baseGFontNameBI = tt2ps(_baseGFontName,1,1) 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' : 10, # don't know yet so let bomb here 'strokeDashArray': None, 'strokeOpacity': None, #100% 'fillOpacity': None, 'fillOverprint': False, 'strokeOverprint': False, 'overprintMask': 0, 'fillColor': colors.black, #...or text will be invisible 'fillMode': FILL_EVEN_ODD, #same as pdfgen.canvas 'fontSize': 10, 'fontName': _baseGFontName, 'textAnchor': 'start' # can be start, middle, end, inherited } #################################################################### # math utilities. These are now in reportlab.graphics.transform #################################################################### from . transform import * 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 = [x[0] for x in C] Y = [x[1] for x in 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 = [x for x in rectList if x is not None] 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 _getBezierExtrema(y0,y1,y2,y3): ''' this is used to find if a curveTo path operator has extrema in its range The curveTo operator is defined by the points y0, y1, y2, y3 B(t):=(1-t)^3*y0+3*(1-t)^2*t*y1+3*(1-t)*t^2*y2+t^3*y3 :=t^3*(y3-3*y2+3*y1-y0)+t^2*(3*y2-6*y1+3*y0)+t*(3*y1-3*y0)+y0 and is a cubic bezier curve. The differential is a quadratic t^2*(3*y3-9*y2+9*y1-3*y0)+t*(6*y2-12*y1+6*y0)+3*y1-3*y0 The extrema must be at real roots, r, of the above which lie in 0<=r<=1 The quadratic coefficients are a=3*y3-9*y2+9*y1-3*y0 b=6*y2-12*y1+6*y0 c=3*y1-3*y0 or a=y3-3*y2+3*y1-y0 b=2*y2-4*y1+2*y0 c=y1-y0 (remove common factor of 3) or a=y3-3*(y2-y1)-y0 b=2*(y2-2*y1+y0) c=y1-y0 The returned value is [y0,x1,x2,y3] where if found x1, x2 are any extremals that were found; there can be 0, 1 or 2 extremals ''' a=y3-3*(y2-y1)-y0 b=2*(y2-2*y1+y0) c=y1-y0 Y = [y0] #the set of points #standard method to find roots of quadratic d = b*b - 4*a*c if d>=0: d = sqrt(d) if b<0: d = -d q = -0.5*(b+d) R = [] try: R.append(q/a) except: pass try: R.append(c/q) except: pass b *= 1.5 c *= 3 for t in R: if 0<=t<=1: #real root in range evaluate spline there and add to X Y.append(t*(t*(t*a+b)+c)+y0) Y.append(y3) return Y def getPathBounds(points): n = len(points) f = lambda i,p = points: p[i] xs = list(map(f,range(0,n,2))) ys = list(map(f,range(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 isSeq(first): xs = [xy[0] for xy in pointList] ys = [xy[1] for xy in 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 = list(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 key in self._attrMap, "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",advancedUsage=1), contents = AttrMapValue(isListOfShapes,desc="Contained drawable elements"), strokeOverprint = AttrMapValue(isBoolean,desc='Turn on stroke overprinting'), fillOverprint = AttrMapValue(isBoolean,desc='Turn on fill overprinting',advancedUsage=1), overprintMask = AttrMapValue(isBoolean,desc='overprinting for ordinary CMYK',advancedUsage=1), ) 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 list(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(self,'__label__'): obj.__label__=self.__label__ if hasattr(obj,'transform'): obj.transform = self.transform[:] P = self.getContents()[:] # 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 = list(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 m not in I: 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 isinstance(self,float): return fp_str(self) elif isSeq(self): s = '' for v in self: s = s + '%s,' % _repr(v,I) if isinstance(self,list): return '[%s]' % s[:-1] else: return '(%s%s)' % (s[:-1],len(self)==1 and ',' or '') elif self is EmptyClipPath: if I: _addObjImport(self,I,'EmptyClipPath') return 'EmptyClipPath' elif isinstance(self,Shape): if I: _addObjImport(self,I) from inspect import getargspec args, varargs, varkw, defaults = getargspec(self.__init__) if defaults: kargs = args[-len(defaults):] del args[-len(defaults):] else: kargs = [] 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 in kargs: v = P[n] del P[n] s = s + '%s=%s,' % (n,_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) 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 += 1 l = getattr(n,'__label__','') if l: l='#'+l s = s + '%s%s=%s._nn(Group())%s\n' % (indent,npfx,pfx,l) s = s + _renderGroupPy(n,npfx,I,i,indent) 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.""" _saveModes = { 'bmp', 'eps', 'gif', 'jpeg', 'jpg', 'pct', 'pdf', 'pict', 'png', 'ps', 'py', 'svg', 'tif', 'tiff', 'tiff1', 'tiffl', 'tiffp', } _bmModes = _saveModes - {'eps','pdf','ps','py','svg'} _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 e.g. Rect(0,0,width,height)"), 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"), initialFontName = AttrMapValue(isStringOrNone,desc="override the STATE_DEFAULTS value for fontName"), initialFontSize = AttrMapValue(isNumberOrNone,desc="override the STATE_DEFAULTS value for fontSize"), ) _attrMap = AttrMap(BASE=Group, formats = AttrMapValue(SequenceOf( OneOf(*_saveModes), lo=1,emptyOK=0), desc='One or more plot modes'), ) _attrMap.update(_xtraAttrMap) def __init__(self, width=400, height=200, *nodes, **keywords): self.background = None 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'], 'reportlab.lib.colors': ['Color','CMYKColor','PCMYKColor'], } 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,str(o)[1:-1].replace("'","")) 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\tDrawing.__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.""" from reportlab.graphics import renderPDF renderPDF.draw(self, self.canv, 0, 0, showBoundary=showBoundary if showBoundary is not _unset_ else getattr(self,'_showBoundary',_unset_)) 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(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) """ genFmt = kw.pop('seqNumber','') if isinstance(genFmt,int): genFmt = '%4d: ' % genFmt else: genFmt = '' genFmt += 'generating %s file %s' from reportlab import rl_config ext = '' if not fnRoot: fnRoot = getattr(self,'fileNamePattern',(self.__class__.__name__+'%03d')) chartId = getattr(self,'chartId',0) if hasattr(chartId,'__call__'): chartId = chartId(self) if hasattr(fnRoot,'__call__'): fnRoot = fnRoot(chartId) else: try: fnRoot = fnRoot % chartId except TypeError as 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 outDir is None: outDir = getattr(self,'outDir',None) if hasattr(outDir,'__call__'): outDir = outDir(self) 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 plotMode[1][1:].lower() in self._saveModes: fnroot = plotMode[0] plotMode = [x.lower() for x in (formats or getattr(self,'formats',['pdf']))] verbose = (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(genFmt % ('PDF',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 self._bmModes: if bmFmt in plotMode: from reportlab.graphics import renderPM filename = '%s.%s' % (fnroot,bmFmt) if verbose: print(genFmt % (bmFmt,filename)) dtc = getattr(self,'_drawTimeCollector',None) if dtc: dtcfmts = getattr(dtc,'formats',[bmFmt]) if bmFmt in dtcfmts and not getattr(dtc,'disabled',0): dtc.clear() else: dtc = None renderPM.drawToFile(self, filename,fmt=bmFmt,showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPM_',**kw)) ext = ext + '/.' + bmFmt if dtc: dtc.save(filename) if 'eps' in plotMode: try: from rlextra.graphics import renderPS_SEP as renderPS except ImportError: from reportlab.graphics import renderPS filename = fnroot+'.eps' if verbose: print(genFmt % ('EPS',filename)) renderPS.drawToFile(self, filename, title = fnroot, dept = getattr(self,'EPS_info',['Testing'])[0], company = getattr(self,'EPS_info',['','ReportLab'])[1], preview = getattr(self,'preview',rl_config.eps_preview), showBoundary=getattr(self,'showBorder',rl_config.showBoundary), ttf_embed=getattr(self,'ttf_embed',rl_config.eps_ttf_embed), **_extraKW(self,'_renderPS_',**kw)) ext = ext + '/.eps' if 'svg' in plotMode: from reportlab.graphics import renderSVG filename = fnroot+'.svg' if verbose: print(genFmt % ('SVG',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(genFmt % ('EPS',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(genFmt % ('py',filename)) with open(filename,'wb') as f: f.write(asBytes(self._renderPy().replace('\n',os.linesep))) 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, **kw): """Converts to an 8 bit string in given format.""" assert format in self._saveModes, '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 self._bmModes: from reportlab.graphics import renderPM return renderPM.drawToString(self, fmt=format,showBoundary=getattr(self,'showBorder', rl_config.showBoundary),**_extraKW(self,'_renderPM_',**kw)) elif format == 'eps': try: from rlextra.graphics import renderPS_SEP as renderPS except ImportError: from reportlab.graphics import renderPS return renderPS.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() elif format == 'svg': from reportlab.graphics import renderSVG return renderSVG.drawToString(self,showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderSVG_',**kw)) def resized(self,kind='fit',lpad=0,rpad=0,bpad=0,tpad=0): '''return a base class drawing which ensures all the contents fits''' C = self.getContents() oW = self.width oH = self.height drawing = Drawing(oW,oH,*C) xL,yL,xH,yH = drawing.getBounds() if kind=='fit' or (kind=='expand' and (xLoW-rpad or yLoH-tpad)): drawing.width = xH-xL+lpad+rpad drawing.height = yH-yL+tpad+bpad drawing.transform = (1,0,0,1,lpad-xL,bpad-yL) elif kind=='fitx' or (kind=='expandx' and (xLoW-rpad)): drawing.width = xH-xL+lpad+rpad drawing.transform = (1,0,0,1,lpad-xL,0) elif kind=='fity' or (kind=='expandy' and (yLoH-tpad)): drawing.height = yH-yL+tpad+bpad drawing.transform = (1,0,0,1,0,bpad-yL) return drawing 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 '_attrMap' not in obj.__dict__: 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 isStrokeDashArray(Validator): def test(self,x): return isListOfNumbersOrNone.test(x) or (isinstance(x,(list,tuple)) and isNumber(x[0]) and isListOfNumbers(x[1])) isStrokeDashArray = isStrokeDashArray() class LineShape(Shape): # base for types of lines _attrMap = AttrMap( strokeColor = AttrMapValue(isColorOrNone), strokeWidth = AttrMapValue(isNumber), strokeLineCap = AttrMapValue(OneOf(0,1,2),desc="Line cap 0=butt, 1=round & 2=square"), strokeLineJoin = AttrMapValue(OneOf(0,1,2),desc="Line join 0=miter, 1=round & 2=bevel"), strokeMiterLimit = AttrMapValue(isNumber,desc="miter limit control miter line joins"), strokeDashArray = AttrMapValue(isStrokeDashArray,desc="[numbers] or [phase,[numbers]]"), strokeOpacity = AttrMapValue(isOpacity,desc="The level of transparency of the line, any real number betwen 0 and 1"), strokeOverprint = AttrMapValue(isBoolean,desc='Turn on stroke overprinting'), overprintMask = AttrMapValue(isBoolean,desc='overprinting for ordinary CMYK',advancedUsage=1), ) 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.strokeOpacity = None self.setProperties(kw) class Line(LineShape): _attrMap = AttrMap(BASE=LineShape, x1 = AttrMapValue(isNumber,desc=""), y1 = AttrMapValue(isNumber,desc=""), x2 = AttrMapValue(isNumber,desc=""), y2 = AttrMapValue(isNumber,desc=""), ) 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,desc="filling color of the shape, e.g. red"), fillOpacity = AttrMapValue(isOpacity,desc="the level of transparency of the color, any real number between 0 and 1"), fillOverprint = AttrMapValue(isBoolean,desc='Turn on fill overprinting'), overprintMask = AttrMapValue(isBoolean,desc='overprinting for ordinary CMYK',advancedUsage=1), fillMode = AttrMapValue(OneOf(FILL_EVEN_ODD,FILL_NON_ZERO)), ) def __init__(self, kw): self.fillColor = STATE_DEFAULTS['fillColor'] self.fillOpacity = None # do this at the end so keywords overwrite #the above settings LineShape.__init__(self, kw) # path operator constants _MOVETO, _LINETO, _CURVETO, _CLOSEPATH = list(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,countOnly=False,forceClose=False): """Helper function for renderers.""" # this could be a method of Path... points = path.points i = 0 hadClosePath = 0 hadMoveTo = 0 active = not countOnly for op in path.operators: if op == _MOVETO: if forceClose: if hadMoveTo and pop!=_CLOSEPATH: hadClosePath += 1 if active: drawFuncs[_CLOSEPATH]() hadMoveTo += 1 nArgs = _PATH_OP_ARG_COUNT[op] j = i + nArgs drawFuncs[op](*points[i:j]) i = j if op == _CLOSEPATH: hadClosePath += 1 pop = op if forceClose and hadMoveTo and pop!=_CLOSEPATH: hadClosePath += 1 if active: drawFuncs[_CLOSEPATH]() return hadMoveTo == hadClosePath _fillModeMap = { None: None, FILL_NON_ZERO: FILL_NON_ZERO, 'non-zero': FILL_NON_ZERO, 'nonzero': FILL_NON_ZERO, FILL_EVEN_ODD: FILL_EVEN_ODD, 'even-odd': FILL_EVEN_ODD, 'evenodd': FILL_EVEN_ODD, } 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), autoclose = AttrMapValue(NoneOr(OneOf('svg','pdf'))), fillMode = AttrMapValue(OneOf(FILL_EVEN_ODD,FILL_NON_ZERO)), ) def __init__(self, points=None, operators=None, isClipPath=0, autoclose=None, fillMode=FILL_EVEN_ODD, **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 self.autoclose=autoclose self.fillMode = fillMode 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): points = self.points try: #in case this complex algorithm is not yet ready :) X = [] aX = X.append eX = X.extend Y=[] aY = Y.append eY = Y.extend i = 0 for op in self.operators: nArgs = _PATH_OP_ARG_COUNT[op] j = i + nArgs if nArgs==2: #either moveTo or lineT0 aX(points[i]) aY(points[i+1]) elif nArgs==6: #curveTo x1,x2,x3 = points[i:j:2] eX(_getBezierExtrema(X[-1],x1,x2,x3)) y1,y2,y3 = points[i+1:j:2] eY(_getBezierExtrema(Y[-1],y1,y2,y3)) i = j return min(X),min(Y),max(X),max(Y) except: return getPathBounds(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 = [] 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 range(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 not isSeq(seg): 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 range(o,len(P),2): P[i] = P[i]+d #if there's a bounding box given we constrain so our points lie in it #partial bbox is allowed and does something sensible bbox = kw.pop('bbox',None) if bbox: for j in 0,1: d = bbox[j],bbox[j+2] if d[0] is None and d[1] is None: continue a = P[j::2] a, b = min(a), max(a) if d[0] is not None and d[1] is not None: c, d = min(d), max(d) fac = (b-a) if abs(fac)>=1e-6: fac = (d-c)/fac for i in range(j,len(P),2): P[i] = c + fac*(P[i]-a) else: #there's no range in the bbox so fixed as average for i in range(j,len(P),2): P[i] = (c + d)*0.5 else: #if there's a lower bound shift so min is lower bound #else there's an upper bound shift so max is upper bound c = d[0] - a if d[0] is not None else d[1] - b for i in range(i,len(P),2): P[i] += c return 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,desc="width of the object in points"), height = AttrMapValue(isNumber,desc="height of the objects in points"), 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,desc="width of the object in points"), height = AttrMapValue(isNumberOrNone,desc="height of the objects in points"), 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): # bug fix contributed by Marcel Tromp return (self.x, self.y, self.x + self.width, self.y + self.height) class Circle(SolidShape): _attrMap = AttrMap(BASE=SolidShape, cx = AttrMapValue(isNumber,desc="x of the centre"), cy = AttrMapValue(isNumber,desc="y of the centre"), r = AttrMapValue(isNumber,desc="radius in points"), ) 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,desc="x of the centre"), cy = AttrMapValue(isNumber,desc="y of the centre"), rx = AttrMapValue(isNumber,desc="x radius"), ry = AttrMapValue(isNumber,desc="y radius"), ) 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,desc="x of the centre"), centery = AttrMapValue(isNumber,desc="y of the centre"), radius = AttrMapValue(isNumber,desc="radius in points"), startangledegrees = AttrMapValue(isNumber), endangledegrees = AttrMapValue(isNumber), yradius = AttrMapValue(isNumberOrNone), radius1 = AttrMapValue(isNumberOrNone), yradius1 = AttrMapValue(isNumberOrNone), annular = AttrMapValue(isBoolean,desc='treat as annular ring'), ) degreedelta = 1 # jump every 1 degrees def __init__(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, annular=False, **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 range(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) if self.annular: P = Path(fillMode=getattr(self,'fillMode', FILL_EVEN_ODD)) P.moveTo(points[0],points[1]) for x in range(2,2*n,2): P.lineTo(points[x],points[x+1]) P.closePath() P.moveTo(points[2*n],points[2*n+1]) for x in range(2*n+2,4*n,2): P.lineTo(points[x],points[x+1]) P.closePath() return P else: 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,desc="list of numbers in the form x1, y1, x2, y2 ... xn, yn"), ) 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 or [] 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,desc="list of numbers in the form x1, y1, x2, y2 ... xn, yn"), ) def __init__(self, points=[], **kw): LineShape.__init__(self, kw) points = points or [] lenPoints = len(points) if lenPoints: if isSeq(points[0]): 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 Hatching(Path): '''define a hatching of a set of polygons defined by lists of the form [x0,y0,x1,y1,....,xn,yn]''' _attrMap = AttrMap(BASE=Path, xyLists = AttrMapValue(EitherOr((isListOfNumbers,SequenceOf(isListOfNumbers,lo=1)),"xy list(s)"),desc="list(s) of numbers in the form x1, y1, x2, y2 ... xn, yn"), angles = AttrMapValue(EitherOr((isNumber,isListOfNumbers,"angle(s)")),desc="the angle or list of angles at which hatching lines should be drawn"), spacings = AttrMapValue(EitherOr((isNumber,isListOfNumbers,"spacings(s)")),desc="orthogonal distance(s) between hatching lines"), ) def __init__(self, spacings=2, angles=45, xyLists=[], **kwds): Path.__init__(self, **kwds) if isListOfNumbers(xyLists): xyLists = (xyLists,) if isNumber(angles): angles = (angles,) #turn into a sequence if isNumber(spacings): spacings = (spacings,) #turn into a sequence i = len(angles)-len(spacings) if i>0: spacings = list(spacings)+i*[spacings[-1]] self.xyLists = xyLists self.angles = angles self.spacings = spacings moveTo = self.moveTo lineTo = self.lineTo for i, theta in enumerate(angles): spacing = spacings[i] theta = radians(theta) cosTheta = cos(theta) sinTheta = sin(theta) spanMin = 0x7fffffff spanMax = -spanMin # Loop to determine the span over which diagonal lines must be drawn. for P in xyLists: for j in range(0,len(P),2): # rotated point, since the stripes may be at an angle. y = P[j+1]*cosTheta-P[j]*sinTheta spanMin = min(y,spanMin) spanMax = max(y,spanMax) # Turn the span into a discrete step range. spanStart = int(floor(spanMin/spacing)) - 1 spanEnd = int(floor(spanMax/spacing)) + 1 # Loop to create all stripes. for step in range(spanStart,spanEnd): nodeX = [] stripeY = spacing*step # Loop to build a node list for one row of stripes. for P in xyLists: k = len(P)-2 #start by comparing with the last point for j in range(0,len(P),2): a = P[k] b = P[k+1] a1 = a*cosTheta + b*sinTheta b1 = b*cosTheta - a*sinTheta x = P[j] y = P[j+1] x1 = x*cosTheta+y*sinTheta y1 = y*cosTheta-x*sinTheta # Find the node, if any. if (b1=stripeY) or y1=stripeY: nodeX.append(a1+(x1-a1)*(stripeY-b1)/(y1-b1)) k = j nodeX.sort() # Loop to draw one row of line segments. for j in range(0,len(nodeX),2): # Rotate the points back to their original coordinate system. a = nodeX[j]*cosTheta - stripeY*sinTheta b = stripeY*cosTheta+nodeX[j]*sinTheta x = nodeX[j+1]*cosTheta - stripeY*sinTheta y = stripeY*cosTheta + nodeX[j+1]*sinTheta #Draw a single stripe segment. moveTo(a,b) lineTo(x,y) def numericXShift(tA,text,w,fontName,fontSize,encoding=None,pivotCharacter=decimalSymbol): dp = getattr(tA,'_dp',pivotCharacter) i = text.rfind(dp) if i>=0: dpOffs = getattr(tA,'_dpLen',0) w = dpOffs + stringWidth(text[:i],fontName,fontSize,encoding) return w 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,desc="x point of anchoring"), y = AttrMapValue(isNumber,desc="y point of anchoring"), text = AttrMapValue(isString,desc="the text of the string"), fontName = AttrMapValue(None,desc="font name of the text - font is either acrobat standard or registered when using external font."), fontSize = AttrMapValue(isNumber,desc="font size"), fillColor = AttrMapValue(isColorOrNone,desc="color of the font"), textAnchor = AttrMapValue(OneOf('start','middle','end','numeric'),desc="treat (x,y) as one of the options below."), encoding = AttrMapValue(isString), textRenderMode = AttrMapValue(OneOf(0,1,2,3,4,5,6,7),desc="Control whether text is filled/stroked etc etc"), ) encoding = 'utf8' 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) 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 t = self.text w = stringWidth(t,self.fontName,self.fontSize,self.encoding) tA = self.textAnchor x = self.x if tA!='start': if tA=='middle': x -= 0.5*w elif tA=='end': x -= w elif tA=='numeric': x -= numericXShift(tA,t,w,self.fontName,self.fontSize,self.encoding) 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") class DirectDraw(Shape): """try to draw directly on the canvas""" def drawDirectly(self,canvas): raise NotImplementedError("this method must be redefined by the user/programmer") def test(): r = Rect(10,10,200,50) import pprint pp = pprint.pprint w = sys.stdout.write w('a Rectangle: ') pp(r.getProperties()) w('\nverifying...') r.verify() w(' OK\n') #print 'setting rect.z = "spam"' #r.z = 'spam' w('deleting rect.width ') del r.width w('verifying...') r.verify() if __name__=='__main__': test()