Repository: harfbuzz/uharfbuzz (language: python)

Issue:
glyph indexes
uharfbuzz has only support to add glyphs to a buffer as text or as a list of unicodes: `add_codepoints`, `add_str`, `add_utf8` but not as glyph indexes this would be super handy... 

I dont know if harfbuzz its self has support for this

see https://github.com/typemytype/featurePreviewRoboFontExtension/issues/8
glyph indexes
uharfbuzz has only support to add glyphs to a buffer as text or as a list of unicodes: `add_codepoints`, `add_str`, `add_utf8` but not as glyph indexes this would be super handy... 

I dont know if harfbuzz its self has support for this

see https://github.com/typemytype/featurePreviewRoboFontExtension/issues/8


Discussion / hints:
I don’t know if uharfbuzz exposes enpugh of HarfBuzz API to do this, but here a C file that shows how to do this trick:
```c
#include <hb.h>
#include <stdbool.h>
#include <stdio.h>

#define GID_OFFSET 0x110000

static hb_bool_t
_get_nominal_glyph (hb_font_t *font,
                    void *font_data,
                    hb_codepoint_t unicode,
                    hb_codepoint_t *glyph,
                    void *user_data)
{
  hb_font_t *parent = (hb_font_t*) font_data;

  if (unicode >= GID_OFFSET)
  {
    hb_face_t *face = hb_font_get_face(font);
    if (unicode >= hb_face_get_glyph_count(face) + GID_OFFSET)
    {
      fprintf(stderr, "Invalid glyph index: %d\n", unicode - GID_OFFSET);
      *glyph = 0;
      return false;
    }
    *glyph = unicode - GID_OFFSET;
    return true;
  }
  return hb_font_get_nominal_glyph(parent, unicode, glyph);
}

int
main(int argc, char **argv)
{
  if (argc < 3)
  {
    fprintf(stderr, "Usage: %s font glyphname [glyphname]\n", argv[0]);
    return 1;
  }

  hb_blob_t *blob = hb_blob_create_from_file(argv[1]);
  hb_face_t *face = hb_face_create(blob, 0);
  hb_font_t *parent = hb_font_create(face);

  hb_font_set_scale(parent, 1000, 1000);

  hb_font_funcs_t* funcs = hb_font_funcs_create();
  hb_font_funcs_set_nominal_glyph_func(funcs, _get_nominal_glyph, NULL, NULL);

  hb_font_t *font = hb_font_create_sub_font(parent);
  hb_font_set_funcs(font, funcs, parent, NULL);


  hb_buffer_t *buffer = hb_buffer_create();
  hb_buffer_set_content_type(buffer, HB_BUFFER_CONTENT_TYPE_UNICODE);
  hb_buffer_set_direction(buffer, HB_DIRECTION_LTR);

  for (int i = 2, j = 0; i < argc; i++)
  {
    char *glyphname = argv[i];
    hb_codepoint_t glyph = 0;
    if (hb_font_get_glyph_from_name(font, glyphname, -1, &glyph))
      hb_buffer_add(buffer, GID_OFFSET + glyph, j++);
    else
      fprintf(stderr, "Font does not have such glyph: %s\n", glyphname);
  }

  hb_shape(font, buffer, NULL, 0);

  char out[1000];
  hb_buffer_serialize_glyphs(buffer, 0, hb_buffer_get_length(buffer),
                             out, sizeof(out), NULL,
                             font, HB_BUFFER_SERIALIZE_FORMAT_TEXT,
                             HB_BUFFER_SERIALIZE_FLAG_NO_GLYPH_NAMES);
  fprintf(stdout, "[%s]\n", out);
}
```

Basically you use invalid code points (above the allowed Unicode range) as glyph indices, and override the Unicode to glyph font function to detect these and return the glyph id instead of consulting the font cmap table.
oke this works in a higher level (tested with a fontTools fontBuilder)

however a nice api callback for uharfbuzz would be nice! 

thanks for the tip!!


Yeah let's see if @anthrotype et al can wrap the font funcs callbacks in uharfbuzz.  That's exactly what those are designed for.
Some font functions seem to be wrapped so this is covered, but `hb_font_get_nominal_glyph()` is not wrapped, so the fallback to parent for regular Unicode cod points can’t be done.
I don’t know if uharfbuzz exposes enpugh of HarfBuzz API to do this, but here a C file that shows how to do this trick:
```c
#include <hb.h>
#include <stdbool.h>
#include <stdio.h>

#define GID_OFFSET 0x110000

static hb_bool_t
_get_nominal_glyph (hb_font_t *font,
                    void *font_data,
                    hb_codepoint_t unicode,
                    hb_codepoint_t *glyph,
                    void *user_data)
{
  hb_font_t *parent = (hb_font_t*) font_data;

  if (unicode >= GID_OFFSET)
  {
    hb_face_t *face = hb_font_get_face(font);
    if (unicode >= hb_face_get_glyph_count(face) + GID_OFFSET)
    {
      fprintf(stderr, "Invalid glyph index: %d\n", unicode - GID_OFFSET);
      *glyph = 0;
      return false;
    }
    *glyph = unicode - GID_OFFSET;
    return true;
  }
  return hb_font_get_nominal_glyph(parent, unicode, glyph);
}

int
main(int argc, char **argv)
{
  if (argc < 3)
  {
    fprintf(stderr, "Usage: %s font glyphname [glyphname]\n", argv[0]);
    return 1;
  }

  hb_blob_t *blob = hb_blob_create_from_file(argv[1]);
  hb_face_t *face = hb_face_create(blob, 0);
  hb_font_t *parent = hb_font_create(face);

  hb_font_set_scale(parent, 1000, 1000);

  hb_font_funcs_t* funcs = hb_font_funcs_create();
  hb_font_funcs_set_nominal_glyph_func(funcs, _get_nominal_glyph, NULL, NULL);

  hb_font_t *font = hb_font_create_sub_font(parent);
  hb_font_set_funcs(font, funcs, parent, NULL);


  hb_buffer_t *buffer = hb_buffer_create();
  hb_buffer_set_content_type(buffer, HB_BUFFER_CONTENT_TYPE_UNICODE);
  hb_buffer_set_direction(buffer, HB_DIRECTION_LTR);

  for (int i = 2, j = 0; i < argc; i++)
  {
    char *glyphname = argv[i];
    hb_codepoint_t glyph = 0;
    if (hb_font_get_glyph_from_name(font, glyphname, -1, &glyph))
      hb_buffer_add(buffer, GID_OFFSET + glyph, j++);
    else
      fprintf(stderr, "Font does not have such glyph: %s\n", glyphname);
  }

  hb_shape(font, buffer, NULL, 0);

  char out[1000];
  hb_buffer_serialize_glyphs(buffer, 0, hb_buffer_get_length(buffer),
                             out, sizeof(out), NULL,
                             font, HB_BUFFER_SERIALIZE_FORMAT_TEXT,
                             HB_BUFFER_SERIALIZE_FLAG_NO_GLYPH_NAMES);
  fprintf(stdout, "[%s]\n", out);
}
```

Basically you use invalid code points (above the allowed Unicode range) as glyph indices, and override the Unicode to glyph font function to detect these and return the glyph id instead of consulting the font cmap table.
oke this works in a higher level (tested with a fontTools fontBuilder)

however a nice api callback for uharfbuzz would be nice! 

thanks for the tip!!


Yeah let's see if @anthrotype et al can wrap the font funcs callbacks in uharfbuzz.  That's exactly what those are designed for.
Some font functions seem to be wrapped so this is covered, but `hb_font_get_nominal_glyph()` is not wrapped, so the fallback to parent for regular Unicode cod points can’t be done.

Fix patch:
diff --git a/src/uharfbuzz/_harfbuzz.pyx b/src/uharfbuzz/_harfbuzz.pyx
index e6b7573..c0cc0ae 100644
--- a/src/uharfbuzz/_harfbuzz.pyx
+++ b/src/uharfbuzz/_harfbuzz.pyx
@@ -423,6 +423,30 @@ cdef class Font:
         else:
             return None
 
+    def get_nominal_glyph(self, unicode: int):
+        cdef hb_codepoint_t gid
+        success = hb_font_get_nominal_glyph(self._hb_font, unicode, &gid)
+        return gid if success else None
+
+    def get_var_coords_normalized(self):
+        cdef unsigned int length
+        cdef const int *coords
+        coords = hb_font_get_var_coords_normalized(self._hb_font, &length)
+        return [coords[i] / 0x4000 for i in range(length)]
+
+    def set_var_coords_normalized(self, coords):
+        cdef unsigned int length
+        cdef int *coords_2dot14
+        length = len(coords)
+        coords_2dot14 = <int *>malloc(length * sizeof(int))
+        if coords_2dot14 is NULL:
+            raise MemoryError()
+        try:
+            for i in range(length):
+                coords_2dot14[i] = round(coords[i] * 0x4000)
+            hb_font_set_var_coords_normalized(self._hb_font, coords_2dot14, length)
+        finally:
+            free(coords_2dot14)
 
     def glyph_to_string(self, gid: int):
         cdef char name[64]
diff --git a/src/uharfbuzz/charfbuzz.pxd b/src/uharfbuzz/charfbuzz.pxd
index ca35c5d..d87b599 100644
--- a/src/uharfbuzz/charfbuzz.pxd
+++ b/src/uharfbuzz/charfbuzz.pxd
@@ -238,6 +238,17 @@ cdef extern from "hb.h":
         hb_font_t* font,
         hb_codepoint_t glyph,
         hb_glyph_extents_t *extents)
+    hb_bool_t hb_font_get_nominal_glyph(
+        hb_font_t *font,
+        hb_codepoint_t unicode,
+        hb_codepoint_t *glyph)
+    const int * hb_font_get_var_coords_normalized(
+        hb_font_t *font,
+        unsigned int *length)
+    void hb_font_set_var_coords_normalized(
+        hb_font_t *font,
+        const int *coords,
+        unsigned int coords_length)
     void hb_font_glyph_to_string(
         hb_font_t* font,
         hb_codepoint_t glyph,


Test patch:
diff --git a/tests/data/LICENSE_MutatorSans.txt b/tests/data/LICENSE_MutatorSans.txt
new file mode 100644
index 0000000..affdc0e
--- /dev/null
+++ b/tests/data/LICENSE_MutatorSans.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Erik van Blokland
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/tests/data/MutatorSans-VF.subset.ttf b/tests/data/MutatorSans-VF.subset.ttf
new file mode 100644
index 0000000..a05495f
Binary files /dev/null and b/tests/data/MutatorSans-VF.subset.ttf differ
diff --git a/tests/test_uharfbuzz.py b/tests/test_uharfbuzz.py
index 8ce1e75..67fb131 100644
--- a/tests/test_uharfbuzz.py
+++ b/tests/test_uharfbuzz.py
@@ -6,6 +6,7 @@
 TESTDATA = Path(__file__).parent / "data"
 ADOBE_BLANK_TTF_PATH = TESTDATA / "AdobeBlank.subset.ttf"
 OPEN_SANS_TTF_PATH = TESTDATA / "OpenSans.subset.ttf"
+MUTATOR_SANS_TTF_PATH = TESTDATA / "MutatorSans-VF.subset.ttf"
 
 
 @pytest.fixture
@@ -41,6 +42,14 @@ def opensans():
     return font
 
 
+@pytest.fixture
+def mutatorsans():
+    """Return a subset of MutatorSans-VF with a wdth and wght axis."""
+    face = hb.Face(MUTATOR_SANS_TTF_PATH.read_bytes())
+    font = hb.Font(face)
+    return font
+
+
 class TestBuffer:
     def test_init(self):
         buf = hb.Buffer()
@@ -147,6 +156,39 @@ def test_get_glyph_name(self, blankfont):
         glyph_name = blankfont.get_glyph_name(1000)
         assert glyph_name is None
 
+    def test_get_nominal_glyph(self, blankfont):
+        gid = blankfont.get_nominal_glyph(ord("a"))
+        assert gid == 1
+        gid = blankfont.get_nominal_glyph(ord("å"))
+        assert gid is None
+
+    def test_get_var_coords_normalized(self, mutatorsans):
+        coords = mutatorsans.get_var_coords_normalized()
+        assert coords == []
+        mutatorsans.set_variations({"wght": 500})
+        coords = mutatorsans.get_var_coords_normalized()
+        assert coords == [0, 0.5]
+        mutatorsans.set_variations({"wdth": 1000})
+        coords = mutatorsans.get_var_coords_normalized()
+        assert coords == [1.0, 0]
+        mutatorsans.set_variations({"wdth": 250, "wght": 250})
+        coords = mutatorsans.get_var_coords_normalized()
+        assert coords == [0.25, 0.25]
+
+    def test_set_var_coords_normalized(self, mutatorsans):
+        expected_coords = [0.5, 0.25]
+        mutatorsans.set_var_coords_normalized(expected_coords)
+        coords = mutatorsans.get_var_coords_normalized()
+        assert expected_coords == coords
+
+        expected_coords = [0.5]
+        mutatorsans.set_var_coords_normalized(expected_coords)
+        coords = mutatorsans.get_var_coords_normalized()
+        assert expected_coords == coords
+
+        with pytest.raises(TypeError):
+            mutatorsans.set_var_coords_normalized(["a"])
+
 
 class TestShape:
     @pytest.mark.parametrize(