[FIX] StringIO
[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
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.write(image_data)
360             else:
361                 import base64
362                 image_data = base64.decodestring(node.firstChild.nodeValue)
363                 if not image_data: return False
364                 s.write(image_data)
365         else:
366             if node.getAttribute('file') in self.images:
367                 s.write(self.images[node.getAttribute('file')])
368             else:
369                 try:
370                     u = urllib.urlopen(str(node.getAttribute('file')))
371                 except:
372                     u = file(os.path.join(self.path,str(node.getAttribute('file'))), 'rb')
373                 s.write(u.read())
374         img = ImageReader(s)
375         (sx,sy) = img.getSize()
376
377         args = {}
378         for tag in ('width','height','x','y'):
379             if node.hasAttribute(tag):
380                 args[tag] = utils.unit_get(node.getAttribute(tag))
381         if ('width' in args) and (not 'height' in args):
382             args['height'] = sy * args['width'] / sx
383         elif ('height' in args) and (not 'width' in args):
384             args['width'] = sx * args['height'] / sy
385         elif ('width' in args) and ('height' in args):
386             if (float(args['width'])/args['height'])>(float(sx)>sy):
387                 args['width'] = sx * args['height'] / sy
388             else:
389                 args['height'] = sy * args['width'] / sx
390         self.canvas.drawImage(img, **args)
391
392     def _path(self, node):
393         self.path = self.canvas.beginPath()
394         self.path.moveTo(**utils.attr_get(node, ['x','y']))
395         for n in node.childNodes:
396             if n.nodeType == node.ELEMENT_NODE:
397                 if n.localName=='moveto':
398                     vals = utils.text_get(n).split()
399                     self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1]))
400                 elif n.localName=='curvesto':
401                     vals = utils.text_get(n).split()
402                     while len(vals)>5:
403                         pos=[]
404                         while len(pos)<6:
405                             pos.append(utils.unit_get(vals.pop(0)))
406                         self.path.curveTo(*pos)
407             elif (n.nodeType == node.TEXT_NODE):
408                 data = n.data.split()               # Not sure if I must merge all TEXT_NODE ?
409                 while len(data)>1:
410                     x = utils.unit_get(data.pop(0))
411                     y = utils.unit_get(data.pop(0))
412                     self.path.lineTo(x,y)
413         if (not node.hasAttribute('close')) or utils.bool_get(node.getAttribute('close')):
414             self.path.close()
415         self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
416
417     def render(self, node):
418         tags = {
419             'drawCentredString': self._drawCenteredString,
420             'drawRightString': self._drawRightString,
421             'drawString': self._drawString,
422             'rect': self._rect,
423             'ellipse': self._ellipse,
424             'lines': self._lines,
425             'grid': self._grid,
426             'curves': self._curves,
427             'fill': lambda node: self.canvas.setFillColor(color.get(node.getAttribute('color'))),
428             'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.getAttribute('color'))),
429             'setFont': lambda node: self.canvas.setFont(node.getAttribute('name'), utils.unit_get(node.getAttribute('size'))),
430             'place': self._place,
431             'circle': self._circle,
432             'lineMode': self._line_mode,
433             'path': self._path,
434             'rotate': lambda node: self.canvas.rotate(float(node.getAttribute('degrees'))),
435             'translate': self._translate,
436             'image': self._image
437         }
438         for nd in node.childNodes:
439             if nd.nodeType==nd.ELEMENT_NODE:
440                 for tag in tags:
441                     if nd.localName==tag:
442                         tags[tag](nd)
443                         break
444
445 class _rml_draw(object):
446     def __init__(self, node, styles, images={}, path='.', title=None):
447         self.node = node
448         self.styles = styles
449         self.canvas = None
450         self.images = images
451         self.path = path
452         self.canvas_title = title
453
454     def render(self, canvas, doc):
455         canvas.saveState()
456         cnv = _rml_canvas(canvas, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title)
457         cnv.render(self.node)
458         canvas.restoreState()
459
460 class _rml_flowable(object):
461     def __init__(self, doc, images={}, path='.', title=None):
462         self.doc = doc
463         self.styles = doc.styles
464         self.images = images
465         self.path = path
466         self.title = title
467
468     def _textual(self, node):
469         rc = ''
470         for n in node.childNodes:
471             if n.nodeType == node.ELEMENT_NODE:
472                 if n.localName == 'getName':
473                     newNode = self.doc.dom.createTextNode(self.styles.names.get(n.getAttribute('id'),'Unknown name'))
474                     node.insertBefore(newNode, n)
475                     node.removeChild(n)
476                     n = newNode
477                 elif n.localName == 'pageNumber':
478                     rc += '<pageNumber/>'            # TODO: change this !
479                 else:
480                     #CHECKME: I wonder if this is useful since we don't stock the result. Maybe for the getName tag?
481                     self._textual(n)
482                 rc += n.toxml()
483             elif n.nodeType in (node.CDATA_SECTION_NODE, node.TEXT_NODE):
484                 rc += str2xml(n.data)
485         return rc.encode(encoding, 'replace')
486
487     def _table(self, node):
488         length = 0
489         colwidths = None
490         rowheights = None
491         data = []
492         childs = _child_get(node,'tr')
493         if not childs:
494             return None
495         posy = 0
496         styles = []
497         for tr in childs:
498             paraStyle = None
499             if tr.hasAttribute('style'):
500                 st = copy.deepcopy(self.styles.table_styles[tr.getAttribute('style')])
501                 for s in st._cmds:
502                     s[1][1] = posy
503                     s[2][1] = posy
504                 styles.append(st)
505             if tr.hasAttribute('paraStyle'):
506                 paraStyle = self.styles.styles[tr.getAttribute('paraStyle')]
507
508             data2 = []
509             posx = 0
510             for td in _child_get(tr, 'td'):
511                 if td.hasAttribute('style'):
512                     st = copy.deepcopy(self.styles.table_styles[td.getAttribute('style')])
513                     for s in st._cmds:
514                         s[1][1] = posy
515                         s[2][1] = posy
516                         s[1][0] = posx
517                         s[2][0] = posx
518                     styles.append(st)
519                 if td.hasAttribute('paraStyle'):
520                     # TODO: merge styles
521                     paraStyle = self.styles.styles[td.getAttribute('paraStyle')]
522                 posx += 1
523
524                 flow = []
525                 for n in td.childNodes:
526                     if n.nodeType==node.ELEMENT_NODE:
527                         fl = self._flowable(n, extra_style=paraStyle)
528                         flow.append( fl )
529                 if not len(flow):
530                     flow = self._textual(td)
531                 data2.append( flow )
532             if len(data2)>length:
533                 length=len(data2)
534                 for ab in data:
535                     while len(ab)<length:
536                         ab.append('')
537             while len(data2)<length:
538                 data2.append('')
539             data.append( data2 )
540             posy += 1
541         if node.hasAttribute('colWidths'):
542             assert length == len(node.getAttribute('colWidths').split(','))
543             colwidths = [utils.unit_get(f.strip()) for f in node.getAttribute('colWidths').split(',')]
544         if node.hasAttribute('rowHeights'):
545             rowheights = [utils.unit_get(f.strip()) for f in node.getAttribute('rowHeights').split(',')]
546             if len(rowheights) == 1:
547                 rowheights = rowheights[0]
548         table = platypus.LongTable(data = data, colWidths=colwidths, rowHeights=rowheights, **(utils.attr_get(node, ['splitByRow'] ,{'repeatRows':'int','repeatCols':'int'})))
549         if node.hasAttribute('style'):
550             table.setStyle(self.styles.table_styles[node.getAttribute('style')])
551         for s in styles:
552             table.setStyle(s)
553         return table
554
555     def _illustration(self, node):
556         class Illustration(platypus.flowables.Flowable):
557             def __init__(self, node, styles, self2):
558                 self.node = node
559                 self.styles = styles
560                 self.width = utils.unit_get(node.getAttribute('width'))
561                 self.height = utils.unit_get(node.getAttribute('height'))
562                 self.self2 = self2
563             def wrap(self, *args):
564                 return (self.width, self.height)
565             def draw(self):
566                 canvas = self.canv
567                 drw = _rml_draw(self.node, self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title)
568                 drw.render(self.canv, None)
569         return Illustration(node, self.styles, self)
570
571     def _textual_image(self, node):
572         import base64
573         rc = ''
574         for n in node.childNodes:
575             if n.nodeType in (node.CDATA_SECTION_NODE, node.TEXT_NODE):
576                 rc += n.data
577         return base64.decodestring(rc)
578
579     def _flowable(self, node, extra_style=None):
580         if node.localName=='para':
581             style = self.styles.para_style_get(node)
582             if extra_style:
583                 style.__dict__.update(extra_style)
584             return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
585         elif node.localName=='barCode':
586             try:
587                 from reportlab.graphics.barcode import code128
588                 from reportlab.graphics.barcode import code39
589                 from reportlab.graphics.barcode import code93
590                 from reportlab.graphics.barcode import common
591                 from reportlab.graphics.barcode import fourstate
592                 from reportlab.graphics.barcode import usps
593             except Exception, e:
594                 print 'Warning: Reportlab barcode extension not installed !'
595                 return None
596             args = utils.attr_get(node, [], {'ratio':'float','xdim':'unit','height':'unit','checksum':'bool','quiet':'bool'})
597             codes = {
598                 'codabar': lambda x: common.Codabar(x, **args),
599                 'code11': lambda x: common.Code11(x, **args),
600                 'code128': lambda x: code128.Code128(x, **args),
601                 'standard39': lambda x: code39.Standard39(x, **args),
602                 'standard93': lambda x: code93.Standard93(x, **args),
603                 'i2of5': lambda x: common.I2of5(x, **args),
604                 'extended39': lambda x: code39.Extended39(x, **args),
605                 'extended93': lambda x: code93.Extended93(x, **args),
606                 'msi': lambda x: common.MSI(x, **args),
607                 'fim': lambda x: usps.FIM(x, **args),
608                 'postnet': lambda x: usps.POSTNET(x, **args),
609             }
610             code = 'code128'
611             if node.hasAttribute('code'):
612                 code = node.getAttribute('code').lower()
613             return codes[code](self._textual(node))
614         elif node.localName=='name':
615             self.styles.names[ node.getAttribute('id')] = node.getAttribute('value')
616             return None
617         elif node.localName=='xpre':
618             style = self.styles.para_style_get(node)
619             return platypus.XPreformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int','frags':'int'})))
620         elif node.localName=='pre':
621             style = self.styles.para_style_get(node)
622             return platypus.Preformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int'})))
623         elif node.localName=='illustration':
624             return  self._illustration(node)
625         elif node.localName=='blockTable':
626             return  self._table(node)
627         elif node.localName=='title':
628             styles = reportlab.lib.styles.getSampleStyleSheet()
629             style = styles['Title']
630             return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
631         elif re.match('^h([1-9]+[0-9]*)$', node.localName):
632             styles = reportlab.lib.styles.getSampleStyleSheet()
633             style = styles['Heading'+str(node.localName[1:])]
634             return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
635         elif node.localName=='image':
636             if not node.hasAttribute('file'):
637                 if node.hasAttribute('name'):
638                     image_data = self.doc.images[node.getAttribute('name')].read()
639                 else:
640                     import base64
641                     image_data = base64.decodestring(node.firstChild.nodeValue)
642                 image = StringIO()
643                 image.write(image_data)
644                 return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
645             else:
646                 return platypus.Image(node.getAttribute('file'), mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
647
648             from reportlab.lib.utils import ImageReader
649             name = str(node.getAttribute('file'))
650             img = ImageReader(name)
651             (sx,sy) = img.getSize()
652
653             args = {}
654             for tag in ('width','height'):
655                 if node.hasAttribute(tag):
656                     args[tag] = utils.unit_get(node.getAttribute(tag))
657             if ('width' in args) and (not 'height' in args):
658                 args['height'] = sy * args['width'] / sx
659             elif ('height' in args) and (not 'width' in args):
660                 args['width'] = sx * args['height'] / sy
661             elif ('width' in args) and ('height' in args):
662                 if (float(args['width'])/args['height'])>(float(sx)>sy):
663                     args['width'] = sx * args['height'] / sy
664                 else:
665                     args['height'] = sy * args['width'] / sx
666             return platypus.Image(name, mask=(250,255,250,255,250,255), **args)
667         elif node.localName=='spacer':
668             if node.hasAttribute('width'):
669                 width = utils.unit_get(node.getAttribute('width'))
670             else:
671                 width = utils.unit_get('1cm')
672             length = utils.unit_get(node.getAttribute('length'))
673             return platypus.Spacer(width=width, height=length)
674         elif node.localName=='section':
675             return self.render(node)
676         elif node.localName == 'pageNumberReset':
677             return PageReset()
678         elif node.localName in ('pageBreak', 'nextPage'):
679             return platypus.PageBreak()
680         elif node.localName=='condPageBreak':
681             return platypus.CondPageBreak(**(utils.attr_get(node, ['height'])))
682         elif node.localName=='setNextTemplate':
683             return platypus.NextPageTemplate(str(node.getAttribute('name')))
684         elif node.localName=='nextFrame':
685             return platypus.CondPageBreak(1000)           # TODO: change the 1000 !
686         elif node.localName == 'setNextFrame':
687             from reportlab.platypus.doctemplate import NextFrameFlowable
688             return NextFrameFlowable(str(node.getAttribute('name')))
689         elif node.localName == 'currentFrame':
690             from reportlab.platypus.doctemplate import CurrentFrameFlowable
691             return CurrentFrameFlowable(str(node.getAttribute('name')))
692         elif node.localName == 'frameEnd':
693             return EndFrameFlowable()
694         elif node.localName == 'hr':
695             width_hr=node.hasAttribute('width') and node.getAttribute('width') or '100%'
696             color_hr=node.hasAttribute('color') and node.getAttribute('color') or 'black'
697             thickness_hr=node.hasAttribute('thickness') and node.getAttribute('thickness') or 1
698             lineCap_hr=node.hasAttribute('lineCap') and node.getAttribute('lineCap') or 'round'
699             return platypus.flowables.HRFlowable(width=width_hr,color=color.get(color_hr),thickness=float(thickness_hr),lineCap=str(lineCap_hr))
700         else:
701             sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.localName,))
702             return None
703
704     def render(self, node_story):
705         story = []
706         node = node_story.firstChild
707         while node:
708             if node.nodeType == node.ELEMENT_NODE:
709                 flow = self._flowable(node)
710                 if flow:
711                     if type(flow) == type([]):
712                         story = story + flow
713                     else:
714                         story.append(flow)
715             node = node.nextSibling
716         return story
717
718 from reportlab.platypus.doctemplate import ActionFlowable
719
720 class EndFrameFlowable(ActionFlowable):
721     def __init__(self,resume=0):
722         ActionFlowable.__init__(self,('frameEnd',resume))
723
724 class TinyDocTemplate(platypus.BaseDocTemplate):
725     def ___handle_pageBegin(self):
726         self.page = self.page + 1
727         self.pageTemplate.beforeDrawPage(self.canv,self)
728         self.pageTemplate.checkPageSize(self.canv,self)
729         self.pageTemplate.onPage(self.canv,self)
730         for f in self.pageTemplate.frames: f._reset()
731         self.beforePage()
732         #keep a count of flowables added to this page.  zero indicates bad stuff
733         self._curPageFlowableCount = 0
734         if hasattr(self,'_nextFrameIndex'):
735             del self._nextFrameIndex
736         for f in self.pageTemplate.frames:
737             if f.id == 'first':
738                 self.frame = f
739                 break
740         self.handle_frameBegin()
741     def afterFlowable(self, flowable):
742         if isinstance(flowable, PageReset):
743             self.canv._pageNumber = 0
744
745 class _rml_template(object):
746     def __init__(self, out, node, doc, images={}, path='.', title=None):
747         self.images= images
748         self.path = path
749         self.title = title
750         if not node.hasAttribute('pageSize'):
751             pageSize = (utils.unit_get('21cm'), utils.unit_get('29.7cm'))
752         else:
753             ps = map(lambda x:x.strip(), node.getAttribute('pageSize').replace(')', '').replace('(', '').split(','))
754             pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
755         cm = reportlab.lib.units.cm
756         self.doc_tmpl = TinyDocTemplate(out, pagesize=pageSize, **utils.attr_get(node, ['leftMargin','rightMargin','topMargin','bottomMargin'], {'allowSplitting':'int','showBoundary':'bool','title':'str','author':'str'}))
757         self.page_templates = []
758         self.styles = doc.styles
759         self.doc = doc
760         pts = node.getElementsByTagName('pageTemplate')
761         for pt in pts:
762             frames = []
763             for frame_el in pt.getElementsByTagName('frame'):
764                 frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) )
765                 if utils.attr_get(frame_el, ['last']):
766                     frame.lastFrame = True
767                 frames.append( frame )
768             gr = pt.getElementsByTagName('pageGraphics')
769             if len(gr):
770                 drw = _rml_draw(gr[0], self.doc, images=images, path=self.path, title=self.title)
771                 self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
772             else:
773                 drw = _rml_draw(node,self.doc,title=self.title)
774                 self.page_templates.append( platypus.PageTemplate(frames=frames,onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
775         self.doc_tmpl.addPageTemplates(self.page_templates)
776
777     def render(self, node_stories):
778         fis = []
779         r = _rml_flowable(self.doc,images=self.images, path=self.path, title=self.title)
780         for node_story in node_stories:
781             fis += r.render(node_story)
782             if node_story==node_stories[-1]:
783
784                 fis.append(PageCount())
785
786             fis.append(platypus.PageBreak())
787
788         self.doc_tmpl.build(fis)
789
790 def parseString(data, fout=None, images={}, path='.',title=None):
791     r = _rml_doc(data, images, path, title=title)
792     if fout:
793         fp = file(fout,'wb')
794         r.render(fp)
795         fp.close()
796         return fout
797     else:
798         fp = StringIO()
799         r.render(fp)
800         return fp.getvalue()
801
802 def trml2pdf_help():
803     print 'Usage: trml2pdf input.rml >output.pdf'
804     print 'Render the standard input (RML) and output a PDF file'
805     sys.exit(0)
806
807 if __name__=="__main__":
808     if len(sys.argv)>1:
809         if sys.argv[1]=='--help':
810             trml2pdf_help()
811         print parseString(file(sys.argv[1], 'r').read()),
812     else:
813         print 'Usage: trml2pdf input.rml >output.pdf'
814         print 'Try \'trml2pdf --help\' for more information.'
815
816 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
817