changes for the logo
[odoo/odoo.git] / bin / report / render / rml2pdf / trml2pdf.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 # trml2pdf - An RML to PDF converter
24 # Copyright (C) 2003, Fabien Pinckaers, UCL, FSA
25 # Contributors
26 #     Richard Waid <richard@iopen.net>
27 #
28 # This library is free software; you can redistribute it and/or
29 # modify it under the terms of the GNU Lesser General Public
30 # License as published by the Free Software Foundation; either
31 # version 2.1 of the License, or (at your option) any later version.
32 #
33 # This library is distributed in the hope that it will be useful,
34 # but WITHOUT ANY WARRANTY; without even the implied warranty of
35 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
36 # Lesser General Public License for more details.
37 #
38 # You should have received a copy of the GNU Lesser General Public
39 # License along with this library; if not, write to the Free Software
40 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
41
42 import sys
43 from StringIO import StringIO
44 import xml.dom.minidom
45 import copy
46
47 import reportlab
48 import re
49 from reportlab.pdfgen import canvas
50 from reportlab import platypus
51 import cStringIO
52 import utils
53 import color
54 import os
55
56 #
57 # Change this to UTF-8 if you plan tu use Reportlab's UTF-8 support
58 #
59 # reportlab use "code page 1252" encoding by default. cfr reportlab user guide p.46
60 encoding = 'utf-8'
61
62 def str2xml(s):
63     return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
64
65 def _child_get(node, childs):
66     clds = []
67     for n in node.childNodes:
68         if (n.nodeType==n.ELEMENT_NODE) and (n.localName==childs):
69             clds.append(n)
70     return clds
71
72 class PageCount(platypus.Flowable):
73     def draw(self):
74         self.canv.beginForm("pageCount")
75         self.canv.setFont("Helvetica", utils.unit_get(str(8)))
76         self.canv.drawString(0, 0, str(self.canv.getPageNumber()))
77         self.canv.endForm()
78
79 class PageReset(platypus.Flowable):
80     def draw(self):
81         self.canv._pageNumber = 0
82
83 class _rml_styles(object):
84     def __init__(self, nodes):
85         self.styles = {}
86         self.names = {}
87         self.table_styles = {}
88         for node in nodes:
89             for style in node.getElementsByTagName('blockTableStyle'):
90                 self.table_styles[style.getAttribute('id')] = self._table_style_get(style)
91             for style in node.getElementsByTagName('paraStyle'):
92                 self.styles[style.getAttribute('name')] = self._para_style_update(style)
93             for variable in node.getElementsByTagName('initialize'):
94                 for name in variable.getElementsByTagName('name'):
95                     self.names[ name.getAttribute('id')] = name.getAttribute('value')
96
97     def _para_style_update(self, node):
98         data = {}
99         for attr in ['textColor', 'backColor', 'bulletColor', 'borderColor']:
100             if node.hasAttribute(attr):
101                 data[attr] = color.get(node.getAttribute(attr))
102         for attr in ['fontName', 'bulletFontName', 'bulletText']:
103             if node.hasAttribute(attr):
104                 data[attr] = node.getAttribute(attr)
105         for attr in ['fontSize', 'leftIndent', 'rightIndent', 'spaceBefore', 'spaceAfter',
106             'firstLineIndent', 'bulletIndent', 'bulletFontSize', 'leading',
107             'borderWidth','borderPadding','borderRadius']:
108             if node.hasAttribute(attr):
109                 data[attr] = utils.unit_get(node.getAttribute(attr))
110         if node.hasAttribute('alignment'):
111             align = {
112                 'right':reportlab.lib.enums.TA_RIGHT,
113                 'center':reportlab.lib.enums.TA_CENTER,
114                 'justify':reportlab.lib.enums.TA_JUSTIFY
115             }
116             data['alignment'] = align.get(node.getAttribute('alignment').lower(), reportlab.lib.enums.TA_LEFT)
117         return data
118
119     def _table_style_get(self, style_node):
120         styles = []
121         for node in style_node.childNodes:
122             if node.nodeType==node.ELEMENT_NODE:
123                 start = utils.tuple_int_get(node, 'start', (0,0) )
124                 stop = utils.tuple_int_get(node, 'stop', (-1,-1) )
125                 if node.localName=='blockValign':
126                     styles.append(('VALIGN', start, stop, str(node.getAttribute('value'))))
127                 elif node.localName=='blockFont':
128                     styles.append(('FONT', start, stop, str(node.getAttribute('name'))))
129                 elif node.localName=='blockTextColor':
130                     styles.append(('TEXTCOLOR', start, stop, color.get(str(node.getAttribute('colorName')))))
131                 elif node.localName=='blockLeading':
132                     styles.append(('LEADING', start, stop, utils.unit_get(node.getAttribute('length'))))
133                 elif node.localName=='blockAlignment':
134                     styles.append(('ALIGNMENT', start, stop, str(node.getAttribute('value'))))
135                 elif node.localName=='blockSpan':
136                     styles.append(('SPAN', start, stop))
137                 elif node.localName=='blockLeftPadding':
138                     styles.append(('LEFTPADDING', start, stop, utils.unit_get(node.getAttribute('length'))))
139                 elif node.localName=='blockRightPadding':
140                     styles.append(('RIGHTPADDING', start, stop, utils.unit_get(node.getAttribute('length'))))
141                 elif node.localName=='blockTopPadding':
142                     styles.append(('TOPPADDING', start, stop, utils.unit_get(node.getAttribute('length'))))
143                 elif node.localName=='blockBottomPadding':
144                     styles.append(('BOTTOMPADDING', start, stop, utils.unit_get(node.getAttribute('length'))))
145                 elif node.localName=='blockBackground':
146                     styles.append(('BACKGROUND', start, stop, color.get(node.getAttribute('colorName'))))
147                 if node.hasAttribute('size'):
148                     styles.append(('FONTSIZE', start, stop, utils.unit_get(node.getAttribute('size'))))
149                 elif node.localName=='lineStyle':
150                     kind = node.getAttribute('kind')
151                     kind_list = [ 'GRID', 'BOX', 'OUTLINE', 'INNERGRID', 'LINEBELOW', 'LINEABOVE','LINEBEFORE', 'LINEAFTER' ]
152                     assert kind in kind_list
153                     thick = 1
154                     if node.hasAttribute('thickness'):
155                         thick = float(node.getAttribute('thickness'))
156                     styles.append((kind, start, stop, thick, color.get(node.getAttribute('colorName'))))
157         return platypus.tables.TableStyle(styles)
158
159     def para_style_get(self, node):
160         style = False
161         if node.hasAttribute('style'):
162             if node.getAttribute('style') in self.styles:
163                 styles = reportlab.lib.styles.getSampleStyleSheet()
164                 sname = node.getAttribute('style')
165                 style = reportlab.lib.styles.ParagraphStyle(sname, styles["Normal"], **self.styles[sname])
166             else:
167                 sys.stderr.write('Warning: style not found, %s - setting default!\n' % (node.getAttribute('style'),) )
168         if not style:
169             styles = reportlab.lib.styles.getSampleStyleSheet()
170             style = copy.deepcopy(styles['Normal'])
171         style.__dict__.update(self._para_style_update(node))
172         return style
173
174 class _rml_doc(object):
175     def __init__(self, data, images={}, path='.', title=None):
176         self.dom = xml.dom.minidom.parseString(data)
177         self.filename = self.dom.documentElement.getAttribute('filename')
178         self.images = images
179         self.path = path
180         self.title = title
181
182     def docinit(self, els):
183         from reportlab.lib.fonts import addMapping
184         from reportlab.pdfbase import pdfmetrics
185         from reportlab.pdfbase.ttfonts import TTFont
186
187         for node in els:
188             for font in node.getElementsByTagName('registerFont'):
189                 name = font.getAttribute('fontName').encode('ascii')
190                 fname = font.getAttribute('fontFile').encode('ascii')
191                 pdfmetrics.registerFont(TTFont(name, fname ))
192                 addMapping(name, 0, 0, name)    #normal
193                 addMapping(name, 0, 1, name)    #italic
194                 addMapping(name, 1, 0, name)    #bold
195                 addMapping(name, 1, 1, name)    #italic and bold
196
197     def _textual_image(self, node):
198         import base64
199         rc = ''
200         for n in node.childNodes:
201             if n.nodeType in (node.CDATA_SECTION_NODE, node.TEXT_NODE):
202                 rc += n.data
203         return base64.decodestring(rc)
204
205     def _images(self, el):
206         result = {}
207         for node in el.getElementsByTagName('image'):
208             result[node.getAttribute('name')] = self._textual_image(node)
209         return result
210
211     def render(self, out):
212         el = self.dom.documentElement.getElementsByTagName('docinit')
213         if el:
214             self.docinit(el)
215
216         el = self.dom.documentElement.getElementsByTagName('stylesheet')
217         self.styles = _rml_styles(el)
218
219         el = self.dom.documentElement.getElementsByTagName('images')
220         if el:
221             self.images.update( self._images(el[0]) )
222
223         el = self.dom.documentElement.getElementsByTagName('template')
224         if len(el):
225             pt_obj = _rml_template(out, el[0], self, images=self.images, path=self.path, title=self.title)
226             pt_obj.render(self.dom.documentElement.getElementsByTagName('story'))
227         else:
228             self.canvas = canvas.Canvas(out)
229             pd = self.dom.documentElement.getElementsByTagName('pageDrawing')[0]
230             pd_obj = _rml_canvas(self.canvas, None, self, self.images, path=self.path, title=self.title)
231             pd_obj.render(pd)
232
233             self.canvas.showPage()
234             self.canvas.save()
235
236 class _rml_canvas(object):
237     def __init__(self, canvas, doc_tmpl=None, doc=None, images={}, path='.', title=None):
238         self.canvas = canvas
239         self.styles = doc.styles
240         self.doc_tmpl = doc_tmpl
241         self.doc = doc
242         self.images = images
243         self.path = path
244         self.title = title
245         if self.title:
246             self.canvas.setTitle(self.title)
247
248     def _textual(self, node, x=0, y=0):
249         rc = ''
250         for n in node.childNodes:
251             if n.nodeType == n.ELEMENT_NODE:
252                 if n.localName == 'seq':
253                     from reportlab.lib.sequencer import getSequencer
254                     seq = getSequencer()
255                     rc += str(seq.next(n.getAttribute('id')))
256                 if n.localName == 'pageCount':
257                     if x or y:
258                         self.canvas.translate(x,y)
259                     self.canvas.doForm('pageCount')
260                     if x or y:
261                         self.canvas.translate(-x,-y)
262                 if n.localName == 'pageNumber':
263                     rc += str(self.canvas.getPageNumber())
264             elif n.nodeType in (node.CDATA_SECTION_NODE, node.TEXT_NODE):
265                 # this doesn't need to be "entities" encoded like flowables need to
266                 rc += n.data
267         return rc.encode(encoding, 'replace')
268
269     def _drawString(self, node):
270         v = utils.attr_get(node, ['x','y'])
271         self.canvas.drawString(text=self._textual(node, **v), **v)
272     def _drawCenteredString(self, node):
273         v = utils.attr_get(node, ['x','y'])
274         self.canvas.drawCentredString(text=self._textual(node, **v), **v)
275     def _drawRightString(self, node):
276         v = utils.attr_get(node, ['x','y'])
277         self.canvas.drawRightString(text=self._textual(node, **v), **v)
278     def _rect(self, node):
279         if node.hasAttribute('round'):
280             self.canvas.roundRect(radius=utils.unit_get(node.getAttribute('round')), **utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
281         else:
282             self.canvas.rect(**utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
283
284     def _ellipse(self, node):
285         x1 = utils.unit_get(node.getAttribute('x'))
286         x2 = utils.unit_get(node.getAttribute('width'))
287         y1 = utils.unit_get(node.getAttribute('y'))
288         y2 = utils.unit_get(node.getAttribute('height'))
289         self.canvas.ellipse(x1,y1,x2,y2, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
290     def _curves(self, node):
291         line_str = utils.text_get(node).split()
292         lines = []
293         while len(line_str)>7:
294             self.canvas.bezier(*[utils.unit_get(l) for l in line_str[0:8]])
295             line_str = line_str[8:]
296     def _lines(self, node):
297         line_str = utils.text_get(node).split()
298         lines = []
299         while len(line_str)>3:
300             lines.append([utils.unit_get(l) for l in line_str[0:4]])
301             line_str = line_str[4:]
302         self.canvas.lines(lines)
303     def _grid(self, node):
304         xlist = [utils.unit_get(s) for s in node.getAttribute('xs').split(',')]
305         ylist = [utils.unit_get(s) for s in node.getAttribute('ys').split(',')]
306         self.canvas.grid(xlist, ylist)
307     def _translate(self, node):
308         dx = 0
309         dy = 0
310         if node.hasAttribute('dx'):
311             dx = utils.unit_get(node.getAttribute('dx'))
312         if node.hasAttribute('dy'):
313             dy = utils.unit_get(node.getAttribute('dy'))
314         self.canvas.translate(dx,dy)
315
316     def _circle(self, node):
317         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'}))
318
319     def _place(self, node):
320         flows = _rml_flowable(self.doc, images=self.images, path=self.path, title=self.title).render(node)
321         infos = utils.attr_get(node, ['x','y','width','height'])
322
323         infos['y']+=infos['height']
324         for flow in flows:
325             w,h = flow.wrap(infos['width'], infos['height'])
326             if w<=infos['width'] and h<=infos['height']:
327                 infos['y']-=h
328                 flow.drawOn(self.canvas,infos['x'],infos['y'])
329                 infos['height']-=h
330             else:
331                 raise ValueError, "Not enough space"
332
333     def _line_mode(self, node):
334         ljoin = {'round':1, 'mitered':0, 'bevelled':2}
335         lcap = {'default':0, 'round':1, 'square':2}
336         if node.hasAttribute('width'):
337             self.canvas.setLineWidth(utils.unit_get(node.getAttribute('width')))
338         if node.hasAttribute('join'):
339             self.canvas.setLineJoin(ljoin[node.getAttribute('join')])
340         if node.hasAttribute('cap'):
341             self.canvas.setLineCap(lcap[node.getAttribute('cap')])
342         if node.hasAttribute('miterLimit'):
343             self.canvas.setDash(utils.unit_get(node.getAttribute('miterLimit')))
344         if node.hasAttribute('dash'):
345             dashes = node.getAttribute('dash').split(',')
346             for x in range(len(dashes)):
347                 dashes[x]=utils.unit_get(dashes[x])
348             self.canvas.setDash(node.getAttribute('dash').split(','))
349
350     def _image(self, node):
351         import urllib
352         from reportlab.lib.utils import ImageReader
353
354 #        s = StringIO()
355         if not node.hasAttribute('file'):
356
357             if node.hasAttribute('name'):
358                 image_data = self.images[node.getAttribute('name')]
359                 s = cStringIO.StringIO(image_data)
360             else:
361                 import base64
362                 image_data = base64.decodestring(node.firstChild.nodeValue)
363                 if not image_data: return False
364                 s = cStringIO.StringIO(image_data)                
365 #                s.write(image_data)
366         else:
367             if node.getAttribute('file') in self.images:
368                 s = cStringIO.StringIO(self.images[node.getAttribute('file')])                
369 #                s.write(self.images[node.getAttribute('file')])
370             else:
371                 try:
372                     u = urllib.urlopen(str(node.getAttribute('file')))
373                 except:
374                     u = file(os.path.join(self.path,str(node.getAttribute('file'))), 'rb')
375                 s = cStringIO.StringIO(u.read())
376         img = ImageReader(s)
377         (sx,sy) = img.getSize()
378
379         args = {}
380         for tag in ('width','height','x','y'):
381             if node.hasAttribute(tag):
382                 args[tag] = utils.unit_get(node.getAttribute(tag))
383         if ('width' in args) and (not 'height' in args):
384             args['height'] = sy * args['width'] / sx
385         elif ('height' in args) and (not 'width' in args):
386             args['width'] = sx * args['height'] / sy
387         elif ('width' in args) and ('height' in args):
388             if (float(args['width'])/args['height'])>(float(sx)>sy):
389                 args['width'] = sx * args['height'] / sy
390             else:
391                 args['height'] = sy * args['width'] / sx
392         self.canvas.drawImage(img, **args)
393
394     def _path(self, node):
395         self.path = self.canvas.beginPath()
396         self.path.moveTo(**utils.attr_get(node, ['x','y']))
397         for n in node.childNodes:
398             if n.nodeType == node.ELEMENT_NODE:
399                 if n.localName=='moveto':
400                     vals = utils.text_get(n).split()
401                     self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1]))
402                 elif n.localName=='curvesto':
403                     vals = utils.text_get(n).split()
404                     while len(vals)>5:
405                         pos=[]
406                         while len(pos)<6:
407                             pos.append(utils.unit_get(vals.pop(0)))
408                         self.path.curveTo(*pos)
409             elif (n.nodeType == node.TEXT_NODE):
410                 data = n.data.split()               # Not sure if I must merge all TEXT_NODE ?
411                 while len(data)>1:
412                     x = utils.unit_get(data.pop(0))
413                     y = utils.unit_get(data.pop(0))
414                     self.path.lineTo(x,y)
415         if (not node.hasAttribute('close')) or utils.bool_get(node.getAttribute('close')):
416             self.path.close()
417         self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
418
419     def render(self, node):
420         tags = {
421             'drawCentredString': self._drawCenteredString,
422             'drawRightString': self._drawRightString,
423             'drawString': self._drawString,
424             'rect': self._rect,
425             'ellipse': self._ellipse,
426             'lines': self._lines,
427             'grid': self._grid,
428             'curves': self._curves,
429             'fill': lambda node: self.canvas.setFillColor(color.get(node.getAttribute('color'))),
430             'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.getAttribute('color'))),
431             'setFont': lambda node: self.canvas.setFont(node.getAttribute('name'), utils.unit_get(node.getAttribute('size'))),
432             'place': self._place,
433             'circle': self._circle,
434             'lineMode': self._line_mode,
435             'path': self._path,
436             'rotate': lambda node: self.canvas.rotate(float(node.getAttribute('degrees'))),
437             'translate': self._translate,
438             'image': self._image
439         }
440         for nd in node.childNodes:
441             if nd.nodeType==nd.ELEMENT_NODE:
442                 for tag in tags:
443                     if nd.localName==tag:
444                         tags[tag](nd)
445                         break
446
447 class _rml_draw(object):
448     def __init__(self, node, styles, images={}, path='.', title=None):
449         self.node = node
450         self.styles = styles
451         self.canvas = None
452         self.images = images
453         self.path = path
454         self.canvas_title = title
455
456     def render(self, canvas, doc):
457         canvas.saveState()
458         cnv = _rml_canvas(canvas, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title)
459         cnv.render(self.node)
460         canvas.restoreState()
461
462 class _rml_flowable(object):
463     def __init__(self, doc, images={}, path='.', title=None):
464         self.doc = doc
465         self.styles = doc.styles
466         self.images = images
467         self.path = path
468         self.title = title
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.LongTable(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, title=self.self2.title)
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()
645                 image.write(image_data)
646                 return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
647             else:
648                 return platypus.Image(node.getAttribute('file'), mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
649
650             from reportlab.lib.utils import ImageReader
651             name = str(node.getAttribute('file'))
652             img = ImageReader(name)
653             (sx,sy) = img.getSize()
654
655             args = {}
656             for tag in ('width','height'):
657                 if node.hasAttribute(tag):
658                     args[tag] = utils.unit_get(node.getAttribute(tag))
659             if ('width' in args) and (not 'height' in args):
660                 args['height'] = sy * args['width'] / sx
661             elif ('height' in args) and (not 'width' in args):
662                 args['width'] = sx * args['height'] / sy
663             elif ('width' in args) and ('height' in args):
664                 if (float(args['width'])/args['height'])>(float(sx)>sy):
665                     args['width'] = sx * args['height'] / sy
666                 else:
667                     args['height'] = sy * args['width'] / sx
668             return platypus.Image(name, mask=(250,255,250,255,250,255), **args)
669         elif node.localName=='spacer':
670             if node.hasAttribute('width'):
671                 width = utils.unit_get(node.getAttribute('width'))
672             else:
673                 width = utils.unit_get('1cm')
674             length = utils.unit_get(node.getAttribute('length'))
675             return platypus.Spacer(width=width, height=length)
676         elif node.localName=='section':
677             return self.render(node)
678         elif node.localName == 'pageNumberReset':
679             return PageReset()
680         elif node.localName in ('pageBreak', 'nextPage'):
681             return platypus.PageBreak()
682         elif node.localName=='condPageBreak':
683             return platypus.CondPageBreak(**(utils.attr_get(node, ['height'])))
684         elif node.localName=='setNextTemplate':
685             return platypus.NextPageTemplate(str(node.getAttribute('name')))
686         elif node.localName=='nextFrame':
687             return platypus.CondPageBreak(1000)           # TODO: change the 1000 !
688         elif node.localName == 'setNextFrame':
689             from reportlab.platypus.doctemplate import NextFrameFlowable
690             return NextFrameFlowable(str(node.getAttribute('name')))
691         elif node.localName == 'currentFrame':
692             from reportlab.platypus.doctemplate import CurrentFrameFlowable
693             return CurrentFrameFlowable(str(node.getAttribute('name')))
694         elif node.localName == 'frameEnd':
695             return EndFrameFlowable()
696         elif node.localName == 'hr':
697             width_hr=node.hasAttribute('width') and node.getAttribute('width') or '100%'
698             color_hr=node.hasAttribute('color') and node.getAttribute('color') or 'black'
699             thickness_hr=node.hasAttribute('thickness') and node.getAttribute('thickness') or 1
700             lineCap_hr=node.hasAttribute('lineCap') and node.getAttribute('lineCap') or 'round'
701             return platypus.flowables.HRFlowable(width=width_hr,color=color.get(color_hr),thickness=float(thickness_hr),lineCap=str(lineCap_hr))
702         else:
703             sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.localName,))
704             return None
705
706     def render(self, node_story):
707         story = []
708         node = node_story.firstChild
709         while node:
710             if node.nodeType == node.ELEMENT_NODE:
711                 flow = self._flowable(node)
712                 if flow:
713                     if type(flow) == type([]):
714                         story = story + flow
715                     else:
716                         story.append(flow)
717             node = node.nextSibling
718         return story
719
720 from reportlab.platypus.doctemplate import ActionFlowable
721
722 class EndFrameFlowable(ActionFlowable):
723     def __init__(self,resume=0):
724         ActionFlowable.__init__(self,('frameEnd',resume))
725
726 class TinyDocTemplate(platypus.BaseDocTemplate):
727     def ___handle_pageBegin(self):
728         self.page = self.page + 1
729         self.pageTemplate.beforeDrawPage(self.canv,self)
730         self.pageTemplate.checkPageSize(self.canv,self)
731         self.pageTemplate.onPage(self.canv,self)
732         for f in self.pageTemplate.frames: f._reset()
733         self.beforePage()
734         #keep a count of flowables added to this page.  zero indicates bad stuff
735         self._curPageFlowableCount = 0
736         if hasattr(self,'_nextFrameIndex'):
737             del self._nextFrameIndex
738         for f in self.pageTemplate.frames:
739             if f.id == 'first':
740                 self.frame = f
741                 break
742         self.handle_frameBegin()
743     def afterFlowable(self, flowable):
744         if isinstance(flowable, PageReset):
745             self.canv._pageNumber = 0
746
747 class _rml_template(object):
748     def __init__(self, out, node, doc, images={}, path='.', title=None):
749         self.images= images
750         self.path = path
751         self.title = title
752         if not node.hasAttribute('pageSize'):
753             pageSize = (utils.unit_get('21cm'), utils.unit_get('29.7cm'))
754         else:
755             ps = map(lambda x:x.strip(), node.getAttribute('pageSize').replace(')', '').replace('(', '').split(','))
756             pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
757         cm = reportlab.lib.units.cm
758         self.doc_tmpl = TinyDocTemplate(out, pagesize=pageSize, **utils.attr_get(node, ['leftMargin','rightMargin','topMargin','bottomMargin'], {'allowSplitting':'int','showBoundary':'bool','title':'str','author':'str'}))
759         self.page_templates = []
760         self.styles = doc.styles
761         self.doc = doc
762         pts = node.getElementsByTagName('pageTemplate')
763         for pt in pts:
764             frames = []
765             for frame_el in pt.getElementsByTagName('frame'):
766                 frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) )
767                 if utils.attr_get(frame_el, ['last']):
768                     frame.lastFrame = True
769                 frames.append( frame )
770             gr = pt.getElementsByTagName('pageGraphics')
771             if len(gr):
772                 drw = _rml_draw(gr[0], self.doc, images=images, path=self.path, title=self.title)
773                 self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
774             else:
775                 drw = _rml_draw(node,self.doc,title=self.title)
776                 self.page_templates.append( platypus.PageTemplate(frames=frames,onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
777         self.doc_tmpl.addPageTemplates(self.page_templates)
778
779     def render(self, node_stories):
780         fis = []
781         r = _rml_flowable(self.doc,images=self.images, path=self.path, title=self.title)
782         for node_story in node_stories:
783             fis += r.render(node_story)
784             if node_story==node_stories[-1]:
785
786                 fis.append(PageCount())
787
788             fis.append(platypus.PageBreak())
789
790         self.doc_tmpl.build(fis)
791
792 def parseString(data, fout=None, images={}, path='.',title=None):
793     r = _rml_doc(data, images, path, title=title)
794     if fout:
795         fp = file(fout,'wb')
796         r.render(fp)
797         fp.close()
798         return fout
799     else:
800         fp = StringIO()
801         r.render(fp)
802         return fp.getvalue()
803
804 def trml2pdf_help():
805     print 'Usage: trml2pdf input.rml >output.pdf'
806     print 'Render the standard input (RML) and output a PDF file'
807     sys.exit(0)
808
809 if __name__=="__main__":
810     if len(sys.argv)>1:
811         if sys.argv[1]=='--help':
812             trml2pdf_help()
813         print parseString(file(sys.argv[1], 'r').read()),
814     else:
815         print 'Usage: trml2pdf input.rml >output.pdf'
816         print 'Try \'trml2pdf --help\' for more information.'
817
818 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
819