Skip to content

Commit

Permalink
Merge pull request #219 from harfbuzz/hb-ot-name
Browse files Browse the repository at this point in the history
Bind hb-ot-name APIs
  • Loading branch information
khaledhosny authored Nov 23, 2024
2 parents 6d2d7d8 + cd43875 commit 1cd30d6
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 21 deletions.
105 changes: 85 additions & 20 deletions src/uharfbuzz/_harfbuzz.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ from .charfbuzz cimport *
from libc.stdlib cimport free, malloc, calloc
from libc.string cimport const_char
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_FromKindAndData
)
from typing import Callable, Dict, List, Sequence, Tuple, Union, NamedTuple
from pathlib import Path
from functools import wraps
Expand All @@ -15,22 +20,6 @@ from functools import wraps
DEF STATIC_ARRAY_SIZE = 128


cdef extern from "Python.h":
# PEP 393
bint PyUnicode_IS_READY(object u)
Py_ssize_t PyUnicode_GET_LENGTH(object u)
int PyUnicode_KIND(object u)
void* PyUnicode_DATA(object u)
ctypedef uint8_t Py_UCS1
ctypedef uint16_t Py_UCS2
Py_UCS1 PyUnicode_1BYTE_DATA(object u)
Py_UCS2 PyUnicode_2BYTE_DATA(object u)
Py_UCS4 PyUnicode_4BYTE_DATA(object u)
int PyUnicode_1BYTE_KIND
int PyUnicode_2BYTE_KIND
int PyUnicode_4BYTE_KIND


cdef int msgcallback(hb_buffer_t *buffer, hb_font_t *font, const char* message, void* userdata) noexcept:
ret = (<object>userdata)(message.decode('utf-8'))
if ret is None:
Expand Down Expand Up @@ -351,8 +340,6 @@ cdef class Buffer:

def add_str(self, text: str,
item_offset: int = 0, item_length: int = -1) -> None:
# ensure unicode string is in the "canonical" representation
assert PyUnicode_IS_READY(text)

cdef Py_ssize_t length = PyUnicode_GET_LENGTH(text)
cdef int kind = PyUnicode_KIND(text)
Expand Down Expand Up @@ -530,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
language: str | None


cdef class Face:
cdef hb_face_t* _hb_face
cdef object _reference_table_func
Expand Down Expand Up @@ -870,7 +891,7 @@ cdef class Face:
start_offset += language_count
return tags

def get_table_script_tags(face: Face, tag: str) -> List[str]:
def get_table_script_tags(self, tag: str) -> List[str]:
cdef bytes packed = tag.encode()
cdef hb_tag_t hb_tag = hb_tag_from_string(<char*>packed, -1)
cdef unsigned int script_count = STATIC_ARRAY_SIZE
Expand All @@ -881,7 +902,7 @@ cdef class Face:
cdef unsigned int start_offset = 0
while script_count == STATIC_ARRAY_SIZE:
hb_ot_layout_table_get_script_tags(
face._hb_face,
self._hb_face,
hb_tag,
start_offset,
&script_count,
Expand All @@ -894,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 = <hb_language_t>0 # HB_LANGUAGE_INVALID
else:
packed = language.encode()
lang = hb_language_from_string(<char*>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 = <uint32_t*>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
Expand Down
26 changes: 26 additions & 0 deletions src/uharfbuzz/charfbuzz.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
114 changes: 113 additions & 1 deletion tests/test_uharfbuzz.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand All @@ -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
Expand Down Expand Up @@ -673,6 +685,106 @@ 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.FULL_NAME, None, "Adobe Blank"),
(hb.OTNameIdPredefined.FULL_NAME, "ar", None),
(hb.OTNameIdPredefined.INVALID, "en", None),
(hb.OTNameIdPredefined.INVALID, None, None),
(hb.OTNameIdPredefined.DESCRIPTION, None, 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):
face = blankfont.face
names = face.list_names()
assert 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"),
]

assert [face.get_name(*name) for name in names] == [
"Copyright © 2013, 2015 Adobe Systems Incorporated "
"(http://www.adobe.com/).",
"Adobe Blank",
"Regular",
"1.045;ADBO;AdobeBlank;ADOBE",
"Adobe Blank",
"Version 1.045;PS 1.045;hotconv 1.0.82;makeotf.lib2.5.63406",
"AdobeBlank",
]

def test_list_names_with_user_names(self, mutatorsans):
face = mutatorsans.face
names = face.list_names()
assert 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"),
]

assert [face.get_name(*name) for name in names] == [
"License same as MutatorMath. BSD 3-clause. [test-token: C]",
"Regular",
"1.002;LTTR;MutatorMathTest-LightCondensed",
"MutatorMathTest LightCondensed",
"Version 1.002",
"MutatorMathTest-LightCondensed",
"Width",
"Weight",
"LightCondensed",
"MutatorMathTest-LightCondensed",
"BoldCondensed",
"MutatorMathTest-BoldCondensed",
"LightWide",
"MutatorMathTest-LightWide",
"BoldWide",
"MutatorMathTest-BoldWide",
"Medium_Narrow_I",
"MutatorMathTest-Medium_Narrow_I",
"Two",
"MutatorMathTest-Two",
"One",
"MutatorMathTest-One",
"width_794.52_weight_775.61",
"MutatorSans-width_794.52_weight_775.61",
"width_93.05_weight_658.60",
"Medium_Wide_I",
"MutatorMathTest-Medium_Narrow_I",
]


class TestFont:
def test_get_glyph_extents(self, opensans):
Expand Down

0 comments on commit 1cd30d6

Please sign in to comment.