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