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