A random link Home Compute Inkscape Contact   
   
   
   

My InkEx Module

This is the most current version of my Inkscape extension helper module. Your comments and suggestions are appreciated.

#!/usr/bin/env python
"""
inkex.py
A helper module for creating Inkscape extensions
"""
import sys, optparse, re, math
import xml.dom.ext
import xml.dom.ext.reader.Sax2
import xml.xpath

def parseStyle(s):
	"""Create a dictionary from the value of an inline style attribute"""
	return dict([i.split(":") for i in s.split(";")])
def formatStyle(a):
	"""Format an inline style attribute from a dictionary"""
	return ";".join([":".join(i) for i in a.iteritems()])

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 = lexer.next()
		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 token.isupper():
					command = pathdefs[lastCommand.upper()][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 = lexer.next()
					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])
		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

class Effect:
	"""A class for creating Inkscape SVG Effects"""
	def __init__(self):
		self.document=None
		self.selected={}
		self.options=None
		self.args=None
		self.OptionParser = optparse.OptionParser(usage="usage: %prog [options] SVGfile")
		self.OptionParser.add_option("--id",
						action="append", type="string", dest="ids", default=[], 
						help="id attribute of object to manipulate")
	def effect(self):
		pass
	def getoptions(self,args=sys.argv[1:]):
		"""Collect command line arguments"""
		self.options, self.args = self.OptionParser.parse_args(args)
	def parse(self,file=None):
		"""Parse document in specified file or on stdin"""
		reader = xml.dom.ext.reader.Sax2.Reader()
		try:
			try:
				stream = open(file,'r')
			except:
				stream = open(self.args[-1],'r')
		except:
			stream = sys.stdin
		self.document = reader.fromStream(stream)
		stream.close()
	def getselected(self):
		"""Collect selected nodes"""
		for id in self.options.ids:
			path = '//*[@id="%s"]' % id
			for node in xml.xpath.Evaluate(path,self.document):
				self.selected[id] = node
	def output(self):
		"""Serialize document into XML on stdout"""
		xml.dom.ext.Print(self.document)
	def affect(self):
		"""Affect an SVG document with a callback effect"""
		self.getoptions()
		self.parse()
		self.getselected()
		self.effect()
		self.output()

Files:

     
   
   
Home Compute Inkscape Contact