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 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
43 from cStringIO import StringIO
44 _hush_pyflakes = [ StringIO ]
46 from StringIO import StringIO
48 _logger = logging.getLogger(__name__)
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
57 pdfmetrics.getFont(fontname)
61 addition = ". Your font contains spaces which is not valid in RML."
62 _logger.warning('Could not locate font %s, substituting default: %s%s',
63 fontname, default_fontname, addition)
64 fontname = default_fontname
68 def _open_image(filename, path=None):
69 """Attempt to open a binary file and return the descriptor
71 if os.path.isfile(filename):
72 return open(filename, 'rb')
73 for p in (path or []):
74 if p and os.path.isabs(p):
75 fullpath = os.path.join(p, filename)
76 if os.path.isfile(fullpath):
77 return open(fullpath, 'rb')
80 fullpath = os.path.join(p, filename)
83 return file_open(fullpath)
86 raise IOError("File %s cannot be found in image path" % filename)
88 class NumberedCanvas(canvas.Canvas):
89 def __init__(self, *args, **kwargs):
90 canvas.Canvas.__init__(self, *args, **kwargs)
91 self._saved_page_states = []
94 self._saved_page_states.append(dict(self.__dict__))
98 """add page info to each page (page x of y)"""
99 for state in self._saved_page_states:
100 self.__dict__.update(state)
101 self.draw_page_number()
102 canvas.Canvas.showPage(self)
103 canvas.Canvas.save(self)
105 def draw_page_number(self):
106 page_count = len(self._saved_page_states)
107 self.setFont("Helvetica", 8)
108 self.drawRightString((self._pagesize[0]-30), (self._pagesize[1]-40),
109 " %(this)i / %(total)i" % {
110 'this': self._pageNumber,
116 class PageCount(platypus.Flowable):
117 def __init__(self, story_count=0):
118 platypus.Flowable.__init__(self)
119 self.story_count = story_count
122 self.canv.beginForm("pageCount%d" % self.story_count)
123 self.canv.setFont("Helvetica", utils.unit_get(str(8)))
124 self.canv.drawString(0, 0, str(self.canv.getPageNumber()))
127 class PageReset(platypus.Flowable):
129 self.canv._doPageReset = True
131 class _rml_styles(object,):
132 def __init__(self, nodes, localcontext):
133 self.localcontext = localcontext
137 self.table_styles = {}
138 self.default_style = reportlab.lib.styles.getSampleStyleSheet()
141 for style in node.findall('blockTableStyle'):
142 self.table_styles[style.get('id')] = self._table_style_get(style)
143 for style in node.findall('paraStyle'):
144 sname = style.get('name')
145 self.styles[sname] = self._para_style_update(style)
146 if self.default_style.has_key(sname):
147 for key, value in self.styles[sname].items():
148 setattr(self.default_style[sname], key, value)
150 self.styles_obj[sname] = reportlab.lib.styles.ParagraphStyle(sname, self.default_style["Normal"], **self.styles[sname])
151 for variable in node.findall('initialize'):
152 for name in variable.findall('name'):
153 self.names[ name.get('id')] = name.get('value')
155 def _para_style_update(self, node):
157 for attr in ['textColor', 'backColor', 'bulletColor', 'borderColor']:
159 data[attr] = color.get(node.get(attr))
160 for attr in ['bulletFontName', 'fontName']:
162 fontname= select_fontname(node.get(attr), None)
163 if fontname is not None:
164 data['fontName'] = fontname
165 for attr in ['bulletText']:
167 data[attr] = node.get(attr)
168 for attr in ['fontSize', 'leftIndent', 'rightIndent', 'spaceBefore', 'spaceAfter',
169 'firstLineIndent', 'bulletIndent', 'bulletFontSize', 'leading',
170 'borderWidth','borderPadding','borderRadius']:
172 data[attr] = utils.unit_get(node.get(attr))
173 if node.get('alignment'):
175 'right':reportlab.lib.enums.TA_RIGHT,
176 'center':reportlab.lib.enums.TA_CENTER,
177 'justify':reportlab.lib.enums.TA_JUSTIFY
179 data['alignment'] = align.get(node.get('alignment').lower(), reportlab.lib.enums.TA_LEFT)
182 def _table_style_get(self, style_node):
184 for node in style_node:
185 start = utils.tuple_int_get(node, 'start', (0,0) )
186 stop = utils.tuple_int_get(node, 'stop', (-1,-1) )
187 if node.tag=='blockValign':
188 styles.append(('VALIGN', start, stop, str(node.get('value'))))
189 elif node.tag=='blockFont':
190 styles.append(('FONT', start, stop, str(node.get('name'))))
191 elif node.tag=='blockTextColor':
192 styles.append(('TEXTCOLOR', start, stop, color.get(str(node.get('colorName')))))
193 elif node.tag=='blockLeading':
194 styles.append(('LEADING', start, stop, utils.unit_get(node.get('length'))))
195 elif node.tag=='blockAlignment':
196 styles.append(('ALIGNMENT', start, stop, str(node.get('value'))))
197 elif node.tag=='blockSpan':
198 styles.append(('SPAN', start, stop))
199 elif node.tag=='blockLeftPadding':
200 styles.append(('LEFTPADDING', start, stop, utils.unit_get(node.get('length'))))
201 elif node.tag=='blockRightPadding':
202 styles.append(('RIGHTPADDING', start, stop, utils.unit_get(node.get('length'))))
203 elif node.tag=='blockTopPadding':
204 styles.append(('TOPPADDING', start, stop, utils.unit_get(node.get('length'))))
205 elif node.tag=='blockBottomPadding':
206 styles.append(('BOTTOMPADDING', start, stop, utils.unit_get(node.get('length'))))
207 elif node.tag=='blockBackground':
208 styles.append(('BACKGROUND', start, stop, color.get(node.get('colorName'))))
210 styles.append(('FONTSIZE', start, stop, utils.unit_get(node.get('size'))))
211 elif node.tag=='lineStyle':
212 kind = node.get('kind')
213 kind_list = [ 'GRID', 'BOX', 'OUTLINE', 'INNERGRID', 'LINEBELOW', 'LINEABOVE','LINEBEFORE', 'LINEAFTER' ]
214 assert kind in kind_list
216 if node.get('thickness'):
217 thick = float(node.get('thickness'))
218 styles.append((kind, start, stop, thick, color.get(node.get('colorName'))))
219 return platypus.tables.TableStyle(styles)
221 def para_style_get(self, node):
223 sname = node.get('style')
225 if sname in self.styles_obj:
226 style = self.styles_obj[sname]
228 _logger.debug('Warning: style not found, %s - setting default!', node.get('style'))
230 style = self.default_style['Normal']
231 para_update = self._para_style_update(node)
233 # update style only is necessary
234 style = copy.deepcopy(style)
235 style.__dict__.update(para_update)
238 class _rml_doc(object):
239 def __init__(self, node, localcontext=None, images=None, path='.', title=None):
242 if localcontext is None:
244 self.localcontext = localcontext
246 self.filename = self.etree.get('filename')
251 def docinit(self, els):
252 from reportlab.lib.fonts import addMapping
253 from reportlab.pdfbase import pdfmetrics
254 from reportlab.pdfbase.ttfonts import TTFont
258 for font in node.findall('registerFont'):
259 name = font.get('fontName').encode('ascii')
260 fname = font.get('fontFile').encode('ascii')
261 if name not in pdfmetrics._fonts:
262 pdfmetrics.registerFont(TTFont(name, fname))
263 #by default, we map the fontName to each style (bold, italic, bold and italic), so that
264 #if there isn't any font defined for one of these style (via a font family), the system
265 #will fallback on the normal font.
266 addMapping(name, 0, 0, name) #normal
267 addMapping(name, 0, 1, name) #italic
268 addMapping(name, 1, 0, name) #bold
269 addMapping(name, 1, 1, name) #italic and bold
271 #if registerFontFamily is defined, we register the mapping of the fontName to use for each style.
272 for font_family in node.findall('registerFontFamily'):
273 family_name = font_family.get('normal').encode('ascii')
274 if font_family.get('italic'):
275 addMapping(family_name, 0, 1, font_family.get('italic').encode('ascii'))
276 if font_family.get('bold'):
277 addMapping(family_name, 1, 0, font_family.get('bold').encode('ascii'))
278 if font_family.get('boldItalic'):
279 addMapping(family_name, 1, 1, font_family.get('boldItalic').encode('ascii'))
281 def setTTFontMapping(self,face, fontname, filename, mode='all'):
282 from reportlab.lib.fonts import addMapping
283 from reportlab.pdfbase import pdfmetrics
284 from reportlab.pdfbase.ttfonts import TTFont
289 if fontname not in pdfmetrics._fonts:
290 pdfmetrics.registerFont(TTFont(fontname, filename))
292 addMapping(face, 0, 0, fontname) #normal
293 addMapping(face, 0, 1, fontname) #italic
294 addMapping(face, 1, 0, fontname) #bold
295 addMapping(face, 1, 1, fontname) #italic and bold
296 elif mode in ['italic', 'oblique']:
297 addMapping(face, 0, 1, fontname) #italic
299 addMapping(face, 1, 0, fontname) #bold
300 elif mode in ('bolditalic', 'bold italic','boldoblique', 'bold oblique'):
301 addMapping(face, 1, 1, fontname) #italic and bold
303 addMapping(face, 0, 0, fontname) #normal
305 def _textual_image(self, node):
308 rc +=( etree.tostring(n) or '') + n.tail
309 return base64.decodestring(node.tostring())
311 def _images(self, el):
313 for node in el.findall('.//image'):
314 rc =( node.text or '')
315 result[node.get('name')] = base64.decodestring(rc)
318 def render(self, out):
319 el = self.etree.findall('.//docinit')
323 el = self.etree.findall('.//stylesheet')
324 self.styles = _rml_styles(el,self.localcontext)
326 el = self.etree.findall('.//images')
328 self.images.update( self._images(el[0]) )
330 el = self.etree.findall('.//template')
332 pt_obj = _rml_template(self.localcontext, out, el[0], self, images=self.images, path=self.path, title=self.title)
333 el = utils._child_get(self.etree, self, 'story')
336 self.canvas = canvas.Canvas(out)
337 pd = self.etree.find('pageDrawing')[0]
338 pd_obj = _rml_canvas(self.canvas, self.localcontext, None, self, self.images, path=self.path, title=self.title)
341 self.canvas.showPage()
344 class _rml_canvas(object):
345 def __init__(self, canvas, localcontext, doc_tmpl=None, doc=None, images=None, path='.', title=None):
348 self.localcontext = localcontext
350 self.styles = doc.styles
351 self.doc_tmpl = doc_tmpl
357 self.canvas.setTitle(self.title)
359 def _textual(self, node, x=0, y=0):
360 text = node.text and node.text.encode('utf-8') or ''
361 rc = utils._process_text(self, text)
364 from reportlab.lib.sequencer import getSequencer
366 rc += str(seq.next(n.get('id')))
367 if n.tag == 'pageCount':
369 self.canvas.translate(x,y)
370 self.canvas.doForm('pageCount%s' % (self.canvas._storyCount,))
372 self.canvas.translate(-x,-y)
373 if n.tag == 'pageNumber':
374 rc += str(self.canvas.getPageNumber())
375 rc += utils._process_text(self, n.tail)
376 return rc.replace('\n','')
378 def _drawString(self, node):
379 v = utils.attr_get(node, ['x','y'])
380 text=self._textual(node, **v)
381 text = utils.xml2str(text)
382 self.canvas.drawString(text=text, **v)
384 def _drawCenteredString(self, node):
385 v = utils.attr_get(node, ['x','y'])
386 text=self._textual(node, **v)
387 text = utils.xml2str(text)
388 self.canvas.drawCentredString(text=text, **v)
390 def _drawRightString(self, node):
391 v = utils.attr_get(node, ['x','y'])
392 text=self._textual(node, **v)
393 text = utils.xml2str(text)
394 self.canvas.drawRightString(text=text, **v)
396 def _rect(self, node):
397 if node.get('round'):
398 self.canvas.roundRect(radius=utils.unit_get(node.get('round')), **utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
400 self.canvas.rect(**utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
402 def _ellipse(self, node):
403 x1 = utils.unit_get(node.get('x'))
404 x2 = utils.unit_get(node.get('width'))
405 y1 = utils.unit_get(node.get('y'))
406 y2 = utils.unit_get(node.get('height'))
408 self.canvas.ellipse(x1,y1,x2,y2, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
410 def _curves(self, node):
411 line_str = node.text.split()
413 while len(line_str)>7:
414 self.canvas.bezier(*[utils.unit_get(l) for l in line_str[0:8]])
415 line_str = line_str[8:]
417 def _lines(self, node):
418 line_str = node.text.split()
420 while len(line_str)>3:
421 lines.append([utils.unit_get(l) for l in line_str[0:4]])
422 line_str = line_str[4:]
423 self.canvas.lines(lines)
425 def _grid(self, node):
426 xlist = [utils.unit_get(s) for s in node.get('xs').split(',')]
427 ylist = [utils.unit_get(s) for s in node.get('ys').split(',')]
429 self.canvas.grid(xlist, ylist)
431 def _translate(self, node):
432 dx = utils.unit_get(node.get('dx')) or 0
433 dy = utils.unit_get(node.get('dy')) or 0
434 self.canvas.translate(dx,dy)
436 def _circle(self, node):
437 self.canvas.circle(x_cen=utils.unit_get(node.get('x')), y_cen=utils.unit_get(node.get('y')), r=utils.unit_get(node.get('radius')), **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
439 def _place(self, node):
440 flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title, canvas=self.canvas).render(node)
441 infos = utils.attr_get(node, ['x','y','width','height'])
443 infos['y']+=infos['height']
445 w,h = flow.wrap(infos['width'], infos['height'])
446 if w<=infos['width'] and h<=infos['height']:
448 flow.drawOn(self.canvas,infos['x'],infos['y'])
451 raise ValueError("Not enough space")
453 def _line_mode(self, node):
454 ljoin = {'round':1, 'mitered':0, 'bevelled':2}
455 lcap = {'default':0, 'round':1, 'square':2}
457 if node.get('width'):
458 self.canvas.setLineWidth(utils.unit_get(node.get('width')))
460 self.canvas.setLineJoin(ljoin[node.get('join')])
462 self.canvas.setLineCap(lcap[node.get('cap')])
463 if node.get('miterLimit'):
464 self.canvas.setDash(utils.unit_get(node.get('miterLimit')))
466 dashes = node.get('dash').split(',')
467 for x in range(len(dashes)):
468 dashes[x]=utils.unit_get(dashes[x])
469 self.canvas.setDash(node.get('dash').split(','))
471 def _image(self, node):
474 from reportlab.lib.utils import ImageReader
475 nfile = node.get('file')
478 image_data = self.images[node.get('name')]
479 _logger.debug("Image %s used", node.get('name'))
480 s = StringIO(image_data)
483 if self.localcontext:
484 res = utils._regex.findall(newtext)
486 newtext = eval(key, {}, self.localcontext) or ''
489 image_data = base64.decodestring(newtext)
491 s = StringIO(image_data)
493 _logger.debug("No image data!")
496 if nfile in self.images:
497 s = StringIO(self.images[nfile])
500 up = urlparse.urlparse(str(nfile))
504 # RFC: do we really want to open external URLs?
505 # Are we safe from cross-site scripting or attacks?
506 _logger.debug("Retrieve image from %s", nfile)
507 u = urllib.urlopen(str(nfile))
508 s = StringIO(u.read())
510 _logger.debug("Open image file %s ", nfile)
511 s = _open_image(nfile, path=self.path)
514 (sx,sy) = img.getSize()
515 _logger.debug("Image is %dx%d", sx, sy)
516 args = { 'x': 0.0, 'y': 0.0, 'mask': 'auto'}
517 for tag in ('width','height','x','y'):
519 args[tag] = utils.unit_get(node.get(tag))
520 if ('width' in args) and (not 'height' in args):
521 args['height'] = sy * args['width'] / sx
522 elif ('height' in args) and (not 'width' in args):
523 args['width'] = sx * args['height'] / sy
524 elif ('width' in args) and ('height' in args):
525 if (float(args['width'])/args['height'])>(float(sx)>sy):
526 args['width'] = sx * args['height'] / sy
528 args['height'] = sy * args['width'] / sx
529 self.canvas.drawImage(img, **args)
532 # self.canvas._doc.SaveToFile(self.canvas._filename, self.canvas)
534 def _path(self, node):
535 self.path = self.canvas.beginPath()
536 self.path.moveTo(**utils.attr_get(node, ['x','y']))
537 for n in utils._child_get(node, self):
540 vals = utils.text_get(n).split()
541 self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1]))
542 elif n.tag=='curvesto':
543 vals = utils.text_get(n).split()
547 pos.append(utils.unit_get(vals.pop(0)))
548 self.path.curveTo(*pos)
550 data = n.text.split() # Not sure if I must merge all TEXT_NODE ?
552 x = utils.unit_get(data.pop(0))
553 y = utils.unit_get(data.pop(0))
554 self.path.lineTo(x,y)
555 if (not node.get('close')) or utils.bool_get(node.get('close')):
557 self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
559 def setFont(self, node):
560 fontname = select_fontname(node.get('name'), self.canvas._fontname)
561 return self.canvas.setFont(fontname, utils.unit_get(node.get('size')))
563 def render(self, node):
565 'drawCentredString': self._drawCenteredString,
566 'drawRightString': self._drawRightString,
567 'drawString': self._drawString,
569 'ellipse': self._ellipse,
570 'lines': self._lines,
572 'curves': self._curves,
573 'fill': lambda node: self.canvas.setFillColor(color.get(node.get('color'))),
574 'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.get('color'))),
575 'setFont': self.setFont ,
576 'place': self._place,
577 'circle': self._circle,
578 'lineMode': self._line_mode,
580 'rotate': lambda node: self.canvas.rotate(float(node.get('degrees'))),
581 'translate': self._translate,
584 for n in utils._child_get(node, self):
588 class _rml_draw(object):
589 def __init__(self, localcontext, node, styles, images=None, path='.', title=None):
592 self.localcontext = localcontext
598 self.canvas_title = title
600 def render(self, canvas, doc):
602 cnv = _rml_canvas(canvas, self.localcontext, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title)
603 cnv.render(self.node)
604 canvas.restoreState()
606 class _rml_Illustration(platypus.flowables.Flowable):
607 def __init__(self, node, localcontext, styles, self2):
608 self.localcontext = (localcontext or {}).copy()
611 self.width = utils.unit_get(node.get('width'))
612 self.height = utils.unit_get(node.get('height'))
614 def wrap(self, *args):
615 return self.width, self.height
617 drw = _rml_draw(self.localcontext ,self.node,self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title)
618 drw.render(self.canv, None)
620 # Workaround for issue #15: https://bitbucket.org/rptlab/reportlab/issue/15/infinite-pages-produced-when-splitting
621 original_pto_split = platypus.flowables.PTOContainer.split
622 def split(self, availWidth, availHeight):
623 res = original_pto_split(self, availWidth, availHeight)
624 if len(res) > 2 and len(self._content) > 0:
625 header = self._content[0]._ptoinfo.header
626 trailer = self._content[0]._ptoinfo.trailer
627 if isinstance(res[-2], platypus.flowables.UseUpSpace) and len(header + trailer) == len(res[:-2]):
630 platypus.flowables.PTOContainer.split = split
632 class _rml_flowable(object):
633 def __init__(self, doc, localcontext, images=None, path='.', title=None, canvas=None):
636 self.localcontext = localcontext
638 self.styles = doc.styles
644 def _textual(self, node):
645 rc1 = utils._process_text(self, node.text or '')
646 for n in utils._child_get(node,self):
647 txt_n = copy.deepcopy(n)
648 for key in txt_n.attrib.keys():
649 if key in ('rml_except', 'rml_loop', 'rml_tag'):
650 del txt_n.attrib[key]
651 if not n.tag == 'bullet':
652 if n.tag == 'pageNumber':
653 txt_n.text = self.canvas and str(self.canvas.getPageNumber()) or ''
655 txt_n.text = utils.xml2str(self._textual(n))
656 txt_n.tail = n.tail and utils.xml2str(utils._process_text(self, n.tail.replace('\n',''))) or ''
657 rc1 += etree.tostring(txt_n)
660 def _table(self, node):
661 children = utils._child_get(node,self,'tr')
673 st = copy.deepcopy(self.styles.table_styles[tr.get('style')])
674 for si in range(len(st._cmds)):
675 s = list(st._cmds[si])
676 s[1] = (s[1][0],posy)
677 s[2] = (s[2][0],posy)
678 st._cmds[si] = tuple(s)
680 if tr.get('paraStyle'):
681 paraStyle = self.styles.styles[tr.get('paraStyle')]
684 for td in utils._child_get(tr, self,'td'):
686 st = copy.deepcopy(self.styles.table_styles[td.get('style')])
693 if td.get('paraStyle'):
695 paraStyle = self.styles.styles[td.get('paraStyle')]
699 for n in utils._child_get(td, self):
700 if n.tag == etree.Comment:
703 fl = self._flowable(n, extra_style=paraStyle)
704 if isinstance(fl,list):
710 flow = self._textual(td)
712 if len(data2)>length:
715 while len(ab)<length:
717 while len(data2)<length:
722 if node.get('colWidths'):
723 assert length == len(node.get('colWidths').split(','))
724 colwidths = [utils.unit_get(f.strip()) for f in node.get('colWidths').split(',')]
725 if node.get('rowHeights'):
726 rowheights = [utils.unit_get(f.strip()) for f in node.get('rowHeights').split(',')]
727 if len(rowheights) == 1:
728 rowheights = rowheights[0]
729 table = platypus.LongTable(data = data, colWidths=colwidths, rowHeights=rowheights, **(utils.attr_get(node, ['splitByRow'] ,{'repeatRows':'int','repeatCols':'int'})))
730 if node.get('style'):
731 table.setStyle(self.styles.table_styles[node.get('style')])
736 def _illustration(self, node):
737 return _rml_Illustration(node, self.localcontext, self.styles, self)
739 def _textual_image(self, node):
740 return base64.decodestring(node.text)
742 def _pto(self, node):
746 for node in utils._child_get(node, self):
747 if node.tag == etree.Comment:
750 elif node.tag=='pto_header':
751 pto_header = self.render(node)
752 elif node.tag=='pto_trailer':
753 pto_trailer = self.render(node)
755 flow = self._flowable(node)
757 if isinstance(flow,list):
758 sub_story = sub_story + flow
760 sub_story.append(flow)
761 return platypus.flowables.PTOContainer(sub_story, trailer=pto_trailer, header=pto_header)
763 def _flowable(self, node, extra_style=None):
765 return self._pto(node)
767 style = self.styles.para_style_get(node)
769 style.__dict__.update(extra_style)
771 for i in self._textual(node).split('\n'):
772 result.append(platypus.Paragraph(i, style, **(utils.attr_get(node, [], {'bulletText':'str'}))))
774 elif node.tag=='barCode':
776 from reportlab.graphics.barcode import code128
777 from reportlab.graphics.barcode import code39
778 from reportlab.graphics.barcode import code93
779 from reportlab.graphics.barcode import common
780 from reportlab.graphics.barcode import fourstate
781 from reportlab.graphics.barcode import usps
782 from reportlab.graphics.barcode import createBarcodeDrawing
785 _logger.warning("Cannot use barcode renderers:", exc_info=True)
787 args = utils.attr_get(node, [], {'ratio':'float','xdim':'unit','height':'unit','checksum':'int','quiet':'int','width':'unit','stop':'bool','bearers':'int','barWidth':'float','barHeight':'float'})
789 'codabar': lambda x: common.Codabar(x, **args),
790 'code11': lambda x: common.Code11(x, **args),
791 'code128': lambda x: code128.Code128(str(x), **args),
792 'standard39': lambda x: code39.Standard39(str(x), **args),
793 'standard93': lambda x: code93.Standard93(str(x), **args),
794 'i2of5': lambda x: common.I2of5(x, **args),
795 'extended39': lambda x: code39.Extended39(str(x), **args),
796 'extended93': lambda x: code93.Extended93(str(x), **args),
797 'msi': lambda x: common.MSI(x, **args),
798 'fim': lambda x: usps.FIM(x, **args),
799 'postnet': lambda x: usps.POSTNET(x, **args),
800 'ean13': lambda x: createBarcodeDrawing('EAN13', value=str(x), **args),
801 'qrcode': lambda x: createBarcodeDrawing('QR', value=x, **args),
805 code = node.get('code').lower()
806 return codes[code](self._textual(node))
807 elif node.tag=='name':
808 self.styles.names[ node.get('id')] = node.get('value')
810 elif node.tag=='xpre':
811 style = self.styles.para_style_get(node)
812 return platypus.XPreformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int','frags':'int'})))
813 elif node.tag=='pre':
814 style = self.styles.para_style_get(node)
815 return platypus.Preformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int'})))
816 elif node.tag=='illustration':
817 return self._illustration(node)
818 elif node.tag=='blockTable':
819 return self._table(node)
820 elif node.tag=='title':
821 styles = reportlab.lib.styles.getSampleStyleSheet()
822 style = styles['Title']
823 return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
824 elif re.match('^h([1-9]+[0-9]*)$', (node.tag or '')):
825 styles = reportlab.lib.styles.getSampleStyleSheet()
826 style = styles['Heading'+str(node.tag[1:])]
827 return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
828 elif node.tag=='image':
830 if not node.get('file'):
832 if node.get('name') in self.doc.images:
833 _logger.debug("Image %s read ", node.get('name'))
834 image_data = self.doc.images[node.get('name')].read()
836 _logger.warning("Image %s not defined", node.get('name'))
841 if self.localcontext:
842 newtext = utils._process_text(self, node.text or '')
843 image_data = base64.decodestring(newtext)
845 _logger.debug("No inline image data")
847 image = StringIO(image_data)
849 _logger.debug("Image get from file %s", node.get('file'))
850 image = _open_image(node.get('file'), path=self.doc.path)
851 return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
852 elif node.tag=='spacer':
853 if node.get('width'):
854 width = utils.unit_get(node.get('width'))
856 width = utils.unit_get('1cm')
857 length = utils.unit_get(node.get('length'))
858 return platypus.Spacer(width=width, height=length)
859 elif node.tag=='section':
860 return self.render(node)
861 elif node.tag == 'pageNumberReset':
863 elif node.tag in ('pageBreak', 'nextPage'):
864 return platypus.PageBreak()
865 elif node.tag=='condPageBreak':
866 return platypus.CondPageBreak(**(utils.attr_get(node, ['height'])))
867 elif node.tag=='setNextTemplate':
868 return platypus.NextPageTemplate(str(node.get('name')))
869 elif node.tag=='nextFrame':
870 return platypus.CondPageBreak(1000) # TODO: change the 1000 !
871 elif node.tag == 'setNextFrame':
872 from reportlab.platypus.doctemplate import NextFrameFlowable
873 return NextFrameFlowable(str(node.get('name')))
874 elif node.tag == 'currentFrame':
875 from reportlab.platypus.doctemplate import CurrentFrameFlowable
876 return CurrentFrameFlowable(str(node.get('name')))
877 elif node.tag == 'frameEnd':
878 return EndFrameFlowable()
879 elif node.tag == 'hr':
880 width_hr=node.get('width') or '100%'
881 color_hr=node.get('color') or 'black'
882 thickness_hr=node.get('thickness') or 1
883 lineCap_hr=node.get('lineCap') or 'round'
884 return platypus.flowables.HRFlowable(width=width_hr,color=color.get(color_hr),thickness=float(thickness_hr),lineCap=str(lineCap_hr))
886 sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.tag,))
889 def render(self, node_story):
890 def process_story(node_story):
892 for node in utils._child_get(node_story, self):
893 if node.tag == etree.Comment:
896 flow = self._flowable(node)
898 if isinstance(flow,list):
899 sub_story = sub_story + flow
901 sub_story.append(flow)
903 return process_story(node_story)
906 class EndFrameFlowable(ActionFlowable):
907 def __init__(self,resume=0):
908 ActionFlowable.__init__(self,('frameEnd',resume))
910 class TinyDocTemplate(platypus.BaseDocTemplate):
912 def beforeDocument(self):
913 # Store some useful value directly inside canvas, so it's available
914 # on flowable drawing (needed for proper PageCount handling)
915 self.canv._doPageReset = False
916 self.canv._storyCount = 0
918 def ___handle_pageBegin(self):
920 self.pageTemplate.beforeDrawPage(self.canv,self)
921 self.pageTemplate.checkPageSize(self.canv,self)
922 self.pageTemplate.onPage(self.canv,self)
923 for f in self.pageTemplate.frames: f._reset()
925 self._curPageFlowableCount = 0
926 if hasattr(self,'_nextFrameIndex'):
927 del self._nextFrameIndex
928 for f in self.pageTemplate.frames:
932 self.handle_frameBegin()
935 if self.canv._doPageReset:
936 # Following a <pageReset/> tag:
937 # - we reset page number to 0
938 # - we add an new PageCount flowable (relative to the current
939 # story number), but not for NumeredCanvas at is handle page
941 # NOTE: _rml_template render() method add a PageReset flowable at end
942 # of each story, so we're sure to pass here at least once per story.
943 if not isinstance(self.canv, NumberedCanvas):
944 self.handle_flowable([ PageCount(story_count=self.canv._storyCount) ])
945 self.canv._pageCount = self.page
947 self.canv._flag = True
948 self.canv._pageNumber = 0
949 self.canv._doPageReset = False
950 self.canv._storyCount += 1
952 class _rml_template(object):
953 def __init__(self, localcontext, out, node, doc, images=None, path='.', title=None):
957 localcontext={'internal_header':True}
958 self.localcontext = localcontext
963 pagesize_map = {'a4': A4,
967 if self.localcontext.get('company'):
968 pageSize = pagesize_map.get(self.localcontext.get('company').paper_format, A4)
969 if node.get('pageSize'):
970 ps = map(lambda x:x.strip(), node.get('pageSize').replace(')', '').replace('(', '').split(','))
971 pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
973 self.doc_tmpl = TinyDocTemplate(out, pagesize=pageSize, **utils.attr_get(node, ['leftMargin','rightMargin','topMargin','bottomMargin'], {'allowSplitting':'int','showBoundary':'bool','rotation':'int','title':'str','author':'str'}))
974 self.page_templates = []
975 self.styles = doc.styles
978 pts = node.findall('pageTemplate')
981 for frame_el in pt.findall('frame'):
982 frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) )
983 if utils.attr_get(frame_el, ['last']):
984 frame.lastFrame = True
985 frames.append( frame )
987 gr = pt.findall('pageGraphics')\
988 or pt[1].findall('pageGraphics')
989 except Exception: # FIXME: be even more specific, perhaps?
992 # self.image=[ n for n in utils._child_get(gr[0], self) if n.tag=='image' or not self.localcontext]
993 drw = _rml_draw(self.localcontext,gr[0], self.doc, images=images, path=self.path, title=self.title)
994 self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
996 drw = _rml_draw(self.localcontext,node,self.doc,title=self.title)
997 self.page_templates.append( platypus.PageTemplate(frames=frames,onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
998 self.doc_tmpl.addPageTemplates(self.page_templates)
1000 def render(self, node_stories):
1001 if self.localcontext and not self.localcontext.get('internal_header',False):
1002 del self.localcontext['internal_header']
1004 r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title, canvas=None)
1006 for node_story in node_stories:
1008 fis.append(platypus.PageBreak())
1009 fis += r.render(node_story)
1010 # Reset Page Number with new story tag
1011 fis.append(PageReset())
1014 if self.localcontext and self.localcontext.get('internal_header',False):
1015 self.doc_tmpl.afterFlowable(fis)
1016 self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas)
1018 self.doc_tmpl.build(fis)
1019 except platypus.doctemplate.LayoutError, e:
1020 e.name = 'Print Error'
1021 e.value = 'The document you are trying to print contains a table row that does not fit on one page. Please try to split it in smaller rows or contact your administrator.'
1024 def parseNode(rml, localcontext=None, fout=None, images=None, path='.', title=None):
1025 node = etree.XML(rml)
1026 r = _rml_doc(node, localcontext, images, path, title=title)
1027 #try to override some font mappings
1029 from customfonts import SetCustomFonts
1032 # means there is no custom fonts mapping in this system.
1035 _logger.warning('Cannot set font mapping', exc_info=True)
1039 return fp.getvalue()
1041 def parseString(rml, localcontext=None, fout=None, images=None, path='.', title=None):
1042 node = etree.XML(rml)
1043 r = _rml_doc(node, localcontext, images, path, title=title)
1045 #try to override some font mappings
1047 from customfonts import SetCustomFonts
1053 fp = file(fout,'wb')
1060 return fp.getvalue()
1062 def trml2pdf_help():
1063 print 'Usage: trml2pdf input.rml >output.pdf'
1064 print 'Render the standard input (RML) and output a PDF file'
1067 if __name__=="__main__":
1069 if sys.argv[1]=='--help':
1071 print parseString(file(sys.argv[1], 'r').read()),
1073 print 'Usage: trml2pdf input.rml >output.pdf'
1074 print 'Try \'trml2pdf --help\' for more information.'
1077 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: