Added graphics module
authorandy_robinson
Mon, 22 Jan 2001 22:05:38 +0000
changeset 568 9cadc5ef53db
parent 567 0c7064b86e9e
child 569 91eb80120323
Added graphics module
reportlab/graphics/__init__.py
reportlab/graphics/renderPDF.py
reportlab/graphics/renderbase.py
reportlab/graphics/shapes.py
reportlab/graphics/testdrawings.py
reportlab/graphics/widgetbase.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/reportlab/graphics/renderPDF.py	Mon Jan 22 22:05:38 2001 +0000
@@ -0,0 +1,332 @@
+# renderPDF - draws Drawings onto a canvas
+"""Usage:
+    import renderpdf
+    renderpdf.draw(drawing, canvas, x, y)
+Execute the script to see some test drawings.
+changed
+"""
+
+
+from reportlab.graphics.shapes import *
+from reportlab.pdfgen.canvas import Canvas
+from reportlab.pdfbase.pdfmetrics import stringWidth
+
+
+# the main entry point for users...
+def draw(drawing, canvas, x, y):
+    """As it says"""
+    R = _PDFRenderer()
+    R.draw(drawing, canvas, x, y)
+
+from renderbase import Renderer, StateTracker, getStateDelta
+
+    
+class _PDFRenderer(Renderer):
+    """This draws onto a PDF document.  It needs to be a class
+    rather than a function, as some PDF-specific state tracking is
+    needed outside of the state info in the SVG model."""
+
+    def __init__(self):
+        self._stroke = 0
+        self._fill = 0
+        self._tracker = StateTracker()
+
+    def draw(self, drawing, canvas, x, y):
+        """This is the top level function, which
+        draws the drawing at the given location.
+        The recursive part is handled by drawNode."""
+        #stash references for the other objects to draw on
+        self._canvas = canvas
+        self._drawing = drawing
+        try:
+            #bounding box
+            canvas.rect(x, y, drawing.width, drawing.height)
+
+            #set up coords:
+            canvas.saveState()
+            canvas.translate(x, y)
+            #canvas.scale(1, -1)
+
+            # do this gently - no one-liners!
+            deltas = STATE_DEFAULTS.copy()
+            self._tracker.push(deltas)
+            self.applyStateChanges(deltas, {})
+
+            for node in drawing.contents:
+                # it might be a user node, if so decompose it
+                # into a Shape
+                if isinstance(node, UserNode):
+                    node = node.provideNode()
+
+                self.drawNode(node)
+
+            self._tracker.pop()
+            canvas.restoreState()
+        finally:
+            #remove any circular references
+            del self._canvas, self._drawing
+
+    def drawNode(self, node):
+        """This is the recursive method called for each node
+        in the tree"""
+        #print "pdf:drawNode", self
+        #if node.__class__ is Wedge: stop
+        self._canvas.saveState()
+
+        #apply state changes
+        deltas = getStateDelta(node)
+        self._tracker.push(deltas)
+        self.applyStateChanges(deltas, {})
+
+        #draw the object, or recurse
+        self.drawNodeDispatcher(node)
+
+        self._tracker.pop()
+        self._canvas.restoreState()
+
+    def drawRect(self, rect):
+        if rect.rx == rect.ry == 0:
+            #plain old rectangle
+            self._canvas.rect(
+                    rect.x, rect.y,
+                    rect.width, rect.height,
+                    stroke=self._stroke,
+                    fill=self._fill
+                    )
+        else:
+            #cheat and assume ry = rx; better to generalize
+            #pdfgen roundRect function.  TODO
+            self._canvas.roundRect(
+                    rect.x, rect.y,
+                    rect.width, rect.height, rect.rx,
+                    fill=self._fill,
+                    stroke=self._stroke
+                    )
+
+    def drawLine(self, line):
+        if self._stroke:
+            self._canvas.line(line.x1, line.y1, line.x2, line.y2)
+
+    def drawCircle(self, circle):
+            self._canvas.circle(
+                    circle.cx, circle.cy, circle.r,
+                    fill=self._fill,
+                    stroke=self._stroke
+                    )
+
+    def drawPolyLine(self, polyline):
+        if self._stroke:
+            assert len(polyline.points) >= 2, 'Polyline must have 2 or more points'
+            head, tail = polyline.points[0:2], polyline.points[2:],
+            path = self._canvas.beginPath()
+            path.moveTo(head[0], head[1])
+            for i in range(0, len(tail), 2):
+                path.lineTo(tail[i], tail[i+1])
+            self._canvas.drawPath(path)
+
+    def drawWedge(self, wedge):
+        centerx, centery, radius, startangledegrees, endangledegrees = \
+         wedge.centerx, wedge.centery, wedge.radius, wedge.startangledegrees, wedge.endangledegrees
+        yradius = wedge.yradius
+        path = self._canvas.beginPath()
+        path.moveTo(centerx, centery)
+        angle = endangledegrees-startangledegrees
+        path.arcTo(centerx-radius, centery-yradius, centerx+radius, centery+yradius,
+                   startangledegrees, angle)
+        path.close()
+        self._canvas.drawPath(path, 
+                    fill=self._fill,
+                    stroke=self._stroke)
+
+    def drawEllipse(self, ellipse):
+        #need to convert to pdfgen's bounding box representation
+        x1 = ellipse.cx - ellipse.rx
+        x2 = ellipse.cx + ellipse.rx
+        y1 = ellipse.cy - ellipse.ry
+        y2 = ellipse.cy + ellipse.ry
+        self._canvas.ellipse(x1,y1,x2,y2,fill=1)
+
+    def drawPolygon(self, polygon):
+        assert len(polygon.points) >= 2, 'Polyline must have 2 or more points'
+        head, tail = polygon.points[0:2], polygon.points[2:],
+        path = self._canvas.beginPath()
+        path.moveTo(head[0], head[1])
+        for i in range(0, len(tail), 2):
+            path.lineTo(tail[i], tail[i+1])
+        path.close()
+        self._canvas.drawPath(
+                            path,
+                            stroke=self._stroke,
+                            fill=self._fill
+                            )
+
+    def drawString(self, stringObj):
+        if self._fill:
+            S = self._tracker.getState()
+            text_anchor, x, y, text = S['textAnchor'], stringObj.x,stringObj.y,stringObj.text
+            if not text_anchor in ['start','inherited']:
+                font, font_size = S['fontName'], S['fontSize'] 
+                textLen = stringWidth(text, font,font_size)
+                if text_anchor=='end':
+                    x = x-textLen
+                elif text_anchor=='middle':
+                    x = x - textLen/2
+                else:
+                    raise ValueError, 'bad value for textAnchor '+str(textAnchor)
+            self._canvas.addLiteral('BT 1 0 0 1 %0.2f %0.2f Tm (%s) Tj ET' % (x, y, self._canvas._escape(text)))
+
+    #def drawPath(self, path):
+    
+    def applyStateChanges(self, delta, newState):
+        """This takes a set of states, and outputs the PDF operators
+        needed to set those properties"""
+        for key, value in delta.items():
+            if key == 'transform':
+                self._canvas.transform(value[0], value[1], value[2],
+                                 value[3], value[4], value[5])
+            elif key == 'strokeColor':
+                #this has different semantics in PDF to SVG;
+                #we always have a color, and either do or do
+                #not apply it; in SVG one can have a 'None' color
+                if value is None:
+                    self._stroke = 0
+                else:
+                    self._stroke = 1
+                    self._canvas.setStrokeColor(value)
+            elif key == 'strokeWidth':
+                self._canvas.setLineWidth(value)
+            elif key == 'strokeLineCap':  #0,1,2
+                self._canvas.setLineCap(value)
+            elif key == 'strokeLineJoin':
+                self._canvas.setLineJoin(value)
+#            elif key == 'stroke_dasharray':
+#                self._canvas.setDash(array=value)
+            elif key == 'strokeDashArray':
+                if value:
+                    self._canvas.setDash(value)
+                else:
+                    self._canvas.setDash()
+            elif key == 'fillColor':
+                #this has different semantics in PDF to SVG;
+                #we always have a color, and either do or do
+                #not apply it; in SVG one can have a 'None' color
+                if value is None:
+                    self._fill = 0
+                else:
+                    self._fill = 1
+                    self._canvas.setFillColor(value)
+            elif key in ['fontSize', 'fontName']:
+                # both need setting together in PDF
+                # one or both might be in the deltas,
+                # so need to get whichever is missing
+                fontname = delta.get('font_family', self._canvas._fontname)
+                fontsize = delta.get('font_size', self._canvas._fontsize)
+                self._canvas.setFont(fontname, fontsize)
+
+from reportlab.platypus import Flowable
+
+class GraphicsFlowable(Flowable):
+    """Flowable wrapper around a Pingo drawing"""
+    def __init__(self, drawing):
+        self.drawing = drawing
+        self.width = self.drawing.width
+        self.height = self.drawing.height
+
+    def draw(self):
+        draw(self.drawing, self.canv, 0, 0)
+
+def drawToFile(d,fn,msg):
+    c = Canvas(fn)
+    c.setFont('Times-Roman', 36)
+    c.drawString(80, 750, msg)
+
+    #print in a loop, with their doc strings
+    c.setFont('Times-Roman', 12)
+    y = 740
+    i = 1
+    y = y - d.height
+    draw(d, c, 80, y)
+
+    c.save()
+
+#########################################################
+#
+#   test code.  First, defin a bunch of drawings.
+#   Routine to draw them comes at the end.
+#
+#########################################################
+
+
+def test():
+    c = Canvas('testdrawings.pdf')
+    c.setFont('Times-Roman', 36)
+    c.drawString(80, 750, 'Graphics Test')
+
+    # print all drawings and their doc strings from the test
+    # file
+
+    #grab all drawings from the test module
+    import testdrawings
+    drawings = []
+
+    for funcname in dir(testdrawings):
+        if funcname[0:10] == 'getDrawing':
+            drawing = eval('testdrawings.' + funcname + '()')  #execute it
+            docstring = eval('testdrawings.' + funcname + '.__doc__')
+            drawings.append((drawing, docstring))
+
+    #print in a loop, with their doc strings
+    c.setFont('Times-Roman', 12)
+    y = 740
+    i = 1
+    for (drawing, docstring) in drawings:
+        assert (docstring is not None), "Drawing %d has no docstring!" % i
+        if y < 300:  #allows 5-6 lines of text
+            c.showPage()
+            y = 740
+        # draw a title
+        y = y - 30
+        c.setFont('Times-BoldItalic',12)
+        c.drawString(80, y, 'Drawing %d' % i)
+        c.setFont('Times-Roman',12)
+        y = y - 14
+        textObj = c.beginText(80, y)
+        textObj.textLines(docstring)
+        c.drawText(textObj)
+        y = textObj.getY()
+        y = y - drawing.height
+        draw(drawing, c, 80, y)
+        i = i + 1
+
+    c.save()
+    print 'saved testdrawings.pdf'
+
+def testFlowable():
+    """Makes a platypus document"""
+    from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
+    from reportlab.lib.styles import getSampleStyleSheet
+    styles = getSampleStyleSheet()
+    styNormal = styles['Normal']
+
+    doc = SimpleDocTemplate('test_flowable.pdf')
+    story = []
+    story.append(Paragraph("This sees is a drawing can work as a flowable", styNormal))
+    
+    import testdrawings
+    drawings = []
+
+    for funcname in dir(testdrawings):
+        if funcname[0:10] == 'getDrawing':
+            drawing = eval('testdrawings.' + funcname + '()')  #execute it
+            docstring = eval('testdrawings.' + funcname + '.__doc__')
+            story.append(Paragraph(docstring, styNormal))
+            story.append(Spacer(18,18))
+            story.append(drawing)
+            story.append(Spacer(36,36))
+
+    doc.build(story)
+    print 'saves test_flowable.pdf'
+
+if __name__=='__main__':
+    #test()
+    testFlowable()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/reportlab/graphics/renderbase.py	Mon Jan 22 22:05:38 2001 +0000
@@ -0,0 +1,238 @@
+###############################################################################
+#   $Log $
+#   
+#   
+"""
+Superclass for renderers to factor out common functionality and default implementations.
+"""
+
+
+__version__=''' $Id $ '''
+
+from reportlab.graphics.shapes import *
+
+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 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 getStateDelta(shape):
+    """Used to compute when we need to change the graphics state.
+    For example, if we have two adjacent red shapes we don't need
+    to set the pen color to red in between. Returns the effect
+    the given shape would have on the graphics state"""
+    delta = {}
+    for (prop, value) in shape.getProperties().items():
+        if STATE_DEFAULTS.has_key(prop):
+            delta[prop] = value
+    return delta
+
+
+class StateTracker:
+    """Keeps a stack of transforms and state
+    properties.  It can contain any properties you
+    want, but the keys 'transform' and 'ctm' have
+    special meanings.  The getCTM()
+    method returns the current transformation
+    matrix at any point, without needing to
+    invert matrixes when you pop."""
+    def __init__(self, defaults=None):
+        # one stack to keep track of what changes...
+        self.__deltas = []
+
+        # and another to keep track of cumulative effects.  Last one in
+        # list is the current graphics state.  We put one in to simplify
+        # loops below.
+        self.__combined = []
+        if defaults is None:
+            defaults = STATE_DEFAULTS.copy()
+        self.__combined.append(defaults)
+
+    def push(self,delta):
+        """Take a new state dictionary of changes and push it onto
+        the stack.  After doing this, the combined state is accessible
+        through getState()"""
+
+        newstate = self.__combined[-1].copy()
+        for (key, value) in delta.items():
+            if key == 'transform':  #do cumulative matrix
+                newstate['transform'] = delta['transform']
+                newstate['ctm'] = mmult(self.__combined[-1]['transform'], delta['transform'])
+            else:  #just overwrite it
+                newstate[key] = value
+
+        self.__combined.append(newstate)
+        self.__deltas.append(delta)
+
+    def pop(self):
+        """steps back one, and returns a state dictionary with the
+        deltas to reverse out of wherever you are.  Depending
+        on your back endm, you may not need the return value,
+        since you can get the complete state afterwards with getState()"""
+        del self.__combined[-1]
+        newState = self.__combined[-1]
+        lastDelta = self.__deltas[-1]
+        del  self.__deltas[-1]
+        #need to diff this against the last one in the state
+        reverseDelta = {}
+        for key, curValue in lastDelta.items():
+            prevValue = newState[key]
+            if prevValue <> curValue:
+                if key == 'transform':
+                    reverseDelta[key] = inverse(lastDelta['transform'])
+                else:  #just return to previous state
+                    reverseDelta[key] = prevValue
+        return reverseDelta
+
+    def getState(self):
+        "returns the complete graphics state at this point"
+        return self.__combined[-1]
+
+    def getCTM(self):
+        "returns the current transformation matrix at this point"""
+        return self.__combined[-1]['ctm']
+
+
+def testStateTracker():
+    print 'Testing state tracker'
+    defaults = {'fillColor':None, 'strokeColor':None,'fontName':None, 'transform':[1,0,0,1,0,0]}
+    deltas = [
+        {'fillColor':'red'},
+        {'fillColor':'green', 'strokeColor':'blue','fontName':'Times-Roman'},
+        {'transform':[0.5,0,0,0.5,0,0]},
+        {'transform':[0.5,0,0,0.5,2,3]},
+        {'strokeColor':'red'}
+        ]
+
+    st = StateTracker(defaults)
+    print 'initial:', st.getState()
+    print
+    for delta in deltas:
+        print 'pushing:', delta
+        st.push(delta)
+        print 'state:  ',st.getState(),'\n'
+
+    for delta in deltas:
+        print 'popping:',st.pop()
+        print 'state:  ',st.getState(),'\n'
+
+
+class Renderer:
+    """Virtual superclass for Pingo renderers."""
+
+    def __init__(self):
+        self._tracker = StateTracker()
+        
+    def undefined(self, operation):
+        raise ValueError, "%s operation not defined at superclass class=%s" %(operation, self.__class__)
+
+    def draw(self, drawing, canvas, x, y):
+        """This is the top level function, which
+        draws the drawing at the given location.
+        The recursive part is handled by drawNode."""
+        self.undefined("draw")
+
+    def drawNode(self, node):
+        """This is the recursive method called for each node
+        in the tree"""
+        # Undefined here, but with closer analysis probably can be handled in superclass
+        self.undefined("drawNode")
+        
+    def drawNodeDispatcher(self, node):
+        """dispatch on the node's (super) class: shared code"""
+        #print "drawNodeDispatcher", self, node.__class__
+
+        # replace UserNode with its contents
+        if isinstance(node, UserNode):
+            node = node.provideNode()
+
+        #draw the object, or recurse
+
+        if isinstance(node, Line):
+            self.drawLine(node)
+        elif isinstance(node, Rect):
+            self.drawRect(node)
+        elif isinstance(node, Circle):
+            self.drawCircle(node)
+        elif isinstance(node, Ellipse):
+            self.drawEllipse(node)
+        elif isinstance(node, PolyLine):
+            self.drawPolyLine(node)
+        elif isinstance(node, Polygon):
+            self.drawPolygon(node)
+        elif isinstance(node, Path):
+            self.drawPath(node)
+        elif isinstance(node, String):
+            self.drawString(node)
+        elif isinstance(node, Group):
+            for childNode in node.contents:
+                self.drawNode(childNode)
+        elif isinstance(node, Wedge):
+            #print "drawWedge"
+            self.drawWedge(node)
+        else:
+            print 'DrawingError','Unexpected element %s in pingo drawing!' % str(node)
+        #print "done dispatching"
+            
+    _restores = {'stroke':'_stroke','stroke_width': '_lineWidth','stroke_linecap':'_lineCap',
+                'stroke_linejoin':'_lineJoin','fill':'_fill','font_family':'_font',
+                'font_size':'_fontSize'}
+                
+    def drawWedge(self, wedge):
+        # by default ask the wedge to make a polygon of itself and draw that!
+        #print "drawWedge"
+        polygon = wedge.asPolygon()
+        self.drawPolygon(polygon)
+        
+    def drawPath(self, path):
+        polygons = path.asPolygons()
+        for polygon in polygons:
+                self.drawPolygon(polygon)
+
+    def drawRect(self, rect):
+        # could be implemented in terms of polygon
+        self.undefined("drawRect")
+        
+    def drawLine(self, line):
+        self.undefined("drawLine")
+
+    def drawCircle(self, circle):
+        self.undefined("drawCircle")
+
+    def drawPolyLine(self, p):
+        self.undefined("drawPolyLine")
+
+    def drawEllipse(self, ellipse):
+        self.undefined("drawEllipse")
+
+    def drawPolygon(self, p):
+        self.undefined("drawPolygon")
+
+    def drawString(self, stringObj):
+        self.undefined("drawString")
+
+    def applyStateChanges(self, delta, newState):
+        """This takes a set of states, and outputs the operators
+        needed to set those properties"""
+        self.undefined("applyStateChanges")
+
+if __name__=='__main__':
+    print "this file has no script interpretation"
+    print __doc__
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/reportlab/graphics/shapes.py	Mon Jan 22 22:05:38 2001 +0000
@@ -0,0 +1,593 @@
+# core of the graphics library - defines Drawing and Shapes
+"""
+"""
+
+from types import FloatType, IntType, ListType, TupleType, StringType
+from pprint import pprint
+from reportlab.platypus import Flowable
+import string
+
+from reportlab.config import shapeChecking
+
+from reportlab.lib import colors
+
+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 shaapes.  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': 'Times-Roman',
+    'textAnchor':  'start' # can be start, middle, end, inherited
+    }
+#####verifying functions, can be used in a verifyMap####
+def isBoolean(x):
+    return (x in (0, 1))
+def isString(x):
+    return (type(x) == StringType)
+            
+def isNumber(x):
+    """Don't think we really want complex numbers for widths!"""
+    return (type(x) in (FloatType, IntType))
+
+def isNumberOrNone(x):
+    """Don't think we really want complex numbers for widths!"""
+    if x is None:
+        return 1
+    else:
+        return (type(x) in (FloatType, IntType))
+
+def isListOfNumbers(x):
+    """Don't think we really want complex numbers for widths!"""
+    if type(x) in (ListType, TupleType):
+        for element in x:
+            if not isNumber(element):
+                return 0
+        return 1
+    else:
+        return 0
+
+def isListOfShapes(x):
+    if type(x) in (ListType, TupleType):
+        answer = 1
+        for element in x:
+            if not isinstance(x, Shape):
+                answer = 0
+        return answer
+    else:
+        return 0
+
+def isTransform(x):
+    if type(x) in (ListType, TupleType):
+        if len(x) == 6:
+            for element in x:
+                if not isNumber(element):
+                    return 0
+            return 1
+        else:
+            return 0
+    else:
+        return 0
+    
+
+def isColor(x):
+    return isinstance(x, colors.Color)
+
+def isColorOrNone(x):
+    if x is None:
+        return 1
+    else:
+        return isinstance(x, colors.Color)
+
+def isValidChild(x):
+    """Is it allowed in a drawing or group?  i.e.
+    descends from Shape or UserNode"""
+    return isinstance(x, UserNode) or isinstance(x, Shape)
+
+
+   
+class Shape:
+    """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 = None
+    def __init__(self, keywords={}):
+        """In general properties may be supplied to the
+        constructor."""
+        for key, value in keywords.items():
+            #print 'setting keyword %s.%s = %s' % (self, key, value)
+            setattr(self, key, value)
+
+       
+    def getProperties(self):
+        """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, checkerFunc) in self._attrMap.items():
+                assert hasattr(self, attr), "Missing attribute %s from %s" % (attr, self)
+                if checkerFunc:
+                    value = getattr(self, attr)
+                    assert checkerFunc(value), "Invalid value %s for attribute %s in class %s" % (value, attr, self.__class__.__name__)
+    if shapeChecking:
+        """This adds the ability to check every attribite assignment as it is made.
+        It slows down shapes but is a big help when developing. It does not
+        get defined if config.shapeChecking = 0"""
+        def __setattr__(self, attr, value):
+            """By default we verify.  This could be off
+            in some parallel base classes."""
+            if self._attrMap is not None:
+                if attr[0:1] <> '_':
+                    try:
+                        checker = self._attrMap[attr]
+                        if checker:
+                            if not checker(value):
+                                raise AttributeError, "Illegal assignment of '%s' to '%s' in class %s" % (value, attr, self.__class__.__name__)
+                    except KeyError:
+                        raise AttributeError, "Illegal attribute '%s' in class %s" % (attr, self.__class__.__name__)
+            #if we are still here, set it.
+            self.__dict__[attr] = value
+            #print 'set %s.%s = %s' % (self.__class__.__name__, attr, value)
+
+class Drawing(Shape, Flowable):
+    """Outermost container; the thing a renderer works on.
+    This has no properties except a height, width and list
+    of contents."""
+    _attrMap = {'width':isNumber, 'height':isNumber, 'contents':isListOfShapes, 'canv':None}
+    
+    def __init__(self, width, height, *nodes):
+        # Drawings need _attrMap to be an instance rather than
+        # a class attribute, as it may be extended at run time.
+        self._attrMap = self._attrMap.copy()
+            
+        self.width = width
+        self.height = height
+        self.contents = list(nodes)
+        
+
+    def add(self, node, name=None):
+        """Adds a shape to a drawing.  If a name is provided, it may
+        subsequently be accessed by name and becomes a regular
+        attribute of the drawing."""
+        assert isValidChild(node), "Can only add Shape or UserNode objects to a drawing"
+        self.contents.append(node)
+        if name:
+            #it better be valid or the checker will scream; and we need
+            #to make _attrMap an instance rather than a class attribite
+            #at this point too
+            self._attrMap[name] = isValidChild
+            setattr(self, name, node)
+            
+    def draw(self):
+        """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
+        R = renderPDF._PDFRenderer()
+        R.draw(self, self.canv, 0, 0)
+    
+
+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 = {'transform':isTransform, 'contents':isListOfShapes}
+    
+    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.copy()
+        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
+        Shape.__init__(self, keywords)
+        
+
+    def add(self, node, name=None):
+        """Appends child node to the 'contents' attribute.  In addition,
+        if a name is provided, it is subsequently accessible by name"""
+        # propagates properties down
+        assert isValidChild(node), "Can only add Shape or UserNode objects to a Group"
+        self.contents.append(node)
+        if name:
+            #it better be valid or the checker will scream; and we need
+            #to make _attrMap an instance rather than a class attribite
+            #at this point too
+            self._attrMap[name] = isValidChild
+            setattr(self, name, node)
+        
+
+    def rotate(self, theta):
+        """Convenience to help you set transforms"""
+        raise NotImplementedError, "Finish me off please!"
+
+    def translate(self, dx, dy):
+        """Convenience to help you set transforms"""
+        raise NotImplementedError, "Finish me off please!"
+    
+    def scale(self, sx, sy):
+        """Convenience to help you set transforms"""
+        raise NotImplementedError, "Finish me off please!"
+
+
+    def skew(self, kx, ky):
+        """Convenience to help you set transforms"""
+        raise NotImplementedError, "Finish me off please!"
+
+
+
+class LineShape(Shape):
+    # base for types of lines
+    _attrMap = {
+        'strokeColor':isColorOrNone,
+        'strokeWidth':isNumber,
+        'strokeLineCap':None,
+        'strokeLineJoin':None,
+        'strokeMiterLimit':isNumber,
+        'strokeDashArray':None,
+        }
+    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 = {
+        'strokeColor':isColorOrNone,
+        'strokeWidth':isNumber,
+        'strokeLineCap':None,
+        'strokeLineJoin':None,
+        'strokeMiterLimit':isNumber,
+        'strokeDashArray':None,
+        'x1':isNumber,
+        'y1':isNumber,
+        'x2':isNumber,
+        'y2':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
+
+    
+class SolidShape(Shape):
+    # base for anything with outline and content
+    _attrMap = {
+        'strokeColor':isColorOrNone,
+        'strokeWidth':isNumber,
+        'strokeLineCap':None,
+        'strokeLineJoin':None,
+        'strokeMiterLimit':isNumber,
+        'strokeDashArray':None,
+        'fillColor':isColorOrNone
+        }
+    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.fillColor = STATE_DEFAULTS['fillColor']
+        # do this at the end so keywords overwrite
+        #the above settings
+        Shape.__init__(self, kw)
+        
+class Path(SolidShape):
+    # same as current implementation; to do
+    pass
+  
+class Rect(SolidShape):
+    """Rectangle, possibly with rounded corners."""    
+    _attrMap = {
+        'strokeColor': isColorOrNone,
+        'strokeWidth': isNumber,
+        'strokeLineCap': None,   #TODO - define the types expected and add a checker function
+        'strokeLineJoin': None,  #TODO - define the types expected and add a checker function
+        'strokeMiterLimit': None, #TODO - define the types expected and add a checker function
+        'strokeDashArray': None, #TODO - define the types expected and add a checker function
+        'fillColor': isColorOrNone,
+        'x': isNumber,
+        'y': isNumber,
+        'width': isNumber,
+        'height': isNumber,
+        'rx': isNumber,
+        'ry': 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    
+
+class Circle(SolidShape):
+    _attrMap = {
+        'strokeColor': None,
+        'strokeWidth': isNumber,
+        'strokeLineCap': None,
+        'strokeLineJoin': None,
+        'strokeMiterLimit': None,
+        'strokeDashArray': None,
+        'fillColor': None,
+        'cx': isNumber,
+        'cy': isNumber,
+        'r': isNumber
+        }
+    
+    def __init__(self, cx, cy, r, **kw):
+        SolidShape.__init__(self, kw)
+        self.cx = cx
+        self.cy = cy
+        self.r = r
+
+class Ellipse(SolidShape):
+    _attrMap = {
+        'strokeColor': None,
+        'strokeWidth': isNumber,
+        'strokeLineCap': None,
+        'strokeLineJoin': None,
+        'strokeMiterLimit': None,
+        'strokeDashArray': None,
+        'fillColor': None,
+        'cx': isNumber,
+        'cy': isNumber,
+        'rx': isNumber,
+        'ry': 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
+
+class Wedge(SolidShape):
+    """A "slice of a pie" by default translates to a polygon moves anticlockwise
+       from start angle to end angle"""
+    _attrMap = {
+        'strokeColor': None,
+        'strokeWidth': isNumber,
+        'strokeLineCap': None,
+        'strokeLineJoin': None,
+        'strokeMiterLimit': None,
+        'strokeDashArray': None,
+        'fillColor': None,
+        'centerx': isNumber,
+        'centery': isNumber,
+        'radius': isNumber,
+        'startangledegrees': isNumber,
+        'endangledegrees': isNumber,
+        'yradius':isNumberOrNone
+        }
+    degreedelta = 1 # jump every 1 degrees
+    def __init__(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, **kw):
+        if yradius is None: yradius = radius
+        SolidShape.__init__(self, kw)
+        while endangledegrees<startangledegrees:
+            endangledegrees = endangledegrees+360
+        #print "__init__"
+        self.centerx, self.centery, self.radius, self.startangledegrees, self.endangledegrees = \
+           centerx, centery, radius, startangledegrees, endangledegrees
+        self.yradius = yradius
+    #def __repr__(self):
+    #        return "Wedge"+repr((self.centerx, self.centery, self.radius, self.startangledegrees, self.endangledegrees ))
+    #__str__ = __repr__
+    def asPolygon(self):
+        #print "asPolygon"
+        centerx, centery, radius, startangledegrees, endangledegrees = \
+           self.centerx, self.centery, self.radius, self.startangledegrees, self.endangledegrees
+        yradius = self.yradius
+        degreedelta = self.degreedelta
+        points = []
+        a = points.append
+        a(centerx); a(centery)
+        from math import sin, cos, pi
+        degreestoradians = pi/180.0
+        radiansdelta = degreedelta*degreestoradians
+        startangle = startangledegrees*degreestoradians
+        endangle = endangledegrees*degreestoradians
+        while endangle<startangle:
+              #print "endangle", endangle
+              endangle = endangle+2*pi
+        angle = startangle
+        #print "start", startangle, "end", endangle
+        while angle<endangle:
+            #print angle
+            x = centerx + cos(angle)*radius
+            y = centery + sin(angle)*yradius
+            a(x); a(y)
+            angle = angle+radiansdelta
+        #print "done"
+        x = centerx + cos(endangle)*radius
+        y = centery + sin(endangle)*yradius
+        a(x); a(y)
+        return Polygon(points)
+
+class Polygon(SolidShape):
+    """Defines a closed shape; Is implicitly
+    joined back to the start for you."""
+    _attrMap = {
+        'strokeColor': None,
+        'strokeWidth': isNumber,
+        'strokeLineCap': None,
+        'strokeLineJoin': None,
+        'strokeMiterLimit': None,
+        'strokeDashArray': None,
+        'fillColor': None,
+        'points': 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
+
+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 = {
+        'strokeColor':isColorOrNone,
+        'strokeWidth':isNumber,
+        'strokeLineCap':None,
+        'strokeLineJoin':None,
+        'strokeMiterLimit':isNumber,
+        'strokeDashArray':None,
+        'points':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
+
+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 = {
+        'x': isNumber,
+        'y': isNumber,
+        'text': isString,
+        'fontName':None,  #TODO - checker
+        'fontSize':isNumber,
+        'alignment':None #TODO - add checker
+        }
+    def __init__(self, x, y, text, **kw):
+        self.x = x
+        self.y = y
+        self.text = text
+        self.alignment = 'left'
+        self.fontName = STATE_DEFAULTS['fontName']
+        self.fontSize = STATE_DEFAULTS['fontSize']
+        self.setProperties(kw)
+
+class UserNode:
+        """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()
+    
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/reportlab/graphics/testdrawings.py	Mon Jan 22 22:05:38 2001 +0000
@@ -0,0 +1,275 @@
+#!/bin/env python
+###############################################################################
+#	$Log $
+#	
+__version__=''' $Id $ '''
+"""This contains a number of routines to generate test drawings
+for reportlab/graphics.  For now they are contrived, but we will expand them
+to try and trip up any parser. Feel free to add more.
+
+"""
+
+from reportlab.graphics.shapes import *
+from reportlab.lib import colors
+
+def getDrawing1():
+    """Hello World, on a rectangular background"""
+    
+    D = Drawing(400, 200)
+    D.add(Rect(50, 50, 300, 100, fillColor=colors.yellow))  #round corners    
+    D.add(String(180,100, 'Hello World', fillColor=colors.red))
+
+    
+    return D
+
+
+def getDrawing2():
+    """This demonstrates the basic shapes.  There are
+    no groups or references.  Each solid shape should have
+    a purple fill."""
+    D = Drawing(400, 200) #, fillColor=colors.purple)
+    
+    D.add(Line(10,10,390,190))
+    D.add(Circle(100,100,20, fillColor=colors.purple))
+    D.add(Circle(200,100,20, fillColor=colors.purple))
+    D.add(Circle(300,100,20, fillColor=colors.purple))
+
+    D.add(Wedge(330,100,40, -10,40, fillColor=colors.purple))
+
+    D.add(PolyLine([120,10,130,20,140,10,150,20,160,10,
+                    170,20,180,10,190,20,200,10]))    
+
+    D.add(Polygon([300,20,350,20,390,80,300,75, 330, 40]))
+
+    D.add(Ellipse(50, 150, 40, 20))
+
+    D.add(Rect(120, 150, 60, 30,
+               strokeWidth=10,
+               strokeColor=colors.red,
+               fillColor=colors.yellow))  #square corners
+    
+    D.add(Rect(220, 150, 60, 30, 10, 10))  #round corners    
+
+    D.add(String(10,50, 'Basic Shapes', fillColor=colors.black))
+
+    return D
+
+
+##def getDrawing2():
+##    """This drawing uses groups. Each group has two circles and a comment.
+##    The line style is set at group level and should be red for the left,
+##    bvlue for the right."""
+##    D = Drawing(400, 200)
+##
+##    Group1 = Group()
+##
+##    Group1.add(String(50, 50, 'Group 1', fillColor=colors.black))
+##    Group1.add(Circle(75,100,25))
+##    Group1.add(Circle(125,100,25))
+##    D.add(Group1)
+##
+##    Group2 = Group(
+##        String(250, 50, 'Group 2', fillColor=colors.black),
+##        Circle(275,100,25),
+##        Circle(325,100,25)#,
+##
+##        #group attributes
+##        #strokeColor=colors.blue
+##        )        
+##    D.add(Group2)
+
+##    return D
+##
+##
+##def getDrawing3():
+##    """This uses a named reference object.  The house is a 'subroutine'
+##    the basic brick colored walls are defined, but the roof and window
+##    color are undefined and may be set by the container."""
+##    
+##    D = Drawing(400, 200, fill=colors.bisque)
+##
+##    
+##    House = Group(
+##        Rect(2,20,36,30, fill=colors.bisque),  #walls
+##        Polygon([0,20,40,20,20,5]), #roof
+##        Rect(8, 38, 8, 12), #door
+##        Rect(25, 38, 8, 7), #window
+##        Rect(8, 25, 8, 7), #window
+##        Rect(25, 25, 8, 7) #window
+##        
+##        )        
+##    D.addDef('MyHouse', House)
+##
+##    # one row all the same color
+##    D.add(String(20, 40, 'British Street...',fill=colors.black))
+##    for i in range(6):
+##        x = i * 50
+##        D.add(NamedReference('MyHouse',
+##                             House,
+##                             transform=translate(x, 40),
+##                             fill = colors.brown
+##                             )
+##              )
+##
+##    # now do a row all different
+##    D.add(String(20, 120, 'Mediterranean Street...',fill=colors.black))
+##    x = 0
+##    for color in (colors.blue, colors.yellow, colors.orange,
+##                       colors.red, colors.green, colors.chartreuse):
+##        D.add(NamedReference('MyHouse',
+##                             House,
+##                             transform=translate(x,120),
+##                             fill = color,
+##                             )
+##              )
+##        x = x + 50
+##    #..by popular demand, the mayor gets a big one at the end
+##    D.add(NamedReference('MyHouse',
+##                             House,
+##                             transform=mmult(translate(x,110), scale(1.2,1.2)),
+##                             fill = color,
+##                             )
+##              )
+##        
+##        
+##    return D
+##
+##def getDrawing4():
+##    """This tests that attributes are 'unset' correctly when
+##    one steps back out of a drawing node. All the circles are part of a
+##    group setting the line color to blue; the second circle explicitly
+##    sets it to red.  Ideally, the third circle should go back to blue."""
+##    D = Drawing(400, 200)
+##
+##
+##    G = Group(
+##            Circle(100,100,20),
+##            Circle(200,100,20, stroke=colors.blue),
+##            Circle(300,100,20),
+##            stroke=colors.red,
+##            stroke_width=3,
+##            fill=colors.aqua
+##            )
+##    D.add(G)    
+##
+##    
+##    D.add(String(10,50, 'Stack Unwinding - should be red, blue, red'))
+##
+##    return D
+##
+##
+##def getDrawing5():
+##    """This Rotates Coordinate Axes"""
+##    D = Drawing(400, 200)
+##    
+##
+##
+##    Axis = Group(
+##        Line(0,0,100,0), #x axis
+##        Line(0,0,0,50),   # y axis
+##        Line(0,10,10,10), #ticks on y axis
+##        Line(0,20,10,20),
+##        Line(0,30,10,30),
+##        Line(0,40,10,40),
+##        Line(10,0,10,10), #ticks on x axis
+##        Line(20,0,20,10), 
+##        Line(30,0,30,10), 
+##        Line(40,0,40,10), 
+##        Line(50,0,50,10), 
+##        Line(60,0,60,10), 
+##        Line(70,0,70,10), 
+##        Line(80,0,80,10), 
+##        Line(90,0,90,10),
+##        String(20, 35, 'Axes', fill=colors.black)
+##        )
+##
+##    D.addDef('Axes', Axis)        
+##    
+##    D.add(NamedReference('Axis', Axis,
+##            transform=translate(10,10)))
+##    D.add(NamedReference('Axis', Axis,
+##            transform=mmult(translate(150,10),rotate(15)))
+##          )
+##    return D
+##
+##def getDrawing6():
+##    """This Rotates Text"""
+##    D = Drawing(400, 300, fill=colors.black)
+##
+##    xform = translate(200,150)
+##    C = (colors.black,colors.red,colors.green,colors.blue,colors.brown,colors.gray, colors.pink,
+##        colors.lavender,colors.lime, colors.mediumblue, colors.magenta, colors.limegreen)
+##
+##    for i in range(12):    
+##        D.add(String(0, 0, ' - - Rotated Text', fill=C[i%len(C)], transform=mmult(xform, rotate(30*i))))
+##    
+##    return D
+##
+##def getDrawing7():
+##    """This defines and tests a simple UserNode0 (the trailing zero denotes
+##    an experimental method which is not part of the supported API yet).
+##    Each of the four charts is a subclass of UserNode which generates a random
+##    series when rendered."""
+##
+##    class MyUserNode(UserNode0):
+##        import whrandom, math
+##        
+##
+##        def provideNode(self, sender):
+##            """draw a simple chart that changes everytime it's drawn"""
+##            # print "here's a random  number %s" % self.whrandom.random()
+##            #print "MyUserNode.provideNode being called by %s" % sender
+##            g = Group()
+##            #g._state = self._state  # this is naughty
+##            PingoNode.__init__(g, self._state)  # is this less naughty ?
+##            w = 80.0
+##            h = 50.0
+##            g.add(Rect(0,0, w, h, stroke=colors.black))
+##            N = 10.0
+##            x,y = (0,h)
+##            dx = w/N
+##            for ii in range(N):
+##                dy = (h/N) * self.whrandom.random()
+##                g.add(Line(x,y,x+dx, y-dy))
+##                x = x + dx
+##                y = y - dy
+##            return g
+##
+##    D = Drawing(400,200, fill=colors.white)  # AR - same size as others
+##    
+##    D.add(MyUserNode())
+##
+##    graphcolor= [colors.green, colors.red, colors.brown, colors.purple]
+##    for ii in range(4):
+##        D.add(Group( MyUserNode(stroke=graphcolor[ii], stroke_width=2),
+##                     transform=translate(ii*90,0) ))
+##
+##    #un = MyUserNode()
+##    #print un.provideNode()
+##    return D
+##
+##def getDrawing8():
+##    """Test Path operations--lineto, curveTo, etc."""
+##    D = Drawing(400, 200, fill=None, stroke=colors.purple, stroke_width=2)
+##
+##    xform = translate(200,100)
+##    C = (colors.black,colors.red,colors.green,colors.blue,colors.brown,colors.gray, colors.pink,
+##        colors.lavender,colors.lime, colors.mediumblue, colors.magenta, colors.limegreen)
+##    p = Path(50,50)
+##    p.lineTo(100,100)
+##    p.moveBy(-25,25)
+##    p.curveTo(150,125, 125,125, 200,50)
+##    p.curveTo(175, 75, 175, 98, 62, 87)
+##
+##
+##    D.add(p)
+##    D.add(String(10,30, 'Tests of path elements-lines and bezier curves-and text formating'))
+##    D.add(Line(220,150, 220,200, stroke=colors.red))
+##    D.add(String(220,180, "Text should be centered", text_anchor="middle") )
+##
+##    
+##    return D
+    
+
+if __name__=='__main__':
+    print __doc__
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/reportlab/graphics/widgetbase.py	Mon Jan 22 22:05:38 2001 +0000
@@ -0,0 +1,243 @@
+#widgets.py
+import string
+
+from reportlab.graphics import shapes
+from reportlab import config
+from reportlab.lib import colors
+
+class Widget(shapes.UserNode):
+    """Base for all user-defined widgets.  Keep as simple as possible. Does
+    not inherit from Shape so that we can rewrite shapes without breaking
+    widgets and vice versa."""
+    _attrMap = None
+
+    
+    def verify(self):
+        """If the _attrMap attribute is not None, this
+        checks all expected attributes are present; no
+        unwanted attributes are present; and (if a
+        checking function is found) checks each
+        attribute has a valid value.  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, checkerFunc) in self._attrMap.items():
+                assert hasattr(self, attr), "Missing attribute %s from %s" % (key, self)
+                if checkerFunc:
+                    value = getattr(self, attr)
+                    assert checkerFunc(value), "Invalid value %s for attribute %s in class %s" % (value, attr, self.__class__.__name__)
+                    
+    if config.shapeChecking:
+        """This adds the ability to check every attribite assignment as it is made.
+        It slows down shapes but is a big help when developing. It does not
+        get defined if config.shapeChecking = 0"""
+        def __setattr__(self, attr, value):
+            """By default we verify.  This could be off
+            in some parallel base classes."""
+            if self._attrMap is not None:
+                if attr[0:1] <> '_':
+                    try:
+                        checker = self._attrMap[attr]
+                        if checker:
+                            if not checker(value):
+                                raise AttributeError, "Illegal assignment of '%s' to '%s' in class %s" % (value, attr, self.__class__.__name__)
+                    except KeyError:
+                        raise AttributeError, "Illegal attribute '%s' in class %s" % (attr, self.__class__.__name__)
+            #if we are still here, set it.
+            self.__dict__[attr] = value
+            #print 'set %s.%s = %s' % (self.__class__.__name__, attr, value)
+
+    def draw(self):
+        raise shapes.NotImplementedError, "draw() must be implemented for each Widget!"
+    
+    def demo(self):
+        raise shapes.NotImplementedError, "demo() must be implemented for each Widget!"
+
+    def provideNode(self):
+        return self.draw()
+
+    def getProperties(self):
+        """Returns a list of all properties which can be edited and
+        which are not marked as private. This may include 'child
+        widgets' or 'primitive shapes'.  You are free to override
+        this and provide alternative implementations; the default
+        one simply returns everything without a leading underscore."""
+        # TODO when we need it, but not before -
+        # expose sequence contents?
+        props = {}
+        for name in self.__dict__.keys():
+            if name[0:1] <> '_':
+                component = getattr(self, name)
+                
+                if shapes.isValidChild(component):
+                    # child object, get its properties too
+                    childProps = component.getProperties()
+                    for (childKey, childValue) in childProps.items():
+                        props['%s.%s' % (name, childKey)] = childValue
+                else:
+                    props[name] = component
+               
+        return props
+
+    def setProperties(self, propDict):
+        """Permits bulk setting of properties.  These may include
+        child objects e.g. "chart.legend.width = 200".
+        
+        All assignments will be validated by the object as if they
+        were set individually in python code.
+
+        All properties of a top-level object are guaranteed to be
+        set before any of the children, which may be helpful to
+        widget designers."""
+        
+        childPropDicts = {}
+        for (name, value) in propDict.items():
+            parts = string.split(name, '.', 1)
+            if len(parts) == 1:
+                #simple attribute, set it now
+                setattr(self, name, value)
+            else:
+                (childName, remains) = parts
+                try:
+                    childPropDicts[childName][remains] = value
+                except KeyError:
+                    childPropDicts[childName] = {remains: value}
+        # now assign to children
+        for (childName, childPropDict) in childPropDicts.items():
+            child = getattr(self, childName)
+            child.setProperties(childPropDict)
+            
+            
+        
+    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)
+                    
+                
+                        
+
+class TwoCircles(Widget):
+    def __init__(self):
+        self.leftCircle = shapes.Circle(100,100,20, fillColor=colors.red)
+        self.rightCircle = shapes.Circle(300,100,20, fillColor=colors.red)
+
+    def draw(self):
+        return shapes.Group(self.leftCircle, self.rightCircle)
+
+class Face(Widget):
+    """This draws a face with two eyes.  It exposes a couple of properties
+    to configure itself and hides all other details"""
+    def checkMood(moodName):
+        return (moodName in ('happy','sad','ok'))
+    _attrMap = {
+        'x': shapes.isNumber,
+        'y': shapes.isNumber,
+        'size': shapes.isNumber,
+        'skinColor':shapes.isColorOrNone,
+        'eyeColor': shapes.isColorOrNone,
+        'mood': checkMood 
+        }
+
+        
+    def __init__(self):
+        self.x = 10
+        self.y = 10
+        self.size = 80
+        self.skinColor = None
+        self.eyeColor = colors.blue
+        self.mood = 'happy'
+
+    def demo(self):
+        pass
+    
+    def draw(self):
+        s = self.size  # abbreviate as we will use this a lot
+        g = shapes.Group()
+        g.transform = [1,0,0,1,self.x, self.y]
+        # background
+        g.add(shapes.Circle(s * 0.5, s * 0.5, s * 0.5, fillColor=self.skinColor))
+        
+
+        # left eye
+        g.add(shapes.Circle(s * 0.35, s * 0.65, s * 0.1, fillColor=colors.white))
+        g.add(shapes.Circle(s * 0.35, s * 0.65, s * 0.05, fillColor=self.eyeColor))
+        
+        # right eye
+        g.add(shapes.Circle(s * 0.65, s * 0.65, s * 0.1, fillColor=colors.white))
+        g.add(shapes.Circle(s * 0.65, s * 0.65, s * 0.05, fillColor=self.eyeColor))
+
+        # nose
+        g.add(shapes.Polygon(points=[s * 0.5, s * 0.6, s * 0.4, s * 0.3, s * 0.6, s * 0.3],
+                             fillColor=None))
+
+        # mouth
+        if self.mood == 'happy':
+            offset = -0.05
+        elif self.mood == 'sad':
+            offset = +0.05
+        else:
+            offset = 0
+            
+        g.add(shapes.Polygon(points = [
+                                s * 0.3, s * 0.2, #left of mouth
+                                s * 0.7, s * 0.2, #right of mouth
+                                s * 0.6, s * (0.2 + offset), # the bit going up or down
+                                s * 0.4, s * (0.2 + offset) # the bit going up or down
+                                
+                                ],
+                             fillColor = colors.pink,
+                             strokeColor = colors.red,
+                             strokeWidth = s * 0.03
+                             ))
+        
+        return g
+
+class TwoFaces(Widget):
+    def __init__(self):
+        self.faceOne = Face()
+        self.faceOne.mood = "happy"
+        self.faceTwo = Face()
+        self.faceTwo.x = 100
+        self.faceTwo.mood = "sad"
+        
+    def draw(self):
+        """Just return a group"""
+        return shapes.Group(self.faceOne, self.faceTwo)
+
+    def demo(self):
+        """The default case already looks good enough,
+        no implementation needed here"""
+        pass
+    
+def test():
+    d = shapes.Drawing(400, 200)
+    tc = TwoCircles()
+    d.add(tc)
+    import renderPDF
+    renderPDF.drawToFile(d, 'sample_widget.pdf', 'A Sample Widget')
+    print 'saved sample_widget.pdf'
+
+    d = shapes.Drawing(400, 200)
+    f = Face()
+    f.skinColor = colors.yellow
+    f.mood = "sad"
+    d.add(f)
+    renderPDF.drawToFile(d, 'face.pdf', 'A Sample Widget')
+    print 'saved face.pdf'
+
+    tf = TwoFaces()
+    
+
+if __name__=='__main__':
+    test()
+    
+    
\ No newline at end of file