src/reportlab/graphics/renderbase.py
changeset 2964 32352db0d71e
parent 2827 ec03c767079c
child 3032 22224b1b4d24
equal deleted inserted replaced
2963:c414c0ab69e7 2964:32352db0d71e
       
     1 #Copyright ReportLab Europe Ltd. 2000-2004
       
     2 #see license.txt for license details
       
     3 #history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/renderbase.py
       
     4 """
       
     5 Superclass for renderers to factor out common functionality and default implementations.
       
     6 """
       
     7 
       
     8 
       
     9 __version__=''' $Id $ '''
       
    10 
       
    11 from reportlab.graphics.shapes import *
       
    12 from reportlab.lib.validators import DerivedValue
       
    13 from reportlab import rl_config
       
    14 
       
    15 def inverse(A):
       
    16     "For A affine 2D represented as 6vec return 6vec version of A**(-1)"
       
    17     # I checked this RGB
       
    18     det = float(A[0]*A[3] - A[2]*A[1])
       
    19     R = [A[3]/det, -A[1]/det, -A[2]/det, A[0]/det]
       
    20     return tuple(R+[-R[0]*A[4]-R[2]*A[5],-R[1]*A[4]-R[3]*A[5]])
       
    21 
       
    22 def mmult(A, B):
       
    23     "A postmultiplied by B"
       
    24     # I checked this RGB
       
    25     # [a0 a2 a4]    [b0 b2 b4]
       
    26     # [a1 a3 a5] *  [b1 b3 b5]
       
    27     # [      1 ]    [      1 ]
       
    28     #
       
    29     return (A[0]*B[0] + A[2]*B[1],
       
    30             A[1]*B[0] + A[3]*B[1],
       
    31             A[0]*B[2] + A[2]*B[3],
       
    32             A[1]*B[2] + A[3]*B[3],
       
    33             A[0]*B[4] + A[2]*B[5] + A[4],
       
    34             A[1]*B[4] + A[3]*B[5] + A[5])
       
    35 
       
    36 
       
    37 def getStateDelta(shape):
       
    38     """Used to compute when we need to change the graphics state.
       
    39     For example, if we have two adjacent red shapes we don't need
       
    40     to set the pen color to red in between. Returns the effect
       
    41     the given shape would have on the graphics state"""
       
    42     delta = {}
       
    43     for (prop, value) in shape.getProperties().items():
       
    44         if STATE_DEFAULTS.has_key(prop):
       
    45             delta[prop] = value
       
    46     return delta
       
    47 
       
    48 
       
    49 class StateTracker:
       
    50     """Keeps a stack of transforms and state
       
    51     properties.  It can contain any properties you
       
    52     want, but the keys 'transform' and 'ctm' have
       
    53     special meanings.  The getCTM()
       
    54     method returns the current transformation
       
    55     matrix at any point, without needing to
       
    56     invert matrixes when you pop."""
       
    57     def __init__(self, defaults=None):
       
    58         # one stack to keep track of what changes...
       
    59         self._deltas = []
       
    60 
       
    61         # and another to keep track of cumulative effects.  Last one in
       
    62         # list is the current graphics state.  We put one in to simplify
       
    63         # loops below.
       
    64         self._combined = []
       
    65         if defaults is None:
       
    66             defaults = STATE_DEFAULTS.copy()
       
    67         #ensure  that if we have a transform, we have a CTM
       
    68         if defaults.has_key('transform'):
       
    69             defaults['ctm'] = defaults['transform']
       
    70         self._combined.append(defaults)
       
    71 
       
    72     def push(self,delta):
       
    73         """Take a new state dictionary of changes and push it onto
       
    74         the stack.  After doing this, the combined state is accessible
       
    75         through getState()"""
       
    76 
       
    77         newstate = self._combined[-1].copy()
       
    78         for (key, value) in delta.items():
       
    79             if key == 'transform':  #do cumulative matrix
       
    80                 newstate['transform'] = delta['transform']
       
    81                 newstate['ctm'] = mmult(self._combined[-1]['ctm'], delta['transform'])
       
    82                 #print 'statetracker transform = (%0.2f, %0.2f, %0.2f, %0.2f, %0.2f, %0.2f)' % tuple(newstate['transform'])
       
    83                 #print 'statetracker ctm = (%0.2f, %0.2f, %0.2f, %0.2f, %0.2f, %0.2f)' % tuple(newstate['ctm'])
       
    84 
       
    85             else:  #just overwrite it
       
    86                 newstate[key] = value
       
    87 
       
    88         self._combined.append(newstate)
       
    89         self._deltas.append(delta)
       
    90 
       
    91     def pop(self):
       
    92         """steps back one, and returns a state dictionary with the
       
    93         deltas to reverse out of wherever you are.  Depending
       
    94         on your back end, you may not need the return value,
       
    95         since you can get the complete state afterwards with getState()"""
       
    96         del self._combined[-1]
       
    97         newState = self._combined[-1]
       
    98         lastDelta = self._deltas[-1]
       
    99         del  self._deltas[-1]
       
   100         #need to diff this against the last one in the state
       
   101         reverseDelta = {}
       
   102         #print 'pop()...'
       
   103         for key, curValue in lastDelta.items():
       
   104             #print '   key=%s, value=%s' % (key, curValue)
       
   105             prevValue = newState[key]
       
   106             if prevValue <> curValue:
       
   107                 #print '    state popping "%s"="%s"' % (key, curValue)
       
   108                 if key == 'transform':
       
   109                     reverseDelta[key] = inverse(lastDelta['transform'])
       
   110                 else:  #just return to previous state
       
   111                     reverseDelta[key] = prevValue
       
   112         return reverseDelta
       
   113 
       
   114     def getState(self):
       
   115         "returns the complete graphics state at this point"
       
   116         return self._combined[-1]
       
   117 
       
   118     def getCTM(self):
       
   119         "returns the current transformation matrix at this point"""
       
   120         return self._combined[-1]['ctm']
       
   121 
       
   122     def __getitem__(self,key):
       
   123         "returns the complete graphics state value of key at this point"
       
   124         return self._combined[-1][key]
       
   125 
       
   126     def __setitem__(self,key,value):
       
   127         "sets the complete graphics state value of key to value"
       
   128         self._combined[-1][key] = value
       
   129 
       
   130 def testStateTracker():
       
   131     print 'Testing state tracker'
       
   132     defaults = {'fillColor':None, 'strokeColor':None,'fontName':None, 'transform':[1,0,0,1,0,0]}
       
   133     deltas = [
       
   134         {'fillColor':'red'},
       
   135         {'fillColor':'green', 'strokeColor':'blue','fontName':'Times-Roman'},
       
   136         {'transform':[0.5,0,0,0.5,0,0]},
       
   137         {'transform':[0.5,0,0,0.5,2,3]},
       
   138         {'strokeColor':'red'}
       
   139         ]
       
   140 
       
   141     st = StateTracker(defaults)
       
   142     print 'initial:', st.getState()
       
   143     print
       
   144     for delta in deltas:
       
   145         print 'pushing:', delta
       
   146         st.push(delta)
       
   147         print 'state:  ',st.getState(),'\n'
       
   148 
       
   149     for delta in deltas:
       
   150         print 'popping:',st.pop()
       
   151         print 'state:  ',st.getState(),'\n'
       
   152 
       
   153 
       
   154 def _expandUserNode(node,canvas):
       
   155     if isinstance(node, UserNode):
       
   156         try:
       
   157             if hasattr(node,'_canvas'):
       
   158                 ocanvas = 1
       
   159             else:
       
   160                 node._canvas = canvas
       
   161                 ocanvas = None
       
   162             onode = node
       
   163             node = node.provideNode()
       
   164         finally:
       
   165             if not ocanvas: del onode._canvas
       
   166     return node
       
   167 
       
   168 def renderScaledDrawing(d):
       
   169     renderScale = d.renderScale
       
   170     if renderScale!=1.0:
       
   171         o = d
       
   172         d = d.__class__(o.width*renderScale,o.height*renderScale)
       
   173         d.__dict__ = o.__dict__.copy()
       
   174         d.scale(renderScale,renderScale)
       
   175         d.renderScale = 1.0
       
   176     return d
       
   177 
       
   178 class Renderer:
       
   179     """Virtual superclass for graphics renderers."""
       
   180 
       
   181     def __init__(self):
       
   182         self._tracker = StateTracker()
       
   183         self._nodeStack = []   #track nodes visited
       
   184 
       
   185     def undefined(self, operation):
       
   186         raise ValueError, "%s operation not defined at superclass class=%s" %(operation, self.__class__)
       
   187 
       
   188     def draw(self, drawing, canvas, x=0, y=0, showBoundary=rl_config._unset_):
       
   189         """This is the top level function, which draws the drawing at the given
       
   190         location. The recursive part is handled by drawNode."""
       
   191         #stash references for ease of  communication
       
   192         if showBoundary is rl_config._unset_: showBoundary=rl_config.showBoundary
       
   193         self._canvas = canvas
       
   194         canvas.__dict__['_drawing'] = self._drawing = drawing
       
   195         drawing._parent = None
       
   196         try:
       
   197             #bounding box
       
   198             if showBoundary: canvas.rect(x, y, drawing.width, drawing.height)
       
   199             canvas.saveState()
       
   200             self.initState(x,y)  #this is the push()
       
   201             self.drawNode(drawing)
       
   202             self.pop()
       
   203             canvas.restoreState()
       
   204         finally:
       
   205             #remove any circular references
       
   206             del self._canvas, self._drawing, canvas._drawing, drawing._parent
       
   207 
       
   208     def initState(self,x,y):
       
   209         deltas = STATE_DEFAULTS.copy()
       
   210         deltas['transform'] = [1,0,0,1,x,y]
       
   211         self._tracker.push(deltas)
       
   212         self.applyStateChanges(deltas, {})
       
   213 
       
   214     def pop(self):
       
   215         self._tracker.pop()
       
   216 
       
   217     def drawNode(self, node):
       
   218         """This is the recursive method called for each node
       
   219         in the tree"""
       
   220         # Undefined here, but with closer analysis probably can be handled in superclass
       
   221         self.undefined("drawNode")
       
   222 
       
   223     def getStateValue(self, key):
       
   224         """Return current state parameter for given key"""
       
   225         currentState = self._tracker._combined[-1]
       
   226         return currentState[key]
       
   227     
       
   228     def fillDerivedValues(self, node):
       
   229         """Examine a node for any values which are Derived,
       
   230         and replace them with their calculated values.
       
   231         Generally things may look at the drawing or their
       
   232         parent.
       
   233         
       
   234         """
       
   235         for (key, value) in node.__dict__.items():
       
   236             if isinstance(value, DerivedValue):
       
   237                 #just replace with default for key?
       
   238                 #print '    fillDerivedValues(%s)' % key
       
   239                 newValue = value.getValue(self, key)
       
   240                 #print '   got value of %s' % newValue
       
   241                 node.__dict__[key] = newValue
       
   242 
       
   243     def drawNodeDispatcher(self, node):
       
   244         """dispatch on the node's (super) class: shared code"""
       
   245 
       
   246         canvas = getattr(self,'_canvas',None)
       
   247         # replace UserNode with its contents
       
   248 
       
   249         try:
       
   250             node = _expandUserNode(node,canvas)
       
   251             if hasattr(node,'_canvas'):
       
   252                 ocanvas = 1
       
   253             else:
       
   254                 node._canvas = canvas
       
   255                 ocanvas = None
       
   256 
       
   257             self.fillDerivedValues(node)
       
   258             #draw the object, or recurse
       
   259             if isinstance(node, Line):
       
   260                 self.drawLine(node)
       
   261             elif isinstance(node, Image):
       
   262                 self.drawImage(node)
       
   263             elif isinstance(node, Rect):
       
   264                 self.drawRect(node)
       
   265             elif isinstance(node, Circle):
       
   266                 self.drawCircle(node)
       
   267             elif isinstance(node, Ellipse):
       
   268                 self.drawEllipse(node)
       
   269             elif isinstance(node, PolyLine):
       
   270                 self.drawPolyLine(node)
       
   271             elif isinstance(node, Polygon):
       
   272                 self.drawPolygon(node)
       
   273             elif isinstance(node, Path):
       
   274                 self.drawPath(node)
       
   275             elif isinstance(node, String):
       
   276                 self.drawString(node)
       
   277             elif isinstance(node, Group):
       
   278                 self.drawGroup(node)
       
   279             elif isinstance(node, Wedge):
       
   280                 self.drawWedge(node)
       
   281             else:
       
   282                 print 'DrawingError','Unexpected element %s in drawing!' % str(node)
       
   283         finally:
       
   284             if not ocanvas: del node._canvas
       
   285 
       
   286     _restores = {'stroke':'_stroke','stroke_width': '_lineWidth','stroke_linecap':'_lineCap',
       
   287                 'stroke_linejoin':'_lineJoin','fill':'_fill','font_family':'_font',
       
   288                 'font_size':'_fontSize'}
       
   289 
       
   290     def drawGroup(self, group):
       
   291         # just do the contents.  Some renderers might need to override this
       
   292         # if they need a flipped transform
       
   293         canvas = getattr(self,'_canvas',None)
       
   294         for node in group.getContents():
       
   295             node = _expandUserNode(node,canvas)
       
   296 
       
   297             #here is where we do derived values - this seems to get everything. Touch wood.            
       
   298             self.fillDerivedValues(node)
       
   299             try:
       
   300                 if hasattr(node,'_canvas'):
       
   301                     ocanvas = 1
       
   302                 else:
       
   303                     node._canvas = canvas
       
   304                     ocanvas = None
       
   305                 node._parent = group
       
   306                 self.drawNode(node)
       
   307             finally:
       
   308                 del node._parent
       
   309                 if not ocanvas: del node._canvas
       
   310 
       
   311     def drawWedge(self, wedge):
       
   312         # by default ask the wedge to make a polygon of itself and draw that!
       
   313         #print "drawWedge"
       
   314         polygon = wedge.asPolygon()
       
   315         self.drawPolygon(polygon)
       
   316 
       
   317     def drawPath(self, path):
       
   318         polygons = path.asPolygons()
       
   319         for polygon in polygons:
       
   320                 self.drawPolygon(polygon)
       
   321 
       
   322     def drawRect(self, rect):
       
   323         # could be implemented in terms of polygon
       
   324         self.undefined("drawRect")
       
   325 
       
   326     def drawLine(self, line):
       
   327         self.undefined("drawLine")
       
   328 
       
   329     def drawCircle(self, circle):
       
   330         self.undefined("drawCircle")
       
   331 
       
   332     def drawPolyLine(self, p):
       
   333         self.undefined("drawPolyLine")
       
   334 
       
   335     def drawEllipse(self, ellipse):
       
   336         self.undefined("drawEllipse")
       
   337 
       
   338     def drawPolygon(self, p):
       
   339         self.undefined("drawPolygon")
       
   340 
       
   341     def drawString(self, stringObj):
       
   342         self.undefined("drawString")
       
   343 
       
   344     def applyStateChanges(self, delta, newState):
       
   345         """This takes a set of states, and outputs the operators
       
   346         needed to set those properties"""
       
   347         self.undefined("applyStateChanges")
       
   348 
       
   349 if __name__=='__main__':
       
   350     print "this file has no script interpretation"
       
   351     print __doc__