kitty/kitty/core_text.m
Kovid Goyal 1f4fdb174a
Fix rendering of the new infinite length ligatures on CoreText
Apparently on CoreText, harfbuzz gives incorrect values for glyph
positions. So we use it only for selection and grouping of glyphs.
Actual positioning is done using CoreText. This means sophisticated
positioning using GPOS tables is probably broken, but that isn't really
useable in a character grid anyway.

Also remove the hack where glyph_centering was done for calt ligatures
as it seems to not be needed with modern FiraCode and CoreText
rendering. Fixes #3372
2021-03-11 20:11:40 +05:30

619 lines
26 KiB
Objective-C

/*
* core_text.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "state.h"
#include "fonts.h"
#include "unicode-data.h"
#include <structmember.h>
#include <stdint.h>
#include <math.h>
#include <hb-coretext.h>
#include <hb-ot.h>
#import <CoreGraphics/CGBitmapContext.h>
#import <CoreText/CTFont.h>
#include <Foundation/Foundation.h>
#include <CoreText/CoreText.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
typedef struct {
PyObject_HEAD
unsigned int units_per_em;
float ascent, descent, leading, underline_position, underline_thickness, point_sz, scaled_point_sz;
CTFontRef ct_font;
hb_font_t *hb_font;
PyObject *family_name, *full_name, *postscript_name, *path;
} CTFace;
PyTypeObject CTFace_Type;
static inline char*
convert_cfstring(CFStringRef src, int free_src) {
#define SZ 4094
static char buf[SZ+2] = {0};
bool ok = false;
if(!CFStringGetCString(src, buf, SZ, kCFStringEncodingUTF8)) PyErr_SetString(PyExc_ValueError, "Failed to convert CFString");
else ok = true;
if (free_src) CFRelease(src);
return ok ? buf : NULL;
#undef SZ
}
static inline void
init_face(CTFace *self, CTFontRef font) {
if (self->hb_font) hb_font_destroy(self->hb_font);
self->hb_font = NULL;
if (self->ct_font) CFRelease(self->ct_font);
self->ct_font = font;
self->units_per_em = CTFontGetUnitsPerEm(self->ct_font);
self->ascent = CTFontGetAscent(self->ct_font);
self->descent = CTFontGetDescent(self->ct_font);
self->leading = CTFontGetLeading(self->ct_font);
self->underline_position = CTFontGetUnderlinePosition(self->ct_font);
self->underline_thickness = CTFontGetUnderlineThickness(self->ct_font);
self->scaled_point_sz = CTFontGetSize(self->ct_font);
}
static inline CTFace*
ct_face(CTFontRef font) {
CTFace *self = (CTFace *)CTFace_Type.tp_alloc(&CTFace_Type, 0);
if (self) {
init_face(self, font);
self->family_name = Py_BuildValue("s", convert_cfstring(CTFontCopyFamilyName(self->ct_font), true));
self->full_name = Py_BuildValue("s", convert_cfstring(CTFontCopyFullName(self->ct_font), true));
self->postscript_name = Py_BuildValue("s", convert_cfstring(CTFontCopyPostScriptName(self->ct_font), true));
NSURL *url = (NSURL*)CTFontCopyAttribute(self->ct_font, kCTFontURLAttribute);
self->path = Py_BuildValue("s", [[url path] UTF8String]);
[url release];
if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL || self->path == NULL) { Py_CLEAR(self); }
}
return self;
}
static void
dealloc(CTFace* self) {
if (self->hb_font) hb_font_destroy(self->hb_font);
if (self->ct_font) CFRelease(self->ct_font);
self->hb_font = NULL;
self->ct_font = NULL;
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 PyObject*
font_descriptor_to_python(CTFontDescriptorRef descriptor) {
NSURL *url = (NSURL *) CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute);
NSString *psName = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontNameAttribute);
NSString *family = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontFamilyNameAttribute);
NSString *style = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontStyleNameAttribute);
NSDictionary *traits = (NSDictionary *) CTFontDescriptorCopyAttribute(descriptor, kCTFontTraitsAttribute);
unsigned int straits = [traits[(id)kCTFontSymbolicTrait] unsignedIntValue];
float weightVal = [traits[(id)kCTFontWeightTrait] floatValue];
float widthVal = [traits[(id)kCTFontWidthTrait] floatValue];
PyObject *ans = Py_BuildValue("{ssssssss sOsOsOsOsOsO sfsfsI}",
"path", [[url path] UTF8String],
"postscript_name", [psName UTF8String],
"family", [family UTF8String],
"style", [style UTF8String],
"bold", (straits & kCTFontBoldTrait) != 0 ? Py_True : Py_False,
"italic", (straits & kCTFontItalicTrait) != 0 ? Py_True : Py_False,
"monospace", (straits & kCTFontMonoSpaceTrait) != 0 ? Py_True : Py_False,
"expanded", (straits & kCTFontExpandedTrait) != 0 ? Py_True : Py_False,
"condensed", (straits & kCTFontCondensedTrait) != 0 ? Py_True : Py_False,
"color_glyphs", (straits & kCTFontColorGlyphsTrait) != 0 ? Py_True : Py_False,
"weight", weightVal,
"width", widthVal,
"traits", straits
);
[url release];
[psName release];
[family release];
[style release];
[traits release];
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] = @(PyUnicode_AsUTF8(t));
SET(family, kCTFontFamilyNameAttribute);
SET(style, kCTFontStyleNameAttribute);
SET(postscript_name, kCTFontNameAttribute);
#undef SET
return CTFontDescriptorCreateWithAttributes((CFDictionaryRef) attrs);
}
static CTFontCollectionRef all_fonts_collection_data = NULL;
static CTFontCollectionRef
all_fonts_collection() {
if (all_fonts_collection_data == NULL) all_fonts_collection_data = CTFontCollectionCreateFromAvailableFonts(NULL);
return all_fonts_collection_data;
}
static PyObject*
coretext_all_fonts(PyObject UNUSED *_self) {
CFArrayRef matches = CTFontCollectionCreateMatchingFontDescriptors(all_fonts_collection());
const CFIndex count = CFArrayGetCount(matches);
PyObject *ans = PyTuple_New(count), *temp;
if (ans == NULL) { CFRelease(matches); return PyErr_NoMemory(); }
for (CFIndex i = 0; i < count; i++) {
temp = font_descriptor_to_python((CTFontDescriptorRef) CFArrayGetValueAtIndex(matches, i));
if (temp == NULL) { CFRelease(matches); Py_DECREF(ans); return NULL; }
PyTuple_SET_ITEM(ans, i, temp); temp = NULL;
}
CFRelease(matches);
return ans;
}
static inline unsigned int
glyph_id_for_codepoint_ctfont(CTFontRef ct_font, char_type ch) {
unichar chars[2] = {0};
CGGlyph glyphs[2] = {0};
int count = CFStringGetSurrogatePairForLongCharacter(ch, chars) ? 2 : 1;
CTFontGetGlyphsForCharacters(ct_font, chars, glyphs, count);
return glyphs[0];
}
static inline bool
is_last_resort_font(CTFontRef new_font) {
CFStringRef name = CTFontCopyPostScriptName(new_font);
CFComparisonResult cr = CFStringCompare(name, CFSTR("LastResort"), 0);
CFRelease(name);
return cr == kCFCompareEqualTo;
}
static inline CTFontRef
manually_search_fallback_fonts(CTFontRef current_font, CPUCell *cell) {
CFArrayRef fonts = CTFontCollectionCreateMatchingFontDescriptors(all_fonts_collection());
CTFontRef ans = NULL;
const CFIndex count = CFArrayGetCount(fonts);
for (CFIndex i = 0; i < count; i++) {
CTFontDescriptorRef descriptor = (CTFontDescriptorRef)CFArrayGetValueAtIndex(fonts, i);
CTFontRef new_font = CTFontCreateWithFontDescriptor(descriptor, CTFontGetSize(current_font), NULL);
if (new_font) {
if (!is_last_resort_font(new_font)) {
char_type ch = cell->ch ? cell->ch : ' ';
bool found = true;
if (!glyph_id_for_codepoint_ctfont(new_font, ch)) found = false;
for (unsigned i = 0; i < arraysz(cell->cc_idx) && cell->cc_idx[i] && found; i++) {
ch = codepoint_for_mark(cell->cc_idx[i]);
if (!glyph_id_for_codepoint_ctfont(new_font, ch)) found = false;
}
if (found) {
ans = new_font;
break;
}
}
CFRelease(new_font);
}
}
CFRelease(fonts);
return ans;
}
static inline CTFontRef
find_substitute_face(CFStringRef str, CTFontRef old_font, CPUCell *cpu_cell) {
// CTFontCreateForString returns the original font when there are combining
// diacritics in the font and the base character is in the original font,
// so we have to check each character individually
CFIndex len = CFStringGetLength(str), start = 0, amt = len;
while (start < len) {
CTFontRef new_font = CTFontCreateForString(old_font, str, CFRangeMake(start, amt));
if (amt == len && len != 1) amt = 1;
else start++;
if (new_font == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to find fallback CTFont"); return NULL; }
if (new_font == old_font) { CFRelease(new_font); continue; }
if (is_last_resort_font(new_font)) {
CFRelease(new_font);
if (is_private_use(cpu_cell->ch)) {
// CoreTexts fallback font mechanism does not work for private use characters
new_font = manually_search_fallback_fonts(old_font, cpu_cell);
if (new_font) return new_font;
}
PyErr_SetString(PyExc_ValueError, "Failed to find fallback CTFont other than the LastResort font");
return NULL;
}
return new_font;
}
PyErr_SetString(PyExc_ValueError, "CoreText returned the same font as a fallback font");
return NULL;
}
PyObject*
create_fallback_face(PyObject *base_face, CPUCell* cell, bool UNUSED bold, bool UNUSED italic, bool emoji_presentation, FONTS_DATA_HANDLE fg UNUSED) {
CTFace *self = (CTFace*)base_face;
CTFontRef new_font;
if (emoji_presentation) new_font = CTFontCreateWithName((CFStringRef)@"AppleColorEmoji", self->scaled_point_sz, NULL);
else {
char text[64] = {0};
cell_as_utf8_for_fallback(cell, text);
CFStringRef str = CFStringCreateWithCString(NULL, text, kCFStringEncodingUTF8);
if (str == NULL) return PyErr_NoMemory();
new_font = find_substitute_face(str, self->ct_font, cell);
CFRelease(str);
}
if (new_font == NULL) return NULL;
return (PyObject*)ct_face(new_font);
}
unsigned int
glyph_id_for_codepoint(PyObject *s, char_type ch) {
CTFace *self = (CTFace*)s;
return glyph_id_for_codepoint_ctfont(self->ct_font, ch);
}
bool
is_glyph_empty(PyObject *s, glyph_index g) {
CTFace *self = (CTFace*)s;
CGGlyph gg = g;
CGRect bounds;
CTFontGetBoundingRectsForGlyphs(self->ct_font, kCTFontOrientationHorizontal, &gg, &bounds, 1);
return bounds.size.width <= 0;
}
int
get_glyph_width(PyObject *s, glyph_index g) {
CTFace *self = (CTFace*)s;
CGGlyph gg = g;
CGRect bounds;
CTFontGetBoundingRectsForGlyphs(self->ct_font, kCTFontOrientationHorizontal, &gg, &bounds, 1);
return (int)ceil(bounds.size.width);
}
static inline float
scaled_point_sz(FONTS_DATA_HANDLE fg) {
return ((fg->logical_dpi_x + fg->logical_dpi_y) / 144.0) * fg->font_sz_in_pts;
}
bool
set_size_for_face(PyObject *s, unsigned int UNUSED desired_height, bool force, FONTS_DATA_HANDLE fg) {
CTFace *self = (CTFace*)s;
float sz = scaled_point_sz(fg);
if (!force && self->scaled_point_sz == sz) return true;
CTFontRef new_font = CTFontCreateCopyWithAttributes(self->ct_font, sz, NULL, NULL);
if (new_font == NULL) fatal("Out of memory");
init_face(self, new_font);
return true;
}
hb_font_t*
harfbuzz_font_for_face(PyObject* s) {
CTFace *self = (CTFace*)s;
if (!self->hb_font) {
self->hb_font = hb_coretext_font_create(self->ct_font);
if (!self->hb_font) fatal("Failed to create hb_font");
hb_ot_font_set_funcs(self->hb_font);
}
return self->hb_font;
}
void
cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, unsigned int* baseline, unsigned int* underline_position, unsigned int* underline_thickness, unsigned int* strikethrough_position, unsigned int* strikethrough_thickness) {
// See https://developer.apple.com/library/content/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/TypoFeatures/TextSystemFeatures.html
CTFace *self = (CTFace*)s;
#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->ct_font, chars, glyphs, count);
for (i = 0; i < count; i++) {
if (glyphs[i]) {
w = (unsigned int)(ceilf(
CTFontGetAdvancesForGlyphs(self->ct_font, kCTFontOrientationHorizontal, glyphs+i, NULL, 1)));
if (w > width) width = w;
}
}
*cell_width = MAX(1u, width);
*underline_position = (unsigned int)floor(self->ascent - self->underline_position + 0.5);
*underline_thickness = (unsigned int)ceil(MAX(0.1, self->underline_thickness));
*baseline = (unsigned int)self->ascent;
*strikethrough_position = (unsigned int)floor(*baseline * 0.65);
*strikethrough_thickness = *underline_thickness;
// float line_height = MAX(1, floor(self->ascent + self->descent + MAX(0, self->leading) + 0.5));
// Let CoreText's layout engine calculate the line height. Slower, but hopefully more accurate.
#define W "AQWMH_gyl "
CFStringRef ts = CFSTR(W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W W);
#undef W
CFMutableAttributedStringRef test_string = CFAttributedStringCreateMutable(kCFAllocatorDefault, CFStringGetLength(ts));
CFAttributedStringReplaceString(test_string, CFRangeMake(0, 0), ts);
CFAttributedStringSetAttribute(test_string, CFRangeMake(0, CFStringGetLength(ts)), kCTFontAttributeName, self->ct_font);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(10, 10, 200, 200));
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(test_string);
CFRelease(test_string);
CTFrameRef test_frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CGPoint origin1, origin2;
CTFrameGetLineOrigins(test_frame, CFRangeMake(0, 1), &origin1);
CTFrameGetLineOrigins(test_frame, CFRangeMake(1, 1), &origin2);
CGFloat line_height = origin1.y - origin2.y;
CFRelease(test_frame); CFRelease(path); CFRelease(framesetter);
*cell_height = MAX(4u, (unsigned int)ceilf(line_height));
#undef count
}
PyObject*
face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
CTFontDescriptorRef desc = font_descriptor_from_python(descriptor);
if (!desc) return NULL;
CTFontRef font = CTFontCreateWithFontDescriptor(desc, scaled_point_sz(fg), NULL);
CFRelease(desc); desc = NULL;
if (!font) { PyErr_SetString(PyExc_ValueError, "Failed to create CTFont object"); return NULL; }
return (PyObject*) ct_face(font);
}
PyObject*
face_from_path(const char *path, int UNUSED index, FONTS_DATA_HANDLE fg UNUSED) {
CFStringRef s = CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8);
CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, s, kCFURLPOSIXPathStyle, false);
CGDataProviderRef dp = CGDataProviderCreateWithURL(url);
CGFontRef cg_font = CGFontCreateWithDataProvider(dp);
CTFontRef ct_font = CTFontCreateWithGraphicsFont(cg_font, 0.0, NULL, NULL);
CFRelease(cg_font); CFRelease(dp); CFRelease(url); CFRelease(s);
return (PyObject*) ct_face(ct_font);
}
PyObject*
specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE fg UNUSED) {
Py_INCREF(base_descriptor);
return base_descriptor;
}
static uint8_t *render_buf = NULL;
static size_t render_buf_sz = 0;
static CGGlyph glyphs[128];
static CGRect boxes[128];
static CGPoint positions[128];
static CGSize advances[128];
static void
finalize(void) {
free(render_buf);
if (all_fonts_collection_data) CFRelease(all_fonts_collection_data);
}
static inline void
render_color_glyph(CTFontRef font, uint8_t *buf, int glyph_id, unsigned int width, unsigned int height, unsigned int baseline) {
CGColorSpaceRef color_space = CGColorSpaceCreateDeviceRGB();
if (color_space == NULL) fatal("Out of memory");
CGContextRef ctx = CGBitmapContextCreate(buf, width, height, 8, 4 * width, color_space, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrderDefault);
if (ctx == NULL) fatal("Out of memory");
CGContextSetShouldAntialias(ctx, true);
CGContextSetShouldSmoothFonts(ctx, true); // sub-pixel antialias
CGContextSetRGBFillColor(ctx, 1, 1, 1, 1);
CGAffineTransform transform = CGAffineTransformIdentity;
CGContextSetTextDrawingMode(ctx, kCGTextFill);
CGGlyph glyph = glyph_id;
CGContextSetTextMatrix(ctx, transform);
CGContextSetTextPosition(ctx, -boxes[0].origin.x, MAX(2, height - baseline));
CGPoint p = CGPointMake(0, 0);
CTFontDrawGlyphs(font, &glyph, &p, 1, ctx);
CGContextRelease(ctx);
CGColorSpaceRelease(color_space);
for (size_t r = 0; r < width; r++) {
for (size_t c = 0; c < height; c++, buf += 4) {
uint32_t px = (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
*((pixel*)buf) = px;
}
}
}
static inline void
ensure_render_space(size_t width, size_t height) {
if (render_buf_sz >= width * height) return;
free(render_buf);
render_buf_sz = width * height;
render_buf = malloc(render_buf_sz);
if (render_buf == NULL) fatal("Out of memory");
}
static inline void
render_glyphs(CTFontRef font, unsigned int width, unsigned int height, unsigned int baseline, unsigned int num_glyphs) {
memset(render_buf, 0, width * height);
CGColorSpaceRef gray_color_space = CGColorSpaceCreateDeviceGray();
if (gray_color_space == NULL) fatal("Out of memory");
CGContextRef render_ctx = CGBitmapContextCreate(render_buf, width, height, 8, width, gray_color_space, (kCGBitmapAlphaInfoMask & kCGImageAlphaNone));
if (render_ctx == NULL) fatal("Out of memory");
CGContextSetShouldAntialias(render_ctx, true);
CGContextSetShouldSmoothFonts(render_ctx, true);
CGContextSetGrayFillColor(render_ctx, 1, 1); // white glyphs
CGContextSetGrayStrokeColor(render_ctx, 1, 1);
CGContextSetLineWidth(render_ctx, OPT(macos_thicken_font));
CGContextSetTextDrawingMode(render_ctx, kCGTextFillStroke);
CGContextSetTextMatrix(render_ctx, CGAffineTransformIdentity);
CGContextSetTextPosition(render_ctx, 0, height - baseline);
CTFontDrawGlyphs(font, glyphs, positions, num_glyphs, render_ctx);
CGContextRelease(render_ctx);
CGColorSpaceRelease(gray_color_space);
}
StringCanvas
render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
CTFace *self = (CTFace*)s;
CTFontRef font = self->ct_font;
size_t num_chars = strnlen(text, 32);
unichar chars[num_chars];
CGSize local_advances[num_chars];
for (size_t i = 0; i < num_chars; i++) chars[i] = text[i];
CTFontGetGlyphsForCharacters(font, chars, glyphs, num_chars);
CTFontGetAdvancesForGlyphs(font, kCTFontOrientationDefault, glyphs, local_advances, num_chars);
CGRect bounding_box = CTFontGetBoundingRectsForGlyphs(font, kCTFontOrientationDefault, glyphs, boxes, num_chars);
CGFloat x = 0, y = 0;
for (size_t i = 0; i < num_chars; i++) {
positions[i] = CGPointMake(x, y);
x += local_advances[i].width; y += local_advances[i].height;
}
StringCanvas ans = { .width = (size_t)ceil(x), .height = (size_t)(2 * bounding_box.size.height) };
ensure_render_space(ans.width, ans.height);
render_glyphs(font, ans.width, ans.height, baseline, num_chars);
ans.canvas = malloc(ans.width * ans.height);
if (ans.canvas) memcpy(ans.canvas, render_buf, ans.width * ans.height);
return ans;
}
static inline bool
do_render(CTFontRef ct_font, bool bold, bool italic, hb_glyph_info_t *info, hb_glyph_position_t *hb_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, bool allow_resize, FONTS_DATA_HANDLE fg, bool center_glyph) {
unsigned int canvas_width = cell_width * num_cells;
CGRect br = CTFontGetBoundingRectsForGlyphs(ct_font, kCTFontOrientationHorizontal, glyphs, boxes, num_glyphs);
const bool debug_rendering = false;
if (allow_resize) {
// Resize glyphs that would bleed into neighboring cells, by scaling the font size
float right = 0;
for (unsigned i=0; i < num_glyphs; i++) right = MAX(right, boxes[i].origin.x + boxes[i].size.width);
if (!bold && !italic && right > canvas_width + 1) {
if (debug_rendering) printf("resizing glyphs, right: %f canvas_width: %u\n", right, canvas_width);
CGFloat sz = CTFontGetSize(ct_font);
sz *= canvas_width / right;
CTFontRef new_font = CTFontCreateCopyWithAttributes(ct_font, sz, NULL, NULL);
bool ret = do_render(new_font, bold, italic, info, hb_positions, num_glyphs, canvas, cell_width, cell_height, num_cells, baseline, was_colored, false, fg, center_glyph);
CFRelease(new_font);
return ret;
}
}
CGFloat x = 0, y = 0;
CTFontGetAdvancesForGlyphs(ct_font, kCTFontOrientationDefault, glyphs, advances, num_glyphs);
for (unsigned i=0; i < num_glyphs; i++) {
positions[i].x = x; positions[i].y = y;
if (debug_rendering) printf("x=%f origin=%f width=%f advance=%f\n", x, boxes[i].origin.x, boxes[i].size.width, advances[i].width);
x += advances[i].width; y += advances[i].height;
}
if (*was_colored) {
render_color_glyph(ct_font, (uint8_t*)canvas, info[0].codepoint, cell_width * num_cells, cell_height, baseline);
} else {
ensure_render_space(canvas_width, cell_height);
render_glyphs(ct_font, canvas_width, cell_height, baseline, num_glyphs);
Region src = {.bottom=cell_height, .right=canvas_width}, dest = {.bottom=cell_height, .right=canvas_width};
render_alpha_mask(render_buf, canvas, &src, &dest, canvas_width, canvas_width);
}
if (num_cells && (center_glyph || (num_cells == 2 && *was_colored))) {
if (debug_rendering) printf("centering glyphs: center_glyph: %d\n", center_glyph);
// center glyphs (two cell emoji, PUA glyphs, ligatures, etc)
CGFloat delta = (((CGFloat)canvas_width - br.size.width) / 2.f);
// FiraCode ligatures result in negative origins
if (br.origin.x > 0) delta -= br.origin.x;
if (delta >= 1.f) right_shift_canvas(canvas, canvas_width, cell_height, (unsigned)(delta));
}
return true;
}
bool
render_glyphs_in_cells(PyObject *s, bool bold, bool italic, hb_glyph_info_t *info, hb_glyph_position_t *hb_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 fg, bool center_glyph) {
CTFace *self = (CTFace*)s;
for (unsigned i=0; i < num_glyphs; i++) glyphs[i] = info[i].codepoint;
return do_render(self->ct_font, bold, italic, info, hb_positions, num_glyphs, canvas, cell_width, cell_height, num_cells, baseline, was_colored, true, fg, center_glyph);
}
// Boilerplate {{{
static PyObject*
display_name(CTFace *self) {
CFStringRef dn = CTFontCopyDisplayName(self->ct_font);
const char *d = convert_cfstring(dn, true);
return Py_BuildValue("s", d);
}
static PyMethodDef methods[] = {
METHODB(display_name, METH_NOARGS),
{NULL} /* Sentinel */
};
const char*
postscript_name_for_face(const PyObject *face_) {
const CTFace *self = (const CTFace*)face_;
if (self->postscript_name) return PyUnicode_AsUTF8(self->postscript_name);
return "";
}
static PyObject *
repr(CTFace *self) {
char buf[1024] = {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 PyMethodDef module_methods[] = {
METHODB(coretext_all_fonts, METH_NOARGS),
{NULL, NULL, 0, NULL} /* Sentinel */
};
static PyMemberDef members[] = {
#define MEM(name, type) {#name, type, offsetof(CTFace, 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 */
};
PyTypeObject CTFace_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.CTFace",
.tp_basicsize = sizeof(CTFace),
.tp_dealloc = (destructor)dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "CoreText Font face",
.tp_methods = methods,
.tp_members = members,
.tp_repr = (reprfunc)repr,
};
int
init_CoreText(PyObject *module) {
if (PyType_Ready(&CTFace_Type) < 0) return 0;
if (PyModule_AddObject(module, "CTFace", (PyObject *)&CTFace_Type) != 0) return 0;
if (PyModule_AddFunctions(module, module_methods) != 0) return 0;
if (Py_AtExit(finalize) != 0) {
PyErr_SetString(PyExc_RuntimeError, "Failed to register the CoreText at exit handler");
return false;
}
return 1;
}
// }}}