1 # Parse an xml file into a canvas group element
3 # @param group The canvas.Group instance to append the parsed elements to
4 # @param path The path of the svg file (absolute or relative to FG_ROOT)
5 # @param options Optional hash of options
6 var parsesvg = func(group, path, options = nil)
8 if( !isa(group, Group) )
9 die("Invalid argument group (type != Group)");
14 if( typeof(options) != "hash" )
15 die("Options need to be of type hash!");
17 var custom_font_mapper = options['font-mapper'];
18 var font_mapper = func(family, weight)
20 if( typeof(custom_font_mapper) == 'func' )
22 var font = custom_font_mapper(family, weight);
27 return "LiberationFonts/LiberationMono-Bold.ttf";
33 var close_stack = []; # helper for check tag closing
35 # lookup table for element ids (for <use> element)
38 # ----------------------------------------------------------------------------
39 # Create a new child an push it onto the stack
40 var pushElement = func(type, id = nil)
42 append(stack, stack[-1].createChild(type, id));
43 append(close_stack, level);
45 if( typeof(id) == 'scalar' and size(id) )
46 id_dict[ id ] = stack[-1];
49 # ----------------------------------------------------------------------------
50 # Parse a transformation (matrix)
51 # http://www.w3.org/TR/SVG/coords.html#TransformAttribute
52 var parseTransform = func(tf)
57 tf = std.string.new(tf);
62 var start_type = tf.find_first_not_of("\t\n ", end);
66 var end_type = tf.find_first_of("(\t\n ", start_type + 1);
70 var start_args = tf.find('(', end_type);
78 var start_num = tf.find_first_not_of(",\t\n ", end + 1);
81 if( tf[start_num] == ')' )
84 end = tf.find_first_of("),\t\n ", start_num + 1);
87 append(values, tf.substr(start_num, end - start_num));
90 var type = tf.substr(start_type, end_type - start_type);
92 if( type == "translate" )
93 # translate(<tx> [<ty>]), which specifies a translation by tx and ty. If
94 # <ty> is not provided, it is assumed to be zero.
95 stack[-1].createTransform().setTranslation
98 size(values) > 1 ? values[1] : 0,
100 else if( type == "matrix" )
102 if( size(values) == 6 )
103 stack[-1].createTransform(values);
105 debug.dump('invalid transform', type, values);
108 debug.dump(['unknown transform', type, values]);
112 # ----------------------------------------------------------------------------
114 # http://www.w3.org/TR/SVG/paths.html#PathData
116 # map svg commands OpenVG commands
118 z: Path.VG_CLOSE_PATH,
129 var parsePath = func(d)
134 var path_data = std.string.new(d);
142 # skip trailing spaces
143 pos = path_data.find_first_not_of("\t\n ", pos);
148 var cmd = path_data.substr(pos, 1);
151 # and get all following arguments
155 pos = path_data.find_first_not_of(",\t\n ", pos);
160 pos = path_data.find_first_not_of("e-.0123456789", start_num);
161 if( start_num == pos )
164 append(args, path_data.substr(start_num, pos > 0 ? pos - start_num : nil));
167 # now execute the command
168 var rel = string.islower(cmd[0]);
169 var cmd = string.lc(cmd);
172 for(var i = 0; i + 7 <= size(args); i += 7)
174 # SVG: (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+
175 # OpenVG: rh,rv,rot,x0,y0
177 var cmd_vg = args[i + 4] ? Path.VG_LCCWARC_TO : Path.VG_LCWARC_TO;
179 var cmd_vg = args[i + 4] ? Path.VG_SCCWARC_TO : Path.VG_SCWARC_TO;
180 append(cmds, rel ? cmd_vg + 1: cmd_vg);
181 append(coords, args[i],
188 if( math.mod(size(args), 7) > 0 )
189 debug.dump('too much coords for cmd', cmd, args);
193 var cmd_vg = cmd_map[cmd];
196 debug.dump('command not found', cmd, args);
200 var num_coords = Path.num_coords[int(cmd_vg)];
201 if( num_coords == 0 )
202 append(cmds, cmd_vg);
205 for(var i = 0; i + num_coords <= size(args); i += num_coords)
207 append(cmds, rel ? cmd_vg + 1: cmd_vg);
208 for(var j = i; j < i + num_coords; j += 1)
209 append(coords, args[j]);
211 # If a moveto is followed by multiple pairs of coordinates, the
212 # subsequent pairs are treated as implicit lineto commands.
214 cmd_vg = cmd_map['l'];
217 if( math.mod(size(args), num_coords) > 0 )
218 debug.warn('too much coords for cmd: ' ~ cmd);
223 stack[-1].setData(cmds, coords);
226 # ----------------------------------------------------------------------------
227 # Parse a css style attribute
228 var parseStyle = func(style)
234 foreach(var part; split(';', style))
236 if( !size(part = string.trim(part)) )
238 if( size(part = split(':',part)) != 2 )
241 var key = string.trim(part[0]);
245 var value = string.trim(part[1]);
255 # ----------------------------------------------------------------------------
257 var parseColor = func(s)
259 var color = [0, 0, 0];
263 if( size(s) == 7 and substr(s, 0, 1) == '#' )
265 return [ std.stoul(substr(s, 1, 2), 16) / 255,
266 std.stoul(substr(s, 3, 2), 16) / 255,
267 std.stoul(substr(s, 5, 2), 16) / 255 ];
273 # ----------------------------------------------------------------------------
274 # XML parsers element open callback
275 var start = func(name, attr)
285 die("Not an svg file (root=" ~ name ~ ")");
290 var style = parseStyle(attr['style']);
292 if( style['display'] == 'none' )
297 else if( name == "g" )
299 pushElement('group', attr['id']);
301 else if( name == "text" )
303 pushElement('text', attr['id']);
304 stack[-1].setTranslation(attr['x'], attr['y']);
306 # http://www.w3.org/TR/SVG/text.html#TextAnchorProperty
307 var h_align = style["text-anchor"];
308 if( h_align == "end" )
310 else if( h_align == "middle" )
314 stack[-1].setAlignment(h_align ~ "-baseline");
315 # TODO vertical align
317 stack[-1].setColor(parseColor(style['fill']));
320 font_mapper(style["font-family"], style["font-weight"])
323 var font_size = style["font-size"];
324 if( font_size != nil )
325 # eg. font-size: 123px
326 stack[-1].setFontSize(substr(font_size, 0, size(font_size) - 2));
328 else if( name == "path" or name == "rect" )
330 pushElement('path', attr['id']);
335 var width = attr['width'];
336 var height = attr['height'];
340 d = sprintf("M%f,%f v%f h%f v%fz", x, y, height, width, -height);
345 var w = style['stroke-width'];
346 stack[-1].setStrokeLineWidth( w != nil ? w : 1 );
347 stack[-1].setColor(parseColor(style['stroke']));
349 var linecap = style['stroke-linecap'];
351 stack[-1].setStrokeLineCap(style['stroke-linecap']);
353 var fill = style['fill'];
354 if( fill != nil and fill != "none" )
355 stack[-1].setColorFill(parseColor(fill));
357 # http://www.w3.org/TR/SVG/painting.html#StrokeDasharrayProperty
358 var dash = style['stroke-dasharray'];
359 if( dash and size(dash) > 3 )
360 # at least 2 comma separated values...
361 stack[-1].setStrokeDashArray(split(',', dash));
363 var cx = attr['inkscape:transform-center-x'];
364 var cy = attr['inkscape:transform-center-y'];
365 if( cx != nil or cy != nil )
366 stack[-1].setCenter(cx or 0, -(cy or 0));
368 else if( name == "tspan" )
372 else if( name == "use" )
374 var ref = attr["xlink:href"];
375 if( ref == nil or size(ref) < 2 or ref[0] != `#` )
376 return debug.dump("Invalid or missing href", ref);
378 var el_src = id_dict[ substr(ref, 1) ];
380 return print("parsesvg: Reference to unknown element (" ~ ref ~ ")");
382 # Create new element and copy sub branch from source node
383 pushElement(el_src._node.getName(), attr['id']);
384 props.copy(el_src._node, stack[-1]._node);
386 # copying also overrides the id so we need to set it again
387 stack[-1]._node.getNode("id").setValue(attr['id']);
391 print("parsesvg: skipping unknown element '" ~ name ~ "'");
396 parseTransform(attr['transform']);
399 # XML parsers element close callback
411 if( size(close_stack) and (level + 1) == close_stack[-1] )
418 # XML parsers element data callback
419 var data = func(data)
424 if( size(data) and isa(stack[-1], Text) )
425 stack[-1].setText(data);
429 path = getprop("/sim/fg-root") ~ "/" ~ path;
431 call(func parsexml(path, start, end, data), nil, var err = []);