diff --git a/kitty/fonts/freetype.py b/kitty/fonts/freetype.py index e61ba6f3b..ef9977032 100644 --- a/kitty/fonts/freetype.py +++ b/kitty/fonts/freetype.py @@ -152,13 +152,12 @@ def render_using_face(font, face, text, width, italic, bold): ) -def render_char(text, bold=False, italic=False, width=1): +def face_for_char(ch, bold=False, italic=False): key = 'regular' if bold: key = 'bi' if italic else 'bold' elif italic: key = 'italic' - ch = text[0] font = symbol_map.get(ch) if font is None or not font.face.get_char_index(ch): font = current_font_family.get(key) or current_font_family['regular'] @@ -177,9 +176,20 @@ def render_char(text, bold=False, italic=False, width=1): set_char_size(face, **cff_size) else: face = font.face + return font, face + + +def render_char(text, bold=False, italic=False, width=1): + font, face = face_for_char(text[0], bold, italic) return render_using_face(font, face, text, width, italic, bold) +def render_complex_char(text, bold=False, italic=False, width=1): + font, face = face_for_char(text[0], bold, italic) + import pprint + pprint.pprint(face.shape(text, font.hinting, font.hintstyle)) + + def place_char_in_cell(bitmap_char): # We want the glyph to be positioned inside the cell based on the bearingX # and bearingY values, making sure that it does not overflow the cell. @@ -249,12 +259,13 @@ def missing_glyph(width): def render_cell(text=' ', bold=False, italic=False): - # TODO: Handle non-normalizable combining chars. Probably need to use - # harfbuzz for that - text = text[0] - width = wcwidth(text) + width = wcwidth(text[0]) + try: - bitmap_char = render_char(text, bold, italic, width) + if len(text) > 1: + bitmap_char = render_complex_char(text, bold, italic, width) + else: + bitmap_char = render_char(text, bold, italic, width) except FontNotFound as err: safe_print('ERROR:', err, file=sys.stderr) return missing_glyph(width) diff --git a/kitty/freetype.c b/kitty/freetype.c index 471f5a1d4..353c0ecbe 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -8,6 +8,11 @@ #include "data-types.h" #include #include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +#include +#pragma GCC diagnostic pop +#include #include FT_FREETYPE_H typedef struct { PyObject_HEAD @@ -17,6 +22,8 @@ typedef struct { int ascender, descender, height, max_advance_width, max_advance_height, underline_position, underline_thickness; bool is_scalable; PyObject *path; + hb_buffer_t *harfbuzz_buffer; + hb_font_t *harfbuzz_font; } Face; static PyObject* FreeType_Exception = NULL; @@ -73,13 +80,19 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { CPY(units_per_EM); CPY(ascender); CPY(descender); CPY(height); CPY(max_advance_width); CPY(max_advance_height); CPY(underline_position); CPY(underline_thickness); #undef CPY self->is_scalable = FT_IS_SCALABLE(self->face); + self->harfbuzz_buffer = hb_buffer_create(); + if (self->harfbuzz_buffer == NULL || !hb_buffer_allocation_successful(self->harfbuzz_buffer) || !hb_buffer_pre_allocate(self->harfbuzz_buffer, 20)) { Py_CLEAR(self); return PyErr_NoMemory(); } + self->harfbuzz_font = hb_ft_font_create(self->face, NULL); + if (self->harfbuzz_font == NULL) { Py_CLEAR(self); return PyErr_NoMemory(); } } return (PyObject*)self; } static void dealloc(Face* self) { - FT_Done_Face(self->face); + if (self->harfbuzz_buffer) hb_buffer_destroy(self->harfbuzz_buffer); + if (self->harfbuzz_font) hb_font_destroy(self->harfbuzz_font); + if (self->face) FT_Done_Face(self->face); Py_CLEAR(self->path); Py_TYPE(self)->tp_free((PyObject*)self); } @@ -103,21 +116,29 @@ set_char_size(Face *self, PyObject *args) { if (!PyArg_ParseTuple(args, "llII", &char_width, &char_height, &xdpi, &ydpi)) return NULL; error = FT_Set_Char_Size(self->face, char_width, char_height, xdpi, ydpi); if (error) { set_freetype_error("Failed to set char size, with error:", error); Py_CLEAR(self); return NULL; } + if (self->harfbuzz_font) hb_font_destroy(self->harfbuzz_font); + self->harfbuzz_font = hb_ft_font_create(self->face, NULL); + if (self->harfbuzz_font == NULL) { Py_CLEAR(self); return PyErr_NoMemory(); } Py_RETURN_NONE; } +static inline int +get_load_flags(int hinting, int hintstyle, int base) { + int flags = base; + if (hinting) { + if (hintstyle >= 3) flags |= FT_LOAD_TARGET_NORMAL; + else if (0 < hintstyle && hintstyle < 3) flags |= FT_LOAD_TARGET_LIGHT; + } else flags |= FT_LOAD_NO_HINTING; + return flags; +} + static PyObject* load_char(Face *self, PyObject *args) { #define load_char_doc "load_char(char, hinting, hintstyle)" int char_code, hinting, hintstyle, error; if (!PyArg_ParseTuple(args, "Cpp", &char_code, &hinting, &hintstyle)) return NULL; - + int flags = get_load_flags(hinting, hintstyle, FT_LOAD_RENDER); int glyph_index = FT_Get_Char_Index(self->face, char_code); - int flags = FT_LOAD_RENDER; - if (hinting) { - if (hintstyle >= 3) flags |= FT_LOAD_TARGET_NORMAL; - else if (0 < hintstyle && hintstyle < 3) flags |= FT_LOAD_TARGET_LIGHT; - } else flags |= FT_LOAD_NO_HINTING; error = FT_Load_Glyph(self->face, glyph_index, flags); if (error) { set_freetype_error("Failed to load glyph, with error:", error); Py_CLEAR(self); return NULL; } Py_RETURN_NONE; @@ -194,6 +215,76 @@ bitmap(Face *self) { return ans; } +static PyStructSequence_Field shape_fields[] = { + {"glyph_id", NULL}, + {"cluster", NULL}, + {"mask", NULL}, + {"x_offset", NULL}, + {"y_offset", NULL}, + {"x_advance", NULL}, + {"y_advance", NULL}, + {NULL} +}; +static PyStructSequence_Desc shape_fields_desc = {"Shape", NULL, shape_fields, 7}; +static PyTypeObject ShapeFieldsType = {{{0}}}; + +static inline PyObject* +shape_to_py(unsigned int i, hb_glyph_info_t *info, hb_glyph_position_t *pos) { + PyObject *ans = PyStructSequence_New(&ShapeFieldsType); + if (ans == NULL) return NULL; +#define SI(num, src, attr, conv, func, div) PyStructSequence_SET_ITEM(ans, num, func(((conv)src[i].attr) / div)); if (PyStructSequence_GET_ITEM(ans, num) == NULL) { Py_CLEAR(ans); return PyErr_NoMemory(); } +#define INFO(num, attr) SI(num, info, attr, unsigned long, PyLong_FromUnsignedLong, 1) +#define POS(num, attr) SI(num + 3, pos, attr, double, PyFloat_FromDouble, 64.0) + INFO(0, codepoint); INFO(1, cluster); INFO(2, mask); + POS(0, x_offset); POS(1, y_offset); POS(2, x_advance); POS(3, y_advance); +#undef INFO +#undef POS +#undef SI + return ans; +} + +typedef struct { + unsigned int length; + hb_glyph_info_t *info; + hb_glyph_position_t *positions; +} ShapeData; + + +static inline void +_shape(Face *self, const char *string, int len, int hinting, int hintstyle, ShapeData *ans) { + hb_buffer_clear_contents(self->harfbuzz_buffer); + hb_ft_font_set_load_flags(self->harfbuzz_font, get_load_flags(hinting, hintstyle, FT_LOAD_DEFAULT)); + hb_buffer_add_utf8(self->harfbuzz_buffer, string, len, 0, len); + hb_buffer_guess_segment_properties(self->harfbuzz_buffer); + hb_shape(self->harfbuzz_font, self->harfbuzz_buffer, NULL, 0); + + unsigned int info_length, positions_length; + ans->info = hb_buffer_get_glyph_infos(self->harfbuzz_buffer, &info_length); + ans->positions = hb_buffer_get_glyph_positions(self->harfbuzz_buffer, &positions_length); + ans->length = MIN(info_length, positions_length); +} + + +static PyObject* +shape(Face *self, PyObject *args) { +#define shape_doc "shape(text, hinting, hintstyle)" + const char *string; + int hinting, hintstyle, len; + if (!PyArg_ParseTuple(args, "s#ii", &string, &len, &hinting, &hintstyle)) return NULL; + + ShapeData sd; + _shape(self, string, len, hinting, hintstyle, &sd); + PyObject *ans = PyTuple_New(sd.length); + if (ans == NULL) return NULL; + for (unsigned int i = 0; i < sd.length; i++) { + PyObject *s = shape_to_py(i, sd.info, sd.positions); + if (s == NULL) { Py_CLEAR(ans); return NULL; } + PyTuple_SET_ITEM(ans, i, s); + } + return ans; +} + + static PyObject* trim_to_width(Face UNUSED *self, PyObject *args) { #define trim_to_width_doc "Trim edges from the supplied bitmap to make it fit in the specified cell-width" @@ -254,6 +345,7 @@ static PyMemberDef members[] = { static PyMethodDef methods[] = { METHOD(set_char_size, METH_VARARGS) METHOD(load_char, METH_VARARGS) + METHOD(shape, METH_VARARGS) METHOD(get_char_index, METH_VARARGS) METHOD(glyph_metrics, METH_NOARGS) METHOD(bitmap, METH_NOARGS) @@ -298,8 +390,10 @@ init_freetype_library(PyObject *m) { } if (PyStructSequence_InitType2(&GlpyhMetricsType, &gm_desc) != 0) return false; if (PyStructSequence_InitType2(&BitmapType, &bm_desc) != 0) return false; + if (PyStructSequence_InitType2(&ShapeFieldsType, &shape_fields_desc) != 0) return false; PyModule_AddObject(m, "GlyphMetrics", (PyObject*)&GlpyhMetricsType); PyModule_AddObject(m, "Bitmap", (PyObject*)&BitmapType); + PyModule_AddObject(m, "ShapeFields", (PyObject*)&ShapeFieldsType); PyModule_AddIntMacro(m, FT_LOAD_RENDER); PyModule_AddIntMacro(m, FT_LOAD_TARGET_NORMAL); PyModule_AddIntMacro(m, FT_LOAD_TARGET_LIGHT);