mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-05 04:06:08 -05:00
*All* glyphs are cached. But we never set the 'wc' member of failed glyphs, causing a later cache lookup of the same glyph miss in the cache, and thus re-generate yet another glyph (that is inserted into the cache). I.e. if the same (invalid) glyph is rendered over and over again, we'll end up growing the cache indefinitely. Fix by setting the 'wc' member of invalid glyphs. This causes a cache lookup to hit. But, we must also check the glyphs validity, and return NULL if the glyph isn't valid. Finally, when destroying a font, skip invalid glyphs, since they have no resources that needs to be freed.
517 lines
15 KiB
C
517 lines
15 KiB
C
#include "font.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <wchar.h>
|
|
#include <assert.h>
|
|
#include <threads.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)
|
|
{
|
|
mtx_destroy(&ft_lock);
|
|
FT_Done_FreeType(ft_lib);
|
|
FcFini();
|
|
}
|
|
|
|
static bool
|
|
from_font_set(FcPattern *pattern, FcFontSet *fonts, int start_idx, 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;
|
|
|
|
FcChar8 *face_file = NULL;
|
|
FcPattern *final_pattern = NULL;
|
|
int font_idx = -1;
|
|
|
|
for (int i = start_idx; i < fonts->nfont; i++) {
|
|
FcPattern *pat = FcFontRenderPrepare(NULL, pattern, fonts->fonts[i]);
|
|
assert(pat != NULL);
|
|
|
|
if (FcPatternGetString(pat, FC_FT_FACE, 0, &face_file) != FcResultMatch) {
|
|
if (FcPatternGetString(pat, FC_FILE, 0, &face_file) != FcResultMatch) {
|
|
FcPatternDestroy(pat);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
final_pattern = pat;
|
|
font_idx = i;
|
|
break;
|
|
}
|
|
|
|
assert(font_idx != -1);
|
|
assert(final_pattern != NULL);
|
|
|
|
double dpi;
|
|
if (FcPatternGetDouble(final_pattern, FC_DPI, 0, &dpi) != FcResultMatch)
|
|
dpi = 96;
|
|
|
|
double size;
|
|
if (FcPatternGetDouble(final_pattern, FC_PIXEL_SIZE, 0, &size)) {
|
|
LOG_ERR("%s: failed to get size", face_file);
|
|
FcPatternDestroy(final_pattern);
|
|
return false;
|
|
}
|
|
|
|
FcBool scalable;
|
|
if (FcPatternGetBool(final_pattern, FC_SCALABLE, 0, &scalable) != FcResultMatch)
|
|
scalable = FcTrue;
|
|
|
|
double pixel_fixup;
|
|
if (FcPatternGetDouble(final_pattern, "pixelsizefixupfactor", 0, &pixel_fixup) != FcResultMatch)
|
|
pixel_fixup = 1.;
|
|
|
|
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);
|
|
|
|
if ((ft_err = FT_Set_Char_Size(ft_face, size * 64, 0, 0, 0)) != 0)
|
|
LOG_WARN("failed to set character size");
|
|
|
|
FcBool fc_hinting;
|
|
if (FcPatternGetBool(final_pattern, FC_HINTING,0, &fc_hinting) != FcResultMatch)
|
|
fc_hinting = FcTrue;
|
|
|
|
FcBool fc_antialias;
|
|
if (FcPatternGetBool(final_pattern, FC_ANTIALIAS, 0, &fc_antialias) != FcResultMatch)
|
|
fc_antialias = FcTrue;
|
|
|
|
int fc_hintstyle;
|
|
if (FcPatternGetInteger(final_pattern, FC_HINT_STYLE, 0, &fc_hintstyle) != FcResultMatch)
|
|
fc_hintstyle = FC_HINT_SLIGHT;
|
|
|
|
int fc_rgba;
|
|
if (FcPatternGetInteger(final_pattern, FC_RGBA, 0, &fc_rgba) != FcResultMatch)
|
|
fc_rgba = FC_RGBA_UNKNOWN;
|
|
|
|
int load_flags = 0;
|
|
if (!fc_antialias) {
|
|
if (!fc_hinting || fc_hintstyle == FC_HINT_NONE)
|
|
load_flags |= FT_LOAD_MONOCHROME | FT_LOAD_NO_HINTING | FT_LOAD_TARGET_NORMAL;
|
|
else
|
|
load_flags |= FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO;
|
|
} else {
|
|
if (!fc_hinting || fc_hintstyle == FC_HINT_NONE)
|
|
load_flags |= FT_LOAD_DEFAULT | FT_LOAD_NO_HINTING | FT_LOAD_TARGET_NORMAL;
|
|
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) {
|
|
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) {
|
|
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
|
|
load_flags |= FT_LOAD_DEFAULT | FT_LOAD_TARGET_NORMAL;
|
|
}
|
|
|
|
FcBool fc_embeddedbitmap;
|
|
if (FcPatternGetBool(final_pattern, FC_EMBEDDED_BITMAP, 0, &fc_embeddedbitmap) != FcResultMatch)
|
|
fc_embeddedbitmap = FcTrue;
|
|
|
|
if (!fc_embeddedbitmap)
|
|
load_flags |= FT_LOAD_NO_BITMAP;
|
|
|
|
int render_flags = 0;
|
|
if (!fc_antialias)
|
|
render_flags |= FT_RENDER_MODE_MONO;
|
|
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;
|
|
}
|
|
|
|
int fc_lcdfilter;
|
|
if (FcPatternGetInteger(final_pattern, FC_LCD_FILTER, 0, &fc_lcdfilter) != FcResultMatch)
|
|
fc_lcdfilter = FC_LCD_DEFAULT;
|
|
|
|
switch (fc_lcdfilter) {
|
|
case FC_LCD_NONE: font->lcd_filter = FT_LCD_FILTER_NONE; break;
|
|
case FC_LCD_DEFAULT: font->lcd_filter = FT_LCD_FILTER_DEFAULT; break;
|
|
case FC_LCD_LIGHT: font->lcd_filter = FT_LCD_FILTER_LIGHT; break;
|
|
case FC_LCD_LEGACY: font->lcd_filter = FT_LCD_FILTER_LEGACY; break;
|
|
}
|
|
|
|
FcPatternDestroy(final_pattern);
|
|
|
|
mtx_init(&font->lock, mtx_plain);
|
|
font->face = ft_face;
|
|
font->load_flags = load_flags | FT_LOAD_COLOR;
|
|
font->render_flags = render_flags;
|
|
font->is_fallback = is_fallback;
|
|
font->pixel_size_fixup = scalable ? pixel_fixup : 1.;
|
|
font->fc_idx = font_idx;
|
|
|
|
if (!is_fallback) {
|
|
font->fc_pattern = pattern;
|
|
font->fc_fonts = fonts;
|
|
font->cache = calloc(cache_size, sizeof(font->cache[0]));
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
from_name(const char *base_name, const font_list_t *fallbacks, const char *attributes, struct font *font, bool is_fallback)
|
|
{
|
|
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);
|
|
return false;
|
|
}
|
|
|
|
if (!FcConfigSubstitute(NULL, pattern, FcMatchPattern)) {
|
|
LOG_ERR("%s: failed to do config substitution", name);
|
|
FcPatternDestroy(pattern);
|
|
return false;
|
|
}
|
|
|
|
FcDefaultSubstitute(pattern);
|
|
|
|
FcResult result;
|
|
FcFontSet *fonts = FcFontSort(NULL, pattern, FcTrue, NULL, &result);
|
|
if (result != FcResultMatch) {
|
|
LOG_ERR("%s: failed to match font", name);
|
|
FcPatternDestroy(pattern);
|
|
return false;
|
|
}
|
|
|
|
if (!from_font_set(pattern, fonts, 0, fallbacks, attributes, font, is_fallback)) {
|
|
FcFontSetDestroy(fonts);
|
|
FcPatternDestroy(pattern);
|
|
return false;
|
|
}
|
|
|
|
if (is_fallback) {
|
|
FcFontSetDestroy(fonts);
|
|
FcPatternDestroy(pattern);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
font_from_name(font_list_t names, const char *attributes, struct font *font)
|
|
{
|
|
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);
|
|
}
|
|
|
|
bool ret = from_name(tll_front(names), &fallbacks, attributes, font, false);
|
|
|
|
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...
|
|
*
|
|
* Also note that many freetype builds lack this feature
|
|
* (FT_CONFIG_OPTION_SUBPIXEL_RENDERING must be defined, and isn't
|
|
* by default) */
|
|
FT_Error err = FT_Library_SetLcdFilter(ft_lib, font->lcd_filter);
|
|
if (err != 0 && err != FT_Err_Unimplemented_Feature)
|
|
goto err;
|
|
|
|
FT_UInt idx = FT_Get_Char_Index(font->face, wc);
|
|
if (idx == 0) {
|
|
/* No glyph in this font, try fallback fonts */
|
|
struct font fallback;
|
|
|
|
/* Try user configured fallback fonts */
|
|
tll_foreach(font->fallbacks, it) {
|
|
if (from_name(it->item, NULL, "", &fallback, true)) {
|
|
if (glyph_for_wchar(&fallback, wc, glyph)) {
|
|
LOG_DBG("%C: used fallback %s (fixup = %f)",
|
|
wc, it->item, fallback.pixel_size_fixup);
|
|
font_destroy(&fallback);
|
|
return true;
|
|
}
|
|
|
|
font_destroy(&fallback);
|
|
}
|
|
}
|
|
|
|
if (font->is_fallback)
|
|
return false;
|
|
|
|
/* Try fontconfig fallback fonts */
|
|
|
|
assert(font->fc_pattern != NULL);
|
|
assert(font->fc_fonts != NULL);
|
|
assert(font->fc_idx != -1);
|
|
|
|
for (int i = font->fc_idx + 1; i < font->fc_fonts->nfont; i++) {
|
|
if (from_font_set(font->fc_pattern, font->fc_fonts, i, NULL, "", &fallback, true)) {
|
|
if (glyph_for_wchar(&fallback, wc, glyph)) {
|
|
LOG_DBG("%C: used fontconfig fallback", wc);
|
|
font_destroy(&fallback);
|
|
return true;
|
|
}
|
|
|
|
font_destroy(&fallback);
|
|
}
|
|
}
|
|
|
|
LOG_WARN("%C: no glyph found (in neither the main font, "
|
|
"nor any fallback fonts)", wc);
|
|
}
|
|
|
|
err = FT_Load_Glyph(font->face, idx, font->load_flags);
|
|
if (err != 0) {
|
|
LOG_ERR("load failed");
|
|
goto err;
|
|
}
|
|
|
|
err = FT_Render_Glyph(font->face->glyph, font->render_flags);
|
|
if (err != 0)
|
|
goto err;
|
|
|
|
assert(font->face->glyph->format == FT_GLYPH_FORMAT_BITMAP);
|
|
|
|
FT_Bitmap *bitmap = &font->face->glyph->bitmap;
|
|
assert(bitmap->pixel_mode == FT_PIXEL_MODE_MONO ||
|
|
bitmap->pixel_mode == FT_PIXEL_MODE_GRAY ||
|
|
bitmap->pixel_mode == FT_PIXEL_MODE_BGRA);
|
|
|
|
/* Map FT pixel format to cairo surface format */
|
|
cairo_format_t cr_format =
|
|
bitmap->pixel_mode == FT_PIXEL_MODE_MONO ? CAIRO_FORMAT_A1 :
|
|
bitmap->pixel_mode == FT_PIXEL_MODE_GRAY ? CAIRO_FORMAT_A8 :
|
|
bitmap->pixel_mode == FT_PIXEL_MODE_BGRA ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_INVALID;
|
|
|
|
int stride = cairo_format_stride_for_width(cr_format, bitmap->width);
|
|
assert(stride >= bitmap->pitch);
|
|
|
|
uint8_t *data = malloc(bitmap->rows * stride);
|
|
assert(bitmap->pitch >= 0);
|
|
|
|
/* Convert FT bitmap to cairo surface (well, the backing image) */
|
|
switch (bitmap->pixel_mode) {
|
|
case FT_PIXEL_MODE_MONO:
|
|
for (size_t r = 0; r < bitmap->rows; r++) {
|
|
for (size_t c = 0; c < (bitmap->width + 7) / 8; c++) {
|
|
uint8_t v = bitmap->buffer[r * bitmap->pitch + c];
|
|
uint8_t reversed = 0;
|
|
for (size_t i = 0; i < min(8, bitmap->width - c * 8); i++)
|
|
reversed |= ((v >> (7 - i)) & 1) << i;
|
|
|
|
data[r * stride + c] = reversed;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case FT_PIXEL_MODE_GRAY:
|
|
for (size_t r = 0; r < bitmap->rows; r++) {
|
|
for (size_t c = 0; c < bitmap->width; c++)
|
|
data[r * stride + c] = bitmap->buffer[r * bitmap->pitch + c];
|
|
}
|
|
break;
|
|
|
|
case FT_PIXEL_MODE_BGRA:
|
|
assert(stride == bitmap->pitch);
|
|
memcpy(data, bitmap->buffer, bitmap->rows * bitmap->pitch);
|
|
break;
|
|
|
|
default:
|
|
LOG_ERR("unimplemented FreeType bitmap pixel mode: %d",
|
|
bitmap->pixel_mode);
|
|
free(data);
|
|
goto err;
|
|
}
|
|
|
|
cairo_surface_t *surf = cairo_image_surface_create_for_data(
|
|
data, cr_format, bitmap->width, bitmap->rows, stride);
|
|
|
|
if (cairo_surface_status(surf) != CAIRO_STATUS_SUCCESS) {
|
|
free(data);
|
|
cairo_surface_destroy(surf);
|
|
goto err;
|
|
}
|
|
|
|
*glyph = (struct glyph){
|
|
.wc = wc,
|
|
.width = wcwidth(wc),
|
|
.surf = surf,
|
|
.left = font->face->glyph->bitmap_left,
|
|
.top = font->face->glyph->bitmap_top,
|
|
.pixel_size_fixup = font->pixel_size_fixup,
|
|
.valid = true,
|
|
};
|
|
|
|
return true;
|
|
|
|
err:
|
|
*glyph = (struct glyph){
|
|
.wc = wc,
|
|
.valid = false,
|
|
};
|
|
return false;
|
|
}
|
|
|
|
const struct glyph *
|
|
font_glyph_for_wc(struct font *font, wchar_t wc)
|
|
{
|
|
mtx_lock(&font->lock);
|
|
|
|
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.valid ? &it->item : NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
struct glyph glyph;
|
|
bool got_glyph = glyph_for_wchar(font, wc, &glyph);
|
|
|
|
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 got_glyph ? &tll_back(*hash_entry) : NULL;
|
|
}
|
|
|
|
void
|
|
font_destroy(struct font *font)
|
|
{
|
|
tll_free_and_free(font->fallbacks, free);
|
|
|
|
if (font->face != NULL) {
|
|
mtx_lock(&ft_lock);
|
|
FT_Done_Face(font->face);
|
|
mtx_unlock(&ft_lock);
|
|
}
|
|
|
|
mtx_destroy(&font->lock);
|
|
|
|
if (font->fc_pattern != NULL)
|
|
FcPatternDestroy(font->fc_pattern);
|
|
if (font->fc_fonts != NULL)
|
|
FcFontSetDestroy(font->fc_fonts);
|
|
|
|
if (font->cache == NULL)
|
|
return;
|
|
|
|
for (size_t i = 0; i < cache_size; i++) {
|
|
if (font->cache[i] == NULL)
|
|
continue;
|
|
|
|
tll_foreach(*font->cache[i], it) {
|
|
if (!it->item.valid)
|
|
continue;
|
|
|
|
cairo_surface_flush(it->item.surf);
|
|
void *image = cairo_image_surface_get_data(it->item.surf);
|
|
|
|
cairo_surface_destroy(it->item.surf);
|
|
free(image);
|
|
}
|
|
|
|
tll_free(*font->cache[i]);
|
|
free(font->cache[i]);
|
|
}
|
|
free(font->cache);
|
|
}
|