diff --git a/kitty/core_text.m b/kitty/core_text.m index b7d0dfd8e..7541c9930 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -118,18 +118,7 @@ coretext_all_fonts(PyObject UNUSED *_self) { } static inline bool -apply_size(Face *self, float point_sz, float dpi) { - self->point_sz = point_sz; - self->dpi = dpi; - self->scaled_point_sz = (dpi / 72.0) * point_sz; - if (self->font) { - CTFontRef f = CTFontCreateCopyWithAttributes(self->font, self->scaled_point_sz, NULL, NULL); - CFRelease(self->font); - self->font = f; - } else { - self->font = CTFontCreateWithFontDescriptor(self->descriptor, self->scaled_point_sz, NULL); - } - if (self->font == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to create or copy font object"); return false; } +init_font(Face *self) { self->units_per_em = CTFontGetUnitsPerEm(self->font); self->ascent = CTFontGetAscent(self->font); self->descent = CTFontGetDescent(self->font); @@ -151,6 +140,34 @@ apply_size(Face *self, float point_sz, float dpi) { return true; } +static inline bool +apply_size(Face *self, float point_sz, float dpi) { + self->point_sz = point_sz; + self->dpi = dpi; + self->scaled_point_sz = (dpi / 72.0) * point_sz; + if (self->font) { + CTFontRef f = CTFontCreateCopyWithAttributes(self->font, self->scaled_point_sz, NULL, NULL); + CFRelease(self->font); + self->font = f; + } else { + self->font = CTFontCreateWithFontDescriptor(self->descriptor, self->scaled_point_sz, NULL); + } + if (self->font == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to create or copy font object"); return false; } + return init_font(self); +} + +static inline bool +init_font_names(Face *self) { + self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font), 1); + self->full_name = convert_cfstring(CTFontCopyFullName(self->font), 1); + self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->font), 1); + NSURL *url = (NSURL*)CTFontCopyAttribute(self->font, kCTFontURLAttribute); + self->path = PyUnicode_FromString([[url path] UTF8String]); + [url release]; + if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL || self->path == NULL) {return false;} + return true; +} + static PyObject* new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { Face *self; @@ -163,13 +180,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { if (desc) { self->descriptor = desc; if (apply_size(self, point_sz, dpi)) { - self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font), 1); - self->full_name = convert_cfstring(CTFontCopyFullName(self->font), 1); - self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->font), 1); - NSURL *url = (NSURL*)CTFontCopyAttribute(self->font, kCTFontURLAttribute); - self->path = PyUnicode_FromString([[url path] UTF8String]); - [url release]; - if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL || self->path == NULL) { Py_CLEAR(self); } + if (!init_font_names(self)) Py_CLEAR(self); } else Py_CLEAR(self); } else { Py_CLEAR(self); @@ -209,6 +220,32 @@ face_has_codepoint(PyObject *s, char_type ch) { return CTFontGetGlyphsForCharacters(self->font, chars, glyphs, count); } +static PyObject* +face_for_text(Face *self, PyObject *args) { + char *s; + int bold, italic; + Face *ans = NULL; + if (!PyArg_ParseTuple(args, "espp", "UTF-8", &s, &bold, &italic)) return NULL; + CFStringRef str = CFStringCreateWithCString(NULL, s, kCFStringEncodingUTF8); + if (!str) return PyErr_NoMemory(); + unichar chars[10] = {0}; + CFRange range = CFRangeMake(0, CFStringGetLength(str)); + CFStringGetCharacters(str, range, chars); + CTFontRef font = CTFontCreateForString(self->font, str, range); + if (font == self->font) return (PyObject*)self; + if (font == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to find fallback font"); goto end; } + ans = (Face *)Face_Type.tp_alloc(&Face_Type, 0); + if (ans == NULL) { PyErr_NoMemory(); goto end; } + ans->font = font; + if (!init_font(ans) || !init_font_names(ans)) { Py_CLEAR(ans); goto end; } + ans->point_sz = self->point_sz; + ans->dpi = self->dpi; +end: + CFRelease(str); + if (ans) return (PyObject*)ans; + return NULL; +} + static inline void calc_cell_size(Face *self, unsigned int *cell_width, unsigned int *cell_height) { #define count (128 - 32) @@ -284,6 +321,7 @@ static PyMemberDef members[] = { }; static PyMethodDef methods[] = { + METHODB(face_for_text, METH_VARARGS), {NULL} /* Sentinel */ }; diff --git a/kitty/fonts/core_text.py b/kitty/fonts/core_text.py index b15ce36aa..dcd12b304 100644 --- a/kitty/fonts/core_text.py +++ b/kitty/fonts/core_text.py @@ -2,17 +2,17 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2017, Kovid Goyal -import ctypes import re import sys +from collections import namedtuple from kitty.fast_data_types import CTFace as Face, coretext_all_fonts -from kitty.utils import ceil_int, get_logical_dpi, safe_print, wcwidth, adjust_line_height +from kitty.utils import safe_print -main_font = {} -symbol_map = {} -cell_width = cell_height = baseline = CellTexture = WideCellTexture = underline_thickness = underline_position = None -attr_map = {(False, False): 'font_family', (True, False): 'bold_font', (False, True): 'italic_font', (True, True): 'bold_italic_font'} +attr_map = {(False, False): 'font_family', + (True, False): 'bold_font', + (False, True): 'italic_font', + (True, True): 'bold_italic_font'} def create_font_map(all_fonts): @@ -27,11 +27,21 @@ def create_font_map(all_fonts): return ans -def find_best_match(font_map, family, bold, italic): +def all_fonts_map(): + ans = getattr(all_fonts_map, 'ans', None) + if ans is None: + ans = all_fonts_map.ans = create_font_map(coretext_all_fonts()) + return ans + + +def find_best_match(family, bold, italic): q = re.sub(r'\s+', ' ', family.lower()) + font_map = all_fonts_map() def score(candidate): - style_match = 1 if candidate['bold'] == bold and candidate['italic'] == italic else 0 + style_match = 1 if candidate['bold'] == bold and candidate[ + 'italic' + ] == italic else 0 monospace_match = 1 if candidate['monospace'] else 0 return style_match, monospace_match @@ -45,7 +55,10 @@ def find_best_match(font_map, family, bold, italic): # Let CoreText choose the font if the family exists, otherwise # fallback to Menlo if q not in font_map['family_map']: - safe_print('The font {} was not found, falling back to Menlo'.format(family), file=sys.stderr) + safe_print( + 'The font {} was not found, falling back to Menlo'.format(family), + file=sys.stderr + ) family = 'Menlo' return { 'monospace': True, @@ -55,53 +68,65 @@ def find_best_match(font_map, family, bold, italic): } -def get_face(font_map, family, main_family, font_size, dpi, bold=False, italic=False): - def resolve_family(f): - if (bold or italic) and f == 'auto': - f = main_family - if f.lower() == 'monospace': - f = 'Menlo' - return f - descriptor = find_best_match(font_map, resolve_family(family), bold, italic) +def resolve_family(f, main_family, bold, italic): + if (bold or italic) and f == 'auto': + f = main_family + if f.lower() == 'monospace': + f = 'Menlo' + return f + + +FaceDescription = namedtuple( + 'FaceDescription', 'resolved_family family bold italic' +) + + +def face_description(family, main_family, bold=False, italic=False): + return FaceDescription( + resolve_family(family, main_family, bold, italic), family, bold, italic + ) + + +def get_face(family, font_size, dpi, bold=False, italic=False): + descriptor = find_best_match(family, bold, italic) return Face(descriptor, font_size, dpi) -def install_symbol_map(all_fonts, val, font_size, dpi): - global symbol_map - symbol_map = {} - family_map = {f: get_face(all_fonts, f, 'Menlo', font_size, dpi) for f in set(val.values())} - for ch, family in val.items(): - symbol_map[ch] = family_map[family] - - -def set_font_family(opts, override_font_size=None, ignore_dpi_failure=False): - global cell_width, cell_height, baseline, CellTexture, WideCellTexture, underline_thickness, underline_position - try: - dpi = get_logical_dpi() - except Exception: - if not ignore_dpi_failure: - raise - dpi = (72, 72) # Happens when running via develop() in an ssh session - dpi = sum(dpi) / 2.0 - font_size = override_font_size or opts.font_size - all_fonts = create_font_map(coretext_all_fonts()) - +def get_font_files(opts): + ans = {} for (bold, italic), attr in attr_map.items(): - main_font[(bold, italic)] = get_face(all_fonts, getattr(opts, attr), opts.font_family, font_size, dpi, bold, italic) - - install_symbol_map(all_fonts, opts.symbol_map, font_size, dpi) - mf = main_font[(False, False)] - cell_width, cell_height = mf.cell_size() - cell_height = adjust_line_height(cell_height, opts.adjust_line_height) - CellTexture = ctypes.c_ubyte * (cell_width * cell_height) - WideCellTexture = ctypes.c_ubyte * (2 * cell_width * cell_height) - baseline = int(round(mf.ascent)) - underline_position = int(round(baseline - mf.underline_position)) - underline_thickness = ceil_int(mf.underline_thickness) - return cell_width, cell_height + face = face_description( + getattr(opts, attr), opts.font_family, bold, italic + ) + key = {(False, False): 'medium', + (True, False): 'bold', + (False, True): 'italic', + (True, True): 'bi'}[(bold, italic)] + ans[key] = face + if key == 'medium': + save_medium_face.family = face.resolved_family + return ans -def test_font_matching(name='Menlo', bold=False, italic=False, dpi=72.0, font_size=11.0): +def face_from_font(font, pt_sz, xdpi, ydpi): + return get_face(font.resolved_family, pt_sz, (xdpi + ydpi) / 2, bold=font.bold, italic=font.italic) + + +def save_medium_face(face, family): + save_medium_face.face = face + + +def font_for_text(text, current_font_family, pt_sz, xdpi, ydpi, bold=False, italic=False): + save_medium_face.face.face_for_text(text, bold, italic) + + +def font_for_family(family): + return face_description(family, save_medium_face.family) + + +def test_font_matching( + name='Menlo', bold=False, italic=False, dpi=72.0, font_size=11.0 +): all_fonts = create_font_map(coretext_all_fonts()) face = get_face(all_fonts, name, 'Menlo', font_size, dpi, bold, italic) return face @@ -111,55 +136,7 @@ def test_family_matching(name='Menlo', dpi=72.0, font_size=11.0): all_fonts = create_font_map(coretext_all_fonts()) for bold in (False, True): for italic in (False, True): - face = get_face(all_fonts, name, 'Menlo', font_size, dpi, bold, italic) + face = get_face( + all_fonts, name, 'Menlo', font_size, dpi, bold, italic + ) print(bold, italic, face) - - -def current_cell(): - return CellTexture, cell_width, cell_height, baseline, underline_thickness, underline_position - - -def split(buf, cell_width, cell_height): - first, second = CellTexture(), CellTexture() - for y in range(cell_height): - offset, woffset = y * cell_width, y * cell_width * 2 - for x in range(cell_width): - first[offset + x] = buf[woffset + x] - second[offset + x] = buf[woffset + cell_width + x] - return first, second - - -def render_cell(text=' ', bold=False, italic=False): - ch = text[0] - width = wcwidth(ch) - face = symbol_map.get(ch) or main_font[(bold, italic)] - if width == 2: - buf, width = WideCellTexture(), cell_width * 2 - else: - buf, width = CellTexture(), cell_width - face.render_char(text, width, cell_height, ctypes.addressof(buf)) - if width == 2: - first, second = split(buf, cell_width, cell_height) - else: - first, second = buf, None - return first, second - - -def develop(family='monospace', sz=288): - import pickle - from .render import render_string - from kitty.fast_data_types import glfw_init - from kitty.config import defaults - import os - glfw_init() - try: - os.remove('/tmp/cell.data') - except EnvironmentError: - pass - opts = defaults._replace(font_family=family, font_size=sz) - set_font_family(opts, ignore_dpi_failure=True) - for (bold, italic), face in main_font.items(): - print('bold: {} italic: {} {}'.format(bold, italic, face)) - print('cell_width: {}, cell_height: {}, baseline: {}'.format(cell_width, cell_height, baseline)) - buf, w, h = render_string() - open('/tmp/cell.data', 'wb').write(pickle.dumps((bytearray(buf), w, h))) diff --git a/kitty/fonts/render.py b/kitty/fonts/render.py index 2787b4996..b59e0a381 100644 --- a/kitty/fonts/render.py +++ b/kitty/fonts/render.py @@ -11,10 +11,13 @@ from kitty.utils import get_logical_dpi from kitty.fast_data_types import set_font, set_font_size from .box_drawing import render_box_char, is_renderable_box_char if isosx: - pass + 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 + def create_face(font): s = set_font_family.state @@ -50,6 +53,7 @@ def set_font_family(opts, override_font_size=None): set_font_family.state = FontState('', sz, xdpi, ydpi, 0, 0, 0, 0, 0) font_map = get_font_files(opts) faces = [create_face(font_map['medium'])] + save_medium_face(faces[0]) for k in 'bold italic bi'.split(): if k in font_map: faces.append(create_face(font_map[k])) @@ -103,11 +107,15 @@ def add_curl(buf, cell_width, position, thickness, cell_height): def render_cell(text=' ', bold=False, italic=False, underline=0, strikethrough=False): - CharTexture, cell_width, cell_height, baseline, underline_thickness, underline_position = current_cell() + s = set_font_family.state + cell_width, cell_height, baseline = s.cell_width, s.cell_height, s.baseline + underline_thickness, underline_position = s.underline_thickness, s.underline_position + CharTexture = ctypes.c_ubyte * cell_width * cell_height if is_renderable_box_char(text): first, second = render_box_char(text, CharTexture(), cell_width, cell_height), None else: - first, second = rc(text, bold, italic) + first = CharTexture() + second = None def dl(f, *a): f(first, cell_width, *a) @@ -181,7 +189,8 @@ def render_string(text='\'Qing👁a⧽', underline=2, strikethrough=True): current_text = c if current_text: render_one(current_text) - cell_width, cell_height = current_cell()[1:3] + s = set_font_family.state + cell_width, cell_height = s.cell_width, s.cell_height char_data = join_cells(cell_width, cell_height, *cells) return char_data, cell_width * len(cells), cell_height