font: add support for fallback fonts

A top-level font now has a list of fallback fonts. When a glyph cannot
be found, we try each fallback font in turn, until we either find one
that has the glyph, or until we've exhausted the list.

To make this actually work in practise (read: to make performance
acceptable), the cache is re-worked and is now populated on demand.

It also supports non-ASCII characters, by using the 4-byte unicode
character as index instead.

Since having an array that can be indexed by a 4-byte value isn't
really viable, we now have a simple hash table instead of an array.
This commit is contained in:
Daniel Eklöf 2019-07-30 18:04:28 +02:00
parent 85ef9df586
commit 73b4d5d05a
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
7 changed files with 280 additions and 127 deletions

View file

@ -147,8 +147,12 @@ parse_section_main(const char *key, const char *value, struct config *conf,
} }
else if (strcmp(key, "font") == 0) { else if (strcmp(key, "font") == 0) {
free(conf->font); //free(conf->font);
conf->font = strdup(value); //conf->font = strdup(value);
char *copy = strdup(value);
for (const char *font = strtok(copy, ","); font != NULL; font = strtok(NULL, ","))
tll_push_back(conf->fonts, strdup(font));
free(copy);
} }
else if (strcmp(key, "workers") == 0) { else if (strcmp(key, "workers") == 0) {
@ -404,7 +408,7 @@ config_load(struct config *conf)
*conf = (struct config) { *conf = (struct config) {
.term = strdup("foot"), .term = strdup("foot"),
.shell = get_shell(), .shell = get_shell(),
.font = strdup("monospace"), .fonts = tll_init(),
.colors = { .colors = {
.fg = default_foreground, .fg = default_foreground,
@ -462,6 +466,7 @@ config_load(struct config *conf)
fclose(f); fclose(f);
out: out:
tll_push_back(conf->fonts, strdup("monospace"));
free(path); free(path);
return ret; return ret;
} }
@ -471,5 +476,6 @@ config_free(struct config conf)
{ {
free(conf.term); free(conf.term);
free(conf.shell); free(conf.shell);
free(conf.font); //free(conf.font);
tll_free_and_free(conf.fonts, free);
} }

View file

@ -4,11 +4,12 @@
#include <stdbool.h> #include <stdbool.h>
#include "terminal.h" #include "terminal.h"
#include "tllist.h"
struct config { struct config {
char *term; char *term;
char *shell; char *shell;
char *font; tll(char *) fonts;
struct { struct {
uint32_t fg; uint32_t fg;

221
font.c
View file

@ -1,33 +1,42 @@
#include "font.h" #include "font.h"
#include <stdlib.h> #include <stdlib.h>
#include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <wchar.h> #include <wchar.h>
#include <assert.h> #include <assert.h>
#include <threads.h>
#include <fontconfig/fontconfig.h> #include <fontconfig/fontconfig.h>
#define LOG_MODULE "font" #define LOG_MODULE "font"
#define LOG_ENABLE_DBG 0
#include "log.h" #include "log.h"
#define min(x, y) ((x) < (y) ? (x) : (y)) #define min(x, y) ((x) < (y) ? (x) : (y))
static FT_Library ft_lib; static FT_Library ft_lib;
static mtx_t ft_lock;
static const size_t cache_size = 512;
static void __attribute__((constructor)) static void __attribute__((constructor))
init(void) init(void)
{ {
FcInit(); FcInit();
FT_Init_FreeType(&ft_lib); FT_Init_FreeType(&ft_lib);
mtx_init(&ft_lock, mtx_plain);
} }
static void __attribute__((destructor)) static void __attribute__((destructor))
fini(void) fini(void)
{ {
FcFini(); mtx_destroy(&ft_lock);
FT_Done_FreeType(ft_lib); FT_Done_FreeType(ft_lib);
FcFini();
} }
#if 0
static void static void
font_populate_glyph_cache(struct font *font) font_populate_glyph_cache(struct font *font)
{ {
@ -35,12 +44,25 @@ font_populate_glyph_cache(struct font *font)
for (size_t i = 0; i < 256; i++) for (size_t i = 0; i < 256; i++)
font_glyph_for_utf8(font, &(char){i}, &font->cache[i]); font_glyph_for_utf8(font, &(char){i}, &font->cache[i]);
} }
#endif
bool static bool
font_from_name(const char *name, struct font *font) from_name(const char *base_name, const font_list_t *fallbacks, const char *attributes, struct font *font, bool is_fallback)
{ {
memset(font, 0, sizeof(*font)); memset(font, 0, sizeof(*font));
size_t attr_len = attributes == NULL ? 0 : strlen(attributes);
bool have_attrs = attr_len > 0;
char name[strlen(base_name) + (have_attrs ? 1 : 0) + attr_len + 1];
strcpy(name, base_name);
if (have_attrs){
strcat(name, ":");
strcat(name, attributes);
}
LOG_DBG("instantiating %s", name);
FcPattern *pattern = FcNameParse((const unsigned char *)name); FcPattern *pattern = FcNameParse((const unsigned char *)name);
if (pattern == NULL) { if (pattern == NULL) {
LOG_ERR("%s: failed to lookup font", name); LOG_ERR("%s: failed to lookup font", name);
@ -86,8 +108,10 @@ font_from_name(const char *name, struct font *font)
LOG_DBG("loading: %s", face_file); LOG_DBG("loading: %s", face_file);
mtx_lock(&ft_lock);
FT_Face ft_face; FT_Face ft_face;
FT_Error ft_err = FT_New_Face(ft_lib, (const char *)face_file, 0, &ft_face); FT_Error ft_err = FT_New_Face(ft_lib, (const char *)face_file, 0, &ft_face);
mtx_unlock(&ft_lock);
if (ft_err != 0) if (ft_err != 0)
LOG_ERR("%s: failed to create FreeType face", face_file); LOG_ERR("%s: failed to create FreeType face", face_file);
@ -122,11 +146,13 @@ font_from_name(const char *name, struct font *font)
else if (fc_hinting && fc_hintstyle == FC_HINT_SLIGHT) else if (fc_hinting && fc_hintstyle == FC_HINT_SLIGHT)
load_flags |= FT_LOAD_DEFAULT | FT_LOAD_TARGET_LIGHT; load_flags |= FT_LOAD_DEFAULT | FT_LOAD_TARGET_LIGHT;
else if (fc_rgba == FC_RGBA_RGB) { else if (fc_rgba == FC_RGBA_RGB) {
LOG_WARN("unimplemented: subpixel antialiasing"); if (!is_fallback)
LOG_WARN("unimplemented: subpixel antialiasing");
// load_flags |= FT_LOAD_DEFAULT | FT_LOAD_TARGET_LCD; // load_flags |= FT_LOAD_DEFAULT | FT_LOAD_TARGET_LCD;
load_flags |= FT_LOAD_DEFAULT | FT_LOAD_TARGET_NORMAL; load_flags |= FT_LOAD_DEFAULT | FT_LOAD_TARGET_NORMAL;
} else if (fc_rgba == FC_RGBA_VRGB) { } else if (fc_rgba == FC_RGBA_VRGB) {
LOG_WARN("unimplemented: subpixel antialiasing"); if (!is_fallback)
LOG_WARN("unimplemented: subpixel antialiasing");
//load_flags |= FT_LOAD_DEFAULT | FT_LOAD_TARGET_LCD_V; //load_flags |= FT_LOAD_DEFAULT | FT_LOAD_TARGET_LCD_V;
load_flags |= FT_LOAD_DEFAULT | FT_LOAD_TARGET_NORMAL; load_flags |= FT_LOAD_DEFAULT | FT_LOAD_TARGET_NORMAL;
} else } else
@ -144,15 +170,17 @@ font_from_name(const char *name, struct font *font)
if (!fc_antialias) if (!fc_antialias)
render_flags |= FT_RENDER_MODE_MONO; render_flags |= FT_RENDER_MODE_MONO;
else { else {
if (false) if (fc_rgba == FC_RGBA_RGB) {
; if (!is_fallback)
#if 0 LOG_WARN("unimplemented: subpixel antialiasing");
if (fc_rgba == FC_RGBA_RGB) //render_flags |= FT_RENDER_MODE_LCD;
render_flags |= FT_RENDER_MODE_LCD; render_flags |= FT_RENDER_MODE_NORMAL;
else if (fc_rgba == FC_RGBA_VRGB) } else if (fc_rgba == FC_RGBA_VRGB) {
render_flags |= FT_RENDER_MODE_LCD_V; if (!is_fallback)
#endif LOG_WARN("unimplemented: subpixel antialiasing");
else //render_flags |= FT_RENDER_MODE_LCD_V;
render_flags |= FT_RENDER_MODE_NORMAL;
} else
render_flags |= FT_RENDER_MODE_NORMAL; render_flags |= FT_RENDER_MODE_NORMAL;
} }
@ -173,25 +201,64 @@ font_from_name(const char *name, struct font *font)
font->face = ft_face; font->face = ft_face;
font->load_flags = load_flags; font->load_flags = load_flags;
font->render_flags = render_flags; font->render_flags = render_flags;
font_populate_glyph_cache(font); font->is_fallback = is_fallback;
if (fallbacks != NULL) {
tll_foreach(*fallbacks, it) {
size_t len = strlen(it->item) + (have_attrs ? 1 : 0) + attr_len + 1;
char *fallback = malloc(len);
strcpy(fallback, it->item);
if (have_attrs) {
strcat(fallback, ":");
strcat(fallback, attributes);
}
LOG_DBG("%s: adding fallback: %s", name, fallback);
tll_push_back(font->fallbacks, fallback);
}
}
if (is_fallback)
return true;
//font_populate_glyph_cache(font);
font->cache = calloc(cache_size, sizeof(font->cache[0]));
return true; return true;
} }
bool bool
font_glyph_for_utf8(struct font *font, const char *utf8, font_from_name(font_list_t names, const char *attributes, struct font *font)
struct glyph *glyph)
{ {
mbstate_t ps = {0}; if (tll_length(names) == 0)
wchar_t wc;
if (mbrtowc(&wc, utf8, 4, &ps) < 0) {
LOG_ERR("FAILED: %.4s", utf8);
return false; return false;
font_list_t fallbacks = tll_init();
bool skip_first = true;
tll_foreach(names, it) {
if (skip_first) {
skip_first = false;
continue;
}
tll_push_back(fallbacks, it->item);
} }
wprintf(L"CONVERTED: %.1s\n", &wc); bool ret = from_name(tll_front(names), &fallbacks, attributes, font, false);
mtx_lock(&font->lock); tll_free(fallbacks);
return ret;
}
static size_t
hash_index(wchar_t wc)
{
return wc % cache_size;
}
static bool
glyph_for_wchar(struct font *font, wchar_t wc, struct glyph *glyph)
{
/* /*
* LCD filter is per library instance. Thus we need to re-set it * LCD filter is per library instance. Thus we need to re-set it
* every time... * every time...
@ -204,6 +271,28 @@ font_glyph_for_utf8(struct font *font, const char *utf8,
goto err; goto err;
FT_UInt idx = FT_Get_Char_Index(font->face, wc); FT_UInt idx = FT_Get_Char_Index(font->face, wc);
if (idx == 0) {
/* LOG_DBG("no glyph found for %02x %02x %02x %02x", */
/* (unsigned char)utf8[0], (unsigned char)utf8[1], */
/* (unsigned char)utf8[2], (unsigned char)utf8[3]); */
/* Try fallback fonts */
tll_foreach(font->fallbacks, it) {
struct font fallback;
if (from_name(it->item, NULL, "", &fallback, true)) {
if (glyph_for_wchar(&fallback, wc, glyph)) {
font_destroy(&fallback);
return true;
}
font_destroy(&fallback);
}
}
if (font->is_fallback)
return false;
}
err = FT_Load_Glyph(font->face, idx, font->load_flags); err = FT_Load_Glyph(font->face, idx, font->load_flags);
if (err != 0) { if (err != 0) {
LOG_ERR("load failed"); LOG_ERR("load failed");
@ -267,36 +356,102 @@ font_glyph_for_utf8(struct font *font, const char *utf8,
} }
*glyph = (struct glyph){ *glyph = (struct glyph){
.wc = wc,
.data = data, .data = data,
.surf = surf, .surf = surf,
.left = font->face->glyph->bitmap_left, .left = font->face->glyph->bitmap_left,
.top = font->face->glyph->bitmap_top, .top = font->face->glyph->bitmap_top,
.format = cr_format,
.width = bitmap->width,
.height = bitmap->rows,
.stride = stride,
}; };
mtx_unlock(&font->lock);
return true; return true;
err: err:
mtx_unlock(&font->lock);
return false; return false;
} }
const struct glyph *
font_glyph_for_utf8(struct font *font, const char *utf8)
{
mtx_lock(&font->lock);
mbstate_t ps = {0};
wchar_t wc;
if (mbrtowc(&wc, utf8, 4, &ps) < 0) {
LOG_DBG("failed to convert utf-8 sequence %02x %02x %02x %02x to unicode",
(unsigned char)utf8[0], (unsigned char)utf8[1],
(unsigned char)utf8[2], (unsigned char)utf8[3]);
mtx_unlock(&font->lock);
return NULL;
}
assert(font->cache != NULL);
size_t hash_idx = hash_index(wc);
hash_entry_t *hash_entry = font->cache[hash_idx];
if (hash_entry != NULL) {
tll_foreach(*hash_entry, it) {
if (it->item.wc == wc) {
mtx_unlock(&font->lock);
return &it->item;
}
}
}
struct glyph glyph;
if (!glyph_for_wchar(font, wc, &glyph)) {
mtx_unlock(&font->lock);
return NULL;
}
if (hash_entry == NULL) {
hash_entry = calloc(1, sizeof(*hash_entry));
assert(font->cache[hash_idx] == NULL);
font->cache[hash_idx] = hash_entry;
}
assert(hash_entry != NULL);
tll_push_back(*hash_entry, glyph);
mtx_unlock(&font->lock);
return &tll_back(*hash_entry);
}
void void
font_destroy(struct font *font) font_destroy(struct font *font)
{ {
if (font->face != NULL) tll_free_and_free(font->fallbacks, free);
FT_Done_Face(font->face);
if (font->face != NULL) {
mtx_lock(&ft_lock);
FT_Done_Face(font->face);
mtx_unlock(&ft_lock);
}
if (font->cache != NULL) {
for (size_t i = 0; i < cache_size; i++) {
if (font->cache[i] == NULL)
continue;
tll_foreach(*font->cache[i], it) {
cairo_surface_destroy(it->item.surf);
free(it->item.data);
}
tll_free(*font->cache[i]);
free(font->cache[i]);
}
free(font->cache);
}
#if 0
for (size_t i = 0; i < 256; i++) { for (size_t i = 0; i < 256; i++) {
if (font->cache[i].surf != NULL) if (font->cache[i].surf != NULL)
cairo_surface_destroy(font->cache[i].surf); cairo_surface_destroy(font->cache[i].surf);
if (font->cache[i].data != NULL) if (font->cache[i].data != NULL)
free(font->cache[i].data); free(font->cache[i].data);
} }
#endif
mtx_destroy(&font->lock); mtx_destroy(&font->lock);
} }

55
font.h
View file

@ -3,9 +3,56 @@
#include <stdbool.h> #include <stdbool.h>
#include <threads.h> #include <threads.h>
#include "terminal.h" #include <ft2build.h>
#include FT_FREETYPE_H
#include FT_LCD_FILTER_H
#include <cairo.h>
bool font_from_name(const char *name, struct font *result); #include "tllist.h"
bool font_glyph_for_utf8( //#include "terminal.h"
struct font *font, const char *utf8, struct glyph *glyph);
typedef tll(const char *) font_list_t;
struct glyph {
wchar_t wc;
void *data;
cairo_surface_t *surf;
int left;
int top;
#if 0
int format;
int width;
int height;
int stride;
#endif
};
typedef tll(struct glyph) hash_entry_t;
struct font {
FT_Face face;
int load_flags;
int render_flags;
FT_LcdFilter lcd_filter;
struct {
double position;
double thickness;
} underline;
struct {
double position;
double thickness;
} strikeout;
bool is_fallback;
tll(char *) fallbacks;
//struct glyph cache[256];
hash_entry_t **cache;
mtx_t lock;
};
bool font_from_name(font_list_t names, const char *attributes, struct font *result);
const struct glyph *font_glyph_for_utf8(struct font *font, const char *utf8);
void font_destroy(struct font *font); void font_destroy(struct font *font);

28
main.c
View file

@ -291,8 +291,8 @@ main(int argc, char *const *argv)
break; break;
case 'f': case 'f':
free(conf.font); tll_free_and_free(conf.fonts, free);
conf.font = strdup(optarg); tll_push_back(conf.fonts, strdup(optarg));
break; break;
case 'h': case 'h':
@ -431,21 +431,21 @@ main(int argc, char *const *argv)
thrd_create(&term.render.workers.threads[i], &render_worker_thread, &worker_context[i]); thrd_create(&term.render.workers.threads[i], &render_worker_thread, &worker_context[i]);
} }
if (!font_from_name(conf.font, &term.fonts[0])) font_list_t font_names = tll_init();
tll_foreach(conf.fonts, it)
tll_push_back(font_names, it->item);
if (!font_from_name(font_names, "", &term.fonts[0])) {
tll_free(font_names);
goto out; goto out;
{
char fname[1024];
snprintf(fname, sizeof(fname), "%s:style=bold", conf.font);
font_from_name(fname, &term.fonts[1]);
snprintf(fname, sizeof(fname), "%s:style=italic", conf.font);
font_from_name(fname, &term.fonts[2]);
snprintf(fname, sizeof(fname), "%s:style=bold italic", conf.font);
font_from_name(fname, &term.fonts[3]);
} }
font_from_name(font_names, "style=bold", &term.fonts[1]);
font_from_name(font_names, "style=italic", &term.fonts[2]);
font_from_name(font_names, "style=bold italic", &term.fonts[3]);
tll_free(font_names);
/* Underline position and size */ /* Underline position and size */
for (size_t i = 0; i < sizeof(term.fonts) / sizeof(term.fonts[0]); i++) { for (size_t i = 0; i < sizeof(term.fonts) / sizeof(term.fonts[0]); i++) {
struct font *f = &term.fonts[i]; struct font *f = &term.fonts[i];

View file

@ -273,38 +273,12 @@ render_cell(struct terminal *term, struct buffer *buf, size_t buf_idx,
} }
struct font *font = attrs_to_font(term, &cell->attrs); struct font *font = attrs_to_font(term, &cell->attrs);
const struct glyph *glyph = font_glyph_for_utf8(font, cell->c);
struct glyph *glyph = NULL; if (glyph != NULL) {
if (strnlen(cell->c, 4) == 1) { cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
if (font->cache[(unsigned char)cell->c[0]].surf != NULL) cairo_set_source_rgb(cr, fg.r, fg.g, fg.b);
glyph = &font->cache[(unsigned char)cell->c[0]]; cairo_mask_surface(
} cr, glyph->surf, x + glyph->left, y + term->fextents.ascent - glyph->top);
struct glyph _glyph;
if (glyph == NULL) {
if (!font_glyph_for_utf8(font, cell->c, &_glyph)) {
LOG_ERR("FAILED: %.4s", cell->c);
return;
}
glyph = &_glyph;
}
assert(glyph != NULL);
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
cairo_set_source_rgb(cr, fg.r, fg.g, fg.b);
#if 0
cairo_surface_t *surf = cairo_image_surface_create_for_data(
glyph->data, glyph->format, glyph->width, glyph->height, glyph->stride);
cairo_mask_surface(cr, surf, x + glyph->left, y + term->fextents.ascent - glyph->top);
cairo_surface_destroy(surf);
#else
cairo_mask_surface(cr, glyph->surf, x + glyph->left, y + term->fextents.ascent - glyph->top);
#endif
if (glyph == &_glyph) {
cairo_surface_destroy(_glyph.surf);
free(_glyph.data);
} }
} }
@ -456,9 +430,12 @@ grid_render(struct terminal *term)
/* Erase old cursor (if we rendered a cursor last time) */ /* Erase old cursor (if we rendered a cursor last time) */
if (term->render.last_cursor.cell != NULL) { if (term->render.last_cursor.cell != NULL) {
struct cell *hack = (struct cell *)term->render.last_cursor.cell;
hack->attrs.clean = 0;
render_cell( render_cell(
term, buf, 0, term, buf, 0,
term->render.last_cursor.cell, //term->render.last_cursor.cell,
hack,
term->render.last_cursor.in_view.col, term->render.last_cursor.in_view.col,
term->render.last_cursor.in_view.row, false); term->render.last_cursor.in_view.row, false);
@ -622,11 +599,12 @@ grid_render(struct terminal *term)
term->cursor.col, view_aligned_row}; term->cursor.col, view_aligned_row};
struct row *row = grid_row_in_view(term->grid, view_aligned_row); struct row *row = grid_row_in_view(term->grid, view_aligned_row);
struct cell *cell = &row->cells[term->cursor.col];
term->render.last_cursor.cell = &row->cells[term->cursor.col]; cell->attrs.clean = 0;
term->render.last_cursor.cell = cell;
render_cell( render_cell(
term, buf, 0, term->render.last_cursor.cell, term, buf, 0, cell, term->cursor.col, view_aligned_row, true);
term->cursor.col, view_aligned_row, true);
wl_surface_damage_buffer( wl_surface_damage_buffer(
term->wl.surface, term->wl.surface,

View file

@ -7,17 +7,13 @@
#include <threads.h> #include <threads.h>
#include <semaphore.h> #include <semaphore.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_LCD_FILTER_H
#include <cairo.h> #include <cairo.h>
#include <wayland-client.h> #include <wayland-client.h>
#include <primary-selection-unstable-v1.h> #include <primary-selection-unstable-v1.h>
#include <xkbcommon/xkbcommon.h> #include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-keysyms.h> #include <xkbcommon/xkbcommon-keysyms.h>
#include "font.h"
#include "tllist.h" #include "tllist.h"
#define likely(c) __builtin_expect(!!(c), 1) #define likely(c) __builtin_expect(!!(c), 1)
@ -211,36 +207,6 @@ struct primary {
uint32_t serial; uint32_t serial;
}; };
struct glyph {
void *data;
cairo_surface_t *surf;
int left;
int top;
int format;
int width;
int height;
int stride;
};
struct font {
FT_Face face;
int load_flags;
int render_flags;
FT_LcdFilter lcd_filter;
struct {
double position;
double thickness;
} underline;
struct {
double position;
double thickness;
} strikeout;
struct glyph cache[256];
mtx_t lock;
};
enum cursor_style { CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BAR }; enum cursor_style { CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BAR };
struct terminal { struct terminal {