2004-01-18 Hans Breuer <hans@breuer.org>
[dia.git] / plug-ins / python / diasvg_import.py
blob42dcfa8e27a61dd11fee378a9423eb9d1e269097
1 # PyDia SVG Import
2 # Copyright (c) 2003, Hans Breuer <hans@breuer.org>
4 # Pure Python Dia Import Filter - to show how it is done.
5 # It also tries to be more featureful and robust then the
6 # SVG importer written in C, but as long as PyDia has issues
7 # this will _not_ be the case. Known issues (at least) :
8 # - Can't set 'bez_points' yet, requires support in PyDia and lib/bez*.c
9 # and here
10 # - xlink stuff (should probably have some StdProp equivalent)
11 # - total lack of transformation dealing
12 # - see FIXME in this file
14 # This program is free software; you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation; either version 2 of the License, or
17 # (at your option) any later version.
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
28 import string, math, os
30 # Dias unit is cm, the default scale should be determined from svg:width and viewBox
31 dfUserScale = 0.05
32 dictUnitScales = {
33 "em" : 0.03, "ex" : 0.03, #FIXME: these should be _relative_ to current font
34 "px" : 1.0, "pt" : 0.0352778, "pc" : 0.4233333,
35 "cm" : 1.0, "mm" : 10.0, "in" : 25.4}
37 def Scaled(s) :
38 # em, ex, px, pt, pc, cm, mm, in, and percentages
39 if s[-1] in string.digits :
40 # use global scale
41 return float(s) * dfUserScale
42 else :
43 unit = s[-2:]
44 try :
45 return float(s[:-2]) * dictUnitScales[unit]
46 except :
47 # warn about invalid unit ??
48 print "Unknown unit", s[:-2], s[-2:]
49 return float(s) * dfUserScale
50 def Color(s) :
51 # deliver a StdProp compatible Color (or the original string)
52 import re
53 r = re.compile(r"rgb\s*\(\s*(\d+)[, ]+(\d+)[, +](\d+)\s*\)")
54 m = r.match(s)
55 if m :
56 return (int(m.group(1)) / 255.0, int(m.group(2)) / 255.0, int(m.group(2)) / 255.0)
57 # any more ugly color definitions not compatible with pango_color_parse() ?
58 return string.strip(s)
59 class Object :
60 def __init__(self) :
61 self.props = {"x" : 0, "y" : 0}
62 # "line_width", "line_colour", "line_style"
63 def style(self, s) :
64 sp1 = string.split(s, ";")
65 for s1 in sp1 :
66 sp2 = string.split(string.strip(s1), ":")
67 if len(sp2) == 2 :
68 try :
69 eval("self." + string.replace(sp2[0], "-", "_") + "(\"" + string.strip(sp2[1]) + "\")")
70 except AttributeError :
71 self.props[sp2[0]] = string.strip(sp2[1])
72 def x(self, s) :
73 self.props["x"] = Scaled(s)
74 def y(self, s) :
75 self.props["y"] = Scaled(s)
76 def width(self, s) :
77 self.props["width"] = Scaled(s)
78 def height(self, s) :
79 self.props["height"] = Scaled(s)
80 def stroke(self,s) :
81 self.props["stroke"] = s.encode("UTF-8")
82 def stroke_width(self,s) :
83 self.props["stroke-width"] = Scaled(s)
84 def fill(self,s) :
85 self.props["fill"] = s
86 def fill_rule(self,s) :
87 self.props["fill-rule"] = s
88 def stroke_dasharray(self,s) :
89 # just an approximation
90 sp = string.split(s,",")
91 n = len(sp)
92 if n > 0 :
93 dlen = Scaled(sp[0])
94 if n == 0 : # can't really happen
95 self.props["line-style"] = (0, 1.0) # LINESTYLE_SOLID,
96 elif n == 2 :
97 if dlen > 0.1 : # FIXME:
98 self.props["line-style"] = (1, dlen) # LINESTYLE_DASHED,
99 else :
100 self.props["line-style"] = (4, dlen) # LINESTYLE_DOTTED
101 elif n == 4 :
102 self.props["line-style"] = (2, dlen) # LINESTYLE_DASH_DOT,
103 elif n == 6 :
104 self.props["line-style"] = (3, dlen) # LINESTYLE_DASH_DOT_DOT,
105 def id(self, s) :
106 # just to handle/ignore it
107 self.props["id"] = s
108 def __repr__(self) :
109 return self.dt + " : " + str(self.props)
110 def Dump(self, indent) :
111 print " " * indent, self
112 def Set(self, d) :
113 pass
114 def ApplyProps(self, o) :
115 pass
116 def Create(self) :
117 ot = dia.get_object_type (self.dt)
118 o, h1, h2 = ot.create(self.props["x"], self.props["y"])
119 # apply common props
120 if self.props.has_key("stroke-width") and o.properties.has_key("line_width") :
121 o.properties["line_width"] = self.props["stroke-width"]
122 if self.props.has_key("stroke") and o.properties.has_key("line_colour") :
123 if self.props["stroke"] != "none" :
124 try :
125 o.properties["line_colour"] = Color(self.props["stroke"])
126 except :
127 # rgb(192,27,38) handled by Color() but ...
128 # o.properties["line_colour"] = self.props["stroke"]
129 pass
130 else :
131 # Dia can't really display stroke none, some workaround :
132 o.properties["line_colour"] = Color(self.props["fill"])
133 o.properties["line_width"] = 0.0
134 if self.props.has_key("fill") and o.properties.has_key("fill_colour") :
135 if self.props["fill"] == "none" :
136 o.properties["show_background"] = 0
137 else :
138 o.properties["show_background"] = 1
139 try :
140 o.properties["fill_colour"] =Color(self.props["fill"])
141 except :
142 # rgb(192,27,38) handled by Color() but ...
143 # o.properties["fill_colour"] =self.props["fill"]
144 pass
145 if self.props.has_key("line-style") and o.properties.has_key("line_style") :
146 o.properties["line_style"] = self.props["line-style"]
147 self.ApplyProps(o)
148 return o
150 class Svg(Object) :
151 # not a placeable object but similar while parsing
152 def __init__(self) :
153 Object.__init__(self)
154 self.dt = "svg"
155 self.bbox_w = None
156 self.bbox_h = None
157 def width(self,s) :
158 global dfUserScale
159 d = dfUserScale
160 dfUserScale = 0.05
161 self.bbox_w = Scaled(s)
162 self.props["width"] = self.bbox_w
163 dfUserScale = d
164 def height(self,s) :
165 global dfUserScale
166 d = dfUserScale
167 # with stupid info Dia still has a problem cause zooming is limited to 5.0%
168 dfUserScale = 0.05
169 self.bbox_h = Scaled(s)
170 self.props["height"] = self.bbox_h
171 dfUserScale = d
172 def viewBox(self,s) :
173 global dfUserScale
174 self.props["viewBox"] = s
175 sp = string.split(s, " ")
176 w = float(sp[2]) - float(sp[0])
177 h = float(sp[3]) - float(sp[1])
178 # FIXME: the following relies on the call order of width,height,viewBox
179 # which is _not_ the order it is in the file
180 if self.bbox_w and self.bbox_h :
181 dfUserScale = math.sqrt((self.bbox_w / w)*(self.bbox_h / h))
182 elif self.bbox_w :
183 dfUserScale = self.bbox_w / w
184 elif self.bbox_h :
185 dfUserScale = self.bbox_h / h
186 def xmlns(self,s) :
187 self.props["xmlns"] = s
188 def version(self,s) :
189 self.props["version"] = s
190 def __repr__(self) :
191 global dfUserScale
192 return Object.__repr__(self) + "\nUserScale : " + str(dfUserScale)
193 def Create(self) :
194 return None
195 class Style(Object) :
196 # the beginning of a css implementation, currently only hiding it ...
197 def __init__(self) :
198 Object.__init__(self)
199 def type(self, s) :
200 self.props["type"] = s
201 def Create(self) :
202 return None
203 class Group(Object) :
204 def __init__(self) :
205 Object.__init__(self)
206 self.dt = "Group"
207 self.childs = []
208 def Add(self, o) :
209 self.childs.append(o)
210 def Create(self) :
211 lst = []
212 for o in self.childs :
213 od = o.Create()
214 if od :
215 #print od
216 #DON'T : layer.add_object(od)
217 lst.append(od)
218 # create group including list objects
219 if len(lst) > 0 :
220 return dia.group_create(lst)
221 else :
222 return None
223 def Dump(self, indent) :
224 print " " * indent, self
225 for o in self.childs :
226 o.Dump(indent + 1)
227 def CopyProps(self, dest) :
228 # to be used to inherit group props to childs _before_ they get their own
229 for p in self.props.keys() :
230 sf = "dest." + string.replace(p, "-", "_") + "(\"" + str(self.props[p]) + "\")"
231 try : # accessor first
232 eval(sf)
233 except :
234 dest.props[p] = self.props[p]
236 # One of my test files is quite ugly (produced by Batik) : it dumps identical image data
237 # multiple times into the svg. This directory helps to reduce them to the necessary
238 # memory comsumption
239 _imageData = {}
241 class Image(Object) :
242 def __init__(self) :
243 Object.__init__(self)
244 self.dt = "Standard - Image"
245 def preserveAspectRatio(self,s) :
246 self.props["keep_aspect"] = s
247 def xlink__href(self,s) :
248 #print s
249 if s[:8] == "file:///" :
250 self.props["uri"] = s.encode("UTF-8")
251 elif s[:22] == "data:image/png;base64," :
252 if _imageData.has_key(s[22:]) :
253 self.props["uri"] = _imageData[s[22:]] # use file reference
254 else :
255 # an ugly temporary file name, on windoze in %TEMP%
256 fname = os.tempnam(None, "diapy-") + ".png"
257 dd = s[22:].decode ("base64")
258 f = open(fname, "wb")
259 f.write(dd)
260 f.close()
261 # not really an uri but the reader appears to be robust enough ;-)
262 _imageData[s[22:]] = "file:///" + fname
263 else :
264 pass #FIXME how to import data into dia ??
265 def Create(self) :
266 if not (self.props.has_key("uri") or self.props.has_key("data")) :
267 return None
268 return Object.Create(self)
269 def ApplyProps(self,o) :
270 if self.props.has_key("width") :
271 o.properties["elem_width"] = self.props["width"]
272 if self.props.has_key("width") :
273 o.properties["elem_height"] = self.props["height"]
274 if self.props.has_key("uri") :
275 o.properties["image_file"] = self.props["uri"][8:]
276 class Line(Object) :
277 def __init__(self) :
278 Object.__init__(self)
279 self.dt = "Standard - Line"
280 # "line_width". "line_color"
281 # "start_point". "end_point"
282 def x1(self, s) :
283 self.props["x"] = Scaled(s)
284 def y1(self, s) :
285 self.props["y"] = Scaled(s)
286 def x2(self, s) :
287 self.props["x2"] = Scaled(s)
288 def y2(self, s) :
289 self.props["y2"] = Scaled(s)
290 def ApplyProps(self, o) :
291 #pass
292 o.properties["end_point"] = (self.props["x2"], self.props["y2"])
293 class Path(Object) :
294 def __init__(self) :
295 Object.__init__(self)
296 self.dt = "Standard - BezierLine" # or Beziergon ?
297 self.pts = []
298 def d(self, s) :
299 self.props["data"] = s
300 #FIXME: parse more - e.g. AQT - of the strange path data
301 import re
302 rw = re.compile("[MmLlCcSsz]") # what
303 rd = re.compile("[^MmLlCcSsz]+") # data
304 rv = re.compile("[\s,]+") # values
305 spd = rw.split(s)
306 spw = rd.split(s)
307 i = 1
308 # current point
309 xc = 0.0; yc = 0.0 # the current or second control point - ugly svg states ;(
310 for s1 in spw :
311 k = 0 # range further adjusted for last possibly empty -k-1
312 if s1 == "M" : # moveto
313 sp = rv.split(spd[i])
314 if sp[0] == "" : k = 1
315 xc = Scaled(sp[k]); yc = Scaled(sp[k+1])
316 self.pts.append((0, xc, yc))
317 elif s1 == "L" : #lineto
318 sp = rv.split(spd[i])
319 if sp[0] == "" : k = 1
320 for j in range(k, len(sp)-k-1, 2) :
321 xc = Scaled(sp[j]); yc = Scaled(sp[j+1])
322 self.pts.append((1, xc, yc))
323 elif s1 == "C" : # curveto
324 sp = rv.split(spd[i])
325 if sp[0] == "" : k = 1
326 for j in range(k, len(sp)-k-1, 6) :
327 self.pts.append((2, Scaled(sp[j]), Scaled(sp[j+1]),
328 Scaled(sp[j+2]), Scaled(sp[j+3]),
329 Scaled(sp[j+4]), Scaled(sp[j+5])))
330 # reflexion second control to current point, really ?
331 xc =2 * Scaled(sp[j+4]) - Scaled(sp[j+2])
332 yc =2 * Scaled(sp[j+5]) - Scaled(sp[j+3])
333 elif s1 == "S" : # smooth curveto
334 sp = rv.split(spd[i])
335 if sp[0] == "" : k = 1
336 for j in range(k, len(sp)-k-1, 4) :
337 x = Scaled(sp[j+2])
338 y = Scaled(sp[j+3])
339 x1 = Scaled(sp[j])
340 y1 = Scaled(sp[j+1])
341 self.pts.append((2, xc, yc, # FIXME: current point ?
342 x1, y1,
343 x, y))
344 xc = 2 * x - x1; yc = 2 * y - y1
345 elif s1 == "z" : # close
346 self.dt = "Standard - Beziergon"
347 elif s1 == "" : # too much whitespaces ;-)
348 pass
349 else :
350 print "Huh?", s1
351 break
352 i += 1
353 def ApplyProps(self,o) :
354 o.properties["bez_points"] = self.pts
355 def Dump(self, indent) :
356 print " " * indent, self
357 for t in self.pts :
358 print " " * indent, t
359 #def Create(self) :
360 # return None # not yet
361 class Rect(Object) :
362 def __init__(self) :
363 Object.__init__(self)
364 self.dt = "Standard - Box"
365 # "corner_radius",
366 def ApplyProps(self,o) :
367 o.properties["elem_width"] = self.props["width"]
368 o.properties["elem_height"] = self.props["height"]
369 class Ellipse(Object) :
370 def __init__(self) :
371 Object.__init__(self)
372 self.dt = "Standard - Ellipse"
373 self.props["cx"] = 0
374 self.props["cy"] = 0
375 self.props["rx"] = 1
376 self.props["ry"] = 1
377 def cx(self,s) :
378 self.props["cx"] = Scaled(s)
379 self.props["x"] = self.props["cx"] - self.props["rx"]
380 def cy(self,s) :
381 self.props["cy"] = Scaled(s)
382 self.props["y"] = self.props["cy"] - self.props["ry"]
383 def rx(self,s) :
384 self.props["rx"] = Scaled(s)
385 self.props["x"] = self.props["cx"] - self.props["rx"]
386 def ry(self,s) :
387 self.props["ry"] = Scaled(s)
388 self.props["y"] = self.props["cy"] - self.props["ry"]
389 def ApplyProps(self,o) :
390 o.properties["elem_width"] = 2.0 * self.props["rx"]
391 o.properties["elem_height"] = 2.0 * self.props["ry"]
392 class Circle(Ellipse) :
393 def __init__(self) :
394 Ellipse.__init__(self)
395 def r(self,s) :
396 Ellipse.rx(self,s)
397 Ellipse.ry(self,s)
398 class Poly(Object) :
399 def __init__(self) :
400 Object.__init__(self)
401 self.dt = None # abstract class !
402 def points(self,s) :
403 sp1 = string.split(s)
404 pts = []
405 for s1 in sp1 :
406 sp2 = string.split(s1, ",")
407 if len(sp2) == 2 :
408 pts.append((Scaled(sp2[0]), Scaled(sp2[1])))
409 self.props["points"] = pts
410 def ApplyProps(self,o) :
411 o.properties["poly_points"] = self.props["points"]
412 class Polygon(Poly) :
413 def __init__(self) :
414 Poly.__init__(self)
415 self.dt = "Standard - Polygon"
416 class Polyline(Poly) :
417 def __init__(self) :
418 Poly.__init__(self)
419 self.dt = "Standard - PolyLine"
420 class Text(Object) :
421 def __init__(self) :
422 Object.__init__(self)
423 self.dt = "Standard - Text"
424 # text_font, text_height, text_color, text_alignment
425 def Set(self, d) :
426 if self.props.has_key("text") :
427 self.props["text"] += d
428 else :
429 self.props["text"] = d
430 def text_anchor(self,s) :
431 self.props["text-anchor"] = s
432 def font_size(self,s) :
433 self.props["font-size"] = Scaled(s)
434 # ?? self.props["y"] = self.props["y"] - Scaled(s)
435 def font_weight(self, s) :
436 self.props["font-weight"] = s
437 def font_style(self, s) :
438 self.props["font-style"] = s
439 def font_family(self, s) :
440 self.props["font-family"] = s
441 def ApplyProps(self, o) :
442 o.properties["text"] = self.props["text"].encode("UTF-8")
443 if self.props.has_key("text-anchor") :
444 if self.props["text-anchor"] == "middle" : o.properties["text_alignment"] = 1
445 elif self.props["text-anchor"] == "end" : o.properties["text_alignment"] = 2
446 else : o.properties["text_alignment"] = 0
447 if self.props.has_key("fill") :
448 o.properties["text_colour"] = self.props["fill"]
449 if self.props.has_key("font-size") :
450 o.properties["text_height"] = self.props["font-size"]
451 class Desc(Object) :
452 #FIXME is this useful ?
453 def __init__(self) :
454 Object.__init__(self)
455 self.dt = "UML - Note"
456 def Set(self, d) :
457 if self.props.has_key("text") :
458 self.props["text"] += d
459 else :
460 self.props["text"] = d
461 def Create(self) :
462 if self.props.has_key("text") :
463 dia.message(0, self.props["text"].encode("UTF-8"))
464 return None
465 class Title(Object) :
466 #FIXME is this useful ?
467 def __init__(self) :
468 Object.__init__(self)
469 self.dt = "UML - LargePackage"
470 def Set(self, d) :
471 if self.props.has_key("text") :
472 self.props["text"] += d
473 else :
474 self.props["text"] = d
475 def Create(self) :
476 if self.props.has_key("text") :
477 pass
478 return None
479 class Unknown(Object) :
480 def __init__(self, name) :
481 Object.__init__(self)
482 self.dt = "svg:" + name
483 def Create(self) :
484 return None
486 class Importer :
487 def __init__(self) :
488 self.errors = {}
489 self.objects = []
490 def Parse(self, sData) :
491 import xml.parsers.expat
492 ctx = []
493 stack = []
494 # 3 handler functions
495 def start_element(name, attrs) :
496 #print "<" + name + ">"
497 if 0 == string.find(name, "svg:") :
498 name = name[4:]
499 if len(stack) > 0 :
500 grp = stack[-1]
501 else :
502 grp = None
503 if 'g' == name :
504 o = Group()
505 stack.append(o)
506 else :
507 s = string.capitalize(name) + "()"
508 try :
509 o = eval(s)
510 except :
511 o = Unknown(name)
512 if grp :
513 grp.CopyProps(o)
514 for a in attrs :
515 if a == "class" : # eeek : keyword !
516 o.props[a] = attrs[a]
517 continue
518 ma = string.replace(a, "-", "_")
519 # e.g. xlink:href -> xlink__href
520 ma = string.replace(ma, ":", "__")
521 s = "o." + ma + "(\"" + attrs[a] + "\")"
522 try :
523 eval(s)
524 except AttributeError, msg :
525 if not self.errors.has_key(s) :
526 self.errors[s] = msg
527 except SyntaxError, msg :
528 if not self.errors.has_key(s) :
529 self.errors[s] = msg
530 if grp is None :
531 self.objects.append(o)
532 else :
533 grp.Add(o)
534 ctx.append((name, o)) #push
535 def end_element(name) :
536 if 'g' == name :
537 del stack[-1]
538 del ctx[-1] # pop
539 def char_data(data):
540 # may be called multiple times for one string
541 if ctx[-1][0] == "text" :
542 ctx[-1][1].Set(data)
544 p = xml.parsers.expat.ParserCreate()
545 p.StartElementHandler = start_element
546 p.EndElementHandler = end_element
547 p.CharacterDataHandler = char_data
549 p.Parse(sData)
551 def Render(self,data) :
552 layer = data.active_layer
553 for o in self.objects :
554 od = o.Create()
555 if od :
556 layer.add_object(od)
557 # create an 'Unhandled' layer and dump our Unknown
558 # create an 'Errors' layer and dump our errors
559 if len(self.errors.keys()) > 0 :
560 layer = data.add_layer("Errors")
561 s = "To hide the error messages delete or disable the 'Errors' layer\n"
562 for e in self.errors.keys() :
563 s = s + e + " -> " + str(self.errors[e]) + "\n"
564 o = Text()
565 o.props["fill"] = "red"
566 o.Set(s)
567 layer.add_object(o.Create())
568 # create a 'Description' layer
569 data.update_extents ()
570 return 1
571 def Dump(self) :
572 for o in self.objects :
573 o.Dump(0)
574 for e in self.errors.keys() :
575 print e, "->", self.errors[e]
577 def Test() :
578 import sys
579 imp = Importer()
580 sName = sys.argv[1]
581 if sName[-1] == "z" :
582 import gzip
583 f = gzip.open(sName)
584 else :
585 f = open(sName)
586 imp.Parse(f.read())
587 if len(sys.argv) > 2 :
588 sys.stdout = open(sys.argv[2], "wb")
589 imp.Dump()
590 sys.exit(0)
592 if __name__ == '__main__': Test()
594 def import_svg(sFile, diagramData) :
595 imp = Importer()
596 f = open(sFile)
597 imp.Parse(f.read())
598 return imp.Render(diagramData)
600 def import_svgz(sFile, diagramData) :
601 import gzip
602 imp = Importer()
603 f = gzip.open(sFile)
604 imp.Parse(f.read())
605 return imp.Render(diagramData)
607 import dia
608 dia.register_import("SVG plain", "svg", import_svg)
609 dia.register_import("SVG compressed", "svgz", import_svgz)