/* * font-names.c * Copyright (C) 2024 Kovid Goyal * * Distributed under terms of the GPL3 license. */ #include "fonts.h" static PyObject* decode_name_record(PyObject *namerec) { #define d(x) PyLong_AsUnsignedLong(PyTuple_GET_ITEM(namerec, x)) unsigned long platform_id = d(0), encoding_id = d(1), language_id = d(2); #undef d const char *encoding = "unicode_escape"; if ((platform_id == 3 && encoding_id == 1) || platform_id == 0) encoding = "utf-16-be"; else if (platform_id == 1 && encoding_id == 0 && language_id == 0) encoding = "mac-roman"; PyObject *b = PyTuple_GET_ITEM(namerec, 3); return PyUnicode_Decode(PyBytes_AS_STRING(b), PyBytes_GET_SIZE(b), encoding, "replace"); } static bool namerec_matches(PyObject *namerec, unsigned platform_id, unsigned encoding_id, unsigned language_id) { #define d(x) PyLong_AsUnsignedLong(PyTuple_GET_ITEM(namerec, x)) return d(0) == platform_id && d(1) == encoding_id && d(2) == language_id; #undef d } static PyObject* find_matching_namerec(PyObject *namerecs, unsigned platform_id, unsigned encoding_id, unsigned language_id) { for (Py_ssize_t i = 0; i < PyList_GET_SIZE(namerecs); i++) { PyObject *namerec = PyList_GET_ITEM(namerecs, i); if (namerec_matches(namerec, platform_id, encoding_id, language_id)) return decode_name_record(namerec); } return NULL; } 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) { RAII_PyObject(key, PyLong_FromUnsignedLong((unsigned long)name_id)); if (!key) return false; RAII_PyObject(list, PyDict_GetItem(table, key)); if (list == NULL) { list = PyList_New(0); if (!list) return false; if (PyDict_SetItem(table, key, list) != 0) return false; } else Py_INCREF(list); RAII_PyObject(value, Py_BuildValue("(H H H y#)", platform_id, encoding_id, language_id, string, (Py_ssize_t)string_len)); if (!value) return false; if (PyList_Append(list, value) != 0) return false; return true; } PyObject* get_best_name_from_name_table(PyObject *table, PyObject *name_id) { PyObject *namerecs = PyDict_GetItem(table, name_id); if (namerecs == NULL) return PyUnicode_FromString(""); if (PyList_GET_SIZE(namerecs) == 1) return decode_name_record(PyList_GET_ITEM(namerecs, 0)); #define d(...) { PyObject *ans = find_matching_namerec(namerecs, __VA_ARGS__); if (ans != NULL || PyErr_Occurred()) return ans; } d(3, 1, 1033); // Microsoft/Windows/US English d(1, 0, 0); // Mac/Roman/English d(0, 6, 0); // Unicode/SMP/* d(0, 4, 0); // Unicode/SMP/* d(0, 3, 0); // Unicode/BMP/* d(0, 2, 0); // Unicode/10646-BMP/* d(0, 1, 0); // Unicode/1.1/* #undef d 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; } static bool is_digit(char x) { return '0' <= x && x <= '9'; } static PyObject* read_cv_feature_table(const uint8_t *table, const uint8_t *limit, PyObject *name_lookup_table) { RAII_PyObject(ans, PyDict_New()); if (!ans) return NULL; if (limit - table >= 12) { uint16_t *p = (uint16_t*)(table + 2); uint16_t name_id = next, tooltip_id = next, sample_id = next, num_params = next, first_value_id = next; if (name_id) { RAII_PyObject(name, get_best_name(name_lookup_table, name_id)); if (!name) return NULL; if (PyDict_SetItemString(ans, "name", name) != 0) return NULL; } if (tooltip_id) { RAII_PyObject(tooltip, get_best_name(name_lookup_table, tooltip_id)); if (!tooltip) return NULL; if (PyDict_SetItemString(ans, "tooltip", tooltip) != 0) return NULL; } if (sample_id) { RAII_PyObject(sample, get_best_name(name_lookup_table, sample_id)); if (!sample) return NULL; if (PyDict_SetItemString(ans, "sample", sample) != 0) return NULL; } if (num_params && first_value_id) { RAII_PyObject(params, PyTuple_New(num_params)); if (!params) return NULL; for (uint16_t i = 0; i < num_params; i++) { PyObject *pval = get_best_name(name_lookup_table, first_value_id + i); if (!pval) return NULL; PyTuple_SET_ITEM(params, i, pval); } if (PyDict_SetItemString(ans, "params", params) != 0) return NULL; } } Py_INCREF(ans); return ans; } static PyObject* read_ss_feature_table(const uint8_t *table, const uint8_t *limit, PyObject *name_lookup_table) { RAII_PyObject(ans, PyDict_New()); if (!ans) return NULL; if (limit - table < 4) { Py_INCREF(ans); return ans; } uint16_t *p = (uint16_t*)(table + 2); uint16_t name_id = next; if (name_id) { RAII_PyObject(name, get_best_name(name_lookup_table, name_id)); if (!name) return NULL; if (PyDict_SetItemString(ans, "name", name) != 0) return NULL; } Py_INCREF(ans); return ans; } bool read_features_from_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output) { if (table_len < 20) return true; const uint16_t *p = (uint16_t*)table; const uint8_t *limit = table + table_len; uint16_t major_version = next, minor_version = next, script_list_offset = next, feature_list_offset = next; (void)major_version; (void)minor_version; (void)script_list_offset; const uint8_t *feature_list_table = table + feature_list_offset; char tag_buf[5] = {0}; if (feature_list_table + 2 >= limit) return true; p = (uint16_t*)feature_list_table; uint16_t feature_count = next; const uint8_t *pos = (uint8_t*)p; for (uint16_t i = 0; i < feature_count && pos + 6 <= limit; pos += 6, i++) { memcpy(tag_buf, pos, 4); RAII_PyObject(tag, PyUnicode_FromString(tag_buf)); if (!tag) return false; if (PyDict_Contains(output, tag) == 1) continue; if (PyDict_SetItem(output, tag, Py_None) != 0) return false; p = (uint16_t*)(pos + 4); uint16_t offset_to_feature_table = next; const uint8_t *feature_table = feature_list_table + offset_to_feature_table; if (feature_table + 2 > limit) continue; p = (uint16_t*)(feature_table); uint16_t offset_to_feature_params_table = next; if (tag_buf[0] == 'c' && tag_buf[1] == 'v' && is_digit(tag_buf[2]) && is_digit(tag_buf[3])) { if (offset_to_feature_params_table) { RAII_PyObject(cv, read_cv_feature_table(feature_table + offset_to_feature_params_table, limit, name_lookup_table)); if (!cv) return false; if (PyDict_SetItem(output, tag, cv) != 0) return false; } } else if (tag_buf[0] == 's' && tag_buf[1] == 's' && '0' <= tag_buf[2] && tag_buf[2] <= '2' && is_digit(tag_buf[3])) { if (offset_to_feature_params_table) { RAII_PyObject(ss, read_ss_feature_table(feature_table + offset_to_feature_params_table, limit, name_lookup_table)); if (!ss) return false; if (PyDict_SetItem(output, tag, ss) != 0) return false; } } } return true; } bool read_STAT_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output) { uint16_t elided_fallback_name_id = 0; RAII_PyObject(design_axes, PyTuple_New(0)); RAII_PyObject(multi_axis_styles, PyTuple_New(0)); if (!design_axes || !multi_axis_styles) return false; if (table_len < 20) goto ok; const uint16_t *p = (uint16_t*)table; uint16_t major_version = next, minor_version = next, size_of_design_axis_entry = next, count_of_design_axis_entries = next; const uint32_t *p32 = (uint32_t*)p; uint32_t offset_to_start_of_design_axes_entries = byteswap32(p32++); p = (uint16_t*)p32; uint16_t count_of_axis_value_entries = next; p32 = (uint32_t*)p; uint32_t offset_to_start_of_axis_value_entries = byteswap32(p32++); p = (uint16_t*)p32; elided_fallback_name_id = next; if (major_version == 1 && minor_version < 1) elided_fallback_name_id = 0; const uint8_t *table_limit = table + table_len; size_t count = 0; if (_PyTuple_Resize(&design_axes, count_of_design_axis_entries) == -1) return false; for ( const uint8_t *pos = table + offset_to_start_of_design_axes_entries; pos + size_of_design_axis_entry <= table_limit && count < count_of_design_axis_entries; pos += size_of_design_axis_entry, count++ ) { p = (uint16_t*)(pos + 4); uint16_t name_id = next, ordering = next; PyObject *rec = Py_BuildValue("{ss# sN sH sN}", "tag", (char*)pos, 4, "name", get_best_name(name_lookup_table, name_id), "ordering", ordering, "values", PyList_New(0)); if (!rec) return false; PyTuple_SET_ITEM(design_axes, count, rec); } if (_PyTuple_Resize(&design_axes, count) == -1) return false; count = 0; const uint8_t *start_of_axis_values_offsets_array = table + offset_to_start_of_axis_value_entries; Py_ssize_t num_of_multi_axis_styles = 0; if (_PyTuple_Resize(&multi_axis_styles, count_of_axis_value_entries) == -1) return false; for ( const uint8_t *pos = start_of_axis_values_offsets_array; pos + 2 <= table_limit && count < count_of_axis_value_entries; pos += 2, count++ ) { p = (uint16_t*)pos; uint16_t offset = next; const uint8_t *start_of_axis_values_table = start_of_axis_values_offsets_array + offset; if (start_of_axis_values_table + 12 > table_limit) continue; p = (uint16_t*)(start_of_axis_values_table); uint16_t format = next, axis_index = next, flags = next, value_name_id = next; p32 = (uint32_t*)p; #define app(fmt, ...) { \ RAII_PyObject(v, Py_BuildValue("{sH sH sN " fmt "}", \ "format", format, "flags", flags, "name", get_best_name(name_lookup_table, value_name_id), __VA_ARGS__)); \ if (!v) return false; \ PyObject *l = PyDict_GetItemString(PyTuple_GET_ITEM(design_axes, axis_index), "values"); \ if (l && PyList_Append(l, v) != 0) return false; \ } switch(format) { case 1: if (p32 + 1 <= (uint32_t*)table_limit && axis_index < PyTuple_GET_SIZE(design_axes)) { const double value = next32; app("sd", "value", value); } break; case 2: if (p32 + 3 <= (uint32_t*)table_limit && axis_index < PyTuple_GET_SIZE(design_axes)) { const double value = next32, minimum = next32, maximum = next32; app("sd sd sd", "value", value, "minimum", minimum, "maximum", maximum); } break; case 3: if (p32 + 2 <= (uint32_t*)table_limit && axis_index < PyTuple_GET_SIZE(design_axes)) { const double value = next32, linked_value = next32; app("sd sd", "value", value, "linked_value", linked_value); } break; case 4: if ((uint8_t*)(p) + 6 * axis_index <= table_limit) { if (num_of_multi_axis_styles >= PyTuple_GET_SIZE(multi_axis_styles)) { PyErr_Format(PyExc_IndexError, "corrupted STAT table in font too many multi_axis_styles (%d >= %d)", (int)num_of_multi_axis_styles, (int)PyTuple_GET_SIZE(multi_axis_styles)); return false; } RAII_PyObject(values, PyTuple_New(axis_index)); if (!values) return false; for (Py_ssize_t n = 0; n < PyTuple_GET_SIZE(values); n++) { uint16_t actual_axis_index = next; double value = load_fixed((uint32_t*)p); p += 2; PyObject *e = Py_BuildValue("{sH sd}", "design_index", actual_axis_index, "value", value); if (!e) return false; PyTuple_SET_ITEM(values, n, e); } PyObject *e = Py_BuildValue("{sH sN sO}", "flags", flags, "name", get_best_name(name_lookup_table, value_name_id), "values", values); if (!e) return false; PyTuple_SET_ITEM(multi_axis_styles, num_of_multi_axis_styles, e); num_of_multi_axis_styles++; } break; } } if (_PyTuple_Resize(&multi_axis_styles, num_of_multi_axis_styles) == -1) return false; ok: if (PyDict_SetItemString(output, "design_axes", design_axes) != 0) return false; if (PyDict_SetItemString(output, "multi_axis_styles", multi_axis_styles) != 0) return false; if (PyDict_SetItemString(output, "elided_fallback_name", elided_fallback_name_id ? get_best_name(name_lookup_table, elided_fallback_name_id) : PyUnicode_FromString("")) != 0) return false; return true; } bool read_fvar_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output) { RAII_PyObject(named_styles, PyTuple_New(0)); if (!named_styles) return false; RAII_PyObject(axes, PyTuple_New(0)); if (!axes) return false; if (!table || table_len < 14 * sizeof(uint16_t)) goto ok; 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 sN sO sN}", "minimum", minimum, "maximum", maximum, "default", def, "tag", PyUnicode_FromStringAndSize((const char*)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; ok: if (PyDict_SetItemString(output, "variations_postscript_name_prefix", get_best_name(name_lookup_table, 25)) != 0) return false; if (PyDict_SetItemString(output, "axes", axes) != 0) return false; if (PyDict_SetItemString(output, "named_styles", named_styles) != 0) return false; return true; } #undef next32 #undef next