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 = []
97 """add page info to each page (page x of y)"""
98 for state in self._saved_page_states:
99 self.__dict__.update(state)
100 self.draw_page_number()
101 canvas.Canvas.showPage(self)
102 canvas.Canvas.save(self)
104 def draw_page_number(self):
105 page_count = len(self._saved_page_states)
106 self.setFont("Helvetica", 8)
107 self.drawRightString((self._pagesize[0]-30), (self._pagesize[1]-40),
108 " %(this)i / %(total)i" % {
109 'this': self._pageNumber,
115 class PageCount(platypus.Flowable):
116 def __init__(self, story_count=0):
117 platypus.Flowable.__init__(self)
118 self.story_count = story_count
121 self.canv.beginForm("pageCount%d" % self.story_count)
122 self.canv.setFont("Helvetica", utils.unit_get(str(8)))
123 self.canv.drawString(0, 0, str(self.canv.getPageNumber()))
126 class PageReset(platypus.Flowable):
128 """Flag to close current story page numbering and prepare for the next
129 should be executed after the rendering of the full story"""
130 self.canv._doPageReset = True
132 class _rml_styles(object,):
133 def __init__(self, nodes, localcontext):
134 self.localcontext = localcontext
138 self.table_styles = {}
139 self.default_style = reportlab.lib.styles.getSampleStyleSheet()
142 for style in node.findall('blockTableStyle'):
143 self.table_styles[style.get('id')] = self._table_style_get(style)
144 for style in node.findall('paraStyle'):
145 sname = style.get('name')
146 self.styles[sname] = self._para_style_update(style)
147 if self.default_style.has_key(sname):
148 for key, value in self.styles[sname].items():
149 setattr(self.default_style[sname], key, value)
151 self.styles_obj[sname] = reportlab.lib.styles.ParagraphStyle(sname, self.default_style["Normal"], **self.styles[sname])
152 for variable in node.findall('initialize'):
153 for name in variable.findall('name'):
154 self.names[ name.get('id')] = name.get('value')
156 def _para_style_update(self, node):
158 for attr in ['textColor', 'backColor', 'bulletColor', 'borderColor']:
160 data[attr] = color.get(node.get(attr))
161 for attr in ['bulletFontName', 'fontName']:
163 fontname= select_fontname(node.get(attr), None)
164 if fontname is not None:
165 data['fontName'] = fontname
166 for attr in ['bulletText']:
168 data[attr] = node.get(attr)
169 for attr in ['fontSize', 'leftIndent', 'rightIndent', 'spaceBefore', 'spaceAfter',
170 'firstLineIndent', 'bulletIndent', 'bulletFontSize', 'leading',
171 'borderWidth','borderPadding','borderRadius']:
173 data[attr] = utils.unit_get(node.get(attr))
174 if node.get('alignment'):
176 'right':reportlab.lib.enums.TA_RIGHT,
177 'center':reportlab.lib.enums.TA_CENTER,
178 'justify':reportlab.lib.enums.TA_JUSTIFY
180 data['alignment'] = align.get(node.get('alignment').lower(), reportlab.lib.enums.TA_LEFT)
183 def _table_style_get(self, style_node):
185 for node in style_node:
186 start = utils.tuple_int_get(node, 'start', (0,0) )
187 stop = utils.tuple_int_get(node, 'stop', (-1,-1) )
188 if node.tag=='blockValign':
189 styles.append(('VALIGN', start, stop, str(node.get('value'))))
190 elif node.tag=='blockFont':
191 styles.append(('FONT', start, stop, str(node.get('name'))))
192 elif node.tag=='blockTextColor':
193 styles.append(('TEXTCOLOR', start, stop, color.get(str(node.get('colorName')))))
194 elif node.tag=='blockLeading':
195 styles.append(('LEADING', start, stop, utils.unit_get(node.get('length'))))
196 elif node.tag=='blockAlignment':
197 styles.append(('ALIGNMENT', start, stop, str(node.get('value'))))
198 elif node.tag=='blockSpan':
199 styles.append(('SPAN', start, stop))
200 elif node.tag=='blockLeftPadding':
201 styles.append(('LEFTPADDING', start, stop, utils.unit_get(node.get('length'))))
202 elif node.tag=='blockRightPadding':
203 styles.append(('RIGHTPADDING', start, stop, utils.unit_get(node.get('length'))))
204 elif node.tag=='blockTopPadding':
205 styles.append(('TOPPADDING', start, stop, utils.unit_get(node.get('length'))))
206 elif node.tag=='blockBottomPadding':
207 styles.append(('BOTTOMPADDING', start, stop, utils.unit_get(node.get('length'))))
208 elif node.tag=='blockBackground':
209 styles.append(('BACKGROUND', start, stop, color.get(node.get('colorName'))))
211 styles.append(('FONTSIZE', start, stop, utils.unit_get(node.get('size'))))
212 elif node.tag=='lineStyle':
213 kind = node.get('kind')
214 kind_list = [ 'GRID', 'BOX', 'OUTLINE', 'INNERGRID', 'LINEBELOW', 'LINEABOVE','LINEBEFORE', 'LINEAFTER' ]
215 assert kind in kind_list
217 if node.get('thickness'):
218 thick = float(node.get('thickness'))
219 styles.append((kind, start, stop, thick, color.get(node.get('colorName'))))
220 return platypus.tables.TableStyle(styles)
222 def para_style_get(self, node):
224 sname = node.get('style')
226 if sname in self.styles_obj:
227 style = self.styles_obj[sname]
229 _logger.debug('Warning: style not found, %s - setting default!', node.get('style'))
231 style = self.default_style['Normal']
232 para_update = self._para_style_update(node)
234 # update style only is necessary
235 style = copy.deepcopy(style)
236 style.__dict__.update(para_update)
239 class _rml_doc(object):
240 def __init__(self, node, localcontext=None, images=None, path='.', title=None):
243 if localcontext is None:
245 self.localcontext = localcontext
247 self.filename = self.etree.get('filename')
252 def docinit(self, els):
253 from reportlab.lib.fonts import addMapping
254 from reportlab.pdfbase import pdfmetrics
255 from reportlab.pdfbase.ttfonts import TTFont
259 for font in node.findall('registerFont'):
260 name = font.get('fontName').encode('ascii')
261 fname = font.get('fontFile').encode('ascii')
262 if name not in pdfmetrics._fonts:
263 pdfmetrics.registerFont(TTFont(name, fname))
264 #by default, we map the fontName to each style (bold, italic, bold and italic), so that
265 #if there isn't any font defined for one of these style (via a font family), the system
266 #will fallback on the normal font.
267 addMapping(name, 0, 0, name) #normal
268 addMapping(name, 0, 1, name) #italic
269 addMapping(name, 1, 0, name) #bold
270 addMapping(name, 1, 1, name) #italic and bold
272 #if registerFontFamily is defined, we register the mapping of the fontName to use for each style.
273 for font_family in node.findall('registerFontFamily'):
274 family_name = font_family.get('normal').encode('ascii')
275 if font_family.get('italic'):
276 addMapping(family_name, 0, 1, font_family.get('italic').encode('ascii'))
277 if font_family.get('bold'):
278 addMapping(family_name, 1, 0, font_family.get('bold').encode('ascii'))
279 if font_family.get('boldItalic'):
280 addMapping(family_name, 1, 1, font_family.get('boldItalic').encode('ascii'))
282 def setTTFontMapping(self,face, fontname, filename, mode='all'):
283 from reportlab.lib.fonts import addMapping
284 from reportlab.pdfbase import pdfmetrics
285 from reportlab.pdfbase.ttfonts import TTFont
290 if fontname not in pdfmetrics._fonts:
291 pdfmetrics.registerFont(TTFont(fontname, filename))
293 addMapping(face, 0, 0, fontname) #normal
294 addMapping(face, 0, 1, fontname) #italic
295 addMapping(face, 1, 0, fontname) #bold
296 addMapping(face, 1, 1, fontname) #italic and bold
297 elif mode in ['italic', 'oblique']:
298 addMapping(face, 0, 1, fontname) #italic
300 addMapping(face, 1, 0, fontname) #bold
301 elif mode in ('bolditalic', 'bold italic','boldoblique', 'bold oblique'):
302 addMapping(face, 1, 1, fontname) #italic and bold
304 addMapping(face, 0, 0, fontname) #normal
306 def _textual_image(self, node):
309 rc +=( etree.tostring(n) or '') + n.tail
310 return base64.decodestring(node.tostring())
312 def _images(self, el):
314 for node in el.findall('.//image'):
315 rc =( node.text or '')
316 result[node.get('name')] = base64.decodestring(rc)
319 def render(self, out):
320 el = self.etree.findall('.//docinit')
324 el = self.etree.findall('.//stylesheet')
325 self.styles = _rml_styles(el,self.localcontext)
327 el = self.etree.findall('.//images')
329 self.images.update( self._images(el[0]) )
331 el = self.etree.findall('.//template')
333 pt_obj = _rml_template(self.localcontext, out, el[0], self, images=self.images, path=self.path, title=self.title)
334 el = utils._child_get(self.etree, self, 'story')
337 self.canvas = canvas.Canvas(out)
338 pd = self.etree.find('pageDrawing')[0]
339 pd_obj = _rml_canvas(self.canvas, self.localcontext, None, self, self.images, path=self.path, title=self.title)
342 self.canvas.showPage()
345 class _rml_canvas(object):
346 def __init__(self, canvas, localcontext, doc_tmpl=None, doc=None, images=None, path='.', title=None):
349 self.localcontext = localcontext
351 self.styles = doc.styles
352 self.doc_tmpl = doc_tmpl
358 self.canvas.setTitle(self.title)
360 def _textual(self, node, x=0, y=0):
361 text = node.text and node.text.encode('utf-8') or ''
362 rc = utils._process_text(self, text)
365 from reportlab.lib.sequencer import getSequencer
367 rc += str(seq.next(n.get('id')))
368 if n.tag == 'pageCount':
370 self.canvas.translate(x,y)
371 self.canvas.doForm('pageCount%s' % (self.canvas._storyCount,))
373 self.canvas.translate(-x,-y)
374 if n.tag == 'pageNumber':
375 rc += str(self.canvas.getPageNumber())
376 rc += utils._process_text(self, n.tail)
377 return rc.replace('\n','')
379 def _drawString(self, node):
380 v = utils.attr_get(node, ['x','y'])
381 text=self._textual(node, **v)
382 text = utils.xml2str(text)
384 self.canvas.drawString(text=text, **v)
386 _logger.error("Bad RML: <drawString> tag requires attributes 'x' and 'y'!")
389 def _drawCenteredString(self, node):
390 v = utils.attr_get(node, ['x','y'])
391 text=self._textual(node, **v)
392 text = utils.xml2str(text)
393 self.canvas.drawCentredString(text=text, **v)
395 def _drawRightString(self, node):
396 v = utils.attr_get(node, ['x','y'])
397 text=self._textual(node, **v)
398 text = utils.xml2str(text)
399 self.canvas.drawRightString(text=text, **v)
401 def _rect(self, node):
402 if node.get('round'):
403 self.canvas.roundRect(radius=utils.unit_get(node.get('round')), **utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
405 self.canvas.rect(**utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
407 def _ellipse(self, node):
408 x1 = utils.unit_get(node.get('x'))
409 x2 = utils.unit_get(node.get('width'))
410 y1 = utils.unit_get(node.get('y'))
411 y2 = utils.unit_get(node.get('height'))
413 self.canvas.ellipse(x1,y1,x2,y2, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
415 def _curves(self, node):
416 line_str = node.text.split()
418 while len(line_str)>7:
419 self.canvas.bezier(*[utils.unit_get(l) for l in line_str[0:8]])
420 line_str = line_str[8:]
422 def _lines(self, node):
423 line_str = node.text.split()
425 while len(line_str)>3:
426 lines.append([utils.unit_get(l) for l in line_str[0:4]])
427 line_str = line_str[4:]
428 self.canvas.lines(lines)
430 def _grid(self, node):
431 xlist = [utils.unit_get(s) for s in node.get('xs').split(',')]
432 ylist = [utils.unit_get(s) for s in node.get('ys').split(',')]
434 self.canvas.grid(xlist, ylist)
436 def _translate(self, node):
437 dx = utils.unit_get(node.get('dx')) or 0
438 dy = utils.unit_get(node.get('dy')) or 0
439 self.canvas.translate(dx,dy)
441 def _circle(self, node):
442 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'}))
444 def _place(self, node):
445 flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title, canvas=self.canvas).render(node)
446 infos = utils.attr_get(node, ['x','y','width','height'])
448 infos['y']+=infos['height']
450 w,h = flow.wrap(infos['width'], infos['height'])
451 if w<=infos['width'] and h<=infos['height']:
453 flow.drawOn(self.canvas,infos['x'],infos['y'])
456 raise ValueError("Not enough space")
458 def _line_mode(self, node):
459 ljoin = {'round':1, 'mitered':0, 'bevelled':2}
460 lcap = {'default':0, 'round':1, 'square':2}
462 if node.get('width'):
463 self.canvas.setLineWidth(utils.unit_get(node.get('width')))
465 self.canvas.setLineJoin(ljoin[node.get('join')])
467 self.canvas.setLineCap(lcap[node.get('cap')])
468 if node.get('miterLimit'):
469 self.canvas.setDash(utils.unit_get(node.get('miterLimit')))
471 dashes = node.get('dash').split(',')
472 for x in range(len(dashes)):
473 dashes[x]=utils.unit_get(dashes[x])
474 self.canvas.setDash(node.get('dash').split(','))
476 def _image(self, node):
479 from reportlab.lib.utils import ImageReader
480 nfile = node.get('file')
483 image_data = self.images[node.get('name')]
484 _logger.debug("Image %s used", node.get('name'))
485 s = StringIO(image_data)
488 if self.localcontext:
489 res = utils._regex.findall(newtext)
491 newtext = eval(key, {}, self.localcontext) or ''
494 image_data = base64.decodestring(newtext)
496 s = StringIO(image_data)
498 _logger.debug("No image data!")
501 if nfile in self.images:
502 s = StringIO(self.images[nfile])
505 up = urlparse.urlparse(str(nfile))
509 # RFC: do we really want to open external URLs?
510 # Are we safe from cross-site scripting or attacks?
511 _logger.debug("Retrieve image from %s", nfile)
512 u = urllib.urlopen(str(nfile))
513 s = StringIO(u.read())
515 _logger.debug("Open image file %s ", nfile)
516 s = _open_image(nfile, path=self.path)
519 (sx,sy) = img.getSize()
520 _logger.debug("Image is %dx%d", sx, sy)
521 args = { 'x': 0.0, 'y': 0.0, 'mask': 'auto'}
522 for tag in ('width','height','x','y'):
524 args[tag] = utils.unit_get(node.get(tag))
525 if ('width' in args) and (not 'height' in args):
526 args['height'] = sy * args['width'] / sx
527 elif ('height' in args) and (not 'width' in args):
528 args['width'] = sx * args['height'] / sy
529 elif ('width' in args) and ('height' in args):
530 if (float(args['width'])/args['height'])>(float(sx)>sy):
531 args['width'] = sx * args['height'] / sy
533 args['height'] = sy * args['width'] / sx
534 self.canvas.drawImage(img, **args)
537 # self.canvas._doc.SaveToFile(self.canvas._filename, self.canvas)
539 def _path(self, node):
540 self.path = self.canvas.beginPath()
541 self.path.moveTo(**utils.attr_get(node, ['x','y']))
542 for n in utils._child_get(node, self):
545 vals = utils.text_get(n).split()
546 self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1]))
547 elif n.tag=='curvesto':
548 vals = utils.text_get(n).split()
552 pos.append(utils.unit_get(vals.pop(0)))
553 self.path.curveTo(*pos)
555 data = n.text.split() # Not sure if I must merge all TEXT_NODE ?
557 x = utils.unit_get(data.pop(0))
558 y = utils.unit_get(data.pop(0))
559 self.path.lineTo(x,y)
560 if (not node.get('close')) or utils.bool_get(node.get('close')):
562 self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
564 def setFont(self, node):
565 fontname = select_fontname(node.get('name'), self.canvas._fontname)
566 return self.canvas.setFont(fontname, utils.unit_get(node.get('size')))
568 def render(self, node):
570 'drawCentredString': self._drawCenteredString,
571 'drawRightString': self._drawRightString,
572 'drawString': self._drawString,
574 'ellipse': self._ellipse,
575 'lines': self._lines,
577 'curves': self._curves,
578 'fill': lambda node: self.canvas.setFillColor(color.get(node.get('color'))),
579 'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.get('color'))),
580 'setFont': self.setFont ,
581 'place': self._place,
582 'circle': self._circle,
583 'lineMode': self._line_mode,
585 'rotate': lambda node: self.canvas.rotate(float(node.get('degrees'))),
586 'translate': self._translate,
589 for n in utils._child_get(node, self):
593 class _rml_draw(object):
594 def __init__(self, localcontext, node, styles, images=None, path='.', title=None):
597 self.localcontext = localcontext
603 self.canvas_title = title
605 def render(self, canvas, doc):
607 cnv = _rml_canvas(canvas, self.localcontext, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title)
608 cnv.render(self.node)
609 canvas.restoreState()
611 class _rml_Illustration(platypus.flowables.Flowable):
612 def __init__(self, node, localcontext, styles, self2):
613 self.localcontext = (localcontext or {}).copy()
616 self.width = utils.unit_get(node.get('width'))
617 self.height = utils.unit_get(node.get('height'))
619 def wrap(self, *args):
620 return self.width, self.height
622 drw = _rml_draw(self.localcontext ,self.node,self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title)
623 drw.render(self.canv, None)
625 # Workaround for issue #15: https://bitbucket.org/rptlab/reportlab/issue/15/infinite-pages-produced-when-splitting
626 original_pto_split = platypus.flowables.PTOContainer.split
627 def split(self, availWidth, availHeight):
628 res = original_pto_split(self, availWidth, availHeight)
629 if len(res) > 2 and len(self._content) > 0:
630 header = self._content[0]._ptoinfo.header
631 trailer = self._content[0]._ptoinfo.trailer
632 if isinstance(res[-2], platypus.flowables.UseUpSpace) and len(header + trailer) == len(res[:-2]):
635 platypus.flowables.PTOContainer.split = split
637 class _rml_flowable(object):
638 def __init__(self, doc, localcontext, images=None, path='.', title=None, canvas=None):
641 self.localcontext = localcontext
643 self.styles = doc.styles
649 def _textual(self, node):
650 rc1 = utils._process_text(self, node.text or '')
651 for n in utils._child_get(node,self):
652 txt_n = copy.deepcopy(n)
653 for key in txt_n.attrib.keys():
654 if key in ('rml_except', 'rml_loop', 'rml_tag'):
655 del txt_n.attrib[key]
656 if not n.tag == 'bullet':
657 if n.tag == 'pageNumber':
658 txt_n.text = self.canvas and str(self.canvas.getPageNumber()) or ''
660 txt_n.text = utils.xml2str(self._textual(n))
661 txt_n.tail = n.tail and utils.xml2str(utils._process_text(self, n.tail.replace('\n',''))) or ''
662 rc1 += etree.tostring(txt_n)
665 def _table(self, node):
666 children = utils._child_get(node,self,'tr')
678 st = copy.deepcopy(self.styles.table_styles[tr.get('style')])
679 for si in range(len(st._cmds)):
680 s = list(st._cmds[si])
681 s[1] = (s[1][0],posy)
682 s[2] = (s[2][0],posy)
683 st._cmds[si] = tuple(s)
685 if tr.get('paraStyle'):
686 paraStyle = self.styles.styles[tr.get('paraStyle')]
689 for td in utils._child_get(tr, self,'td'):
691 st = copy.deepcopy(self.styles.table_styles[td.get('style')])
698 if td.get('paraStyle'):
700 paraStyle = self.styles.styles[td.get('paraStyle')]
704 for n in utils._child_get(td, self):
705 if n.tag == etree.Comment:
708 fl = self._flowable(n, extra_style=paraStyle)
709 if isinstance(fl,list):
715 flow = self._textual(td)
717 if len(data2)>length:
720 while len(ab)<length:
722 while len(data2)<length:
727 if node.get('colWidths'):
728 assert length == len(node.get('colWidths').split(','))
729 colwidths = [utils.unit_get(f.strip()) for f in node.get('colWidths').split(',')]
730 if node.get('rowHeights'):
731 rowheights = [utils.unit_get(f.strip()) for f in node.get('rowHeights').split(',')]
732 if len(rowheights) == 1:
733 rowheights = rowheights[0]
734 table = platypus.LongTable(data = data, colWidths=colwidths, rowHeights=rowheights, **(utils.attr_get(node, ['splitByRow'] ,{'repeatRows':'int','repeatCols':'int'})))
735 if node.get('style'):
736 table.setStyle(self.styles.table_styles[node.get('style')])
741 def _illustration(self, node):
742 return _rml_Illustration(node, self.localcontext, self.styles, self)
744 def _textual_image(self, node):
745 return base64.decodestring(node.text)
747 def _pto(self, node):
751 for node in utils._child_get(node, self):
752 if node.tag == etree.Comment:
755 elif node.tag=='pto_header':
756 pto_header = self.render(node)
757 elif node.tag=='pto_trailer':
758 pto_trailer = self.render(node)
760 flow = self._flowable(node)
762 if isinstance(flow,list):
763 sub_story = sub_story + flow
765 sub_story.append(flow)
766 return platypus.flowables.PTOContainer(sub_story, trailer=pto_trailer, header=pto_header)
768 def _flowable(self, node, extra_style=None):
770 return self._pto(node)
772 style = self.styles.para_style_get(node)
774 style.__dict__.update(extra_style)
776 for i in self._textual(node).split('\n'):
777 result.append(platypus.Paragraph(i, style, **(utils.attr_get(node, [], {'bulletText':'str'}))))
779 elif node.tag=='barCode':
781 from reportlab.graphics.barcode import code128
782 from reportlab.graphics.barcode import code39
783 from reportlab.graphics.barcode import code93
784 from reportlab.graphics.barcode import common
785 from reportlab.graphics.barcode import fourstate
786 from reportlab.graphics.barcode import usps
787 from reportlab.graphics.barcode import createBarcodeDrawing
790 _logger.warning("Cannot use barcode renderers:", exc_info=True)
792 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'})
794 'codabar': lambda x: common.Codabar(x, **args),
795 'code11': lambda x: common.Code11(x, **args),
796 'code128': lambda x: code128.Code128(str(x), **args),
797 'standard39': lambda x: code39.Standard39(str(x), **args),
798 'standard93': lambda x: code93.Standard93(str(x), **args),
799 'i2of5': lambda x: common.I2of5(x, **args),
800 'extended39': lambda x: code39.Extended39(str(x), **args),
801 'extended93': lambda x: code93.Extended93(str(x), **args),
802 'msi': lambda x: common.MSI(x, **args),
803 'fim': lambda x: usps.FIM(x, **args),
804 'postnet': lambda x: usps.POSTNET(x, **args),
805 'ean13': lambda x: createBarcodeDrawing('EAN13', value=str(x), **args),
806 'qrcode': lambda x: createBarcodeDrawing('QR', value=x, **args),
810 code = node.get('code').lower()
811 return codes[code](self._textual(node))
812 elif node.tag=='name':
813 self.styles.names[ node.get('id')] = node.get('value')
815 elif node.tag=='xpre':
816 style = self.styles.para_style_get(node)
817 return platypus.XPreformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int','frags':'int'})))
818 elif node.tag=='pre':
819 style = self.styles.para_style_get(node)
820 return platypus.Preformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int'})))
821 elif node.tag=='illustration':
822 return self._illustration(node)
823 elif node.tag=='blockTable':
824 return self._table(node)
825 elif node.tag=='title':
826 styles = reportlab.lib.styles.getSampleStyleSheet()
827 style = styles['Title']
828 return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
829 elif re.match('^h([1-9]+[0-9]*)$', (node.tag or '')):
830 styles = reportlab.lib.styles.getSampleStyleSheet()
831 style = styles['Heading'+str(node.tag[1:])]
832 return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
833 elif node.tag=='image':
835 if not node.get('file'):
837 if node.get('name') in self.doc.images:
838 _logger.debug("Image %s read ", node.get('name'))
839 image_data = self.doc.images[node.get('name')].read()
841 _logger.warning("Image %s not defined", node.get('name'))
846 if self.localcontext:
847 newtext = utils._process_text(self, node.text or '')
848 image_data = base64.decodestring(newtext)
850 _logger.debug("No inline image data")
852 image = StringIO(image_data)
854 _logger.debug("Image get from file %s", node.get('file'))
855 image = _open_image(node.get('file'), path=self.doc.path)
856 return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
857 elif node.tag=='spacer':
858 if node.get('width'):
859 width = utils.unit_get(node.get('width'))
861 width = utils.unit_get('1cm')
862 length = utils.unit_get(node.get('length'))
863 return platypus.Spacer(width=width, height=length)
864 elif node.tag=='section':
865 return self.render(node)
866 elif node.tag == 'pageNumberReset':
868 elif node.tag in ('pageBreak', 'nextPage'):
869 return platypus.PageBreak()
870 elif node.tag=='condPageBreak':
871 return platypus.CondPageBreak(**(utils.attr_get(node, ['height'])))
872 elif node.tag=='setNextTemplate':
873 return platypus.NextPageTemplate(str(node.get('name')))
874 elif node.tag=='nextFrame':
875 return platypus.CondPageBreak(1000) # TODO: change the 1000 !
876 elif node.tag == 'setNextFrame':
877 from reportlab.platypus.doctemplate import NextFrameFlowable
878 return NextFrameFlowable(str(node.get('name')))
879 elif node.tag == 'currentFrame':
880 from reportlab.platypus.doctemplate import CurrentFrameFlowable
881 return CurrentFrameFlowable(str(node.get('name')))
882 elif node.tag == 'frameEnd':
883 return EndFrameFlowable()
884 elif node.tag == 'hr':
885 width_hr=node.get('width') or '100%'
886 color_hr=node.get('color') or 'black'
887 thickness_hr=node.get('thickness') or 1
888 lineCap_hr=node.get('lineCap') or 'round'
889 return platypus.flowables.HRFlowable(width=width_hr,color=color.get(color_hr),thickness=float(thickness_hr),lineCap=str(lineCap_hr))
891 sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.tag,))
894 def render(self, node_story):
895 def process_story(node_story):
897 for node in utils._child_get(node_story, self):
898 if node.tag == etree.Comment:
901 flow = self._flowable(node)
903 if isinstance(flow,list):
904 sub_story = sub_story + flow
906 sub_story.append(flow)
908 return process_story(node_story)
911 class EndFrameFlowable(ActionFlowable):
912 def __init__(self,resume=0):
913 ActionFlowable.__init__(self,('frameEnd',resume))
915 class TinyDocTemplate(platypus.BaseDocTemplate):
917 def beforeDocument(self):
918 # Store some useful value directly inside canvas, so it's available
919 # on flowable drawing (needed for proper PageCount handling)
920 self.canv._doPageReset = False
921 self.canv._storyCount = 0
923 def ___handle_pageBegin(self):
925 self.pageTemplate.beforeDrawPage(self.canv,self)
926 self.pageTemplate.checkPageSize(self.canv,self)
927 self.pageTemplate.onPage(self.canv,self)
928 for f in self.pageTemplate.frames: f._reset()
930 self._curPageFlowableCount = 0
931 if hasattr(self,'_nextFrameIndex'):
932 del self._nextFrameIndex
933 for f in self.pageTemplate.frames:
937 self.handle_frameBegin()
940 if isinstance(self.canv, NumberedCanvas):
941 # save current page states before eventual reset
942 self.canv._saved_page_states.append(dict(self.canv.__dict__))
943 if self.canv._doPageReset:
944 # Following a <pageReset/> tag:
945 # - we reset page number to 0
946 # - we add an new PageCount flowable (relative to the current
947 # story number), but not for NumeredCanvas at is handle page
949 # NOTE: _rml_template render() method add a PageReset flowable at end
950 # of each story, so we're sure to pass here at least once per story.
951 if not isinstance(self.canv, NumberedCanvas):
952 self.handle_flowable([ PageCount(story_count=self.canv._storyCount) ])
953 self.canv._pageCount = self.page
955 self.canv._flag = True
956 self.canv._pageNumber = 0
957 self.canv._doPageReset = False
958 self.canv._storyCount += 1
960 class _rml_template(object):
961 def __init__(self, localcontext, out, node, doc, images=None, path='.', title=None):
965 localcontext={'internal_header':True}
966 self.localcontext = localcontext
971 pagesize_map = {'a4': A4,
975 if self.localcontext.get('company'):
976 pageSize = pagesize_map.get(self.localcontext.get('company').rml_paper_format, A4)
977 if node.get('pageSize'):
978 ps = map(lambda x:x.strip(), node.get('pageSize').replace(')', '').replace('(', '').split(','))
979 pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
981 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'}))
982 self.page_templates = []
983 self.styles = doc.styles
986 pts = node.findall('pageTemplate')
989 for frame_el in pt.findall('frame'):
990 frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) )
991 if utils.attr_get(frame_el, ['last']):
992 frame.lastFrame = True
993 frames.append( frame )
995 gr = pt.findall('pageGraphics')\
996 or pt[1].findall('pageGraphics')
997 except Exception: # FIXME: be even more specific, perhaps?
1000 # self.image=[ n for n in utils._child_get(gr[0], self) if n.tag=='image' or not self.localcontext]
1001 drw = _rml_draw(self.localcontext,gr[0], self.doc, images=images, path=self.path, title=self.title)
1002 self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
1004 drw = _rml_draw(self.localcontext,node,self.doc,title=self.title)
1005 self.page_templates.append( platypus.PageTemplate(frames=frames,onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
1006 self.doc_tmpl.addPageTemplates(self.page_templates)
1008 def render(self, node_stories):
1009 if self.localcontext and not self.localcontext.get('internal_header',False):
1010 del self.localcontext['internal_header']
1012 r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title, canvas=None)
1014 for node_story in node_stories:
1016 fis.append(platypus.PageBreak())
1017 fis += r.render(node_story)
1018 # end of story numbering computation
1019 fis.append(PageReset())
1022 if self.localcontext and self.localcontext.get('internal_header',False):
1023 self.doc_tmpl.afterFlowable(fis)
1024 self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas)
1026 self.doc_tmpl.build(fis)
1027 except platypus.doctemplate.LayoutError, e:
1028 e.name = 'Print Error'
1029 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.'
1032 def parseNode(rml, localcontext=None, fout=None, images=None, path='.', title=None):
1033 node = etree.XML(rml)
1034 r = _rml_doc(node, localcontext, images, path, title=title)
1035 #try to override some font mappings
1037 from customfonts import SetCustomFonts
1040 # means there is no custom fonts mapping in this system.
1043 _logger.warning('Cannot set font mapping', exc_info=True)
1047 return fp.getvalue()
1049 def parseString(rml, localcontext=None, fout=None, images=None, path='.', title=None):
1050 node = etree.XML(rml)
1051 r = _rml_doc(node, localcontext, images, path, title=title)
1053 #try to override some font mappings
1055 from customfonts import SetCustomFonts
1061 fp = file(fout,'wb')
1068 return fp.getvalue()
1070 def trml2pdf_help():
1071 print 'Usage: trml2pdf input.rml >output.pdf'
1072 print 'Render the standard input (RML) and output a PDF file'
1075 if __name__=="__main__":
1077 if sys.argv[1]=='--help':
1079 print parseString(file(sys.argv[1], 'r').read()),
1081 print 'Usage: trml2pdf input.rml >output.pdf'
1082 print 'Try \'trml2pdf --help\' for more information.'
1085 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: