From 3ca20ebc4eabcce2746d159f46b2fe9c89296e5e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 24 Apr 2024 07:40:48 +0530 Subject: [PATCH] Implement parsing of fvar table Cant rely on CoreText for this as it has incomplete and buggy APIs --- kitty/core_text.m | 118 ++++----------------------------------------- kitty/font-names.c | 116 ++++++++++++++++++++++++++++++++++++++++++++ kitty/fonts.h | 4 ++ 3 files changed, 129 insertions(+), 109 deletions(-) diff --git a/kitty/core_text.m b/kitty/core_text.m index 02215e0d3..cff5f486e 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -782,39 +782,16 @@ render_glyphs_in_cells(PyObject *s, bool bold, bool italic, hb_glyph_info_t *inf return do_render(self->ct_font, self->units_per_em, bold, italic, info, hb_positions, num_glyphs, canvas, cell_width, cell_height, num_cells, baseline, was_colored, true, fg, center_glyph); } -// Name table - -static uint16_t byteswap(uint16_t x) { return (x << 8) | (x >> 8); } +// Font tables {{{ static bool ensure_name_table(CTFace *self) { if (self->name_lookup_table) return true; RAII_CoreFoundation(CFDataRef, cftable, CTFontCopyTable(self->ct_font, kCTFontTableName, kCTFontTableOptionNoOptions)); const uint8_t *table = cftable ? CFDataGetBytePtr(cftable) : NULL; - size_t table_len = 0; - if (!table || (table_len = CFDataGetLength(cftable)) < 9 * sizeof(uint16_t)) { - self->name_lookup_table = PyDict_New(); - return true; - } - RAII_PyObject(ans, PyDict_New()); - // OpenType tables are big-endian for god knows what reason so need to byteswap -#define next byteswap(*(p++)) - uint16_t *p = (uint16_t*)table; p++; - uint16_t num_of_name_records = next, storage_offset = next; - const uint8_t *storage = table + storage_offset, *slimit = table + table_len; - if (storage >= slimit) { - self->name_lookup_table = PyDict_New(); - return true; - } - for (; num_of_name_records > 0 && p + 6 <= (uint16_t*)slimit; num_of_name_records--) { - uint16_t platform_id = next, encoding_id = next, language_id = next, name_id = next, length = next, offset = next; - const uint8_t *s = storage + offset; - if (s + length <= slimit && !add_font_name_record( - ans, platform_id, encoding_id, language_id, name_id, (const char*)(s), length)) return NULL; - } - self->name_lookup_table = ans; Py_INCREF(ans); - return true; -#undef next + size_t table_len = cftable ? CFDataGetLength(cftable) : 0; + self->name_lookup_table = read_name_font_table(table, table_len); + return !!self->name_lookup_table; } static PyObject* @@ -823,90 +800,13 @@ get_best_name(CTFace *self, PyObject *nameid) { return get_best_name_from_name_table(self->name_lookup_table, nameid); } -static PyObject* -_get_best_name(CTFace *self, unsigned long nameid) { - RAII_PyObject(key, PyLong_FromUnsignedLong(nameid)); - return key ? get_best_name(self, key) : NULL; -} - -// }}} - -// Variations {{{ - -static const char* -tag_to_string(uint32_t tag, uint8_t bytes[5]) { - bytes[0] = (tag >> 24) & 0xff; - bytes[1] = (tag >> 16) & 0xff; - bytes[2] = (tag >> 8) & 0xff; - bytes[3] = (tag) & 0xff; - bytes[4] = 0; - return (const char*)bytes; -} - -static void -convert_variation_to_python(const void *key, const void *value_, void *axis_values) { - unsigned long tag; float value; - CFNumberGetValue(key, kCFNumberLongType, &tag); - CFNumberGetValue(value_, kCFNumberFloatType, &value); - uint8_t tag_buf[5] = {0}; - RAII_PyObject(val, PyFloat_FromDouble((double)value)); - if (val) PyDict_SetItemString(axis_values, tag_to_string(tag, tag_buf), val); -} - static PyObject* get_variable_data(CTFace *self) { - uint8_t tag_buf[5] = {0}; - RAII_CoreFoundation(CFArrayRef, cfaxes, CTFontCopyVariationAxes(self->ct_font)); - if (!cfaxes) return Py_BuildValue("{sN sN}", "axes", PyTuple_New(0), "named_styles", PyDict_New()); //, "named_styles", named_styles); - RAII_PyObject(axes, PyTuple_New(CFArrayGetCount(cfaxes))); - for (CFIndex i = 0; i < CFArrayGetCount(cfaxes); i++) { - CFDictionaryRef a = (CFDictionaryRef)CFArrayGetValueAtIndex(cfaxes, i); -#define get_number(key, output, type, optional) { \ - CFNumberRef value = (CFNumberRef)CFDictionaryGetValue(a, key); \ - if (value) CFNumberGetValue(value, type, &output); \ - else if (!optional) { PyErr_Format(PyExc_RuntimeError, "The %s key was not present in the varioation axis dictionary", #key); return NULL; } \ -} - float minimum = 0, maximum = 0, def = 0; unsigned long tag = 0, hidden = 0; - get_number(kCTFontVariationAxisDefaultValueKey, def, kCFNumberFloatType, false); - get_number(kCTFontVariationAxisMaximumValueKey, maximum, kCFNumberFloatType, false); - get_number(kCTFontVariationAxisMinimumValueKey, minimum, kCFNumberFloatType, false); - get_number(kCTFontVariationAxisIdentifierKey, tag, kCFNumberLongType, false); - get_number(kCTFontVariationAxisHiddenKey, hidden, kCFNumberLongType, true); -#undef get_number - CFStringRef n = (CFStringRef)CFDictionaryGetValue(a, kCTFontVariationAxisNameKey); - PyObject *axis = Py_BuildValue("{sd sd sd ss sO sO}", - "minimum", minimum, "maximum", maximum, "default", def, "tag", tag_to_string(tag, tag_buf), - "hidden", hidden ? Py_True : Py_False, "strid", convert_cfstring(n, false) - ); - if (!axis) return NULL; - PyTuple_SET_ITEM(axes, i, axis); - } - RAII_CoreFoundation(CFStringRef, font_family_name, CTFontCopyFamilyName(self->ct_font)); - RAII_CoreFoundation(CTFontDescriptorRef, descriptor, - CTFontDescriptorCreateWithAttributes((__bridge CFDictionaryRef)@{(id)kCTFontFamilyNameAttribute: (__bridge NSString*)font_family_name})); - RAII_CoreFoundation(CTFontCollectionRef, collection, - CTFontCollectionCreateWithFontDescriptors((__bridge CFArrayRef)@[(__bridge id)descriptor], NULL)); - RAII_CoreFoundation(CFArrayRef, descriptors, CTFontCollectionCreateMatchingFontDescriptors(collection)); - RAII_PyObject(named_styles, PyTuple_New(CFArrayGetCount(descriptors))); - Py_ssize_t actual_num = 0; - RAII_CoreFoundation(CFURLRef, url, CTFontCopyAttribute(self->ct_font, kCTFontURLAttribute)); - for (CFIndex i = 0; i < CFArrayGetCount(descriptors); i++) { - CTFontDescriptorRef descriptor = (CTFontDescriptorRef)CFArrayGetValueAtIndex(descriptors, i); - RAII_CoreFoundation(CFDictionaryRef, variation, CTFontDescriptorCopyAttribute(descriptor, kCTFontVariationAttribute)); - if (!variation) continue; - RAII_CoreFoundation(CFURLRef, candidate_url, CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute)); - if (!CFEqual(url, candidate_url)) continue; - RAII_CoreFoundation(CFStringRef, style, CTFontDescriptorCopyAttribute(descriptor, kCTFontStyleNameAttribute)); - RAII_CoreFoundation(CFStringRef, psname, CTFontDescriptorCopyAttribute(descriptor, kCTFontNameAttribute)); - RAII_PyObject(axis_values, PyDict_New()); - if (!axis_values) return NULL; - CFDictionaryApplyFunction(variation, convert_variation_to_python, axis_values); - PyObject *ns = Py_BuildValue("{sO sN sN}", "axis_values", axis_values, "name", convert_cfstring(style, false), "psname", convert_cfstring(psname, false)); - if (!ns) return NULL; - PyTuple_SET_ITEM(named_styles, actual_num, ns); actual_num++; - } - _PyTuple_Resize(&named_styles, actual_num); - return Py_BuildValue("{sO sO sN}", "axes", axes, "named_styles", named_styles, "variations_postscript_name_prefix", _get_best_name(self, 25)); + if (!ensure_name_table(self)) return NULL; + RAII_CoreFoundation(CFDataRef, cftable, CTFontCopyTable(self->ct_font, kCTFontTableFvar, kCTFontTableOptionNoOptions)); + const uint8_t *table = cftable ? CFDataGetBytePtr(cftable) : NULL; + size_t table_len = cftable ? CFDataGetLength(cftable) : 0; + return read_fvar_font_table(table, table_len, self->name_lookup_table); } // }}} diff --git a/kitty/font-names.c b/kitty/font-names.c index b4e0e529a..04883d3a3 100644 --- a/kitty/font-names.c +++ b/kitty/font-names.c @@ -70,3 +70,119 @@ get_best_name_from_name_table(PyObject *table, PyObject *name_id) { return PyUnicode_FromString(""); } + +static PyObject* +get_best_name(PyObject *table, unsigned long name_id) { + RAII_PyObject(id, PyLong_FromUnsignedLong(name_id)); + return get_best_name_from_name_table(table, id); +} + +// OpenType tables are big-endian for god knows what reason so need to byteswap +static uint16_t +byteswap(const uint16_t *p) { + const uint8_t *b = (const uint8_t*)p; + return (((uint16_t)b[0]) << 8) | b[1]; +} + +static uint32_t +byteswap32(const uint32_t *val) { + const uint8_t *p = (const uint8_t*)val; + return (((uint32_t)p[0]) << 24) | (((uint32_t)p[1]) << 16) | (((uint32_t)p[2]) << 8) | p[3]; +} + +static double +load_fixed(const uint32_t *p_) { + uint32_t p = byteswap32(p_); + static const double denom = 1 << 16; + return ((int32_t)p) / denom; +} + +#define next byteswap(p++) +#define next32 load_fixed(p32++) + +PyObject* +read_name_font_table(const uint8_t *table, size_t table_len) { + if (!table || table_len < 9 * sizeof(uint16_t)) return PyDict_New(); + uint16_t *p = (uint16_t*)table; p++; + uint16_t num_of_name_records = next, storage_offset = next; + const uint8_t *storage = table + storage_offset, *slimit = table + table_len; + if (storage >= slimit) return PyDict_New(); + RAII_PyObject(ans, PyDict_New()); + for (; num_of_name_records > 0 && p + 6 <= (uint16_t*)slimit; num_of_name_records--) { + uint16_t platform_id = next, encoding_id = next, language_id = next, name_id = next, length = next, offset = next; + const uint8_t *s = storage + offset; + if (s + length <= slimit && !add_font_name_record( + ans, platform_id, encoding_id, language_id, name_id, (const char*)(s), length)) return NULL; + } + Py_INCREF(ans); + return ans; + +} + +PyObject* +read_fvar_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table) { + RAII_PyObject(named_styles, PyTuple_New(0)); if (!named_styles) return NULL; + RAII_PyObject(axes, PyTuple_New(0)); if (!axes) return NULL; +#define retval Py_BuildValue("{sO sO sN}", "axes", axes, "named_styles", named_styles, "variations_postscript_name_prefix", get_best_name(name_lookup_table, 25)) + + if (!table || table_len < 14 * sizeof(uint16_t)) return retval; + const uint16_t *p = (uint16_t*)table; + p += 2; + const uint16_t offset_to_start_of_axis_array = next; next; + const uint16_t num_of_axis_records = next, size_of_axis_record = next, num_of_name_records = next, size_of_name_record = next; + const uint16_t size_of_coordinates = num_of_axis_records * sizeof(int32_t); + if (size_of_name_record < size_of_coordinates + 4) { + PyErr_Format(PyExc_ValueError, "size of name record: %u too small", size_of_name_record); return NULL; + } + const bool has_postscript_name = size_of_name_record >= 3 * sizeof(uint16_t) + size_of_coordinates; + uint16_t i = 0; + if (size_of_axis_record < 20) { PyErr_Format(PyExc_ValueError, "size of axis record: %u too small", size_of_axis_record); return NULL; } + if (_PyTuple_Resize(&axes, num_of_axis_records) == -1) return NULL; + for ( + const uint8_t *pos = table + offset_to_start_of_axis_array; + pos + size_of_axis_record <= table + table_len && i < num_of_axis_records; + i++, pos += size_of_axis_record + ) { + uint32_t *p32 = (uint32_t*)(pos + 4); + const double minimum = next32, def = next32, maximum = next32; + p = (uint16_t*)(pos + 16); + int32_t flags = next, strid = next; + PyObject *axis = Py_BuildValue("{sd sd sd ss# sO sN}", + "minimum", minimum, "maximum", maximum, "default", def, "tag", pos, 4, + "hidden", (flags & 1) ? Py_True : Py_False, "strid", get_best_name(name_lookup_table, strid) + ); if (!axis) return NULL; + PyTuple_SET_ITEM(axes, i, axis); + } + if (_PyTuple_Resize(&axes, i) == -1) return NULL; + char tag_buf[5] = {0}; + i = 0; + if (_PyTuple_Resize(&named_styles, num_of_name_records) == -1) return NULL; + for ( + const uint8_t *pos = table + offset_to_start_of_axis_array + num_of_axis_records * size_of_axis_record; + pos + size_of_name_record <= table + table_len && i < num_of_name_records; + i++, pos += size_of_name_record + ) { + p = (uint16_t*)pos; + uint16_t name_id = next, psname_id = 0xffff; next; + const uint32_t *p32 = (uint32_t*)p; + RAII_PyObject(axis_values, PyDict_New()); + if (!axis_values) return NULL; + for (uint16_t i = 0; i < num_of_axis_records; i++) { + const uint8_t *t = table + offset_to_start_of_axis_array + i * size_of_axis_record; + memcpy(tag_buf, t, 4); + RAII_PyObject(pval, PyFloat_FromDouble(next32)); + if (!pval || PyDict_SetItemString(axis_values, tag_buf, pval) != 0) return NULL; + } + if (has_postscript_name) { p = (uint16_t*)p32; psname_id = next; } + PyObject *ns = Py_BuildValue("{sO sN sN}", + "axis_values", axis_values, "name", get_best_name(name_lookup_table, name_id), + "psname", (psname_id != 0xffff && psname_id ? get_best_name(name_lookup_table, psname_id) : PyUnicode_FromString(""))); + if (!ns) return NULL; + PyTuple_SET_ITEM(named_styles, i, ns); + } + if (_PyTuple_Resize(&named_styles, i) == -1) return NULL; + return retval; +#undef retval +} +#undef next32 +#undef next diff --git a/kitty/fonts.h b/kitty/fonts.h index 190f6bb1e..35b54569a 100644 --- a/kitty/fonts.h +++ b/kitty/fonts.h @@ -46,6 +46,10 @@ bool add_font_name_record(PyObject *table, uint16_t platform_id, uint16_t encoding_id, uint16_t language_id, uint16_t name_id, const char *string, uint16_t string_len); PyObject* get_best_name_from_name_table(PyObject *table, PyObject *name_id); +PyObject* +read_name_font_table(const uint8_t *table, size_t table_len); +PyObject* +read_fvar_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table); static inline void right_shift_canvas(pixel *canvas, size_t width, size_t height, size_t amt) {