Work on using FreeType to render on macOS as well

This commit is contained in:
Kovid Goyal 2017-11-10 15:39:53 +05:30
parent d2654db6f0
commit 541f389a06
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 47 additions and 281 deletions

View File

@ -88,6 +88,7 @@ install: |
brew install python3;
brew install glfw;
brew install libunistring;
brew install freetype;
brew install harfbuzz;
else
mkdir -p $SW;

View File

@ -85,6 +85,7 @@ the following dependencies are installed first.
* libunistring
* zlib
* libpng
* freetype
* fontconfig (not needed on macOS)
* harfbuzz >= 1.5.0
* xsel (only on X11 systems with glfw < 3.3)

View File

@ -15,30 +15,17 @@
#include <CoreText/CoreText.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#include <hb-coretext.h>
typedef struct {
PyObject_HEAD
unsigned int units_per_em;
float dpi, ascent, descent, leading, underline_position, underline_thickness, point_sz, scaled_point_sz;
CTFontRef font;
CTFontDescriptorRef descriptor;
PyObject *family_name, *full_name, *postscript_name, *path;
hb_font_t *harfbuzz_font;
} Face;
PyTypeObject Face_Type;
static inline PyObject*
static inline char*
convert_cfstring(CFStringRef src, int free_src) {
#define SZ 2048
static char buf[SZ+2] = {0};
PyObject *ans = NULL;
bool ok = false;
if(!CFStringGetCString(src, buf, SZ, kCFStringEncodingUTF8)) PyErr_SetString(PyExc_ValueError, "Failed to convert CFString");
else ans = PyUnicode_FromString(buf);
else ok = true;
if (free_src) CFRelease(src);
return ans;
return ok ? buf : NULL;
#undef SZ
}
@ -118,271 +105,50 @@ coretext_all_fonts(PyObject UNUSED *_self) {
return ans;
}
static inline bool
create_harfbuzz_font(Face *self) {
CGFontRef cg_font = CTFontCopyGraphicsFont(self->font, NULL);
if (cg_font == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to create CGFont"); return false; }
hb_face_t *face = hb_coretext_face_create(cg_font);
if (face == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to create hb_face"); return false; }
CGFontRelease(cg_font);
unsigned int upem = hb_face_get_upem(face);
self->harfbuzz_font = hb_font_create(face);
hb_face_destroy(face);
hb_font_set_scale(self->harfbuzz_font, upem, upem);
return true;
static PyObject*
face_for_text(PyObject UNUSED *self, PyObject UNUSED *args) {
Py_RETURN_NONE; // TODO: Implement this
}
static inline bool
actual_new(Face *self, CTFontDescriptorRef desc, float point_sz, float dpi) {
self->descriptor = desc;
self->dpi = dpi;
self->point_sz = point_sz;
float scaled_point_sz = (dpi / 72.0) * point_sz;
self->font = CTFontCreateWithFontDescriptor(self->descriptor, scaled_point_sz, NULL);
if (self->font == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to create font object"); return false; }
self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font), 1);
self->full_name = convert_cfstring(CTFontCopyFullName(self->font), 1);
self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->font), 1);
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) {return false;}
self->units_per_em = CTFontGetUnitsPerEm(self->font);
self->ascent = CTFontGetAscent(self->font);
self->descent = CTFontGetDescent(self->font);
self->leading = CTFontGetLeading(self->font);
self->underline_position = CTFontGetUnderlinePosition(self->font);
self->underline_thickness = CTFontGetUnderlineThickness(self->font);
self->scaled_point_sz = CTFontGetSize(self->font);
return create_harfbuzz_font(self);
static void
free_font(void *f) {
CFRelease((CTFontRef)f);
}
static PyObject*
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
Face *self;
create_face(PyObject UNUSED *self, PyObject *args) {
PyObject *descriptor;
float point_sz, dpi;
if (!PyArg_ParseTuple(args, "Off", &descriptor, &point_sz, &dpi)) return NULL;
self = (Face *)type->tp_alloc(type, 0);
if (!self) return NULL;
float point_sz, xdpi, ydpi;
if(!PyArg_ParseTuple(args, "Offf", &descriptor, &point_sz, &xdpi, &ydpi)) return NULL;
CTFontDescriptorRef desc = font_descriptor_from_python(descriptor);
if (!desc) { Py_CLEAR(self); if(!PyErr_Occurred()) PyErr_NoMemory(); return NULL; }
if (!actual_new(self, desc, point_sz, dpi)) Py_CLEAR(self);
return (PyObject*)self;
}
static void
dealloc(Face* self) {
if (self->descriptor) CFRelease(self->descriptor);
if (self->harfbuzz_font) hb_font_destroy(self->harfbuzz_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->path);
Py_TYPE(self)->tp_free((PyObject*)self);
}
static void
encode_utf16_pair(uint32_t character, unichar *units) {
unsigned int code;
assert(0x10000 <= character && character <= 0x10FFFF);
code = (character - 0x10000);
units[0] = 0xD800 | (code >> 10);
units[1] = 0xDC00 | (code & 0x3FF);
}
bool
face_has_codepoint(PyObject *s, char_type ch) {
Face *self = (Face*)s;
unichar chars[2] = {0};
CGGlyph glyphs[2] = {0};
int count = 1;
if (ch <= 0xffff) chars[0] = (unichar)ch;
else { encode_utf16_pair(ch, chars); count = 2; }
return CTFontGetGlyphsForCharacters(self->font, chars, glyphs, count);
}
static PyObject*
face_for_text(Face *self, PyObject *args) {
char *s;
int bold, italic;
Face *ans = NULL;
if (!PyArg_ParseTuple(args, "espp", "UTF-8", &s, &bold, &italic)) return NULL;
CFStringRef str = CFStringCreateWithCString(NULL, s, kCFStringEncodingUTF8);
if (!str) return PyErr_NoMemory();
unichar chars[10] = {0};
CFRange range = CFRangeMake(0, CFStringGetLength(str));
CFStringGetCharacters(str, range, chars);
CTFontRef font = CTFontCreateForString(self->font, str, range);
if (font == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to find fallback font"); goto end; }
ans = (Face *)Face_Type.tp_alloc(&Face_Type, 0);
if (ans == NULL) { CFRelease(font); PyErr_NoMemory(); goto end; }
ans->font = font;
ans->descriptor = self->descriptor; CFRetain(ans->descriptor);
if(!actual_new(ans, ans->descriptor, self->point_sz, self->dpi)) Py_CLEAR(ans);
end:
CFRelease(str);
return (PyObject*)ans;
}
static inline void
calc_cell_size(Face *self, unsigned int *cell_width, unsigned int *cell_height) {
#define count (128 - 32)
unichar chars[count+1] = {0};
CGGlyph glyphs[count+1] = {0};
unsigned int width = 0, w, i;
for (i = 0; i < count; i++) chars[i] = 32 + i;
CTFontGetGlyphsForCharacters(self->font, chars, glyphs, count);
for (i = 0; i < count; i++) {
if (glyphs[i]) {
w = (unsigned int)(ceilf(
CTFontGetAdvancesForGlyphs(self->font, kCTFontOrientationHorizontal, glyphs+i, NULL, 1)));
if (w > width) width = w;
}
}
// See https://stackoverflow.com/questions/5511830/how-does-line-spacing-work-in-core-text-and-why-is-it-different-from-nslayoutm
CGFloat leading = MAX(0, self->leading);
leading = floor(leading + 0.5);
CGFloat line_height = floor(self->ascent + 0.5) + floor(self->descent + 0.5) + leading;
CGFloat ascender_delta = (leading > 0) ? 0 : floor(0.2 * line_height + 0.5);
*cell_width = width; *cell_height = (unsigned int)(line_height + ascender_delta);
#undef count
}
void
cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, unsigned int* baseline, unsigned int* underline_position, unsigned int* underline_thickness) {
Face *self = (Face*)s;
calc_cell_size(self, cell_width, cell_height);
*baseline = (unsigned int)roundf(self->ascent);
*underline_position = (unsigned int)(roundf(*baseline - self->underline_position));
*underline_thickness = (unsigned int)ceilf(self->underline_thickness);
}
hb_font_t*
harfbuzz_font_for_face(PyObject *self) { return ((Face*)self)->harfbuzz_font; }
bool
set_size_for_face(PyObject *s, float pt_sz, float xdpi, float ydpi) {
Face *self = (Face*)s;
float dpi = (xdpi + ydpi) / 2.f;
if (self->dpi == dpi && self->point_sz == pt_sz) return true;
CTFontRef f = CTFontCreateCopyWithAttributes(self->font, self->scaled_point_sz, NULL, NULL);
if (f == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to create font copy with different size"); return false; }
CFRelease(self->font); self->font = NULL;
self->point_sz = pt_sz; self->dpi = dpi;
self->scaled_point_sz = CTFontGetSize(self->font);
self->font = f;
hb_font_destroy(self->harfbuzz_font); self->harfbuzz_font = NULL;
return create_harfbuzz_font(self);
}
static inline CGContextRef
create_context(uint8_t *canvas, unsigned int width, unsigned int height) {
CGColorSpaceRef color_space = CGColorSpaceCreateDeviceGray();
if (color_space == NULL) return NULL;
CGContextRef ctx = CGBitmapContextCreate(canvas, width, height, 8, width, color_space, (kCGBitmapAlphaInfoMask & kCGImageAlphaNone));
CGColorSpaceRelease(color_space);
if(ctx) {
CGContextSetShouldAntialias(ctx, true);
CGContextSetShouldSmoothFonts(ctx, true); // sub-pixel antialias
CGContextSetShouldSubpixelQuantizeFonts(ctx, true);
CGContextSetShouldSubpixelPositionFonts(ctx, true);
CGContextSetRGBFillColor(ctx, 1, 1, 1, 1); // white glyphs
CGContextSetTextDrawingMode(ctx, kCGTextFill);
CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
}
return ctx;
}
bool
render_glyphs_in_cells(PyObject *f, bool UNUSED bold, bool UNUSED italic, hb_glyph_info_t *info, hb_glyph_position_t *positions, unsigned int num_glyphs, uint8_t *canvas, unsigned int cell_width, unsigned int cell_height, unsigned int num_cells, unsigned int UNUSED baseline) {
Face *self = (Face*)f;
CGContextRef ctx = create_context(canvas, cell_width * num_cells, cell_height);
if (ctx == NULL) return false;
static CGGlyph glyphs[100];
static CGPoint cg_positions[sizeof(glyphs)/sizeof(glyphs[0])];
num_glyphs = MIN(sizeof(glyphs)/sizeof(glyphs[0]), num_glyphs);
float x = 0.f;
// TODO: Scale the glyph if its bbox is larger than the image by using a non-identity transform
/* CGRect rect = CTFontGetBoundingRectsForGlyphs(font, kCTFontOrientationHorizontal, glyphs, 0, 1); */
for (unsigned int i=0; i < num_glyphs; i++) {
glyphs[i] = info[i].codepoint;
cg_positions[i].x = x + (float)positions[i].x_offset / 64.0f;
cg_positions[i].y = (float)positions[i].y_offset / 64.0f;
x += (float)positions[i].x_advance / 64.0f;
}
CTFontDrawGlyphs(self->font, glyphs, cg_positions, num_glyphs, ctx);
CGContextRelease(ctx);
return true;
}
static PyObject *
repr(Face *self) {
char buf[400] = {0};
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));
return PyUnicode_FromFormat(
"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->path, self->units_per_em, buf
);
}
static PyObject*
display_name(Face *self) {
#define R(x) if (self->x) { Py_INCREF(self->x); return self->x; }
R(full_name); R(postscript_name); R(family_name); R(path);
#undef R
Py_RETURN_NONE;
if (!desc) return NULL;
float scaled_point_sz = ((xdpi + ydpi) / 144.0) * point_sz;
CTFontRef font = CTFontCreateWithFontDescriptor(desc, scaled_point_sz, NULL);
CFRelease(desc); desc = NULL;
if (!font) { PyErr_SetString(PyExc_ValueError, "Failed to create CTFont object"); return NULL; }
const char *psname = convert_cfstring(CTFontCopyPostScriptName(font), 1);
NSURL *url = (NSURL*)CTFontCopyAttribute(font, kCTFontURLAttribute);
PyObject *path = PyUnicode_FromString([[url path] UTF8String]);
[url release];
if (path == NULL) { CFRelease(font); return NULL; }
PyObject *ans = ft_face_from_path_and_psname(path, psname, (void*)font, free_font, true, 3, point_sz, xdpi, ydpi);
Py_DECREF(path);
if (ans == NULL) { CFRelease(font); }
return ans;
}
// Boilerplate {{{
static PyMemberDef members[] = {
#define MEM(name, type) {#name, type, offsetof(Face, name), READONLY, #name}
MEM(units_per_em, T_UINT),
MEM(point_sz, T_FLOAT),
MEM(scaled_point_sz, T_FLOAT),
MEM(ascent, T_FLOAT),
MEM(descent, T_FLOAT),
MEM(leading, T_FLOAT),
MEM(underline_position, T_FLOAT),
MEM(underline_thickness, T_FLOAT),
MEM(family_name, T_OBJECT),
MEM(path, T_OBJECT),
MEM(full_name, T_OBJECT),
MEM(postscript_name, T_OBJECT),
{NULL} /* Sentinel */
};
static PyMethodDef methods[] = {
METHODB(face_for_text, METH_VARARGS),
METHODB(display_name, METH_NOARGS),
{NULL} /* Sentinel */
};
PyTypeObject Face_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.CTFace",
.tp_basicsize = sizeof(Face),
.tp_dealloc = (destructor)dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "CoreText Font face",
.tp_methods = methods,
.tp_members = members,
.tp_new = new,
.tp_repr = (reprfunc)repr,
};
static PyMethodDef module_methods[] = {
{"coretext_all_fonts", (PyCFunction)coretext_all_fonts, METH_NOARGS, ""},
METHODB(coretext_all_fonts, METH_NOARGS),
METHODB(face_for_text, METH_VARARGS),
METHODB(create_face, METH_VARARGS),
{NULL, NULL, 0, NULL} /* Sentinel */
};
int
init_CoreText(PyObject *module) {
if (PyType_Ready(&Face_Type) < 0) return 0;
if (PyModule_AddObject(module, "CTFace", (PyObject *)&Face_Type) != 0) return 0;
if (PyModule_AddFunctions(module, module_methods) != 0) return 0;
return 1;
}

View File

@ -25,4 +25,4 @@ void sprite_tracker_set_limits(size_t max_texture_size, size_t max_array_len);
void sprite_tracker_set_layout(unsigned int cell_width, unsigned int cell_height);
typedef void (*free_extra_data_func)(void*);
PyObject* ft_face_from_data(const uint8_t* data, size_t sz, void *extra_data, free_extra_data_func fed, PyObject *path, int hinting, int hintstyle, float size_in_pts, float xdpi, float ydpi);
PyObject* ft_face_from_path_and_psname(const char* path, const char* psname, void *extra_data, free_extra_data_func fed, int hinting, int hintstyle, float size_in_pts, float xdpi, float ydpi);
PyObject* ft_face_from_path_and_psname(PyObject* path, const char* psname, void *extra_data, free_extra_data_func fed, int hinting, int hintstyle, float size_in_pts, float xdpi, float ydpi);

View File

@ -6,7 +6,7 @@ import re
import sys
from collections import namedtuple
from kitty.fast_data_types import CTFace as Face, coretext_all_fonts
from kitty.fast_data_types import coretext_all_fonts, create_face, face_for_text
from kitty.utils import safe_print
attr_map = {(False, False): 'font_family',
@ -96,9 +96,9 @@ def face_description(family, main_family, bold=False, italic=False):
)
def get_face(family, font_size, dpi, bold=False, italic=False):
def get_face(family, font_size, xdpi, ydpi, bold=False, italic=False):
descriptor = find_best_match(family, bold, italic)
return Face(descriptor, font_size, dpi)
return create_face(descriptor, font_size, xdpi, ydpi)
def get_font_files(opts):
@ -118,7 +118,7 @@ def get_font_files(opts):
def face_from_font(font, pt_sz, xdpi, ydpi):
return get_face(font.resolved_family, pt_sz, (xdpi + ydpi) / 2, bold=font.bold, italic=font.italic)
return get_face(font.resolved_family, pt_sz, xdpi, ydpi, bold=font.bold, italic=font.italic)
def save_medium_face(face):
@ -126,7 +126,7 @@ def save_medium_face(face):
def font_for_text(text, current_font_family, pt_sz, xdpi, ydpi, bold=False, italic=False):
save_medium_face.face.face_for_text(text, bold, italic)
return face_for_text(text, pt_sz, xdpi, ydpi, bold, italic)
def font_for_family(family):
@ -138,7 +138,7 @@ 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)
face = get_face(all_fonts, name, 'Menlo', font_size, dpi, dpi, bold, italic)
return face
@ -147,6 +147,6 @@ def test_family_matching(name='Menlo', dpi=72.0, font_size=11.0):
for bold in (False, True):
for italic in (False, True):
face = get_face(
all_fonts, name, 'Menlo', font_size, dpi, bold, italic
all_fonts, name, 'Menlo', font_size, dpi, dpi, bold, italic
)
print(bold, italic, face)

View File

@ -157,15 +157,12 @@ load_from_path_and_psname(const char *path, const char* psname, FT_Face ans) {
}
PyObject*
ft_face_from_path_and_psname(const char* path, const char* psname, void *extra_data, free_extra_data_func fed, int hinting, int hintstyle, float size_in_pts, float xdpi, float ydpi) {
ft_face_from_path_and_psname(PyObject* path, const char* psname, void *extra_data, free_extra_data_func fed, int hinting, int hintstyle, float size_in_pts, float xdpi, float ydpi) {
Face *ans = (Face*)Face_Type.tp_alloc(&Face_Type, 0);
if (!load_from_path_and_psname(path, psname, ans->face)) { Py_CLEAR(ans); return NULL; }
PyObject *p = PyUnicode_FromString(path);
if (p == NULL) { Py_CLEAR(ans); return NULL; }
if (!init_ft_face(ans, p, hinting, hintstyle, size_in_pts, xdpi, ydpi)) Py_CLEAR(ans);
if (!load_from_path_and_psname(PyUnicode_AsUTF8(path), psname, ans->face)) { Py_CLEAR(ans); return NULL; }
if (!init_ft_face(ans, path, hinting, hintstyle, size_in_pts, xdpi, ydpi)) Py_CLEAR(ans);
ans->extra_data = extra_data;
ans->free_extra_data = fed;
Py_DECREF(p);
return (PyObject*)ans;
}

View File

@ -175,6 +175,8 @@ def init_env(
cflags.extend(pkg_config('libpng', '--cflags-only-I'))
if isosx:
font_libs = ['-framework', 'CoreText', '-framework', 'CoreGraphics']
cflags.extend(pkg_config('freetype2', '--cflags-only-I'))
font_libs += pkg_config('freetype2', '--libs')
else:
cflags.extend(pkg_config('fontconfig', '--cflags-only-I'))
font_libs = pkg_config('fontconfig', '--libs')
@ -327,8 +329,7 @@ def option_parser():
def find_c_files():
ans, headers = [], []
d = os.path.join(base, 'kitty')
exclude = {'freetype.c',
'fontconfig.c'} if isosx else {'core_text.m', 'cocoa_window.m'}
exclude = {'fontconfig.c'} if isosx else {'core_text.m', 'cocoa_window.m'}
for x in os.listdir(d):
ext = os.path.splitext(x)[1]
if ext in ('.c', '.m') and os.path.basename(x) not in exclude: