renderbase.py 13 KB

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