pdf to ipe
[Misc/ipe.git] / tools / svgtoipe.py
1 #!/usr/bin/env python
2 # --------------------------------------------------------------------
3 # convert SVG to Ipe format
4 # --------------------------------------------------------------------
5
6 # Copyright (C) 2009-2014  Otfried Cheong
7 #
8 # svgtoipe is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12
13 # svgtoipe is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 # General Public License for more details.
17
18 # You should have received a copy of the GNU General Public License
19 # along with svgtoipe; if not, you can find it at
20 # "http://www.gnu.org/copyleft/gpl.html", or write to the Free
21 # Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
22 #
23 # --------------------------------------------------------------------
24
25 svgtoipe_version = "20091018"
26
27 import sys
28 import xml.dom.minidom as xml
29 from xml.dom.minidom import Node
30 import re
31 import math
32
33 import base64
34 import cStringIO
35
36 try:
37   from PIL import Image
38   have_pil = True
39 except:
40   have_pil = False
41
42 # --------------------------------------------------------------------
43
44 color_keywords = {
45   "black" : "rgb(0, 0, 0)",
46   "green" :"rgb(0, 128, 0)",
47   "silver" :"rgb(192, 192, 192)",
48   "lime" :"rgb(0, 255, 0)",
49   "gray" :"rgb(128, 128, 128)",
50   "olive" :"rgb(128, 128, 0)",
51   "white" :"rgb(255, 255, 255)",
52   "yellow" :"rgb(255, 255, 0)",
53   "maroon" :"rgb(128, 0, 0)",
54   "navy" :"rgb(0, 0, 128)",
55   "red" :"rgb(255, 0, 0)",
56   "blue" :"rgb(0, 0, 255)",
57   "purple" :"rgb(128, 0, 128)",
58   "teal" :"rgb(0, 128, 128)",
59   "fuchsia" :"rgb(255, 0, 255)",
60   "aqua" :"rgb(0, 255, 255)",
61 }
62
63 attribute_names = [ "stroke", 
64                     "fill",
65                     "stroke-opacity",
66                     "fill-opacity",
67                     "stroke-width",
68                     "fill-rule",
69                     "stroke-linecap",
70                     "stroke-linejoin",
71                     "stroke-dasharray",
72                     "stroke-dashoffset",
73                     "stroke-miterlimit",
74                     "opacity", 
75                     "font-size" ]
76   
77 def printAttributes(n):
78   a = n.attributes
79   for i in range(a.length):
80     name = a.item(i).name
81     if name[:9] != "sodipodi:" and name[:9] != "inkscape:":
82       print "   ", name, n.getAttribute(name)
83
84 def parse_float(txt):
85   if not txt:
86     return None
87   if txt.endswith('px') or txt.endswith('pt'):
88     return float(txt[:-2])
89   elif txt.endswith('pc'):
90     return 12 * float(txt[:-2])
91   elif txt.endswith('mm'):
92     return 72.0 * float(txt[:-2]) / 25.4
93   elif txt.endswith('cm'):
94     return 72.0 * float(txt[:-2]) / 2.54
95   elif txt.endswith('in'):
96     return 72.0 * float(txt[:-2])
97   else:
98     return float(txt)
99
100 def parse_opacity(txt):
101   if not txt: 
102     return None
103   m = int(10 * (float(txt) + 0.05))
104   if m == 0: m = 1
105   return 10 * m
106
107 def parse_list(string):
108   return re.findall("([A-Za-z]|-?[0-9]+\.?[0-9]*(?:e-?[0-9]*)?)", string)
109
110 def parse_style(string):
111   sdict = {}
112   for item in string.split(';'):
113     if ':' in item:
114       key, value = item.split(':')
115       sdict[key.strip()] = value.strip()
116   return sdict
117
118 def parse_color_component(txt):
119   if txt.endswith("%"):
120     return float(txt[:-1]) / 100.0
121   else:
122     return int(txt) / 255.0
123   
124 def parse_color(c):
125   if not c or c == 'none':
126     return None
127   if c in color_keywords:
128     c = color_keywords[c]
129   m =  re.match(r"rgb\(([0-9\.]+%?),\s*([0-9\.]+%?),\s*([0-9\.]+%?)\s*\)", c)
130   if m:
131     r = parse_color_component(m.group(1))
132     g = parse_color_component(m.group(2))
133     b = parse_color_component(m.group(3))
134     return (r, g, b)
135   m = re.match(r"#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$", c)
136   if m:
137     r = int(m.group(1), 16) / 15.0
138     g = int(m.group(2), 16) / 15.0
139     b = int(m.group(3), 16) / 15.0
140     return (r, g, b)
141   m = re.match(r"#([0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F])"
142                + r"([0-9a-fA-F][0-9a-fA-F])$", c)
143   if m:
144     r = int(m.group(1), 16) / 255.0
145     g = int(m.group(2), 16) / 255.0
146     b = int(m.group(3), 16) / 255.0
147     return (r, g, b)
148   sys.stderr.write("Unknown color: %s\n" % c)
149   return None
150
151 def pnext(d, n):
152   l = []
153   while n > 0:
154     l.append(float(d.pop(0)))
155     n -= 1
156   return tuple(l)
157
158 def parse_path(out, d):
159   d = re.findall("([A-Za-z]|-?[0-9]+\.?[0-9]*(?:e-?[0-9]*)?)", d)
160   x, y = 0.0, 0.0
161   xs, ys = 0.0, 0.0
162   while d:
163     if not d[0][0] in "01234567890.-":
164       opcode = d.pop(0)
165     if opcode == 'M':
166       x, y = pnext(d, 2)
167       out.write("%g %g m\n" % (x, y))
168       opcode = 'L'
169     elif opcode == 'm':
170       x1, y1 = pnext(d, 2)
171       x += x1
172       y += y1
173       out.write("%g %g m\n" % (x, y))
174       opcode = 'l'
175     elif opcode == 'L':
176       x, y = pnext(d, 2)
177       out.write("%g %g l\n" % (x, y))
178     elif opcode == 'l':
179       x1, y1 = pnext(d, 2)
180       x += x1
181       y += y1
182       out.write("%g %g l\n" % (x, y))
183     elif opcode == 'H':
184       x = pnext(d, 1)[0]
185       out.write("%g %g l\n" % (x, y))
186     elif opcode == 'h':
187       x += pnext(d, 1)[0]
188       out.write("%g %g l\n" % (x, y))
189     elif opcode == 'V':
190       y = pnext(d, 1)[0]
191       out.write("%g %g l\n" % (x, y))
192     elif opcode == 'v':
193       y += pnext(d, 1)[0]
194       out.write("%g %g l\n" % (x, y))
195     elif opcode == 'C':
196       x1, y1, xs, ys, x, y = pnext(d, 6)
197       out.write("%g %g %g %g %g %g c\n" % (x1, y1, xs, ys, x, y))
198     elif opcode == 'c':
199       x1, y1, xs, ys, xf, yf = pnext(d, 6)
200       x1 += x; y1 += y
201       xs += x; ys += y
202       x += xf; y += yf
203       out.write("%g %g %g %g %g %g c\n" % (x1, y1, xs, ys, x, y))
204     elif opcode == 'S' or opcode == 's':
205       x2, y2, xf, yf = pnext(d, 4)
206       if opcode == 's':
207         x2 += x; y2 += y
208         xf += x; yf += y
209       x1 = x + (x - xs); y1 = y + (y - ys)
210       out.write("%g %g %g %g %g %g c\n" % (x1, y1, x2, y2, xf, yf))
211       xs, ys = x2, y2
212       x, y = xf, yf
213     elif opcode == 'Q':
214       xs, ys, x, y = pnext(d, 4)
215       out.write("%g %g %g %g q\n" % (xs, ys, x, y))
216     elif opcode == 'q':
217       xs, ys, xf, yf = pnext(d, 4)
218       xs += x; ys += y
219       x += xf; y += yf
220       out.write("%g %g %g %g q\n" % (xs, ys, x, y))
221     elif opcode == 'T' or opcode == 't':
222       xf, yf = pnext(d, 2)
223       if opcode == 't':
224         xf += x; yf += y
225       x1 = x + (x - xs); y1 = y + (y - ys)
226       out.write("%g %g %g %g q\n" % (x1, y1, xf, yf))
227       xs, ys = x1, y1
228       x, y = xf, yf
229     elif opcode == 'A' or opcode == 'a':
230       rx, ry, phi, large_arc, sweep, x2, y2 = pnext(d, 7)
231       if opcode == 'a':
232         x2 += x; y2 += y
233       draw_arc(out, x, y, rx, ry, phi, large_arc, sweep, x2, y2)
234       x, y = x2, y2
235     elif opcode in 'zZ':
236       out.write("h\n")
237     else:
238       sys.stderr.write("Unrecognised opcode: %s\n" % opcode)
239
240 def parse_transformation(txt):
241   d = re.findall("[a-zA-Z]+\([^)]*\)", txt)
242   m = Matrix()
243   while d:
244     m1 = Matrix(d.pop(0))
245     m = m * m1
246   return m
247
248 def get_gradientTransform(n):
249   if n.hasAttribute("gradientTransform"):
250     return parse_transformation(n.getAttribute("gradientTransform"))
251   return Matrix()
252
253 def parse_transform(n):
254   if n.hasAttribute("transform"):
255     return parse_transformation(n.getAttribute("transform"))
256   return None
257
258 # Convert from endpoint to center parameterization
259 # www.w3.org/TR/2003/REC-SVG11-20030114/implnote.html#ArcImplementationNotes
260 def draw_arc(out, x1, y1, rx, ry, phi, large_arc, sweep, x2, y2):
261   phi = math.pi * phi / 180.0
262   cp = math.cos(phi); sp = math.sin(phi)
263   dx = .5 * (x1 - x2); dy = .5 * (y1 - y2)
264   x1p = cp * dx + sp * dy; y1p = -sp * dx + cp * dy
265   r2 = (((rx * ry)**2 - (rx * y1p)**2 - (ry * x1p)**2)/
266         ((rx * y1p)**2 + (ry * x1p)**2))
267   if r2 < 0: r2 = 0
268   r = math.sqrt(r2)
269   if large_arc == sweep:
270     r = -r
271   cxp = r * rx * y1p / ry; cyp = -r * ry * x1p / rx
272   cx = cp * cxp - sp * cyp + .5 * (x1 + x2)
273   cy = sp * cxp + cp * cyp + .5 * (y1 + y2)
274   m = Matrix([rx, 0, 0, ry, 0, 0])
275   m = Matrix([cp, sp, -sp, cp, cx, cy]) * m
276   if sweep == 0:
277     m = m * Matrix([1, 0, 0, -1, 0, 0])
278   out.write("%s %g %g a\n" % (str(m), x2, y2))
279
280 # --------------------------------------------------------------------
281
282 class Matrix(object):
283
284   # Default is identity matrix
285   def __init__(self, string = None):
286     self.values = [1, 0, 0, 1, 0, 0] 
287     if not string or string == "":
288       return
289     if isinstance(string, list):
290       self.values = string
291       return
292     mat = re.match(r"([a-zA-Z]+)\(([^)]*)\)$", string)
293     if not mat:
294       sys.stderr.write("Unknown transform: %s\n" % string)
295     op = mat.group(1)
296     d = [float(x) for x in parse_list(mat.group(2))]
297     if op == "matrix":
298       self.values = d
299     elif op == "translate":
300       if len(d) == 1: d.append(0.0)
301       self.values = [1, 0, 0, 1, d[0], d[1]]
302     elif op == "scale":
303       if len(d) == 1: d.append(d[0])
304       sx, sy = d
305       self.values = [sx, 0, 0, sy, 0, 0]
306     elif op == "rotate":
307       phi = math.pi * d[0] / 180.0
308       self.values = [math.cos(phi), math.sin(phi), 
309                      -math.sin(phi), math.cos(phi), 0, 0]           
310     elif op == "skewX":
311       tphi = math.tan(math.pi * d[0] / 180.0)
312       self.values = [1, 0, tphi, 1, 0, 0]
313     elif op == "skewY":
314       tphi = math.tan(math.pi * d[0] / 180.0)
315       self.values = [1, tphi, 0, 1, 0, 0]
316     else:
317       sys.stderr.write("Unknown transform: %s\n" % string)
318       
319   def __call__(self, other):
320     return (self.values[0]*other[0] + self.values[2]*other[1] + self.values[4],
321             self.values[1]*other[0] + self.values[3]*other[1] + self.values[5])
322   
323   def inverse(self):
324     d = float(self.values[0]*self.values[3] - self.values[1]*self.values[2])
325     return Matrix([self.values[3]/d, -self.values[1]/d, 
326                    -self.values[2]/d, self.values[0]/d,
327                    (self.values[2]*self.values[5] - 
328                     self.values[3]*self.values[4])/d,
329                    (self.values[1]*self.values[4] - 
330                     self.values[0]*self.values[5])/d])
331
332   def __mul__(self, other):
333     a, b, c, d, e, f = self.values
334     u, v, w, x, y, z = other.values
335     return Matrix([a*u + c*v, b*u + d*v, a*w + c*x, 
336                    b*w + d*x, a*y + c*z + e, b*y + d*z + f])
337   
338   def __str__(self):
339     a, b, c, d, e, f = self.values
340     return "%g %g %g %g %g %g" % (a, b, c, d, e, f)
341     
342 # --------------------------------------------------------------------
343                                
344 class Svg():
345
346   def __init__(self, fname):
347     self.dom = xml.parse(fname)
348     attr = { }
349     for a in attribute_names:
350       attr[a] = None
351     self.attributes = [ attr ]
352     self.defs = { }
353     for n in self.dom.childNodes:
354       if n.nodeType == Node.ELEMENT_NODE and n.tagName == "svg":
355         if n.hasAttribute("viewBox"):
356           x, y, w, h = [float(x) for x in parse_list(n.getAttribute("viewBox"))]
357           self.width = w
358           self.height = h
359           self.origin = (x, y)
360         else:
361           self.width = parse_float(n.getAttribute("width"))
362           self.height = parse_float(n.getAttribute("height"))
363           self.origin = (0, 0)
364         self.root = n
365         return
366
367 # --------------------------------------------------------------------
368
369   def parse_svg(self, outname):
370     self.out = open(outname, "w")
371     self.out.write('<?xml version="1.0"?>\n')
372     self.out.write('<!DOCTYPE ipe SYSTEM "ipe.dtd">\n')
373     self.out.write('<ipe version="70005" creator="svgtoipe %s">\n' %
374                    svgtoipe_version)
375     self.out.write('<ipestyle>\n')
376     self.out.write(('<layout paper="%d %d" frame="%d %d" ' + 
377                     'origin="0 0" crop="no"/>\n') % 
378                    (self.width, self.height, self.width, self.height))
379     for t in range(10, 100, 10):
380       self.out.write('<opacity name="%d%%" value="0.%d"/>\n' % (t, t))
381     # set SVG defaults
382     self.out.write('<pathstyle cap="0" join="0" fillrule="wind"/>\n')
383     self.out.write('</ipestyle>\n')
384     # collect definitions
385     for n in self.root.childNodes:
386       if n.nodeType != Node.ELEMENT_NODE:
387         continue
388       if hasattr(self, "def_" + n.tagName):
389         getattr(self, "def_" + n.tagName)(n)
390     # write definitions into stylesheet
391     if len(self.defs) > 0:
392       self.out.write('<ipestyle>\n')
393       for k in self.defs:
394         if self.defs[k][0] == "linearGradient":
395           self.write_linear_gradient(k)
396         elif self.defs[k][0] == "radialGradient":
397           self.write_radial_gradient(k)
398       self.out.write('</ipestyle>\n')
399     # start real data
400     self.out.write('<page>\n')
401     m = Matrix([1, 0, 0, 1, 0, self.height / 2.0])
402     m = m * Matrix([1, 0, 0, -1, 0, 0])
403     m = m * Matrix([1, 0, 0, 1, 
404                     -self.origin[0], -(self.origin[1] + self.height / 2.0)])
405     self.out.write('<group matrix="%s">\n' % str(m))
406     for n in self.root.childNodes:
407       if n.nodeType != Node.ELEMENT_NODE:
408         continue
409       if hasattr(self, "node_" + n.tagName):
410         getattr(self, "node_" + n.tagName)(n)
411       else:
412         sys.stderr.write("Unhandled node: %s\n" % n.tagName)
413     self.out.write('</group>\n')
414     self.out.write('</page>\n')
415     self.out.write('</ipe>\n')
416     self.out.close()
417
418 # --------------------------------------------------------------------
419
420   def write_linear_gradient(self, k):
421     typ, x1, x2, y1, y2, stops, matrix = self.defs[k]
422     self.out.write('<gradient name="g%s" type="axial" extend="yes"\n' % k)
423     self.out.write(' matrix="%s"' % str(matrix))
424     self.out.write(' coords="%g %g %g %g">\n' % (x1, y1, x2, y2))
425     for s in stops:
426       offset, color = s
427       self.out.write(' <stop offset="%g" color="%g %g %g"/>\n' % 
428                      (offset, color[0], color[1], color[2]))
429     self.out.write('</gradient>\n')
430     
431   def write_radial_gradient(self, k):
432     typ, cx, cy, r, fx, fy, stops, matrix = self.defs[k]
433     self.out.write('<gradient name="g%s" type="radial" extend="yes"\n' % k)
434     self.out.write(' matrix="%s"' % str(matrix))
435     self.out.write(' coords="%g %g %g %g %g %g">\n' % (fx, fy, 0, cx, cy, r))
436     for s in stops:
437       offset, color = s
438       self.out.write(' <stop offset="%g" color="%g %g %g"/>\n' % 
439                      (offset, color[0], color[1], color[2]))
440     self.out.write('</gradient>\n')
441
442   def get_stops(self, n):
443     stops = []
444     for m in n.childNodes:
445       if m.nodeType != Node.ELEMENT_NODE:
446         continue
447       if m.tagName != "stop":
448         continue # should not happen
449       offs = m.getAttribute("offset")
450       if offs.endswith("%"):
451         offs = float(offs[:-1]) / 100.0
452       else:
453         offs = float(offs)
454       color = parse_color(m.getAttribute("stop-color"))
455       if m.hasAttribute("style"):
456         sdict = parse_style(m.getAttribute("style"))
457         if "stop-color" in sdict:
458           color = parse_color(sdict["stop-color"])
459       stops.append((offs, color))
460     if len(stops) == 0:
461       if n.hasAttribute("xlink:href"):
462         ref = n.getAttribute("xlink:href")
463         if ref.startswith("#") and ref[1:] in self.defs:
464           stops = self.defs[ref[1:]][5]
465     return stops
466
467   def def_linearGradient(self, n):
468     #printAttributes(n)
469     kid = n.getAttribute("id")
470     x1 = 0; y1 = 0
471     x2 = self.width; y2 = self.height
472     if n.hasAttribute("x1"):
473       s = n.getAttribute("x1")
474       if s.endswith("%"):
475         x1 = self.width * float(s[:-1]) / 100.0
476       else:
477         x1 = parse_float(s)
478     if n.hasAttribute("x2"):
479       s = n.getAttribute("x2")
480       if s.endswith("%"):
481         x2 = self.width * float(s[:-1]) / 100.0
482       else:
483         x2 = parse_float(s)
484     if n.hasAttribute("y1"):
485       s = n.getAttribute("y1")
486       if s.endswith("%"):
487         y1 = self.width * float(s[:-1]) / 100.0
488       else:
489         y1 = parse_float(s)
490     if n.hasAttribute("y2"):
491       s = n.getAttribute("y2")
492       if s.endswith("%"):
493         y2 = self.width * float(s[:-1]) / 100.0
494       else:
495         y2 = parse_float(s)
496     matrix = get_gradientTransform(n)
497     stops = self.get_stops(n)
498     self.defs[kid] = ("linearGradient", x1, x2, y1, y2, stops, matrix)
499     
500   def def_radialGradient(self, n):
501     #printAttributes(n)
502     kid = n.getAttribute("id")
503     cx = "50%"; cy = "50%"; r = "50%"
504     if n.hasAttribute("cx"):
505       cx = n.getAttribute("cx")
506     if cx.endswith("%"):
507       cx = self.width * float(cx[:-1]) / 100.0
508     else:
509       cx = parse_float(cx)
510     if n.hasAttribute("cy"):
511       cy = n.getAttribute("cy")
512     if cy.endswith("%"):
513       cy = self.width * float(cy[:-1]) / 100.0
514     else:
515       cy = parse_float(cy)
516     if n.hasAttribute("r"):
517       r = n.getAttribute("r")
518     if r.endswith("%"):
519       r = self.width * float(r[:-1]) / 100.0
520     else:
521       r = parse_float(r)
522     if n.hasAttribute("fx"):
523       s = n.getAttribute("fx")
524       if s.endswith("%"):
525         fx = self.width * float(s[:-1]) / 100.0
526       else:
527         fx = parse_float(s)
528     else:
529       fx = cx
530     if n.hasAttribute("fy"):
531       s = n.getAttribute("fy")
532       if s.endswith("%"):
533         fy = self.width * float(s[:-1]) / 100.0
534       else:
535         fy = parse_float(s)
536     else:
537       fy = cy
538     matrix = get_gradientTransform(n)
539     stops = self.get_stops(n)
540     self.defs[kid] = ("radialGradient", cx, cy, r, fx, fy, stops, matrix)
541
542   def def_clipPath(self, node):
543     kid = node.getAttribute("id")
544     # only a single path is implemented
545     for n in node.childNodes:
546       if n.nodeType != Node.ELEMENT_NODE or n.tagName != "path":
547         continue
548       m = parse_transform(n)
549       d = n.getAttribute("d")
550       output = cStringIO.StringIO()
551       parse_path(output, d)
552       path = output.getvalue()
553       output.close()
554       self.defs[kid] = ("clipPath", m, path)
555       return
556
557   def def_g(self, group):
558     for n in group.childNodes:
559       if n.nodeType != Node.ELEMENT_NODE: 
560         continue
561       if hasattr(self, "def_" + n.tagName):
562         getattr(self, "def_" + n.tagName)(n)
563
564   def def_defs(self, node):
565     self.def_g(node)
566
567 # --------------------------------------------------------------------
568
569   def parse_attributes(self, n):
570     pattr = self.attributes[-1]
571     attr = { }
572     for a in attribute_names:
573       if n.hasAttribute(a):
574         attr[a] = n.getAttribute(a)
575       else:
576         attr[a] = pattr[a]
577     if n.hasAttribute("style"):
578       sdict = parse_style(n.getAttribute("style"))
579       for a in attribute_names:
580         if a in sdict:
581           attr[a] = sdict[a]
582     return attr
583
584   def write_pathattributes(self, a):
585     stroke = parse_color(a["stroke"])
586     if stroke:
587       self.out.write(' stroke="%g %g %g"' % stroke)
588     fill = a["fill"]
589     if fill and fill.startswith("url("):
590       mat = re.match("url\(#([^)]+)\).*", fill)
591       if mat:
592         grad = mat.group(1)
593         if grad in self.defs and (self.defs[grad][0] == "linearGradient" or
594                                   self.defs[grad][0] == "radialGradient"):
595           self.out.write(' fill="1" gradient="g%s"' % grad)
596     else:
597       fill = parse_color(a["fill"])
598       if fill:
599         self.out.write(' fill="%g %g %g"' % fill)
600     opacity = parse_opacity(a["opacity"])
601     fill_opacity = parse_opacity(a["fill-opacity"])
602     stroke_opacity = parse_opacity(a["stroke-opacity"])
603     if fill and fill_opacity:
604       opacity = fill_opacity
605     if not fill and stroke and stroke_opacity:
606       opacity = stroke_opacity
607     if opacity and opacity != 100:
608       self.out.write(' opacity="%d%%"' % opacity)
609     stroke_width = parse_float(a["stroke-width"])
610     if a["stroke-width"]:
611       self.out.write(' pen="%g"' % stroke_width)
612     if a["fill-rule"] == "nonzero":
613       self.out.write(' fillrule="wind"')
614     k = {"butt" : 0, "round" : 1, "square" : 2 }
615     if a["stroke-linecap"] in k:
616       self.out.write(' cap="%d"' % k[a["stroke-linecap"]])
617     k = {"miter" : 0, "round" : 1, "bevel" : 2 }
618     if a["stroke-linejoin"] in k:
619       self.out.write(' join="%d"' % k[a["stroke-linejoin"]])
620     dasharray = a["stroke-dasharray"]
621     dashoffset = a["stroke-dashoffset"]
622     if dasharray and dashoffset and dasharray != "none":
623       d = parse_list(dasharray)
624       off = parse_float(dashoffset)
625       self.out.write(' dash="[%s] %g"' % (" ".join(d), off))
626
627 # --------------------------------------------------------------------
628
629   def node_g(self, group):
630     # printAttributes(group)
631     attr = self.parse_attributes(group)
632     self.attributes.append(attr)
633     self.out.write('<group')
634     m = parse_transform(group)
635     if m:   
636       self.out.write(' matrix="%s"' % m)
637     self.out.write('>\n')
638     for n in group.childNodes:
639       if n.nodeType != Node.ELEMENT_NODE: 
640         continue
641       if hasattr(self, "node_" + n.tagName):
642         getattr(self, "node_" + n.tagName)(n)
643       else:
644         sys.stderr.write("Unhandled node: %s\n" % n.tagName)
645     self.out.write('</group>\n')
646     self.attributes.pop()
647
648   def collect_text(self, root):
649     for n in root.childNodes:
650       if n.nodeType == Node.TEXT_NODE:
651         self.text += n.data
652       if n.nodeType != Node.ELEMENT_NODE: 
653         continue
654       if n.tagName == "tspan":  # recurse
655         self.collect_text(n)
656         
657   def node_text(self, t):
658     if not t.hasAttribute("x") or not t.hasAttribute("y"):
659       sys.stderr.write("Text without coordinates ignored\n")
660       return
661     x = float(t.getAttribute("x"))
662     y = float(t.getAttribute("y"))
663     attr = self.parse_attributes(t)
664     self.out.write('<text pos="%g %g"' % (x,y))
665     self.out.write(' transformations="affine" valign="baseline"')
666     m = parse_transform(t)
667     if not m: m = Matrix()
668     m = m * Matrix([1, 0, 0, -1, x, y]) * Matrix([1, 0, 0, 1, -x, -y])
669     self.out.write(' matrix="%s"' % m)
670     if attr["font-size"]:
671       self.out.write(' size="%g"' % parse_float(attr["font-size"]))
672     color = parse_color(attr["fill"])
673     if color:
674       self.out.write(' stroke="%g %g %g"' % color)
675     self.text = ""
676     self.collect_text(t)
677     self.out.write('>%s</text>\n' % self.text.encode("UTF-8"))
678     
679   def node_image(self, node):
680     if not have_pil:
681       sys.stderr.write("No Python image library, <image> ignored\n")
682       return
683     href = node.getAttribute("xlink:href")
684     if not href.startswith("data:image/png;base64,"):
685       sys.stderr.write("Image ignored, href = %s...\n" % href[:40])
686       return
687     x = float(node.getAttribute("x"))
688     y = float(node.getAttribute("y"))
689     w = float(node.getAttribute("width"))
690     h = float(node.getAttribute("height"))
691     clipped = False
692     if node.hasAttribute("clip-path"):
693       mat = re.match("url\(#([^)]+)\).*", node.getAttribute("clip-path"))
694       if mat:
695         cp = mat.group(1)
696         if cp in self.defs and self.defs[cp][0] == "clipPath":
697           cp, m, path = self.defs[cp]
698           clipped = True
699           self.out.write('<group matrix="%s" clip="%s">\n' % (str(m), path))
700           self.out.write('<group matrix="%s">\n' % str(m.inverse()))
701     self.out.write('<image rect="%g %g %g %g"' % (x, y, x + w, y + h))
702     data = base64.b64decode(href[22:])
703     fin = cStringIO.StringIO(data)
704     image = Image.open(fin)
705     m = parse_transform(node)
706     if not m:   
707       m = Matrix()
708     m = m * Matrix([1, 0, 0, -1, x, y+h]) * Matrix([1, 0, 0, 1, -x, -y])
709     self.out.write(' matrix="%s"' % m)
710     self.out.write(' width="%d" height="%d" ColorSpace="DeviceRGB"' %
711                    image.size)
712     self.out.write(' BitsPerComponent="8" encoding="base64"> \n')
713     if True:
714       data = cStringIO.StringIO()
715       for pixel in image.getdata():
716         data.write("%c%c%c" % pixel[:3])
717       self.out.write(base64.b64encode(data.getvalue()))
718       data.close()
719     else:
720       count = 0
721       for pixel in image.getdata():
722         self.out.write("%02x%02x%02x" % pixel[:3])
723         count += 1
724         if count == 10:
725           self.out.write("\n")
726           count = 0
727     fin.close()
728     self.out.write('</image>\n')
729     if clipped:
730       self.out.write('</group>\n</group>\n')
731
732   # handled in def pass
733   def node_linearGradient(self, n):
734     pass 
735
736   def node_radialGradient(self, n):
737     pass 
738
739   def node_rect(self, n):
740     attr = self.parse_attributes(n)
741     self.out.write('<path')
742     m = parse_transform(n)
743     if m:   
744       self.out.write(' matrix="%s"' % m)
745     self.write_pathattributes(attr)
746     self.out.write('>\n')
747     x = float(n.getAttribute("x"))
748     y = float(n.getAttribute("y"))
749     w = float(n.getAttribute("width"))
750     h = float(n.getAttribute("height"))
751     self.out.write("%g %g m %g %g l %g %g l %g %g l h\n" %
752                    (x, y, x + w, y, x + w, y + h, x, y + h))
753     self.out.write('</path>\n')
754
755   def node_circle(self, n):
756     self.out.write('<path')
757     m = parse_transform(n)
758     if m:   
759       self.out.write(' matrix="%s"' % m)
760     attr = self.parse_attributes(n)
761     self.write_pathattributes(attr)
762     self.out.write('>\n')
763     cx = float(n.getAttribute("cx"))
764     cy = float(n.getAttribute("cy"))
765     r = float(n.getAttribute("r"))
766     self.out.write("%g 0 0 %g %g %g e\n" % (r, r, cx, cy))
767     self.out.write('</path>\n')
768
769   def node_ellipse(self, n):
770     self.out.write('<path')
771     m = parse_transform(n)
772     if m:   
773       self.out.write(' matrix="%s"' % m)
774     attr = self.parse_attributes(n)
775     self.write_pathattributes(attr)
776     self.out.write('>\n')
777     cx = 0
778     cy = 0
779     if n.hasAttribute("cx"):
780       cx = float(n.getAttribute("cx"))
781     if n.hasAttribute("cy"):
782       cy = float(n.getAttribute("cy"))
783     rx = float(n.getAttribute("rx"))
784     ry = float(n.getAttribute("ry"))
785     self.out.write("%g 0 0 %g %g %g e\n" % (rx, ry, cx, cy))
786     self.out.write('</path>\n')
787
788   def node_line(self, n):
789     self.out.write('<path')
790     m = parse_transform(n)
791     if m:   
792       self.out.write(' matrix="%s"' % m)
793     attr = self.parse_attributes(n)
794     self.write_pathattributes(attr)
795     self.out.write('>\n')
796     x1 = 0; y1 = 0; x2 = 0; y2 = 0
797     if n.hasAttribute("x1"):
798       x1 = float(n.getAttribute("x1"))
799     if n.hasAttribute("y1"):
800       y1 = float(n.getAttribute("y1"))
801     if n.hasAttribute("x2"):
802       x2 = float(n.getAttribute("x2"))
803     if n.hasAttribute("y2"):
804       y2 = float(n.getAttribute("y2"))
805     self.out.write("%g %g m %g %g l\n" % (x1, y1, x2, y2))
806     self.out.write('</path>\n')
807
808   def node_polyline(self, n):
809     self.polygon(n, closed=False)
810     
811   def node_polygon(self, n):
812     self.polygon(n, closed=True)
813
814   def polygon(self, n, closed):
815     self.out.write('<path')
816     m = parse_transform(n)
817     if m:   
818       self.out.write(' matrix="%s"' % m)
819     attr = self.parse_attributes(n)
820     self.write_pathattributes(attr)
821     self.out.write('>\n')
822     d = parse_list(n.getAttribute("points"))
823     op = "m"
824     while d:
825       x = float(d.pop(0))
826       y = float(d.pop(0))
827       self.out.write("%g %g %s\n" % (x, y, op))
828       op = "l"
829     if closed:
830       self.out.write("h\n")
831     self.out.write('</path>\n')
832
833   def node_path(self, n):
834     self.out.write('<path')
835     m = parse_transform(n)
836     if m:   
837       self.out.write(' matrix="%s"' % m)
838     attr = self.parse_attributes(n)
839     self.write_pathattributes(attr)
840     self.out.write('>\n')
841     d = n.getAttribute("d")
842     parse_path(self.out, d)
843     self.out.write('</path>\n')
844
845 # --------------------------------------------------------------------
846
847 def main():
848   if len(sys.argv) != 2 and len(sys.argv) != 3:
849     sys.stderr.write("Usage: svgtoipe <figure.svg> [ <figure.ipe> ]\n")
850     return
851   fname = sys.argv[1]
852   if len(sys.argv) > 2:
853     outname = sys.argv[2]
854   else:
855     if fname[-4:].lower() == ".svg":
856       outname = fname[:-4] + ".ipe"
857     else:
858       outname = fname + ".ipe"
859   svg = Svg(fname)
860   svg.parse_svg(outname)
861
862 if __name__ == '__main__':
863   main()
864
865 # --------------------------------------------------------------------