1 #!/usr/bin/env python 2 3 """ 4 SVG-specific document support. 5 See: http://www.w3.org/TR/SVGMobile12/python-binding.html 6 See: http://www.w3.org/TR/SVGMobile12/svgudom.html 7 8 Copyright (C) 2007 Paul Boddie <paul@boddie.org.uk> 9 10 This library is free software; you can redistribute it and/or 11 modify it under the terms of the GNU Lesser General Public 12 License as published by the Free Software Foundation; either 13 version 2.1 of the License, or (at your option) any later version. 14 15 This library is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 18 Lesser General Public License for more details. 19 20 You should have received a copy of the GNU Lesser General Public 21 License along with this library; if not, write to the Free Software 22 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 23 """ 24 25 import libxml2dom 26 from libxml2dom.events import * 27 from libxml2dom.macrolib import * 28 from libxml2dom.macrolib import \ 29 createDocument as Node_createDocument 30 import xml.dom 31 import math 32 import re 33 34 SVG_NAMESPACE = "http://www.w3.org/2000/svg" 35 36 class _Exception(Exception): 37 38 "A generic SVG exception." 39 40 def __init__(self, code): 41 Exception.__init__(self, code) 42 self.code = code 43 44 class SVGException(_Exception): 45 46 "An SVG exception." 47 48 SVG_WRONG_TYPE_ERR = 0 49 SVG_INVALID_VALUE_ERR = 1 50 SVG_MATRIX_NOT_INVERTABLE = 2 51 52 class GlobalException(_Exception): 53 54 "A global exception." 55 56 NOT_CONNECTED_ERR = 1 57 ENCODING_ERR = 2 58 DENIED_ERR = 3 59 UNKNOWN_ERR = 4 60 61 class SVGImplementation(libxml2dom.Implementation): 62 63 "Contains an SVG-specific implementation." 64 65 # Wrapping of documents. 66 67 def adoptDocument(self, node): 68 return SVGDocument(node, self) 69 70 # Factory functions. 71 72 def get_node(self, _node, context_node): 73 if Node_nodeType(_node) == context_node.ELEMENT_NODE and \ 74 Node_namespaceURI(_node) == SVG_NAMESPACE: 75 76 if Node_localName(_node) == "svg": 77 return SVGSVGElement(_node, self, context_node.ownerDocument) 78 else: 79 return SVGElement(_node, self, context_node.ownerDocument) 80 else: 81 return libxml2dom.Implementation.get_node(self, _node, context_node) 82 83 # Convenience functions. 84 85 def createSVGDocument(self): 86 87 "Create a new SVG document." 88 89 return SVGDocument(Node_createDocument(SVG_NAMESPACE, "svg", None), self) 90 91 # Interfaces and helper classes. 92 93 class AsyncStatusCallback: 94 95 "An asynchronous callback interface." 96 97 def operationComplete(self, status): 98 pass 99 100 class AsyncURLStatus: 101 102 "The status of a URL retrieval operation." 103 104 def __init__(self, success, contentType, content): 105 self.success, self.contentType, self.content = success, contentType, content 106 107 class ElementTraversal: 108 109 "An interface for element traversal." 110 111 def _firstElementChild(self): 112 l = self.xpath("*") 113 if l: 114 return l[0] 115 else: 116 return None 117 118 def _lastElementChild(self): 119 l = self.xpath("*") 120 if l: 121 return l[-1] 122 else: 123 return None 124 125 def _nextElementSibling(self): 126 l = self.xpath("following-sibling::*") 127 if l: 128 return l[0] 129 else: 130 return None 131 132 def _previousElementSibling(self): 133 l = self.xpath("preceding-sibling::*") 134 if l: 135 return l[0] 136 else: 137 return None 138 139 firstElementChild = property(_firstElementChild) 140 lastElementChild = property(_lastElementChild) 141 nextElementSibling = property(_nextElementSibling) 142 previousElementSibling = property(_previousElementSibling) 143 144 class SVGGlobal: # Global, EventListenerInitializer2 145 146 "An SVG global." 147 148 def __init__(self, document): # parent 149 150 "Initialise the global with the given 'document'." 151 152 self.document = document 153 154 def createConnection(self): 155 raise NotImplementedError, "createConnection" 156 157 def createTimer(self, initialInterval, repeatInterval): 158 raise NotImplementedError, "createTimer" 159 160 def gotoLocation(self, newIRI): 161 raise NotImplementedError, "gotoLocation" 162 163 def binaryToString(self, octets, encoding): 164 raise NotImplementedError, "binaryToString" 165 166 def stringToBinary(self, data, encoding): 167 raise NotImplementedError, "stringToBinary" 168 169 def getURL(self, iri, callback): 170 171 # NOTE: Not asynchronous. 172 # NOTE: The urlopen function may not support IRIs. 173 # No exceptions are supposed to be raised, which is a bit nasty. 174 175 f = urllib.urlopen(iri) 176 try: 177 try: 178 content = f.read() 179 contentType = f.headers["Content-Type"] 180 callback.operationComplete(AsyncURLStatus(1, contentType, content)) 181 except: 182 callback.operationComplete(AsyncURLStatus(0, None, None)) 183 finally: 184 f.close() 185 186 def postURL(self, iri, data, callback, type, encoding): 187 raise NotImplementedError, "postURL" 188 189 def parseXML(self, data, contextDoc): 190 doc = parseString(data) 191 return contextDoc.importNode(doc.documentElement, 1) 192 193 class SVGLocatable: 194 195 "A locatable interface." 196 197 pass 198 199 class SVGMatrix: 200 201 """ 202 A matrix. 203 See: http://www.w3.org/TR/SVGMobile12/svgudom.html#svg__SVGMatrix 204 """ 205 206 translate_regexp = re.compile("translate\((.*)\)$") 207 scale_regexp = re.compile("scale\((.*)\)$") 208 rotate_regexp = re.compile("rotate\((.*)\)$") 209 skewX_regexp = re.compile("skewX\((.*)\)$") 210 skewY_regexp = re.compile("skewY\((.*)\)$") 211 matrix_regexp = re.compile("matrix\((.*)\)$") 212 213 def __init__(self, a=0, b=0, c=0, d=0, e=0, f=0): 214 self.matrix = a, b, c, d, e, f 215 216 def __eq__(self, other): 217 return self.matrix == other.matrix 218 219 def __ne__(self, other): 220 return not (self == other) 221 222 def _get_params(self, param_string): 223 return map(float, map(lambda s: s.strip(), param_string.split(","))) 224 225 def fromNode(self, node, name): 226 227 """ 228 Initialise this object from the trait on the 'node' having the given 229 'name'. 230 """ 231 232 value = node.getAttribute(name) 233 if value is None: 234 raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR) 235 236 value = value.strip() 237 238 # Translation. 239 240 m = self.translate_regexp.match(value) 241 if m: 242 a, b, c, d = 1, 0, 0, 1 243 e, f = self._get_params(m.group(1)) 244 self.matrix = a, b, c, d, e, f 245 return 246 247 # Scaling. 248 249 m = self.scale_regexp.match(value) 250 if m: 251 b, c, e, f = 0, 0, 0, 0 252 a, d = self._get_params(m.group(1)) 253 self.matrix = a, b, c, d, e, f 254 return 255 256 # Rotation. 257 258 m = self.rotate_regexp.match(value) 259 if m: 260 e, f = 0, 0 261 angle = float(m.group(1).strip()) 262 a = d = math.cos(math.radians(angle)) 263 b = math.sin(math.radians(angle)) 264 c = -b 265 self.matrix = a, b, c, d, e, f 266 return 267 268 # Skew. 269 270 m = self.skewX_regexp.match(value) 271 if m: 272 a, b, d, e, f = 1, 0, 1, 0, 0 273 angle = float(m.group(1).strip()) 274 c = math.tan(math.radians(angle)) 275 self.matrix = a, b, c, d, e, f 276 return 277 278 m = self.skewY_regexp.match(value) 279 if m: 280 a, c, d, e, f = 1, 0, 1, 0, 0 281 angle = float(m.group(1).strip()) 282 b = math.tan(math.radians(angle)) 283 self.matrix = a, b, c, d, e, f 284 return 285 286 # Generic. 287 288 m = self.matrix_regexp.match(value) 289 if m: 290 self.matrix = self._get_params(m.group(1)) 291 return 292 293 # Otherwise, complain. 294 295 raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR) 296 297 def toNode(self, node, name): 298 299 """ 300 Set the trait on the given 'node' using the given 'name' according to 301 this object's attributes. 302 """ 303 304 a, b, c, d, e, f = self.matrix 305 306 # Translation. 307 308 if (a, b, c, d) == (1, 0, 0, 1): 309 node.setAttribute(name, "translate(%f, %f)" % (e, f)) 310 311 # Scaling. 312 313 elif (b, c, e, f) == (0, 0, 0, 0): 314 node.setAttribute(name, "scale(%f, %f)" % (a, d)) 315 316 # Rotation. 317 318 elif a == d and b == -c and (e, f) == (0, 0) and math.degrees(math.acos(a)) == math.degrees(math.asin(b)): 319 node.setAttribute(name, "rotate(%f)" % math.degrees(math.acos(a))) 320 321 # Skew. 322 323 elif (a, b, d, e, f) == (1, 0, 1, 0, 0) and c != 0: 324 node.setAttribute(name, "skewX(%f)" % math.degrees(math.atan(c))) 325 326 elif (a, c, d, e, f) == (1, 0, 1, 0, 0) and b != 0: 327 node.setAttribute(name, "skewX(%f)" % math.degrees(math.atan(b))) 328 329 # Generic matrix. 330 331 else: 332 node.setAttribute(name, "matrix(%f, %f, %f, %f, %f, %f)" % (a, b, c, d, e, f)) 333 334 def getComponent(self, index): 335 336 """ 337 Return the component with the given 'index' (starting at zero) from the 338 sequence a, b, c, d, e, f where each element corresponds to the matrix 339 as follows: 340 341 [ a c e ] 342 [ b d f ] 343 [ 0 0 1 ] 344 """ 345 346 try: 347 return self.matrix[index] 348 except IndexError: 349 raise xml.dom.DOMException(xml.dom.INDEX_SIZE_ERR) 350 351 def mMultiply(self, secondMatrix): 352 353 """ 354 Multiply this matrix with 'secondMatrix' and update its contents to the 355 result of the multiplication operation defined as follows: 356 357 [ a c e ] [ A C E ] 358 [ b d f ] [ B D F ] 359 [ 0 0 1 ] [ 0 0 1 ] 360 361 Return this object as a result. 362 """ 363 364 a, b, c, d, e, f = self.matrix 365 A, B, C, D, E, F = secondMatrix.matrix 366 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 367 return self 368 369 def inverse(self): 370 371 """ 372 det = ad - cb 373 374 See (for example): http://mathworld.wolfram.com/MatrixInverse.html 375 """ 376 377 det = a*d - c*b 378 if det != 0: 379 m = 1/det 380 a, b, c, d, e, f = self.matrix 381 self.matrix = m * d, m * -b, m * -c, m * a, m * (c*f - e*d), m * (e*b - a*f) 382 return self 383 else: 384 raise SVGException(SVGException.SVG_MATRIX_NOT_INVERTABLE) 385 386 def mTranslate(self, x, y): 387 388 """ 389 [ 1 0 x ] 390 [ 0 1 y ] 391 [ 0 0 1 ] 392 """ 393 394 return self.mMultiply(SVGMatrix(1, 0, 0, 1, x, y)) 395 396 def mScale(self, scaleFactor): 397 398 """ 399 [ scaleFactor 0 0 ] 400 [ 0 scaleFactor 0 ] 401 [ 0 0 1 ] 402 """ 403 404 return self.mMultiply(SVGMatrix(scaleFactor, 0, 0, scaleFactor, 0, 0)) 405 406 def mRotate(self, angle): 407 408 """ 409 [ cos(angle) -sin(angle) 0 ] 410 [ sin(angle) cos(angle) 0 ] 411 [ 0 0 1 ] 412 """ 413 414 return self.mMultiply( 415 SVGMatrix( 416 math.cos(math.radians(angle)), 417 math.sin(math.radians(angle)), 418 -math.sin(math.radians(angle)), 419 math.cos(math.radians(angle)), 420 0, 0 421 ) 422 ) 423 424 class SVGPath: 425 426 """ 427 A path. 428 See: http://www.w3.org/TR/SVGMobile12/svgudom.html#svg__SVGPath 429 See: http://www.w3.org/TR/SVGMobile12/paths.html 430 """ 431 432 MOVE_TO = 77 433 LINE_TO = 76 434 CURVE_TO = 67 435 QUAD_TO = 81 436 CLOSE = 90 437 _CLOSE = 122 # More baggage (name not standard). 438 439 nparams = { 440 MOVE_TO : 2, 441 LINE_TO : 2, 442 CURVE_TO : 6, 443 QUAD_TO : 4, 444 CLOSE : 0, 445 _CLOSE : 0 446 } 447 448 def __init__(self): 449 self.segments = [] 450 451 def __eq__(self, other): 452 return self.segments == other.segments 453 454 def __ne__(self, other): 455 return not (self == other) 456 457 def fromNode(self, node, name): 458 459 """ 460 Initialise this object from the trait on the 'node' having the given 461 'name'. 462 """ 463 464 value = node.getAttribute(name) 465 if value is None: 466 raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR) 467 468 # Try and unpack the attribute value. 469 470 data = value.split() 471 self.segments = [] 472 try: 473 i = 0 474 while i < len(data): 475 cmd = ord(data[i]) 476 if cmd == self._CLOSE: 477 cmd = self.CLOSE 478 i += 1 479 n = self.nparams[cmd] 480 params = map(float, data[i:i+n]) 481 self.segments.append((cmd, params)) 482 i += n 483 except (IndexError, ValueError): 484 raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR) 485 486 def toNode(self, node, name): 487 488 """ 489 Set the trait on the given 'node' using the given 'name' according to 490 this object's attributes. 491 """ 492 493 try: 494 l = [] 495 for cmd, params in self.segments: 496 l.append(unichr(cmd)) 497 for param in params: 498 l.append(str(param)) 499 node.setAttribute(name, " ".join(l)) 500 except (IndexError, ValueError): 501 raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR) 502 503 # Interface methods. 504 505 def _numberOfSegments(self): 506 return len(self.segments) 507 508 numberOfSegments = property(_numberOfSegments) 509 510 def getSegment(self, cmdIndex): 511 try: 512 return self.segments[cmdIndex][0] 513 except IndexError: 514 raise xml.dom.DOMException(xml.dom.INDEX_SIZE_ERR) 515 516 def getSegmentParam(self, cmdIndex, paramIndex): 517 try: 518 return self.segments[cmdIndex][1][paramIndex] 519 except IndexError: 520 raise xml.dom.DOMException(xml.dom.INDEX_SIZE_ERR) 521 522 def moveTo(self, x, y): 523 self.segments.append((self.MOVE_TO, (x, y))) 524 525 def lineTo(self, x, y): 526 self.segments.append((self.LINE_TO, (x, y))) 527 528 def quadTo(self, x1, y1, x2, y2): 529 self.segments.append((self.QUAD_TO, (x1, y1, x2, y2))) 530 531 def curveTo(self, x1, y1, x2, y2, x3, y3): 532 self.segments.append((self.CURVE_TO, (x1, y1, x2, y2, x3, y3))) 533 534 def close(self): 535 self.segments.append((self.CLOSE,)) 536 537 class SVGPoint: 538 539 "A point used to provide currentTranslate." 540 541 def __init__(self, x, y): 542 self.x = x 543 self.y = y 544 545 class SVGRect: 546 547 "A rectangle." 548 549 def __init__(self, x=0, y=0, width=0, height=0): 550 self.x, self.y, self.width, self.height = x, y, width, height 551 552 def __eq__(self, other): 553 return (self.x, self.y, self.width, self.height) == (other.x, other.y, other.width, other.height) 554 555 def __ne__(self, other): 556 return not (self == other) 557 558 def fromNode(self, node, name): 559 560 """ 561 Initialise this object from the trait on the 'node' having the given 562 'name'. 563 """ 564 565 value = node.getAttribute(name) 566 if value is None: 567 raise xml.dom.DOMException(xml.dom.NOT_SUPPORTED_ERR) 568 try: 569 values = map(float, value.split()) 570 self.x, self.y, self.width, self.height = values 571 except (IndexError, ValueError): 572 raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR) 573 574 def toNode(self, node, name): 575 576 """ 577 Set the trait on the given 'node' using the given 'name' according to 578 this object's attributes. 579 """ 580 581 try: 582 values = map(str, [self.x, self.y, self.width, self.height]) 583 node.setAttribute(name, " ".join(values)) 584 except (IndexError, ValueError): 585 raise xml.dom.DOMException(xml.dom.TYPE_MISMATCH_ERR) 586 587 class SVGRGBColor: 588 589 "A colour." 590 591 def __init__(self, red, green, blue): 592 self.red, self.green, self.blue = red, green, blue 593 594 class TraitAccess: 595 596 """ 597 Access to traits stored on elements. 598 See: http://www.w3.org/TR/SVGMobile12/svgudom.html#svg__TraitAccess 599 """ 600 601 def getPathTrait(self, name): 602 path = SVGPath() 603 path.fromNode(self, name) 604 return path 605 606 def setPathTrait(self, name, path): 607 path.toNode(self, name) 608 609 def getRectTrait(self, name): 610 rect = SVGRect() 611 rect.fromNode(self, name) 612 return rect 613 614 def setRectTrait(self, name, rect): 615 rect.toNode(self, name) 616 617 def getMatrixTrait(self, name): 618 matrix = SVGMatrix() 619 matrix.fromNode(self, name) 620 return matrix 621 622 def setMatrixTrait(self, name, matrix): 623 matrix.toNode(self, name) 624 625 # Node classes. 626 627 class SVGNode(libxml2dom.Node): 628 629 "Convenience modifications to nodes specific to libxml2dom.svg." 630 631 def xpath(self, expr, variables=None, namespaces=None): 632 633 """ 634 Evaluate the given 'expr' using the optional 'variables' and 635 'namespaces'. If not otherwise specified, the "svg" prefix will be bound 636 to SVG_NAMESPACE as defined in this module. 637 """ 638 639 namespaces = namespaces or {} 640 if not namespaces.has_key("svg"): 641 namespaces["svg"] = SVG_NAMESPACE 642 return libxml2dom.Node.xpath(self, expr, variables, namespaces) 643 644 class SVGDocument(libxml2dom._Document, SVGNode, DocumentEvent, EventTarget): 645 646 "An SVG-specific document node." 647 648 def __init__(self, node, impl): 649 650 """ 651 Initialise the document with the given 'node', implementation 'impl', 652 and global (SVGGlobal) details. 653 """ 654 655 libxml2dom._Document.__init__(self, node, impl) 656 self.global_ = SVGGlobal(self) # parent 657 658 class SVGElement(SVGNode, EventTarget, TraitAccess, ElementTraversal): # NOTE: SVGNode instead of Element. 659 660 "An SVG-specific element." 661 662 def _id(self): 663 return self.getAttribute("id") 664 665 def _setId(self, value): 666 self.setAttribute("id", value) 667 668 id = property(_id, _setId) 669 670 class SVGLocatableElement(SVGElement, SVGLocatable): 671 672 "A locatable element." 673 674 pass 675 676 class SVGTimedElement(SVGElement): # smil::ElementTimeControl 677 678 "A timed element." 679 680 def __init__(self, *args): 681 682 "Initialise the element with the underlying 'args'." 683 684 SVGElement.__init__(self, *args) 685 self.document_time = 0 686 self.paused = 0 687 688 def _isPaused(self): 689 return self.paused 690 691 def pauseElement(self): 692 self.paused = 1 693 694 def resumeElement(self): 695 self.paused = 0 696 697 class SVGSVGElement(SVGLocatableElement, SVGTimedElement): 698 699 "An SVG-specific top-level element." 700 701 NAV_AUTO = 1 702 NAV_NEXT = 2 703 NAV_PREV = 3 704 NAV_UP = 4 705 NAV_UP_RIGHT = 5 706 NAV_RIGHT = 6 707 NAV_DOWN_RIGHT = 7 708 NAV_DOWN = 8 709 NAV_DOWN_LEFT = 9 710 NAV_LEFT = 10 711 NAV_UP_LEFT = 11 712 713 def __init__(self, *args): 714 715 "Initialise the element with the underlying 'args'." 716 717 SVGTimedElement.__init__(self, *args) 718 self.scale = 1 719 self.rotate = 0 720 self.translate = SVGPoint(0, 0) 721 722 def _currentScale(self): 723 return self.scale 724 725 def _currentRotate(self): 726 return self.rotate 727 728 def _currentTranslate(self): 729 return self.translate 730 731 def _viewport(self): 732 return self.getRectTrait("viewBox") 733 734 def getCurrentTime(self): 735 return self.document_time 736 737 def setCurrentTime(self, setCurrentTime): 738 self.document_time = setCurrentTime 739 740 def createSVGMatrixComponents(self, a, b, c, d, e, f): 741 return SVGMatrix(a, b, c, d, e, f) 742 743 def createSVGRect(self): 744 return SVGRect() 745 746 def createSVGPath(self): 747 return SVGPath() 748 749 def createSVGRGBColor(self, red, green, blue): 750 return SVGRGBColor(red, green, blue) 751 752 def moveFocus(self, motionType): 753 raise NotImplementedError, "moveFocus" 754 755 def setFocus(self, object): 756 raise NotImplementedError, "setFocus" 757 758 def getCurrentFocusedObject(self): 759 raise NotImplementedError, "getCurrentFocusedObject" 760 761 currentScale = property(_currentScale) 762 currentRotate = property(_currentRotate) 763 currentTranslate = property(_currentTranslate) 764 viewport = property(_viewport) 765 766 # Utility functions. 767 768 createDocument = libxml2dom.createDocument 769 createDocumentType = libxml2dom.createDocumentType 770 771 def createSVGDocument(): 772 return default_impl.createSVGDocument() 773 774 def parse(stream_or_string, html=0, htmlencoding=None): 775 return libxml2dom.parse(stream_or_string, html, htmlencoding, default_impl) 776 777 def parseFile(filename, html=0, htmlencoding=None): 778 return libxml2dom.parseFile(filename, html, htmlencoding, default_impl) 779 780 def parseString(s, html=0, htmlencoding=None): 781 return libxml2dom.parseString(s, html, htmlencoding, default_impl) 782 783 def parseURI(uri, html=0, htmlencoding=None): 784 return libxml2dom.parseURI(uri, html, htmlencoding, default_impl) 785 786 # Single instance of the implementation. 787 788 default_impl = SVGImplementation() 789 790 # vim: tabstop=4 expandtab shiftwidth=4