renderPS.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973
  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/renderPS.py
  4. __version__='3.3.0'
  5. __doc__="""Render drawing objects in Postscript"""
  6. from reportlab.pdfbase.pdfmetrics import getFont, stringWidth, unicode2T1 # for font info
  7. from reportlab.lib.utils import getBytesIO, getStringIO, asBytes, char2int, rawBytes, asNative, isUnicode
  8. from reportlab.lib.rl_accel import fp_str
  9. from reportlab.lib.colors import black
  10. from reportlab.graphics.renderbase import Renderer, StateTracker, getStateDelta, renderScaledDrawing
  11. from reportlab.graphics.shapes import STATE_DEFAULTS
  12. import math
  13. from operator import getitem
  14. from reportlab import rl_config
  15. from reportlab.pdfgen.canvas import FILL_EVEN_ODD, FILL_NON_ZERO
  16. _ESCAPEDICT={}
  17. for c in range(256):
  18. if c<32 or c>=127:
  19. _ESCAPEDICT[c]= '\\%03o' % c
  20. elif c in (ord('\\'),ord('('),ord(')')):
  21. _ESCAPEDICT[c] = '\\'+chr(c)
  22. else:
  23. _ESCAPEDICT[c] = chr(c)
  24. del c
  25. def _escape_and_limit(s):
  26. s = asBytes(s)
  27. R = []
  28. aR = R.append
  29. n = 0
  30. for c in s:
  31. c = _ESCAPEDICT[char2int(c)]
  32. aR(c)
  33. n += len(c)
  34. if n>=200:
  35. n = 0
  36. aR('\\\n')
  37. return ''.join(R)
  38. # we need to create encoding vectors for each font we use, or they will
  39. # come out in Adobe's old StandardEncoding, which NOBODY uses.
  40. PS_WinAnsiEncoding="""
  41. /RE { %def
  42. findfont begin
  43. currentdict dup length dict begin
  44. { %forall
  45. 1 index /FID ne { def } { pop pop } ifelse
  46. } forall
  47. /FontName exch def dup length 0 ne { %if
  48. /Encoding Encoding 256 array copy def
  49. 0 exch { %forall
  50. dup type /nametype eq { %ifelse
  51. Encoding 2 index 2 index put
  52. pop 1 add
  53. }{ %else
  54. exch pop
  55. } ifelse
  56. } forall
  57. } if pop
  58. currentdict dup end end
  59. /FontName get exch definefont pop
  60. } bind def
  61. /WinAnsiEncoding [
  62. 39/quotesingle 96/grave 128/euro 130/quotesinglbase/florin/quotedblbase
  63. /ellipsis/dagger/daggerdbl/circumflex/perthousand
  64. /Scaron/guilsinglleft/OE 145/quoteleft/quoteright
  65. /quotedblleft/quotedblright/bullet/endash/emdash
  66. /tilde/trademark/scaron/guilsinglright/oe/dotlessi
  67. 159/Ydieresis 164/currency 166/brokenbar 168/dieresis/copyright
  68. /ordfeminine 172/logicalnot 174/registered/macron/ring
  69. 177/plusminus/twosuperior/threesuperior/acute/mu
  70. 183/periodcentered/cedilla/onesuperior/ordmasculine
  71. 188/onequarter/onehalf/threequarters 192/Agrave/Aacute
  72. /Acircumflex/Atilde/Adieresis/Aring/AE/Ccedilla
  73. /Egrave/Eacute/Ecircumflex/Edieresis/Igrave/Iacute
  74. /Icircumflex/Idieresis/Eth/Ntilde/Ograve/Oacute
  75. /Ocircumflex/Otilde/Odieresis/multiply/Oslash
  76. /Ugrave/Uacute/Ucircumflex/Udieresis/Yacute/Thorn
  77. /germandbls/agrave/aacute/acircumflex/atilde/adieresis
  78. /aring/ae/ccedilla/egrave/eacute/ecircumflex
  79. /edieresis/igrave/iacute/icircumflex/idieresis
  80. /eth/ntilde/ograve/oacute/ocircumflex/otilde
  81. /odieresis/divide/oslash/ugrave/uacute/ucircumflex
  82. /udieresis/yacute/thorn/ydieresis
  83. ] def
  84. """
  85. class PSCanvas:
  86. def __init__(self,size=(300,300), PostScriptLevel=2):
  87. self.width, self.height = size
  88. xtraState = []
  89. self._xtraState_push = xtraState.append
  90. self._xtraState_pop = xtraState.pop
  91. self.comments = 0
  92. self.code = []
  93. self.code_append = self.code.append
  94. self._sep = '\n'
  95. self._strokeColor = self._fillColor = self._lineWidth = \
  96. self._font = self._fontSize = self._lineCap = \
  97. self._lineJoin = self._color = None
  98. self._fontsUsed = [] # track them as we go
  99. self.setFont(STATE_DEFAULTS['fontName'],STATE_DEFAULTS['fontSize'])
  100. self.setStrokeColor(STATE_DEFAULTS['strokeColor'])
  101. self.setLineCap(2)
  102. self.setLineJoin(0)
  103. self.setLineWidth(1)
  104. self.PostScriptLevel=PostScriptLevel
  105. self._fillMode = FILL_EVEN_ODD
  106. def comment(self,msg):
  107. if self.comments: self.code_append('%'+msg)
  108. def drawImage(self, image, x1,y1, width=None,height=None): # Postscript Level2 version
  109. # select between postscript level 1 or level 2
  110. if self.PostScriptLevel==1:
  111. self._drawImageLevel1(image, x1,y1, width, height)
  112. elif self.PostScriptLevel==2:
  113. self._drawImageLevel2(image, x1, y1, width, height)
  114. else :
  115. raise ValueError('Unsupported Postscript Level %s' % self.PostScriptLevel)
  116. def clear(self):
  117. self.code_append('showpage') # ugh, this makes no sense oh well.
  118. def _t1_re_encode(self):
  119. if not self._fontsUsed: return
  120. # for each font used, reencode the vectors
  121. C = []
  122. for fontName in self._fontsUsed:
  123. fontObj = getFont(fontName)
  124. if not fontObj._dynamicFont and fontObj.encName=='WinAnsiEncoding':
  125. C.append('WinAnsiEncoding /%s /%s RE' % (fontName, fontName))
  126. if C:
  127. C.insert(0,PS_WinAnsiEncoding)
  128. self.code.insert(1, self._sep.join(C))
  129. def save(self,f=None):
  130. if not hasattr(f,'write'):
  131. _f = open(f,'wb')
  132. else:
  133. _f = f
  134. if self.code[-1]!='showpage': self.clear()
  135. self.code.insert(0,'''\
  136. %%!PS-Adobe-3.0 EPSF-3.0
  137. %%%%BoundingBox: 0 0 %d %d
  138. %%%% Initialization:
  139. /m {moveto} bind def
  140. /l {lineto} bind def
  141. /c {curveto} bind def
  142. ''' % (self.width,self.height))
  143. self._t1_re_encode()
  144. _f.write(rawBytes(self._sep.join(self.code)))
  145. if _f is not f:
  146. _f.close()
  147. from reportlab.lib.utils import markfilename
  148. markfilename(f,creatorcode='XPR3',filetype='EPSF')
  149. def saveState(self):
  150. self._xtraState_push((self._fontCodeLoc,))
  151. self.code_append('gsave')
  152. def restoreState(self):
  153. self.code_append('grestore')
  154. self._fontCodeLoc, = self._xtraState_pop()
  155. def stringWidth(self, s, font=None, fontSize=None):
  156. """Return the logical width of the string if it were drawn
  157. in the current font (defaults to self.font)."""
  158. font = font or self._font
  159. fontSize = fontSize or self._fontSize
  160. return stringWidth(s, font, fontSize)
  161. def setLineCap(self,v):
  162. if self._lineCap!=v:
  163. self._lineCap = v
  164. self.code_append('%d setlinecap'%v)
  165. def setLineJoin(self,v):
  166. if self._lineJoin!=v:
  167. self._lineJoin = v
  168. self.code_append('%d setlinejoin'%v)
  169. def setDash(self, array=[], phase=0):
  170. """Two notations. pass two numbers, or an array and phase"""
  171. # copied and modified from reportlab.canvas
  172. psoperation = "setdash"
  173. if isinstance(array,(float,int)):
  174. self.code_append('[%s %s] 0 %s' % (array, phase, psoperation))
  175. elif isinstance(array,(tuple,list)):
  176. assert phase >= 0, "phase is a length in user space"
  177. textarray = ' '.join(map(str, array))
  178. self.code_append('[%s] %s %s' % (textarray, phase, psoperation))
  179. def setStrokeColor(self, color):
  180. self._strokeColor = color
  181. self.setColor(color)
  182. def setColor(self, color):
  183. if self._color!=color:
  184. self._color = color
  185. if color:
  186. if hasattr(color, "cyan"):
  187. self.code_append('%s setcmykcolor' % fp_str(color.cyan, color.magenta, color.yellow, color.black))
  188. else:
  189. self.code_append('%s setrgbcolor' % fp_str(color.red, color.green, color.blue))
  190. def setFillColor(self, color):
  191. self._fillColor = color
  192. self.setColor(color)
  193. def setFillMode(self, v):
  194. self._fillMode = v
  195. def setLineWidth(self, width):
  196. if width != self._lineWidth:
  197. self._lineWidth = width
  198. self.code_append('%s setlinewidth' % width)
  199. def setFont(self,font,fontSize,leading=None):
  200. if self._font!=font or self._fontSize!=fontSize:
  201. self._fontCodeLoc = len(self.code)
  202. self._font = font
  203. self._fontSize = fontSize
  204. self.code_append('')
  205. def line(self, x1, y1, x2, y2):
  206. if self._strokeColor != None:
  207. self.setColor(self._strokeColor)
  208. self.code_append('%s m %s l stroke' % (fp_str(x1, y1), fp_str(x2, y2)))
  209. def _escape(self, s):
  210. '''
  211. return a copy of string s with special characters in postscript strings
  212. escaped with backslashes.
  213. '''
  214. try:
  215. return _escape_and_limit(s)
  216. except:
  217. raise ValueError("cannot escape %s" % ascii(s))
  218. def _textOut(self, x, y, s, textRenderMode=0):
  219. if textRenderMode==3: return
  220. xy = fp_str(x,y)
  221. s = self._escape(s)
  222. if textRenderMode==0: #the standard case
  223. self.setColor(self._fillColor)
  224. self.code_append('%s m (%s) show ' % (xy,s))
  225. return
  226. fill = textRenderMode==0 or textRenderMode==2 or textRenderMode==4 or textRenderMode==6
  227. stroke = textRenderMode==1 or textRenderMode==2 or textRenderMode==5 or textRenderMode==6
  228. addToClip = textRenderMode>=4
  229. if fill and stroke:
  230. if self._fillColor is None:
  231. op = ''
  232. else:
  233. op = 'fill '
  234. self.setColor(self._fillColor)
  235. self.code_append('%s m (%s) true charpath gsave %s' % (xy,s,op))
  236. self.code_append('grestore ')
  237. if self._strokeColor is not None:
  238. self.setColor(self._strokeColor)
  239. self.code_append('stroke ')
  240. else: #can only be stroke alone
  241. self.setColor(self._strokeColor)
  242. self.code_append('%s m (%s) true charpath stroke ' % (xy,s))
  243. def _issueT1String(self,fontObj,x,y,s, textRenderMode=0):
  244. fc = fontObj
  245. code_append = self.code_append
  246. fontSize = self._fontSize
  247. fontsUsed = self._fontsUsed
  248. escape = self._escape
  249. if not isUnicode(s):
  250. try:
  251. s = s.decode('utf8')
  252. except UnicodeDecodeError as e:
  253. i,j = e.args[2:4]
  254. raise UnicodeDecodeError(*(e.args[:4]+('%s\n%s-->%s<--%s' % (e.args[4],s[i-10:i],s[i:j],s[j:j+10]),)))
  255. for f, t in unicode2T1(s,[fontObj]+fontObj.substitutionFonts):
  256. if f!=fc:
  257. psName = asNative(f.face.name)
  258. code_append('(%s) findfont %s scalefont setfont' % (psName,fp_str(fontSize)))
  259. if psName not in fontsUsed:
  260. fontsUsed.append(psName)
  261. fc = f
  262. self._textOut(x,y,t,textRenderMode)
  263. x += f.stringWidth(t.decode(f.encName),fontSize)
  264. if fontObj!=fc:
  265. self._font = None
  266. self.setFont(fontObj.face.name,fontSize)
  267. def drawString(self, x, y, s, angle=0, text_anchor='left', textRenderMode=0):
  268. needFill = textRenderMode in (0,2,4,6)
  269. needStroke = textRenderMode in (1,2,5,6)
  270. if needFill or needStroke:
  271. if text_anchor!='left':
  272. textLen = stringWidth(s, self._font,self._fontSize)
  273. if text_anchor=='end':
  274. x -= textLen
  275. elif text_anchor=='middle':
  276. x -= textLen/2.
  277. elif text_anchor=='numeric':
  278. x -= numericXShift(text_anchor,s,textLen,self._font,self._fontSize)
  279. fontObj = getFont(self._font)
  280. if not self.code[self._fontCodeLoc]:
  281. psName = asNative(fontObj.face.name)
  282. self.code[self._fontCodeLoc]='(%s) findfont %s scalefont setfont' % (psName,fp_str(self._fontSize))
  283. if psName not in self._fontsUsed:
  284. self._fontsUsed.append(psName)
  285. if angle!=0:
  286. self.code_append('gsave %s translate %s rotate' % (fp_str(x,y),fp_str(angle)))
  287. x = y = 0
  288. oldColor = self._color
  289. if fontObj._dynamicFont:
  290. self._textOut(x, y, s, textRenderMode=textRenderMode)
  291. else:
  292. self._issueT1String(fontObj,x,y,s, textRenderMode=textRenderMode)
  293. self.setColor(oldColor)
  294. if angle!=0:
  295. self.code_append('grestore')
  296. def drawCentredString(self, x, y, text, text_anchor='middle', textRenderMode=0):
  297. self.drawString(x,y,text, text_anchor=text_anchor, textRenderMode=textRenderMode)
  298. def drawRightString(self, text, x, y, text_anchor='end', textRenderMode=0):
  299. self.drawString(text,x,y,text_anchor=text_anchor, textRenderMode=textRenderMode)
  300. def drawCurve(self, x1, y1, x2, y2, x3, y3, x4, y4, closed=0):
  301. codeline = '%s m %s curveto'
  302. data = (fp_str(x1, y1), fp_str(x2, y2, x3, y3, x4, y4))
  303. if self._fillColor != None:
  304. self.setColor(self._fillColor)
  305. self.code_append((codeline % data) + ' eofill')
  306. if self._strokeColor != None:
  307. self.setColor(self._strokeColor)
  308. self.code_append((codeline % data)
  309. + ((closed and ' closepath') or '')
  310. + ' stroke')
  311. ########################################################################################
  312. def rect(self, x1,y1, x2,y2, stroke=1, fill=1):
  313. "Draw a rectangle between x1,y1, and x2,y2"
  314. # Path is drawn in counter-clockwise direction"
  315. x1, x2 = min(x1,x2), max(x1, x2) # from piddle.py
  316. y1, y2 = min(y1,y2), max(y1, y2)
  317. self.polygon(((x1,y1),(x2,y1),(x2,y2),(x1,y2)), closed=1, stroke=stroke, fill = fill)
  318. def roundRect(self, x1,y1, x2,y2, rx=8, ry=8):
  319. """Draw a rounded rectangle between x1,y1, and x2,y2,
  320. with corners inset as ellipses with x radius rx and y radius ry.
  321. These should have x1<x2, y1<y2, rx>0, and ry>0."""
  322. # Path is drawn in counter-clockwise direction
  323. x1, x2 = min(x1,x2), max(x1, x2) # from piddle.py
  324. y1, y2 = min(y1,y2), max(y1, y2)
  325. # Note: arcto command draws a line from current point to beginning of arc
  326. # save current matrix, translate to center of ellipse, scale by rx ry, and draw
  327. # a circle of unit radius in counterclockwise dir, return to original matrix
  328. # arguments are (cx, cy, rx, ry, startAngle, endAngle)
  329. ellipsePath = 'matrix currentmatrix %s %s translate %s %s scale 0 0 1 %s %s arc setmatrix'
  330. # choice between newpath and moveTo beginning of arc
  331. # go with newpath for precision, does this violate any assumptions in code???
  332. rr = ['newpath'] # Round Rect code path
  333. a = rr.append
  334. # upper left corner ellipse is first
  335. a(ellipsePath % (x1+rx, y1+ry, rx, -ry, 90, 180))
  336. a(ellipsePath % (x1+rx, y2-ry, rx, -ry, 180, 270))
  337. a(ellipsePath % (x2-rx, y2-ry, rx, -ry, 270, 360))
  338. a(ellipsePath % (x2-rx, y1+ry, rx, -ry, 0, 90) )
  339. a('closepath')
  340. self._fillAndStroke(rr)
  341. def ellipse(self, x1,y1, x2,y2):
  342. """Draw an orthogonal ellipse inscribed within the rectangle x1,y1,x2,y2.
  343. These should have x1<x2 and y1<y2."""
  344. #Just invoke drawArc to actually draw the ellipse
  345. self.drawArc(x1,y1, x2,y2)
  346. def circle(self, xc, yc, r):
  347. self.ellipse(xc-r,yc-r, xc+r,yc+r)
  348. def drawArc(self, x1,y1, x2,y2, startAng=0, extent=360, fromcenter=0):
  349. """Draw a partial ellipse inscribed within the rectangle x1,y1,x2,y2,
  350. starting at startAng degrees and covering extent degrees. Angles
  351. start with 0 to the right (+x) and increase counter-clockwise.
  352. These should have x1<x2 and y1<y2."""
  353. #calculate centre of ellipse
  354. #print "x1,y1,x2,y2,startAng,extent,fromcenter", x1,y1,x2,y2,startAng,extent,fromcenter
  355. cx, cy = (x1+x2)/2.0, (y1+y2)/2.0
  356. rx, ry = (x2-x1)/2.0, (y2-y1)/2.0
  357. codeline = self._genArcCode(x1, y1, x2, y2, startAng, extent)
  358. startAngleRadians = math.pi*startAng/180.0
  359. extentRadians = math.pi*extent/180.0
  360. endAngleRadians = startAngleRadians + extentRadians
  361. codelineAppended = 0
  362. # fill portion
  363. if self._fillColor != None:
  364. self.setColor(self._fillColor)
  365. self.code_append(codeline)
  366. codelineAppended = 1
  367. if self._strokeColor!=None: self.code_append('gsave')
  368. self.lineTo(cx,cy)
  369. self.code_append('eofill')
  370. if self._strokeColor!=None: self.code_append('grestore')
  371. # stroke portion
  372. if self._strokeColor != None:
  373. # this is a bit hacked up. There is certainly a better way...
  374. self.setColor(self._strokeColor)
  375. (startx, starty) = (cx+rx*math.cos(startAngleRadians), cy+ry*math.sin(startAngleRadians))
  376. if not codelineAppended:
  377. self.code_append(codeline)
  378. if fromcenter:
  379. # move to center
  380. self.lineTo(cx,cy)
  381. self.lineTo(startx, starty)
  382. self.code_append('closepath')
  383. self.code_append('stroke')
  384. def _genArcCode(self, x1, y1, x2, y2, startAng, extent):
  385. "Calculate the path for an arc inscribed in rectangle defined by (x1,y1),(x2,y2)"
  386. #calculate semi-minor and semi-major axes of ellipse
  387. xScale = abs((x2-x1)/2.0)
  388. yScale = abs((y2-y1)/2.0)
  389. #calculate centre of ellipse
  390. x, y = (x1+x2)/2.0, (y1+y2)/2.0
  391. codeline = 'matrix currentmatrix %s %s translate %s %s scale 0 0 1 %s %s %s setmatrix'
  392. if extent >= 0:
  393. arc='arc'
  394. else:
  395. arc='arcn'
  396. data = (x,y, xScale, yScale, startAng, startAng+extent, arc)
  397. return codeline % data
  398. def polygon(self, p, closed=0, stroke=1, fill=1):
  399. assert len(p) >= 2, 'Polygon must have 2 or more points'
  400. start = p[0]
  401. p = p[1:]
  402. poly = []
  403. a = poly.append
  404. a("%s m" % fp_str(start))
  405. for point in p:
  406. a("%s l" % fp_str(point))
  407. if closed:
  408. a("closepath")
  409. self._fillAndStroke(poly,stroke=stroke,fill=fill)
  410. def lines(self, lineList, color=None, width=None):
  411. if self._strokeColor != None:
  412. self._setColor(self._strokeColor)
  413. codeline = '%s m %s l stroke'
  414. for line in lineList:
  415. self.code_append(codeline % (fp_str(line[0]),fp_str(line[1])))
  416. def moveTo(self,x,y):
  417. self.code_append('%s m' % fp_str(x, y))
  418. def lineTo(self,x,y):
  419. self.code_append('%s l' % fp_str(x, y))
  420. def curveTo(self,x1,y1,x2,y2,x3,y3):
  421. self.code_append('%s c' % fp_str(x1,y1,x2,y2,x3,y3))
  422. def closePath(self):
  423. self.code_append('closepath')
  424. def polyLine(self, p):
  425. assert len(p) >= 1, 'Polyline must have 1 or more points'
  426. if self._strokeColor != None:
  427. self.setColor(self._strokeColor)
  428. self.moveTo(p[0][0], p[0][1])
  429. for t in p[1:]:
  430. self.lineTo(t[0], t[1])
  431. self.code_append('stroke')
  432. def drawFigure(self, partList, closed=0):
  433. figureCode = []
  434. a = figureCode.append
  435. first = 1
  436. for part in partList:
  437. op = part[0]
  438. args = list(part[1:])
  439. if op == figureLine:
  440. if first:
  441. first = 0
  442. a("%s m" % fp_str(args[:2]))
  443. else:
  444. a("%s l" % fp_str(args[:2]))
  445. a("%s l" % fp_str(args[2:]))
  446. elif op == figureArc:
  447. first = 0
  448. x1,y1,x2,y2,startAngle,extent = args[:6]
  449. a(self._genArcCode(x1,y1,x2,y2,startAngle,extent))
  450. elif op == figureCurve:
  451. if first:
  452. first = 0
  453. a("%s m" % fp_str(args[:2]))
  454. else:
  455. a("%s l" % fp_str(args[:2]))
  456. a("%s curveto" % fp_str(args[2:]))
  457. else:
  458. raise TypeError("unknown figure operator: "+op)
  459. if closed:
  460. a("closepath")
  461. self._fillAndStroke(figureCode)
  462. def _fillAndStroke(self,code,clip=0,fill=1,stroke=1,fillMode=None):
  463. fill = self._fillColor and fill
  464. stroke = self._strokeColor and stroke
  465. if fill or stroke or clip:
  466. self.code.extend(code)
  467. if fill:
  468. if fillMode is None:
  469. fillMode = self._fillMode
  470. if stroke or clip: self.code_append("gsave")
  471. self.setColor(self._fillColor)
  472. self.code_append("eofill" if fillMode==FILL_EVEN_ODD else "fill")
  473. if stroke or clip: self.code_append("grestore")
  474. if stroke:
  475. if clip: self.code_append("gsave")
  476. self.setColor(self._strokeColor)
  477. self.code_append("stroke")
  478. if clip: self.code_append("grestore")
  479. if clip:
  480. self.code_append("clip")
  481. self.code_append("newpath")
  482. def translate(self,x,y):
  483. self.code_append('%s translate' % fp_str(x,y))
  484. def scale(self,x,y):
  485. self.code_append('%s scale' % fp_str(x,y))
  486. def transform(self,a,b,c,d,e,f):
  487. self.code_append('[%s] concat' % fp_str(a,b,c,d,e,f))
  488. def _drawTimeResize(self,w,h):
  489. '''if this is used we're probably in the wrong world'''
  490. self.width, self.height = w, h
  491. def _drawImageLevel1(self, image, x1, y1, width=None, height=None):
  492. # Postscript Level1 version available for fallback mode when Level2 doesn't work
  493. # For now let's start with 24 bit RGB images (following piddlePDF again)
  494. component_depth = 8
  495. myimage = image.convert('RGB')
  496. imgwidth, imgheight = myimage.size
  497. if not width:
  498. width = imgwidth
  499. if not height:
  500. height = imgheight
  501. #print 'Image size (%d, %d); Draw size (%d, %d)' % (imgwidth, imgheight, width, height)
  502. # now I need to tell postscript how big image is
  503. # "image operators assume that they receive sample data from
  504. # their data source in x-axis major index order. The coordinate
  505. # of the lower-left corner of the first sample is (0,0), of the
  506. # second (1,0) and so on" -PS2 ref manual p. 215
  507. #
  508. # The ImageMatrix maps unit squre of user space to boundary of the source image
  509. #
  510. # The CurrentTransformationMatrix (CTM) maps the unit square of
  511. # user space to the rect...on the page that is to receive the
  512. # image. A common ImageMatrix is [width 0 0 -height 0 height]
  513. # (for a left to right, top to bottom image )
  514. # first let's map the user coordinates start at offset x1,y1 on page
  515. self.code.extend([
  516. 'gsave',
  517. '%s %s translate' % (x1,y1), # need to start are lower left of image
  518. '%s %s scale' % (width,height),
  519. '/scanline %d 3 mul string def' % imgwidth # scanline by multiples of image width
  520. ])
  521. # now push the dimensions and depth info onto the stack
  522. # and push the ImageMatrix to map the source to the target rectangle (see above)
  523. # finally specify source (PS2 pp. 225 ) and by exmample
  524. self.code.extend([
  525. '%s %s %s' % (imgwidth, imgheight, component_depth),
  526. '[%s %s %s %s %s %s]' % (imgwidth, 0, 0, -imgheight, 0, imgheight),
  527. '{ currentfile scanline readhexstring pop } false 3',
  528. 'colorimage '
  529. ])
  530. # data source output--now we just need to deliver a hex encode
  531. # series of lines of the right overall size can follow
  532. # piddlePDF again
  533. rawimage = (myimage.tobytes if hasattr(myimage,'tobytes') else myimage.tostring)()
  534. hex_encoded = self._AsciiHexEncode(rawimage)
  535. # write in blocks of 78 chars per line
  536. outstream = getStringIO(hex_encoded)
  537. dataline = outstream.read(78)
  538. while dataline != "":
  539. self.code_append(dataline)
  540. dataline= outstream.read(78)
  541. self.code_append('% end of image data') # for clarity
  542. self.code_append('grestore') # return coordinates to normal
  543. # end of drawImage
  544. def _AsciiHexEncode(self, input): # also based on piddlePDF
  545. "Helper function used by images"
  546. output = getStringIO()
  547. for char in asBytes(input):
  548. output.write('%02x' % char2int(char))
  549. return output.getvalue()
  550. def _drawImageLevel2(self, image, x1,y1, width=None,height=None): # Postscript Level2 version
  551. '''At present we're handling only PIL'''
  552. ### what sort of image are we to draw
  553. if image.mode=='L' :
  554. imBitsPerComponent = 8
  555. imNumComponents = 1
  556. myimage = image
  557. elif image.mode == '1':
  558. myimage = image.convert('L')
  559. imNumComponents = 1
  560. myimage = image
  561. else :
  562. myimage = image.convert('RGB')
  563. imNumComponents = 3
  564. imBitsPerComponent = 8
  565. imwidth, imheight = myimage.size
  566. if not width:
  567. width = imwidth
  568. if not height:
  569. height = imheight
  570. self.code.extend([
  571. 'gsave',
  572. '%s %s translate' % (x1,y1), # need to start are lower left of image
  573. '%s %s scale' % (width,height)])
  574. if imNumComponents == 3 :
  575. self.code_append('/DeviceRGB setcolorspace')
  576. elif imNumComponents == 1 :
  577. self.code_append('/DeviceGray setcolorspace')
  578. # create the image dictionary
  579. self.code_append("""
  580. <<
  581. /ImageType 1
  582. /Width %d /Height %d %% dimensions of source image
  583. /BitsPerComponent %d""" % (imwidth, imheight, imBitsPerComponent) )
  584. if imNumComponents == 1:
  585. self.code_append('/Decode [0 1]')
  586. if imNumComponents == 3:
  587. self.code_append('/Decode [0 1 0 1 0 1] %% decode color values normally')
  588. self.code.extend([ '/ImageMatrix [%s 0 0 %s 0 %s]' % (imwidth, -imheight, imheight),
  589. '/DataSource currentfile /ASCIIHexDecode filter',
  590. '>> % End image dictionary',
  591. 'image'])
  592. # after image operator just need to dump image dat to file as hexstring
  593. rawimage = (myimage.tobytes if hasattr(myimage,'tobytes') else myimage.tostring)()
  594. hex_encoded = self._AsciiHexEncode(rawimage)
  595. # write in blocks of 78 chars per line
  596. outstream = getStringIO(hex_encoded)
  597. dataline = outstream.read(78)
  598. while dataline != "":
  599. self.code_append(dataline)
  600. dataline= outstream.read(78)
  601. self.code_append('> % end of image data') # > is EOD for hex encoded filterfor clarity
  602. self.code_append('grestore') # return coordinates to normal
  603. # renderpdf - draws them onto a canvas
  604. """Usage:
  605. from reportlab.graphics import renderPS
  606. renderPS.draw(drawing, canvas, x, y)
  607. Execute the script to see some test drawings."""
  608. from reportlab.graphics.shapes import *
  609. # hack so we only get warnings once each
  610. #warnOnce = WarnOnce()
  611. # the main entry point for users...
  612. def draw(drawing, canvas, x=0, y=0, showBoundary=rl_config.showBoundary):
  613. """As it says"""
  614. R = _PSRenderer()
  615. R.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary)
  616. def _pointsFromList(L):
  617. '''
  618. given a list of coordinates [x0, y0, x1, y1....]
  619. produce a list of points [(x0,y0), (y1,y0),....]
  620. '''
  621. P=[]
  622. a = P.append
  623. for i in range(0,len(L),2):
  624. a((L[i],L[i+1]))
  625. return P
  626. class _PSRenderer(Renderer):
  627. """This draws onto a EPS document. It needs to be a class
  628. rather than a function, as some EPS-specific state tracking is
  629. needed outside of the state info in the SVG model."""
  630. def drawNode(self, node):
  631. """This is the recursive method called for each node
  632. in the tree"""
  633. self._canvas.comment('begin node %r'%node)
  634. color = self._canvas._color
  635. if not (isinstance(node, Path) and node.isClipPath):
  636. self._canvas.saveState()
  637. #apply state changes
  638. deltas = getStateDelta(node)
  639. self._tracker.push(deltas)
  640. self.applyStateChanges(deltas, {})
  641. #draw the object, or recurse
  642. self.drawNodeDispatcher(node)
  643. rDeltas = self._tracker.pop()
  644. if not (isinstance(node, Path) and node.isClipPath):
  645. self._canvas.restoreState()
  646. self._canvas.comment('end node %r'%node)
  647. self._canvas._color = color
  648. #restore things we might have lost (without actually doing anything).
  649. for k, v in rDeltas.items():
  650. if k in self._restores:
  651. setattr(self._canvas,self._restores[k],v)
  652. ## _restores = {'stroke':'_stroke','stroke_width': '_lineWidth','stroke_linecap':'_lineCap',
  653. ## 'stroke_linejoin':'_lineJoin','fill':'_fill','font_family':'_font',
  654. ## 'font_size':'_fontSize'}
  655. _restores = {'strokeColor':'_strokeColor','strokeWidth': '_lineWidth','strokeLineCap':'_lineCap',
  656. 'strokeLineJoin':'_lineJoin','fillColor':'_fillColor','fontName':'_font',
  657. 'fontSize':'_fontSize'}
  658. def drawRect(self, rect):
  659. if rect.rx == rect.ry == 0:
  660. #plain old rectangle
  661. self._canvas.rect(
  662. rect.x, rect.y,
  663. rect.x+rect.width, rect.y+rect.height)
  664. else:
  665. #cheat and assume ry = rx; better to generalize
  666. #pdfgen roundRect function. TODO
  667. self._canvas.roundRect(
  668. rect.x, rect.y,
  669. rect.x+rect.width, rect.y+rect.height, rect.rx, rect.ry
  670. )
  671. def drawLine(self, line):
  672. if self._canvas._strokeColor:
  673. self._canvas.line(line.x1, line.y1, line.x2, line.y2)
  674. def drawCircle(self, circle):
  675. self._canvas.circle( circle.cx, circle.cy, circle.r)
  676. def drawWedge(self, wedge):
  677. yradius, radius1, yradius1 = wedge._xtraRadii()
  678. if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None) and not wedge.annular:
  679. startangledegrees = wedge.startangledegrees
  680. endangledegrees = wedge.endangledegrees
  681. centerx= wedge.centerx
  682. centery = wedge.centery
  683. radius = wedge.radius
  684. extent = endangledegrees - startangledegrees
  685. self._canvas.drawArc(centerx-radius, centery-yradius, centerx+radius, centery+yradius,
  686. startangledegrees, extent, fromcenter=1)
  687. else:
  688. P = wedge.asPolygon()
  689. if isinstance(P,Path):
  690. self.drawPath(P)
  691. else:
  692. self.drawPolygon(P)
  693. def drawPolyLine(self, p):
  694. if self._canvas._strokeColor:
  695. self._canvas.polyLine(_pointsFromList(p.points))
  696. def drawEllipse(self, ellipse):
  697. #need to convert to pdfgen's bounding box representation
  698. x1 = ellipse.cx - ellipse.rx
  699. x2 = ellipse.cx + ellipse.rx
  700. y1 = ellipse.cy - ellipse.ry
  701. y2 = ellipse.cy + ellipse.ry
  702. self._canvas.ellipse(x1,y1,x2,y2)
  703. def drawPolygon(self, p):
  704. self._canvas.polygon(_pointsFromList(p.points), closed=1)
  705. def drawString(self, stringObj):
  706. textRenderMode = getattr(stringObj,'textRenderMode',0)
  707. if self._canvas._fillColor or textRenderMode:
  708. S = self._tracker.getState()
  709. text_anchor, x, y, text = S['textAnchor'], stringObj.x,stringObj.y,stringObj.text
  710. if not text_anchor in ['start','inherited']:
  711. font, fontSize = S['fontName'], S['fontSize']
  712. textLen = stringWidth(text, font,fontSize)
  713. if text_anchor=='end':
  714. x -= textLen
  715. elif text_anchor=='middle':
  716. x -= textLen/2
  717. elif text_anchor=='numeric':
  718. x -= numericXShift(text_anchor,text,textLen,font,fontSize,encoding='winansi')
  719. else:
  720. raise ValueError('bad value for text_anchor '+str(text_anchor))
  721. self._canvas.drawString(x,y,text, textRenderMode=textRenderMode)
  722. def drawPath(self, path, fillMode=None):
  723. from reportlab.graphics.shapes import _renderPath
  724. c = self._canvas
  725. drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.closePath)
  726. autoclose = getattr(path,'autoclose','')
  727. def rP(**kwds):
  728. return _renderPath(path, drawFuncs, **kwds)
  729. if fillMode is None:
  730. fillMode = getattr(path,'fillMode',c._fillMode)
  731. fill = c._fillColor is not None
  732. stroke = c._strokeColor is not None
  733. clip = path.isClipPath
  734. fas = lambda **kwds: c._fillAndStroke([], fillMode=fillMode, **kwds)
  735. pathFill = lambda : c._fillAndStroke([], stroke=0, fillMode=fillMode)
  736. pathStroke = lambda : c._fillAndStroke([], fill=0)
  737. if autoclose=='svg':
  738. rP()
  739. fas(stroke=stroke,fill=fill,clip=clip)
  740. elif autoclose=='pdf':
  741. if fill:
  742. rP(forceClose=True)
  743. fas(stroke=stroke,fill=fill,clip=clip)
  744. elif stroke or clip:
  745. rP()
  746. fas(stroke=stroke,fill=0,clip=clip)
  747. else:
  748. if fill and rP(countOnly=True):
  749. rP()
  750. elif stroke or clip:
  751. rP()
  752. fas(stroke=stroke,fill=0,clip=clip)
  753. def applyStateChanges(self, delta, newState):
  754. """This takes a set of states, and outputs the operators
  755. needed to set those properties"""
  756. for key, value in delta.items():
  757. if key == 'transform':
  758. self._canvas.transform(value[0], value[1], value[2],
  759. value[3], value[4], value[5])
  760. elif key == 'strokeColor':
  761. #this has different semantics in PDF to SVG;
  762. #we always have a color, and either do or do
  763. #not apply it; in SVG one can have a 'None' color
  764. self._canvas.setStrokeColor(value)
  765. elif key == 'strokeWidth':
  766. self._canvas.setLineWidth(value)
  767. elif key == 'strokeLineCap': #0,1,2
  768. self._canvas.setLineCap(value)
  769. elif key == 'strokeLineJoin':
  770. self._canvas.setLineJoin(value)
  771. elif key == 'strokeDashArray':
  772. if value:
  773. if isinstance(value,(list,tuple)) and len(value)==2 and isinstance(value[1],(tuple,list)):
  774. phase = value[0]
  775. value = value[1]
  776. else:
  777. phase = 0
  778. self._canvas.setDash(value,phase)
  779. else:
  780. self._canvas.setDash()
  781. ## elif key == 'stroke_opacity':
  782. ## warnOnce('Stroke Opacity not supported yet')
  783. elif key == 'fillColor':
  784. #this has different semantics in PDF to SVG;
  785. #we always have a color, and either do or do
  786. #not apply it; in SVG one can have a 'None' color
  787. self._canvas.setFillColor(value)
  788. ## elif key == 'fill_rule':
  789. ## warnOnce('Fill rules not done yet')
  790. ## elif key == 'fill_opacity':
  791. ## warnOnce('Fill opacity not done yet')
  792. elif key in ['fontSize', 'fontName']:
  793. # both need setting together in PDF
  794. # one or both might be in the deltas,
  795. # so need to get whichever is missing
  796. fontname = delta.get('fontName', self._canvas._font)
  797. fontsize = delta.get('fontSize', self._canvas._fontSize)
  798. self._canvas.setFont(fontname, fontsize)
  799. def drawImage(self, image):
  800. from reportlab.lib.utils import ImageReader
  801. im = ImageReader(image.path)
  802. self._canvas.drawImage(im._image,image.x,image.y,image.width,image.height)
  803. def drawToFile(d,fn, showBoundary=rl_config.showBoundary,**kwd):
  804. d = renderScaledDrawing(d)
  805. c = PSCanvas((d.width,d.height))
  806. draw(d, c, 0, 0, showBoundary=showBoundary)
  807. c.save(fn)
  808. def drawToString(d, showBoundary=rl_config.showBoundary):
  809. "Returns a PS as a string in memory, without touching the disk"
  810. s = getBytesIO()
  811. drawToFile(d, s, showBoundary=showBoundary)
  812. return s.getvalue()
  813. #########################################################
  814. #
  815. # test code. First, define a bunch of drawings.
  816. # Routine to draw them comes at the end.
  817. #
  818. #########################################################
  819. def test(outDir='epsout',shout=False):
  820. from reportlab.graphics import testshapes
  821. from reportlab.rl_config import verbose
  822. OLDFONTS = testshapes._FONTS[:]
  823. testshapes._FONTS[:] = ['Times-Roman','Times-Bold','Times-Italic', 'Times-BoldItalic','Courier']
  824. try:
  825. import os
  826. # save all drawings and their doc strings from the test file
  827. if not os.path.isdir(outDir):
  828. os.mkdir(outDir)
  829. #grab all drawings from the test module
  830. drawings = []
  831. for funcname in dir(testshapes):
  832. if funcname[0:10] == 'getDrawing':
  833. func = getattr(testshapes,funcname)
  834. drawing = func()
  835. docstring = getattr(func,'__doc__','')
  836. drawings.append((drawing, docstring))
  837. i = 0
  838. for (d, docstring) in drawings:
  839. filename = outDir + os.sep + 'renderPS_%d.eps'%i
  840. drawToFile(d,filename)
  841. if shout or verbose>2: print('renderPS test saved %s' % ascii(filename))
  842. i += 1
  843. finally:
  844. testshapes._FONTS[:] = OLDFONTS
  845. if __name__=='__main__':
  846. import sys
  847. if len(sys.argv)>1:
  848. outdir = sys.argv[1]
  849. else:
  850. outdir = 'epsout'
  851. test(outdir,shout=True)