From 9b17dcadee43ee6b51d10f4da7b860edf82532cd Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Mon, 16 Sep 2024 17:41:28 +0300 Subject: [PATCH] Make hb-ot-layout API a little more Pythonic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of module-level ot_layout_* functions, make them methods of Face or Font classes, as appropriate. A couple of functions that don’t take Face or Font as input were left unchanged. --- src/uharfbuzz/_harfbuzz.pyx | 299 ++++++++++++++++++++++-------------- tests/test_uharfbuzz.py | 147 +++++++++--------- 2 files changed, 250 insertions(+), 196 deletions(-) diff --git a/src/uharfbuzz/_harfbuzz.pyx b/src/uharfbuzz/_harfbuzz.pyx index 6dff6b9..77b8880 100644 --- a/src/uharfbuzz/_harfbuzz.pyx +++ b/src/uharfbuzz/_harfbuzz.pyx @@ -502,6 +502,14 @@ class OTColorLayer(NamedTuple): color_index: int +class OTLayoutGlyphClass(IntEnum): + UNCLASSIFIED = HB_OT_LAYOUT_GLYPH_CLASS_UNCLASSIFIED + BASE_GLYPH = HB_OT_LAYOUT_GLYPH_CLASS_BASE_GLYPH + LIGATURE = HB_OT_LAYOUT_GLYPH_CLASS_LIGATURE + MARK = HB_OT_LAYOUT_GLYPH_CLASS_MARK + COMPONENT = HB_OT_LAYOUT_GLYPH_CLASS_COMPONENT + + cdef hb_user_data_key_t k @@ -779,6 +787,112 @@ cdef class Face: def has_color_png(self) -> bool: return hb_ot_color_has_png(self._hb_face) + # layout + @property + def has_layout_glyph_classes(self) -> bool: + return hb_ot_layout_has_glyph_classes(self._hb_face) + + def get_layout_glyph_class(self, glyph: int) -> OTLayoutGlyphClass: + return OTLayoutGlyphClass(hb_ot_layout_get_glyph_class(self._hb_face, glyph)) + + @property + def has_layout_positioning(self) -> bool: + return hb_ot_layout_has_positioning(self._hb_face) + + @property + def has_layout_substitution(self) -> bool: + return hb_ot_layout_has_substitution(self._hb_face) + + def get_lookup_glyph_alternates(self, lookup_index: int, glyph: int) -> List[int]: + cdef list alternates = [] + cdef unsigned int i + cdef unsigned int start_offset = 0 + cdef unsigned int alternate_count = STATIC_ARRAY_SIZE + cdef hb_codepoint_t c_alternates[STATIC_ARRAY_SIZE] + while alternate_count == STATIC_ARRAY_SIZE: + hb_ot_layout_lookup_get_glyph_alternates(self._hb_face, lookup_index, glyph, start_offset, + &alternate_count, c_alternates) + for i in range(alternate_count): + alternates.append(c_alternates[i]) + start_offset += alternate_count + return alternates + + def get_language_feature_tags(self, + tag: str, + script_index: int = 0, + language_index: int = 0xFFFF) -> List[str]: + cdef bytes packed = tag.encode() + cdef hb_tag_t hb_tag = hb_tag_from_string(packed, -1) + cdef unsigned int feature_count = STATIC_ARRAY_SIZE + cdef hb_tag_t c_tags[STATIC_ARRAY_SIZE] + cdef list tags = [] + cdef char cstr[5] + cdef unsigned int i + cdef unsigned int start_offset = 0 + while feature_count == STATIC_ARRAY_SIZE: + hb_ot_layout_language_get_feature_tags( + self._hb_face, + hb_tag, script_index, + language_index, + start_offset, + &feature_count, + c_tags) + for i in range(feature_count): + hb_tag_to_string(c_tags[i], cstr) + cstr[4] = b'\0' + packed = cstr + tags.append(packed.decode()) + start_offset += feature_count + return tags + + def get_script_language_tags(self, tag: str, script_index: int = 0) -> List[str]: + cdef bytes packed = tag.encode() + cdef hb_tag_t hb_tag = hb_tag_from_string(packed, -1) + cdef unsigned int language_count = STATIC_ARRAY_SIZE + cdef hb_tag_t c_tags[STATIC_ARRAY_SIZE] + cdef list tags = [] + cdef char cstr[5] + cdef unsigned int i + cdef unsigned int start_offset = 0 + while language_count == STATIC_ARRAY_SIZE: + hb_ot_layout_script_get_language_tags( + self._hb_face, + hb_tag, + script_index, + start_offset, + &language_count, + c_tags) + for i in range(language_count): + hb_tag_to_string(c_tags[i], cstr) + cstr[4] = b'\0' + packed = cstr + tags.append(packed.decode()) + start_offset += language_count + return tags + + def get_table_script_tags(face: Face, tag: str) -> List[str]: + cdef bytes packed = tag.encode() + cdef hb_tag_t hb_tag = hb_tag_from_string(packed, -1) + cdef unsigned int script_count = STATIC_ARRAY_SIZE + cdef hb_tag_t c_tags[STATIC_ARRAY_SIZE] + cdef list tags = [] + cdef char cstr[5] + cdef unsigned int i + cdef unsigned int start_offset = 0 + while script_count == STATIC_ARRAY_SIZE: + hb_ot_layout_table_get_script_tags( + face._hb_face, + hb_tag, + start_offset, + &script_count, + c_tags) + for i in range(script_count): + hb_tag_to_string(c_tags[i], cstr) + cstr[4] = b'\0' + packed = cstr + tags.append(packed.decode()) + start_offset += script_count + return tags class GlyphExtents(NamedTuple): x_bearing: int @@ -1344,6 +1458,54 @@ cdef class Font: blob = hb_ot_color_glyph_reference_png(self._hb_font, glyph) return Blob.from_ptr(blob) + #layout + def get_layout_baseline(self, + baseline_tag: str, + direction: str, + script_tag: str, + language_tag: str) -> int: + cdef hb_ot_layout_baseline_tag_t hb_baseline_tag + cdef hb_direction_t hb_direction + cdef hb_tag_t hb_script_tag + cdef hb_tag_t hb_language_tag + cdef hb_position_t hb_position + cdef hb_bool_t success + cdef bytes packed + + if baseline_tag == "romn": + hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_ROMAN + elif baseline_tag == "hang": + hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_HANGING + elif baseline_tag == "icfb": + hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_IDEO_FACE_BOTTOM_OR_LEFT + elif baseline_tag == "icft": + hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_IDEO_FACE_TOP_OR_RIGHT + elif baseline_tag == "ideo": + hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_IDEO_EMBOX_BOTTOM_OR_LEFT + elif baseline_tag == "idtp": + hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_IDEO_EMBOX_TOP_OR_RIGHT + elif baseline_tag == "math": + hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_MATH + else: + raise ValueError(f"invalid baseline tag '{baseline_tag}'") + packed = direction.encode() + hb_direction = hb_direction_from_string(packed, -1) + packed = script_tag.encode() + hb_script_tag = hb_tag_from_string(packed, -1) + packed = language_tag.encode() + hb_language_tag = hb_tag_from_string(packed, -1) + success = hb_ot_layout_get_baseline(self._hb_font, + hb_baseline_tag, + hb_direction, + hb_script_tag, + hb_language_tag, + &hb_position) + if success: + return hb_position + else: + return None + + cdef struct _pen_methods: void *moveTo void *lineTo @@ -1680,152 +1842,51 @@ def ot_tag_to_language(tag: str) -> str: return packed.decode() +@deprecated("Face.get_lookup_glyph_alternates()") def ot_layout_lookup_get_glyph_alternates( face: Face, lookup_index : int, glyph : hb_codepoint_t) -> List[int]: - cdef list alternates = [] - cdef unsigned int i - cdef unsigned int start_offset = 0 - cdef unsigned int alternate_count = STATIC_ARRAY_SIZE - cdef hb_codepoint_t alternate_glyphs[STATIC_ARRAY_SIZE] - while alternate_count == STATIC_ARRAY_SIZE: - hb_ot_layout_lookup_get_glyph_alternates(face._hb_face, lookup_index, glyph, start_offset, - &alternate_count, alternate_glyphs) - for i in range(alternate_count): - alternates.append(alternate_glyphs[i]) - start_offset += alternate_count - return alternates + return face.get_lookup_glyph_alternates(lookup_index, glyph) +@deprecated("Face.get_language_feature_tags()") def ot_layout_language_get_feature_tags( face: Face, tag: str, script_index: int = 0, language_index: int = 0xFFFF) -> List[str]: - cdef bytes packed = tag.encode() - cdef hb_tag_t hb_tag = hb_tag_from_string(packed, -1) - cdef unsigned int feature_count = STATIC_ARRAY_SIZE - cdef hb_tag_t feature_tags[STATIC_ARRAY_SIZE] - cdef list tags = [] - cdef char cstr[5] - cdef unsigned int i - cdef unsigned int start_offset = 0 - while feature_count == STATIC_ARRAY_SIZE: - hb_ot_layout_language_get_feature_tags( - face._hb_face, hb_tag, script_index, language_index, start_offset, &feature_count, - feature_tags) - for i in range(feature_count): - hb_tag_to_string(feature_tags[i], cstr) - cstr[4] = b'\0' - packed = cstr - tags.append(packed.decode()) - start_offset += feature_count - return tags + return face.get_language_feature_tags(tag, script_index, language_index) +@deprecated("Face.get_script_language_tags()") def ot_layout_script_get_language_tags( face: Face, tag: str, script_index: int = 0) -> List[str]: - cdef bytes packed = tag.encode() - cdef hb_tag_t hb_tag = hb_tag_from_string(packed, -1) - cdef unsigned int language_count = STATIC_ARRAY_SIZE - cdef hb_tag_t language_tags[STATIC_ARRAY_SIZE] - cdef list tags = [] - cdef char cstr[5] - cdef unsigned int i - cdef unsigned int start_offset = 0 - while language_count == STATIC_ARRAY_SIZE: - hb_ot_layout_script_get_language_tags( - face._hb_face, hb_tag, script_index, start_offset, &language_count, language_tags) - for i in range(language_count): - hb_tag_to_string(language_tags[i], cstr) - cstr[4] = b'\0' - packed = cstr - tags.append(packed.decode()) - start_offset += language_count - return tags + return face.get_script_language_tags(tag, script_index) +@deprecated("Face.get_table_script_tags()") def ot_layout_table_get_script_tags(face: Face, tag: str) -> List[str]: - cdef bytes packed = tag.encode() - cdef hb_tag_t hb_tag = hb_tag_from_string(packed, -1) - cdef unsigned int script_count = STATIC_ARRAY_SIZE - cdef hb_tag_t script_tags[STATIC_ARRAY_SIZE] - cdef list tags = [] - cdef char cstr[5] - cdef unsigned int i - cdef unsigned int start_offset = 0 - while script_count == STATIC_ARRAY_SIZE: - hb_ot_layout_table_get_script_tags( - face._hb_face, hb_tag, start_offset, &script_count, script_tags) - for i in range(script_count): - hb_tag_to_string(script_tags[i], cstr) - cstr[4] = b'\0' - packed = cstr - tags.append(packed.decode()) - start_offset += script_count - return tags + return face.get_table_script_tags(tag) +@deprecated("Face.get_layout_baseline()") def ot_layout_get_baseline(font: Font, baseline_tag: str, direction: str, script_tag: str, language_tag: str) -> int: - cdef hb_ot_layout_baseline_tag_t hb_baseline_tag - cdef hb_direction_t hb_direction - cdef hb_tag_t hb_script_tag - cdef hb_tag_t hb_language_tag - cdef hb_position_t hb_position - cdef hb_bool_t success - cdef bytes packed - - if baseline_tag == "romn": - hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_ROMAN - elif baseline_tag == "hang": - hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_HANGING - elif baseline_tag == "icfb": - hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_IDEO_FACE_BOTTOM_OR_LEFT - elif baseline_tag == "icft": - hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_IDEO_FACE_TOP_OR_RIGHT - elif baseline_tag == "ideo": - hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_IDEO_EMBOX_BOTTOM_OR_LEFT - elif baseline_tag == "idtp": - hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_IDEO_EMBOX_TOP_OR_RIGHT - elif baseline_tag == "math": - hb_baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_MATH - else: - raise ValueError(f"invalid baseline tag '{baseline_tag}'") - packed = direction.encode() - hb_direction = hb_direction_from_string(packed, -1) - packed = script_tag.encode() - hb_script_tag = hb_tag_from_string(packed, -1) - packed = language_tag.encode() - hb_language_tag = hb_tag_from_string(packed, -1) - success = hb_ot_layout_get_baseline(font._hb_font, - hb_baseline_tag, - hb_direction, - hb_script_tag, - hb_language_tag, - &hb_position) - if success: - return hb_position - else: - return None + return font.get_layout_baseline(baseline_tag, direction, script_tag, language_tag) +@deprecated("Face.face.has_layout_glyph_classes") def ot_layout_has_glyph_classes(face: Face) -> bool: - return hb_ot_layout_has_glyph_classes(face._hb_face) + return face.has_layout_glyph_classes +@deprecated("Face.has_layout_positioning") def ot_layout_has_positioning(face: Face) -> bool: - return hb_ot_layout_has_positioning(face._hb_face) + return face.has_layout_positioning +@deprecated("Face.has_layout_substitution") def ot_layout_has_substitution(face: Face) -> bool: - return hb_ot_layout_has_substitution(face._hb_face) - -class OTLayoutGlyphClass(IntEnum): - UNCLASSIFIED = HB_OT_LAYOUT_GLYPH_CLASS_UNCLASSIFIED - BASE_GLYPH = HB_OT_LAYOUT_GLYPH_CLASS_BASE_GLYPH - LIGATURE = HB_OT_LAYOUT_GLYPH_CLASS_LIGATURE - MARK = HB_OT_LAYOUT_GLYPH_CLASS_MARK - COMPONENT = HB_OT_LAYOUT_GLYPH_CLASS_COMPONENT + return face.has_layout_substitution +@deprecated("Face.get_layout_glyph_class()") def ot_layout_get_glyph_class(face: Face, glyph: int) -> OTLayoutGlyphClass: - return OTLayoutGlyphClass(hb_ot_layout_get_glyph_class(face._hb_face, glyph)) - + return face.get_layout_glyph_class(glyph) @deprecated("Face.has_color_palettes") def ot_color_has_palettes(face: Face) -> bool: diff --git a/tests/test_uharfbuzz.py b/tests/test_uharfbuzz.py index b7394f9..a4dccfe 100644 --- a/tests/test_uharfbuzz.py +++ b/tests/test_uharfbuzz.py @@ -637,6 +637,42 @@ def test_has_color_svg(self, blankfont): def test_has_color_png(self, blankfont): assert blankfont.face.has_color_png == False + def test_get_language_feature_tags(self, blankfont): + assert blankfont.face.get_language_feature_tags("GPOS") == ["kern"] + assert blankfont.face.get_language_feature_tags("GSUB") == ["calt"] + + def test_get_table_script_tags(self, blankfont): + assert blankfont.face.get_table_script_tags("GPOS") == ["DFLT"] + + def test_script_get_language_tags(self, blankfont): + assert blankfont.face.get_script_language_tags("GPOS", 0) == [] + + def test_lookup_get_glyph_alternates(self, blankfont): + gid = blankfont.get_nominal_glyph(ord("c")) + assert blankfont.face.get_lookup_glyph_alternates(1, gid) == [1] + + def test_has_layout_glyph_classes(self, opensans): + assert opensans.face.has_layout_glyph_classes + + def test_has_no_layout_glyph_classes(self, blankfont): + assert blankfont.face.has_layout_glyph_classes == False + + def test_get_layout_glyph_class(self, opensans): + glyph_class = opensans.face.get_layout_glyph_class(1) + assert glyph_class == hb.OTLayoutGlyphClass.BASE_GLYPH + + def test_has_layout_positioning(self, opensans): + assert opensans.face.has_layout_positioning + + def test_has_no_positioning(self, mathfont): + assert mathfont.face.has_layout_positioning == False + + def test_has_layout_substitution(self, opensans): + assert opensans.face.has_layout_substitution + + def test_has_no_layout_substitution(self, mathfont): + assert mathfont.face.has_layout_substitution == False + class TestFont: def test_get_glyph_extents(self, opensans): @@ -721,6 +757,40 @@ def test_get_glyph_color_png(self, blankfont): blob = blankfont.get_glyph_color_png(1) assert len(blob) == 0 + # The test font contains a BASE table with some test values + def test_get_layout_baseline_invalid_tag(self, blankfont): + with pytest.raises(ValueError): + # invalid baseline tag + baseline = blankfont.get_layout_baseline("xxxx", "LTR", "", "") + + @pytest.mark.parametrize( + "baseline_tag, script_tag, direction, expected_value", + [ + ("icfb", "grek", "LTR", None), # BASE table doesn't contain grek script + ("icfb", "latn", "LTR", -70), + ("icft", "latn", "LTR", 830), + ("romn", "latn", "LTR", 0), + ("ideo", "latn", "LTR", -120), + ("icfb", "kana", "LTR", -71), + ("icft", "kana", "LTR", 831), + ("romn", "kana", "LTR", 1), + ("ideo", "kana", "LTR", -121), + ("icfb", "latn", "TTB", 50), + ("icft", "latn", "TTB", 950), + ("romn", "latn", "TTB", 120), + ("ideo", "latn", "TTB", 0), + ("icfb", "kana", "TTB", 51), + ("icft", "kana", "TTB", 951), + ("romn", "kana", "TTB", 121), + ("ideo", "kana", "TTB", 1), + ], + ) + def test_get_layout_baseline( + self, blankfont, baseline_tag, script_tag, direction, expected_value + ): + value = blankfont.get_layout_baseline(baseline_tag, direction, script_tag, "") + assert value == expected_value + class TestShape: @pytest.mark.parametrize( @@ -1339,83 +1409,6 @@ def message(self, message): class TestOTLayout: - # The test font contains a BASE table with some test values - def test_ot_layout_get_baseline_invalid_tag(self, blankfont): - with pytest.raises(ValueError): - # invalid baseline tag - baseline = hb.ot_layout_get_baseline(blankfont, "xxxx", "LTR", "", "") - - @pytest.mark.parametrize( - "baseline_tag, script_tag, direction, expected_value", - [ - ("icfb", "grek", "LTR", None), # BASE table doesn't contain grek script - ("icfb", "latn", "LTR", -70), - ("icft", "latn", "LTR", 830), - ("romn", "latn", "LTR", 0), - ("ideo", "latn", "LTR", -120), - ("icfb", "kana", "LTR", -71), - ("icft", "kana", "LTR", 831), - ("romn", "kana", "LTR", 1), - ("ideo", "kana", "LTR", -121), - ("icfb", "latn", "TTB", 50), - ("icft", "latn", "TTB", 950), - ("romn", "latn", "TTB", 120), - ("ideo", "latn", "TTB", 0), - ("icfb", "kana", "TTB", 51), - ("icft", "kana", "TTB", 951), - ("romn", "kana", "TTB", 121), - ("ideo", "kana", "TTB", 1), - ], - ) - def test_ot_layout_get_baseline( - self, blankfont, baseline_tag, script_tag, direction, expected_value - ): - value = hb.ot_layout_get_baseline( - blankfont, baseline_tag, direction, script_tag, "" - ) - assert value == expected_value - - def test_ot_layout_language_get_feature_tags(self, blankfont): - tags = hb.ot_layout_language_get_feature_tags(blankfont.face, "GPOS") - assert tags == ["kern"] - tags = hb.ot_layout_language_get_feature_tags(blankfont.face, "GSUB") - assert tags == ["calt"] - - def test_ot_layout_table_get_script_tags(self, blankfont): - tags = hb.ot_layout_table_get_script_tags(blankfont.face, "GPOS") - assert tags == ["DFLT"] - - def test_ot_layout_script_get_language_tags(self, blankfont): - tags = hb.ot_layout_script_get_language_tags(blankfont.face, "GPOS", 0) - assert tags == [] - - def test_ot_layout_lookup_get_glyph_alternates(self, blankfont): - gid = blankfont.get_nominal_glyph(ord("c")) - alternates = hb.ot_layout_lookup_get_glyph_alternates(blankfont.face, 1, gid) - assert alternates == [1] - - def test_ot_layout_has_glyph_classes(self, opensans): - assert hb.ot_layout_has_glyph_classes(opensans.face) - - def test_ot_layout_has_no_glyph_classes(self, blankfont): - assert hb.ot_layout_has_glyph_classes(blankfont.face) == False - - def test_ot_layout_get_glyph_class(self, opensans): - glyph_class = hb.ot_layout_get_glyph_class(opensans.face, 1) - assert glyph_class == hb.OTLayoutGlyphClass.BASE_GLYPH - - def test_ot_layout_has_positioning(self, opensans): - assert hb.ot_layout_has_positioning(opensans.face) - - def test_ot_layout_has_no_positioning(self, mathfont): - assert hb.ot_layout_has_positioning(mathfont.face) == False - - def test_ot_layout_has_substitution(self, opensans): - assert hb.ot_layout_has_substitution(opensans.face) - - def test_ot_layout_has_no_substitution(self, mathfont): - assert hb.ot_layout_has_substitution(mathfont.face) == False - def test_ot_tag_to_script(self): assert hb.ot_tag_to_script("mym2") == "Mymr"