diff --git a/kitty/fontconfig.c b/kitty/fontconfig.c index 8bf9313b2..b80b5b403 100644 --- a/kitty/fontconfig.c +++ b/kitty/fontconfig.c @@ -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; } diff --git a/kitty/fonts/core_text.py b/kitty/fonts/core_text.py index eb090443e..98f441489 100644 --- a/kitty/fonts/core_text.py +++ b/kitty/fonts/core_text.py @@ -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( diff --git a/kitty/fonts/fontconfig.py b/kitty/fonts/fontconfig.py index 9f785a628..1e5118e31 100644 --- a/kitty/fonts/fontconfig.py +++ b/kitty/fonts/fontconfig.py @@ -2,128 +2,93 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal -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) diff --git a/kitty/fonts/render.py b/kitty/fonts/render.py index 6f06e2429..31f98b411 100644 --- a/kitty/fonts/render.py +++ b/kitty/fonts/render.py @@ -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)