352 lines
12 KiB
Python
352 lines
12 KiB
Python
#Copyright ReportLab Europe Ltd. 2000-2004
|
|
#see license.txt for license details
|
|
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/renderbase.py
|
|
"""
|
|
Superclass for renderers to factor out common functionality and default implementations.
|
|
"""
|
|
|
|
|
|
__version__=''' $Id $ '''
|
|
|
|
from reportlab.graphics.shapes import *
|
|
from reportlab.lib.validators import DerivedValue
|
|
from reportlab import rl_config
|
|
|
|
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()
|
|
#ensure that if we have a transform, we have a CTM
|
|
if defaults.has_key('transform'):
|
|
defaults['ctm'] = defaults['transform']
|
|
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]['ctm'], delta['transform'])
|
|
#print 'statetracker transform = (%0.2f, %0.2f, %0.2f, %0.2f, %0.2f, %0.2f)' % tuple(newstate['transform'])
|
|
#print 'statetracker ctm = (%0.2f, %0.2f, %0.2f, %0.2f, %0.2f, %0.2f)' % tuple(newstate['ctm'])
|
|
|
|
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 end, 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 = {}
|
|
#print 'pop()...'
|
|
for key, curValue in lastDelta.items():
|
|
#print ' key=%s, value=%s' % (key, curValue)
|
|
prevValue = newState[key]
|
|
if prevValue <> curValue:
|
|
#print ' state popping "%s"="%s"' % (key, 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 __getitem__(self,key):
|
|
"returns the complete graphics state value of key at this point"
|
|
return self._combined[-1][key]
|
|
|
|
def __setitem__(self,key,value):
|
|
"sets the complete graphics state value of key to value"
|
|
self._combined[-1][key] = value
|
|
|
|
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'
|
|
|
|
|
|
def _expandUserNode(node,canvas):
|
|
if isinstance(node, UserNode):
|
|
try:
|
|
if hasattr(node,'_canvas'):
|
|
ocanvas = 1
|
|
else:
|
|
node._canvas = canvas
|
|
ocanvas = None
|
|
onode = node
|
|
node = node.provideNode()
|
|
finally:
|
|
if not ocanvas: del onode._canvas
|
|
return node
|
|
|
|
def renderScaledDrawing(d):
|
|
renderScale = d.renderScale
|
|
if renderScale!=1.0:
|
|
d = d.copy()
|
|
d.width *= renderScale
|
|
d.height *= renderScale
|
|
d.scale(renderScale,renderScale)
|
|
d.renderScale = 1.0
|
|
return d
|
|
|
|
class Renderer:
|
|
"""Virtual superclass for graphics renderers."""
|
|
|
|
def __init__(self):
|
|
self._tracker = StateTracker()
|
|
self._nodeStack = [] #track nodes visited
|
|
|
|
def undefined(self, operation):
|
|
raise ValueError, "%s operation not defined at superclass class=%s" %(operation, self.__class__)
|
|
|
|
def draw(self, drawing, canvas, x=0, y=0, showBoundary=rl_config._unset_):
|
|
"""This is the top level function, which draws the drawing at the given
|
|
location. The recursive part is handled by drawNode."""
|
|
#stash references for ease of communication
|
|
if showBoundary is rl_config._unset_: showBoundary=rl_config.showBoundary
|
|
self._canvas = canvas
|
|
canvas.__dict__['_drawing'] = self._drawing = drawing
|
|
drawing._parent = None
|
|
try:
|
|
#bounding box
|
|
if showBoundary: canvas.rect(x, y, drawing.width, drawing.height)
|
|
canvas.saveState()
|
|
self.initState(x,y) #this is the push()
|
|
self.drawNode(drawing)
|
|
self.pop()
|
|
canvas.restoreState()
|
|
finally:
|
|
#remove any circular references
|
|
del self._canvas, self._drawing, canvas._drawing, drawing._parent
|
|
|
|
def initState(self,x,y):
|
|
deltas = STATE_DEFAULTS.copy()
|
|
deltas['transform'] = [1,0,0,1,x,y]
|
|
self._tracker.push(deltas)
|
|
self.applyStateChanges(deltas, {})
|
|
|
|
def pop(self):
|
|
self._tracker.pop()
|
|
|
|
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 getStateValue(self, key):
|
|
"""Return current state parameter for given key"""
|
|
currentState = self._tracker._combined[-1]
|
|
return currentState[key]
|
|
|
|
def fillDerivedValues(self, node):
|
|
"""Examine a node for any values which are Derived,
|
|
and replace them with their calculated values.
|
|
Generally things may look at the drawing or their
|
|
parent.
|
|
|
|
"""
|
|
for (key, value) in node.__dict__.items():
|
|
if isinstance(value, DerivedValue):
|
|
#just replace with default for key?
|
|
#print ' fillDerivedValues(%s)' % key
|
|
newValue = value.getValue(self, key)
|
|
#print ' got value of %s' % newValue
|
|
node.__dict__[key] = newValue
|
|
|
|
def drawNodeDispatcher(self, node):
|
|
"""dispatch on the node's (super) class: shared code"""
|
|
|
|
canvas = getattr(self,'_canvas',None)
|
|
# replace UserNode with its contents
|
|
|
|
try:
|
|
node = _expandUserNode(node,canvas)
|
|
if hasattr(node,'_canvas'):
|
|
ocanvas = 1
|
|
else:
|
|
node._canvas = canvas
|
|
ocanvas = None
|
|
|
|
self.fillDerivedValues(node)
|
|
#draw the object, or recurse
|
|
if isinstance(node, Line):
|
|
self.drawLine(node)
|
|
elif isinstance(node, Image):
|
|
self.drawImage(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):
|
|
self.drawGroup(node)
|
|
elif isinstance(node, Wedge):
|
|
self.drawWedge(node)
|
|
else:
|
|
print 'DrawingError','Unexpected element %s in drawing!' % str(node)
|
|
finally:
|
|
if not ocanvas: del node._canvas
|
|
|
|
_restores = {'stroke':'_stroke','stroke_width': '_lineWidth','stroke_linecap':'_lineCap',
|
|
'stroke_linejoin':'_lineJoin','fill':'_fill','font_family':'_font',
|
|
'font_size':'_fontSize'}
|
|
|
|
def drawGroup(self, group):
|
|
# just do the contents. Some renderers might need to override this
|
|
# if they need a flipped transform
|
|
canvas = getattr(self,'_canvas',None)
|
|
for node in group.getContents():
|
|
node = _expandUserNode(node,canvas)
|
|
|
|
#here is where we do derived values - this seems to get everything. Touch wood.
|
|
self.fillDerivedValues(node)
|
|
try:
|
|
if hasattr(node,'_canvas'):
|
|
ocanvas = 1
|
|
else:
|
|
node._canvas = canvas
|
|
ocanvas = None
|
|
node._parent = group
|
|
self.drawNode(node)
|
|
finally:
|
|
del node._parent
|
|
if not ocanvas: del node._canvas
|
|
|
|
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__
|