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 = []
94 """add page info to each page (page x of y)"""
95 for state in self._saved_page_states:
96 self.__dict__.update(state)
97 self.draw_page_number()
98 canvas.Canvas.showPage(self)
99 canvas.Canvas.save(self)
101 def draw_page_number(self):
102 page_count = len(self._saved_page_states)
103 self.setFont("Helvetica", 8)
104 self.drawRightString((self._pagesize[0]-30), (self._pagesize[1]-40),
105 " %(this)i / %(total)i" % {
106 'this': self._pageNumber,
112 class PageCount(platypus.Flowable):
113 def __init__(self, story_count=0):
114 platypus.Flowable.__init__(self)
115 self.story_count = story_count
118 self.canv.beginForm("pageCount%d" % self.story_count)
119 self.canv.setFont("Helvetica", utils.unit_get(str(8)))
120 self.canv.drawString(0, 0, str(self.canv.getPageNumber()))
123 class PageReset(platypus.Flowable):
125 """Flag to close current story page numbering and prepare for the next
126 should be executed after the rendering of the full story"""
127 self.canv._doPageReset = True
129 class _rml_styles(object,):
130 def __init__(self, nodes, localcontext):
131 self.localcontext = localcontext
135 self.table_styles = {}
136 self.default_style = reportlab.lib.styles.getSampleStyleSheet()
139 for style in node.findall('blockTableStyle'):
140 self.table_styles[style.get('id')] = self._table_style_get(style)
141 for style in node.findall('paraStyle'):
142 sname = style.get('name')
143 self.styles[sname] = self._para_style_update(style)
144 if self.default_style.has_key(sname):
145 for key, value in self.styles[sname].items():
146 setattr(self.default_style[sname], key, value)
148 self.styles_obj[sname] = reportlab.lib.styles.ParagraphStyle(sname, self.default_style["Normal"], **self.styles[sname])
149 for variable in node.findall('initialize'):
150 for name in variable.findall('name'):
151 self.names[ name.get('id')] = name.get('value')
153 def _para_style_update(self, node):
155 for attr in ['textColor', 'backColor', 'bulletColor', 'borderColor']:
157 data[attr] = color.get(node.get(attr))
158 for attr in ['bulletFontName', 'fontName']:
160 fontname= select_fontname(node.get(attr), None)
161 if fontname is not None:
162 data['fontName'] = fontname
163 for attr in ['bulletText']:
165 data[attr] = node.get(attr)
166 for attr in ['fontSize', 'leftIndent', 'rightIndent', 'spaceBefore', 'spaceAfter',
167 'firstLineIndent', 'bulletIndent', 'bulletFontSize', 'leading',
168 'borderWidth','borderPadding','borderRadius']:
170 data[attr] = utils.unit_get(node.get(attr))
171 if node.get('alignment'):
173 'right':reportlab.lib.enums.TA_RIGHT,
174 'center':reportlab.lib.enums.TA_CENTER,
175 'justify':reportlab.lib.enums.TA_JUSTIFY
177 data['alignment'] = align.get(node.get('alignment').lower(), reportlab.lib.enums.TA_LEFT)
180 def _table_style_get(self, style_node):
182 for node in style_node:
183 start = utils.tuple_int_get(node, 'start', (0,0) )
184 stop = utils.tuple_int_get(node, 'stop', (-1,-1) )
185 if node.tag=='blockValign':
186 styles.append(('VALIGN', start, stop, str(node.get('value'))))
187 elif node.tag=='blockFont':
188 styles.append(('FONT', start, stop, str(node.get('name'))))
189 elif node.tag=='blockTextColor':
190 styles.append(('TEXTCOLOR', start, stop, color.get(str(node.get('colorName')))))
191 elif node.tag=='blockLeading':
192 styles.append(('LEADING', start, stop, utils.unit_get(node.get('length'))))
193 elif node.tag=='blockAlignment':
194 styles.append(('ALIGNMENT', start, stop, str(node.get('value'))))
195 elif node.tag=='blockSpan':
196 styles.append(('SPAN', start, stop))
197 elif node.tag=='blockLeftPadding':
198 styles.append(('LEFTPADDING', start, stop, utils.unit_get(node.get('length'))))
199 elif node.tag=='blockRightPadding':
200 styles.append(('RIGHTPADDING', start, stop, utils.unit_get(node.get('length'))))
201 elif node.tag=='blockTopPadding':
202 styles.append(('TOPPADDING', start, stop, utils.unit_get(node.get('length'))))
203 elif node.tag=='blockBottomPadding':
204 styles.append(('BOTTOMPADDING', start, stop, utils.unit_get(node.get('length'))))
205 elif node.tag=='blockBackground':
206 styles.append(('BACKGROUND', start, stop, color.get(node.get('colorName'))))
208 styles.append(('FONTSIZE', start, stop, utils.unit_get(node.get('size'))))
209 elif node.tag=='lineStyle':
210 kind = node.get('kind')
211 kind_list = [ 'GRID', 'BOX', 'OUTLINE', 'INNERGRID', 'LINEBELOW', 'LINEABOVE','LINEBEFORE', 'LINEAFTER' ]
212 assert kind in kind_list
214 if node.get('thickness'):
215 thick = float(node.get('thickness'))
216 styles.append((kind, start, stop, thick, color.get(node.get('colorName'))))
217 return platypus.tables.TableStyle(styles)
219 def para_style_get(self, node):
221 sname = node.get('style')
223 if sname in self.styles_obj:
224 style = self.styles_obj[sname]
226 _logger.debug('Warning: style not found, %s - setting default!', node.get('style'))
228 style = self.default_style['Normal']
229 para_update = self._para_style_update(node)
231 # update style only is necessary
232 style = copy.deepcopy(style)
233 style.__dict__.update(para_update)
236 class _rml_doc(object):
237 def __init__(self, node, localcontext=None, images=None, path='.', title=None):
240 if localcontext is None:
242 self.localcontext = localcontext
244 self.filename = self.etree.get('filename')
249 def docinit(self, els):
250 from reportlab.lib.fonts import addMapping
251 from reportlab.pdfbase import pdfmetrics
252 from reportlab.pdfbase.ttfonts import TTFont
256 for font in node.findall('registerFont'):
257 name = font.get('fontName').encode('ascii')
258 fname = font.get('fontFile').encode('ascii')
259 if name not in pdfmetrics._fonts:
260 pdfmetrics.registerFont(TTFont(name, fname))
261 #by default, we map the fontName to each style (bold, italic, bold and italic), so that
262 #if there isn't any font defined for one of these style (via a font family), the system
263 #will fallback on the normal font.
264 addMapping(name, 0, 0, name) #normal
265 addMapping(name, 0, 1, name) #italic
266 addMapping(name, 1, 0, name) #bold
267 addMapping(name, 1, 1, name) #italic and bold
269 #if registerFontFamily is defined, we register the mapping of the fontName to use for each style.
270 for font_family in node.findall('registerFontFamily'):
271 family_name = font_family.get('normal').encode('ascii')
272 if font_family.get('italic'):
273 addMapping(family_name, 0, 1, font_family.get('italic').encode('ascii'))
274 if font_family.get('bold'):
275 addMapping(family_name, 1, 0, font_family.get('bold').encode('ascii'))
276 if font_family.get('boldItalic'):
277 addMapping(family_name, 1, 1, font_family.get('boldItalic').encode('ascii'))
279 def setTTFontMapping(self,face, fontname, filename, mode='all'):
280 from reportlab.lib.fonts import addMapping
281 from reportlab.pdfbase import pdfmetrics
282 from reportlab.pdfbase.ttfonts import TTFont
284 if fontname not in pdfmetrics._fonts:
285 pdfmetrics.registerFont(TTFont(fontname, filename))
287 addMapping(face, 0, 0, fontname) #normal
288 addMapping(face, 0, 1, fontname) #italic
289 addMapping(face, 1, 0, fontname) #bold
290 addMapping(face, 1, 1, fontname) #italic and bold
291 elif (mode== 'normal') or (mode == 'regular'):
292 addMapping(face, 0, 0, fontname) #normal
293 elif mode == 'italic':
294 addMapping(face, 0, 1, fontname) #italic
296 addMapping(face, 1, 0, fontname) #bold
297 elif mode == 'bolditalic':
298 addMapping(face, 1, 1, fontname) #italic and bold
300 def _textual_image(self, node):
303 rc +=( etree.tostring(n) or '') + n.tail
304 return base64.decodestring(node.tostring())
306 def _images(self, el):
308 for node in el.findall('.//image'):
309 rc =( node.text or '')
310 result[node.get('name')] = base64.decodestring(rc)
313 def render(self, out):
314 el = self.etree.findall('.//docinit')
318 el = self.etree.findall('.//stylesheet')
319 self.styles = _rml_styles(el,self.localcontext)
321 el = self.etree.findall('.//images')
323 self.images.update( self._images(el[0]) )
325 el = self.etree.findall('.//template')
327 pt_obj = _rml_template(self.localcontext, out, el[0], self, images=self.images, path=self.path, title=self.title)
328 el = utils._child_get(self.etree, self, 'story')
331 self.canvas = canvas.Canvas(out)
332 pd = self.etree.find('pageDrawing')[0]
333 pd_obj = _rml_canvas(self.canvas, self.localcontext, None, self, self.images, path=self.path, title=self.title)
336 self.canvas.showPage()
339 class _rml_canvas(object):
340 def __init__(self, canvas, localcontext, doc_tmpl=None, doc=None, images=None, path='.', title=None):
343 self.localcontext = localcontext
345 self.styles = doc.styles
346 self.doc_tmpl = doc_tmpl
352 self.canvas.setTitle(self.title)
354 def _textual(self, node, x=0, y=0):
355 text = node.text and node.text.encode('utf-8') or ''
356 rc = utils._process_text(self, text)
359 from reportlab.lib.sequencer import getSequencer
361 rc += str(seq.next(n.get('id')))
362 if n.tag == 'pageCount':
364 self.canvas.translate(x,y)
365 self.canvas.doForm('pageCount%s' % (self.canvas._storyCount,))
367 self.canvas.translate(-x,-y)
368 if n.tag == 'pageNumber':
369 rc += str(self.canvas.getPageNumber())
370 rc += utils._process_text(self, n.tail)
371 return rc.replace('\n','')
373 def _drawString(self, node):
374 v = utils.attr_get(node, ['x','y'])
375 text=self._textual(node, **v)
376 text = utils.xml2str(text)
378 self.canvas.drawString(text=text, **v)
379 except TypeError as e:
380 _logger.error("Bad RML: <drawString> tag requires attributes 'x' and 'y'!")
383 def _drawCenteredString(self, node):
384 v = utils.attr_get(node, ['x','y'])
385 text=self._textual(node, **v)
386 text = utils.xml2str(text)
387 self.canvas.drawCentredString(text=text, **v)
389 def _drawRightString(self, node):
390 v = utils.attr_get(node, ['x','y'])
391 text=self._textual(node, **v)
392 text = utils.xml2str(text)
393 self.canvas.drawRightString(text=text, **v)
395 def _rect(self, node):
396 if node.get('round'):
397 self.canvas.roundRect(radius=utils.unit_get(node.get('round')), **utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
399 self.canvas.rect(**utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
401 def _ellipse(self, node):
402 x1 = utils.unit_get(node.get('x'))
403 x2 = utils.unit_get(node.get('width'))
404 y1 = utils.unit_get(node.get('y'))
405 y2 = utils.unit_get(node.get('height'))
407 self.canvas.ellipse(x1,y1,x2,y2, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
409 def _curves(self, node):
410 line_str = node.text.split()
412 while len(line_str)>7:
413 self.canvas.bezier(*[utils.unit_get(l) for l in line_str[0:8]])
414 line_str = line_str[8:]
416 def _lines(self, node):
417 line_str = node.text.split()
419 while len(line_str)>3:
420 lines.append([utils.unit_get(l) for l in line_str[0:4]])
421 line_str = line_str[4:]
422 self.canvas.lines(lines)
424 def _grid(self, node):
425 xlist = [utils.unit_get(s) for s in node.get('xs').split(',')]
426 ylist = [utils.unit_get(s) for s in node.get('ys').split(',')]
428 self.canvas.grid(xlist, ylist)
430 def _translate(self, node):
431 dx = utils.unit_get(node.get('dx')) or 0
432 dy = utils.unit_get(node.get('dy')) or 0
433 self.canvas.translate(dx,dy)
435 def _circle(self, node):
436 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'}))
438 def _place(self, node):
439 flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title, canvas=self.canvas).render(node)
440 infos = utils.attr_get(node, ['x','y','width','height'])
442 infos['y']+=infos['height']
444 w,h = flow.wrap(infos['width'], infos['height'])
445 if w<=infos['width'] and h<=infos['height']:
447 flow.drawOn(self.canvas,infos['x'],infos['y'])
450 raise ValueError("Not enough space")
452 def _line_mode(self, node):
453 ljoin = {'round':1, 'mitered':0, 'bevelled':2}
454 lcap = {'default':0, 'round':1, 'square':2}
456 if node.get('width'):
457 self.canvas.setLineWidth(utils.unit_get(node.get('width')))
459 self.canvas.setLineJoin(ljoin[node.get('join')])
461 self.canvas.setLineCap(lcap[node.get('cap')])
462 if node.get('miterLimit'):
463 self.canvas.setDash(utils.unit_get(node.get('miterLimit')))
465 dashes = node.get('dash').split(',')
466 for x in range(len(dashes)):
467 dashes[x]=utils.unit_get(dashes[x])
468 self.canvas.setDash(node.get('dash').split(','))
470 def _image(self, node):
473 from reportlab.lib.utils import ImageReader
474 nfile = node.get('file')
477 image_data = self.images[node.get('name')]
478 _logger.debug("Image %s used", node.get('name'))
479 s = StringIO(image_data)
482 if self.localcontext:
483 res = utils._regex.findall(newtext)
485 newtext = eval(key, {}, self.localcontext) or ''
488 image_data = base64.decodestring(newtext)
490 s = StringIO(image_data)
492 _logger.debug("No image data!")
495 if nfile in self.images:
496 s = StringIO(self.images[nfile])
499 up = urlparse.urlparse(str(nfile))
503 # RFC: do we really want to open external URLs?
504 # Are we safe from cross-site scripting or attacks?
505 _logger.debug("Retrieve image from %s", nfile)
506 u = urllib.urlopen(str(nfile))
507 s = StringIO(u.read())
509 _logger.debug("Open image file %s ", nfile)
510 s = _open_image(nfile, path=self.path)
513 (sx,sy) = img.getSize()
514 _logger.debug("Image is %dx%d", sx, sy)
515 args = { 'x': 0.0, 'y': 0.0, 'mask': 'auto'}
516 for tag in ('width','height','x','y'):
518 args[tag] = utils.unit_get(node.get(tag))
519 if ('width' in args) and (not 'height' in args):
520 args['height'] = sy * args['width'] / sx
521 elif ('height' in args) and (not 'width' in args):
522 args['width'] = sx * args['height'] / sy
523 elif ('width' in args) and ('height' in args):
524 if (float(args['width'])/args['height'])>(float(sx)>sy):
525 args['width'] = sx * args['height'] / sy
527 args['height'] = sy * args['width'] / sx
528 self.canvas.drawImage(img, **args)
531 # self.canvas._doc.SaveToFile(self.canvas._filename, self.canvas)
533 def _path(self, node):
534 self.path = self.canvas.beginPath()
535 self.path.moveTo(**utils.attr_get(node, ['x','y']))
536 for n in utils._child_get(node, self):
539 vals = utils.text_get(n).split()
540 self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1]))
541 elif n.tag=='curvesto':
542 vals = utils.text_get(n).split()
546 pos.append(utils.unit_get(vals.pop(0)))
547 self.path.curveTo(*pos)
549 data = n.text.split() # Not sure if I must merge all TEXT_NODE ?
551 x = utils.unit_get(data.pop(0))
552 y = utils.unit_get(data.pop(0))
553 self.path.lineTo(x,y)
554 if (not node.get('close')) or utils.bool_get(node.get('close')):
556 self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
558 def setFont(self, node):
559 fontname = select_fontname(node.get('name'), self.canvas._fontname)
560 return self.canvas.setFont(fontname, utils.unit_get(node.get('size')))
562 def render(self, node):
564 'drawCentredString': self._drawCenteredString,
565 'drawRightString': self._drawRightString,
566 'drawString': self._drawString,
568 'ellipse': self._ellipse,
569 'lines': self._lines,
571 'curves': self._curves,
572 'fill': lambda node: self.canvas.setFillColor(color.get(node.get('color'))),
573 'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.get('color'))),
574 'setFont': self.setFont ,
575 'place': self._place,
576 'circle': self._circle,
577 'lineMode': self._line_mode,
579 'rotate': lambda node: self.canvas.rotate(float(node.get('degrees'))),
580 'translate': self._translate,
583 for n in utils._child_get(node, self):
587 class _rml_draw(object):
588 def __init__(self, localcontext, node, styles, images=None, path='.', title=None):
591 self.localcontext = localcontext
597 self.canvas_title = title
599 def render(self, canvas, doc):
601 cnv = _rml_canvas(canvas, self.localcontext, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title)
602 cnv.render(self.node)
603 canvas.restoreState()
605 class _rml_Illustration(platypus.flowables.Flowable):
606 def __init__(self, node, localcontext, styles, self2):
607 self.localcontext = (localcontext or {}).copy()
610 self.width = utils.unit_get(node.get('width'))
611 self.height = utils.unit_get(node.get('height'))
613 def wrap(self, *args):
614 return self.width, self.height
616 drw = _rml_draw(self.localcontext ,self.node,self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title)
617 drw.render(self.canv, None)
619 # Workaround for issue #15: https://bitbucket.org/rptlab/reportlab/issue/15/infinite-pages-produced-when-splitting
620 original_pto_split = platypus.flowables.PTOContainer.split
621 def split(self, availWidth, availHeight):
622 res = original_pto_split(self, availWidth, availHeight)
623 if len(res) > 2 and len(self._content) > 0:
624 header = self._content[0]._ptoinfo.header
625 trailer = self._content[0]._ptoinfo.trailer
626 if isinstance(res[-2], platypus.flowables.UseUpSpace) and len(header + trailer) == len(res[:-2]):
629 platypus.flowables.PTOContainer.split = split
631 class _rml_flowable(object):
632 def __init__(self, doc, localcontext, images=None, path='.', title=None, canvas=None):
635 self.localcontext = localcontext
637 self.styles = doc.styles
643 def _textual(self, node):
644 rc1 = utils._process_text(self, node.text or '')
645 for n in utils._child_get(node,self):
646 txt_n = copy.deepcopy(n)
647 for key in txt_n.attrib.keys():
648 if key in ('rml_except', 'rml_loop', 'rml_tag'):
649 del txt_n.attrib[key]
650 if not n.tag == 'bullet':
651 if n.tag == 'pageNumber':
652 txt_n.text = self.canvas and str(self.canvas.getPageNumber()) or ''
654 txt_n.text = utils.xml2str(self._textual(n))
655 txt_n.tail = n.tail and utils.xml2str(utils._process_text(self, n.tail.replace('\n',''))) or ''
656 rc1 += etree.tostring(txt_n)
659 def _table(self, node):
660 children = utils._child_get(node,self,'tr')
672 st = copy.deepcopy(self.styles.table_styles[tr.get('style')])
673 for si in range(len(st._cmds)):
674 s = list(st._cmds[si])
675 s[1] = (s[1][0],posy)
676 s[2] = (s[2][0],posy)
677 st._cmds[si] = tuple(s)
679 if tr.get('paraStyle'):
680 paraStyle = self.styles.styles[tr.get('paraStyle')]
683 for td in utils._child_get(tr, self,'td'):
685 st = copy.deepcopy(self.styles.table_styles[td.get('style')])
692 if td.get('paraStyle'):
694 paraStyle = self.styles.styles[td.get('paraStyle')]
698 for n in utils._child_get(td, self):
699 if n.tag == etree.Comment:
702 fl = self._flowable(n, extra_style=paraStyle)
703 if isinstance(fl,list):
709 flow = self._textual(td)
711 if len(data2)>length:
714 while len(ab)<length:
716 while len(data2)<length:
721 if node.get('colWidths'):
722 assert length == len(node.get('colWidths').split(','))
723 colwidths = [utils.unit_get(f.strip()) for f in node.get('colWidths').split(',')]
724 if node.get('rowHeights'):
725 rowheights = [utils.unit_get(f.strip()) for f in node.get('rowHeights').split(',')]
726 if len(rowheights) == 1:
727 rowheights = rowheights[0]
728 table = platypus.LongTable(data = data, colWidths=colwidths, rowHeights=rowheights, **(utils.attr_get(node, ['splitByRow'] ,{'repeatRows':'int','repeatCols':'int'})))
729 if node.get('style'):
730 table.setStyle(self.styles.table_styles[node.get('style')])
735 def _illustration(self, node):
736 return _rml_Illustration(node, self.localcontext, self.styles, self)
738 def _textual_image(self, node):
739 return base64.decodestring(node.text)
741 def _pto(self, node):
745 for node in utils._child_get(node, self):
746 if node.tag == etree.Comment:
749 elif node.tag=='pto_header':
750 pto_header = self.render(node)
751 elif node.tag=='pto_trailer':
752 pto_trailer = self.render(node)
754 flow = self._flowable(node)
756 if isinstance(flow,list):
757 sub_story = sub_story + flow
759 sub_story.append(flow)
760 return platypus.flowables.PTOContainer(sub_story, trailer=pto_trailer, header=pto_header)
762 def _flowable(self, node, extra_style=None):
764 return self._pto(node)
766 style = self.styles.para_style_get(node)
768 style.__dict__.update(extra_style)
770 textuals = self._textual(node).split('\n')
771 keep_empty_lines = (len(textuals) > 1) and len(node.text.strip())
773 if keep_empty_lines and len(i.strip()) == 0:
774 i = '<font color="white"> </font>'
778 utils.attr_get(node, [], {'bulletText':'str'}))
782 elif node.tag=='barCode':
784 from reportlab.graphics.barcode import code128
785 from reportlab.graphics.barcode import code39
786 from reportlab.graphics.barcode import code93
787 from reportlab.graphics.barcode import common
788 from reportlab.graphics.barcode import fourstate
789 from reportlab.graphics.barcode import usps
790 from reportlab.graphics.barcode import createBarcodeDrawing
793 _logger.warning("Cannot use barcode renderers:", exc_info=True)
795 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'})
797 'codabar': lambda x: common.Codabar(x, **args),
798 'code11': lambda x: common.Code11(x, **args),
799 'code128': lambda x: code128.Code128(str(x), **args),
800 'standard39': lambda x: code39.Standard39(str(x), **args),
801 'standard93': lambda x: code93.Standard93(str(x), **args),
802 'i2of5': lambda x: common.I2of5(x, **args),
803 'extended39': lambda x: code39.Extended39(str(x), **args),
804 'extended93': lambda x: code93.Extended93(str(x), **args),
805 'msi': lambda x: common.MSI(x, **args),
806 'fim': lambda x: usps.FIM(x, **args),
807 'postnet': lambda x: usps.POSTNET(x, **args),
808 'ean13': lambda x: createBarcodeDrawing('EAN13', value=str(x), **args),
809 'qrcode': lambda x: createBarcodeDrawing('QR', value=x, **args),
813 code = node.get('code').lower()
814 return codes[code](self._textual(node))
815 elif node.tag=='name':
816 self.styles.names[ node.get('id')] = node.get('value')
818 elif node.tag=='xpre':
819 style = self.styles.para_style_get(node)
820 return platypus.XPreformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int','frags':'int'})))
821 elif node.tag=='pre':
822 style = self.styles.para_style_get(node)
823 return platypus.Preformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int'})))
824 elif node.tag=='illustration':
825 return self._illustration(node)
826 elif node.tag=='blockTable':
827 return self._table(node)
828 elif node.tag=='title':
829 styles = reportlab.lib.styles.getSampleStyleSheet()
830 style = styles['Title']
831 return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
832 elif re.match('^h([1-9]+[0-9]*)$', (node.tag or '')):
833 styles = reportlab.lib.styles.getSampleStyleSheet()
834 style = styles['Heading'+str(node.tag[1:])]
835 return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
836 elif node.tag=='image':
838 if not node.get('file'):
840 if node.get('name') in self.doc.images:
841 _logger.debug("Image %s read ", node.get('name'))
842 image_data = self.doc.images[node.get('name')].read()
844 _logger.warning("Image %s not defined", node.get('name'))
849 if self.localcontext:
850 newtext = utils._process_text(self, node.text or '')
851 image_data = base64.decodestring(newtext)
853 _logger.debug("No inline image data")
855 image = StringIO(image_data)
857 _logger.debug("Image get from file %s", node.get('file'))
858 image = _open_image(node.get('file'), path=self.doc.path)
859 return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
860 elif node.tag=='spacer':
861 if node.get('width'):
862 width = utils.unit_get(node.get('width'))
864 width = utils.unit_get('1cm')
865 length = utils.unit_get(node.get('length'))
866 return platypus.Spacer(width=width, height=length)
867 elif node.tag=='section':
868 return self.render(node)
869 elif node.tag == 'pageNumberReset':
871 elif node.tag in ('pageBreak', 'nextPage'):
872 return platypus.PageBreak()
873 elif node.tag=='condPageBreak':
874 return platypus.CondPageBreak(**(utils.attr_get(node, ['height'])))
875 elif node.tag=='setNextTemplate':
876 return platypus.NextPageTemplate(str(node.get('name')))
877 elif node.tag=='nextFrame':
878 return platypus.CondPageBreak(1000) # TODO: change the 1000 !
879 elif node.tag == 'setNextFrame':
880 from reportlab.platypus.doctemplate import NextFrameFlowable
881 return NextFrameFlowable(str(node.get('name')))
882 elif node.tag == 'currentFrame':
883 from reportlab.platypus.doctemplate import CurrentFrameFlowable
884 return CurrentFrameFlowable(str(node.get('name')))
885 elif node.tag == 'frameEnd':
886 return EndFrameFlowable()
887 elif node.tag == 'hr':
888 width_hr=node.get('width') or '100%'
889 color_hr=node.get('color') or 'black'
890 thickness_hr=node.get('thickness') or 1
891 lineCap_hr=node.get('lineCap') or 'round'
892 return platypus.flowables.HRFlowable(width=width_hr,color=color.get(color_hr),thickness=float(thickness_hr),lineCap=str(lineCap_hr))
894 sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.tag,))
897 def render(self, node_story):
898 def process_story(node_story):
900 for node in utils._child_get(node_story, self):
901 if node.tag == etree.Comment:
904 flow = self._flowable(node)
906 if isinstance(flow,list):
907 sub_story = sub_story + flow
909 sub_story.append(flow)
911 return process_story(node_story)
914 class EndFrameFlowable(ActionFlowable):
915 def __init__(self,resume=0):
916 ActionFlowable.__init__(self,('frameEnd',resume))
918 class TinyDocTemplate(platypus.BaseDocTemplate):
920 def beforeDocument(self):
921 # Store some useful value directly inside canvas, so it's available
922 # on flowable drawing (needed for proper PageCount handling)
923 self.canv._doPageReset = False
924 self.canv._storyCount = 0
926 def ___handle_pageBegin(self):
928 self.pageTemplate.beforeDrawPage(self.canv,self)
929 self.pageTemplate.checkPageSize(self.canv,self)
930 self.pageTemplate.onPage(self.canv,self)
931 for f in self.pageTemplate.frames: f._reset()
933 self._curPageFlowableCount = 0
934 if hasattr(self,'_nextFrameIndex'):
935 del self._nextFrameIndex
936 for f in self.pageTemplate.frames:
940 self.handle_frameBegin()
943 if isinstance(self.canv, NumberedCanvas):
944 # save current page states before eventual reset
945 self.canv._saved_page_states.append(dict(self.canv.__dict__))
946 if self.canv._doPageReset:
947 # Following a <pageReset/> tag:
948 # - we reset page number to 0
949 # - we add an new PageCount flowable (relative to the current
950 # story number), but not for NumeredCanvas at is handle page
952 # NOTE: _rml_template render() method add a PageReset flowable at end
953 # of each story, so we're sure to pass here at least once per story.
954 if not isinstance(self.canv, NumberedCanvas):
955 self.handle_flowable([ PageCount(story_count=self.canv._storyCount) ])
956 self.canv._pageCount = self.page
958 self.canv._flag = True
959 self.canv._pageNumber = 0
960 self.canv._doPageReset = False
961 self.canv._storyCount += 1
963 class _rml_template(object):
964 def __init__(self, localcontext, out, node, doc, images=None, path='.', title=None):
968 localcontext={'internal_header':True}
969 self.localcontext = localcontext
974 pagesize_map = {'a4': A4,
978 if self.localcontext.get('company'):
979 pageSize = pagesize_map.get(self.localcontext.get('company').paper_format, A4)
980 if node.get('pageSize'):
981 ps = map(lambda x:x.strip(), node.get('pageSize').replace(')', '').replace('(', '').split(','))
982 pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
984 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'}))
985 self.page_templates = []
986 self.styles = doc.styles
989 pts = node.findall('pageTemplate')
992 for frame_el in pt.findall('frame'):
993 frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) )
994 if utils.attr_get(frame_el, ['last']):
995 frame.lastFrame = True
996 frames.append( frame )
998 gr = pt.findall('pageGraphics')\
999 or pt[1].findall('pageGraphics')
1000 except Exception: # FIXME: be even more specific, perhaps?
1003 # self.image=[ n for n in utils._child_get(gr[0], self) if n.tag=='image' or not self.localcontext]
1004 drw = _rml_draw(self.localcontext,gr[0], self.doc, images=images, path=self.path, title=self.title)
1005 self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
1007 drw = _rml_draw(self.localcontext,node,self.doc,title=self.title)
1008 self.page_templates.append( platypus.PageTemplate(frames=frames,onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
1009 self.doc_tmpl.addPageTemplates(self.page_templates)
1011 def render(self, node_stories):
1012 if self.localcontext and not self.localcontext.get('internal_header',False):
1013 del self.localcontext['internal_header']
1015 r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title, canvas=None)
1017 for node_story in node_stories:
1019 fis.append(platypus.PageBreak())
1020 fis += r.render(node_story)
1021 # end of story numbering computation
1022 fis.append(PageReset())
1025 if self.localcontext and self.localcontext.get('internal_header',False):
1026 self.doc_tmpl.afterFlowable(fis)
1027 self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas)
1029 self.doc_tmpl.build(fis)
1030 except platypus.doctemplate.LayoutError, e:
1031 e.name = 'Print Error'
1032 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.'
1035 def parseNode(rml, localcontext=None, fout=None, images=None, path='.', title=None):
1036 node = etree.XML(rml)
1037 r = _rml_doc(node, localcontext, images, path, title=title)
1038 #try to override some font mappings
1040 from customfonts import SetCustomFonts
1043 # means there is no custom fonts mapping in this system.
1046 _logger.warning('Cannot set font mapping', exc_info=True)
1050 return fp.getvalue()
1052 def parseString(rml, localcontext=None, fout=None, images=None, path='.', title=None):
1053 node = etree.XML(rml)
1054 r = _rml_doc(node, localcontext, images, path, title=title)
1056 #try to override some font mappings
1058 from customfonts import SetCustomFonts
1064 fp = file(fout,'wb')
1071 return fp.getvalue()
1073 def trml2pdf_help():
1074 print 'Usage: trml2pdf input.rml >output.pdf'
1075 print 'Render the standard input (RML) and output a PDF file'
1078 if __name__=="__main__":
1080 if sys.argv[1]=='--help':
1082 print parseString(file(sys.argv[1], 'r').read()),
1084 print 'Usage: trml2pdf input.rml >output.pdf'
1085 print 'Try \'trml2pdf --help\' for more information.'
1088 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: