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)
59 _logger.warning('Could not locate font %s, substituting default: %s',
60 fontname, default_fontname)
61 fontname = default_fontname
65 def _open_image(filename, path=None):
66 """Attempt to open a binary file and return the descriptor
68 if os.path.isfile(filename):
69 return open(filename, 'rb')
70 for p in (path or []):
71 if p and os.path.isabs(p):
72 fullpath = os.path.join(p, filename)
73 if os.path.isfile(fullpath):
74 return open(fullpath, 'rb')
77 fullpath = os.path.join(p, filename)
80 return file_open(fullpath)
83 raise IOError("File %s cannot be found in image path" % filename)
85 class NumberedCanvas(canvas.Canvas):
86 def __init__(self, *args, **kwargs):
87 canvas.Canvas.__init__(self, *args, **kwargs)
88 self._saved_page_states = []
91 self._saved_page_states.append(dict(self.__dict__))
95 """add page info to each page (page x of y)"""
96 for state in self._saved_page_states:
97 self.__dict__.update(state)
98 self.draw_page_number()
99 canvas.Canvas.showPage(self)
100 canvas.Canvas.save(self)
102 def draw_page_number(self):
103 page_count = len(self._saved_page_states)
104 self.setFont("Helvetica", 8)
105 self.drawRightString((self._pagesize[0]-30), (self._pagesize[1]-40),
106 " %(this)i / %(total)i" % {
107 'this': self._pageNumber,
113 class PageCount(platypus.Flowable):
114 def __init__(self, story_count=0):
115 platypus.Flowable.__init__(self)
116 self.story_count = story_count
119 self.canv.beginForm("pageCount%d" % self.story_count)
120 self.canv.setFont("Helvetica", utils.unit_get(str(8)))
121 self.canv.drawString(0, 0, str(self.canv.getPageNumber()))
124 class PageReset(platypus.Flowable):
126 self.canv._doPageReset = True
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)
143 if self.default_style.has_key(sname):
144 for key, value in self.styles[sname].items():
145 setattr(self.default_style[sname], key, value)
147 self.styles_obj[sname] = reportlab.lib.styles.ParagraphStyle(sname, self.default_style["Normal"], **self.styles[sname])
148 for variable in node.findall('initialize'):
149 for name in variable.findall('name'):
150 self.names[ name.get('id')] = name.get('value')
152 def _para_style_update(self, node):
154 for attr in ['textColor', 'backColor', 'bulletColor', 'borderColor']:
156 data[attr] = color.get(node.get(attr))
157 for attr in ['bulletFontName', 'fontName']:
159 fontname= select_fontname(node.get(attr), None)
160 if fontname is not None:
161 data['fontName'] = fontname
162 for attr in ['bulletText']:
164 data[attr] = node.get(attr)
165 for attr in ['fontSize', 'leftIndent', 'rightIndent', 'spaceBefore', 'spaceAfter',
166 'firstLineIndent', 'bulletIndent', 'bulletFontSize', 'leading',
167 'borderWidth','borderPadding','borderRadius']:
169 data[attr] = utils.unit_get(node.get(attr))
170 if node.get('alignment'):
172 'right':reportlab.lib.enums.TA_RIGHT,
173 'center':reportlab.lib.enums.TA_CENTER,
174 'justify':reportlab.lib.enums.TA_JUSTIFY
176 data['alignment'] = align.get(node.get('alignment').lower(), reportlab.lib.enums.TA_LEFT)
179 def _table_style_get(self, style_node):
181 for node in style_node:
182 start = utils.tuple_int_get(node, 'start', (0,0) )
183 stop = utils.tuple_int_get(node, 'stop', (-1,-1) )
184 if node.tag=='blockValign':
185 styles.append(('VALIGN', start, stop, str(node.get('value'))))
186 elif node.tag=='blockFont':
187 styles.append(('FONT', start, stop, str(node.get('name'))))
188 elif node.tag=='blockTextColor':
189 styles.append(('TEXTCOLOR', start, stop, color.get(str(node.get('colorName')))))
190 elif node.tag=='blockLeading':
191 styles.append(('LEADING', start, stop, utils.unit_get(node.get('length'))))
192 elif node.tag=='blockAlignment':
193 styles.append(('ALIGNMENT', start, stop, str(node.get('value'))))
194 elif node.tag=='blockSpan':
195 styles.append(('SPAN', start, stop))
196 elif node.tag=='blockLeftPadding':
197 styles.append(('LEFTPADDING', start, stop, utils.unit_get(node.get('length'))))
198 elif node.tag=='blockRightPadding':
199 styles.append(('RIGHTPADDING', start, stop, utils.unit_get(node.get('length'))))
200 elif node.tag=='blockTopPadding':
201 styles.append(('TOPPADDING', start, stop, utils.unit_get(node.get('length'))))
202 elif node.tag=='blockBottomPadding':
203 styles.append(('BOTTOMPADDING', start, stop, utils.unit_get(node.get('length'))))
204 elif node.tag=='blockBackground':
205 styles.append(('BACKGROUND', start, stop, color.get(node.get('colorName'))))
207 styles.append(('FONTSIZE', start, stop, utils.unit_get(node.get('size'))))
208 elif node.tag=='lineStyle':
209 kind = node.get('kind')
210 kind_list = [ 'GRID', 'BOX', 'OUTLINE', 'INNERGRID', 'LINEBELOW', 'LINEABOVE','LINEBEFORE', 'LINEAFTER' ]
211 assert kind in kind_list
213 if node.get('thickness'):
214 thick = float(node.get('thickness'))
215 styles.append((kind, start, stop, thick, color.get(node.get('colorName'))))
216 return platypus.tables.TableStyle(styles)
218 def para_style_get(self, node):
220 sname = node.get('style')
222 if sname in self.styles_obj:
223 style = self.styles_obj[sname]
225 _logger.debug('Warning: style not found, %s - setting default!', node.get('style'))
227 style = self.default_style['Normal']
228 para_update = self._para_style_update(node)
230 # update style only is necessary
231 style = copy.deepcopy(style)
232 style.__dict__.update(para_update)
235 class _rml_doc(object):
236 def __init__(self, node, localcontext=None, images=None, path='.', title=None):
239 if localcontext is None:
241 self.localcontext = localcontext
243 self.filename = self.etree.get('filename')
248 def docinit(self, els):
249 from reportlab.lib.fonts import addMapping
250 from reportlab.pdfbase import pdfmetrics
251 from reportlab.pdfbase.ttfonts import TTFont
255 for font in node.findall('registerFont'):
256 name = font.get('fontName').encode('ascii')
257 fname = font.get('fontFile').encode('ascii')
258 if name not in pdfmetrics._fonts:
259 pdfmetrics.registerFont(TTFont(name, fname))
260 #by default, we map the fontName to each style (bold, italic, bold and italic), so that
261 #if there isn't any font defined for one of these style (via a font family), the system
262 #will fallback on the normal font.
263 addMapping(name, 0, 0, name) #normal
264 addMapping(name, 0, 1, name) #italic
265 addMapping(name, 1, 0, name) #bold
266 addMapping(name, 1, 1, name) #italic and bold
268 #if registerFontFamily is defined, we register the mapping of the fontName to use for each style.
269 for font_family in node.findall('registerFontFamily'):
270 family_name = font_family.get('normal').encode('ascii')
271 if font_family.get('italic'):
272 addMapping(family_name, 0, 1, font_family.get('italic').encode('ascii'))
273 if font_family.get('bold'):
274 addMapping(family_name, 1, 0, font_family.get('bold').encode('ascii'))
275 if font_family.get('boldItalic'):
276 addMapping(family_name, 1, 1, font_family.get('boldItalic').encode('ascii'))
278 def setTTFontMapping(self,face, fontname, filename, mode='all'):
279 from reportlab.lib.fonts import addMapping
280 from reportlab.pdfbase import pdfmetrics
281 from reportlab.pdfbase.ttfonts import TTFont
283 if fontname not in pdfmetrics._fonts:
284 pdfmetrics.registerFont(TTFont(fontname, filename))
286 addMapping(face, 0, 0, fontname) #normal
287 addMapping(face, 0, 1, fontname) #italic
288 addMapping(face, 1, 0, fontname) #bold
289 addMapping(face, 1, 1, fontname) #italic and bold
290 elif (mode== 'normal') or (mode == 'regular'):
291 addMapping(face, 0, 0, fontname) #normal
292 elif mode == 'italic':
293 addMapping(face, 0, 1, fontname) #italic
295 addMapping(face, 1, 0, fontname) #bold
296 elif mode == 'bolditalic':
297 addMapping(face, 1, 1, fontname) #italic and bold
299 def _textual_image(self, node):
302 rc +=( etree.tostring(n) or '') + n.tail
303 return base64.decodestring(node.tostring())
305 def _images(self, el):
307 for node in el.findall('.//image'):
308 rc =( node.text or '')
309 result[node.get('name')] = base64.decodestring(rc)
312 def render(self, out):
313 el = self.etree.findall('.//docinit')
317 el = self.etree.findall('.//stylesheet')
318 self.styles = _rml_styles(el,self.localcontext)
320 el = self.etree.findall('.//images')
322 self.images.update( self._images(el[0]) )
324 el = self.etree.findall('.//template')
326 pt_obj = _rml_template(self.localcontext, out, el[0], self, images=self.images, path=self.path, title=self.title)
327 el = utils._child_get(self.etree, self, 'story')
330 self.canvas = canvas.Canvas(out)
331 pd = self.etree.find('pageDrawing')[0]
332 pd_obj = _rml_canvas(self.canvas, self.localcontext, None, self, self.images, path=self.path, title=self.title)
335 self.canvas.showPage()
338 class _rml_canvas(object):
339 def __init__(self, canvas, localcontext, doc_tmpl=None, doc=None, images=None, path='.', title=None):
342 self.localcontext = localcontext
344 self.styles = doc.styles
345 self.doc_tmpl = doc_tmpl
351 self.canvas.setTitle(self.title)
353 def _textual(self, node, x=0, y=0):
354 text = node.text and node.text.encode('utf-8') or ''
355 rc = utils._process_text(self, text)
358 from reportlab.lib.sequencer import getSequencer
360 rc += str(seq.next(n.get('id')))
361 if n.tag == 'pageCount':
363 self.canvas.translate(x,y)
364 self.canvas.doForm('pageCount%s' % (self.canvas._storyCount,))
366 self.canvas.translate(-x,-y)
367 if n.tag == 'pageNumber':
368 rc += str(self.canvas.getPageNumber())
369 rc += utils._process_text(self, n.tail)
370 return rc.replace('\n','')
372 def _drawString(self, node):
373 v = utils.attr_get(node, ['x','y'])
374 text=self._textual(node, **v)
375 text = utils.xml2str(text)
377 self.canvas.drawString(text=text, **v)
378 except TypeError as e:
379 _logger.error("Bad RML: <drawString> tag requires attributes 'x' and 'y'!")
382 def _drawCenteredString(self, node):
383 v = utils.attr_get(node, ['x','y'])
384 text=self._textual(node, **v)
385 text = utils.xml2str(text)
386 self.canvas.drawCentredString(text=text, **v)
388 def _drawRightString(self, node):
389 v = utils.attr_get(node, ['x','y'])
390 text=self._textual(node, **v)
391 text = utils.xml2str(text)
392 self.canvas.drawRightString(text=text, **v)
394 def _rect(self, node):
395 if node.get('round'):
396 self.canvas.roundRect(radius=utils.unit_get(node.get('round')), **utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
398 self.canvas.rect(**utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
400 def _ellipse(self, node):
401 x1 = utils.unit_get(node.get('x'))
402 x2 = utils.unit_get(node.get('width'))
403 y1 = utils.unit_get(node.get('y'))
404 y2 = utils.unit_get(node.get('height'))
406 self.canvas.ellipse(x1,y1,x2,y2, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
408 def _curves(self, node):
409 line_str = node.text.split()
411 while len(line_str)>7:
412 self.canvas.bezier(*[utils.unit_get(l) for l in line_str[0:8]])
413 line_str = line_str[8:]
415 def _lines(self, node):
416 line_str = node.text.split()
418 while len(line_str)>3:
419 lines.append([utils.unit_get(l) for l in line_str[0:4]])
420 line_str = line_str[4:]
421 self.canvas.lines(lines)
423 def _grid(self, node):
424 xlist = [utils.unit_get(s) for s in node.get('xs').split(',')]
425 ylist = [utils.unit_get(s) for s in node.get('ys').split(',')]
427 self.canvas.grid(xlist, ylist)
429 def _translate(self, node):
430 dx = utils.unit_get(node.get('dx')) or 0
431 dy = utils.unit_get(node.get('dy')) or 0
432 self.canvas.translate(dx,dy)
434 def _circle(self, node):
435 self.canvas.circle(x_cen=utils.unit_get(node.get('x')), y_cen=utils.unit_get(node.get('y')), r=utils.unit_get(node.get('radius')), **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
437 def _place(self, node):
438 flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title, canvas=self.canvas).render(node)
439 infos = utils.attr_get(node, ['x','y','width','height'])
441 infos['y']+=infos['height']
443 w,h = flow.wrap(infos['width'], infos['height'])
444 if w<=infos['width'] and h<=infos['height']:
446 flow.drawOn(self.canvas,infos['x'],infos['y'])
449 raise ValueError("Not enough space")
451 def _line_mode(self, node):
452 ljoin = {'round':1, 'mitered':0, 'bevelled':2}
453 lcap = {'default':0, 'round':1, 'square':2}
455 if node.get('width'):
456 self.canvas.setLineWidth(utils.unit_get(node.get('width')))
458 self.canvas.setLineJoin(ljoin[node.get('join')])
460 self.canvas.setLineCap(lcap[node.get('cap')])
461 if node.get('miterLimit'):
462 self.canvas.setDash(utils.unit_get(node.get('miterLimit')))
464 dashes = node.get('dash').split(',')
465 for x in range(len(dashes)):
466 dashes[x]=utils.unit_get(dashes[x])
467 self.canvas.setDash(node.get('dash').split(','))
469 def _image(self, node):
472 from reportlab.lib.utils import ImageReader
473 nfile = node.get('file')
476 image_data = self.images[node.get('name')]
477 _logger.debug("Image %s used", node.get('name'))
478 s = StringIO(image_data)
481 if self.localcontext:
482 res = utils._regex.findall(newtext)
484 newtext = eval(key, {}, self.localcontext) or ''
487 image_data = base64.decodestring(newtext)
489 s = StringIO(image_data)
491 _logger.debug("No image data!")
494 if nfile in self.images:
495 s = StringIO(self.images[nfile])
498 up = urlparse.urlparse(str(nfile))
502 # RFC: do we really want to open external URLs?
503 # Are we safe from cross-site scripting or attacks?
504 _logger.debug("Retrieve image from %s", nfile)
505 u = urllib.urlopen(str(nfile))
506 s = StringIO(u.read())
508 _logger.debug("Open image file %s ", nfile)
509 s = _open_image(nfile, path=self.path)
512 (sx,sy) = img.getSize()
513 _logger.debug("Image is %dx%d", sx, sy)
514 args = { 'x': 0.0, 'y': 0.0, 'mask': 'auto'}
515 for tag in ('width','height','x','y'):
517 args[tag] = utils.unit_get(node.get(tag))
518 if ('width' in args) and (not 'height' in args):
519 args['height'] = sy * args['width'] / sx
520 elif ('height' in args) and (not 'width' in args):
521 args['width'] = sx * args['height'] / sy
522 elif ('width' in args) and ('height' in args):
523 if (float(args['width'])/args['height'])>(float(sx)>sy):
524 args['width'] = sx * args['height'] / sy
526 args['height'] = sy * args['width'] / sx
527 self.canvas.drawImage(img, **args)
530 # self.canvas._doc.SaveToFile(self.canvas._filename, self.canvas)
532 def _path(self, node):
533 self.path = self.canvas.beginPath()
534 self.path.moveTo(**utils.attr_get(node, ['x','y']))
535 for n in utils._child_get(node, self):
538 vals = utils.text_get(n).split()
539 self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1]))
540 elif n.tag=='curvesto':
541 vals = utils.text_get(n).split()
545 pos.append(utils.unit_get(vals.pop(0)))
546 self.path.curveTo(*pos)
548 data = n.text.split() # Not sure if I must merge all TEXT_NODE ?
550 x = utils.unit_get(data.pop(0))
551 y = utils.unit_get(data.pop(0))
552 self.path.lineTo(x,y)
553 if (not node.get('close')) or utils.bool_get(node.get('close')):
555 self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
557 def setFont(self, node):
558 fontname = select_fontname(node.get('name'), self.canvas._fontname)
559 return self.canvas.setFont(fontname, utils.unit_get(node.get('size')))
561 def render(self, node):
563 'drawCentredString': self._drawCenteredString,
564 'drawRightString': self._drawRightString,
565 'drawString': self._drawString,
567 'ellipse': self._ellipse,
568 'lines': self._lines,
570 'curves': self._curves,
571 'fill': lambda node: self.canvas.setFillColor(color.get(node.get('color'))),
572 'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.get('color'))),
573 'setFont': self.setFont ,
574 'place': self._place,
575 'circle': self._circle,
576 'lineMode': self._line_mode,
578 'rotate': lambda node: self.canvas.rotate(float(node.get('degrees'))),
579 'translate': self._translate,
582 for n in utils._child_get(node, self):
586 class _rml_draw(object):
587 def __init__(self, localcontext, node, styles, images=None, path='.', title=None):
590 self.localcontext = localcontext
596 self.canvas_title = title
598 def render(self, canvas, doc):
600 cnv = _rml_canvas(canvas, self.localcontext, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title)
601 cnv.render(self.node)
602 canvas.restoreState()
604 class _rml_Illustration(platypus.flowables.Flowable):
605 def __init__(self, node, localcontext, styles, self2):
606 self.localcontext = (localcontext or {}).copy()
609 self.width = utils.unit_get(node.get('width'))
610 self.height = utils.unit_get(node.get('height'))
612 def wrap(self, *args):
613 return self.width, self.height
615 drw = _rml_draw(self.localcontext ,self.node,self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title)
616 drw.render(self.canv, None)
618 # Workaround for issue #15: https://bitbucket.org/rptlab/reportlab/issue/15/infinite-pages-produced-when-splitting
619 original_pto_split = platypus.flowables.PTOContainer.split
620 def split(self, availWidth, availHeight):
621 res = original_pto_split(self, availWidth, availHeight)
622 if len(res) > 2 and len(self._content) > 0:
623 header = self._content[0]._ptoinfo.header
624 trailer = self._content[0]._ptoinfo.trailer
625 if isinstance(res[-2], platypus.flowables.UseUpSpace) and len(header + trailer) == len(res[:-2]):
628 platypus.flowables.PTOContainer.split = split
630 class _rml_flowable(object):
631 def __init__(self, doc, localcontext, images=None, path='.', title=None, canvas=None):
634 self.localcontext = localcontext
636 self.styles = doc.styles
642 def _textual(self, node):
643 rc1 = utils._process_text(self, node.text or '')
644 for n in utils._child_get(node,self):
645 txt_n = copy.deepcopy(n)
646 for key in txt_n.attrib.keys():
647 if key in ('rml_except', 'rml_loop', 'rml_tag'):
648 del txt_n.attrib[key]
649 if not n.tag == 'bullet':
650 if n.tag == 'pageNumber':
651 txt_n.text = self.canvas and str(self.canvas.getPageNumber()) or ''
653 txt_n.text = utils.xml2str(self._textual(n))
654 txt_n.tail = n.tail and utils.xml2str(utils._process_text(self, n.tail.replace('\n',''))) or ''
655 rc1 += etree.tostring(txt_n)
658 def _table(self, node):
659 children = utils._child_get(node,self,'tr')
671 st = copy.deepcopy(self.styles.table_styles[tr.get('style')])
672 for si in range(len(st._cmds)):
673 s = list(st._cmds[si])
674 s[1] = (s[1][0],posy)
675 s[2] = (s[2][0],posy)
676 st._cmds[si] = tuple(s)
678 if tr.get('paraStyle'):
679 paraStyle = self.styles.styles[tr.get('paraStyle')]
682 for td in utils._child_get(tr, self,'td'):
684 st = copy.deepcopy(self.styles.table_styles[td.get('style')])
691 if td.get('paraStyle'):
693 paraStyle = self.styles.styles[td.get('paraStyle')]
697 for n in utils._child_get(td, self):
698 if n.tag == etree.Comment:
701 fl = self._flowable(n, extra_style=paraStyle)
702 if isinstance(fl,list):
708 flow = self._textual(td)
710 if len(data2)>length:
713 while len(ab)<length:
715 while len(data2)<length:
720 if node.get('colWidths'):
721 assert length == len(node.get('colWidths').split(','))
722 colwidths = [utils.unit_get(f.strip()) for f in node.get('colWidths').split(',')]
723 if node.get('rowHeights'):
724 rowheights = [utils.unit_get(f.strip()) for f in node.get('rowHeights').split(',')]
725 if len(rowheights) == 1:
726 rowheights = rowheights[0]
727 table = platypus.LongTable(data = data, colWidths=colwidths, rowHeights=rowheights, **(utils.attr_get(node, ['splitByRow'] ,{'repeatRows':'int','repeatCols':'int'})))
728 if node.get('style'):
729 table.setStyle(self.styles.table_styles[node.get('style')])
734 def _illustration(self, node):
735 return _rml_Illustration(node, self.localcontext, self.styles, self)
737 def _textual_image(self, node):
738 return base64.decodestring(node.text)
740 def _pto(self, node):
744 for node in utils._child_get(node, self):
745 if node.tag == etree.Comment:
748 elif node.tag=='pto_header':
749 pto_header = self.render(node)
750 elif node.tag=='pto_trailer':
751 pto_trailer = self.render(node)
753 flow = self._flowable(node)
755 if isinstance(flow,list):
756 sub_story = sub_story + flow
758 sub_story.append(flow)
759 return platypus.flowables.PTOContainer(sub_story, trailer=pto_trailer, header=pto_header)
761 def _flowable(self, node, extra_style=None):
763 return self._pto(node)
765 style = self.styles.para_style_get(node)
767 style.__dict__.update(extra_style)
769 for i in self._textual(node).split('\n'):
770 result.append(platypus.Paragraph(i, style, **(utils.attr_get(node, [], {'bulletText':'str'}))))
772 elif node.tag=='barCode':
774 from reportlab.graphics.barcode import code128
775 from reportlab.graphics.barcode import code39
776 from reportlab.graphics.barcode import code93
777 from reportlab.graphics.barcode import common
778 from reportlab.graphics.barcode import fourstate
779 from reportlab.graphics.barcode import usps
780 from reportlab.graphics.barcode import createBarcodeDrawing
783 _logger.warning("Cannot use barcode renderers:", exc_info=True)
785 args = utils.attr_get(node, [], {'ratio':'float','xdim':'unit','height':'unit','checksum':'int','quiet':'int','width':'unit','stop':'bool','bearers':'int','barWidth':'float','barHeight':'float'})
787 'codabar': lambda x: common.Codabar(x, **args),
788 'code11': lambda x: common.Code11(x, **args),
789 'code128': lambda x: code128.Code128(str(x), **args),
790 'standard39': lambda x: code39.Standard39(str(x), **args),
791 'standard93': lambda x: code93.Standard93(str(x), **args),
792 'i2of5': lambda x: common.I2of5(x, **args),
793 'extended39': lambda x: code39.Extended39(str(x), **args),
794 'extended93': lambda x: code93.Extended93(str(x), **args),
795 'msi': lambda x: common.MSI(x, **args),
796 'fim': lambda x: usps.FIM(x, **args),
797 'postnet': lambda x: usps.POSTNET(x, **args),
798 'ean13': lambda x: createBarcodeDrawing('EAN13', value=str(x), **args),
799 'qrcode': lambda x: createBarcodeDrawing('QR', value=x, **args),
803 code = node.get('code').lower()
804 return codes[code](self._textual(node))
805 elif node.tag=='name':
806 self.styles.names[ node.get('id')] = node.get('value')
808 elif node.tag=='xpre':
809 style = self.styles.para_style_get(node)
810 return platypus.XPreformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int','frags':'int'})))
811 elif node.tag=='pre':
812 style = self.styles.para_style_get(node)
813 return platypus.Preformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int'})))
814 elif node.tag=='illustration':
815 return self._illustration(node)
816 elif node.tag=='blockTable':
817 return self._table(node)
818 elif node.tag=='title':
819 styles = reportlab.lib.styles.getSampleStyleSheet()
820 style = styles['Title']
821 return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
822 elif re.match('^h([1-9]+[0-9]*)$', (node.tag or '')):
823 styles = reportlab.lib.styles.getSampleStyleSheet()
824 style = styles['Heading'+str(node.tag[1:])]
825 return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
826 elif node.tag=='image':
828 if not node.get('file'):
830 if node.get('name') in self.doc.images:
831 _logger.debug("Image %s read ", node.get('name'))
832 image_data = self.doc.images[node.get('name')].read()
834 _logger.warning("Image %s not defined", node.get('name'))
839 if self.localcontext:
840 newtext = utils._process_text(self, node.text or '')
841 image_data = base64.decodestring(newtext)
843 _logger.debug("No inline image data")
845 image = StringIO(image_data)
847 _logger.debug("Image get from file %s", node.get('file'))
848 image = _open_image(node.get('file'), path=self.doc.path)
849 return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
850 elif node.tag=='spacer':
851 if node.get('width'):
852 width = utils.unit_get(node.get('width'))
854 width = utils.unit_get('1cm')
855 length = utils.unit_get(node.get('length'))
856 return platypus.Spacer(width=width, height=length)
857 elif node.tag=='section':
858 return self.render(node)
859 elif node.tag == 'pageNumberReset':
861 elif node.tag in ('pageBreak', 'nextPage'):
862 return platypus.PageBreak()
863 elif node.tag=='condPageBreak':
864 return platypus.CondPageBreak(**(utils.attr_get(node, ['height'])))
865 elif node.tag=='setNextTemplate':
866 return platypus.NextPageTemplate(str(node.get('name')))
867 elif node.tag=='nextFrame':
868 return platypus.CondPageBreak(1000) # TODO: change the 1000 !
869 elif node.tag == 'setNextFrame':
870 from reportlab.platypus.doctemplate import NextFrameFlowable
871 return NextFrameFlowable(str(node.get('name')))
872 elif node.tag == 'currentFrame':
873 from reportlab.platypus.doctemplate import CurrentFrameFlowable
874 return CurrentFrameFlowable(str(node.get('name')))
875 elif node.tag == 'frameEnd':
876 return EndFrameFlowable()
877 elif node.tag == 'hr':
878 width_hr=node.get('width') or '100%'
879 color_hr=node.get('color') or 'black'
880 thickness_hr=node.get('thickness') or 1
881 lineCap_hr=node.get('lineCap') or 'round'
882 return platypus.flowables.HRFlowable(width=width_hr,color=color.get(color_hr),thickness=float(thickness_hr),lineCap=str(lineCap_hr))
884 sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.tag,))
887 def render(self, node_story):
888 def process_story(node_story):
890 for node in utils._child_get(node_story, self):
891 if node.tag == etree.Comment:
894 flow = self._flowable(node)
896 if isinstance(flow,list):
897 sub_story = sub_story + flow
899 sub_story.append(flow)
901 return process_story(node_story)
904 class EndFrameFlowable(ActionFlowable):
905 def __init__(self,resume=0):
906 ActionFlowable.__init__(self,('frameEnd',resume))
908 class TinyDocTemplate(platypus.BaseDocTemplate):
910 def beforeDocument(self):
911 # Store some useful value directly inside canvas, so it's available
912 # on flowable drawing (needed for proper PageCount handling)
913 self.canv._doPageReset = False
914 self.canv._storyCount = 0
916 def ___handle_pageBegin(self):
918 self.pageTemplate.beforeDrawPage(self.canv,self)
919 self.pageTemplate.checkPageSize(self.canv,self)
920 self.pageTemplate.onPage(self.canv,self)
921 for f in self.pageTemplate.frames: f._reset()
923 self._curPageFlowableCount = 0
924 if hasattr(self,'_nextFrameIndex'):
925 del self._nextFrameIndex
926 for f in self.pageTemplate.frames:
930 self.handle_frameBegin()
933 if self.canv._doPageReset:
934 # Following a <pageReset/> tag:
935 # - we reset page number to 0
936 # - we add an new PageCount flowable (relative to the current
937 # story number), but not for NumeredCanvas at is handle page
939 # NOTE: _rml_template render() method add a PageReset flowable at end
940 # of each story, so we're sure to pass here at least once per story.
941 if not isinstance(self.canv, NumberedCanvas):
942 self.handle_flowable([ PageCount(story_count=self.canv._storyCount) ])
943 self.canv._pageCount = self.page
945 self.canv._flag = True
946 self.canv._pageNumber = 0
947 self.canv._doPageReset = False
948 self.canv._storyCount += 1
950 class _rml_template(object):
951 def __init__(self, localcontext, out, node, doc, images=None, path='.', title=None):
955 localcontext={'internal_header':True}
956 self.localcontext = localcontext
961 pagesize_map = {'a4': A4,
965 if self.localcontext.get('company'):
966 pageSize = pagesize_map.get(self.localcontext.get('company').paper_format, A4)
967 if node.get('pageSize'):
968 ps = map(lambda x:x.strip(), node.get('pageSize').replace(')', '').replace('(', '').split(','))
969 pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
971 self.doc_tmpl = TinyDocTemplate(out, pagesize=pageSize, **utils.attr_get(node, ['leftMargin','rightMargin','topMargin','bottomMargin'], {'allowSplitting':'int','showBoundary':'bool','rotation':'int','title':'str','author':'str'}))
972 self.page_templates = []
973 self.styles = doc.styles
976 pts = node.findall('pageTemplate')
979 for frame_el in pt.findall('frame'):
980 frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) )
981 if utils.attr_get(frame_el, ['last']):
982 frame.lastFrame = True
983 frames.append( frame )
985 gr = pt.findall('pageGraphics')\
986 or pt[1].findall('pageGraphics')
987 except Exception: # FIXME: be even more specific, perhaps?
990 # self.image=[ n for n in utils._child_get(gr[0], self) if n.tag=='image' or not self.localcontext]
991 drw = _rml_draw(self.localcontext,gr[0], self.doc, images=images, path=self.path, title=self.title)
992 self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
994 drw = _rml_draw(self.localcontext,node,self.doc,title=self.title)
995 self.page_templates.append( platypus.PageTemplate(frames=frames,onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
996 self.doc_tmpl.addPageTemplates(self.page_templates)
998 def render(self, node_stories):
999 if self.localcontext and not self.localcontext.get('internal_header',False):
1000 del self.localcontext['internal_header']
1002 r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title, canvas=None)
1004 for node_story in node_stories:
1006 # Reset Page Number with new story tag
1007 fis.append(PageReset())
1008 fis.append(platypus.PageBreak())
1009 fis += r.render(node_story)
1012 if self.localcontext and self.localcontext.get('internal_header',False):
1013 self.doc_tmpl.afterFlowable(fis)
1014 self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas)
1016 self.doc_tmpl.build(fis)
1017 except platypus.doctemplate.LayoutError, e:
1018 e.name = 'Print Error'
1019 e.value = 'The document you are trying to print contains a table row that does not fit on one page. Please try to split it in smaller rows or contact your administrator.'
1022 def parseNode(rml, localcontext=None, fout=None, images=None, path='.', title=None):
1023 node = etree.XML(rml)
1024 r = _rml_doc(node, localcontext, images, path, title=title)
1025 #try to override some font mappings
1027 from customfonts import SetCustomFonts
1030 # means there is no custom fonts mapping in this system.
1033 _logger.warning('Cannot set font mapping', exc_info=True)
1037 return fp.getvalue()
1039 def parseString(rml, localcontext=None, fout=None, images=None, path='.', title=None):
1040 node = etree.XML(rml)
1041 r = _rml_doc(node, localcontext, images, path, title=title)
1043 #try to override some font mappings
1045 from customfonts import SetCustomFonts
1051 fp = file(fout,'wb')
1058 return fp.getvalue()
1060 def trml2pdf_help():
1061 print 'Usage: trml2pdf input.rml >output.pdf'
1062 print 'Render the standard input (RML) and output a PDF file'
1065 if __name__=="__main__":
1067 if sys.argv[1]=='--help':
1069 print parseString(file(sys.argv[1], 'r').read()),
1071 print 'Usage: trml2pdf input.rml >output.pdf'
1072 print 'Try \'trml2pdf --help\' for more information.'
1075 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: