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