libxml2dom

Changeset

360:ed3ba80d9cce
2012-01-29 Paul Boddie raw files shortlog changelog graph Improved/fixed transform attribute parsing and path data parsing. Added a test of transformations and paths.
libxml2dom/svg.py (file) tests/test_svg_basic.py (file) tests/test_svg_basic.xml (file)
     1.1 --- a/libxml2dom/svg.py	Sun Jan 29 00:35:51 2012 +0100
     1.2 +++ b/libxml2dom/svg.py	Sun Jan 29 00:36:39 2012 +0100
     1.3 @@ -5,7 +5,7 @@
     1.4  See: http://www.w3.org/TR/SVGMobile12/python-binding.html
     1.5  See: http://www.w3.org/TR/SVGMobile12/svgudom.html
     1.6  
     1.7 -Copyright (C) 2007 Paul Boddie <paul@boddie.org.uk>
     1.8 +Copyright (C) 2007, 2008, 2012 Paul Boddie <paul@boddie.org.uk>
     1.9  
    1.10  This program is free software; you can redistribute it and/or modify it under
    1.11  the terms of the GNU Lesser General Public License as published by the Free
    1.12 @@ -33,6 +33,13 @@
    1.13  
    1.14  SVG_NAMESPACE = "http://www.w3.org/2000/svg"
    1.15  
    1.16 +comma_wsp = re.compile("\s*,\s*|\s+")
    1.17 +
    1.18 +TYPE_MISMATCH_ERR = 17
    1.19 +
    1.20 +class TypeMismatchErr(xml.dom.DOMException):
    1.21 +    code = TYPE_MISMATCH_ERR
    1.22 +
    1.23  class _Exception(Exception):
    1.24  
    1.25      "A generic SVG exception."
    1.26 @@ -251,12 +258,9 @@
    1.27      See: http://www.w3.org/TR/SVGMobile12/svgudom.html#svg__SVGMatrix
    1.28      """
    1.29  
    1.30 -    translate_regexp = re.compile("translate\((.*)\)$")
    1.31 -    scale_regexp = re.compile("scale\((.*)\)$")
    1.32 -    rotate_regexp = re.compile("rotate\((.*)\)$")
    1.33 -    skewX_regexp = re.compile("skewX\((.*)\)$")
    1.34 -    skewY_regexp = re.compile("skewY\((.*)\)$")
    1.35 -    matrix_regexp = re.compile("matrix\((.*)\)$")
    1.36 +    transform_regexp = re.compile(
    1.37 +        "(translate|scale|rotate|skewX|skewY|matrix)\((.*?)\)"
    1.38 +        )
    1.39  
    1.40      def __init__(self, a=0, b=0, c=0, d=0, e=0, f=0):
    1.41          self.matrix = a, b, c, d, e, f
    1.42 @@ -271,7 +275,7 @@
    1.43          return not (self == other)
    1.44  
    1.45      def _get_params(self, param_string):
    1.46 -        return map(float, map(lambda s: s.strip(), param_string.split(",")))
    1.47 +        return [float(s.strip()) for s in comma_wsp.split(param_string.strip())]
    1.48  
    1.49      def fromNode(self, node, name):
    1.50  
    1.51 @@ -282,68 +286,78 @@
    1.52  
    1.53          value = node.getAttribute(name)
    1.54          if value is None:
    1.55 -            raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR)
    1.56 +            raise xml.dom.NotSupportedErr()
    1.57 +
    1.58 +        last = 0
    1.59 +
    1.60 +        matrix = 1, 0, 0, 1, 0, 0
    1.61  
    1.62 -        value = value.strip()
    1.63 +        for m in self.transform_regexp.finditer(value):
    1.64 +            start, end = m.span()
    1.65  
    1.66 -        # Translation.
    1.67 +            # Make sure that nothing significant was found between the last
    1.68 +            # match and this one.
    1.69 +
    1.70 +            if value[last:start].strip():
    1.71 +                raise TypeMismatchErr()
    1.72  
    1.73 -        m = self.translate_regexp.match(value)
    1.74 -        if m:
    1.75 -            a, b, c, d = 1, 0, 0, 1
    1.76 -            e, f = self._get_params(m.group(1))
    1.77 -            self.matrix = a, b, c, d, e, f
    1.78 -            return
    1.79 +            last = end
    1.80 +            transform, arguments = m.groups()
    1.81  
    1.82 -        # Scaling.
    1.83 +            # Translation.
    1.84  
    1.85 -        m = self.scale_regexp.match(value)
    1.86 -        if m:
    1.87 -            b, c, e, f = 0, 0, 0, 0
    1.88 -            a, d = self._get_params(m.group(1))
    1.89 -            self.matrix = a, b, c, d, e, f
    1.90 -            return
    1.91 +            if transform == "translate":
    1.92 +                a, b, c, d = 1, 0, 0, 1
    1.93 +                e, f = self._get_params(arguments)
    1.94 +
    1.95 +            # Scaling.
    1.96  
    1.97 -        # Rotation.
    1.98 +            elif transform == "scale":
    1.99 +                b, c, e, f = 0, 0, 0, 0
   1.100 +                a, d = self._get_params(arguments)
   1.101 +
   1.102 +            # Rotation.
   1.103  
   1.104 -        m = self.rotate_regexp.match(value)
   1.105 -        if m:
   1.106 -            e, f = 0, 0
   1.107 -            angle = float(m.group(1).strip())
   1.108 -            a = d = math.cos(math.radians(angle))
   1.109 -            b = math.sin(math.radians(angle))
   1.110 -            c = -b
   1.111 -            self.matrix = a, b, c, d, e, f
   1.112 -            return
   1.113 +            elif transform == "rotate":
   1.114 +                e, f = 0, 0
   1.115 +                angle = float(arguments.strip())
   1.116 +                a = d = math.cos(math.radians(angle))
   1.117 +                b = math.sin(math.radians(angle))
   1.118 +                c = -b
   1.119 +
   1.120 +            # Skew.
   1.121  
   1.122 -        # Skew.
   1.123 +            elif transform == "skewX":
   1.124 +                a, b, d, e, f = 1, 0, 1, 0, 0
   1.125 +                angle = float(arguments.strip())
   1.126 +                c = math.tan(math.radians(angle))
   1.127 +
   1.128 +            elif transform == "skewY":
   1.129 +                a, c, d, e, f = 1, 0, 1, 0, 0
   1.130 +                angle = float(arguments.strip())
   1.131 +                b = math.tan(math.radians(angle))
   1.132  
   1.133 -        m = self.skewX_regexp.match(value)
   1.134 -        if m:
   1.135 -            a, b, d, e, f = 1, 0, 1, 0, 0
   1.136 -            angle = float(m.group(1).strip())
   1.137 -            c = math.tan(math.radians(angle))
   1.138 -            self.matrix = a, b, c, d, e, f
   1.139 -            return
   1.140 +            # Generic.
   1.141 +
   1.142 +            elif transform == "matrix":
   1.143 +                a, b, c, d, e, f = self._get_params(arguments)
   1.144 +
   1.145 +            else:
   1.146 +                raise TypeMismatchErr()
   1.147 +
   1.148 +            # Combine the existing matrix with the new one.
   1.149  
   1.150 -        m = self.skewY_regexp.match(value)
   1.151 -        if m:
   1.152 -            a, c, d, e, f = 1, 0, 1, 0, 0
   1.153 -            angle = float(m.group(1).strip())
   1.154 -            b = math.tan(math.radians(angle))
   1.155 -            self.matrix = a, b, c, d, e, f
   1.156 -            return
   1.157 +            matrix = self._multiply(matrix, (a, b, c, d, e, f))
   1.158 +
   1.159 +        else:
   1.160 +            # Make sure that nothing significant was found after the final
   1.161 +            # match.
   1.162  
   1.163 -        # Generic.
   1.164 +            if value[last:].strip():
   1.165 +                raise TypeMismatchErr()
   1.166  
   1.167 -        m = self.matrix_regexp.match(value)
   1.168 -        if m:
   1.169 -            self.matrix = self._get_params(m.group(1))
   1.170 -            return
   1.171 -
   1.172 -        # Otherwise, complain.
   1.173 -
   1.174 -        raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR)
   1.175 +        if last != 0:
   1.176 +            self.matrix = matrix
   1.177  
   1.178      def toNode(self, node, name):
   1.179  
   1.180 @@ -397,7 +411,12 @@
   1.181          try:
   1.182              return self.matrix[index]
   1.183          except IndexError:
   1.184 -            raise xml.dom.DOMException(xml.dom.INDEX_SIZE_ERR)
   1.185 +            raise xml.dom.IndexSizeErr()
   1.186 +
   1.187 +    def _multiply(self, matrix1, matrix2):
   1.188 +        a1, b1, c1, d1, e1, f1 = matrix1
   1.189 +        a2, b2, c2, d2, e2, f2 = matrix2
   1.190 +        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
   1.191  
   1.192      def mMultiply(self, secondMatrix):
   1.193  
   1.194 @@ -412,9 +431,7 @@
   1.195          Return this object as a result.
   1.196          """
   1.197  
   1.198 -        a, b, c, d, e, f = self.matrix
   1.199 -        A, B, C, D, E, F = secondMatrix.matrix
   1.200 -        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
   1.201 +        self.matrix = self._multiply(secondMatrix.matrix, self.matrix)
   1.202          return self
   1.203  
   1.204      def inverse(self):
   1.205 @@ -480,20 +497,29 @@
   1.206      See: http://www.w3.org/TR/SVGMobile12/paths.html
   1.207      """
   1.208  
   1.209 -    MOVE_TO = 77
   1.210 -    LINE_TO = 76
   1.211 -    CURVE_TO = 67
   1.212 -    QUAD_TO = 81
   1.213 -    CLOSE = 90
   1.214 -    _CLOSE = 122 # More baggage (name not standard).
   1.215 +    path_regexp = re.compile("([mMzZlLhHvVcCsSqQtTaA])\s*([^mMzZlLhHvVcCsSqQtTaA]*)")
   1.216 +
   1.217 +    # NOTE: Upper case involves absolute coordinates; lower case involves
   1.218 +    # NOTE: relative coordinates. The IDL constants only seem to represent the
   1.219 +    # NOTE: former commands.
   1.220 +
   1.221 +    MOVE_TO = ord("M")
   1.222 +    LINE_TO = ord("L")
   1.223 +    CURVE_TO = ord("C")
   1.224 +    QUAD_TO = ord("Q")
   1.225 +    CLOSE = ord("Z")
   1.226  
   1.227      nparams = {
   1.228 -        MOVE_TO : 2,
   1.229 -        LINE_TO : 2,
   1.230 -        CURVE_TO : 6,
   1.231 -        QUAD_TO : 4,
   1.232 -        CLOSE : 0,
   1.233 -        _CLOSE : 0
   1.234 +        "A" : 7,
   1.235 +        "C" : 6,
   1.236 +        "H" : 1,
   1.237 +        "L" : 2,
   1.238 +        "M" : 2,
   1.239 +        "Q" : 4,
   1.240 +        "S" : 4,
   1.241 +        "T" : 2,
   1.242 +        "V" : 1,
   1.243 +        "Z" : 0,
   1.244          }
   1.245  
   1.246      def __init__(self):
   1.247 @@ -514,25 +540,43 @@
   1.248  
   1.249          value = node.getAttribute(name)
   1.250          if value is None:
   1.251 -            raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR)
   1.252 +            raise xml.dom.NotSupportedErr()
   1.253  
   1.254          # Try and unpack the attribute value.
   1.255 +        # NOTE: This does not yet satisfy the permissive parsing behaviour
   1.256 +        # NOTE: described in the specification.
   1.257 +        # See: http://www.w3.org/TR/SVG11/paths.html#PathDataBNF
   1.258  
   1.259 -        data = value.split()
   1.260          self.segments = []
   1.261 -        try:
   1.262 -            i = 0
   1.263 -            while i < len(data):
   1.264 -                cmd = ord(data[i])
   1.265 -                if cmd == self._CLOSE:
   1.266 -                    cmd = self.CLOSE
   1.267 -                i += 1
   1.268 -                n = self.nparams[cmd]
   1.269 -                params = map(float, data[i:i+n])
   1.270 +        last = 0
   1.271 +
   1.272 +        for m in self.path_regexp.finditer(value):
   1.273 +            start, end = m.span()
   1.274 +            if value[last:start].strip():
   1.275 +                raise TypeMismatchErr()
   1.276 +
   1.277 +            last = end
   1.278 +            cmd, arguments = m.groups()
   1.279 +
   1.280 +            try:
   1.281 +                n = self.nparams[cmd.upper()]
   1.282 +
   1.283 +                if arguments.strip():
   1.284 +                    params = [float(s.strip()) for s in comma_wsp.split(arguments.strip())]
   1.285 +                else:
   1.286 +                    params = []
   1.287 +
   1.288 +                if n != 0 and len(params) % n != 0:
   1.289 +                    raise TypeMismatchErr(cmd, arguments)
   1.290 +
   1.291                  self.segments.append((cmd, params))
   1.292 -                i += n
   1.293 -        except (IndexError, ValueError):
   1.294 -            raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR)
   1.295 +
   1.296 +            except (IndexError, ValueError):
   1.297 +                raise TypeMismatchErr(cmd, arguments)
   1.298 +
   1.299 +        else:
   1.300 +            if value[last:start].strip():
   1.301 +                raise TypeMismatchErr()
   1.302  
   1.303      def toNode(self, node, name):
   1.304  
   1.305 @@ -549,7 +593,7 @@
   1.306                      l.append(str(param))
   1.307              node.setAttribute(name, " ".join(l))
   1.308          except (IndexError, ValueError):
   1.309 -            raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR)
   1.310 +            raise TypeMismatchErr()
   1.311  
   1.312      # Interface methods.
   1.313  
   1.314 @@ -560,30 +604,30 @@
   1.315  
   1.316      def getSegment(self, cmdIndex):
   1.317          try:
   1.318 -            return self.segments[cmdIndex][0]
   1.319 +            return ord(self.segments[cmdIndex][0])
   1.320          except IndexError:
   1.321 -            raise xml.dom.DOMException(xml.dom.INDEX_SIZE_ERR)
   1.322 +            raise xml.dom.IndexSizeErr()
   1.323  
   1.324      def getSegmentParam(self, cmdIndex, paramIndex):
   1.325          try:
   1.326              return self.segments[cmdIndex][1][paramIndex]
   1.327          except IndexError:
   1.328 -            raise xml.dom.DOMException(xml.dom.INDEX_SIZE_ERR)
   1.329 +            raise xml.dom.IndexSizeErr()
   1.330  
   1.331      def moveTo(self, x, y):
   1.332 -        self.segments.append((self.MOVE_TO, (x, y)))
   1.333 +        self.segments.append(("M", (x, y)))
   1.334  
   1.335      def lineTo(self, x, y):
   1.336 -        self.segments.append((self.LINE_TO, (x, y)))
   1.337 +        self.segments.append(("L", (x, y)))
   1.338  
   1.339      def quadTo(self, x1, y1, x2, y2):
   1.340 -        self.segments.append((self.QUAD_TO, (x1, y1, x2, y2)))
   1.341 +        self.segments.append(("Q", (x1, y1, x2, y2)))
   1.342  
   1.343      def curveTo(self, x1, y1, x2, y2, x3, y3):
   1.344 -        self.segments.append((self.CURVE_TO, (x1, y1, x2, y2, x3, y3)))
   1.345 +        self.segments.append(("C", (x1, y1, x2, y2, x3, y3)))
   1.346  
   1.347      def close(self):
   1.348 -        self.segments.append((self.CLOSE,))
   1.349 +        self.segments.append(("Z",))
   1.350  
   1.351  class SVGPoint:
   1.352  
   1.353 @@ -618,12 +662,12 @@
   1.354  
   1.355          value = node.getAttribute(name)
   1.356          if value is None:
   1.357 -            raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR)
   1.358 +            raise xml.dom.NotSupportedErr()
   1.359          try:
   1.360              values = map(float, value.split())
   1.361              self.x, self.y, self.width, self.height = values
   1.362          except (IndexError, ValueError):
   1.363 -            raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR)
   1.364 +            raise TypeMismatchErr()
   1.365  
   1.366      def toNode(self, node, name):
   1.367  
   1.368 @@ -636,7 +680,7 @@
   1.369              values = map(str, [self.x, self.y, self.width, self.height])
   1.370              node.setAttribute(name, " ".join(values))
   1.371          except (IndexError, ValueError):
   1.372 -            raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR)
   1.373 +            raise TypeMismatchErr()
   1.374  
   1.375      def __repr__(self):
   1.376          return "SVGRect(%f, %f, %f, %f)" % (self.x, self.y, self.width, self.height)
   1.377 @@ -679,14 +723,14 @@
   1.378  
   1.379      def getMatrixTrait(self, name):
   1.380          if name != "transform":
   1.381 -            raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR)
   1.382 +            raise xml.dom.NotSupportedErr()
   1.383          matrix = SVGMatrix()
   1.384          matrix.fromNode(self, name)
   1.385          return matrix
   1.386  
   1.387      def setMatrixTrait(self, name, matrix):
   1.388          if name != "transform":
   1.389 -            raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR)
   1.390 +            raise xml.dom.NotSupportedErr()
   1.391          matrix.toNode(self, name) 
   1.392  
   1.393  # Node classes.
   1.394 @@ -806,7 +850,7 @@
   1.395  
   1.396      def _setCurrentScale(self, scale):
   1.397          if scale == 0:
   1.398 -            raise xml.dom.DOMException(xml.dom.INVALID_ACCESS_ERR)
   1.399 +            raise xml.dom.InvalidAccessErr()
   1.400          self.scale = scale
   1.401  
   1.402      def _setCurrentRotate(self, rotate):
   1.403 @@ -815,9 +859,11 @@
   1.404      def _viewport(self):
   1.405          if self.hasAttribute("viewBox"):
   1.406              return self.getRectTrait("viewBox")
   1.407 -        else:
   1.408 +        elif self.hasAttribute("width") and self.hasAttribute("height"):
   1.409              return SVGRect(0, 0, self._convertMeasurement(self.getAttribute("width")),
   1.410                  self._convertMeasurement(self.getAttribute("height")))
   1.411 +        else:
   1.412 +            return None
   1.413  
   1.414      # Utility methods.
   1.415  
   1.416 @@ -830,7 +876,7 @@
   1.417                  # NOTE: No conversion yet!
   1.418                  return float(value[:-len(unit)].strip())
   1.419  
   1.420 -        raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR)
   1.421 +        raise TypeMismatchErr()
   1.422  
   1.423      # Standard methods.
   1.424  
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/tests/test_svg_basic.py	Sun Jan 29 00:36:39 2012 +0100
     2.3 @@ -0,0 +1,16 @@
     2.4 +#!/usr/bin/env python
     2.5 +
     2.6 +import libxml2dom.svg
     2.7 +
     2.8 +d = libxml2dom.svg.parse("tests/test_svg_basic.xml")
     2.9 +g = d.xpath("//svg:g")[0]
    2.10 +
    2.11 +matrix = g.getMatrixTrait("transform")
    2.12 +print matrix
    2.13 +
    2.14 +p = g.xpath("svg:path")[0]
    2.15 +
    2.16 +path = p.getPathTrait("d")
    2.17 +print path.segments
    2.18 +
    2.19 +# vim: tabstop=4 expandtab shiftwidth=4
     3.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     3.2 +++ b/tests/test_svg_basic.xml	Sun Jan 29 00:36:39 2012 +0100
     3.3 @@ -0,0 +1,8 @@
     3.4 +<?xml version="1.0" encoding="iso-8859-1"?>
     3.5 +<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
     3.6 +    width="800px" height="600px" viewBox="0 0 800 600">
     3.7 +<g transform="translate(400 300) scale(200 150)">
     3.8 +  <rect x="-2" y="-2" width="4" height="4" fill="#000"/>
     3.9 +  <path d="M -1,-1 L 1,-1 L 1,1 L -1,1 z" stroke="#fff" stroke-width="0.005"/>
    3.10 +</g>
    3.11 +</svg>