1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
27 from reportlab.pdfgen import canvas
28 from reportlab import platypus
33 from lxml import etree
35 from reportlab.platypus.doctemplate import ActionFlowable
36 from tools.safe_eval import safe_eval as eval
37 from reportlab.lib.units import inch,cm,mm
38 from tools.misc import file_open
39 from reportlab.pdfbase import pdfmetrics
42 from cStringIO import StringIO
43 _hush_pyflakes = [ StringIO ]
45 from StringIO import StringIO
49 def _open_image(filename, path=None):
50 """Attempt to open a binary file and return the descriptor
52 if os.path.isfile(filename):
53 return open(filename, 'rb')
54 for p in (path or []):
55 if p and os.path.isabs(p):
56 fullpath = os.path.join(p, filename)
57 if os.path.isfile(fullpath):
58 return open(fullpath, 'rb')
61 fullpath = os.path.join(p, filename)
64 return file_open(fullpath)
67 raise IOError("File %s cannot be found in image path" % filename)
69 class NumberedCanvas(canvas.Canvas):
70 def __init__(self, *args, **kwargs):
71 canvas.Canvas.__init__(self, *args, **kwargs)
84 self.pages.update({self._currentPage:self._pageCount})
85 self._codes.append({'code': self._code, 'stack': self._codeStack})
90 if self.pages.get(self._pageCounter,False):
94 if not self.pages.get(key,False):
95 while not self.pages.get(key,False):
97 self.setFont("Helvetica", 8)
98 self.drawRightString((self._pagesize[0]-30), (self._pagesize[1]-40),
99 "Page %(this)i of %(total)i" % {
100 'this': self._pageNumber+1,
101 'total': self.pages.get(key,False),
106 """add page info to each page (page x of y)"""
109 for code in self._codes:
110 self._code = code['code']
111 self._codeStack = code['stack']
113 canvas.Canvas.showPage(self)
114 # self.restoreState()
115 self._doc.SaveToFile(self._filename, self)
117 class PageCount(platypus.Flowable):
119 self.canv.beginForm("pageCount")
120 self.canv.setFont("Helvetica", utils.unit_get(str(8)))
121 self.canv.drawString(0, 0, str(self.canv.getPageNumber()))
124 class PageReset(platypus.Flowable):
126 self.canv._pageNumber = 0
128 class _rml_styles(object,):
129 def __init__(self, nodes, localcontext):
130 self.localcontext = localcontext
134 self.table_styles = {}
135 self.default_style = reportlab.lib.styles.getSampleStyleSheet()
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)
144 self.styles_obj[sname] = reportlab.lib.styles.ParagraphStyle(sname, self.default_style["Normal"], **self.styles[sname])
146 for variable in node.findall('initialize'):
147 for name in variable.findall('name'):
148 self.names[ name.get('id')] = name.get('value')
150 def _para_style_update(self, node):
152 for attr in ['textColor', 'backColor', 'bulletColor', 'borderColor']:
154 data[attr] = color.get(node.get(attr))
155 for attr in ['fontName', 'bulletFontName', 'bulletText']:
157 data[attr] = node.get(attr)
158 for attr in ['fontSize', 'leftIndent', 'rightIndent', 'spaceBefore', 'spaceAfter',
159 'firstLineIndent', 'bulletIndent', 'bulletFontSize', 'leading',
160 'borderWidth','borderPadding','borderRadius']:
162 data[attr] = utils.unit_get(node.get(attr))
163 if node.get('alignment'):
165 'right':reportlab.lib.enums.TA_RIGHT,
166 'center':reportlab.lib.enums.TA_CENTER,
167 'justify':reportlab.lib.enums.TA_JUSTIFY
169 data['alignment'] = align.get(node.get('alignment').lower(), reportlab.lib.enums.TA_LEFT)
172 def _table_style_get(self, style_node):
174 for node in style_node:
175 start = utils.tuple_int_get(node, 'start', (0,0) )
176 stop = utils.tuple_int_get(node, 'stop', (-1,-1) )
177 if node.tag=='blockValign':
178 styles.append(('VALIGN', start, stop, str(node.get('value'))))
179 elif node.tag=='blockFont':
180 styles.append(('FONT', start, stop, str(node.get('name'))))
181 elif node.tag=='blockTextColor':
182 styles.append(('TEXTCOLOR', start, stop, color.get(str(node.get('colorName')))))
183 elif node.tag=='blockLeading':
184 styles.append(('LEADING', start, stop, utils.unit_get(node.get('length'))))
185 elif node.tag=='blockAlignment':
186 styles.append(('ALIGNMENT', start, stop, str(node.get('value'))))
187 elif node.tag=='blockSpan':
188 styles.append(('SPAN', start, stop))
189 elif node.tag=='blockLeftPadding':
190 styles.append(('LEFTPADDING', start, stop, utils.unit_get(node.get('length'))))
191 elif node.tag=='blockRightPadding':
192 styles.append(('RIGHTPADDING', start, stop, utils.unit_get(node.get('length'))))
193 elif node.tag=='blockTopPadding':
194 styles.append(('TOPPADDING', start, stop, utils.unit_get(node.get('length'))))
195 elif node.tag=='blockBottomPadding':
196 styles.append(('BOTTOMPADDING', start, stop, utils.unit_get(node.get('length'))))
197 elif node.tag=='blockBackground':
198 styles.append(('BACKGROUND', start, stop, color.get(node.get('colorName'))))
200 styles.append(('FONTSIZE', start, stop, utils.unit_get(node.get('size'))))
201 elif node.tag=='lineStyle':
202 kind = node.get('kind')
203 kind_list = [ 'GRID', 'BOX', 'OUTLINE', 'INNERGRID', 'LINEBELOW', 'LINEABOVE','LINEBEFORE', 'LINEAFTER' ]
204 assert kind in kind_list
206 if node.get('thickness'):
207 thick = float(node.get('thickness'))
208 styles.append((kind, start, stop, thick, color.get(node.get('colorName'))))
209 return platypus.tables.TableStyle(styles)
211 def para_style_get(self, node):
213 sname = node.get('style')
215 if sname in self.styles_obj:
216 style = self.styles_obj[sname]
218 sys.stderr.write('Warning: style not found, %s - setting default!\n' % (node.get('style'),) )
220 style = self.default_style['Normal']
221 para_update = self._para_style_update(node)
223 # update style only is necessary
224 style = copy.deepcopy(style)
225 style.__dict__.update(para_update)
228 class _rml_doc(object):
229 def __init__(self, node, localcontext, images={}, path='.', title=None):
230 self.localcontext = localcontext
232 self.filename = self.etree.get('filename')
237 def docinit(self, els):
238 from reportlab.lib.fonts import addMapping
239 from reportlab.pdfbase import pdfmetrics
240 from reportlab.pdfbase.ttfonts import TTFont
243 for font in node.findall('registerFont'):
244 name = font.get('fontName').encode('ascii')
245 fname = font.get('fontFile').encode('ascii')
246 if name not in pdfmetrics._fonts:
247 pdfmetrics.registerFont(TTFont(name, fname ))
248 addMapping(name, 0, 0, name) #normal
249 addMapping(name, 0, 1, name) #italic
250 addMapping(name, 1, 0, name) #bold
251 addMapping(name, 1, 1, name) #italic and bold
253 def setTTFontMapping(self,face, fontname, filename, mode='all'):
254 from reportlab.lib.fonts import addMapping
255 from reportlab.pdfbase import pdfmetrics
256 from reportlab.pdfbase.ttfonts import TTFont
258 if fontname not in pdfmetrics._fonts:
259 pdfmetrics.registerFont(TTFont(name, fname ))
260 pdfmetrics.registerFont(TTFont(fontname, filename ))
262 addMapping(face, 0, 0, fontname) #normal
263 addMapping(face, 0, 1, fontname) #italic
264 addMapping(face, 1, 0, fontname) #bold
265 addMapping(face, 1, 1, fontname) #italic and bold
266 elif (mode== 'normal') or (mode == 'regular'):
267 addMapping(face, 0, 0, fontname) #normal
268 elif (mode == 'italic'):
269 addMapping(face, 0, 1, fontname) #italic
270 elif (mode == 'bold'):
271 addMapping(face, 1, 0, fontname) #bold
272 elif (mode == 'bolditalic'):
273 addMapping(face, 1, 1, fontname) #italic and bold
275 def _textual_image(self, node):
278 rc +=( etree.tostring(n) or '') + n.tail
279 return base64.decodestring(node.tostring())
281 def _images(self, el):
283 for node in el.findall('.//image'):
284 rc =( node.text or '')
285 result[node.get('name')] = base64.decodestring(rc)
288 def render(self, out):
289 el = self.etree.findall('.//docinit')
293 el = self.etree.findall('.//stylesheet')
294 self.styles = _rml_styles(el,self.localcontext)
296 el = self.etree.findall('.//images')
298 self.images.update( self._images(el[0]) )
300 el = self.etree.findall('.//template')
302 pt_obj = _rml_template(self.localcontext, out, el[0], self, images=self.images, path=self.path, title=self.title)
303 el = utils._child_get(self.etree, self, 'story')
306 self.canvas = canvas.Canvas(out)
307 pd = self.etree.find('pageDrawing')[0]
308 pd_obj = _rml_canvas(self.canvas, self.localcontext, None, self, self.images, path=self.path, title=self.title)
311 self.canvas.showPage()
314 class _rml_canvas(object):
315 def __init__(self, canvas, localcontext, doc_tmpl=None, doc=None, images={}, path='.', title=None):
316 self.localcontext = localcontext
318 self.styles = doc.styles
319 self.doc_tmpl = doc_tmpl
324 self._logger = logging.getLogger('report.rml.canvas')
326 self.canvas.setTitle(self.title)
328 def _textual(self, node, x=0, y=0):
329 text = node.text and node.text.encode('utf-8') or ''
330 rc = utils._process_text(self, text)
333 from reportlab.lib.sequencer import getSequencer
335 rc += str(seq.next(n.get('id')))
336 if n.tag == 'pageCount':
338 self.canvas.translate(x,y)
339 self.canvas.doForm('pageCount')
341 self.canvas.translate(-x,-y)
342 if n.tag == 'pageNumber':
343 rc += str(self.canvas.getPageNumber())
344 rc += utils._process_text(self, n.tail)
345 return rc.replace('\n','')
347 def _drawString(self, node):
348 v = utils.attr_get(node, ['x','y'])
349 text=self._textual(node, **v)
350 text = utils.xml2str(text)
351 self.canvas.drawString(text=text, **v)
353 def _drawCenteredString(self, node):
354 v = utils.attr_get(node, ['x','y'])
355 text=self._textual(node, **v)
356 text = utils.xml2str(text)
357 self.canvas.drawCentredString(text=text, **v)
359 def _drawRightString(self, node):
360 v = utils.attr_get(node, ['x','y'])
361 text=self._textual(node, **v)
362 text = utils.xml2str(text)
363 self.canvas.drawRightString(text=text, **v)
365 def _rect(self, node):
366 if node.get('round'):
367 self.canvas.roundRect(radius=utils.unit_get(node.get('round')), **utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
369 self.canvas.rect(**utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
371 def _ellipse(self, node):
372 x1 = utils.unit_get(node.get('x'))
373 x2 = utils.unit_get(node.get('width'))
374 y1 = utils.unit_get(node.get('y'))
375 y2 = utils.unit_get(node.get('height'))
377 self.canvas.ellipse(x1,y1,x2,y2, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
379 def _curves(self, node):
380 line_str = node.text.split()
382 while len(line_str)>7:
383 self.canvas.bezier(*[utils.unit_get(l) for l in line_str[0:8]])
384 line_str = line_str[8:]
386 def _lines(self, node):
387 line_str = node.text.split()
389 while len(line_str)>3:
390 lines.append([utils.unit_get(l) for l in line_str[0:4]])
391 line_str = line_str[4:]
392 self.canvas.lines(lines)
394 def _grid(self, node):
395 xlist = [utils.unit_get(s) for s in node.get('xs').split(',')]
396 ylist = [utils.unit_get(s) for s in node.get('ys').split(',')]
398 self.canvas.grid(xlist, ylist)
400 def _translate(self, node):
401 dx = utils.unit_get(node.get('dx')) or 0
402 dy = utils.unit_get(node.get('dy')) or 0
403 self.canvas.translate(dx,dy)
405 def _circle(self, node):
406 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'}))
408 def _place(self, node):
409 flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title).render(node)
410 infos = utils.attr_get(node, ['x','y','width','height'])
412 infos['y']+=infos['height']
414 w,h = flow.wrap(infos['width'], infos['height'])
415 if w<=infos['width'] and h<=infos['height']:
417 flow.drawOn(self.canvas,infos['x'],infos['y'])
420 raise ValueError, "Not enough space"
422 def _line_mode(self, node):
423 ljoin = {'round':1, 'mitered':0, 'bevelled':2}
424 lcap = {'default':0, 'round':1, 'square':2}
426 if node.get('width'):
427 self.canvas.setLineWidth(utils.unit_get(node.get('width')))
429 self.canvas.setLineJoin(ljoin[node.get('join')])
431 self.canvas.setLineCap(lcap[node.get('cap')])
432 if node.get('miterLimit'):
433 self.canvas.setDash(utils.unit_get(node.get('miterLimit')))
435 dashes = node.get('dash').split(',')
436 for x in range(len(dashes)):
437 dashes[x]=utils.unit_get(dashes[x])
438 self.canvas.setDash(node.get('dash').split(','))
440 def _image(self, node):
443 from reportlab.lib.utils import ImageReader
444 nfile = node.get('file')
447 image_data = self.images[node.get('name')]
448 self._logger.debug("Image %s used", node.get('name'))
449 s = StringIO(image_data)
451 if self.localcontext:
452 res = utils._regex.findall(node.text)
454 newtext = eval(key, {}, self.localcontext)
458 image_data = base64.decodestring(node.text)
460 s = StringIO(image_data)
462 self._logger.debug("No image data!")
465 if nfile in self.images:
466 s = StringIO(self.images[nfile])
469 up = urlparse.urlparse(str(nfile))
473 # RFC: do we really want to open external URLs?
474 # Are we safe from cross-site scripting or attacks?
475 self._logger.debug("Retrieve image from %s", nfile)
476 u = urllib.urlopen(str(nfile))
477 s = StringIO(u.read())
479 self._logger.debug("Open image file %s ", nfile)
480 s = _open_image(nfile, path=self.path)
482 (sx,sy) = img.getSize()
483 self._logger.debug("Image is %dx%d", sx, sy)
485 args = { 'x': 0.0, 'y': 0.0 }
486 for tag in ('width','height','x','y'):
488 args[tag] = utils.unit_get(node.get(tag))
489 if ('width' in args) and (not 'height' in args):
490 args['height'] = sy * args['width'] / sx
491 elif ('height' in args) and (not 'width' in args):
492 args['width'] = sx * args['height'] / sy
493 elif ('width' in args) and ('height' in args):
494 if (float(args['width'])/args['height'])>(float(sx)>sy):
495 args['width'] = sx * args['height'] / sy
497 args['height'] = sy * args['width'] / sx
498 self.canvas.drawImage(img, **args)
499 # self.canvas._doc.SaveToFile(self.canvas._filename, self.canvas)
501 def _path(self, node):
502 self.path = self.canvas.beginPath()
503 self.path.moveTo(**utils.attr_get(node, ['x','y']))
504 for n in utils._child_get(node, self):
507 vals = utils.text_get(n).split()
508 self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1]))
509 elif n.tag=='curvesto':
510 vals = utils.text_get(n).split()
514 pos.append(utils.unit_get(vals.pop(0)))
515 self.path.curveTo(*pos)
517 data = n.text.split() # Not sure if I must merge all TEXT_NODE ?
519 x = utils.unit_get(data.pop(0))
520 y = utils.unit_get(data.pop(0))
521 self.path.lineTo(x,y)
522 if (not node.get('close')) or utils.bool_get(node.get('close')):
524 self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
526 def setFont(self, node):
527 fontname = node.get('name')
528 if fontname not in pdfmetrics.getRegisteredFontNames()\
529 or fontname not in pdfmetrics.standardFonts:
530 # let reportlab attempt to find it
532 pdfmetrics.getFont(fontname)
534 logging.getLogger('report.fonts').debug('Could not locate font %s, substituting default: %s',
536 self.canvas._fontname)
537 fontname = self.canvas._fontname
538 return self.canvas.setFont(fontname, utils.unit_get(node.get('size')))
540 def render(self, node):
542 'drawCentredString': self._drawCenteredString,
543 'drawRightString': self._drawRightString,
544 'drawString': self._drawString,
546 'ellipse': self._ellipse,
547 'lines': self._lines,
549 'curves': self._curves,
550 'fill': lambda node: self.canvas.setFillColor(color.get(node.get('color'))),
551 'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.get('color'))),
552 'setFont': self.setFont ,
553 'place': self._place,
554 'circle': self._circle,
555 'lineMode': self._line_mode,
557 'rotate': lambda node: self.canvas.rotate(float(node.get('degrees'))),
558 'translate': self._translate,
561 for n in utils._child_get(node, self):
565 class _rml_draw(object):
566 def __init__(self, localcontext ,node, styles, images={}, path='.', title=None):
567 self.localcontext = localcontext
573 self.canvas_title = title
575 def render(self, canvas, doc):
577 cnv = _rml_canvas(canvas, self.localcontext, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title)
578 cnv.render(self.node)
579 canvas.restoreState()
581 class _rml_Illustration(platypus.flowables.Flowable):
582 def __init__(self, node, localcontext, styles, self2):
583 self.localcontext = (localcontext or {}).copy()
586 self.width = utils.unit_get(node.get('width'))
587 self.height = utils.unit_get(node.get('height'))
589 def wrap(self, *args):
590 return (self.width, self.height)
592 drw = _rml_draw(self.localcontext ,self.node,self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title)
593 drw.render(self.canv, None)
595 class _rml_flowable(object):
596 def __init__(self, doc, localcontext, images=None, path='.', title=None):
597 self.localcontext = localcontext
599 self.styles = doc.styles
600 self.images = images or {}
603 self._logger = logging.getLogger('report.rml.flowable')
605 def _textual(self, node):
606 rc1 = utils._process_text(self, node.text or '')
607 for n in utils._child_get(node,self):
608 txt_n = copy.deepcopy(n)
609 for key in txt_n.attrib.keys():
610 if key in ('rml_except', 'rml_loop', 'rml_tag'):
611 del txt_n.attrib[key]
612 if True or not self._textual(n).isspace():
613 if not n.tag == 'bullet':
614 txt_n.text = utils.xml2str(self._textual(n))
615 txt_n.tail = n.tail and utils._process_text(self, n.tail.replace('\n','')) or ''
616 rc1 += etree.tostring(txt_n)
619 def _table(self, node):
620 children = utils._child_get(node,self,'tr')
632 st = copy.deepcopy(self.styles.table_styles[tr.get('style')])
637 if tr.get('paraStyle'):
638 paraStyle = self.styles.styles[tr.get('paraStyle')]
641 for td in utils._child_get(tr, self,'td'):
643 st = copy.deepcopy(self.styles.table_styles[td.get('style')])
650 if td.get('paraStyle'):
652 paraStyle = self.styles.styles[td.get('paraStyle')]
656 for n in utils._child_get(td, self):
657 if n.tag == etree.Comment:
660 fl = self._flowable(n, extra_style=paraStyle)
661 if isinstance(fl,list):
667 flow = self._textual(td)
669 if len(data2)>length:
672 while len(ab)<length:
674 while len(data2)<length:
679 if node.get('colWidths'):
680 assert length == len(node.get('colWidths').split(','))
681 colwidths = [utils.unit_get(f.strip()) for f in node.get('colWidths').split(',')]
682 if node.get('rowHeights'):
683 rowheights = [utils.unit_get(f.strip()) for f in node.get('rowHeights').split(',')]
684 if len(rowheights) == 1:
685 rowheights = rowheights[0]
686 table = platypus.LongTable(data = data, colWidths=colwidths, rowHeights=rowheights, **(utils.attr_get(node, ['splitByRow'] ,{'repeatRows':'int','repeatCols':'int'})))
687 if node.get('style'):
688 table.setStyle(self.styles.table_styles[node.get('style')])
693 def _illustration(self, node):
694 return _rml_Illustration(node, self.localcontext, self.styles, self)
696 def _textual_image(self, node):
697 return base64.decodestring(node.text)
699 def _pto(self, node):
703 for node in utils._child_get(node, self):
704 if node.tag == etree.Comment:
707 elif node.tag=='pto_header':
708 pto_header = self.render(node)
709 elif node.tag=='pto_trailer':
710 pto_trailer = self.render(node)
712 flow = self._flowable(node)
714 if isinstance(flow,list):
715 sub_story = sub_story + flow
717 sub_story.append(flow)
718 return platypus.flowables.PTOContainer(sub_story, trailer=pto_trailer, header=pto_header)
720 def _flowable(self, node, extra_style=None):
722 return self._pto(node)
724 style = self.styles.para_style_get(node)
726 style.__dict__.update(extra_style)
728 for i in self._textual(node).split('\n'):
729 result.append(platypus.Paragraph(i, style, **(utils.attr_get(node, [], {'bulletText':'str'}))))
731 elif node.tag=='barCode':
733 from reportlab.graphics.barcode import code128
734 from reportlab.graphics.barcode import code39
735 from reportlab.graphics.barcode import code93
736 from reportlab.graphics.barcode import common
737 from reportlab.graphics.barcode import fourstate
738 from reportlab.graphics.barcode import usps
740 self._logger.warning("Cannot use barcode renderers:", exc_info=True)
742 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'})
744 'codabar': lambda x: common.Codabar(x, **args),
745 'code11': lambda x: common.Code11(x, **args),
746 'code128': lambda x: code128.Code128(x, **args),
747 'standard39': lambda x: code39.Standard39(x, **args),
748 'standard93': lambda x: code93.Standard93(x, **args),
749 'i2of5': lambda x: common.I2of5(x, **args),
750 'extended39': lambda x: code39.Extended39(x, **args),
751 'extended93': lambda x: code93.Extended93(x, **args),
752 'msi': lambda x: common.MSI(x, **args),
753 'fim': lambda x: usps.FIM(x, **args),
754 'postnet': lambda x: usps.POSTNET(x, **args),
758 code = node.get('code').lower()
759 return codes[code](self._textual(node))
760 elif node.tag=='name':
761 self.styles.names[ node.get('id')] = node.get('value')
763 elif node.tag=='xpre':
764 style = self.styles.para_style_get(node)
765 return platypus.XPreformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int','frags':'int'})))
766 elif node.tag=='pre':
767 style = self.styles.para_style_get(node)
768 return platypus.Preformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int'})))
769 elif node.tag=='illustration':
770 return self._illustration(node)
771 elif node.tag=='blockTable':
772 return self._table(node)
773 elif node.tag=='title':
774 styles = reportlab.lib.styles.getSampleStyleSheet()
775 style = styles['Title']
776 return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
777 elif re.match('^h([1-9]+[0-9]*)$', (node.tag or '')):
778 styles = reportlab.lib.styles.getSampleStyleSheet()
779 style = styles['Heading'+str(node.tag[1:])]
780 return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
781 elif node.tag=='image':
783 if not node.get('file'):
785 if node.get('name') in self.doc.images:
786 self._logger.debug("Image %s read ", node.get('name'))
787 image_data = self.doc.images[node.get('name')].read()
789 self._logger.warning("Image %s not defined", node.get('name'))
793 if self.localcontext:
794 newtext = utils._process_text(self, node.text or '')
796 image_data = base64.decodestring(node.text)
798 self._logger.debug("No inline image data")
800 image = StringIO(image_data)
802 self._logger.debug("Image get from file %s", node.get('file'))
803 image = _open_image(node.get('file'), path=self.doc.path)
804 return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
805 elif node.tag=='spacer':
806 if node.get('width'):
807 width = utils.unit_get(node.get('width'))
809 width = utils.unit_get('1cm')
810 length = utils.unit_get(node.get('length'))
811 return platypus.Spacer(width=width, height=length)
812 elif node.tag=='section':
813 return self.render(node)
814 elif node.tag == 'pageNumberReset':
816 elif node.tag in ('pageBreak', 'nextPage'):
817 return platypus.PageBreak()
818 elif node.tag=='condPageBreak':
819 return platypus.CondPageBreak(**(utils.attr_get(node, ['height'])))
820 elif node.tag=='setNextTemplate':
821 return platypus.NextPageTemplate(str(node.get('name')))
822 elif node.tag=='nextFrame':
823 return platypus.CondPageBreak(1000) # TODO: change the 1000 !
824 elif node.tag == 'setNextFrame':
825 from reportlab.platypus.doctemplate import NextFrameFlowable
826 return NextFrameFlowable(str(node.get('name')))
827 elif node.tag == 'currentFrame':
828 from reportlab.platypus.doctemplate import CurrentFrameFlowable
829 return CurrentFrameFlowable(str(node.get('name')))
830 elif node.tag == 'frameEnd':
831 return EndFrameFlowable()
832 elif node.tag == 'hr':
833 width_hr=node.get('width') or '100%'
834 color_hr=node.get('color') or 'black'
835 thickness_hr=node.get('thickness') or 1
836 lineCap_hr=node.get('lineCap') or 'round'
837 return platypus.flowables.HRFlowable(width=width_hr,color=color.get(color_hr),thickness=float(thickness_hr),lineCap=str(lineCap_hr))
839 sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.tag,))
842 def render(self, node_story):
843 def process_story(node_story):
845 for node in utils._child_get(node_story, self):
846 if node.tag == etree.Comment:
849 flow = self._flowable(node)
851 if isinstance(flow,list):
852 sub_story = sub_story + flow
854 sub_story.append(flow)
856 return process_story(node_story)
859 class EndFrameFlowable(ActionFlowable):
860 def __init__(self,resume=0):
861 ActionFlowable.__init__(self,('frameEnd',resume))
863 class TinyDocTemplate(platypus.BaseDocTemplate):
864 def ___handle_pageBegin(self):
865 self.page = self.page + 1
866 self.pageTemplate.beforeDrawPage(self.canv,self)
867 self.pageTemplate.checkPageSize(self.canv,self)
868 self.pageTemplate.onPage(self.canv,self)
869 for f in self.pageTemplate.frames: f._reset()
871 self._curPageFlowableCount = 0
872 if hasattr(self,'_nextFrameIndex'):
873 del self._nextFrameIndex
874 for f in self.pageTemplate.frames:
878 self.handle_frameBegin()
879 def afterFlowable(self, flowable):
880 if isinstance(flowable, PageReset):
881 self.canv._pageCount=self.page
884 self.canv._pageNumber = 0
886 class _rml_template(object):
887 def __init__(self, localcontext, out, node, doc, images={}, path='.', title=None):
889 localcontext={'internal_header':True}
890 self.localcontext = localcontext
895 if not node.get('pageSize'):
896 pageSize = (utils.unit_get('21cm'), utils.unit_get('29.7cm'))
898 ps = map(lambda x:x.strip(), node.get('pageSize').replace(')', '').replace('(', '').split(','))
899 pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
901 self.doc_tmpl = TinyDocTemplate(out, pagesize=pageSize, **utils.attr_get(node, ['leftMargin','rightMargin','topMargin','bottomMargin'], {'allowSplitting':'int','showBoundary':'bool','title':'str','author':'str'}))
902 self.page_templates = []
903 self.styles = doc.styles
906 pts = node.findall('pageTemplate')
909 for frame_el in pt.findall('frame'):
910 frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) )
911 if utils.attr_get(frame_el, ['last']):
912 frame.lastFrame = True
913 frames.append( frame )
915 gr = pt.findall('pageGraphics')\
916 or pt[1].findall('pageGraphics')
917 except Exception: # FIXME: be even more specific, perhaps?
920 # self.image=[ n for n in utils._child_get(gr[0], self) if n.tag=='image' or not self.localcontext]
921 drw = _rml_draw(self.localcontext,gr[0], self.doc, images=images, path=self.path, title=self.title)
922 self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
924 drw = _rml_draw(self.localcontext,node,self.doc,title=self.title)
925 self.page_templates.append( platypus.PageTemplate(frames=frames,onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
926 self.doc_tmpl.addPageTemplates(self.page_templates)
928 def render(self, node_stories):
929 if self.localcontext and not self.localcontext.get('internal_header',False):
930 del self.localcontext['internal_header']
932 r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title)
934 for node_story in node_stories:
936 fis.append(platypus.PageBreak())
937 fis += r.render(node_story)
938 # Reset Page Number with new story tag
939 fis.append(PageReset())
941 if self.localcontext and self.localcontext.get('internal_header',False):
942 self.doc_tmpl.afterFlowable(fis)
943 self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas)
945 fis.append(PageCount())
946 self.doc_tmpl.build(fis)
948 def parseNode(rml, localcontext=None,fout=None, images=None, path='.',title=None):
949 node = etree.XML(rml)
950 if localcontext is None:
954 r = _rml_doc(node, localcontext, images, path, title=title)
955 #try to override some font mappings
957 from customfonts import SetCustomFonts
960 # means there is no custom fonts mapping in this system.
963 logging.getLogger('report').warning('Cannot set font mapping', exc_info=True)
969 def parseString(rml, localcontext = {},fout=None, images={}, path='.',title=None):
970 node = etree.XML(rml)
971 r = _rml_doc(node, localcontext, images, path, title=title)
973 #try to override some font mappings
975 from customfonts import SetCustomFonts
991 print 'Usage: trml2pdf input.rml >output.pdf'
992 print 'Render the standard input (RML) and output a PDF file'
995 if __name__=="__main__":
997 if sys.argv[1]=='--help':
999 print parseString(file(sys.argv[1], 'r').read()),
1001 print 'Usage: trml2pdf input.rml >output.pdf'
1002 print 'Try \'trml2pdf --help\' for more information.'