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