Pushed LongTable
[odoo/odoo.git] / bin / report / render / rml2pdf / trml2pdf.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 # Copyright (c) 2004-2008 Tiny SPRL (http://tiny.be) All Rights Reserved.
5 #
6 # $Id$
7 #
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
13 # Service Company
14 #
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
19 #
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
28 ###############################################################################
29 #!/usr/bin/python
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 ftitle=""
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         global ftitle
252         if  ftitle:
253             self.canvas.setTitle(ftitle)
254
255     def _textual(self, node, x=0, y=0):
256         rc = ''
257         for n in node.childNodes:
258             if n.nodeType == n.ELEMENT_NODE:
259                 if n.localName == 'seq':
260                     from reportlab.lib.sequencer import getSequencer
261                     seq = getSequencer()
262                     rc += str(seq.next(n.getAttribute('id')))
263                 if n.localName == 'pageCount':
264                     if x or y:
265                         self.canvas.translate(x,y)
266                     self.canvas.doForm('pageCount')
267                     if x or y:
268                         self.canvas.translate(-x,-y)
269                 if n.localName == 'pageNumber':
270                     rc += str(self.canvas.getPageNumber())
271             elif n.nodeType in (node.CDATA_SECTION_NODE, node.TEXT_NODE):
272                 # this doesn't need to be "entities" encoded like flowables need to
273                 rc += n.data
274         return rc.encode(encoding, 'replace')
275
276     def _drawString(self, node):
277         v = utils.attr_get(node, ['x','y'])
278         self.canvas.drawString(text=self._textual(node, **v), **v)
279     def _drawCenteredString(self, node):
280         v = utils.attr_get(node, ['x','y'])
281         self.canvas.drawCentredString(text=self._textual(node, **v), **v)
282     def _drawRightString(self, node):
283         v = utils.attr_get(node, ['x','y'])
284         self.canvas.drawRightString(text=self._textual(node, **v), **v)
285     def _rect(self, node):
286         if node.hasAttribute('round'):
287             self.canvas.roundRect(radius=utils.unit_get(node.getAttribute('round')), **utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
288         else:
289             self.canvas.rect(**utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
290
291     def _ellipse(self, node):
292         x1 = utils.unit_get(node.getAttribute('x'))
293         x2 = utils.unit_get(node.getAttribute('width'))
294         y1 = utils.unit_get(node.getAttribute('y'))
295         y2 = utils.unit_get(node.getAttribute('height'))
296         self.canvas.ellipse(x1,y1,x2,y2, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
297     def _curves(self, node):
298         line_str = utils.text_get(node).split()
299         lines = []
300         while len(line_str)>7:
301             self.canvas.bezier(*[utils.unit_get(l) for l in line_str[0:8]])
302             line_str = line_str[8:]
303     def _lines(self, node):
304         line_str = utils.text_get(node).split()
305         lines = []
306         while len(line_str)>3:
307             lines.append([utils.unit_get(l) for l in line_str[0:4]])
308             line_str = line_str[4:]
309         self.canvas.lines(lines)
310     def _grid(self, node):
311         xlist = [utils.unit_get(s) for s in node.getAttribute('xs').split(',')]
312         ylist = [utils.unit_get(s) for s in node.getAttribute('ys').split(',')]
313         self.canvas.grid(xlist, ylist)
314     def _translate(self, node):
315         dx = 0
316         dy = 0
317         if node.hasAttribute('dx'):
318             dx = utils.unit_get(node.getAttribute('dx'))
319         if node.hasAttribute('dy'):
320             dy = utils.unit_get(node.getAttribute('dy'))
321         self.canvas.translate(dx,dy)
322
323     def _circle(self, node):
324         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'}))
325
326     def _place(self, node):
327         flows = _rml_flowable(self.doc, images=self.images, path=self.path).render(node)
328         infos = utils.attr_get(node, ['x','y','width','height'])
329
330         infos['y']+=infos['height']
331         for flow in flows:
332             w,h = flow.wrap(infos['width'], infos['height'])
333             if w<=infos['width'] and h<=infos['height']:
334                 infos['y']-=h
335                 flow.drawOn(self.canvas,infos['x'],infos['y'])
336                 infos['height']-=h
337             else:
338                 raise ValueError, "Not enough space"
339
340     def _line_mode(self, node):
341         ljoin = {'round':1, 'mitered':0, 'bevelled':2}
342         lcap = {'default':0, 'round':1, 'square':2}
343         if node.hasAttribute('width'):
344             self.canvas.setLineWidth(utils.unit_get(node.getAttribute('width')))
345         if node.hasAttribute('join'):
346             self.canvas.setLineJoin(ljoin[node.getAttribute('join')])
347         if node.hasAttribute('cap'):
348             self.canvas.setLineCap(lcap[node.getAttribute('cap')])
349         if node.hasAttribute('miterLimit'):
350             self.canvas.setDash(utils.unit_get(node.getAttribute('miterLimit')))
351         if node.hasAttribute('dash'):
352             dashes = node.getAttribute('dash').split(',')
353             for x in range(len(dashes)):
354                 dashes[x]=utils.unit_get(dashes[x])
355             self.canvas.setDash(node.getAttribute('dash').split(','))
356
357     def _image(self, node):
358         import urllib
359         from reportlab.lib.utils import ImageReader
360
361         if not node.hasAttribute('file'):
362
363             if node.hasAttribute('name'):
364                 image_data = self.images[node.getAttribute('name')]
365                 s = StringIO.StringIO(image_data)
366             else:
367                 import base64
368                 image_data = base64.decodestring(node.firstChild.nodeValue)
369                 if not image_data: return False
370                 s = StringIO.StringIO(image_data)
371         else:
372             if node.getAttribute('file') in self.images:
373                 s = StringIO.StringIO(self.images[node.getAttribute('file')])
374             else:
375                 try:
376                     u = urllib.urlopen(str(node.getAttribute('file')))
377                     s = StringIO.StringIO(u.read())
378                 except:
379                     u = file(os.path.join(self.path,str(node.getAttribute('file'))), 'rb')
380                     s = StringIO.StringIO(u.read())
381         img = ImageReader(s)
382         (sx,sy) = img.getSize()
383
384         args = {}
385         for tag in ('width','height','x','y'):
386             if node.hasAttribute(tag):
387                 args[tag] = utils.unit_get(node.getAttribute(tag))
388         if ('width' in args) and (not 'height' in args):
389             args['height'] = sy * args['width'] / sx
390         elif ('height' in args) and (not 'width' in args):
391             args['width'] = sx * args['height'] / sy
392         elif ('width' in args) and ('height' in args):
393             if (float(args['width'])/args['height'])>(float(sx)>sy):
394                 args['width'] = sx * args['height'] / sy
395             else:
396                 args['height'] = sy * args['width'] / sx
397         self.canvas.drawImage(img, **args)
398
399     def _path(self, node):
400         self.path = self.canvas.beginPath()
401         self.path.moveTo(**utils.attr_get(node, ['x','y']))
402         for n in node.childNodes:
403             if n.nodeType == node.ELEMENT_NODE:
404                 if n.localName=='moveto':
405                     vals = utils.text_get(n).split()
406                     self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1]))
407                 elif n.localName=='curvesto':
408                     vals = utils.text_get(n).split()
409                     while len(vals)>5:
410                         pos=[]
411                         while len(pos)<6:
412                             pos.append(utils.unit_get(vals.pop(0)))
413                         self.path.curveTo(*pos)
414             elif (n.nodeType == node.TEXT_NODE):
415                 data = n.data.split()               # Not sure if I must merge all TEXT_NODE ?
416                 while len(data)>1:
417                     x = utils.unit_get(data.pop(0))
418                     y = utils.unit_get(data.pop(0))
419                     self.path.lineTo(x,y)
420         if (not node.hasAttribute('close')) or utils.bool_get(node.getAttribute('close')):
421             self.path.close()
422         self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
423
424     def render(self, node):
425         tags = {
426             'drawCentredString': self._drawCenteredString,
427             'drawRightString': self._drawRightString,
428             'drawString': self._drawString,
429             'rect': self._rect,
430             'ellipse': self._ellipse,
431             'lines': self._lines,
432             'grid': self._grid,
433             'curves': self._curves,
434             'fill': lambda node: self.canvas.setFillColor(color.get(node.getAttribute('color'))),
435             'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.getAttribute('color'))),
436             'setFont': lambda node: self.canvas.setFont(node.getAttribute('name'), utils.unit_get(node.getAttribute('size'))),
437             'place': self._place,
438             'circle': self._circle,
439             'lineMode': self._line_mode,
440             'path': self._path,
441             'rotate': lambda node: self.canvas.rotate(float(node.getAttribute('degrees'))),
442             'translate': self._translate,
443             'image': self._image
444         }
445         for nd in node.childNodes:
446             if nd.nodeType==nd.ELEMENT_NODE:
447                 for tag in tags:
448                     if nd.localName==tag:
449                         tags[tag](nd)
450                         break
451
452 class _rml_draw(object):
453     def __init__(self, node, styles, images={}, path='.'):
454         self.node = node
455         self.styles = styles
456         self.canvas = None
457         self.images = images
458         self.path = path
459
460     def render(self, canvas, doc):
461         canvas.saveState()
462         cnv = _rml_canvas(canvas, doc, self.styles, images=self.images, path=self.path)
463         cnv.render(self.node)
464         canvas.restoreState()
465
466 class _rml_flowable(object):
467     def __init__(self, doc, images={}, path='.'):
468         self.doc = doc
469         self.styles = doc.styles
470         self.images = images
471         self.path = path
472
473     def _textual(self, node):
474         rc = ''
475         for n in node.childNodes:
476             if n.nodeType == node.ELEMENT_NODE:
477                 if n.localName == 'getName':
478                     newNode = self.doc.dom.createTextNode(self.styles.names.get(n.getAttribute('id'),'Unknown name'))
479                     node.insertBefore(newNode, n)
480                     node.removeChild(n)
481                     n = newNode
482                 elif n.localName == 'pageNumber':
483                     rc += '<pageNumber/>'            # TODO: change this !
484                 else:
485                     #CHECKME: I wonder if this is useful since we don't stock the result. Maybe for the getName tag?
486                     self._textual(n)
487                 rc += n.toxml()
488             elif n.nodeType in (node.CDATA_SECTION_NODE, node.TEXT_NODE):
489                 rc += str2xml(n.data)
490         return rc.encode(encoding, 'replace')
491
492     def _table(self, node):
493         length = 0
494         colwidths = None
495         rowheights = None
496         data = []
497         childs = _child_get(node,'tr')
498         if not childs:
499             return None
500         posy = 0
501         styles = []
502         for tr in childs:
503             paraStyle = None
504             if tr.hasAttribute('style'):
505                 st = copy.deepcopy(self.styles.table_styles[tr.getAttribute('style')])
506                 for s in st._cmds:
507                     s[1][1] = posy
508                     s[2][1] = posy
509                 styles.append(st)
510             if tr.hasAttribute('paraStyle'):
511                 paraStyle = self.styles.styles[tr.getAttribute('paraStyle')]
512
513             data2 = []
514             posx = 0
515             for td in _child_get(tr, 'td'):
516                 if td.hasAttribute('style'):
517                     st = copy.deepcopy(self.styles.table_styles[td.getAttribute('style')])
518                     for s in st._cmds:
519                         s[1][1] = posy
520                         s[2][1] = posy
521                         s[1][0] = posx
522                         s[2][0] = posx
523                     styles.append(st)
524                 if td.hasAttribute('paraStyle'):
525                     # TODO: merge styles
526                     paraStyle = self.styles.styles[td.getAttribute('paraStyle')]
527                 posx += 1
528
529                 flow = []
530                 for n in td.childNodes:
531                     if n.nodeType==node.ELEMENT_NODE:
532                         fl = self._flowable(n, extra_style=paraStyle)
533                         flow.append( fl )
534                 if not len(flow):
535                     flow = self._textual(td)
536                 data2.append( flow )
537             if len(data2)>length:
538                 length=len(data2)
539                 for ab in data:
540                     while len(ab)<length:
541                         ab.append('')
542             while len(data2)<length:
543                 data2.append('')
544             data.append( data2 )
545             posy += 1
546         if node.hasAttribute('colWidths'):
547             assert length == len(node.getAttribute('colWidths').split(','))
548             colwidths = [utils.unit_get(f.strip()) for f in node.getAttribute('colWidths').split(',')]
549         if node.hasAttribute('rowHeights'):
550             rowheights = [utils.unit_get(f.strip()) for f in node.getAttribute('rowHeights').split(',')]
551             if len(rowheights) == 1:
552                 rowheights = rowheights[0]
553         table = platypus.LongTable(data = data, colWidths=colwidths, rowHeights=rowheights, **(utils.attr_get(node, ['splitByRow'] ,{'repeatRows':'int','repeatCols':'int'})))
554         if node.hasAttribute('style'):
555             table.setStyle(self.styles.table_styles[node.getAttribute('style')])
556         for s in styles:
557             table.setStyle(s)
558         return table
559
560     def _illustration(self, node):
561         class Illustration(platypus.flowables.Flowable):
562             def __init__(self, node, styles, self2):
563                 self.node = node
564                 self.styles = styles
565                 self.width = utils.unit_get(node.getAttribute('width'))
566                 self.height = utils.unit_get(node.getAttribute('height'))
567                 self.self2 = self2
568             def wrap(self, *args):
569                 return (self.width, self.height)
570             def draw(self):
571                 canvas = self.canv
572                 drw = _rml_draw(self.node, self.styles, images=self.self2.images, path=self.self2.path)
573                 drw.render(self.canv, None)
574         return Illustration(node, self.styles, self)
575
576     def _textual_image(self, node):
577         import base64
578         rc = ''
579         for n in node.childNodes:
580             if n.nodeType in (node.CDATA_SECTION_NODE, node.TEXT_NODE):
581                 rc += n.data
582         return base64.decodestring(rc)
583
584     def _flowable(self, node, extra_style=None):
585         if node.localName=='para':
586             style = self.styles.para_style_get(node)
587             if extra_style:
588                 style.__dict__.update(extra_style)
589             return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
590         elif node.localName=='barCode':
591             try:
592                 from reportlab.graphics.barcode import code128
593                 from reportlab.graphics.barcode import code39
594                 from reportlab.graphics.barcode import code93
595                 from reportlab.graphics.barcode import common
596                 from reportlab.graphics.barcode import fourstate
597                 from reportlab.graphics.barcode import usps
598             except Exception, e:
599                 print 'Warning: Reportlab barcode extension not installed !'
600                 return None
601             args = utils.attr_get(node, [], {'ratio':'float','xdim':'unit','height':'unit','checksum':'bool','quiet':'bool'})
602             codes = {
603                 'codabar': lambda x: common.Codabar(x, **args),
604                 'code11': lambda x: common.Code11(x, **args),
605                 'code128': lambda x: code128.Code128(x, **args),
606                 'standard39': lambda x: code39.Standard39(x, **args),
607                 'standard93': lambda x: code93.Standard93(x, **args),
608                 'i2of5': lambda x: common.I2of5(x, **args),
609                 'extended39': lambda x: code39.Extended39(x, **args),
610                 'extended93': lambda x: code93.Extended93(x, **args),
611                 'msi': lambda x: common.MSI(x, **args),
612                 'fim': lambda x: usps.FIM(x, **args),
613                 'postnet': lambda x: usps.POSTNET(x, **args),
614             }
615             code = 'code128'
616             if node.hasAttribute('code'):
617                 code = node.getAttribute('code').lower()
618             return codes[code](self._textual(node))
619         elif node.localName=='name':
620             self.styles.names[ node.getAttribute('id')] = node.getAttribute('value')
621             return None
622         elif node.localName=='xpre':
623             style = self.styles.para_style_get(node)
624             return platypus.XPreformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int','frags':'int'})))
625         elif node.localName=='pre':
626             style = self.styles.para_style_get(node)
627             return platypus.Preformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int'})))
628         elif node.localName=='illustration':
629             return  self._illustration(node)
630         elif node.localName=='blockTable':
631             return  self._table(node)
632         elif node.localName=='title':
633             styles = reportlab.lib.styles.getSampleStyleSheet()
634             style = styles['Title']
635             return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
636         elif re.match('^h([1-9]+[0-9]*)$', node.localName):
637             styles = reportlab.lib.styles.getSampleStyleSheet()
638             style = styles['Heading'+str(node.localName[1:])]
639             return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
640         elif node.localName=='image':
641             if not node.hasAttribute('file'):
642                 if node.hasAttribute('name'):
643                     image_data = self.doc.images[node.getAttribute('name')].read()
644                 else:
645                     import base64
646                     image_data = base64.decodestring(node.firstChild.nodeValue)
647                 image = StringIO.StringIO(image_data)
648                 return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
649             else:
650                 return platypus.Image(node.getAttribute('file'), mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
651
652             from reportlab.lib.utils import ImageReader
653             name = str(node.getAttribute('file'))
654             img = ImageReader(name)
655             (sx,sy) = img.getSize()
656
657             args = {}
658             for tag in ('width','height'):
659                 if node.hasAttribute(tag):
660                     args[tag] = utils.unit_get(node.getAttribute(tag))
661             if ('width' in args) and (not 'height' in args):
662                 args['height'] = sy * args['width'] / sx
663             elif ('height' in args) and (not 'width' in args):
664                 args['width'] = sx * args['height'] / sy
665             elif ('width' in args) and ('height' in args):
666                 if (float(args['width'])/args['height'])>(float(sx)>sy):
667                     args['width'] = sx * args['height'] / sy
668                 else:
669                     args['height'] = sy * args['width'] / sx
670             return platypus.Image(name, mask=(250,255,250,255,250,255), **args)
671         elif node.localName=='spacer':
672             if node.hasAttribute('width'):
673                 width = utils.unit_get(node.getAttribute('width'))
674             else:
675                 width = utils.unit_get('1cm')
676             length = utils.unit_get(node.getAttribute('length'))
677             return platypus.Spacer(width=width, height=length)
678         elif node.localName=='section':
679             return self.render(node)
680         elif node.localName == 'pageNumberReset':
681             return PageReset()
682         elif node.localName in ('pageBreak', 'nextPage'):
683             return platypus.PageBreak()
684         elif node.localName=='condPageBreak':
685             return platypus.CondPageBreak(**(utils.attr_get(node, ['height'])))
686         elif node.localName=='setNextTemplate':
687             return platypus.NextPageTemplate(str(node.getAttribute('name')))
688         elif node.localName=='nextFrame':
689             return platypus.CondPageBreak(1000)           # TODO: change the 1000 !
690         elif node.localName == 'setNextFrame':
691             from reportlab.platypus.doctemplate import NextFrameFlowable
692             return NextFrameFlowable(str(node.getAttribute('name')))
693         elif node.localName == 'currentFrame':
694             from reportlab.platypus.doctemplate import CurrentFrameFlowable
695             return CurrentFrameFlowable(str(node.getAttribute('name')))
696         elif node.localName == 'frameEnd':
697             return EndFrameFlowable()
698         else:
699             sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.localName,))
700             return None
701
702     def render(self, node_story):
703         story = []
704         node = node_story.firstChild
705         while node:
706             if node.nodeType == node.ELEMENT_NODE:
707                 flow = self._flowable(node)
708                 if flow:
709                     if type(flow) == type([]):
710                         story = story + flow
711                     else:
712                         story.append(flow)
713             node = node.nextSibling
714         return story
715
716 from reportlab.platypus.doctemplate import ActionFlowable
717
718 class EndFrameFlowable(ActionFlowable):
719     def __init__(self,resume=0):
720         ActionFlowable.__init__(self,('frameEnd',resume))
721
722 class TinyDocTemplate(platypus.BaseDocTemplate):
723     def ___handle_pageBegin(self):
724         self.page = self.page + 1
725         self.pageTemplate.beforeDrawPage(self.canv,self)
726         self.pageTemplate.checkPageSize(self.canv,self)
727         self.pageTemplate.onPage(self.canv,self)
728         for f in self.pageTemplate.frames: f._reset()
729         self.beforePage()
730         #keep a count of flowables added to this page.  zero indicates bad stuff
731         self._curPageFlowableCount = 0
732         if hasattr(self,'_nextFrameIndex'):
733             del self._nextFrameIndex
734         for f in self.pageTemplate.frames:
735             if f.id == 'first':
736                 self.frame = f
737                 break
738         self.handle_frameBegin()
739     def afterFlowable(self, flowable):
740         if isinstance(flowable, PageReset):
741             self.canv._pageNumber = 0
742
743 class _rml_template(object):
744     def __init__(self, out, node, doc, images={}, path='.'):
745         self.images= images
746         self.path = path
747         if not node.hasAttribute('pageSize'):
748             pageSize = (utils.unit_get('21cm'), utils.unit_get('29.7cm'))
749         else:
750             ps = map(lambda x:x.strip(), node.getAttribute('pageSize').replace(')', '').replace('(', '').split(','))
751             pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
752         cm = reportlab.lib.units.cm
753         self.doc_tmpl = TinyDocTemplate(out, pagesize=pageSize, **utils.attr_get(node, ['leftMargin','rightMargin','topMargin','bottomMargin'], {'allowSplitting':'int','showBoundary':'bool','title':'str','author':'str'}))
754         self.page_templates = []
755         self.styles = doc.styles
756         self.doc = doc
757         pts = node.getElementsByTagName('pageTemplate')
758         for pt in pts:
759             frames = []
760             for frame_el in pt.getElementsByTagName('frame'):
761                 frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) )
762                 if utils.attr_get(frame_el, ['last']):
763                     frame.lastFrame = True
764                 frames.append( frame )
765             gr = pt.getElementsByTagName('pageGraphics')
766             if len(gr):
767                 drw = _rml_draw(gr[0], self.doc, images=images, path=self.path)
768                 self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
769             else:
770                 self.page_templates.append( platypus.PageTemplate(frames=frames, **utils.attr_get(pt, [], {'id':'str'}) ))
771         self.doc_tmpl.addPageTemplates(self.page_templates)
772
773     def render(self, node_stories):
774         fis = []
775         r = _rml_flowable(self.doc,images=self.images, path=self.path)
776         for node_story in node_stories:
777             fis += r.render(node_story)
778             if node_story==node_stories[-1]:
779
780                 fis.append(PageCount())
781
782             fis.append(platypus.PageBreak())
783
784         self.doc_tmpl.build(fis)
785
786 def parseString(data, fout=None, images={}, path='.',title=None):
787     r = _rml_doc(data, images, path)
788     global ftitle
789     ftitle=title
790     if fout:
791         fp = file(fout,'wb')
792         r.render(fp)
793         fp.close()
794         return fout
795     else:
796         fp = StringIO.StringIO()
797         r.render(fp)
798         return fp.getvalue()
799
800 def trml2pdf_help():
801     print 'Usage: trml2pdf input.rml >output.pdf'
802     print 'Render the standard input (RML) and output a PDF file'
803     sys.exit(0)
804
805 if __name__=="__main__":
806     if len(sys.argv)>1:
807         if sys.argv[1]=='--help':
808             trml2pdf_help()
809         print parseString(file(sys.argv[1], 'r').read()),
810     else:
811         print 'Usage: trml2pdf input.rml >output.pdf'
812         print 'Try \'trml2pdf --help\' for more information.'
813
814 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
815