Use fc-list instead of fc-match to select fonts

This is because there is no way to force fc-match to return a monospace
font. So if the user specifies a non-existent font we end up with some
non-monospaced font, which looks terrible.

Also, this makes the font selection logic or macOS and Linux similar.
This commit is contained in:
Kovid Goyal
2017-11-10 10:15:37 +05:30
parent b58e900806
commit fefe6c9024
4 changed files with 162 additions and 178 deletions

View File

@@ -28,20 +28,20 @@ pattern_as_dict(FcPattern *pat) {
#define PS(x) PyUnicode_FromString((char*)x) #define PS(x) PyUnicode_FromString((char*)x)
#define G(type, get, which, conv, name) { \ #define G(type, get, which, conv, name) { \
type out; PyObject *p; \ type out; PyObject *p; \
if (get(pat, which, 0, &out) == FcResultMatch) { p = conv(out); if (p == NULL) { Py_CLEAR(ans); return NULL; } \ if (get(pat, which, 0, &out) == FcResultMatch) { \
} else { p = Py_None; Py_INCREF(p); } \ p = conv(out); if (p == NULL) { Py_CLEAR(ans); return NULL; } \
if (PyDict_SetItemString(ans, #name, p) != 0) { Py_CLEAR(p); Py_CLEAR(ans); return NULL; } \ if (PyDict_SetItemString(ans, #name, p) != 0) { Py_CLEAR(p); Py_CLEAR(ans); return NULL; } \
Py_CLEAR(p); \ Py_CLEAR(p); \
} }}
#define S(which, key) G(FcChar8*, FcPatternGetString, which, PS, key) #define S(which, key) G(FcChar8*, FcPatternGetString, which, PS, key)
#define I(which, key) G(int, FcPatternGetInteger, which, PyLong_FromLong, key) #define I(which, key) G(int, FcPatternGetInteger, which, PyLong_FromLong, key)
#define B(which, key) G(int, FcPatternGetInteger, which, pybool, key) #define B(which, key) G(int, FcPatternGetBool, which, pybool, key)
#define E(which, key, conv) G(int, FcPatternGetInteger, which, conv, key) #define E(which, key, conv) G(int, FcPatternGetInteger, which, conv, key)
S(FC_FILE, path); S(FC_FILE, path);
S(FC_FAMILY, family); S(FC_FAMILY, family);
S(FC_STYLE, style); S(FC_STYLE, style);
S(FC_FULLNAME, fullname); S(FC_FULLNAME, full_name);
S(FC_POSTSCRIPT_NAME, psname); S(FC_POSTSCRIPT_NAME, postscript_name);
I(FC_WEIGHT, weight); I(FC_WEIGHT, weight);
I(FC_SLANT, slant); I(FC_SLANT, slant);
I(FC_HINT_STYLE, hint_style); I(FC_HINT_STYLE, hint_style);
@@ -73,8 +73,10 @@ font_set(FcFontSet *fs) {
return ans; return ans;
} }
#define AP(func, which, in, desc) if (!func(pat, which, in)) { PyErr_Format(PyExc_ValueError, "Failed to add %s to fontconfig pattern", desc, NULL); goto end; }
static PyObject* static PyObject*
list_fontconfig_fonts(PyObject UNUSED *self, PyObject *args) { fc_list(PyObject UNUSED *self, PyObject *args) {
int allow_bitmapped_fonts = 0, only_monospaced_fonts = 1; int allow_bitmapped_fonts = 0, only_monospaced_fonts = 1;
PyObject *ans = NULL; PyObject *ans = NULL;
FcObjectSet *os = NULL; FcObjectSet *os = NULL;
@@ -83,13 +85,11 @@ list_fontconfig_fonts(PyObject UNUSED *self, PyObject *args) {
if (!PyArg_ParseTuple(args, "|pp", &only_monospaced_fonts, &allow_bitmapped_fonts)) return NULL; if (!PyArg_ParseTuple(args, "|pp", &only_monospaced_fonts, &allow_bitmapped_fonts)) return NULL;
pat = FcPatternCreate(); pat = FcPatternCreate();
if (pat == NULL) return PyErr_NoMemory(); if (pat == NULL) return PyErr_NoMemory();
#define AP(func, which, in, desc) if (!func(pat, which, in)) { PyErr_Format(PyExc_ValueError, "Failed to add %s to fontconfig pattern", desc, NULL); goto end; }
if (!allow_bitmapped_fonts) { if (!allow_bitmapped_fonts) {
AP(FcPatternAddBool, FC_OUTLINE, true, "outline"); AP(FcPatternAddBool, FC_OUTLINE, true, "outline");
AP(FcPatternAddBool, FC_SCALABLE, true, "scalable"); AP(FcPatternAddBool, FC_SCALABLE, true, "scalable");
} }
if (only_monospaced_fonts) AP(FcPatternAddInteger, FC_SPACING, FC_MONO, "spacing"); if (only_monospaced_fonts) AP(FcPatternAddInteger, FC_SPACING, FC_MONO, "spacing");
#undef AP
os = FcObjectSetBuild(FC_FILE, FC_POSTSCRIPT_NAME, FC_FAMILY, FC_STYLE, FC_FULLNAME, FC_WEIGHT, FC_WIDTH, FC_SLANT, FC_HINT_STYLE, FC_INDEX, FC_HINTING, FC_SCALABLE, FC_OUTLINE, FC_COLOR, FC_SPACING, NULL); os = FcObjectSetBuild(FC_FILE, FC_POSTSCRIPT_NAME, FC_FAMILY, FC_STYLE, FC_FULLNAME, FC_WEIGHT, FC_WIDTH, FC_SLANT, FC_HINT_STYLE, FC_INDEX, FC_HINTING, FC_SCALABLE, FC_OUTLINE, FC_COLOR, FC_SPACING, NULL);
if (!os) { PyErr_SetString(PyExc_ValueError, "Failed to create fontconfig object set"); goto end; } if (!os) { PyErr_SetString(PyExc_ValueError, "Failed to create fontconfig object set"); goto end; }
fs = FcFontList(NULL, pat, os); fs = FcFontList(NULL, pat, os);
@@ -102,34 +102,25 @@ end:
return ans; return ans;
} }
static PyObject* static inline PyObject*
get_fontconfig_font(PyObject UNUSED *self, PyObject *args) { _fc_match(FcPattern *pat) {
char *family; FcPattern *match = NULL;
int bold, italic, allow_bitmapped_fonts, index = 0, hint_style=0, weight=0, slant=0;
double size_in_pts, dpi;
PyObject *characters;
FcBool hinting, scalable, outline;
FcChar8 *path = NULL;
FcPattern *pat = NULL, *match = NULL;
FcResult result;
FcCharSet *charset = NULL;
PyObject *ans = NULL; PyObject *ans = NULL;
FcResult result;
FcConfigSubstitute(NULL, pat, FcMatchPattern);
FcDefaultSubstitute(pat);
match = FcFontMatch(NULL, pat, &result);
if (match == NULL) { PyErr_SetString(PyExc_KeyError, "FcFontMatch() failed"); goto end; }
ans = pattern_as_dict(match);
end:
if (match) FcPatternDestroy(match);
return ans;
}
if (!PyArg_ParseTuple(args, "spppdO!d", &family, &bold, &italic, &allow_bitmapped_fonts, &size_in_pts, &PyUnicode_Type, &characters, &dpi)) return NULL; static inline void
if (PyUnicode_READY(characters) != 0) return NULL; add_charset(PyObject *characters, FcPattern *pat) {
pat = FcPatternCreate(); FcCharSet *charset = NULL;
if (pat == NULL) return PyErr_NoMemory(); if (PyUnicode_READY(characters) != 0) goto end;
#define AP(func, which, in, desc) if (!func(pat, which, in)) { PyErr_Format(PyExc_RuntimeError, "Failed to add %s to fontconfig patter", desc, NULL); goto end; }
AP(FcPatternAddString, FC_FAMILY, (const FcChar8*)family, "family");
if (!allow_bitmapped_fonts) {
AP(FcPatternAddBool, FC_OUTLINE, true, "outline");
AP(FcPatternAddBool, FC_SCALABLE, true, "scalable");
}
if (size_in_pts > 0) { AP(FcPatternAddDouble, FC_SIZE, size_in_pts, "size"); }
if (dpi > 0) { AP(FcPatternAddDouble, FC_DPI, dpi, "dpi"); }
if (bold) { AP(FcPatternAddInteger, FC_WEIGHT, FC_WEIGHT_BOLD, "weight"); }
if (italic) { AP(FcPatternAddInteger, FC_SLANT, FC_SLANT_ITALIC, "slant"); }
if (PyUnicode_GET_LENGTH(characters) > 0) { if (PyUnicode_GET_LENGTH(characters) > 0) {
charset = FcCharSetCreate(); charset = FcCharSetCreate();
if (charset == NULL) { PyErr_NoMemory(); goto end; } if (charset == NULL) { PyErr_NoMemory(); goto end; }
@@ -142,42 +133,66 @@ get_fontconfig_font(PyObject UNUSED *self, PyObject *args) {
} }
AP(FcPatternAddCharSet, FC_CHARSET, charset, "charset"); AP(FcPatternAddCharSet, FC_CHARSET, charset, "charset");
} }
#undef AP end:
FcConfigSubstitute(NULL, pat, FcMatchPattern); if (charset != NULL) FcCharSetDestroy(charset);
FcDefaultSubstitute(pat); }
match = FcFontMatch(NULL, pat, &result);
if (match == NULL) { PyErr_SetString(PyExc_KeyError, "FcFontMatch() failed"); goto end; }
#define GI(func, which, out, desc) \ static PyObject*
if (func(match, which, 0, & out) != FcResultMatch) { \ fc_match(PyObject UNUSED *self, PyObject *args) {
PyErr_Format(PyExc_RuntimeError, "Failed to get %s from match object", desc, NULL); goto end; \ char *family = NULL;
int bold = 0, italic = 0, allow_bitmapped_fonts = 0;
double size_in_pts = 0, dpi = 0;
PyObject *characters = NULL;
FcPattern *pat = NULL;
PyObject *ans = NULL;
if (!PyArg_ParseTuple(args, "|zpppdO!d", &family, &bold, &italic, &allow_bitmapped_fonts, &size_in_pts, &PyUnicode_Type, &characters, &dpi)) return NULL;
pat = FcPatternCreate();
if (pat == NULL) return PyErr_NoMemory();
if (family && strlen(family) > 0) AP(FcPatternAddString, FC_FAMILY, (const FcChar8*)family, "family");
if (!allow_bitmapped_fonts) {
AP(FcPatternAddBool, FC_OUTLINE, true, "outline");
AP(FcPatternAddBool, FC_SCALABLE, true, "scalable");
} }
if (size_in_pts > 0) { AP(FcPatternAddDouble, FC_SIZE, size_in_pts, "size"); }
GI(FcPatternGetString, FC_FILE, path, "file path"); if (dpi > 0) { AP(FcPatternAddDouble, FC_DPI, dpi, "dpi"); }
GI(FcPatternGetInteger, FC_INDEX, index, "face index"); if (bold) { AP(FcPatternAddInteger, FC_WEIGHT, FC_WEIGHT_BOLD, "weight"); }
GI(FcPatternGetInteger, FC_WEIGHT, weight, "weight"); if (italic) { AP(FcPatternAddInteger, FC_SLANT, FC_SLANT_ITALIC, "slant"); }
GI(FcPatternGetInteger, FC_SLANT, slant, "slant"); if (characters) add_charset(characters, pat);
GI(FcPatternGetInteger, FC_HINT_STYLE, hint_style, "hint style"); ans = _fc_match(pat);
GI(FcPatternGetBool, FC_HINTING, hinting, "hinting");
GI(FcPatternGetBool, FC_SCALABLE, scalable, "scalable");
GI(FcPatternGetBool, FC_OUTLINE, outline, "outline");
#undef GI
#define BP(x) (x ? Py_True : Py_False)
ans = Py_BuildValue("siiOOOii", path, index, hint_style, BP(hinting), BP(scalable), BP(outline), weight, slant);
#undef BP
end: end:
if (pat != NULL) FcPatternDestroy(pat); if (pat != NULL) FcPatternDestroy(pat);
if (match != NULL) FcPatternDestroy(match);
if (charset != NULL) FcCharSetDestroy(charset);
if (PyErr_Occurred()) return NULL;
return ans; return ans;
} }
static PyObject*
fc_font(PyObject UNUSED *self, PyObject *args) {
double size_in_pts, dpi;
int index;
char *path;
PyObject *ans = NULL, *chars = NULL;
if (!PyArg_ParseTuple(args, "ddsi|O!", &size_in_pts, &dpi, &path, &index, &PyUnicode_Type, &chars)) return NULL;
FcPattern *pat = FcPatternCreate();
if (pat == NULL) return PyErr_NoMemory();
if (size_in_pts > 0) { AP(FcPatternAddDouble, FC_SIZE, size_in_pts, "size"); }
if (dpi > 0) { AP(FcPatternAddDouble, FC_DPI, dpi, "dpi"); }
AP(FcPatternAddString, FC_FILE, (const FcChar8*)path, "path");
AP(FcPatternAddInteger, FC_INDEX, index, "index");
if (chars) add_charset(chars, pat);
ans = _fc_match(pat);
end:
if (pat != NULL) FcPatternDestroy(pat);
return ans;
}
#undef AP
static PyMethodDef module_methods[] = { static PyMethodDef module_methods[] = {
METHODB(list_fontconfig_fonts, METH_VARARGS), METHODB(fc_list, METH_VARARGS),
METHODB(get_fontconfig_font, METH_VARARGS), METHODB(fc_match, METH_VARARGS),
METHODB(fc_font, METH_VARARGS),
{NULL, NULL, 0, NULL} /* Sentinel */ {NULL, NULL, 0, NULL} /* Sentinel */
}; };
@@ -192,5 +207,11 @@ init_fontconfig_library(PyObject *module) {
return false; return false;
} }
if (PyModule_AddFunctions(module, module_methods) != 0) return false; if (PyModule_AddFunctions(module, module_methods) != 0) return false;
PyModule_AddIntMacro(module, FC_WEIGHT_REGULAR);
PyModule_AddIntMacro(module, FC_WEIGHT_MEDIUM);
PyModule_AddIntMacro(module, FC_WEIGHT_SEMIBOLD);
PyModule_AddIntMacro(module, FC_WEIGHT_BOLD);
PyModule_AddIntMacro(module, FC_SLANT_ITALIC);
PyModule_AddIntMacro(module, FC_SLANT_ROMAN);
return true; return true;
} }

View File

@@ -121,7 +121,8 @@ def font_for_text(text, current_font_family, pt_sz, xdpi, ydpi, bold=False, ital
def font_for_family(family): def font_for_family(family):
return face_description(family, save_medium_face.family) ans = face_description(family, save_medium_face.family)
return ans, ans.bold, ans.italic
def test_font_matching( def test_font_matching(

View File

@@ -2,128 +2,93 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import os import re
from collections import namedtuple from functools import lru_cache
from kitty.fast_data_types import Face, get_fontconfig_font from kitty.fast_data_types import (
FC_SLANT_ITALIC, FC_SLANT_ROMAN, FC_WEIGHT_BOLD, FC_WEIGHT_REGULAR, Face,
fc_list, fc_match, fc_font
def face_from_font(font, pt_sz, xdpi, ydpi):
return Face(font.path, font.index, font.hinting, font.hintstyle, pt_sz, xdpi, ydpi)
Font = namedtuple(
'Font',
'path hinting hintstyle bold italic scalable outline weight slant index'
) )
attr_map = {(False, False): 'font_family',
class FontNotFound(ValueError): (True, False): 'bold_font',
pass (False, True): 'italic_font',
(True, True): 'bold_italic_font'}
def to_bool(x): def create_font_map(all_fonts):
return x.lower() == 'true' ans = {'family_map': {}, 'ps_map': {}, 'full_map': {}}
for x in all_fonts:
f = (x['family'] or '').lower()
def font_not_found(err, chars): full = (x['full_name'] or '').lower()
msg = 'Failed to find font' ps = (x['postscript_name'] or '').lower()
if chars: ans['family_map'].setdefault(f, []).append(x)
chars = ', '.join('U+{:X}'.format(ord(c)) for c in chars) ans['ps_map'].setdefault(ps, []).append(x)
msg = 'Failed to find font for characters U+{:X}, error from fontconfig: {}'. format(chars, err) ans['full_map'].setdefault(full, []).append(x)
return FontNotFound(msg)
def get_font(
family='monospace',
bold=False,
italic=False,
allow_bitmaped_fonts=False,
size_in_pts=None,
characters='',
dpi=None
):
try:
path, index, hintstyle, hinting, scalable, outline, weight, slant = get_fontconfig_font(
family, bold, italic, allow_bitmaped_fonts, size_in_pts or 0,
characters or '', dpi or 0
)
except KeyError as err:
raise font_not_found(err, characters)
return Font(
path, hinting, hintstyle, bold, italic, scalable, outline, weight,
slant, index
)
def find_font_for_characters(
family,
chars,
bold=False,
italic=False,
allow_bitmaped_fonts=False,
size_in_pts=None,
dpi=None
):
ans = get_font(
family,
bold,
italic,
characters=chars,
allow_bitmaped_fonts=allow_bitmaped_fonts,
size_in_pts=size_in_pts,
dpi=dpi
)
if not ans.path or not os.path.exists(ans.path):
raise FontNotFound(
'Failed to find font for characters: {!r}'.format(chars)
)
return ans return ans
def font_for_text(text, current_font_family, pt_sz, xdpi, ydpi, bold=False, italic=False): @lru_cache()
dpi = (xdpi + ydpi) / 2 def all_fonts_map(monospaced=True):
try: return create_font_map(fc_list(monospaced))
return find_font_for_characters(current_font_family, text, bold=bold, italic=italic, size_in_pts=pt_sz, dpi=dpi)
except FontNotFound:
return find_font_for_characters(current_font_family, text, bold=bold, italic=italic, size_in_pts=pt_sz, dpi=dpi, allow_bitmaped_fonts=True)
def get_font_information(family, bold=False, italic=False): def find_best_match(family, bold=False, italic=False, monospaced=True):
return get_font(family, bold, italic) q = re.sub(r'\s+', ' ', family.lower())
font_map = all_fonts_map(monospaced)
def score(candidate):
bold_score = abs((FC_WEIGHT_BOLD if bold else FC_WEIGHT_REGULAR) - candidate['weight'])
italic_score = abs((FC_SLANT_ITALIC if italic else FC_SLANT_ROMAN) - candidate['slant'])
monospace_match = 0 if candidate['spacing'] == 'MONO' else 1
return bold_score + italic_score, monospace_match
# First look for an exact match
for selector in ('ps_map', 'full_map', 'family_map'):
candidates = font_map[selector].get(q)
if candidates:
candidates.sort(key=score)
return candidates[0]
# Use fc-match with a generic family
family = 'monospace' if monospaced else 'sans-serif'
return fc_match(family, bold, italic)
def face_from_font(font, pt_sz=11.0, xdpi=96.0, ydpi=96.0):
font = fc_font(pt_sz, (xdpi + ydpi) / 2.0, font['path'], font['index'])
return Face(font['path'], font['index'], font['hinting'], font['hint_style'], pt_sz, xdpi, ydpi)
def resolve_family(f, main_family, bold, italic):
if (bold or italic) and f == 'auto':
f = main_family
return f
def save_medium_face(face):
pass
def get_font_files(opts): def get_font_files(opts):
ans = {} ans = {}
attr_map = { for (bold, italic), attr in attr_map.items():
'bold': 'bold_font', rf = resolve_family(getattr(opts, attr), opts.font_family, bold, italic)
'italic': 'italic_font', font = find_best_match(rf, bold, italic)
'bi': 'bold_italic_font' key = {(False, False): 'medium',
} (True, False): 'bold',
(False, True): 'italic',
def get_family(key=None): (True, True): 'bi'}[(bold, italic)]
ans = getattr(opts, attr_map.get(key, 'font_family')) ans[key] = font
if ans == 'auto' and key: if key == 'medium':
ans = get_family() save_medium_face.medium_font = font
return ans
n = get_font_information(get_family())
ans['medium'] = n
def do(key):
b = get_font_information(
get_family(key),
bold=key in ('bold', 'bi'),
italic=key in ('italic', 'bi')
)
if b.path != n.path:
ans[key] = b
do('bold'), do('italic'), do('bi')
return ans return ans
def font_for_family(family): def font_for_family(family):
ans = get_font_information(family) ans = find_best_match(family)
return ans return ans, ans['weight'] >= FC_WEIGHT_BOLD, ans['slant'] != FC_SLANT_ROMAN
def font_for_text(text, current_font_family='monospace', pt_sz=11.0, xdpi=96.0, ydpi=96.0, bold=False, italic=False):
return fc_match('monospace', bold, italic, False, pt_sz, str(text), (xdpi + ydpi) / 2.0)

View File

@@ -18,10 +18,7 @@ from kitty.utils import get_logical_dpi
if isosx: if isosx:
from .core_text import get_font_files, font_for_text, face_from_font, font_for_family, save_medium_face from .core_text import get_font_files, font_for_text, face_from_font, font_for_family, save_medium_face
else: else:
from .fontconfig import get_font_files, font_for_text, face_from_font, font_for_family from .fontconfig import get_font_files, font_for_text, face_from_font, font_for_family, save_medium_face
def save_medium_face(f):
pass
def create_face(font): def create_face(font):
@@ -35,10 +32,10 @@ def create_symbol_map(opts):
faces = [] faces = []
for family in val.values(): for family in val.values():
if family not in family_map: if family not in family_map:
font = font_for_family(family) font, bold, italic = font_for_family(family)
o = create_face(font) o = create_face(font)
family_map[family] = len(faces) family_map[family] = len(faces)
faces.append((o, font.bold, font.italic)) faces.append((o, bold, italic))
sm = tuple((a, b, family_map[f]) for (a, b), f in val.items()) sm = tuple((a, b, family_map[f]) for (a, b), f in val.items())
return sm, tuple(faces) return sm, tuple(faces)