Clone in a terminal:
/*
	Copyright (C) 2016 Johan Mattsson

	This library is free software; you can redistribute it and/or modify 
	it under the terms of the GNU Lesser General Public License as 
	published by the Free Software Foundation; either version 3 of the 
	License, or (at your option) any later version.

	This library is distributed in the hope that it will be useful, but 
	WITHOUT ANY WARRANTY; without even the implied warranty of 
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
	Lesser General Public License for more details.
*/

using B;
using Cairo;

namespace BirdFont {

public class SvgFile : GLib.Object {

	SvgDrawing drawing;

	public SvgFile () {		
	}
	
	public EmbeddedSvg parse (string path) {
		string xml_data;
		
		try {
			FileUtils.get_contents (path, out xml_data);
			return parse_data (xml_data);
		} catch (GLib.Error error) {
			warning (error.message);
		}
		
		SvgDrawing drawing = new SvgDrawing ();
		return new EmbeddedSvg (drawing);
	}

	public EmbeddedSvg parse_data (string xml_data) {
		XmlParser xmlparser = new XmlParser (xml_data);
		SvgDrawing drawing = new SvgDrawing ();	
		
		if (xmlparser.validate ()) {
			Tag root = xmlparser.get_root_tag ();
			drawing = parse_svg_file (root);
			EmbeddedSvg svg = new EmbeddedSvg (drawing);
			svg.svg_data = xml_data;
			return svg;
		} else {
			warning ("Invalid xml file.");
		}
	
		return new EmbeddedSvg (drawing);
	}
	
	private SvgDrawing parse_svg_file (Tag tag) {
		drawing = new SvgDrawing ();

		foreach (Attribute attr in tag.get_attributes ()) {	
			if (attr.get_name () == "width") {
				drawing.width = parse_number (attr.get_content ());
			}

			if (attr.get_name () == "height") {
				drawing.height = parse_number (attr.get_content ());
			}
		}
				
		foreach (Tag t in tag) {
			string name = t.get_name ();
			
			if (name == "g") {
				parse_layer (drawing.root_layer, t);
			}

			if (name == "defs") {
				parse_defs (drawing, t);
			}
			
			parse_object (drawing.root_layer, t);
		}
		
		return drawing;
	}

	private void parse_layer (Layer layer, Tag tag) {
		bool hidden = false;

		foreach (Attribute attr in tag.get_attributes ()) {	
			if (attr.get_name () == "display" && attr.get_content () == "none") {
				hidden = true;
			}
			
			if (attr.get_name () == "visibility"
				&& (attr.get_content () == "hidden" 
					|| attr.get_content () == "collapse")) {
				hidden = true;
			}
		}
		
		if (hidden) {
			layer.visible = !hidden;
		}
					
		foreach (Tag t in tag) {
			string name = t.get_name ();
			
			if (name == "g") {
				Layer sublayer = new Layer ();
				parse_layer (layer, t);
				layer.subgroups.add (sublayer);
			}
			
			parse_object (layer, t);
		}

		foreach (Attribute attr in tag.get_attributes ()) {	
			if (attr.get_name () == "transform") {
				layer.transforms = parse_transform (attr.get_content ());
			}
		}
	}

	void parse_defs (SvgDrawing drawing, Tag tag) {
		foreach (Tag t in tag) {
			// FIXME: radial
			string name = t.get_name ();
			
			if (name == "linearGradient") {
				parse_linear_gradient (drawing, t);
			}
		}
		
		foreach (Gradient gradient in drawing.defs.gradients) {
			if (gradient.href != null) {
				Gradient? referenced;
				referenced = drawing.defs.get_gradient_for_id ((!) gradient.href);
				
				if (referenced != null) {
					gradient.copy_stops ((!) referenced);
				}
				
				gradient.href = null;
			}
		}
	}

	void parse_linear_gradient (SvgDrawing drawing, Tag tag) {
		Gradient gradient = new Gradient ();
		
		drawing.defs.add (gradient);
		
		foreach (Attribute attr in tag.get_attributes ()) {
			string name = attr.get_name ();
			
			// FIXME: gradientUnits
			
			if (name == "gradientTransform") {
				gradient.transforms = parse_transform (attr.get_content ());
			}

			if (name == "href") {
				gradient.href = attr.get_content ();
			}

			if (name == "x1") {
				gradient.x1 = parse_number (attr.get_content ());
			}

			if (name == "y1") {	
				gradient.y1 = parse_number (attr.get_content ());
			}
			
			if (name == "x2") {
				gradient.x2 = parse_number (attr.get_content ());
			}

			if (name == "y2") {
				gradient.y2 = parse_number (attr.get_content ());
			}

			if (name == "id") {
				gradient.id = attr.get_content ();
			}
		}
		
		foreach (Tag t in tag) {
			// FIXME: radial
			string name = t.get_name ();
			
			if (name == "stop") {
				parse_stop (gradient, t);
			}
		}
	}

	void parse_stop (Gradient gradient, Tag tag) {
		SvgStyle style = SvgStyle.parse (drawing.defs, tag.get_attributes ());
		Stop stop = new Stop ();
		
		gradient.stops.add (stop);
		
		foreach (Attribute attr in tag.get_attributes ()) {
			string name = attr.get_name ();
			
			if (name == "offset") {
				string stop_offset = attr.get_content ();
				
				if (stop_offset.index_of ("%") > -1) {
					stop_offset = stop_offset.replace ("%", "");
					stop.offset = parse_number (stop_offset) / 100.0;
				} else {
					stop.offset = parse_number (stop_offset);
				}
			}
		}
		
		string? stop_color = style.style.get ("stop-color");
		string? stop_opacity = style.style.get ("stop-opacity");
		Color? color = Color.black ();
		
		if (stop_color != null) {
			color = Color.parse (stop_color);
			
			if (color != null) {
				stop.color = (!) color;
			}
		}

		if (stop_opacity != null && color != null) {
			((!) color).a = parse_number (stop_opacity);
		}
	}
	
	void parse_object (Layer layer, Tag tag) {
		string name = tag.get_name ();
		
		if (name == "path") {
			parse_path (layer, tag);
		}
					
		if (name == "polygon") {
			parse_polygon (layer, tag);
		}

		if (name == "polyline") {
			parse_polyline (layer, tag);
		}
		
		if (name == "rect") {
			parse_rect (layer, tag);
		}

		if (name == "circle") {
			parse_circle (layer, tag);
		}

		if (name == "ellipse") {
			parse_ellipse (layer, tag);
		}
		
		if (name == "line") {
			parse_line (layer, tag);
		}
	}

	private void parse_polygon (Layer layer, Tag tag) {
	}
	
	private void parse_polyline (Layer layer, Tag tag) {
	}
	
	private void parse_rect (Layer layer, Tag tag) {
		Rectangle rectangle = new Rectangle ();

		foreach (Attribute attr in tag.get_attributes ()) {
			string attribute = attr.get_name ();
			
			if (attribute == "x") {
				rectangle.x = parse_number (attr.get_content ());
			}

			if (attribute == "y") {
				rectangle.y = parse_number (attr.get_content ());
			}

			if (attribute == "width") {
				rectangle.width = parse_number (attr.get_content ());
			}

			if (attribute == "height") {
				rectangle.height = parse_number (attr.get_content ());
			}
			
			if (attribute == "rx") {
				rectangle.rx = parse_number (attr.get_content ());
			}

			if (attribute == "ry") {
				rectangle.ry = parse_number (attr.get_content ());
			}
		}
		
		rectangle.transforms = get_transform (tag.get_attributes ());
		rectangle.style = SvgStyle.parse (drawing.defs,tag.get_attributes ());
		rectangle.visible = is_visible (tag);	
		
		layer.add_object (rectangle);
	}
	
	private void parse_circle (Layer layer, Tag tag) {
	}
	
	private void parse_ellipse (Layer layer, Tag tag) {
	}
	
	private void parse_line (Layer layer, Tag tag) {
	}
	
	// FIXME: reverse order?
	public SvgTransforms parse_transform (string transforms) {
		string[] functions;
		string transform = transforms;
		SvgTransforms transform_functions;
		
		transform_functions = new SvgTransforms ();
		
		transform = transform.replace ("\t", " ");
		transform = transform.replace ("\n", " ");
		transform = transform.replace ("\r", " ");
		
		// use only a single space as separator
		while (transform.index_of ("  ") > -1) {
			transform = transform.replace ("  ", " ");
		}
		
		if (unlikely (transform.index_of (")") == -1)) {
			warning ("No parenthesis in transform function.");
			return transform_functions;
		}
		
		 // add separator
		transform = transform.replace (") ", "|");
		transform = transform.replace (")", "|"); 
		functions = transform.split ("|");
		
		for (int i = 0; i < functions.length; i++) {
			if (functions[i].has_prefix ("translate")) {
				transform_functions.add (translate (functions[i]));
			}
			
			if (functions[i].has_prefix ("scale")) {
				transform_functions.add (scale (functions[i]));
			}

			if (functions[i].has_prefix ("matrix")) {
				transform_functions.add (matrix (functions[i]));
			}
			
			// TODO: rotate etc.
		}
		
		return transform_functions;
	}

	private SvgTransform matrix (string function) {
		string parameters = get_transform_parameters (function);
		string[] p = parameters.split (" ");
		SvgTransform transform = new SvgTransform ();
		transform.type = TransformType.MATRIX;
		
		if (unlikely (p.length != 6)) {
			warning ("Expecting six parameters for matrix transformation.");
			return transform;
		}
		
		for (int i = 0; i < 6; i++) {
			double argument = SvgParser.parse_double (p[i]);
			transform.arguments.add (argument);
		}
		
		return transform;
	}

    private static string remove_unit (string d) {
		string s = d.replace ("pt", "");
		s = s.replace ("pc", "");
		s = s.replace ("mm", "");
		s = s.replace ("cm", "");
		s = s.replace ("in", "");
		s = s.replace ("px", "");
		return s;
	}
    
	public static double parse_number (string? number_with_unit) {
		if (number_with_unit == null) {
			return 0;
		}
		
		string d = (!) number_with_unit;
		string s = remove_unit (d);
		double n = SvgParser.parse_double (s);
		
		if (d.has_suffix ("pt")) {
			n *= 1.25;
		} else if (d.has_suffix ("pc")) {
			n *= 15;
		} else if (d.has_suffix ("mm")) {
			n *= 3.543307;
		} else if (d.has_suffix ("cm")) {
			n *= 35.43307;
		} else if (d.has_suffix ("in")) {
			n *= 90;
		}
		
		return n;
	}
	
	private SvgTransform scale (string function) {
		string parameters = get_transform_parameters (function);
		string[] p = parameters.split (" ");
		SvgTransform transform = new SvgTransform ();
		transform.type = TransformType.SCALE;

		if (p.length > 0) {
			transform.arguments.add (SvgParser.parse_double (p[0]));
		}
		
		if (p.length > 1) {
			transform.arguments.add (SvgParser.parse_double (p[1]));
		}
		
		return transform;
	}
	
	private SvgTransform translate (string function) {
		string parameters = get_transform_parameters (function);
		string[] p = parameters.split (" ");
		SvgTransform transform = new SvgTransform ();
		transform.type = TransformType.TRANSLATE;
		
		if (p.length > 0) {
			transform.arguments.add (SvgParser.parse_double (p[0]));
		}
		
		if (p.length > 1) {
			transform.arguments.add (SvgParser.parse_double (p[1]));
		}
		
		return transform;
	}

	private string get_transform_parameters (string function) {
		int i;
		string param = "";
		
		i = function.index_of ("(");
		return_val_if_fail (i != -1, param);
		param = function.substring (i);

		param = param.replace ("(", "");
		param = param.replace ("\n", " ");
		param = param.replace ("\t", " ");
		param = param.replace (",", " ");
		
		while (param.index_of ("  ") > -1) {
			param.replace ("  ", " ");
		}
			
		return param.strip();			
	}

	private bool is_visible (Tag tag) {
		bool hidden = false;

		foreach (Attribute attr in tag.get_attributes ()) {
			if (attr.get_name () == "display" && attr.get_content () == "none") {
				hidden = true;
			}
			
			if (attr.get_name () == "visibility"
				&& (attr.get_content () == "hidden" 
					|| attr.get_content () == "collapse")) {
				hidden = true;
			}
		}
		
		return !hidden;
	}

	private SvgTransforms get_transform (Attributes attributes) {
		foreach (Attribute attr in attributes) {
			if (attr.get_name () == "transform") {
				return parse_transform (attr.get_content ());
			}
		}
		
		return new SvgTransforms ();
	}
	
	private void parse_path (Layer layer, Tag tag) {
		SvgPath path = new SvgPath ();

		foreach (Attribute attr in tag.get_attributes ()) {
			if (attr.get_name () == "d") {
				path.points = parse_points (attr.get_content ());
			}
		}
		
		path.transforms = get_transform (tag.get_attributes ());
		path.style = SvgStyle.parse (drawing.defs, tag.get_attributes ());
		path.visible = is_visible (tag);	
		
		layer.add_object (path);
	}

	public Gee.ArrayList<Points> parse_points (string data) {
		Gee.ArrayList<Points> path_data = new Gee.ArrayList<Points> ();
		Points points = new Points ();
		BezierPoints[] bezier_points;
		int points_size;
	
		SvgParser.get_bezier_points (data, out bezier_points, out points_size, true);

		for (int i = 0; i < points_size; i++) {
			// FIXME: add more types
			if (bezier_points[i].type == 'M') {
				points.x = bezier_points[i].x0;
				points.y = bezier_points[i].y0;
			} else if (bezier_points[i].type == 'C') {
				points.add (bezier_points[i].x0);
				points.add (bezier_points[i].y0);
				points.add (bezier_points[i].x1);
				points.add (bezier_points[i].y1);
				points.add (bezier_points[i].x2);
				points.add (bezier_points[i].y2);
			} else if (bezier_points[i].type == 'L') {
				points.add (bezier_points[i].x0);
				points.add (bezier_points[i].y0);
				points.add (bezier_points[i].x0);
				points.add (bezier_points[i].y0);
				points.add (bezier_points[i].x0);
				points.add (bezier_points[i].y0);
			} else if (bezier_points[i].type == 'z') {
				points.closed = true;
				path_data.add (points);
				points = new Points ();
			} else {
				string type = (!) bezier_points[i].type.to_string ();
				warning (@"SVG conversion not implemented for $type");
			}
		}

		if (points.point_data.size > 0) {
			path_data.add (points);
		}

		return path_data;
	}
}

}

Contact

Email: johan.mattsson.m@gmail.com
Mastodon: typo.social/@birdfont
GitHub: johanmattssonm
LinkedIn: johan-mattsson-2ab324112