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) {
free(conf->font);
conf->font = strdup(value);
//free(conf->font);
//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) {
@ -404,7 +408,7 @@ config_load(struct config *conf)
*conf = (struct config) {
.term = strdup("foot"),
.shell = get_shell(),
.font = strdup("monospace"),
.fonts = tll_init(),
.colors = {
.fg = default_foreground,
@ -462,6 +466,7 @@ config_load(struct config *conf)
fclose(f);
out:
tll_push_back(conf->fonts, strdup("monospace"));
free(path);
return ret;
}
@ -471,5 +476,6 @@ config_free(struct config conf)
{
free(conf.term);
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 "terminal.h"
#include "tllist.h"
struct config {
char *term;
char *shell;
char *font;
tll(char *) fonts;
struct {
uint32_t fg;

221
font.c
View file

@ -1,33 +1,42 @@
#include "font.h"
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <wchar.h>
#include <assert.h>
#include <threads.h>
#include <fontconfig/fontconfig.h>
#define LOG_MODULE "font"
#define LOG_ENABLE_DBG 0
#include "log.h"
#define min(x, y) ((x) < (y) ? (x) : (y))
static FT_Library ft_lib;
static mtx_t ft_lock;
static const size_t cache_size = 512;
static void __attribute__((constructor))
init(void)
{
FcInit();
FT_Init_FreeType(&ft_lib);
mtx_init(&ft_lock, mtx_plain);
}
static void __attribute__((destructor))
fini(void)
{
FcFini();
mtx_destroy(&ft_lock);
FT_Done_FreeType(ft_lib);
FcFini();
}
#if 0
static void
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++)
font_glyph_for_utf8(font, &(char){i}, &font->cache[i]);
}
#endif
bool
font_from_name(const char *name, struct font *font)
static bool
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));
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);
if (pattern == NULL) {
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);
mtx_lock(&ft_lock);
FT_Face 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)
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)
load_flags |= FT_LOAD_DEFAULT | FT_LOAD_TARGET_LIGHT;
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_NORMAL;
} 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_NORMAL;
} else
@ -144,15 +170,17 @@ font_from_name(const char *name, struct font *font)
if (!fc_antialias)
render_flags |= FT_RENDER_MODE_MONO;
else {
if (false)
;
#if 0
if (fc_rgba == FC_RGBA_RGB)
render_flags |= FT_RENDER_MODE_LCD;
else if (fc_rgba == FC_RGBA_VRGB)
render_flags |= FT_RENDER_MODE_LCD_V;
#endif
else
if (fc_rgba == FC_RGBA_RGB) {
if (!is_fallback)
LOG_WARN("unimplemented: subpixel antialiasing");
//render_flags |= FT_RENDER_MODE_LCD;
render_flags |= FT_RENDER_MODE_NORMAL;
} else if (fc_rgba == FC_RGBA_VRGB) {
if (!is_fallback)
LOG_WARN("unimplemented: subpixel antialiasing");
//render_flags |= FT_RENDER_MODE_LCD_V;
render_flags |= FT_RENDER_MODE_NORMAL;
} else
render_flags |= FT_RENDER_MODE_NORMAL;
}
@ -173,25 +201,64 @@ font_from_name(const char *name, struct font *font)
font->face = ft_face;
font->load_flags = load_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;
}
bool
font_glyph_for_utf8(struct font *font, const char *utf8,
struct glyph *glyph)
font_from_name(font_list_t names, const char *attributes, struct font *font)
{
mbstate_t ps = {0};
wchar_t wc;
if (mbrtowc(&wc, utf8, 4, &ps) < 0) {
LOG_ERR("FAILED: %.4s", utf8);
if (tll_length(names) == 0)
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
* every time...
@ -204,6 +271,28 @@ font_glyph_for_utf8(struct font *font, const char *utf8,
goto err;
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);
if (err != 0) {
LOG_ERR("load failed");
@ -267,36 +356,102 @@ font_glyph_for_utf8(struct font *font, const char *utf8,
}
*glyph = (struct glyph){
.wc = wc,
.data = data,
.surf = surf,
.left = font->face->glyph->bitmap_left,
.top = font->face->glyph->bitmap_top,
.format = cr_format,
.width = bitmap->width,
.height = bitmap->rows,
.stride = stride,
};
mtx_unlock(&font->lock);
return true;
err:
mtx_unlock(&font->lock);
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
font_destroy(struct font *font)
{
if (font->face != NULL)
FT_Done_Face(font->face);
tll_free_and_free(font->fallbacks, free);
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++) {
if (font->cache[i].surf != NULL)
cairo_surface_destroy(font->cache[i].surf);
if (font->cache[i].data != NULL)
free(font->cache[i].data);
}
#endif
mtx_destroy(&font->lock);
}

55
font.h
View file

@ -3,9 +3,56 @@
#include <stdbool.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);
bool font_glyph_for_utf8(
struct font *font, const char *utf8, struct glyph *glyph);
#include "tllist.h"
//#include "terminal.h"
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);

28
main.c
View file

@ -291,8 +291,8 @@ main(int argc, char *const *argv)
break;
case 'f':
free(conf.font);
conf.font = strdup(optarg);
tll_free_and_free(conf.fonts, free);
tll_push_back(conf.fonts, strdup(optarg));
break;
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]);
}
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;
{
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 */
for (size_t i = 0; i < sizeof(term.fonts) / sizeof(term.fonts[0]); 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 glyph *glyph = NULL;
if (strnlen(cell->c, 4) == 1) {
if (font->cache[(unsigned char)cell->c[0]].surf != NULL)
glyph = &font->cache[(unsigned char)cell->c[0]];
}
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);
const struct glyph *glyph = font_glyph_for_utf8(font, cell->c);
if (glyph != NULL) {
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
cairo_set_source_rgb(cr, fg.r, fg.g, fg.b);
cairo_mask_surface(
cr, glyph->surf, x + glyph->left, y + term->fextents.ascent - glyph->top);
}
}
@ -456,9 +430,12 @@ grid_render(struct terminal *term)
/* Erase old cursor (if we rendered a cursor last time) */
if (term->render.last_cursor.cell != NULL) {
struct cell *hack = (struct cell *)term->render.last_cursor.cell;
hack->attrs.clean = 0;
render_cell(
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.row, false);
@ -622,11 +599,12 @@ grid_render(struct terminal *term)
term->cursor.col, 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(
term, buf, 0, term->render.last_cursor.cell,
term->cursor.col, view_aligned_row, true);
term, buf, 0, cell, term->cursor.col, view_aligned_row, true);
wl_surface_damage_buffer(
term->wl.surface,

View file

@ -7,17 +7,13 @@
#include <threads.h>
#include <semaphore.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_LCD_FILTER_H
#include <cairo.h>
#include <wayland-client.h>
#include <primary-selection-unstable-v1.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-keysyms.h>
#include "font.h"
#include "tllist.h"
#define likely(c) __builtin_expect(!!(c), 1)
@ -211,36 +207,6 @@ struct primary {
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 };
struct terminal {