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