2012-08-01 22:49:15 +00:00
|
|
|
# Parse an xml file into a canvas group element
|
|
|
|
#
|
2012-08-01 23:28:56 +00:00
|
|
|
# @param group The canvas.Group instance to append the parsed elements to
|
|
|
|
# @param path The path of the svg file (absolute or relative to FG_ROOT)
|
|
|
|
# @param options Optional hash of options
|
|
|
|
var parsesvg = func(group, path, options = nil)
|
2012-08-01 22:49:15 +00:00
|
|
|
{
|
|
|
|
if( !isa(group, Group) )
|
|
|
|
die("Invalid argument group (type != Group)");
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 23:28:56 +00:00
|
|
|
if( options == nil )
|
|
|
|
options = {};
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 23:28:56 +00:00
|
|
|
if( typeof(options) != "hash" )
|
|
|
|
die("Options need to be of type hash!");
|
|
|
|
|
2013-10-06 16:52:30 +00:00
|
|
|
# resolve paths using standard SimGear logic
|
|
|
|
var file_path = resolvepath(path);
|
|
|
|
if (file_path == "")
|
|
|
|
die("File not found: "~path);
|
|
|
|
path = file_path;
|
|
|
|
|
2014-08-24 11:55:32 +00:00
|
|
|
var _printlog = printlog;
|
|
|
|
var printlog = func(level, msg)
|
|
|
|
{
|
|
|
|
_printlog(level, "parsesvg: " ~ msg ~ " [path='" ~ path ~ "']");
|
|
|
|
};
|
|
|
|
|
2012-08-01 23:28:56 +00:00
|
|
|
var custom_font_mapper = options['font-mapper'];
|
2014-09-10 14:51:12 +00:00
|
|
|
var font_mapper = func(family, weight, style)
|
2012-08-01 23:28:56 +00:00
|
|
|
{
|
|
|
|
if( typeof(custom_font_mapper) == 'func' )
|
|
|
|
{
|
2014-09-10 14:51:12 +00:00
|
|
|
var font = custom_font_mapper(family, weight, style);
|
2012-08-01 23:28:56 +00:00
|
|
|
if( font != nil )
|
|
|
|
return font;
|
|
|
|
}
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2014-09-10 14:51:12 +00:00
|
|
|
if( string.match(family,"Liberation*") ) {
|
|
|
|
style = style == "italic" ? "Italic" : "";
|
|
|
|
weight = weight == "bold" ? "Bold" : "";
|
|
|
|
|
|
|
|
var s = weight ~ style;
|
|
|
|
if( s == "" ) s = "Regular";
|
|
|
|
|
|
|
|
return "LiberationFonts/" ~ string.replace(family," ", "") ~ "-" ~ s ~ ".ttf";
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2012-08-01 23:28:56 +00:00
|
|
|
return "LiberationFonts/LiberationMono-Bold.ttf";
|
|
|
|
};
|
2012-08-01 22:49:15 +00:00
|
|
|
|
2013-07-15 20:29:19 +00:00
|
|
|
# Helper to get number without unit (eg. px)
|
2013-12-01 13:03:19 +00:00
|
|
|
var evalCSSNum = func(css_num)
|
2013-07-15 20:29:19 +00:00
|
|
|
{
|
|
|
|
if( css_num.ends_with("px") )
|
|
|
|
return substr(css_num, 0, size(css_num) - 2);
|
2013-10-20 21:41:19 +00:00
|
|
|
else if( css_num.ends_with("%") )
|
|
|
|
return substr(css_num, 0, size(css_num) - 1) / 100;
|
2013-07-15 20:29:19 +00:00
|
|
|
|
|
|
|
return css_num;
|
|
|
|
}
|
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
var level = 0;
|
|
|
|
var skip = 0;
|
|
|
|
var stack = [group];
|
2012-08-01 23:28:56 +00:00
|
|
|
var close_stack = []; # helper for check tag closing
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2014-07-19 19:43:51 +00:00
|
|
|
var defs_stack = [];
|
|
|
|
|
2013-02-23 17:27:22 +00:00
|
|
|
var text = nil;
|
|
|
|
var tspans = nil;
|
|
|
|
|
2012-08-01 23:28:56 +00:00
|
|
|
# lookup table for element ids (for <use> element)
|
|
|
|
var id_dict = {};
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2014-07-19 19:43:51 +00:00
|
|
|
# lookup table for mask and clipPath element ids
|
|
|
|
var clip_dict = {};
|
|
|
|
var cur_clip = nil;
|
|
|
|
|
2012-08-01 23:28:56 +00:00
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# Create a new child an push it onto the stack
|
|
|
|
var pushElement = func(type, id = nil)
|
|
|
|
{
|
|
|
|
append(stack, stack[-1].createChild(type, id));
|
|
|
|
append(close_stack, level);
|
|
|
|
|
|
|
|
if( typeof(id) == 'scalar' and size(id) )
|
|
|
|
id_dict[ id ] = stack[-1];
|
2014-07-19 19:43:51 +00:00
|
|
|
|
|
|
|
if( cur_clip != nil )
|
|
|
|
{
|
2014-08-24 11:55:32 +00:00
|
|
|
if( cur_clip['x'] != nil
|
|
|
|
and cur_clip['y'] != nil
|
|
|
|
and cur_clip['width'] != nil
|
|
|
|
and cur_clip['height'] != nil )
|
|
|
|
{
|
|
|
|
var rect = sprintf(
|
|
|
|
"rect(%f, %f, %f, %f)",
|
|
|
|
cur_clip['y'],
|
|
|
|
cur_clip['x'] + cur_clip['width'],
|
|
|
|
cur_clip['y'] + cur_clip['height'],
|
|
|
|
cur_clip['x']
|
|
|
|
);
|
|
|
|
|
|
|
|
stack[-1].set("clip", rect);
|
|
|
|
stack[-1].set("clip-frame", canvas.Element.LOCAL);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
printlog(
|
|
|
|
"warn",
|
|
|
|
"Invalid or unsupported clip for element '" ~ id ~ "'"
|
|
|
|
);
|
|
|
|
|
2014-07-19 19:43:51 +00:00
|
|
|
cur_clip = nil;
|
|
|
|
}
|
2013-02-23 17:27:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# Remove the topmost element from the stack
|
|
|
|
var popElement = func
|
|
|
|
{
|
2014-02-07 23:38:30 +00:00
|
|
|
stack[-1].updateCenter();
|
|
|
|
# Create rotation matrix after all SVG defined transformations
|
|
|
|
stack[-1].set("tf-rot-index", stack[-1].createTransform()._node.getIndex());
|
|
|
|
|
2013-02-23 17:27:22 +00:00
|
|
|
pop(stack);
|
|
|
|
pop(close_stack);
|
|
|
|
}
|
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# Parse a transformation (matrix)
|
|
|
|
# http://www.w3.org/TR/SVG/coords.html#TransformAttribute
|
|
|
|
var parseTransform = func(tf)
|
|
|
|
{
|
|
|
|
if( tf == nil )
|
|
|
|
return;
|
|
|
|
|
|
|
|
var end = 0;
|
|
|
|
while(1)
|
|
|
|
{
|
|
|
|
var start_type = tf.find_first_not_of("\t\n ", end);
|
|
|
|
if( start_type < 0 )
|
|
|
|
break;
|
|
|
|
|
|
|
|
var end_type = tf.find_first_of("(\t\n ", start_type + 1);
|
|
|
|
if( end_type < 0 )
|
|
|
|
break;
|
|
|
|
|
|
|
|
var start_args = tf.find('(', end_type);
|
|
|
|
if( start_args < 0 )
|
|
|
|
break;
|
|
|
|
|
|
|
|
var values = [];
|
2013-02-23 13:43:56 +00:00
|
|
|
end = start_args + 1;
|
2012-08-01 22:49:15 +00:00
|
|
|
while(1)
|
|
|
|
{
|
2013-02-23 13:43:56 +00:00
|
|
|
var start_num = tf.find_first_not_of(",\t\n ", end);
|
2012-08-01 22:49:15 +00:00
|
|
|
if( start_num < 0 )
|
|
|
|
break;
|
2013-02-23 13:43:56 +00:00
|
|
|
if( tf[start_num] == `)` )
|
2012-08-01 22:49:15 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
end = tf.find_first_of("),\t\n ", start_num + 1);
|
|
|
|
if( end < 0 )
|
|
|
|
break;
|
2013-01-31 19:17:13 +00:00
|
|
|
append(values, substr(tf, start_num, end - start_num));
|
2012-08-01 22:49:15 +00:00
|
|
|
}
|
2013-02-23 13:43:56 +00:00
|
|
|
|
|
|
|
if( end > 0 )
|
|
|
|
end += 1;
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2013-01-31 19:17:13 +00:00
|
|
|
var type = substr(tf, start_type, end_type - start_type);
|
2012-08-01 22:49:15 +00:00
|
|
|
|
2013-02-23 13:43:56 +00:00
|
|
|
# TODO should we warn if to much/wrong number of arguments given?
|
2012-08-01 22:49:15 +00:00
|
|
|
if( type == "translate" )
|
2014-08-24 11:55:32 +00:00
|
|
|
{
|
2012-08-01 22:49:15 +00:00
|
|
|
# translate(<tx> [<ty>]), which specifies a translation by tx and ty. If
|
|
|
|
# <ty> is not provided, it is assumed to be zero.
|
|
|
|
stack[-1].createTransform().setTranslation
|
|
|
|
(
|
|
|
|
values[0],
|
|
|
|
size(values) > 1 ? values[1] : 0,
|
|
|
|
);
|
2014-08-24 11:55:32 +00:00
|
|
|
}
|
2013-02-23 13:43:56 +00:00
|
|
|
else if( type == "scale" )
|
2014-08-24 11:55:32 +00:00
|
|
|
{
|
2013-02-23 13:43:56 +00:00
|
|
|
# scale(<sx> [<sy>]), which specifies a scale operation by sx and sy. If
|
|
|
|
# <sy> is not provided, it is assumed to be equal to <sx>.
|
|
|
|
stack[-1].createTransform().setScale(values);
|
2014-08-24 11:55:32 +00:00
|
|
|
}
|
2013-02-23 13:43:56 +00:00
|
|
|
else if( type == "rotate" )
|
2014-08-24 11:55:32 +00:00
|
|
|
{
|
2013-02-23 13:43:56 +00:00
|
|
|
# rotate(<rotate-angle> [<cx> <cy>]), which specifies a rotation by
|
|
|
|
# <rotate-angle> degrees about a given point.
|
|
|
|
stack[-1].createTransform().setRotation
|
|
|
|
(
|
|
|
|
values[0] * D2R, # internal functions use rad
|
|
|
|
size(values) > 1 ? values[1:] : nil
|
|
|
|
);
|
2014-08-24 11:55:32 +00:00
|
|
|
}
|
2012-08-01 22:49:15 +00:00
|
|
|
else if( type == "matrix" )
|
|
|
|
{
|
|
|
|
if( size(values) == 6 )
|
|
|
|
stack[-1].createTransform(values);
|
|
|
|
else
|
2014-08-24 11:55:32 +00:00
|
|
|
printlog(
|
|
|
|
"warn",
|
|
|
|
"Invalid arguments to matrix transform: " ~ debug.string(values, 0)
|
|
|
|
);
|
2012-08-01 22:49:15 +00:00
|
|
|
}
|
|
|
|
else
|
2014-08-24 11:55:32 +00:00
|
|
|
printlog("warn", "Unknown transform type: '" ~ type ~ "'");
|
2012-08-01 22:49:15 +00:00
|
|
|
}
|
|
|
|
};
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# Parse a path
|
|
|
|
# http://www.w3.org/TR/SVG/paths.html#PathData
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
# map svg commands OpenVG commands
|
|
|
|
var cmd_map = {
|
|
|
|
z: Path.VG_CLOSE_PATH,
|
|
|
|
m: Path.VG_MOVE_TO,
|
|
|
|
l: Path.VG_LINE_TO,
|
|
|
|
h: Path.VG_HLINE_TO,
|
|
|
|
v: Path.VG_VLINE_TO,
|
|
|
|
q: Path.VG_QUAD_TO,
|
|
|
|
c: Path.VG_CUBIC_TO,
|
|
|
|
t: Path.VG_SQUAD_TO,
|
|
|
|
s: Path.VG_SCUBIC_TO
|
|
|
|
};
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2013-01-31 19:17:13 +00:00
|
|
|
var parsePath = func(path_data)
|
2012-08-01 22:49:15 +00:00
|
|
|
{
|
2013-01-31 19:17:13 +00:00
|
|
|
if( path_data == nil )
|
2012-08-01 22:49:15 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
var pos = 0;
|
|
|
|
var cmds = [];
|
|
|
|
var coords = [];
|
|
|
|
|
|
|
|
while(1)
|
|
|
|
{
|
|
|
|
# skip trailing spaces
|
|
|
|
pos = path_data.find_first_not_of("\t\n ", pos);
|
|
|
|
if( pos < 0 )
|
|
|
|
break;
|
|
|
|
|
|
|
|
# get command
|
2013-01-31 19:17:13 +00:00
|
|
|
var cmd = substr(path_data, pos, 1);
|
2012-08-01 22:49:15 +00:00
|
|
|
pos += 1;
|
|
|
|
|
|
|
|
# and get all following arguments
|
|
|
|
var args = [];
|
|
|
|
while(1)
|
|
|
|
{
|
|
|
|
pos = path_data.find_first_not_of(",\t\n ", pos);
|
|
|
|
if( pos < 0 )
|
|
|
|
break;
|
|
|
|
|
|
|
|
var start_num = pos;
|
|
|
|
pos = path_data.find_first_not_of("e-.0123456789", start_num);
|
|
|
|
if( start_num == pos )
|
|
|
|
break;
|
|
|
|
|
2014-08-24 11:55:32 +00:00
|
|
|
append(args, substr( path_data,
|
|
|
|
start_num,
|
|
|
|
pos > 0 ? pos - start_num : nil ));
|
2012-08-01 22:49:15 +00:00
|
|
|
}
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
# now execute the command
|
|
|
|
var rel = string.islower(cmd[0]);
|
|
|
|
var cmd = string.lc(cmd);
|
|
|
|
if( cmd == 'a' )
|
|
|
|
{
|
|
|
|
for(var i = 0; i + 7 <= size(args); i += 7)
|
|
|
|
{
|
|
|
|
# SVG: (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+
|
|
|
|
# OpenVG: rh,rv,rot,x0,y0
|
|
|
|
if( args[i + 3] )
|
|
|
|
var cmd_vg = args[i + 4] ? Path.VG_LCCWARC_TO : Path.VG_LCWARC_TO;
|
|
|
|
else
|
|
|
|
var cmd_vg = args[i + 4] ? Path.VG_SCCWARC_TO : Path.VG_SCWARC_TO;
|
|
|
|
append(cmds, rel ? cmd_vg + 1: cmd_vg);
|
|
|
|
append(coords, args[i],
|
|
|
|
args[i + 1],
|
|
|
|
args[i + 2],
|
|
|
|
args[i + 5],
|
|
|
|
args[i + 6] );
|
|
|
|
}
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
if( math.mod(size(args), 7) > 0 )
|
2014-08-24 11:55:32 +00:00
|
|
|
printlog(
|
|
|
|
"warn",
|
|
|
|
"Invalid number of coords for cmd 'a' "
|
|
|
|
~ "(" ~ size(args) ~ " mod 7 != 0)"
|
|
|
|
);
|
2012-08-01 22:49:15 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
var cmd_vg = cmd_map[cmd];
|
|
|
|
if( cmd_vg == nil )
|
|
|
|
{
|
2014-08-24 11:55:32 +00:00
|
|
|
printlog("warn", "command not found: '" ~ cmd ~ "'");
|
2012-08-01 22:49:15 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
var num_coords = Path.num_coords[int(cmd_vg)];
|
|
|
|
if( num_coords == 0 )
|
|
|
|
append(cmds, cmd_vg);
|
|
|
|
else
|
|
|
|
{
|
|
|
|
for(var i = 0; i + num_coords <= size(args); i += num_coords)
|
|
|
|
{
|
|
|
|
append(cmds, rel ? cmd_vg + 1: cmd_vg);
|
|
|
|
for(var j = i; j < i + num_coords; j += 1)
|
|
|
|
append(coords, args[j]);
|
|
|
|
|
|
|
|
# If a moveto is followed by multiple pairs of coordinates, the
|
2013-02-23 17:27:22 +00:00
|
|
|
# subsequent pairs are treated as implicit lineto commands.
|
2012-08-01 22:49:15 +00:00
|
|
|
if( cmd == 'm' )
|
|
|
|
cmd_vg = cmd_map['l'];
|
|
|
|
}
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
if( math.mod(size(args), num_coords) > 0 )
|
2014-08-24 11:55:32 +00:00
|
|
|
printlog(
|
|
|
|
"warn",
|
|
|
|
"Invalid number of coords for cmd '" ~ cmd ~ "' "
|
|
|
|
~ "(" ~ size(args) ~ " mod " ~ num_coords ~ " != 0)"
|
|
|
|
);
|
2012-08-01 22:49:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
stack[-1].setData(cmds, coords);
|
|
|
|
};
|
2013-02-23 17:27:22 +00:00
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# Parse text styles (and apply them to the topmost element)
|
|
|
|
var parseTextStyles = func(style)
|
|
|
|
{
|
|
|
|
# http://www.w3.org/TR/SVG/text.html#TextAnchorProperty
|
|
|
|
var h_align = style["text-anchor"];
|
|
|
|
if( h_align != nil )
|
|
|
|
{
|
|
|
|
if( h_align == "end" )
|
|
|
|
h_align = "right";
|
|
|
|
else if( h_align == "middle" )
|
|
|
|
h_align = "center";
|
|
|
|
else # "start"
|
|
|
|
h_align = "left";
|
|
|
|
stack[-1].set("alignment", h_align ~ "-baseline");
|
|
|
|
}
|
|
|
|
# TODO vertical align
|
|
|
|
|
|
|
|
var fill = style['fill'];
|
|
|
|
if( fill != nil )
|
|
|
|
stack[-1].set("fill", fill);
|
|
|
|
|
|
|
|
var font_family = style["font-family"];
|
|
|
|
var font_weight = style["font-weight"];
|
2014-09-10 14:51:12 +00:00
|
|
|
var font_style = style["font-style"];
|
|
|
|
if( font_family != nil or font_weight != nil or font_style != nil )
|
|
|
|
stack[-1].set("font", font_mapper(font_family, font_weight, font_style));
|
2013-02-23 17:27:22 +00:00
|
|
|
|
|
|
|
var font_size = style["font-size"];
|
|
|
|
if( font_size != nil )
|
2013-12-01 13:03:19 +00:00
|
|
|
stack[-1].setDouble("character-size", evalCSSNum(font_size));
|
2013-10-20 21:41:19 +00:00
|
|
|
|
|
|
|
var line_height = style["line-height"];
|
|
|
|
if( line_height != nil )
|
2013-12-01 13:03:19 +00:00
|
|
|
stack[-1].setDouble("line-height", evalCSSNum(line_height));
|
2013-02-23 17:27:22 +00:00
|
|
|
}
|
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# Parse a css style attribute
|
|
|
|
var parseStyle = func(style)
|
|
|
|
{
|
|
|
|
if( style == nil )
|
|
|
|
return {};
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
var styles = {};
|
|
|
|
foreach(var part; split(';', style))
|
|
|
|
{
|
|
|
|
if( !size(part = string.trim(part)) )
|
|
|
|
continue;
|
|
|
|
if( size(part = split(':',part)) != 2 )
|
|
|
|
continue;
|
|
|
|
|
|
|
|
var key = string.trim(part[0]);
|
|
|
|
if( !size(key) )
|
|
|
|
continue;
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
var value = string.trim(part[1]);
|
|
|
|
if( !size(value) )
|
|
|
|
continue;
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
styles[key] = value;
|
|
|
|
}
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
return styles;
|
|
|
|
}
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# Parse a css color
|
|
|
|
var parseColor = func(s)
|
|
|
|
{
|
|
|
|
var color = [0, 0, 0];
|
|
|
|
if( s == nil )
|
|
|
|
return color;
|
|
|
|
|
|
|
|
if( size(s) == 7 and substr(s, 0, 1) == '#' )
|
|
|
|
{
|
|
|
|
return [ std.stoul(substr(s, 1, 2), 16) / 255,
|
|
|
|
std.stoul(substr(s, 3, 2), 16) / 255,
|
|
|
|
std.stoul(substr(s, 5, 2), 16) / 255 ];
|
|
|
|
}
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
return color;
|
|
|
|
};
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# XML parsers element open callback
|
|
|
|
var start = func(name, attr)
|
|
|
|
{
|
|
|
|
level += 1;
|
|
|
|
|
|
|
|
if( skip )
|
|
|
|
return;
|
|
|
|
|
|
|
|
if( level == 1 )
|
|
|
|
{
|
|
|
|
if( name != 'svg' )
|
|
|
|
die("Not an svg file (root=" ~ name ~ ")");
|
|
|
|
else
|
|
|
|
return;
|
|
|
|
}
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2014-07-19 19:43:51 +00:00
|
|
|
if( size(defs_stack) > 0 )
|
|
|
|
{
|
|
|
|
if( name == "mask" or name == "clipPath" )
|
|
|
|
{
|
|
|
|
append(defs_stack, {'type': name, 'id': attr['id']});
|
|
|
|
}
|
|
|
|
else if( name == "rect" )
|
|
|
|
{
|
|
|
|
foreach(var p; ["x", "y", "width", "height"])
|
|
|
|
defs_stack[-1][p] = evalCSSNum(attr[p]);
|
|
|
|
skip = level;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2014-08-24 11:55:32 +00:00
|
|
|
printlog("info", "Skipping unknown element in <defs>: <" ~ name ~ ">");
|
2014-07-19 19:43:51 +00:00
|
|
|
skip = level;
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
var style = parseStyle(attr['style']);
|
|
|
|
|
2014-07-19 19:43:51 +00:00
|
|
|
var clip_id = attr['clip-path'] or attr['mask'];
|
2014-08-24 11:55:32 +00:00
|
|
|
if( clip_id != nil and clip_id != "none" )
|
2014-07-19 19:43:51 +00:00
|
|
|
{
|
|
|
|
if( clip_id.starts_with("url(#")
|
|
|
|
and clip_id[-1] == `)` )
|
|
|
|
clip_id = substr(clip_id, 5, size(clip_id) - 5 - 1);
|
|
|
|
|
|
|
|
cur_clip = clip_dict[clip_id];
|
|
|
|
if( cur_clip == nil )
|
2014-08-24 11:55:32 +00:00
|
|
|
printlog("warn", "Clip not found: '" ~ clip_id ~ "'");
|
2014-07-19 19:43:51 +00:00
|
|
|
}
|
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
if( style['display'] == 'none' )
|
|
|
|
{
|
2014-07-19 19:43:51 +00:00
|
|
|
skip = level;
|
2012-08-01 22:49:15 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
else if( name == "g" )
|
|
|
|
{
|
2012-08-01 23:28:56 +00:00
|
|
|
pushElement('group', attr['id']);
|
2012-08-01 22:49:15 +00:00
|
|
|
}
|
|
|
|
else if( name == "text" )
|
|
|
|
{
|
2013-02-23 17:27:22 +00:00
|
|
|
text = {
|
|
|
|
"attr": attr,
|
2013-07-19 22:53:34 +00:00
|
|
|
"style": style,
|
|
|
|
"text": ""
|
2013-02-23 17:27:22 +00:00
|
|
|
};
|
|
|
|
tspans = [];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
else if( name == "tspan" )
|
|
|
|
{
|
|
|
|
append(tspans, {
|
|
|
|
"attr": attr,
|
2013-07-19 22:53:34 +00:00
|
|
|
"style": style,
|
|
|
|
"text": ""
|
2013-02-23 17:27:22 +00:00
|
|
|
});
|
|
|
|
return;
|
2012-08-01 22:49:15 +00:00
|
|
|
}
|
|
|
|
else if( name == "path" or name == "rect" )
|
|
|
|
{
|
2012-08-01 23:28:56 +00:00
|
|
|
pushElement('path', attr['id']);
|
2012-08-01 22:49:15 +00:00
|
|
|
|
|
|
|
if( name == "rect" )
|
|
|
|
{
|
2013-12-01 13:03:19 +00:00
|
|
|
var width = evalCSSNum(attr['width']);
|
|
|
|
var height = evalCSSNum(attr['height']);
|
|
|
|
var x = evalCSSNum(attr['x']);
|
|
|
|
var y = evalCSSNum(attr['y']);
|
2013-02-23 18:19:32 +00:00
|
|
|
var rx = attr['rx'];
|
|
|
|
var ry = attr['ry'];
|
2012-08-01 22:49:15 +00:00
|
|
|
|
2013-02-23 18:19:32 +00:00
|
|
|
if( ry == nil )
|
|
|
|
ry = rx;
|
|
|
|
else if( rx == nil )
|
|
|
|
rx = ry;
|
|
|
|
|
|
|
|
var cfg = {};
|
|
|
|
if( rx != nil )
|
2013-12-01 13:03:19 +00:00
|
|
|
cfg["border-radius"] = [evalCSSNum(rx), evalCSSNum(ry)];
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2013-12-01 13:03:19 +00:00
|
|
|
stack[-1].rect(x, y, width, height, cfg);
|
2013-02-23 18:19:32 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
parsePath(attr['d']);
|
2015-04-08 22:51:24 +00:00
|
|
|
|
2015-04-10 19:09:14 +00:00
|
|
|
var fillOpacity = style['fill-opacity'];
|
2015-04-08 22:51:24 +00:00
|
|
|
if( fillOpacity != nil)
|
|
|
|
stack[-1].set('fill', style['fill'] ~ sprintf("%02x", int(style['fill-opacity']*255)));
|
|
|
|
else
|
|
|
|
stack[-1].set('fill', style['fill']);
|
2012-08-23 19:05:52 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
var w = style['stroke-width'];
|
2013-12-01 13:03:19 +00:00
|
|
|
stack[-1].setStrokeLineWidth( w != nil ? evalCSSNum(w) : 1 );
|
2015-04-10 19:09:14 +00:00
|
|
|
|
|
|
|
var strokeOpacity = style['stroke-opacity'];
|
2015-04-08 22:51:24 +00:00
|
|
|
if(strokeOpacity != nil)
|
|
|
|
stack[-1].set('stroke', (style['stroke'] ~ sprintf("%02x", int(style['stroke-opacity']*255))));
|
|
|
|
else
|
|
|
|
stack[-1].set('stroke', style['stroke'] or "none");
|
|
|
|
|
2012-08-01 23:28:56 +00:00
|
|
|
var linecap = style['stroke-linecap'];
|
|
|
|
if( linecap != nil )
|
|
|
|
stack[-1].setStrokeLineCap(style['stroke-linecap']);
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2014-08-25 22:06:59 +00:00
|
|
|
var linejoin = style['stroke-linejoin'];
|
|
|
|
if( linejoin != nil )
|
|
|
|
stack[-1].setStrokeLineJoin(style['stroke-linejoin']);
|
|
|
|
|
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
# http://www.w3.org/TR/SVG/painting.html#StrokeDasharrayProperty
|
|
|
|
var dash = style['stroke-dasharray'];
|
|
|
|
if( dash and size(dash) > 3 )
|
|
|
|
# at least 2 comma separated values...
|
2012-08-01 23:28:56 +00:00
|
|
|
stack[-1].setStrokeDashArray(split(',', dash));
|
2012-08-01 22:49:15 +00:00
|
|
|
}
|
2012-08-01 23:28:56 +00:00
|
|
|
else if( name == "use" )
|
|
|
|
{
|
|
|
|
var ref = attr["xlink:href"];
|
|
|
|
if( ref == nil or size(ref) < 2 or ref[0] != `#` )
|
2014-08-24 11:55:32 +00:00
|
|
|
return printlog("warn", "Invalid or missing href: '" ~ ref ~ '"');
|
2012-08-01 23:28:56 +00:00
|
|
|
|
|
|
|
var el_src = id_dict[ substr(ref, 1) ];
|
|
|
|
if( el_src == nil )
|
2014-08-24 11:55:32 +00:00
|
|
|
return printlog("warn", "Reference to unknown element '" ~ ref ~ "'");
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 23:28:56 +00:00
|
|
|
# Create new element and copy sub branch from source node
|
|
|
|
pushElement(el_src._node.getName(), attr['id']);
|
|
|
|
props.copy(el_src._node, stack[-1]._node);
|
|
|
|
|
|
|
|
# copying also overrides the id so we need to set it again
|
|
|
|
stack[-1]._node.getNode("id").setValue(attr['id']);
|
|
|
|
}
|
2014-07-19 19:43:51 +00:00
|
|
|
else if( name == "defs" )
|
|
|
|
{
|
|
|
|
append(defs_stack, "defs");
|
|
|
|
return;
|
|
|
|
}
|
2012-08-01 22:49:15 +00:00
|
|
|
else
|
|
|
|
{
|
2014-08-24 11:55:32 +00:00
|
|
|
printlog("info", "Skipping unknown element '" ~ name ~ "'");
|
2012-08-01 22:49:15 +00:00
|
|
|
skip = level;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
parseTransform(attr['transform']);
|
2013-01-09 11:14:31 +00:00
|
|
|
|
|
|
|
var cx = attr['inkscape:transform-center-x'];
|
|
|
|
if( cx != nil and cx != 0 )
|
2013-12-01 13:03:19 +00:00
|
|
|
stack[-1].setDouble("center-offset-x", evalCSSNum(cx));
|
2013-01-09 11:14:31 +00:00
|
|
|
|
|
|
|
var cy = attr['inkscape:transform-center-y'];
|
|
|
|
if( cy != nil and cy != 0 )
|
2013-12-01 13:03:19 +00:00
|
|
|
stack[-1].setDouble("center-offset-y", -evalCSSNum(cy));
|
2012-08-01 22:49:15 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
# XML parsers element close callback
|
|
|
|
var end = func(name)
|
|
|
|
{
|
|
|
|
level -= 1;
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
if( skip )
|
|
|
|
{
|
2014-07-19 19:43:51 +00:00
|
|
|
if( level < skip )
|
2012-08-01 22:49:15 +00:00
|
|
|
skip = 0;
|
|
|
|
return;
|
|
|
|
}
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2014-07-19 19:43:51 +00:00
|
|
|
if( size(defs_stack) > 0 )
|
|
|
|
{
|
|
|
|
if( name != "defs" )
|
|
|
|
{
|
|
|
|
var type = defs_stack[-1]['type'];
|
|
|
|
if( type == "mask" or type == "clipPath" )
|
|
|
|
clip_dict[defs_stack[-1]['id']] = defs_stack[-1];
|
|
|
|
}
|
|
|
|
|
|
|
|
pop(defs_stack);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-08-01 23:28:56 +00:00
|
|
|
if( size(close_stack) and (level + 1) == close_stack[-1] )
|
2013-02-23 17:27:22 +00:00
|
|
|
popElement();
|
|
|
|
|
|
|
|
if( name == "text" )
|
2012-08-01 23:28:56 +00:00
|
|
|
{
|
2013-02-23 17:27:22 +00:00
|
|
|
# Inkscape/SVG text is a bit complicated. If we only got a single tspan
|
|
|
|
# or text without tspan we create just a single canvas.Text, otherwise
|
|
|
|
# we create a canvas.Group with a canvas.Text as child for each tspan.
|
|
|
|
# We need to take care to apply the transform attribute of the text
|
|
|
|
# element to the correct canvas element, and also correctly inherit
|
|
|
|
# the style properties.
|
|
|
|
var character_size = 24;
|
|
|
|
if( size(tspans) > 1 )
|
|
|
|
{
|
|
|
|
pushElement('group', text.attr['id']);
|
|
|
|
parseTextStyles(text.style);
|
|
|
|
parseTransform(text.attr['transform']);
|
|
|
|
|
|
|
|
character_size = stack[-1].get("character-size", character_size);
|
|
|
|
}
|
|
|
|
|
|
|
|
# Helper for getting first number in space separated list of numbers.
|
|
|
|
var first_num = func(str)
|
|
|
|
{
|
|
|
|
if( str == nil )
|
|
|
|
return 0;
|
|
|
|
var end = str.find_first_of(" \n\t");
|
|
|
|
if( end < 0 )
|
|
|
|
return str;
|
|
|
|
else
|
|
|
|
return substr(str, 0, end);
|
|
|
|
}
|
|
|
|
|
|
|
|
var line = 0;
|
|
|
|
foreach(var tspan; tspans)
|
|
|
|
{
|
|
|
|
# Always take first number and ignore individual character placment
|
|
|
|
var x = first_num(tspan.attr['x'] or text.attr['x']);
|
|
|
|
var y = first_num(tspan.attr['y'] or text.attr['y']);
|
|
|
|
|
|
|
|
# Sometimes Inkscape forgets writing x and y coordinates and instead
|
|
|
|
# just indicates a multiline text with sodipodi:role="line".
|
|
|
|
if( tspan.attr['y'] == nil and tspan.attr['sodipodi:role'] == "line" )
|
|
|
|
# TODO should we combine multiple lines into a single text separated
|
|
|
|
# with newline characters?
|
|
|
|
y += line
|
2013-10-20 21:41:19 +00:00
|
|
|
* stack[-1].get("line-height", 1.25)
|
2013-02-23 17:27:22 +00:00
|
|
|
* stack[-1].get("character-size", character_size);
|
|
|
|
|
2013-02-23 19:04:57 +00:00
|
|
|
# Use id of text element with single tspan child, fall back to id of
|
|
|
|
# tspan if text has no id.
|
|
|
|
var id = text.attr['id'];
|
|
|
|
if( id == nil or size(tspans) > 1 )
|
|
|
|
id = tspan.attr['id'];
|
|
|
|
|
|
|
|
pushElement('text', id);
|
2013-02-23 17:27:22 +00:00
|
|
|
stack[-1].set("text", tspan.text);
|
|
|
|
|
|
|
|
if( x != 0 or y != 0 )
|
|
|
|
stack[-1].setTranslation(x, y);
|
|
|
|
|
|
|
|
if( size(tspans) == 1 )
|
|
|
|
{
|
|
|
|
parseTextStyles(text.style);
|
|
|
|
parseTransform(text.attr['transform']);
|
|
|
|
}
|
|
|
|
|
|
|
|
parseTextStyles(tspan.style);
|
|
|
|
popElement();
|
|
|
|
|
|
|
|
line += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( size(tspans) > 1 )
|
|
|
|
popElement();
|
|
|
|
|
|
|
|
text = nil;
|
|
|
|
tspans = nil;
|
2012-08-01 23:28:56 +00:00
|
|
|
}
|
2012-08-01 22:49:15 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
# XML parsers element data callback
|
|
|
|
var data = func(data)
|
|
|
|
{
|
|
|
|
if( skip )
|
|
|
|
return;
|
2013-02-23 17:27:22 +00:00
|
|
|
|
|
|
|
if( size(data) and tspans != nil )
|
|
|
|
{
|
|
|
|
if( size(tspans) == 0 )
|
2013-07-19 22:53:34 +00:00
|
|
|
# If no tspan is found use text element itself
|
|
|
|
append(tspans, text);
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2013-07-15 20:29:19 +00:00
|
|
|
# If text contains xml entities it gets split at each entity. So let's
|
|
|
|
# glue it back into a single text...
|
2013-07-19 22:53:34 +00:00
|
|
|
tspans[-1]["text"] ~= data;
|
2013-02-23 17:27:22 +00:00
|
|
|
}
|
2012-08-01 22:49:15 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
call(func parsexml(path, start, end, data), nil, var err = []);
|
|
|
|
if( size(err) )
|
|
|
|
{
|
2014-08-24 11:55:32 +00:00
|
|
|
var msg = err[0];
|
|
|
|
for(var i = 1; i + 1 < size(err); i += 2)
|
|
|
|
{
|
|
|
|
# err = ['error message', 'file', line]
|
|
|
|
msg ~= (i == 1 ? "\n at " : "\n called from: ")
|
|
|
|
~ err[i] ~ ", line " ~ err[i + 1]
|
|
|
|
}
|
|
|
|
printlog("alert", msg ~ "\n ");
|
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
return 0;
|
|
|
|
}
|
2013-02-23 17:27:22 +00:00
|
|
|
|
2012-08-01 22:49:15 +00:00
|
|
|
return 1;
|
|
|
|
}
|