diff --git a/src/z3c/rml/attr.py b/src/z3c/rml/attrng.py similarity index 51% rename from src/z3c/rml/attr.py rename to src/z3c/rml/attrng.py index f777e27..523982f 100644 --- a/src/z3c/rml/attr.py +++ b/src/z3c/rml/attrng.py @@ -11,7 +11,7 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## -"""RML-specific XML tools +"""RML Attribute Implementation $Id$ """ @@ -19,160 +19,162 @@ import cStringIO import os import re -import reportlab +import reportlab.graphics.widgets.markers import reportlab.lib.colors +import reportlab.lib.pagesizes import reportlab.lib.styles import reportlab.lib.units import reportlab.lib.utils -import reportlab.lib.pagesizes -import reportlab.graphics.widgets.markers import urllib +import zope.schema from lxml import etree from xml.sax import saxutils -from z3c.rml import interfaces - -DEFAULT = object() +MISSING = object() - -def getManager(context, interface): +def getManager(context, interface=None): + if interface is None: + # Avoid circular imports + from z3c.rml import interfaces + interface = interfaces.IManager + # Walk up the path until the manager is found while (not interface.providedBy(context) and context is not None): context = context.parent + # If no manager was found, raise an error if context is None: - raise ValueError( - 'Manager for %s could not be found.' %interface.getName()) + raise ValueError('The manager could not be found.') return context -class Attribute(object): +class RMLAttribute(zope.schema.Field): + """An attribute of the RML directive.""" - def __init__(self, name=None, default=DEFAULT): - self.name = name - self.default = default + missing_value = MISSING + default = MISSING - def convert(self, value, context=None): - return value + def fromUnicode(self, ustr): + """See zope.schema.interfaces.IField""" + if self.context is None: + raise ValueError('Attribute not bound to a context.') + return super(RMLAttribute, self).fromUnicode(unicode(ustr)) - def get(self, element, default=DEFAULT, context=None): - value = element.get(self.name, DEFAULT) - if value is DEFAULT or value is default: - if default is DEFAULT: + def get(self): + """See zope.schema.interfaces.IField""" + value = self.context.element.get(self.__name__, self.missing_value) + if value is self.missing_value: + if self.default is not None: return self.default - return default - return self.convert(value, context) + return self.missing_value + return self.fromUnicode(value) + + +class BaseChoice(RMLAttribute): + choices = {} + + def fromUnicode(self, value): + value = value.lower() + if value in self.choices: + return self.choices[value] + raise ValueError( + '%r not a valid value for attribute "%s"' % (value, self.__name__)) -class Combination(Attribute): - def __init__(self, name=None, valueTypes=(), default=DEFAULT): - super(Combination, self).__init__(name, default) - self.valueTypes = valueTypes +class Combination(RMLAttribute): - def convert(self, value, context=None): - for valueType in self.valueTypes: + def __init__(self, value_types=(), *args, **kw): + super(Combination, self).__init__(*args, **kw) + self.value_types = value_types + + def fromUnicode(self, value): + for value_type in self.value_types: + bound = value_type.bind(self) try: - return valueType.convert(value, context) + return bound.fromUnicode(value) except ValueError: pass raise ValueError(value) -class Text(Attribute): - def convert(self, value, context=None): - return unicode(value) +class String(RMLAttribute, zope.schema.Bytes): + """A simple Bytes string.""" -class Int(Attribute): +class Text(RMLAttribute, zope.schema.Text): + """A simple unicode string.""" - def convert(self, value, context=None): - return int(value) +class Integer(RMLAttribute, zope.schema.Int): + # By making min and max simple attributes, we avoid some validation + # problems. + min = None + max = None -class Float(Attribute): - def convert(self, value, context=None): - return float(value) +class Float(RMLAttribute, zope.schema.Float): + # By making min and max simple attributes, we avoid some validation + # problems. + min = None + max = None -class StringOrInt(Attribute): +class StringOrInt(RMLAttribute): - def convert(self, value, context=None): + def fromUnicode(self, value): try: return int(value) except ValueError: return str(value) -class Sequence(Attribute): +class Sequence(RMLAttribute, zope.schema._field.AbstractCollection): splitre = re.compile('[ \t\n,;]*') - minLength = None - maxLength = None - - def __init__(self, name=None, valueType=None, default=DEFAULT, - splitre=None, minLength=None, maxLength=None, length=None): - super(Sequence, self).__init__(name, default) - self.valueType = valueType - if minLength is not None: - self.minLength = minLength - if maxLength is not None: - self.maxLength = maxLength - if length is not None: - self.minLength = self.maxLength = length + + def __init__(self, splitre=None, *args, **kw): + super(Sequence, self).__init__(*args, **kw) if splitre is not None: self.splitre = splitre - def convert(self, value, context=None): - if value.startswith('(') and value.endswith(')'): - value = value[1:-1] - value = value.strip() - values = self.splitre.split(value) - result = [self.valueType.convert(value.strip(), context) - for value in values] - if ((self.minLength is not None and len(result) < self.minLength) and - (self.maxLength is not None and len(result) > self.maxLength)): + def fromUnicode(self, ustr): + if ustr.startswith('(') and ustr.endswith(')'): + ustr = ustr[1:-1] + ustr = ustr.strip() + raw_values = self.splitre.split(ustr) + result = [self.value_type.bind(self.context).fromUnicode(raw.strip()) + for raw in raw_values] + if ((self.min_length is not None and len(result) < self.min_length) and + (self.max_length is not None and len(result) > self.max_length)): raise ValueError( 'Length of sequence must be at least %s and at most %i' % ( - self.minLength, self.maxLength)) + self.min_length, self.max_length)) return result -class BaseChoice(Attribute): - choices = {} - - def convert(self, value, context=None): - value = value.lower() - if value in self.choices: - return self.choices[value] - raise ValueError( - '%r not a valid value for attribute "%s"' % (value, self.name)) - - class Choice(BaseChoice): - def __init__(self, name=None, choices=None, default=DEFAULT): - super(Choice, self).__init__(name, default) + def __init__(self, choices=None, *args, **kw): + super(Choice, self).__init__(*args, **kw) if isinstance(choices, (tuple, list)): choices = dict([(val.lower(), val) for val in choices]) self.choices = choices -class Bool(BaseChoice): +class Boolean(BaseChoice): choices = {'true': True, 'false': False, 'yes': True, 'no': False, '1': True, '0': False, } -class DefaultBool(Bool): - choices = Bool.choices.copy() +class BooleanWithDefault(Boolean): + choices = Boolean.choices.copy() choices.update({'default': None}) -class Measurement(Attribute): +class Measurement(RMLAttribute): - def __init__(self, name=None, default=DEFAULT, - allowPercentage=False, allowStar=False): - super(Measurement, self).__init__(name, default) + def __init__(self, allowPercentage=False, allowStar=False, *args, **kw): + super(Measurement, self).__init__(*args, **kw) self.allowPercentage = allowPercentage self.allowStar = allowStar @@ -186,7 +188,7 @@ def __init__(self, name=None, default=DEFAULT, allowPercentage = False allowStar = False - def convert(self, value, context=None): + def fromUnicode(self, value): if value == 'None': return None if value == '*' and self.allowStar: @@ -199,6 +201,7 @@ def convert(self, value, context=None): return unit[1]*float(res.group(1)) raise ValueError('The value %r is not a valid measurement.' %value) + class File(Text): open = staticmethod(urllib.urlopen) @@ -206,12 +209,11 @@ class File(Text): doNotOpen = False - def __init__(self, name=None, default=DEFAULT, doNotOpen=False): - super(File, self).__init__(name, default) + def __init__(self, doNotOpen=False, *args, **kw): + super(File, self).__init__(*args, **kw) self.doNotOpen = doNotOpen - - def convert(self, value, context=None): + def fromUnicode(self, value): # Check whether the value is of the form: # []/rel/path/image.gif" if value.startswith('['): @@ -234,35 +236,38 @@ def convert(self, value, context=None): class Image(File): - def __init__(self, name=None, default=DEFAULT, onlyOpen=False): - super(Image, self).__init__(name, default) + def __init__(self, onlyOpen=False, *args, **kw): + super(Image, self).__init__(*args, **kw) self.onlyOpen = onlyOpen - def convert(self, value, context=None): - fileObj = super(Image, self).convert(value, context) + def fromUnicode(self, value): + fileObj = super(Image, self).fromUnicode(value) if self.onlyOpen: return fileObj return reportlab.lib.utils.ImageReader(fileObj) -class Color(Text): +class Color(RMLAttribute): - def convert(self, value, context=None): - if value == 'None': + def __init__(self, acceptNone=False, *args, **kw): + super(Color, self).__init__(*args, **kw) + self.acceptNone = acceptNone + + def fromUnicode(self, value): + if self.acceptNone and value == 'None': return None - manager = getManager(context, interfaces.IColorsManager) + manager = getManager(self.context) if value in manager.colors: return manager.colors[value] return reportlab.lib.colors.toColor(value) -class Style(Text): +class Style(String): - def __init__(self, name=None, default='Normal'): - super(Style, self).__init__(name, default) + default = reportlab.lib.styles.getSampleStyleSheet().byName['Normal'] - def convert(self, value, context=None): - manager = getManager(context, interfaces.IStylesManager) + def fromUnicode(self, value): + manager = getManager(self.context) for styles in (manager.styles, reportlab.lib.styles.getSampleStyleSheet().byName): if value in styles: @@ -273,36 +278,27 @@ def convert(self, value, context=None): return styles[value[6:]] raise ValueError('Style %r could not be found.' %value) - def get(self, element, default=DEFAULT, context=None): - value = element.get(self.name, DEFAULT) - if value is DEFAULT: - if default is DEFAULT: - return self.convert(self.default, context) - elif default is None: - return None - return self.convert(default, context) - return self.convert(value, context) - class Symbol(Text): - def convert(self, value, context=None): + def fromUnicode(self, value): return reportlab.graphics.widgets.markers.makeMarker(value) -class PageSize(Attribute): - sizePair = Sequence(valueType=Measurement()) - words = Sequence(valueType=Attribute()) +class PageSize(RMLAttribute): - def convert(self, value, context=None): + sizePair = Sequence(value_type=Measurement()) + words = Sequence(value_type=String()) + + def fromUnicode(self, value): # First try to get a pair try: - return self.sizePair.convert(value, context) + return self.sizePair.fromUnicode(value) except ValueError: pass # Now we try to lookup a name. The following type of combinations must # work: "Letter" "LETTER" "A4 landscape" "letter portrait" - words = self.words.convert(value, context) + words = self.words.fromUnicode(value) words = [word.lower() for word in words] # First look for the orientation orienter = None @@ -318,83 +314,70 @@ def convert(self, value, context=None): return pagesize -class TextNode(Attribute): - """Text ndoes are not really attributes, but behave mostly like it.""" - - def __init__(self): - super(TextNode, self).__init__('TEXT') +class TextNode(RMLAttribute): + """Text nodes are not really attributes, but behave mostly like it.""" - def get(self, element, default=DEFAULT, context=None): - if element.text is None: + def get(self): + if self.context.element.text is None: return u'' - return unicode(element.text).strip() + return unicode(self.context.element.text).strip() + class FirstLevelTextNode(TextNode): """Text ndoes are not really attributes, but behave mostly like it.""" - def __init__(self): - super(TextNode, self).__init__('TEXT') - - def get(self, element, default=DEFAULT, context=None): - text = element.text or u'' - for child in element.getchildren(): + def get(self): + text = self.context.element.text or u'' + for child in self.context.element.getchildren(): text += child.tail or u'' - return text + return text.strip() class TextNodeSequence(Sequence): - def __init__(self, *args, **kw): - super(TextNodeSequence, self).__init__('TEXT', *args, **kw) - - def get(self, element, default=DEFAULT, context=None): - return self.convert(element.text, context) + def get(self): + return self.fromUnicode(self.context.element.text) class TextNodeGrid(TextNodeSequence): - def __init__(self, valueType=None, cols=None, default=DEFAULT): - super(TextNodeSequence, self).__init__( - 'TEXT', valueType, default, length=cols) - self.cols = cols + def __init__(self, columns=None, *args, **kw): + super(TextNodeGrid, self).__init__(*args, **kw) + self.columns = columns - def convert(self, value, context=None): - result = super(TextNodeGrid, self).convert(value, context) - if len(result) % self.cols != 0: + def fromUnicode(self, ustr): + result = super(TextNodeGrid, self).fromUnicode(ustr) + if len(result) % self.columns != 0: raise ValueError( - 'Number of elements must be divisible by %i.' %self.cols) - return [result[i*self.cols:(i+1)*self.cols] - for i in range(len(result)/self.cols)] + 'Number of elements must be divisible by %i.' %self.columns) + return [result[i*self.columns:(i+1)*self.columns] + for i in range(len(result)/self.columns)] -class RawXMLContent(Attribute): +class RawXMLContent(RMLAttribute): - def __init__(self, default=DEFAULT): - super(RawXMLContent, self).__init__('XML', default) + def __init__(self, *args, **kw): + super(RawXMLContent, self).__init__(*args, **kw) # Do it in here, since we hace a recursive problem otherwise from z3c.rml import special self.handleElements = {'getName': special.GetName} - def get(self, element, default=DEFAULT, context=None): + def get(self): # Replace what we can replace - for subElement in element.iterdescendants(): + for subElement in self.context.element.iterdescendants(): if subElement.tag in self.handleElements: substitute = self.handleElements[subElement.tag]( - subElement, context, None) + subElement, self.context) substitute.process() # Now create the text - text = saxutils.escape(element.text or u'') - for child in element.getchildren(): + text = saxutils.escape(self.context.element.text or u'') + for child in self.context.element.getchildren(): text += etree.tounicode(child) - if text is None: - if default is DEFAULT: - return self.default - return default return text class XMLContent(RawXMLContent): - def get(self, element, default=DEFAULT, context=None): - result = super(XMLContent, self).get(element, default, context) + def get(self): + result = super(XMLContent, self).get() return result.strip().replace('\t', ' ') diff --git a/src/z3c/rml/canvas.py b/src/z3c/rml/canvas.py index 4b48305..1caa262 100644 --- a/src/z3c/rml/canvas.py +++ b/src/z3c/rml/canvas.py @@ -16,218 +16,474 @@ $Id$ """ __docformat__ = "reStructuredText" -import cStringIO import zope.interface import reportlab.pdfgen.canvas -from z3c.rml import attr, element, flowable, interfaces, stylesheet -from z3c.rml import chart, form, page +from z3c.rml import attrng, directive, interfaces, occurence, stylesheet +from z3c.rml import chart, flowable, form, page -class DrawString(element.FunctionElement): - functionName = 'drawString' - args = ( - attr.Measurement('x'), - attr.Measurement('y'), - attr.TextNode()) +class IShape(interfaces.IRMLDirectiveSignature): + """A shape to be drawn on the canvas.""" + x = attrng.Measurement( + title=u'X-Coordinate', + description=(u'The X-coordinate of the lower-left position of the ' + u'shape.'), + required=True) + + y = attrng.Measurement( + title=u'Y-Coordinate', + description=(u'The Y-coordinate of the lower-left position of the ' + u'shape.'), + required=True) + + fill = attrng.Boolean( + title=u'Fill', + description=u'A flag to specify whether the shape should be filled.', + required=False) + + stroke = attrng.Boolean( + title=u'Stroke', + description=(u"A flag to specify whether the shape's outline should " + u"be drawn."), + required=False) + + +class CanvasRMLDirective(directive.RMLDirective): + callable = None + attrMapping = None + + def process(self): + kwargs = dict(self.getAttributeValues(attrMapping=self.attrMapping)) + canvas = attrng.getManager(self, interfaces.ICanvasManager).canvas + getattr(canvas, self.callable)(**kwargs) + + +class IDrawString(interfaces.IRMLDirectiveSignature): + """Draws a simple string (left aligned) onto the canvas at the specified + location.""" + + x = attrng.Measurement( + title=u'X-Coordinate', + description=(u'The X-coordinate of the lower-left position of the ' + u'string.'), + required=True) + + y = attrng.Measurement( + title=u'Y-Coordinate', + description=(u'The Y-coordinate of the lower-left position of the ' + u'string.'), + required=True) + + text = attrng.TextNode( + title=u'Text', + description=(u'The string/text that is put onto the canvas.'), + required=True) + +class DrawString(CanvasRMLDirective): + signature = IDrawString + callable = 'drawString' + +class IDrawRightString(IDrawString): + """Draws a simple string (right aligned) onto the canvas at the specified + location.""" class DrawRightString(DrawString): - functionName = 'drawRightString' + signature = IDrawRightString + callable = 'drawRightString' + +class IDrawCenteredString(IDrawString): + """Draws a simple string (centered aligned) onto the canvas at the specified + location.""" class DrawCenteredString(DrawString): - functionName = 'drawCentredString' + signature = IDrawCenteredString + callable = 'drawCentredString' + + +class IDrawAlignedString(IDrawString): + """Draws a simple string (aligned to the pivot character) onto the canvas + at the specified location.""" + pivotChar = attrng.Text( + title=u'Text', + description=(u'The string/text that is put onto the canvas.'), + min_length=1, + max_length=1, + default=u'.', + required=True) class DrawAlignedString(DrawString): - functionName = 'drawAlignedString' - args = DrawString.args + ( - attr.Text('pivotChar', '.'), ) + signature = IDrawAlignedString + callable = 'drawAlignedString' -class Shape(element.FunctionElement): - args = ( - attr.Measurement('x'), - attr.Measurement('y') ) - kw = ( - ('fill', attr.Bool('fill')), - ('stroke', attr.Bool('stroke')) ) +class IEllipse(IShape): + """Draws an ellipse on the canvas.""" + width = attrng.Measurement( + title=u'Width', + description=u'The width of the ellipse.', + required=True) -class Ellipse(Shape): - functionName = 'ellipse' - args = Shape.args + ( - attr.Measurement('width'), - attr.Measurement('height') ) + height = attrng.Measurement( + title=u'Height', + description=u'The height of the ellipse.', + required=True) + +class Ellipse(CanvasRMLDirective): + signature = IEllipse + callable = 'ellipse' + attrMapping = {'x': 'x1', 'y': 'y1'} def process(self): - args = self.getPositionalArguments() - kw = self.getKeywordArguments() + kwargs = dict(self.getAttributeValues(attrMapping=self.attrMapping)) + canvas = attrng.getManager(self, interfaces.ICanvasManager).canvas # Convert width and height to end locations - args[2] += args[0] - args[3] += args[1] - getattr(self.context, self.functionName)(*args, **kw) - -class Circle(Shape): - functionName = 'circle' - args = Shape.args + ( - attr.Measurement('radius'), ) - - -class Rectangle(Shape): - functionName = 'rect' - args = Shape.args + ( - attr.Measurement('width'), - attr.Measurement('height') ) - kw = Shape.kw + ( - ('radius', attr.Measurement('round')), - ) + kwargs['x2'] = kwargs['x1'] + kwargs['width'] + del kwargs['width'] + kwargs['y2'] = kwargs['y1'] + kwargs['height'] + del kwargs['height'] + getattr(canvas, self.callable)(**kwargs) + + +class ICircle(IShape): + """Draws a circle on the canvas.""" + + radius = attrng.Measurement( + title=u'Radius', + description=u'The radius of the circle.', + required=True) + +class Circle(CanvasRMLDirective): + signature = ICircle + callable = 'circle' + attrMapping = {'x': 'x_cen', 'y': 'y_cen', 'radius': 'r'} + + +class IRectangle(IShape): + """Draws an ellipse on the canvas.""" + + width = attrng.Measurement( + title=u'Width', + description=u'The width of the rectangle.', + required=True) + + height = attrng.Measurement( + title=u'Height', + description=u'The height of the rectangle.', + required=True) + + round = attrng.Measurement( + title=u'Corner Radius', + description=u'The radius of the rounded corners.', + required=False) + +class Rectangle(CanvasRMLDirective): + signature = IRectangle + callable = 'rect' + attrMapping = {'round': 'radius'} def process(self): if 'round' in self.element.keys(): - self.functionName = 'roundRect' + self.callable = 'roundRect' super(Rectangle, self).process() -class Grid(element.FunctionElement): - functionName = 'grid' - args = ( - attr.Sequence('xs', attr.Measurement()), - attr.Sequence('ys', attr.Measurement()) ) +class IGrid(interfaces.IRMLDirectiveSignature): + """A shape to be drawn on the canvas.""" + xs = attrng.Sequence( + title=u'X-Coordinates', + description=(u'A sequence x-coordinates that represent the vertical ' + u'line positions.'), + value_type=attrng.Measurement(), + required=True) -class Lines(element.FunctionElement): - functionName = 'lines' - args = ( - attr.TextNodeGrid(attr.Measurement(), 4), - ) + ys = attrng.Sequence( + title=u'Y-Coordinates', + description=(u'A sequence y-coordinates that represent the horizontal ' + u'line positions.'), + value_type=attrng.Measurement(), + required=True) -class Curves(element.FunctionElement): - functionName = 'bezier' - args = ( - attr.TextNodeGrid(attr.Measurement(), 8), - ) +class Grid(CanvasRMLDirective): + signature = IGrid + callable = 'grid' + attrMapping = {'xs': 'xlist', 'ys': 'ylist'} - def process(self): - argset = self.getPositionalArguments() - for args in argset[0]: - getattr(self.context, self.functionName)(*args) +class ILines(interfaces.IRMLDirectiveSignature): + """A path of connected lines drawn on the canvas.""" + + linelist = attrng.TextNodeGrid( + title=u'Line List', + description=(u'A list of lines coordinates to draw.'), + value_type=attrng.Measurement(), + columns=4, + required=True) -class Image(element.FunctionElement): - functionName = 'drawImage' - args = ( - attr.Image('file'), - attr.Measurement('x'), - attr.Measurement('y') ) - kw = ( - ('width', attr.Measurement('width')), - ('height', attr.Measurement('height')) ) +class Lines(CanvasRMLDirective): + signature = ILines + callable = 'lines' + + +class ICurves(interfaces.IRMLDirectiveSignature): + """A path of connected bezier curves drawn on the canvas.""" + + curvelist = attrng.TextNodeGrid( + title=u'Curve List', + description=(u'A list of curve coordinates to draw.'), + value_type=attrng.Measurement(), + columns=8, + required=True) + +class Curves(CanvasRMLDirective): + signature = ICurves + callable = 'bezier' def process(self): - args = self.getPositionalArguments() - kw = self.getKeywordArguments() + argset = self.getAttributeValues(valuesOnly=True)[0] + canvas = attrng.getManager(self, interfaces.ICanvasManager).canvas + for args in argset: + getattr(canvas, self.callable)(*args) + + +class IImage(interfaces.IRMLDirectiveSignature): + """Draws an external image on the canvas.""" + + file = attrng.Image( + title=u'File', + description=(u'Reference to the external file of the iamge.'), + required=True) + + x = attrng.Measurement( + title=u'X-Coordinate', + description=(u'The X-coordinate of the lower-left position of the ' + u'shape.'), + required=True) + + y = attrng.Measurement( + title=u'Y-Coordinate', + description=(u'The Y-coordinate of the lower-left position of the ' + u'shape.'), + required=True) + + width = attrng.Measurement( + title=u'Width', + description=u'The width of the image.', + required=False) + + height = attrng.Measurement( + title=u'Height', + description=u'The height of the image.', + required=False) + + showBoundary = attrng.Boolean( + title=u'Show Boundary', + description=(u'A flag determining whether a border should be drawn ' + u'around the image.'), + default=False, + required=False) + + preserveAspectRatio = attrng.Boolean( + title=u'Preserve Aspect Ratio', + description=(u"A flag determining whether the image's aspect ration " + u"should be conserved under any circumstances."), + default=False, + required=False) + +class Image(CanvasRMLDirective): + signature = IImage + callable = 'drawImage' + attrMapping = {'file': 'image'} - preserve = attr.Bool('preserveAspectRatio').get(self.element, False) + def process(self): + kwargs = dict(self.getAttributeValues(attrMapping=self.attrMapping)) + preserve = kwargs.pop('preserveAspectRatio') + show = kwargs.pop('showBoundary') if preserve: - imgX, imgY = args[0].getSize() + imgX, imgY = kwargs['image'].getSize() # Scale image correctly, if width and/or height were specified - if 'width' in kw and 'height' not in kw: - kw['height'] = imgY * kw['width'] / imgX - elif 'height' in kw and 'width' not in kw: - kw['width'] = imgX * kw['height'] / imgY - elif 'width' in kw and 'height' in kw: - if float(kw['width']) / kw['height'] > float(imgX) / imgY: - kw['width'] = imgX * kw['height'] / imgY + if 'width' in kwargs and 'height' not in kwargs: + kwargs['height'] = imgY * kwargs['width'] / imgX + elif 'height' in kwargs and 'width' not in kwargs: + kwargs['width'] = imgX * kwargs['height'] / imgY + elif 'width' in kwargs and 'height' in kwargs: + if float(kwargs['width'])/kwargs['height'] > float(imgX)/imgY: + kwargs['width'] = imgX * kwargs['height'] / imgY else: - kw['height'] = imgY * kw['width'] / imgX + kwargs['height'] = imgY * kwargs['width'] / imgX - getattr(self.context, self.functionName)(*args, **kw) + canvas = attrng.getManager(self, interfaces.ICanvasManager).canvas + getattr(canvas, self.callable)(**kwargs) - show = attr.Bool('showBoundary').get(self.element, False) if show: - width = kw.get('width', args[0].getSize()[0]) - height = kw.get('height', args[0].getSize()[1]) - self.context.rect(args[1], args[2], width, height) + width = kwargs.get('width', kwargs['image'].getSize()[0]) + height = kwargs.get('height', kwargs['image'].getSize()[1]) + canvas.rect(kwargs['x'], kwargs['y'], width, height) + +class IPlace(interfaces.IRMLDirectiveSignature): + """Draws a set of flowables on the canvas within a given region.""" -class Place(element.FunctionElement): - args = ( - attr.Measurement('x'), attr.Measurement('y'), - attr.Measurement('width'), attr.Measurement('height') ) + x = attrng.Measurement( + title=u'X-Coordinate', + description=(u'The X-coordinate of the lower-left position of the ' + u'place.'), + required=True) + + y = attrng.Measurement( + title=u'Y-Coordinate', + description=(u'The Y-coordinate of the lower-left position of the ' + u'place.'), + required=True) + + width = attrng.Measurement( + title=u'Width', + description=u'The width of the place.', + required=False) + + height = attrng.Measurement( + title=u'Height', + description=u'The height of the place.', + required=False) + +class Place(CanvasRMLDirective): + signature = IPlace def process(self): - x, y, width, height = self.getPositionalArguments() + x, y, width, height = self.getAttributeValues( + select=('x', 'y', 'width', 'height'), valuesOnly=True) y += height - flows = flowable.Flow(self.element, self.parent, self.context) + flows = flowable.Flow(self.element, self.parent) flows.process() + + canvas = attrng.getManager(self, interfaces.ICanvasManager).canvas for flow in flows.flow: flowWidth, flowHeight = flow.wrap(width, height) if flowWidth <= width and flowHeight <= height: y -= flowHeight - flow.drawOn(self.context, x, y) + flow.drawOn(canvas, x, y) height -= flowHeight else: raise ValueError("Not enough space") -class Param(element.FunctionElement): - args = (attr.Attribute('name'), attr.TextNode() ) +class IParam(interfaces.IRMLDirectiveSignature): + """Sets one paramter for the text annotation.""" + + name = attrng.String( + title=u'Name', + description=u'The name of the paramter.', + required=True) + + value = attrng.TextNode( + title=u'Value', + description=(u'The parameter value.'), + required=True) + +class Param(directive.RMLDirective): + signature = IParam def process(self): - name, value = self.getPositionalArguments() - self.context[name] = value + args = dict(self.getAttributeValues()) + self.parent.params[args['name']] = args['value'] -class TextAnnotation(element.ContainerElement, element.FunctionElement): - args = (attr.FirstLevelTextNode(), ) - paramTypes = { - 'escape': attr.Int(), - } +class ITextAnnotation(interfaces.IRMLDirectiveSignature): + """Writes a low-level text annotation into the PDF.""" + occurence.containing( + occurence.ZeroOrMore('param', IParam)) - subElements = {'param': Param} + contents = attrng.FirstLevelTextNode( + title=u'Contents', + description=u'The PDF commands that are inserted as annotation.', + required=True) + +class TextAnnotation(CanvasRMLDirective): + signature = ITextAnnotation + factories = {'param': Param} + + paramTypes = {'escape': attrng.Integer()} def process(self): - contents = self.getPositionalArguments()[0] - params = {} - self.processSubElements(params) + contents = self.getAttributeValues(valuesOnly=True)[0] + self.params = {} + self.processSubDirectives() for name, type in self.paramTypes.items(): - if name in params: - params[name] = type.convert(params[name]) - self.context.textAnnotation(contents, **params) + if name in self.params: + bound = type.bind(self) + self.params[name] = bound.fromUnicode(self.params[name]) + canvas = attrng.getManager(self, interfaces.ICanvasManager).canvas + canvas.textAnnotation(contents, **self.params) -class MoveTo(element.FunctionElement): - args = ( - attr.TextNodeSequence(attr.Measurement(), length=2), - ) +class IMoveTo(interfaces.IRMLDirectiveSignature): + """Move the path cursor to the specified location.""" + + position = attrng.TextNodeSequence( + title=u'Position', + description=u'Position to which the path pointer is moved to.', + value_type=attrng.Measurement(), + min_length=2, + max_length=2, + required=True) + +class MoveTo(directive.RMLDirective): + signature = IMoveTo def process(self): - args = self.getPositionalArguments() - self.context.moveTo(*args[0]) + args = self.getAttributeValues(valuesOnly=True) + self.parent.path.moveTo(*args[0]) -class CurvesTo(Curves): - functionName = 'curveTo' - args = ( - attr.TextNodeGrid(attr.Measurement(), 6), - ) -class Path(element.FunctionElement): - args = ( - attr.Measurement('x'), - attr.Measurement('y') ) - kw = ( - ('close', attr.Bool('close')), - ('fill', attr.Bool('fill')), - ('stroke', attr.Bool('stroke')) ) +class ICurvesTo(interfaces.IRMLDirectiveSignature): + """Create a bezier curve from the current location to the specified one.""" + + curvelist = attrng.TextNodeGrid( + title=u'Curve Specification', + description=u'Describes the end position and the curve properties.', + value_type=attrng.Measurement(), + columns=6, + required=True) - points = attr.TextNodeGrid(attr.Measurement(), 2) +class CurvesTo(directive.RMLDirective): + signature = ICurvesTo + + def process(self): + argset = self.getAttributeValues(valuesOnly=True)[0] + for args in argset: + self.parent.path.curveTo(*args) + +class IPath(IShape): + """Create a line path.""" + occurence.containing( + occurence.ZeroOrMore('moveTo', IMoveTo), + occurence.ZeroOrMore('curvesTo', ICurvesTo), + ) - subElements = { + points = attrng.TextNodeGrid( + title=u'Points', + description=(u'A list of coordinate points that define th path.'), + value_type=attrng.Measurement(), + columns=2, + required=True) + + close = attrng.Boolean( + title=u'Close Path', + description=(u"A flag specifying whether the path should be closed."), + default=False, + required=False) + +class Path(CanvasRMLDirective): + signature = IPath + factories = { 'moveto': MoveTo, 'curvesto': CurvesTo } @@ -235,107 +491,270 @@ class Path(element.FunctionElement): def processPoints(self, text): if text.strip() == '': return - for coords in self.points.convert(text): + bound = self.signature['points'].bind(self) + for coords in bound.fromUnicode(text): self.path.lineTo(*coords) def process(self): - args = self.getPositionalArguments() - kw = self.getKeywordArguments() + kwargs = dict(self.getAttributeValues(ignore=('points',))) - self.path = self.context.beginPath() - self.path.moveTo(*args) + # Start the path and set the cursor to the start location. + canvas = attrng.getManager(self, interfaces.ICanvasManager).canvas + self.path = canvas.beginPath() + self.path.moveTo(kwargs.pop('x'), kwargs.pop('y')) + # Process the text before the first sub-directive. if self.element.text is not None: self.processPoints(self.element.text) - for subElement in self.element.getchildren(): - if subElement.tag in self.subElements: - self.subElements[subElement.tag]( - subElement, self, self.path).process() - if subElement.tail is not None: - self.processPoints(subElement.tail) - - if kw.pop('close', False): + # Handle each sub-directive. + for directive in self.element.getchildren(): + if directive.tag in self.factories: + self.factories[directive.tag](directive, self).process() + # If there is more text after sub-directive, process it. + if directive.tail is not None: + self.processPoints(directive.tail) + + if kwargs.pop('close', False): self.path.close() - self.context.drawPath(self.path, **kw) + canvas.drawPath(self.path, **kwargs) + + +class IFill(interfaces.IRMLDirectiveSignature): + """Set the fill color.""" + + color = attrng.Color( + title=u'Color', + description=(u'The color value to be set.'), + required=True) + +class Fill(CanvasRMLDirective): + signature = IFill + callable = 'setFillColor' + attrMapping = {'color': 'aColor'} + + +class IStroke(interfaces.IRMLDirectiveSignature): + """Set the fill color.""" + + color = attrng.Color( + title=u'Color', + description=(u'The color value to be set.'), + required=True) + +class Stroke(CanvasRMLDirective): + signature = IStroke + callable = 'setStrokeColor' + attrMapping = {'color': 'aColor'} + + +class ISetFont(interfaces.IRMLDirectiveSignature): + """Set the font name and/or size.""" + + name = attrng.String( + title=u'Font Name', + description=(u'The name of the font as it was registered.'), + required=True) + + size = attrng.Measurement( + title=u'Size', + description=(u'The font size.'), + required=True) + + leading = attrng.Measurement( + title=u'Leading', + description=(u'The font leading.'), + required=False) + +class SetFont(CanvasRMLDirective): + signature = ISetFont + callable = 'setFont' + attrMapping = {'name': 'psfontname'} + + +class IScale(interfaces.IRMLDirectiveSignature): + """Scale the drawing using x and y sclaing factors.""" + + sx = attrng.Float( + title=u'X-Scaling-Factor', + description=(u'The scaling factor applied on x-coordinates.'), + required=True) + + sy = attrng.Float( + title=u'Y-Scaling-Factor', + description=(u'The scaling factor applied on y-coordinates.'), + required=True) +class Scale(CanvasRMLDirective): + signature = IScale + callable = 'scale' + attrMapping = {'sx': 'x', 'sy': 'y'} -class Fill(element.FunctionElement): - functionName = 'setFillColor' - args = ( - attr.Color('color'), ) +class ITranslate(interfaces.IRMLDirectiveSignature): + """Translate the drawing coordinates by the specified x and y offset.""" -class Stroke(element.FunctionElement): - functionName = 'setStrokeColor' - args = ( - attr.Color('color'), ) + dx = attrng.Measurement( + title=u'X-Offset', + description=(u'The amount to move the drawing to the right.'), + required=True) + dy = attrng.Measurement( + title=u'Y-Offset', + description=(u'The amount to move the drawing upward.'), + required=True) -class SetFont(element.FunctionElement): - functionName = 'setFont' - args = ( - attr.Text('name'), - attr.Measurement('size'), ) +class Translate(CanvasRMLDirective): + signature = ITranslate + callable = 'translate' -class Scale(element.FunctionElement): - functionName = 'scale' - args = (attr.Float('sx'), attr.Float('sy'), ) +class IRotate(interfaces.IRMLDirectiveSignature): + """Rotate the drawing counterclockwise.""" + degrees = attrng.Measurement( + title=u'Angle', + description=(u'The angle in degrees.'), + required=True) -class Translate(element.FunctionElement): - functionName = 'translate' - args = (attr.Measurement('dx', 0), attr.Measurement('dy', 0), ) +class Rotate(CanvasRMLDirective): + signature = IRotate + callable = 'rotate' + attrMapping = {'degrees': 'theta'} -class Rotate(element.FunctionElement): - functionName = 'rotate' - args = (attr.Float('degrees'), ) +class ISkew(interfaces.IRMLDirectiveSignature): + """Skew the drawing.""" + alpha = attrng.Measurement( + title=u'Alpha', + description=(u'The amount to skew the drawing in the horizontal.'), + required=True) -class Skew(element.FunctionElement): - functionName = 'skew' - args = (attr.Measurement('alpha'), attr.Measurement('beta'), ) + beta = attrng.Measurement( + title=u'Beta', + description=(u'The amount to skew the drawing in the vertical.'), + required=True) +class Skew(CanvasRMLDirective): + signature = ISkew + callable = 'skew' -class Transform(element.FunctionElement): - functionName = 'transform' - args = (attr.TextNodeSequence(attr.Float()), ) + +class ITransform(interfaces.IRMLDirectiveSignature): + """A full 2-D matrix transformation""" + + matrix = attrng.TextNodeSequence( + title=u'Matrix', + description=u'The transformation matrix.', + value_type=attrng.Float(), + min_length=6, + max_length=6, + required=True) + +class Transform(CanvasRMLDirective): + signature = ITransform def process(self): - args = self.getPositionalArguments() - getattr(self.context, self.functionName)(*args[0]) - - -class LineMode(element.FunctionElement): - kw = ( - ('width', attr.Measurement('width')), - ('dash', attr.Sequence('dash', attr.Measurement())), - ('miterLimit', attr.Measurement('miterLimit')), - ('join', attr.Choice( - 'join', {'round': 1, 'mitered': 0, 'bevelled': 2})), - ('cap', attr.Choice( - 'cap', {'default': 0, 'round': 1, 'square': 2})), - ) + args = self.getAttributeValues(valuesOnly=True) + canvas = attrng.getManager(self, interfaces.ICanvasManager).canvas + canvas.transform(*args[0]) + + +class ILineMode(interfaces.IRMLDirectiveSignature): + """Set the line mode for the following graphics elements.""" + + width = attrng.Measurement( + title=u'Width', + description=(u'The line width.'), + required=False) + + dash = attrng.Sequence( + title=u'Dash-Pattern', + description=(u'The dash-pattern of a line.'), + value_type=attrng.Measurement(), + required=False) + + miterLimit = attrng.Measurement( + title=u'Miter Limit', + description=(u'The ???.'), + required=False) + + join = attrng.Choice( + title=u'Join', + description=u'The way lines are joined together.', + choices=interfaces.JOIN_CHOICES, + required=False) + + cap = attrng.Choice( + title=u'Cap', + description=u'The cap is the desciption of how the line-endings look.', + choices=interfaces.CAP_CHOICES, + required=False) + +class LineMode(CanvasRMLDirective): + signature = ILineMode def process(self): - kw = self.getKeywordArguments() + kw = dict(self.getAttributeValues()) + canvas = attrng.getManager(self, interfaces.ICanvasManager).canvas if 'width' in kw: - self.context.setLineWidth(kw['width']) + canvas.setLineWidth(kw['width']) if 'join' in kw: - self.context.setLineJoin(kw['join']) + canvas.setLineJoin(kw['join']) if 'cap' in kw: - self.context.setLineCap(kw['cap']) + canvas.setLineCap(kw['cap']) if 'miterLimit' in kw: - self.context.setMiterLimit(kw['miterLimit']) + canvas.setMiterLimit(kw['miterLimit']) if 'dash' in kw: - self.context.setDash(kw['dash']) - + canvas.setDash(kw['dash']) + + +class IDrawing(interfaces.IRMLDirectiveSignature): + """A container directive for all directives that draw directly on the + cnavas.""" + occurence.containing( + occurence.ZeroOrMore('drawString', IDrawString), + occurence.ZeroOrMore('drawRightString', IDrawRightString), + occurence.ZeroOrMore('drawCenteredString', IDrawCenteredString), + occurence.ZeroOrMore('drawCentredString', IDrawCenteredString), + occurence.ZeroOrMore('drawAlignedString', IDrawAlignedString), + # Drawing Operations + occurence.ZeroOrMore('ellipse', IEllipse), + occurence.ZeroOrMore('circle', ICircle), + occurence.ZeroOrMore('rect', IRectangle), + occurence.ZeroOrMore('grid', IGrid), + occurence.ZeroOrMore('lines', ILines), + occurence.ZeroOrMore('curves', ICurves), + occurence.ZeroOrMore('image', IImage), + occurence.ZeroOrMore('place', IPlace), + occurence.ZeroOrMore('textAnnotation', ITextAnnotation), + occurence.ZeroOrMore('path', IPath), + # State Change Operations + occurence.ZeroOrMore('fill', IFill), + occurence.ZeroOrMore('stroke', IStroke), + occurence.ZeroOrMore('setFont', ISetFont), + occurence.ZeroOrMore('scale', IScale), + occurence.ZeroOrMore('translate', ITranslate), + occurence.ZeroOrMore('rotate', IRotate), + occurence.ZeroOrMore('skew', ISkew), + occurence.ZeroOrMore('transform', ITransform), + occurence.ZeroOrMore('lineMode', ILineMode), + # Form Field Elements + occurence.ZeroOrMore('barCode', form.IBarCode), + # Charts + #ZeroOrMore('barChart', IBarChart), + #ZeroOrMore('barChart3D', IBarChart3D), + #ZeroOrMore('linePlot', ILinePlot), + #ZeroOrMore('pieChart', IPieChart), + #ZeroOrMore('pieChart3D', IPieChart3D), + #ZeroOrMore('spiderChart', ISpiderChart), + ) -class Drawing(element.ContainerElement): +class Drawing(directive.RMLDirective): + signature = IDrawing - subElements = { + factories = { 'drawString': DrawString, 'drawRightString': DrawRightString, 'drawCenteredString': DrawCenteredString, @@ -374,58 +793,37 @@ class Drawing(element.ContainerElement): } +class IPageDrawing(IDrawing): + """Draws directly on the content of one page's canvas. Every call of this + directive creates a new page.""" + + occurence.containing( + #'mergePage': IMergePage, + *IDrawing.getTaggedValue('directives')) + class PageDrawing(Drawing): + signature = IDrawing - subElements = Drawing.subElements.copy() - subElements.update({ + factories = Drawing.factories.copy() + factories.update({ 'mergePage': page.MergePage }) def process(self): super(Drawing, self).process() - self.context.showPage() - - -class PageInfo(element.Element): - - def process(self): - pageSize = attr.PageSize('pageSize').get(self.element) - self.context.setPageSize(pageSize) + canvas = attrng.getManager(self, interfaces.ICanvasManager).canvas + canvas.showPage() -class Canvas(element.ContainerElement): - zope.interface.implements(interfaces.IPostProcessorManager) +class IPageInfo(interfaces.IRMLDirectiveSignature): + """Set's up page-global settings.""" - subElements = { - 'stylesheet': stylesheet.Stylesheet, - 'pageDrawing': PageDrawing, - 'pageInfo': PageInfo, - } + pageSize = attrng.PageSize( + title=u'Page Size', + description=(u'The page size of all pages within this document.'), + required=True) - def __init__(self, element, parent, context): - super(Canvas, self).__init__(element, parent, context) - self.postProcessors = [] - - def process(self, outputFile): - verbosity = attr.Bool('verbosity').get(self.element, 0) - compression = attr.DefaultBool('compression').get(self.element, 0) - - # Create a temporary output file, so that post-processors can massage - # the output - tempOutput = cStringIO.StringIO() - - canvas = reportlab.pdfgen.canvas.Canvas( - tempOutput, - pageCompression=compression, - verbosity=verbosity) - self.processSubElements(canvas) - canvas.save() - - # Process all post processors - for name, processor in self.postProcessors: - tempOutput.seek(0) - tempOutput = processor.process(tempOutput) - - # Save the result into our real output file - tempOutput.seek(0) - outputFile.write(tempOutput.getvalue()) +class PageInfo(CanvasRMLDirective): + signature=IPageInfo + callable = 'setPageSize' + attrMapping = {'pageSize': 'size'} diff --git a/src/z3c/rml/chart.py b/src/z3c/rml/chart.py index 77f738f..cb441d4 100644 --- a/src/z3c/rml/chart.py +++ b/src/z3c/rml/chart.py @@ -20,62 +20,94 @@ from reportlab.graphics import shapes from reportlab.graphics.charts import barcharts, lineplots, piecharts from reportlab.graphics.charts import spider, doughnut -from z3c.rml import attr, element +from z3c.rml import attrng, directive, interfaces, occurence # Patches against Reportlab 2.0 lineplots.Formatter = reportlab.lib.formatters.Formatter -class PropertyItem(element.Element): - attrs = None +class PropertyItem(directive.RMLDirective): def process(self): - attrs = element.extractAttributes(self.attrs, self.element, self) - self.context.append(attrs) + attrs = dict(self.getAttributeValues()) + self.parent.dataList.append(attrs) -class PropertyCollection(element.ContainerElement): +class PropertyCollection(directive.RMLDirective): propertyName = None - attrs = None - subElements = None def processAttributes(self): - prop = getattr(self.context, self.propertyName) + prop = getattr(self.parent.context, self.propertyName) # Get global properties - attrs = element.extractAttributes(self.attrs, self.element, self) - for name, value in attrs.items(): + for name, value in self.getAttributeValues(): setattr(prop, name, value) def process(self): self.processAttributes() # Get item specific properties - prop = getattr(self.context, self.propertyName) - dataList = [] - self.processSubElements(dataList) - for index, data in enumerate(dataList): + prop = getattr(self.parent.context, self.propertyName) + self.dataList = [] + self.processSubDirectives() + for index, data in enumerate(self.dataList): for name, value in data.items(): setattr(prop[index], name, value) -class Text(element.Element): - attrs = ( - attr.Measurement('x'), - attr.Measurement('y'), - attr.Float('angle', 0), - attr.TextNode(), - attr.Measurement('fontSize'), - attr.Color('fillColor'), - attr.Text('fontName'), - attr.Choice( - 'textAnchor', - ('start','middle','end','boxauto')), - ) +class IText(interfaces.IRMLDirectiveSignature): + """Draw a text on the chart.""" + + x = attrng.Measurement( + title=u'X-Coordinate', + description=(u'The X-coordinate of the lower-left position of the ' + u'text.'), + required=True) + + y = attrng.Measurement( + title=u'Y-Coordinate', + description=(u'The Y-coordinate of the lower-left position of the ' + u'text.'), + required=True) + + angle = attrng.Float( + title=u'Rotation Angle', + description=(u'The angle about which the text will be rotated.'), + required=False) + + text = attrng.TextNode( + title=u'Text', + description=u'The text to be printed.', + required=True) + + fontName = attrng.String( + title=u'Font Name', + description=u'The name of the font.', + required=False) + + fontSize = attrng.Measurement( + title=u'Font Size', + description=u'The font size for the text.', + required=False) + + fillColor = attrng.Color( + title=u'Fill Color', + description=u'The color in which the text will appear.', + required=False) + + textAnchor = attrng.Choice( + title=u'Text Anchor', + description=u'The position in the text to which the coordinates refer.', + choices=('start', 'middle', 'end', 'boxauto'), + required=False) + + +class Text(directive.RMLDirective): + signature = IText def process(self): - attrs = element.extractAttributes(self.attrs, self.element, self) + attrs = dict(self.getAttributeValues()) string = shapes.String( - attrs.pop('x'), attrs.pop('y'), attrs.pop('TEXT')) - angle = attrs.pop('angle') + attrs.pop('x'), attrs.pop('y'), attrs.pop('text')) + angle = attrs.pop('angle', 0) for name, value in attrs.items(): setattr(string, name, value) group = shapes.Group(string) @@ -84,29 +116,45 @@ def process(self): self.parent.parent.drawing.add(group) -class Texts(element.ContainerElement): - subElements = {'text': Text} +class ITexts(interfaces.IRMLDirectiveSignature): + """A set of texts drawn on the chart.""" + occurence.containing( + occurence.ZeroOrMore('text', IText) + ) + +class Texts(directive.RMLDirective): + signature = ITexts + factories = {'text': Text} -class Series(element.Element): - attrList = None +class Series(directive.RMLDirective): def process(self): - attrs = element.extractPositionalArguments( - self.attrList, self.element, self) - self.context.append(attrs[0]) + attrs = self.getAttributeValues(valuesOnly=True) + self.parent.data.append(attrs[0]) -class Data(element.ContainerElement): + +class Data(directive.RMLDirective): series = None def process(self): - data = [] - self.subElements = {'series': self.series} - self.processSubElements(data) - self.context.data = data + self.data = [] + self.factories = {'series': self.series} + self.processSubDirectives() + self.parent.context.data = self.data + + +class ISeries1D(interfaces.IRMLDirectiveSignature): + """A one-dimensional series.""" + + values = attrng.TextNodeSequence( + title=u'Values', + description=u"Numerical values representing the series' data.", + value_type=attrng.Float(), + required=True) class Series1D(Series): - attrList = (attr.TextNodeSequence(attr.Float()),) + signature = ISeries1D class Data1D(Data): series = Series1D @@ -114,414 +162,852 @@ class Data1D(Data): class SingleData1D(Data1D): def process(self): - data = [] - self.subElements = {'series': self.series} - self.processSubElements(data) - self.context.data = data[0] + self.data = [] + self.factories = {'series': self.series} + self.processSubDirectives() + self.parent.context.data = self.data[0] + + +class ISeries2D(interfaces.IRMLDirectiveSignature): + """A two-dimensional series.""" + + values = attrng.TextNodeGrid( + title=u'Values', + description=u"Numerical values representing the series' data.", + value_type=attrng.Float(), + columns=2, + required=True) class Series2D(Series): - attrList = (attr.TextNodeGrid(attr.Float(), 2),) + signature = ISeries2D class Data2D(Data): series = Series2D +class IBar(interfaces.IRMLDirectiveSignature): + """Define the look of a bar.""" + + strokeColor = attrng.Color( + title=u'Stroke Color', + description=u'The color in which the bar border is drawn.', + required=False) + + strokeWidth = attrng.Measurement( + title=u'Stroke Width', + description=u'The width of the bar border line.', + required=False) + + fillColor = attrng.Color( + title=u'Fill Color', + description=u'The color with which the bar is filled.', + required=False) + class Bar(PropertyItem): - attrs = ( - attr.Color('strokeColor'), - attr.Measurement('strokeWidth'), - attr.Color('fillColor') ) + signature = IBar +class IBars(IBar): + """Collection of bar subscriptions.""" + occurence.containing( + occurence.ZeroOrMore('bar', IBar) + ) class Bars(PropertyCollection): + signature = IBars propertyName = 'bars' - attrs = Bar.attrs - subElements = {'bar': Bar} - + factories = {'bar': Bar} + + +class ILabelBase(interfaces.IRMLDirectiveSignature): + + dx = attrng.Measurement( + title=u'Horizontal Extension', + description=(u'The width of the label.'), + required=False) + + dy = attrng.Measurement( + title=u'Vertical Extension', + description=(u'The height of the label.'), + required=False) + + angle = attrng.Float( + title=u'Angle', + description=(u'The angle to rotate the label.'), + required=False) + + boxAnchor = attrng.Choice( + title=u'Box Anchor', + description=(u'The position relative to the label.'), + choices=('nw','n','ne','w','c','e','sw','s','se', 'autox', 'autoy'), + required=False) + + boxStrokeColor = attrng.Color( + title=u'Box Stroke Color', + description=(u'The color of the box border line.'), + required=False) + + boxStrokeWidth = attrng.Measurement( + title=u'Box Stroke Width', + description=u'The width of the box border line.', + required=False) + + boxFillColor = attrng.Color( + title=u'Box Fill Color', + description=(u'The color in which the box is filled.'), + required=False) + + boxTarget = attrng.Text( + title=u'Box Target', + description=u'The box target.', + required=False) + + fillColor = attrng.Color( + title=u'Fill Color', + description=(u'The color in which the label is filled.'), + required=False) + + strokeColor = attrng.Color( + title=u'Stroke Color', + description=(u'The color of the label.'), + required=False) + + strokeWidth = attrng.Measurement( + title=u'Stroke Width', + description=u'The width of the label line.', + required=False) + + frontName = attrng.String( + title=u'Font Name', + description=u'The font used to print the value.', + required=False) + + frontSize = attrng.Measurement( + title=u'Font Size', + description=u'The size of the value text.', + required=False) + + leading = attrng.Measurement( + title=u'Leading', + description=(u'The height of a single text line. It includes ' + u'character height.'), + required=False) + + width = attrng.Measurement( + title=u'Width', + description=u'The width the label.', + required=False) + + maxWidth = attrng.Measurement( + title=u'Maximum Width', + description=u'The maximum width the label.', + required=False) + + height = attrng.Measurement( + title=u'Height', + description=u'The height the label.', + required=False) + + textAnchor = attrng.Choice( + title=u'Text Anchor', + description=u'The position in the text to which the coordinates refer.', + choices=('start', 'middle', 'end', 'boxauto'), + required=False) + + visible = attrng.Boolean( + title=u'Visible', + description=u'A flag making the label text visible.', + required=False) + + leftPadding = attrng.Measurement( + title=u'Left Padding', + description=u'The size of the padding on the left side.', + required=False) + + rightPadding = attrng.Measurement( + title=u'Right Padding', + description=u'The size of the padding on the right side.', + required=False) + + topPadding = attrng.Measurement( + title=u'Top Padding', + description=u'The size of the padding on the top.', + required=False) + + bottomPadding = attrng.Measurement( + title=u'Bottom Padding', + description=u'The size of the padding on the bottom.', + required=False) + + +class IPositionLabelBase(ILabelBase): + + x = attrng.Measurement( + title=u'X-Coordinate', + description=(u'The X-coordinate of the lower-left position of the ' + u'label.'), + required=False) + + y = attrng.Measurement( + title=u'Y-Coordinate', + description=(u'The Y-coordinate of the lower-left position of the ' + u'label.'), + required=False) + + +class ILabel(IPositionLabelBase): + """A label for the chart.""" + + text = attrng.TextNode( + title=u'Text', + description=u'The label text to be displayed.', + required=True) class Label(PropertyItem): - attrs = ( - attr.Measurement('x'), - attr.Measurement('y'), - attr.Measurement('dx'), - attr.Measurement('dy'), - attr.Float('angle'), - attr.Choice( - 'boxAnchor', - ('nw','n','ne','w','c','e','sw','s','se', 'autox', 'autoy')), - attr.Color('boxStrokeColor'), - attr.Measurement('boxStrokeWidth'), - attr.Color('boxFillColor'), - attr.Text('boxTarget'), - attr.Color('fillColor'), - attr.Color('strokeColor'), - attr.Measurement('strokeWidth'), - attr.Text('fontName'), - attr.Measurement('fontSize'), - attr.Measurement('leading'), - attr.Measurement('width'), - attr.Measurement('maxWidth'), - attr.Measurement('height'), - attr.Choice('textAnchor', ('start','middle','end','boxauto')), - attr.Bool('visible'), - attr.Measurement('topPadding'), - attr.Measurement('leftPadding'), - attr.Measurement('rightPadding'), - attr.Measurement('bottomPadding'), - attr.TextNode() - ) - attrs[-1].name = 'text' + signature = ILabel + +class ILabels(IPositionLabelBase): + """A set of labels.""" + occurence.containing( + occurence.ZeroOrMore('label', ILabel) + ) class Labels(PropertyCollection): + signature = ILabels propertyName = 'labels' - attrs = Label.attrs[:-1] - subElements = {'label': Label} + factories = {'label': Label} -class Axis(element.ContainerElement): - name = '' - attrs = ( - attr.Bool('visible'), - attr.Bool('visibleAxis'), - attr.Bool('visibleTicks'), - attr.Bool('visibleLabels'), - attr.Bool('visibleGrid'), - attr.Measurement('strokeWidth'), - attr.Color('strokeColor'), - attr.Sequence('strokeDashArray', attr.Float()), - attr.Measurement('gridStrokeWidth'), - attr.Color('gridStrokeColor'), - attr.Sequence('gridStrokeDashArray', attr.Float()), - attr.Measurement('gridStart'), - attr.Measurement('gridEnd'), - attr.Choice('style', ('parallel', 'stacked', 'parallel_3d')), +class IAxis(interfaces.IRMLDirectiveSignature): + occurence.containing( + occurence.ZeroOrMore('labels', ILabels) ) - subElements = {'labels': Labels} + visible = attrng.Boolean( + required=False) + + visibleAxis = attrng.Boolean( + required=False) + + visibleTicks = attrng.Boolean( + required=False) + + visibleLabels = attrng.Boolean( + required=False) + + visibleGrid = attrng.Boolean( + required=False) + + strokeWidth = attrng.Measurement( + required=False) + + strokeColor = attrng.Color( + required=False) + + strokeDashArray = attrng.Sequence( + value_type=attrng.Float(), + required=False) + + gridStrokeWidth = attrng.Measurement( + required=False) + + gridStrokeColor = attrng.Color( + required=False) + + gridStrokeDashArray = attrng.Sequence( + value_type=attrng.Float(), + required=False) + + gridStart = attrng.Measurement( + required=False) + + gridEnd = attrng.Measurement( + required=False) + + style = attrng.Choice( + choices=('parallel', 'stacked', 'parallel_3d'), + required=False) + + +class Axis(directive.RMLDirective): + signature = IAxis + name = '' + factories = {'labels': Labels} def process(self): - attrs = element.extractAttributes(self.attrs, self.element, self) - axis = getattr(self.context, self.__name__) - for name, value in attrs.items(): + self.context = axis = getattr(self.parent.context, self.name) + for name, value in self.getAttributeValues(): setattr(axis, name, value) - self.processSubElements(axis) + self.processSubDirectives() -class Name(element.Element): - attrs = (attr.TextNode(),) +class IName(interfaces.IRMLDirectiveSignature): + + text = attrng.TextNode( + title=u'Text', + required=True) + +class Name(directive.RMLDirective): + signature = IName def process(self): - attrs = element.extractAttributes(self.attrs, self.element, self) - self.context.append(attrs['TEXT']) + text = self.getAttributeValues(valuesOnly=True)[0] + self.parent.names.append(text) -class CategoryNames(element.ContainerElement): - subElements = {'name': Name} +class CategoryNames(directive.RMLDirective): + factories = {'name': Name} def process(self): - self.context.categoryNames = [] - self.processSubElements(self.context.categoryNames) + self.names = [] + self.processSubDirectives() + self.parent.context.categoryNames = self.names + + +class ICategoryAxis(IAxis): + + categoryNames = attrng.Sequence( + value_type=attrng.Text(), + required=False) + + joinAxis = attrng.Boolean( + required=False) + + joinAxisPos = attrng.Measurement( + required=False) + + reverseDirection = attrng.Boolean( + required=False) + + labelAxisMode = attrng.Choice( + choices=('high', 'low', 'axis'), + required=False) + + tickShift = attrng.Boolean( + required=False) class CategoryAxis(Axis): + signature = ICategoryAxis name = 'categoryAxis' - attrs = Axis.attrs + ( - attr.Sequence('categoryNames', attr.Text()), - attr.Bool('joinAxis'), - attr.Measurement('joinAxisPos'), - attr.Bool('reverseDirection'), - attr.Choice('labelAxisMode', ('high', 'low', 'axis')), - attr.Bool('tickShift'), - ) - subElements = Axis.subElements.copy() - subElements.update({ + factories = Axis.factories.copy() + factories.update({ 'categoryNames': CategoryNames, }) + +class IXCategoryAxis(ICategoryAxis): + + tickUp = attrng.Measurement( + required=False) + + tickDown = attrng.Measurement( + required=False) + + joinAxisMode = attrng.Choice( + choices=('bottom', 'top', 'value', 'points', 'None'), + required=False) + class XCategoryAxis(CategoryAxis): - attrs = CategoryAxis.attrs + ( - attr.Measurement('tickUp'), - attr.Measurement('tickDown'), - attr.Choice('joinAxisMode', - ('bottom', 'top', 'value', 'points', 'None')) ) + signature = IXCategoryAxis + + +class IYCategoryAxis(ICategoryAxis): + + tickLeft = attrng.Measurement( + required=False) + tickRight = attrng.Measurement( + required=False) + + joinAxisMode = attrng.Choice( + choices=('bottom', 'top', 'value', 'points', 'None'), + required=False) class YCategoryAxis(CategoryAxis): - attrs = CategoryAxis.attrs + ( - attr.Measurement('tickLeft'), - attr.Measurement('tickRight'), - attr.Choice('joinAxisMode', - ('bottom', 'top', 'value', 'points', 'None')) ) + signature = IYCategoryAxis + + +class IValueAxis(IAxis): + + forceZero = attrng.Boolean( + required=False) + + minimumTickSpacing = attrng.Measurement( + required=False) + + maximumTicks = attrng.Integer( + required=False) + + labelTextFormat = attrng.String( + required=False) + labelTextPostFormat = attrng.Text( + required=False) + + labelTextScale = attrng.Float( + required=False) + + valueMin = attrng.Float( + required=False) + + valueMax = attrng.Float( + required=False) + + valueStep = attrng.Float( + required=False) + + valueSteps = attrng.Measurement( + required=False) + + rangeRound = attrng.Text( + required=False) + + zrangePref = attrng.Float( + required=False) class ValueAxis(Axis): + signature = IValueAxis name = 'valueAxis' - attrs = Axis.attrs + ( - attr.Bool('forceZero'), # TODO: Support 'near' - attr.Measurement('minimumTickSpacing'), - attr.Int('maximumTicks'), - attr.Attribute('labelTextFormat'), - attr.Text('labelTextPostFormat'), - attr.Float('labelTextScale'), - attr.Float('valueMin'), - attr.Float('valueMax'), - attr.Float('valueStep'), - attr.Measurement('valueSteps'), - attr.Text('rangeRound'), - attr.Float('zrangePref'), - ) +class IXValueAxis(IValueAxis): + + tickUp = attrng.Measurement( + required=False) + + tickDown = attrng.Measurement( + required=False) + + joinAxis = attrng.Boolean( + required=False) + + joinAxisMode = attrng.Choice( + choices=('bottom', 'top', 'value', 'points', 'None'), + required=False) + + joinAxisPos = attrng.Measurement( + required=False) + class XValueAxis(ValueAxis): - attrs = ValueAxis.attrs + ( - attr.Measurement('tickUp'), - attr.Measurement('tickDown'), - attr.Bool('joinAxis'), - attr.Choice('joinAxisMode', - ('bottom', 'top', 'value', 'points', 'None')), - attr.Float('joinAxisPos'), - ) + signature = IXValueAxis + +class LineXValueAxis(XValueAxis): + name = 'xValueAxis' + +class IYValueAxis(IValueAxis): + + tickLeft = attrng.Measurement( + required=False) + + tickRight = attrng.Measurement( + required=False) + + joinAxis = attrng.Boolean( + required=False) + + joinAxisMode = attrng.Choice( + choices=('bottom', 'top', 'value', 'points', 'None'), + required=False) + + joinAxisPos = attrng.Measurement( + required=False) class YValueAxis(ValueAxis): - attrs = ValueAxis.attrs + ( - attr.Measurement('tickLeft'), - attr.Measurement('tickRight'), - attr.Bool('joinAxis'), - attr.Choice('joinAxisMode', - ('bottom', 'top', 'value', 'points', 'None')), - attr.Float('joinAxisPos'), - ) + signature = IYValueAxis + +class LineYValueAxis(YValueAxis): + name = 'yValueAxis' + + +class ILineBase(interfaces.IRMLDirectiveSignature): + + strokeWidth = attrng.Measurement( + required=False) + + strokeColor = attrng.Color( + required=False) + + strokeDashArray = attrng.Sequence( + value_type = attrng.Float(), + required=False) + + symbol = attrng.Symbol( + required=False) +class ILine(ILineBase): + + name = attrng.Text( + required=False) class Line(PropertyItem): - attrs = ( - attr.Measurement('strokeWidth'), - attr.Color('strokeColor'), - attr.Sequence('strokeDashArray', attr.Float()), - attr.Symbol('symbol'), - attr.Text('name'), - ) + signature = ILine +class ILines(ILineBase): + pass class Lines(PropertyCollection): + signature = ILines propertyName = 'lines' - attrs = Line.attrs[:-1] - subElements = {'line': Line} + factories = {'line': Line} + + +class ISliceLabel(ILabelBase): + text = attrng.TextNode( + title=u'Text', + description=u'The label text to be displayed.', + required=True) class SliceLabel(Label): - attrs = Label.attrs[2:] + signature = ISliceLabel def process(self): - attrs = element.extractAttributes(self.attrs, self.element, self) - for name, value in attrs.items(): - self.context['label_'+name] = value + for name, value in self.getAttributeValues(): + self.parent.context['label_'+name] = value # Now we do not have simple labels anymore - self.parent.parent.context.simpleLabels = False - -class SlicePointer(element.Element): - attrs = ( - attr.Color('strokeColor'), - attr.Measurement('strokeWidth'), - attr.Measurement('elbowLength'), - attr.Measurement('edgePad'), - attr.Measurement('piePad'), - ) + self.parent.parent.parent.context.simpleLabels = False + + +class ISlicePointer(interfaces.IRMLDirectiveSignature): + + strokeColor = attrng.Color( + required=False) + + strokeWidth = attrng.Measurement( + required=False) + + elbowLength = attrng.Measurement( + required=False) + + edgePad = attrng.Measurement( + required=False) + + piePad = attrng.Measurement( + required=False) + +class SlicePointer(directive.RMLDirective): + signature = ISlicePointer def process(self): - attrs = element.extractAttributes(self.attrs, self.element, self) - for name, value in attrs.items(): - self.context['label_pointer_'+name] = value - -class Slice(element.ContainerElement): - attrs = ( - attr.Measurement('strokeWidth'), - attr.Color('fillColor'), - attr.Color('strokeColor'), - attr.Sequence('strokeDashArray', attr.Float()), - attr.Measurement('popout'), - attr.Text('fontName'), - attr.Measurement('fontSize'), - attr.Measurement('labelRadius'), - attr.Symbol('swatchMarker'), - ) + for name, value in self.getAttributeValues(): + self.parent.context['label_pointer_'+name] = value + + +class ISliceBase(interfaces.IRMLDirectiveSignature): + + strokeWidth = attrng.Measurement( + required=False) + + fillColor = attrng.Color( + required=False) + + strokeColor = attrng.Color( + required=False) + + strokeDashArray = attrng.Sequence( + value_type=attrng.Float(), + required=False) + + popout = attrng.Measurement( + required=False) - subElements = { + fontName = attrng.String( + required=False) + + fontSize = attrng.Measurement( + required=False) + + labelRadius = attrng.Measurement( + required=False) + +class ISlice(ISliceBase): + + swatchMarker = attrng.Symbol( + required=False) + + +class Slice(directive.RMLDirective): + signature = ISlice + factories = { 'label': SliceLabel, 'pointer': SlicePointer} def process(self): - attrs = element.extractAttributes(self.attrs, self.element, self) - self.processSubElements(attrs) - self.context.append(attrs) + self.context = attrs = dict(self.getAttributeValues()) + self.processSubDirectives() + self.parent.context.append(attrs) -class Slice3D(Slice): - attrs = Slice.attrs + ( - attr.Color('fillColorShaded'), - ) +class ISlice3D(ISlice): + + fillColorShaded = attrng.Color( + required=False) + +class Slice3D(Slice): + signature = ISlice3D subElements = {} # Sigh, the 3-D Pie does not support advanced slice labels. :-( # 'label': SliceLabel} -class Slices(element.ContainerElement): - attrs = Slice.attrs[:-1] - subElements = {'slice': Slice} + +class ISlices(ISliceBase): + pass + +class Slices(directive.RMLDirective): + signature = ISlices + factories = {'slice': Slice} def process(self): # Get global slice properties - attrs = element.extractAttributes(self.attrs, self.element, self) - for name, value in attrs.items(): - setattr(self.context.slices, name, value) + for name, value in self.getAttributeValues(): + setattr(self.parent.context.slices, name, value) # Get slice specific properties - slicesData = [] - self.processSubElements(slicesData) + self.context = slicesData = [] + self.processSubDirectives() for index, sliceData in enumerate(slicesData): for name, value in sliceData.items(): - setattr(self.context.slices[index], name, value) + setattr(self.parent.context.slices[index], name, value) + + +class ISlices3D(ISliceBase): + + fillColorShaded = attrng.Color( + required=False) class Slices3D(Slices): - attrs = Slice3D.attrs[:-1] - subElements = {'slice': Slice3D} + signature = ISlices3D + factories = {'slice': Slice3D} -class SimpleLabels(element.ContainerElement): - subElements = {'label': Name} +class SimpleLabels(directive.RMLDirective): + factories = {'label': Name} def process(self): - self.context.labels = [] - self.processSubElements(self.context.labels) + self.names = [] + self.processSubDirectives() + self.parent.context.labels = self.names -class Strand(PropertyItem): - attrs = ( - attr.Measurement('strokeWidth'), - attr.Color('fillColor'), - attr.Color('strokeColor'), - attr.Sequence('strokeDashArray', attr.Float()), - attr.Symbol('symbol'), - attr.Measurement('symbolSize'), - attr.Text('name'), - ) +class IStrandBase(interfaces.IRMLDirectiveSignature): + + strokeWidth = attrng.Measurement( + required=False) + + fillColor = attrng.Color( + required=False) + + strokeColor= attrng.Color( + required=False) + strokeDashArray = attrng.Sequence( + value_type=attrng.Float(), + required=False) + + symbol = attrng.Symbol( + required=False) + + symbolSize = attrng.Measurement( + required=False) + +class IStrand(IStrandBase): + + name = attrng.Text( + required=False) + +class Strand(PropertyItem): + signature = IStrand class Strands(PropertyCollection): + signature = IStrandBase propertyName = 'strands' - attrs = Strand.attrs[:-1] - subElements = {'strand': Strand} + attrs = IStrandBase + factories = {'strand': Strand} + + +class IStrandLabelBase(ILabelBase): + _text = attrng.TextNode( + required=False) + + row = attrng.Integer( + required=False) + + col = attrng.Integer( + required=False) + + format = attrng.String( + required=False) + +class IStrandLabel(IStrandLabelBase): + + dR = attrng.Float( + required=False) class StrandLabel(Label): - attrs = Label.attrs[2:-1] + (attr.TextNode(),) - attrs[-1].name = '_text' - attrs += ( - attr.Int('row'), - attr.Int('col'), - attr.Attribute('format'), - attr.Float('dR') - ) + signature = IStrandLabel class StrandLabels(PropertyCollection): + signature = IStrandLabelBase propertyName = 'strandLabels' - attrs = StrandLabel.attrs[:-1] - subElements = {'label': StrandLabel} + factories = {'label': StrandLabel} def process(self): self.processAttributes() # Get item specific properties - prop = getattr(self.context, self.propertyName) - dataList = [] - self.processSubElements(dataList) - for data in dataList: + prop = getattr(self.parent.context, self.propertyName) + self.dataList = [] + self.processSubDirectives() + for data in self.dataList: row = data.pop('row') col = data.pop('col') for name, value in data.items(): setattr(prop[row, col], name, value) -class Spoke(PropertyItem): - attrs = ( - attr.Measurement('strokeWidth'), - attr.Color('fillColor'), - attr.Color('strokeColor'), - attr.Sequence('strokeDashArray', attr.Float()), - attr.Measurement('labelRadius'), - attr.Bool('visible'), - ) +class ISpoke(interfaces.IRMLDirectiveSignature): + + strokeWidth = attrng.Measurement( + required=False) + + fillColor = attrng.Color( + required=False) + + strokeColor= attrng.Color( + required=False) + strokeDashArray = attrng.Sequence( + value_type=attrng.Float(), + required=False) + + labelRadius = attrng.Measurement( + required=False) + + visible = attrng.Measurement( + required=False) + +class Spoke(PropertyItem): + signature = ISpoke class Spokes(PropertyCollection): + signature = ISpoke propertyName = 'spokes' - attrs = Spoke.attrs[:-1] - subElements = {'spoke': Spoke} + factories = {'spoke': Spoke} +class ISpokeLabelBase(ILabelBase): + pass + +class ISpokeLabel(ISpokeLabelBase): + + _text = attrng.TextNode( + required=False) + class SpokeLabel(Label): - attrs = Label.attrs[2:-1] + (attr.TextNode(),) - attrs[-1].name = '_text' + signature = ISpokeLabel class SpokeLabels(PropertyCollection): + signature = ISpokeLabelBase propertyName = 'spokeLabels' - attrs = SpokeLabel.attrs[:-1] - subElements = {'label': SpokeLabel} - - -class Chart(element.ContainerElement): - attrs = ( - # Drawing Options - attr.Measurement('dx'), - attr.Measurement('dy'), - attr.Measurement('dwidth'), - attr.Measurement('dheight'), - attr.Float('angle'), - # Plot Area Options - attr.Measurement('x'), - attr.Measurement('y'), - attr.Measurement('width'), - attr.Measurement('height'), - attr.Color('strokeColor'), - attr.Measurement('strokeWidth'), - attr.Color('fillColor'), - attr.Bool('debug'), - ) + factories = {'label': SpokeLabel} + + +class IChart(interfaces.IRMLDirectiveSignature): + + # Drawing Options - subElements = { + dx = attrng.Measurement( + required=False) + + dy = attrng.Measurement( + required=False) + + dwidth = attrng.Measurement( + required=False) + + dheight = attrng.Measurement( + required=False) + + angle = attrng.Float( + required=False) + + # Plot Area Options + + x = attrng.Measurement( + required=False) + + y = attrng.Measurement( + required=False) + + width = attrng.Measurement( + required=False) + + height = attrng.Measurement( + required=False) + + strokeColor = attrng.Color( + required=False) + + strokeWidth = attrng.Measurement( + required=False) + + fillColor = attrng.Color( + required=False) + + debug = attrng.Boolean( + required=False) + +class Chart(directive.RMLDirective): + signature = IChart + factories = { 'texts': Texts } - def getAttributes(self): - attrs = [(attr.name, attr) for attr in self.attrs] - return element.extractKeywordArguments(attrs, self.element, self) - def createChart(self, attributes): raise NotImplementedError def process(self): - attrs = self.getAttributes() + attrs = dict(self.getAttributeValues()) angle = attrs.pop('angle', 0) x, y = attrs.pop('dx'), attrs.pop('dy') self.drawing = shapes.Drawing(attrs.pop('dwidth'), attrs.pop('dheight')) - chart = self.createChart(attrs) - self.processSubElements(chart) + self.context = chart = self.createChart(attrs) + self.processSubDirectives() group = shapes.Group(chart) group.translate(0,0) group.rotate(angle) self.drawing.add(group) - self.drawing.drawOn(self.context, x, y) + manager = attrng.getManager(self, interfaces.ICanvasManager) + self.drawing.drawOn(manager.canvas, x, y) +class IBarChart(IChart): + + direction = attrng.Choice( + choices=('horizontal', 'vertical'), + default='horizontal', + required=False) + + useAbsolute = attrng.Boolean( + default=False, + required=False) + + barWidth = attrng.Measurement( + default=10, + required=False) + + groupSpacing = attrng.Measurement( + default=5, + required=False) + + barSpacing = attrng.Measurement( + default=0, + required=False) + class BarChart(Chart): + signature = IBarChart nameBase = 'BarChart' - attrs = Chart.attrs + ( - attr.Choice('direction', ('horizontal', 'vertical'), 'horizontal'), - attr.Bool('useAbsolute', False), - attr.Measurement('barWidth', 10), - attr.Measurement('groupSpacing', 5), - attr.Measurement('barSpacing', 0), - ) - - subElements = Chart.subElements.copy() - subElements.update({ + factories = Chart.factories.copy() + factories.update({ 'data': Data1D, 'bars': Bars, }) @@ -530,11 +1016,11 @@ def createChart(self, attrs): direction = attrs.pop('direction') # Setup sub-elements based on direction if direction == 'horizontal': - self.subElements['categoryAxis'] = YCategoryAxis - self.subElements['valueAxis'] = XValueAxis + self.factories['categoryAxis'] = YCategoryAxis + self.factories['valueAxis'] = XValueAxis else: - self.subElements['categoryAxis'] = XCategoryAxis - self.subElements['valueAxis'] = YValueAxis + self.factories['categoryAxis'] = XCategoryAxis + self.factories['valueAxis'] = YValueAxis # Generate the chart chart = getattr( barcharts, direction.capitalize()+self.nameBase)() @@ -543,30 +1029,48 @@ def createChart(self, attrs): return chart +class IBarChart3D(IBarChart): + + theta_x = attrng.Float( + required=False) + + theta_y = attrng.Float( + required=False) + + zDepth = attrng.Measurement( + required=False) + + zSpace = attrng.Measurement( + required=False) + class BarChart3D(BarChart): + signature = IBarChart3D nameBase = 'BarChart3D' - attrs = BarChart.attrs + ( - attr.Float('theta_x'), - attr.Float('theta_y'), - attr.Measurement('zDepth'), - attr.Measurement('zSpace') - ) +class ILinePlot(IChart): + + reversePlotOrder = attrng.Boolean( + required=False) + + lineLabelNudge = attrng.Measurement( + required=False) + + lineLabelFormat = attrng.String( + required=False) + + joinedLines = attrng.Boolean( + required=False) + class LinePlot(Chart): - attrs = Chart.attrs + ( - attr.Bool('reversePlotOrder'), - attr.Measurement('lineLabelNudge'), - attr.Attribute('lineLabelFormat'), - attr.Bool('joinedLines'), - ) + signature = ILinePlot - subElements = Chart.subElements.copy() - subElements.update({ + factories = Chart.factories.copy() + factories.update({ 'data': Data2D, 'lines': Lines, - 'xValueAxis': XValueAxis, - 'yValueAxis': YValueAxis, + 'xValueAxis': LineXValueAxis, + 'yValueAxis': LineYValueAxis, 'lineLabels': Labels, }) @@ -577,24 +1081,45 @@ def createChart(self, attrs): setattr(chart, name, value) return chart + +class IPieChart(IChart): + + startAngle = attrng.Integer( + required=False) + + direction = attrng.Choice( + choices=('clockwise', 'anticlockwise'), + required=False) + + checkLabelOverlap = attrng.Boolean( + required=False) + + pointerLabelMode = attrng.Choice( + choices={'none': None, + 'leftright': 'LeftRight', + 'leftandright': 'LeftAndRight'}, + required=False) + + sameRadii = attrng.Boolean( + required=False) + + orderMode = attrng.Choice( + choices=('fixed', 'alternate'), + required=False) + + xradius = attrng.Measurement( + required=False) + + yradius = attrng.Measurement( + required=False) + + class PieChart(Chart): + signature = IPieChart chartClass = piecharts.Pie - attrs = Chart.attrs + ( - attr.Int('startAngle'), - attr.Choice('direction', ('clockwise', 'anticlockwise')), - attr.Bool('checkLabelOverlap'), - attr.Choice('pointerLabelMode', - {'none': None, - 'leftright': 'LeftRight', - 'leftandright': 'LeftAndRight'}), - attr.Bool('sameRadii'), - attr.Choice('orderMode', ('fixed', 'alternate')), - attr.Measurement('xradius'), - attr.Measurement('yradius'), - ) - subElements = Chart.subElements.copy() - subElements.update({ + factories = Chart.factories.copy() + factories.update({ 'data': SingleData1D, 'slices': Slices, 'labels': SimpleLabels, @@ -607,28 +1132,41 @@ def createChart(self, attrs): setattr(chart, name, value) return chart + +class IPieChart3D(IPieChart): + + perspective = attrng.Float( + required=False) + + depth_3d = attrng.Measurement( + required=False) + + angle_3d = attrng.Float( + required=False) + class PieChart3D(PieChart): + signature = IPieChart3D chartClass = piecharts.Pie3d - attrs = PieChart.attrs + ( - attr.Float('perspective'), - attr.Measurement('depth_3d'), - attr.Float('angle_3d'), - ) - subElements = PieChart.subElements.copy() - subElements.update({ + factories = PieChart.factories.copy() + factories.update({ 'slices': Slices3D, }) -class SpiderChart(Chart): - attrs = Chart.attrs + ( - attr.Int('startAngle'), - attr.Choice('direction', ('clockwise', 'anticlockwise')), - attr.Float('startAngle'), - ) - subElements = Chart.subElements.copy() - subElements.update({ +class ISpiderChart(IChart): + + startAngle = attrng.Integer( + required=False) + + direction = attrng.Choice( + choices=('clockwise', 'anticlockwise'), + required=False) + +class SpiderChart(Chart): + signature = ISpiderChart + factories = Chart.factories.copy() + factories.update({ 'data': Data1D, 'strands': Strands, 'strandLabels': StrandLabels, diff --git a/src/z3c/rml/directive.py b/src/z3c/rml/directive.py new file mode 100644 index 0000000..61b8eec --- /dev/null +++ b/src/z3c/rml/directive.py @@ -0,0 +1,91 @@ +############################################################################## +# +# Copyright (c) 2007 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""RML Directive Implementation + +$Id$ +""" +__docformat__ = "reStructuredText" +import logging +import zope.interface +import zope.schema + +from z3c.rml import interfaces + +logger = logging.getLogger("z3c.rml") + + +class RMLDirective(object): + zope.interface.implements(interfaces.IRMLDirective) + signature = None + factories = {} + + def __init__(self, element, parent): + self.element = element + self.parent = parent + + def getAttributeValues(self, ignore=None, select=None, attrMapping=None, + includeMissing=False, valuesOnly=False): + """See interfaces.IRMLDirective""" + items = [] + for name, attr in zope.schema.getFieldsInOrder(self.signature): + # Only add the attribute to the list, if it is supposed there + if ((ignore is None or name not in ignore) and + (select is None or name in select)): + # Get the value. + value = attr.bind(self).get() + # If no value was found for a required field, raise a value + # error + if attr.required and value is attr.missing_value: + raise ValueError( + 'No value for required attribute %s' %name) + # Only add the entry if the value is not the missing value or + # missing values are requested to be included. + if value is not attr.missing_value or includeMissing: + items.append((name, value)) + + # Sort the items based on the section + if select is not None: + select = list(select) + items = sorted(items, key=lambda (n, v): select.index(n)) + + # If the attribute name does not match the internal API + # name, then convert the name to the internal one + if attrMapping: + items = [(attrMapping.get(name, name), value) + for name, value in items] + + # Sometimes we only want the values without the names + if valuesOnly: + return [value for name, value in items] + + return items + + def processSubDirectives(self, select=None, ignore=None): + # Go through all children of the directive and try to process them. + for element in self.element.getchildren(): + if select is not None and element.tag not in select: + continue + if ignore is not None and element.tag in ignore: + continue + # If the element is a directive, process it + if element.tag in self.factories: + directive = self.factories[element.tag](element, self) + directive.process() + else: + # Record any tags/elements that could not be processed. + logger.warn("Directive %r could not be processed and was " + "ignored." %element.tag) + + def process(self): + self.processSubDirectives() diff --git a/src/z3c/rml/document.py b/src/z3c/rml/document.py index 18e7289..ad2b42b 100644 --- a/src/z3c/rml/document.py +++ b/src/z3c/rml/document.py @@ -16,67 +16,143 @@ $Id$ """ __docformat__ = "reStructuredText" +import cStringIO import sys import zope.interface +import reportlab.pdfgen.canvas from reportlab.pdfbase import pdfmetrics, ttfonts, cidfonts -from z3c.rml import attr, element, error, interfaces +from z3c.rml import attrng, directive, interfaces, occurence from z3c.rml import canvas, stylesheet, template -class RegisterType1Face(element.Element): - args = ( attr.Attribute('afmFile'), attr.Attribute('pfbFile') ) +class IRegisterType1Face(interfaces.IRMLDirectiveSignature): + """Register a new Type 1 font face.""" + + afmFile = attrng.String( + title=u'AFM File', + description=u'Path to AFM file used to register the Type 1 face.', + required=True) + + pfbFile = attrng.String( + title=u'PFB File', + description=u'Path to PFB file used to register the Type 1 face.', + required=True) + +class RegisterType1Face(directive.RMLDirective): + signature = IRegisterType1Face def process(self): - args = element.extractPositionalArguments(self.args, self.element, self) + args = self.getAttributeValues(valuesOnly=True) face = pdfmetrics.EmbeddedType1Face(*args) pdfmetrics.registerTypeFace(face) -class RegisterFont(element.Element): - args = ( - attr.Attribute('name'), - attr.Attribute('faceName'), - attr.Attribute('encName') ) +class IRegisterFont(interfaces.IRMLDirectiveSignature): + """Register a new font based on a face and encoding.""" + + name = attrng.String( + title=u'Name', + description=(u'The name under which the font can be used in style ' + u'declarations or other parameters that lookup a font.'), + required=True) + + faceName = attrng.String( + title=u'Face Name', + description=(u'The name of the face the font uses. The face has to ' + u'be previously registered.'), + required=True) + + encName = attrng.String( + title=u'Encoding Name', + description=(u'The name of the encdoing to be used.'), + required=True) + +class RegisterFont(directive.RMLDirective): + signature = IRegisterFont def process(self): - args = element.extractPositionalArguments(self.args, self.element, self) + args = self.getAttributeValues(valuesOnly=True) font = pdfmetrics.Font(*args) pdfmetrics.registerFont(font) -class RegisterTTFont(element.Element): - args = ( - attr.Attribute('faceName'), - attr.Attribute('fileName') ) +class IRegisterTTFont(interfaces.IRMLDirectiveSignature): + """Register a new TrueType font given the TT file and face name.""" + + faceName = attrng.String( + title=u'Face Name', + description=(u'The name of the face the font uses. The face has to ' + u'be previously registered.'), + required=True) + + fileName = attrng.String( + title=u'File Name', + description=u'File path of the of the TrueType font.', + required=True) + +class RegisterTTFont(directive.RMLDirective): + signature = IRegisterTTFont def process(self): - args = element.extractPositionalArguments(self.args, self.element, self) + args = self.getAttributeValues(valuesOnly=True) font = ttfonts.TTFont(*args) pdfmetrics.registerFont(font) -class RegisterCidFont(element.Element): - args = ( attr.Attribute('faceName'), ) +class IRegisterCidFont(interfaces.IRMLDirectiveSignature): + """Register a new CID font given the face name.""" + + faceName = attrng.String( + title=u'Face Name', + description=(u'The name of the face the font uses. The face has to ' + u'be previously registered.'), + required=True) + +class RegisterCidFont(directive.RMLDirective): + signature = IRegisterCidFont def process(self): - args = element.extractPositionalArguments(self.args, self.element, self) - pdfmetrics.registerFont(cidfonts.UnicodeCIDFont(*args)) + args = self.getAttributeValues(valuesOnly=True) + font = cidfonts.UnicodeCIDFont(*args) + pdfmetrics.registerFont(font) + +class IColorDefinition(interfaces.IRMLDirectiveSignature): + """Define a new color and give it a name to be known under.""" -class ColorDefinition(element.FunctionElement): - args = ( - attr.Text('id'), - attr.Color('RGB'), ) + id = attrng.String( + title=u'Id', + description=(u'The id/name the color will be available under.'), + required=True) + + # XXX: This is really disgusting; need to rename to "color"! + # This is only here for compatibility with the original RML. + RGB = attrng.Color( + title=u'Color', + description=(u'The color value that is represented.'), + required=True) + +class ColorDefinition(directive.RMLDirective): + signature = IColorDefinition def process(self): - id, value = self.getPositionalArguments() - manager = attr.getManager(self, interfaces.IColorsManager) + id, value = self.getAttributeValues(valuesOnly=True) + manager = attrng.getManager(self) manager.colors[id] = value -class DocInit(element.ContainerElement): +class IDocInit(interfaces.IRMLDirectiveSignature): + occurence.containing( + occurence.ZeroOrMore('registerType1Face', IRegisterType1Face), + occurence.ZeroOrMore('registerFont', IRegisterFont), + occurence.ZeroOrMore('registerTTFont', IRegisterTTFont), + occurence.ZeroOrMore('registerCidFont', IRegisterCidFont), + occurence.ZeroOrMore('color', IColorDefinition), + ) - subElements = { +class DocInit(directive.RMLDirective): + signature = IDocInit + factories = { 'registerType1Face': RegisterType1Face, 'registerFont': RegisterFont, 'registerTTFont': RegisterTTFont, @@ -85,21 +161,56 @@ class DocInit(element.ContainerElement): } -class Document(element.ContainerElement): - zope.interface.implements( - interfaces.INamesManager, - interfaces.IStylesManager, - interfaces.IColorsManager) - - subElements = { - 'docinit': DocInit +class IDocument(interfaces.IRMLDirectiveSignature): + occurence.containing( + occurence.ZeroOrOne('docinit', IDocInit), + ) + + filename = attrng.String( + title=u'File Name', + description=(u'The default name of the output file, if no output ' + u'file was provided.'), + required=True) + + debug = attrng.Boolean( + title=u'Debug', + description=u'A flag to activate the debug output.', + required=False) + + compression = attrng.BooleanWithDefault( + title=u'Compression', + description=(u'A flag determining whether page compression should ' + u'be used.'), + required=False) + + invariant = attrng.BooleanWithDefault( + title=u'Invariant', + description=(u'A flag that determines whether the produced PDF ' + u'should be invariant with respect to the date and ' + u'the exact contents.'), + required=False) + +class Document(directive.RMLDirective): + signature = IDocument + zope.interface.implements(interfaces.IManager, + interfaces.IPostProcessorManager, + interfaces.ICanvasManager) + + factories = { + 'docinit': DocInit, + 'stylesheet': stylesheet.Stylesheet, + 'template': template.Template, + 'story': template.Story, + 'pageInfo': canvas.PageInfo, + 'pageDrawing': canvas.PageDrawing, } def __init__(self, element): - self.element = element + super(Document, self).__init__(element, None) self.names = {} self.styles = {} self.colors = {} + self.postProcessors = [] def process(self, outputFile=None): """Process document""" @@ -107,10 +218,36 @@ def process(self, outputFile=None): # TODO: This is relative to the input file *not* the CWD!!! outputFile = open(self.element.get('filename'), 'w') - self.processSubElements(None) + # Create a temporary output file, so that post-processors can + # massage the output + self.outputFile = tempOutput = cStringIO.StringIO() + + # Process common sub-directives + self.processSubDirectives(select=('docinit', 'stylesheet')) + # Handle Page Drawing Documents if self.element.find('pageDrawing') is not None: - canvas.Canvas(self.element, self, None).process(outputFile) + kwargs = dict(self.getAttributeValues( + select=('compression', 'debug'), + attrMapping={'compression': 'pageCompression', + 'debug': 'verbosity'} + )) + + self.canvas = reportlab.pdfgen.canvas.Canvas(tempOutput, **kwargs) + self.processSubDirectives(select=('pageInfo', 'pageDrawing')) + self.canvas.save() + + # Handle Flowable-based documents. + elif self.element.find('template') is not None: + self.processSubDirectives(select=('template', 'story')) + self.doc.multiBuild(self.flowables) + + # Process all post processors + for name, processor in self.postProcessors: + tempOutput.seek(0) + tempOutput = processor.process(tempOutput) + + # Save the result into our real output file + tempOutput.seek(0) + outputFile.write(tempOutput.getvalue()) - if self.element.find('template') is not None: - template.Template(self.element, self, None).process(outputFile) diff --git a/src/z3c/rml/element.py b/src/z3c/rml/element.py deleted file mode 100644 index 0b15575..0000000 --- a/src/z3c/rml/element.py +++ /dev/null @@ -1,93 +0,0 @@ -############################################################################## -# -# Copyright (c) 2007 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Generic RML element - -$Id$ -""" -__docformat__ = "reStructuredText" -from z3c.rml import attr, error - -class Element(object): - - def __init__(self, element, parent, context): - self.element = element - self.parent = parent - self.context = context - -class ContainerElement(Element): - - subElements = {} - order = None - - def processSubElements(self, context): - if self.order is not None: - for tag in self.order: - for element in self.element.findall(tag): - self.subElements[tag](element, self, context).process() - else: - for subElement in self.element.getchildren(): - if subElement.tag in self.subElements: - elem = self.subElements[subElement.tag]( - subElement, self, context) - elem.__name__ = subElement.tag - elem.process() - - - def process(self): - self.processSubElements(self.context) - - -def extractAttributes(attrs, element, context=None): - values = {} - for Attr in attrs: - value = Attr.get(element, context=context) - if value is not attr.DEFAULT: - values[Attr.name] = value - return values - - -def extractPositionalArguments(argsList, element, context=None): - args = [] - for Attr in argsList: - value = Attr.get(element, context=context) - if value is attr.DEFAULT: - raise error.RequiredAttributeMissing(element, Attr.name) - args.append(value) - return args - -def extractKeywordArguments(kwList, element, context=None): - kw = {} - for apiName, Attr in kwList: - value = Attr.get(element, context=context) - if value is not attr.DEFAULT: - kw[apiName] = value - return kw - - -class FunctionElement(Element): - - functionName = None - args = () - kw = () - - def getPositionalArguments(self): - return extractPositionalArguments(self.args, self.element, self) - - def getKeywordArguments(self): - return extractKeywordArguments(self.kw, self.element, self) - - def process(self): - args = self.getPositionalArguments() - kw = self.getKeywordArguments() - getattr(self.context, self.functionName)(*args, **kw) diff --git a/src/z3c/rml/flowable.py b/src/z3c/rml/flowable.py index 74bf69b..88122ce 100644 --- a/src/z3c/rml/flowable.py +++ b/src/z3c/rml/flowable.py @@ -18,12 +18,15 @@ __docformat__ = "reStructuredText" import copy import re +import reportlab.lib.styles import reportlab.platypus import reportlab.platypus.doctemplate import reportlab.platypus.flowables import reportlab.platypus.tables +import zope.schema from reportlab.lib import styles -from z3c.rml import attr, element, form, platypus, special, stylesheet +from z3c.rml import attrng, directive, interfaces, occurence +from z3c.rml import form, platypus, special, stylesheet try: import reportlab.graphics.barcode @@ -34,45 +37,96 @@ reportlab.graphics.barcode = types.ModuleType('barcode') reportlab.graphics.barcode.createBarcodeDrawing = None -class Flowable(element.FunctionElement): +class Flowable(directive.RMLDirective): klass=None + attrMapping = None def process(self): - args = self.getPositionalArguments() - kw = self.getKeywordArguments() - self.parent.flow.append(self.klass(*args, **kw)) + args = dict(self.getAttributeValues(attrMapping=self.attrMapping)) + self.parent.flow.append(self.klass(**args)) + + +class ISpacer(interfaces.IRMLDirectiveSignature): + """Creates a vertical space in the flow.""" + + width = attrng.Measurement( + title=u'Width', + description=u'The width of the spacer. Currently not implemented.', + default=100, + required=False) + + length = attrng.Measurement( + title=u'Length', + description=u'The height of the spacer.', + required=True) class Spacer(Flowable): + signature = ISpacer klass = reportlab.platypus.Spacer - args = ( attr.Measurement('width', 100), attr.Measurement('length'), ) + attrMapping = {'length': 'height'} + + +class IIllustration(interfaces.IRMLDirectiveSignature): + """Inserts an illustration with graphics elements.""" + + width = attrng.Measurement( + title=u'Width', + description=u'The width of the illustration.', + required=True) + height = attrng.Measurement( + title=u'Height', + description=u'The height of the illustration.', + default=100, + required=True) class Illustration(Flowable): + signature = IIllustration klass = platypus.Illustration - args = ( attr.Measurement('width'), attr.Measurement('height', 100)) def process(self): - args = self.getPositionalArguments() - self.parent.flow.append(self.klass(self, *args)) + args = dict(self.getAttributeValues()) + self.parent.flow.append(self.klass(self, **args)) + + +class IBarCodeFlowable(form.IBarCodeBase): + """Creates a bar code as a flowable.""" + + value = attrng.String( + title=u'Value', + description=u'The value represented by the code.', + required=True) class BarCodeFlowable(Flowable): + signature = IBarCodeFlowable klass = staticmethod(reportlab.graphics.barcode.createBarcodeDrawing) - args = form.BarCode.args[:-1] - kw = form.BarCode.kw[2:] + ( ('value', attr.Attribute('value')), ) + attrMapping = {'code': 'codeName'} -class Preformatted(Flowable): - klass = reportlab.platypus.Preformatted - args = ( attr.RawXMLContent(u''), attr.Style('style', 'Normal') ) +class IPluginFlowable(interfaces.IRMLDirectiveSignature): + """Inserts a custom flowable developed in Python.""" -class XPreformatted(Flowable): - klass = reportlab.platypus.XPreformatted - args = ( attr.RawXMLContent(u''), attr.Style('style', 'Normal') ) + module = attrng.String( + title=u'Module', + description=u'The Python module in which the flowable is located.', + required=True) + + function = attrng.String( + title=u'Function', + description=(u'The name of the factory function within the module ' + u'that returns the custom flowable.'), + required=True) + + params = attrng.TextNode( + title=u'Parameters', + description=(u'A list of parameters encoded as a long string.'), + required=False) class PluginFlowable(Flowable): - args = ( attr.Text('module'), attr.Text('function'), attr.TextNode()) + signature = IPluginFlowable def process(self): - modulePath, functionName, text = self.getPositionalArguments() + modulePath, functionName, text = self.getAttributeValues( + valuesOnly=True) module = __import__(modulePath, {}, {}, [modulePath]) function = getattr(module, functionName) flowables = function(text) @@ -81,294 +135,737 @@ def process(self): self.parent.flow += list(flowables) +class IMinimalParagraphBase(interfaces.IRMLDirectiveSignature): + + style = attrng.Style( + title=u'Style', + description=(u'The paragraph style that is applied to the paragraph. ' + u'See the ``paraStyle`` tag for creating a paragraph ' + u'style.'), + default=reportlab.lib.styles.getSampleStyleSheet()['Normal'], + required=True) + + bulletText = attrng.String( + title=u'Bullet Character', + description=(u'The bullet character is the ASCII representation of ' + u'the symbol making up the bullet in a listing.'), + required=False) + + dedent = attrng.Integer( + title=u'Dedent', + description=(u'Number of characters to be removed in front of every ' + u'line of the text.'), + required=False) + + +class IBold(interfaces.IRMLDirectiveSignature): + """Renders the text inside as bold.""" + +class IItalic(interfaces.IRMLDirectiveSignature): + """Renders the text inside as italic.""" + +class IUnderLine(interfaces.IRMLDirectiveSignature): + """Underlines the contained text.""" + +class IBreak(interfaces.IRMLDirectiveSignature): + """Inserts a line break in the paragraph.""" + +class IPageNumber(interfaces.IRMLDirectiveSignature): + """Inserts the current page number into the text.""" + +class IParagraphBase(IMinimalParagraphBase): + occurence.containing( + occurence.ZeroOrMore('b', IBold), + occurence.ZeroOrMore('i', IItalic), + occurence.ZeroOrMore('u', IUnderLine), + occurence.ZeroOrMore('br', IBreak, + condition=occurence.laterThanReportlab21), + occurence.ZeroOrMore('pageNumber', IPageNumber) + ) + +class IPreformatted(IMinimalParagraphBase): + """A preformatted text, similar to the
 tag in HTML."""
+
+    text = attrng.RawXMLContent(
+        title=u'Text',
+        description=(u'The text that will be layed out.'),
+        required=True)
+
+class Preformatted(Flowable):
+    signature = IPreformatted
+    klass = reportlab.platypus.Preformatted
+
+
+class IXPreformatted(IParagraphBase):
+    """A preformatted text that allows paragraph markup."""
+
+    text = attrng.RawXMLContent(
+        title=u'Text',
+        description=(u'The text that will be layed out.'),
+        required=True)
+
+class XPreformatted(Flowable):
+    signature = IXPreformatted
+    klass = reportlab.platypus.XPreformatted
+
+
+class IParagraph(IParagraphBase, stylesheet.IBaseParagraphStyle):
+    """Lays out an entire paragraph."""
+
+    text = attrng.XMLContent(
+        title=u'Text',
+        description=(u'The text that will be layed out.'),
+        required=True)
+
 class Paragraph(Flowable):
+    signature = IParagraph
     klass = reportlab.platypus.Paragraph
-    args = ( attr.XMLContent(u''), attr.Style('style', 'Normal') )
-    kw = ( ('bulletText', attr.Attribute('bulletText')), )
 
-    styleAttrs = stylesheet.ParagraphStyle.attrs[3:]
+    styleAttributes = zope.schema.getFieldNames(stylesheet.IBaseParagraphStyle)
 
     def processStyle(self, style):
-        attrs = element.extractAttributes(self.styleAttrs, self.element, self)
+        attrs = self.getAttributeValues(select=self.styleAttributes)
         if attrs:
             style = copy.deepcopy(style)
-            for name, value in attrs.items():
+            for name, value in attrs:
                 setattr(style, name, value)
         return style
 
     def process(self):
-        args = self.getPositionalArguments()
-        kw = self.getKeywordArguments()
-        args[1] = self.processStyle(args[1])
-        self.parent.flow.append(self.klass(*args, **kw))
+        args = dict(self.getAttributeValues(ignore=self.styleAttributes))
+        args['style'] = self.processStyle(args['style'])
+        self.parent.flow.append(self.klass(**args))
+
+
+class ITitle(IParagraph):
+    """The title is a simple paragraph with a special title style."""
+
+    style = attrng.Style(
+        title=u'Style',
+        description=(u'The paragraph style that is applied to the paragraph. '
+                     u'See the ``paraStyle`` tag for creating a paragraph '
+                     u'style.'),
+        default=reportlab.lib.styles.getSampleStyleSheet()['Title'],
+        required=True)
 
 class Title(Paragraph):
-    args = ( attr.XMLContent(u''), attr.Style('style', 'Title'), )
+    signature = ITitle
+
+
+class IHeading1(IParagraph):
+    """Heading 1 is a simple paragraph with a special heading 1 style."""
+
+    style = attrng.Style(
+        title=u'Style',
+        description=(u'The paragraph style that is applied to the paragraph. '
+                     u'See the ``paraStyle`` tag for creating a paragraph '
+                     u'style.'),
+        default=reportlab.lib.styles.getSampleStyleSheet()['Heading1'],
+        required=True)
 
 class Heading1(Paragraph):
-    args = ( attr.XMLContent(u''), attr.Style('style', 'Heading1'), )
+    signature = IHeading1
+
+
+class IHeading2(IParagraph):
+    """Heading 2 is a simple paragraph with a special heading 2 style."""
+
+    style = attrng.Style(
+        title=u'Style',
+        description=(u'The paragraph style that is applied to the paragraph. '
+                     u'See the ``paraStyle`` tag for creating a paragraph '
+                     u'style.'),
+        default=reportlab.lib.styles.getSampleStyleSheet()['Heading2'],
+        required=True)
 
 class Heading2(Paragraph):
-    args = ( attr.XMLContent(u''), attr.Style('style', 'Heading2'), )
+    signature = IHeading2
+
+
+class IHeading3(IParagraph):
+    """Heading 3 is a simple paragraph with a special heading 3 style."""
+
+    style = attrng.Style(
+        title=u'Style',
+        description=(u'The paragraph style that is applied to the paragraph. '
+                     u'See the ``paraStyle`` tag for creating a paragraph '
+                     u'style.'),
+        default=reportlab.lib.styles.getSampleStyleSheet()['Heading3'],
+        required=True)
 
 class Heading3(Paragraph):
-    args = ( attr.XMLContent(u''), attr.Style('style', 'Heading3'), )
-
-class TableCell(element.Element):
-
-    styleAttrs = (
-        ('FONTNAME', (attr.Text('fontName'),)),
-        ('FONTSIZE', (attr.Measurement('fontSize'),)),
-        ('TEXTCOLOR', (attr.Color('fontColor'),)),
-        ('LEADING', (attr.Measurement('leading'),)),
-        ('LEFTPADDING', (attr.Measurement('leftPadding'),)),
-        ('RIGHTPADDING', (attr.Measurement('rightPadding'),)),
-        ('TOPPADDING', (attr.Measurement('topPadding'),)),
-        ('BOTTOMPADDING', (attr.Measurement('bottomPadding'),)),
-        ('BACKGROUND', (attr.Color('background'),)),
-        ('ALIGNMENT', (attr.Choice('align',
-                           {'left': 'LEFT', 'right': 'RIGHT',
-                            'center': 'CENTER', 'decimal': 'DECIMAL'}),)),
-        ('VALIGN', (attr.Choice('vAlign',
-                        {'top': 'TOP', 'middle': 'MIDDLE',
-                         'bottom': 'BOTTOM'}), )),
-        ('LINEBELOW', (attr.Measurement('lineBelowThickness'),
-                       attr.Color('lineBelowColor'),
-                       attr.Choice('lineBelowCap',
-                                   {'butt': 0, 'round': 1, 'square': 2}),
-                       attr.Int('lineBelowCount'),
-                       attr.Measurement('lineBelowSpace'))),
-        ('LINEABOVE', (attr.Measurement('lineAboveThickness'),
-                       attr.Color('lineAboveColor'),
-                       attr.Choice('lineAboveCap',
-                                   {'butt': 0, 'round': 1, 'square': 2}),
-                       attr.Int('lineAboveCount'),
-                       attr.Measurement('lineAboveSpace'))),
-        ('LINEBEFORE', (attr.Measurement('lineLeftThickness'),
-                        attr.Color('lineLeftColor'),
-                        attr.Choice('lineLeftCap',
-                                    {'butt': 0, 'round': 1, 'square': 2}),
-                        attr.Int('lineLeftCount'),
-                        attr.Measurement('lineLeftSpace'))),
-        ('LINEAFTER', (attr.Measurement('lineRightThickness'),
-                       attr.Color('lineRightColor'),
-                       attr.Choice('lineRightCap',
-                                   {'butt': 0, 'round': 1, 'square': 2}),
-                       attr.Int('lineRightCount'),
-                       attr.Measurement('lineRightSpace'))),
+    signature = IHeading3
+
+
+class ITableCell(interfaces.IRMLDirectiveSignature):
+    """A table cell within a table."""
+
+    content = attrng.RawXMLContent(
+        title=u'Content',
+        description=(u'The content of the cell; can be text or any flowable.'),
+        required=True)
+
+    fontName = attrng.String(
+        title=u'Font Name',
+        description=u'The name of the font for the cell.',
+        required=False)
+
+    fontSize = attrng.Measurement(
+        title=u'Font Size',
+        description=u'The font size for the text of the cell.',
+        required=False)
+
+    leading = attrng.Measurement(
+        title=u'Leading',
+        description=(u'The height of a single text line. It includes '
+                     u'character height.'),
+        required=False)
+
+    fontColor = attrng.Color(
+        title=u'Font Color',
+        description=u'The color in which the text will appear.',
+        required=False)
+
+    leftPadding = attrng.Measurement(
+        title=u'Left Padding',
+        description=u'The size of the padding on the left side.',
+        required=False)
+
+    rightPadding = attrng.Measurement(
+        title=u'Right Padding',
+        description=u'The size of the padding on the right side.',
+        required=False)
+
+    topPadding = attrng.Measurement(
+        title=u'Top Padding',
+        description=u'The size of the padding on the top.',
+        required=False)
+
+    bottomPadding = attrng.Measurement(
+        title=u'Bottom Padding',
+        description=u'The size of the padding on the bottom.',
+        required=False)
+
+    background = attrng.Color(
+        title=u'Background Color',
+        description=u'The color to use as the background for the cell.',
+        required=False)
+
+    align = attrng.Choice(
+        title=u'Text Alignment',
+        description=u'The text alignment within the cell.',
+        choices=interfaces.ALIGN_TEXT_CHOICES,
+        required=False)
+
+    vAlign = attrng.Choice(
+        title=u'Vertical Alignment',
+        description=u'The vertical alignment of the text within the cell.',
+        choices=interfaces.VALIGN_TEXT_CHOICES,
+        required=False)
+
+    lineBelowThickness = attrng.Measurement(
+        title=u'Line Below Thickness',
+        description=u'The thickness of the line below the cell.',
+        required=False)
+
+    lineBelowColor = attrng.Color(
+        title=u'Line Below Color',
+        description=u'The color of the line below the cell.',
+        required=False)
+
+    lineBelowCap = attrng.Choice(
+        title=u'Line Below Cap',
+        description=u'The cap at the end of the line below the cell.',
+        choices=interfaces.CAP_CHOICES,
+        required=False)
+
+    lineBelowCount = attrng.Integer(
+        title=u'Line Below Count',
+        description=(u'Describes whether the line below is a single (1) or '
+                     u'double (2) line.'),
+        required=False)
+
+    lineBelowSpace = attrng.Measurement(
+        title=u'Line Below Space',
+        description=u'The space of the line below the cell.',
+        required=False)
+
+    lineAboveThickness = attrng.Measurement(
+        title=u'Line Above Thickness',
+        description=u'The thickness of the line above the cell.',
+        required=False)
+
+    lineAboveColor = attrng.Color(
+        title=u'Line Above Color',
+        description=u'The color of the line above the cell.',
+        required=False)
+
+    lineAboveCap = attrng.Choice(
+        title=u'Line Above Cap',
+        description=u'The cap at the end of the line above the cell.',
+        choices=interfaces.CAP_CHOICES,
+        required=False)
+
+    lineAboveCount = attrng.Integer(
+        title=u'Line Above Count',
+        description=(u'Describes whether the line above is a single (1) or '
+                     u'double (2) line.'),
+        required=False)
+
+    lineAboveSpace = attrng.Measurement(
+        title=u'Line Above Space',
+        description=u'The space of the line above the cell.',
+        required=False)
+
+    lineLeftThickness = attrng.Measurement(
+        title=u'Left Line Thickness',
+        description=u'The thickness of the line left of the cell.',
+        required=False)
+
+    lineLeftColor = attrng.Color(
+        title=u'Left Line Color',
+        description=u'The color of the line left of the cell.',
+        required=False)
+
+    lineLeftCap = attrng.Choice(
+        title=u'Line Left Cap',
+        description=u'The cap at the end of the line left of the cell.',
+        choices=interfaces.CAP_CHOICES,
+        required=False)
+
+    lineLeftCount = attrng.Integer(
+        title=u'Line Left Count',
+        description=(u'Describes whether the left line is a single (1) or '
+                     u'double (2) line.'),
+        required=False)
+
+    lineLeftSpace = attrng.Measurement(
+        title=u'Line Left Space',
+        description=u'The space of the line left of the cell.',
+        required=False)
+
+    lineRightThickness = attrng.Measurement(
+        title=u'Right Line Thickness',
+        description=u'The thickness of the line right of the cell.',
+        required=False)
+
+    lineRightColor = attrng.Color(
+        title=u'Right Line Color',
+        description=u'The color of the line right of the cell.',
+        required=False)
+
+    lineRightCap = attrng.Choice(
+        title=u'Line Right Cap',
+        description=u'The cap at the end of the line right of the cell.',
+        choices=interfaces.CAP_CHOICES,
+        required=False)
+
+    lineRightCount = attrng.Integer(
+        title=u'Line Right Count',
+        description=(u'Describes whether the right line is a single (1) or '
+                     u'double (2) line.'),
+        required=False)
+
+    lineRightSpace = attrng.Measurement(
+        title=u'Line Right Space',
+        description=u'The space of the line right of the cell.',
+        required=False)
+
+class TableCell(directive.RMLDirective):
+    signature = ITableCell
+    styleAttributesMapping = (
+        ('FONTNAME', ('fontName',)),
+        ('FONTSIZE', ('fontSize',)),
+        ('TEXTCOLOR', ('fontColor',)),
+        ('LEADING', ('leading',)),
+        ('LEFTPADDING', ('leftPadding',)),
+        ('RIGHTPADDING', ('rightPadding',)),
+        ('TOPPADDING', ('topPadding',)),
+        ('BOTTOMPADDING', ('bottomPadding',)),
+        ('BACKGROUND', ('background',)),
+        ('ALIGNMENT', ('align',)),
+        ('VALIGN', ('vAlign',)),
+        ('LINEBELOW', ('lineBelowThickness', 'lineBelowColor',
+                       'lineBelowCap', 'lineBelowCount', 'lineBelowSpace')),
+        ('LINEABOVE', ('lineAboveThickness', 'lineAboveColor',
+                       'lineAboveCap', 'lineAboveCount', 'lineAboveSpace')),
+        ('LINEBEFORE', ('lineLeftThickness', 'lineLeftColor',
+                        'lineLeftCap', 'lineLeftCount', 'lineLeftSpace')),
+        ('LINEAFTER', ('lineRightThickness', 'lineRightColor',
+                       'lineRightCap', 'lineRightCount', 'lineRightSpace')),
         )
 
     def processStyle(self):
         row = len(self.parent.parent.rows)
         col = len(self.parent.cols)
-        for styleName, attrs in self.styleAttrs:
-            args = []
-            for attribute in attrs:
-                value = attribute.get(self.element, context=self)
-                if value is not attr.DEFAULT:
-                    args.append(value)
-            if args or len(attrs) == 0:
+        for styleAction, attrNames in self.styleAttributesMapping:
+            args = self.getAttributeValues(select=attrNames, valuesOnly=True)
+            if args or len(attrNames) == 0:
                 self.parent.parent.style.add(
-                    styleName, [col, row], [col, row], *args)
+                    styleAction, [col, row], [col, row], *args)
 
     def process(self):
         # Produce style
         self.processStyle()
         # Produce cell data
-        flow = Flow(self.element, self.parent, self.context)
+        flow = Flow(self.element, self.parent)
         flow.process()
         content = flow.flow
         if len(content) == 0:
-            content = attr.TextNode().get(self.element)
+            content = self.getAttributeValues(
+                select=('content',), valuesOnly=True)[0]
         self.parent.cols.append(content)
 
-class TableRow(element.ContainerElement):
 
-    subElements = {'td': TableCell}
+class ITableRow(interfaces.IRMLDirectiveSignature):
+    """A table row in the block table."""
+    occurence.containing(
+        occurence.OneOrMore('td', ITableCell),
+        )
+
+class TableRow(directive.RMLDirective):
+    signature = ITableRow
+    factories = {'td': TableCell}
 
     def process(self):
         self.cols = []
-        self.processSubElements(None)
+        self.processSubDirectives()
         self.parent.rows.append(self.cols)
 
-class TableBulkData(element.Element):
+
+class ITableBulkData(interfaces.IRMLDirectiveSignature):
+    """Bulk Data allows one to wuickly create a table."""
+
+    content = attrng.TextNodeSequence(
+        title=u'Content',
+        description=u'The bulk data.',
+        splitre=re.compile('\n'),
+        value_type=attrng.Sequence(splitre=re.compile(','),
+                                 value_type=attrng.Text())
+        )
+
+class TableBulkData(directive.RMLDirective):
+    signature = ITableBulkData
 
     def process(self):
-        attribute = attr.TextNodeSequence(
-            splitre=re.compile('\n'),
-            valueType=attr.Sequence(
-                splitre=re.compile(','),
-                valueType=attr.Text()
-                ))
-        self.parent.rows = attribute.get(self.element)
+        self.parent.rows = self.getAttributeValues(valuesOnly=True)[0]
 
 
 class BlockTableStyle(stylesheet.BlockTableStyle):
 
     def process(self):
-        self.parent.style = copy.deepcopy(self.parent.style)
-        attrs = element.extractAttributes(self.attrs, self.element, self)
-        for name, value in attrs.items():
-            setattr(self.parent.style, name, value)
-        self.processSubElements(self.parent.style)
+        self.style = copy.deepcopy(self.parent.style)
+        attrs = self.getAttributeValues()
+        for name, value in attrs:
+            setattr(self.style, name, value)
+        self.processSubDirectives()
+        self.parent.style = self.style
+
+
+class IBlockTable(interfaces.IRMLDirectiveSignature):
+    """A typical block table."""
+    occurence.containing(
+        occurence.ZeroOrMore('tr', ITableRow),
+        occurence.ZeroOrOne('bulkData', ITableBulkData),
+        occurence.ZeroOrMore('blockTableStyle', stylesheet.IBlockTableStyle),
+        )
 
+    style = attrng.Style(
+        title=u'Style',
+        description=(u'The table style that is applied to the table. '),
+        required=False)
 
-class BlockTable(element.ContainerElement, Flowable):
-    klass = reportlab.platypus.Table
-    kw = (
-        ('rowHeights', attr.Sequence('rowHeights', attr.Measurement())),
-        ('colWidths', attr.Sequence('colWidths',
-             attr.Measurement(allowPercentage=True, allowStar=True))),
-        )
+    rowHeights = attrng.Sequence(
+        title=u'Row Heights',
+        description=u'A list of row heights in the table.',
+        value_type=attrng.Measurement(),
+        required=False)
+
+    colWidths = attrng.Sequence(
+        title=u'Column Widths',
+        description=u'A list of column widths in the table.',
+        value_type=attrng.Measurement(allowPercentage=True, allowStar=True),
+        required=False)
 
-    attrs = ( ('repeatRows', attr.Int('repeatRows')), )
+    repeatRows = attrng.Integer(
+        title=u'Repeat Rows',
+        description=u'A flag to repeat rows upon table splits.',
+        required=False)
 
 
-    subElements = {
+class BlockTable(Flowable):
+    signature = IBlockTable
+    klass = reportlab.platypus.Table
+    factories = {
         'tr': TableRow,
         'bulkData': TableBulkData,
         'blockTableStyle': BlockTableStyle}
 
     def process(self):
+        attrs = dict(self.getAttributeValues())
         # Get the table style; create a new one, if none is found
-        self.style = attr.Style('style', 'table').get(self.element, None, self)
+        self.style = attrs.pop('style', None)
         if self.style is None:
             self.style = reportlab.platypus.tables.TableStyle()
         # Extract all table rows and cells
         self.rows = []
-        self.processSubElements(None)
+        self.processSubDirectives(None)
         # Create the table
-        kw = self.getKeywordArguments()
-
-        table = self.klass(self.rows, style=self.style, **kw)
-
-        attrs = element.extractKeywordArguments(self.attrs, self.element)
-        for name, value in attrs.items():
-            setattr(table, name, value)
-
-        # Must set keepWithNExt on table, since the style is not stored corr.
+        repeatRows = attrs.pop('repeatRows', None)
+        table = self.klass(self.rows, style=self.style, **attrs)
+        if repeatRows:
+            table.repeatRows = repeatRows
+        # Must set keepWithNext on table, since the style is not stored corr.
         if hasattr(self.style, 'keepWithNext'):
             table.keepWithNext = self.style.keepWithNext
         self.parent.flow.append(table)
 
 
+class INextFrame(interfaces.IRMLDirectiveSignature):
+    """Switch to the next frame."""
+    name = attrng.StringOrInt(
+        title=u'Name',
+        description=(u'The name or index of the next frame.'),
+        required=False)
+
 class NextFrame(Flowable):
+    signature = INextFrame
     klass = reportlab.platypus.doctemplate.FrameBreak
-    kw = (
-        ('ix', attr.StringOrInt('name')), )
+    attrMapping = {'name': 'ix'}
+
+
+class ISetNextFrame(interfaces.IRMLDirectiveSignature):
+    """Define the next frame to switch to."""
+    name = attrng.StringOrInt(
+        title=u'Name',
+        description=(u'The name or index of the next frame.'),
+        required=True)
 
 class SetNextFrame(Flowable):
+    signature = INextFrame
     klass = reportlab.platypus.doctemplate.NextFrameFlowable
-    kw = (
-        ('ix', attr.StringOrInt('name')), )
+    attrMapping = {'name': 'ix'}
+
+
+class INextPage(interfaces.IRMLDirectiveSignature):
+    """Switch to the next page."""
 
 class NextPage(Flowable):
+    signature = INextPage
     klass = reportlab.platypus.PageBreak
 
+
+class ISetNextTemplate(interfaces.IRMLDirectiveSignature):
+    """Define the next page template to use."""
+    name = attrng.StringOrInt(
+        title=u'Name',
+        description=u'The name or index of the next page template.',
+        required=True)
+
 class SetNextTemplate(Flowable):
+    signature = ISetNextTemplate
     klass = reportlab.platypus.doctemplate.NextPageTemplate
-    args = ( attr.StringOrInt('name'), )
+    attrMapping = {'name': 'pt'}
+
+
+class IConditionalPageBreak(interfaces.IRMLDirectiveSignature):
+    """Switch to the next page if not enough vertical space is available."""
+    height = attrng.Measurement(
+        title=u'height',
+        description=u'The minimal height that must be remaining on the page.',
+        required=True)
 
 class ConditionalPageBreak(Flowable):
+    signature = IConditionalPageBreak
     klass = reportlab.platypus.CondPageBreak
-    args = ( attr.Measurement('height'), )
 
 
+class IKeepInFrame(interfaces.IRMLDirectiveSignature):
+    """Ask a flowable to stay within the frame."""
+
+    maxWidth = attrng.Measurement(
+        title=u'Maximum Width',
+        description=u'The maximum width the flowables are allotted.',
+        default=None,
+        required=False)
+
+    maxHeight = attrng.Measurement(
+        title=u'Maximum Height',
+        description=u'The maximum height the flowables are allotted.',
+        default=None,
+        required=False)
+
+    mergeSpace = attrng.Boolean(
+        title=u'Merge Space',
+        description=u'A flag to set whether the space should be merged.',
+        required=False)
+
+    onOverflow = attrng.Choice(
+        title=u'On Overflow',
+        description=u'Defines what has to be done, if an overflow is detected.',
+        choices=('error', 'overflow', 'shrink', 'truncate'),
+        required=False)
+
+    id = attrng.Text(
+        title=u'Name/Id',
+        description=u'The name/id of the flowable.',
+        required=False)
+
+    frame = attrng.StringOrInt(
+        title=u'Frame',
+        description=u'The frame to which the flowable should be fitted.',
+        required=False)
+
 class KeepInFrame(Flowable):
+    signature = IKeepInFrame
     klass = reportlab.platypus.flowables.KeepInFrame
-    args = (
-        attr.Measurement('maxWidth', None),
-        attr.Measurement('maxHeight', None), )
-    kw = (
-        ('mergeSpace', attr.Bool('mergeSpace')),
-        ('mode', attr.Choice('onOverflow',
-                             ('error', 'overflow', 'shrink', 'truncate'))),
-        ('name', attr.Text('id')),
-        ('frame', attr.StringOrInt('frame')), )
+    attrMapping = {'onOverflow': 'mode', 'id': 'name'}
 
     def process(self):
-        args = self.getPositionalArguments()
-        kw = self.getKeywordArguments()
+        args = dict(self.getAttributeValues(attrMapping=self.attrMapping))
+        # Circumvent broken-ness in zope.schema
+        args['maxWidth'] = args.get('maxWidth', None)
+        args['maxHeight'] = args.get('maxHeight', None)
         # If the frame was specifed, get us there
-        frame = kw.pop('frame', None)
+        frame = args.pop('frame', None)
         if frame:
             self.parent.flow.append(
                 reportlab.platypus.doctemplate.FrameBreak(frame))
         # Create the content of the container
-        flow = Flow(self.element, self.parent, self.context)
+        flow = Flow(self.element, self.parent)
         flow.process()
-        kw['content'] = flow.flow
+        args['content'] = flow.flow
         # Create the keep in frame container
-        frame = self.klass(*args, **kw)
+        frame = self.klass(**args)
         self.parent.flow.append(frame)
 
 
+class IImageAndFlowables(interfaces.IRMLDirectiveSignature):
+    """An image with flowables around it."""
+
+    imageName = attrng.Image(
+        title=u'Image',
+        description=u'The file that is used to extract the image data.',
+        onlyOpen=True,
+        required=True)
+
+    imageWidth = attrng.Measurement(
+        title=u'Image Width',
+        description=u'The width of the image.',
+        required=False)
+
+    imageHeight = attrng.Measurement(
+        title=u'Image Height',
+        description=u'The height the image.',
+        required=False)
+
+    imageMask = attrng.Color(
+        title=u'Mask',
+        description=u'The height the image.',
+        required=False)
+
+    imageLeftPadding = attrng.Measurement(
+        title=u'Image Left Padding',
+        description=u'The padding on the left side of the image.',
+        required=False)
+
+    imageRightPadding = attrng.Measurement(
+        title=u'Image Right Padding',
+        description=u'The padding on the right side of the image.',
+        required=False)
+
+    imageTopPadding = attrng.Measurement(
+        title=u'Image Top Padding',
+        description=u'The padding on the top of the image.',
+        required=False)
+
+    imageBottomPadding = attrng.Measurement(
+        title=u'Image Bottom Padding',
+        description=u'The padding on the bottom of the image.',
+        required=False)
+
+    iamgeSide = attrng.Choice(
+        title=u'Image Side',
+        description=u'The side at which the image will be placed.',
+        choices=('left', 'right'),
+        required=False)
+
 class ImageAndFlowables(Flowable):
+    signature = IImageAndFlowables
     klass = reportlab.platypus.flowables.ImageAndFlowables
-    args = ( attr.Image('imageName', onlyOpen=True), )
-    kw = (
-        ('width', attr.Measurement('imageWidth')),
-        ('height', attr.Measurement('imageHeight')),
-        ('mask', attr.Color('imageMask')),
-        ('imageLeftPadding', attr.Measurement('imageLeftPadding')),
-        ('imageRightPadding', attr.Measurement('imageRightPadding')),
-        ('imageTopPadding', attr.Measurement('imageTopPadding')),
-        ('imageBottomPadding', attr.Measurement('imageBottomPadding')),
-        ('imageSide', attr.Choice('imageSide', ('left', 'right'))) )
+    attrMapping = {'imageWidth': 'width', 'imageHeight': 'height',
+                   'imageMask': 'mask', 'imageName': 'filename'}
 
     def process(self):
-        flow = Flow(self.element, self.parent, self.context)
+        flow = Flow(self.element, self.parent)
         flow.process()
-        args = self.getPositionalArguments()
-        kw = self.getKeywordArguments()
         # Create the image
-        img = reportlab.platypus.flowables.Image(
-            width=kw.get('width'), height=kw.get('height'),
-            mask=kw.get('mask', 'auto'), *args)
-        for option in ('width', 'height', 'mask'):
-            if option in kw:
-                del kw[option]
+        args = dict(self.getAttributeValues(
+            select=('imageName', 'imageWidth', 'imageHeight', 'imageMask'),
+            attrMapping=self.attrMapping))
+        img = reportlab.platypus.flowables.Image(**args)
         # Create the flowable and add it
+        args = dict(self.getAttributeValues(
+            ignore=('imageName', 'imageWidth', 'imageHeight', 'imageMask'),
+            attrMapping=self.attrMapping))
         self.parent.flow.append(
-            self.klass(img, flow.flow, **kw))
+            self.klass(img, flow.flow, **args))
+
 
+class IPTO(interfaces.IRMLDirectiveSignature):
+    '''A container for flowables decorated with trailer & header lists.
+    If the split operation would be called then the trailer and header
+    lists are injected before and after the split. This allows specialist
+    "please turn over" and "continued from previous" like behaviours.'''
 
 class PTO(Flowable):
+    signature = IPTO
     klass = reportlab.platypus.flowables.PTOContainer
 
     def process(self):
         # Get Content
-        flow = Flow(self.element, self.parent, self.context)
+        flow = Flow(self.element, self.parent)
         flow.process()
         # Get the header
         ptoHeader = self.element.find('pto_header')
         header = None
         if ptoHeader:
-            header = Flow(ptoHeader, self.parent, self.context)
+            header = Flow(ptoHeader, self.parent)
             header.process()
             header = header.flow
         # Get the trailer
         ptoTrailer = self.element.find('pto_trailer')
         trailer = None
         if ptoTrailer:
-            trailer = Flow(ptoTrailer, self.parent, self.context)
+            trailer = Flow(ptoTrailer, self.parent)
             trailer.process()
             trailer = trailer.flow
         # Create and add the PTO Container
         self.parent.flow.append(self.klass(flow.flow, trailer, header))
 
 
+class IIndent(interfaces.IRMLDirectiveSignature):
+    """Indent the contained flowables."""
+
+    left = attrng.Measurement(
+        title=u'Left',
+        description=u'The indentation to the left.',
+        required=False)
+
+    right = attrng.Measurement(
+        title=u'Right',
+        description=u'The indentation to the right.',
+        required=False)
+
 class Indent(Flowable):
-    kw = (
-        ('left', attr.Measurement('left')),
-        ('right', attr.Measurement('right')) )
+    signature = IIndent
 
     def process(self):
-        kw = self.getKeywordArguments()
+        kw = dict(self.getAttributeValues())
         # Indent
         self.parent.flow.append(reportlab.platypus.doctemplate.Indenter(**kw))
         # Add Content
-        flow = Flow(self.element, self.parent, self.context)
+        flow = Flow(self.element, self.parent)
         flow.process()
         self.parent.flow += flow.flow
         # Dedent
@@ -377,57 +874,181 @@ def process(self):
         self.parent.flow.append(reportlab.platypus.doctemplate.Indenter(**kw))
 
 
+class IFixedSize(interfaces.IRMLDirectiveSignature):
+    """Create a container flowable of a fixed size."""
+
+    width = attrng.Measurement(
+        title=u'Width',
+        description=u'The width the flowables are allotted.',
+        required=True)
+
+    height = attrng.Measurement(
+        title=u'Height',
+        description=u'The height the flowables are allotted.',
+        required=True)
+
 class FixedSize(Flowable):
+    signature = IFixedSize
     klass = reportlab.platypus.flowables.KeepInFrame
-    args = (
-        attr.Measurement('width'),
-        attr.Measurement('height'), )
+    attrMapping = {'width': 'maxWidth', 'height': 'maxHeight'}
 
     def process(self):
-        flow = Flow(self.element, self.parent, self.context)
+        flow = Flow(self.element, self.parent)
         flow.process()
-        args = self.getPositionalArguments()
-        frame = self.klass(content=flow.flow, mode='shrink', *args)
+        args = dict(self.getAttributeValues(attrMapping=self.attrMapping))
+        frame = self.klass(content=flow.flow, mode='shrink', **args)
         self.parent.flow.append(frame)
 
+
+class IBookmark(interfaces.IRMLDirectiveSignature):
+    """
+    This creates a bookmark to the current page which can be referred to with
+    the given key elsewhere.
+
+    PDF offers very fine grained control over how Acrobat reader is zoomed
+    when people link to this. The default is to keep the user's current zoom
+    settings. the last arguments may or may not be needed depending on the
+    choice of 'fitType'.
+    """
+
+    name = attrng.Text(
+        title=u'Name',
+        description=u'The name of the bookmark.',
+        required=True)
+
+    fitType = attrng.Choice(
+        title=u'Fit Type',
+        description=u'The Fit Type.',
+        choices=('Fit', 'FitH', 'FitV', 'FitR'),
+        required=False)
+
+    left = attrng.Measurement(
+        title=u'Left',
+        description=u'The left position.',
+        required=False)
+
+    right = attrng.Measurement(
+        title=u'Right',
+        description=u'The right position.',
+        required=False)
+
+    top = attrng.Measurement(
+        title=u'Top',
+        description=u'The top position.',
+        required=False)
+
+    right = attrng.Measurement(
+        title=u'Right',
+        description=u'The right position.',
+        required=False)
+
+    zoom = attrng.Float(
+        title=u'Zoom',
+        description=u'The zoom level when clicking on the bookmark.',
+        required=False)
+
 class Bookmark(Flowable):
+    signature = IBookmark
     klass = platypus.BookmarkPage
-    args = ( attr.Text('name'), )
-    kw = (
-        ('fitType', attr.Choice('fitType', ('Fit', 'FitH', 'FitV', 'FitR'))),
-        ('left', attr.Measurement('left')),
-        ('right', attr.Measurement('right')),
-        ('top', attr.Measurement('top')),
-        ('bottom', attr.Measurement('bottom')),
-        ('zoom', attr.Float('zoom')),
-        )
+    attrMapping = {'name': 'key'}
+
+
+class IHorizontalRow(interfaces.IRMLDirectiveSignature):
+    """Create a horizontal line on the page."""
+
+    width = attrng.Measurement(
+        title=u'Width',
+        description=u'The width of the line on the page.',
+        allowPercentage=True,
+        required=False)
+
+    thickness = attrng.Measurement(
+        title=u'Thickness',
+        description=u'Line Thickness',
+        required=False)
+
+    color = attrng.Color(
+        title=u'Color',
+        description=u'The color of the line.',
+        required=False)
+
+    lineCap = attrng.Choice(
+        title=u'Cap',
+        description=u'The cap at the end of the line.',
+        choices=interfaces.CAP_CHOICES.keys(),
+        required=False)
+
+    spaceBefore = attrng.Measurement(
+        title=u'Space Before',
+        description=u'The vertical space before the line.',
+        required=False)
+
+    spaceAfter = attrng.Measurement(
+        title=u'Space After',
+        description=u'The vertical space after the line.',
+        required=False)
+
+    align = attrng.Choice(
+        title=u'Alignment',
+        description=u'The alignment of the line within the frame.',
+        choices=interfaces.ALIGN_TEXT_CHOICES,
+        required=False)
+
+    valign = attrng.Choice(
+        title=u'Vertical Alignment',
+        description=u'The vertical alignment of the line.',
+        choices=interfaces.VALIGN_TEXT_CHOICES,
+        required=False)
+
+    dash = attrng.Sequence(
+        title=u'Dash-Pattern',
+        description=u'The dash-pattern of a line.',
+        value_type=attrng.Measurement(),
+        default=None,
+        required=False)
 
 class HorizontalRow(Flowable):
+    signature = IHorizontalRow
     klass = reportlab.platypus.flowables.HRFlowable
-    kw = (
-        ('width', attr.Measurement('width', allowPercentage=True)),
-        ('thickness', attr.Measurement('thickness')),
-        ('color', attr.Color('color')),
-        ('lineCap', attr.Choice('lineCap', ('butt', 'round', 'square') )),
-        ('spaceBefore', attr.Measurement('spaceBefore')),
-        ('spaceAfter', attr.Measurement('spaceAfter')),
-        ('hAlign', attr.Choice(
-             'align', ('left', 'right', 'center', 'centre', 'decimal') )),
-        ('vAlign', attr.Choice('vAlign', ('top', 'middle', 'bottom') )),
-        ('dash', attr.Sequence('dash', attr.Measurement())),
-        )
+    attrMapping = {'align': 'hAlign'}
+
+
+class IOutlineAdd(interfaces.IRMLDirectiveSignature):
+    """Add a new entry to the outline of the PDF."""
+
+    title = attrng.TextNode(
+        title=u'Title',
+        description=u'The text displayed for this item.',
+        required=True)
+
+    key = attrng.String(
+        title=u'Key',
+        description=u'The unique key of the item.',
+        required=False)
+
+    level = attrng.Integer(
+        title=u'Level',
+        description=u'The level in the outline tree.',
+        required=False)
+
+    closed = attrng.Boolean(
+        title=u'Closed',
+        description=(u'A flag to determine whether the sub-tree is closed '
+                     u'by default.'),
+        required=False)
+
 
 class OutlineAdd(Flowable):
+    signature = IOutlineAdd
     klass = platypus.OutlineAdd
-    args = ( attr.TextNode(), attr.Text('key', None) )
-    kw = (
-        ('level', attr.Int('level')),
-        ('closed', attr.Bool('closed')),
-        )
 
-class Flow(element.ContainerElement):
 
-    subElements = {
+class IFlow(interfaces.IRMLDirectiveSignature):
+    """A list of flowables."""
+
+class Flow(directive.RMLDirective):
+
+    factories = {
         # Generic Flowables
         'spacer': Spacer,
         'illustration': Illustration,
@@ -466,5 +1087,5 @@ def __init__(self, *args, **kw):
         self.flow = []
 
     def process(self):
-        self.processSubElements(None)
+        self.processSubDirectives()
         return self.flow
diff --git a/src/z3c/rml/form.py b/src/z3c/rml/form.py
index f7b0c36..2c02f7b 100644
--- a/src/z3c/rml/form.py
+++ b/src/z3c/rml/form.py
@@ -17,7 +17,7 @@
 """
 __docformat__ = "reStructuredText"
 import types
-from z3c.rml import attr, element
+from z3c.rml import attrng, directive, interfaces
 
 try:
     import reportlab.graphics.barcode
@@ -29,62 +29,199 @@
     reportlab.graphics.barcode.getCodeNames = lambda : ()
 
 
-class BarCode(element.FunctionElement):
-    args = (
-        attr.Choice('code', reportlab.graphics.barcode.getCodeNames()),
-        attr.TextNode(),
-        )
-    kw = (
-        ('x', attr.Measurement('x')),
-        ('y', attr.Measurement('y')),
-        ('width', attr.Measurement('width')),
-        ('height', attr.Measurement('height')),
-        ('strokeColor', attr.Color('strokeColor')),
-        ('strokeWidth', attr.Measurement('strokeWidth')),
-        ('fillColor', attr.Color('fillColor')),
-        ('barStrokeColor', attr.Color('barStrokeColor')),
-        ('barStrokeWidth', attr.Measurement('barStrokeWidth')),
-        ('barFillColor', attr.Color('barFillColor')),
-        ('gap', attr.Measurement('gap')),
-        # Bar code dependent attributes
-        # I2of5, Code128, Standard93, FIM, POSTNET, Ean13B
-        ('barWidth', attr.Measurement('barWidth')),
-        # I2of5, Code128, Standard93, FIM, POSTNET
-        ('barHeight', attr.Measurement('barHeight')),
-        # I2of5
-        ('ratio', attr.Float('ratio')),
-        # I2of5
-        # Should be boolean, but some code want it as int; will still work
-        ('checksum', attr.Int('checksum')),
-        # I2of5
-        ('bearers', attr.Float('bearers')),
-        # I2of5, Code128, Standard93, FIM, Ean13
-        ('quiet', attr.Bool('quiet')),
-        # I2of5, Code128, Standard93, FIM, Ean13
-        ('lquiet', attr.Measurement('lquiet')),
-        # I2of5, Code128, Standard93, FIM, Ean13
-        ('rquiet', attr.Measurement('rquiet')),
-        # I2of5, Code128, Standard93, FIM, POSTNET, Ean13
-        ('fontName', attr.Text('fontName')),
-        # I2of5, Code128, Standard93, FIM, POSTNET, Ean13
-        ('fontSize', attr.Measurement('fontSize')),
-        # I2of5, Code128, Standard93, FIM, POSTNET, Ean13
-        ('humanReadable', attr.Bool('humanReadable')),
-        # I2of5, Standard93
-        ('stop', attr.Bool('atop')),
-        # FIM, POSTNET
-        ('spaceWidth', attr.Measurement('spaceWidth')),
-        # POSTNET
-        ('shortHeight', attr.Measurement('shortHeight')),
-        # Ean13
-        ('textColor', attr.Color('textColor')),
-        )
+class IBarCodeBase(interfaces.IRMLDirectiveSignature):
+    """Create a bar code."""
+
+    code = attrng.Choice(
+        title=u'Code',
+        description=u'The name of the type of code to use.',
+        choices=reportlab.graphics.barcode.getCodeNames(),
+        required=True)
+
+    value = attrng.TextNode(
+        title=u'Value',
+        description=u'The value represented by the code.',
+        required=True)
+
+    width = attrng.Measurement(
+        title=u'Width',
+        description=u'The width of the barcode.',
+        required=False)
+
+    height = attrng.Measurement(
+        title=u'Height',
+        description=u'The height of the barcode.',
+        required=False)
+
+    strokeColor = attrng.Color(
+        title=u'Stroke Color',
+        description=(u'The color of the line strokes in the area.'),
+        required=False)
+
+    strokeWidth = attrng.Measurement(
+        title=u'Stroke Width',
+        description=u'The width of the line strokes in the area.',
+        required=False)
+
+    fillColor = attrng.Color(
+        title=u'Fill Color',
+        description=(u'The color of the filled shapes in the area.'),
+        required=False)
+
+    barStrokeColor = attrng.Color(
+        title=u'Bar Stroke Color',
+        description=(u'The color of the line strokes in the barcode.'),
+        required=False)
+
+    barStrokeWidth = attrng.Measurement(
+        title=u'Bar Stroke Width',
+        description=u'The width of the line strokes in the barcode.',
+        required=False)
+
+    barFillColor = attrng.Color(
+        title=u'Bar Fill Color',
+        description=(u'The color of the filled shapes in the barcode.'),
+        required=False)
+
+    gap = attrng.Measurement(
+        title=u'Gap',
+        description=u'The width of the inter-character gaps.',
+        required=False)
+
+    # Bar code dependent attributes
+    # I2of5, Code128, Standard93, FIM, POSTNET, Ean13B
+    barWidth = attrng.Measurement(
+        title=u'Bar Width',
+        description=u'The width of the smallest bar within the barcode',
+        required=False)
+
+    # I2of5, Code128, Standard93, FIM, POSTNET
+    barHeight = attrng.Measurement(
+        title=u'Bar Height',
+        description=u'The height of the symbol.',
+        required=False)
+
+    # I2of5
+    ratio = attrng.Float(
+        title=u'Ratio',
+        description=(u'The ratio of wide elements to narrow elements. '
+                     u'Must be between 2.0 and 3.0 (or 2.2 and 3.0 if the '
+                     u'barWidth is greater than 20 mils (.02 inch)).'),
+        min=2.0,
+        max=3.0,
+        required=False)
+
+    # I2of5
+    # Should be boolean, but some code want it as int; will still work
+    checksum = attrng.Integer(
+        title=u'Ratio',
+        description=(u'A flag that enables the computation and inclusion of '
+                     u'the check digit.'),
+        required=False)
+
+    # I2of5
+    bearers = attrng.Float(
+        title=u'Bearers',
+        description=(u'Height of bearer bars (horizontal bars along the top '
+                     u'and bottom of the barcode). Default is 3 '
+                     u'x-dimensions. Set to zero for no bearer bars.'
+                     u'(Bearer bars help detect misscans, so it is '
+                     u'suggested to leave them on).'),
+        required=False)
+
+    # I2of5, Code128, Standard93, FIM, Ean13
+    quiet = attrng.Boolean(
+        title=u'Quiet Zone',
+        description=(u'A flag to include quiet zones in the symbol.'),
+        required=False)
+
+    # I2of5, Code128, Standard93, FIM, Ean13
+    lquiet = attrng.Measurement(
+        title=u'Left Quiet Zone',
+        description=(u"Quiet zone size to the left of code, if quiet is "
+                     u"true. Default is the greater of .25 inch or .15 times "
+                     u"the symbol's length."),
+        required=False)
+
+    # I2of5, Code128, Standard93, FIM, Ean13
+    rquiet = attrng.Measurement(
+        title=u'Right Quiet Zone',
+        description=(u"Quiet zone size to the right of code, if quiet is "
+                     u"true. Default is the greater of .25 inch or .15 times "
+                     u"the symbol's length."),
+        required=False)
+
+    # I2of5, Code128, Standard93, FIM, POSTNET, Ean13
+    frontName = attrng.String(
+        title=u'Font Name',
+        description=(u'The font used to print the value.'),
+        required=False)
+
+    # I2of5, Code128, Standard93, FIM, POSTNET, Ean13
+    frontSize = attrng.Measurement(
+        title=u'Font Size',
+        description=(u'The size of the value text.'),
+        required=False)
+
+    # I2of5, Code128, Standard93, FIM, POSTNET, Ean13
+    humanReadable = attrng.Boolean(
+        title=u'Human Readable',
+        description=(u'A flag when set causes the value to be printed below '
+                     u'the bar code.'),
+        required=False)
+
+    # I2of5, Standard93
+    stop = attrng.Boolean(
+        title=u'Show Start/Stop',
+        description=(u'A flag to specify whether the start/stop symbols '
+                     u'are to be shown.'),
+        required=False)
+
+    # FIM, POSTNET
+    spaceWidth = attrng.Measurement(
+        title=u'Space Width',
+        description=u'The space of the inter-character gaps.',
+        required=False)
+
+    # POSTNET
+    shortHeight = attrng.Measurement(
+        title=u'Short Height',
+        description=u'The height of the short bar.',
+        required=False)
+
+    # Ean13
+    textColor = attrng.Color(
+        title=u'Text Color',
+        description=(u'The color of human readable text.'),
+        required=False)
+
+
+class IBarCode(IBarCodeBase):
+    """A barcode graphic."""
+
+    x = attrng.Measurement(
+        title=u'X-Position',
+        description=u'The x-position of the lower-left corner of the barcode.',
+        default=0,
+        required=False)
+
+    y = attrng.Measurement(
+        title=u'Y-Position',
+        description=u'The y-position of the lower-left corner of the barcode.',
+        default=0,
+        required=False)
+
+
+
+class BarCode(directive.RMLDirective):
+    signature = IBarCode
 
     def process(self):
-        kw = self.getKeywordArguments()
-        name, value = self.getPositionalArguments()
-        kw['value'] = str(value)
+        kw = dict(self.getAttributeValues())
+        name = kw.pop('code')
+        kw['value'] = str(kw['value'])
         x = kw.pop('x', 0)
         y = kw.pop('y', 0)
         code = reportlab.graphics.barcode.createBarcodeDrawing(name, **kw)
-        code.drawOn(self.context, x, y)
+        manager = attrng.getManager(self, interfaces.ICanvasManager)
+        code.drawOn(manager.canvas, x, y)
diff --git a/src/z3c/rml/interfaces.py b/src/z3c/rml/interfaces.py
index 505255c..2bf72aa 100644
--- a/src/z3c/rml/interfaces.py
+++ b/src/z3c/rml/interfaces.py
@@ -16,11 +16,38 @@
 $Id$
 """
 __docformat__ = "reStructuredText"
+import reportlab.lib.enums
 import zope.interface
+import zope.schema
+
+from z3c.rml import attrng
+from z3c.rml.occurence import ZeroOrMore, ZeroOrOne, OneOrMore
+
+JOIN_CHOICES = {'round': 1, 'mitered': 0, 'bevelled': 2}
+CAP_CHOICES = {'default': 0, 'butt': 0, 'round': 1, 'square': 2}
+ALIGN_CHOICES = {
+    'left': reportlab.lib.enums.TA_LEFT,
+    'right': reportlab.lib.enums.TA_RIGHT,
+    'center': reportlab.lib.enums.TA_CENTER,
+    'centre': reportlab.lib.enums.TA_CENTER,
+    'justify': reportlab.lib.enums.TA_JUSTIFY}
+ALIGN_TEXT_CHOICES = {
+    'left': 'LEFT', 'right': 'RIGHT', 'center': 'CENTER', 'centre': 'CENTER',
+    'decimal': 'DECIMAL'}
+VALIGN_TEXT_CHOICES = {
+    'top': 'TOP', 'middle': 'MIDDLE', 'bottom': 'BOTTOM'}
+SPLIT_CHOICES = ('splitfirst', 'splitlast')
+
 
 class IRML2PDF(zope.interface.Interface):
     """This is the main public API of z3c.rml"""
 
+    def parseString(xml):
+        """Parse an XML string and convert it to PDF.
+
+        The output is a ``StringIO`` object.
+        """
+
     def go(xmlInputName, outputFileName=None, outDir=None, dtdDir=None):
         """Convert RML 2 PDF.
 
@@ -28,25 +55,68 @@ def go(xmlInputName, outputFileName=None, outDir=None, dtdDir=None):
         ``outputFileName``.
         """
 
-class INamesManager(zope.interface.Interface):
-    """Manages custom names"""
-
+class IManager(zope.interface.Interface):
+    """A manager of all document-global variables."""
     names = zope.interface.Attribute("Names dict")
-
-class IStylesManager(zope.interface.Interface):
-    """Manages custom styles"""
-
     styles = zope.interface.Attribute("Styles dict")
-
-
-class IColorsManager(zope.interface.Interface):
-    """Manages custom colors"""
-
     colors = zope.interface.Attribute("Colors dict")
 
-
 class IPostProcessorManager(zope.interface.Interface):
     """Manages all post processors"""
 
     postProcessors = zope.interface.Attribute(
         "List of tuples of the form: (name, processor)")
+
+class ICanvasManager(zope.interface.Interface):
+    """A manager for the canvas."""
+    canvas = zope.interface.Attribute("Canvas")
+
+class IRMLDirectiveSignature(zope.interface.Interface):
+    """The attribute and sub-directives signature of the current
+    RML directive."""
+
+
+class IRMLDirective(zope.interface.Interface):
+    """A directive in RML extracted from an Element Tree element."""
+
+    signature = zope.schema.Field(
+        title=u'Signature',
+        description=(u'The signature of the RML directive.'),
+        required=True)
+
+    parent = zope.schema.Field(
+        title=u'Parent RML Element',
+        description=u'The parent in the RML element hierarchy',
+        required=True,)
+
+    element = zope.schema.Field(
+        title=u'Element',
+        description=(u'The Element Tree element from which the data '
+                     u'is retrieved.'),
+        required=True)
+
+    def getAttributeValues(ignore=None, select=None, includeMissing=False):
+        """Return a list of name-value-tuples based on the signature.
+
+        If ``ignore`` is specified, all attributes are returned except the
+        ones listed in the argument. The values of the sequence are the
+        attribute names.
+
+        If ``select`` is specified, only attributes listed in the argument are
+        returned. The values of the sequence are the attribute names.
+
+        If ``includeMissing`` is set to true, then even missing attributes are
+        included in the value list.
+        """
+
+    def processSubDirectives(self):
+        """Process all sub-directives."""
+
+    def process(self):
+        """Process the directive.
+
+        The main task for this method is to interpret the available data and
+        to make the corresponding calls in the Reportlab document.
+
+        This call should also process all sub-directives and process them.
+        """
diff --git a/src/z3c/rml/occurence.py b/src/z3c/rml/occurence.py
new file mode 100644
index 0000000..f60cf43
--- /dev/null
+++ b/src/z3c/rml/occurence.py
@@ -0,0 +1,110 @@
+##############################################################################
+#
+# Copyright (c) 2007 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Condition Implementation
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import reportlab
+import sys
+import zope.interface
+import zope.schema
+from zope.schema import fieldproperty
+
+class ICondition(zope.interface.Interface):
+    """Condition that is checked before a  directive is available."""
+
+    __doc__ = zope.schema.TextLine(
+        title=u'Description',
+        description=u'The description of the condition.',
+        required=True)
+
+    def __call__(directive):
+        """Check whether the condition is fulfilled for the given directive."""
+
+
+class IOccurence(zope.interface.Interface):
+    """Description of the occurence of a sub-directive."""
+
+    __doc__ = zope.schema.TextLine(
+        title=u'Description',
+        description=u'The description of the occurence.',
+        required=True)
+
+    tag = zope.schema.BytesLine(
+        title=u'Tag',
+        description=u'The tag of the sub-directive within the directive',
+        required=True)
+
+    signature = zope.schema.Field(
+        title=u'Signature',
+        description=u'The signature of the sub-directive.',
+        required=True)
+
+    condition = zope.schema.Field(
+        title=u'Condition',
+        description=u'The condition that the directive is available.',
+        required=False)
+
+
+@zope.interface.implementer(ICondition)
+def laterThanReportlab21(directive):
+    """The directive is only available in Reportlab 2.1 and higher."""
+    return [int(num) for num in reportlab.Version.split('.')] >= (2, 0)
+
+
+def containing(*occurences):
+    frame = sys._getframe(1)
+    f_locals = frame.f_locals
+    f_globals = frame.f_globals
+
+    if not (f_locals is not f_globals
+            and f_locals.get('__module__')
+            and f_locals.get('__module__') == f_globals.get('__name__')
+            ):
+        raise TypeError("contains not called from signature interface")
+
+    f_locals['__interface_tagged_values__'] = {'directives': occurences}
+
+
+class Occurence(object):
+    zope.interface.implements(IOccurence)
+
+    tag = fieldproperty.FieldProperty(IOccurence['tag'])
+    signature = fieldproperty.FieldProperty(IOccurence['signature'])
+    condition = fieldproperty.FieldProperty(IOccurence['condition'])
+
+    def __init__(self, tag, signature, condition=None):
+        self.tag = tag
+        self.signature = signature
+        self.condition = condition
+
+
+class ZeroOrMore(Occurence):
+    """Zero or More
+
+    This sub-directive can occur zero or more times.
+    """
+
+class ZeroOrOne(Occurence):
+    """Zero or one
+
+    This sub-directive can occur zero or one time.
+    """
+
+class OneOrMore(Occurence):
+    """One or More
+
+    This sub-directive can occur one or more times.
+    """
diff --git a/src/z3c/rml/page.py b/src/z3c/rml/page.py
index e52b4b2..6678e1f 100644
--- a/src/z3c/rml/page.py
+++ b/src/z3c/rml/page.py
@@ -17,7 +17,7 @@
 """
 __docformat__ = "reStructuredText"
 import cStringIO
-from z3c.rml import attr, element, interfaces
+from z3c.rml import attrng, directive, interfaces
 
 try:
     import pyPdf
@@ -48,11 +48,25 @@ def process(self, inputFile1):
         return outputFile
 
 
-class MergePage(element.FunctionElement):
-    args = ( attr.File('filename'), attr.Int('page') )
+class IMergePage(interfaces.IRMLDirectiveSignature):
+    """Merges an existing PDF Page into the one to be generated."""
+
+    filename = attrng.File(
+        title=u'File',
+        description=(u'Reference to the PDF file to extract the page from.'),
+        required=True)
+
+    page = attrng.Integer(
+        title=u'Page Number',
+        description=u'The page number of the PDF file that is used to merge..',
+        required=True)
+
+
+class MergePage(directive.RMLDirective):
+    signature = IMergePage
 
     def getProcessor(self):
-        manager = attr.getManager(self, interfaces.IPostProcessorManager)
+        manager = attrng.getManager(self, interfaces.IPostProcessorManager)
         procs = dict(manager.postProcessors)
         if 'MERGE' not in procs:
             proc = MergePostProcessor()
@@ -64,8 +78,9 @@ def process(self):
         if pyPdf is None:
             raise Exception(
                 'pyPdf is not installed, so this feature is not available.')
-        inputFile, inPage = self.getPositionalArguments()
-        outPage = self.context.getPageNumber()-1
+        inputFile, inPage = self.getAttributeValues(valuesOnly=True)
+        manager = attrng.getManager(self, interfaces.ICanvasManager)
+        outPage = manager.canvas.getPageNumber()-1
 
         proc = self.getProcessor()
         pageOperations = proc.operations.setdefault(outPage, [])
diff --git a/src/z3c/rml/platypus.py b/src/z3c/rml/platypus.py
index 6e9dbd9..061b0f7 100644
--- a/src/z3c/rml/platypus.py
+++ b/src/z3c/rml/platypus.py
@@ -17,6 +17,10 @@
 """
 __docformat__ = "reStructuredText"
 import reportlab.platypus.flowables
+import zope.interface
+
+from z3c.rml import interfaces
+
 
 class BaseFlowable(reportlab.platypus.flowables.Flowable):
     def __init__(self, *args, **kw):
@@ -44,7 +48,9 @@ def draw(self):
         from z3c.rml import canvas
         self.canv.saveState()
         drawing = canvas.Drawing(
-            self.processor.element, self.processor, self.canv)
+            self.processor.element, self.processor)
+        zope.interface.alsoProvides(drawing, interfaces.ICanvasManager)
+        drawing.canvas = self.canv
         drawing.process()
         self.canv.restoreState()
 
@@ -55,8 +61,7 @@ def draw(self):
 
 class OutlineAdd(BaseFlowable):
     def draw(self):
-        title, key = self.args
-        if key is None:
-            key = str(hash(self))
-        self.canv.bookmarkPage(key)
-        self.canv.addOutlineEntry(title, key, **self.kw)
+        if self.kw.get('key', None) is None:
+            self.kw['key'] = str(hash(self))
+        self.canv.bookmarkPage(self.kw['key'])
+        self.canv.addOutlineEntry(**self.kw)
diff --git a/src/z3c/rml/special.py b/src/z3c/rml/special.py
index 794f21f..e9319ec 100644
--- a/src/z3c/rml/special.py
+++ b/src/z3c/rml/special.py
@@ -16,25 +16,45 @@
 $Id$
 """
 __docformat__ = "reStructuredText"
-from z3c.rml import attr, element, interfaces
+from z3c.rml import attrng, directive, interfaces
 
 
-class Name(element.FunctionElement):
-    args = (
-        attr.Text('id'),
-        attr.Text('value'), )
+class IName(interfaces.IRMLDirectiveSignature):
+    """Defines a name for a string."""
+
+    id = attrng.String(
+        title=u'Id',
+        description=u'The id under which the value will be known.',
+        required=True)
+
+    value = attrng.Text(
+        title=u'Value',
+        description=u'The text that is displayed if the id is called.',
+        required=True)
+
+class Name(directive.RMLDirective):
+    signature = IName
 
     def process(self):
-        id, value = self.getPositionalArguments()
-        manager = attr.getManager(self, interfaces.INamesManager)
+        id, value = self.getAttributeValues(valuesOnly=True)
+        manager = attrng.getManager(self)
         manager.names[id] = value
 
 
-class GetName(element.Element):
+class IGetName(interfaces.IRMLDirectiveSignature):
+    """Get the text for the id."""
+
+    id = attrng.String(
+        title=u'Id',
+        description=u'The id as which the value is known.',
+        required=True)
+
+class GetName(directive.RMLDirective):
+    signature = IGetName
 
     def process(self):
-        id = attr.Text('id').get(self.element)
-        manager = attr.getManager(self, interfaces.INamesManager)
+        id = dict(self.getAttributeValues()).pop('id')
+        manager = attrng.getManager(self)
         text = manager.names[id] + (self.element.tail or u'')
         # Now replace the element with the text
         parent = self.element.getparent()
@@ -45,12 +65,23 @@ def process(self):
         parent.remove(self.element)
 
 
-class Alias(element.FunctionElement):
-    args = (
-        attr.Text('id'),
-        attr.Style('value'), )
+class IAlias(interfaces.IRMLDirectiveSignature):
+    """Defines an alias for a given style."""
+
+    id = attrng.String(
+        title=u'Id',
+        description=u'The id as which the style will be known.',
+        required=True)
+
+    value = attrng.Style(
+        title=u'Value',
+        description=u'The style that is represented.',
+        required=True)
+
+class Alias(directive.RMLDirective):
+    signature = IAlias
 
     def process(self):
-        id, value = self.getPositionalArguments()
-        manager = attr.getManager(self, interfaces.IStylesManager)
+        id, value = self.getAttributeValues(valuesOnly=True)
+        manager = attrng.getManager(self)
         manager.styles[id] = value
diff --git a/src/z3c/rml/stylesheet.py b/src/z3c/rml/stylesheet.py
index 649bbd0..b2c60d0 100644
--- a/src/z3c/rml/stylesheet.py
+++ b/src/z3c/rml/stylesheet.py
@@ -20,122 +20,311 @@
 import reportlab.lib.styles
 import reportlab.lib.enums
 import reportlab.platypus
-from z3c.rml import attr, element, error, interfaces, special
+from z3c.rml import attrng, directive, interfaces, occurence, special
 
 
-class Initialize(element.ContainerElement):
+class IInitialize(interfaces.IRMLDirectiveSignature):
+    """Do some RML processing initialization."""
+    occurence.containing(
+        occurence.ZeroOrMore('name', special.IName),
+        occurence.ZeroOrMore('alias', special.IAlias),
+        )
 
-    subElements = {
+class Initialize(directive.RMLDirective):
+    signature = IInitialize
+    factories = {
         'name': special.Name,
         'alias': special.Alias,
         }
 
-class ParagraphStyle(element.Element):
-    attrs = (
-        attr.Text('name'),
-        attr.Text('alias'),
-        attr.Style('parent'),
-        attr.Text('fontName'),
-        attr.Measurement('fontSize'),
-        attr.Measurement('leading'),
-        attr.Measurement('leftIndent'),
-        attr.Measurement('rightIndent'),
-        attr.Measurement('firstLineIndent'),
-        attr.Measurement('spaceBefore'),
-        attr.Measurement('spaceAfter'),
-        attr.Choice('alignment',
-            {'left':reportlab.lib.enums.TA_LEFT,
-             'right':reportlab.lib.enums.TA_RIGHT,
-             'center':reportlab.lib.enums.TA_CENTER,
-             'justify':reportlab.lib.enums.TA_JUSTIFY}),
-        attr.Text('bulletFontName'),
-        attr.Measurement('bulletFontSize'),
-        attr.Measurement('bulletIndent'),
-        attr.Color('textColor'),
-        attr.Color('backColor'),
-        attr.Bool('keepWithNext')
-        )
+
+class IBaseParagraphStyle(interfaces.IRMLDirectiveSignature):
+
+    fontName = attrng.String(
+        title=u'Font Name',
+        description=u'The name of the font for the paragraph.',
+        required=False)
+
+    fontSize = attrng.Measurement(
+        title=u'Font Size',
+        description=u'The font size for the text of the paragraph.',
+        required=False)
+
+    leading = attrng.Measurement(
+        title=u'Leading',
+        description=(u'The height of a single paragraph line. It includes '
+                     u'character height.'),
+        required=False)
+
+    leftIndent = attrng.Measurement(
+        title=u'Left Indentation',
+        description=u'General indentation on the left side.',
+        required=False)
+
+    rightIndent = attrng.Measurement(
+        title=u'Right Indentation',
+        description=u'General indentation on the right side.',
+        required=False)
+
+    firstLineIndent = attrng.Measurement(
+        title=u'First Line Indentation',
+        description=u'The indentation of the first line in the paragraph.',
+        required=False)
+
+    spaceBefore = attrng.Measurement(
+        title=u'Space Before',
+        description=u'The vertical space before the paragraph.',
+        required=False)
+
+    spaceAfter = attrng.Measurement(
+        title=u'Space After',
+        description=u'The vertical space after the paragraph.',
+        required=False)
+
+    alignment = attrng.Choice(
+        title=u'Alignment',
+        description=u'The text alignment.',
+        choices=interfaces.ALIGN_CHOICES,
+        required=False)
+
+    bulletFontName = attrng.String(
+        title=u'Bullet Font Name',
+        description=u'The font in which the bullet character will be rendered.',
+        required=False)
+
+    bulletFontSize = attrng.Measurement(
+        title=u'Bullet Font Size',
+        description=u'The font size of the bullet character.',
+        required=False)
+
+    bulletIndent = attrng.Measurement(
+        title=u'Bullet Indentation',
+        description=u'The indentation that is kept for a bullet point.',
+        required=False)
+
+    textColor = attrng.Color(
+        title=u'Text Color',
+        description=u'The color in which the text will appear.',
+        required=False)
+
+    backColor = attrng.Color(
+        title=u'Background Color',
+        description=u'The background color of the paragraph.',
+        required=False)
+
+    keepWithNext = attrng.Boolean(
+        title=u'Keep with Next',
+        description=(u'When set, this paragraph will always be in the same '
+                     u'frame as the following flowable.'),
+        required=False)
+
+
+class IParagraphStyle(IBaseParagraphStyle):
+    """Defines a paragraph style and gives it a name."""
+
+    name = attrng.String(
+        title=u'Name',
+        description=u'The name of the style.',
+        required=True)
+
+    alias = attrng.String(
+        title=u'Alias',
+        description=u'An alias under which the style will also be known as.',
+        required=False)
+
+    parent = attrng.Style(
+        title=u'Parent',
+        description=(u'The apragraph style that will be used as a base for '
+                     u'this one.'),
+        required=False)
+
+class ParagraphStyle(directive.RMLDirective):
+    signature = IParagraphStyle
 
     def process(self):
-        attrs = element.extractKeywordArguments(
-            [(attrib.name, attrib) for attrib in self.attrs], self.element,
-            self.parent)
+        kwargs = dict(self.getAttributeValues())
 
-        parent = attrs.pop(
+        parent = kwargs.pop(
             'parent', reportlab.lib.styles.getSampleStyleSheet()['Normal'])
         style = copy.deepcopy(parent)
 
-        for name, value in attrs.items():
+        for name, value in kwargs.items():
             setattr(style, name, value)
 
-        manager = attr.getManager(self, interfaces.IStylesManager)
+        manager = attrng.getManager(self)
         manager.styles[style.name] = style
 
 
-class TableStyleCommand(element.Element):
+class ITableStyleCommand(interfaces.IRMLDirectiveSignature):
+
+    start = attrng.Sequence(
+        title=u'Start Coordinates',
+        description=u'The start table coordinates for the style instruction',
+        value_type=attrng.Combination(
+            value_types=(attrng.Integer(),
+                         attrng.Choice(choices=interfaces.SPLIT_CHOICES))
+            ),
+        default=[0, 0],
+        min_length=2,
+        max_length=2,
+        required=True)
+
+    end = attrng.Sequence(
+        title=u'End Coordinates',
+        description=u'The end table coordinates for the style instruction',
+        value_type=attrng.Combination(
+            value_types=(attrng.Integer(),
+                         attrng.Choice(choices=interfaces.SPLIT_CHOICES))
+            ),
+        default=[-1, -1],
+        min_length=2,
+        max_length=2,
+        required=True)
+
+class TableStyleCommand(directive.RMLDirective):
     name = None
-    attrs = (
-        attr.Sequence('start', attr.Combination(
-            valueTypes=(attr.Int(),
-                        attr.Choice(choices=('splitfirst', 'splitlast')) )),
-            [0, 0], length=2),
-        attr.Sequence('stop', attr.Combination(
-            valueTypes=(attr.Int(),
-                        attr.Choice(choices=('splitfirst', 'splitlast')) )),
-            [-1, -1], length=2) )
 
     def process(self):
         args = [self.name]
-        for attribute in self.attrs:
-            value = attribute.get(self.element, context=self)
-            if value is not attr.DEFAULT:
-                args.append(value)
-        self.context.add(*args)
+        args += self.getAttributeValues(valuesOnly=True)
+        self.parent.style.add(*args)
+
+
+class IBlockFont(ITableStyleCommand):
+    """Set the font properties for the texts."""
+
+    name = attrng.String(
+        title=u'Font Name',
+        description=u'The name of the font for the cell.',
+        required=False)
+
+    size = attrng.Measurement(
+        title=u'Font Size',
+        description=u'The font size for the text of the cell.',
+        required=False)
+
+    leading = attrng.Measurement(
+        title=u'Leading',
+        description=(u'The height of a single text line. It includes '
+                     u'character height.'),
+        required=False)
 
 class BlockFont(TableStyleCommand):
+    signature = IBlockFont
     name = 'FONT'
-    attrs = TableStyleCommand.attrs + (
-        attr.Text('name'),
-        attr.Measurement('size'),
-        attr.Measurement('leading') )
+
+class IBlockLeading(ITableStyleCommand):
+    """Set the text leading."""
+
+    length = attrng.Measurement(
+        title=u'Length',
+        description=(u'The height of a single text line. It includes '
+                     u'character height.'),
+        required=True)
 
 class BlockLeading(TableStyleCommand):
+    signature = IBlockLeading
     name = 'LEADING'
-    attrs = TableStyleCommand.attrs + (attr.Measurement('length'), )
+
+class IBlockTextColor(ITableStyleCommand):
+    """Set the text color."""
+
+    colorName = attrng.Color(
+        title=u'Color Name',
+        description=u'The color in which the text will appear.',
+        required=True)
 
 class BlockTextColor(TableStyleCommand):
+    signature = IBlockTextColor
     name = 'TEXTCOLOR'
-    attrs = TableStyleCommand.attrs + (attr.Color('colorName'), )
+
+class IBlockAlignment(ITableStyleCommand):
+    """Set the text alignment."""
+
+    value = attrng.Choice(
+        title=u'Text Alignment',
+        description=u'The text alignment within the cell.',
+        choices=interfaces.ALIGN_TEXT_CHOICES,
+        required=True)
 
 class BlockAlignment(TableStyleCommand):
+    signature = IBlockAlignment
     name = 'ALIGNMENT'
-    attrs = TableStyleCommand.attrs + (
-        attr.Choice('value',
-                    {'left': 'LEFT', 'right': 'RIGHT',
-                     'center': 'CENTER', 'decimal': 'DECIMAL'}), )
+
+class IBlockLeftPadding(ITableStyleCommand):
+    """Set the left padding of the cells."""
+
+    length = attrng.Measurement(
+        title=u'Length',
+        description=u'The size of the padding.',
+        required=True)
 
 class BlockLeftPadding(TableStyleCommand):
+    signature = IBlockLeftPadding
     name = 'LEFTPADDING'
-    attrs = TableStyleCommand.attrs + (attr.Measurement('length'), )
+
+class IBlockRightPadding(ITableStyleCommand):
+    """Set the right padding of the cells."""
+
+    length = attrng.Measurement(
+        title=u'Length',
+        description=u'The size of the padding.',
+        required=True)
 
 class BlockRightPadding(TableStyleCommand):
+    signature = IBlockRightPadding
     name = 'RIGHTPADDING'
-    attrs = TableStyleCommand.attrs + (attr.Measurement('length'), )
+
+class IBlockBottomPadding(ITableStyleCommand):
+    """Set the bottom padding of the cells."""
+
+    length = attrng.Measurement(
+        title=u'Length',
+        description=u'The size of the padding.',
+        required=True)
 
 class BlockBottomPadding(TableStyleCommand):
+    signature = IBlockBottomPadding
     name = 'BOTTOMPADDING'
-    attrs = TableStyleCommand.attrs + (attr.Measurement('length'), )
+
+class IBlockTopPadding(ITableStyleCommand):
+    """Set the top padding of the cells."""
+
+    length = attrng.Measurement(
+        title=u'Length',
+        description=u'The size of the padding.',
+        required=True)
 
 class BlockTopPadding(TableStyleCommand):
+    signature = IBlockTopPadding
     name = 'TOPPADDING'
-    attrs = TableStyleCommand.attrs + (attr.Measurement('length'), )
+
+class IBlockBackground(ITableStyleCommand):
+    """Define the background color of the cells.
+
+    It also supports alternating colors.
+    """
+
+    colorName = attrng.Color(
+        title=u'Color Name',
+        description=u'The color to use as the background for every cell.',
+        required=False)
+
+    colorsByRow = attrng.Sequence(
+        title=u'Colors By Row',
+        description=u'A list of colors to be used circularly for rows.',
+        value_type=attrng.Color(acceptNone=True),
+        required=False)
+
+    colorsByCol = attrng.Sequence(
+        title=u'Colors By Column',
+        description=u'A list of colors to be used circularly for columns.',
+        value_type=attrng.Color(acceptNone=True),
+        required=False)
 
 class BlockBackground(TableStyleCommand):
+    signature = IBlockBackground
     name = 'BACKGROUND'
-    attrs = TableStyleCommand.attrs + (
-        attr.Color('colorName'),
-        attr.Sequence('colorsByRow', attr.Color()),
-        attr.Sequence('colorsByCol', attr.Color()) )
 
     def process(self):
         args = [self.name]
@@ -144,64 +333,148 @@ def process(self):
         elif 'colorsByCol' in self.element.keys():
             args = [BlockColBackground.name]
 
-        for attribute in self.attrs:
-            value = attribute.get(self.element, context=self)
-            if value is not attr.DEFAULT:
-                args.append(value)
-        self.context.add(*args)
+        args += self.getAttributeValues(valuesOnly=True)
+        self.parent.style.add(*args)
+
+class IBlockRowBackground(ITableStyleCommand):
+    """Define the background colors for rows."""
+
+    colorNames = attrng.Sequence(
+        title=u'Colors By Row',
+        description=u'A list of colors to be used circularly for rows.',
+        value_type=attrng.Color(),
+        required=True)
 
 class BlockRowBackground(TableStyleCommand):
+    signature = IBlockRowBackground
     name = 'ROWBACKGROUNDS'
-    attrs = TableStyleCommand.attrs + (
-        attr.Sequence('colorNames', attr.Color()), )
+
+class IBlockColBackground(ITableStyleCommand):
+    """Define the background colors for columns."""
+
+    colorNames = attrng.Sequence(
+        title=u'Colors By Row',
+        description=u'A list of colors to be used circularly for rows.',
+        value_type=attrng.Color(),
+        required=True)
 
 class BlockColBackground(TableStyleCommand):
+    signature = IBlockColBackground
     name = 'COLBACKGROUNDS'
-    attrs = TableStyleCommand.attrs + (
-        attr.Sequence('colorNames', attr.Color()), )
+
+class IBlockValign(ITableStyleCommand):
+    """Define the vertical alignment of the cells."""
+
+    value = attrng.Choice(
+        title=u'Vertical Alignment',
+        description=u'The vertical alignment of the text with the cells.',
+        choices=interfaces.VALIGN_TEXT_CHOICES,
+        required=True)
 
 class BlockValign(TableStyleCommand):
+    signature = IBlockValign
     name = 'VALIGN'
-    attrs = TableStyleCommand.attrs + (
-        attr.Choice('value',
-                    {'top': 'TOP', 'middle': 'MIDDLE', 'bottom': 'BOTTOM'}), )
+
+class IBlockSpan(ITableStyleCommand):
+    """Define a span over multiple cells (rows and columns)."""
 
 class BlockSpan(TableStyleCommand):
+    signature = IBlockSpan
     name = 'SPAN'
 
-class LineStyle(TableStyleCommand):
-    attrs = TableStyleCommand.attrs + (
-        attr.Measurement('thickness', default=1),
-        attr.Color('colorName', default=None),
-        attr.Choice('cap', ('butt', 'round', 'square'), default=1),
-        attr.Sequence('dash', attr.Measurement(), default=None),
-        attr.Bool('join', default=1),
-        attr.Int('count', default=1),
-        )
+class ILineStyle(ITableStyleCommand):
+    """Define the border line style of each cell."""
+
+    kind = attrng.Choice(
+        title=u'Kind',
+        description=u'The kind of line actions to be taken.',
+        choices=('GRID', 'BOX', 'OUTLINE', 'INNERGRID',
+                 'LINEBELOW', 'LINEABOVE', 'LINEBEFORE', 'LINEAFTER'),
+        required=True)
+
+    thickness = attrng.Measurement(
+        title=u'Thickness',
+        description=u'Line Thickness',
+        default=1,
+        required=True)
+
+    colorName = attrng.Color(
+        title=u'Color',
+        description=u'The color of the border line.',
+        default=None,
+        required=True)
+
+    cap = attrng.Choice(
+        title=u'Cap',
+        description=u'The cap at the end of a border line.',
+        choices=interfaces.CAP_CHOICES,
+        default=1,
+        required=True)
+
+    dash = attrng.Sequence(
+        title=u'Dash-Pattern',
+        description=u'The dash-pattern of a line.',
+        value_type=attrng.Measurement(),
+        default=None,
+        required=False)
+
+    join = attrng.Choice(
+        title=u'Join',
+        description=u'The way lines are joined together.',
+        choices=interfaces.JOIN_CHOICES,
+        default=1,
+        required=False)
+
+    count = attrng.Integer(
+        title=u'Count',
+        description=(u'Describes whether the line is a single (1) or '
+                     u'double (2) line.'),
+        default=1,
+        required=False)
 
-    @property
-    def name(self):
-        cmds = ['GRID', 'BOX', 'OUTLINE', 'INNERGRID',
-                'LINEBELOW', 'LINEABOVE', 'LINEBEFORE', 'LINEAFTER']
-        return attr.Choice(
-            'kind', dict([(cmd.lower(), cmd) for cmd in cmds])
-            ).get(self.element, context=self)
+class LineStyle(TableStyleCommand):
+    signature = ILineStyle
 
     def process(self):
-        args = [self.name]
-        for attribute in self.attrs:
-            value = attribute.get(self.element, context=self)
-            if value is not attr.DEFAULT:
-                args.append(value)
-        self.context.add(*args)
+        name = self.getAttributeValues(select=('kind',), valuesOnly=True)[0]
+        args = [name]
+        args += self.getAttributeValues(ignore=('kind',), valuesOnly=True)
+        self.parent.style.add(*args)
+
+class IBlockTableStyle(interfaces.IRMLDirectiveSignature):
+    """A style defining the look of a table."""
+    occurence.containing(
+        occurence.ZeroOrMore('blockFont', IBlockFont),
+        occurence.ZeroOrMore('blockLeading', IBlockLeading),
+        occurence.ZeroOrMore('blockTextColor', IBlockTextColor),
+        occurence.ZeroOrMore('blockAlignment', IBlockAlignment),
+        occurence.ZeroOrMore('blockLeftPadding', IBlockLeftPadding),
+        occurence.ZeroOrMore('blockRightPadding', IBlockRightPadding),
+        occurence.ZeroOrMore('blockBottomPadding', IBlockBottomPadding),
+        occurence.ZeroOrMore('blockTopPadding', IBlockTopPadding),
+        occurence.ZeroOrMore('blockBackground', IBlockBackground),
+        occurence.ZeroOrMore('blockRowBackground', IBlockRowBackground),
+        occurence.ZeroOrMore('blockColBackground', IBlockColBackground),
+        occurence.ZeroOrMore('blockValign', IBlockValign),
+        occurence.ZeroOrMore('blockSpan', IBlockSpan),
+        occurence.ZeroOrMore('lineStyle', ILineStyle)
+        )
 
-class BlockTableStyle(element.ContainerElement):
+    id = attrng.String(
+        title=u'Id',
+        description=u'The name/id of the style.',
+        required=True)
 
-    attrs = (
-        attr.Bool('keepWithNext'),
-        )
+    keepWithNext = attrng.Boolean(
+        title=u'Keep with Next',
+        description=(u'When set, this paragraph will always be in the same '
+                     u'frame as the following flowable.'),
+        required=False)
+
+class BlockTableStyle(directive.RMLDirective):
+    signature = IBlockTableStyle
 
-    subElements = {
+    factories = {
         'blockFont': BlockFont,
         'blockLeading': BlockLeading,
         'blockTextColor': BlockTextColor,
@@ -219,25 +492,34 @@ class BlockTableStyle(element.ContainerElement):
         }
 
     def process(self):
-        id = attr.Text('id').get(self.element, context=self)
+        kw = dict(self.getAttributeValues())
+        id  = kw.pop('id')
         # Create Style
-        style = reportlab.platypus.tables.TableStyle()
-        attrs = element.extractAttributes(self.attrs, self.element, self)
-        for name, value in attrs.items():
-            setattr(style, name, value)
+        self.style = reportlab.platypus.tables.TableStyle()
+        for name, value in kw.items():
+            setattr(self.style, name, value)
         # Fill style
-        self.processSubElements(style)
+        self.processSubDirectives()
         # Add style to the manager
-        manager = attr.getManager(self, interfaces.IStylesManager)
-        manager.styles[id] = style
-
+        manager = attrng.getManager(self)
+        manager.styles[id] = self.style
+
+
+class IStylesheet(interfaces.IRMLDirectiveSignature):
+    """A styleheet defines the styles that can be used in the document."""
+    occurence.containing(
+        occurence.ZeroOrOne('initialize', IInitialize),
+        occurence.ZeroOrMore('paraStyle', IParagraphStyle),
+        occurence.ZeroOrMore('blockTableStyle', IBlockTableStyle),
+        # TODO:
+        #occurence.ZeroOrMore('boxStyle', IBoxStyle),
+        )
 
-class Stylesheet(element.ContainerElement):
+class Stylesheet(directive.RMLDirective):
+    signature = IStylesheet
 
-    subElements = {
+    factories = {
         'initialize': Initialize,
         'paraStyle': ParagraphStyle,
         'blockTableStyle': BlockTableStyle,
-        # TODO: 'boxStyle': BoxStyle,
         }
-    order = ('initialize', 'paraStyle', 'blockTableStyle')
diff --git a/src/z3c/rml/template.py b/src/z3c/rml/template.py
index 3054804..0cb1798 100644
--- a/src/z3c/rml/template.py
+++ b/src/z3c/rml/template.py
@@ -16,136 +16,256 @@
 $Id$
 """
 __docformat__ = "reStructuredText"
+import zope.interface
 from reportlab import platypus
-from z3c.rml import attr, canvas, element, flowable, interfaces, stylesheet
+from z3c.rml import attrng, directive, interfaces, occurence
+from z3c.rml import canvas, flowable, stylesheet
 
 
+class IStory(flowable.IFlow):
+    """The story of the PDF file."""
+
+    firstPageTemplate = attrng.Text(
+        title=u'First Page Template',
+        description=u'The first page template to be used.',
+        default=None,
+        required=False)
+
 class Story(flowable.Flow):
+    signature = IStory
 
-    def getFirstPageTemplateIndex(self, doc):
-        fpt = attr.Text('firstPageTemplate').get(self.element, None)
+    def process(self):
+        self.parent.flowables = super(Story, self).process()
+        self.parent.doc._firstPageTemplateIndex = self.getFirstPTIndex()
+
+    def getFirstPTIndex(self):
+        args = dict(self.getAttributeValues(select=('firstPageTemplate',)))
+        fpt = args.pop('firstPageTemplate', None)
         if fpt is None:
             return 0
-        for idx, pageTemplate in enumerate(doc.pageTemplates):
+        for idx, pageTemplate in enumerate(self.parent.doc.pageTemplates):
             if pageTemplate.id == fpt:
                 return idx
         raise ValueError('%r is not a correct page template id.' %fpt)
 
-class Frame(element.FunctionElement):
-    args = (
-        attr.Measurement('x1', allowPercentage=True),
-        attr.Measurement('y1', allowPercentage=True),
-        attr.Measurement('width', allowPercentage=True),
-        attr.Measurement('height', allowPercentage=True),
-        )
-    kw = (
-        ('id', attr.Text('id')),
-        # Non-RML compliant extensions
-        ('leftPadding', attr.Measurement('leftPadding', 0)),
-        ('rightPadding', attr.Measurement('rightPadding', 0)),
-        ('topPadding', attr.Measurement('topPadding', 0)),
-        ('bottomPadding', attr.Measurement('bottomPadding', 0)),
-        ('showBoundary', attr.Bool('showBoundary')),
-        )
+
+class IFrame(interfaces.IRMLDirectiveSignature):
+    """A frame on a page."""
+
+    x1 = attrng.Measurement(
+        title=u'X-Position',
+        description=u'The X-Position of the lower-left corner of the frame.',
+        allowPercentage=True,
+        required=True)
+
+    y1 = attrng.Measurement(
+        title=u'Y-Position',
+        description=u'The Y-Position of the lower-left corner of the frame.',
+        allowPercentage=True,
+        required=True)
+
+    width = attrng.Measurement(
+        title=u'Width',
+        description=u'The width of the frame.',
+        allowPercentage=True,
+        required=True)
+
+    height = attrng.Measurement(
+        title=u'Height',
+        description=u'The height of the frame.',
+        allowPercentage=True,
+        required=True)
+
+    id = attrng.Text(
+        title=u'Id',
+        description=u'The id of the frame.',
+        required=False)
+
+    leftPadding = attrng.Measurement(
+        title=u'Left Padding',
+        description=u'The left padding of the frame.',
+        default=0,
+        required=False)
+
+    rightPadding = attrng.Measurement(
+        title=u'Right Padding',
+        description=u'The right padding of the frame.',
+        default=0,
+        required=False)
+
+    topPadding = attrng.Measurement(
+        title=u'Top Padding',
+        description=u'The top padding of the frame.',
+        default=0,
+        required=False)
+
+    bottomPadding = attrng.Measurement(
+        title=u'Bottom Padding',
+        description=u'The bottom padding of the frame.',
+        default=0,
+        required=False)
+
+    showBoundary = attrng.Boolean(
+        title=u'Show Boundary',
+        description=u'A flag to show the boundary of the frame.',
+        required=False)
+
+
+class Frame(directive.RMLDirective):
+    signature = IFrame
 
     def process(self):
         # get the page size
-        size = self.context.pagesize
+        size = self.parent.pt.pagesize
         if size is None:
-            size = self.parent.context.pagesize
+            size = self.parent.parent.parent.doc.pagesize
         # Get the arguments
-        args = self.getPositionalArguments()
-        kw = self.getKeywordArguments()
+        args = dict(self.getAttributeValues())
         # Deal with percentages
-        if isinstance(args[0], basestring) and args[0].endswith('%'):
-            args[0] = float(args[0][:-1])/100*size[0]
-        if isinstance(args[1], basestring) and args[1].endswith('%'):
-            args[1] = float(args[1][:-1])/100*size[1]
-        if isinstance(args[2], basestring) and args[2].endswith('%'):
-            args[2] = float(args[2][:-1])/100*size[0]
-        if isinstance(args[3], basestring) and args[3].endswith('%'):
-            args[3] = float(args[3][:-1])/100*size[1]
-        frame = platypus.Frame(*args, **kw)
+        for name, dir in (('x1', 0), ('y1', 1), ('width', 0), ('height', 1)):
+            if isinstance(args[name], basestring) and args[name].endswith('%'):
+                args[name] = float(args[name][:-1])/100*size[dir]
+        frame = platypus.Frame(**args)
         self.parent.frames.append(frame)
 
 
-class PageGraphics(element.Element):
+class IPageGraphics(canvas.IDrawing):
+    """Define the page graphics for the page template."""
+
+class PageGraphics(directive.RMLDirective):
+    zope.interface.implements(interfaces.ICanvasManager)
+    signature = IPageGraphics
 
     def process(self):
         def drawOnCanvas(canv, doc):
             canv.saveState()
-            drawing = canvas.Drawing(self.element, self, canv)
+            self.canvas = canv
+            drawing = canvas.Drawing(self.element, self)
             drawing.process()
             canv.restoreState()
 
-        self.context.onPage = drawOnCanvas
+        self.parent.pt.onPage = drawOnCanvas
+
+
+class IPageTemplate(interfaces.IRMLDirectiveSignature):
+    """Define a page template."""
+    occurence.containing(
+        occurence.OneOrMore('frame', IFrame),
+        occurence.ZeroOrOne('pageGraphics', IPageGraphics),
+        )
+
+    id = attrng.Text(
+        title=u'Id',
+        description=u'The id of the template.',
+        required=True)
 
+    pagesize = attrng.PageSize(
+        title=u'Page Size',
+        description=u'The Page Size.',
+        required=False)
 
-class PageTemplate(element.FunctionElement, element.ContainerElement):
-    args = (attr.Text('id'),)
-    kw = (
-        ('pagesize', attr.PageSize('pageSize',)),
-        ('rotation', attr.Int('rotation')) )
+    rotation = attrng.Integer(
+        title=u'Rotation',
+        description=u'The rotation of the page in multiples of 90 degrees.',
+        required=False)
 
-    subElements = {
+
+
+class PageTemplate(directive.RMLDirective):
+    signature = IPageTemplate
+    factories = {
         'frame': Frame,
         'pageGraphics': PageGraphics,
         }
 
     def process(self):
-        args = self.getPositionalArguments()
+        args = dict(self.getAttributeValues())
+        pagesize = args.pop('pagesize', None)
+
         self.frames = []
-        pt = platypus.PageTemplate(*args)
-        self.processSubElements(pt)
-        pt.frames = self.frames
+        self.pt = platypus.PageTemplate(**args)
 
-        kw = self.getKeywordArguments()
-        if 'pagesize' in kw:
-            pt.pagesize = kw['pagesize']
+        self.processSubDirectives()
+        self.pt.frames = self.frames
 
-        self.context.addPageTemplates(pt)
+        if pagesize:
+            self.pt.pagesize = pagesize
 
+        self.parent.parent.doc.addPageTemplates(self.pt)
 
-class Template(element.ContainerElement):
 
-    templateArgs = (
-        ('pagesize', attr.PageSize('pageSize',)),
-        ('rotation', attr.Int('rotation')),
-        ('leftMargin', attr.Measurement('leftMargin')),
-        ('rightMargin', attr.Measurement('rightMargin')),
-        ('topMargin', attr.Measurement('topMargin')),
-        ('bottomMargin', attr.Measurement('bottomMargin')),
-        ('showBoundary', attr.Bool('showBoundary')),
-        ('allowSplitting', attr.Bool('allowSplitting')),
-        ('title', attr.Text('title')),
-        ('author', attr.Text('author')) )
+class ITemplate(interfaces.IRMLDirectiveSignature):
+    """Define a page template."""
+    occurence.containing(
+        occurence.OneOrMore('pagetemplate', IPageTemplate),
+        )
 
-    documentArgs = (
-        ('_debug', attr.Bool('debug')),
-        ('pageCompression', attr.DefaultBool('compression')),
-        ('invariant', attr.DefaultBool('invariant')) )
+    pagesize = attrng.PageSize(
+        title=u'Page Size',
+        description=u'The Page Size.',
+        required=False)
 
-    subElements = {
-        'pageTemplate': PageTemplate,
-        'stylesheet': stylesheet.Stylesheet,
-        }
+    rotation = attrng.Integer(
+        title=u'Rotation',
+        description=u'The rotation of the page in multiples of 90 degrees.',
+        required=False)
+
+    leftMargin = attrng.Measurement(
+        title=u'Left Margin',
+        description=u'The left margin of the template.',
+        default=0,
+        required=False)
+
+    rightMargin = attrng.Measurement(
+        title=u'Right Margin',
+        description=u'The right margin of the template.',
+        default=0,
+        required=False)
+
+    topMargin = attrng.Measurement(
+        title=u'Top Margin',
+        description=u'The top margin of the template.',
+        default=0,
+        required=False)
+
+    bottomMargin = attrng.Measurement(
+        title=u'Bottom Margin',
+        description=u'The bottom margin of the template.',
+        default=0,
+        required=False)
 
-    def process(self, outputFile):
-        docElement = self.element
-        self.processSubElements(None)
+    showBoundary = attrng.Boolean(
+        title=u'Show Boundary',
+        description=u'A flag to show the boundary of the template.',
+        required=False)
 
-        self.element = self.element.find('template')
+    allowSplitting = attrng.Boolean(
+        title=u'Allow Splitting',
+        description=u'A flag to allow splitting over multiple templates.',
+        required=False)
 
-        kw = element.extractKeywordArguments(
-            self.documentArgs, docElement, self)
-        kw.update(element.extractKeywordArguments(
-            self.templateArgs, self.element, self))
-        doc = platypus.BaseDocTemplate(outputFile, **kw)
+    title = attrng.Text(
+        title=u'Title',
+        description=u'The title of the PDF document.',
+        required=False)
 
-        self.processSubElements(doc)
+    author = attrng.Text(
+        title=u'Author',
+        description=u'The author of the PDF document.',
+        required=False)
 
-        story = Story(docElement.find('story'), self, doc)
-        flowables = story.process()
+class Template(directive.RMLDirective):
+    signature = ITemplate
+    factories = {
+        'pageTemplate': PageTemplate,
+        }
+
+    def process(self):
+        args = self.getAttributeValues()
+        args += self.parent.getAttributeValues(
+            select=('debug', 'compression', 'invariant'),
+            attrMapping={'debug': '_debug', 'compression': 'pageCompression'})
 
-        doc._firstPageTemplateIndex = story.getFirstPageTemplateIndex(doc)
-        doc.multiBuild(flowables)
+        self.parent.doc = platypus.BaseDocTemplate(
+            self.parent.outputFile, **dict(args))
+        self.processSubDirectives()
diff --git a/src/z3c/rml/tests/input/tag-color.rml b/src/z3c/rml/tests/input/tag-color.rml
new file mode 100644
index 0000000..27ab2ba
--- /dev/null
+++ b/src/z3c/rml/tests/input/tag-color.rml
@@ -0,0 +1,17 @@
+
+
+
+  
+    
+  
+  
+  
+    
+      This is my favorite color!
+    
+  
+
diff --git a/src/z3c/rml/tests/input/tag-image-1.rml b/src/z3c/rml/tests/input/tag-image-1.rml
index 5f913e9..184c13b 100644
--- a/src/z3c/rml/tests/input/tag-image-1.rml
+++ b/src/z3c/rml/tests/input/tag-image-1.rml
@@ -1,19 +1,22 @@
 
 
-
 
   
 
-    
 
-    
 
-    
 
-    
 
   
diff --git a/src/z3c/rml/tests/input/tag-image.rml b/src/z3c/rml/tests/input/tag-image.rml
index 51a1475..db27d55 100644
--- a/src/z3c/rml/tests/input/tag-image.rml
+++ b/src/z3c/rml/tests/input/tag-image.rml
@@ -1,16 +1,19 @@
 
 
-
-
+
   
 
-    
+    
 
-    
+    
 
-    
+    
 
-    
+    
 
   
 
diff --git a/src/z3c/rml/tests/input/tag-path.rml b/src/z3c/rml/tests/input/tag-path.rml
new file mode 100644
index 0000000..c4f7fb4
--- /dev/null
+++ b/src/z3c/rml/tests/input/tag-path.rml
@@ -0,0 +1,36 @@
+
+
+
+
+  
+
+    
+      6cm 26cm
+      8cm 23cm
+      4cm 23cm
+    
+
+    
+      14cm 26cm
+      16cm 23cm
+      12cm 23cm
+    
+
+    
+      4cm 22cm
+      
+        6cm 22cm
+      
+      7cm 20.5cm
+      
+        8cm 19cm
+      
+      6cm 19cm
+      
+        4cm 19cm
+      
+      3cm 20.5cm
+    
+
+  
+
diff --git a/src/z3c/rml/tests/input/tag-registerCidFont.rml b/src/z3c/rml/tests/input/tag-registerCidFont.rml
new file mode 100644
index 0000000..0f13b02
--- /dev/null
+++ b/src/z3c/rml/tests/input/tag-registerCidFont.rml
@@ -0,0 +1,17 @@
+
+
+
+  
+    
+  
+  
+  
+    
+      日本語は難しいですね!
+    
+  
+
diff --git a/src/z3c/rml/tests/input/tag-registerTTFont.rml b/src/z3c/rml/tests/input/tag-registerTTFont.rml
new file mode 100644
index 0000000..a4e99a1
--- /dev/null
+++ b/src/z3c/rml/tests/input/tag-registerTTFont.rml
@@ -0,0 +1,15 @@
+
+
+
+  
+    
+  
+  
+  
+    Hello World!
+  
+
diff --git a/src/z3c/rml/tests/input/tag-registerType1Face.rml b/src/z3c/rml/tests/input/tag-registerType1Face.rml
new file mode 100644
index 0000000..20ae34e
--- /dev/null
+++ b/src/z3c/rml/tests/input/tag-registerType1Face.rml
@@ -0,0 +1,19 @@
+
+
+
+  
+    
+    
+  
+  
+  
+    Hello World!
+  
+
diff --git a/src/z3c/rml/tests/input/tag-textAnnotation.rml b/src/z3c/rml/tests/input/tag-textAnnotation.rml
new file mode 100644
index 0000000..f5e3891
--- /dev/null
+++ b/src/z3c/rml/tests/input/tag-textAnnotation.rml
@@ -0,0 +1,15 @@
+
+
+
+
+  
+    
+      0,0,1,1
+      3
+      6
+X::PDF
+PX(S)
+MT(PINK)
+    
+  
+
diff --git a/src/z3c/rml/tests/test_rml.py b/src/z3c/rml/tests/test_rml.py
index 3bc5308..09d30d0 100644
--- a/src/z3c/rml/tests/test_rml.py
+++ b/src/z3c/rml/tests/test_rml.py
@@ -19,7 +19,7 @@
 import unittest
 import sys
 import z3c.rml.tests
-from z3c.rml import rml2pdf, attr
+from z3c.rml import rml2pdf, attrng
 
 class RMLRenderingTestCase(unittest.TestCase):
 
@@ -30,17 +30,17 @@ def __init__(self, inPath, outPath):
 
     def setUp(self):
         # Switch file opener for Image attibute
-        self._imageOpen = attr.Image.open
+        self._fileOpen = attrng.File.open
         def testOpen(img, filename):
             path = os.path.join(os.path.dirname(self._inPath), filename)
             return open(path)
-        attr.Image.open = testOpen
+        attrng.File.open = testOpen
         import z3c.rml.tests.module
         sys.modules['module'] = z3c.rml.tests.module
         sys.modules['mymodule'] = z3c.rml.tests.module
 
     def tearDown(self):
-        attr.Image.open = self._imageOpen
+        attrng.File.open = self._fileOpen
         del sys.modules['module']
         del sys.modules['mymodule']
 
@@ -58,7 +58,7 @@ def test_suite():
        inPath = os.path.join(inputDir, filename)
        outPath = os.path.join(outputDir, filename[:-4] + '.pdf')
        # Create new type, so that we can get test matching
-       TestCase = type(filename[:-4].title(), (RMLRenderingTestCase,), {})
+       TestCase = type(filename[:-4], (RMLRenderingTestCase,), {})
        case = TestCase(inPath, outPath)
        suite.addTest(case)
    return suite