Reuse most of the code from the draw_complex_glyph code path for drawing
simple glyphs. Avoids overhead of harfbuzz for simple glyphs.
This commit is contained in:
Kovid Goyal 2017-10-28 10:17:27 +05:30
parent 0652fa1696
commit d8629a2d5b
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 53 additions and 286 deletions

View File

@ -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

View File

@ -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);