1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """
4 inkex.py
5 A helper module for creating Inkscape extensions
7 Copyright (C) 2005,2010 Aaron Spike <aaron@ekips.org> and contributors
9 Contributors:
10 Aurélio A. Heckert <aurium(a)gmail.com>
11 Bulia Byak <buliabyak@users.sf.net>
12 Nicolas Dufour, nicoduf@yahoo.fr
13 Peter J. R. Moulder <pjrm@users.sourceforge.net>
15 This program is free software; you can redistribute it and/or modify
16 it under the terms of the GNU General Public License as published by
17 the Free Software Foundation; either version 2 of the License, or
18 (at your option) any later version.
20 This program is distributed in the hope that it will be useful,
21 but WITHOUT ANY WARRANTY; without even the implied warranty of
22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 GNU General Public License for more details.
25 You should have received a copy of the GNU General Public License
26 along with this program; if not, write to the Free Software
27 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
28 """
29 import sys, copy, optparse, random, re
30 import gettext
31 from math import *
33 gettext.install('inkscape')
34 # _ = gettext.gettext
35 # gettext.bindtextdomain('inkscape', '/usr/share/locale')
36 # gettext.textdomain('inkscape')
38 #a dictionary of all of the xmlns prefixes in a standard inkscape doc
39 NSS = {
40 u'sodipodi' :u'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd',
41 u'cc' :u'http://creativecommons.org/ns#',
42 u'ccOLD' :u'http://web.resource.org/cc/',
43 u'svg' :u'http://www.w3.org/2000/svg',
44 u'dc' :u'http://purl.org/dc/elements/1.1/',
45 u'rdf' :u'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
46 u'inkscape' :u'http://www.inkscape.org/namespaces/inkscape',
47 u'xlink' :u'http://www.w3.org/1999/xlink',
48 u'xml' :u'http://www.w3.org/XML/1998/namespace'
49 }
51 #a dictionary of unit to user unit conversion factors
52 uuconv = {'in':90.0, 'pt':1.25, 'px':1, 'mm':3.5433070866, 'cm':35.433070866, 'm':3543.3070866,
53 'km':3543307.0866, 'pc':15.0, 'yd':3240 , 'ft':1080}
54 def unittouu(string):
55 '''Returns userunits given a string representation of units in another system'''
56 unit = re.compile('(%s)$' % '|'.join(uuconv.keys()))
57 param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
59 p = param.match(string)
60 u = unit.search(string)
61 if p:
62 retval = float(p.string[p.start():p.end()])
63 else:
64 retval = 0.0
65 if u:
66 try:
67 return retval * uuconv[u.string[u.start():u.end()]]
68 except KeyError:
69 pass
70 return retval
72 def uutounit(val, unit):
73 return val/uuconv[unit]
75 try:
76 from lxml import etree
77 except Exception, e:
78 sys.exit(_("The fantastic lxml wrapper for libxml2 is required by inkex.py and therefore this extension. Please download and install the latest version from http://cheeseshop.python.org/pypi/lxml/, or install it through your package manager by a command like: sudo apt-get install python-lxml\n\nTechnical details:\n%s" % (e,)))
81 def debug(what):
82 sys.stderr.write(str(what) + "\n")
83 return what
85 def errormsg(msg):
86 """Intended for end-user-visible error messages.
88 (Currently just writes to stderr with an appended newline, but could do
89 something better in future: e.g. could add markup to distinguish error
90 messages from status messages or debugging output.)
92 Note that this should always be combined with translation:
94 import gettext
95 _ = gettext.gettext
96 ...
97 inkex.errormsg(_("This extension requires two selected paths."))
98 """
99 sys.stderr.write((unicode(msg) + "\n").encode("UTF-8"))
101 def check_inkbool(option, opt, value):
102 if str(value).capitalize() == 'True':
103 return True
104 elif str(value).capitalize() == 'False':
105 return False
106 else:
107 raise optparse.OptionValueError("option %s: invalid inkbool value: %s" % (opt, value))
109 def addNS(tag, ns=None):
110 val = tag
111 if ns!=None and len(ns)>0 and NSS.has_key(ns) and len(tag)>0 and tag[0]!='{':
112 val = "{%s}%s" % (NSS[ns], tag)
113 return val
115 class InkOption(optparse.Option):
116 TYPES = optparse.Option.TYPES + ("inkbool",)
117 TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER)
118 TYPE_CHECKER["inkbool"] = check_inkbool
120 class Effect:
121 """A class for creating Inkscape SVG Effects"""
123 def __init__(self, *args, **kwargs):
124 self.document=None
125 self.ctx=None
126 self.selected={}
127 self.doc_ids={}
128 self.options=None
129 self.args=None
130 self.OptionParser = optparse.OptionParser(usage="usage: %prog [options] SVGfile",option_class=InkOption)
131 self.OptionParser.add_option("--id",
132 action="append", type="string", dest="ids", default=[],
133 help="id attribute of object to manipulate")
135 def effect(self):
136 pass
138 def getoptions(self,args=sys.argv[1:]):
139 """Collect command line arguments"""
140 self.options, self.args = self.OptionParser.parse_args(args)
142 def parse(self,file=None):
143 """Parse document in specified file or on stdin"""
144 try:
145 try:
146 stream = open(file,'r')
147 except:
148 stream = open(self.svg_file,'r')
149 except:
150 stream = sys.stdin
151 self.document = etree.parse(stream)
152 stream.close()
154 def getposinlayer(self):
155 #defaults
156 self.current_layer = self.document.getroot()
157 self.view_center = (0.0,0.0)
159 layerattr = self.document.xpath('//sodipodi:namedview/@inkscape:current-layer', namespaces=NSS)
160 if layerattr:
161 layername = layerattr[0]
162 layer = self.document.xpath('//svg:g[@id="%s"]' % layername, namespaces=NSS)
163 if layer:
164 self.current_layer = layer[0]
166 xattr = self.document.xpath('//sodipodi:namedview/@inkscape:cx', namespaces=NSS)
167 yattr = self.document.xpath('//sodipodi:namedview/@inkscape:cy', namespaces=NSS)
168 doc_height = unittouu(self.document.getroot().get('height'))
169 if xattr and yattr:
170 x = xattr[0]
171 y = yattr[0]
172 if x and y:
173 self.view_center = (float(x), doc_height - float(y)) # FIXME: y-coordinate flip, eliminate it when it's gone in Inkscape
175 def getselected(self):
176 """Collect selected nodes"""
177 for i in self.options.ids:
178 path = '//*[@id="%s"]' % i
179 for node in self.document.xpath(path, namespaces=NSS):
180 self.selected[i] = node
182 def getElementById(self, id):
183 path = '//*[@id="%s"]' % id
184 el_list = self.document.xpath(path, namespaces=NSS)
185 if el_list:
186 return el_list[0]
187 else:
188 return None
190 def getParentNode(self, node):
191 for parent in self.document.getiterator():
192 if node in parent.getchildren():
193 return parent
194 break
197 def getdocids(self):
198 docIdNodes = self.document.xpath('//@id', namespaces=NSS)
199 for m in docIdNodes:
200 self.doc_ids[m] = 1
202 def getNamedView(self):
203 return self.document.xpath('//sodipodi:namedview', namespaces=NSS)[0]
205 def createGuide(self, posX, posY, angle):
206 atts = {
207 'position': str(posX)+','+str(posY),
208 'orientation': str(sin(radians(angle)))+','+str(-cos(radians(angle)))
209 }
210 guide = etree.SubElement(
211 self.getNamedView(),
212 addNS('guide','sodipodi'), atts )
213 return guide
215 def output(self):
216 """Serialize document into XML on stdout"""
217 self.document.write(sys.stdout)
219 def affect(self, args=sys.argv[1:], output=True):
220 """Affect an SVG document with a callback effect"""
221 self.svg_file = args[-1]
222 self.getoptions(args)
223 self.parse()
224 self.getposinlayer()
225 self.getselected()
226 self.getdocids()
227 self.effect()
228 if output: self.output()
230 def uniqueId(self, old_id, make_new_id = True):
231 new_id = old_id
232 if make_new_id:
233 while new_id in self.doc_ids:
234 new_id += random.choice('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
235 self.doc_ids[new_id] = 1
236 return new_id
238 def xpathSingle(self, path):
239 try:
240 retval = self.document.xpath(path, namespaces=NSS)[0]
241 except:
242 errormsg(_("No matching node for expression: %s") % path)
243 retval = None
244 return retval
247 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99