macOS: Fix selecting fonts using full names (including sub-family) not working

Fixes #83
This commit is contained in:
Kovid Goyal 2017-08-24 19:16:12 +05:30
parent 58644e2b37
commit 697a7b78b3
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 124 additions and 37 deletions

View File

@ -22,18 +22,19 @@ typedef struct {
unsigned int units_per_em; unsigned int units_per_em;
float ascent, descent, leading, underline_position, underline_thickness, point_sz, scaled_point_sz; float ascent, descent, leading, underline_position, underline_thickness, point_sz, scaled_point_sz;
CTFontRef font; CTFontRef font;
PyObject *family_name, *full_name, *postscript_name; PyObject *family_name, *full_name, *postscript_name, *path;
} Face; } Face;
static PyObject* static inline PyObject*
convert_cfstring(CFStringRef src) { convert_cfstring(CFStringRef src, int free_src) {
#define SZ 2048 #define SZ 2048
static char buf[SZ+2] = {0}; static char buf[SZ+2] = {0};
const char *p = CFStringGetCStringPtr(src, kCFStringEncodingUTF8); PyObject *ans = NULL;
if (p != NULL) return PyUnicode_FromString(buf); if(!CFStringGetCString(src, buf, SZ, kCFStringEncodingUTF8)) PyErr_SetString(PyExc_ValueError, "Failed to convert CFString");
if(!CFStringGetCString(src, buf, SZ, kCFStringEncodingUTF8)) { PyErr_SetString(PyExc_ValueError, "Failed to convert CFString"); return NULL; } else ans = PyUnicode_FromString(buf);
return PyUnicode_FromString(buf); if (free_src) CFRelease(src);
return ans;
#undef SZ #undef SZ
} }
@ -70,6 +71,34 @@ font_descriptor_to_python(CTFontDescriptorRef descriptor) {
return ans; return ans;
} }
static CTFontDescriptorRef
font_descriptor_from_python(PyObject *src) {
CTFontSymbolicTraits symbolic_traits = 0;
NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
PyObject *t = PyDict_GetItemString(src, "traits");
if (t == NULL) {
symbolic_traits = (
(PyDict_GetItemString(src, "bold") == Py_True) ? kCTFontBoldTrait : 0 |
(PyDict_GetItemString(src, "italic") == Py_True) ? kCTFontItalicTrait : 0 |
(PyDict_GetItemString(src, "monospace") == Py_True) ? kCTFontMonoSpaceTrait : 0);
} else {
symbolic_traits = PyLong_AsUnsignedLong(t);
}
NSDictionary *traits = @{(id)kCTFontSymbolicTrait:[NSNumber numberWithUnsignedInt:symbolic_traits]};
attrs[(id)kCTFontTraitsAttribute] = traits;
#define SET(x, attr) \
t = PyDict_GetItemString(src, #x); \
if (t) attrs[(id)attr] = [NSString stringWithUTF8String:PyUnicode_AsUTF8(t)];
SET(family, kCTFontFamilyNameAttribute);
SET(style, kCTFontStyleNameAttribute);
SET(postscript_name, kCTFontNameAttribute);
#undef SET
return CTFontDescriptorCreateWithAttributes((CFDictionaryRef) attrs);
}
PyObject* PyObject*
coretext_all_fonts(PyObject UNUSED *_self) { coretext_all_fonts(PyObject UNUSED *_self) {
static CTFontCollectionRef collection = NULL; static CTFontCollectionRef collection = NULL;
@ -89,23 +118,17 @@ coretext_all_fonts(PyObject UNUSED *_self) {
static PyObject* static PyObject*
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
Face *self; Face *self;
int bold, italic, monospace; PyObject *descriptor;
char *cfamily;
float point_sz, dpi; float point_sz, dpi;
if(!PyArg_ParseTuple(args, "spppff", &cfamily, &bold, &italic, &monospace, &point_sz, &dpi)) return NULL; if(!PyArg_ParseTuple(args, "Off", &descriptor, &point_sz, &dpi)) return NULL;
NSString *family = [[NSString alloc] initWithCString:cfamily encoding:NSUTF8StringEncoding];
if (family == NULL) return PyErr_NoMemory();
self = (Face *)type->tp_alloc(type, 0); self = (Face *)type->tp_alloc(type, 0);
if (self) { if (self) {
CTFontSymbolicTraits symbolic_traits = (bold ? kCTFontBoldTrait : 0) | (italic ? kCTFontItalicTrait : 0) | (monospace ? kCTFontMonoSpaceTrait : 0); CTFontDescriptorRef desc = font_descriptor_from_python(descriptor);
NSDictionary *font_traits = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:symbolic_traits] forKey:(NSString *)kCTFontSymbolicTrait]; if (desc) {
NSDictionary *font_attributes = [NSDictionary dictionaryWithObjectsAndKeys:family, kCTFontFamilyNameAttribute, font_traits, kCTFontTraitsAttribute, nil];
CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)font_attributes);
if (descriptor) {
self->point_sz = point_sz; self->point_sz = point_sz;
self->scaled_point_sz = (dpi / 72.0) * point_sz; self->scaled_point_sz = (dpi / 72.0) * point_sz;
self->font = CTFontCreateWithFontDescriptor(descriptor, self->scaled_point_sz, NULL); self->font = CTFontCreateWithFontDescriptor(desc, self->scaled_point_sz, NULL);
CFRelease(descriptor); CFRelease(desc);
if (!self->font) { Py_CLEAR(self); PyErr_SetString(PyExc_ValueError, "Failed to create CTFont object"); } if (!self->font) { Py_CLEAR(self); PyErr_SetString(PyExc_ValueError, "Failed to create CTFont object"); }
else { else {
self->units_per_em = CTFontGetUnitsPerEm(self->font); self->units_per_em = CTFontGetUnitsPerEm(self->font);
@ -115,17 +138,19 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
self->underline_position = CTFontGetUnderlinePosition(self->font); self->underline_position = CTFontGetUnderlinePosition(self->font);
self->underline_thickness = CTFontGetUnderlineThickness(self->font); self->underline_thickness = CTFontGetUnderlineThickness(self->font);
self->scaled_point_sz = CTFontGetSize(self->font); self->scaled_point_sz = CTFontGetSize(self->font);
self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font)); self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font), 1);
self->full_name = convert_cfstring(CTFontCopyFullName(self->font)); self->full_name = convert_cfstring(CTFontCopyFullName(self->font), 1);
self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->font)); self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->font), 1);
if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL) { Py_CLEAR(self); } NSURL *url = (NSURL*)CTFontCopyAttribute(self->font, kCTFontURLAttribute);
self->path = PyUnicode_FromString([[url path] UTF8String]);
[url release];
if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL || self->path == NULL) { Py_CLEAR(self); }
} }
} else { } else {
Py_CLEAR(self); Py_CLEAR(self);
PyErr_NoMemory(); PyErr_NoMemory();
} }
} }
[ family release ];
return (PyObject*)self; return (PyObject*)self;
} }
@ -133,7 +158,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
static void static void
dealloc(Face* self) { dealloc(Face* self) {
if (self->font) CFRelease(self->font); if (self->font) CFRelease(self->font);
Py_CLEAR(self->family_name); Py_CLEAR(self->full_name); Py_CLEAR(self->postscript_name); Py_CLEAR(self->family_name); Py_CLEAR(self->full_name); Py_CLEAR(self->postscript_name); Py_CLEAR(self->path);
Py_TYPE(self)->tp_free((PyObject*)self); Py_TYPE(self)->tp_free((PyObject*)self);
} }
@ -248,8 +273,8 @@ repr(Face *self) {
snprintf(buf, sizeof(buf)/sizeof(buf[0]), "ascent=%.1f, descent=%.1f, leading=%.1f, point_sz=%.1f, scaled_point_sz=%.1f, underline_position=%.1f underline_thickness=%.1f", snprintf(buf, sizeof(buf)/sizeof(buf[0]), "ascent=%.1f, descent=%.1f, leading=%.1f, point_sz=%.1f, scaled_point_sz=%.1f, underline_position=%.1f underline_thickness=%.1f",
(self->ascent), (self->descent), (self->leading), (self->point_sz), (self->scaled_point_sz), (self->underline_position), (self->underline_thickness)); (self->ascent), (self->descent), (self->leading), (self->point_sz), (self->scaled_point_sz), (self->underline_position), (self->underline_thickness));
return PyUnicode_FromFormat( return PyUnicode_FromFormat(
"Face(family=%U, full_name=%U, postscript_name=%U, units_per_em=%u, %s)", "Face(family=%U, full_name=%U, postscript_name=%U, path=%U, units_per_em=%u, %s)",
self->family_name, self->full_name, self->postscript_name, self->units_per_em, buf self->family_name, self->full_name, self->postscript_name, self->path, self->units_per_em, buf
); );
} }
@ -267,6 +292,7 @@ static PyMemberDef members[] = {
MEM(underline_position, T_FLOAT), MEM(underline_position, T_FLOAT),
MEM(underline_thickness, T_FLOAT), MEM(underline_thickness, T_FLOAT),
MEM(family_name, T_OBJECT), MEM(family_name, T_OBJECT),
MEM(path, T_OBJECT),
MEM(full_name, T_OBJECT), MEM(full_name, T_OBJECT),
MEM(postscript_name, T_OBJECT), MEM(postscript_name, T_OBJECT),
{NULL} /* Sentinel */ {NULL} /* Sentinel */

View File

@ -3,18 +3,67 @@
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import ctypes import ctypes
from kitty.fast_data_types import CTFace as Face import re
from kitty.utils import get_logical_dpi, wcwidth, ceil_int
from kitty.fast_data_types import CTFace as Face, coretext_all_fonts
from kitty.utils import ceil_int, get_logical_dpi, wcwidth
main_font = {} main_font = {}
symbol_map = {} symbol_map = {}
cell_width = cell_height = baseline = CellTexture = WideCellTexture = underline_thickness = underline_position = None cell_width = cell_height = baseline = CellTexture = WideCellTexture = underline_thickness = underline_position = None
attr_map = {(False, False): 'font_family', (True, False): 'bold_font', (False, True): 'italic_font', (True, True): 'bold_italic_font'}
def install_symbol_map(val, font_size, dpi): def create_font_map(all_fonts):
ans = {'family_map': {}, 'ps_map': {}, 'full_map': {}}
for x in all_fonts:
f = (x['family'] or '').lower()
s = (x['style'] or '').lower()
ps = (x['postscript_name'] or '').lower()
ans['family_map'].setdefault(f, []).append(x)
ans['ps_map'].setdefault(ps, []).append(x)
ans['full_map'].setdefault(f + ' ' + s, []).append(x)
return ans
def find_best_match(font_map, family, bold, italic):
q = re.sub(r'\s+', ' ', family.lower())
def matches(candidate, monospace):
return candidate['bold'] == bold and candidate['italic'] == italic and candidate['monospace'] == monospace
for monospace in (True, False):
for selector in 'ps_map full_map family_map'.split():
candidates = font_map[selector].get(q, ())
if candidates:
for candidate in candidates:
if matches(candidate, monospace):
return candidate
if selector != 'family_map':
return candidates[0]
def get_face(font_map, family, main_family, font_size, dpi, bold=False, italic=False):
def resolve_family(f):
if f.lower() == 'monospace':
f = 'Menlo'
if (bold or italic) and f == 'auto':
f = main_family
return f
family = resolve_family(family)
descriptor = find_best_match(font_map, family, bold, italic) or {
'monospace': True,
'bold': bold,
'italic': italic,
'family': family
}
return Face(descriptor, font_size, dpi)
def install_symbol_map(all_fonts, val, font_size, dpi):
global symbol_map global symbol_map
symbol_map = {} symbol_map = {}
family_map = {f: Face(f, False, False, False, font_size, dpi) for f in set(val.values())} family_map = {f: get_face(all_fonts, f, 'Menlo', font_size, dpi) for f in set(val.values())}
for ch, family in val.items(): for ch, family in val.items():
symbol_map[ch] = family_map[family] symbol_map[ch] = family_map[family]
@ -28,7 +77,11 @@ def set_font_family(opts, override_font_size=None, ignore_dpi_failure=False):
raise raise
dpi = (72, 72) # Happens when running via develop() in an ssh session dpi = (72, 72) # Happens when running via develop() in an ssh session
dpi = sum(dpi) / 2.0 dpi = sum(dpi) / 2.0
attr_map = {(False, False): 'font_family', (True, False): 'bold_font', (False, True): 'italic_font', (True, True): 'bold_italic_font'} font_size = override_font_size or opts.font_size
all_fonts = create_font_map(coretext_all_fonts())
for (bold, italic), attr in attr_map.items():
main_font[(bold, italic)] = get_face(all_fonts, getattr(opts, attr), opts.font_family, font_size, dpi, bold, italic)
def get_family(bold, italic): def get_family(bold, italic):
ans = getattr(opts, attr_map[(bold, italic)]) ans = getattr(opts, attr_map[(bold, italic)])
@ -37,12 +90,8 @@ def set_font_family(opts, override_font_size=None, ignore_dpi_failure=False):
if ans == 'auto' and (bold or italic): if ans == 'auto' and (bold or italic):
ans = get_family(False, False) ans = get_family(False, False)
return ans return ans
font_size = override_font_size or opts.font_size
for bold in (False, True): install_symbol_map(all_fonts, opts.symbol_map, font_size, dpi)
for italic in (False, True):
main_font[(bold, italic)] = Face(get_family(bold, italic), bold, italic, True, font_size, dpi)
install_symbol_map(opts.symbol_map, font_size, dpi)
mf = main_font[(False, False)] mf = main_font[(False, False)]
cell_width, cell_height = mf.cell_size() cell_width, cell_height = mf.cell_size()
CellTexture = ctypes.c_ubyte * (cell_width * cell_height) CellTexture = ctypes.c_ubyte * (cell_width * cell_height)
@ -53,6 +102,12 @@ def set_font_family(opts, override_font_size=None, ignore_dpi_failure=False):
return cell_width, cell_height return cell_width, cell_height
def test_font_matching(name='Menlo', bold=False, italic=False, dpi=72.0, font_size=11.0):
all_fonts = create_font_map(coretext_all_fonts())
face = get_face(all_fonts, name, 'Menlo', font_size, dpi, bold, italic)
return face
def current_cell(): def current_cell():
return CellTexture, cell_width, cell_height, baseline, underline_thickness, underline_position return CellTexture, cell_width, cell_height, baseline, underline_thickness, underline_position

View File

@ -7,6 +7,12 @@
# font_family Operator Mono Book # font_family Operator Mono Book
# bold_font Operator Mono Thick # bold_font Operator Mono Thick
# bold_italic_font Operator Mono Medium # bold_italic_font Operator Mono Medium
# or
# font_family SF Mono Medium
# bold_font SF Mono Semibold
# bold_italic_font SF Mono Semibold
# Note that you should use the full family name but do not add Bold or Italic qualifiers
# to the name.
font_family monospace font_family monospace
italic_font auto italic_font auto
bold_font auto bold_font auto