[FIX] rml: avoid reports ending by zero (opw 608073)
[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         self.canvas.drawString(text=text, **v)
377
378     def _drawCenteredString(self, node):
379         v = utils.attr_get(node, ['x','y'])
380         text=self._textual(node, **v)
381         text = utils.xml2str(text)
382         self.canvas.drawCentredString(text=text, **v)
383
384     def _drawRightString(self, node):
385         v = utils.attr_get(node, ['x','y'])
386         text=self._textual(node, **v)
387         text = utils.xml2str(text)
388         self.canvas.drawRightString(text=text, **v)
389
390     def _rect(self, node):
391         if node.get('round'):
392             self.canvas.roundRect(radius=utils.unit_get(node.get('round')), **utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
393         else:
394             self.canvas.rect(**utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
395
396     def _ellipse(self, node):
397         x1 = utils.unit_get(node.get('x'))
398         x2 = utils.unit_get(node.get('width'))
399         y1 = utils.unit_get(node.get('y'))
400         y2 = utils.unit_get(node.get('height'))
401
402         self.canvas.ellipse(x1,y1,x2,y2, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
403
404     def _curves(self, node):
405         line_str = node.text.split()
406         lines = []
407         while len(line_str)>7:
408             self.canvas.bezier(*[utils.unit_get(l) for l in line_str[0:8]])
409             line_str = line_str[8:]
410
411     def _lines(self, node):
412         line_str = node.text.split()
413         lines = []
414         while len(line_str)>3:
415             lines.append([utils.unit_get(l) for l in line_str[0:4]])
416             line_str = line_str[4:]
417         self.canvas.lines(lines)
418
419     def _grid(self, node):
420         xlist = [utils.unit_get(s) for s in node.get('xs').split(',')]
421         ylist = [utils.unit_get(s) for s in node.get('ys').split(',')]
422
423         self.canvas.grid(xlist, ylist)
424
425     def _translate(self, node):
426         dx = utils.unit_get(node.get('dx')) or 0
427         dy = utils.unit_get(node.get('dy')) or 0
428         self.canvas.translate(dx,dy)
429
430     def _circle(self, node):
431         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'}))
432
433     def _place(self, node):
434         flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title, canvas=self.canvas).render(node)
435         infos = utils.attr_get(node, ['x','y','width','height'])
436
437         infos['y']+=infos['height']
438         for flow in flows:
439             w,h = flow.wrap(infos['width'], infos['height'])
440             if w<=infos['width'] and h<=infos['height']:
441                 infos['y']-=h
442                 flow.drawOn(self.canvas,infos['x'],infos['y'])
443                 infos['height']-=h
444             else:
445                 raise ValueError("Not enough space")
446
447     def _line_mode(self, node):
448         ljoin = {'round':1, 'mitered':0, 'bevelled':2}
449         lcap = {'default':0, 'round':1, 'square':2}
450
451         if node.get('width'):
452             self.canvas.setLineWidth(utils.unit_get(node.get('width')))
453         if node.get('join'):
454             self.canvas.setLineJoin(ljoin[node.get('join')])
455         if node.get('cap'):
456             self.canvas.setLineCap(lcap[node.get('cap')])
457         if node.get('miterLimit'):
458             self.canvas.setDash(utils.unit_get(node.get('miterLimit')))
459         if node.get('dash'):
460             dashes = node.get('dash').split(',')
461             for x in range(len(dashes)):
462                 dashes[x]=utils.unit_get(dashes[x])
463             self.canvas.setDash(node.get('dash').split(','))
464
465     def _image(self, node):
466         import urllib
467         import urlparse
468         from reportlab.lib.utils import ImageReader
469         nfile = node.get('file')
470         if not nfile:
471             if node.get('name'):
472                 image_data = self.images[node.get('name')]
473                 _logger.debug("Image %s used", node.get('name'))
474                 s = StringIO(image_data)
475             else:
476                 newtext = node.text
477                 if self.localcontext:
478                     res = utils._regex.findall(newtext)
479                     for key in res:
480                         newtext = eval(key, {}, self.localcontext) or ''
481                 image_data = None
482                 if newtext:
483                     image_data = base64.decodestring(newtext)
484                 if image_data:
485                     s = StringIO(image_data)
486                 else:
487                     _logger.debug("No image data!")
488                     return False
489         else:
490             if nfile in self.images:
491                 s = StringIO(self.images[nfile])
492             else:
493                 try:
494                     up = urlparse.urlparse(str(nfile))
495                 except ValueError:
496                     up = False
497                 if up and up.scheme:
498                     # RFC: do we really want to open external URLs?
499                     # Are we safe from cross-site scripting or attacks?
500                     _logger.debug("Retrieve image from %s", nfile)
501                     u = urllib.urlopen(str(nfile))
502                     s = StringIO(u.read())
503                 else:
504                     _logger.debug("Open image file %s ", nfile)
505                     s = _open_image(nfile, path=self.path)
506         try:
507             img = ImageReader(s)
508             (sx,sy) = img.getSize()
509             _logger.debug("Image is %dx%d", sx, sy)
510             args = { 'x': 0.0, 'y': 0.0, 'mask': 'auto'}
511             for tag in ('width','height','x','y'):
512                 if node.get(tag):
513                     args[tag] = utils.unit_get(node.get(tag))
514             if ('width' in args) and (not 'height' in args):
515                 args['height'] = sy * args['width'] / sx
516             elif ('height' in args) and (not 'width' in args):
517                 args['width'] = sx * args['height'] / sy
518             elif ('width' in args) and ('height' in args):
519                 if (float(args['width'])/args['height'])>(float(sx)>sy):
520                     args['width'] = sx * args['height'] / sy
521                 else:
522                     args['height'] = sy * args['width'] / sx
523             self.canvas.drawImage(img, **args)
524         finally:
525             s.close()
526 #        self.canvas._doc.SaveToFile(self.canvas._filename, self.canvas)
527
528     def _path(self, node):
529         self.path = self.canvas.beginPath()
530         self.path.moveTo(**utils.attr_get(node, ['x','y']))
531         for n in utils._child_get(node, self):
532             if not n.text :
533                 if n.tag=='moveto':
534                     vals = utils.text_get(n).split()
535                     self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1]))
536                 elif n.tag=='curvesto':
537                     vals = utils.text_get(n).split()
538                     while len(vals)>5:
539                         pos=[]
540                         while len(pos)<6:
541                             pos.append(utils.unit_get(vals.pop(0)))
542                         self.path.curveTo(*pos)
543             elif n.text:
544                 data = n.text.split()               # Not sure if I must merge all TEXT_NODE ?
545                 while len(data)>1:
546                     x = utils.unit_get(data.pop(0))
547                     y = utils.unit_get(data.pop(0))
548                     self.path.lineTo(x,y)
549         if (not node.get('close')) or utils.bool_get(node.get('close')):
550             self.path.close()
551         self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
552
553     def setFont(self, node):
554         fontname = select_fontname(node.get('name'), self.canvas._fontname)
555         return self.canvas.setFont(fontname, utils.unit_get(node.get('size')))
556
557     def render(self, node):
558         tags = {
559             'drawCentredString': self._drawCenteredString,
560             'drawRightString': self._drawRightString,
561             'drawString': self._drawString,
562             'rect': self._rect,
563             'ellipse': self._ellipse,
564             'lines': self._lines,
565             'grid': self._grid,
566             'curves': self._curves,
567             'fill': lambda node: self.canvas.setFillColor(color.get(node.get('color'))),
568             'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.get('color'))),
569             'setFont': self.setFont ,
570             'place': self._place,
571             'circle': self._circle,
572             'lineMode': self._line_mode,
573             'path': self._path,
574             'rotate': lambda node: self.canvas.rotate(float(node.get('degrees'))),
575             'translate': self._translate,
576             'image': self._image
577         }
578         for n in utils._child_get(node, self):
579             if n.tag in tags:
580                 tags[n.tag](n)
581
582 class _rml_draw(object):
583     def __init__(self, localcontext, node, styles, images=None, path='.', title=None):
584         if images is None:
585             images = {}
586         self.localcontext = localcontext
587         self.node = node
588         self.styles = styles
589         self.canvas = None
590         self.images = images
591         self.path = path
592         self.canvas_title = title
593
594     def render(self, canvas, doc):
595         canvas.saveState()
596         cnv = _rml_canvas(canvas, self.localcontext, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title)
597         cnv.render(self.node)
598         canvas.restoreState()
599
600 class _rml_Illustration(platypus.flowables.Flowable):
601     def __init__(self, node, localcontext, styles, self2):
602         self.localcontext = (localcontext or {}).copy()
603         self.node = node
604         self.styles = styles
605         self.width = utils.unit_get(node.get('width'))
606         self.height = utils.unit_get(node.get('height'))
607         self.self2 = self2
608     def wrap(self, *args):
609         return self.width, self.height
610     def draw(self):
611         drw = _rml_draw(self.localcontext ,self.node,self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title)
612         drw.render(self.canv, None)
613
614 # Workaround for issue #15: https://bitbucket.org/rptlab/reportlab/issue/15/infinite-pages-produced-when-splitting 
615 original_pto_split = platypus.flowables.PTOContainer.split
616 def split(self, availWidth, availHeight):
617     res = original_pto_split(self, availWidth, availHeight)
618     if len(res) > 2 and len(self._content) > 0:
619         header = self._content[0]._ptoinfo.header
620         trailer = self._content[0]._ptoinfo.trailer
621         if isinstance(res[-2], platypus.flowables.UseUpSpace) and len(header + trailer) == len(res[:-2]):
622             return []
623     return res
624 platypus.flowables.PTOContainer.split = split
625
626 class _rml_flowable(object):
627     def __init__(self, doc, localcontext, images=None, path='.', title=None, canvas=None):
628         if images is None:
629             images = {}
630         self.localcontext = localcontext
631         self.doc = doc
632         self.styles = doc.styles
633         self.images = images
634         self.path = path
635         self.title = title
636         self.canvas = canvas
637
638     def _textual(self, node):
639         rc1 = utils._process_text(self, node.text or '')
640         for n in utils._child_get(node,self):
641             txt_n = copy.deepcopy(n)
642             for key in txt_n.attrib.keys():
643                 if key in ('rml_except', 'rml_loop', 'rml_tag'):
644                     del txt_n.attrib[key]
645             if not n.tag == 'bullet':
646                 if n.tag == 'pageNumber': 
647                     txt_n.text = self.canvas and str(self.canvas.getPageNumber()) or ''
648                 else:
649                     txt_n.text = utils.xml2str(self._textual(n))
650             txt_n.tail = n.tail and utils.xml2str(utils._process_text(self, n.tail.replace('\n',''))) or ''
651             rc1 += etree.tostring(txt_n)
652         return rc1
653
654     def _table(self, node):
655         children = utils._child_get(node,self,'tr')
656         if not children:
657             return None
658         length = 0
659         colwidths = None
660         rowheights = None
661         data = []
662         styles = []
663         posy = 0
664         for tr in children:
665             paraStyle = None
666             if tr.get('style'):
667                 st = copy.deepcopy(self.styles.table_styles[tr.get('style')])
668                 for si in range(len(st._cmds)):
669                     s = list(st._cmds[si])
670                     s[1] = (s[1][0],posy)
671                     s[2] = (s[2][0],posy)
672                     st._cmds[si] = tuple(s)
673                 styles.append(st)
674             if tr.get('paraStyle'):
675                 paraStyle = self.styles.styles[tr.get('paraStyle')]
676             data2 = []
677             posx = 0
678             for td in utils._child_get(tr, self,'td'):
679                 if td.get('style'):
680                     st = copy.deepcopy(self.styles.table_styles[td.get('style')])
681                     for s in st._cmds:
682                         s[1][1] = posy
683                         s[2][1] = posy
684                         s[1][0] = posx
685                         s[2][0] = posx
686                     styles.append(st)
687                 if td.get('paraStyle'):
688                     # TODO: merge styles
689                     paraStyle = self.styles.styles[td.get('paraStyle')]
690                 posx += 1
691
692                 flow = []
693                 for n in utils._child_get(td, self):
694                     if n.tag == etree.Comment:
695                         n.text = ''
696                         continue
697                     fl = self._flowable(n, extra_style=paraStyle)
698                     if isinstance(fl,list):
699                         flow  += fl
700                     else:
701                         flow.append( fl )
702
703                 if not len(flow):
704                     flow = self._textual(td)
705                 data2.append( flow )
706             if len(data2)>length:
707                 length=len(data2)
708                 for ab in data:
709                     while len(ab)<length:
710                         ab.append('')
711             while len(data2)<length:
712                 data2.append('')
713             data.append( data2 )
714             posy += 1
715
716         if node.get('colWidths'):
717             assert length == len(node.get('colWidths').split(','))
718             colwidths = [utils.unit_get(f.strip()) for f in node.get('colWidths').split(',')]
719         if node.get('rowHeights'):
720             rowheights = [utils.unit_get(f.strip()) for f in node.get('rowHeights').split(',')]
721             if len(rowheights) == 1:
722                 rowheights = rowheights[0]
723         table = platypus.LongTable(data = data, colWidths=colwidths, rowHeights=rowheights, **(utils.attr_get(node, ['splitByRow'] ,{'repeatRows':'int','repeatCols':'int'})))
724         if node.get('style'):
725             table.setStyle(self.styles.table_styles[node.get('style')])
726         for s in styles:
727             table.setStyle(s)
728         return table
729
730     def _illustration(self, node):
731         return _rml_Illustration(node, self.localcontext, self.styles, self)
732
733     def _textual_image(self, node):
734         return base64.decodestring(node.text)
735
736     def _pto(self, node):
737         sub_story = []
738         pto_header = None
739         pto_trailer = None
740         for node in utils._child_get(node, self):
741             if node.tag == etree.Comment:
742                 node.text = ''
743                 continue
744             elif node.tag=='pto_header':
745                 pto_header = self.render(node)
746             elif node.tag=='pto_trailer':
747                 pto_trailer = self.render(node)
748             else:
749                 flow = self._flowable(node)
750                 if flow:
751                     if isinstance(flow,list):
752                         sub_story = sub_story + flow
753                     else:
754                         sub_story.append(flow)
755         return platypus.flowables.PTOContainer(sub_story, trailer=pto_trailer, header=pto_header)
756
757     def _flowable(self, node, extra_style=None):
758         if node.tag=='pto':
759             return self._pto(node)
760         if node.tag=='para':
761             style = self.styles.para_style_get(node)
762             if extra_style:
763                 style.__dict__.update(extra_style)
764             result = []
765             for i in self._textual(node).split('\n'):
766                 result.append(platypus.Paragraph(i, style, **(utils.attr_get(node, [], {'bulletText':'str'}))))
767             return result
768         elif node.tag=='barCode':
769             try:
770                 from reportlab.graphics.barcode import code128
771                 from reportlab.graphics.barcode import code39
772                 from reportlab.graphics.barcode import code93
773                 from reportlab.graphics.barcode import common
774                 from reportlab.graphics.barcode import fourstate
775                 from reportlab.graphics.barcode import usps
776                 from reportlab.graphics.barcode import createBarcodeDrawing
777
778             except ImportError:
779                 _logger.warning("Cannot use barcode renderers:", exc_info=True)
780                 return None
781             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'})
782             codes = {
783                 'codabar': lambda x: common.Codabar(x, **args),
784                 'code11': lambda x: common.Code11(x, **args),
785                 'code128': lambda x: code128.Code128(str(x), **args),
786                 'standard39': lambda x: code39.Standard39(str(x), **args),
787                 'standard93': lambda x: code93.Standard93(str(x), **args),
788                 'i2of5': lambda x: common.I2of5(x, **args),
789                 'extended39': lambda x: code39.Extended39(str(x), **args),
790                 'extended93': lambda x: code93.Extended93(str(x), **args),
791                 'msi': lambda x: common.MSI(x, **args),
792                 'fim': lambda x: usps.FIM(x, **args),
793                 'postnet': lambda x: usps.POSTNET(x, **args),
794                 'ean13': lambda x: createBarcodeDrawing('EAN13', value=str(x), **args),
795                 'qrcode': lambda x: createBarcodeDrawing('QR', value=x, **args),
796             }
797             code = 'code128'
798             if node.get('code'):
799                 code = node.get('code').lower()
800             return codes[code](self._textual(node))
801         elif node.tag=='name':
802             self.styles.names[ node.get('id')] = node.get('value')
803             return None
804         elif node.tag=='xpre':
805             style = self.styles.para_style_get(node)
806             return platypus.XPreformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int','frags':'int'})))
807         elif node.tag=='pre':
808             style = self.styles.para_style_get(node)
809             return platypus.Preformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int'})))
810         elif node.tag=='illustration':
811             return  self._illustration(node)
812         elif node.tag=='blockTable':
813             return  self._table(node)
814         elif node.tag=='title':
815             styles = reportlab.lib.styles.getSampleStyleSheet()
816             style = styles['Title']
817             return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
818         elif re.match('^h([1-9]+[0-9]*)$', (node.tag or '')):
819             styles = reportlab.lib.styles.getSampleStyleSheet()
820             style = styles['Heading'+str(node.tag[1:])]
821             return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
822         elif node.tag=='image':
823             image_data = False
824             if not node.get('file'):
825                 if node.get('name'):
826                     if node.get('name') in self.doc.images:
827                         _logger.debug("Image %s read ", node.get('name'))
828                         image_data = self.doc.images[node.get('name')].read()
829                     else:
830                         _logger.warning("Image %s not defined", node.get('name'))
831                         return False
832                 else:
833                     import base64
834                     newtext = node.text
835                     if self.localcontext:
836                         newtext = utils._process_text(self, node.text or '')
837                     image_data = base64.decodestring(newtext)
838                 if not image_data:
839                     _logger.debug("No inline image data")
840                     return False
841                 image = StringIO(image_data)
842             else:
843                 _logger.debug("Image get from file %s", node.get('file'))
844                 image = _open_image(node.get('file'), path=self.doc.path)
845             return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
846         elif node.tag=='spacer':
847             if node.get('width'):
848                 width = utils.unit_get(node.get('width'))
849             else:
850                 width = utils.unit_get('1cm')
851             length = utils.unit_get(node.get('length'))
852             return platypus.Spacer(width=width, height=length)
853         elif node.tag=='section':
854             return self.render(node)
855         elif node.tag == 'pageNumberReset':
856             return PageReset()
857         elif node.tag in ('pageBreak', 'nextPage'):
858             return platypus.PageBreak()
859         elif node.tag=='condPageBreak':
860             return platypus.CondPageBreak(**(utils.attr_get(node, ['height'])))
861         elif node.tag=='setNextTemplate':
862             return platypus.NextPageTemplate(str(node.get('name')))
863         elif node.tag=='nextFrame':
864             return platypus.CondPageBreak(1000)           # TODO: change the 1000 !
865         elif node.tag == 'setNextFrame':
866             from reportlab.platypus.doctemplate import NextFrameFlowable
867             return NextFrameFlowable(str(node.get('name')))
868         elif node.tag == 'currentFrame':
869             from reportlab.platypus.doctemplate import CurrentFrameFlowable
870             return CurrentFrameFlowable(str(node.get('name')))
871         elif node.tag == 'frameEnd':
872             return EndFrameFlowable()
873         elif node.tag == 'hr':
874             width_hr=node.get('width') or '100%'
875             color_hr=node.get('color')  or 'black'
876             thickness_hr=node.get('thickness') or 1
877             lineCap_hr=node.get('lineCap') or 'round'
878             return platypus.flowables.HRFlowable(width=width_hr,color=color.get(color_hr),thickness=float(thickness_hr),lineCap=str(lineCap_hr))
879         else:
880             sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.tag,))
881             return None
882
883     def render(self, node_story):
884         def process_story(node_story):
885             sub_story = []
886             for node in utils._child_get(node_story, self):
887                 if node.tag == etree.Comment:
888                     node.text = ''
889                     continue
890                 flow = self._flowable(node)
891                 if flow:
892                     if isinstance(flow,list):
893                         sub_story = sub_story + flow
894                     else:
895                         sub_story.append(flow)
896             return sub_story
897         return process_story(node_story)
898
899
900 class EndFrameFlowable(ActionFlowable):
901     def __init__(self,resume=0):
902         ActionFlowable.__init__(self,('frameEnd',resume))
903
904 class TinyDocTemplate(platypus.BaseDocTemplate):
905
906     def beforeDocument(self):
907         # Store some useful value directly inside canvas, so it's available
908         # on flowable drawing (needed for proper PageCount handling)
909         self.canv._doPageReset = False
910         self.canv._storyCount = 0
911
912     def ___handle_pageBegin(self):
913         self.page += 1
914         self.pageTemplate.beforeDrawPage(self.canv,self)
915         self.pageTemplate.checkPageSize(self.canv,self)
916         self.pageTemplate.onPage(self.canv,self)
917         for f in self.pageTemplate.frames: f._reset()
918         self.beforePage()
919         self._curPageFlowableCount = 0
920         if hasattr(self,'_nextFrameIndex'):
921             del self._nextFrameIndex
922         for f in self.pageTemplate.frames:
923             if f.id == 'first':
924                 self.frame = f
925                 break
926         self.handle_frameBegin()
927
928     def afterPage(self):
929         if self.canv._doPageReset:
930             # Following a <pageReset/> tag:
931             # - we reset page number to 0
932             # - we add  an new PageCount flowable (relative to the current
933             #   story number), but not for NumeredCanvas at is handle page
934             #   count itself)
935             # NOTE: _rml_template render() method add a PageReset flowable at end
936             #   of each story, so we're sure to pass here at least once per story.
937             if not isinstance(self.canv, NumberedCanvas):
938                 self.handle_flowable([ PageCount(story_count=self.canv._storyCount) ])
939             self.canv._pageCount = self.page
940             self.page = 0
941             self.canv._flag = True
942             self.canv._pageNumber = 0
943             self.canv._doPageReset = False
944             self.canv._storyCount += 1
945
946 class _rml_template(object):
947     def __init__(self, localcontext, out, node, doc, images=None, path='.', title=None):
948         if images is None:
949             images = {}
950         if not localcontext:
951             localcontext={'internal_header':True}
952         self.localcontext = localcontext
953         self.images= images
954         self.path = path
955         self.title = title
956
957         pagesize_map = {'a4': A4,
958                     'us_letter': letter
959                     }
960         pageSize = A4
961         if self.localcontext.get('company'):
962             pageSize = pagesize_map.get(self.localcontext.get('company').paper_format, A4)
963         if node.get('pageSize'):
964             ps = map(lambda x:x.strip(), node.get('pageSize').replace(')', '').replace('(', '').split(','))
965             pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
966
967         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'}))
968         self.page_templates = []
969         self.styles = doc.styles
970         self.doc = doc
971         self.image=[]
972         pts = node.findall('pageTemplate')
973         for pt in pts:
974             frames = []
975             for frame_el in pt.findall('frame'):
976                 frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) )
977                 if utils.attr_get(frame_el, ['last']):
978                     frame.lastFrame = True
979                 frames.append( frame )
980             try :
981                 gr = pt.findall('pageGraphics')\
982                     or pt[1].findall('pageGraphics')
983             except Exception: # FIXME: be even more specific, perhaps?
984                 gr=''
985             if len(gr):
986 #                self.image=[ n for n in utils._child_get(gr[0], self) if n.tag=='image' or not self.localcontext]
987                 drw = _rml_draw(self.localcontext,gr[0], self.doc, images=images, path=self.path, title=self.title)
988                 self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
989             else:
990                 drw = _rml_draw(self.localcontext,node,self.doc,title=self.title)
991                 self.page_templates.append( platypus.PageTemplate(frames=frames,onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
992         self.doc_tmpl.addPageTemplates(self.page_templates)
993
994     def render(self, node_stories):
995         if self.localcontext and not self.localcontext.get('internal_header',False):
996             del self.localcontext['internal_header']
997         fis = []
998         r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title, canvas=None)
999         story_cnt = 0
1000         for node_story in node_stories:
1001             if story_cnt > 0:
1002                 # Reset Page Number with new story tag
1003                 fis.append(PageReset())
1004                 fis.append(platypus.PageBreak())
1005             fis += r.render(node_story)
1006             story_cnt += 1
1007         try:
1008             if self.localcontext and self.localcontext.get('internal_header',False):
1009                 self.doc_tmpl.afterFlowable(fis)
1010                 self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas)
1011             else:
1012                 self.doc_tmpl.build(fis)
1013         except platypus.doctemplate.LayoutError, e:
1014             e.name = 'Print Error'
1015             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.'
1016             raise
1017
1018 def parseNode(rml, localcontext=None, fout=None, images=None, path='.', title=None):
1019     node = etree.XML(rml)
1020     r = _rml_doc(node, localcontext, images, path, title=title)
1021     #try to override some font mappings
1022     try:
1023         from customfonts import SetCustomFonts
1024         SetCustomFonts(r)
1025     except ImportError:
1026         # means there is no custom fonts mapping in this system.
1027         pass
1028     except Exception:
1029         _logger.warning('Cannot set font mapping', exc_info=True)
1030         pass
1031     fp = StringIO()
1032     r.render(fp)
1033     return fp.getvalue()
1034
1035 def parseString(rml, localcontext=None, fout=None, images=None, path='.', title=None):
1036     node = etree.XML(rml)
1037     r = _rml_doc(node, localcontext, images, path, title=title)
1038
1039     #try to override some font mappings
1040     try:
1041         from customfonts import SetCustomFonts
1042         SetCustomFonts(r)
1043     except Exception:
1044         pass
1045
1046     if fout:
1047         fp = file(fout,'wb')
1048         r.render(fp)
1049         fp.close()
1050         return fout
1051     else:
1052         fp = StringIO()
1053         r.render(fp)
1054         return fp.getvalue()
1055
1056 def trml2pdf_help():
1057     print 'Usage: trml2pdf input.rml >output.pdf'
1058     print 'Render the standard input (RML) and output a PDF file'
1059     sys.exit(0)
1060
1061 if __name__=="__main__":
1062     if len(sys.argv)>1:
1063         if sys.argv[1]=='--help':
1064             trml2pdf_help()
1065         print parseString(file(sys.argv[1], 'r').read()),
1066     else:
1067         print 'Usage: trml2pdf input.rml >output.pdf'
1068         print 'Try \'trml2pdf --help\' for more information.'
1069
1070
1071 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: