diff --git a/kitty/fonts/freetype.py b/kitty/fonts/freetype.py index 8b0750ea3..b6c846688 100644 --- a/kitty/fonts/freetype.py +++ b/kitty/fonts/freetype.py @@ -4,11 +4,10 @@ import ctypes import sys -from collections import namedtuple -from functools import lru_cache +from functools import lru_cache, partial from itertools import chain -from kitty.fast_data_types import FT_PIXEL_MODE_GRAY, Face, FreeTypeError +from kitty.fast_data_types import Face, FreeTypeError from kitty.fonts.box_drawing import render_missing_glyph from kitty.utils import ceil_int, get_logical_dpi, safe_print, wcwidth, adjust_line_height @@ -26,20 +25,6 @@ def set_char_size(face, width=0, height=0, hres=0, vres=0): face.set_char_size(width, height, hres, vres) -def load_char(font, face, text): - face.load_char(ord(text[0])) - - -def calc_cell_width(font, face): - ans = 0 - for i in range(32, 128): - ch = chr(i) - load_char(font, face, ch) - m = face.glyph_metrics() - ans = max(ans, ceil_int(m.horiAdvance / 64)) - return ans - - @lru_cache(maxsize=2**10) def font_for_text(char, bold=False, italic=False, allow_bitmaped_fonts=False): if allow_bitmaped_fonts: @@ -87,7 +72,7 @@ def set_font_family(opts, override_font_size=None): for fobj in chain(current_font_family.values(), symbol_map.values()): set_char_size(fobj.face, **cff_size) face = current_font_family['regular'].face - cell_width = calc_cell_width(current_font_family['regular'], face) + cell_width = face.calc_cell_width() cell_height = font_units_to_pixels( face.height, face.units_per_EM, size_in_pts, dpi[1] ) @@ -109,49 +94,6 @@ def set_font_family(opts, override_font_size=None): return cell_width, cell_height -CharBitmap = namedtuple( - 'CharBitmap', 'data bearingX bearingY advance rows columns' -) - - -def render_to_bitmap(font, face, text): - load_char(font, face, text) - bitmap = face.bitmap() - if bitmap.pixel_mode != FT_PIXEL_MODE_GRAY: - raise ValueError( - 'FreeType rendered the glyph for {!r} with an unsupported pixel mode: {}'. - format(text, bitmap.pixel_mode) - ) - return bitmap - - -def render_using_face(font, face, text, width, italic, bold): - bitmap = render_to_bitmap(font, face, text) - if width == 1 and bitmap.width > cell_width: - extra = bitmap.width - cell_width - if italic and extra < cell_width // 2: - bitmap = face.trim_to_width(bitmap, cell_width) - elif extra > max(2, 0.3 * cell_width) and face.is_scalable: - # rescale the font size so that the glyph is visible in a single - # cell and hope somebody updates libc's wcwidth - sz = cff_size.copy() - sz['width'] = int(sz['width'] * cell_width / bitmap.width) - # Preserve aspect ratio - sz['height'] = int(sz['height'] * cell_width / bitmap.width) - try: - set_char_size(face, **sz) - bitmap = render_to_bitmap(font, face, text) - finally: - set_char_size(face, **cff_size) - m = face.glyph_metrics() - return CharBitmap( - bitmap.buffer, - ceil_int(abs(m.horiBearingX) / 64), - ceil_int(abs(m.horiBearingY) / 64), - ceil_int(m.horiAdvance / 64), bitmap.rows, bitmap.width - ) - - def font_has_text(font, text): for c in text: if not font.face.get_char_index(c): @@ -186,139 +128,58 @@ def face_for_text(text, bold=False, italic=False): return font, face -def render_char(text, bold=False, italic=False, width=1): - font, face = face_for_text(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): +def render_text(text, bold=False, italic=False, num_cells=1): font, face = face_for_text(text, bold, italic) - if width == 1: + func = partial(face.draw_single_glyph, ord(text[0])) if len(text) == 1 else partial(face.draw_complex_glyph, text) + if num_cells == 1: buf = CharTexture() ans = (buf,) else: - buf = (ctypes.c_ubyte * (cell_width * width * cell_height))() - ans = tuple(CharTexture() for i in range(width)) - face.draw_complex_glyph(text, cell_width, cell_height, ctypes.addressof(buf), width, bold, italic, baseline) - if width > 1: + buf = (ctypes.c_ubyte * (cell_width * num_cells * cell_height))() + ans = tuple(CharTexture() for i in range(num_cells)) + func(cell_width, cell_height, ctypes.addressof(buf), num_cells, bold, italic, baseline) + if num_cells > 1: face.split_cells(cell_width, cell_height, ctypes.addressof(buf), *(ctypes.addressof(x) for x in ans)) return ans -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. - - # Calculate column bounds - if bitmap_char.columns > cell_width: - src_start_column, dest_start_column = 0, 0 - else: - src_start_column, dest_start_column = 0, bitmap_char.bearingX - extra = dest_start_column + bitmap_char.columns - cell_width - if extra > 0: - dest_start_column -= extra - column_count = min( - bitmap_char.columns - src_start_column, cell_width - dest_start_column - ) - - # Calculate row bounds, making sure the baseline is aligned with the cell - # baseline - if bitmap_char.bearingY > baseline: - src_start_row, dest_start_row = bitmap_char.bearingY - baseline, 0 - else: - src_start_row, dest_start_row = 0, baseline - bitmap_char.bearingY - row_count = min( - bitmap_char.rows - src_start_row, cell_height - dest_start_row - ) - return create_cell_buffer( - bitmap_char, src_start_row, dest_start_row, row_count, - src_start_column, dest_start_column, column_count - ) - - -def split_char_bitmap(bitmap_char): - stride = bitmap_char.columns - extra = stride - cell_width - rows = bitmap_char.rows - first_buf = (ctypes.c_ubyte * (cell_width * rows))() - second_buf = (ctypes.c_ubyte * (extra * rows))() - src = bitmap_char.data - for r in range(rows): - soff, off = r * stride, r * cell_width - first_buf[off:off + cell_width] = src[soff:soff + cell_width] - off = r * extra - soff += cell_width - second_buf[off:off + extra] = src[soff:soff + extra] - first = bitmap_char._replace(data=first_buf, columns=cell_width) - second = bitmap_char._replace(data=second_buf, bearingX=0, columns=extra) - return first, second - - def current_cell(): return CharTexture, cell_width, cell_height, baseline, underline_thickness, underline_position @lru_cache(maxsize=8) -def missing_glyph(width): - w = cell_width * width +def missing_glyph(num_cells, cell_width, cell_height): + w = cell_width * num_cells ans = bytearray(w * cell_height) render_missing_glyph(ans, w, cell_height) - first, second = CharBitmap(ans, 0, 0, 0, cell_height, w), None - if width == 2: - first, second = split_char_bitmap(first) - second = create_cell_buffer( - second, 0, 0, second.rows, 0, 0, second.columns - ) - first = create_cell_buffer(first, 0, 0, first.rows, 0, 0, first.columns) + buf = (ctypes.c_ubyte * w * cell_height).from_buffer(ans) + face = current_font_family['regular'].face + bufs = tuple(CharTexture() for i in range(num_cells)) + face.split_cells(cell_width, cell_height, ctypes.addressof(buf), *(ctypes.addressof(x) for x in bufs)) + if num_cells == 2: + first, second = bufs + else: + first, second = bufs[0], None return first, second def render_cell(text=' ', bold=False, italic=False): - width = wcwidth(text[0]) + num_cells = wcwidth(text[0]) def safe_freetype(func): try: - return func(text, bold, italic, width) + return func(text, bold, italic, num_cells) except FontNotFound as err: safe_print('ERROR:', err, file=sys.stderr) except FreeTypeError as err: safe_print('Failed to render text:', repr(text), 'with error:', err, file=sys.stderr) - if len(text) > 1: - ret = safe_freetype(render_complex_char) - if ret is None: - return missing_glyph(width) - if width == 1: - first, second = ret[0], None - else: - first, second = ret + ret = safe_freetype(render_text) + if ret is None: + return missing_glyph(num_cells, cell_width, cell_height) + if num_cells == 1: + first, second = ret[0], None else: - bitmap_char = safe_freetype(render_char) - if bitmap_char is None: - return missing_glyph(width) - second = None - if width == 2: - if bitmap_char.columns > cell_width: - bitmap_char, second = split_char_bitmap(bitmap_char) - second = place_char_in_cell(second) - else: - second = render_cell()[0] - - first = place_char_in_cell(bitmap_char) + first, second = ret return first, second - - -def create_cell_buffer( - bitmap_char, src_start_row, dest_start_row, row_count, src_start_column, - dest_start_column, column_count -): - src = bitmap_char.data - src_stride = bitmap_char.columns - dest = CharTexture() - for r in range(row_count): - sr, dr = src_start_column + ( - src_start_row + r - ) * src_stride, dest_start_column + (dest_start_row + r) * cell_width - dest[dr:dr + column_count] = src[sr:sr + column_count] - return dest diff --git a/kitty/freetype.c b/kitty/freetype.c index 8563ebdab..e132ae2f4 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -158,15 +158,6 @@ load_glyph(Face *self, int glyph_index) { return true; } -static PyObject* -load_char(Face *self, PyObject *ch) { -#define load_char_doc "load_char(char)" - char_type codepoint = (char_type)PyLong_AsUnsignedLong(ch); - int glyph_index = FT_Get_Char_Index(self->face, codepoint); - if (!load_glyph(self, glyph_index)) return NULL; - Py_RETURN_NONE; -} - static PyObject* get_char_index(Face *self, PyObject *args) { #define get_char_index_doc "" @@ -178,71 +169,18 @@ get_char_index(Face *self, PyObject *args) { return Py_BuildValue("I", ans); } -static PyStructSequence_Field gm_fields[] = { - {"width", NULL}, - {"height", NULL}, - {"horiBearingX", NULL}, - {"horiBearingY", NULL}, - {"horiAdvance", NULL}, - {"vertBearingX", NULL}, - {"vertBearingY", NULL}, - {"vertAdvance", NULL}, - {NULL} -}; - -static PyStructSequence_Desc gm_desc = {"GlpyhMetrics", NULL, gm_fields, 8}; -static PyTypeObject GlpyhMetricsType = {{{0}}}; - -static inline PyObject* -gm_to_py(FT_Glyph_Metrics *gm) { - PyObject *ans = PyStructSequence_New(&GlpyhMetricsType); - if (ans != NULL) { -#define SI(num, attr) PyStructSequence_SET_ITEM(ans, num, PyLong_FromLong(gm->attr)); if (PyStructSequence_GET_ITEM(ans, num) == NULL) { Py_CLEAR(ans); return PyErr_NoMemory(); } - SI(0, width); SI(1, height); - SI(2, horiBearingX); SI(3, horiBearingY); SI(4, horiAdvance); - SI(5, vertBearingX); SI(6, vertBearingY); SI(7, vertAdvance); -#undef SI - } else return PyErr_NoMemory(); - return ans; -} - static PyObject* -glyph_metrics(Face *self) { -#define glyph_metrics_doc "" - return gm_to_py(&self->face->glyph->metrics); +calc_cell_width(Face *self) { +#define calc_cell_width_doc "" + unsigned long ans = 0; + for (char_type i = 32; i < 128; i++) { + int glyph_index = FT_Get_Char_Index(self->face, i); + if (!load_glyph(self, glyph_index)) return NULL; + ans = MAX(ans, (unsigned long)ceilf((float)self->face->glyph->metrics.horiAdvance / 64.f)); + } + return PyLong_FromUnsignedLong(ans); } -static PyStructSequence_Field bm_fields[] = { - {"rows", NULL}, - {"width", NULL}, - {"pitch", NULL}, - {"buffer", NULL}, - {"num_grays", NULL}, - {"pixel_mode", NULL}, - {"palette_mode", NULL}, - {NULL} -}; -static PyStructSequence_Desc bm_desc = {"Bitmap", NULL, bm_fields, 7}; -static PyTypeObject BitmapType = {{{0}}}; - -static PyObject* -bitmap(Face *self) { -#define bitmap_doc "" - PyObject *ans = PyStructSequence_New(&BitmapType); - if (ans != NULL) { -#define SI(num, attr, func, conv) PyStructSequence_SET_ITEM(ans, num, func((conv)self->face->glyph->bitmap.attr)); if (PyStructSequence_GET_ITEM(ans, num) == NULL) { Py_CLEAR(ans); return PyErr_NoMemory(); } - SI(0, rows, PyLong_FromUnsignedLong, unsigned long); SI(1, width, PyLong_FromUnsignedLong, unsigned long); - SI(2, pitch, PyLong_FromLong, long); - PyObject *t = PyByteArray_FromStringAndSize((const char*)self->face->glyph->bitmap.buffer, self->face->glyph->bitmap.rows * self->face->glyph->bitmap.pitch); - if (t == NULL) { Py_CLEAR(ans); return PyErr_NoMemory(); } - PyStructSequence_SET_ITEM(ans, 3, t); - SI(4, num_grays, PyLong_FromUnsignedLong, unsigned long); - SI(5, pixel_mode, PyLong_FromUnsignedLong, unsigned long); SI(6, palette_mode, PyLong_FromUnsignedLong, unsigned long); -#undef SI - } else return PyErr_NoMemory(); - return ans; -} - static PyStructSequence_Field shape_fields[] = { {"glyph_id", NULL}, {"cluster", NULL}, @@ -396,6 +334,20 @@ place_bitmap_in_cell(unsigned char *cell, ProcessedBitmap *bm, size_t cell_width } } +static PyObject* +draw_single_glyph(Face *self, PyObject *args) { +#define draw_single_glyph_doc "draw_complex_glyph(codepoint, cell_width, cell_height, cell_buffer, num_cells, bold, italic, baseline)" + int bold, italic; + unsigned int cell_width, cell_height, num_cells, baseline, codepoint; + PyObject *addr; + if (!PyArg_ParseTuple(args, "IIIO!IppI", &codepoint, &cell_width, &cell_height, &PyLong_Type, &addr, &num_cells, &bold, &italic, &baseline)) return NULL; + unsigned char *cell = PyLong_AsVoidPtr(addr); + int glyph_id = FT_Get_Char_Index(self->face, codepoint); + ProcessedBitmap bm; + if (!render_bitmap(self, glyph_id, &bm, cell_width, num_cells, bold, italic, true)) return NULL; + place_bitmap_in_cell(cell, &bm, cell_width * num_cells, cell_height, 0, 0, &self->face->glyph->metrics, baseline); + Py_RETURN_NONE; +} static PyObject* draw_complex_glyph(Face *self, PyObject *args) { @@ -448,46 +400,6 @@ split_cells(Face UNUSED *self, PyObject *args) { } -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" - PyObject *bitmap, *t; - unsigned long cell_width, rows, width, rtrim = 0, extra, ltrim; - unsigned char *src, *dest; - bool column_has_text = false; - if (!PyArg_ParseTuple(args, "O!k", &BitmapType, &bitmap, &cell_width)) return NULL; - rows = PyLong_AsUnsignedLong(PyStructSequence_GET_ITEM(bitmap, 0)); - width = PyLong_AsUnsignedLong(PyStructSequence_GET_ITEM(bitmap, 1)); - extra = width - cell_width; - if (extra >= cell_width) { PyErr_SetString(FreeType_Exception, "Too large for trimming"); return NULL; } - PyObject *ans = PyStructSequence_New(&BitmapType); - if (ans == NULL) return PyErr_NoMemory(); - src = (unsigned char*)PyByteArray_AS_STRING(PyStructSequence_GET_ITEM(bitmap, 3)); - PyObject *abuf = PyByteArray_FromStringAndSize(NULL, cell_width * rows); - if (abuf == NULL) { Py_CLEAR(ans); return PyErr_NoMemory(); } - dest = (unsigned char*)PyByteArray_AS_STRING(abuf); - PyStructSequence_SET_ITEM(ans, 1, PyLong_FromUnsignedLong(cell_width)); - PyStructSequence_SET_ITEM(ans, 2, PyLong_FromUnsignedLong(cell_width)); - PyStructSequence_SET_ITEM(ans, 3, abuf); -#define COPY(which) t = PyStructSequence_GET_ITEM(bitmap, which); Py_INCREF(t); PyStructSequence_SET_ITEM(ans, which, t); - COPY(0); COPY(4); COPY(5); COPY(6); -#undef COPY - - for (long x = width - 1; !column_has_text && x > -1 && rtrim < extra; x--) { - for (unsigned long y = 0; y < rows * width; y += width) { - if (src[x + y] > 200) { column_has_text = true; break; } - } - if (!column_has_text) rtrim++; - } - rtrim = MIN(extra, rtrim); - ltrim = extra - rtrim; - for (unsigned long y = 0; y < rows; y++) { - memcpy(dest + y*cell_width, src + ltrim + y*width, cell_width); - } - - return ans; -} - // Boilerplate {{{ static PyMemberDef members[] = { @@ -507,14 +419,12 @@ static PyMemberDef members[] = { static PyMethodDef methods[] = { METHOD(set_char_size, METH_VARARGS) - METHOD(load_char, METH_O) METHOD(shape, METH_VARARGS) METHOD(draw_complex_glyph, METH_VARARGS) + METHOD(draw_single_glyph, METH_VARARGS) METHOD(split_cells, METH_VARARGS) METHOD(get_char_index, METH_VARARGS) - METHOD(glyph_metrics, METH_NOARGS) - METHOD(bitmap, METH_NOARGS) - METHOD(trim_to_width, METH_VARARGS) + METHOD(calc_cell_width, METH_NOARGS) {NULL} /* Sentinel */ }; @@ -553,11 +463,7 @@ init_freetype_library(PyObject *m) { PyErr_SetString(FreeType_Exception, "Failed to register the freetype library at exit handler"); return false; } - 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);