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