shapes.py 58 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593
  1. #Copyright ReportLab Europe Ltd. 2000-2017
  2. #see license.txt for license details
  3. #history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/shapes.py
  4. __version__='3.5.60'
  5. __doc__='''Core of the graphics library - defines Drawing and Shapes'''
  6. import os, sys
  7. from math import pi, cos, sin, sqrt, radians, floor
  8. from pprint import pprint
  9. from reportlab.platypus import Flowable
  10. from reportlab.rl_config import shapeChecking, verbose, defaultGraphicsFontName as _baseGFontName, _unset_, decimalSymbol
  11. from reportlab.lib import logger
  12. from reportlab.lib import colors
  13. from reportlab.lib.validators import *
  14. from reportlab.lib.utils import isSeq, asBytes
  15. isOpacity = NoneOr(isNumberInRange(0,1))
  16. from reportlab.lib.attrmap import *
  17. from reportlab.lib.rl_accel import fp_str
  18. from reportlab.pdfbase.pdfmetrics import stringWidth
  19. from reportlab.lib.fonts import tt2ps
  20. from reportlab.pdfgen.canvas import FILL_EVEN_ODD, FILL_NON_ZERO
  21. _baseGFontNameB = tt2ps(_baseGFontName,1,0)
  22. _baseGFontNameI = tt2ps(_baseGFontName,0,1)
  23. _baseGFontNameBI = tt2ps(_baseGFontName,1,1)
  24. class NotImplementedError(Exception):
  25. pass
  26. # two constants for filling rules
  27. NON_ZERO_WINDING = 'Non-Zero Winding'
  28. EVEN_ODD = 'Even-Odd'
  29. ## these can be overridden at module level before you start
  30. #creating shapes. So, if using a special color model,
  31. #this provides support for the rendering mechanism.
  32. #you can change defaults globally before you start
  33. #making shapes; one use is to substitute another
  34. #color model cleanly throughout the drawing.
  35. STATE_DEFAULTS = { # sensible defaults for all
  36. 'transform': (1,0,0,1,0,0),
  37. # styles follow SVG naming
  38. 'strokeColor': colors.black,
  39. 'strokeWidth': 1,
  40. 'strokeLineCap': 0,
  41. 'strokeLineJoin': 0,
  42. 'strokeMiterLimit' : 10, # don't know yet so let bomb here
  43. 'strokeDashArray': None,
  44. 'strokeOpacity': None, #100%
  45. 'fillOpacity': None,
  46. 'fillOverprint': False,
  47. 'strokeOverprint': False,
  48. 'overprintMask': 0,
  49. 'fillColor': colors.black, #...or text will be invisible
  50. 'fillMode': FILL_EVEN_ODD, #same as pdfgen.canvas
  51. 'fontSize': 10,
  52. 'fontName': _baseGFontName,
  53. 'textAnchor': 'start' # can be start, middle, end, inherited
  54. }
  55. ####################################################################
  56. # math utilities. These are now in reportlab.graphics.transform
  57. ####################################################################
  58. from . transform import *
  59. def _textBoxLimits(text, font, fontSize, leading, textAnchor, boxAnchor):
  60. w = 0
  61. for t in text:
  62. w = max(w,stringWidth(t,font, fontSize))
  63. h = len(text)*leading
  64. yt = fontSize
  65. if boxAnchor[0]=='s':
  66. yb = -h
  67. yt = yt - h
  68. elif boxAnchor[0]=='n':
  69. yb = 0
  70. else:
  71. yb = -h/2.0
  72. yt = yt + yb
  73. if boxAnchor[-1]=='e':
  74. xb = -w
  75. if textAnchor=='end': xt = 0
  76. elif textAnchor=='start': xt = -w
  77. else: xt = -w/2.0
  78. elif boxAnchor[-1]=='w':
  79. xb = 0
  80. if textAnchor=='end': xt = w
  81. elif textAnchor=='start': xt = 0
  82. else: xt = w/2.0
  83. else:
  84. xb = -w/2.0
  85. if textAnchor=='end': xt = -xb
  86. elif textAnchor=='start': xt = xb
  87. else: xt = 0
  88. return xb, yb, w, h, xt, yt
  89. def _rotatedBoxLimits( x, y, w, h, angle):
  90. '''
  91. Find the corner points of the rotated w x h sized box at x,y
  92. return the corner points and the min max points in the original space
  93. '''
  94. C = zTransformPoints(rotate(angle),((x,y),(x+w,y),(x+w,y+h),(x,y+h)))
  95. X = [x[0] for x in C]
  96. Y = [x[1] for x in C]
  97. return min(X), max(X), min(Y), max(Y), C
  98. class _DrawTimeResizeable:
  99. '''Addin class to provide the horribleness of _drawTimeResize'''
  100. def _drawTimeResize(self,w,h):
  101. if hasattr(self,'_canvas'):
  102. canvas = self._canvas
  103. drawing = canvas._drawing
  104. drawing.width, drawing.height = w, h
  105. if hasattr(canvas,'_drawTimeResize'):
  106. canvas._drawTimeResize(w,h)
  107. class _SetKeyWordArgs:
  108. def __init__(self, keywords={}):
  109. """In general properties may be supplied to the constructor."""
  110. for key, value in keywords.items():
  111. setattr(self, key, value)
  112. #################################################################
  113. #
  114. # Helper functions for working out bounds
  115. #
  116. #################################################################
  117. def getRectsBounds(rectList):
  118. # filter out any None objects, e.g. empty groups
  119. L = [x for x in rectList if x is not None]
  120. if not L: return None
  121. xMin, yMin, xMax, yMax = L[0]
  122. for (x1, y1, x2, y2) in L[1:]:
  123. if x1 < xMin:
  124. xMin = x1
  125. if x2 > xMax:
  126. xMax = x2
  127. if y1 < yMin:
  128. yMin = y1
  129. if y2 > yMax:
  130. yMax = y2
  131. return (xMin, yMin, xMax, yMax)
  132. def _getBezierExtrema(y0,y1,y2,y3):
  133. '''
  134. this is used to find if a curveTo path operator has extrema in its range
  135. The curveTo operator is defined by the points y0, y1, y2, y3
  136. B(t):=(1-t)^3*y0+3*(1-t)^2*t*y1+3*(1-t)*t^2*y2+t^3*y3
  137. :=t^3*(y3-3*y2+3*y1-y0)+t^2*(3*y2-6*y1+3*y0)+t*(3*y1-3*y0)+y0
  138. and is a cubic bezier curve.
  139. The differential is a quadratic
  140. t^2*(3*y3-9*y2+9*y1-3*y0)+t*(6*y2-12*y1+6*y0)+3*y1-3*y0
  141. The extrema must be at real roots, r, of the above which lie in 0<=r<=1
  142. The quadratic coefficients are
  143. a=3*y3-9*y2+9*y1-3*y0 b=6*y2-12*y1+6*y0 c=3*y1-3*y0
  144. or
  145. a=y3-3*y2+3*y1-y0 b=2*y2-4*y1+2*y0 c=y1-y0 (remove common factor of 3)
  146. or
  147. a=y3-3*(y2-y1)-y0 b=2*(y2-2*y1+y0) c=y1-y0
  148. The returned value is [y0,x1,x2,y3] where if found x1, x2 are any extremals that were found;
  149. there can be 0, 1 or 2 extremals
  150. '''
  151. a=y3-3*(y2-y1)-y0
  152. b=2*(y2-2*y1+y0)
  153. c=y1-y0
  154. Y = [y0] #the set of points
  155. #standard method to find roots of quadratic
  156. d = b*b - 4*a*c
  157. if d>=0:
  158. d = sqrt(d)
  159. if b<0: d = -d
  160. q = -0.5*(b+d)
  161. R = []
  162. try:
  163. R.append(q/a)
  164. except:
  165. pass
  166. try:
  167. R.append(c/q)
  168. except:
  169. pass
  170. b *= 1.5
  171. c *= 3
  172. for t in R:
  173. if 0<=t<=1:
  174. #real root in range evaluate spline there and add to X
  175. Y.append(t*(t*(t*a+b)+c)+y0)
  176. Y.append(y3)
  177. return Y
  178. def getPathBounds(points):
  179. n = len(points)
  180. f = lambda i,p = points: p[i]
  181. xs = list(map(f,range(0,n,2)))
  182. ys = list(map(f,range(1,n,2)))
  183. return (min(xs), min(ys), max(xs), max(ys))
  184. def getPointsBounds(pointList):
  185. "Helper function for list of points"
  186. first = pointList[0]
  187. if isSeq(first):
  188. xs = [xy[0] for xy in pointList]
  189. ys = [xy[1] for xy in pointList]
  190. return (min(xs), min(ys), max(xs), max(ys))
  191. else:
  192. return getPathBounds(pointList)
  193. #################################################################
  194. #
  195. # And now the shapes themselves....
  196. #
  197. #################################################################
  198. class Shape(_SetKeyWordArgs,_DrawTimeResizeable):
  199. """Base class for all nodes in the tree. Nodes are simply
  200. packets of data to be created, stored, and ultimately
  201. rendered - they don't do anything active. They provide
  202. convenience methods for verification but do not
  203. check attribiute assignments or use any clever setattr
  204. tricks this time."""
  205. _attrMap = AttrMap()
  206. def copy(self):
  207. """Return a clone of this shape."""
  208. # implement this in the descendants as they need the right init methods.
  209. raise NotImplementedError("No copy method implemented for %s" % self.__class__.__name__)
  210. def getProperties(self,recur=1):
  211. """Interface to make it easy to extract automatic
  212. documentation"""
  213. #basic nodes have no children so this is easy.
  214. #for more complex objects like widgets you
  215. #may need to override this.
  216. props = {}
  217. for key, value in self.__dict__.items():
  218. if key[0:1] != '_':
  219. props[key] = value
  220. return props
  221. def setProperties(self, props):
  222. """Supports the bulk setting if properties from,
  223. for example, a GUI application or a config file."""
  224. self.__dict__.update(props)
  225. #self.verify()
  226. def dumpProperties(self, prefix=""):
  227. """Convenience. Lists them on standard output. You
  228. may provide a prefix - mostly helps to generate code
  229. samples for documentation."""
  230. propList = list(self.getProperties().items())
  231. propList.sort()
  232. if prefix:
  233. prefix = prefix + '.'
  234. for (name, value) in propList:
  235. print('%s%s = %s' % (prefix, name, value))
  236. def verify(self):
  237. """If the programmer has provided the optional
  238. _attrMap attribute, this checks all expected
  239. attributes are present; no unwanted attributes
  240. are present; and (if a checking function is found)
  241. checks each attribute. Either succeeds or raises
  242. an informative exception."""
  243. if self._attrMap is not None:
  244. for key in self.__dict__.keys():
  245. if key[0] != '_':
  246. assert key in self._attrMap, "Unexpected attribute %s found in %s" % (key, self)
  247. for attr, metavalue in self._attrMap.items():
  248. assert hasattr(self, attr), "Missing attribute %s from %s" % (attr, self)
  249. value = getattr(self, attr)
  250. assert metavalue.validate(value), "Invalid value %s for attribute %s in class %s" % (value, attr, self.__class__.__name__)
  251. if shapeChecking:
  252. """This adds the ability to check every attribute assignment as it is made.
  253. It slows down shapes but is a big help when developing. It does not
  254. get defined if rl_config.shapeChecking = 0"""
  255. def __setattr__(self, attr, value):
  256. """By default we verify. This could be off
  257. in some parallel base classes."""
  258. validateSetattr(self,attr,value) #from reportlab.lib.attrmap
  259. def getBounds(self):
  260. "Returns bounding rectangle of object as (x1,y1,x2,y2)"
  261. raise NotImplementedError("Shapes and widgets must implement getBounds")
  262. class Group(Shape):
  263. """Groups elements together. May apply a transform
  264. to its contents. Has a publicly accessible property
  265. 'contents' which may be used to iterate over contents.
  266. In addition, child nodes may be given a name in which
  267. case they are subsequently accessible as properties."""
  268. _attrMap = AttrMap(
  269. transform = AttrMapValue(isTransform,desc="Coordinate transformation to apply",advancedUsage=1),
  270. contents = AttrMapValue(isListOfShapes,desc="Contained drawable elements"),
  271. strokeOverprint = AttrMapValue(isBoolean,desc='Turn on stroke overprinting'),
  272. fillOverprint = AttrMapValue(isBoolean,desc='Turn on fill overprinting',advancedUsage=1),
  273. overprintMask = AttrMapValue(isBoolean,desc='overprinting for ordinary CMYK',advancedUsage=1),
  274. )
  275. def __init__(self, *elements, **keywords):
  276. """Initial lists of elements may be provided to allow
  277. compact definitions in literal Python code. May or
  278. may not be useful."""
  279. # Groups need _attrMap to be an instance rather than
  280. # a class attribute, as it may be extended at run time.
  281. self._attrMap = self._attrMap.clone()
  282. self.contents = []
  283. self.transform = (1,0,0,1,0,0)
  284. for elt in elements:
  285. self.add(elt)
  286. # this just applies keywords; do it at the end so they
  287. #don;t get overwritten
  288. _SetKeyWordArgs.__init__(self, keywords)
  289. def _addNamedNode(self,name,node):
  290. 'if name is not None add an attribute pointing to node and add to the attrMap'
  291. if name:
  292. if name not in list(self._attrMap.keys()):
  293. self._attrMap[name] = AttrMapValue(isValidChild)
  294. setattr(self, name, node)
  295. def add(self, node, name=None):
  296. """Appends non-None child node to the 'contents' attribute. In addition,
  297. if a name is provided, it is subsequently accessible by name
  298. """
  299. # propagates properties down
  300. if node is not None:
  301. assert isValidChild(node), "Can only add Shape or UserNode objects to a Group"
  302. self.contents.append(node)
  303. self._addNamedNode(name,node)
  304. def _nn(self,node):
  305. self.add(node)
  306. return self.contents[-1]
  307. def insert(self, i, n, name=None):
  308. 'Inserts sub-node n in contents at specified location'
  309. if n is not None:
  310. assert isValidChild(n), "Can only insert Shape or UserNode objects in a Group"
  311. if i<0:
  312. self.contents[i:i] =[n]
  313. else:
  314. self.contents.insert(i,n)
  315. self._addNamedNode(name,n)
  316. def expandUserNodes(self):
  317. """Return a new object which only contains primitive shapes."""
  318. # many limitations - shared nodes become multiple ones,
  319. obj = isinstance(self,Drawing) and Drawing(self.width,self.height) or Group()
  320. obj._attrMap = self._attrMap.clone()
  321. if hasattr(obj,'transform'): obj.transform = self.transform[:]
  322. self_contents = self.contents
  323. a = obj.contents.append
  324. for child in self_contents:
  325. if isinstance(child, UserNode):
  326. newChild = child.provideNode()
  327. elif isinstance(child, Group):
  328. newChild = child.expandUserNodes()
  329. else:
  330. newChild = child.copy()
  331. a(newChild)
  332. self._copyNamedContents(obj)
  333. return obj
  334. def _explode(self):
  335. ''' return a fully expanded object'''
  336. from reportlab.graphics.widgetbase import Widget
  337. obj = Group()
  338. if hasattr(self,'__label__'):
  339. obj.__label__=self.__label__
  340. if hasattr(obj,'transform'): obj.transform = self.transform[:]
  341. P = self.getContents()[:] # pending nodes
  342. while P:
  343. n = P.pop(0)
  344. if isinstance(n, UserNode):
  345. P.append(n.provideNode())
  346. elif isinstance(n, Group):
  347. n = n._explode()
  348. if n.transform==(1,0,0,1,0,0):
  349. obj.contents.extend(n.contents)
  350. else:
  351. obj.add(n)
  352. else:
  353. obj.add(n)
  354. return obj
  355. def _copyContents(self,obj):
  356. for child in self.contents:
  357. obj.contents.append(child)
  358. def _copyNamedContents(self,obj,aKeys=None,noCopy=('contents',)):
  359. from copy import copy
  360. self_contents = self.contents
  361. if not aKeys: aKeys = list(self._attrMap.keys())
  362. for k, v in self.__dict__.items():
  363. if v in self_contents:
  364. pos = self_contents.index(v)
  365. setattr(obj, k, obj.contents[pos])
  366. elif k in aKeys and k not in noCopy:
  367. setattr(obj, k, copy(v))
  368. def _copy(self,obj):
  369. """copies to obj"""
  370. obj._attrMap = self._attrMap.clone()
  371. self._copyContents(obj)
  372. self._copyNamedContents(obj)
  373. return obj
  374. def copy(self):
  375. """returns a copy"""
  376. return self._copy(self.__class__())
  377. def rotate(self, theta):
  378. """Convenience to help you set transforms"""
  379. self.transform = mmult(self.transform, rotate(theta))
  380. def translate(self, dx, dy):
  381. """Convenience to help you set transforms"""
  382. self.transform = mmult(self.transform, translate(dx, dy))
  383. def scale(self, sx, sy):
  384. """Convenience to help you set transforms"""
  385. self.transform = mmult(self.transform, scale(sx, sy))
  386. def skew(self, kx, ky):
  387. """Convenience to help you set transforms"""
  388. self.transform = mmult(mmult(self.transform, skewX(kx)),skewY(ky))
  389. def shift(self, x, y):
  390. '''Convenience function to set the origin arbitrarily'''
  391. self.transform = self.transform[:-2]+(x,y)
  392. def asDrawing(self, width, height):
  393. """ Convenience function to make a drawing from a group
  394. After calling this the instance will be a drawing!
  395. """
  396. self.__class__ = Drawing
  397. self._attrMap.update(self._xtraAttrMap)
  398. self.width = width
  399. self.height = height
  400. def getContents(self):
  401. '''Return the list of things to be rendered
  402. override to get more complicated behaviour'''
  403. b = getattr(self,'background',None)
  404. C = self.contents
  405. if b and b not in C: C = [b]+C
  406. return C
  407. def getBounds(self):
  408. if self.contents:
  409. b = []
  410. for elem in self.contents:
  411. b.append(elem.getBounds())
  412. x1 = getRectsBounds(b)
  413. if x1 is None: return None
  414. x1, y1, x2, y2 = x1
  415. trans = self.transform
  416. corners = [[x1,y1], [x1, y2], [x2, y1], [x2,y2]]
  417. newCorners = []
  418. for corner in corners:
  419. newCorners.append(transformPoint(trans, corner))
  420. return getPointsBounds(newCorners)
  421. else:
  422. #empty group needs a sane default; this
  423. #will happen when interactively creating a group
  424. #nothing has been added to yet. The alternative is
  425. #to handle None as an allowed return value everywhere.
  426. return None
  427. def _addObjImport(obj,I,n=None):
  428. '''add an import of obj's class to a dictionary of imports''' #'
  429. from inspect import getmodule
  430. c = obj.__class__
  431. m = getmodule(c).__name__
  432. n = n or c.__name__
  433. if m not in I:
  434. I[m] = [n]
  435. elif n not in I[m]:
  436. I[m].append(n)
  437. def _repr(self,I=None):
  438. '''return a repr style string with named fixed args first, then keywords'''
  439. if isinstance(self,float):
  440. return fp_str(self)
  441. elif isSeq(self):
  442. s = ''
  443. for v in self:
  444. s = s + '%s,' % _repr(v,I)
  445. if isinstance(self,list):
  446. return '[%s]' % s[:-1]
  447. else:
  448. return '(%s%s)' % (s[:-1],len(self)==1 and ',' or '')
  449. elif self is EmptyClipPath:
  450. if I: _addObjImport(self,I,'EmptyClipPath')
  451. return 'EmptyClipPath'
  452. elif isinstance(self,Shape):
  453. if I: _addObjImport(self,I)
  454. from inspect import getargspec
  455. args, varargs, varkw, defaults = getargspec(self.__init__)
  456. if defaults:
  457. kargs = args[-len(defaults):]
  458. del args[-len(defaults):]
  459. else:
  460. kargs = []
  461. P = self.getProperties()
  462. s = self.__class__.__name__+'('
  463. for n in args[1:]:
  464. v = P[n]
  465. del P[n]
  466. s = s + '%s,' % _repr(v,I)
  467. for n in kargs:
  468. v = P[n]
  469. del P[n]
  470. s = s + '%s=%s,' % (n,_repr(v,I))
  471. for n,v in P.items():
  472. v = P[n]
  473. s = s + '%s=%s,' % (n, _repr(v,I))
  474. return s[:-1]+')'
  475. else:
  476. return repr(self)
  477. def _renderGroupPy(G,pfx,I,i=0,indent='\t\t'):
  478. s = ''
  479. C = getattr(G,'transform',None)
  480. if C: s = s + ('%s%s.transform = %s\n' % (indent,pfx,_repr(C)))
  481. C = G.contents
  482. for n in C:
  483. if isinstance(n, Group):
  484. npfx = 'v%d' % i
  485. i += 1
  486. l = getattr(n,'__label__','')
  487. if l: l='#'+l
  488. s = s + '%s%s=%s._nn(Group())%s\n' % (indent,npfx,pfx,l)
  489. s = s + _renderGroupPy(n,npfx,I,i,indent)
  490. i -= 1
  491. else:
  492. s = s + '%s%s.add(%s)\n' % (indent,pfx,_repr(n,I))
  493. return s
  494. def _extraKW(self,pfx,**kw):
  495. kw.update(self.__dict__)
  496. R = {}
  497. n = len(pfx)
  498. for k in kw.keys():
  499. if k.startswith(pfx):
  500. R[k[n:]] = kw[k]
  501. return R
  502. class Drawing(Group, Flowable):
  503. """Outermost container; the thing a renderer works on.
  504. This has no properties except a height, width and list
  505. of contents."""
  506. _saveModes = {
  507. 'bmp',
  508. 'eps',
  509. 'gif',
  510. 'jpeg',
  511. 'jpg',
  512. 'pct',
  513. 'pdf',
  514. 'pict',
  515. 'png',
  516. 'ps',
  517. 'py',
  518. 'svg',
  519. 'tif',
  520. 'tiff',
  521. 'tiff1',
  522. 'tiffl',
  523. 'tiffp',
  524. }
  525. _bmModes = _saveModes - {'eps','pdf','ps','py','svg'}
  526. _xtraAttrMap = AttrMap(
  527. width = AttrMapValue(isNumber,desc="Drawing width in points."),
  528. height = AttrMapValue(isNumber,desc="Drawing height in points."),
  529. canv = AttrMapValue(None),
  530. background = AttrMapValue(isValidChildOrNone,desc="Background widget for the drawing e.g. Rect(0,0,width,height)"),
  531. hAlign = AttrMapValue(OneOf("LEFT", "RIGHT", "CENTER", "CENTRE"), desc="Horizontal alignment within parent document"),
  532. vAlign = AttrMapValue(OneOf("TOP", "BOTTOM", "CENTER", "CENTRE"), desc="Vertical alignment within parent document"),
  533. #AR temporary hack to track back up.
  534. #fontName = AttrMapValue(isStringOrNone),
  535. renderScale = AttrMapValue(isNumber,desc="Global scaling for rendering"),
  536. initialFontName = AttrMapValue(isStringOrNone,desc="override the STATE_DEFAULTS value for fontName"),
  537. initialFontSize = AttrMapValue(isNumberOrNone,desc="override the STATE_DEFAULTS value for fontSize"),
  538. )
  539. _attrMap = AttrMap(BASE=Group,
  540. formats = AttrMapValue(SequenceOf(
  541. OneOf(*_saveModes),
  542. lo=1,emptyOK=0), desc='One or more plot modes'),
  543. )
  544. _attrMap.update(_xtraAttrMap)
  545. def __init__(self, width=400, height=200, *nodes, **keywords):
  546. self.background = None
  547. Group.__init__(self,*nodes,**keywords)
  548. self.width = width
  549. self.height = height
  550. self.hAlign = 'LEFT'
  551. self.vAlign = 'BOTTOM'
  552. self.renderScale = 1.0
  553. def _renderPy(self):
  554. I = {
  555. 'reportlab.graphics.shapes': ['_DrawingEditorMixin','Drawing','Group'],
  556. 'reportlab.lib.colors': ['Color','CMYKColor','PCMYKColor'],
  557. }
  558. G = _renderGroupPy(self._explode(),'self',I)
  559. n = 'ExplodedDrawing_' + self.__class__.__name__
  560. s = '#Autogenerated by ReportLab guiedit do not edit\n'
  561. for m, o in I.items():
  562. s = s + 'from %s import %s\n' % (m,str(o)[1:-1].replace("'",""))
  563. s = s + '\nclass %s(_DrawingEditorMixin,Drawing):\n' % n
  564. s = s + '\tdef __init__(self,width=%s,height=%s,*args,**kw):\n' % (self.width,self.height)
  565. s = s + '\t\tDrawing.__init__(self,width,height,*args,**kw)\n'
  566. s = s + G
  567. s = s + '\n\nif __name__=="__main__": #NORUNTESTS\n\t%s().save(formats=[\'pdf\'],outDir=\'.\',fnRoot=None)\n' % n
  568. return s
  569. def draw(self,showBoundary=_unset_):
  570. """This is used by the Platypus framework to let the document
  571. draw itself in a story. It is specific to PDF and should not
  572. be used directly."""
  573. from reportlab.graphics import renderPDF
  574. renderPDF.draw(self, self.canv, 0, 0,
  575. showBoundary=showBoundary if showBoundary is not _unset_ else getattr(self,'_showBoundary',_unset_))
  576. def wrap(self, availWidth, availHeight):
  577. width = self.width
  578. height = self.height
  579. renderScale = self.renderScale
  580. if renderScale!=1.0:
  581. width *= renderScale
  582. height *= renderScale
  583. return width, height
  584. def expandUserNodes(self):
  585. """Return a new drawing which only contains primitive shapes."""
  586. obj = Group.expandUserNodes(self)
  587. obj.width = self.width
  588. obj.height = self.height
  589. return obj
  590. def copy(self):
  591. """Returns a copy"""
  592. return self._copy(self.__class__(self.width, self.height))
  593. def asGroup(self,*args,**kw):
  594. return self._copy(Group(*args,**kw))
  595. def save(self, formats=None, verbose=None, fnRoot=None, outDir=None, title='', **kw):
  596. """Saves copies of self in desired location and formats.
  597. Multiple formats can be supported in one call
  598. the extra keywords can be of the form
  599. _renderPM_dpi=96 (which passes dpi=96 to renderPM)
  600. """
  601. genFmt = kw.pop('seqNumber','')
  602. if isinstance(genFmt,int):
  603. genFmt = '%4d: ' % genFmt
  604. else:
  605. genFmt = ''
  606. genFmt += 'generating %s file %s'
  607. from reportlab import rl_config
  608. ext = ''
  609. if not fnRoot:
  610. fnRoot = getattr(self,'fileNamePattern',(self.__class__.__name__+'%03d'))
  611. chartId = getattr(self,'chartId',0)
  612. if hasattr(chartId,'__call__'):
  613. chartId = chartId(self)
  614. if hasattr(fnRoot,'__call__'):
  615. fnRoot = fnRoot(chartId)
  616. else:
  617. try:
  618. fnRoot = fnRoot % chartId
  619. except TypeError as err:
  620. #the exact error message changed from 2.2 to 2.3 so we need to
  621. #check a substring
  622. if str(err).find('not all arguments converted') < 0: raise
  623. if outDir is None:
  624. outDir = getattr(self,'outDir',None)
  625. if hasattr(outDir,'__call__'):
  626. outDir = outDir(self)
  627. if os.path.isabs(fnRoot):
  628. outDir, fnRoot = os.path.split(fnRoot)
  629. else:
  630. outDir = outDir or getattr(self,'outDir','.')
  631. outDir = outDir.rstrip().rstrip(os.sep)
  632. if not outDir: outDir = '.'
  633. if not os.path.isabs(outDir): outDir = os.path.join(getattr(self,'_override_CWD',os.path.dirname(sys.argv[0])),outDir)
  634. if not os.path.isdir(outDir): os.makedirs(outDir)
  635. fnroot = os.path.normpath(os.path.join(outDir,fnRoot))
  636. plotMode = os.path.splitext(fnroot)
  637. if plotMode[1][1:].lower() in self._saveModes:
  638. fnroot = plotMode[0]
  639. plotMode = [x.lower() for x in (formats or getattr(self,'formats',['pdf']))]
  640. verbose = (verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),))[0]
  641. _saved = logger.warnOnce.enabled, logger.infoOnce.enabled
  642. logger.warnOnce.enabled = logger.infoOnce.enabled = verbose
  643. if 'pdf' in plotMode:
  644. from reportlab.graphics import renderPDF
  645. filename = fnroot+'.pdf'
  646. if verbose: print(genFmt % ('PDF',filename))
  647. renderPDF.drawToFile(self, filename, title, showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPDF_',**kw))
  648. ext = ext + '/.pdf'
  649. if sys.platform=='mac':
  650. import macfs, macostools
  651. macfs.FSSpec(filename).SetCreatorType("CARO", "PDF ")
  652. macostools.touched(filename)
  653. for bmFmt in self._bmModes:
  654. if bmFmt in plotMode:
  655. from reportlab.graphics import renderPM
  656. filename = '%s.%s' % (fnroot,bmFmt)
  657. if verbose: print(genFmt % (bmFmt,filename))
  658. dtc = getattr(self,'_drawTimeCollector',None)
  659. if dtc:
  660. dtcfmts = getattr(dtc,'formats',[bmFmt])
  661. if bmFmt in dtcfmts and not getattr(dtc,'disabled',0):
  662. dtc.clear()
  663. else:
  664. dtc = None
  665. renderPM.drawToFile(self, filename,fmt=bmFmt,showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPM_',**kw))
  666. ext = ext + '/.' + bmFmt
  667. if dtc: dtc.save(filename)
  668. if 'eps' in plotMode:
  669. try:
  670. from rlextra.graphics import renderPS_SEP as renderPS
  671. except ImportError:
  672. from reportlab.graphics import renderPS
  673. filename = fnroot+'.eps'
  674. if verbose: print(genFmt % ('EPS',filename))
  675. renderPS.drawToFile(self,
  676. filename,
  677. title = fnroot,
  678. dept = getattr(self,'EPS_info',['Testing'])[0],
  679. company = getattr(self,'EPS_info',['','ReportLab'])[1],
  680. preview = getattr(self,'preview',rl_config.eps_preview),
  681. showBoundary=getattr(self,'showBorder',rl_config.showBoundary),
  682. ttf_embed=getattr(self,'ttf_embed',rl_config.eps_ttf_embed),
  683. **_extraKW(self,'_renderPS_',**kw))
  684. ext = ext + '/.eps'
  685. if 'svg' in plotMode:
  686. from reportlab.graphics import renderSVG
  687. filename = fnroot+'.svg'
  688. if verbose: print(genFmt % ('SVG',filename))
  689. renderSVG.drawToFile(self,
  690. filename,
  691. showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderSVG_',**kw))
  692. ext = ext + '/.svg'
  693. if 'ps' in plotMode:
  694. from reportlab.graphics import renderPS
  695. filename = fnroot+'.ps'
  696. if verbose: print(genFmt % ('EPS',filename))
  697. renderPS.drawToFile(self, filename, showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPS_',**kw))
  698. ext = ext + '/.ps'
  699. if 'py' in plotMode:
  700. filename = fnroot+'.py'
  701. if verbose: print(genFmt % ('py',filename))
  702. with open(filename,'wb') as f:
  703. f.write(asBytes(self._renderPy().replace('\n',os.linesep)))
  704. ext = ext + '/.py'
  705. logger.warnOnce.enabled, logger.infoOnce.enabled = _saved
  706. if hasattr(self,'saveLogger'):
  707. self.saveLogger(fnroot,ext)
  708. return ext and fnroot+ext[1:] or ''
  709. def asString(self, format, verbose=None, preview=0, **kw):
  710. """Converts to an 8 bit string in given format."""
  711. assert format in self._saveModes, 'Unknown file format "%s"' % format
  712. from reportlab import rl_config
  713. #verbose = verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),)[0]
  714. if format == 'pdf':
  715. from reportlab.graphics import renderPDF
  716. return renderPDF.drawToString(self)
  717. elif format in self._bmModes:
  718. from reportlab.graphics import renderPM
  719. return renderPM.drawToString(self, fmt=format,showBoundary=getattr(self,'showBorder',
  720. rl_config.showBoundary),**_extraKW(self,'_renderPM_',**kw))
  721. elif format == 'eps':
  722. try:
  723. from rlextra.graphics import renderPS_SEP as renderPS
  724. except ImportError:
  725. from reportlab.graphics import renderPS
  726. return renderPS.drawToString(self,
  727. preview = preview,
  728. showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
  729. elif format == 'ps':
  730. from reportlab.graphics import renderPS
  731. return renderPS.drawToString(self, showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
  732. elif format == 'py':
  733. return self._renderPy()
  734. elif format == 'svg':
  735. from reportlab.graphics import renderSVG
  736. return renderSVG.drawToString(self,showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderSVG_',**kw))
  737. def resized(self,kind='fit',lpad=0,rpad=0,bpad=0,tpad=0):
  738. '''return a base class drawing which ensures all the contents fits'''
  739. C = self.getContents()
  740. oW = self.width
  741. oH = self.height
  742. drawing = Drawing(oW,oH,*C)
  743. xL,yL,xH,yH = drawing.getBounds()
  744. if kind=='fit' or (kind=='expand' and (xL<lpad or xH>oW-rpad or yL<bpad or yH>oH-tpad)):
  745. drawing.width = xH-xL+lpad+rpad
  746. drawing.height = yH-yL+tpad+bpad
  747. drawing.transform = (1,0,0,1,lpad-xL,bpad-yL)
  748. elif kind=='fitx' or (kind=='expandx' and (xL<lpad or xH>oW-rpad)):
  749. drawing.width = xH-xL+lpad+rpad
  750. drawing.transform = (1,0,0,1,lpad-xL,0)
  751. elif kind=='fity' or (kind=='expandy' and (yL<bpad or yH>oH-tpad)):
  752. drawing.height = yH-yL+tpad+bpad
  753. drawing.transform = (1,0,0,1,0,bpad-yL)
  754. return drawing
  755. class _DrawingEditorMixin:
  756. '''This is a mixin to provide functionality for edited drawings'''
  757. def _add(self,obj,value,name=None,validate=None,desc=None,pos=None):
  758. '''
  759. effectively setattr(obj,name,value), but takes care of things with _attrMaps etc
  760. '''
  761. ivc = isValidChild(value)
  762. if name and hasattr(obj,'_attrMap'):
  763. if '_attrMap' not in obj.__dict__:
  764. obj._attrMap = obj._attrMap.clone()
  765. if ivc and validate is None: validate = isValidChild
  766. obj._attrMap[name] = AttrMapValue(validate,desc)
  767. if hasattr(obj,'add') and ivc:
  768. if pos:
  769. obj.insert(pos,value,name)
  770. else:
  771. obj.add(value,name)
  772. elif name:
  773. setattr(obj,name,value)
  774. else:
  775. raise ValueError("Can't add, need name")
  776. class isStrokeDashArray(Validator):
  777. def test(self,x):
  778. return isListOfNumbersOrNone.test(x) or (isinstance(x,(list,tuple)) and isNumber(x[0]) and isListOfNumbers(x[1]))
  779. isStrokeDashArray = isStrokeDashArray()
  780. class LineShape(Shape):
  781. # base for types of lines
  782. _attrMap = AttrMap(
  783. strokeColor = AttrMapValue(isColorOrNone),
  784. strokeWidth = AttrMapValue(isNumber),
  785. strokeLineCap = AttrMapValue(OneOf(0,1,2),desc="Line cap 0=butt, 1=round & 2=square"),
  786. strokeLineJoin = AttrMapValue(OneOf(0,1,2),desc="Line join 0=miter, 1=round & 2=bevel"),
  787. strokeMiterLimit = AttrMapValue(isNumber,desc="miter limit control miter line joins"),
  788. strokeDashArray = AttrMapValue(isStrokeDashArray,desc="[numbers] or [phase,[numbers]]"),
  789. strokeOpacity = AttrMapValue(isOpacity,desc="The level of transparency of the line, any real number betwen 0 and 1"),
  790. strokeOverprint = AttrMapValue(isBoolean,desc='Turn on stroke overprinting'),
  791. overprintMask = AttrMapValue(isBoolean,desc='overprinting for ordinary CMYK',advancedUsage=1),
  792. )
  793. def __init__(self, kw):
  794. self.strokeColor = STATE_DEFAULTS['strokeColor']
  795. self.strokeWidth = 1
  796. self.strokeLineCap = 0
  797. self.strokeLineJoin = 0
  798. self.strokeMiterLimit = 0
  799. self.strokeDashArray = None
  800. self.strokeOpacity = None
  801. self.setProperties(kw)
  802. class Line(LineShape):
  803. _attrMap = AttrMap(BASE=LineShape,
  804. x1 = AttrMapValue(isNumber,desc=""),
  805. y1 = AttrMapValue(isNumber,desc=""),
  806. x2 = AttrMapValue(isNumber,desc=""),
  807. y2 = AttrMapValue(isNumber,desc=""),
  808. )
  809. def __init__(self, x1, y1, x2, y2, **kw):
  810. LineShape.__init__(self, kw)
  811. self.x1 = x1
  812. self.y1 = y1
  813. self.x2 = x2
  814. self.y2 = y2
  815. def getBounds(self):
  816. "Returns bounding rectangle of object as (x1,y1,x2,y2)"
  817. return (self.x1, self.y1, self.x2, self.y2)
  818. class SolidShape(LineShape):
  819. # base for anything with outline and content
  820. _attrMap = AttrMap(BASE=LineShape,
  821. fillColor = AttrMapValue(isColorOrNone,desc="filling color of the shape, e.g. red"),
  822. fillOpacity = AttrMapValue(isOpacity,desc="the level of transparency of the color, any real number between 0 and 1"),
  823. fillOverprint = AttrMapValue(isBoolean,desc='Turn on fill overprinting'),
  824. overprintMask = AttrMapValue(isBoolean,desc='overprinting for ordinary CMYK',advancedUsage=1),
  825. fillMode = AttrMapValue(OneOf(FILL_EVEN_ODD,FILL_NON_ZERO)),
  826. )
  827. def __init__(self, kw):
  828. self.fillColor = STATE_DEFAULTS['fillColor']
  829. self.fillOpacity = None
  830. # do this at the end so keywords overwrite
  831. #the above settings
  832. LineShape.__init__(self, kw)
  833. # path operator constants
  834. _MOVETO, _LINETO, _CURVETO, _CLOSEPATH = list(range(4))
  835. _PATH_OP_ARG_COUNT = (2, 2, 6, 0) # [moveTo, lineTo, curveTo, closePath]
  836. _PATH_OP_NAMES=['moveTo','lineTo','curveTo','closePath']
  837. def _renderPath(path,drawFuncs,countOnly=False,forceClose=False):
  838. """Helper function for renderers."""
  839. # this could be a method of Path...
  840. points = path.points
  841. i = 0
  842. hadClosePath = 0
  843. hadMoveTo = 0
  844. active = not countOnly
  845. for op in path.operators:
  846. if op == _MOVETO:
  847. if forceClose:
  848. if hadMoveTo and pop!=_CLOSEPATH:
  849. hadClosePath += 1
  850. if active:
  851. drawFuncs[_CLOSEPATH]()
  852. hadMoveTo += 1
  853. nArgs = _PATH_OP_ARG_COUNT[op]
  854. j = i + nArgs
  855. drawFuncs[op](*points[i:j])
  856. i = j
  857. if op == _CLOSEPATH:
  858. hadClosePath += 1
  859. pop = op
  860. if forceClose and hadMoveTo and pop!=_CLOSEPATH:
  861. hadClosePath += 1
  862. if active:
  863. drawFuncs[_CLOSEPATH]()
  864. return hadMoveTo == hadClosePath
  865. _fillModeMap = {
  866. None: None,
  867. FILL_NON_ZERO: FILL_NON_ZERO,
  868. 'non-zero': FILL_NON_ZERO,
  869. 'nonzero': FILL_NON_ZERO,
  870. FILL_EVEN_ODD: FILL_EVEN_ODD,
  871. 'even-odd': FILL_EVEN_ODD,
  872. 'evenodd': FILL_EVEN_ODD,
  873. }
  874. class Path(SolidShape):
  875. """Path, made up of straight lines and bezier curves."""
  876. _attrMap = AttrMap(BASE=SolidShape,
  877. points = AttrMapValue(isListOfNumbers),
  878. operators = AttrMapValue(isListOfNumbers),
  879. isClipPath = AttrMapValue(isBoolean),
  880. autoclose = AttrMapValue(NoneOr(OneOf('svg','pdf'))),
  881. fillMode = AttrMapValue(OneOf(FILL_EVEN_ODD,FILL_NON_ZERO)),
  882. )
  883. def __init__(self, points=None, operators=None, isClipPath=0, autoclose=None, fillMode=FILL_EVEN_ODD, **kw):
  884. SolidShape.__init__(self, kw)
  885. if points is None:
  886. points = []
  887. if operators is None:
  888. operators = []
  889. assert len(points) % 2 == 0, 'Point list must have even number of elements!'
  890. self.points = points
  891. self.operators = operators
  892. self.isClipPath = isClipPath
  893. self.autoclose=autoclose
  894. self.fillMode = fillMode
  895. def copy(self):
  896. new = self.__class__(self.points[:], self.operators[:])
  897. new.setProperties(self.getProperties())
  898. return new
  899. def moveTo(self, x, y):
  900. self.points.extend([x, y])
  901. self.operators.append(_MOVETO)
  902. def lineTo(self, x, y):
  903. self.points.extend([x, y])
  904. self.operators.append(_LINETO)
  905. def curveTo(self, x1, y1, x2, y2, x3, y3):
  906. self.points.extend([x1, y1, x2, y2, x3, y3])
  907. self.operators.append(_CURVETO)
  908. def closePath(self):
  909. self.operators.append(_CLOSEPATH)
  910. def getBounds(self):
  911. points = self.points
  912. try: #in case this complex algorithm is not yet ready :)
  913. X = []
  914. aX = X.append
  915. eX = X.extend
  916. Y=[]
  917. aY = Y.append
  918. eY = Y.extend
  919. i = 0
  920. for op in self.operators:
  921. nArgs = _PATH_OP_ARG_COUNT[op]
  922. j = i + nArgs
  923. if nArgs==2:
  924. #either moveTo or lineT0
  925. aX(points[i])
  926. aY(points[i+1])
  927. elif nArgs==6:
  928. #curveTo
  929. x1,x2,x3 = points[i:j:2]
  930. eX(_getBezierExtrema(X[-1],x1,x2,x3))
  931. y1,y2,y3 = points[i+1:j:2]
  932. eY(_getBezierExtrema(Y[-1],y1,y2,y3))
  933. i = j
  934. return min(X),min(Y),max(X),max(Y)
  935. except:
  936. return getPathBounds(points)
  937. EmptyClipPath=Path() #special path
  938. def getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, reverse=None):
  939. if yradius is None: yradius = radius
  940. points = []
  941. degreestoradians = pi/180.0
  942. startangle = startangledegrees*degreestoradians
  943. endangle = endangledegrees*degreestoradians
  944. while endangle<startangle:
  945. endangle = endangle+2*pi
  946. angle = float(endangle - startangle)
  947. a = points.append
  948. if angle>.001:
  949. degreedelta = min(angle,degreedelta or 1.)
  950. radiansdelta = degreedelta*degreestoradians
  951. n = max(int(angle/radiansdelta+0.5),1)
  952. radiansdelta = angle/n
  953. n += 1
  954. else:
  955. n = 1
  956. radiansdelta = 0
  957. for angle in range(n):
  958. angle = startangle+angle*radiansdelta
  959. a((centerx+radius*cos(angle),centery+yradius*sin(angle)))
  960. if reverse: points.reverse()
  961. return points
  962. class ArcPath(Path):
  963. '''Path with an addArc method'''
  964. def addArc(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, moveTo=None, reverse=None):
  965. P = getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=yradius, degreedelta=degreedelta, reverse=reverse)
  966. if moveTo or not len(self.operators):
  967. self.moveTo(P[0][0],P[0][1])
  968. del P[0]
  969. for x, y in P: self.lineTo(x,y)
  970. def definePath(pathSegs=[],isClipPath=0, dx=0, dy=0, **kw):
  971. O = []
  972. P = []
  973. for seg in pathSegs:
  974. if not isSeq(seg):
  975. opName = seg
  976. args = []
  977. else:
  978. opName = seg[0]
  979. args = seg[1:]
  980. if opName not in _PATH_OP_NAMES:
  981. raise ValueError('bad operator name %s' % opName)
  982. op = _PATH_OP_NAMES.index(opName)
  983. if len(args)!=_PATH_OP_ARG_COUNT[op]:
  984. raise ValueError('%s bad arguments %s' % (opName,str(args)))
  985. O.append(op)
  986. P.extend(list(args))
  987. for d,o in (dx,0), (dy,1):
  988. for i in range(o,len(P),2):
  989. P[i] = P[i]+d
  990. #if there's a bounding box given we constrain so our points lie in it
  991. #partial bbox is allowed and does something sensible
  992. bbox = kw.pop('bbox',None)
  993. if bbox:
  994. for j in 0,1:
  995. d = bbox[j],bbox[j+2]
  996. if d[0] is None and d[1] is None: continue
  997. a = P[j::2]
  998. a, b = min(a), max(a)
  999. if d[0] is not None and d[1] is not None:
  1000. c, d = min(d), max(d)
  1001. fac = (b-a)
  1002. if abs(fac)>=1e-6:
  1003. fac = (d-c)/fac
  1004. for i in range(j,len(P),2):
  1005. P[i] = c + fac*(P[i]-a)
  1006. else:
  1007. #there's no range in the bbox so fixed as average
  1008. for i in range(j,len(P),2):
  1009. P[i] = (c + d)*0.5
  1010. else:
  1011. #if there's a lower bound shift so min is lower bound
  1012. #else there's an upper bound shift so max is upper bound
  1013. c = d[0] - a if d[0] is not None else d[1] - b
  1014. for i in range(i,len(P),2):
  1015. P[i] += c
  1016. return Path(P,O,isClipPath,**kw)
  1017. class Rect(SolidShape):
  1018. """Rectangle, possibly with rounded corners."""
  1019. _attrMap = AttrMap(BASE=SolidShape,
  1020. x = AttrMapValue(isNumber),
  1021. y = AttrMapValue(isNumber),
  1022. width = AttrMapValue(isNumber,desc="width of the object in points"),
  1023. height = AttrMapValue(isNumber,desc="height of the objects in points"),
  1024. rx = AttrMapValue(isNumber),
  1025. ry = AttrMapValue(isNumber),
  1026. )
  1027. def __init__(self, x, y, width, height, rx=0, ry=0, **kw):
  1028. SolidShape.__init__(self, kw)
  1029. self.x = x
  1030. self.y = y
  1031. self.width = width
  1032. self.height = height
  1033. self.rx = rx
  1034. self.ry = ry
  1035. def copy(self):
  1036. new = self.__class__(self.x, self.y, self.width, self.height)
  1037. new.setProperties(self.getProperties())
  1038. return new
  1039. def getBounds(self):
  1040. return (self.x, self.y, self.x + self.width, self.y + self.height)
  1041. class Image(SolidShape):
  1042. """Bitmap image."""
  1043. _attrMap = AttrMap(BASE=SolidShape,
  1044. x = AttrMapValue(isNumber),
  1045. y = AttrMapValue(isNumber),
  1046. width = AttrMapValue(isNumberOrNone,desc="width of the object in points"),
  1047. height = AttrMapValue(isNumberOrNone,desc="height of the objects in points"),
  1048. path = AttrMapValue(None),
  1049. )
  1050. def __init__(self, x, y, width, height, path, **kw):
  1051. SolidShape.__init__(self, kw)
  1052. self.x = x
  1053. self.y = y
  1054. self.width = width
  1055. self.height = height
  1056. self.path = path
  1057. def copy(self):
  1058. new = self.__class__(self.x, self.y, self.width, self.height, self.path)
  1059. new.setProperties(self.getProperties())
  1060. return new
  1061. def getBounds(self):
  1062. # bug fix contributed by Marcel Tromp <mtromp.docbook@gmail.com>
  1063. return (self.x, self.y, self.x + self.width, self.y + self.height)
  1064. class Circle(SolidShape):
  1065. _attrMap = AttrMap(BASE=SolidShape,
  1066. cx = AttrMapValue(isNumber,desc="x of the centre"),
  1067. cy = AttrMapValue(isNumber,desc="y of the centre"),
  1068. r = AttrMapValue(isNumber,desc="radius in points"),
  1069. )
  1070. def __init__(self, cx, cy, r, **kw):
  1071. SolidShape.__init__(self, kw)
  1072. self.cx = cx
  1073. self.cy = cy
  1074. self.r = r
  1075. def copy(self):
  1076. new = self.__class__(self.cx, self.cy, self.r)
  1077. new.setProperties(self.getProperties())
  1078. return new
  1079. def getBounds(self):
  1080. return (self.cx - self.r, self.cy - self.r, self.cx + self.r, self.cy + self.r)
  1081. class Ellipse(SolidShape):
  1082. _attrMap = AttrMap(BASE=SolidShape,
  1083. cx = AttrMapValue(isNumber,desc="x of the centre"),
  1084. cy = AttrMapValue(isNumber,desc="y of the centre"),
  1085. rx = AttrMapValue(isNumber,desc="x radius"),
  1086. ry = AttrMapValue(isNumber,desc="y radius"),
  1087. )
  1088. def __init__(self, cx, cy, rx, ry, **kw):
  1089. SolidShape.__init__(self, kw)
  1090. self.cx = cx
  1091. self.cy = cy
  1092. self.rx = rx
  1093. self.ry = ry
  1094. def copy(self):
  1095. new = self.__class__(self.cx, self.cy, self.rx, self.ry)
  1096. new.setProperties(self.getProperties())
  1097. return new
  1098. def getBounds(self):
  1099. return (self.cx - self.rx, self.cy - self.ry, self.cx + self.rx, self.cy + self.ry)
  1100. class Wedge(SolidShape):
  1101. """A "slice of a pie" by default translates to a polygon moves anticlockwise
  1102. from start angle to end angle"""
  1103. _attrMap = AttrMap(BASE=SolidShape,
  1104. centerx = AttrMapValue(isNumber,desc="x of the centre"),
  1105. centery = AttrMapValue(isNumber,desc="y of the centre"),
  1106. radius = AttrMapValue(isNumber,desc="radius in points"),
  1107. startangledegrees = AttrMapValue(isNumber),
  1108. endangledegrees = AttrMapValue(isNumber),
  1109. yradius = AttrMapValue(isNumberOrNone),
  1110. radius1 = AttrMapValue(isNumberOrNone),
  1111. yradius1 = AttrMapValue(isNumberOrNone),
  1112. annular = AttrMapValue(isBoolean,desc='treat as annular ring'),
  1113. )
  1114. degreedelta = 1 # jump every 1 degrees
  1115. def __init__(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None,
  1116. annular=False, **kw):
  1117. SolidShape.__init__(self, kw)
  1118. while endangledegrees<startangledegrees:
  1119. endangledegrees = endangledegrees+360
  1120. #print "__init__"
  1121. self.centerx, self.centery, self.radius, self.startangledegrees, self.endangledegrees = \
  1122. centerx, centery, radius, startangledegrees, endangledegrees
  1123. self.yradius = yradius
  1124. self.annular = annular
  1125. def _xtraRadii(self):
  1126. yradius = getattr(self, 'yradius', None)
  1127. if yradius is None: yradius = self.radius
  1128. radius1 = getattr(self,'radius1', None)
  1129. yradius1 = getattr(self,'yradius1',radius1)
  1130. if radius1 is None: radius1 = yradius1
  1131. return yradius, radius1, yradius1
  1132. #def __repr__(self):
  1133. # return "Wedge"+repr((self.centerx, self.centery, self.radius, self.startangledegrees, self.endangledegrees ))
  1134. #__str__ = __repr__
  1135. def asPolygon(self):
  1136. #print "asPolygon"
  1137. centerx= self.centerx
  1138. centery = self.centery
  1139. radius = self.radius
  1140. yradius, radius1, yradius1 = self._xtraRadii()
  1141. startangledegrees = self.startangledegrees
  1142. endangledegrees = self.endangledegrees
  1143. degreestoradians = pi/180.0
  1144. startangle = startangledegrees*degreestoradians
  1145. endangle = endangledegrees*degreestoradians
  1146. while endangle<startangle:
  1147. endangle = endangle+2*pi
  1148. angle = float(endangle-startangle)
  1149. points = []
  1150. if angle>0.001:
  1151. degreedelta = min(self.degreedelta or 1.,angle)
  1152. radiansdelta = degreedelta*degreestoradians
  1153. n = max(1,int(angle/radiansdelta+0.5))
  1154. radiansdelta = angle/n
  1155. n += 1
  1156. else:
  1157. n = 1
  1158. radiansdelta = 0
  1159. CA = []
  1160. CAA = CA.append
  1161. a = points.append
  1162. for angle in range(n):
  1163. angle = startangle+angle*radiansdelta
  1164. CAA((cos(angle),sin(angle)))
  1165. for c,s in CA:
  1166. a(centerx+radius*c)
  1167. a(centery+yradius*s)
  1168. if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None):
  1169. a(centerx); a(centery)
  1170. else:
  1171. CA.reverse()
  1172. for c,s in CA:
  1173. a(centerx+radius1*c)
  1174. a(centery+yradius1*s)
  1175. if self.annular:
  1176. P = Path(fillMode=getattr(self,'fillMode', FILL_EVEN_ODD))
  1177. P.moveTo(points[0],points[1])
  1178. for x in range(2,2*n,2):
  1179. P.lineTo(points[x],points[x+1])
  1180. P.closePath()
  1181. P.moveTo(points[2*n],points[2*n+1])
  1182. for x in range(2*n+2,4*n,2):
  1183. P.lineTo(points[x],points[x+1])
  1184. P.closePath()
  1185. return P
  1186. else:
  1187. return Polygon(points)
  1188. def copy(self):
  1189. new = self.__class__(self.centerx,
  1190. self.centery,
  1191. self.radius,
  1192. self.startangledegrees,
  1193. self.endangledegrees)
  1194. new.setProperties(self.getProperties())
  1195. return new
  1196. def getBounds(self):
  1197. return self.asPolygon().getBounds()
  1198. class Polygon(SolidShape):
  1199. """Defines a closed shape; Is implicitly
  1200. joined back to the start for you."""
  1201. _attrMap = AttrMap(BASE=SolidShape,
  1202. points = AttrMapValue(isListOfNumbers,desc="list of numbers in the form x1, y1, x2, y2 ... xn, yn"),
  1203. )
  1204. def __init__(self, points=[], **kw):
  1205. SolidShape.__init__(self, kw)
  1206. assert len(points) % 2 == 0, 'Point list must have even number of elements!'
  1207. self.points = points or []
  1208. def copy(self):
  1209. new = self.__class__(self.points)
  1210. new.setProperties(self.getProperties())
  1211. return new
  1212. def getBounds(self):
  1213. return getPointsBounds(self.points)
  1214. class PolyLine(LineShape):
  1215. """Series of line segments. Does not define a
  1216. closed shape; never filled even if apparently joined.
  1217. Put the numbers in the list, not two-tuples."""
  1218. _attrMap = AttrMap(BASE=LineShape,
  1219. points = AttrMapValue(isListOfNumbers,desc="list of numbers in the form x1, y1, x2, y2 ... xn, yn"),
  1220. )
  1221. def __init__(self, points=[], **kw):
  1222. LineShape.__init__(self, kw)
  1223. points = points or []
  1224. lenPoints = len(points)
  1225. if lenPoints:
  1226. if isSeq(points[0]):
  1227. L = []
  1228. for (x,y) in points:
  1229. L.append(x)
  1230. L.append(y)
  1231. points = L
  1232. else:
  1233. assert len(points) % 2 == 0, 'Point list must have even number of elements!'
  1234. self.points = points
  1235. def copy(self):
  1236. new = self.__class__(self.points)
  1237. new.setProperties(self.getProperties())
  1238. return new
  1239. def getBounds(self):
  1240. return getPointsBounds(self.points)
  1241. class Hatching(Path):
  1242. '''define a hatching of a set of polygons defined by lists of the form [x0,y0,x1,y1,....,xn,yn]'''
  1243. _attrMap = AttrMap(BASE=Path,
  1244. xyLists = AttrMapValue(EitherOr((isListOfNumbers,SequenceOf(isListOfNumbers,lo=1)),"xy list(s)"),desc="list(s) of numbers in the form x1, y1, x2, y2 ... xn, yn"),
  1245. angles = AttrMapValue(EitherOr((isNumber,isListOfNumbers,"angle(s)")),desc="the angle or list of angles at which hatching lines should be drawn"),
  1246. spacings = AttrMapValue(EitherOr((isNumber,isListOfNumbers,"spacings(s)")),desc="orthogonal distance(s) between hatching lines"),
  1247. )
  1248. def __init__(self, spacings=2, angles=45, xyLists=[], **kwds):
  1249. Path.__init__(self, **kwds)
  1250. if isListOfNumbers(xyLists):
  1251. xyLists = (xyLists,)
  1252. if isNumber(angles):
  1253. angles = (angles,) #turn into a sequence
  1254. if isNumber(spacings):
  1255. spacings = (spacings,) #turn into a sequence
  1256. i = len(angles)-len(spacings)
  1257. if i>0:
  1258. spacings = list(spacings)+i*[spacings[-1]]
  1259. self.xyLists = xyLists
  1260. self.angles = angles
  1261. self.spacings = spacings
  1262. moveTo = self.moveTo
  1263. lineTo = self.lineTo
  1264. for i, theta in enumerate(angles):
  1265. spacing = spacings[i]
  1266. theta = radians(theta)
  1267. cosTheta = cos(theta)
  1268. sinTheta = sin(theta)
  1269. spanMin = 0x7fffffff
  1270. spanMax = -spanMin
  1271. # Loop to determine the span over which diagonal lines must be drawn.
  1272. for P in xyLists:
  1273. for j in range(0,len(P),2):
  1274. # rotated point, since the stripes may be at an angle.
  1275. y = P[j+1]*cosTheta-P[j]*sinTheta
  1276. spanMin = min(y,spanMin)
  1277. spanMax = max(y,spanMax)
  1278. # Turn the span into a discrete step range.
  1279. spanStart = int(floor(spanMin/spacing)) - 1
  1280. spanEnd = int(floor(spanMax/spacing)) + 1
  1281. # Loop to create all stripes.
  1282. for step in range(spanStart,spanEnd):
  1283. nodeX = []
  1284. stripeY = spacing*step
  1285. # Loop to build a node list for one row of stripes.
  1286. for P in xyLists:
  1287. k = len(P)-2 #start by comparing with the last point
  1288. for j in range(0,len(P),2):
  1289. a = P[k]
  1290. b = P[k+1]
  1291. a1 = a*cosTheta + b*sinTheta
  1292. b1 = b*cosTheta - a*sinTheta
  1293. x = P[j]
  1294. y = P[j+1]
  1295. x1 = x*cosTheta+y*sinTheta
  1296. y1 = y*cosTheta-x*sinTheta
  1297. # Find the node, if any.
  1298. if (b1<stripeY and y1>=stripeY) or y1<stripeY and b1>=stripeY:
  1299. nodeX.append(a1+(x1-a1)*(stripeY-b1)/(y1-b1))
  1300. k = j
  1301. nodeX.sort()
  1302. # Loop to draw one row of line segments.
  1303. for j in range(0,len(nodeX),2):
  1304. # Rotate the points back to their original coordinate system.
  1305. a = nodeX[j]*cosTheta - stripeY*sinTheta
  1306. b = stripeY*cosTheta+nodeX[j]*sinTheta
  1307. x = nodeX[j+1]*cosTheta - stripeY*sinTheta
  1308. y = stripeY*cosTheta + nodeX[j+1]*sinTheta
  1309. #Draw a single stripe segment.
  1310. moveTo(a,b)
  1311. lineTo(x,y)
  1312. def numericXShift(tA,text,w,fontName,fontSize,encoding=None,pivotCharacter=decimalSymbol):
  1313. dp = getattr(tA,'_dp',pivotCharacter)
  1314. i = text.rfind(dp)
  1315. if i>=0:
  1316. dpOffs = getattr(tA,'_dpLen',0)
  1317. w = dpOffs + stringWidth(text[:i],fontName,fontSize,encoding)
  1318. return w
  1319. class String(Shape):
  1320. """Not checked against the spec, just a way to make something work.
  1321. Can be anchored left, middle or end."""
  1322. # to do.
  1323. _attrMap = AttrMap(
  1324. x = AttrMapValue(isNumber,desc="x point of anchoring"),
  1325. y = AttrMapValue(isNumber,desc="y point of anchoring"),
  1326. text = AttrMapValue(isString,desc="the text of the string"),
  1327. fontName = AttrMapValue(None,desc="font name of the text - font is either acrobat standard or registered when using external font."),
  1328. fontSize = AttrMapValue(isNumber,desc="font size"),
  1329. fillColor = AttrMapValue(isColorOrNone,desc="color of the font"),
  1330. textAnchor = AttrMapValue(OneOf('start','middle','end','numeric'),desc="treat (x,y) as one of the options below."),
  1331. encoding = AttrMapValue(isString),
  1332. textRenderMode = AttrMapValue(OneOf(0,1,2,3,4,5,6,7),desc="Control whether text is filled/stroked etc etc"),
  1333. )
  1334. encoding = 'utf8'
  1335. def __init__(self, x, y, text, **kw):
  1336. self.x = x
  1337. self.y = y
  1338. self.text = text
  1339. self.textAnchor = 'start'
  1340. self.fontName = STATE_DEFAULTS['fontName']
  1341. self.fontSize = STATE_DEFAULTS['fontSize']
  1342. self.fillColor = STATE_DEFAULTS['fillColor']
  1343. self.setProperties(kw)
  1344. def getEast(self):
  1345. return self.x + stringWidth(self.text,self.fontName,self.fontSize, self.encoding)
  1346. def copy(self):
  1347. new = self.__class__(self.x, self.y, self.text)
  1348. new.setProperties(self.getProperties())
  1349. return new
  1350. def getBounds(self):
  1351. # assumes constant drop of 0.2*size to baseline
  1352. t = self.text
  1353. w = stringWidth(t,self.fontName,self.fontSize,self.encoding)
  1354. tA = self.textAnchor
  1355. x = self.x
  1356. if tA!='start':
  1357. if tA=='middle':
  1358. x -= 0.5*w
  1359. elif tA=='end':
  1360. x -= w
  1361. elif tA=='numeric':
  1362. x -= numericXShift(tA,t,w,self.fontName,self.fontSize,self.encoding)
  1363. return (x, self.y - 0.2 * self.fontSize, x+w, self.y + self.fontSize)
  1364. class UserNode(_DrawTimeResizeable):
  1365. """A simple template for creating a new node. The user (Python
  1366. programmer) may subclasses this. provideNode() must be defined to
  1367. provide a Shape primitive when called by a renderer. It does
  1368. NOT inherit from Shape, as the renderer always replaces it, and
  1369. your own classes can safely inherit from it without getting
  1370. lots of unintended behaviour."""
  1371. def provideNode(self):
  1372. """Override this to create your own node. This lets widgets be
  1373. added to drawings; they must create a shape (typically a group)
  1374. so that the renderer can draw the custom node."""
  1375. raise NotImplementedError("this method must be redefined by the user/programmer")
  1376. class DirectDraw(Shape):
  1377. """try to draw directly on the canvas"""
  1378. def drawDirectly(self,canvas):
  1379. raise NotImplementedError("this method must be redefined by the user/programmer")
  1380. def test():
  1381. r = Rect(10,10,200,50)
  1382. import pprint
  1383. pp = pprint.pprint
  1384. w = sys.stdout.write
  1385. w('a Rectangle: ')
  1386. pp(r.getProperties())
  1387. w('\nverifying...')
  1388. r.verify()
  1389. w(' OK\n')
  1390. #print 'setting rect.z = "spam"'
  1391. #r.z = 'spam'
  1392. w('deleting rect.width ')
  1393. del r.width
  1394. w('verifying...')
  1395. r.verify()
  1396. if __name__=='__main__':
  1397. test()