Clone in a terminal:
text_drawing.c
#include <stdlib.h>
#include <glib.h>
#include <stdio.h>
#include <math.h>
#include <harfbuzz/hb.h>
#include <harfbuzz/hb-ft.h>
#include <cairo.h>
#include <cairo-ft.h>
 
static GMutex font_config_lock;
FcConfig* font_config = NULL; 

typedef struct svg_bird_font_item_t {
	int font_size;
	FT_Library ft_library;
	FT_Face ft_face;
	hb_font_t *hb_font;
} svg_bird_font_item;

gchar* svg_bird_find_font_file (const gchar* font_name);
void svg_bird_font_item_delete (svg_bird_font_item* item);

void svg_bird_get_extent (svg_bird_font_item* font, const char* text, double* width, double* height) {
	hb_buffer_t *hb_buffer;	

	hb_buffer = hb_buffer_create ();
	hb_buffer_add_utf8 (hb_buffer, text, -1, 0, -1);
	hb_buffer_guess_segment_properties (hb_buffer);

	hb_shape (font->hb_font, hb_buffer, NULL, 0);

	unsigned int len = hb_buffer_get_length (hb_buffer);
	hb_glyph_info_t *info = hb_buffer_get_glyph_infos (hb_buffer, NULL);
	hb_glyph_position_t *pos = hb_buffer_get_glyph_positions (hb_buffer, NULL);
	
	double current_x = 0;
	double current_y = 0;
	
	for (unsigned int i = 0; i < len; i++) {
		double x_position = current_x + pos[i].x_offset / 64.;
		double y_position = current_y + pos[i].y_offset / 64.;
 		
		current_x += pos[i].x_advance / 64.;
		current_y += pos[i].y_advance / 64.;
	}

	*width = current_x;
	*height = current_y;

	if (HB_DIRECTION_IS_HORIZONTAL (hb_buffer_get_direction(hb_buffer))) {
		*height += font->font_size * 1.25;
	} else {
		*width += font->font_size * 1.25;
	}
}

gboolean svg_bird_has_font_config () {
	g_mutex_lock (&font_config_lock);
	gboolean exists = font_config != NULL;
	g_mutex_unlock (&font_config_lock);
	return exists;
}

void svg_bird_set_font_config (FcConfig* f) {
	g_mutex_lock (&font_config_lock);
	font_config = f;
	g_mutex_unlock (&font_config_lock);
}

svg_bird_font_item* svg_bird_font_item_create (const char* font_family, int font_size) {
	FT_Error ft_error;
	svg_bird_font_item* font = malloc (sizeof (svg_bird_font_item));
	memset (font, 0, sizeof (svg_bird_font_item));
 
	char* font_file = svg_bird_find_font_file (font_family);

	FT_Library ft_library = 0;
	FT_Face ft_face = 0;
	
	font->font_size = font_size;
	if ((ft_error = FT_Init_FreeType (&ft_library))) {
		g_warning ("Can't init freetype");
		svg_bird_font_item_delete (font);
		return NULL;
 	}
 	
 	font->ft_library = ft_library;
 	
	if ((ft_error = FT_New_Face (font->ft_library, font_file, 0, &ft_face))) {
		g_warning ("Can't find freetype font %s %s.", font_family, font_file);
		svg_bird_font_item_delete (font);
		return NULL;
 	}
 	
 	font->ft_face = ft_face;
 	
	if ((ft_error = FT_Set_Char_Size (font->ft_face, font_size * 64, font_size * 64, 0, 0)))  {
		g_warning ("Can't set font size.");
		svg_bird_font_item_delete (font);
		return NULL;
	}

	font->hb_font = hb_ft_font_create (font->ft_face, NULL);

	if (font->hb_font == NULL) {
		g_warning ("Can't create harfbuzz font for %s", font_file);
		svg_bird_font_item_delete (font);
		return NULL;
	}

	free (font_file);
	return font;
}

void svg_bird_font_item_delete (svg_bird_font_item* item) {
	if (item) {
		if (item->ft_face) {
			FT_Done_Face (item->ft_face);
		}

		if (item->hb_font) {
			hb_font_destroy (item->hb_font);
		}
		
		if (item->ft_library) {
			FT_Done_FreeType (item->ft_library);
		}
				
		free (item);
	}
}

void svg_bird_draw_text (cairo_t* cr, svg_bird_font_item* font, const char* text) {
	cairo_save (cr);
	
	hb_buffer_t *hb_buffer;	

	hb_buffer = hb_buffer_create ();
	hb_buffer_add_utf8 (hb_buffer, text, -1, 0, -1);
	hb_buffer_guess_segment_properties (hb_buffer);

	hb_shape (font->hb_font, hb_buffer, NULL, 0);

	unsigned int len = hb_buffer_get_length (hb_buffer);
	hb_glyph_info_t *info = hb_buffer_get_glyph_infos (hb_buffer, NULL);
	hb_glyph_position_t *pos = hb_buffer_get_glyph_positions (hb_buffer, NULL);
	
	double current_x = 0;
	double current_y = 0;
	for (unsigned int i = 0; i < len; i++) {
		hb_codepoint_t gid   = info[i].codepoint;
		unsigned int cluster = info[i].cluster;
		double x_position = current_x + pos[i].x_offset / 64.;
		double y_position = current_y + pos[i].y_offset / 64.;
 		
		current_x += pos[i].x_advance / 64.;
		current_y += pos[i].y_advance / 64.;
	}

	double width = 0;
	double height = 0;
	
	cairo_font_face_t *cairo_face;
	cairo_face = cairo_ft_font_face_create_for_ft_face (font->ft_face, 0);
	cairo_set_font_face (cr, cairo_face);
	cairo_set_font_size (cr, font->font_size);

	cairo_font_extents_t font_extents;
	cairo_font_extents (cr, &font_extents);

	if (HB_DIRECTION_IS_HORIZONTAL (hb_buffer_get_direction(hb_buffer))) {
		double baseline = (font->font_size - font_extents.height) * .5 + font_extents.ascent;
		cairo_translate (cr, 0, baseline);
	} else {
		cairo_translate (cr, font->font_size * .5, 0);
	}
	
	cairo_glyph_t *cairo_glyphs = cairo_glyph_allocate (len);
	current_x = 0;
	current_y = 0;
	
	cairo_matrix_t matrix;
	cairo_get_matrix (cr, &matrix);
	cairo_matrix_invert (&matrix);
	
	for (unsigned int i = 0; i < len; i++) {
		cairo_glyphs[i].index = info[i].codepoint;
		cairo_glyphs[i].x = current_x + pos[i].x_offset / 64.;
		cairo_glyphs[i].y = -(current_y + pos[i].y_offset / 64.);

		double dx = pos[i].x_advance / 64.0;
		double dy = pos[i].y_advance / 64.0;
 		
		cairo_matrix_transform_distance (&matrix, &dx, &dy);

		current_x += dx;
		current_y += dy;
 	}
	
	cairo_show_glyphs (cr, cairo_glyphs, len);
	cairo_glyph_free (cairo_glyphs);

	cairo_font_face_destroy (cairo_face);
 
	hb_buffer_destroy (hb_buffer);
	cairo_restore (cr);
 }

gchar* svg_bird_find_font_file (const gchar* font_name) {
	const FcChar8* name;
	FcPattern* search_pattern;
	FcPattern* font;
	FcChar8* file;
	gchar* path;
	FcObjectSet* font_properties;
	FcFontSet* fonts;
	int i;
	
	if (!svg_bird_has_font_config ()) {
		g_warning("Font config not loaded.");
		return NULL;
	}

	g_mutex_lock (&font_config_lock);

	if (font_config == NULL) {
		g_warning("Font config not loaded.");
		return NULL;
	}
	
	path = NULL;
	name = font_name;

	// match any font as fallback
	search_pattern = FcPatternCreate ();
	FcPatternAddInteger (search_pattern, FC_SLANT, FC_SLANT_ROMAN);
	
	font_properties = FcObjectSetBuild (FC_FILE, NULL);

	fonts = FcFontList (font_config, search_pattern, font_properties);
	
	if (fonts->nfont > 0) {
		for (i = 0; i < fonts->nfont; i++) {
			font = fonts->fonts[i];
			
			if (FcPatternGetString(font, FC_FILE, 0, &file) == FcResultMatch) {
				path = g_strdup ((gchar*) file);
				break;
			}
			
			FcPatternDestroy (font);
		}
	}
	
	FcPatternDestroy (search_pattern);
	
	// search for a family name
	search_pattern = FcPatternCreate ();
	FcPatternAddString (search_pattern, FC_FAMILY, name);
	FcPatternAddInteger (search_pattern, FC_SLANT, FC_SLANT_ROMAN);
	FcPatternAddBool (search_pattern, FC_SCALABLE, FcTrue);
	FcPatternAddInteger (search_pattern, FC_WEIGHT, FC_WEIGHT_MEDIUM);

	fonts = FcFontList (font_config, search_pattern, font_properties);
	
	if (fonts->nfont > 0) {
		for (i = 0; i < fonts->nfont; i++) {
			font = fonts->fonts[i];
			
			if (FcPatternGetString(font, FC_FILE, 0, &file) == FcResultMatch) {
				g_free (path);
				path = g_strdup ((gchar*) file);
				break;
			}
			
			FcPatternDestroy (font);
		}
	}

	FcPatternDestroy (search_pattern);
	
	g_mutex_unlock (&font_config_lock);
	return path;
}

Contact

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