Add support for font features when rendering sample text

This commit is contained in:
Kovid Goyal
2024-05-29 13:23:25 +05:30
parent f10f4e8e5b
commit 0cc8dd28de
6 changed files with 99 additions and 53 deletions

View File

@@ -624,7 +624,7 @@ new(PyTypeObject *type UNUSED, PyObject *args, PyObject *kw) {
} }
PyObject* 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); Py_INCREF(base_descriptor);
return base_descriptor; return base_descriptor;
} }
@@ -742,6 +742,8 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
return ans; return ans;
} }
static void destroy_hb_buffer(hb_buffer_t **x) { if (*x) hb_buffer_destroy(*x); }
static PyObject* static PyObject*
render_sample_text(CTFace *self, PyObject *args) { render_sample_text(CTFace *self, PyObject *args) {
unsigned long canvas_width, canvas_height; 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); canvas_height = MIN(canvas_height, num_of_lines * cell_height);
RAII_PyObject(pbuf, PyBytes_FromStringAndSize(NULL, sizeof(pixel) * canvas_width * canvas_height)); RAII_PyObject(pbuf, PyBytes_FromStringAndSize(NULL, sizeof(pixel) * canvas_width * canvas_height));
if (!pbuf) return NULL; if (!pbuf) return NULL;
RAII_ALLOC(unichar, chars, calloc(sizeof(unichar), num_chars));
if (!chars) return PyErr_NoMemory(); __attribute__((cleanup(destroy_hb_buffer))) hb_buffer_t *hb_buffer = hb_buffer_create();
for (size_t i = 0; i < num_chars; i++) chars[i] = PyUnicode_READ_CHAR(ptext, i); if (!hb_buffer_pre_allocate(hb_buffer, 4*num_chars)) { PyErr_NoMemory(); return NULL; }
RAII_ALLOC(CGSize, local_advances, calloc(sizeof(CGSize), num_chars)); for (size_t n = 0; n < num_chars; n++) {
if (!local_advances) return PyErr_NoMemory(); Py_UCS4 codep = PyUnicode_READ_CHAR(ptext, n);
ensure_render_space(0, 0, num_chars); hb_buffer_add_utf32(hb_buffer, &codep, 1, 0, 1);
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;
} }
unsigned long height = MIN((int)ceil(y) + cell_height, canvas_height); hb_buffer_guess_segment_properties(hb_buffer);
ensure_render_space(canvas_width, height, num_chars); if (!HB_DIRECTION_IS_HORIZONTAL(hb_buffer_get_direction(hb_buffer))) goto end;
render_glyphs(font, canvas_width, height, baseline, num_chars); 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; 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]; p[0] = r; p[1] = g; p[2] = b; p[3] = s[0];
} }
end: end:

View File

@@ -402,7 +402,7 @@ end:
} }
PyObject* 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(); ensure_initialized();
PyObject *p = PyDict_GetItemString(base_descriptor, "path"); PyObject *p = PyDict_GetItemString(base_descriptor, "path");
PyObject *idx = PyDict_GetItemString(base_descriptor, "index"); 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); RAII_PyObject(ans, NULL);
AP(FcPatternAddString, FC_FILE, (const FcChar8*)PyUnicode_AsUTF8(p), "path"); AP(FcPatternAddString, FC_FILE, (const FcChar8*)PyUnicode_AsUTF8(p), "path");
AP(FcPatternAddInteger, FC_INDEX, face_idx, "index"); AP(FcPatternAddInteger, FC_INDEX, face_idx, "index");
AP(FcPatternAddDouble, FC_SIZE, fg->font_sz_in_pts, "size"); AP(FcPatternAddDouble, FC_SIZE, font_sz_in_pts, "size");
AP(FcPatternAddDouble, FC_DPI, (fg->logical_dpi_x + fg->logical_dpi_y) / 2.0, "dpi"); AP(FcPatternAddDouble, FC_DPI, (dpi_x + dpi_y) / 2.0, "dpi");
ans = _fc_match(pat); ans = _fc_match(pat);
FcPatternDestroy(pat); pat = NULL; FcPatternDestroy(pat); pat = NULL;

View File

@@ -283,7 +283,7 @@ sprite_tracker_set_layout(GPUSpriteTracker *sprite_tracker, unsigned int cell_wi
static PyObject* static PyObject*
desc_to_face(PyObject *desc, FONTS_DATA_HANDLE fg) { 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; if (d == NULL) return NULL;
PyObject *ans = face_from_descriptor(d, fg); PyObject *ans = face_from_descriptor(d, fg);
Py_DECREF(d); Py_DECREF(d);
@@ -1829,7 +1829,12 @@ PyTypeObject ParsedFontFeature_Type = {
.tp_call = parsed_font_feature_call, .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[] = { static PyMethodDef module_methods[] = {
METHODB(set_font_data, METH_VARARGS), METHODB(set_font_data, METH_VARARGS),
@@ -1843,6 +1848,7 @@ static PyMethodDef module_methods[] = {
METHODB(current_fonts, METH_VARARGS), METHODB(current_fonts, METH_VARARGS),
METHODB(test_render_line, METH_VARARGS), METHODB(test_render_line, METH_VARARGS),
METHODB(get_fallback_font, METH_VARARGS), METHODB(get_fallback_font, METH_VARARGS),
{"specialize_font_descriptor", (PyCFunction)pyspecialize_font_descriptor, METH_VARARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */ {NULL, NULL, 0, NULL} /* Sentinel */
}; };

View File

@@ -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*); 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); 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* 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_path(const char *path, int index, FONTS_DATA_HANDLE);
PyObject* face_from_descriptor(PyObject*, FONTS_DATA_HANDLE); PyObject* face_from_descriptor(PyObject*, FONTS_DATA_HANDLE);
PyObject* iter_fallback_faces(FONTS_DATA_HANDLE fgh, ssize_t *idx); PyObject* iter_fallback_faces(FONTS_DATA_HANDLE fgh, ssize_t *idx);

View File

@@ -21,7 +21,8 @@ if TYPE_CHECKING:
prefer_variable: bool = False, prefer_variable: bool = False,
) -> Descriptor: ... ) -> Descriptor: ...
def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospaced: bool = True) -> 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_monospace(descriptor: Descriptor) -> bool: ...
def is_variable(descriptor: Descriptor) -> bool: ... def is_variable(descriptor: Descriptor) -> bool: ...
def set_named_style(name: str, font: Descriptor, vd: VariableData) -> 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]: ... def get_axis_values(font: Descriptor, vd: VariableData) -> Dict[str, float]: ...
else: else:
FontCollectionMapType = FontMap = None FontCollectionMapType = FontMap = None
from kitty.fast_data_types import specialize_font_descriptor
if is_macos: if is_macos:
from kitty.fast_data_types import CTFace as Face from kitty.fast_data_types import CTFace as Face
from kitty.fonts.core_text import ( from kitty.fonts.core_text import (
@@ -55,7 +57,10 @@ else:
set_axis_values, set_axis_values,
set_named_style, 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] = {} cache_for_variable_data_by_path: Dict[str, VariableData] = {}

View File

@@ -316,7 +316,9 @@ new(PyTypeObject *type UNUSED, PyObject *args, PyObject *kw) {
static char *kwds[] = {"descriptor", "path", "index", NULL}; static char *kwds[] = {"descriptor", "path", "index", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Osi", kwds, &descriptor, &path, &index)) return 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); if (path) return face_from_path(path, index, NULL);
PyErr_SetString(PyExc_TypeError, "Must specify either path or descriptor"); PyErr_SetString(PyExc_TypeError, "Must specify either path or descriptor");
return NULL; return NULL;
@@ -958,9 +960,11 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
return ans; return ans;
} }
static void destroy_hb_buffer(hb_buffer_t **x) { if (*x) hb_buffer_destroy(*x); }
static PyObject* static PyObject*
render_sample_text(Face *self, PyObject *args) { 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; unsigned long fg = 0xffffff;
PyObject *ptext; PyObject *ptext;
if (!PyArg_ParseTuple(args, "Ukk|k", &ptext, &canvas_width, &canvas_height, &fg)) return NULL; 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); canvas_height = MIN(canvas_height, num_of_lines * cell_height);
RAII_PyObject(pbuf, PyBytes_FromStringAndSize(NULL, sizeof(pixel) * canvas_width * canvas_height)); RAII_PyObject(pbuf, PyBytes_FromStringAndSize(NULL, sizeof(pixel) * canvas_width * canvas_height));
if (!pbuf) return NULL; if (!pbuf) return NULL;
pixel *canvas = (pixel*)PyBytes_AS_STRING(pbuf); memset(PyBytes_AS_STRING(pbuf), 0, PyBytes_GET_SIZE(pbuf));
memset(canvas, 0, PyBytes_GET_SIZE(pbuf));
if (cell_width > canvas_width) goto end;
__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++) { 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_y += cell_height;
pen_x = 0; pen_x = 0;
if (pen_y >= canvas_height) break;
} }
if (pen_y + cell_height > canvas_height) break; size_t x = (size_t)round(pen_x + (float)positions[i].x_offset / 64.0f);
Py_UCS4 ch = PyUnicode_READ_CHAR(ptext, n); size_t y = (size_t)round(pen_y + (float)positions[i].y_offset / 64.0f);
FT_UInt glyph_index = FT_Get_Char_Index(self->face, ch); pen_x += advance;
if (!glyph_index) continue; if ((error = FT_Load_Glyph(self->face, info[i].codepoint, load_flags))) continue;
int error = FT_Load_Glyph(self->face, glyph_index, FT_LOAD_DEFAULT); if ((error = FT_Render_Glyph(self->face->glyph, FT_RENDER_MODE_NORMAL))) continue;
if (error) continue;
error = FT_Render_Glyph(self->face->glyph, FT_RENDER_MODE_NORMAL);
if (error) continue;
FT_Bitmap *bitmap = &self->face->glyph->bitmap; FT_Bitmap *bitmap = &self->face->glyph->bitmap;
ProcessedBitmap pbm = EMPTY_PBM; ProcessedBitmap pbm = EMPTY_PBM;
populate_processed_bitmap(self->face->glyph, bitmap, &pbm, false); 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); place_bitmap_in_canvas(canvas, &pbm, canvas_width, canvas_height, 0, 0, baseline, 99999, fg, x, y);
pen_x += self->face->glyph->advance.x >> 6;
} }
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]; 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; p[0] = r; p[1] = g; p[2] = b; p[3] = a;
} }