trml2pdf.py 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044
  1. # -*- encoding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # Copyright (c) 2013 ZestyBeanz Technologies Pvt. Ltd.
  5. # (http://wwww.zbeanztech.com)
  6. # contact@zbeanztech.com
  7. # prajul@zbeanztech.com
  8. #
  9. # This program is free software: you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation, either version 3 of the License, or
  12. # (at your option) any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License
  20. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. #
  22. ##############################################################################
  23. import sys
  24. import copy
  25. import reportlab
  26. import re
  27. from reportlab.pdfgen import canvas
  28. from reportlab import platypus
  29. from openerp.report.render.rml2pdf import utils
  30. from openerp.report.render.rml2pdf import color
  31. import os
  32. import logging
  33. from lxml import etree
  34. import base64
  35. from reportlab.platypus.doctemplate import ActionFlowable
  36. from openerp.tools.safe_eval import safe_eval as eval
  37. from reportlab.lib.units import inch,cm,mm
  38. from openerp.tools.misc import file_open
  39. from reportlab.pdfbase import pdfmetrics
  40. from reportlab.lib.pagesizes import A4, letter
  41. try:
  42. from cStringIO import StringIO
  43. _hush_pyflakes = [ StringIO ]
  44. except ImportError:
  45. from StringIO import StringIO
  46. _logger = logging.getLogger(__name__)
  47. encoding = 'utf-8'
  48. def _open_image(filename, path=None):
  49. """Attempt to open a binary file and return the descriptor
  50. """
  51. if os.path.isfile(filename):
  52. return open(filename, 'rb')
  53. for p in (path or []):
  54. if p and os.path.isabs(p):
  55. fullpath = os.path.join(p, filename)
  56. if os.path.isfile(fullpath):
  57. return open(fullpath, 'rb')
  58. try:
  59. if p:
  60. fullpath = os.path.join(p, filename)
  61. else:
  62. fullpath = filename
  63. return file_open(fullpath)
  64. except IOError:
  65. pass
  66. raise IOError("File %s cannot be found in image path" % filename)
  67. class NumberedCanvas(canvas.Canvas):
  68. def __init__(self, *args, **kwargs):
  69. canvas.Canvas.__init__(self, *args, **kwargs)
  70. self._codes = []
  71. self._flag=False
  72. self._pageCount=0
  73. self._currentPage =0
  74. self._pageCounter=0
  75. self.pages={}
  76. def showPage(self):
  77. self._currentPage +=1
  78. if not self._flag:
  79. self._pageCount += 1
  80. else:
  81. self.pages.update({self._currentPage:self._pageCount})
  82. self._codes.append({'code': self._code, 'stack': self._codeStack})
  83. self._startPage()
  84. self._flag=False
  85. def pageCount(self):
  86. if self.pages.get(self._pageCounter,False):
  87. self._pageNumber=0
  88. self._pageCounter +=1
  89. key=self._pageCounter
  90. if not self.pages.get(key,False):
  91. while not self.pages.get(key,False):
  92. key += 1
  93. self.setFont("Helvetica", 8)
  94. self.drawRightString((self._pagesize[0]-30), (self._pagesize[1]-40),
  95. " %(this)i / %(total)i" % {
  96. 'this': self._pageNumber+1,
  97. 'total': self.pages.get(key,False),
  98. }
  99. )
  100. def save(self):
  101. """add page info to each page (page x of y)"""
  102. # reset page counter
  103. self._pageNumber = 0
  104. for code in self._codes:
  105. self._code = code['code']
  106. self._codeStack = code['stack']
  107. self.pageCount()
  108. canvas.Canvas.showPage(self)
  109. # self.restoreState()
  110. self._doc.SaveToFile(self._filename, self)
  111. class PageCount(platypus.Flowable):
  112. def __init__(self, story_count=0):
  113. platypus.Flowable.__init__(self)
  114. self.story_count = story_count
  115. def draw(self):
  116. self.canv.beginForm("pageCount%d" % self.story_count)
  117. self.canv.setFont("Helvetica", utils.unit_get(str(8)))
  118. self.canv.drawString(0, 0, str(self.canv.getPageNumber()))
  119. self.canv.endForm()
  120. class PageReset(platypus.Flowable):
  121. def draw(self):
  122. self.canv._doPageReset = True
  123. class PageReset(platypus.Flowable):
  124. def draw(self):
  125. self.canv._doPageReset = True
  126. class _rml_styles(object,):
  127. def __init__(self, nodes, localcontext):
  128. self.localcontext = localcontext
  129. self.styles = {}
  130. self.styles_obj = {}
  131. self.names = {}
  132. self.table_styles = {}
  133. self.default_style = reportlab.lib.styles.getSampleStyleSheet()
  134. for node in nodes:
  135. for style in node.findall('blockTableStyle'):
  136. self.table_styles[style.get('id')] = self._table_style_get(style)
  137. for style in node.findall('paraStyle'):
  138. sname = style.get('name')
  139. self.styles[sname] = self._para_style_update(style)
  140. self.styles_obj[sname] = reportlab.lib.styles.ParagraphStyle(sname, self.default_style["Normal"], **self.styles[sname])
  141. for variable in node.findall('initialize'):
  142. for name in variable.findall('name'):
  143. self.names[ name.get('id')] = name.get('value')
  144. def _para_style_update(self, node):
  145. data = {}
  146. for attr in ['textColor', 'backColor', 'bulletColor', 'borderColor']:
  147. if node.get(attr):
  148. data[attr] = color.get(node.get(attr))
  149. for attr in ['fontName', 'bulletFontName', 'bulletText']:
  150. if node.get(attr):
  151. data[attr] = node.get(attr)
  152. for attr in ['fontSize', 'leftIndent', 'rightIndent', 'spaceBefore', 'spaceAfter',
  153. 'firstLineIndent', 'bulletIndent', 'bulletFontSize', 'leading',
  154. 'borderWidth','borderPadding','borderRadius']:
  155. if node.get(attr):
  156. data[attr] = utils.unit_get(node.get(attr))
  157. if node.get('alignment'):
  158. align = {
  159. 'right':reportlab.lib.enums.TA_RIGHT,
  160. 'center':reportlab.lib.enums.TA_CENTER,
  161. 'justify':reportlab.lib.enums.TA_JUSTIFY
  162. }
  163. data['alignment'] = align.get(node.get('alignment').lower(), reportlab.lib.enums.TA_LEFT)
  164. return data
  165. def _table_style_get(self, style_node):
  166. styles = []
  167. for node in style_node:
  168. start = utils.tuple_int_get(node, 'start', (0,0) )
  169. stop = utils.tuple_int_get(node, 'stop', (-1,-1) )
  170. if node.tag=='blockValign':
  171. styles.append(('VALIGN', start, stop, str(node.get('value'))))
  172. elif node.tag=='blockFont':
  173. styles.append(('FONT', start, stop, str(node.get('name'))))
  174. elif node.tag=='blockTextColor':
  175. styles.append(('TEXTCOLOR', start, stop, color.get(str(node.get('colorName')))))
  176. elif node.tag=='blockLeading':
  177. styles.append(('LEADING', start, stop, utils.unit_get(node.get('length'))))
  178. elif node.tag=='blockAlignment':
  179. styles.append(('ALIGNMENT', start, stop, str(node.get('value'))))
  180. elif node.tag=='blockSpan':
  181. styles.append(('SPAN', start, stop))
  182. elif node.tag=='blockLeftPadding':
  183. styles.append(('LEFTPADDING', start, stop, utils.unit_get(node.get('length'))))
  184. elif node.tag=='blockRightPadding':
  185. styles.append(('RIGHTPADDING', start, stop, utils.unit_get(node.get('length'))))
  186. elif node.tag=='blockTopPadding':
  187. styles.append(('TOPPADDING', start, stop, utils.unit_get(node.get('length'))))
  188. elif node.tag=='blockBottomPadding':
  189. styles.append(('BOTTOMPADDING', start, stop, utils.unit_get(node.get('length'))))
  190. elif node.tag=='blockBackground':
  191. styles.append(('BACKGROUND', start, stop, color.get(node.get('colorName'))))
  192. if node.get('size'):
  193. styles.append(('FONTSIZE', start, stop, utils.unit_get(node.get('size'))))
  194. elif node.tag=='lineStyle':
  195. kind = node.get('kind')
  196. kind_list = [ 'GRID', 'BOX', 'OUTLINE', 'INNERGRID', 'LINEBELOW', 'LINEABOVE','LINEBEFORE', 'LINEAFTER' ]
  197. assert kind in kind_list
  198. thick = 1
  199. if node.get('thickness'):
  200. thick = float(node.get('thickness'))
  201. styles.append((kind, start, stop, thick, color.get(node.get('colorName'))))
  202. return platypus.tables.TableStyle(styles)
  203. def para_style_get(self, node):
  204. style = False
  205. sname = node.get('style')
  206. if sname:
  207. if sname in self.styles_obj:
  208. style = self.styles_obj[sname]
  209. else:
  210. _logger.warning('Warning: style not found, %s - setting default!\n' % (node.get('style'),) )
  211. if not style:
  212. style = self.default_style['Normal']
  213. para_update = self._para_style_update(node)
  214. if para_update:
  215. # update style only is necessary
  216. style = copy.deepcopy(style)
  217. style.__dict__.update(para_update)
  218. return style
  219. class _rml_doc(object):
  220. def __init__(self, node, localcontext=None, images=None, path='.', title=None):
  221. if images is None:
  222. images = {}
  223. if localcontext is None:
  224. localcontext = {}
  225. self.localcontext = localcontext
  226. self.etree = node
  227. self.filename = self.etree.get('filename')
  228. self.images = images
  229. self.path = path
  230. self.title = title
  231. def docinit(self, els):
  232. from reportlab.lib.fonts import addMapping
  233. from reportlab.pdfbase import pdfmetrics
  234. from reportlab.pdfbase.ttfonts import TTFont
  235. for node in els:
  236. for font in node.findall('registerFont'):
  237. name = font.get('fontName').encode('ascii')
  238. fname = font.get('fontFile').encode('ascii')
  239. if name not in pdfmetrics._fonts:
  240. pdfmetrics.registerFont(TTFont(name, fname))
  241. addMapping(name, 0, 0, name) #normal
  242. addMapping(name, 0, 1, name) #italic
  243. addMapping(name, 1, 0, name) #bold
  244. addMapping(name, 1, 1, name) #italic and bold
  245. def setTTFontMapping(self,face, fontname, filename, mode='all'):
  246. from reportlab.lib.fonts import addMapping
  247. from reportlab.pdfbase import pdfmetrics
  248. from reportlab.pdfbase.ttfonts import TTFont
  249. if fontname not in pdfmetrics._fonts:
  250. pdfmetrics.registerFont(TTFont(fontname, filename))
  251. if mode == 'all':
  252. addMapping(face, 0, 0, fontname) #normal
  253. addMapping(face, 0, 1, fontname) #italic
  254. addMapping(face, 1, 0, fontname) #bold
  255. addMapping(face, 1, 1, fontname) #italic and bold
  256. elif (mode== 'normal') or (mode == 'regular'):
  257. addMapping(face, 0, 0, fontname) #normal
  258. elif mode == 'italic':
  259. addMapping(face, 0, 1, fontname) #italic
  260. elif mode == 'bold':
  261. addMapping(face, 1, 0, fontname) #bold
  262. elif mode == 'bolditalic':
  263. addMapping(face, 1, 1, fontname) #italic and bold
  264. def _textual_image(self, node):
  265. rc = ''
  266. for n in node:
  267. rc +=( etree.tostring(n) or '') + n.tail
  268. return base64.decodestring(node.tostring())
  269. def _images(self, el):
  270. result = {}
  271. for node in el.findall('.//image'):
  272. rc =( node.text or '')
  273. result[node.get('name')] = base64.decodestring(rc)
  274. return result
  275. def render(self, out):
  276. el = self.etree.findall('.//docinit')
  277. if el:
  278. self.docinit(el)
  279. el = self.etree.findall('.//stylesheet')
  280. self.styles = _rml_styles(el,self.localcontext)
  281. el = self.etree.findall('.//images')
  282. if el:
  283. self.images.update( self._images(el[0]) )
  284. el = self.etree.findall('.//template')
  285. if len(el):
  286. pt_obj = _rml_template(self.localcontext, out, el[0], self, images=self.images, path=self.path, title=self.title)
  287. el = utils._child_get(self.etree, self, 'story')
  288. pt_obj.render(el)
  289. else:
  290. self.canvas = canvas.Canvas(out)
  291. pd = self.etree.find('pageDrawing')[0]
  292. pd_obj = _rml_canvas(self.canvas, self.localcontext, None, self, self.images, path=self.path, title=self.title)
  293. pd_obj.render(pd)
  294. self.canvas.showPage()
  295. self.canvas.save()
  296. class _rml_canvas(object):
  297. def __init__(self, canvas, localcontext, doc_tmpl=None, doc=None, images=None, path='.', title=None):
  298. if images is None:
  299. images = {}
  300. self.localcontext = localcontext
  301. self.canvas = canvas
  302. self.styles = doc.styles
  303. self.doc_tmpl = doc_tmpl
  304. self.doc = doc
  305. self.images = images
  306. self.path = path
  307. self.title = title
  308. if self.title:
  309. self.canvas.setTitle(self.title)
  310. def _textual(self, node, x=0, y=0):
  311. text = node.text and node.text.encode('utf-8') or ''
  312. rc = utils._process_text(self, text)
  313. for n in node:
  314. if n.tag == 'seq':
  315. from reportlab.lib.sequencer import getSequencer
  316. seq = getSequencer()
  317. rc += str(seq.next(n.get('id')))
  318. if n.tag == 'pageCount':
  319. if x or y:
  320. self.canvas.translate(x,y)
  321. self.canvas.doForm('pageCount%s' % (self.canvas._storyCount,))
  322. if x or y:
  323. self.canvas.translate(-x,-y)
  324. if n.tag == 'pageNumber':
  325. rc += str(self.canvas.getPageNumber())
  326. rc += utils._process_text(self, n.tail)
  327. return rc.replace('\n','')
  328. def _drawString(self, node):
  329. v = utils.attr_get(node, ['x','y'])
  330. text=self._textual(node, **v)
  331. text = utils.xml2str(text)
  332. self.canvas.drawString(text=text, **v)
  333. def _drawCenteredString(self, node):
  334. v = utils.attr_get(node, ['x','y'])
  335. text=self._textual(node, **v)
  336. text = utils.xml2str(text)
  337. self.canvas.drawCentredString(text=text, **v)
  338. def _drawRightString(self, node):
  339. v = utils.attr_get(node, ['x','y'])
  340. text=self._textual(node, **v)
  341. text = utils.xml2str(text)
  342. self.canvas.drawRightString(text=text, **v)
  343. def _rect(self, node):
  344. if node.get('round'):
  345. self.canvas.roundRect(radius=utils.unit_get(node.get('round')), **utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
  346. else:
  347. self.canvas.rect(**utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
  348. def _ellipse(self, node):
  349. x1 = utils.unit_get(node.get('x'))
  350. x2 = utils.unit_get(node.get('width'))
  351. y1 = utils.unit_get(node.get('y'))
  352. y2 = utils.unit_get(node.get('height'))
  353. self.canvas.ellipse(x1,y1,x2,y2, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
  354. def _curves(self, node):
  355. line_str = node.text.split()
  356. lines = []
  357. while len(line_str)>7:
  358. self.canvas.bezier(*[utils.unit_get(l) for l in line_str[0:8]])
  359. line_str = line_str[8:]
  360. def _lines(self, node):
  361. line_str = node.text.split()
  362. lines = []
  363. while len(line_str)>3:
  364. lines.append([utils.unit_get(l) for l in line_str[0:4]])
  365. line_str = line_str[4:]
  366. self.canvas.lines(lines)
  367. def _grid(self, node):
  368. xlist = [utils.unit_get(s) for s in node.get('xs').split(',')]
  369. ylist = [utils.unit_get(s) for s in node.get('ys').split(',')]
  370. self.canvas.grid(xlist, ylist)
  371. def _translate(self, node):
  372. dx = utils.unit_get(node.get('dx')) or 0
  373. dy = utils.unit_get(node.get('dy')) or 0
  374. self.canvas.translate(dx,dy)
  375. def _circle(self, node):
  376. self.canvas.circle(x_cen=utils.unit_get(node.get('x')), y_cen=utils.unit_get(node.get('y')), r=utils.unit_get(node.get('radius')), **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
  377. def _place(self, node):
  378. flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title).render(node)
  379. infos = utils.attr_get(node, ['x','y','width','height'])
  380. infos['y']+=infos['height']
  381. for flow in flows:
  382. w,h = flow.wrap(infos['width'], infos['height'])
  383. if w<=infos['width'] and h<=infos['height']:
  384. infos['y']-=h
  385. flow.drawOn(self.canvas,infos['x'],infos['y'])
  386. infos['height']-=h
  387. else:
  388. raise ValueError("Not enough space")
  389. def _line_mode(self, node):
  390. ljoin = {'round':1, 'mitered':0, 'bevelled':2}
  391. lcap = {'default':0, 'round':1, 'square':2}
  392. if node.get('width'):
  393. self.canvas.setLineWidth(utils.unit_get(node.get('width')))
  394. if node.get('join'):
  395. self.canvas.setLineJoin(ljoin[node.get('join')])
  396. if node.get('cap'):
  397. self.canvas.setLineCap(lcap[node.get('cap')])
  398. if node.get('miterLimit'):
  399. self.canvas.setDash(utils.unit_get(node.get('miterLimit')))
  400. if node.get('dash'):
  401. dashes = node.get('dash').split(',')
  402. for x in range(len(dashes)):
  403. dashes[x]=utils.unit_get(dashes[x])
  404. self.canvas.setDash(node.get('dash').split(','))
  405. def _image(self, node):
  406. import urllib
  407. import urlparse
  408. from reportlab.lib.utils import ImageReader
  409. nfile = node.get('file')
  410. if not nfile:
  411. if node.get('name'):
  412. image_data = self.images[node.get('name')]
  413. _logger.debug("Image %s used", node.get('name'))
  414. s = StringIO(image_data)
  415. else:
  416. newtext = node.text
  417. if self.localcontext:
  418. res = utils._regex.findall(newtext)
  419. for key in res:
  420. newtext = eval(key, {}, self.localcontext) or ''
  421. image_data = None
  422. if newtext:
  423. image_data = base64.decodestring(newtext)
  424. if image_data:
  425. s = StringIO(image_data)
  426. else:
  427. _logger.debug("No image data!")
  428. return False
  429. else:
  430. if nfile in self.images:
  431. s = StringIO(self.images[nfile])
  432. else:
  433. try:
  434. up = urlparse.urlparse(str(nfile))
  435. except ValueError:
  436. up = False
  437. if up and up.scheme:
  438. # RFC: do we really want to open external URLs?
  439. # Are we safe from cross-site scripting or attacks?
  440. _logger.debug("Retrieve image from %s", nfile)
  441. u = urllib.urlopen(str(nfile))
  442. s = StringIO(u.read())
  443. else:
  444. _logger.debug("Open image file %s ", nfile)
  445. s = _open_image(nfile, path=self.path)
  446. try:
  447. img = ImageReader(s)
  448. (sx,sy) = img.getSize()
  449. _logger.debug("Image is %dx%d", sx, sy)
  450. args = { 'x': 0.0, 'y': 0.0, 'mask': 'auto'}
  451. for tag in ('width','height','x','y'):
  452. if node.get(tag):
  453. args[tag] = utils.unit_get(node.get(tag))
  454. if ('width' in args) and (not 'height' in args):
  455. args['height'] = sy * args['width'] / sx
  456. elif ('height' in args) and (not 'width' in args):
  457. args['width'] = sx * args['height'] / sy
  458. elif ('width' in args) and ('height' in args):
  459. if (float(args['width'])/args['height'])>(float(sx)>sy):
  460. args['width'] = sx * args['height'] / sy
  461. else:
  462. args['height'] = sy * args['width'] / sx
  463. self.canvas.drawImage(img, **args)
  464. finally:
  465. s.close()
  466. # self.canvas._doc.SaveToFile(self.canvas._filename, self.canvas)
  467. def _path(self, node):
  468. self.path = self.canvas.beginPath()
  469. self.path.moveTo(**utils.attr_get(node, ['x','y']))
  470. for n in utils._child_get(node, self):
  471. if not n.text :
  472. if n.tag=='moveto':
  473. vals = utils.text_get(n).split()
  474. self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1]))
  475. elif n.tag=='curvesto':
  476. vals = utils.text_get(n).split()
  477. while len(vals)>5:
  478. pos=[]
  479. while len(pos)<6:
  480. pos.append(utils.unit_get(vals.pop(0)))
  481. self.path.curveTo(*pos)
  482. elif n.text:
  483. data = n.text.split() # Not sure if I must merge all TEXT_NODE ?
  484. while len(data)>1:
  485. x = utils.unit_get(data.pop(0))
  486. y = utils.unit_get(data.pop(0))
  487. self.path.lineTo(x,y)
  488. if (not node.get('close')) or utils.bool_get(node.get('close')):
  489. self.path.close()
  490. self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
  491. def setFont(self, node):
  492. fontname = node.get('name')
  493. if fontname not in pdfmetrics.getRegisteredFontNames()\
  494. or fontname not in pdfmetrics.standardFonts:
  495. # let reportlab attempt to find it
  496. try:
  497. pdfmetrics.getFont(fontname)
  498. except Exception:
  499. _logger.debug('Could not locate font %s, substituting default: %s',
  500. fontname,
  501. self.canvas._fontname)
  502. fontname = self.canvas._fontname
  503. return self.canvas.setFont(fontname, utils.unit_get(node.get('size')))
  504. def render(self, node):
  505. tags = {
  506. 'drawCentredString': self._drawCenteredString,
  507. 'drawRightString': self._drawRightString,
  508. 'drawString': self._drawString,
  509. 'rect': self._rect,
  510. 'ellipse': self._ellipse,
  511. 'lines': self._lines,
  512. 'grid': self._grid,
  513. 'curves': self._curves,
  514. 'fill': lambda node: self.canvas.setFillColor(color.get(node.get('color'))),
  515. 'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.get('color'))),
  516. 'setFont': self.setFont ,
  517. 'place': self._place,
  518. 'circle': self._circle,
  519. 'lineMode': self._line_mode,
  520. 'path': self._path,
  521. 'rotate': lambda node: self.canvas.rotate(float(node.get('degrees'))),
  522. 'translate': self._translate,
  523. 'image': self._image
  524. }
  525. for n in utils._child_get(node, self):
  526. if n.tag in tags:
  527. tags[n.tag](n)
  528. class _rml_draw(object):
  529. def __init__(self, localcontext, node, styles, images=None, path='.', title=None):
  530. if images is None:
  531. images = {}
  532. self.localcontext = localcontext
  533. self.node = node
  534. self.styles = styles
  535. self.canvas = None
  536. self.images = images
  537. self.path = path
  538. self.canvas_title = title
  539. def render(self, canvas, doc):
  540. canvas.saveState()
  541. cnv = _rml_canvas(canvas, self.localcontext, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title)
  542. cnv.render(self.node)
  543. canvas.restoreState()
  544. class _rml_Illustration(platypus.flowables.Flowable):
  545. def __init__(self, node, localcontext, styles, self2):
  546. self.localcontext = (localcontext or {}).copy()
  547. self.node = node
  548. self.styles = styles
  549. self.width = utils.unit_get(node.get('width'))
  550. self.height = utils.unit_get(node.get('height'))
  551. self.self2 = self2
  552. def wrap(self, *args):
  553. return self.width, self.height
  554. def draw(self):
  555. drw = _rml_draw(self.localcontext ,self.node,self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title)
  556. drw.render(self.canv, None)
  557. class _rml_flowable(object):
  558. def __init__(self, doc, localcontext, images=None, path='.', title=None):
  559. if images is None:
  560. images = {}
  561. self.localcontext = localcontext
  562. self.doc = doc
  563. self.styles = doc.styles
  564. self.images = images
  565. self.path = path
  566. self.title = title
  567. def _textual(self, node):
  568. rc1 = utils._process_text(self, node.text or '')
  569. for n in utils._child_get(node,self):
  570. txt_n = copy.deepcopy(n)
  571. for key in txt_n.attrib.keys():
  572. if key in ('rml_except', 'rml_loop', 'rml_tag'):
  573. del txt_n.attrib[key]
  574. if not n.tag == 'bullet':
  575. txt_n.text = utils.xml2str(self._textual(n))
  576. txt_n.tail = n.tail and utils.xml2str(utils._process_text(self, n.tail.replace('\n',''))) or ''
  577. rc1 += etree.tostring(txt_n)
  578. return rc1
  579. def _table(self, node):
  580. children = utils._child_get(node,self,'tr')
  581. if not children:
  582. return None
  583. length = 0
  584. colwidths = None
  585. rowheights = None
  586. data = []
  587. styles = []
  588. posy = 0
  589. for tr in children:
  590. paraStyle = None
  591. if tr.get('style'):
  592. st = copy.deepcopy(self.styles.table_styles[tr.get('style')])
  593. for si in range(len(st._cmds)):
  594. s = list(st._cmds[si])
  595. s[1] = (s[1][0],posy)
  596. s[2] = (s[2][0],posy)
  597. st._cmds[si] = tuple(s)
  598. styles.append(st)
  599. if tr.get('paraStyle'):
  600. paraStyle = self.styles.styles[tr.get('paraStyle')]
  601. data2 = []
  602. posx = 0
  603. for td in utils._child_get(tr, self,'td'):
  604. if td.get('style'):
  605. st = copy.deepcopy(self.styles.table_styles[td.get('style')])
  606. for s in st._cmds:
  607. s[1][1] = posy
  608. s[2][1] = posy
  609. s[1][0] = posx
  610. s[2][0] = posx
  611. styles.append(st)
  612. if td.get('paraStyle'):
  613. # TODO: merge styles
  614. paraStyle = self.styles.styles[td.get('paraStyle')]
  615. posx += 1
  616. flow = []
  617. for n in utils._child_get(td, self):
  618. if n.tag == etree.Comment:
  619. n.text = ''
  620. continue
  621. fl = self._flowable(n, extra_style=paraStyle)
  622. if isinstance(fl,list):
  623. flow += fl
  624. else:
  625. flow.append( fl )
  626. if not len(flow):
  627. flow = self._textual(td)
  628. data2.append( flow )
  629. if len(data2)>length:
  630. length=len(data2)
  631. for ab in data:
  632. while len(ab)<length:
  633. ab.append('')
  634. while len(data2)<length:
  635. data2.append('')
  636. data.append( data2 )
  637. posy += 1
  638. if node.get('colWidths'):
  639. assert length == len(node.get('colWidths').split(','))
  640. colwidths = [utils.unit_get(f.strip()) for f in node.get('colWidths').split(',')]
  641. if node.get('rowHeights'):
  642. rowheights = [utils.unit_get(f.strip()) for f in node.get('rowHeights').split(',')]
  643. if len(rowheights) == 1:
  644. rowheights = rowheights[0]
  645. table = platypus.LongTable(data = data, colWidths=colwidths, rowHeights=rowheights, **(utils.attr_get(node, ['splitByRow'] ,{'repeatRows':'int','repeatCols':'int'})))
  646. if node.get('style'):
  647. table.setStyle(self.styles.table_styles[node.get('style')])
  648. for s in styles:
  649. table.setStyle(s)
  650. return table
  651. def _illustration(self, node):
  652. return _rml_Illustration(node, self.localcontext, self.styles, self)
  653. def _textual_image(self, node):
  654. return base64.decodestring(node.text)
  655. def _pto(self, node):
  656. sub_story = []
  657. pto_header = None
  658. pto_trailer = None
  659. for node in utils._child_get(node, self):
  660. if node.tag == etree.Comment:
  661. node.text = ''
  662. continue
  663. elif node.tag=='pto_header':
  664. pto_header = self.render(node)
  665. elif node.tag=='pto_trailer':
  666. pto_trailer = self.render(node)
  667. else:
  668. flow = self._flowable(node)
  669. if flow:
  670. if isinstance(flow,list):
  671. sub_story = sub_story + flow
  672. else:
  673. sub_story.append(flow)
  674. return platypus.flowables.PTOContainer(sub_story, trailer=pto_trailer, header=pto_header)
  675. def _flowable(self, node, extra_style=None):
  676. if node.tag=='pto':
  677. return self._pto(node)
  678. if node.tag=='para':
  679. style = self.styles.para_style_get(node)
  680. if extra_style:
  681. style.__dict__.update(extra_style)
  682. result = []
  683. for i in self._textual(node).split('\n'):
  684. result.append(platypus.Paragraph(i, style, **(utils.attr_get(node, [], {'bulletText':'str'}))))
  685. return result
  686. elif node.tag=='barCode':
  687. try:
  688. from reportlab.graphics.barcode import code128
  689. from reportlab.graphics.barcode import code39
  690. from reportlab.graphics.barcode import code93
  691. from reportlab.graphics.barcode import common
  692. from reportlab.graphics.barcode import fourstate
  693. from reportlab.graphics.barcode import usps
  694. from reportlab.graphics.barcode import createBarcodeDrawing
  695. except ImportError:
  696. _logger.warning("Cannot use barcode renderers:", exc_info=True)
  697. return None
  698. args = utils.attr_get(node, [], {'ratio':'float','xdim':'unit','height':'unit','checksum':'int','quiet':'int','width':'unit','stop':'bool','bearers':'int','barWidth':'float','barHeight':'float'})
  699. codes = {
  700. 'codabar': lambda x: common.Codabar(x, **args),
  701. 'code11': lambda x: common.Code11(x, **args),
  702. 'code128': lambda x: code128.Code128(str(x), **args),
  703. 'standard39': lambda x: code39.Standard39(str(x), **args),
  704. 'standard93': lambda x: code93.Standard93(str(x), **args),
  705. 'i2of5': lambda x: common.I2of5(x, **args),
  706. 'extended39': lambda x: code39.Extended39(str(x), **args),
  707. 'extended93': lambda x: code93.Extended93(str(x), **args),
  708. 'msi': lambda x: common.MSI(x, **args),
  709. 'fim': lambda x: usps.FIM(x, **args),
  710. 'postnet': lambda x: usps.POSTNET(x, **args),
  711. 'ean13': lambda x: createBarcodeDrawing('EAN13', value=str(x), **args),
  712. 'qrcode': lambda x: createBarcodeDrawing('QR', value=x, **args),
  713. }
  714. code = 'code128'
  715. if node.get('code'):
  716. code = node.get('code').lower()
  717. return codes[code](self._textual(node))
  718. elif node.tag=='name':
  719. self.styles.names[ node.get('id')] = node.get('value')
  720. return None
  721. elif node.tag=='xpre':
  722. style = self.styles.para_style_get(node)
  723. return platypus.XPreformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int','frags':'int'})))
  724. elif node.tag=='pre':
  725. style = self.styles.para_style_get(node)
  726. return platypus.Preformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int'})))
  727. elif node.tag=='illustration':
  728. return self._illustration(node)
  729. elif node.tag=='blockTable':
  730. return self._table(node)
  731. elif node.tag=='title':
  732. styles = reportlab.lib.styles.getSampleStyleSheet()
  733. style = styles['Title']
  734. return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
  735. elif re.match('^h([1-9]+[0-9]*)$', (node.tag or '')):
  736. styles = reportlab.lib.styles.getSampleStyleSheet()
  737. style = styles['Heading'+str(node.tag[1:])]
  738. return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
  739. elif node.tag=='image':
  740. image_data = False
  741. if not node.get('file'):
  742. if node.get('name'):
  743. if node.get('name') in self.doc.images:
  744. _logger.debug("Image %s read ", node.get('name'))
  745. image_data = self.doc.images[node.get('name')].read()
  746. else:
  747. _logger.warning("Image %s not defined", node.get('name'))
  748. return False
  749. else:
  750. import base64
  751. newtext = node.text
  752. if self.localcontext:
  753. newtext = utils._process_text(self, node.text or '')
  754. image_data = base64.decodestring(newtext)
  755. if not image_data:
  756. _logger.debug("No inline image data")
  757. return False
  758. image = StringIO(image_data)
  759. else:
  760. _logger.debug("Image get from file %s", node.get('file'))
  761. image = _open_image(node.get('file'), path=self.doc.path)
  762. return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
  763. elif node.tag=='spacer':
  764. if node.get('width'):
  765. width = utils.unit_get(node.get('width'))
  766. else:
  767. width = utils.unit_get('1cm')
  768. length = utils.unit_get(node.get('length'))
  769. return platypus.Spacer(width=width, height=length)
  770. elif node.tag=='section':
  771. return self.render(node)
  772. elif node.tag == 'pageNumberReset':
  773. return PageReset()
  774. elif node.tag in ('pageBreak', 'nextPage'):
  775. return platypus.PageBreak()
  776. elif node.tag=='condPageBreak':
  777. return platypus.CondPageBreak(**(utils.attr_get(node, ['height'])))
  778. elif node.tag=='setNextTemplate':
  779. return platypus.NextPageTemplate(str(node.get('name')))
  780. elif node.tag=='nextFrame':
  781. return platypus.CondPageBreak(1000) # TODO: change the 1000 !
  782. elif node.tag == 'setNextFrame':
  783. from reportlab.platypus.doctemplate import NextFrameFlowable
  784. return NextFrameFlowable(str(node.get('name')))
  785. elif node.tag == 'currentFrame':
  786. from reportlab.platypus.doctemplate import CurrentFrameFlowable
  787. return CurrentFrameFlowable(str(node.get('name')))
  788. elif node.tag == 'frameEnd':
  789. return EndFrameFlowable()
  790. elif node.tag == 'hr':
  791. width_hr=node.get('width') or '100%'
  792. color_hr=node.get('color') or 'black'
  793. thickness_hr=node.get('thickness') or 1
  794. lineCap_hr=node.get('lineCap') or 'round'
  795. return platypus.flowables.HRFlowable(width=width_hr,color=color.get(color_hr),thickness=float(thickness_hr),lineCap=str(lineCap_hr))
  796. else:
  797. sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.tag,))
  798. return None
  799. def render(self, node_story):
  800. def process_story(node_story):
  801. sub_story = []
  802. for node in utils._child_get(node_story, self):
  803. if node.tag == etree.Comment:
  804. node.text = ''
  805. continue
  806. flow = self._flowable(node)
  807. if flow:
  808. if isinstance(flow,list):
  809. sub_story = sub_story + flow
  810. else:
  811. sub_story.append(flow)
  812. return sub_story
  813. return process_story(node_story)
  814. class EndFrameFlowable(ActionFlowable):
  815. def __init__(self,resume=0):
  816. ActionFlowable.__init__(self,('frameEnd',resume))
  817. class TinyDocTemplate(platypus.BaseDocTemplate):
  818. def beforeDocument(self):
  819. # Store some useful value directly inside canvas, so it's available
  820. # on flowable drawing (needed for proper PageCount handling)
  821. self.canv._doPageReset = False
  822. self.canv._storyCount = 0
  823. def ___handle_pageBegin(self):
  824. self.page += 1
  825. self.pageTemplate.beforeDrawPage(self.canv,self)
  826. self.pageTemplate.checkPageSize(self.canv,self)
  827. self.pageTemplate.onPage(self.canv,self)
  828. for f in self.pageTemplate.frames: f._reset()
  829. self.beforePage()
  830. self._curPageFlowableCount = 0
  831. if hasattr(self,'_nextFrameIndex'):
  832. del self._nextFrameIndex
  833. for f in self.pageTemplate.frames:
  834. if f.id == 'first':
  835. self.frame = f
  836. break
  837. self.handle_frameBegin()
  838. def afterPage(self):
  839. if self.canv._doPageReset:
  840. # Following a <pageReset/> tag:
  841. # - we reset page number to 0
  842. # - we add an new PageCount flowable (relative to the current
  843. # story number), but not for NumeredCanvas at is handle page
  844. # count itself)
  845. # NOTE: _rml_template render() method add a PageReset flowable at end
  846. # of each story, so we're sure to pass here at least once per story.
  847. if not isinstance(self.canv, NumberedCanvas):
  848. self.handle_flowable([ PageCount(story_count=self.canv._storyCount) ])
  849. self.canv._pageCount = self.page
  850. self.page = 0
  851. self.canv._flag = True
  852. self.canv._pageNumber = 0
  853. self.canv._doPageReset = False
  854. self.canv._storyCount += 1
  855. class _rml_template(object):
  856. def __init__(self, localcontext, out, node, doc, images=None, path='.', title=None):
  857. if images is None:
  858. images = {}
  859. if not localcontext:
  860. localcontext={'internal_header':True}
  861. self.localcontext = localcontext
  862. self.images= images
  863. self.path = path
  864. self.title = title
  865. pagesize_map = {'a4': A4,
  866. 'us_letter': letter
  867. }
  868. pageSize = (841.8897637795275, 595.275590551181)
  869. self.doc_tmpl = TinyDocTemplate(out, pagesize=pageSize, **utils.attr_get(node, ['leftMargin','rightMargin','topMargin','bottomMargin'], {'allowSplitting':'int','showBoundary':'bool','rotation':'int','title':'str','author':'str'}))
  870. self.page_templates = []
  871. self.styles = doc.styles
  872. self.doc = doc
  873. self.image=[]
  874. pts = node.findall('pageTemplate')
  875. for pt in pts:
  876. frames = []
  877. for frame_el in pt.findall('frame'):
  878. frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) )
  879. if utils.attr_get(frame_el, ['last']):
  880. frame.lastFrame = True
  881. frames.append( frame )
  882. try :
  883. gr = pt.findall('pageGraphics')\
  884. or pt[1].findall('pageGraphics')
  885. except Exception: # FIXME: be even more specific, perhaps?
  886. gr=''
  887. if len(gr):
  888. # self.image=[ n for n in utils._child_get(gr[0], self) if n.tag=='image' or not self.localcontext]
  889. drw = _rml_draw(self.localcontext,gr[0], self.doc, images=images, path=self.path, title=self.title)
  890. self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
  891. else:
  892. drw = _rml_draw(self.localcontext,node,self.doc,title=self.title)
  893. self.page_templates.append( platypus.PageTemplate(frames=frames,onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
  894. self.doc_tmpl.addPageTemplates(self.page_templates)
  895. def render(self, node_stories):
  896. if self.localcontext and not self.localcontext.get('internal_header',False):
  897. del self.localcontext['internal_header']
  898. fis = []
  899. r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title)
  900. story_cnt = 0
  901. for node_story in node_stories:
  902. if story_cnt > 0:
  903. fis.append(platypus.PageBreak())
  904. fis += r.render(node_story)
  905. # Reset Page Number with new story tag
  906. fis.append(PageReset())
  907. story_cnt += 1
  908. if self.localcontext and self.localcontext.get('internal_header',False):
  909. self.doc_tmpl.afterFlowable(fis)
  910. self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas)
  911. else:
  912. self.doc_tmpl.build(fis)
  913. def parseNode(rml, localcontext=None, fout=None, images=None, path='.', title=None):
  914. node = etree.XML(rml)
  915. r = _rml_doc(node, localcontext, images, path, title=title)
  916. #try to override some font mappings
  917. try:
  918. from customfonts import SetCustomFonts
  919. SetCustomFonts(r)
  920. except ImportError:
  921. # means there is no custom fonts mapping in this system.
  922. pass
  923. except Exception:
  924. _logger.warning('Cannot set font mapping', exc_info=True)
  925. pass
  926. fp = StringIO()
  927. r.render(fp)
  928. return fp.getvalue()
  929. def parseString(rml, localcontext=None, fout=None, images=None, path='.', title=None):
  930. node = etree.XML(rml)
  931. r = _rml_doc(node, localcontext, images, path, title=title)
  932. #try to override some font mappings
  933. try:
  934. from customfonts import SetCustomFonts
  935. SetCustomFonts(r)
  936. except Exception:
  937. pass
  938. if fout:
  939. fp = file(fout,'wb')
  940. r.render(fp)
  941. fp.close()
  942. return fout
  943. else:
  944. fp = StringIO()
  945. r.render(fp)
  946. return fp.getvalue()
  947. def trml2pdf_help():
  948. print 'Usage: trml2pdf input.rml >output.pdf'
  949. print 'Render the standard input (RML) and output a PDF file'
  950. sys.exit(0)
  951. if __name__=="__main__":
  952. if len(sys.argv)>1:
  953. if sys.argv[1]=='--help':
  954. trml2pdf_help()
  955. print parseString(file(sys.argv[1], 'r').read()),
  956. else:
  957. print 'Usage: trml2pdf input.rml >output.pdf'
  958. print 'Try \'trml2pdf --help\' for more information.'
  959. # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: