From 3592f82e4fb3b5410d89b49e55737f147920cc70 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 11 Nov 2017 08:08:03 +0530 Subject: [PATCH] Implement fallback fonts in CoreText --- kitty/core_text.m | 44 ++++++++++++++++++++++++++------------- kitty/fonts.c | 1 + kitty/fonts/core_text.py | 7 +++---- kitty/fonts/fontconfig.py | 5 ++++- kitty/fonts/render.py | 19 +++++++++++++---- 5 files changed, 53 insertions(+), 23 deletions(-) diff --git a/kitty/core_text.m b/kitty/core_text.m index ed5537483..568487116 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -105,16 +105,40 @@ coretext_all_fonts(PyObject UNUSED *_self) { return ans; } -static PyObject* -face_for_text(PyObject UNUSED *self, PyObject UNUSED *args) { - Py_RETURN_NONE; // TODO: Implement this -} - static void free_font(void *f) { CFRelease((CTFontRef)f); } +static inline PyObject* +ft_face(CTFontRef font, float pt_sz, float xdpi, float ydpi) { + const char *psname = convert_cfstring(CTFontCopyPostScriptName(font), 1); + NSURL *url = (NSURL*)CTFontCopyAttribute(font, kCTFontURLAttribute); + PyObject *path = PyUnicode_FromString([[url path] UTF8String]); + [url release]; + if (path == NULL) { CFRelease(font); return NULL; } + PyObject *ans = ft_face_from_path_and_psname(path, psname, (void*)font, free_font, true, 3, pt_sz, xdpi, ydpi, CTFontGetLeading(font)); + Py_DECREF(path); + if (ans == NULL) { CFRelease(font); } + return ans; +} + +static PyObject* +face_for_text(PyObject UNUSED *self, PyObject UNUSED *args) { + char *text; + PyObject *lp; + float pt_sz, xdpi, ydpi; + int bold, italic; + if (!PyArg_ParseTuple(args, "sO!fffpp", &text, &PyLong_Type, &lp, &pt_sz, &xdpi, &ydpi, &bold, &italic)) return NULL; + CTFontRef font = PyLong_AsVoidPtr(lp); + CFStringRef str = CFStringCreateWithCString(NULL, text, kCFStringEncodingUTF8); + if (str == NULL) return PyErr_NoMemory(); + CFRange range = CFRangeMake(0, CFStringGetLength(str)); + CTFontRef new_font = CTFontCreateForString(font, str, range); + if (new_font == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to find fallback CTFont"); CFRelease(str); return NULL; } + return ft_face(new_font, pt_sz, xdpi, ydpi); +} + static PyObject* create_face(PyObject UNUSED *self, PyObject *args) { PyObject *descriptor; @@ -127,15 +151,7 @@ create_face(PyObject UNUSED *self, PyObject *args) { CTFontRef font = CTFontCreateWithFontDescriptor(desc, scaled_point_sz, NULL); CFRelease(desc); desc = NULL; if (!font) { PyErr_SetString(PyExc_ValueError, "Failed to create CTFont object"); return NULL; } - const char *psname = convert_cfstring(CTFontCopyPostScriptName(font), 1); - NSURL *url = (NSURL*)CTFontCopyAttribute(font, kCTFontURLAttribute); - PyObject *path = PyUnicode_FromString([[url path] UTF8String]); - [url release]; - if (path == NULL) { CFRelease(font); return NULL; } - PyObject *ans = ft_face_from_path_and_psname(path, psname, (void*)font, free_font, true, 3, point_sz, xdpi, ydpi, CTFontGetLeading(font)); - Py_DECREF(path); - if (ans == NULL) { CFRelease(font); } - return ans; + return ft_face(font, point_sz, xdpi, ydpi); } // Boilerplate {{{ diff --git a/kitty/fonts.c b/kitty/fonts.c index 64556d252..1e3b74e59 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -224,6 +224,7 @@ update_cell_metrics(float pt_sz, float xdpi, float ydpi) { if (cell_height > 1000) { PyErr_SetString(PyExc_ValueError, "line height too large after adjustment"); return NULL; } underline_position = MIN(cell_height - 1, underline_position); sprite_tracker_set_layout(cell_width, cell_height); + global_state.cell_width = cell_width; global_state.cell_height = cell_height; free(canvas); canvas = malloc(CELLS_IN_CANVAS * cell_width * cell_height); if (canvas == NULL) return PyErr_NoMemory(); return Py_BuildValue("IIIII", cell_width, cell_height, baseline, underline_position, underline_thickness); diff --git a/kitty/fonts/core_text.py b/kitty/fonts/core_text.py index d4367c6bc..f5d00dacf 100644 --- a/kitty/fonts/core_text.py +++ b/kitty/fonts/core_text.py @@ -117,7 +117,7 @@ def get_font_files(opts): return ans -def face_from_font(font, pt_sz, xdpi, ydpi): +def face_from_font(font, pt_sz=11.0, xdpi=72.0, ydpi=72.0): return get_face(font.resolved_family, pt_sz, xdpi, ydpi, bold=font.bold, italic=font.italic) @@ -126,7 +126,7 @@ def save_medium_face(face): def font_for_text(text, current_font_family, pt_sz, xdpi, ydpi, bold=False, italic=False): - return face_for_text(text, pt_sz, xdpi, ydpi, bold, italic) + return face_for_text(text, save_medium_face.face.extra_data(), pt_sz, xdpi, ydpi, bold, italic) def font_for_family(family): @@ -143,10 +143,9 @@ def test_font_matching( 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, dpi, bold, italic + name, font_size, dpi, dpi, bold, italic ) print(bold, italic, face) diff --git a/kitty/fonts/fontconfig.py b/kitty/fonts/fontconfig.py index 7d00b85bc..fb7b3be0f 100644 --- a/kitty/fonts/fontconfig.py +++ b/kitty/fonts/fontconfig.py @@ -102,4 +102,7 @@ def font_for_family(family): 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) + ans = fc_match('monospace', bold, italic, False, pt_sz, str(text), (xdpi + ydpi) / 2.0) + if ans is not None: + ans = face_from_font(ans, pt_sz, xdpi, ydpi) + return ans diff --git a/kitty/fonts/render.py b/kitty/fonts/render.py index d513057cc..2cbcc79d6 100644 --- a/kitty/fonts/render.py +++ b/kitty/fonts/render.py @@ -10,7 +10,7 @@ from math import ceil, floor, pi, sin, sqrt from kitty.config import defaults from kitty.constants import isosx from kitty.fast_data_types import ( - Screen, send_prerendered_sprites, set_font, set_font_size, + Screen, change_wcwidth, send_prerendered_sprites, set_font, set_font_size, set_send_sprite_to_gpu, sprite_map_set_limits, test_render_line ) from kitty.fonts.box_drawing import render_box_char, render_missing_glyph @@ -49,11 +49,10 @@ FontState = namedtuple( def get_fallback_font(text, bold, italic): state = set_font_family.state - font = font_for_text( + return font_for_text( text, state.family, state.pt_sz, state.xdpi, state.ydpi, bold, italic ) - return None if font is None else create_face(font) def set_font_family(opts=None, override_font_size=None, override_dpi=None): @@ -230,8 +229,20 @@ def test_render_string(text='Hello, world!', family='monospace', size=144.0, dpi print('\n') +def test_fallback_font(qtext=None, bold=False, italic=False): + set_font_family(override_dpi=(96.0, 96.0)) + for text in (qtext, '你好', 'He\u0347\u0305', '\U0001F929'): + if text: + f = get_fallback_font(text, bold, italic) + try: + print(text, f) + except UnicodeEncodeError: + sys.stdout.buffer.write((text + ' %s\n' % f).encode('utf-8')) + + def showcase(): + change_wcwidth(True) test_render_string('He\u0347\u0305llo\u0341, w\u0302or\u0306l\u0354d!', family='Noto Sans Mono') test_render_string('你好,世界', family='Noto Sans Mono') - test_render_string('|\U0001F929|\U0001F921|\U0001F91f|', family='Noto Sans Mono') + test_render_string('|\U0001F601|\U0001F64f|\U0001F63a|', family='Noto Sans Mono') test_render_string('A=>>B!=C', family='Fira Code')