1 #!/usr/bin/env python\r
2 '''\r
3 Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr\r
4 \r
5 This program is free software; you can redistribute it and/or modify\r
6 it under the terms of the GNU General Public License as published by\r
7 the Free Software Foundation; either version 2 of the License, or\r
8 (at your option) any later version.\r
9 \r
10 This program is distributed in the hope that it will be useful,\r
11 but WITHOUT ANY WARRANTY; without even the implied warranty of\r
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
13 GNU General Public License for more details.\r
14 \r
15 You should have received a copy of the GNU General Public License\r
16 along with this program; if not, write to the Free Software\r
17 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\r
18 barraud@math.univ-lille1.fr\r
19 '''\r
20 import inkex, cubicsuperpath, bezmisc, simplestyle\r
21 import copy, math, re, random\r
22 \r
23 def parseTransform(transf,mat=[[1.0,0.0,0.0],[0.0,1.0,0.0]]):\r
24 if transf=="":\r
25 return(mat)\r
26 result=re.match("(translate|scale|rotate|skewX|skewY|matrix)\(([^)]*)\)",transf)\r
27 #-- translate --\r
28 if result.group(1)=="translate":\r
29 args=result.group(2).split(",")\r
30 dx=float(args[0])\r
31 if len(args)==1:\r
32 dy=0.0\r
33 else:\r
34 dy=float(args[1])\r
35 matrix=[[1,0,dx],[0,1,dy]]\r
36 #-- scale --\r
37 if result.groups(1)=="scale":\r
38 args=result.group(2).split(",")\r
39 sx=float(args[0])\r
40 if len(args)==1:\r
41 sy=sx\r
42 else:\r
43 sy=float(args[1])\r
44 matrix=[[sx,0,0],[0,sy,0]]\r
45 #-- rotate --\r
46 if result.groups(1)=="rotate":\r
47 args=result.group(2).split(",")\r
48 a=float(args[0])*math.pi/180\r
49 if len(args)==1:\r
50 cx,cy=(0.0,0.0)\r
51 else:\r
52 cx,cy=args[1:]\r
53 matrix=[[math.cos(a),-math.sin(a),cx],[math.sin(a),math.cos(a),cy]]\r
54 #-- skewX --\r
55 if result.groups(1)=="skewX":\r
56 a=float(result.group(2))*math.pi/180\r
57 matrix=[[1,math.tan(a),0],[0,1,0]]\r
58 #-- skewX --\r
59 if result.groups(1)=="skewX":\r
60 a=float(result.group(2))*math.pi/180\r
61 matrix=[[1,0,0],[math.tan(a),1,0]]\r
62 #-- matrix --\r
63 if result.group(1)=="matrix":\r
64 a11,a21,a12,a22,v1,v2=result.group(2).split(",")\r
65 matrix=[[float(a11),float(a12),float(v1)],[float(a21),float(a22),float(v2)]]\r
66 \r
67 matrix=composeTransform(mat,matrix)\r
68 if result.end()<len(transf):\r
69 return(parseTransform(transf[result.end():],matrix))\r
70 else:\r
71 return matrix\r
72 \r
73 def formatTransform(mat):\r
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]))\r
75 \r
76 def composeTransform(M1,M2):\r
77 a11=M1[0][0]*M2[0][0]+M1[0][1]*M2[1][0]\r
78 a12=M1[0][0]*M2[0][1]+M1[0][1]*M2[1][1]\r
79 a21=M1[1][0]*M2[0][0]+M1[1][1]*M2[1][0]\r
80 a22=M1[1][0]*M2[0][1]+M1[1][1]*M2[1][1]\r
81 \r
82 v1=M1[0][0]*M2[0][2]+M1[0][1]*M2[1][2]+M1[0][2]\r
83 v2=M1[1][0]*M2[0][2]+M1[1][1]*M2[1][2]+M1[1][2]\r
84 return [[a11,a12,v1],[a21,a22,v2]]\r
85 \r
86 def applyTransformToNode(mat,node):\r
87 m=parseTransform(node.get("transform"))\r
88 newtransf=formatTransform(composeTransform(mat,m))\r
89 node.set("transform", newtransf)\r
90 \r
91 def applyTransformToPoint(mat,pt):\r
92 x=mat[0][0]*pt[0]+mat[0][1]*pt[1]+mat[0][2]\r
93 y=mat[1][0]*pt[0]+mat[1][1]*pt[1]+mat[1][2]\r
94 pt[0]=x\r
95 pt[1]=y\r
96 \r
97 def fuseTransform(node):\r
98 t = node.get("transform")\r
99 if t == None:\r
100 return\r
101 m=parseTransform(t)\r
102 d = node.get('d')\r
103 p=cubicsuperpath.parsePath(d)\r
104 for comp in p:\r
105 for ctl in comp:\r
106 for pt in ctl:\r
107 applyTransformToPoint(m,pt)\r
108 node.set('d', cubicsuperpath.formatPath(p))\r
109 del node.attrib["transform"]\r
110 \r
111 \r
112 def boxunion(b1,b2):\r
113 if b1 is None:\r
114 return b2\r
115 elif b2 is None:\r
116 return b1 \r
117 else:\r
118 return((min(b1[0],b2[0]),max(b1[1],b2[1]),min(b1[2],b2[2]),max(b1[3],b2[3])))\r
119 \r
120 def roughBBox(path):\r
121 xmin,xMax,ymin,yMax=path[0][0][0][0],path[0][0][0][0],path[0][0][0][1],path[0][0][0][1]\r
122 for pathcomp in path:\r
123 for ctl in pathcomp:\r
124 for pt in ctl:\r
125 xmin=min(xmin,pt[0])\r
126 xMax=max(xMax,pt[0])\r
127 ymin=min(ymin,pt[1])\r
128 yMax=max(yMax,pt[1])\r
129 return xmin,xMax,ymin,yMax\r
130 \r
131 \r
132 class PathModifier(inkex.Effect):\r
133 def __init__(self):\r
134 inkex.Effect.__init__(self)\r
135 \r
136 ##################################\r
137 #-- Selectionlists manipulation --\r
138 ##################################\r
139 def computeBBox(self, aList):\r
140 bbox=None\r
141 for id, node in aList.iteritems():\r
142 if node.tag == inkex.addNS('path','svg') or node.tag == 'path':\r
143 d = node.get('d')\r
144 p = cubicsuperpath.parsePath(d)\r
145 bbox=boxunion(roughBBox(p),bbox)\r
146 return bbox\r
147 \r
148 def duplicateNodes(self, aList):\r
149 clones={}\r
150 for id,node in aList.iteritems():\r
151 clone=copy.deepcopy(node)\r
152 #!!!--> should it be given an id?\r
153 #seems to work without this!?!\r
154 myid = node.tag.split('}')[-1]\r
155 clone.set("id", self.uniqueId(myid))\r
156 node.getparent().append(clone)\r
157 clones[clone.get("id")]=clone\r
158 return(clones)\r
159 \r
160 def uniqueId(self, prefix):\r
161 id="%s%04i"%(prefix,random.randint(0,9999))\r
162 while len(self.document.getroot().xpath('//*[@id="%s"]' % id,inkex.NSS)):\r
163 id="%s%04i"%(prefix,random.randint(0,9999))\r
164 return(id)\r
165 \r
166 def expandGroups(self,aList,transferTransform=True):\r
167 for id, node in aList.items(): \r
168 if node.tag == inkex.addNS('g','svg') or node.tag=='g':\r
169 mat=parseTransform(node.get("transform"))\r
170 for child in node:\r
171 if transferTransform:\r
172 applyTransformToNode(mat,child)\r
173 aList.update(self.expandGroups({child.get('id'):child}))\r
174 if transferTransform:\r
175 del node.attrib["transform"]\r
176 del aList[id]\r
177 return(aList)\r
178 \r
179 def expandGroupsUnlinkClones(self,aList,transferTransform=True,doReplace=True):\r
180 for id in aList.keys()[:]: \r
181 node=aList[id]\r
182 if node.tag == inkex.addNS('g','svg') or node.tag=='g':\r
183 self.expandGroups(aList,transferTransform)\r
184 self.expandGroupsUnlinkClones(aList,transferTransform,doReplace)\r
185 #Hum... not very efficient if there are many clones of groups...\r
186 elif node.tag == inkex.addNS('use','svg') or node.tag=='use':\r
187 refid=node.get(inkex.addNS('href','xlink'))\r
188 path = '//*[@id="%s"]' % refid[1:]\r
189 refnode = self.document.getroot().xpath(path,inkex.NSS)\r
190 newnode=copy.deepcopy(refnode)\r
191 self.recursNewIds(newnode)\r
192 \r
193 s = node.get('style')\r
194 if s:\r
195 style=simplestyle.parseStyle(s)\r
196 refstyle=simplestyle.parseStyle(refnode.get('style'))\r
197 style.update(refstyle)\r
198 newnode.set('style',simplestyle.formatStyle(style))\r
199 applyTransformToNode(parseTransform(node.get('transform')),newnode)\r
200 if doReplace:\r
201 parent=node.getparent()\r
202 parent.insert(node.index,newnode)\r
203 parent.remove(node)\r
204 del aList[id]\r
205 newid=newnode.get('id')\r
206 aList.update(self.expandGroupsUnlinkClones({newid:newnode},transferTransform,doReplace))\r
207 return aList\r
208 \r
209 def recursNewIds(self,node):\r
210 if node.get('id'):\r
211 node.set('id',self.uniqueId(node.tag))\r
212 for child in node:\r
213 self.recursNewIds(child)\r
214 \r
215 \r
216 \r
217 # Had not been rewritten for ElementTree\r
218 # def makeClonesReal(self,aList,doReplace=True,recursivelytransferTransform=True):\r
219 # for id in aList.keys(): \r
220 # node=aList[id]\r
221 # if node.tagName == 'g':\r
222 # childs={}\r
223 # for child in node.childNodes:\r
224 # if child.nodeType==child.ELEMENT_NODE:\r
225 # childid=child.getAttributeNS(None,'id')\r
226 # del aList[childid]\r
227 # aList.update(self.makeClonesReal({childid:child},doReplace))\r
228 # elif node.tagName == 'use':\r
229 # refid=node.getAttributeNS(inkex.NSS[u'xlink'],'href')\r
230 # path = '//*[@id="%s"]' % refid[1:]\r
231 # refnode = xml.xpath.Evaluate(path,document)[0]\r
232 # clone=refnode.cloneNode(True)\r
233 # cloneid=self.uniqueId(clone.tagName)\r
234 # clone.setAttributeNS(None,'id', cloneid)\r
235 # style=simplestyle.parseStyle(node.getAttributeNS(None,u'style'))\r
236 # refstyle=simplestyle.parseStyle(refnode.getAttributeNS(None,u'style'))\r
237 # style.update(refstyle)\r
238 # clone.setAttributeNS(None,'style',simplestyle.formatStyle(style))\r
239 # applyTransformToNode(parseTransform(node.getAttributeNS(None,'transform')),clone)\r
240 # if doReplace:\r
241 # parent=node.parentNode\r
242 # parent.insertBefore(clone,node)\r
243 # parent.removeChild(node)\r
244 # del aList[id]\r
245 # aList.update(self.expandGroupsUnlinkClones({cloneid:clone}))\r
246 # return aList\r
247 \r
248 ################################\r
249 #-- Object conversion ----------\r
250 ################################\r
251 \r
252 def rectToPath(self,node,doReplace=True):\r
253 if node.tag == inkex.addNS('rect','svg'):\r
254 x =float(node.get('x'))\r
255 y =float(node.get('y'))\r
256 #FIXME: no exception anymore and sometimes just one\r
257 try:\r
258 rx=float(node.get('rx'))\r
259 ry=float(node.get('ry'))\r
260 except:\r
261 rx=0\r
262 ry=0\r
263 w =float(node.get('width' ))\r
264 h =float(node.get('height'))\r
265 d ='M %f,%f '%(x+rx,y)\r
266 d+='L %f,%f '%(x+w-rx,y)\r
267 d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+w,y+ry)\r
268 d+='L %f,%f '%(x+w,y+h-ry)\r
269 d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+w-rx,y+h)\r
270 d+='L %f,%f '%(x+rx,y+h)\r
271 d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x,y+h-ry)\r
272 d+='L %f,%f '%(x,y+ry)\r
273 d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+rx,y)\r
274 \r
275 newnode=inkex.etree.Element('path')\r
276 newnode.set('d',d)\r
277 newnode.set('id', self.uniqueId('path'))\r
278 newnode.set('style',node.get('style'))\r
279 nnt = node.get('transform')\r
280 if nnt:\r
281 newnode.set('transform',nnt)\r
282 fuseTransform(newnode)\r
283 if doReplace:\r
284 parent=node.getparent()\r
285 parent.insert(node.index,newnode)\r
286 parent.remove(node)\r
287 return newnode\r
288 \r
289 def objectToPath(self,node,doReplace=True):\r
290 #--TODO: support other object types!!!!\r
291 #--TODO: make sure cubicsuperpath supports A and Q commands... \r
292 if node.tag == inkex.addNS('rect','svg'):\r
293 return(self.rectToPath(node,doReplace))\r
294 elif node.tag == inkex.addNS('path','svg') or node.tag == 'path':\r
295 attributes = node.keys()\r
296 for attName in attributes:\r
297 uri = None\r
298 if attName[0] == '{':\r
299 uri,attName = attName.split('}')\r
300 uri = uri[1:]\r
301 if uri in [inkex.NSS[u'sodipodi'],inkex.NSS[u'inkscape']]:\r
302 #if attName not in ["d","id","style","transform"]:\r
303 del node.attrib[inkex.addNS(attName,uri)]\r
304 fuseTransform(node)\r
305 return node\r
306 else:\r
307 inkex.debug("Please first convert objects to paths!...(got '%s')"%node.tag)\r
308 return None\r
309 \r
310 def objectsToPaths(self,aList,doReplace=True):\r
311 newSelection={}\r
312 for id,node in aList.items():\r
313 newnode=self.objectToPath(node,self.document)\r
314 del aList[id]\r
315 aList[newnode.get('id')]=newnode\r
316 \r
317 \r
318 ################################\r
319 #-- Action ----------\r
320 ################################\r
321 \r
322 #-- overwrite this method in subclasses...\r
323 def effect(self):\r
324 #self.duplicateNodes(self.selected)\r
325 self.expandGroupsUnlinkClones(self.selected, True)\r
326 self.objectsToPaths(self.selected, True)\r
327 self.bbox=self.computeBBox(self.selected)\r
328 for id, node in self.selected.iteritems():\r
329 if node.tag == inkex.addNS('path','svg'):\r
330 d = node.get('d')\r
331 p = cubicsuperpath.parsePath(d)\r
332 \r
333 #do what ever you want with p!\r
334 \r
335 node.set('d',cubicsuperpath.formatPath(p))\r
336 \r
337 \r
338 class Diffeo(PathModifier):\r
339 def __init__(self):\r
340 inkex.Effect.__init__(self)\r
341 \r
342 def applyDiffeo(self,bpt,vects=()):\r
343 '''\r
344 bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt. \r
345 Defaults to identity!\r
346 '''\r
347 for v in vects:\r
348 v[0]-=bpt[0]\r
349 v[1]-=bpt[1]\r
350 \r
351 #-- your transformations go here:\r
352 #x,y=bpt\r
353 #bpt[0]=f(x,y)\r
354 #bpt[1]=g(x,y)\r
355 #for v in vects:\r
356 # vx,vy=v\r
357 # v[0]=df/dx(x,y)*vx+df/dy(x,y)*vy\r
358 # v[1]=dg/dx(x,y)*vx+dg/dy(x,y)*vy\r
359 #\r
360 #-- !caution! y-axis is pointing downward!\r
361 \r
362 for v in vects:\r
363 v[0]+=bpt[0]\r
364 v[1]+=bpt[1]\r
365 \r
366 \r
367 def effect(self):\r
368 #self.duplicateNodes(self.selected)\r
369 self.expandGroupsUnlinkClones(self.selected, True)\r
370 self.expandGroups(self.selected, True)\r
371 self.objectsToPaths(self.selected, True)\r
372 self.bbox=self.computeBBox(self.selected)\r
373 for id, node in self.selected.iteritems():\r
374 if node.tag == inkex.addNS('path','svg') or node.tag=='path':\r
375 d = node.get('d')\r
376 p = cubicsuperpath.parsePath(d)\r
377 \r
378 for sub in p:\r
379 for ctlpt in sub:\r
380 self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))\r
381 \r
382 node.set('d',cubicsuperpath.formatPath(p))\r
383 \r
384 #e = Diffeo()\r
385 #e.affect()\r