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
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
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}
38 # em, ex, px, pt, pc, cm, mm, in, and percentages
39 if s
[-1] in string
.digits
:
41 return float(s
) * dfUserScale
45 return float(s
[:-2]) * dictUnitScales
[unit
]
47 # warn about invalid unit ??
48 print "Unknown unit", s
[:-2], s
[-2:]
49 return float(s
) * dfUserScale
51 # deliver a StdProp compatible Color (or the original string)
53 r
= re
.compile(r
"rgb\s*\(\s*(\d+)[, ]+(\d+)[, +](\d+)\s*\)")
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
)
61 self
.props
= {"x" : 0, "y" : 0}
62 # "line_width", "line_colour", "line_style"
64 sp1
= string
.split(s
, ";")
66 sp2
= string
.split(string
.strip(s1
), ":")
69 eval("self." + string
.replace(sp2
[0], "-", "_") + "(\"" + string
.strip(sp2
[1]) + "\")")
70 except AttributeError :
71 self
.props
[sp2
[0]] = string
.strip(sp2
[1])
73 self
.props
["x"] = Scaled(s
)
75 self
.props
["y"] = Scaled(s
)
77 self
.props
["width"] = Scaled(s
)
79 self
.props
["height"] = Scaled(s
)
81 self
.props
["stroke"] = s
.encode("UTF-8")
82 def stroke_width(self
,s
) :
83 self
.props
["stroke-width"] = Scaled(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
,",")
94 if n
== 0 : # can't really happen
95 self
.props
["line-style"] = (0, 1.0) # LINESTYLE_SOLID,
97 if dlen
> 0.1 : # FIXME:
98 self
.props
["line-style"] = (1, dlen
) # LINESTYLE_DASHED,
100 self
.props
["line-style"] = (4, dlen
) # LINESTYLE_DOTTED
102 self
.props
["line-style"] = (2, dlen
) # LINESTYLE_DASH_DOT,
104 self
.props
["line-style"] = (3, dlen
) # LINESTYLE_DASH_DOT_DOT,
106 # just to handle/ignore it
109 return self
.dt
+ " : " + str(self
.props
)
110 def Dump(self
, indent
) :
111 print " " * indent
, self
114 def ApplyProps(self
, o
) :
117 ot
= dia
.get_object_type (self
.dt
)
118 o
, h1
, h2
= ot
.create(self
.props
["x"], self
.props
["y"])
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" :
125 o
.properties
["line_colour"] = Color(self
.props
["stroke"])
127 # rgb(192,27,38) handled by Color() but ...
128 # o.properties["line_colour"] = self.props["stroke"]
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
138 o
.properties
["show_background"] = 1
140 o
.properties
["fill_colour"] =Color(self
.props
["fill"])
142 # rgb(192,27,38) handled by Color() but ...
143 # o.properties["fill_colour"] =self.props["fill"]
145 if self
.props
.has_key("line-style") and o
.properties
.has_key("line_style") :
146 o
.properties
["line_style"] = self
.props
["line-style"]
151 # not a placeable object but similar while parsing
153 Object
.__init
__(self
)
161 self
.bbox_w
= Scaled(s
)
162 self
.props
["width"] = self
.bbox_w
167 # with stupid info Dia still has a problem cause zooming is limited to 5.0%
169 self
.bbox_h
= Scaled(s
)
170 self
.props
["height"] = self
.bbox_h
172 def viewBox(self
,s
) :
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
))
183 dfUserScale
= self
.bbox_w
/ w
185 dfUserScale
= self
.bbox_h
/ h
187 self
.props
["xmlns"] = s
188 def version(self
,s
) :
189 self
.props
["version"] = s
192 return Object
.__repr
__(self
) + "\nUserScale : " + str(dfUserScale
)
195 class Style(Object
) :
196 # the beginning of a css implementation, currently only hiding it ...
198 Object
.__init
__(self
)
200 self
.props
["type"] = s
203 class Group(Object
) :
205 Object
.__init
__(self
)
209 self
.childs
.append(o
)
212 for o
in self
.childs
:
216 #DON'T : layer.add_object(od)
218 # create group including list objects
220 return dia
.group_create(lst
)
223 def Dump(self
, indent
) :
224 print " " * indent
, self
225 for o
in self
.childs
:
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
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
241 class Image(Object
) :
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
) :
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
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")
261 # not really an uri but the reader appears to be robust enough ;-)
262 _imageData
[s
[22:]] = "file:///" + fname
264 pass #FIXME how to import data into dia ??
266 if not (self
.props
.has_key("uri") or self
.props
.has_key("data")) :
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:]
278 Object
.__init
__(self
)
279 self
.dt
= "Standard - Line"
280 # "line_width". "line_color"
281 # "start_point". "end_point"
283 self
.props
["x"] = Scaled(s
)
285 self
.props
["y"] = Scaled(s
)
287 self
.props
["x2"] = Scaled(s
)
289 self
.props
["y2"] = Scaled(s
)
290 def ApplyProps(self
, o
) :
292 o
.properties
["end_point"] = (self
.props
["x2"], self
.props
["y2"])
295 Object
.__init
__(self
)
296 self
.dt
= "Standard - BezierLine" # or Beziergon ?
299 self
.props
["data"] = s
300 #FIXME: parse more - e.g. AQT - of the strange path data
302 rw
= re
.compile("[MmLlCcSsz]") # what
303 rd
= re
.compile("[^MmLlCcSsz]+") # data
304 rv
= re
.compile("[\s,]+") # values
309 xc
= 0.0; yc
= 0.0 # the current or second control point - ugly svg states ;(
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) :
341 self
.pts
.append((2, xc
, yc
, # FIXME: current point ?
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 ;-)
353 def ApplyProps(self
,o
) :
354 o
.properties
["bez_points"] = self
.pts
355 def Dump(self
, indent
) :
356 print " " * indent
, self
358 print " " * indent
, t
360 # return None # not yet
363 Object
.__init
__(self
)
364 self
.dt
= "Standard - Box"
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
) :
371 Object
.__init
__(self
)
372 self
.dt
= "Standard - Ellipse"
378 self
.props
["cx"] = Scaled(s
)
379 self
.props
["x"] = self
.props
["cx"] - self
.props
["rx"]
381 self
.props
["cy"] = Scaled(s
)
382 self
.props
["y"] = self
.props
["cy"] - self
.props
["ry"]
384 self
.props
["rx"] = Scaled(s
)
385 self
.props
["x"] = self
.props
["cx"] - self
.props
["rx"]
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
) :
394 Ellipse
.__init
__(self
)
400 Object
.__init
__(self
)
401 self
.dt
= None # abstract class !
403 sp1
= string
.split(s
)
406 sp2
= string
.split(s1
, ",")
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
) :
415 self
.dt
= "Standard - Polygon"
416 class Polyline(Poly
) :
419 self
.dt
= "Standard - PolyLine"
422 Object
.__init
__(self
)
423 self
.dt
= "Standard - Text"
424 # text_font, text_height, text_color, text_alignment
426 if self
.props
.has_key("text") :
427 self
.props
["text"] += d
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"]
452 #FIXME is this useful ?
454 Object
.__init
__(self
)
455 self
.dt
= "UML - Note"
457 if self
.props
.has_key("text") :
458 self
.props
["text"] += d
460 self
.props
["text"] = d
462 if self
.props
.has_key("text") :
463 dia
.message(0, self
.props
["text"].encode("UTF-8"))
465 class Title(Object
) :
466 #FIXME is this useful ?
468 Object
.__init
__(self
)
469 self
.dt
= "UML - LargePackage"
471 if self
.props
.has_key("text") :
472 self
.props
["text"] += d
474 self
.props
["text"] = d
476 if self
.props
.has_key("text") :
479 class Unknown(Object
) :
480 def __init__(self
, name
) :
481 Object
.__init
__(self
)
482 self
.dt
= "svg:" + name
490 def Parse(self
, sData
) :
491 import xml
.parsers
.expat
494 # 3 handler functions
495 def start_element(name
, attrs
) :
496 #print "<" + name + ">"
497 if 0 == string
.find(name
, "svg:") :
507 s
= string
.capitalize(name
) + "()"
515 if a
== "class" : # eeek : keyword !
516 o
.props
[a
] = attrs
[a
]
518 ma
= string
.replace(a
, "-", "_")
519 # e.g. xlink:href -> xlink__href
520 ma
= string
.replace(ma
, ":", "__")
521 s
= "o." + ma
+ "(\"" + attrs
[a
] + "\")"
524 except AttributeError, msg
:
525 if not self
.errors
.has_key(s
) :
527 except SyntaxError, msg
:
528 if not self
.errors
.has_key(s
) :
531 self
.objects
.append(o
)
534 ctx
.append((name
, o
)) #push
535 def end_element(name
) :
540 # may be called multiple times for one string
541 if ctx
[-1][0] == "text" :
544 p
= xml
.parsers
.expat
.ParserCreate()
545 p
.StartElementHandler
= start_element
546 p
.EndElementHandler
= end_element
547 p
.CharacterDataHandler
= char_data
551 def Render(self
,data
) :
552 layer
= data
.active_layer
553 for o
in self
.objects
:
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"
565 o
.props
["fill"] = "red"
567 layer
.add_object(o
.Create())
568 # create a 'Description' layer
569 data
.update_extents ()
572 for o
in self
.objects
:
574 for e
in self
.errors
.keys() :
575 print e
, "->", self
.errors
[e
]
581 if sName
[-1] == "z" :
587 if len(sys
.argv
) > 2 :
588 sys
.stdout
= open(sys
.argv
[2], "wb")
592 if __name__
== '__main__': Test()
594 def import_svg(sFile
, diagramData
) :
598 return imp
.Render(diagramData
)
600 def import_svgz(sFile
, diagramData
) :
605 return imp
.Render(diagramData
)
608 dia
.register_import("SVG plain", "svg", import_svg
)
609 dia
.register_import("SVG compressed", "svgz", import_svgz
)