|
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__ |