From 4d4989d7717fa8964ff23c1597bd9aa3b418d571 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sat, 23 Nov 2024 06:46:15 +0200 Subject: [PATCH] Bind hb-ot-name APIs --- src/uharfbuzz/_harfbuzz.pyx | 80 ++++++++++++++++++++++++++++++++++++- src/uharfbuzz/charfbuzz.pxd | 26 ++++++++++++ tests/test_uharfbuzz.py | 79 +++++++++++++++++++++++++++++++++++- 3 files changed, 183 insertions(+), 2 deletions(-) diff --git a/src/uharfbuzz/_harfbuzz.pyx b/src/uharfbuzz/_harfbuzz.pyx index ef1b912..e8b187e 100644 --- a/src/uharfbuzz/_harfbuzz.pyx +++ b/src/uharfbuzz/_harfbuzz.pyx @@ -10,7 +10,7 @@ from cpython.pycapsule cimport PyCapsule_GetPointer, PyCapsule_IsValid from cpython.unicode cimport ( PyUnicode_1BYTE_DATA, PyUnicode_2BYTE_DATA, PyUnicode_4BYTE_DATA, PyUnicode_1BYTE_KIND, PyUnicode_2BYTE_KIND, PyUnicode_4BYTE_KIND, - PyUnicode_KIND, PyUnicode_GET_LENGTH + PyUnicode_KIND, PyUnicode_GET_LENGTH, PyUnicode_FromKindAndData ) from typing import Callable, Dict, List, Sequence, Tuple, Union, NamedTuple from pathlib import Path @@ -517,6 +517,40 @@ cdef hb_blob_t* _reference_table_func( table, len(table), HB_MEMORY_MODE_READONLY, NULL, NULL) +class OTNameIdPredefined(IntEnum): + COPYRIGHT = HB_OT_NAME_ID_COPYRIGHT + FONT_FAMILY = HB_OT_NAME_ID_FONT_FAMILY + FONT_SUBFAMILY = HB_OT_NAME_ID_FONT_SUBFAMILY + UNIQUE_ID = HB_OT_NAME_ID_UNIQUE_ID + FULL_NAME = HB_OT_NAME_ID_FULL_NAME + VERSION_STRING = HB_OT_NAME_ID_VERSION_STRING + POSTSCRIPT_NAME = HB_OT_NAME_ID_POSTSCRIPT_NAME + TRADEMARK = HB_OT_NAME_ID_TRADEMARK + MANUFACTURER = HB_OT_NAME_ID_MANUFACTURER + DESIGNER = HB_OT_NAME_ID_DESIGNER + DESCRIPTION = HB_OT_NAME_ID_DESCRIPTION + VENDOR_URL = HB_OT_NAME_ID_VENDOR_URL + DESIGNER_URL = HB_OT_NAME_ID_DESIGNER_URL + LICENSE = HB_OT_NAME_ID_LICENSE + LICENSE_URL = HB_OT_NAME_ID_LICENSE_URL + TYPOGRAPHIC_FAMILY = HB_OT_NAME_ID_TYPOGRAPHIC_FAMILY + TYPOGRAPHIC_SUBFAMILY = HB_OT_NAME_ID_TYPOGRAPHIC_SUBFAMILY + MAC_FULL_NAME = HB_OT_NAME_ID_MAC_FULL_NAME + SAMPLE_TEXT = HB_OT_NAME_ID_SAMPLE_TEXT + CID_FINDFONT_NAME = HB_OT_NAME_ID_CID_FINDFONT_NAME + WWS_FAMILY = HB_OT_NAME_ID_WWS_FAMILY + WWS_SUBFAMILY = HB_OT_NAME_ID_WWS_SUBFAMILY + LIGHT_BACKGROUND = HB_OT_NAME_ID_LIGHT_BACKGROUND + DARK_BACKGROUND = HB_OT_NAME_ID_DARK_BACKGROUND + VARIATIONS_PS_PREFIX = HB_OT_NAME_ID_VARIATIONS_PS_PREFIX + INVALID = HB_OT_NAME_ID_INVALID + + +class OTNameEntry(NamedTuple): + name_id: OTNameIdPredefined | int | None + language: str | None + + cdef class Face: cdef hb_face_t* _hb_face cdef object _reference_table_func @@ -881,6 +915,50 @@ cdef class Face: start_offset += script_count return tags + def list_names(self) -> List[OTNameEntry]: + cdef list ret = [] + cdef unsigned int num_entries + cdef const hb_ot_name_entry_t* entries + cdef unsigned int i + cdef const_char *cstr + cdef bytes packed + + entries = hb_ot_name_list_names(self._hb_face, &num_entries) + for i in range(num_entries): + cstr = hb_language_to_string(entries[i].language) + if cstr is NULL: + language = None + else: + packed = cstr + language = packed.decode() + if entries[i].name_id in iter(OTNameIdPredefined): + name_id = OTNameIdPredefined(entries[i].name_id) + else: + name_id = entries[i].name_id + ret.append(OTNameEntry(name_id=name_id, language=language)) + return ret + + def get_name(self, name_id: OTNameIdPredefined | int, language: str | None = None) -> str | None: + cdef bytes packed + cdef hb_language_t lang + cdef uint32_t *text + cdef unsigned int length + + if language is None: + lang = 0 # HB_LANGUAGE_INVALID + else: + packed = language.encode() + lang = hb_language_from_string(packed, -1) + + length = hb_ot_name_get_utf32(self._hb_face, name_id, lang, NULL, NULL) + if length: + length += 1 # for the null terminator + text = malloc(length * sizeof(uint32_t)) + hb_ot_name_get_utf32(self._hb_face, name_id, lang, &length, text) + return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, text, length) + return None + + class GlyphExtents(NamedTuple): x_bearing: int y_bearing: int diff --git a/src/uharfbuzz/charfbuzz.pxd b/src/uharfbuzz/charfbuzz.pxd index e8ba59b..9dc525f 100644 --- a/src/uharfbuzz/charfbuzz.pxd +++ b/src/uharfbuzz/charfbuzz.pxd @@ -984,6 +984,32 @@ cdef extern from "hb-ot.h": HB_OT_NAME_ID_VARIATIONS_PS_PREFIX HB_OT_NAME_ID_INVALID + ctypedef struct hb_ot_name_entry_t: + hb_ot_name_id_t name_id + hb_language_t language + + const hb_ot_name_entry_t *hb_ot_name_list_names( + hb_face_t *face, + unsigned int *num_entries) # OUT. May be NULL. + unsigned int hb_ot_name_get_utf16( + hb_face_t *face, + hb_ot_name_id_t name_id, + hb_language_t language, + unsigned int *text_size, # IN/OUT. May be NULL. + uint16_t *text) # OUT. + unsigned int hb_ot_name_get_utf32( + hb_face_t *face, + hb_ot_name_id_t name_id, + hb_language_t language, + unsigned int *text_size, # IN/OUT. May be NULL. + uint32_t *text) # OUT. + unsigned int hb_ot_name_get_utf8( + hb_face_t *face, + hb_ot_name_id_t name_id, + hb_language_t language, + unsigned int *text_size, # IN/OUT. May be NULL. + char *text) # OUT. + # hb-ot-color.h hb_bool_t hb_ot_color_has_palettes(hb_face_t *face) unsigned int hb_ot_color_palette_get_count(hb_face_t *face) diff --git a/tests/test_uharfbuzz.py b/tests/test_uharfbuzz.py index a4dccfe..087703a 100644 --- a/tests/test_uharfbuzz.py +++ b/tests/test_uharfbuzz.py @@ -273,7 +273,8 @@ def test_axis_infos(self, mutatorsans): def test_named_instances(self, mutatorsans): face = mutatorsans.face - assert face.named_instances == [ + named_instances = face.named_instances + assert named_instances == [ (258, 259, [0.0, 0.0]), (260, 261, [0.0, 1000.0]), (262, 263, [1000.0, 0.0]), @@ -286,6 +287,17 @@ def test_named_instances(self, mutatorsans): (275, 276, [328.0, 500.0]), ] + assert face.get_name(named_instances[0].subfamily_name_id) == "LightCondensed" + assert ( + face.get_name(named_instances[0].postscript_name_id) + == "MutatorMathTest-LightCondensed" + ) + assert face.get_name(named_instances[-1].subfamily_name_id) == "Medium_Wide_I" + assert ( + face.get_name(named_instances[-1].postscript_name_id) + == "MutatorMathTest-Medium_Narrow_I" + ) + def test_has_math_data(self, blankfont, mathfont): assert blankfont.face.has_math_data == False assert mathfont.face.has_math_data == True @@ -673,6 +685,71 @@ def test_has_layout_substitution(self, opensans): def test_has_no_layout_substitution(self, mathfont): assert mathfont.face.has_layout_substitution == False + @pytest.mark.parametrize( + "name_id, language, expected", + [ + ( + hb.OTNameIdPredefined.COPYRIGHT, + None, + "Copyright © 2013, 2015 Adobe Systems Incorporated (http://www.adobe.com/).", + ), + (hb.OTNameIdPredefined.FONT_FAMILY, "en", "Adobe Blank"), + (hb.OTNameIdPredefined.FONT_SUBFAMILY, None, "Regular"), + (hb.OTNameIdPredefined.UNIQUE_ID, None, "1.045;ADBO;AdobeBlank;ADOBE"), + (hb.OTNameIdPredefined.FULL_NAME, None, "Adobe Blank"), + ( + hb.OTNameIdPredefined.VERSION_STRING, + "en", + "Version 1.045;PS 1.045;hotconv 1.0.82;makeotf.lib2.5.63406", + ), + (hb.OTNameIdPredefined.FULL_NAME, "ar", None), + ], + ) + def test_get_name(self, blankfont, name_id, language, expected): + assert blankfont.face.get_name(name_id, language) == expected + + def test_list_names(self, blankfont): + assert blankfont.face.list_names() == [ + (hb.OTNameIdPredefined.COPYRIGHT, "en"), + (hb.OTNameIdPredefined.FONT_FAMILY, "en"), + (hb.OTNameIdPredefined.FONT_SUBFAMILY, "en"), + (hb.OTNameIdPredefined.UNIQUE_ID, "en"), + (hb.OTNameIdPredefined.FULL_NAME, "en"), + (hb.OTNameIdPredefined.VERSION_STRING, "en"), + (hb.OTNameIdPredefined.POSTSCRIPT_NAME, "en"), + ] + + def test_list_names_with_user_names(self, mutatorsans): + assert mutatorsans.face.list_names() == [ + (hb.OTNameIdPredefined.COPYRIGHT, "en"), + (hb.OTNameIdPredefined.FONT_SUBFAMILY, "en"), + (hb.OTNameIdPredefined.UNIQUE_ID, "en"), + (hb.OTNameIdPredefined.FULL_NAME, "en"), + (hb.OTNameIdPredefined.VERSION_STRING, "en"), + (hb.OTNameIdPredefined.POSTSCRIPT_NAME, "en"), + (256, "en"), + (257, "en"), + (258, "en"), + (259, "en"), + (260, "en"), + (261, "en"), + (262, "en"), + (263, "en"), + (264, "en"), + (265, "en"), + (266, "en"), + (267, "en"), + (268, "en"), + (269, "en"), + (270, "en"), + (271, "en"), + (272, "en"), + (273, "en"), + (274, "en"), + (275, "en"), + (276, "en"), + ] + class TestFont: def test_get_glyph_extents(self, opensans):