Index: setup.py =================================================================== --- setup.py (revision 129885) +++ setup.py (working copy) @@ -57,6 +57,7 @@ 'pyPdf', 'reportlab', 'setuptools', + 'svg2rlg', 'zope.interface', 'zope.schema', ], Index: src/z3c/rml/attr.py =================================================================== --- src/z3c/rml/attr.py (revision 129885) +++ src/z3c/rml/attr.py (working copy) @@ -337,12 +337,26 @@ self.onlyOpen = onlyOpen def fromUnicode(self, value): + if value.lower().endswith('.svg') or value.lower().endswith('.svgz'): + return self._load_svg(value) fileObj = super(Image, self).fromUnicode(value) if self.onlyOpen: return fileObj return reportlab.lib.utils.ImageReader(fileObj) + def _load_svg(self, value): + from gzip import GzipFile + from svg2rlg import Renderer + from xml.etree import cElementTree + fileObj = super(Image, self).fromUnicode(value) + svg = fileObj.getvalue() + if svg[:2] == '\037\213': + svg = GzipFile(fileobj=fileObj).read() + svg = cElementTree.fromstring(svg) + return Renderer(value).render(svg) + + class Color(RMLAttribute): """Requires the input of a color. There are several supported formats. @@ -362,7 +376,7 @@ manager = getManager(self.context) if value.startswith('rml:'): - value = manager.names[value[4:]] + value = manager.get_name[value[4:]] if value in manager.colors: return manager.colors[value] Index: src/z3c/rml/canvas.py =================================================================== --- src/z3c/rml/canvas.py (revision 129885) +++ src/z3c/rml/canvas.py (working copy) @@ -104,18 +104,27 @@ return str(canvas.getPageNumber() + int(elem.get('countingFrom', 1)) - 1) def getName(self, elem, canvas): - return attr.getManager(self).names[elem.get('id')] + return attr.getManager(self).get_name(elem.get('id'), elem.get('default')) + def evalString(self, elem, canvas): + value = self._getText(elem, canvas) + if elem.tail: + value = value[:len(elem.tail)] + # Maybe still not safe + return unicode(eval(value.strip(), {'__builtins__': None}, {})) + handleElements = {'pageNumber': getPageNumber, - 'getName': getName} + 'getName': getName, + 'evalString': evalString} def _getText(self, node, canvas): text = node.text or u'' - for sub in node.iterdescendants(): + for sub in node.getchildren(): if sub.tag in self.handleElements: text += self.handleElements[sub.tag](self, sub, canvas) else: self._getText(sub, canvas) + text += sub.tail or u'' text += node.tail or u'' return text @@ -368,13 +377,20 @@ callable = 'drawImage' attrMapping = {'file': 'image'} + def _getSize(self, image): + from reportlab.graphics.shapes import Drawing + if not isinstance(image, Drawing): + return image.getSize() + else: + return (image.width, image.height) + def process(self): kwargs = dict(self.getAttributeValues(attrMapping=self.attrMapping)) preserve = kwargs.pop('preserveAspectRatio') show = kwargs.pop('showBoundary') if preserve: - imgX, imgY = kwargs['image'].getSize() + imgX, imgY = self._getSize(kwargs['image']) # Scale image correctly, if width and/or height were specified if 'width' in kwargs and 'height' not in kwargs: @@ -388,11 +404,20 @@ kwargs['height'] = imgY * kwargs['width'] / imgX canvas = attr.getManager(self, interfaces.ICanvasManager).canvas - getattr(canvas, self.callable)(**kwargs) + if not hasattr(kwargs['image'], 'drawOn'): + getattr(canvas, self.callable)(**kwargs) + else: + width, height = self._getSize(kwargs['image']) + kwargs['image'].scale( + kwargs.get('width', width) / width, + kwargs.get('height', height) / height + ) + kwargs['image'].drawOn(canvas, kwargs['x'], kwargs['y']) + if show: - width = kwargs.get('width', kwargs['image'].getSize()[0]) - height = kwargs.get('height', kwargs['image'].getSize()[1]) + width = kwargs.get('width', self._getSize(kwargs['image'])[0]) + height = kwargs.get('height', self._getSize(kwargs['image'])[1]) canvas.rect(kwargs['x'], kwargs['y'], width, height) Index: src/z3c/rml/document.py =================================================================== --- src/z3c/rml/document.py (revision 129885) +++ src/z3c/rml/document.py (working copy) @@ -24,6 +24,7 @@ from reportlab.pdfbase import pdfmetrics, ttfonts, cidfonts from reportlab.lib import colors, fonts from reportlab.platypus import tableofcontents +from reportlab.platypus.doctemplate import IndexingFlowable from z3c.rml import attr, canvas, directive, doclogic, interfaces, list from z3c.rml import occurence, pdfinclude, special, storyplace, stylesheet @@ -34,14 +35,16 @@ class IRegisterType1Face(interfaces.IRMLDirectiveSignature): """Register a new Type 1 font face.""" - afmFile = attr.String( + afmFile = attr.File( title=u'AFM File', description=u'Path to AFM file used to register the Type 1 face.', + doNotOpen=True, required=True) - pfbFile = attr.String( + pfbFile = attr.File( title=u'PFB File', description=u'Path to PFB file used to register the Type 1 face.', + doNotOpen=True, required=True) class RegisterType1Face(directive.RMLDirective): @@ -122,9 +125,10 @@ u'be previously registered.'), required=True) - fileName = attr.String( + fileName = attr.File( title=u'File Name', description=u'File path of the of the TrueType font.', + doNotOpen=True, required=True) class RegisterTTFont(directive.RMLDirective): @@ -442,7 +446,14 @@ description=u'A flag when set shows crop marks on the page.', required=False) + printScaling = attr.Choice( + title=u'Print Scaling', + description=(u'The print scaling mode in which the document is opened ' + u'in the viewer.'), + choices=('None', 'AppDefault'), + required=False) + class DocInit(directive.RMLDirective): signature = IDocInit factories = { @@ -463,6 +474,7 @@ self.parent.cropMarks = kwargs.get('useCropMarks', False) self.parent.pageMode = kwargs.get('pageMode') self.parent.pageLayout = kwargs.get('pageLayout') + self.parent.printScaling = kwargs.get('printScaling') super(DocInit, self).process() @@ -527,6 +539,7 @@ self.pageLayout = None self.pageMode = None self.logger = None + self.printScaling = None def _indexAdd(self, canvas, name, label): self.indexes[name](canvas, name, label) @@ -542,6 +555,10 @@ canvas._doc._catalog.setPageLayout(self.pageLayout) if self.pageMode: canvas._doc._catalog.setPageMode(self.pageMode) + if self.printScaling: + canvas.setViewerPreference( + 'PrintScaling', self.printScaling + ) def process(self, outputFile=None): """Process document""" @@ -581,7 +598,7 @@ elif self.element.find('template') is not None: self.processSubDirectives(select=('template', 'story')) self.doc.beforeDocument = self._beforeDocument - self.doc.multiBuild(self.flowables) + self.doc.multiBuild(self.flowables, maxPasses=2) # Process all post processors for name, processor in self.postProcessors: @@ -595,3 +612,26 @@ # Cleanup. colors.toColor.setExtraColorsNameSpace({}) + def get_name(self, name, default=None): + + if name not in self.names: + if self.doc._indexingFlowables and \ + isinstance(self.doc._indexingFlowables[-1], DummyIndexingFlowable): + del self.doc._indexingFlowables[-1] + self.doc._indexingFlowables.append(DummyIndexingFlowable()) + + if default is None: + default = u'' + return self.names.get(name, default) + + +class DummyIndexingFlowable(IndexingFlowable): + """A dummy flowable to trick multiBuild into performing +1 pass.""" + + def __init__(self): + self.i = -1 + + def isSatisfied(self): + self.i += 1 + return self.i + Index: src/z3c/rml/flowable.py =================================================================== --- src/z3c/rml/flowable.py (revision 129885) +++ src/z3c/rml/flowable.py (working copy) @@ -1271,9 +1271,40 @@ return (0, 0) def draw(self): - self.manager.names[self.id] = self.value + if self.id not in self.manager.names: + text = self._getText(self.value, self.manager.canvas) + self.manager.names[self.id] = text + self.value = u'' + def getPageNumber(self, elem, canvas): + return str(canvas.getPageNumber() + int(elem.get('countingFrom', 1)) - 1) + def getName(self, elem, canvas): + return self.manager.get_name(elem.get('id'), elem.get('default')) + + def evalString(self, elem, canvas): + value = self._getText(elem, canvas, 2) + if elem.tail: + value = value[:len(elem.tail)] + # Maybe still not safe + return unicode(eval(value.strip(), {'__builtins__': None}, {})) + + handleElements = {'pageNumber': getPageNumber, + 'getName': getName, + 'evalString': evalString} + + def _getText(self, node, canvas, depth=1): + text = node.text or u'' + for sub in node.getchildren(): + if sub.tag in self.handleElements: + text += self.handleElements[sub.tag](self, sub, canvas) + else: + self._getText(sub, canvas) + text += sub.tail or u'' + text += node.tail or u'' + return text + + class INamedString(interfaces.IRMLDirectiveSignature): """Defines a name for a string.""" @@ -1294,7 +1325,7 @@ id, value = self.getAttributeValues(valuesOnly=True) manager = attr.getManager(self) # We have to delay assigning values, otherwise the last one wins. - self.parent.flow.append(NamedStringFlowable(manager, id, value)) + self.parent.flow.append(NamedStringFlowable(manager, id, self.element)) class IShowIndex(interfaces.IRMLDirectiveSignature): Index: src/z3c/rml/paraparser.py =================================================================== --- src/z3c/rml/paraparser.py (revision 129885) +++ src/z3c/rml/paraparser.py (working copy) @@ -41,6 +41,10 @@ canvas = frame.f_locals.get('canv', None) if canvas is None: + # Is it always available here? + canvas = getattr(frame.f_locals.get('self', None), 'canv', None) + + if canvas is None: raise Exception("Can't use in this location.") return str(canvas.getPageNumber()) @@ -52,6 +56,7 @@ def __init__(self, attributes): reportlab.platypus.paraparser.ParaFrag.__init__(self) self.id = attributes['id'] + self.default = attributes.get('default') @property def text(self): @@ -64,24 +69,53 @@ canvas = frame.f_locals.get('canv', None) if canvas is None: + # Is it always available here? + canvas = getattr(frame.f_locals.get('self', None), 'canv', None) + + if canvas is None: raise Exception("Can't use in this location.") - return canvas.manager.names[self.id] + return canvas.manager.get_name(self.id, self.default) +class EvalStringFragment(reportlab.platypus.paraparser.ParaFrag): + """A fragment whose `text` is evaluated at access time.""" + + def __init__(self, attributes): + reportlab.platypus.paraparser.ParaFrag.__init__(self) + self.frags = [] + + @property + def text(self): + text = u'' + for frag in self.frags: + if isinstance(frag, basestring): + text += frag + else: + text += frag.text + # Maybe still not safe + return unicode(eval(text.strip(), {'__builtins__': None}, {})) + + class Z3CParagraphParser(reportlab.platypus.paraparser.ParaParser): """Extensions to paragraph-internal XML parsing.""" + in_eval = False + def startDynamic(self, attributes, klass): frag = klass(attributes) frag.__dict__.update(self._stack[-1].__dict__) frag.fontName = reportlab.lib.fonts.tt2ps( frag.fontName, frag.bold, frag.italic) - self.fragList.append(frag) - self._stack.append(frag) + if self.in_eval: + self._stack[-1].frags.append(frag) + else: + self.fragList.append(frag) + self._stack.append(frag) def endDynamic(self): - self._pop() + if not self.in_eval: + self._pop() def start_pageNumber(self, attributes): self.startDynamic(attributes, PageNumberFragment) @@ -95,7 +129,21 @@ def end_getName(self): self.endDynamic() + def start_evalString(self, attributes): + self.startDynamic(attributes, EvalStringFragment) + self.in_eval = True + def end_evalString(self): + self.in_eval = False + self.endDynamic() + + def handle_data(self, data): + if self.in_eval: + self._stack[-1].frags.append(data) + else: + reportlab.platypus.paraparser.ParaParser.handle_data(self, data) + + # Monkey-patch reportlabs global parser instance. Wah. import reportlab.platypus.paragraph reportlab.platypus.paragraph._parser = Z3CParagraphParser()