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
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
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 G(type, get, which, conv, name) { \
type out; PyObject *p; \
if (get(pat, which, 0, &out) == FcResultMatch) { p = conv(out); if (p == NULL) { Py_CLEAR(ans); return NULL; } \
} else { p = Py_None; Py_INCREF(p); } \
if (PyDict_SetItemString(ans, #name, p) != 0) { Py_CLEAR(p); Py_CLEAR(ans); return NULL; } \
Py_CLEAR(p); \
}
if (get(pat, which, 0, &out) == FcResultMatch) { \
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; } \
Py_CLEAR(p); \
}}
#define S(which, key) G(FcChar8*, FcPatternGetString, which, PS, 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)
S(FC_FILE, path);
S(FC_FAMILY, family);
S(FC_STYLE, style);
S(FC_FULLNAME, fullname);
S(FC_POSTSCRIPT_NAME, psname);
S(FC_FULLNAME, full_name);
S(FC_POSTSCRIPT_NAME, postscript_name);
I(FC_WEIGHT, weight);
I(FC_SLANT, slant);
I(FC_HINT_STYLE, hint_style);
@ -73,8 +73,10 @@ font_set(FcFontSet *fs) {
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*
list_fontconfig_fonts(PyObject UNUSED *self, PyObject *args) {
fc_list(PyObject UNUSED *self, PyObject *args) {
int allow_bitmapped_fonts = 0, only_monospaced_fonts = 1;
PyObject *ans = 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;
pat = FcPatternCreate();
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) {
AP(FcPatternAddBool, FC_OUTLINE, true, "outline");
AP(FcPatternAddBool, FC_SCALABLE, true, "scalable");
}
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);
if (!os) { PyErr_SetString(PyExc_ValueError, "Failed to create fontconfig object set"); goto end; }
fs = FcFontList(NULL, pat, os);
@ -102,34 +102,25 @@ end:
return ans;
}
static PyObject*
get_fontconfig_font(PyObject UNUSED *self, PyObject *args) {
char *family;
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;
static inline PyObject*
_fc_match(FcPattern *pat) {
FcPattern *match = 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;
if (PyUnicode_READY(characters) != 0) return NULL;
pat = FcPatternCreate();
if (pat == NULL) return PyErr_NoMemory();
#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"); }
static inline void
add_charset(PyObject *characters, FcPattern *pat) {
FcCharSet *charset = NULL;
if (PyUnicode_READY(characters) != 0) goto end;
if (PyUnicode_GET_LENGTH(characters) > 0) {
charset = FcCharSetCreate();
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");
}
#undef AP
FcConfigSubstitute(NULL, pat, FcMatchPattern);
FcDefaultSubstitute(pat);
match = FcFontMatch(NULL, pat, &result);
if (match == NULL) { PyErr_SetString(PyExc_KeyError, "FcFontMatch() failed"); goto end; }
end:
if (charset != NULL) FcCharSetDestroy(charset);
}
#define GI(func, which, out, desc) \
if (func(match, which, 0, & out) != FcResultMatch) { \
PyErr_Format(PyExc_RuntimeError, "Failed to get %s from match object", desc, NULL); goto end; \
static PyObject*
fc_match(PyObject UNUSED *self, PyObject *args) {
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");
}
GI(FcPatternGetString, FC_FILE, path, "file path");
GI(FcPatternGetInteger, FC_INDEX, index, "face index");
GI(FcPatternGetInteger, FC_WEIGHT, weight, "weight");
GI(FcPatternGetInteger, FC_SLANT, slant, "slant");
GI(FcPatternGetInteger, FC_HINT_STYLE, hint_style, "hint style");
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
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 (characters) add_charset(characters, pat);
ans = _fc_match(pat);
end:
if (pat != NULL) FcPatternDestroy(pat);
if (match != NULL) FcPatternDestroy(match);
if (charset != NULL) FcCharSetDestroy(charset);
if (PyErr_Occurred()) return NULL;
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[] = {
METHODB(list_fontconfig_fonts, METH_VARARGS),
METHODB(get_fontconfig_font, METH_VARARGS),
METHODB(fc_list, METH_VARARGS),
METHODB(fc_match, METH_VARARGS),
METHODB(fc_font, METH_VARARGS),
{NULL, NULL, 0, NULL} /* Sentinel */
};
@ -192,5 +207,11 @@ init_fontconfig_library(PyObject *module) {
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;
}

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):
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(

View File

@ -2,128 +2,93 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import os
from collections import namedtuple
import re
from functools import lru_cache
from kitty.fast_data_types import Face, get_fontconfig_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'
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
)
class FontNotFound(ValueError):
pass
attr_map = {(False, False): 'font_family',
(True, False): 'bold_font',
(False, True): 'italic_font',
(True, True): 'bold_italic_font'}
def to_bool(x):
return x.lower() == 'true'
def font_not_found(err, chars):
msg = 'Failed to find font'
if chars:
chars = ', '.join('U+{:X}'.format(ord(c)) for c in chars)
msg = 'Failed to find font for characters U+{:X}, error from fontconfig: {}'. format(chars, err)
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)
)
def create_font_map(all_fonts):
ans = {'family_map': {}, 'ps_map': {}, 'full_map': {}}
for x in all_fonts:
f = (x['family'] or '').lower()
full = (x['full_name'] or '').lower()
ps = (x['postscript_name'] or '').lower()
ans['family_map'].setdefault(f, []).append(x)
ans['ps_map'].setdefault(ps, []).append(x)
ans['full_map'].setdefault(full, []).append(x)
return ans
def font_for_text(text, current_font_family, pt_sz, xdpi, ydpi, bold=False, italic=False):
dpi = (xdpi + ydpi) / 2
try:
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)
@lru_cache()
def all_fonts_map(monospaced=True):
return create_font_map(fc_list(monospaced))
def get_font_information(family, bold=False, italic=False):
return get_font(family, bold, italic)
def find_best_match(family, bold=False, italic=False, monospaced=True):
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):
ans = {}
attr_map = {
'bold': 'bold_font',
'italic': 'italic_font',
'bi': 'bold_italic_font'
}
def get_family(key=None):
ans = getattr(opts, attr_map.get(key, 'font_family'))
if ans == 'auto' and key:
ans = get_family()
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')
for (bold, italic), attr in attr_map.items():
rf = resolve_family(getattr(opts, attr), opts.font_family, bold, italic)
font = find_best_match(rf, bold, italic)
key = {(False, False): 'medium',
(True, False): 'bold',
(False, True): 'italic',
(True, True): 'bi'}[(bold, italic)]
ans[key] = font
if key == 'medium':
save_medium_face.medium_font = font
return ans
def font_for_family(family):
ans = get_font_information(family)
return ans
ans = find_best_match(family)
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:
from .core_text import get_font_files, font_for_text, face_from_font, font_for_family, save_medium_face
else:
from .fontconfig import get_font_files, font_for_text, face_from_font, font_for_family
def save_medium_face(f):
pass
from .fontconfig import get_font_files, font_for_text, face_from_font, font_for_family, save_medium_face
def create_face(font):
@ -35,10 +32,10 @@ def create_symbol_map(opts):
faces = []
for family in val.values():
if family not in family_map:
font = font_for_family(family)
font, bold, italic = font_for_family(family)
o = create_face(font)
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())
return sm, tuple(faces)