Canvas: Add a basic SVG parser.
- Add basic std::string implementation to a Nasal submodule named std. - Add basic SVG parser parsing its results into a canvas group element.
This commit is contained in:
parent
07cb9e7df0
commit
007e9bc33a
2 changed files with 445 additions and 0 deletions
371
Nasal/canvas/svg.nas
Normal file
371
Nasal/canvas/svg.nas
Normal file
|
@ -0,0 +1,371 @@
|
|||
# Parse an xml file into a canvas group element
|
||||
#
|
||||
# @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)
|
||||
var parsesvg = func(group, path)
|
||||
{
|
||||
if( !isa(group, Group) )
|
||||
die("Invalid argument group (type != Group)");
|
||||
|
||||
var level = 0;
|
||||
var skip = 0;
|
||||
var stack = [group];
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Parse a transformation (matrix)
|
||||
# http://www.w3.org/TR/SVG/coords.html#TransformAttribute
|
||||
var parseTransform = func(tf)
|
||||
{
|
||||
if( tf == nil )
|
||||
return;
|
||||
|
||||
tf = std.string.new(tf);
|
||||
|
||||
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 = [];
|
||||
end = start_args;
|
||||
while(1)
|
||||
{
|
||||
var start_num = tf.find_first_not_of(",\t\n ", end + 1);
|
||||
if( start_num < 0 )
|
||||
break;
|
||||
if( tf[start_num] == ')' )
|
||||
break;
|
||||
|
||||
end = tf.find_first_of("),\t\n ", start_num + 1);
|
||||
if( end < 0 )
|
||||
break;
|
||||
append(values, tf.substr(start_num, end - start_num));
|
||||
}
|
||||
|
||||
var type = tf.substr(start_type, end_type - start_type);
|
||||
|
||||
if( type == "translate" )
|
||||
# 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,
|
||||
);
|
||||
else if( type == "matrix" )
|
||||
{
|
||||
if( size(values) == 6 )
|
||||
stack[-1].createTransform(values);
|
||||
else
|
||||
debug.dump('invalid transform', type, values);
|
||||
}
|
||||
else
|
||||
debug.dump(['unknown transform', type, values]);
|
||||
}
|
||||
};
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Parse a path
|
||||
# http://www.w3.org/TR/SVG/paths.html#PathData
|
||||
|
||||
# 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
|
||||
};
|
||||
|
||||
var parsePath = func(d)
|
||||
{
|
||||
if( d == nil )
|
||||
return;
|
||||
|
||||
var path_data = std.string.new(d);
|
||||
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
|
||||
var cmd = path_data.substr(pos, 1);
|
||||
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;
|
||||
|
||||
append(args, path_data.substr(start_num, pos > 0 ? pos - start_num : nil));
|
||||
}
|
||||
|
||||
# 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] );
|
||||
}
|
||||
|
||||
if( math.mod(size(args), 7) > 0 )
|
||||
debug.dump('too much coords for cmd', cmd, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
var cmd_vg = cmd_map[cmd];
|
||||
if( cmd_vg == nil )
|
||||
{
|
||||
debug.dump('command not found', cmd, args);
|
||||
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
|
||||
# subsequent pairs are treated as implicit lineto commands.
|
||||
if( cmd == 'm' )
|
||||
cmd_vg = cmd_map['l'];
|
||||
}
|
||||
|
||||
if( math.mod(size(args), num_coords) > 0 )
|
||||
debug.warn('too much coords for cmd: ' ~ cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stack[-1].setData(cmds, coords);
|
||||
};
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Parse a css style attribute
|
||||
var parseStyle = func(style)
|
||||
{
|
||||
if( style == nil )
|
||||
return {};
|
||||
|
||||
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;
|
||||
|
||||
var value = string.trim(part[1]);
|
||||
if( !size(value) )
|
||||
continue;
|
||||
|
||||
styles[key] = value;
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 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 ];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var style = parseStyle(attr['style']);
|
||||
|
||||
if( style['display'] == 'none' )
|
||||
{
|
||||
skip = level - 1;
|
||||
return;
|
||||
}
|
||||
else if( name == "g" )
|
||||
{
|
||||
append(stack, stack[-1].createChild('group', attr['id']));
|
||||
}
|
||||
else if( name == "text" )
|
||||
{
|
||||
append(stack, stack[-1].createChild('text', attr['id']));
|
||||
stack[-1].setTranslation(attr['x'], attr['y']);
|
||||
|
||||
# http://www.w3.org/TR/SVG/text.html#TextAnchorProperty
|
||||
var h_align = style["text-anchor"];
|
||||
if( h_align == "end" )
|
||||
h_align = "right";
|
||||
else if( h_align == "middle" )
|
||||
h_align = "center";
|
||||
else # "start"
|
||||
h_align = "left";
|
||||
stack[-1].setAlignment(h_align ~ "-baseline");
|
||||
# TODO vertical align
|
||||
|
||||
stack[-1].setColor(parseColor(style['fill']));
|
||||
stack[-1].setFont("UbuntuMono-B.ttf");
|
||||
#stack[-1].setFont("LiberationFonts/LiberationMono-Bold.ttf");
|
||||
}
|
||||
else if( name == "path" or name == "rect" )
|
||||
{
|
||||
append(stack, stack[-1].createChild('path', attr['id']));
|
||||
var d = attr['d'];
|
||||
|
||||
if( name == "rect" )
|
||||
{
|
||||
var width = attr['width'];
|
||||
var height = attr['height'];
|
||||
var x = attr['x'];
|
||||
var y = attr['y'];
|
||||
|
||||
d = sprintf("M%f,%f v%f h%f v%fz", x, y, height, width, -height);
|
||||
}
|
||||
|
||||
parsePath(d);
|
||||
|
||||
var w = style['stroke-width'];
|
||||
stack[-1].setStrokeLineWidth( w != nil ? w : 1 );
|
||||
stack[-1].setColor(parseColor(style['stroke']));
|
||||
|
||||
var fill = style['fill'];
|
||||
if( fill != nil and fill != "none" )
|
||||
{
|
||||
stack[-1].setColorFill(parseColor(fill));
|
||||
stack[-1].setFill(1);
|
||||
}
|
||||
|
||||
# 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...
|
||||
stack[-1].setStrokeDashPattern(split(',', dash));
|
||||
}
|
||||
else if( name == "tspan" )
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
print("parsesvg: skipping unknown element '" ~ name ~ "'");
|
||||
skip = level;
|
||||
return;
|
||||
}
|
||||
|
||||
parseTransform(attr['transform']);
|
||||
};
|
||||
|
||||
# XML parsers element close callback
|
||||
var end = func(name)
|
||||
{
|
||||
level -= 1;
|
||||
|
||||
if( skip )
|
||||
{
|
||||
if( level <= skip )
|
||||
skip = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if( name == 'g' or name == 'text' or name == 'path' or name == 'rect' )
|
||||
pop(stack);
|
||||
};
|
||||
|
||||
# XML parsers element data callback
|
||||
var data = func(data)
|
||||
{
|
||||
if( skip )
|
||||
return;
|
||||
|
||||
if( size(data) and isa(stack[-1], Text) )
|
||||
stack[-1].setText(data);
|
||||
};
|
||||
|
||||
if( path[0] != '/' )
|
||||
path = getprop("/sim/fg-root") ~ "/" ~ path;
|
||||
|
||||
call(func parsexml(path, start, end, data), nil, var err = []);
|
||||
if( size(err) )
|
||||
{
|
||||
debug.dump(err);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
74
Nasal/std/string.nas
Normal file
74
Nasal/std/string.nas
Normal file
|
@ -0,0 +1,74 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# A C++ like string class (http://en.cppreference.com/w/cpp/string/basic_string)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# capture global string
|
||||
var _string = string;
|
||||
|
||||
var string = {
|
||||
# public:
|
||||
new: func(str)
|
||||
{
|
||||
return { parents: [string], _str: str };
|
||||
},
|
||||
find_first_of: func(s, pos = 0)
|
||||
{
|
||||
return me._find(pos, size(me._str), s, 1);
|
||||
},
|
||||
find: func(s, pos = 0)
|
||||
{
|
||||
return me.find_first_of(s, pos);
|
||||
},
|
||||
find_first_not_of: func(s, pos = 0)
|
||||
{
|
||||
return me._find(pos, size(me._str), s, 0);
|
||||
},
|
||||
substr: func(pos, len = nil)
|
||||
{
|
||||
return substr(me._str, pos, len);
|
||||
},
|
||||
size: func()
|
||||
{
|
||||
return size(me._str);
|
||||
},
|
||||
# private:
|
||||
_eq: func(pos, s)
|
||||
{
|
||||
for(var i = 0; i < size(s); i += 1)
|
||||
if( me._str[pos] == s[i] )
|
||||
return 1;
|
||||
return 0;
|
||||
},
|
||||
_find: func(first, last, s, eq)
|
||||
{
|
||||
var sign = first <= last ? 1 : -1;
|
||||
for(var i = first; sign * i < last; i += sign)
|
||||
if( me._eq(i, s) == eq )
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
# converts a string to an unsigned integer
|
||||
var stoul = func(str, base = 10)
|
||||
{
|
||||
var val = 0;
|
||||
for(var pos = 0; pos < size(str); pos += 1)
|
||||
{
|
||||
var c = str[pos];
|
||||
|
||||
if( _string.isdigit(c) )
|
||||
var digval = c - `0`;
|
||||
else if( _string.isalpha(c) )
|
||||
var digval = _string.toupper(c) - `A` + 10;
|
||||
else
|
||||
break;
|
||||
|
||||
if( digval >= base )
|
||||
break;
|
||||
|
||||
val = val * base + digval;
|
||||
}
|
||||
|
||||
return val;
|
||||
};
|
Loading…
Reference in a new issue