Clone in a terminal:
GlyfTable.vala
/*
	Copyright (C) 2012, 2013, 2014 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.
*/

namespace BirdFont {

public class GlyfTable : OtfTable {
	// Flags for composite glyph
	const uint16 BOTH_ARE_WORDS = 1 << 0;
	const uint16 BOTH_ARE_XY_VALUES = 1 << 1;
	const uint16 ROUND_TO_GRID = 1 << 2;
	const uint16 SCALE = 1 << 3;
	const uint16 RESERVED = 1 << 4;
	const uint16 MORE_COMPONENTS = 1 << 5;
	const uint16 SCALE_X_Y = 1 << 6;
	const uint16 SCALE_WITH_ROTATTION = 1 << 7;
	const uint16 INSTRUCTIONS = 1 << 8;

	public int16 xmin = int16.MAX;
	public int16 ymin = int16.MAX;
	public int16 xmax = int16.MIN;
	public int16 ymax = int16.MIN;

	public FontData dis;
	public HeadTable head_table;
	public HmtxTable hmtx_table;
	public LocaTable loca_table;
	public CmapTable cmap_table; // cmap and post is tables are null when initialized and set in parse method
	public PostTable post_table;
	public KernTable kern_table;
	
	public Gee.ArrayList<uint32> location_offsets; 

	// sorted array of glyphs
	public Gee.ArrayList<GlyphCollection> glyphs;
	public Gee.ArrayList<GlyfData> glyf_data;
	
	uint16 max_points = 0;
	uint16 max_contours = 0;

	public GlyfTable (LocaTable l) {
		id = "glyf";
		loca_table = l;
		location_offsets = new Gee.ArrayList<uint32> ();
		glyphs = new Gee.ArrayList<GlyphCollection> ();
		glyf_data = new Gee.ArrayList<GlyfData> ();
	}	

	public int get_gid (string name) {
		int i = 0;
		
		foreach (GlyphCollection g in glyphs) {
			if (g.get_name () == name) {
				return i;
			}
			i++;
		}
		
		warning (@"Glyph $name not found in font.");
		return -1;
	}

	public uint16 get_max_contours () {
		return max_contours;
	}

	public uint16 get_max_points () {
		return max_points;
	}

	public uint16 get_first_char () {
		return 32; // space
	}
	
	public uint16 get_last_char () 
	requires (glyphs.size > 0) {
		GlyphCollection gc = glyphs.get (glyphs.size - 1);
		
		for (int i = glyphs.size - 1; i >= 0; i--) {
			gc  = glyphs.get (i);

			if (!gc.is_unassigned ()) {
				break;
			}
		}

		return (uint16) gc.get_unicode_character (); 
	}

	public void process () throws GLib.Error {
		FontData fd = new FontData ();
		uint last_len = 0;
		uint num_glyphs;
		Glyph g;
		
		create_glyph_table ();
		
		num_glyphs = glyphs.size;
		
		if (glyphs.size == 0) {
			warning ("No glyphs in glyf table.");
		}
		
		foreach (GlyphCollection gc in glyphs) {
			g = gc.get_current ();

			printd (@"adding glyph: $(gc.get_name ())\n");
						
			// set values for loca table
			assert (fd.length () % 4 == 0);
			location_offsets.add (fd.length ());
			process_glyph (g, fd);

			printd (@"glyf length: $(fd.length () - last_len)\n");
			printd (@"loca fd.length (): $(fd.length ())\n");
			
			last_len = fd.length ();
		}

		location_offsets.add (fd.length ()); // last entry in loca table is special
		
		// every glyph is padded, no padding to be done here
		assert (fd.length () % 4 == 0);

		font_data = fd;	
	}

	// necessary in order to have glyphs sorted according to ttf specification
	public void create_glyph_table () {
		Glyph g;
		GlyphCollection gc;
		GlyphCollection? gcn;
		Font font = OpenFontFormatWriter.get_current_font ();
		uint32 index;
		Gee.ArrayList<GlyphCollection> unassigned_glyphs;
		bool unassigned;
		
		// add notdef character and other special characters first
		glyphs.add (font.get_notdef_character ());
		glyphs.add (font.get_null_character ());
		glyphs.add (font.get_nonmarking_return ());
		glyphs.add (font.get_space ());
		
		unassigned_glyphs = new Gee.ArrayList<GlyphCollection> ();
		
		if (font.get_glyph_index (0) == null) {
			warning ("No glyphs in font.");
		}
			
		// add glyphs
		for (index = 0; (gcn = font.get_glyph_collection_index (index)) != null; index++) {		
			gc = (!) gcn;
			gc = gc.copy_deep ();
			g = gc.get_current ();
			g.remove_empty_paths ();
			unassigned = gc.is_unassigned ();

			if (unassigned) {
				unassigned_glyphs.add (gc);
			}
			
			if (g.unichar_code <= 27) { // skip control characters
				continue;
			}

			if (g.unichar_code == 32) { // skip space
				continue;
			}
						
			if (g.name == ".notdef") {
				continue;
			}

			if (!gc.is_unassigned ()) {
				glyphs.add (gc);
			}
		}
		
		glyphs.sort ((a, b) => {
			GlyphCollection g1, g2;
			g1 = (GlyphCollection) a;
			g2 = (GlyphCollection) b;
			return (int) (g1.get_unicode_character () - g2.get_unicode_character ());
		});
		
		foreach (GlyphCollection ug in unassigned_glyphs) {
			glyphs.add (ug);
		}
		
		
		int gid = 0;
		foreach (GlyphCollection ug in glyphs) {
			printd (@"Glyph: $(ug.get_name ()) GID: $(gid)\n");
			gid++;
		}
	}

	public void process_glyph (Glyph g, FontData fd) throws GLib.Error {
		uint16 end_point;
		uint16 npoints;
		int16 ncontours;
		uint16 nflags;
		int glyph_offset;
		uint len; 
		uint coordinate_length;
		GlyfData glyf_data;
		
		fd.seek_end (); // append glyph
		
		glyph_offset = (int) fd.length ();
		
		printd (@"glyph_offset: $(glyph_offset)\n");
		
		g.remove_empty_paths ();
		glyf_data = g.get_ttf_data ();
	
		int points = glyf_data.get_num_points ();
		if (unlikely (points >= uint16.MAX)) {
			warning (@"Too many points in glyph $(g.get_name ()) ($points)");
			throw new FileError.FAILED (t_("Too many control points") + " " + t_("in") + @" $(g.get_name ())");
		}
		
		this.glyf_data.add (glyf_data);
		
		if (g.get_visible_paths ().size == 0 || glyf_data.paths.size == 0 || glyf_data.get_ncontours () == 0) {
			// location_offsets will be equal to location_offset + 1 for
			// all empty glyphs
			g.set_empty_ttf (true);
			return;
		}
		
		g.set_empty_ttf (false);

		if (glyf_data.get_ncontours () == 0) {
			warning (@"No paths in $(g.get_name ()) ($(g.get_hex ())) can be exported.");
		}
		
		ncontours = (int16) glyf_data.paths.size;
		fd.add_short (ncontours);

		// bounding box
		fd.add_16 (glyf_data.bounding_box_xmin);
		fd.add_16 (glyf_data.bounding_box_ymin);
		fd.add_16 (glyf_data.bounding_box_xmax);
		fd.add_16 (glyf_data.bounding_box_ymax);

		// end points
		foreach (uint16 end in glyf_data.end_points) {
			fd.add_u16 (end);
		}

		fd.add_u16 (0); // instruction length 
		
		uint glyph_header = 12 + ncontours * 2;
		
		printd (@"next glyf: $(g.name) ($((uint32)g.unichar_code))\n");
		printd (@"glyf header length: $(glyph_header)\n");
				
		end_point = glyf_data.get_end_point ();
		ncontours = glyf_data.get_ncontours ();
		npoints = (ncontours > 0) ? end_point : 0; // +1?
		
		if (npoints > max_points) {
			max_points = npoints;
		}
		
		if (ncontours > max_contours) {
			max_contours = ncontours;
		}
		
		// flags		
		nflags = glyf_data.get_nflags ();
		if (unlikely (nflags != npoints)) {
			print ("glyf table data:\n");
			fd.dump ();
			warning (@"(nflags != npoints)  ($nflags != $npoints) in glyph $(g.name). ncontours: $ncontours");
		}
		assert (nflags == npoints);
	
		foreach (uint8 flag in glyf_data.flags) {
			fd.add_byte (flag);
		}

		printd (@"flags: $(nflags)\n");
		
		// x coordinates
		foreach (int16 x in glyf_data.coordinate_x) {
			fd.add_16 (x);
		}	

		// y coordinates
		foreach (int16 y in glyf_data.coordinate_y) {
			fd.add_16 (y);
		}
	
		len = fd.length ();
		coordinate_length = fd.length () - nflags - glyph_header;
		printd (@"coordinate_length: $(coordinate_length)\n");
		printd (@"fd.length (): $(fd.length ())\n");
		assert (fd.length () > nflags + glyph_header);
		
		printd (@"glyph_offset: $(glyph_offset)\n");
		printd (@"len: $(len)\n");

		// save bounding box for head table
		if (glyf_data.bounding_box_xmin < this.xmin) {
			printd (@"YMin in $(g.get_name ())\n");
			this.xmin = glyf_data.bounding_box_xmin;
		}
		
		if (glyf_data.bounding_box_ymin < this.ymin) {
			printd (@"YMin in $(g.get_name ())\n");
			this.ymin = glyf_data.bounding_box_ymin;
		}
		
		if (glyf_data.bounding_box_xmax > this.xmax) {
			printd (@"XMax in $(g.get_name ())\n");
			this.xmax = glyf_data.bounding_box_xmax;
		}
		
		if (glyf_data.bounding_box_ymax > this.ymax) {
			printd (@"YMax in $(g.get_name ())\n");
			this.ymax = glyf_data.bounding_box_ymax;
		}
		
		printd (@"length before padding: $(fd.length ())\n");
		
		// all glyphs needs padding for loca table to be correct
		while (fd.length () % 4 != 0) {
			fd.add (0);
		}
		printd (@"length after padding: $(fd.length ())\n");
	}
	
	public new void parse (FontData dis, CmapTable cmap_table, LocaTable loca, HmtxTable hmtx_table, HeadTable head_table, PostTable post_table, KernTable kern_table) throws GLib.Error {
		this.cmap_table = cmap_table;
		this.post_table = post_table;
		this.loca_table = loca;
		this.hmtx_table = hmtx_table;
		this.head_table = head_table;
		this.kern_table = kern_table;
		this.dis = dis;
	}
	
	Glyph parse_next_composite_glyf (FontData dis, unichar character, int pgid) throws Error {
		uint16 component_flags = 0;
		uint16 glyph_index;
		int16 arg1 = 0;
		int16 arg2 = 0;

		F2Dot14 scale;
		
		F2Dot14 scalex;
		F2Dot14 scaley;
		
		F2Dot14 scale01;
		F2Dot14 scale10;

		Glyph glyph, linked_glyph;
		Gee.ArrayList<int> x = new Gee.ArrayList<int> ();
		Gee.ArrayList<int> y = new Gee.ArrayList<int> ();
		Gee.ArrayList<int> gid = new Gee.ArrayList<int> ();
		
		double xmin, xmax;
		double units_per_em = head_table.get_units_per_em ();
		
		int glid;
		
		StringBuilder name = new StringBuilder ();
		name.append_unichar (character);
		
		glyph = new Glyph (name.str, character);
		
		do {
			component_flags = dis.read_ushort ();
			glyph_index = dis.read_ushort ();
			
			if ((component_flags & BOTH_ARE_WORDS) > 0) {
				arg1 = dis.read_short ();
				arg2 = dis.read_short ();			
			} else if ((component_flags & BOTH_ARE_XY_VALUES) > 0) {
				arg1 = dis.read_byte ();
				arg2 = dis.read_byte ();
			}
			
			gid.add (glyph_index);
			x.add (arg1);
			y.add (arg2);

			// if ((component_flags & RESERVED) > 0)
			
			if ((component_flags & SCALE) > 0) {
				scale = dis.read_f2dot14 ();
			} else if ((component_flags & SCALE_X_Y) > 0) {
				scalex = dis.read_f2dot14 ();
				scaley = dis.read_f2dot14 ();
			} else if ((component_flags & SCALE_WITH_ROTATTION) > 0) {
				scalex = dis.read_f2dot14 ();
				scale01 = dis.read_f2dot14 ();
				scale10 = dis.read_f2dot14 ();
				scaley = dis.read_f2dot14 ();
			}
			
		} while ((component_flags & MORE_COMPONENTS) > 0);
	
		
		for (int i = 0; i < gid.size; i++) {
			// compensate xmax ymax with coordinate
			glid = gid.get (i);

			if (glid == pgid) {
				warning ("Cannot link a glyph to it self.");
				continue;
			}

			linked_glyph = parse_next_glyf (dis, character, glid, out xmin, out xmax, units_per_em);
		}
		
		return glyph;
	}
	
	Glyph parse_next_glyf (FontData dis, unichar character, int gid, out double xmin, out double xmax, double units_per_em) throws Error {
		uint16* end_points = null;
		uint8* instructions = null;
		uint8* flags = null;
		int16* xcoordinates = null;
		int16* ycoordinates = null;
		
		int npoints = 0;
		
		int16 ncontours;
		int16 ixmin; // set boundaries
		int16 iymin;
		int16 ixmax;
		int16 iymax;
		uint16 ninstructions;

		int16 rxmin = int16.MAX; // real xmin
		int16 rymin = int16.MAX;;
		int16 rxmax = int16.MIN;
		int16 rymax = int16.MIN;
				
		int nflags;
		
		Error? error = null;
		
		uint start, end, len;
		
		StringBuilder name = new StringBuilder ();
		name.append_unichar (character);

		xmin = 0;
		xmax = 0;

		start = loca_table.get_offset (gid);
		end = loca_table.get_offset (gid + 1);
		len = start - end;

		dis.seek (offset + start);
		
		ncontours = dis.read_short ();
		
		return_val_if_fail (start < end, new Glyph (""));

		if (ncontours == 0) {
			warning (@"Zero contours in glyph $(name.str).");

			// should skip body
		}
				
		if (ncontours == -1) {
			return parse_next_composite_glyf (dis, character, gid);
		}

		return_val_if_fail (ncontours < len, new Glyph (""));
						
		if (ncontours < -1) {
			warning (@"$ncontours contours in glyf table.");
			error = new BadFormat.PARSE ("Invalid glyf");
			throw error;
		}
		
		ixmin = dis.read_short ();
		iymin = dis.read_short ();
		ixmax = dis.read_short ();
		iymax = dis.read_short ();

		end_points = new uint16[ncontours + 1];
		for (int i = 0; i < ncontours; i++) {
			end_points[i] = dis.read_ushort (); // FIXA: mind shot vector is negative
			
			if (i > 0 && end_points[i] < end_points[i -1]) {
				warning (@"Next endpoint has bad value in $(name.str). (end_points[i] > end_points[i -1])  ($(end_points[i]) > $(end_points[i -1])) i: $i ncontours: $ncontours");
			}
		}
		
		if (ncontours > 0) {
			npoints = end_points[ncontours - 1] + 1;
		} else {
			npoints = 0;
		}
		
		return_val_if_fail (npoints < len, new Glyph.no_lines (""));
		
		ninstructions = dis.read_ushort ();
		
		return_val_if_fail (ninstructions < len, new Glyph.no_lines (""));
		
		instructions = new uint8[ninstructions + 1];
		uint8 repeat;
		for (int i = 0; i < ninstructions; i++) {
			instructions[i] = dis.read_byte ();
		}

		nflags = 0;
		flags = new uint8[npoints + 1];
		for (int i = 0; i < npoints; i++) {
			flags[i] = dis.read_byte ();
			
			if ((flags[i] & CoordinateFlags.REPEAT) > 0) {
				repeat = dis.read_byte ();
				
				if (i + repeat >= npoints) {
					error = new BadFormat.PARSE ("Too many flags in glyf in glyph $(name.str). (i >= ninstructions).");
					break;
				}
				
				for (int j = 0; j < repeat; j++) {
					flags[j + i + 1] = flags[i];
				}
				
				nflags += repeat;
				i += repeat;
			}
			
			nflags++;
		}
		
		if (nflags != npoints) {
			warning (@"(nflags != npoints) ($nflags != $npoints) in $(name.str)");
			error = new BadFormat.PARSE (@"Wrong number of flags in glyph $(name.str). (nflags != npoints) ($nflags != $npoints)");
		}
		
		warn_if_fail (nflags == npoints);

		printd (@"npoints: $npoints\n");
		printd (@"ncontours: $ncontours\n");
		printd (@"ninstructions: $ninstructions\n");
		printd (@"nflags: $nflags\n");
				
		int16 last = 0;
		xcoordinates = new int16[npoints + 1];
		for (int i = 0; i < npoints; i++) {
			if ((flags[i] & CoordinateFlags.X_SHORT_VECTOR) > 0) {	
				if ((flags[i] & CoordinateFlags.X_SHORT_VECTOR_POSITIVE) > 0) {
					xcoordinates[i] = last + dis.read_byte ();
				} else {
					xcoordinates[i] = last - dis.read_byte ();
				}
			} else {
				if ((flags[i] & CoordinateFlags.X_IS_SAME) > 0) {
					xcoordinates[i] = last;
				} else {
					xcoordinates[i] = last + dis.read_short ();
				}
			}
			
			last = xcoordinates[i];
			
			if (last > rxmax) rxmax = last;
			if (last < rxmin) rxmin = last;
			
			if (!(ixmin <= last <= ixmax))	{
				stderr.printf (@"x is out of bounds in glyph $(name.str). ($ixmin <= $last <= $ixmax) char $((uint)character)\n");
			}
			
			if (!(head_table.xmin <= last <= head_table.xmax))	{
				stderr.printf (@"x is outside of of font bounding box in glyph $(name.str). ($(head_table.xmin) <= $last <= $(head_table.xmax)) char $((uint)character)\n");
			}			
		}
		
		last = 0;
		ycoordinates = new int16[npoints + 1];
		for (int i = 0; i < npoints; i++) {
			if ((flags[i] & CoordinateFlags.Y_SHORT_VECTOR) > 0) {	
				if ((flags[i] & CoordinateFlags.Y_SHORT_VECTOR_POSITIVE) > 0) {
					ycoordinates[i] = last + dis.read_byte ();
				} else {
					ycoordinates[i] = last - dis.read_byte ();
				}
			} else {
				if ((flags[i] & CoordinateFlags.Y_IS_SAME) > 0) {
					ycoordinates[i] = last;
				} else {
					ycoordinates[i] = last + dis.read_short ();
				}
			}
			
			last = ycoordinates[i];

			if (last > rymax) rymax = last;
			if (last < rymin) rymin = last;
			
			if (!(iymin <= last <= iymax))	{
				stderr.printf (@"y is out of bounds in glyph $(name.str). ($iymin <= $last <= $iymax) char $((uint)character)\n");
			}
			
			if (!(head_table.ymin <= last <= head_table.ymax))	{
				stderr.printf (@"y is outside of of font bounding box in glyph $(name.str). ($(head_table.ymin) <= $last <= $(head_table.ymax)) char $((uint)character)\n");
			}
		}
		
		if (rymin != iymin || rxmin != ixmin || rxmax != ixmax || rymax != iymax) {
			warning (@"Warning real boundary for glyph does not match boundary set in glyph header for glyph $(name.str).");
			stderr.printf (@"ymin: $rymin header: $iymin\n");
			stderr.printf (@"xmin: $rxmin header: $ixmin\n");
			stderr.printf (@"ymax: $rymax header: $iymax\n");
			stderr.printf (@"xmax: $rxmax header: $ixmax\n");
		} 
		
		int j = 0;
		int first_point;
		int last_point = 0;
		Glyph glyph;
		double x, y;

		glyph = new Glyph (name.str, character);
		
		xmin = ixmin * 1000.0 / units_per_em;
		xmax = ixmax * 1000.0 / units_per_em;
		
		for (int i = 0; i < ncontours; i++) {
			x = 0;
			y = 0;
			
			Path path = new Path ();
			EditPoint edit_point = new EditPoint ();
			bool prev_is_curve = false;
			
			first_point = j;
			last_point = end_points[i];
			for (; j <= end_points[i]; j++) {

				if (j >= npoints) {
					warning (@"j >= npoints in glyph $(name.str). (j: $j, end_points[i]: $(end_points[i]), npoints: $npoints)");
					break;
				}
								
				x = xcoordinates[j] * 1000.0 / units_per_em; // in proportion to em width
				y = ycoordinates[j] * 1000.0 / units_per_em;
				
				if ((flags[j] & CoordinateFlags.ON_PATH) > 0) {
					// Point
					edit_point = new EditPoint ();
					edit_point.set_position (x, y);
					path.add_point (edit_point);
					
					if (prev_is_curve) {
						edit_point.get_left_handle ().set_point_type (PointType.NONE);
						edit_point.get_left_handle ().length = 0;
					} else {
						path.recalculate_linear_handles_for_point (edit_point);
					}
					
					prev_is_curve = false;
				} else {
									
					if (prev_is_curve) {
						x = x - (x - edit_point.right_handle.x) / 2;
						y = y - (y - edit_point.right_handle.y) / 2;

						edit_point = new EditPoint ();
						edit_point.set_position (x, y);
						path.add_point (edit_point);
					}

					x = xcoordinates[j] * 1000.0 / units_per_em; // in proportion to em width
					y = ycoordinates[j] * 1000.0 / units_per_em;

					edit_point.get_left_handle ().set_point_type (PointType.NONE);
					edit_point.get_left_handle ().length = 0;
						
					edit_point.type = PointType.CUBIC;
					edit_point.get_right_handle ().set_point_type (PointType.CUBIC);
					edit_point.get_right_handle ().move_to_coordinate (x, y);
					
					prev_is_curve = true;
				} 
			}
			
			// last to first point
			if (prev_is_curve) {
				x = xcoordinates[first_point] * 1000.0 / units_per_em; // in proportion to em width
				y = ycoordinates[first_point] * 1000.0 / units_per_em;
				
				x = x - (x - edit_point.right_handle.x) / 2;
				y = y - (y - edit_point.right_handle.y) / 2;
				
				edit_point = new EditPoint ();
				edit_point.set_position (x, y);
				path.add_point (edit_point);
				
				x = xcoordinates[first_point] * 1000.0 / units_per_em; // in proportion to em width
				y = ycoordinates[first_point] * 1000.0 / units_per_em;

				edit_point.get_left_handle ().set_point_type (PointType.CUBIC);
				edit_point.get_left_handle ().length = 0;
					
				edit_point.type = PointType.CUBIC;
				edit_point.get_right_handle ().set_point_type (PointType.CUBIC);
				edit_point.get_right_handle ().move_to_coordinate (x, y);
			}
			
			// curve last to first
			x = xcoordinates[first_point] * 1000.0 / units_per_em; // in proportion to em width
			y = ycoordinates[first_point] * 1000.0 / units_per_em;
			edit_point.type = PointType.CUBIC;
			edit_point.get_right_handle ().set_point_type (PointType.CUBIC);
			edit_point.get_right_handle ().move_to_coordinate (x, y);
			
			path.close ();
			
			glyph.add_path (path);
		}
		
		// glyphs with no bounding boxes
		if (ixmax <= ixmin) {
			warning (@"Bounding box is bad. (xmax == xmin) ($xmax == $xmin)");
			
			var visible_paths = glyph.get_visible_paths ();
			if (visible_paths.size > 0) {
				
				Path ps = visible_paths.get (0);
				
				ps.update_region_boundaries ();
				xmin = ps.xmin;
				xmax = ps.xmax;

				foreach (Path p in visible_paths) {
					p.update_region_boundaries ();
					
					if (p.xmin < xmin) {
						xmin = p.xmin;
					}
					
					if (p.xmax > xmax) {
						xmax = p.xmax;
					}
				}
				
			}
		}
						
		if (end_points != null) {
			delete end_points;
		}
		
		if (instructions != null) {
			delete instructions;
		}
		
		if (flags != null) {
			delete flags;
		}
		
		if (xcoordinates != null) {
			delete xcoordinates;
		}
		
		if (ycoordinates != null) {
			delete ycoordinates;
		}
		
		if (error != null) {
			warning ("Failed to parse glyph");
			throw (!) error;
		}
		
		return glyph;
	}
}

}

Contact

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