From c599b7e02fec0f4673d1e129ca78f5c0851f8ea7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 28 Jan 2025 14:42:43 +0530 Subject: [PATCH] API to render a single codepoint using a Face --- kitty/core_text.m | 49 +++++++++++++++++++++++++++++++++++++++ kitty/fast_data_types.pyi | 14 +++++++---- kitty/fonts.c | 2 +- kitty/fonts/render.py | 19 ++++++++++++++- kitty/freetype.c | 48 ++++++++++++++++++++++++++++++++------ 5 files changed, 118 insertions(+), 14 deletions(-) diff --git a/kitty/core_text.m b/kitty/core_text.m index 4bd22842c..27a5b1e17 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -38,6 +38,19 @@ typedef struct { PyTypeObject CTFace_Type; static CTFontRef window_title_font = nil; +static BOOL +CTFontSupportsColorGlyphs(CTFontRef font) { + CFTypeRef symbolicTraits = CTFontCopyAttribute(font, kCTFontSymbolicTrait); + if (symbolicTraits) { + if ([(NSNumber *)symbolicTraits unsignedLongValue] & kCTFontColorGlyphsTrait) { + CFRelease(symbolicTraits); + return YES; + } + CFRelease(symbolicTraits); + } + return NO; +} + static PyObject* convert_cfstring(CFStringRef src, int free_src) { RAII_CoreFoundation(CFStringRef, releaseme, free_src ? src : nil); @@ -818,6 +831,41 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) { static void destroy_hb_buffer(hb_buffer_t **x) { if (*x) hb_buffer_destroy(*x); } +static PyObject* +render_codepoint(CTFace *self, PyObject *args) { + unsigned long cp, fg = 0xffffff; + if (!PyArg_ParseTuple(args, "k|k", &cp, &fg)) return NULL; + const int num_chars = 1; + ensure_render_space(0, 0, num_chars); + buffers.glyphs[0] = glyph_id_for_codepoint_ctfont(self->ct_font, cp); + CGSize local_advances[num_chars]; + CTFontGetAdvancesForGlyphs(self->ct_font, kCTFontOrientationDefault, buffers.glyphs, local_advances, num_chars); + CGRect bounding_box = CTFontGetBoundingRectsForGlyphs(self->ct_font, kCTFontOrientationDefault, buffers.glyphs, buffers.boxes, num_chars); + StringCanvas ans = { .width = (size_t)(bounding_box.size.width + 1), .height = (size_t)(1 + bounding_box.size.height) }; + size_t baseline = ans.height; + ensure_render_space(ans.width, ans.height, num_chars); + PyObject *pbuf = PyBytes_FromStringAndSize(NULL, ans.width * ans.height * sizeof(pixel)); + if (!pbuf) return NULL; + memset(PyBytes_AS_STRING(pbuf), 0, PyBytes_GET_SIZE(pbuf)); + const unsigned long canvas_width = ans.width, canvas_height = ans.height; + if (CTFontSupportsColorGlyphs(self->ct_font)) { + render_color_glyph(self->ct_font, (uint8_t*)PyBytes_AS_STRING(pbuf), buffers.glyphs[0], ans.width, ans.height, baseline); + } else { + render_glyphs(self->ct_font, ans.width, ans.height, baseline, num_chars); + uint8_t r = (fg >> 16) & 0xff, g = (fg >> 8) & 0xff, b = fg & 0xff; + const uint8_t *last_pixel = (uint8_t*)PyBytes_AS_STRING(pbuf) + PyBytes_GET_SIZE(pbuf) - sizeof(pixel); + const uint8_t *s_limit = buffers.render_buf + canvas_width * canvas_height; + for ( + uint8_t *p = (uint8_t*)PyBytes_AS_STRING(pbuf), *s = buffers.render_buf; + p <= last_pixel && s < s_limit; + p += sizeof(pixel), s++ + ) { + p[0] = r; p[1] = g; p[2] = b; p[3] = s[0]; + } + } + return Py_BuildValue("Nkk", pbuf, canvas_width, canvas_height); +} + static PyObject* render_sample_text(CTFace *self, PyObject *args) { unsigned long canvas_width, canvas_height; @@ -1122,6 +1170,7 @@ static PyMethodDef methods[] = { METHODB(identify_for_debug, METH_NOARGS), METHODB(set_size, METH_VARARGS), METHODB(render_sample_text, METH_VARARGS), + METHODB(render_codepoint, METH_VARARGS), METHODB(get_best_name, METH_O), {NULL} /* Sentinel */ }; diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 4daf12b07..6c19c4b93 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -448,7 +448,10 @@ class Face: def identify_for_debug(self) -> str: ... def postscript_name(self) -> str: ... def set_size(self, sz_in_pts: float, dpi_x: float, dpi_y: float) -> None: ... - def render_sample_text(self, text: str, width: int, height: int, fg_color: int = 0xffffff) -> Tuple[bytes, int, int]: ... + def render_sample_text( + self, text: str, width: int, height: int, fg_color: int = 0xffffff + ) -> tuple[bytes, int, int]: ... + def render_codepoint(self, cp: int, fg_color: int = 0xffffff) -> tuple[bytes, int, int]: ... def get_variation(self) -> Optional[Dict[str, float]]: ... def get_features(self) -> Dict[str, Optional[FeatureData]]: ... def applied_features(self) -> Dict[str, str]: ... @@ -485,7 +488,10 @@ class CTFace: def identify_for_debug(self) -> str: ... def postscript_name(self) -> str: ... def set_size(self, sz_in_pts: float, dpi_x: float, dpi_y: float) -> None: ... - def render_sample_text(self, text: str, width: int, height: int, fg_color: int = 0xffffff) -> Tuple[bytes, int, int]: ... + def render_sample_text( + self, text: str, width: int, height: int, fg_color: int = 0xffffff, + ) -> tuple[bytes, int, int]: ... + def render_codepoint(self, cp: int, fg_color: int = 0xffffff) -> tuple[bytes, int, int]: ... def get_variation(self) -> Optional[Dict[str, float]]: ... def get_features(self) -> Dict[str, Optional[FeatureData]]: ... def applied_features(self) -> Dict[str, str]: ... @@ -1142,9 +1148,7 @@ def get_fallback_font(text: str, bold: bool, italic: bool) -> Any: pass -def create_test_font_group(sz: float, dpix: float, - dpiy: float) -> Tuple[int, int]: - pass +def create_test_font_group(sz: float, dpix: float, dpiy: float) -> tuple[int, int, int]: ... class HistoryBuf: diff --git a/kitty/fonts.c b/kitty/fonts.c index 48522e4ac..68ac16789 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -2199,7 +2199,7 @@ create_test_font_group(PyObject *self UNUSED, PyObject *args) { if (!PyArg_ParseTuple(args, "ddd", &sz, &dpix, &dpiy)) return NULL; FontGroup *fg = font_group_for(sz, dpix, dpiy); if (!fg->sprite_map) send_prerendered_sprites(fg); - return Py_BuildValue("II", fg->fcm.cell_width, fg->fcm.cell_height); + return Py_BuildValue("III", fg->fcm.cell_width, fg->fcm.cell_height, fg->fcm.baseline); } static PyObject* diff --git a/kitty/fonts/render.py b/kitty/fonts/render.py index 7bbdda931..4ac8990d2 100644 --- a/kitty/fonts/render.py +++ b/kitty/fonts/render.py @@ -221,6 +221,7 @@ class setup_for_testing: xnum = 100000 ynum = 100 + baseline = 0 def __init__(self, family: str = 'monospace', size: float = 11.0, dpi: float = 96.0, main_face_path: str = ''): self.family, self.size, self.dpi = family, size, dpi @@ -243,7 +244,7 @@ class setup_for_testing: descriptor_overrides[0] = self.main_face_path, False, False try: set_font_family(opts) - cell_width, cell_height = create_test_font_group(self.size, self.dpi, self.dpi) + cell_width, cell_height, self.baseline = create_test_font_group(self.size, self.dpi, self.dpi) return sprites, cell_width, cell_height except Exception: set_send_sprite_to_gpu(None) @@ -351,3 +352,19 @@ def showcase() -> None: test_render_string('你好,世界', family=f) test_render_string('│😁│🙏│😺│', family=f) test_render_string('A=>>B!=C', family='Fira Code') + + +def test_render_codepoint(char: str = '😺', path: str = '/t/Noto-COLRv1.ttf', font_size: float = 160.0) -> None: + if TYPE_CHECKING: + from kitty.fast_data_types import CTFace, Face + def create_face(path: str) -> 'Union[CTFace, Face]': + if is_macos: + from kitty.fast_data_types import CTFace + return CTFace(path=path) + from kitty.fast_data_types import Face + return Face(path=path) + f = create_face(path=path) + f.set_size(font_size, 96, 96) + bitmap, w, h = f.render_codepoint(ord(char)) + display_bitmap(bitmap, w, h) + print('\n') diff --git a/kitty/freetype.c b/kitty/freetype.c index 2dbb8b56c..72baded99 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -617,14 +617,18 @@ detect_right_edge(ProcessedBitmap *ans) { static bool render_color_bitmap(Face *self, int glyph_id, ProcessedBitmap *ans, unsigned int cell_width, unsigned int cell_height, unsigned int num_cells, unsigned int baseline UNUSED) { unsigned short best = 0, diff = USHRT_MAX; + const unsigned int width_to_render_in = num_cells * cell_width; if (self->face->num_fixed_sizes > 0) { - const short limit = self->face->num_fixed_sizes; - for (short i = 0; i < limit; i++) { + for (short i = 0; i < self->face->num_fixed_sizes; i++) { unsigned short w = self->face->available_sizes[i].width; - unsigned short d = w > (unsigned short)cell_width ? w - (unsigned short)cell_width : (unsigned short)cell_width - w; - if (d < diff) { - diff = d; - best = i; + if (width_to_render_in) { + unsigned short d = w > (unsigned short)width_to_render_in ? w - (unsigned short)width_to_render_in : (unsigned short)width_to_render_in - w; + if (d < diff) { + diff = d; + best = i; + } + } else { + if (w > self->face->available_sizes[best].width) best = i; } } FT_Error error = FT_Select_Size(self->face, best); @@ -638,7 +642,7 @@ render_color_bitmap(Face *self, int glyph_id, ProcessedBitmap *ans, unsigned int ans->stride = bitmap->pitch < 0 ? -bitmap->pitch : bitmap->pitch; ans->rows = bitmap->rows; ans->pixel_mode = bitmap->pixel_mode; - if (ans->width > num_cells * cell_width + 2) downsample_bitmap(ans, num_cells * cell_width, cell_height); + if (width_to_render_in && ans->width > width_to_render_in + 2) downsample_bitmap(ans, width_to_render_in, cell_height); ans->bitmap_top = (int)((float)self->face->glyph->bitmap_top / ans->factor); ans->bitmap_left = (int)((float)self->face->glyph->bitmap_left / ans->factor); detect_right_edge(ans); @@ -992,6 +996,35 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) { static void destroy_hb_buffer(hb_buffer_t **x) { if (*x) hb_buffer_destroy(*x); } +static PyObject* +render_codepoint(Face *self, PyObject *args) { + unsigned long cp, fg = 0xffffff; + if (!PyArg_ParseTuple(args, "k|k", &cp, &fg)) return NULL; + FT_UInt glyph_index = FT_Get_Char_Index(self->face, cp); + ProcessedBitmap pbm = EMPTY_PBM; + if (self->has_color) { + render_color_bitmap(self, glyph_index, &pbm, 0, 0, 0, 0); + } else { + int load_flags = get_load_flags(self->hinting, self->hintstyle, FT_LOAD_RENDER); + FT_Load_Glyph(self->face, glyph_index, load_flags); + FT_Render_Glyph(self->face->glyph, FT_RENDER_MODE_NORMAL); + FT_Bitmap *bitmap = &self->face->glyph->bitmap; + populate_processed_bitmap(self->face->glyph, bitmap, &pbm, false); + } + const unsigned long canvas_width = pbm.width, canvas_height = pbm.rows; + RAII_PyObject(ans, PyBytes_FromStringAndSize(NULL, sizeof(pixel) * canvas_height * canvas_width)); + if (!ans) return NULL; + pixel *canvas = (pixel*)PyBytes_AS_STRING(ans); + memset(canvas, 0, PyBytes_GET_SIZE(ans)); + place_bitmap_in_canvas(canvas, &pbm, canvas_width, canvas_height, 0, 0, 0, 99999, fg, 0, 0); + for (pixel *c = canvas; c < canvas + canvas_width * canvas_height; c++) { + uint8_t *p = (uint8_t*)c; + 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; + } + return Py_BuildValue("Okk", ans, canvas_width, canvas_height); +} + static PyObject* render_sample_text(Face *self, PyObject *args) { unsigned long canvas_width, canvas_height; @@ -1090,6 +1123,7 @@ static PyMethodDef methods[] = { METHODB(get_best_name, METH_O), METHODB(set_size, METH_VARARGS), METHODB(render_sample_text, METH_VARARGS), + METHODB(render_codepoint, METH_VARARGS), {NULL} /* Sentinel */ };