[FIX]trml2pdf.py : translation issue for the Page x of y on report which is hard...
[odoo/odoo.git] / openerp / 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 import copy
25 import reportlab
26 import re
27 from reportlab.pdfgen import canvas
28 from reportlab import platypus
29 import utils
30 import color
31 import os
32 import logging
33 from lxml import etree
34 import base64
35 from reportlab.platypus.doctemplate import ActionFlowable
36 from openerp.tools.safe_eval import safe_eval as eval
37 from reportlab.lib.units import inch,cm,mm
38 from openerp.tools.misc import file_open
39 from reportlab.pdfbase import pdfmetrics
40 from reportlab.lib.pagesizes import A4, letter
41
42 try:
43     from cStringIO import StringIO
44     _hush_pyflakes = [ StringIO ]
45 except ImportError:
46     from StringIO import StringIO
47
48 _logger = logging.getLogger(__name__)
49
50 encoding = 'utf-8'
51
52 def _open_image(filename, path=None):
53     """Attempt to open a binary file and return the descriptor
54     """
55     if os.path.isfile(filename):
56         return open(filename, 'rb')
57     for p in (path or []):
58         if p and os.path.isabs(p):
59             fullpath = os.path.join(p, filename)
60             if os.path.isfile(fullpath):
61                 return open(fullpath, 'rb')
62         try:
63             if p:
64                 fullpath = os.path.join(p, filename)
65             else:
66                 fullpath = filename
67             return file_open(fullpath)
68         except IOError:
69             pass
70     raise IOError("File %s cannot be found in image path" % filename)
71
72 class NumberedCanvas(canvas.Canvas):
73     def __init__(self, *args, **kwargs):
74         canvas.Canvas.__init__(self, *args, **kwargs)
75         self._codes = []
76         self._flag=False
77         self._pageCount=0
78         self._currentPage =0
79         self._pageCounter=0
80         self.pages={}
81
82     def showPage(self):
83         self._currentPage +=1
84         if not self._flag:
85            self._pageCount += 1
86         else:
87             self.pages.update({self._currentPage:self._pageCount})
88         self._codes.append({'code': self._code, 'stack': self._codeStack})
89         self._startPage()
90         self._flag=False
91
92     def pageCount(self):
93         if self.pages.get(self._pageCounter,False):
94             self._pageNumber=0
95         self._pageCounter +=1
96         key=self._pageCounter
97         if not self.pages.get(key,False):
98             while not self.pages.get(key,False):
99                 key = key + 1
100         self.setFont("Helvetica", 8)
101         self.drawRightString((self._pagesize[0]-30), (self._pagesize[1]-40),
102             " %(this)i / %(total)i" % {
103                'this': self._pageNumber+1,
104                'total': self.pages.get(key,False),
105             }
106         )
107
108     def save(self):
109         """add page info to each page (page x of y)"""
110         # reset page counter
111         self._pageNumber = 0
112         for code in self._codes:
113             self._code = code['code']
114             self._codeStack = code['stack']
115             self.pageCount()
116             canvas.Canvas.showPage(self)
117 #        self.restoreState()
118         self._doc.SaveToFile(self._filename, self)
119
120 class PageCount(platypus.Flowable):
121     def draw(self):
122         self.canv.beginForm("pageCount")
123         self.canv.setFont("Helvetica", utils.unit_get(str(8)))
124         self.canv.drawString(0, 0, str(self.canv.getPageNumber()))
125         self.canv.endForm()
126
127 class PageReset(platypus.Flowable):
128     def draw(self):
129         self.canv._pageNumber = 0
130
131 class _rml_styles(object,):
132     def __init__(self, nodes, localcontext):
133         self.localcontext = localcontext
134         self.styles = {}
135         self.styles_obj = {}
136         self.names = {}
137         self.table_styles = {}
138         self.default_style = reportlab.lib.styles.getSampleStyleSheet()
139
140         for node in nodes:
141             for style in node.findall('blockTableStyle'):
142                 self.table_styles[style.get('id')] = self._table_style_get(style)
143             for style in node.findall('paraStyle'):
144                 sname = style.get('name')
145                 self.styles[sname] = self._para_style_update(style)
146
147                 self.styles_obj[sname] = reportlab.lib.styles.ParagraphStyle(sname, self.default_style["Normal"], **self.styles[sname])
148
149             for variable in node.findall('initialize'):
150                 for name in variable.findall('name'):
151                     self.names[ name.get('id')] = name.get('value')
152
153     def _para_style_update(self, node):
154         data = {}
155         for attr in ['textColor', 'backColor', 'bulletColor', 'borderColor']:
156             if node.get(attr):
157                 data[attr] = color.get(node.get(attr))
158         for attr in ['fontName', 'bulletFontName', 'bulletText']:
159             if node.get(attr):
160                 data[attr] = node.get(attr)
161         for attr in ['fontSize', 'leftIndent', 'rightIndent', 'spaceBefore', 'spaceAfter',
162             'firstLineIndent', 'bulletIndent', 'bulletFontSize', 'leading',
163             'borderWidth','borderPadding','borderRadius']:
164             if node.get(attr):
165                 data[attr] = utils.unit_get(node.get(attr))
166         if node.get('alignment'):
167             align = {
168                 'right':reportlab.lib.enums.TA_RIGHT,
169                 'center':reportlab.lib.enums.TA_CENTER,
170                 'justify':reportlab.lib.enums.TA_JUSTIFY
171             }
172             data['alignment'] = align.get(node.get('alignment').lower(), reportlab.lib.enums.TA_LEFT)
173         return data
174
175     def _table_style_get(self, style_node):
176         styles = []
177         for node in style_node:
178             start = utils.tuple_int_get(node, 'start', (0,0) )
179             stop = utils.tuple_int_get(node, 'stop', (-1,-1) )
180             if node.tag=='blockValign':
181                 styles.append(('VALIGN', start, stop, str(node.get('value'))))
182             elif node.tag=='blockFont':
183                 styles.append(('FONT', start, stop, str(node.get('name'))))
184             elif node.tag=='blockTextColor':
185                 styles.append(('TEXTCOLOR', start, stop, color.get(str(node.get('colorName')))))
186             elif node.tag=='blockLeading':
187                 styles.append(('LEADING', start, stop, utils.unit_get(node.get('length'))))
188             elif node.tag=='blockAlignment':
189                 styles.append(('ALIGNMENT', start, stop, str(node.get('value'))))
190             elif node.tag=='blockSpan':
191                 styles.append(('SPAN', start, stop))
192             elif node.tag=='blockLeftPadding':
193                 styles.append(('LEFTPADDING', start, stop, utils.unit_get(node.get('length'))))
194             elif node.tag=='blockRightPadding':
195                 styles.append(('RIGHTPADDING', start, stop, utils.unit_get(node.get('length'))))
196             elif node.tag=='blockTopPadding':
197                 styles.append(('TOPPADDING', start, stop, utils.unit_get(node.get('length'))))
198             elif node.tag=='blockBottomPadding':
199                 styles.append(('BOTTOMPADDING', start, stop, utils.unit_get(node.get('length'))))
200             elif node.tag=='blockBackground':
201                 styles.append(('BACKGROUND', start, stop, color.get(node.get('colorName'))))
202             if node.get('size'):
203                 styles.append(('FONTSIZE', start, stop, utils.unit_get(node.get('size'))))
204             elif node.tag=='lineStyle':
205                 kind = node.get('kind')
206                 kind_list = [ 'GRID', 'BOX', 'OUTLINE', 'INNERGRID', 'LINEBELOW', 'LINEABOVE','LINEBEFORE', 'LINEAFTER' ]
207                 assert kind in kind_list
208                 thick = 1
209                 if node.get('thickness'):
210                     thick = float(node.get('thickness'))
211                 styles.append((kind, start, stop, thick, color.get(node.get('colorName'))))
212         return platypus.tables.TableStyle(styles)
213
214     def para_style_get(self, node):
215         style = False
216         sname = node.get('style')
217         if sname:
218             if sname in self.styles_obj:
219                 style = self.styles_obj[sname]
220             else:
221                 _logger.warning('Warning: style not found, %s - setting default!\n' % (node.get('style'),) )
222         if not style:
223             style = self.default_style['Normal']
224         para_update = self._para_style_update(node)
225         if para_update:
226             # update style only is necessary
227             style = copy.deepcopy(style)
228             style.__dict__.update(para_update)
229         return style
230
231 class _rml_doc(object):
232     def __init__(self, node, localcontext=None, images=None, path='.', title=None):
233         if images is None:
234             images = {}
235         if localcontext is None:
236             localcontext = {}
237         self.localcontext = localcontext
238         self.etree = node
239         self.filename = self.etree.get('filename')
240         self.images = images
241         self.path = path
242         self.title = title
243
244     def docinit(self, els):
245         from reportlab.lib.fonts import addMapping
246         from reportlab.pdfbase import pdfmetrics
247         from reportlab.pdfbase.ttfonts import TTFont
248
249         for node in els:
250             for font in node.findall('registerFont'):
251                 name = font.get('fontName').encode('ascii')
252                 fname = font.get('fontFile').encode('ascii')
253                 if name not in pdfmetrics._fonts:
254                     pdfmetrics.registerFont(TTFont(name, fname))
255                 addMapping(name, 0, 0, name)    #normal
256                 addMapping(name, 0, 1, name)    #italic
257                 addMapping(name, 1, 0, name)    #bold
258                 addMapping(name, 1, 1, name)    #italic and bold
259
260     def setTTFontMapping(self,face, fontname, filename, mode='all'):
261         from reportlab.lib.fonts import addMapping
262         from reportlab.pdfbase import pdfmetrics
263         from reportlab.pdfbase.ttfonts import TTFont
264
265         if fontname not in pdfmetrics._fonts:
266             pdfmetrics.registerFont(TTFont(fontname, filename))
267         if (mode == 'all'):
268             addMapping(face, 0, 0, fontname)    #normal
269             addMapping(face, 0, 1, fontname)    #italic
270             addMapping(face, 1, 0, fontname)    #bold
271             addMapping(face, 1, 1, fontname)    #italic and bold
272         elif (mode== 'normal') or (mode == 'regular'):
273             addMapping(face, 0, 0, fontname)    #normal
274         elif (mode == 'italic'):
275             addMapping(face, 0, 1, fontname)    #italic
276         elif (mode == 'bold'):
277             addMapping(face, 1, 0, fontname)    #bold
278         elif (mode == 'bolditalic'):
279             addMapping(face, 1, 1, fontname)    #italic and bold
280
281     def _textual_image(self, node):
282         rc = ''
283         for n in node:
284             rc +=( etree.tostring(n) or '') + n.tail
285         return base64.decodestring(node.tostring())
286
287     def _images(self, el):
288         result = {}
289         for node in el.findall('.//image'):
290             rc =( node.text or '')
291             result[node.get('name')] = base64.decodestring(rc)
292         return result
293
294     def render(self, out):
295         el = self.etree.findall('.//docinit')
296         if el:
297             self.docinit(el)
298
299         el = self.etree.findall('.//stylesheet')
300         self.styles = _rml_styles(el,self.localcontext)
301
302         el = self.etree.findall('.//images')
303         if el:
304             self.images.update( self._images(el[0]) )
305
306         el = self.etree.findall('.//template')
307         if len(el):
308             pt_obj = _rml_template(self.localcontext, out, el[0], self, images=self.images, path=self.path, title=self.title)
309             el = utils._child_get(self.etree, self, 'story')
310             pt_obj.render(el)
311         else:
312             self.canvas = canvas.Canvas(out)
313             pd = self.etree.find('pageDrawing')[0]
314             pd_obj = _rml_canvas(self.canvas, self.localcontext, None, self, self.images, path=self.path, title=self.title)
315             pd_obj.render(pd)
316
317             self.canvas.showPage()
318             self.canvas.save()
319
320 class _rml_canvas(object):
321     def __init__(self, canvas, localcontext, doc_tmpl=None, doc=None, images=None, path='.', title=None):
322         if images is None:
323             images = {}
324         self.localcontext = localcontext
325         self.canvas = canvas
326         self.styles = doc.styles
327         self.doc_tmpl = doc_tmpl
328         self.doc = doc
329         self.images = images
330         self.path = path
331         self.title = title
332         if self.title:
333             self.canvas.setTitle(self.title)
334
335     def _textual(self, node, x=0, y=0):
336         text = node.text and node.text.encode('utf-8') or ''
337         rc = utils._process_text(self, text)
338         for n in node:
339             if n.tag == 'seq':
340                 from reportlab.lib.sequencer import getSequencer
341                 seq = getSequencer()
342                 rc += str(seq.next(n.get('id')))
343             if n.tag == 'pageCount':
344                 if x or y:
345                     self.canvas.translate(x,y)
346                 self.canvas.doForm('pageCount')
347                 if x or y:
348                     self.canvas.translate(-x,-y)
349             if n.tag == 'pageNumber':
350                 rc += str(self.canvas.getPageNumber())
351             rc += utils._process_text(self, n.tail)
352         return rc.replace('\n','')
353
354     def _drawString(self, node):
355         v = utils.attr_get(node, ['x','y'])
356         text=self._textual(node, **v)
357         text = utils.xml2str(text)
358         self.canvas.drawString(text=text, **v)
359
360     def _drawCenteredString(self, node):
361         v = utils.attr_get(node, ['x','y'])
362         text=self._textual(node, **v)
363         text = utils.xml2str(text)
364         self.canvas.drawCentredString(text=text, **v)
365
366     def _drawRightString(self, node):
367         v = utils.attr_get(node, ['x','y'])
368         text=self._textual(node, **v)
369         text = utils.xml2str(text)
370         self.canvas.drawRightString(text=text, **v)
371
372     def _rect(self, node):
373         if node.get('round'):
374             self.canvas.roundRect(radius=utils.unit_get(node.get('round')), **utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
375         else:
376             self.canvas.rect(**utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
377
378     def _ellipse(self, node):
379         x1 = utils.unit_get(node.get('x'))
380         x2 = utils.unit_get(node.get('width'))
381         y1 = utils.unit_get(node.get('y'))
382         y2 = utils.unit_get(node.get('height'))
383
384         self.canvas.ellipse(x1,y1,x2,y2, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
385
386     def _curves(self, node):
387         line_str = node.text.split()
388         lines = []
389         while len(line_str)>7:
390             self.canvas.bezier(*[utils.unit_get(l) for l in line_str[0:8]])
391             line_str = line_str[8:]
392
393     def _lines(self, node):
394         line_str = node.text.split()
395         lines = []
396         while len(line_str)>3:
397             lines.append([utils.unit_get(l) for l in line_str[0:4]])
398             line_str = line_str[4:]
399         self.canvas.lines(lines)
400
401     def _grid(self, node):
402         xlist = [utils.unit_get(s) for s in node.get('xs').split(',')]
403         ylist = [utils.unit_get(s) for s in node.get('ys').split(',')]
404
405         self.canvas.grid(xlist, ylist)
406
407     def _translate(self, node):
408         dx = utils.unit_get(node.get('dx')) or 0
409         dy = utils.unit_get(node.get('dy')) or 0
410         self.canvas.translate(dx,dy)
411
412     def _circle(self, node):
413         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'}))
414
415     def _place(self, node):
416         flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title).render(node)
417         infos = utils.attr_get(node, ['x','y','width','height'])
418
419         infos['y']+=infos['height']
420         for flow in flows:
421             w,h = flow.wrap(infos['width'], infos['height'])
422             if w<=infos['width'] and h<=infos['height']:
423                 infos['y']-=h
424                 flow.drawOn(self.canvas,infos['x'],infos['y'])
425                 infos['height']-=h
426             else:
427                 raise ValueError, "Not enough space"
428
429     def _line_mode(self, node):
430         ljoin = {'round':1, 'mitered':0, 'bevelled':2}
431         lcap = {'default':0, 'round':1, 'square':2}
432
433         if node.get('width'):
434             self.canvas.setLineWidth(utils.unit_get(node.get('width')))
435         if node.get('join'):
436             self.canvas.setLineJoin(ljoin[node.get('join')])
437         if node.get('cap'):
438             self.canvas.setLineCap(lcap[node.get('cap')])
439         if node.get('miterLimit'):
440             self.canvas.setDash(utils.unit_get(node.get('miterLimit')))
441         if node.get('dash'):
442             dashes = node.get('dash').split(',')
443             for x in range(len(dashes)):
444                 dashes[x]=utils.unit_get(dashes[x])
445             self.canvas.setDash(node.get('dash').split(','))
446
447     def _image(self, node):
448         import urllib
449         import urlparse
450         from reportlab.lib.utils import ImageReader
451         nfile = node.get('file')
452         if not nfile:
453             if node.get('name'):
454                 image_data = self.images[node.get('name')]
455                 _logger.debug("Image %s used", node.get('name'))
456                 s = StringIO(image_data)
457             else:
458                 newtext = node.text
459                 if self.localcontext:
460                     res = utils._regex.findall(newtext)
461                     for key in res:
462                         newtext = eval(key, {}, self.localcontext) or ''
463                 image_data = None
464                 if newtext:
465                     image_data = base64.decodestring(newtext)
466                 if image_data:
467                     s = StringIO(image_data)
468                 else:
469                     _logger.debug("No image data!")
470                     return False
471         else:
472             if nfile in self.images:
473                 s = StringIO(self.images[nfile])
474             else:
475                 try:
476                     up = urlparse.urlparse(str(nfile))
477                 except ValueError:
478                     up = False
479                 if up and up.scheme:
480                     # RFC: do we really want to open external URLs?
481                     # Are we safe from cross-site scripting or attacks?
482                     _logger.debug("Retrieve image from %s", nfile)
483                     u = urllib.urlopen(str(nfile))
484                     s = StringIO(u.read())
485                 else:
486                     _logger.debug("Open image file %s ", nfile)
487                     s = _open_image(nfile, path=self.path)
488         try:
489             img = ImageReader(s)
490             (sx,sy) = img.getSize()
491             _logger.debug("Image is %dx%d", sx, sy)
492             args = { 'x': 0.0, 'y': 0.0 }
493             for tag in ('width','height','x','y'):
494                 if node.get(tag):
495                     args[tag] = utils.unit_get(node.get(tag))
496             if ('width' in args) and (not 'height' in args):
497                 args['height'] = sy * args['width'] / sx
498             elif ('height' in args) and (not 'width' in args):
499                 args['width'] = sx * args['height'] / sy
500             elif ('width' in args) and ('height' in args):
501                 if (float(args['width'])/args['height'])>(float(sx)>sy):
502                     args['width'] = sx * args['height'] / sy
503                 else:
504                     args['height'] = sy * args['width'] / sx
505             self.canvas.drawImage(img, **args)
506         finally:
507             s.close()
508 #        self.canvas._doc.SaveToFile(self.canvas._filename, self.canvas)
509
510     def _path(self, node):
511         self.path = self.canvas.beginPath()
512         self.path.moveTo(**utils.attr_get(node, ['x','y']))
513         for n in utils._child_get(node, self):
514             if not n.text :
515                 if n.tag=='moveto':
516                     vals = utils.text_get(n).split()
517                     self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1]))
518                 elif n.tag=='curvesto':
519                     vals = utils.text_get(n).split()
520                     while len(vals)>5:
521                         pos=[]
522                         while len(pos)<6:
523                             pos.append(utils.unit_get(vals.pop(0)))
524                         self.path.curveTo(*pos)
525             elif n.text:
526                 data = n.text.split()               # Not sure if I must merge all TEXT_NODE ?
527                 while len(data)>1:
528                     x = utils.unit_get(data.pop(0))
529                     y = utils.unit_get(data.pop(0))
530                     self.path.lineTo(x,y)
531         if (not node.get('close')) or utils.bool_get(node.get('close')):
532             self.path.close()
533         self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
534
535     def setFont(self, node):
536         fontname = node.get('name')
537         if fontname not in pdfmetrics.getRegisteredFontNames()\
538              or fontname not in pdfmetrics.standardFonts:
539                 # let reportlab attempt to find it
540                 try:
541                     pdfmetrics.getFont(fontname)
542                 except Exception:
543                     _logger.debug('Could not locate font %s, substituting default: %s',
544                                  fontname,
545                                  self.canvas._fontname)
546                     fontname = self.canvas._fontname
547         return self.canvas.setFont(fontname, utils.unit_get(node.get('size')))
548
549     def render(self, node):
550         tags = {
551             'drawCentredString': self._drawCenteredString,
552             'drawRightString': self._drawRightString,
553             'drawString': self._drawString,
554             'rect': self._rect,
555             'ellipse': self._ellipse,
556             'lines': self._lines,
557             'grid': self._grid,
558             'curves': self._curves,
559             'fill': lambda node: self.canvas.setFillColor(color.get(node.get('color'))),
560             'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.get('color'))),
561             'setFont': self.setFont ,
562             'place': self._place,
563             'circle': self._circle,
564             'lineMode': self._line_mode,
565             'path': self._path,
566             'rotate': lambda node: self.canvas.rotate(float(node.get('degrees'))),
567             'translate': self._translate,
568             'image': self._image
569         }
570         for n in utils._child_get(node, self):
571             if n.tag in tags:
572                 tags[n.tag](n)
573
574 class _rml_draw(object):
575     def __init__(self, localcontext, node, styles, images=None, path='.', title=None):
576         if images is None:
577             images = {}
578         self.localcontext = localcontext
579         self.node = node
580         self.styles = styles
581         self.canvas = None
582         self.images = images
583         self.path = path
584         self.canvas_title = title
585
586     def render(self, canvas, doc):
587         canvas.saveState()
588         cnv = _rml_canvas(canvas, self.localcontext, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title)
589         cnv.render(self.node)
590         canvas.restoreState()
591
592 class _rml_Illustration(platypus.flowables.Flowable):
593     def __init__(self, node, localcontext, styles, self2):
594         self.localcontext = (localcontext or {}).copy()
595         self.node = node
596         self.styles = styles
597         self.width = utils.unit_get(node.get('width'))
598         self.height = utils.unit_get(node.get('height'))
599         self.self2 = self2
600     def wrap(self, *args):
601         return (self.width, self.height)
602     def draw(self):
603         drw = _rml_draw(self.localcontext ,self.node,self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title)
604         drw.render(self.canv, None)
605
606 class _rml_flowable(object):
607     def __init__(self, doc, localcontext, images=None, path='.', title=None):
608         if images is None:
609             images = {}
610         self.localcontext = localcontext
611         self.doc = doc
612         self.styles = doc.styles
613         self.images = images
614         self.path = path
615         self.title = title
616
617     def _textual(self, node):
618         rc1 = utils._process_text(self, node.text or '')
619         for n in utils._child_get(node,self):
620             txt_n = copy.deepcopy(n)
621             for key in txt_n.attrib.keys():
622                 if key in ('rml_except', 'rml_loop', 'rml_tag'):
623                     del txt_n.attrib[key]
624             if not n.tag == 'bullet':
625                 txt_n.text = utils.xml2str(self._textual(n))
626             txt_n.tail = n.tail and utils.xml2str(utils._process_text(self, n.tail.replace('\n',''))) or ''
627             rc1 += etree.tostring(txt_n)
628         return rc1
629
630     def _table(self, node):
631         children = utils._child_get(node,self,'tr')
632         if not children:
633             return None
634         length = 0
635         colwidths = None
636         rowheights = None
637         data = []
638         styles = []
639         posy = 0
640         for tr in children:
641             paraStyle = None
642             if tr.get('style'):
643                 st = copy.deepcopy(self.styles.table_styles[tr.get('style')])
644                 for si in range(len(st._cmds)):
645                     s = list(st._cmds[si])
646                     s[1] = (s[1][0],posy)
647                     s[2] = (s[2][0],posy)
648                     st._cmds[si] = tuple(s)
649                 styles.append(st)
650             if tr.get('paraStyle'):
651                 paraStyle = self.styles.styles[tr.get('paraStyle')]
652             data2 = []
653             posx = 0
654             for td in utils._child_get(tr, self,'td'):
655                 if td.get('style'):
656                     st = copy.deepcopy(self.styles.table_styles[td.get('style')])
657                     for s in st._cmds:
658                         s[1][1] = posy
659                         s[2][1] = posy
660                         s[1][0] = posx
661                         s[2][0] = posx
662                     styles.append(st)
663                 if td.get('paraStyle'):
664                     # TODO: merge styles
665                     paraStyle = self.styles.styles[td.get('paraStyle')]
666                 posx += 1
667
668                 flow = []
669                 for n in utils._child_get(td, self):
670                     if n.tag == etree.Comment:
671                         n.text = ''
672                         continue
673                     fl = self._flowable(n, extra_style=paraStyle)
674                     if isinstance(fl,list):
675                         flow  += fl
676                     else:
677                         flow.append( fl )
678
679                 if not len(flow):
680                     flow = self._textual(td)
681                 data2.append( flow )
682             if len(data2)>length:
683                 length=len(data2)
684                 for ab in data:
685                     while len(ab)<length:
686                         ab.append('')
687             while len(data2)<length:
688                 data2.append('')
689             data.append( data2 )
690             posy += 1
691
692         if node.get('colWidths'):
693             assert length == len(node.get('colWidths').split(','))
694             colwidths = [utils.unit_get(f.strip()) for f in node.get('colWidths').split(',')]
695         if node.get('rowHeights'):
696             rowheights = [utils.unit_get(f.strip()) for f in node.get('rowHeights').split(',')]
697             if len(rowheights) == 1:
698                 rowheights = rowheights[0]
699         table = platypus.LongTable(data = data, colWidths=colwidths, rowHeights=rowheights, **(utils.attr_get(node, ['splitByRow'] ,{'repeatRows':'int','repeatCols':'int'})))
700         if node.get('style'):
701             table.setStyle(self.styles.table_styles[node.get('style')])
702         for s in styles:
703             table.setStyle(s)
704         return table
705
706     def _illustration(self, node):
707         return _rml_Illustration(node, self.localcontext, self.styles, self)
708
709     def _textual_image(self, node):
710         return base64.decodestring(node.text)
711
712     def _pto(self, node):
713         sub_story = []
714         pto_header = None
715         pto_trailer = None
716         for node in utils._child_get(node, self):
717             if node.tag == etree.Comment:
718                 node.text = ''
719                 continue
720             elif node.tag=='pto_header':
721                 pto_header = self.render(node)
722             elif node.tag=='pto_trailer':
723                 pto_trailer = self.render(node)
724             else:
725                 flow = self._flowable(node)
726                 if flow:
727                     if isinstance(flow,list):
728                         sub_story = sub_story + flow
729                     else:
730                         sub_story.append(flow)
731         return platypus.flowables.PTOContainer(sub_story, trailer=pto_trailer, header=pto_header)
732
733     def _flowable(self, node, extra_style=None):
734         if node.tag=='pto':
735             return self._pto(node)
736         if node.tag=='para':
737             style = self.styles.para_style_get(node)
738             if extra_style:
739                 style.__dict__.update(extra_style)
740             result = []
741             for i in self._textual(node).split('\n'):
742                 result.append(platypus.Paragraph(i, style, **(utils.attr_get(node, [], {'bulletText':'str'}))))
743             return result
744         elif node.tag=='barCode':
745             try:
746                 from reportlab.graphics.barcode import code128
747                 from reportlab.graphics.barcode import code39
748                 from reportlab.graphics.barcode import code93
749                 from reportlab.graphics.barcode import common
750                 from reportlab.graphics.barcode import fourstate
751                 from reportlab.graphics.barcode import usps
752                 from reportlab.graphics.barcode import createBarcodeDrawing
753
754             except ImportError:
755                 _logger.warning("Cannot use barcode renderers:", exc_info=True)
756                 return None
757             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'})
758             codes = {
759                 'codabar': lambda x: common.Codabar(x, **args),
760                 'code11': lambda x: common.Code11(x, **args),
761                 'code128': lambda x: code128.Code128(str(x), **args),
762                 'standard39': lambda x: code39.Standard39(str(x), **args),
763                 'standard93': lambda x: code93.Standard93(str(x), **args),
764                 'i2of5': lambda x: common.I2of5(x, **args),
765                 'extended39': lambda x: code39.Extended39(str(x), **args),
766                 'extended93': lambda x: code93.Extended93(str(x), **args),
767                 'msi': lambda x: common.MSI(x, **args),
768                 'fim': lambda x: usps.FIM(x, **args),
769                 'postnet': lambda x: usps.POSTNET(x, **args),
770                 'ean13': lambda x: createBarcodeDrawing('EAN13', value=str(x), **args),
771                 'qrcode': lambda x: createBarcodeDrawing('QR', value=x, **args),
772             }
773             code = 'code128'
774             if node.get('code'):
775                 code = node.get('code').lower()
776             return codes[code](self._textual(node))
777         elif node.tag=='name':
778             self.styles.names[ node.get('id')] = node.get('value')
779             return None
780         elif node.tag=='xpre':
781             style = self.styles.para_style_get(node)
782             return platypus.XPreformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int','frags':'int'})))
783         elif node.tag=='pre':
784             style = self.styles.para_style_get(node)
785             return platypus.Preformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int'})))
786         elif node.tag=='illustration':
787             return  self._illustration(node)
788         elif node.tag=='blockTable':
789             return  self._table(node)
790         elif node.tag=='title':
791             styles = reportlab.lib.styles.getSampleStyleSheet()
792             style = styles['Title']
793             return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
794         elif re.match('^h([1-9]+[0-9]*)$', (node.tag or '')):
795             styles = reportlab.lib.styles.getSampleStyleSheet()
796             style = styles['Heading'+str(node.tag[1:])]
797             return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
798         elif node.tag=='image':
799             image_data = False
800             if not node.get('file'):
801                 if node.get('name'):
802                     if node.get('name') in self.doc.images:
803                         _logger.debug("Image %s read ", node.get('name'))
804                         image_data = self.doc.images[node.get('name')].read()
805                     else:
806                         _logger.warning("Image %s not defined", node.get('name'))
807                         return False
808                 else:
809                     import base64
810                     newtext = node.text
811                     if self.localcontext:
812                         newtext = utils._process_text(self, node.text or '')
813                     image_data = base64.decodestring(newtext)
814                 if not image_data:
815                     _logger.debug("No inline image data")
816                     return False
817                 image = StringIO(image_data)
818             else:
819                 _logger.debug("Image get from file %s", node.get('file'))
820                 image = _open_image(node.get('file'), path=self.doc.path)
821             return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
822         elif node.tag=='spacer':
823             if node.get('width'):
824                 width = utils.unit_get(node.get('width'))
825             else:
826                 width = utils.unit_get('1cm')
827             length = utils.unit_get(node.get('length'))
828             return platypus.Spacer(width=width, height=length)
829         elif node.tag=='section':
830             return self.render(node)
831         elif node.tag == 'pageNumberReset':
832             return PageReset()
833         elif node.tag in ('pageBreak', 'nextPage'):
834             return platypus.PageBreak()
835         elif node.tag=='condPageBreak':
836             return platypus.CondPageBreak(**(utils.attr_get(node, ['height'])))
837         elif node.tag=='setNextTemplate':
838             return platypus.NextPageTemplate(str(node.get('name')))
839         elif node.tag=='nextFrame':
840             return platypus.CondPageBreak(1000)           # TODO: change the 1000 !
841         elif node.tag == 'setNextFrame':
842             from reportlab.platypus.doctemplate import NextFrameFlowable
843             return NextFrameFlowable(str(node.get('name')))
844         elif node.tag == 'currentFrame':
845             from reportlab.platypus.doctemplate import CurrentFrameFlowable
846             return CurrentFrameFlowable(str(node.get('name')))
847         elif node.tag == 'frameEnd':
848             return EndFrameFlowable()
849         elif node.tag == 'hr':
850             width_hr=node.get('width') or '100%'
851             color_hr=node.get('color')  or 'black'
852             thickness_hr=node.get('thickness') or 1
853             lineCap_hr=node.get('lineCap') or 'round'
854             return platypus.flowables.HRFlowable(width=width_hr,color=color.get(color_hr),thickness=float(thickness_hr),lineCap=str(lineCap_hr))
855         else:
856             sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.tag,))
857             return None
858
859     def render(self, node_story):
860         def process_story(node_story):
861             sub_story = []
862             for node in utils._child_get(node_story, self):
863                 if node.tag == etree.Comment:
864                     node.text = ''
865                     continue
866                 flow = self._flowable(node)
867                 if flow:
868                     if isinstance(flow,list):
869                         sub_story = sub_story + flow
870                     else:
871                         sub_story.append(flow)
872             return sub_story
873         return process_story(node_story)
874
875
876 class EndFrameFlowable(ActionFlowable):
877     def __init__(self,resume=0):
878         ActionFlowable.__init__(self,('frameEnd',resume))
879
880 class TinyDocTemplate(platypus.BaseDocTemplate):
881     def ___handle_pageBegin(self):
882         self.page = self.page + 1
883         self.pageTemplate.beforeDrawPage(self.canv,self)
884         self.pageTemplate.checkPageSize(self.canv,self)
885         self.pageTemplate.onPage(self.canv,self)
886         for f in self.pageTemplate.frames: f._reset()
887         self.beforePage()
888         self._curPageFlowableCount = 0
889         if hasattr(self,'_nextFrameIndex'):
890             del self._nextFrameIndex
891         for f in self.pageTemplate.frames:
892             if f.id == 'first':
893                 self.frame = f
894                 break
895         self.handle_frameBegin()
896     def afterFlowable(self, flowable):
897         if isinstance(flowable, PageReset):
898             self.canv._pageCount=self.page
899             self.page=0
900             self.canv._flag=True
901             self.canv._pageNumber = 0
902
903 class _rml_template(object):
904     def __init__(self, localcontext, out, node, doc, images=None, path='.', title=None):
905         if images is None:
906             images = {}
907         if not localcontext:
908             localcontext={'internal_header':True}
909         self.localcontext = localcontext
910         self.images= images
911         self.path = path
912         self.title = title
913
914         pagesize_map = {'a4': A4,
915                     'us_letter': letter
916                     }
917         pageSize = A4
918         if self.localcontext.get('company'):
919             pageSize = pagesize_map.get(self.localcontext.get('company').paper_format, A4)
920         if node.get('pageSize'):
921             ps = map(lambda x:x.strip(), node.get('pageSize').replace(')', '').replace('(', '').split(','))
922             pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
923
924         self.doc_tmpl = TinyDocTemplate(out, pagesize=pageSize, **utils.attr_get(node, ['leftMargin','rightMargin','topMargin','bottomMargin'], {'allowSplitting':'int','showBoundary':'bool','rotation':'int','title':'str','author':'str'}))
925         self.page_templates = []
926         self.styles = doc.styles
927         self.doc = doc
928         self.image=[]
929         pts = node.findall('pageTemplate')
930         for pt in pts:
931             frames = []
932             for frame_el in pt.findall('frame'):
933                 frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) )
934                 if utils.attr_get(frame_el, ['last']):
935                     frame.lastFrame = True
936                 frames.append( frame )
937             try :
938                 gr = pt.findall('pageGraphics')\
939                     or pt[1].findall('pageGraphics')
940             except Exception: # FIXME: be even more specific, perhaps?
941                 gr=''
942             if len(gr):
943 #                self.image=[ n for n in utils._child_get(gr[0], self) if n.tag=='image' or not self.localcontext]
944                 drw = _rml_draw(self.localcontext,gr[0], self.doc, images=images, path=self.path, title=self.title)
945                 self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
946             else:
947                 drw = _rml_draw(self.localcontext,node,self.doc,title=self.title)
948                 self.page_templates.append( platypus.PageTemplate(frames=frames,onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
949         self.doc_tmpl.addPageTemplates(self.page_templates)
950
951     def render(self, node_stories):
952         if self.localcontext and not self.localcontext.get('internal_header',False):
953             del self.localcontext['internal_header']
954         fis = []
955         r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title)
956         story_cnt = 0
957         for node_story in node_stories:
958             if story_cnt > 0:
959                 fis.append(platypus.PageBreak())
960             fis += r.render(node_story)
961             # Reset Page Number with new story tag
962             fis.append(PageReset())
963             story_cnt += 1
964         if self.localcontext and self.localcontext.get('internal_header',False):
965             self.doc_tmpl.afterFlowable(fis)
966             self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas)
967         else:
968             fis.append(PageCount())
969             self.doc_tmpl.build(fis)
970
971 def parseNode(rml, localcontext=None, fout=None, images=None, path='.', title=None):
972     node = etree.XML(rml)
973     r = _rml_doc(node, localcontext, images, path, title=title)
974     #try to override some font mappings
975     try:
976         from customfonts import SetCustomFonts
977         SetCustomFonts(r)
978     except ImportError:
979         # means there is no custom fonts mapping in this system.
980         pass
981     except Exception:
982         _logger.warning('Cannot set font mapping', exc_info=True)
983         pass
984     fp = StringIO()
985     r.render(fp)
986     return fp.getvalue()
987
988 def parseString(rml, localcontext=None, fout=None, images=None, path='.', title=None):
989     node = etree.XML(rml)
990     r = _rml_doc(node, localcontext, images, path, title=title)
991
992     #try to override some font mappings
993     try:
994         from customfonts import SetCustomFonts
995         SetCustomFonts(r)
996     except Exception:
997         pass
998
999     if fout:
1000         fp = file(fout,'wb')
1001         r.render(fp)
1002         fp.close()
1003         return fout
1004     else:
1005         fp = StringIO()
1006         r.render(fp)
1007         return fp.getvalue()
1008
1009 def trml2pdf_help():
1010     print 'Usage: trml2pdf input.rml >output.pdf'
1011     print 'Render the standard input (RML) and output a PDF file'
1012     sys.exit(0)
1013
1014 if __name__=="__main__":
1015     if len(sys.argv)>1:
1016         if sys.argv[1]=='--help':
1017             trml2pdf_help()
1018         print parseString(file(sys.argv[1], 'r').read()),
1019     else:
1020         print 'Usage: trml2pdf input.rml >output.pdf'
1021         print 'Try \'trml2pdf --help\' for more information.'
1022
1023
1024 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: