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