corrected a typo
[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(fontname, filename ))
260         if (mode == 'all'):
261             addMapping(face, 0, 0, fontname)    #normal
262             addMapping(face, 0, 1, fontname)    #italic
263             addMapping(face, 1, 0, fontname)    #bold
264             addMapping(face, 1, 1, fontname)    #italic and bold
265         elif (mode== 'normal') or (mode == 'regular'):
266             addMapping(face, 0, 0, fontname)    #normal
267         elif (mode == 'italic'):
268             addMapping(face, 0, 1, fontname)    #italic
269         elif (mode == 'bold'):
270             addMapping(face, 1, 0, fontname)    #bold
271         elif (mode == 'bolditalic'):
272             addMapping(face, 1, 1, fontname)    #italic and bold
273
274     def _textual_image(self, node):
275         rc = ''
276         for n in node:
277             rc +=( etree.tostring(n) or '') + n.tail
278         return base64.decodestring(node.tostring())
279
280     def _images(self, el):
281         result = {}
282         for node in el.findall('.//image'):
283             rc =( node.text or '')
284             result[node.get('name')] = base64.decodestring(rc)
285         return result
286
287     def render(self, out):
288         el = self.etree.findall('.//docinit')
289         if el:
290             self.docinit(el)
291
292         el = self.etree.findall('.//stylesheet')
293         self.styles = _rml_styles(el,self.localcontext)
294
295         el = self.etree.findall('.//images')
296         if el:
297             self.images.update( self._images(el[0]) )
298
299         el = self.etree.findall('.//template')
300         if len(el):
301             pt_obj = _rml_template(self.localcontext, out, el[0], self, images=self.images, path=self.path, title=self.title)
302             el = utils._child_get(self.etree, self, 'story')
303             pt_obj.render(el)
304         else:
305             self.canvas = canvas.Canvas(out)
306             pd = self.etree.find('pageDrawing')[0]
307             pd_obj = _rml_canvas(self.canvas, self.localcontext, None, self, self.images, path=self.path, title=self.title)
308             pd_obj.render(pd)
309
310             self.canvas.showPage()
311             self.canvas.save()
312
313 class _rml_canvas(object):
314     def __init__(self, canvas, localcontext, doc_tmpl=None, doc=None, images={}, path='.', title=None):
315         self.localcontext = localcontext
316         self.canvas = canvas
317         self.styles = doc.styles
318         self.doc_tmpl = doc_tmpl
319         self.doc = doc
320         self.images = images
321         self.path = path
322         self.title = title
323         self._logger = logging.getLogger('report.rml.canvas')
324         if self.title:
325             self.canvas.setTitle(self.title)
326
327     def _textual(self, node, x=0, y=0):
328         text = node.text and node.text.encode('utf-8') or ''
329         rc = utils._process_text(self, text)
330         for n in node:
331             if n.tag == 'seq':
332                 from reportlab.lib.sequencer import getSequencer
333                 seq = getSequencer()
334                 rc += str(seq.next(n.get('id')))
335             if n.tag == 'pageCount':
336                 if x or y:
337                     self.canvas.translate(x,y)
338                 self.canvas.doForm('pageCount')
339                 if x or y:
340                     self.canvas.translate(-x,-y)
341             if n.tag == 'pageNumber':
342                 rc += str(self.canvas.getPageNumber())
343             rc += utils._process_text(self, n.tail)
344         return rc.replace('\n','')
345
346     def _drawString(self, node):
347         v = utils.attr_get(node, ['x','y'])
348         text=self._textual(node, **v)
349         text = utils.xml2str(text)
350         self.canvas.drawString(text=text, **v)
351
352     def _drawCenteredString(self, node):
353         v = utils.attr_get(node, ['x','y'])
354         text=self._textual(node, **v)
355         text = utils.xml2str(text)
356         self.canvas.drawCentredString(text=text, **v)
357
358     def _drawRightString(self, node):
359         v = utils.attr_get(node, ['x','y'])
360         text=self._textual(node, **v)
361         text = utils.xml2str(text)
362         self.canvas.drawRightString(text=text, **v)
363
364     def _rect(self, node):
365         if node.get('round'):
366             self.canvas.roundRect(radius=utils.unit_get(node.get('round')), **utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
367         else:
368             self.canvas.rect(**utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
369
370     def _ellipse(self, node):
371         x1 = utils.unit_get(node.get('x'))
372         x2 = utils.unit_get(node.get('width'))
373         y1 = utils.unit_get(node.get('y'))
374         y2 = utils.unit_get(node.get('height'))
375
376         self.canvas.ellipse(x1,y1,x2,y2, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
377
378     def _curves(self, node):
379         line_str = node.text.split()
380         lines = []
381         while len(line_str)>7:
382             self.canvas.bezier(*[utils.unit_get(l) for l in line_str[0:8]])
383             line_str = line_str[8:]
384
385     def _lines(self, node):
386         line_str = node.text.split()
387         lines = []
388         while len(line_str)>3:
389             lines.append([utils.unit_get(l) for l in line_str[0:4]])
390             line_str = line_str[4:]
391         self.canvas.lines(lines)
392
393     def _grid(self, node):
394         xlist = [utils.unit_get(s) for s in node.get('xs').split(',')]
395         ylist = [utils.unit_get(s) for s in node.get('ys').split(',')]
396
397         self.canvas.grid(xlist, ylist)
398
399     def _translate(self, node):
400         dx = utils.unit_get(node.get('dx')) or 0
401         dy = utils.unit_get(node.get('dy')) or 0
402         self.canvas.translate(dx,dy)
403
404     def _circle(self, node):
405         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'}))
406
407     def _place(self, node):
408         flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title).render(node)
409         infos = utils.attr_get(node, ['x','y','width','height'])
410
411         infos['y']+=infos['height']
412         for flow in flows:
413             w,h = flow.wrap(infos['width'], infos['height'])
414             if w<=infos['width'] and h<=infos['height']:
415                 infos['y']-=h
416                 flow.drawOn(self.canvas,infos['x'],infos['y'])
417                 infos['height']-=h
418             else:
419                 raise ValueError, "Not enough space"
420
421     def _line_mode(self, node):
422         ljoin = {'round':1, 'mitered':0, 'bevelled':2}
423         lcap = {'default':0, 'round':1, 'square':2}
424
425         if node.get('width'):
426             self.canvas.setLineWidth(utils.unit_get(node.get('width')))
427         if node.get('join'):
428             self.canvas.setLineJoin(ljoin[node.get('join')])
429         if node.get('cap'):
430             self.canvas.setLineCap(lcap[node.get('cap')])
431         if node.get('miterLimit'):
432             self.canvas.setDash(utils.unit_get(node.get('miterLimit')))
433         if node.get('dash'):
434             dashes = node.get('dash').split(',')
435             for x in range(len(dashes)):
436                 dashes[x]=utils.unit_get(dashes[x])
437             self.canvas.setDash(node.get('dash').split(','))
438
439     def _image(self, node):
440         import urllib
441         import urlparse
442         from reportlab.lib.utils import ImageReader
443         nfile = node.get('file')
444         if not nfile:
445             if node.get('name'):
446                 image_data = self.images[node.get('name')]
447                 self._logger.debug("Image %s used", node.get('name'))
448                 s = StringIO(image_data)
449             else:
450                 if self.localcontext:
451                     res = utils._regex.findall(node.text)
452                     for key in res:
453                         newtext = eval(key, {}, self.localcontext)
454                         node.text = newtext
455                 image_data = None
456                 if node.text:
457                     image_data = base64.decodestring(node.text)
458                 if image_data:
459                     s = StringIO(image_data)
460                 else:
461                     self._logger.debug("No image data!")
462                     return False
463         else:
464             if nfile in self.images:
465                 s = StringIO(self.images[nfile])
466             else:
467                 try:
468                     up = urlparse.urlparse(str(nfile))
469                 except ValueError:
470                     up = False
471                 if up and up.scheme:
472                     # RFC: do we really want to open external URLs?
473                     # Are we safe from cross-site scripting or attacks?
474                     self._logger.debug("Retrieve image from %s", nfile)
475                     u = urllib.urlopen(str(nfile))
476                     s = StringIO(u.read())
477                 else:
478                     self._logger.debug("Open image file %s ", nfile)
479                     s = _open_image(nfile, path=self.path)
480         img = ImageReader(s)
481         (sx,sy) = img.getSize()
482         self._logger.debug("Image is %dx%d", sx, sy)
483
484         args = { 'x': 0.0, 'y': 0.0 }
485         for tag in ('width','height','x','y'):
486             if node.get(tag):
487                 args[tag] = utils.unit_get(node.get(tag))
488         if ('width' in args) and (not 'height' in args):
489             args['height'] = sy * args['width'] / sx
490         elif ('height' in args) and (not 'width' in args):
491             args['width'] = sx * args['height'] / sy
492         elif ('width' in args) and ('height' in args):
493             if (float(args['width'])/args['height'])>(float(sx)>sy):
494                 args['width'] = sx * args['height'] / sy
495             else:
496                 args['height'] = sy * args['width'] / sx
497         self.canvas.drawImage(img, **args)
498 #        self.canvas._doc.SaveToFile(self.canvas._filename, self.canvas)
499
500     def _path(self, node):
501         self.path = self.canvas.beginPath()
502         self.path.moveTo(**utils.attr_get(node, ['x','y']))
503         for n in utils._child_get(node, self):
504             if not n.text :
505                 if n.tag=='moveto':
506                     vals = utils.text_get(n).split()
507                     self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1]))
508                 elif n.tag=='curvesto':
509                     vals = utils.text_get(n).split()
510                     while len(vals)>5:
511                         pos=[]
512                         while len(pos)<6:
513                             pos.append(utils.unit_get(vals.pop(0)))
514                         self.path.curveTo(*pos)
515             elif n.text:
516                 data = n.text.split()               # Not sure if I must merge all TEXT_NODE ?
517                 while len(data)>1:
518                     x = utils.unit_get(data.pop(0))
519                     y = utils.unit_get(data.pop(0))
520                     self.path.lineTo(x,y)
521         if (not node.get('close')) or utils.bool_get(node.get('close')):
522             self.path.close()
523         self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
524
525     def setFont(self, node):
526         fontname = node.get('name')
527         if fontname not in pdfmetrics.getRegisteredFontNames()\
528              or fontname not in pdfmetrics.standardFonts:
529                 # let reportlab attempt to find it
530                 try:
531                     pdfmetrics.getFont(fontname)
532                 except Exception:
533                     logging.getLogger('report.fonts').debug('Could not locate font %s, substituting default: %s',
534                                  fontname,
535                                  self.canvas._fontname)
536                     fontname = self.canvas._fontname
537         return self.canvas.setFont(fontname, utils.unit_get(node.get('size')))
538
539     def render(self, node):
540         tags = {
541             'drawCentredString': self._drawCenteredString,
542             'drawRightString': self._drawRightString,
543             'drawString': self._drawString,
544             'rect': self._rect,
545             'ellipse': self._ellipse,
546             'lines': self._lines,
547             'grid': self._grid,
548             'curves': self._curves,
549             'fill': lambda node: self.canvas.setFillColor(color.get(node.get('color'))),
550             'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.get('color'))),
551             'setFont': self.setFont ,
552             'place': self._place,
553             'circle': self._circle,
554             'lineMode': self._line_mode,
555             'path': self._path,
556             'rotate': lambda node: self.canvas.rotate(float(node.get('degrees'))),
557             'translate': self._translate,
558             'image': self._image
559         }
560         for n in utils._child_get(node, self):
561             if n.tag in tags:
562                 tags[n.tag](n)
563
564 class _rml_draw(object):
565     def __init__(self, localcontext ,node, styles, images={}, path='.', title=None):
566         self.localcontext = localcontext
567         self.node = node
568         self.styles = styles
569         self.canvas = None
570         self.images = images
571         self.path = path
572         self.canvas_title = title
573
574     def render(self, canvas, doc):
575         canvas.saveState()
576         cnv = _rml_canvas(canvas, self.localcontext, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title)
577         cnv.render(self.node)
578         canvas.restoreState()
579
580 class _rml_Illustration(platypus.flowables.Flowable):
581     def __init__(self, node, localcontext, styles, self2):
582         self.localcontext = (localcontext or {}).copy()
583         self.node = node
584         self.styles = styles
585         self.width = utils.unit_get(node.get('width'))
586         self.height = utils.unit_get(node.get('height'))
587         self.self2 = self2
588     def wrap(self, *args):
589         return (self.width, self.height)
590     def draw(self):
591         drw = _rml_draw(self.localcontext ,self.node,self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title)
592         drw.render(self.canv, None)
593
594 class _rml_flowable(object):
595     def __init__(self, doc, localcontext, images=None, path='.', title=None):
596         self.localcontext = localcontext
597         self.doc = doc
598         self.styles = doc.styles
599         self.images = images or {}
600         self.path = path
601         self.title = title
602         self._logger = logging.getLogger('report.rml.flowable')
603
604     def _textual(self, node):
605         rc1 = utils._process_text(self, node.text or '')
606         for n in utils._child_get(node,self):
607             txt_n = copy.deepcopy(n)
608             for key in txt_n.attrib.keys():
609                 if key in ('rml_except', 'rml_loop', 'rml_tag'):
610                     del txt_n.attrib[key]
611             if True or not self._textual(n).isspace():
612                 if not n.tag == 'bullet':
613                     txt_n.text = utils.xml2str(self._textual(n))
614                 txt_n.tail = n.tail and utils._process_text(self, n.tail.replace('\n','')) or ''
615                 rc1 += etree.tostring(txt_n)
616         return rc1
617
618     def _table(self, node):
619         children = utils._child_get(node,self,'tr')
620         if not children:
621             return None
622         length = 0
623         colwidths = None
624         rowheights = None
625         data = []
626         styles = []
627         posy = 0
628         for tr in children:
629             paraStyle = None
630             if tr.get('style'):
631                 st = copy.deepcopy(self.styles.table_styles[tr.get('style')])
632                 for s in st._cmds:
633                     s[1][1] = posy
634                     s[2][1] = posy
635                 styles.append(st)
636             if tr.get('paraStyle'):
637                 paraStyle = self.styles.styles[tr.get('paraStyle')]
638             data2 = []
639             posx = 0
640             for td in utils._child_get(tr, self,'td'):
641                 if td.get('style'):
642                     st = copy.deepcopy(self.styles.table_styles[td.get('style')])
643                     for s in st._cmds:
644                         s[1][1] = posy
645                         s[2][1] = posy
646                         s[1][0] = posx
647                         s[2][0] = posx
648                     styles.append(st)
649                 if td.get('paraStyle'):
650                     # TODO: merge styles
651                     paraStyle = self.styles.styles[td.get('paraStyle')]
652                 posx += 1
653
654                 flow = []
655                 for n in utils._child_get(td, self):
656                     if n.tag == etree.Comment:
657                         n.text = ''
658                         continue
659                     fl = self._flowable(n, extra_style=paraStyle)
660                     if isinstance(fl,list):
661                         flow  += fl
662                     else:
663                         flow.append( fl )
664
665                 if not len(flow):
666                     flow = self._textual(td)
667                 data2.append( flow )
668             if len(data2)>length:
669                 length=len(data2)
670                 for ab in data:
671                     while len(ab)<length:
672                         ab.append('')
673             while len(data2)<length:
674                 data2.append('')
675             data.append( data2 )
676             posy += 1
677
678         if node.get('colWidths'):
679             assert length == len(node.get('colWidths').split(','))
680             colwidths = [utils.unit_get(f.strip()) for f in node.get('colWidths').split(',')]
681         if node.get('rowHeights'):
682             rowheights = [utils.unit_get(f.strip()) for f in node.get('rowHeights').split(',')]
683             if len(rowheights) == 1:
684                 rowheights = rowheights[0]
685         table = platypus.LongTable(data = data, colWidths=colwidths, rowHeights=rowheights, **(utils.attr_get(node, ['splitByRow'] ,{'repeatRows':'int','repeatCols':'int'})))
686         if node.get('style'):
687             table.setStyle(self.styles.table_styles[node.get('style')])
688         for s in styles:
689             table.setStyle(s)
690         return table
691
692     def _illustration(self, node):
693         return _rml_Illustration(node, self.localcontext, self.styles, self)
694
695     def _textual_image(self, node):
696         return base64.decodestring(node.text)
697
698     def _pto(self, node):
699         sub_story = []
700         pto_header = None
701         pto_trailer = None
702         for node in utils._child_get(node, self):
703             if node.tag == etree.Comment:
704                 node.text = ''
705                 continue
706             elif node.tag=='pto_header':
707                 pto_header = self.render(node)
708             elif node.tag=='pto_trailer':
709                 pto_trailer = self.render(node)
710             else:
711                 flow = self._flowable(node)
712                 if flow:
713                     if isinstance(flow,list):
714                         sub_story = sub_story + flow
715                     else:
716                         sub_story.append(flow)
717         return platypus.flowables.PTOContainer(sub_story, trailer=pto_trailer, header=pto_header)
718
719     def _flowable(self, node, extra_style=None):
720         if node.tag=='pto':
721             return self._pto(node)
722         if node.tag=='para':
723             style = self.styles.para_style_get(node)
724             if extra_style:
725                 style.__dict__.update(extra_style)
726             result = []
727             for i in self._textual(node).split('\n'):
728                 result.append(platypus.Paragraph(i, style, **(utils.attr_get(node, [], {'bulletText':'str'}))))
729             return result
730         elif node.tag=='barCode':
731             try:
732                 from reportlab.graphics.barcode import code128
733                 from reportlab.graphics.barcode import code39
734                 from reportlab.graphics.barcode import code93
735                 from reportlab.graphics.barcode import common
736                 from reportlab.graphics.barcode import fourstate
737                 from reportlab.graphics.barcode import usps
738             except Exception, e:
739                 self._logger.warning("Cannot use barcode renderers:", exc_info=True)
740                 return None
741             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'})
742             codes = {
743                 'codabar': lambda x: common.Codabar(x, **args),
744                 'code11': lambda x: common.Code11(x, **args),
745                 'code128': lambda x: code128.Code128(x, **args),
746                 'standard39': lambda x: code39.Standard39(x, **args),
747                 'standard93': lambda x: code93.Standard93(x, **args),
748                 'i2of5': lambda x: common.I2of5(x, **args),
749                 'extended39': lambda x: code39.Extended39(x, **args),
750                 'extended93': lambda x: code93.Extended93(x, **args),
751                 'msi': lambda x: common.MSI(x, **args),
752                 'fim': lambda x: usps.FIM(x, **args),
753                 'postnet': lambda x: usps.POSTNET(x, **args),
754             }
755             code = 'code128'
756             if node.get('code'):
757                 code = node.get('code').lower()
758             return codes[code](self._textual(node))
759         elif node.tag=='name':
760             self.styles.names[ node.get('id')] = node.get('value')
761             return None
762         elif node.tag=='xpre':
763             style = self.styles.para_style_get(node)
764             return platypus.XPreformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int','frags':'int'})))
765         elif node.tag=='pre':
766             style = self.styles.para_style_get(node)
767             return platypus.Preformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int'})))
768         elif node.tag=='illustration':
769             return  self._illustration(node)
770         elif node.tag=='blockTable':
771             return  self._table(node)
772         elif node.tag=='title':
773             styles = reportlab.lib.styles.getSampleStyleSheet()
774             style = styles['Title']
775             return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
776         elif re.match('^h([1-9]+[0-9]*)$', (node.tag or '')):
777             styles = reportlab.lib.styles.getSampleStyleSheet()
778             style = styles['Heading'+str(node.tag[1:])]
779             return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
780         elif node.tag=='image':
781             image_data = False
782             if not node.get('file'):
783                 if node.get('name'):
784                     if node.get('name') in self.doc.images:
785                         self._logger.debug("Image %s read ", node.get('name'))
786                         image_data = self.doc.images[node.get('name')].read()
787                     else:
788                         self._logger.warning("Image %s not defined", node.get('name'))
789                         return False
790                 else:
791                     import base64
792                     if self.localcontext:
793                         newtext = utils._process_text(self, node.text or '')
794                         node.text = newtext
795                     image_data = base64.decodestring(node.text)
796                 if not image_data:
797                     self._logger.debug("No inline image data")
798                     return False
799                 image = StringIO(image_data)
800             else:
801                 self._logger.debug("Image get from file %s", node.get('file'))
802                 image = _open_image(node.get('file'), path=self.doc.path)
803             return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
804         elif node.tag=='spacer':
805             if node.get('width'):
806                 width = utils.unit_get(node.get('width'))
807             else:
808                 width = utils.unit_get('1cm')
809             length = utils.unit_get(node.get('length'))
810             return platypus.Spacer(width=width, height=length)
811         elif node.tag=='section':
812             return self.render(node)
813         elif node.tag == 'pageNumberReset':
814             return PageReset()
815         elif node.tag in ('pageBreak', 'nextPage'):
816             return platypus.PageBreak()
817         elif node.tag=='condPageBreak':
818             return platypus.CondPageBreak(**(utils.attr_get(node, ['height'])))
819         elif node.tag=='setNextTemplate':
820             return platypus.NextPageTemplate(str(node.get('name')))
821         elif node.tag=='nextFrame':
822             return platypus.CondPageBreak(1000)           # TODO: change the 1000 !
823         elif node.tag == 'setNextFrame':
824             from reportlab.platypus.doctemplate import NextFrameFlowable
825             return NextFrameFlowable(str(node.get('name')))
826         elif node.tag == 'currentFrame':
827             from reportlab.platypus.doctemplate import CurrentFrameFlowable
828             return CurrentFrameFlowable(str(node.get('name')))
829         elif node.tag == 'frameEnd':
830             return EndFrameFlowable()
831         elif node.tag == 'hr':
832             width_hr=node.get('width') or '100%'
833             color_hr=node.get('color')  or 'black'
834             thickness_hr=node.get('thickness') or 1
835             lineCap_hr=node.get('lineCap') or 'round'
836             return platypus.flowables.HRFlowable(width=width_hr,color=color.get(color_hr),thickness=float(thickness_hr),lineCap=str(lineCap_hr))
837         else:
838             sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.tag,))
839             return None
840
841     def render(self, node_story):
842         def process_story(node_story):
843             sub_story = []
844             for node in utils._child_get(node_story, self):
845                 if node.tag == etree.Comment:
846                     node.text = ''
847                     continue
848                 flow = self._flowable(node)
849                 if flow:
850                     if isinstance(flow,list):
851                         sub_story = sub_story + flow
852                     else:
853                         sub_story.append(flow)
854             return sub_story
855         return process_story(node_story)
856
857
858 class EndFrameFlowable(ActionFlowable):
859     def __init__(self,resume=0):
860         ActionFlowable.__init__(self,('frameEnd',resume))
861
862 class TinyDocTemplate(platypus.BaseDocTemplate):
863     def ___handle_pageBegin(self):
864         self.page = self.page + 1
865         self.pageTemplate.beforeDrawPage(self.canv,self)
866         self.pageTemplate.checkPageSize(self.canv,self)
867         self.pageTemplate.onPage(self.canv,self)
868         for f in self.pageTemplate.frames: f._reset()
869         self.beforePage()
870         self._curPageFlowableCount = 0
871         if hasattr(self,'_nextFrameIndex'):
872             del self._nextFrameIndex
873         for f in self.pageTemplate.frames:
874             if f.id == 'first':
875                 self.frame = f
876                 break
877         self.handle_frameBegin()
878     def afterFlowable(self, flowable):
879         if isinstance(flowable, PageReset):
880             self.canv._pageCount=self.page
881             self.page=0
882             self.canv._flag=True
883             self.canv._pageNumber = 0
884
885 class _rml_template(object):
886     def __init__(self, localcontext, out, node, doc, images={}, path='.', title=None):
887         if not localcontext:
888             localcontext={'internal_header':True}
889         self.localcontext = localcontext
890         self.images= images
891         self.path = path
892         self.title = title
893
894         if not node.get('pageSize'):
895             pageSize = (utils.unit_get('21cm'), utils.unit_get('29.7cm'))
896         else:
897             ps = map(lambda x:x.strip(), node.get('pageSize').replace(')', '').replace('(', '').split(','))
898             pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
899
900         self.doc_tmpl = TinyDocTemplate(out, pagesize=pageSize, **utils.attr_get(node, ['leftMargin','rightMargin','topMargin','bottomMargin'], {'allowSplitting':'int','showBoundary':'bool','title':'str','author':'str'}))
901         self.page_templates = []
902         self.styles = doc.styles
903         self.doc = doc
904         self.image=[]
905         pts = node.findall('pageTemplate')
906         for pt in pts:
907             frames = []
908             for frame_el in pt.findall('frame'):
909                 frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) )
910                 if utils.attr_get(frame_el, ['last']):
911                     frame.lastFrame = True
912                 frames.append( frame )
913             try :
914                 gr = pt.findall('pageGraphics')\
915                     or pt[1].findall('pageGraphics')
916             except Exception: # FIXME: be even more specific, perhaps?
917                 gr=''
918             if len(gr):
919 #                self.image=[ n for n in utils._child_get(gr[0], self) if n.tag=='image' or not self.localcontext]
920                 drw = _rml_draw(self.localcontext,gr[0], self.doc, images=images, path=self.path, title=self.title)
921                 self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
922             else:
923                 drw = _rml_draw(self.localcontext,node,self.doc,title=self.title)
924                 self.page_templates.append( platypus.PageTemplate(frames=frames,onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
925         self.doc_tmpl.addPageTemplates(self.page_templates)
926
927     def render(self, node_stories):
928         if self.localcontext and not self.localcontext.get('internal_header',False):
929             del self.localcontext['internal_header']
930         fis = []
931         r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title)
932         story_cnt = 0
933         for node_story in node_stories:
934             if story_cnt > 0:
935                 fis.append(platypus.PageBreak())
936             fis += r.render(node_story)
937             # Reset Page Number with new story tag
938             fis.append(PageReset())
939             story_cnt += 1
940         if self.localcontext and self.localcontext.get('internal_header',False):
941             self.doc_tmpl.afterFlowable(fis)
942             self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas)
943         else:
944             fis.append(PageCount())
945             self.doc_tmpl.build(fis)
946
947 def parseNode(rml, localcontext=None,fout=None, images=None, path='.',title=None):
948     node = etree.XML(rml)
949     if localcontext is None:
950         localcontext = {}
951     if images is None:
952         images = {}
953     r = _rml_doc(node, localcontext, images, path, title=title)
954     #try to override some font mappings
955     try:
956         from customfonts import SetCustomFonts
957         SetCustomFonts(r)
958     except ImportError:
959         # means there is no custom fonts mapping in this system.
960         pass
961     except Exception:
962         logging.getLogger('report').warning('Cannot set font mapping', exc_info=True)
963         pass
964     fp = StringIO()
965     r.render(fp)
966     return fp.getvalue()
967
968 def parseString(rml, localcontext = {},fout=None, images={}, path='.',title=None):
969     node = etree.XML(rml)
970     r = _rml_doc(node, localcontext, images, path, title=title)
971
972     #try to override some font mappings
973     try:
974         from customfonts import SetCustomFonts
975         SetCustomFonts(r)
976     except Exception:
977         pass
978
979     if fout:
980         fp = file(fout,'wb')
981         r.render(fp)
982         fp.close()
983         return fout
984     else:
985         fp = StringIO()
986         r.render(fp)
987         return fp.getvalue()
988
989 def trml2pdf_help():
990     print 'Usage: trml2pdf input.rml >output.pdf'
991     print 'Render the standard input (RML) and output a PDF file'
992     sys.exit(0)
993
994 if __name__=="__main__":
995     if len(sys.argv)>1:
996         if sys.argv[1]=='--help':
997             trml2pdf_help()
998         print parseString(file(sys.argv[1], 'r').read()),
999     else:
1000         print 'Usage: trml2pdf input.rml >output.pdf'
1001         print 'Try \'trml2pdf --help\' for more information.'
1002