DRYer
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:
parent
0652fa1696
commit
d8629a2d5b
@ -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
|
||||
|
||||
144
kitty/freetype.c
144
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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user