# -*- coding: utf-8 -*- ############################################################################## # # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # ############################################################################## import sys import copy import reportlab import re from reportlab.pdfgen import canvas from reportlab import platypus import utils import color import os import logging from lxml import etree import base64 from distutils.version import LooseVersion from reportlab.platypus.doctemplate import ActionFlowable from openerp.tools.safe_eval import safe_eval as eval from reportlab.lib.units import inch,cm,mm from openerp.tools.misc import file_open from reportlab.pdfbase import pdfmetrics from reportlab.lib.pagesizes import A4, letter try: from cStringIO import StringIO _hush_pyflakes = [ StringIO ] except ImportError: from StringIO import StringIO _logger = logging.getLogger(__name__) encoding = 'utf-8' def select_fontname(fontname, default_fontname): if fontname not in pdfmetrics.getRegisteredFontNames()\ or fontname not in pdfmetrics.standardFonts: # let reportlab attempt to find it try: pdfmetrics.getFont(fontname) except Exception: addition = "" if " " in fontname: addition = ". Your font contains spaces which is not valid in RML." _logger.warning('Could not locate font %s, substituting default: %s%s', fontname, default_fontname, addition) fontname = default_fontname return fontname def _open_image(filename, path=None): """Attempt to open a binary file and return the descriptor """ if os.path.isfile(filename): return open(filename, 'rb') for p in (path or []): if p and os.path.isabs(p): fullpath = os.path.join(p, filename) if os.path.isfile(fullpath): return open(fullpath, 'rb') try: if p: fullpath = os.path.join(p, filename) else: fullpath = filename return file_open(fullpath) except IOError: pass raise IOError("File %s cannot be found in image path" % filename) class NumberedCanvas(canvas.Canvas): def __init__(self, *args, **kwargs): canvas.Canvas.__init__(self, *args, **kwargs) self._saved_page_states = [] def showPage(self): self._startPage() def save(self): """add page info to each page (page x of y)""" for state in self._saved_page_states: self.__dict__.update(state) self.draw_page_number() canvas.Canvas.showPage(self) canvas.Canvas.save(self) def draw_page_number(self): page_count = len(self._saved_page_states) self.setFont("Helvetica", 8) self.drawRightString((self._pagesize[0]-30), (self._pagesize[1]-40), " %(this)i / %(total)i" % { 'this': self._pageNumber, 'total': page_count, } ) class PageCount(platypus.Flowable): def __init__(self, story_count=0): platypus.Flowable.__init__(self) self.story_count = story_count def draw(self): self.canv.beginForm("pageCount%d" % self.story_count) self.canv.setFont("Helvetica", utils.unit_get(str(8))) self.canv.drawString(0, 0, str(self.canv.getPageNumber())) self.canv.endForm() class PageReset(platypus.Flowable): def draw(self): """Flag to close current story page numbering and prepare for the next should be executed after the rendering of the full story""" self.canv._doPageReset = True class _rml_styles(object,): def __init__(self, nodes, localcontext): self.localcontext = localcontext self.styles = {} self.styles_obj = {} self.names = {} self.table_styles = {} self.default_style = reportlab.lib.styles.getSampleStyleSheet() for node in nodes: for style in node.findall('blockTableStyle'): self.table_styles[style.get('id')] = self._table_style_get(style) for style in node.findall('paraStyle'): sname = style.get('name') self.styles[sname] = self._para_style_update(style) if self.default_style.has_key(sname): for key, value in self.styles[sname].items(): setattr(self.default_style[sname], key, value) else: self.styles_obj[sname] = reportlab.lib.styles.ParagraphStyle(sname, self.default_style["Normal"], **self.styles[sname]) for variable in node.findall('initialize'): for name in variable.findall('name'): self.names[ name.get('id')] = name.get('value') def _para_style_update(self, node): data = {} for attr in ['textColor', 'backColor', 'bulletColor', 'borderColor']: if node.get(attr): data[attr] = color.get(node.get(attr)) for attr in ['bulletFontName', 'fontName']: if node.get(attr): fontname= select_fontname(node.get(attr), None) if fontname is not None: data['fontName'] = fontname for attr in ['bulletText']: if node.get(attr): data[attr] = node.get(attr) for attr in ['fontSize', 'leftIndent', 'rightIndent', 'spaceBefore', 'spaceAfter', 'firstLineIndent', 'bulletIndent', 'bulletFontSize', 'leading', 'borderWidth','borderPadding','borderRadius']: if node.get(attr): data[attr] = utils.unit_get(node.get(attr)) if node.get('alignment'): align = { 'right':reportlab.lib.enums.TA_RIGHT, 'center':reportlab.lib.enums.TA_CENTER, 'justify':reportlab.lib.enums.TA_JUSTIFY } data['alignment'] = align.get(node.get('alignment').lower(), reportlab.lib.enums.TA_LEFT) data['splitLongWords'] = 0 return data def _table_style_get(self, style_node): styles = [] for node in style_node: start = utils.tuple_int_get(node, 'start', (0,0) ) stop = utils.tuple_int_get(node, 'stop', (-1,-1) ) if node.tag=='blockValign': styles.append(('VALIGN', start, stop, str(node.get('value')))) elif node.tag=='blockFont': styles.append(('FONT', start, stop, str(node.get('name')))) elif node.tag=='blockTextColor': styles.append(('TEXTCOLOR', start, stop, color.get(str(node.get('colorName'))))) elif node.tag=='blockLeading': styles.append(('LEADING', start, stop, utils.unit_get(node.get('length')))) elif node.tag=='blockAlignment': styles.append(('ALIGNMENT', start, stop, str(node.get('value')))) elif node.tag=='blockSpan': styles.append(('SPAN', start, stop)) elif node.tag=='blockLeftPadding': styles.append(('LEFTPADDING', start, stop, utils.unit_get(node.get('length')))) elif node.tag=='blockRightPadding': styles.append(('RIGHTPADDING', start, stop, utils.unit_get(node.get('length')))) elif node.tag=='blockTopPadding': styles.append(('TOPPADDING', start, stop, utils.unit_get(node.get('length')))) elif node.tag=='blockBottomPadding': styles.append(('BOTTOMPADDING', start, stop, utils.unit_get(node.get('length')))) elif node.tag=='blockBackground': styles.append(('BACKGROUND', start, stop, color.get(node.get('colorName')))) if node.get('size'): styles.append(('FONTSIZE', start, stop, utils.unit_get(node.get('size')))) elif node.tag=='lineStyle': kind = node.get('kind') kind_list = [ 'GRID', 'BOX', 'OUTLINE', 'INNERGRID', 'LINEBELOW', 'LINEABOVE','LINEBEFORE', 'LINEAFTER' ] assert kind in kind_list thick = 1 if node.get('thickness'): thick = float(node.get('thickness')) styles.append((kind, start, stop, thick, color.get(node.get('colorName')))) return platypus.tables.TableStyle(styles) def para_style_get(self, node): style = False sname = node.get('style') if sname: if sname in self.styles_obj: style = self.styles_obj[sname] else: _logger.debug('Warning: style not found, %s - setting default!', node.get('style')) if not style: style = self.default_style['Normal'] para_update = self._para_style_update(node) if para_update: # update style only is necessary style = copy.deepcopy(style) style.__dict__.update(para_update) return style class _rml_doc(object): def __init__(self, node, localcontext=None, images=None, path='.', title=None): if images is None: images = {} if localcontext is None: localcontext = {} self.localcontext = localcontext self.etree = node self.filename = self.etree.get('filename') self.images = images self.path = path self.title = title def docinit(self, els): from reportlab.lib.fonts import addMapping from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont for node in els: for font in node.findall('registerFont'): name = font.get('fontName').encode('ascii') fname = font.get('fontFile').encode('ascii') if name not in pdfmetrics._fonts: pdfmetrics.registerFont(TTFont(name, fname)) #by default, we map the fontName to each style (bold, italic, bold and italic), so that #if there isn't any font defined for one of these style (via a font family), the system #will fallback on the normal font. addMapping(name, 0, 0, name) #normal addMapping(name, 0, 1, name) #italic addMapping(name, 1, 0, name) #bold addMapping(name, 1, 1, name) #italic and bold #if registerFontFamily is defined, we register the mapping of the fontName to use for each style. for font_family in node.findall('registerFontFamily'): family_name = font_family.get('normal').encode('ascii') if font_family.get('italic'): addMapping(family_name, 0, 1, font_family.get('italic').encode('ascii')) if font_family.get('bold'): addMapping(family_name, 1, 0, font_family.get('bold').encode('ascii')) if font_family.get('boldItalic'): addMapping(family_name, 1, 1, font_family.get('boldItalic').encode('ascii')) def setTTFontMapping(self,face, fontname, filename, mode='all'): from reportlab.lib.fonts import addMapping from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont if mode: mode = mode.lower() if fontname not in pdfmetrics._fonts: pdfmetrics.registerFont(TTFont(fontname, filename)) if mode == 'all': addMapping(face, 0, 0, fontname) #normal addMapping(face, 0, 1, fontname) #italic addMapping(face, 1, 0, fontname) #bold addMapping(face, 1, 1, fontname) #italic and bold elif mode in ['italic', 'oblique']: addMapping(face, 0, 1, fontname) #italic elif mode == 'bold': addMapping(face, 1, 0, fontname) #bold elif mode in ('bolditalic', 'bold italic','boldoblique', 'bold oblique'): addMapping(face, 1, 1, fontname) #italic and bold else: addMapping(face, 0, 0, fontname) #normal def _textual_image(self, node): rc = '' for n in node: rc +=( etree.tostring(n) or '') + n.tail return base64.decodestring(node.tostring()) def _images(self, el): result = {} for node in el.findall('.//image'): rc =( node.text or '') result[node.get('name')] = base64.decodestring(rc) return result def render(self, out): el = self.etree.findall('.//docinit') if el: self.docinit(el) el = self.etree.findall('.//stylesheet') self.styles = _rml_styles(el,self.localcontext) el = self.etree.findall('.//images') if el: self.images.update( self._images(el[0]) ) el = self.etree.findall('.//template') if len(el): pt_obj = _rml_template(self.localcontext, out, el[0], self, images=self.images, path=self.path, title=self.title) el = utils._child_get(self.etree, self, 'story') pt_obj.render(el) else: self.canvas = canvas.Canvas(out) pd = self.etree.find('pageDrawing')[0] pd_obj = _rml_canvas(self.canvas, self.localcontext, None, self, self.images, path=self.path, title=self.title) pd_obj.render(pd) self.canvas.showPage() self.canvas.save() class _rml_canvas(object): def __init__(self, canvas, localcontext, doc_tmpl=None, doc=None, images=None, path='.', title=None): if images is None: images = {} self.localcontext = localcontext self.canvas = canvas self.styles = doc.styles self.doc_tmpl = doc_tmpl self.doc = doc self.images = images self.path = path self.title = title if self.title: self.canvas.setTitle(self.title) def _textual(self, node, x=0, y=0): text = node.text and node.text.encode('utf-8') or '' rc = utils._process_text(self, text) for n in node: if n.tag == 'seq': from reportlab.lib.sequencer import getSequencer seq = getSequencer() rc += str(seq.next(n.get('id'))) if n.tag == 'pageCount': if x or y: self.canvas.translate(x,y) self.canvas.doForm('pageCount%s' % (self.canvas._storyCount,)) if x or y: self.canvas.translate(-x,-y) if n.tag == 'pageNumber': rc += str(self.canvas.getPageNumber()) rc += utils._process_text(self, n.tail) return rc.replace('\n','') def _drawString(self, node): v = utils.attr_get(node, ['x','y']) text=self._textual(node, **v) text = utils.xml2str(text) try: self.canvas.drawString(text=text, **v) except TypeError: _logger.error("Bad RML: tag requires attributes 'x' and 'y'!") raise def _drawCenteredString(self, node): v = utils.attr_get(node, ['x','y']) text=self._textual(node, **v) text = utils.xml2str(text) self.canvas.drawCentredString(text=text, **v) def _drawRightString(self, node): v = utils.attr_get(node, ['x','y']) text=self._textual(node, **v) text = utils.xml2str(text) self.canvas.drawRightString(text=text, **v) def _rect(self, node): if node.get('round'): self.canvas.roundRect(radius=utils.unit_get(node.get('round')), **utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'})) else: self.canvas.rect(**utils.attr_get(node, ['x','y','width','height'], {'fill':'bool','stroke':'bool'})) def _ellipse(self, node): x1 = utils.unit_get(node.get('x')) x2 = utils.unit_get(node.get('width')) y1 = utils.unit_get(node.get('y')) y2 = utils.unit_get(node.get('height')) self.canvas.ellipse(x1,y1,x2,y2, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'})) def _curves(self, node): line_str = node.text.split() lines = [] while len(line_str)>7: self.canvas.bezier(*[utils.unit_get(l) for l in line_str[0:8]]) line_str = line_str[8:] def _lines(self, node): line_str = node.text.split() lines = [] while len(line_str)>3: lines.append([utils.unit_get(l) for l in line_str[0:4]]) line_str = line_str[4:] self.canvas.lines(lines) def _grid(self, node): xlist = [utils.unit_get(s) for s in node.get('xs').split(',')] ylist = [utils.unit_get(s) for s in node.get('ys').split(',')] self.canvas.grid(xlist, ylist) def _translate(self, node): dx = utils.unit_get(node.get('dx')) or 0 dy = utils.unit_get(node.get('dy')) or 0 self.canvas.translate(dx,dy) def _circle(self, node): 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'})) def _place(self, node): flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title, canvas=self.canvas).render(node) infos = utils.attr_get(node, ['x','y','width','height']) infos['y']+=infos['height'] for flow in flows: w,h = flow.wrap(infos['width'], infos['height']) if w<=infos['width'] and h<=infos['height']: infos['y']-=h flow.drawOn(self.canvas,infos['x'],infos['y']) infos['height']-=h else: raise ValueError("Not enough space") def _line_mode(self, node): ljoin = {'round':1, 'mitered':0, 'bevelled':2} lcap = {'default':0, 'round':1, 'square':2} if node.get('width'): self.canvas.setLineWidth(utils.unit_get(node.get('width'))) if node.get('join'): self.canvas.setLineJoin(ljoin[node.get('join')]) if node.get('cap'): self.canvas.setLineCap(lcap[node.get('cap')]) if node.get('miterLimit'): self.canvas.setDash(utils.unit_get(node.get('miterLimit'))) if node.get('dash'): dashes = node.get('dash').split(',') for x in range(len(dashes)): dashes[x]=utils.unit_get(dashes[x]) self.canvas.setDash(node.get('dash').split(',')) def _image(self, node): import urllib import urlparse from reportlab.lib.utils import ImageReader nfile = node.get('file') if not nfile: if node.get('name'): image_data = self.images[node.get('name')] _logger.debug("Image %s used", node.get('name')) s = StringIO(image_data) else: newtext = node.text if self.localcontext: res = utils._regex.findall(newtext) for key in res: newtext = eval(key, {}, self.localcontext) or '' image_data = None if newtext: image_data = base64.decodestring(newtext) if image_data: s = StringIO(image_data) else: _logger.debug("No image data!") return False else: if nfile in self.images: s = StringIO(self.images[nfile]) else: try: up = urlparse.urlparse(str(nfile)) except ValueError: up = False if up and up.scheme: # RFC: do we really want to open external URLs? # Are we safe from cross-site scripting or attacks? _logger.debug("Retrieve image from %s", nfile) u = urllib.urlopen(str(nfile)) s = StringIO(u.read()) else: _logger.debug("Open image file %s ", nfile) s = _open_image(nfile, path=self.path) try: img = ImageReader(s) (sx,sy) = img.getSize() _logger.debug("Image is %dx%d", sx, sy) args = { 'x': 0.0, 'y': 0.0, 'mask': 'auto'} for tag in ('width','height','x','y'): if node.get(tag): args[tag] = utils.unit_get(node.get(tag)) if ('width' in args) and (not 'height' in args): args['height'] = sy * args['width'] / sx elif ('height' in args) and (not 'width' in args): args['width'] = sx * args['height'] / sy elif ('width' in args) and ('height' in args): if (float(args['width'])/args['height'])>(float(sx)>sy): args['width'] = sx * args['height'] / sy else: args['height'] = sy * args['width'] / sx self.canvas.drawImage(img, **args) finally: s.close() # self.canvas._doc.SaveToFile(self.canvas._filename, self.canvas) def _path(self, node): self.path = self.canvas.beginPath() self.path.moveTo(**utils.attr_get(node, ['x','y'])) for n in utils._child_get(node, self): if not n.text : if n.tag=='moveto': vals = utils.text_get(n).split() self.path.moveTo(utils.unit_get(vals[0]), utils.unit_get(vals[1])) elif n.tag=='curvesto': vals = utils.text_get(n).split() while len(vals)>5: pos=[] while len(pos)<6: pos.append(utils.unit_get(vals.pop(0))) self.path.curveTo(*pos) elif n.text: data = n.text.split() # Not sure if I must merge all TEXT_NODE ? while len(data)>1: x = utils.unit_get(data.pop(0)) y = utils.unit_get(data.pop(0)) self.path.lineTo(x,y) if (not node.get('close')) or utils.bool_get(node.get('close')): self.path.close() self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'})) def setFont(self, node): fontname = select_fontname(node.get('name'), self.canvas._fontname) return self.canvas.setFont(fontname, utils.unit_get(node.get('size'))) def render(self, node): tags = { 'drawCentredString': self._drawCenteredString, 'drawRightString': self._drawRightString, 'drawString': self._drawString, 'rect': self._rect, 'ellipse': self._ellipse, 'lines': self._lines, 'grid': self._grid, 'curves': self._curves, 'fill': lambda node: self.canvas.setFillColor(color.get(node.get('color'))), 'stroke': lambda node: self.canvas.setStrokeColor(color.get(node.get('color'))), 'setFont': self.setFont , 'place': self._place, 'circle': self._circle, 'lineMode': self._line_mode, 'path': self._path, 'rotate': lambda node: self.canvas.rotate(float(node.get('degrees'))), 'translate': self._translate, 'image': self._image } for n in utils._child_get(node, self): if n.tag in tags: tags[n.tag](n) class _rml_draw(object): def __init__(self, localcontext, node, styles, images=None, path='.', title=None): if images is None: images = {} self.localcontext = localcontext self.node = node self.styles = styles self.canvas = None self.images = images self.path = path self.canvas_title = title def render(self, canvas, doc): canvas.saveState() cnv = _rml_canvas(canvas, self.localcontext, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title) cnv.render(self.node) canvas.restoreState() class _rml_Illustration(platypus.flowables.Flowable): def __init__(self, node, localcontext, styles, self2): self.localcontext = (localcontext or {}).copy() self.node = node self.styles = styles self.width = utils.unit_get(node.get('width')) self.height = utils.unit_get(node.get('height')) self.self2 = self2 def wrap(self, *args): return self.width, self.height def draw(self): drw = _rml_draw(self.localcontext ,self.node,self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title) drw.render(self.canv, None) # Workaround for issue #15: https://bitbucket.org/rptlab/reportlab/issue/15/infinite-pages-produced-when-splitting original_pto_split = platypus.flowables.PTOContainer.split def split(self, availWidth, availHeight): res = original_pto_split(self, availWidth, availHeight) if len(res) > 2 and len(self._content) > 0: header = self._content[0]._ptoinfo.header trailer = self._content[0]._ptoinfo.trailer if isinstance(res[-2], platypus.flowables.UseUpSpace) and len(header + trailer) == len(res[:-2]): return [] return res platypus.flowables.PTOContainer.split = split class _rml_flowable(object): def __init__(self, doc, localcontext, images=None, path='.', title=None, canvas=None): if images is None: images = {} self.localcontext = localcontext self.doc = doc self.styles = doc.styles self.images = images self.path = path self.title = title self.canvas = canvas def _textual(self, node): rc1 = utils._process_text(self, node.text or '') for n in utils._child_get(node,self): txt_n = copy.deepcopy(n) for key in txt_n.attrib.keys(): if key in ('rml_except', 'rml_loop', 'rml_tag'): del txt_n.attrib[key] if not n.tag == 'bullet': if n.tag == 'pageNumber': txt_n.text = self.canvas and str(self.canvas.getPageNumber()) or '' else: txt_n.text = utils.xml2str(self._textual(n)) txt_n.tail = n.tail and utils.xml2str(utils._process_text(self, n.tail.replace('\n',''))) or '' rc1 += etree.tostring(txt_n) return rc1 def _table(self, node): children = utils._child_get(node,self,'tr') if not children: return None length = 0 colwidths = None rowheights = None data = [] styles = [] posy = 0 for tr in children: paraStyle = None if tr.get('style'): st = copy.deepcopy(self.styles.table_styles[tr.get('style')]) for si in range(len(st._cmds)): s = list(st._cmds[si]) s[1] = (s[1][0],posy) s[2] = (s[2][0],posy) st._cmds[si] = tuple(s) styles.append(st) if tr.get('paraStyle'): paraStyle = self.styles.styles[tr.get('paraStyle')] data2 = [] posx = 0 for td in utils._child_get(tr, self,'td'): if td.get('style'): st = copy.deepcopy(self.styles.table_styles[td.get('style')]) for s in st._cmds: s[1][1] = posy s[2][1] = posy s[1][0] = posx s[2][0] = posx styles.append(st) if td.get('paraStyle'): # TODO: merge styles paraStyle = self.styles.styles[td.get('paraStyle')] posx += 1 flow = [] for n in utils._child_get(td, self): if n.tag == etree.Comment: n.text = '' continue fl = self._flowable(n, extra_style=paraStyle) if isinstance(fl,list): flow += fl else: flow.append( fl ) if not len(flow): flow = self._textual(td) data2.append( flow ) if len(data2)>length: length=len(data2) for ab in data: while len(ab) LooseVersion('3.0') and not plain_text and tag_text: result.append(platypus.Paragraph(' 
', style, **(utils.attr_get(node, [], {'bulletText': 'str'})))) return result elif node.tag=='barCode': try: from reportlab.graphics.barcode import code128 from reportlab.graphics.barcode import code39 from reportlab.graphics.barcode import code93 from reportlab.graphics.barcode import common from reportlab.graphics.barcode import fourstate from reportlab.graphics.barcode import usps from reportlab.graphics.barcode import createBarcodeDrawing except ImportError: _logger.warning("Cannot use barcode renderers:", exc_info=True) return None 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'}) codes = { 'codabar': lambda x: common.Codabar(x, **args), 'code11': lambda x: common.Code11(x, **args), 'code128': lambda x: code128.Code128(str(x), **args), 'standard39': lambda x: code39.Standard39(str(x), **args), 'standard93': lambda x: code93.Standard93(str(x), **args), 'i2of5': lambda x: common.I2of5(x, **args), 'extended39': lambda x: code39.Extended39(str(x), **args), 'extended93': lambda x: code93.Extended93(str(x), **args), 'msi': lambda x: common.MSI(x, **args), 'fim': lambda x: usps.FIM(x, **args), 'postnet': lambda x: usps.POSTNET(x, **args), 'ean13': lambda x: createBarcodeDrawing('EAN13', value=str(x), **args), 'qrcode': lambda x: createBarcodeDrawing('QR', value=x, **args), } code = 'code128' if node.get('code'): code = node.get('code').lower() return codes[code](self._textual(node)) elif node.tag=='name': self.styles.names[ node.get('id')] = node.get('value') return None elif node.tag=='xpre': style = self.styles.para_style_get(node) return platypus.XPreformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int','frags':'int'}))) elif node.tag=='pre': style = self.styles.para_style_get(node) return platypus.Preformatted(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str','dedent':'int'}))) elif node.tag=='illustration': return self._illustration(node) elif node.tag=='blockTable': return self._table(node) elif node.tag=='title': styles = reportlab.lib.styles.getSampleStyleSheet() style = styles['Title'] return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'}))) elif re.match('^h([1-9]+[0-9]*)$', (node.tag or '')): styles = reportlab.lib.styles.getSampleStyleSheet() style = styles['Heading'+str(node.tag[1:])] return platypus.Paragraph(self._textual(node), style, **(utils.attr_get(node, [], {'bulletText':'str'}))) elif node.tag=='image': image_data = False if not node.get('file'): if node.get('name'): if node.get('name') in self.doc.images: _logger.debug("Image %s read ", node.get('name')) image_data = self.doc.images[node.get('name')].read() else: _logger.warning("Image %s not defined", node.get('name')) return False else: import base64 newtext = node.text if self.localcontext: newtext = utils._process_text(self, node.text or '') image_data = base64.decodestring(newtext) if not image_data: _logger.debug("No inline image data") return False image = StringIO(image_data) else: _logger.debug("Image get from file %s", node.get('file')) image = _open_image(node.get('file'), path=self.doc.path) return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height']))) elif node.tag=='spacer': if node.get('width'): width = utils.unit_get(node.get('width')) else: width = utils.unit_get('1cm') length = utils.unit_get(node.get('length')) return platypus.Spacer(width=width, height=length) elif node.tag=='section': return self.render(node) elif node.tag == 'pageNumberReset': return PageReset() elif node.tag in ('pageBreak', 'nextPage'): return platypus.PageBreak() elif node.tag=='condPageBreak': return platypus.CondPageBreak(**(utils.attr_get(node, ['height']))) elif node.tag=='setNextTemplate': return platypus.NextPageTemplate(str(node.get('name'))) elif node.tag=='nextFrame': return platypus.CondPageBreak(1000) # TODO: change the 1000 ! elif node.tag == 'setNextFrame': from reportlab.platypus.doctemplate import NextFrameFlowable return NextFrameFlowable(str(node.get('name'))) elif node.tag == 'currentFrame': from reportlab.platypus.doctemplate import CurrentFrameFlowable return CurrentFrameFlowable(str(node.get('name'))) elif node.tag == 'frameEnd': return EndFrameFlowable() elif node.tag == 'hr': width_hr=node.get('width') or '100%' color_hr=node.get('color') or 'black' thickness_hr=node.get('thickness') or 1 lineCap_hr=node.get('lineCap') or 'round' return platypus.flowables.HRFlowable(width=width_hr,color=color.get(color_hr),thickness=float(thickness_hr),lineCap=str(lineCap_hr)) else: sys.stderr.write('Warning: flowable not yet implemented: %s !\n' % (node.tag,)) return None def render(self, node_story): def process_story(node_story): sub_story = [] for node in utils._child_get(node_story, self): if node.tag == etree.Comment: node.text = '' continue flow = self._flowable(node) if flow: if isinstance(flow,list): sub_story = sub_story + flow else: sub_story.append(flow) return sub_story return process_story(node_story) class EndFrameFlowable(ActionFlowable): def __init__(self,resume=0): ActionFlowable.__init__(self,('frameEnd',resume)) class TinyDocTemplate(platypus.BaseDocTemplate): def beforeDocument(self): # Store some useful value directly inside canvas, so it's available # on flowable drawing (needed for proper PageCount handling) self.canv._doPageReset = False self.canv._storyCount = 0 def ___handle_pageBegin(self): self.page += 1 self.pageTemplate.beforeDrawPage(self.canv,self) self.pageTemplate.checkPageSize(self.canv,self) self.pageTemplate.onPage(self.canv,self) for f in self.pageTemplate.frames: f._reset() self.beforePage() self._curPageFlowableCount = 0 if hasattr(self,'_nextFrameIndex'): del self._nextFrameIndex for f in self.pageTemplate.frames: if f.id == 'first': self.frame = f break self.handle_frameBegin() def afterPage(self): if isinstance(self.canv, NumberedCanvas): # save current page states before eventual reset self.canv._saved_page_states.append(dict(self.canv.__dict__)) if self.canv._doPageReset: # Following a tag: # - we reset page number to 0 # - we add an new PageCount flowable (relative to the current # story number), but not for NumeredCanvas at is handle page # count itself) # NOTE: _rml_template render() method add a PageReset flowable at end # of each story, so we're sure to pass here at least once per story. if not isinstance(self.canv, NumberedCanvas): self.handle_flowable([ PageCount(story_count=self.canv._storyCount) ]) self.canv._pageCount = self.page self.page = 0 self.canv._flag = True self.canv._pageNumber = 0 self.canv._doPageReset = False self.canv._storyCount += 1 class _rml_template(object): def __init__(self, localcontext, out, node, doc, images=None, path='.', title=None): if images is None: images = {} if not localcontext: localcontext={'internal_header':True} self.localcontext = localcontext self.images= images self.path = path self.title = title pagesize_map = {'a4': A4, 'us_letter': letter } pageSize = A4 if self.localcontext.get('company'): pageSize = pagesize_map.get(self.localcontext.get('company').paper_format, A4) if node.get('pageSize'): ps = map(lambda x:x.strip(), node.get('pageSize').replace(')', '').replace('(', '').split(',')) pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) ) 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'})) self.page_templates = [] self.styles = doc.styles self.doc = doc self.image=[] pts = node.findall('pageTemplate') for pt in pts: frames = [] for frame_el in pt.findall('frame'): frame = platypus.Frame( **(utils.attr_get(frame_el, ['x1','y1', 'width','height', 'leftPadding', 'rightPadding', 'bottomPadding', 'topPadding'], {'id':'str', 'showBoundary':'bool'})) ) if utils.attr_get(frame_el, ['last']): frame.lastFrame = True frames.append( frame ) try : gr = pt.findall('pageGraphics')\ or pt[1].findall('pageGraphics') except Exception: # FIXME: be even more specific, perhaps? gr='' if len(gr): # self.image=[ n for n in utils._child_get(gr[0], self) if n.tag=='image' or not self.localcontext] drw = _rml_draw(self.localcontext,gr[0], self.doc, images=images, path=self.path, title=self.title) self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) )) else: drw = _rml_draw(self.localcontext,node,self.doc,title=self.title) self.page_templates.append( platypus.PageTemplate(frames=frames,onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) )) self.doc_tmpl.addPageTemplates(self.page_templates) def render(self, node_stories): if self.localcontext and not self.localcontext.get('internal_header',False): del self.localcontext['internal_header'] fis = [] r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title, canvas=None) story_cnt = 0 for node_story in node_stories: if story_cnt > 0: fis.append(platypus.PageBreak()) fis += r.render(node_story) # end of story numbering computation fis.append(PageReset()) story_cnt += 1 try: if self.localcontext and self.localcontext.get('internal_header',False): self.doc_tmpl.afterFlowable(fis) self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas) else: self.doc_tmpl.build(fis) except platypus.doctemplate.LayoutError, e: e.name = 'Print Error' 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.' raise def parseNode(rml, localcontext=None, fout=None, images=None, path='.', title=None): node = etree.XML(rml) r = _rml_doc(node, localcontext, images, path, title=title) #try to override some font mappings try: from customfonts import SetCustomFonts SetCustomFonts(r) except ImportError: # means there is no custom fonts mapping in this system. pass except Exception: _logger.warning('Cannot set font mapping', exc_info=True) pass fp = StringIO() r.render(fp) return fp.getvalue() def parseString(rml, localcontext=None, fout=None, images=None, path='.', title=None): node = etree.XML(rml) r = _rml_doc(node, localcontext, images, path, title=title) #try to override some font mappings try: from customfonts import SetCustomFonts SetCustomFonts(r) except Exception: pass if fout: fp = file(fout,'wb') r.render(fp) fp.close() return fout else: fp = StringIO() r.render(fp) return fp.getvalue() def trml2pdf_help(): print 'Usage: trml2pdf input.rml >output.pdf' print 'Render the standard input (RML) and output a PDF file' sys.exit(0) if __name__=="__main__": if len(sys.argv)>1: if sys.argv[1]=='--help': trml2pdf_help() print parseString(file(sys.argv[1], 'r').read()), else: print 'Usage: trml2pdf input.rml >output.pdf' print 'Try \'trml2pdf --help\' for more information.' # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: