16
16
along with this program; if not, write to the Free Software
17
17
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
18
barraud@math.univ-lille1.fr
20
This code defines a basic class (PathModifier) of effects whose purpose is
21
to somehow deform given objects: one common tasks for all such effect is to
22
convert shapes, groups, clones to paths. The class has several functions to
23
make this (more or less!) easy.
24
As an exemple, a second class (Diffeo) is derived from it,
25
to implement deformations of the form X=f(x,y), Y=g(x,y)...
27
TODO: Several handy functions are defined, that might in fact be of general
28
interest and that should be shipped out in separate files...
20
30
import inkex, cubicsuperpath, bezmisc, simplestyle
21
import copy, math, re, random, xml.xpath
23
def parseTransform(transf,mat=[[1.0,0.0,0.0],[0.0,1.0,0.0]]):
26
result=re.match("(translate|scale|rotate|skewX|skewY|matrix)\(([^)]*)\)",transf)
28
if result.group(1)=="translate":
29
args=result.group(2).split(",")
35
matrix=[[1,0,dx],[0,1,dy]]
37
if result.groups(1)=="scale":
38
args=result.group(2).split(",")
44
matrix=[[sx,0,0],[0,sy,0]]
46
if result.groups(1)=="rotate":
47
args=result.group(2).split(",")
48
a=float(args[0])*math.pi/180
53
matrix=[[math.cos(a),-math.sin(a),cx],[math.sin(a),math.cos(a),cy]]
55
if result.groups(1)=="skewX":
56
a=float(result.group(2))*math.pi/180
57
matrix=[[1,math.tan(a),0],[0,1,0]]
59
if result.groups(1)=="skewX":
60
a=float(result.group(2))*math.pi/180
61
matrix=[[1,0,0],[math.tan(a),1,0]]
63
if result.group(1)=="matrix":
64
a11,a21,a12,a22,v1,v2=result.group(2).split(",")
65
matrix=[[float(a11),float(a12),float(v1)],[float(a21),float(a22),float(v2)]]
67
matrix=composeTransform(mat,matrix)
68
if result.end()<len(transf):
69
return(parseTransform(transf[result.end():],matrix))
73
def formatTransform(mat):
74
return("matrix(%f,%f,%f,%f,%f,%f)"%(mat[0][0],mat[1][0],mat[0][1],mat[1][1],mat[0][2],mat[1][2]))
76
def composeTransform(M1,M2):
77
a11=M1[0][0]*M2[0][0]+M1[0][1]*M2[1][0]
78
a12=M1[0][0]*M2[0][1]+M1[0][1]*M2[1][1]
79
a21=M1[1][0]*M2[0][0]+M1[1][1]*M2[1][0]
80
a22=M1[1][0]*M2[0][1]+M1[1][1]*M2[1][1]
82
v1=M1[0][0]*M2[0][2]+M1[0][1]*M2[1][2]+M1[0][2]
83
v2=M1[1][0]*M2[0][2]+M1[1][1]*M2[1][2]+M1[1][2]
84
return [[a11,a12,v1],[a21,a22,v2]]
86
def applyTransformToNode(mat,node):
87
m=parseTransform(node.getAttributeNS(None,"transform"))
88
newtransf=formatTransform(composeTransform(mat,m))
89
node.setAttributeNS(None,"transform", newtransf)
91
def applyTransformToPoint(mat,pt):
92
x=mat[0][0]*pt[0]+mat[0][1]*pt[1]+mat[0][2]
93
y=mat[1][0]*pt[0]+mat[1][1]*pt[1]+mat[1][2]
97
def fuseTransform(node):
98
m=parseTransform(node.getAttributeNS(None,"transform"))
99
d = node.getAttributeNS(None,'d')
100
p=cubicsuperpath.parsePath(d)
104
applyTransformToPoint(m,pt)
105
node.setAttributeNS(None,'d', cubicsuperpath.formatPath(p))
106
node.removeAttributeNS(None,"transform")
115
return((min(b1[0],b2[0]),max(b1[1],b2[1]),min(b1[2],b2[2]),max(b1[3],b2[3])))
118
xmin,xMax,ymin,yMax=path[0][0][0][0],path[0][0][0][0],path[0][0][0][1],path[0][0][0][1]
119
for pathcomp in path:
126
return xmin,xMax,ymin,yMax
31
from simpletransform import *
32
import copy, math, re, random
34
####################################################################
35
##-- zOrder computation...
36
##-- this should be shipped out in a separate file. inkex.py?
38
def zSort(inNode,idList):
40
theid = inNode.get("id")
42
sortedList.append(theid)
44
if len(sortedList)==len(idList):
46
sortedList+=zSort(child,idList)
129
50
class PathModifier(inkex.Effect):
133
54
##################################
134
55
#-- Selectionlists manipulation --
135
56
##################################
136
def computeBBox(self, aList):
138
for id, node in aList.iteritems():
139
if node.tagName == 'path':
140
d = node.attributes.getNamedItem('d')
141
p = cubicsuperpath.parsePath(d.value)
142
bbox=boxunion(roughBBox(p),bbox)
145
58
def duplicateNodes(self, aList):
147
60
for id,node in aList.iteritems():
148
clone=node.cloneNode(True)
149
#!!!--> should it be given an id?
150
#seems to work without this!?!
151
clone.setAttributeNS(None,"id", self.uniqueId(node.tagName))
152
node.parentNode.appendChild(clone)
153
clones[clone.getAttributeNS(None,"id")]=clone
61
clone=copy.deepcopy(node)
62
#!!!--> should it be given an id?
63
#seems to work without this!?!
64
myid = node.tag.split('}')[-1]
65
clone.set("id", self.uniqueId(myid))
66
node.getparent().append(clone)
67
clones[clone.get("id")]=clone
156
70
def uniqueId(self, prefix):
157
71
id="%s%04i"%(prefix,random.randint(0,9999))
158
while len(xml.xpath.Evaluate('//*[@id="%s"]' % id,self.document)):
72
while len(self.document.getroot().xpath('//*[@id="%s"]' % id,inkex.NSS)):
159
73
id="%s%04i"%(prefix,random.randint(0,9999))
162
76
def expandGroups(self,aList,transferTransform=True):
163
77
for id, node in aList.items():
164
if node.tagName == 'g':
165
mat=parseTransform(node.getAttributeNS(None,"transform"))
166
for child in node.childNodes:
167
if child.nodeType==child.ELEMENT_NODE:
168
if transferTransform:
169
applyTransformToNode(mat,child)
170
aList.update(self.expandGroups({child.getAttribute('id'):child}))
171
if transferTransform:
172
node.removeAttribute("transform")
78
if node.tag == inkex.addNS('g','svg') or node.tag=='g':
79
mat=parseTransform(node.get("transform"))
82
applyTransformToNode(mat,child)
83
aList.update(self.expandGroups({child.get('id'):child}))
84
if transferTransform and node.get("transform"):
85
del node.attrib["transform"]
176
89
def expandGroupsUnlinkClones(self,aList,transferTransform=True,doReplace=True):
177
90
for id in aList.keys()[:]:
179
if node.tagName == 'g':
92
if node.tag == inkex.addNS('g','svg') or node.tag=='g':
180
93
self.expandGroups(aList,transferTransform)
181
94
self.expandGroupsUnlinkClones(aList,transferTransform,doReplace)
182
#Hum... not very efficient if there are many clones of groups...
183
elif node.tagName == 'use':
184
refid=node.getAttributeNS(inkex.NSS[u'xlink'],'href')
185
path = '//*[@id="%s"]' % refid[1:]
186
refnode = xml.xpath.Evaluate(path,self.document)[0]
187
newnode=refnode.cloneNode(True)
188
self.recursNewIds(newnode)
95
#Hum... not very efficient if there are many clones of groups...
190
if node.hasAttributeNS(None,u'style'):
191
style=simplestyle.parseStyle(node.getAttributeNS(None,u'style'))
192
refstyle=simplestyle.parseStyle(refnode.getAttributeNS(None,u'style'))
193
style.update(refstyle)
194
newnode.setAttributeNS(None,'style',simplestyle.formatStyle(style))
195
applyTransformToNode(parseTransform(node.getAttributeNS(None,'transform')),newnode)
197
parent=node.parentNode
198
parent.insertBefore(newnode,node)
199
parent.removeChild(node)
97
elif node.tag == inkex.addNS('use','svg') or node.tag=='use':
98
refnode=self.refNode(node)
99
newnode=self.unlinkClone(node,doReplace)
201
newid=newnode.getAttributeNS(None,'id')
102
style = simplestyle.parseStyle(node.get('style') or "")
103
refstyle=simplestyle.parseStyle(refnode.get('style') or "")
104
style.update(refstyle)
105
newnode.set('style',simplestyle.formatStyle(style))
107
newid=newnode.get('id')
202
108
aList.update(self.expandGroupsUnlinkClones({newid:newnode},transferTransform,doReplace))
205
111
def recursNewIds(self,node):
206
if node.nodeType==node.ELEMENT_NODE and node.hasAttributeNS(None,u'id'):
207
node.setAttributeNS(None,u'id',self.uniqueId(node.tagName))
208
for child in node.childNodes:
209
self.recursNewIds(child)
214
# def makeClonesReal(self,aList,doReplace=True,recursivelytransferTransform=True):
215
# for id in aList.keys():
217
# if node.tagName == 'g':
219
# for child in node.childNodes:
220
# if child.nodeType==child.ELEMENT_NODE:
221
# childid=child.getAttributeNS(None,'id')
223
# aList.update(self.makeClonesReal({childid:child},doReplace))
224
# elif node.tagName == 'use':
225
# refid=node.getAttributeNS(inkex.NSS[u'xlink'],'href')
226
# path = '//*[@id="%s"]' % refid[1:]
227
# refnode = xml.xpath.Evaluate(path,document)[0]
228
# clone=refnode.cloneNode(True)
229
# cloneid=self.uniqueId(clone.tagName)
230
# clone.setAttributeNS(None,'id', cloneid)
231
# style=simplestyle.parseStyle(node.getAttributeNS(None,u'style'))
232
# refstyle=simplestyle.parseStyle(refnode.getAttributeNS(None,u'style'))
233
# style.update(refstyle)
234
# clone.setAttributeNS(None,'style',simplestyle.formatStyle(style))
235
# applyTransformToNode(parseTransform(node.getAttributeNS(None,'transform')),clone)
237
# parent=node.parentNode
238
# parent.insertBefore(clone,node)
239
# parent.removeChild(node)
241
# aList.update(self.expandGroupsUnlinkClones({cloneid:clone}))
113
node.set('id',self.uniqueId(node.tag))
115
self.recursNewIds(child)
117
def refNode(self,node):
118
if node.get(inkex.addNS('href','xlink')):
119
refid=node.get(inkex.addNS('href','xlink'))
120
path = '//*[@id="%s"]' % refid[1:]
121
newNode = self.document.getroot().xpath(path,inkex.NSS)[0]
124
raise AssertionError, "Trying to follow empty xlink.href attribute."
126
def unlinkClone(self,node,doReplace):
127
if node.tag == inkex.addNS('use','svg') or node.tag=='use':
128
newNode = copy.deepcopy(self.refNode(node))
129
self.recursNewIds(newNode)
130
applyTransformToNode(parseTransform(node.get('transform')),newNode)
133
parent=node.getparent()
134
parent.insert(parent.index(node),newNode)
139
raise AssertionError, "Only clones can be unlinked..."
244
143
################################
245
144
#-- Object conversion ----------
246
145
################################
248
147
def rectToPath(self,node,doReplace=True):
249
if node.tagName == 'rect':
250
x =float(node.getAttributeNS(None,u'x'))
251
y =float(node.getAttributeNS(None,u'y'))
148
if node.tag == inkex.addNS('rect','svg'):
149
x =float(node.get('x'))
150
y =float(node.get('y'))
151
#FIXME: no exception anymore and sometimes just one
253
rx=float(node.getAttributeNS(None,u'rx'))
254
ry=float(node.getAttributeNS(None,u'ry'))
153
rx=float(node.get('rx'))
154
ry=float(node.get('ry'))
258
w =float(node.getAttributeNS(None,u'width' ))
259
h =float(node.getAttributeNS(None,u'height'))
158
w =float(node.get('width' ))
159
h =float(node.get('height'))
260
160
d ='M %f,%f '%(x+rx,y)
261
161
d+='L %f,%f '%(x+w-rx,y)
262
162
d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+w,y+ry)
267
167
d+='L %f,%f '%(x,y+ry)
268
168
d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+rx,y)
270
newnode=self.document.createElement('path')
271
newnode.setAttributeNS(None,'d',d)
272
newnode.setAttributeNS(None,'id', self.uniqueId('path'))
273
newnode.setAttributeNS(None,'style',node.getAttributeNS(None,u'style'))
274
newnode.setAttributeNS(None,'transform',node.getAttributeNS(None,u'transform'))
275
fuseTransform(newnode)
170
newnode=inkex.etree.Element('path')
172
newnode.set('id', self.uniqueId('path'))
173
newnode.set('style',node.get('style'))
174
nnt = node.get('transform')
176
newnode.set('transform',nnt)
177
fuseTransform(newnode)
277
parent=node.parentNode
278
parent.insertBefore(newnode,node)
279
parent.removeChild(node)
179
parent=node.getparent()
180
parent.insert(parent.index(node),newnode)
184
def groupToPath(self,node,doReplace=True):
185
if node.tag == inkex.addNS('g','svg'):
186
newNode = inkex.etree.SubElement(self.current_layer,inkex.addNS('path','svg'))
188
newstyle = simplestyle.parseStyle(node.get('style') or "")
191
childstyle = simplestyle.parseStyle(child.get('style') or "")
192
childstyle.update(newstyle)
193
newstyle.update(childstyle)
194
childAsPath = self.objectToPath(child,False)
195
newp += cubicsuperpath.parsePath(childAsPath.get('d'))
196
newNode.set('d',cubicsuperpath.formatPath(newp))
197
newNode.set('style',simplestyle.formatStyle(newstyle))
199
self.current_layer.remove(newNode)
201
parent=node.getparent()
202
parent.insert(parent.index(node),newNode)
282
209
def objectToPath(self,node,doReplace=True):
283
#--TODO: support other object types!!!!
284
#--TODO: make sure cubicsuperpath supports A and Q commands...
285
if node.tagName == 'rect':
286
return(self.rectToPath(node,doReplace))
287
elif node.tagName == 'path':
288
attributes = node.attributes.keys()
289
for uri,attName in attributes:
290
if uri in [inkex.NSS[u'sodipodi'],inkex.NSS[u'inkscape']]:
291
# if attName not in ["d","id","style","transform"]:
292
node.removeAttributeNS(uri,attName)
210
#--TODO: support other object types!!!!
211
#--TODO: make sure cubicsuperpath supports A and Q commands...
212
if node.tag == inkex.addNS('rect','svg'):
213
return(self.rectToPath(node,doReplace))
214
if node.tag == inkex.addNS('g','svg'):
215
return(self.groupToPath(node,doReplace))
216
elif node.tag == inkex.addNS('path','svg') or node.tag == 'path':
217
#remove inkscape attributes, otherwise any modif of 'd' will be discarded!
218
for attName in node.attrib.keys():
219
if ("sodipodi" in attName) or ("inkscape" in attName):
220
del node.attrib[attName]
293
221
fuseTransform(node)
223
elif node.tag == inkex.addNS('use','svg') or node.tag == 'use':
224
newNode = self.unlinkClone(node,doReplace)
225
return self.objectToPath(newNode,doReplace)
296
inkex.debug("Please first convert objects to paths!...(got '%s')"%node.tagName)
227
inkex.debug("Please first convert objects to paths!...(got '%s')"%node.tag)
299
230
def objectsToPaths(self,aList,doReplace=True):
301
232
for id,node in aList.items():
302
newnode=self.objectToPath(node,self.document)
233
newnode=self.objectToPath(node,doReplace)
304
aList[newnode.getAttributeNS(None,u'id')]=newnode
235
aList[newnode.get('id')]=newnode
307
238
################################
308
239
#-- Action ----------
309
240
################################
311
242
#-- overwrite this method in subclasses...
312
243
def effect(self):
313
244
#self.duplicateNodes(self.selected)
314
self.expandGroupsUnlinkClones(self.selected, True)
315
self.objectsToPaths(self.selected, True)
316
self.bbox=self.computeBBox(self.selected)
245
#self.expandGroupsUnlinkClones(self.selected, True)
246
self.objectsToPaths(self.selected, True)
247
self.bbox=computeBBox(self.selected.values())
317
248
for id, node in self.selected.iteritems():
318
if node.tagName == 'path':
319
d = node.attributes.getNamedItem('d')
320
p = cubicsuperpath.parsePath(d.value)
322
#do what ever you want with p!
324
d.value = cubicsuperpath.formatPath(p)
249
if node.tag == inkex.addNS('path','svg'):
251
p = cubicsuperpath.parsePath(d)
253
#do what ever you want with p!
255
node.set('d',cubicsuperpath.formatPath(p))
327
258
class Diffeo(PathModifier):