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