Font Size 8
[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(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.'