From dff91759a24a072cea0525c1f440990b87ee12d7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 11 Jan 2017 12:02:28 +0530 Subject: [PATCH] Refactor the cell rendering code for greater re-use Also start work on CoreText based rendering --- kitty/core_text.m | 84 +++++++++++++++++++++++++++++++++++++ kitty/fonts/box_drawing.py | 2 +- kitty/fonts/core_text.py | 33 ++++++++++++--- kitty/fonts/freetype.py | 76 ++-------------------------------- kitty/fonts/render.py | 85 ++++++++++++++++++++++++++++++++++++-- kitty/utils.py | 5 +++ 6 files changed, 204 insertions(+), 81 deletions(-) diff --git a/kitty/core_text.m b/kitty/core_text.m index c969a5b1f..7a9545b3e 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -8,6 +8,8 @@ #include "data-types.h" #include #include +#include +#import #import #import #import @@ -62,6 +64,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { self->underline_thickness = CTFontGetUnderlineThickness(self->font); self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font)); self->full_name = convert_cfstring(CTFontCopyFullName(self->font)); + self->scaled_point_sz = CTFontGetSize(self->font); if (self->family_name == NULL || self->full_name == NULL) { Py_CLEAR(self); } } } else { @@ -104,6 +107,84 @@ has_char(Face *self, PyObject *args) { return ret; } +static PyObject* +font_units_to_pixels(Face *self, PyObject *args) { +#define font_units_to_pixels_doc "Convert the specified value from font units to pixels at the current font size" + double x; + if (!PyArg_ParseTuple(args, "d", &x)) return NULL; + x *= self->scaled_point_sz / self->units_per_em; + return Py_BuildValue("i", (int)ceil(x)); +} + +static PyObject* +cell_size(Face *self) { +#define cell_size_doc "Return the best cell size for this font based on the advances for the ASCII chars from 32 to 127" +#define count (128 - 32) + unichar chars[count+1] = {0}; + CGGlyph glyphs[count+1] = {0}; + for (int i = 0; i < count; i++) chars[i] = 32 + i; + CTFontGetGlyphsForCharacters(self->font, chars, glyphs, count); + CGSize advances[1] = {0}; + unsigned int width = 0, w; + for (int i = 0; i < count; i++) { + if (glyphs[i]) { + CTFontGetAdvancesForGlyphs(self->font, kCTFontHorizontalOrientation, glyphs+1, advances, 1); + w = (unsigned int)(ceilf(advances[0].width)); + if (w > width) width = w; + } + } + return Py_BuildValue("I", width + 2); // + 2 for antialiasing which needs pixels on either side +#undef count +} + +static PyObject* +render_char(Face *self, PyObject *args) { +#define render_char_doc "" + char *s; + unsigned int width, height; + PyObject *pbuf; + CGColorSpaceRef color_space = NULL; + CGContextRef ctx = NULL; + CTFontRef font = NULL; + if (!PyArg_ParseTuple(args, "esIIO!", "UTF-8", &s, &width, &height, &PyLong_Type, &pbuf)) return NULL; + uint8_t *buf = (uint8_t*)PyLong_AsVoidPtr(pbuf); + CFStringRef str = CFStringCreateWithCString(NULL, s, kCFStringEncodingUTF8); + if (!str) return PyErr_NoMemory(); + CGGlyph glyphs[10] = {0}; + unichar chars[10] = {0}; + CFRange range = CFRangeMake(0, CFStringGetLength(str)); + CFStringGetCharacters(str, range, chars); + font = CTFontCreateForString(self->font, str, range); + if (font == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to find fallback font"); goto end; } + CTFontGetGlyphsForCharacters(font, chars, glyphs, range.length); + color_space = CGColorSpaceCreateDeviceGray(); + if (color_space == NULL) { PyErr_NoMemory(); goto end; } + ctx = CGBitmapContextCreate(buf, width, height, 8, width, color_space, (kCGBitmapAlphaInfoMask & kCGImageAlphaNone)); + if (ctx == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to create bitmap context"); goto end; } + CGContextSetShouldAntialias(ctx, true); + CGContextSetShouldSmoothFonts(ctx, true); // sub-pixel antialias + CGContextSetRGBFillColor(ctx, 1, 1, 1, 1); // white glyphs + CGAffineTransform transform = CGAffineTransformIdentity; + CGContextSetTextDrawingMode(ctx, kCGTextFill); + CGGlyph glyph = glyphs[0]; + if (glyph) { + CGRect rect = CTFontGetBoundingRectsForGlyphs(font, kCTFontHorizontalOrientation, glyphs, 0, 1); + // TODO: Scale the glyph if its bbox is larger than the image by using a non-identity transform + CGContextSetTextMatrix(ctx, transform); + CGFloat pos_x = -rect.origin.x + 1, pos_y = height - rect.origin.y - rect.size.height; + CGContextSetTextPosition(ctx, pos_x, pos_y); + CTFontDrawGlyphs(font, &glyph, &CGPointZero, 1, ctx); + } + +end: + CFRelease(str); + if (ctx) CGContextRelease(ctx); + if (color_space) CGColorSpaceRelease(color_space); + if (font && font != self->font) CFRelease(font); + if (PyErr_Occurred()) return NULL; + Py_RETURN_NONE; +} + // Boilerplate {{{ static PyMemberDef members[] = { @@ -123,6 +204,9 @@ static PyMemberDef members[] = { static PyMethodDef methods[] = { METHOD(has_char, METH_VARARGS) + METHOD(cell_size, METH_NOARGS) + METHOD(font_units_to_pixels, METH_VARARGS) + METHOD(render_char, METH_VARARGS) {NULL} /* Sentinel */ }; diff --git a/kitty/fonts/box_drawing.py b/kitty/fonts/box_drawing.py index 9868b083f..6940f0a8d 100644 --- a/kitty/fonts/box_drawing.py +++ b/kitty/fonts/box_drawing.py @@ -350,7 +350,7 @@ def join_rows(width, height, rows): def test_drawing(sz=32, family='monospace'): - from .freetype import join_cells, display_bitmap, render_cell, set_font_family + from .render import join_cells, display_bitmap, render_cell, set_font_family width, height = set_font_family(family, sz) pos = 0x2500 rows = [] diff --git a/kitty/fonts/core_text.py b/kitty/fonts/core_text.py index b85079767..45adca04a 100644 --- a/kitty/fonts/core_text.py +++ b/kitty/fonts/core_text.py @@ -2,25 +2,47 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2017, Kovid Goyal +import ctypes from kitty.fast_data_types import CTFace as Face -from kitty.utils import get_dpi - +from kitty.utils import get_logical_dpi, wcwidth, ceil_int main_font = {} +cell_width = cell_height = baseline = CellTexture = WideCellTexture = underline_thickness = underline_position = None def set_font_family(family, size_in_pts): - dpi = get_dpi()['logical'] + global cell_width, cell_height, baseline, CellTexture, WideCellTexture, underline_thickness, underline_position + dpi = get_logical_dpi() dpi = sum(dpi) / 2.0 if family.lower() == 'monospace': family = 'Menlo' for bold in (False, True): for italic in (False, True): main_font[(bold, italic)] = Face(family, bold, italic, True, size_in_pts, dpi) + mf = main_font[(False, False)] + cell_width = mf.cell_size() + cell_height = ceil_int(mf.ascent + mf.descent) + cell_height += 1 * int(round(dpi / 72.0)) # some line spacing + CellTexture = ctypes.c_ubyte * (cell_width * cell_height) + WideCellTexture = ctypes.c_ubyte * (2 * cell_width * cell_height) + baseline = int(round(mf.ascent)) + underline_position = int(round(mf.underline_position)) + underline_thickness = ceil_int(mf.underline_thickness) + return cell_width, cell_height -def render_cell(text=' ', bold=False, italic=False, underline=0, strikethrough=False): - pass +def current_cell(): + return CellTexture, cell_width, cell_height, baseline, underline_thickness, underline_position + + +def render_cell(text=' ', bold=False, italic=False): + width = wcwidth(text[0]) + face = main_font[(bold, italic)] + if width == 2: + buf, width = WideCellTexture(), cell_width * 2 + else: + buf, width = CellTexture(), cell_width + face.render_char(text, width, cell_height, ctypes.addressof(buf)) def develop(): @@ -32,3 +54,4 @@ def develop(): f = main_font[(False, False)] for attr in 'units_per_em ascent descent leading underline_position underline_thickness scaled_point_sz'.split(): print(attr, getattr(f, attr)) + print('cell_width: {}, cell_height: {}, baseline: {}'.format(cell_width, cell_height, baseline)) diff --git a/kitty/fonts/freetype.py b/kitty/fonts/freetype.py index c38a19760..52597686a 100644 --- a/kitty/fonts/freetype.py +++ b/kitty/fonts/freetype.py @@ -4,14 +4,13 @@ import unicodedata import ctypes -import math from collections import namedtuple from functools import lru_cache from threading import Lock from kitty.fast_data_types import Face, FT_PIXEL_MODE_GRAY +from kitty.utils import ceil_int from .fontconfig import find_font_for_character, get_font_files -from .box_drawing import is_renderable_box_char, render_box_char from kitty.utils import get_logical_dpi, wcwidth @@ -29,10 +28,6 @@ def load_char(font, face, text): face.load_char(text, font.hinting, font.hintstyle) -def ceil_int(x): - return int(math.ceil(x)) - - def calc_cell_width(font, face): ans = 0 for i in range(32, 128): @@ -169,33 +164,14 @@ def split_char_bitmap(bitmap_char): return first, second -def add_line(buf, position, thickness): - y = position - thickness // 2 - while thickness: - thickness -= 1 - offset = cell_width * y - for x in range(cell_width): - buf[offset + x] = 255 - y += 1 +def current_cell(): + return CharTexture, cell_width, cell_height, baseline, underline_thickness, underline_position -def add_curl(buf, position, thickness): - for y in range(position - thickness, position): - for x in range(0, cell_width // 2): - offset = cell_width * y - buf[offset + x] = 255 - for y in range(position, position + thickness): - for x in range(cell_width // 2, cell_width): - offset = cell_width * y - buf[offset + x] = 255 - - -def render_cell(text=' ', bold=False, italic=False, underline=0, strikethrough=False): +def render_cell(text=' ', bold=False, italic=False): # TODO: Handle non-normalizable combining chars. Probably need to use # harfbuzz for that text = unicodedata.normalize('NFC', text)[0] - if is_renderable_box_char(text): - return render_box_char(text, CharTexture(), cell_width, cell_height), None width = wcwidth(text) bitmap_char = render_char(text, bold, italic, width) second = None @@ -208,19 +184,6 @@ def render_cell(text=' ', bold=False, italic=False, underline=0, strikethrough=F first = place_char_in_cell(bitmap_char) - def dl(f, *a): - f(first, *a) - if second is not None: - f(second, pos, underline_thickness) - - if underline: - t = underline_thickness - if underline == 2: - t = max(1, min(cell_height - underline_position - 1, t)) - dl(add_curl if underline == 2 else add_line, underline_position, t) - if strikethrough: - pos = int(0.65 * baseline) - dl(add_line, pos, underline_thickness) return first, second @@ -232,34 +195,3 @@ def create_cell_buffer(bitmap_char, src_start_row, dest_start_row, row_count, sr 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 - - -def join_cells(cell_width, cell_height, *cells): - dstride = len(cells) * cell_width - ans = (ctypes.c_ubyte * (cell_height * dstride))() - for r in range(cell_height): - soff = r * cell_width - doff = r * dstride - for cnum, cell in enumerate(cells): - doff2 = doff + (cnum * cell_width) - ans[doff2:doff2 + cell_width] = cell[soff:soff + cell_width] - return ans - - -def display_bitmap(data, w, h): - from PIL import Image - img = Image.new('L', (w, h)) - img.putdata(data) - img.show() - - -def test_rendering(text='\'PingšŸ‘a⧽', sz=144, family='Ubuntu Mono for Kovid'): - set_font_family(family, sz) - cells = [] - for c in text: - f, s = render_cell(c, underline=2, strikethrough=True) - cells.append(f) - if s is not None: - cells.append(s) - char_data = join_cells(cell_width, cell_height, *cells) - display_bitmap(char_data, cell_width * len(cells), cell_height) diff --git a/kitty/fonts/render.py b/kitty/fonts/render.py index aad1a4f98..5966f7e2e 100644 --- a/kitty/fonts/render.py +++ b/kitty/fonts/render.py @@ -2,9 +2,88 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal -from kitty.constants import isosx +import ctypes +from kitty.constants import isosx +from .box_drawing import render_box_char, is_renderable_box_char if isosx: - from .core_text import set_font_family, render_cell # noqa + from .core_text import set_font_family, render_cell as rc, current_cell # noqa else: - from .freetype import set_font_family, render_cell # noqa + from .freetype import set_font_family, render_cell as rc, current_cell # noqa + + +def add_line(buf, cell_width, position, thickness): + y = position - thickness // 2 + while thickness: + thickness -= 1 + offset = cell_width * y + for x in range(cell_width): + buf[offset + x] = 255 + y += 1 + + +def add_curl(buf, cell_width, position, thickness): + for y in range(position - thickness, position): + for x in range(0, cell_width // 2): + offset = cell_width * y + buf[offset + x] = 255 + for y in range(position, position + thickness): + for x in range(cell_width // 2, cell_width): + offset = cell_width * y + buf[offset + x] = 255 + + +def render_cell(text=' ', bold=False, italic=False, underline=0, strikethrough=False): + CharTexture, cell_width, cell_height, baseline, underline_thickness, underline_position = current_cell() + if is_renderable_box_char(text): + first, second = render_box_char(text, CharTexture(), cell_width, cell_height), None + else: + first, second = rc(text, bold, italic) + + def dl(f, *a): + f(first, cell_width, *a) + if second is not None: + f(second, cell_width, *a) + + if underline: + t = underline_thickness + if underline == 2: + t = max(1, min(cell_height - underline_position - 1, t)) + dl(add_curl if underline == 2 else add_line, underline_position, t) + if strikethrough: + pos = int(0.65 * baseline) + dl(add_line, pos, underline_thickness) + + return first, second + + +def join_cells(cell_width, cell_height, *cells): + dstride = len(cells) * cell_width + ans = (ctypes.c_ubyte * (cell_height * dstride))() + for r in range(cell_height): + soff = r * cell_width + doff = r * dstride + for cnum, cell in enumerate(cells): + doff2 = doff + (cnum * cell_width) + ans[doff2:doff2 + cell_width] = cell[soff:soff + cell_width] + return ans + + +def display_bitmap(data, w, h): + from PIL import Image + img = Image.new('L', (w, h)) + img.putdata(data) + img.show() + + +def test_rendering(text='\'PingšŸ‘a⧽', sz=144, family='Ubuntu Mono for Kovid'): + set_font_family(family, sz) + cells = [] + for c in text: + f, s = render_cell(c, underline=2, strikethrough=True) + cells.append(f) + if s is not None: + cells.append(s) + cell_width, cell_height = current_cell()[1:3] + char_data = join_cells(cell_width, cell_height, *cells) + display_bitmap(char_data, cell_width * len(cells), cell_height) diff --git a/kitty/utils.py b/kitty/utils.py index 161debd72..a539b9032 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -8,6 +8,7 @@ import signal import shlex import subprocess import ctypes +import math from collections import namedtuple from contextlib import contextmanager from functools import lru_cache @@ -30,6 +31,10 @@ def safe_print(*a, **k): pass +def ceil_int(x): + return int(math.ceil(x)) + + @lru_cache(maxsize=2**13) def wcwidth(c: str) -> int: ans = min(2, wcwidth_native(c))