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 # resolve paths using standard SimGear logic
18 var file_path = resolvepath(path);
20 die("File not found: "~path);
23 var _printlog = printlog;
24 var printlog = func(level, msg)
26 _printlog(level, "parsesvg: " ~ msg ~ " [path='" ~ path ~ "']");
29 var custom_font_mapper = options['font-mapper'];
30 var font_mapper = func(family, weight, style)
32 if( typeof(custom_font_mapper) == 'func' )
34 var font = custom_font_mapper(family, weight, style);
39 if( string.match(family,"Liberation*") ) {
40 style = style == "italic" ? "Italic" : "";
41 weight = weight == "bold" ? "Bold" : "";
43 var s = weight ~ style;
44 if( s == "" ) s = "Regular";
46 return "LiberationFonts/" ~ string.replace(family," ", "") ~ "-" ~ s ~ ".ttf";
50 return "LiberationFonts/LiberationMono-Bold.ttf";
53 # Helper to get number without unit (eg. px)
54 var evalCSSNum = func(css_num)
56 if( css_num.ends_with("px") )
57 return substr(css_num, 0, size(css_num) - 2);
58 else if( css_num.ends_with("%") )
59 return substr(css_num, 0, size(css_num) - 1) / 100;
67 var close_stack = []; # helper for check tag closing
74 # lookup table for element ids (for <use> element)
77 # lookup table for mask and clipPath element ids
81 # ----------------------------------------------------------------------------
82 # Create a new child an push it onto the stack
83 var pushElement = func(type, id = nil)
85 append(stack, stack[-1].createChild(type, id));
86 append(close_stack, level);
88 if( typeof(id) == 'scalar' and size(id) )
89 id_dict[ id ] = stack[-1];
93 if( cur_clip['x'] != nil
94 and cur_clip['y'] != nil
95 and cur_clip['width'] != nil
96 and cur_clip['height'] != nil )
99 "rect(%f, %f, %f, %f)",
101 cur_clip['x'] + cur_clip['width'],
102 cur_clip['y'] + cur_clip['height'],
106 stack[-1].set("clip", rect);
107 stack[-1].set("clip-frame", canvas.Element.LOCAL);
112 "Invalid or unsupported clip for element '" ~ id ~ "'"
119 # ----------------------------------------------------------------------------
120 # Remove the topmost element from the stack
121 var popElement = func
123 stack[-1].updateCenter();
124 # Create rotation matrix after all SVG defined transformations
125 stack[-1].set("tf-rot-index", stack[-1].createTransform()._node.getIndex());
131 # ----------------------------------------------------------------------------
132 # Parse a transformation (matrix)
133 # http://www.w3.org/TR/SVG/coords.html#TransformAttribute
134 var parseTransform = func(tf)
142 var start_type = tf.find_first_not_of("\t\n ", end);
146 var end_type = tf.find_first_of("(\t\n ", start_type + 1);
150 var start_args = tf.find('(', end_type);
155 end = start_args + 1;
158 var start_num = tf.find_first_not_of(",\t\n ", end);
161 if( tf[start_num] == `)` )
164 end = tf.find_first_of("),\t\n ", start_num + 1);
167 append(values, substr(tf, start_num, end - start_num));
173 var type = substr(tf, start_type, end_type - start_type);
175 # TODO should we warn if to much/wrong number of arguments given?
176 if( type == "translate" )
178 # translate(<tx> [<ty>]), which specifies a translation by tx and ty. If
179 # <ty> is not provided, it is assumed to be zero.
180 stack[-1].createTransform().setTranslation
183 size(values) > 1 ? values[1] : 0,
186 else if( type == "scale" )
188 # scale(<sx> [<sy>]), which specifies a scale operation by sx and sy. If
189 # <sy> is not provided, it is assumed to be equal to <sx>.
190 stack[-1].createTransform().setScale(values);
192 else if( type == "rotate" )
194 # rotate(<rotate-angle> [<cx> <cy>]), which specifies a rotation by
195 # <rotate-angle> degrees about a given point.
196 stack[-1].createTransform().setRotation
198 values[0] * D2R, # internal functions use rad
199 size(values) > 1 ? values[1:] : nil
202 else if( type == "matrix" )
204 if( size(values) == 6 )
205 stack[-1].createTransform(values);
209 "Invalid arguments to matrix transform: " ~ debug.string(values, 0)
213 printlog("warn", "Unknown transform type: '" ~ type ~ "'");
217 # ----------------------------------------------------------------------------
219 # http://www.w3.org/TR/SVG/paths.html#PathData
221 # map svg commands OpenVG commands
223 z: Path.VG_CLOSE_PATH,
234 var parsePath = func(path_data)
236 if( path_data == nil )
245 # skip trailing spaces
246 pos = path_data.find_first_not_of("\t\n ", pos);
251 var cmd = substr(path_data, pos, 1);
254 # and get all following arguments
258 pos = path_data.find_first_not_of(",\t\n ", pos);
263 pos = path_data.find_first_not_of("e-.0123456789", start_num);
264 if( start_num == pos )
267 append(args, substr( path_data,
269 pos > 0 ? pos - start_num : nil ));
272 # now execute the command
273 var rel = string.islower(cmd[0]);
274 var cmd = string.lc(cmd);
277 for(var i = 0; i + 7 <= size(args); i += 7)
279 # SVG: (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+
280 # OpenVG: rh,rv,rot,x0,y0
282 var cmd_vg = args[i + 4] ? Path.VG_LCCWARC_TO : Path.VG_LCWARC_TO;
284 var cmd_vg = args[i + 4] ? Path.VG_SCCWARC_TO : Path.VG_SCWARC_TO;
285 append(cmds, rel ? cmd_vg + 1: cmd_vg);
286 append(coords, args[i],
293 if( math.mod(size(args), 7) > 0 )
296 "Invalid number of coords for cmd 'a' "
297 ~ "(" ~ size(args) ~ " mod 7 != 0)"
302 var cmd_vg = cmd_map[cmd];
305 printlog("warn", "command not found: '" ~ cmd ~ "'");
309 var num_coords = Path.num_coords[int(cmd_vg)];
310 if( num_coords == 0 )
311 append(cmds, cmd_vg);
314 for(var i = 0; i + num_coords <= size(args); i += num_coords)
316 append(cmds, rel ? cmd_vg + 1: cmd_vg);
317 for(var j = i; j < i + num_coords; j += 1)
318 append(coords, args[j]);
320 # If a moveto is followed by multiple pairs of coordinates, the
321 # subsequent pairs are treated as implicit lineto commands.
323 cmd_vg = cmd_map['l'];
326 if( math.mod(size(args), num_coords) > 0 )
329 "Invalid number of coords for cmd '" ~ cmd ~ "' "
330 ~ "(" ~ size(args) ~ " mod " ~ num_coords ~ " != 0)"
336 stack[-1].setData(cmds, coords);
339 # ----------------------------------------------------------------------------
340 # Parse text styles (and apply them to the topmost element)
341 var parseTextStyles = func(style)
343 # http://www.w3.org/TR/SVG/text.html#TextAnchorProperty
344 var h_align = style["text-anchor"];
347 if( h_align == "end" )
349 else if( h_align == "middle" )
353 stack[-1].set("alignment", h_align ~ "-baseline");
355 # TODO vertical align
357 var fill = style['fill'];
359 stack[-1].set("fill", fill);
361 var font_family = style["font-family"];
362 var font_weight = style["font-weight"];
363 var font_style = style["font-style"];
364 if( font_family != nil or font_weight != nil or font_style != nil )
365 stack[-1].set("font", font_mapper(font_family, font_weight, font_style));
367 var font_size = style["font-size"];
368 if( font_size != nil )
369 stack[-1].setDouble("character-size", evalCSSNum(font_size));
371 var line_height = style["line-height"];
372 if( line_height != nil )
373 stack[-1].setDouble("line-height", evalCSSNum(line_height));
376 # ----------------------------------------------------------------------------
377 # Parse a css style attribute
378 var parseStyle = func(style)
384 foreach(var part; split(';', style))
386 if( !size(part = string.trim(part)) )
388 if( size(part = split(':',part)) != 2 )
391 var key = string.trim(part[0]);
395 var value = string.trim(part[1]);
405 # ----------------------------------------------------------------------------
407 var parseColor = func(s)
409 var color = [0, 0, 0];
413 if( size(s) == 7 and substr(s, 0, 1) == '#' )
415 return [ std.stoul(substr(s, 1, 2), 16) / 255,
416 std.stoul(substr(s, 3, 2), 16) / 255,
417 std.stoul(substr(s, 5, 2), 16) / 255 ];
423 # ----------------------------------------------------------------------------
424 # XML parsers element open callback
425 var start = func(name, attr)
435 die("Not an svg file (root=" ~ name ~ ")");
440 if( size(defs_stack) > 0 )
442 if( name == "mask" or name == "clipPath" )
444 append(defs_stack, {'type': name, 'id': attr['id']});
446 else if( name == "rect" )
448 foreach(var p; ["x", "y", "width", "height"])
449 defs_stack[-1][p] = evalCSSNum(attr[p]);
454 printlog("info", "Skipping unknown element in <defs>: <" ~ name ~ ">");
460 var style = parseStyle(attr['style']);
462 var clip_id = attr['clip-path'] or attr['mask'];
463 if( clip_id != nil and clip_id != "none" )
465 if( clip_id.starts_with("url(#")
466 and clip_id[-1] == `)` )
467 clip_id = substr(clip_id, 5, size(clip_id) - 5 - 1);
469 cur_clip = clip_dict[clip_id];
470 if( cur_clip == nil )
471 printlog("warn", "Clip not found: '" ~ clip_id ~ "'");
474 if( style['display'] == 'none' )
479 else if( name == "g" )
481 pushElement('group', attr['id']);
483 else if( name == "text" )
493 else if( name == "tspan" )
502 else if( name == "path" or name == "rect" )
504 pushElement('path', attr['id']);
508 var width = evalCSSNum(attr['width']);
509 var height = evalCSSNum(attr['height']);
510 var x = evalCSSNum(attr['x']);
511 var y = evalCSSNum(attr['y']);
522 cfg["border-radius"] = [evalCSSNum(rx), evalCSSNum(ry)];
524 stack[-1].rect(x, y, width, height, cfg);
527 parsePath(attr['d']);
529 stack[-1].set('fill', style['fill']);
531 var w = style['stroke-width'];
532 stack[-1].setStrokeLineWidth( w != nil ? evalCSSNum(w) : 1 );
533 stack[-1].set('stroke', style['stroke'] or "none");
535 var linecap = style['stroke-linecap'];
537 stack[-1].setStrokeLineCap(style['stroke-linecap']);
539 var linejoin = style['stroke-linejoin'];
540 if( linejoin != nil )
541 stack[-1].setStrokeLineJoin(style['stroke-linejoin']);
544 # http://www.w3.org/TR/SVG/painting.html#StrokeDasharrayProperty
545 var dash = style['stroke-dasharray'];
546 if( dash and size(dash) > 3 )
547 # at least 2 comma separated values...
548 stack[-1].setStrokeDashArray(split(',', dash));
550 else if( name == "use" )
552 var ref = attr["xlink:href"];
553 if( ref == nil or size(ref) < 2 or ref[0] != `#` )
554 return printlog("warn", "Invalid or missing href: '" ~ ref ~ '"');
556 var el_src = id_dict[ substr(ref, 1) ];
558 return printlog("warn", "Reference to unknown element '" ~ ref ~ "'");
560 # Create new element and copy sub branch from source node
561 pushElement(el_src._node.getName(), attr['id']);
562 props.copy(el_src._node, stack[-1]._node);
564 # copying also overrides the id so we need to set it again
565 stack[-1]._node.getNode("id").setValue(attr['id']);
567 else if( name == "defs" )
569 append(defs_stack, "defs");
574 printlog("info", "Skipping unknown element '" ~ name ~ "'");
579 parseTransform(attr['transform']);
581 var cx = attr['inkscape:transform-center-x'];
582 if( cx != nil and cx != 0 )
583 stack[-1].setDouble("center-offset-x", evalCSSNum(cx));
585 var cy = attr['inkscape:transform-center-y'];
586 if( cy != nil and cy != 0 )
587 stack[-1].setDouble("center-offset-y", -evalCSSNum(cy));
590 # XML parsers element close callback
602 if( size(defs_stack) > 0 )
606 var type = defs_stack[-1]['type'];
607 if( type == "mask" or type == "clipPath" )
608 clip_dict[defs_stack[-1]['id']] = defs_stack[-1];
615 if( size(close_stack) and (level + 1) == close_stack[-1] )
620 # Inkscape/SVG text is a bit complicated. If we only got a single tspan
621 # or text without tspan we create just a single canvas.Text, otherwise
622 # we create a canvas.Group with a canvas.Text as child for each tspan.
623 # We need to take care to apply the transform attribute of the text
624 # element to the correct canvas element, and also correctly inherit
625 # the style properties.
626 var character_size = 24;
627 if( size(tspans) > 1 )
629 pushElement('group', text.attr['id']);
630 parseTextStyles(text.style);
631 parseTransform(text.attr['transform']);
633 character_size = stack[-1].get("character-size", character_size);
636 # Helper for getting first number in space separated list of numbers.
637 var first_num = func(str)
641 var end = str.find_first_of(" \n\t");
645 return substr(str, 0, end);
649 foreach(var tspan; tspans)
651 # Always take first number and ignore individual character placment
652 var x = first_num(tspan.attr['x'] or text.attr['x']);
653 var y = first_num(tspan.attr['y'] or text.attr['y']);
655 # Sometimes Inkscape forgets writing x and y coordinates and instead
656 # just indicates a multiline text with sodipodi:role="line".
657 if( tspan.attr['y'] == nil and tspan.attr['sodipodi:role'] == "line" )
658 # TODO should we combine multiple lines into a single text separated
659 # with newline characters?
661 * stack[-1].get("line-height", 1.25)
662 * stack[-1].get("character-size", character_size);
664 # Use id of text element with single tspan child, fall back to id of
665 # tspan if text has no id.
666 var id = text.attr['id'];
667 if( id == nil or size(tspans) > 1 )
668 id = tspan.attr['id'];
670 pushElement('text', id);
671 stack[-1].set("text", tspan.text);
673 if( x != 0 or y != 0 )
674 stack[-1].setTranslation(x, y);
676 if( size(tspans) == 1 )
678 parseTextStyles(text.style);
679 parseTransform(text.attr['transform']);
682 parseTextStyles(tspan.style);
688 if( size(tspans) > 1 )
696 # XML parsers element data callback
697 var data = func(data)
702 if( size(data) and tspans != nil )
704 if( size(tspans) == 0 )
705 # If no tspan is found use text element itself
706 append(tspans, text);
708 # If text contains xml entities it gets split at each entity. So let's
709 # glue it back into a single text...
710 tspans[-1]["text"] ~= data;
714 call(func parsexml(path, start, end, data), nil, var err = []);
718 for(var i = 1; i + 1 < size(err); i += 2)
720 # err = ['error message', 'file', line]
721 msg ~= (i == 1 ? "\n at " : "\n called from: ")
722 ~ err[i] ~ ", line " ~ err[i + 1]
724 printlog("alert", msg ~ "\n ");