# HG changeset patch # User Paul Boddie # Date 1327793799 -3600 # Node ID ed3ba80d9cce28767c74120b72fbfa55cc7b2c61 # Parent 2f30ff7df51f7fe3f416eaa2c7d60b839958f672 Improved/fixed transform attribute parsing and path data parsing. Added a test of transformations and paths. diff -r 2f30ff7df51f -r ed3ba80d9cce libxml2dom/svg.py --- a/libxml2dom/svg.py Sun Jan 29 00:35:51 2012 +0100 +++ b/libxml2dom/svg.py Sun Jan 29 00:36:39 2012 +0100 @@ -5,7 +5,7 @@ See: http://www.w3.org/TR/SVGMobile12/python-binding.html See: http://www.w3.org/TR/SVGMobile12/svgudom.html -Copyright (C) 2007 Paul Boddie +Copyright (C) 2007, 2008, 2012 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free @@ -33,6 +33,13 @@ SVG_NAMESPACE = "http://www.w3.org/2000/svg" +comma_wsp = re.compile("\s*,\s*|\s+") + +TYPE_MISMATCH_ERR = 17 + +class TypeMismatchErr(xml.dom.DOMException): + code = TYPE_MISMATCH_ERR + class _Exception(Exception): "A generic SVG exception." @@ -251,12 +258,9 @@ See: http://www.w3.org/TR/SVGMobile12/svgudom.html#svg__SVGMatrix """ - translate_regexp = re.compile("translate\((.*)\)$") - scale_regexp = re.compile("scale\((.*)\)$") - rotate_regexp = re.compile("rotate\((.*)\)$") - skewX_regexp = re.compile("skewX\((.*)\)$") - skewY_regexp = re.compile("skewY\((.*)\)$") - matrix_regexp = re.compile("matrix\((.*)\)$") + transform_regexp = re.compile( + "(translate|scale|rotate|skewX|skewY|matrix)\((.*?)\)" + ) def __init__(self, a=0, b=0, c=0, d=0, e=0, f=0): self.matrix = a, b, c, d, e, f @@ -271,7 +275,7 @@ return not (self == other) def _get_params(self, param_string): - return map(float, map(lambda s: s.strip(), param_string.split(","))) + return [float(s.strip()) for s in comma_wsp.split(param_string.strip())] def fromNode(self, node, name): @@ -282,68 +286,78 @@ value = node.getAttribute(name) if value is None: - raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR) + raise xml.dom.NotSupportedErr() + + last = 0 + + matrix = 1, 0, 0, 1, 0, 0 - value = value.strip() + for m in self.transform_regexp.finditer(value): + start, end = m.span() - # Translation. + # Make sure that nothing significant was found between the last + # match and this one. + + if value[last:start].strip(): + raise TypeMismatchErr() - m = self.translate_regexp.match(value) - if m: - a, b, c, d = 1, 0, 0, 1 - e, f = self._get_params(m.group(1)) - self.matrix = a, b, c, d, e, f - return + last = end + transform, arguments = m.groups() - # Scaling. + # Translation. - m = self.scale_regexp.match(value) - if m: - b, c, e, f = 0, 0, 0, 0 - a, d = self._get_params(m.group(1)) - self.matrix = a, b, c, d, e, f - return + if transform == "translate": + a, b, c, d = 1, 0, 0, 1 + e, f = self._get_params(arguments) + + # Scaling. - # Rotation. + elif transform == "scale": + b, c, e, f = 0, 0, 0, 0 + a, d = self._get_params(arguments) + + # Rotation. - m = self.rotate_regexp.match(value) - if m: - e, f = 0, 0 - angle = float(m.group(1).strip()) - a = d = math.cos(math.radians(angle)) - b = math.sin(math.radians(angle)) - c = -b - self.matrix = a, b, c, d, e, f - return + elif transform == "rotate": + e, f = 0, 0 + angle = float(arguments.strip()) + a = d = math.cos(math.radians(angle)) + b = math.sin(math.radians(angle)) + c = -b + + # Skew. - # Skew. + elif transform == "skewX": + a, b, d, e, f = 1, 0, 1, 0, 0 + angle = float(arguments.strip()) + c = math.tan(math.radians(angle)) + + elif transform == "skewY": + a, c, d, e, f = 1, 0, 1, 0, 0 + angle = float(arguments.strip()) + b = math.tan(math.radians(angle)) - m = self.skewX_regexp.match(value) - if m: - a, b, d, e, f = 1, 0, 1, 0, 0 - angle = float(m.group(1).strip()) - c = math.tan(math.radians(angle)) - self.matrix = a, b, c, d, e, f - return + # Generic. + + elif transform == "matrix": + a, b, c, d, e, f = self._get_params(arguments) + + else: + raise TypeMismatchErr() + + # Combine the existing matrix with the new one. - m = self.skewY_regexp.match(value) - if m: - a, c, d, e, f = 1, 0, 1, 0, 0 - angle = float(m.group(1).strip()) - b = math.tan(math.radians(angle)) - self.matrix = a, b, c, d, e, f - return + matrix = self._multiply(matrix, (a, b, c, d, e, f)) + + else: + # Make sure that nothing significant was found after the final + # match. - # Generic. + if value[last:].strip(): + raise TypeMismatchErr() - m = self.matrix_regexp.match(value) - if m: - self.matrix = self._get_params(m.group(1)) - return - - # Otherwise, complain. - - raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR) + if last != 0: + self.matrix = matrix def toNode(self, node, name): @@ -397,7 +411,12 @@ try: return self.matrix[index] except IndexError: - raise xml.dom.DOMException(xml.dom.INDEX_SIZE_ERR) + raise xml.dom.IndexSizeErr() + + def _multiply(self, matrix1, matrix2): + a1, b1, c1, d1, e1, f1 = matrix1 + a2, b2, c2, d2, e2, f2 = matrix2 + return a1*a2 + c1*b2, b1*a2 + d1*b2, a1*c2 + c1*d2, b1*c2 + d1*d2, a1*e2 + c1*f2 + e1, b1*e2 + d1*f2 + f1 def mMultiply(self, secondMatrix): @@ -412,9 +431,7 @@ Return this object as a result. """ - a, b, c, d, e, f = self.matrix - A, B, C, D, E, F = secondMatrix.matrix - self.matrix = A*a + C*b, B*a + D*b, A*c + C*d, B*c + D*d, A*e + C*f + E, B*e + D*f + F + self.matrix = self._multiply(secondMatrix.matrix, self.matrix) return self def inverse(self): @@ -480,20 +497,29 @@ See: http://www.w3.org/TR/SVGMobile12/paths.html """ - MOVE_TO = 77 - LINE_TO = 76 - CURVE_TO = 67 - QUAD_TO = 81 - CLOSE = 90 - _CLOSE = 122 # More baggage (name not standard). + path_regexp = re.compile("([mMzZlLhHvVcCsSqQtTaA])\s*([^mMzZlLhHvVcCsSqQtTaA]*)") + + # NOTE: Upper case involves absolute coordinates; lower case involves + # NOTE: relative coordinates. The IDL constants only seem to represent the + # NOTE: former commands. + + MOVE_TO = ord("M") + LINE_TO = ord("L") + CURVE_TO = ord("C") + QUAD_TO = ord("Q") + CLOSE = ord("Z") nparams = { - MOVE_TO : 2, - LINE_TO : 2, - CURVE_TO : 6, - QUAD_TO : 4, - CLOSE : 0, - _CLOSE : 0 + "A" : 7, + "C" : 6, + "H" : 1, + "L" : 2, + "M" : 2, + "Q" : 4, + "S" : 4, + "T" : 2, + "V" : 1, + "Z" : 0, } def __init__(self): @@ -514,25 +540,43 @@ value = node.getAttribute(name) if value is None: - raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR) + raise xml.dom.NotSupportedErr() # Try and unpack the attribute value. + # NOTE: This does not yet satisfy the permissive parsing behaviour + # NOTE: described in the specification. + # See: http://www.w3.org/TR/SVG11/paths.html#PathDataBNF - data = value.split() self.segments = [] - try: - i = 0 - while i < len(data): - cmd = ord(data[i]) - if cmd == self._CLOSE: - cmd = self.CLOSE - i += 1 - n = self.nparams[cmd] - params = map(float, data[i:i+n]) + last = 0 + + for m in self.path_regexp.finditer(value): + start, end = m.span() + if value[last:start].strip(): + raise TypeMismatchErr() + + last = end + cmd, arguments = m.groups() + + try: + n = self.nparams[cmd.upper()] + + if arguments.strip(): + params = [float(s.strip()) for s in comma_wsp.split(arguments.strip())] + else: + params = [] + + if n != 0 and len(params) % n != 0: + raise TypeMismatchErr(cmd, arguments) + self.segments.append((cmd, params)) - i += n - except (IndexError, ValueError): - raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR) + + except (IndexError, ValueError): + raise TypeMismatchErr(cmd, arguments) + + else: + if value[last:start].strip(): + raise TypeMismatchErr() def toNode(self, node, name): @@ -549,7 +593,7 @@ l.append(str(param)) node.setAttribute(name, " ".join(l)) except (IndexError, ValueError): - raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR) + raise TypeMismatchErr() # Interface methods. @@ -560,30 +604,30 @@ def getSegment(self, cmdIndex): try: - return self.segments[cmdIndex][0] + return ord(self.segments[cmdIndex][0]) except IndexError: - raise xml.dom.DOMException(xml.dom.INDEX_SIZE_ERR) + raise xml.dom.IndexSizeErr() def getSegmentParam(self, cmdIndex, paramIndex): try: return self.segments[cmdIndex][1][paramIndex] except IndexError: - raise xml.dom.DOMException(xml.dom.INDEX_SIZE_ERR) + raise xml.dom.IndexSizeErr() def moveTo(self, x, y): - self.segments.append((self.MOVE_TO, (x, y))) + self.segments.append(("M", (x, y))) def lineTo(self, x, y): - self.segments.append((self.LINE_TO, (x, y))) + self.segments.append(("L", (x, y))) def quadTo(self, x1, y1, x2, y2): - self.segments.append((self.QUAD_TO, (x1, y1, x2, y2))) + self.segments.append(("Q", (x1, y1, x2, y2))) def curveTo(self, x1, y1, x2, y2, x3, y3): - self.segments.append((self.CURVE_TO, (x1, y1, x2, y2, x3, y3))) + self.segments.append(("C", (x1, y1, x2, y2, x3, y3))) def close(self): - self.segments.append((self.CLOSE,)) + self.segments.append(("Z",)) class SVGPoint: @@ -618,12 +662,12 @@ value = node.getAttribute(name) if value is None: - raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR) + raise xml.dom.NotSupportedErr() try: values = map(float, value.split()) self.x, self.y, self.width, self.height = values except (IndexError, ValueError): - raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR) + raise TypeMismatchErr() def toNode(self, node, name): @@ -636,7 +680,7 @@ values = map(str, [self.x, self.y, self.width, self.height]) node.setAttribute(name, " ".join(values)) except (IndexError, ValueError): - raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR) + raise TypeMismatchErr() def __repr__(self): return "SVGRect(%f, %f, %f, %f)" % (self.x, self.y, self.width, self.height) @@ -679,14 +723,14 @@ def getMatrixTrait(self, name): if name != "transform": - raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR) + raise xml.dom.NotSupportedErr() matrix = SVGMatrix() matrix.fromNode(self, name) return matrix def setMatrixTrait(self, name, matrix): if name != "transform": - raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR) + raise xml.dom.NotSupportedErr() matrix.toNode(self, name) # Node classes. @@ -806,7 +850,7 @@ def _setCurrentScale(self, scale): if scale == 0: - raise xml.dom.DOMException(xml.dom.INVALID_ACCESS_ERR) + raise xml.dom.InvalidAccessErr() self.scale = scale def _setCurrentRotate(self, rotate): @@ -815,9 +859,11 @@ def _viewport(self): if self.hasAttribute("viewBox"): return self.getRectTrait("viewBox") - else: + elif self.hasAttribute("width") and self.hasAttribute("height"): return SVGRect(0, 0, self._convertMeasurement(self.getAttribute("width")), self._convertMeasurement(self.getAttribute("height"))) + else: + return None # Utility methods. @@ -830,7 +876,7 @@ # NOTE: No conversion yet! return float(value[:-len(unit)].strip()) - raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR) + raise TypeMismatchErr() # Standard methods. diff -r 2f30ff7df51f -r ed3ba80d9cce tests/test_svg_basic.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_svg_basic.py Sun Jan 29 00:36:39 2012 +0100 @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +import libxml2dom.svg + +d = libxml2dom.svg.parse("tests/test_svg_basic.xml") +g = d.xpath("//svg:g")[0] + +matrix = g.getMatrixTrait("transform") +print matrix + +p = g.xpath("svg:path")[0] + +path = p.getPathTrait("d") +print path.segments + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 2f30ff7df51f -r ed3ba80d9cce tests/test_svg_basic.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_svg_basic.xml Sun Jan 29 00:36:39 2012 +0100 @@ -0,0 +1,8 @@ + + + + + + +