Skip to content

Commit

Permalink
work on passing font features via font specs
Browse files Browse the repository at this point in the history
  • Loading branch information
kovidgoyal committed May 24, 2024
1 parent 00c0dec commit 15933a6
Show file tree
Hide file tree
Showing 14 changed files with 269 additions and 112 deletions.
15 changes: 11 additions & 4 deletions kitty/core_text.m
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
CTFontRef ct_font;
hb_font_t *hb_font;
PyObject *family_name, *full_name, *postscript_name, *path, *name_lookup_table;
FontFeatures font_features;
} CTFace;
PyTypeObject CTFace_Type;
static CTFontRef window_title_font = nil;
Expand Down Expand Up @@ -87,7 +88,7 @@


static CTFace*
ct_face(CTFontRef font) {
ct_face(CTFontRef font, PyObject *features) {
CTFace *self = (CTFace *)CTFace_Type.tp_alloc(&CTFace_Type, 0);
if (self) {
init_face(self, font);
Expand All @@ -96,6 +97,9 @@
self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->ct_font), true);
self->path = get_path_for_font(self->ct_font);
if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL || self->path == NULL) { Py_CLEAR(self); }
else {
if (!create_features_for_face(postscript_name_for_face((PyObject*)self), features, &self->font_features)) { Py_CLEAR(self); }
}
}
return self;
}
Expand All @@ -106,6 +110,7 @@
if (self->ct_font) CFRelease(self->ct_font);
self->hb_font = NULL;
self->ct_font = NULL;
free(self->font_features.features);
Py_CLEAR(self->family_name); Py_CLEAR(self->full_name); Py_CLEAR(self->postscript_name); Py_CLEAR(self->path);
Py_CLEAR(self->name_lookup_table);
Py_TYPE(self)->tp_free((PyObject*)self);
Expand All @@ -126,6 +131,8 @@
return (((uint32_t)bytes[0]) << 24) | (((uint32_t)bytes[1]) << 16) | (((uint32_t)bytes[2]) << 8) | bytes[3];
}

FontFeatures*
features_for_face(PyObject *s) { return &((CTFace*)s)->font_features; }

static void
add_variation_pair(const void *key_, const void *value_, void *ctx) {
Expand Down Expand Up @@ -444,7 +451,7 @@ static CTFontRef nerd_font(CGFloat sz) {
break;
}
}
return ans ? ans : (PyObject*)ct_face(new_font);
return ans ? ans : (PyObject*)ct_face(new_font, NULL);
}

unsigned int
Expand Down Expand Up @@ -590,7 +597,7 @@ static CTFontRef nerd_font(CGFloat sz) {
if (!desc) return NULL;
RAII_CoreFoundation(CTFontRef, font, CTFontCreateWithFontDescriptor(desc, fg ? scaled_point_sz(fg) : 12, NULL));
if (!font) { PyErr_SetString(PyExc_ValueError, "Failed to create CTFont object"); return NULL; }
return (PyObject*) ct_face(font);
return (PyObject*) ct_face(font, PyDict_GetItemString(descriptor, "features"));
}

PyObject*
Expand All @@ -600,7 +607,7 @@ static CTFontRef nerd_font(CGFloat sz) {
RAII_CoreFoundation(CGDataProviderRef, dp, CGDataProviderCreateWithURL(url));
RAII_CoreFoundation(CGFontRef, cg_font, CGFontCreateWithDataProvider(dp));
RAII_CoreFoundation(CTFontRef, ct_font, CTFontCreateWithGraphicsFont(cg_font, 0.0, NULL, NULL));
return (PyObject*) ct_face(ct_font);
return (PyObject*) ct_face(ct_font, NULL);
}

static PyObject*
Expand Down
13 changes: 7 additions & 6 deletions kitty/fast_data_types.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ from ctypes import Array, c_ubyte
from typing import Any, Callable, Dict, Iterator, List, Literal, NewType, Optional, Tuple, TypedDict, Union, overload

from kitty.boss import Boss
from kitty.fonts import FontFeature, VariableData
from kitty.fonts import VariableData
from kitty.fonts.render import FontObject
from kitty.marks import MarkerFunc
from kitty.options.types import Options
Expand Down Expand Up @@ -400,6 +400,7 @@ class FontConfigPattern(TypedDict):
# The following two are used by C code to get a face from the pattern
named_style: NotRequired[int]
axes: NotRequired[Tuple[float, ...]]
features: NotRequired[Tuple[ParsedFontFeature, ...]]


def fc_list(spacing: int = -1, allow_bitmapped_fonts: bool = False, only_variable: bool = False) -> Tuple[FontConfigPattern, ...]:
Expand Down Expand Up @@ -464,6 +465,7 @@ class CoreTextFont(TypedDict):

# The following is used by C code to get a face from the pattern
axis_map: NotRequired[Dict[str, float]]
features: NotRequired[Tuple[ParsedFontFeature, ...]]


class CTFace:
Expand All @@ -482,6 +484,10 @@ def coretext_all_fonts(monospaced_only: bool) -> Tuple[CoreTextFont, ...]:
pass


class ParsedFontFeature:
def __init__(self, s: str): ...


def add_timer(
callback: Callable[[Optional[int]], None],
interval: float,
Expand Down Expand Up @@ -600,10 +606,6 @@ def get_options() -> Options:
pass


def parse_font_feature(ff: str) -> bytes:
pass


def glfw_primary_monitor_size() -> Tuple[int, int]:
pass

Expand Down Expand Up @@ -1071,7 +1073,6 @@ def set_font_data(
descriptor_for_idx: Callable[[int], Tuple[FontObject, bool, bool]],
bold: int, italic: int, bold_italic: int, num_symbol_fonts: int,
symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float,
font_feature_settings: Dict[str, Tuple[FontFeature, ...]],
narrow_symbols: Tuple[Tuple[int, int, int], ...],
) -> None:
pass
Expand Down
167 changes: 119 additions & 48 deletions kitty/fonts.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

#include "fonts.h"
#include "pyport.h"
#include "state.h"
#include "emoji.h"
#include "unicode-data.h"
Expand Down Expand Up @@ -43,7 +44,6 @@ static hb_feature_t hb_features[3] = {{0}};
static char_type shape_buffer[4096] = {0};
static size_t max_texture_size = 1024, max_array_len = 1024;
typedef enum { LIGA_FEATURE, DLIG_FEATURE, CALT_FEATURE } HBFeature;
static PyObject* font_feature_settings = NULL;

typedef struct {
char_type left, right;
Expand Down Expand Up @@ -290,43 +290,48 @@ desc_to_face(PyObject *desc, FONTS_DATA_HANDLE fg) {
return ans;
}

static bool
init_font(Font *f, PyObject *face, bool bold, bool italic, bool emoji_presentation) {
f->face = face; Py_INCREF(f->face);
f->bold = bold; f->italic = italic; f->emoji_presentation = emoji_presentation;
f->num_ffs_hb_features = 0;
const char *psname = postscript_name_for_face(face);
if (font_feature_settings != NULL){
PyObject* o = PyDict_GetItemString(font_feature_settings, psname);
if (o != NULL && PyTuple_Check(o)) {
Py_ssize_t len = PyTuple_GET_SIZE(o);
if (len > 0) {
f->num_ffs_hb_features = len + 1;
f->ffs_hb_features = calloc(f->num_ffs_hb_features, sizeof(hb_feature_t));
if (!f->ffs_hb_features) return false;
for (Py_ssize_t i = 0; i < len; i++) {
PyObject* parsed = PyObject_GetAttrString(PyTuple_GET_ITEM(o, i), "parsed");
if (parsed) {
memcpy(f->ffs_hb_features + i, PyBytes_AS_STRING(parsed), sizeof(hb_feature_t));
Py_DECREF(parsed);
}
}
memcpy(f->ffs_hb_features + len, &hb_features[CALT_FEATURE], sizeof(hb_feature_t));
}
bool
create_features_for_face(const char *psname, PyObject *features, FontFeatures *output) {
size_t count_from_descriptor = features ? PyTuple_GET_SIZE(features): 0;
__typeof__(OPT(font_features).entries) from_opts = NULL;
if (psname) {
for (size_t i = 0; i < OPT(font_features).num && !from_opts; i++) {
__typeof__(OPT(font_features).entries) e = OPT(font_features).entries + i;
if (strcmp(e->psname, psname) == 0) from_opts = e;
}
}
if (!f->num_ffs_hb_features) {
f->ffs_hb_features = calloc(4, sizeof(hb_feature_t));
if (!f->ffs_hb_features) return false;
size_t count_from_opts = from_opts ? from_opts->num : 0;
output->features = calloc(MAX(2u, count_from_opts + count_from_descriptor), sizeof(output->features[0]));
if (!output->features) { PyErr_NoMemory(); return false; }
for (size_t i = 0; i < count_from_opts; i++) {
output->features[output->count++] = from_opts->features[i];
}
for (size_t i = 0; i < count_from_descriptor; i++) {
ParsedFontFeature *f = (ParsedFontFeature*)PyTuple_GET_ITEM(features, i);
output->features[output->count++] = f->feature;
}
if (!output->count) {
if (strstr(psname, "NimbusMonoPS-") == psname) {
memcpy(f->ffs_hb_features + f->num_ffs_hb_features++, &hb_features[LIGA_FEATURE], sizeof(hb_feature_t));
memcpy(f->ffs_hb_features + f->num_ffs_hb_features++, &hb_features[DLIG_FEATURE], sizeof(hb_feature_t));
output->features[output->count++] = hb_features[LIGA_FEATURE];
output->features[output->count++] = hb_features[DLIG_FEATURE];
}
memcpy(f->ffs_hb_features + f->num_ffs_hb_features++, &hb_features[CALT_FEATURE], sizeof(hb_feature_t));
}
return true;
}

static bool
init_font(Font *f, PyObject *face, bool bold, bool italic, bool emoji_presentation) {
f->face = face; Py_INCREF(f->face);
f->bold = bold; f->italic = italic; f->emoji_presentation = emoji_presentation;
const FontFeatures *features = features_for_face(face);
f->ffs_hb_features = calloc(1 + features->count, sizeof(hb_feature_t));
if (!f->ffs_hb_features) { PyErr_NoMemory(); return false; }
f->num_ffs_hb_features = features->count;
memcpy(f->ffs_hb_features, features->features, sizeof(hb_feature_t) * features->count);
memcpy(f->ffs_hb_features + f->num_ffs_hb_features++, &hb_features[CALT_FEATURE], sizeof(hb_feature_t));
return true;
}

static void
free_font_groups(void) {
if (font_groups) {
Expand Down Expand Up @@ -1433,12 +1438,12 @@ set_symbol_maps(SymbolMap **maps, size_t *num, const PyObject *sm) {
static PyObject*
set_font_data(PyObject UNUSED *m, PyObject *args) {
PyObject *sm, *ns;
Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); Py_CLEAR(font_feature_settings);
if (!PyArg_ParseTuple(args, "OOOIIIIO!dOO!",
Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx);
if (!PyArg_ParseTuple(args, "OOOIIIIO!dO!",
&box_drawing_function, &prerender_function, &descriptor_for_idx,
&descriptor_indices.bold, &descriptor_indices.italic, &descriptor_indices.bi, &descriptor_indices.num_symbol_fonts,
&PyTuple_Type, &sm, &OPT(font_size), &font_feature_settings, &PyTuple_Type, &ns)) return NULL;
Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); Py_INCREF(font_feature_settings);
&PyTuple_Type, &sm, &OPT(font_size), &PyTuple_Type, &ns)) return NULL;
Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx);
free_font_groups();
clear_symbol_maps();
set_symbol_maps(&symbol_maps, &num_symbol_maps, sm);
Expand Down Expand Up @@ -1539,7 +1544,6 @@ finalize(void) {
Py_CLEAR(box_drawing_function);
Py_CLEAR(prerender_function);
Py_CLEAR(descriptor_for_idx);
Py_CLEAR(font_feature_settings);
free_font_groups();
free(ligature_types);
if (harfbuzz_buffer) { hb_buffer_destroy(harfbuzz_buffer); harfbuzz_buffer = NULL; }
Expand Down Expand Up @@ -1710,26 +1714,89 @@ free_font_data(PyObject *self UNUSED, PyObject *args UNUSED) {
Py_RETURN_NONE;
}

static PyObject*
parse_font_feature(PyObject *self UNUSED, PyObject *feature) {
if (!PyUnicode_Check(feature)) {
PyErr_SetString(PyExc_TypeError, "feature must be a unicode object");
return NULL;
static PyObject *
parsed_font_feature_new(PyTypeObject *type, PyObject *args, PyObject *kwds UNUSED) {
const char *s;
if (!PyArg_ParseTuple(args, "s", &s)) return NULL;
ParsedFontFeature *self = (ParsedFontFeature *)type->tp_alloc(type, 0);
if (self != NULL) {
if (!hb_feature_from_string(s, -1, &self->feature)) {
PyErr_Format(PyExc_ValueError, "%s is not a valid font feature", s);
Py_CLEAR(self);
}
}
PyObject *ans = PyBytes_FromStringAndSize(NULL, sizeof(hb_feature_t));
if (!ans) return NULL;
if (!hb_feature_from_string(PyUnicode_AsUTF8(feature), -1, (hb_feature_t*)PyBytes_AS_STRING(ans))) {
Py_CLEAR(ans);
PyErr_Format(PyExc_ValueError, "%U is not a valid font feature", feature);
return NULL;
return (PyObject*) self;
}

static PyObject*
parsed_font_feature_str(PyObject *self_) {
char buf[128];
hb_feature_to_string(&((ParsedFontFeature*)self_)->feature, buf, arraysz(buf));
return PyUnicode_FromString(buf);
}

static PyObject*
parsed_font_feature_repr(PyObject *self_) {
RAII_PyObject(s, parsed_font_feature_str(self_));
return s ? PyObject_Repr(s) : NULL;
}


PyTypeObject ParsedFontFeature_Type;

static PyObject*
parsed_font_feature_cmp(PyObject *self, PyObject *other, int op) {
if (op != Py_EQ && op != Py_NE) return Py_NotImplemented;
if (!PyObject_TypeCheck(other, &ParsedFontFeature_Type)) {
if (op == Py_EQ) Py_RETURN_FALSE;
Py_RETURN_TRUE;
}
ParsedFontFeature *a = (ParsedFontFeature*)self, *b = (ParsedFontFeature*)other;
PyObject *ret = Py_True;
if (memcmp(&a->feature, &b->feature, sizeof(hb_feature_t)) == 0) {
if (op == Py_NE) ret = Py_False;
} else {
if (op == Py_EQ) ret = Py_False;
}
return ans;
Py_INCREF(ret); return ret;
}

static Py_hash_t
parsed_font_feature_hash(PyObject *s) {
ParsedFontFeature *self = (ParsedFontFeature*)s;
if (self->hash_computed) return self->hashval;
self->hash_computed = true;
HASH_FUNCTION(&self->feature, sizeof(hb_feature_t), self->hashval);
return self->hashval;
}

static PyObject*
parsed_font_feature_call(PyObject *s, PyObject *args, PyObject *kwargs UNUSED) {
ParsedFontFeature *self = (ParsedFontFeature*)s;
void *dest = PyLong_AsVoidPtr(args);
memcpy(dest, &self->feature, sizeof(hb_feature_t));
Py_RETURN_NONE;
}

PyTypeObject ParsedFontFeature_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "kitty.fast_data_types.ParsedFontFeature",
.tp_basicsize = sizeof(ParsedFontFeature),
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "FontFeature",
.tp_new = parsed_font_feature_new,
.tp_str = parsed_font_feature_str,
.tp_repr = parsed_font_feature_repr,
.tp_richcompare = parsed_font_feature_cmp,
.tp_hash = parsed_font_feature_hash,
.tp_call = parsed_font_feature_call,
};



static PyMethodDef module_methods[] = {
METHODB(set_font_data, METH_VARARGS),
METHODB(free_font_data, METH_NOARGS),
METHODB(parse_font_feature, METH_O),
METHODB(create_test_font_group, METH_VARARGS),
METHODB(sprite_map_set_layout, METH_VARARGS),
METHODB(test_sprite_position_for, METH_VARARGS),
Expand Down Expand Up @@ -1757,5 +1824,9 @@ init_fonts(PyObject *module) {
create_feature("-calt", CALT_FEATURE);
#undef create_feature
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
if (PyType_Ready(&ParsedFontFeature_Type) < 0) return 0;
if (PyModule_AddObject(module, "ParsedFontFeature", (PyObject *)&ParsedFontFeature_Type) != 0) return 0;
Py_INCREF(&ParsedFontFeature_Type);

return true;
}
16 changes: 16 additions & 0 deletions kitty/fonts.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ typedef struct {
size_t width, height;
} StringCanvas;

typedef struct FontFeatures {
size_t count;
hb_feature_t *features;
} FontFeatures;

typedef struct ParsedFontFeature {
PyObject_HEAD

hb_feature_t feature;
Py_hash_t hashval;
bool hash_computed;
} ParsedFontFeature;


// API that font backends need to implement
unsigned int glyph_id_for_codepoint(PyObject *, char_type);
int get_glyph_width(PyObject *, glyph_index);
Expand Down Expand Up @@ -54,6 +68,8 @@ bool
read_STAT_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output);
bool
read_features_from_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output);
FontFeatures* features_for_face(PyObject *);
bool create_features_for_face(const char* psname, PyObject *features, FontFeatures* output);

static inline void
right_shift_canvas(pixel *canvas, size_t width, size_t height, size_t amt) {
Expand Down
Loading

0 comments on commit 15933a6

Please sign in to comment.