From 0cc8dd28ded74948eb03e32180a753cda42b9865 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 29 May 2024 13:23:25 +0530 Subject: [PATCH] Add support for font features when rendering sample text --- kitty/core_text.m | 71 ++++++++++++++++++++++++++----------------- kitty/fontconfig.c | 6 ++-- kitty/fonts.c | 10 ++++-- kitty/fonts.h | 2 +- kitty/fonts/common.py | 9 ++++-- kitty/freetype.c | 54 +++++++++++++++++++++----------- 6 files changed, 99 insertions(+), 53 deletions(-) diff --git a/kitty/core_text.m b/kitty/core_text.m index e39f04c27..8ba1aef0f 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -624,7 +624,7 @@ new(PyTypeObject *type UNUSED, PyObject *args, PyObject *kw) { } PyObject* -specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE fg UNUSED) { +specialize_font_descriptor(PyObject *base_descriptor, double font_sz_in_pts UNUSED, double dpi_x UNUSED, double dpi_y UNUSED) { Py_INCREF(base_descriptor); return base_descriptor; } @@ -742,6 +742,8 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) { return ans; } +static void destroy_hb_buffer(hb_buffer_t **x) { if (*x) hb_buffer_destroy(*x); } + static PyObject* render_sample_text(CTFace *self, PyObject *args) { unsigned long canvas_width, canvas_height; @@ -756,35 +758,48 @@ render_sample_text(CTFace *self, PyObject *args) { canvas_height = MIN(canvas_height, num_of_lines * cell_height); RAII_PyObject(pbuf, PyBytes_FromStringAndSize(NULL, sizeof(pixel) * canvas_width * canvas_height)); if (!pbuf) return NULL; - RAII_ALLOC(unichar, chars, calloc(sizeof(unichar), num_chars)); - if (!chars) return PyErr_NoMemory(); - for (size_t i = 0; i < num_chars; i++) chars[i] = PyUnicode_READ_CHAR(ptext, i); - RAII_ALLOC(CGSize, local_advances, calloc(sizeof(CGSize), num_chars)); - if (!local_advances) return PyErr_NoMemory(); - ensure_render_space(0, 0, num_chars); - CTFontGetGlyphsForCharacters(font, chars, buffers.glyphs, num_chars); - CTFontGetAdvancesForGlyphs(font, kCTFontOrientationDefault, buffers.glyphs, local_advances, num_chars); - CTFontGetBoundingRectsForGlyphs(font, kCTFontOrientationDefault, buffers.glyphs, buffers.boxes, num_chars); - CGFloat x = 0, y = 0; - memset(PyByteArray_AS_STRING(pbuf), 0, PyBytes_GET_SIZE(pbuf)); - if (cell_width > canvas_width) goto end; - for (size_t i = 0; i < num_chars; i++) { - if (local_advances[i].width + x > canvas_width) { - x = 0; - y += cell_height; - } - if (y + cell_height > canvas_height) { - num_chars = i - 1; - break; - } - buffers.positions[i] = CGPointMake(x, -y); - x += cell_width; + + __attribute__((cleanup(destroy_hb_buffer))) hb_buffer_t *hb_buffer = hb_buffer_create(); + if (!hb_buffer_pre_allocate(hb_buffer, 4*num_chars)) { PyErr_NoMemory(); return NULL; } + for (size_t n = 0; n < num_chars; n++) { + Py_UCS4 codep = PyUnicode_READ_CHAR(ptext, n); + hb_buffer_add_utf32(hb_buffer, &codep, 1, 0, 1); } - unsigned long height = MIN((int)ceil(y) + cell_height, canvas_height); - ensure_render_space(canvas_width, height, num_chars); - render_glyphs(font, canvas_width, height, baseline, num_chars); + hb_buffer_guess_segment_properties(hb_buffer); + if (!HB_DIRECTION_IS_HORIZONTAL(hb_buffer_get_direction(hb_buffer))) goto end; + hb_shape(harfbuzz_font_for_face((PyObject*)self), hb_buffer, self->font_features.features, self->font_features.count); + unsigned int len = hb_buffer_get_length(hb_buffer); + hb_glyph_info_t *info = hb_buffer_get_glyph_infos(hb_buffer, NULL); + hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hb_buffer, NULL); + + memset(PyBytes_AS_STRING(pbuf), 0, PyBytes_GET_SIZE(pbuf)); + if (cell_width > canvas_width) goto end; + + ensure_render_space(canvas_width, canvas_height, len); + float pen_x = 0, pen_y = 0; + unsigned num_glyphs = 0; + CGFloat scale = CTFontGetSize(self->ct_font) / CTFontGetUnitsPerEm(self->ct_font); + for (unsigned int i = 0; i < len; i++) { + float advance = (float)positions[i].x_advance * scale; + if (pen_x + advance > canvas_width) { + pen_y += cell_height; + pen_x = 0; + if (pen_y >= canvas_height) break; + } + double x = pen_x + (double)positions[i].x_offset * scale; + double y = pen_y + (double)positions[i].y_offset * scale; + pen_x += advance; + buffers.positions[i] = CGPointMake(x, -y); + buffers.glyphs[i] = info[i].codepoint; + num_glyphs++; + } + render_glyphs(font, canvas_width, canvas_height, baseline, num_glyphs); uint8_t r = (fg >> 16) & 0xff, g = (fg >> 8) & 0xff, b = fg & 0xff; - for (uint8_t *p = (uint8_t*)PyBytes_AS_STRING(pbuf), *s = buffers.render_buf; p < (uint8_t*)PyBytes_AS_STRING(pbuf) + sizeof(pixel) * canvas_width * height; p += 4, s++) { + for ( + uint8_t *p = (uint8_t*)PyBytes_AS_STRING(pbuf), *s = buffers.render_buf; + p < (uint8_t*)PyBytes_AS_STRING(pbuf) + sizeof(pixel) * canvas_width * canvas_height; + p += 4, s++ + ) { p[0] = r; p[1] = g; p[2] = b; p[3] = s[0]; } end: diff --git a/kitty/fontconfig.c b/kitty/fontconfig.c index 77afd4eaf..bd82a5b44 100644 --- a/kitty/fontconfig.c +++ b/kitty/fontconfig.c @@ -402,7 +402,7 @@ end: } PyObject* -specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE fg) { +specialize_font_descriptor(PyObject *base_descriptor, double font_sz_in_pts, double dpi_x, double dpi_y) { ensure_initialized(); PyObject *p = PyDict_GetItemString(base_descriptor, "path"); PyObject *idx = PyDict_GetItemString(base_descriptor, "index"); @@ -416,8 +416,8 @@ specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE fg) { RAII_PyObject(ans, NULL); AP(FcPatternAddString, FC_FILE, (const FcChar8*)PyUnicode_AsUTF8(p), "path"); AP(FcPatternAddInteger, FC_INDEX, face_idx, "index"); - AP(FcPatternAddDouble, FC_SIZE, fg->font_sz_in_pts, "size"); - AP(FcPatternAddDouble, FC_DPI, (fg->logical_dpi_x + fg->logical_dpi_y) / 2.0, "dpi"); + AP(FcPatternAddDouble, FC_SIZE, font_sz_in_pts, "size"); + AP(FcPatternAddDouble, FC_DPI, (dpi_x + dpi_y) / 2.0, "dpi"); ans = _fc_match(pat); FcPatternDestroy(pat); pat = NULL; diff --git a/kitty/fonts.c b/kitty/fonts.c index 07a33d7eb..35ae79b22 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -283,7 +283,7 @@ sprite_tracker_set_layout(GPUSpriteTracker *sprite_tracker, unsigned int cell_wi static PyObject* desc_to_face(PyObject *desc, FONTS_DATA_HANDLE fg) { - PyObject *d = specialize_font_descriptor(desc, fg); + PyObject *d = specialize_font_descriptor(desc, fg->font_sz_in_pts, fg->logical_dpi_x, fg->logical_dpi_y); if (d == NULL) return NULL; PyObject *ans = face_from_descriptor(d, fg); Py_DECREF(d); @@ -1829,7 +1829,12 @@ PyTypeObject ParsedFontFeature_Type = { .tp_call = parsed_font_feature_call, }; - +static PyObject* +pyspecialize_font_descriptor(PyObject *self UNUSED, PyObject *args) { + PyObject *desc; double font_sz, dpi_x, dpi_y; + if (!PyArg_ParseTuple(args, "Offf", &desc, &font_sz, &dpi_x, &dpi_y)) return NULL; + return specialize_font_descriptor(desc, font_sz, dpi_x, dpi_y); +} static PyMethodDef module_methods[] = { METHODB(set_font_data, METH_VARARGS), @@ -1843,6 +1848,7 @@ static PyMethodDef module_methods[] = { METHODB(current_fonts, METH_VARARGS), METHODB(test_render_line, METH_VARARGS), METHODB(get_fallback_font, METH_VARARGS), + {"specialize_font_descriptor", (PyCFunction)pyspecialize_font_descriptor, METH_VARARGS, ""}, {NULL, NULL, 0, NULL} /* Sentinel */ }; diff --git a/kitty/fonts.h b/kitty/fonts.h index 985a52e7b..53c4e5bcf 100644 --- a/kitty/fonts.h +++ b/kitty/fonts.h @@ -41,7 +41,7 @@ bool set_size_for_face(PyObject*, unsigned int, bool, FONTS_DATA_HANDLE); void cell_metrics(PyObject*, unsigned int*, unsigned int*, unsigned int*, unsigned int*, unsigned int*, unsigned int*, unsigned int*); bool render_glyphs_in_cells(PyObject *f, bool bold, bool italic, hb_glyph_info_t *info, hb_glyph_position_t *positions, unsigned int num_glyphs, pixel *canvas, unsigned int cell_width, unsigned int cell_height, unsigned int num_cells, unsigned int baseline, bool *was_colored, FONTS_DATA_HANDLE, bool center_glyph); PyObject* create_fallback_face(PyObject *base_face, CPUCell* cell, bool bold, bool italic, bool emoji_presentation, FONTS_DATA_HANDLE fg); -PyObject* specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE); +PyObject* specialize_font_descriptor(PyObject *base_descriptor, double, double, double); PyObject* face_from_path(const char *path, int index, FONTS_DATA_HANDLE); PyObject* face_from_descriptor(PyObject*, FONTS_DATA_HANDLE); PyObject* iter_fallback_faces(FONTS_DATA_HANDLE fgh, ssize_t *idx); diff --git a/kitty/fonts/common.py b/kitty/fonts/common.py index c63e22d77..61e24758a 100644 --- a/kitty/fonts/common.py +++ b/kitty/fonts/common.py @@ -21,7 +21,8 @@ if TYPE_CHECKING: prefer_variable: bool = False, ) -> Descriptor: ... def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospaced: bool = True) -> Descriptor: ... - def face_from_descriptor(descriptor: Descriptor) -> Face: ... + def face_from_descriptor(descriptor: Descriptor, font_sz_in_pts: Optional[float] = None, dpi_x: Optional[float] = None, dpi_y: Optional[float] = None + ) -> Face: ... def is_monospace(descriptor: Descriptor) -> bool: ... def is_variable(descriptor: Descriptor) -> bool: ... def set_named_style(name: str, font: Descriptor, vd: VariableData) -> bool: ... @@ -29,6 +30,7 @@ if TYPE_CHECKING: def get_axis_values(font: Descriptor, vd: VariableData) -> Dict[str, float]: ... else: FontCollectionMapType = FontMap = None + from kitty.fast_data_types import specialize_font_descriptor if is_macos: from kitty.fast_data_types import CTFace as Face from kitty.fonts.core_text import ( @@ -55,7 +57,10 @@ else: set_axis_values, set_named_style, ) - def face_from_descriptor(descriptor: Descriptor) -> Face: return Face(descriptor=descriptor) + def face_from_descriptor(descriptor, font_sz_in_pts = None, dpi_x = None, dpi_y = None): + if font_sz_in_pts is not None: + descriptor = specialize_font_descriptor(descriptor, font_sz_in_pts, dpi_x, dpi_y) + return Face(descriptor=descriptor) cache_for_variable_data_by_path: Dict[str, VariableData] = {} diff --git a/kitty/freetype.c b/kitty/freetype.c index fe47e0299..73e594e3f 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -316,7 +316,9 @@ new(PyTypeObject *type UNUSED, PyObject *args, PyObject *kw) { static char *kwds[] = {"descriptor", "path", "index", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kw, "|Osi", kwds, &descriptor, &path, &index)) return NULL; - if (descriptor) return face_from_descriptor(descriptor, NULL); + if (descriptor) { + return face_from_descriptor(descriptor, NULL); + } if (path) return face_from_path(path, index, NULL); PyErr_SetString(PyExc_TypeError, "Must specify either path or descriptor"); return NULL; @@ -958,9 +960,11 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) { return ans; } +static void destroy_hb_buffer(hb_buffer_t **x) { if (*x) hb_buffer_destroy(*x); } + static PyObject* render_sample_text(Face *self, PyObject *args) { - unsigned long canvas_width, canvas_height, pen_x = 0, pen_y = 0; + unsigned long canvas_width, canvas_height; unsigned long fg = 0xffffff; PyObject *ptext; if (!PyArg_ParseTuple(args, "Ukk|k", &ptext, &canvas_width, &canvas_height, &fg)) return NULL; @@ -970,30 +974,46 @@ render_sample_text(Face *self, PyObject *args) { canvas_height = MIN(canvas_height, num_of_lines * cell_height); RAII_PyObject(pbuf, PyBytes_FromStringAndSize(NULL, sizeof(pixel) * canvas_width * canvas_height)); if (!pbuf) return NULL; - pixel *canvas = (pixel*)PyBytes_AS_STRING(pbuf); - memset(canvas, 0, PyBytes_GET_SIZE(pbuf)); - if (cell_width > canvas_width) goto end; + memset(PyBytes_AS_STRING(pbuf), 0, PyBytes_GET_SIZE(pbuf)); + __attribute__((cleanup(destroy_hb_buffer))) hb_buffer_t *hb_buffer = hb_buffer_create(); + if (!hb_buffer_pre_allocate(hb_buffer, 4*PyUnicode_GET_LENGTH(ptext))) { PyErr_NoMemory(); return NULL; } for (ssize_t n = 0; n < PyUnicode_GET_LENGTH(ptext); n++) { - if (pen_x + cell_width > canvas_width) { + Py_UCS4 codep = PyUnicode_READ_CHAR(ptext, n); + hb_buffer_add_utf32(hb_buffer, &codep, 1, 0, 1); + } + hb_buffer_guess_segment_properties(hb_buffer); + if (!HB_DIRECTION_IS_HORIZONTAL(hb_buffer_get_direction(hb_buffer))) goto end; + hb_shape(harfbuzz_font_for_face((PyObject*)self), hb_buffer, self->font_features.features, self->font_features.count); + unsigned int len = hb_buffer_get_length(hb_buffer); + hb_glyph_info_t *info = hb_buffer_get_glyph_infos(hb_buffer, NULL); + hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hb_buffer, NULL); + + if (cell_width > canvas_width) goto end; + pixel *canvas = (pixel*)PyBytes_AS_STRING(pbuf); + int load_flags = get_load_flags(self->hinting, self->hintstyle, FT_LOAD_RENDER); + int error; + + float pen_x = 0, pen_y = 0; + for (unsigned int i = 0; i < len; i++) { + float advance = (float)positions[i].x_advance / 64.0f; + if (pen_x + advance > canvas_width) { pen_y += cell_height; pen_x = 0; + if (pen_y >= canvas_height) break; } - if (pen_y + cell_height > canvas_height) break; - Py_UCS4 ch = PyUnicode_READ_CHAR(ptext, n); - FT_UInt glyph_index = FT_Get_Char_Index(self->face, ch); - if (!glyph_index) continue; - int error = FT_Load_Glyph(self->face, glyph_index, FT_LOAD_DEFAULT); - if (error) continue; - error = FT_Render_Glyph(self->face->glyph, FT_RENDER_MODE_NORMAL); - if (error) continue; + size_t x = (size_t)round(pen_x + (float)positions[i].x_offset / 64.0f); + size_t y = (size_t)round(pen_y + (float)positions[i].y_offset / 64.0f); + pen_x += advance; + if ((error = FT_Load_Glyph(self->face, info[i].codepoint, load_flags))) continue; + if ((error = FT_Render_Glyph(self->face->glyph, FT_RENDER_MODE_NORMAL))) continue; FT_Bitmap *bitmap = &self->face->glyph->bitmap; ProcessedBitmap pbm = EMPTY_PBM; populate_processed_bitmap(self->face->glyph, bitmap, &pbm, false); - place_bitmap_in_canvas(canvas, &pbm, canvas_width, canvas_height, 0, 0, baseline, 99999, fg, pen_x, pen_y); - pen_x += self->face->glyph->advance.x >> 6; + place_bitmap_in_canvas(canvas, &pbm, canvas_width, canvas_height, 0, 0, baseline, 99999, fg, x, y); } - for (uint8_t *p = (uint8_t*)PyBytes_AS_STRING(pbuf); p < (uint8_t*)PyBytes_AS_STRING(pbuf) + sizeof(pixel) * canvas_width * (MIN(canvas_height, pen_y + cell_height)); p += 4) { + + for (uint8_t *p = (uint8_t*)PyBytes_AS_STRING(pbuf); p < (uint8_t*)PyBytes_AS_STRING(pbuf) + sizeof(pixel) * canvas_width * canvas_height; p += 4) { uint8_t a = p[0], b = p[1], g = p[2], r = p[3]; p[0] = r; p[1] = g; p[2] = b; p[3] = a; }