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:
parent
b58e900806
commit
fefe6c9024
@ -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;
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user