renderPM.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799
  1. #Copyright ReportLab Europe Ltd. 2000-2017
  2. #see license.txt for license details
  3. #history www.reportlab.co.uk/rl-cgi/viewcvs.cgi/rlextra/graphics/Csrc/renderPM/renderP.py
  4. __version__='3.3.0'
  5. __doc__="""Render drawing objects in common bitmap formats
  6. Usage::
  7. from reportlab.graphics import renderPM
  8. renderPM.drawToFile(drawing,filename,fmt='GIF',configPIL={....})
  9. Other functions let you create a PM drawing as string or into a PM buffer.
  10. Execute the script to see some test drawings."""
  11. from reportlab.graphics.shapes import *
  12. from reportlab.graphics.renderbase import StateTracker, getStateDelta, renderScaledDrawing
  13. from reportlab.pdfbase.pdfmetrics import getFont, unicode2T1
  14. from math import sin, cos, pi, ceil
  15. from reportlab.lib.utils import getStringIO, getBytesIO, open_and_read, isUnicode
  16. from reportlab import rl_config
  17. from .utils import setFont as _setFont, RenderPMError
  18. import os, sys
  19. try:
  20. from reportlab.graphics import _renderPM
  21. except ImportError as errMsg:
  22. raise ImportError("""No module named _renderPM\nit may be badly or not installed!
  23. You may need to install development tools
  24. or seek advice at the users list see
  25. https://pairlist2.pair.net/mailman/listinfo/reportlab-users""")
  26. def _getImage():
  27. try:
  28. from PIL import Image
  29. except ImportError:
  30. import Image
  31. return Image
  32. def Color2Hex(c):
  33. #assert isinstance(colorobj, colors.Color) #these checks don't work well RGB
  34. if c: return ((0xFF&int(255*c.red)) << 16) | ((0xFF&int(255*c.green)) << 8) | (0xFF&int(255*c.blue))
  35. return c
  36. # the main entry point for users...
  37. def draw(drawing, canvas, x, y, showBoundary=rl_config._unset_):
  38. """As it says"""
  39. R = _PMRenderer()
  40. R.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary)
  41. from reportlab.graphics.renderbase import Renderer
  42. class _PMRenderer(Renderer):
  43. """This draws onto a pix map image. It needs to be a class
  44. rather than a function, as some image-specific state tracking is
  45. needed outside of the state info in the SVG model."""
  46. def pop(self):
  47. self._tracker.pop()
  48. self.applyState()
  49. def push(self,node):
  50. deltas = getStateDelta(node)
  51. self._tracker.push(deltas)
  52. self.applyState()
  53. def applyState(self):
  54. s = self._tracker.getState()
  55. self._canvas.ctm = s['ctm']
  56. self._canvas.strokeWidth = s['strokeWidth']
  57. alpha = s['strokeOpacity']
  58. if alpha is not None:
  59. self._canvas.strokeOpacity = alpha
  60. self._canvas.setStrokeColor(s['strokeColor'])
  61. self._canvas.lineCap = s['strokeLineCap']
  62. self._canvas.lineJoin = s['strokeLineJoin']
  63. self._canvas.fillMode = s['fillMode']
  64. da = s['strokeDashArray']
  65. if not da:
  66. da = None
  67. else:
  68. if not isinstance(da,(list,tuple)):
  69. da = da,
  70. if len(da)!=2 or not isinstance(da[1],(list,tuple)):
  71. da = 0, da #assume phase of 0
  72. self._canvas.dashArray = da
  73. alpha = s['fillOpacity']
  74. if alpha is not None:
  75. self._canvas.fillOpacity = alpha
  76. self._canvas.setFillColor(s['fillColor'])
  77. self._canvas.setFont(s['fontName'], s['fontSize'])
  78. def initState(self,x,y):
  79. deltas = self._tracker._combined[-1]
  80. deltas['transform'] = self._canvas._baseCTM[0:4]+(x,y)
  81. self._tracker.push(deltas)
  82. self.applyState()
  83. def drawNode(self, node):
  84. """This is the recursive method called for each node
  85. in the tree"""
  86. #apply state changes
  87. self.push(node)
  88. #draw the object, or recurse
  89. self.drawNodeDispatcher(node)
  90. # restore the state
  91. self.pop()
  92. def drawRect(self, rect):
  93. c = self._canvas
  94. if rect.rx == rect.ry == 0:
  95. #plain old rectangle, draw clockwise (x-axis to y-axis) direction
  96. c.rect(rect.x,rect.y, rect.width, rect.height)
  97. else:
  98. c.roundRect(rect.x,rect.y, rect.width, rect.height, rect.rx, rect.ry)
  99. def drawLine(self, line):
  100. self._canvas.line(line.x1,line.y1,line.x2,line.y2)
  101. def drawImage(self, image):
  102. path = image.path
  103. if isinstance(path,str):
  104. if not (path and os.path.isfile(path)): return
  105. im = _getImage().open(path).convert('RGB')
  106. elif hasattr(path,'convert'):
  107. im = path.convert('RGB')
  108. else:
  109. return
  110. srcW, srcH = im.size
  111. dstW, dstH = image.width, image.height
  112. if dstW is None: dstW = srcW
  113. if dstH is None: dstH = srcH
  114. self._canvas._aapixbuf(
  115. image.x, image.y, dstW, dstH,
  116. (im if self._canvas._backend=='rlPyCairo' #rlPyCairo has a from_pil method
  117. else (im.tobytes if hasattr(im,'tobytes') else im.tostring)()),
  118. srcW, srcH, 3,
  119. )
  120. def drawCircle(self, circle):
  121. c = self._canvas
  122. c.circle(circle.cx,circle.cy, circle.r)
  123. c.fillstrokepath()
  124. def drawPolyLine(self, polyline, _doClose=0):
  125. P = polyline.points
  126. assert len(P) >= 2, 'Polyline must have 1 or more points'
  127. c = self._canvas
  128. c.pathBegin()
  129. c.moveTo(P[0], P[1])
  130. for i in range(2, len(P), 2):
  131. c.lineTo(P[i], P[i+1])
  132. if _doClose:
  133. c.pathClose()
  134. c.pathFill()
  135. c.pathStroke()
  136. def drawEllipse(self, ellipse):
  137. c=self._canvas
  138. c.ellipse(ellipse.cx, ellipse.cy, ellipse.rx,ellipse.ry)
  139. c.fillstrokepath()
  140. def drawPolygon(self, polygon):
  141. self.drawPolyLine(polygon,_doClose=1)
  142. def drawString(self, stringObj):
  143. canv = self._canvas
  144. fill = canv.fillColor
  145. textRenderMode = getattr(stringObj,'textRenderMode',0)
  146. if fill is not None or textRenderMode:
  147. S = self._tracker.getState()
  148. text_anchor = S['textAnchor']
  149. fontName = S['fontName']
  150. fontSize = S['fontSize']
  151. text = stringObj.text
  152. x = stringObj.x
  153. y = stringObj.y
  154. if not text_anchor in ['start','inherited']:
  155. textLen = stringWidth(text, fontName,fontSize)
  156. if text_anchor=='end':
  157. x -= textLen
  158. elif text_anchor=='middle':
  159. x -= textLen/2
  160. elif text_anchor=='numeric':
  161. x -= numericXShift(text_anchor,text,textLen,fontName,fontSize,stringObj.encoding)
  162. else:
  163. raise ValueError('bad value for textAnchor '+str(text_anchor))
  164. oldTextRenderMode = canv.textRenderMode
  165. canv.textRenderMode = textRenderMode
  166. try:
  167. canv.drawString(x,y,text,_fontInfo=(fontName,fontSize))
  168. finally:
  169. canv.textRenderMode = oldTextRenderMode
  170. def drawPath(self, path):
  171. c = self._canvas
  172. if path is EmptyClipPath:
  173. del c._clipPaths[-1]
  174. if c._clipPaths:
  175. P = c._clipPaths[-1]
  176. icp = P.isClipPath
  177. P.isClipPath = 1
  178. self.drawPath(P)
  179. P.isClipPath = icp
  180. else:
  181. c.clipPathClear()
  182. return
  183. from reportlab.graphics.shapes import _renderPath
  184. drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.pathClose)
  185. autoclose = getattr(path,'autoclose','')
  186. def rP(forceClose=False):
  187. c.pathBegin()
  188. return _renderPath(path, drawFuncs, forceClose=forceClose)
  189. if path.isClipPath:
  190. rP()
  191. c.clipPathSet()
  192. c._clipPaths.append(path)
  193. fill = c.fillColor is not None
  194. stroke = c.strokeColor is not None
  195. fillMode = getattr(path,'fillMode',-1)
  196. if autoclose=='svg':
  197. if fill and stroke:
  198. rP(forceClose=True)
  199. c.pathFill(fillMode)
  200. rP()
  201. c.pathStroke()
  202. elif fill:
  203. rP(forceClose=True)
  204. c.pathFill(fillMode)
  205. elif stroke:
  206. rP()
  207. c.pathStroke()
  208. elif autoclose=='pdf':
  209. rP(forceClose=True)
  210. if fill:
  211. c.pathFill(fillMode)
  212. if stroke:
  213. c.pathStroke()
  214. else:
  215. if rP():
  216. c.pathFill(fillMode)
  217. c.pathStroke()
  218. def _convert2pilp(im):
  219. Image = _getImage()
  220. return im.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE)
  221. def _convert2pilL(im):
  222. return im.convert("L")
  223. def _convert2pil1(im):
  224. return im.convert("1")
  225. def _saveAsPICT(im,fn,fmt,transparent=None):
  226. im = _convert2pilp(im)
  227. cols, rows = im.size
  228. #s = _renderPM.pil2pict(cols,rows,im.tostring(),im.im.getpalette(),transparent is not None and Color2Hex(transparent) or -1)
  229. s = _renderPM.pil2pict(cols,rows,(im.tobytes if hasattr(im,'tobytes') else im.tostring)(),im.im.getpalette())
  230. if not hasattr(fn,'write'):
  231. with open(os.path.splitext(fn)[0]+'.'+fmt.lower(),'wb') as f:
  232. f.write(s)
  233. if os.name=='mac':
  234. from reportlab.lib.utils import markfilename
  235. markfilename(fn,ext='PICT')
  236. else:
  237. fn.write(s)
  238. BEZIER_ARC_MAGIC = 0.5522847498 #constant for drawing circular arcs w/ Beziers
  239. class PMCanvas:
  240. def __init__(self,w,h,dpi=72,bg=0xffffff,configPIL=None,backend=rl_config.renderPMBackend):
  241. '''configPIL dict is passed to image save method'''
  242. scale = dpi/72.0
  243. w = int(w*scale+0.5)
  244. h = int(h*scale+0.5)
  245. self.__dict__['_gs'] = self._getGState(w,h,bg,backend)
  246. self.__dict__['_bg'] = bg
  247. self.__dict__['_baseCTM'] = (scale,0,0,scale,0,0)
  248. self.__dict__['_clipPaths'] = []
  249. self.__dict__['configPIL'] = configPIL
  250. self.__dict__['_dpi'] = dpi
  251. self.__dict__['_backend'] = backend
  252. self.ctm = self._baseCTM
  253. @staticmethod
  254. def _getGState(w, h, bg, backend='_renderPM', fmt='RGB24'):
  255. if backend=='_renderPM':
  256. return _renderPM.gstate(w,h,bg=bg)
  257. elif backend=='rlPyCairo':
  258. try:
  259. from rlPyCairo import GState
  260. except ImportError:
  261. raise RenderPMError('cannot import rlPyCairo; perhaps it needs to be installed!')
  262. else:
  263. return GState(w,h,bg,fmt=fmt)
  264. else:
  265. raise RenderPMError('Invalid backend, %r, given to PMCanvas' % backend)
  266. def _drawTimeResize(self,w,h,bg=None):
  267. if bg is None: bg = self._bg
  268. self._drawing.width, self._drawing.height = w, h
  269. A = {'ctm':None, 'strokeWidth':None, 'strokeColor':None, 'lineCap':None, 'lineJoin':None, 'dashArray':None, 'fillColor':None}
  270. gs = self._gs
  271. fN,fS = gs.fontName, gs.fontSize
  272. for k in A.keys():
  273. A[k] = getattr(gs,k)
  274. del gs, self._gs
  275. gs = self.__dict__['_gs'] = _renderPM.gstate(w,h,bg=bg)
  276. for k in A.keys():
  277. setattr(self,k,A[k])
  278. gs.setFont(fN,fS)
  279. def toPIL(self):
  280. im = _getImage().new('RGB', size=(self._gs.width, self._gs.height))
  281. (getattr(im,'frombytes',None) or getattr(im,'fromstring'))(self._gs.pixBuf)
  282. return im
  283. def saveToFile(self,fn,fmt=None):
  284. im = self.toPIL()
  285. if fmt is None:
  286. if not isinstance(fn,str):
  287. raise ValueError("Invalid value '%s' for fn when fmt is None" % ascii(fn))
  288. fmt = os.path.splitext(fn)[1]
  289. if fmt.startswith('.'): fmt = fmt[1:]
  290. configPIL = self.configPIL or {}
  291. configPIL.setdefault('preConvertCB',None)
  292. preConvertCB=configPIL.pop('preConvertCB')
  293. if preConvertCB:
  294. im = preConvertCB(im)
  295. fmt = fmt.upper()
  296. if fmt in ('GIF',):
  297. im = _convert2pilp(im)
  298. elif fmt in ('TIFF','TIFFP','TIFFL','TIF','TIFF1'):
  299. if fmt.endswith('P'):
  300. im = _convert2pilp(im)
  301. elif fmt.endswith('L'):
  302. im = _convert2pilL(im)
  303. elif fmt.endswith('1'):
  304. im = _convert2pil1(im)
  305. fmt='TIFF'
  306. elif fmt in ('PCT','PICT'):
  307. return _saveAsPICT(im,fn,fmt,transparent=configPIL.get('transparent',None))
  308. elif fmt in ('PNG','BMP', 'PPM'):
  309. if fmt=='PNG':
  310. try:
  311. from PIL import PngImagePlugin
  312. except ImportError:
  313. import PngImagePlugin
  314. elif fmt=='BMP':
  315. try:
  316. from PIL import BmpImagePlugin
  317. except ImportError:
  318. import BmpImagePlugin
  319. elif fmt in ('JPG','JPEG'):
  320. fmt = 'JPEG'
  321. elif fmt in ('GIF',):
  322. pass
  323. else:
  324. raise RenderPMError("Unknown image kind %s" % fmt)
  325. if fmt=='TIFF':
  326. tc = configPIL.get('transparent',None)
  327. if tc:
  328. from PIL import ImageChops, Image
  329. T = 768*[0]
  330. for o, c in zip((0,256,512), tc.bitmap_rgb()):
  331. T[o+c] = 255
  332. #if isinstance(fn,str): ImageChops.invert(im.point(T).convert('L').point(255*[0]+[255])).save(fn+'_mask.gif','GIF')
  333. im = Image.merge('RGBA', im.split()+(ImageChops.invert(im.point(T).convert('L').point(255*[0]+[255])),))
  334. #if isinstance(fn,str): im.save(fn+'_masked.gif','GIF')
  335. for a,d in ('resolution',self._dpi),('resolution unit','inch'):
  336. configPIL[a] = configPIL.get(a,d)
  337. configPIL.setdefault('chops_invert',0)
  338. if configPIL.pop('chops_invert'):
  339. from PIL import ImageChops
  340. im = ImageChops.invert(im)
  341. configPIL.setdefault('preSaveCB',None)
  342. preSaveCB=configPIL.pop('preSaveCB')
  343. if preSaveCB:
  344. im = preSaveCB(im)
  345. im.save(fn,fmt,**configPIL)
  346. if not hasattr(fn,'write') and os.name=='mac':
  347. from reportlab.lib.utils import markfilename
  348. markfilename(fn,ext=fmt)
  349. def saveToString(self,fmt='GIF'):
  350. s = getBytesIO()
  351. self.saveToFile(s,fmt=fmt)
  352. return s.getvalue()
  353. def _saveToBMP(self,f):
  354. '''
  355. Niki Spahiev, <niki@vintech.bg>, asserts that this is a respectable way to get BMP without PIL
  356. f is a file like object to which the BMP is written
  357. '''
  358. import struct
  359. gs = self._gs
  360. pix, width, height = gs.pixBuf, gs.width, gs.height
  361. f.write(struct.pack('=2sLLLLLLhh24x','BM',len(pix)+54,0,54,40,width,height,1,24))
  362. rowb = width * 3
  363. for o in range(len(pix),0,-rowb):
  364. f.write(pix[o-rowb:o])
  365. f.write( '\0' * 14 )
  366. def setFont(self,fontName,fontSize,leading=None):
  367. _setFont(self._gs,fontName,fontSize)
  368. def __setattr__(self,name,value):
  369. setattr(self._gs,name,value)
  370. def __getattr__(self,name):
  371. return getattr(self._gs,name)
  372. def fillstrokepath(self,stroke=1,fill=1):
  373. if fill: self.pathFill()
  374. if stroke: self.pathStroke()
  375. def _bezierArcSegmentCCW(self, cx,cy, rx,ry, theta0, theta1):
  376. """compute the control points for a bezier arc with theta1-theta0 <= 90.
  377. Points are computed for an arc with angle theta increasing in the
  378. counter-clockwise (CCW) direction. returns a tuple with starting point
  379. and 3 control points of a cubic bezier curve for the curvto opertator"""
  380. # Requires theta1 - theta0 <= 90 for a good approximation
  381. assert abs(theta1 - theta0) <= 90
  382. cos0 = cos(pi*theta0/180.0)
  383. sin0 = sin(pi*theta0/180.0)
  384. x0 = cx + rx*cos0
  385. y0 = cy + ry*sin0
  386. cos1 = cos(pi*theta1/180.0)
  387. sin1 = sin(pi*theta1/180.0)
  388. x3 = cx + rx*cos1
  389. y3 = cy + ry*sin1
  390. dx1 = -rx * sin0
  391. dy1 = ry * cos0
  392. #from pdfgeom
  393. halfAng = pi*(theta1-theta0)/(2.0 * 180.0)
  394. k = abs(4.0 / 3.0 * (1.0 - cos(halfAng) ) /(sin(halfAng)) )
  395. x1 = x0 + dx1 * k
  396. y1 = y0 + dy1 * k
  397. dx2 = -rx * sin1
  398. dy2 = ry * cos1
  399. x2 = x3 - dx2 * k
  400. y2 = y3 - dy2 * k
  401. return ((x0,y0), ((x1,y1), (x2,y2), (x3,y3)) )
  402. def bezierArcCCW(self, cx,cy, rx,ry, theta0, theta1):
  403. """return a set of control points for Bezier approximation to an arc
  404. with angle increasing counter clockwise. No requirement on (theta1-theta0) <= 90
  405. However, it must be true that theta1-theta0 > 0."""
  406. # I believe this is also clockwise
  407. # pretty much just like Robert Kern's pdfgeom.BezierArc
  408. angularExtent = theta1 - theta0
  409. # break down the arc into fragments of <=90 degrees
  410. if abs(angularExtent) <= 90.0: # we just need one fragment
  411. angleList = [(theta0,theta1)]
  412. else:
  413. Nfrag = int( ceil( abs(angularExtent)/90.) )
  414. fragAngle = float(angularExtent)/ Nfrag # this could be negative
  415. angleList = []
  416. for ii in range(Nfrag):
  417. a = theta0 + ii * fragAngle
  418. b = a + fragAngle # hmm.. is I wonder if this is precise enought
  419. angleList.append((a,b))
  420. ctrlpts = []
  421. for (a,b) in angleList:
  422. if not ctrlpts: # first time
  423. [(x0,y0), pts] = self._bezierArcSegmentCCW(cx,cy, rx,ry, a,b)
  424. ctrlpts.append(pts)
  425. else:
  426. [(tmpx,tmpy), pts] = self._bezierArcSegmentCCW(cx,cy, rx,ry, a,b)
  427. ctrlpts.append(pts)
  428. return ((x0,y0), ctrlpts)
  429. def addEllipsoidalArc(self, cx,cy, rx, ry, ang1, ang2):
  430. """adds an ellisesoidal arc segment to a path, with an ellipse centered
  431. on cx,cy and with radii (major & minor axes) rx and ry. The arc is
  432. drawn in the CCW direction. Requires: (ang2-ang1) > 0"""
  433. ((x0,y0), ctrlpts) = self.bezierArcCCW(cx,cy, rx,ry,ang1,ang2)
  434. self.lineTo(x0,y0)
  435. for ((x1,y1), (x2,y2),(x3,y3)) in ctrlpts:
  436. self.curveTo(x1,y1,x2,y2,x3,y3)
  437. def drawCentredString(self, x, y, text, text_anchor='middle'):
  438. self.drawString(x,y,text, text_anchor=text_anchor)
  439. def drawRightString(self, text, x, y):
  440. self.drawString(text,x,y,text_anchor='end')
  441. def drawString(self, x, y, text, _fontInfo=None, text_anchor='left'):
  442. gs = self._gs
  443. gs_fontSize = gs.fontSize
  444. gs_fontName = gs.fontName
  445. if _fontInfo and _fontInfo!=(gs_fontSize,gs_fontName):
  446. fontName, fontSize = _fontInfo
  447. _setFont(gs,fontName,fontSize)
  448. else:
  449. fontName = gs_fontName
  450. fontSize = gs_fontSize
  451. try:
  452. if text_anchor in ('end','middle', 'end'):
  453. textLen = stringWidth(text, fontName,fontSize)
  454. if text_anchor=='end':
  455. x -= textLen
  456. elif text_anchor=='middle':
  457. x -= textLen/2.
  458. elif text_anchor=='numeric':
  459. x -= numericXShift(text_anchor,text,textLen,fontName,fontSize)
  460. if self._backend=='rlPyCairo':
  461. gs.drawString(x,y,text)
  462. else:
  463. font = getFont(fontName)
  464. if font._dynamicFont:
  465. gs.drawString(x,y,text)
  466. else:
  467. fc = font
  468. if not isUnicode(text):
  469. try:
  470. text = text.decode('utf8')
  471. except UnicodeDecodeError as e:
  472. i,j = e.args[2:4]
  473. raise UnicodeDecodeError(*(e.args[:4]+('%s\n%s-->%s<--%s' % (e.args[4],text[i-10:i],text[i:j],text[j:j+10]),)))
  474. FT = unicode2T1(text,[font]+font.substitutionFonts)
  475. n = len(FT)
  476. nm1 = n-1
  477. for i in range(n):
  478. f, t = FT[i]
  479. if f!=fc:
  480. _setFont(gs,f.fontName,fontSize)
  481. fc = f
  482. gs.drawString(x,y,t)
  483. if i!=nm1:
  484. x += f.stringWidth(t.decode(f.encName),fontSize)
  485. finally:
  486. gs.setFont(gs_fontName,gs_fontSize)
  487. def line(self,x1,y1,x2,y2):
  488. if self.strokeColor is not None:
  489. self.pathBegin()
  490. self.moveTo(x1,y1)
  491. self.lineTo(x2,y2)
  492. self.pathStroke()
  493. def rect(self,x,y,width,height,stroke=1,fill=1):
  494. self.pathBegin()
  495. self.moveTo(x, y)
  496. self.lineTo(x+width, y)
  497. self.lineTo(x+width, y + height)
  498. self.lineTo(x, y + height)
  499. self.pathClose()
  500. self.fillstrokepath(stroke=stroke,fill=fill)
  501. def roundRect(self, x, y, width, height, rx,ry):
  502. """rect(self, x, y, width, height, rx,ry):
  503. Draw a rectangle if rx or rx and ry are specified the corners are
  504. rounded with ellipsoidal arcs determined by rx and ry
  505. (drawn in the counter-clockwise direction)"""
  506. if rx==0: rx = ry
  507. if ry==0: ry = rx
  508. x2 = x + width
  509. y2 = y + height
  510. self.pathBegin()
  511. self.moveTo(x+rx,y)
  512. self.addEllipsoidalArc(x2-rx, y+ry, rx, ry, 270, 360 )
  513. self.addEllipsoidalArc(x2-rx, y2-ry, rx, ry, 0, 90)
  514. self.addEllipsoidalArc(x+rx, y2-ry, rx, ry, 90, 180)
  515. self.addEllipsoidalArc(x+rx, y+ry, rx, ry, 180, 270)
  516. self.pathClose()
  517. self.fillstrokepath()
  518. def circle(self, cx, cy, r):
  519. "add closed path circle with center cx,cy and axes r: counter-clockwise orientation"
  520. self.ellipse(cx,cy,r,r)
  521. def ellipse(self, cx,cy,rx,ry):
  522. """add closed path ellipse with center cx,cy and axes rx,ry: counter-clockwise orientation
  523. (remember y-axis increases downward) """
  524. self.pathBegin()
  525. # first segment
  526. x0 = cx + rx # (x0,y0) start pt
  527. y0 = cy
  528. x3 = cx # (x3,y3) end pt of arc
  529. y3 = cy-ry
  530. x1 = cx+rx
  531. y1 = cy-ry*BEZIER_ARC_MAGIC
  532. x2 = x3 + rx*BEZIER_ARC_MAGIC
  533. y2 = y3
  534. self.moveTo(x0, y0)
  535. self.curveTo(x1,y1,x2,y2,x3,y3)
  536. # next segment
  537. x0 = x3
  538. y0 = y3
  539. x3 = cx-rx
  540. y3 = cy
  541. x1 = cx-rx*BEZIER_ARC_MAGIC
  542. y1 = cy-ry
  543. x2 = x3
  544. y2 = cy- ry*BEZIER_ARC_MAGIC
  545. self.curveTo(x1,y1,x2,y2,x3,y3)
  546. # next segment
  547. x0 = x3
  548. y0 = y3
  549. x3 = cx
  550. y3 = cy+ry
  551. x1 = cx-rx
  552. y1 = cy+ry*BEZIER_ARC_MAGIC
  553. x2 = cx -rx*BEZIER_ARC_MAGIC
  554. y2 = cy+ry
  555. self.curveTo(x1,y1,x2,y2,x3,y3)
  556. #last segment
  557. x0 = x3
  558. y0 = y3
  559. x3 = cx+rx
  560. y3 = cy
  561. x1 = cx+rx*BEZIER_ARC_MAGIC
  562. y1 = cy+ry
  563. x2 = cx+rx
  564. y2 = cy+ry*BEZIER_ARC_MAGIC
  565. self.curveTo(x1,y1,x2,y2,x3,y3)
  566. self.pathClose()
  567. def saveState(self):
  568. '''do nothing for compatibility'''
  569. pass
  570. def setFillColor(self,aColor):
  571. self.fillColor = Color2Hex(aColor)
  572. alpha = getattr(aColor,'alpha',None)
  573. if alpha is not None:
  574. self.fillOpacity = alpha
  575. def setStrokeColor(self,aColor):
  576. self.strokeColor = Color2Hex(aColor)
  577. alpha = getattr(aColor,'alpha',None)
  578. if alpha is not None:
  579. self.strokeOpacity = alpha
  580. restoreState = saveState
  581. # compatibility routines
  582. def setLineCap(self,cap):
  583. self.lineCap = cap
  584. def setLineJoin(self,join):
  585. self.lineJoin = join
  586. def setLineWidth(self,width):
  587. self.strokeWidth = width
  588. def drawToPMCanvas(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_,backend=rl_config.renderPMBackend):
  589. d = renderScaledDrawing(d)
  590. c = PMCanvas(d.width, d.height, dpi=dpi, bg=bg, configPIL=configPIL, backend=backend)
  591. draw(d, c, 0, 0, showBoundary=showBoundary)
  592. return c
  593. def drawToPIL(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_,backend=rl_config.renderPMBackend):
  594. return drawToPMCanvas(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary, backend=backend).toPIL()
  595. def drawToPILP(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_,backend=rl_config.renderPMBackend):
  596. Image = _getImage()
  597. im = drawToPIL(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary,backend=backend)
  598. return im.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE)
  599. def drawToFile(d,fn,fmt='GIF', dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_,backend=rl_config.renderPMBackend):
  600. '''create a pixmap and draw drawing, d to it then save as a file
  601. configPIL dict is passed to image save method'''
  602. c = drawToPMCanvas(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary,backend=backend)
  603. c.saveToFile(fn,fmt)
  604. def drawToString(d,fmt='GIF', dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_,backend=rl_config.renderPMBackend):
  605. s = getBytesIO()
  606. drawToFile(d,s,fmt=fmt, dpi=dpi, bg=bg, configPIL=configPIL,backend=backend)
  607. return s.getvalue()
  608. save = drawToFile
  609. def test(outDir='pmout', shout=False):
  610. def ext(x):
  611. if x=='tiff': x='tif'
  612. return x
  613. #grab all drawings from the test module and write out.
  614. #make a page of links in HTML to assist viewing.
  615. import os
  616. from reportlab.graphics import testshapes
  617. from reportlab.rl_config import verbose
  618. getAllTestDrawings = testshapes.getAllTestDrawings
  619. drawings = []
  620. if not os.path.isdir(outDir):
  621. os.mkdir(outDir)
  622. htmlTop = """<html><head><title>renderPM output results</title></head>
  623. <body>
  624. <h1>renderPM results of output</h1>
  625. """
  626. htmlBottom = """</body>
  627. </html>
  628. """
  629. html = [htmlTop]
  630. names = {}
  631. argv = sys.argv[1:]
  632. E = [a for a in argv if a.startswith('--ext=')]
  633. if not E:
  634. E = ['gif','tiff', 'png', 'jpg', 'pct', 'py', 'svg']
  635. else:
  636. for a in E:
  637. argv.remove(a)
  638. E = (','.join([a[6:] for a in E])).split(',')
  639. errs = []
  640. import traceback
  641. from xml.sax.saxutils import escape
  642. def handleError(name,fmt):
  643. msg = 'Problem drawing %s fmt=%s file'%(name,fmt)
  644. if shout or verbose>2: print(msg)
  645. errs.append('<br/><h2 style="color:red">%s</h2>' % msg)
  646. buf = getStringIO()
  647. traceback.print_exc(file=buf)
  648. errs.append('<pre>%s</pre>' % escape(buf.getvalue()))
  649. #print in a loop, with their doc strings
  650. for (drawing, docstring, name) in getAllTestDrawings(doTTF=hasattr(_renderPM,'ft_get_face')):
  651. i = names[name] = names.setdefault(name,0)+1
  652. if i>1: name += '.%02d' % (i-1)
  653. if argv and name not in argv: continue
  654. fnRoot = name
  655. w = int(drawing.width)
  656. h = int(drawing.height)
  657. html.append('<hr><h2>Drawing %s</h2>\n<pre>%s</pre>' % (name, docstring))
  658. for k in E:
  659. if k in ['gif','png','jpg','pct']:
  660. html.append('<p>%s format</p>\n' % k.upper())
  661. try:
  662. filename = '%s.%s' % (fnRoot, ext(k))
  663. fullpath = os.path.join(outDir, filename)
  664. if os.path.isfile(fullpath):
  665. os.remove(fullpath)
  666. if k=='pct':
  667. from reportlab.lib.colors import white
  668. drawToFile(drawing,fullpath,fmt=k,configPIL={'transparent':white})
  669. elif k in ['py','svg']:
  670. drawing.save(formats=['py','svg'],outDir=outDir,fnRoot=fnRoot)
  671. else:
  672. drawToFile(drawing,fullpath,fmt=k)
  673. if k in ['gif','png','jpg']:
  674. html.append('<img src="%s" border="1"><br>\n' % filename)
  675. elif k=='py':
  676. html.append('<a href="%s">python source</a><br>\n' % filename)
  677. elif k=='svg':
  678. html.append('<a href="%s">SVG</a><br>\n' % filename)
  679. if shout or verbose>2: print('wrote %s'%ascii(fullpath))
  680. except AttributeError:
  681. handleError(name,k)
  682. if os.environ.get('RL_NOEPSPREVIEW','0')=='1': drawing.__dict__['preview'] = 0
  683. for k in ('eps', 'pdf'):
  684. try:
  685. drawing.save(formats=[k],outDir=outDir,fnRoot=fnRoot)
  686. except:
  687. handleError(name,k)
  688. if errs:
  689. html[0] = html[0].replace('</h1>',' <a href="#errors" style="color: red">(errors)</a></h1>')
  690. html.append('<a name="errors"/>')
  691. html.extend(errs)
  692. html.append(htmlBottom)
  693. htmlFileName = os.path.join(outDir, 'pm-index.html')
  694. with open(htmlFileName, 'w') as f:
  695. f.writelines(html)
  696. if sys.platform=='mac':
  697. from reportlab.lib.utils import markfilename
  698. markfilename(htmlFileName,ext='HTML')
  699. if shout or verbose>2: print('wrote %s' % htmlFileName)
  700. if __name__=='__main__':
  701. test(shout=True)