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)
376 self.canvas.drawString(text=text, **v)
378 def _drawCenteredString(self, node):
379 v = utils.attr_get(node, ['x','y'])
380 text=self._textual(node, **v)
381 text = utils.xml2str(text)
382 self.canvas.drawCentredString(text=text, **v)
384 def _drawRightString(self, node):
385 v = utils.attr_get(node, ['x','y'])
386 text=self._textual(node, **v)
387 text = utils.xml2str(text)
388 self.canvas.drawRightString(text=text, **v)
390 def _rect(self, node):
391 if node.get('round'):
392 self.canvas.roundRect(radius=utils.unit_get(node.get('round')), **utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
394 self.canvas.rect(**utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'}))
396 def _ellipse(self, node):
397 x1 = utils.unit_get(node.get('x'))
398 x2 = utils.unit_get(node.get('width'))
399 y1 = utils.unit_get(node.get('y'))
400 y2 = utils.unit_get(node.get('height'))
402 self.canvas.ellipse(x1,y1,x2,y2, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
404 def _curves(self, node):
405 line_str = node.text.split()
407 while len(line_str)>7:
408 self.canvas.bezier(*[utils.unit_get(l) for l in line_str[0:8]])
409 line_str = line_str[8:]
411 def _lines(self, node):
412 line_str = node.text.split()
414 while len(line_str)>3:
415 lines.append([utils.unit_get(l) for l in line_str[0:4]])
416 line_str = line_str[4:]
417 self.canvas.lines(lines)
419 def _grid(self, node):
420 xlist = [utils.unit_get(s) for s in node.get('xs').split(',')]
421 ylist = [utils.unit_get(s) for s in node.get('ys').split(',')]
423 self.canvas.grid(xlist, ylist)
425 def _translate(self, node):
426 dx = utils.unit_get(node.get('dx')) or 0
427 dy = utils.unit_get(node.get('dy')) or 0
428 self.canvas.translate(dx,dy)
430 def _circle(self, node):
431 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'}))
433 def _place(self, node):
434 flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title, canvas=self.canvas).render(node)
435 infos = utils.attr_get(node, ['x','y','width','height'])
437 infos['y']+=infos['height']
439 w,h = flow.wrap(infos['width'], infos['height'])
440 if w<=infos['width'] and h<=infos['height']:
442 flow.drawOn(self.canvas,infos['x'],infos['y'])
445 raise ValueError("Not enough space")
447 def _line_mode(self, node):
448 ljoin = {'round':1, 'mitered':0, 'bevelled':2}
449 lcap = {'default':0, 'round':1, 'square':2}
451 if node.get('width'):
452 self.canvas.setLineWidth(utils.unit_get(node.get('width')))
454 self.canvas.setLineJoin(ljoin[node.get('join')])
456 self.canvas.setLineCap(lcap[node.get('cap')])
457 if node.get('miterLimit'):
458 self.canvas.setDash(utils.unit_get(node.get('miterLimit')))
460 dashes = node.get('dash').split(',')
461 for x in range(len(dashes)):
462 dashes[x]=utils.unit_get(dashes[x])
463 self.canvas.setDash(node.get('dash').split(','))
465 def _image(self, node):
468 from reportlab.lib.utils import ImageReader
469 nfile = node.get('file')
472 image_data = self.images[node.get('name')]
473 _logger.debug("Image %s used", node.get('name'))
474 s = StringIO(image_data)
477 if self.localcontext:
478 res = utils._regex.findall(newtext)
480 newtext = eval(key, {}, self.localcontext) or ''
483 image_data = base64.decodestring(newtext)
485 s = StringIO(image_data)
487 _logger.debug("No image data!")
490 if nfile in self.images:
491 s = StringIO(self.images[nfile])
494 up = urlparse.urlparse(str(nfile))
498 # RFC: do we really want to open external URLs?
499 # Are we safe from cross-site scripting or attacks?
500 _logger.debug("Retrieve image from %s", nfile)
501 u = urllib.urlopen(str(nfile))
502 s = StringIO(u.read())
504 _logger.debug("Open image file %s ", nfile)
505 s = _open_image(nfile, path=self.path)
508 (sx,sy) = img.getSize()
509 _logger.debug("Image is %dx%d", sx, sy)
510 args = { 'x': 0.0, 'y': 0.0, 'mask': 'auto'}
511 for tag in ('width','height','x','y'):
513 args[tag] = utils.unit_get(node.get(tag))
514 if ('width' in args) and (not 'height' in args):
515 args['height'] = sy * args['width'] / sx
516 elif ('height' in args) and (not 'width' in args):
517 args['width'] = sx * args['height'] / sy
518 elif ('width' in args) and ('height' in args):
519 if (float(args['width'])/args['height'])>(float(sx)>sy):
520 args['width'] = sx * args['height'] / sy
522 args['height'] = sy * args['width'] / sx
523 self.canvas.drawImage(img, **args)
526 # self.canvas._doc.SaveToFile(self.canvas._filename, self.canvas)
528 def _path(self, node):
529 self.path = self.canvas.beginPath()
530 self.path.moveTo(**utils.attr_get(node, ['x','y']))
531 for n in utils._child_get(node, self):
534 vals = utils.text_get(n).split()
535 self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1]))
536 elif n.tag=='curvesto':
537 vals = utils.text_get(n).split()
541 pos.append(utils.unit_get(vals.pop(0)))
542 self.path.curveTo(*pos)
544 data = n.text.split() # Not sure if I must merge all TEXT_NODE ?
546 x = utils.unit_get(data.pop(0))
547 y = utils.unit_get(data.pop(0))
548 self.path.lineTo(x,y)
549 if (not node.get('close')) or utils.bool_get(node.get('close')):
551 self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
553 def setFont(self, node):
554 fontname = select_fontname(node.get('name'), self.canvas._fontname)
555 return self.canvas.setFont(fontname, utils.unit_get(node.get('size')))
557 def render(self, node):
559 'drawCentredString': self._drawCenteredString,
560 'drawRightString': self._drawRightString,
561 'drawString': self._drawString,
563 'ellipse': self._ellipse,
564 'lines': self._lines,
566 'curves': self._curves,
567 'fill': lambda node: self.canvas.setFillColor(color.get(node.get('color'))),
568 'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.get('color'))),
569 'setFont': self.setFont ,
570 'place': self._place,
571 'circle': self._circle,
572 'lineMode': self._line_mode,
574 'rotate': lambda node: self.canvas.rotate(float(node.get('degrees'))),
575 'translate': self._translate,
578 for n in utils._child_get(node, self):
582 class _rml_draw(object):
583 def __init__(self, localcontext, node, styles, images=None, path='.', title=None):
586 self.localcontext = localcontext
592 self.canvas_title = title
594 def render(self, canvas, doc):
596 cnv = _rml_canvas(canvas, self.localcontext, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title)
597 cnv.render(self.node)
598 canvas.restoreState()
600 class _rml_Illustration(platypus.flowables.Flowable):
601 def __init__(self, node, localcontext, styles, self2):
602 self.localcontext = (localcontext or {}).copy()
605 self.width = utils.unit_get(node.get('width'))
606 self.height = utils.unit_get(node.get('height'))
608 def wrap(self, *args):
609 return self.width, self.height
611 drw = _rml_draw(self.localcontext ,self.node,self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title)
612 drw.render(self.canv, None)
614 # Workaround for issue #15: https://bitbucket.org/rptlab/reportlab/issue/15/infinite-pages-produced-when-splitting
615 original_pto_split = platypus.flowables.PTOContainer.split
616 def split(self, availWidth, availHeight):
617 res = original_pto_split(self, availWidth, availHeight)
618 if len(res) > 2 and len(self._content) > 0:
619 header = self._content[0]._ptoinfo.header
620 trailer = self._content[0]._ptoinfo.trailer
621 if isinstance(res[-2], platypus.flowables.UseUpSpace) and len(header + trailer) == len(res[:-2]):
624 platypus.flowables.PTOContainer.split = split
626 class _rml_flowable(object):
627 def __init__(self, doc, localcontext, images=None, path='.', title=None, canvas=None):
630 self.localcontext = localcontext
632 self.styles = doc.styles
638 def _textual(self, node):
639 rc1 = utils._process_text(self, node.text or '')
640 for n in utils._child_get(node,self):
641 txt_n = copy.deepcopy(n)
642 for key in txt_n.attrib.keys():
643 if key in ('rml_except', 'rml_loop', 'rml_tag'):
644 del txt_n.attrib[key]
645 if not n.tag == 'bullet':
646 if n.tag == 'pageNumber':
647 txt_n.text = self.canvas and str(self.canvas.getPageNumber()) or ''
649 txt_n.text = utils.xml2str(self._textual(n))
650 txt_n.tail = n.tail and utils.xml2str(utils._process_text(self, n.tail.replace('\n',''))) or ''
651 rc1 += etree.tostring(txt_n)
654 def _table(self, node):
655 children = utils._child_get(node,self,'tr')
667 st = copy.deepcopy(self.styles.table_styles[tr.get('style')])
668 for si in range(len(st._cmds)):
669 s = list(st._cmds[si])
670 s[1] = (s[1][0],posy)
671 s[2] = (s[2][0],posy)
672 st._cmds[si] = tuple(s)
674 if tr.get('paraStyle'):
675 paraStyle = self.styles.styles[tr.get('paraStyle')]
678 for td in utils._child_get(tr, self,'td'):
680 st = copy.deepcopy(self.styles.table_styles[td.get('style')])
687 if td.get('paraStyle'):
689 paraStyle = self.styles.styles[td.get('paraStyle')]
693 for n in utils._child_get(td, self):
694 if n.tag == etree.Comment:
697 fl = self._flowable(n, extra_style=paraStyle)
698 if isinstance(fl,list):
704 flow = self._textual(td)
706 if len(data2)>length:
709 while len(ab)<length:
711 while len(data2)<length:
716 if node.get('colWidths'):
717 assert length == len(node.get('colWidths').split(','))
718 colwidths = [utils.unit_get(f.strip()) for f in node.get('colWidths').split(',')]
719 if node.get('rowHeights'):
720 rowheights = [utils.unit_get(f.strip()) for f in node.get('rowHeights').split(',')]
721 if len(rowheights) == 1:
722 rowheights = rowheights[0]
723 table = platypus.LongTable(data = data, colWidths=colwidths, rowHeights=rowheights, **(utils.attr_get(node, ['splitByRow'] ,{'repeatRows':'int','repeatCols':'int'})))
724 if node.get('style'):
725 table.setStyle(self.styles.table_styles[node.get('style')])
730 def _illustration(self, node):
731 return _rml_Illustration(node, self.localcontext, self.styles, self)
733 def _textual_image(self, node):
734 return base64.decodestring(node.text)
736 def _pto(self, node):
740 for node in utils._child_get(node, self):
741 if node.tag == etree.Comment:
744 elif node.tag=='pto_header':
745 pto_header = self.render(node)
746 elif node.tag=='pto_trailer':
747 pto_trailer = self.render(node)
749 flow = self._flowable(node)
751 if isinstance(flow,list):
752 sub_story = sub_story + flow
754 sub_story.append(flow)
755 return platypus.flowables.PTOContainer(sub_story, trailer=pto_trailer, header=pto_header)
757 def _flowable(self, node, extra_style=None):
759 return self._pto(node)
761 style = self.styles.para_style_get(node)
763 style.__dict__.update(extra_style)
765 for i in self._textual(node).split('\n'):
766 result.append(platypus.Paragraph(i, style, **(utils.attr_get(node, [], {'bulletText':'str'}))))
768 elif node.tag=='barCode':
770 from reportlab.graphics.barcode import code128
771 from reportlab.graphics.barcode import code39
772 from reportlab.graphics.barcode import code93
773 from reportlab.graphics.barcode import common
774 from reportlab.graphics.barcode import fourstate
775 from reportlab.graphics.barcode import usps
776 from reportlab.graphics.barcode import createBarcodeDrawing
779 _logger.warning("Cannot use barcode renderers:", exc_info=True)
781 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'})
783 'codabar': lambda x: common.Codabar(x, **args),
784 'code11': lambda x: common.Code11(x, **args),
785 'code128': lambda x: code128.Code128(str(x), **args),
786 'standard39': lambda x: code39.Standard39(str(x), **args),
787 'standard93': lambda x: code93.Standard93(str(x), **args),
788 'i2of5': lambda x: common.I2of5(x, **args),
789 'extended39': lambda x: code39.Extended39(str(x), **args),
790 'extended93': lambda x: code93.Extended93(str(x), **args),
791 'msi': lambda x: common.MSI(x, **args),
792 'fim': lambda x: usps.FIM(x, **args),
793 'postnet': lambda x: usps.POSTNET(x, **args),
794 'ean13': lambda x: createBarcodeDrawing('EAN13', value=str(x), **args),
795 'qrcode': lambda x: createBarcodeDrawing('QR', value=x, **args),
799 code = node.get('code').lower()
800 return codes[code](self._textual(node))
801 elif node.tag=='name':
802 self.styles.names[ node.get('id')] = node.get('value')
804 elif node.tag=='xpre':
805 style = self.styles.para_style_get(node)
806 return platypus.XPreformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int','frags':'int'})))
807 elif node.tag=='pre':
808 style = self.styles.para_style_get(node)
809 return platypus.Preformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int'})))
810 elif node.tag=='illustration':
811 return self._illustration(node)
812 elif node.tag=='blockTable':
813 return self._table(node)
814 elif node.tag=='title':
815 styles = reportlab.lib.styles.getSampleStyleSheet()
816 style = styles['Title']
817 return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
818 elif re.match('^h([1-9]+[0-9]*)$', (node.tag or '')):
819 styles = reportlab.lib.styles.getSampleStyleSheet()
820 style = styles['Heading'+str(node.tag[1:])]
821 return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'})))
822 elif node.tag=='image':
824 if not node.get('file'):
826 if node.get('name') in self.doc.images:
827 _logger.debug("Image %s read ", node.get('name'))
828 image_data = self.doc.images[node.get('name')].read()
830 _logger.warning("Image %s not defined", node.get('name'))
835 if self.localcontext:
836 newtext = utils._process_text(self, node.text or '')
837 image_data = base64.decodestring(newtext)
839 _logger.debug("No inline image data")
841 image = StringIO(image_data)
843 _logger.debug("Image get from file %s", node.get('file'))
844 image = _open_image(node.get('file'), path=self.doc.path)
845 return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
846 elif node.tag=='spacer':
847 if node.get('width'):
848 width = utils.unit_get(node.get('width'))
850 width = utils.unit_get('1cm')
851 length = utils.unit_get(node.get('length'))
852 return platypus.Spacer(width=width, height=length)
853 elif node.tag=='section':
854 return self.render(node)
855 elif node.tag == 'pageNumberReset':
857 elif node.tag in ('pageBreak', 'nextPage'):
858 return platypus.PageBreak()
859 elif node.tag=='condPageBreak':
860 return platypus.CondPageBreak(**(utils.attr_get(node, ['height'])))
861 elif node.tag=='setNextTemplate':
862 return platypus.NextPageTemplate(str(node.get('name')))
863 elif node.tag=='nextFrame':
864 return platypus.CondPageBreak(1000) # TODO: change the 1000 !
865 elif node.tag == 'setNextFrame':
866 from reportlab.platypus.doctemplate import NextFrameFlowable
867 return NextFrameFlowable(str(node.get('name')))
868 elif node.tag == 'currentFrame':
869 from reportlab.platypus.doctemplate import CurrentFrameFlowable
870 return CurrentFrameFlowable(str(node.get('name')))
871 elif node.tag == 'frameEnd':
872 return EndFrameFlowable()
873 elif node.tag == 'hr':
874 width_hr=node.get('width') or '100%'
875 color_hr=node.get('color') or 'black'
876 thickness_hr=node.get('thickness') or 1
877 lineCap_hr=node.get('lineCap') or 'round'
878 return platypus.flowables.HRFlowable(width=width_hr,color=color.get(color_hr),thickness=float(thickness_hr),lineCap=str(lineCap_hr))
880 sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.tag,))
883 def render(self, node_story):
884 def process_story(node_story):
886 for node in utils._child_get(node_story, self):
887 if node.tag == etree.Comment:
890 flow = self._flowable(node)
892 if isinstance(flow,list):
893 sub_story = sub_story + flow
895 sub_story.append(flow)
897 return process_story(node_story)
900 class EndFrameFlowable(ActionFlowable):
901 def __init__(self,resume=0):
902 ActionFlowable.__init__(self,('frameEnd',resume))
904 class TinyDocTemplate(platypus.BaseDocTemplate):
906 def beforeDocument(self):
907 # Store some useful value directly inside canvas, so it's available
908 # on flowable drawing (needed for proper PageCount handling)
909 self.canv._doPageReset = False
910 self.canv._storyCount = 0
912 def ___handle_pageBegin(self):
914 self.pageTemplate.beforeDrawPage(self.canv,self)
915 self.pageTemplate.checkPageSize(self.canv,self)
916 self.pageTemplate.onPage(self.canv,self)
917 for f in self.pageTemplate.frames: f._reset()
919 self._curPageFlowableCount = 0
920 if hasattr(self,'_nextFrameIndex'):
921 del self._nextFrameIndex
922 for f in self.pageTemplate.frames:
926 self.handle_frameBegin()
929 if self.canv._doPageReset:
930 # Following a <pageReset/> tag:
931 # - we reset page number to 0
932 # - we add an new PageCount flowable (relative to the current
933 # story number), but not for NumeredCanvas at is handle page
935 # NOTE: _rml_template render() method add a PageReset flowable at end
936 # of each story, so we're sure to pass here at least once per story.
937 if not isinstance(self.canv, NumberedCanvas):
938 self.handle_flowable([ PageCount(story_count=self.canv._storyCount) ])
939 self.canv._pageCount = self.page
941 self.canv._flag = True
942 self.canv._pageNumber = 0
943 self.canv._doPageReset = False
944 self.canv._storyCount += 1
946 class _rml_template(object):
947 def __init__(self, localcontext, out, node, doc, images=None, path='.', title=None):
951 localcontext={'internal_header':True}
952 self.localcontext = localcontext
957 pagesize_map = {'a4': A4,
961 if self.localcontext.get('company'):
962 pageSize = pagesize_map.get(self.localcontext.get('company').paper_format, A4)
963 if node.get('pageSize'):
964 ps = map(lambda x:x.strip(), node.get('pageSize').replace(')', '').replace('(', '').split(','))
965 pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
967 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'}))
968 self.page_templates = []
969 self.styles = doc.styles
972 pts = node.findall('pageTemplate')
975 for frame_el in pt.findall('frame'):
976 frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) )
977 if utils.attr_get(frame_el, ['last']):
978 frame.lastFrame = True
979 frames.append( frame )
981 gr = pt.findall('pageGraphics')\
982 or pt[1].findall('pageGraphics')
983 except Exception: # FIXME: be even more specific, perhaps?
986 # self.image=[ n for n in utils._child_get(gr[0], self) if n.tag=='image' or not self.localcontext]
987 drw = _rml_draw(self.localcontext,gr[0], self.doc, images=images, path=self.path, title=self.title)
988 self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
990 drw = _rml_draw(self.localcontext,node,self.doc,title=self.title)
991 self.page_templates.append( platypus.PageTemplate(frames=frames,onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
992 self.doc_tmpl.addPageTemplates(self.page_templates)
994 def render(self, node_stories):
995 if self.localcontext and not self.localcontext.get('internal_header',False):
996 del self.localcontext['internal_header']
998 r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title, canvas=None)
1000 for node_story in node_stories:
1002 # Reset Page Number with new story tag
1003 fis.append(PageReset())
1004 fis.append(platypus.PageBreak())
1005 fis += r.render(node_story)
1008 if self.localcontext and self.localcontext.get('internal_header',False):
1009 self.doc_tmpl.afterFlowable(fis)
1010 self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas)
1012 self.doc_tmpl.build(fis)
1013 except platypus.doctemplate.LayoutError, e:
1014 e.name = 'Print Error'
1015 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.'
1018 def parseNode(rml, localcontext=None, fout=None, images=None, path='.', title=None):
1019 node = etree.XML(rml)
1020 r = _rml_doc(node, localcontext, images, path, title=title)
1021 #try to override some font mappings
1023 from customfonts import SetCustomFonts
1026 # means there is no custom fonts mapping in this system.
1029 _logger.warning('Cannot set font mapping', exc_info=True)
1033 return fp.getvalue()
1035 def parseString(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)
1039 #try to override some font mappings
1041 from customfonts import SetCustomFonts
1047 fp = file(fout,'wb')
1054 return fp.getvalue()
1056 def trml2pdf_help():
1057 print 'Usage: trml2pdf input.rml >output.pdf'
1058 print 'Render the standard input (RML) and output a PDF file'
1061 if __name__=="__main__":
1063 if sys.argv[1]=='--help':
1065 print parseString(file(sys.argv[1], 'r').read()),
1067 print 'Usage: trml2pdf input.rml >output.pdf'
1068 print 'Try \'trml2pdf --help\' for more information.'
1071 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: