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