Repository: harfbuzz/uharfbuzz (language: python)

Issue:
Obtaining `hb_font_extents_t`?
Currently the binding to `hb_font_get_extents_for_direction()` does not exist.

I think it's useful to implement `hb-view` ish utilities in Python. What are your thoughts?


Discussion / hints:
yes why not, would you like to work on a PR? 
Already done in a different way, a few years ago: https://github.com/HinTak/harfbuzz-python-demos
Thanks, I'll make a PR after having a look at @HinTak's implementation.

Fix patch:
diff --git a/src/uharfbuzz/_harfbuzz.pyx b/src/uharfbuzz/_harfbuzz.pyx
index 866ce8e..412c5f7 100644
--- a/src/uharfbuzz/_harfbuzz.pyx
+++ b/src/uharfbuzz/_harfbuzz.pyx
@@ -375,6 +375,9 @@ GlyphExtents = namedtuple(
     "GlyphExtents", ["x_bearing", "y_bearing", "width", "height"]
 )
 
+FontExtents = namedtuple(
+    "FontExtents", ["ascender", "descender", "line_gap"]
+)
 
 cdef class Font:
     cdef hb_font_t* _hb_font
@@ -464,6 +467,21 @@ cdef class Font:
         else:
             return None
 
+    def get_font_extents(self, direction: str):
+        cdef hb_font_extents_t extents
+        cdef hb_direction_t hb_direction
+        cdef bytes packed
+        packed = direction.encode()
+        hb_direction = hb_direction_from_string(<char*>packed, -1)
+        hb_font_get_extents_for_direction(
+            self._hb_font, hb_direction, &extents
+        )
+        return FontExtents(
+            extents.ascender,
+            extents.descender,
+            extents.line_gap
+        )
+
     def get_nominal_glyph(self, unicode: int):
         cdef hb_codepoint_t gid
         success = hb_font_get_nominal_glyph(self._hb_font, unicode, &gid)
@@ -572,6 +590,40 @@ cdef hb_bool_t _nominal_glyph_func(hb_font_t* font, void* font_data,
     return int(glyph[0] != 0)
 
 
+cdef hb_bool_t _font_h_extents_func(hb_font_t* font, void* font_data,
+                                    hb_font_extents_t *extents,
+                                    void* user_data):
+    cdef Font py_font = <Font>font_data
+    font_extents = (<FontFuncs>py_font.funcs)._font_h_extents_func(
+        py_font, <object>user_data)
+    if font_extents is not None:
+        if font_extents.ascender is not None:
+            extents.ascender = font_extents.ascender
+        if font_extents.descender is not None:
+            extents.descender = font_extents.descender
+        if font_extents.line_gap is not None:
+            extents.line_gap = font_extents.line_gap
+        return 1
+    return 0
+
+
+cdef hb_bool_t _font_v_extents_func(hb_font_t* font, void* font_data,
+                                    hb_font_extents_t *extents,
+                                    void* user_data):
+    cdef Font py_font = <Font>font_data
+    font_extents = (<FontFuncs>py_font.funcs)._font_v_extents_func(
+        py_font, <object>user_data)
+    if font_extents is not None:
+        if font_extents.ascender is not None:
+            extents.ascender = font_extents.ascender
+        if font_extents.descender is not None:
+            extents.descender = font_extents.descender
+        if font_extents.line_gap is not None:
+            extents.line_gap = font_extents.line_gap
+        return 1
+    return 0
+
+
 cdef class FontFuncs:
     cdef hb_font_funcs_t* _hb_ffuncs
     cdef object _glyph_h_advance_func
@@ -579,6 +631,8 @@ cdef class FontFuncs:
     cdef object _glyph_v_origin_func
     cdef object _glyph_name_func
     cdef object _nominal_glyph_func
+    cdef object _font_h_extents_func
+    cdef object _font_v_extents_func
 
     def __cinit__(self):
         self._hb_ffuncs = hb_font_funcs_create()
@@ -647,6 +701,25 @@ cdef class FontFuncs:
             self._hb_ffuncs, _nominal_glyph_func, <void*>user_data, NULL)
         self._nominal_glyph_func = func
 
+    def set_font_h_extents_func(self,
+                                func: Callable[[
+                                    Font,
+                                    object,  # user_data
+                                ], FontExtents],  # extents
+                                user_data: object) -> None:
+        hb_font_funcs_set_font_h_extents_func(
+            self._hb_ffuncs, _font_h_extents_func, <void*>user_data, NULL)
+        self._font_h_extents_func = func
+
+    def set_font_v_extents_func(self,
+                                func: Callable[[
+                                    Font,
+                                    object,  # user_data
+                                ], FontExtents],  # extents
+                                user_data: object) -> None:
+        hb_font_funcs_set_font_v_extents_func(
+            self._hb_ffuncs, _font_v_extents_func, <void*>user_data, NULL)
+        self._font_v_extents_func = func
 
 def shape(font: Font, buffer: Buffer,
         features: Dict[str,Union[int,bool,Sequence[Tuple[int,int,Union[int,bool]]]]] = None,
diff --git a/src/uharfbuzz/charfbuzz.pxd b/src/uharfbuzz/charfbuzz.pxd
index 7361c7d..fe73520 100644
--- a/src/uharfbuzz/charfbuzz.pxd
+++ b/src/uharfbuzz/charfbuzz.pxd
@@ -204,9 +204,28 @@ cdef extern from "hb.h":
         hb_codepoint_t glyph,
         char *name, unsigned int size,
         void *user_data)
+    ctypedef hb_bool_t (*hb_font_get_font_extents_func_t) (
+        hb_font_t *font, void *font_data,
+        hb_font_extents_t *extents,
+        void *user_data)
+    ctypedef hb_font_get_font_extents_func_t hb_font_get_font_h_extents_func_t;
+    ctypedef hb_font_get_font_extents_func_t hb_font_get_font_v_extents_func_t;
     ctypedef struct hb_variation_t:
         hb_tag_t tag
         float value
+    ctypedef struct hb_font_extents_t:
+        hb_position_t ascender
+        hb_position_t descender
+        hb_position_t line_gap
+        hb_position_t reserved9
+        hb_position_t reserved8
+        hb_position_t reserved7
+        hb_position_t reserved6
+        hb_position_t reserved5
+        hb_position_t reserved4
+        hb_position_t reserved3
+        hb_position_t reserved2
+        hb_position_t reserved1
 
     hb_font_funcs_t* hb_font_funcs_create()
     void hb_font_funcs_set_glyph_h_advance_func(
@@ -229,6 +248,14 @@ cdef extern from "hb.h":
         hb_font_funcs_t* ffuncs,
         hb_font_get_nominal_glyph_func_t func,
         void* user_data, hb_destroy_func_t destroy)
+    void hb_font_funcs_set_font_h_extents_func(
+        hb_font_funcs_t *ffuncs,
+        hb_font_get_font_h_extents_func_t func,
+        void *user_data, hb_destroy_func_t destroy)
+    void hb_font_funcs_set_font_v_extents_func(
+        hb_font_funcs_t *ffuncs,
+        hb_font_get_font_v_extents_func_t func,
+        void *user_data, hb_destroy_func_t destroy)
     void hb_font_funcs_destroy(hb_font_funcs_t* ffuncs)
 
     hb_font_t* hb_font_create(hb_face_t* face)
@@ -251,6 +278,10 @@ cdef extern from "hb.h":
         hb_font_t* font,
         hb_codepoint_t glyph,
         hb_glyph_extents_t *extents)
+    void hb_font_get_extents_for_direction(hb_font_t *font,
+        hb_direction_t direction, hb_font_extents_t *extents)
+    hb_bool_t hb_font_get_h_extents(hb_font_t *font, hb_font_extents_t *extents)
+    hb_bool_t hb_font_get_v_extents(hb_font_t *font, hb_font_extents_t *extents)
     hb_bool_t hb_font_get_nominal_glyph(
         hb_font_t *font,
         hb_codepoint_t unicode,


Test patch:
diff --git a/tests/test_uharfbuzz.py b/tests/test_uharfbuzz.py
index bf7c88e..d5821ef 100644
--- a/tests/test_uharfbuzz.py
+++ b/tests/test_uharfbuzz.py
@@ -173,6 +173,18 @@ def test_get_glyph_extents(self, opensans):
         assert -1468 == extents.height
         assert opensans.get_glyph_extents(1000) is None
 
+    def test_get_font_extents(self, blankfont):
+        extents = blankfont.get_font_extents('ltr')
+        assert (880, -120, 0) == extents
+        assert 880 == extents.ascender
+        assert -120 == extents.descender
+        assert 0 == extents.line_gap
+        extents = blankfont.get_font_extents('ttb')
+        assert (500, -500, 0) == extents
+        assert 500 == extents.ascender
+        assert -500 == extents.descender
+        assert 0 == extents.line_gap
+
     def test_get_glyph_name(self, blankfont):
         glyph_name = blankfont.get_glyph_name(1)
         assert glyph_name == "a"
@@ -386,6 +398,20 @@ def v_origin_func(font, gid, data):
         infos = [(pos.y_advance, pos.x_offset, pos.y_offset) for pos in buf.glyph_positions]
         assert infos == expected
 
+    def test_font_extents_funcs(self, blankfont):
+        def font_h_extents_func(font, data):
+            return hb.FontExtents(123, -456, 789)
+
+        def font_v_extents_func(font, data):
+            return hb.FontExtents(987, -654, 321)
+
+        funcs = hb.FontFuncs.create()
+        funcs.set_font_h_extents_func(font_h_extents_func, None)
+        funcs.set_font_v_extents_func(font_v_extents_func, None)
+        blankfont.funcs = funcs
+        assert (123, -456, 789) == blankfont.get_font_extents('ltr')
+        assert (987, -654, 321) == blankfont.get_font_extents('ttb')
+
     def test_message_func(self, blankfont):
         # Glyph IDs 1, 2, 3, 4, 5 map to glyphs a, b, c, d, e.
         # The calt feature replaces c by a in the context e, d, c', b, a.