diff --git a/Nasal/canvas/svg.nas b/Nasal/canvas/svg.nas new file mode 100644 index 000000000..5357b0d84 --- /dev/null +++ b/Nasal/canvas/svg.nas @@ -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( []), which specifies a translation by tx and ty. If + # 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; +} diff --git a/Nasal/std/string.nas b/Nasal/std/string.nas new file mode 100644 index 000000000..d45d83ae9 --- /dev/null +++ b/Nasal/std/string.nas @@ -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; +};