#!/usr/bin/env python """ simplepath.py functions for digesting paths into a simple list structure Copyright (C) 2005 Aaron Spike, aaron@ekips.org This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import re, math def lexPath(d): """ returns and iterator that breaks path data identifies command and parameter tokens """ offset = 0 length = len(d) delim = re.compile(r'[ \t\r\n,]+') command = re.compile(r'[MLHVCSQTAZmlhvcsqtaz]') parameter = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') while 1: m = delim.match(d, offset) if m: offset = m.end() if offset >= length: break m = command.match(d, offset) if m: yield [d[offset:m.end()], True] offset = m.end() continue m = parameter.match(d, offset) if m: yield [d[offset:m.end()], False] offset = m.end() continue #TODO: create new exception raise Exception('Invalid path data!') ''' pathdefs = {commandfamily: [ implicitnext, #params, [casts,cast,cast], [coord type,x,y,0] ]} ''' pathdefs = { 'M':['L', 2, [float, float], ['x','y']], 'L':['L', 2, [float, float], ['x','y']], 'H':['H', 1, [float], ['x']], 'V':['V', 1, [float], ['y']], 'C':['C', 6, [float, float, float, float, float, float], ['x','y','x','y','x','y']], 'S':['S', 4, [float, float, float, float], ['x','y','x','y']], 'Q':['Q', 4, [float, float, float, float], ['x','y','x','y']], 'T':['T', 2, [float, float], ['x','y']], 'A':['A', 7, [float, float, float, int, int, float, float], [0,0,0,0,0,'x','y']], 'Z':['L', 0, [], []] } def parsePath(d): """ Parse SVG path and return an array of segments. Removes all shorthand notation. Converts coordinates to absolute. """ retval = [] lexer = lexPath(d) pen = (0.0,0.0) subPathStart = pen lastControl = pen lastCommand = '' while 1: try: token, isCommand = next(lexer) except StopIteration: break params = [] needParam = True if isCommand: if not lastCommand and token.upper() != 'M': raise Exception('Invalid path, must begin with moveto.') else: command = token else: #command was omited #use last command's implicit next command needParam = False if lastCommand: if lastCommand.isupper(): command = pathdefs[lastCommand][0] else: command = pathdefs[lastCommand.upper()][0].lower() else: raise Exception('Invalid path, no initial command.') numParams = pathdefs[command.upper()][1] while numParams > 0: if needParam: try: token, isCommand = next(lexer) if isCommand: raise Exception('Invalid number of parameters') except StopIteration: raise Exception('Unexpected end of path') cast = pathdefs[command.upper()][2][-numParams] param = cast(token) if command.islower(): if pathdefs[command.upper()][3][-numParams]=='x': param += pen[0] elif pathdefs[command.upper()][3][-numParams]=='y': param += pen[1] params.append(param) needParam = True numParams -= 1 #segment is now absolute so outputCommand = command.upper() #Flesh out shortcut notation if outputCommand in ('H','V'): if outputCommand == 'H': params.append(pen[1]) if outputCommand == 'V': params.insert(0,pen[0]) outputCommand = 'L' if outputCommand in ('S','T'): params.insert(0,pen[1]+(pen[1]-lastControl[1])) params.insert(0,pen[0]+(pen[0]-lastControl[0])) if outputCommand == 'S': outputCommand = 'C' if outputCommand == 'T': outputCommand = 'Q' #current values become "last" values if outputCommand == 'M': subPathStart = tuple(params[0:2]) pen = subPathStart if outputCommand == 'Z': pen = subPathStart else: pen = tuple(params[-2:]) if outputCommand in ('Q','C'): lastControl = tuple(params[-4:-2]) else: lastControl = pen lastCommand = command retval.append([outputCommand,params]) return retval def formatPath(a): """Format SVG path data from an array""" return "".join([cmd + " ".join([str(p) for p in params]) for cmd, params in a]) def translatePath(p, x, y): for cmd,params in p: defs = pathdefs[cmd] for i in range(defs[1]): if defs[3][i] == 'x': params[i] += x elif defs[3][i] == 'y': params[i] += y def scalePath(p, x, y): for cmd,params in p: defs = pathdefs[cmd] for i in range(defs[1]): if defs[3][i] == 'x': params[i] *= x elif defs[3][i] == 'y': params[i] *= y def rotatePath(p, a, cx = 0, cy = 0): if a == 0: return p for cmd,params in p: defs = pathdefs[cmd] for i in range(defs[1]): if defs[3][i] == 'x': x = params[i] - cx y = params[i + 1] - cy r = math.sqrt((x**2) + (y**2)) if r != 0: theta = math.atan2(y, x) + a params[i] = (r * math.cos(theta)) + cx params[i + 1] = (r * math.sin(theta)) + cy