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

View File

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

View File

@@ -283,7 +283,7 @@ sprite_tracker_set_layout(GPUSpriteTracker *sprite_tracker, unsigned int cell_wi
static PyObject*
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;
PyObject *ans = face_from_descriptor(d, fg);
Py_DECREF(d);
@@ -1829,7 +1829,12 @@ PyTypeObject ParsedFontFeature_Type = {
.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[] = {
METHODB(set_font_data, METH_VARARGS),
@@ -1843,6 +1848,7 @@ static PyMethodDef module_methods[] = {
METHODB(current_fonts, METH_VARARGS),
METHODB(test_render_line, METH_VARARGS),
METHODB(get_fallback_font, METH_VARARGS),
{"specialize_font_descriptor", (PyCFunction)pyspecialize_font_descriptor, METH_VARARGS, ""},
{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*);
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* 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_descriptor(PyObject*, FONTS_DATA_HANDLE);
PyObject* iter_fallback_faces(FONTS_DATA_HANDLE fgh, ssize_t *idx);

View File

@@ -21,7 +21,8 @@ if TYPE_CHECKING:
prefer_variable: bool = False,
) -> 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_variable(descriptor: Descriptor) -> 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]: ...
else:
FontCollectionMapType = FontMap = None
from kitty.fast_data_types import specialize_font_descriptor
if is_macos:
from kitty.fast_data_types import CTFace as Face
from kitty.fonts.core_text import (
@@ -55,7 +57,10 @@ else:
set_axis_values,
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] = {}

View File

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