Repository: python-pillow/Pillow

Issue:
Support for variable fonts?
Hi, <br>
are you planning to support variable fonts? Would be a great feature allowing people to work with it. DrawBot is the only Python tool that supports it as far as I know - Mac only, it has some dependencies. Most reliable for others on other platforms is recording it from Google Chrome and that the way I do it as well.  

https://en.wikipedia.org/wiki/Variable_fonts


Discussion:
I've created PR #3802 for this. If you could give it a look, that could be good.
> I've created PR #3802 for this. If you could give it a look, that could be good.

Thanks, looking forward. As a potentional user and designer I wanted to test it, but it seems that I am too lame to build it from source.  
@jansindl3r what operating system do you use?
@radarhere Win7
Courtesy of AppVeyor, here are some [builds](https://github.com/python-pillow/Pillow/files/3186620/builds.zip) of the PR that you should be able to use.

Resolving patch:
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -417,6 +417,59 @@ def font_variant(
             layout_engine=layout_engine or self.layout_engine,
         )
 
+    def get_variation_names(self):
+        """
+        :returns: A list of the named styles in a variation font.
+        :exception IOError: If the font is not a variation font.
+        """
+        try:
+            names = self.font.getvarnames()
+        except AttributeError:
+            raise NotImplementedError("FreeType 2.9.1 or greater is required")
+        return [name.replace(b"\x00", b"") for name in names]
+
+    def set_variation_by_name(self, name):
+        """
+        :param name: The name of the style.
+        :exception IOError: If the font is not a variation font.
+        """
+        names = self.get_variation_names()
+        if not isinstance(name, bytes):
+            name = name.encode()
+        index = names.index(name)
+
+        if index == getattr(self, "_last_variation_index", None):
+            # When the same name is set twice in a row,
+            # there is an 'unknown freetype error'
+            # https://savannah.nongnu.org/bugs/?56186
+            return
+        self._last_variation_index = index
+
+        self.font.setvarname(index)
+
+    def get_variation_axes(self):
+        """
+        :returns: A list of the axes in a variation font.
+        :exception IOError: If the font is not a variation font.
+        """
+        try:
+            axes = self.font.getvaraxes()
+        except AttributeError:
+            raise NotImplementedError("FreeType 2.9.1 or greater is required")
+        for axis in axes:
+            axis["name"] = axis["name"].replace(b"\x00", b"")
+        return axes
+
+    def set_variation_by_axes(self, axes):
+        """
+        :param axes: A list of values for each axis.
+        :exception IOError: If the font is not a variation font.
+        """
+        try:
+            self.font.setvaraxes(axes)
+        except AttributeError:
+            raise NotImplementedError("FreeType 2.9.1 or greater is required")
+
 
 class TransposedFont(object):
     "Wrapper for writing rotated or mirrored text"


Test patch:
diff --git a/Tests/fonts/AdobeVFPrototype.ttf b/Tests/fonts/AdobeVFPrototype.ttf
new file mode 100644
Binary files /dev/null and b/Tests/fonts/AdobeVFPrototype.ttf differ
diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt
--- a/Tests/fonts/LICENSE.txt
+++ b/Tests/fonts/LICENSE.txt
@@ -1,12 +1,12 @@
 
 NotoNastaliqUrdu-Regular.ttf, from https://github.com/googlei18n/noto-fonts
 NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
+AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
+TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
 
-All Noto fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to.
+All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to.
 
 
-10x20-ISO8859-1.pcf
-
-(from https://packages.ubuntu.com/xenial/xfonts-base)
+10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base
 
 "Public domain font.  Share and enjoy."
diff --git a/Tests/fonts/TINY5x3GX.ttf b/Tests/fonts/TINY5x3GX.ttf
new file mode 100755
Binary files /dev/null and b/Tests/fonts/TINY5x3GX.ttf differ
diff --git a/Tests/images/variation_adobe.png b/Tests/images/variation_adobe.png
new file mode 100644
Binary files /dev/null and b/Tests/images/variation_adobe.png differ
diff --git a/Tests/images/variation_adobe_axes.png b/Tests/images/variation_adobe_axes.png
new file mode 100644
Binary files /dev/null and b/Tests/images/variation_adobe_axes.png differ
diff --git a/Tests/images/variation_adobe_name.png b/Tests/images/variation_adobe_name.png
new file mode 100644
Binary files /dev/null and b/Tests/images/variation_adobe_name.png differ
diff --git a/Tests/images/variation_tiny.png b/Tests/images/variation_tiny.png
new file mode 100644
Binary files /dev/null and b/Tests/images/variation_tiny.png differ
diff --git a/Tests/images/variation_tiny_axes.png b/Tests/images/variation_tiny_axes.png
new file mode 100644
Binary files /dev/null and b/Tests/images/variation_tiny_axes.png differ
diff --git a/Tests/images/variation_tiny_name.png b/Tests/images/variation_tiny_name.png
new file mode 100644
Binary files /dev/null and b/Tests/images/variation_tiny_name.png differ
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -570,6 +570,91 @@ def test_complex_font_settings(self):
             self.assertRaises(KeyError, t.getmask, 'абвг', features=['-kern'])
             self.assertRaises(KeyError, t.getmask, 'абвг', language='sr')
 
+    def test_variation_get(self):
+        font = self.get_font()
+
+        freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version)
+        if freetype < '2.9.1':
+            self.assertRaises(NotImplementedError, font.get_variation_names)
+            self.assertRaises(NotImplementedError, font.get_variation_axes)
+            return
+
+        self.assertRaises(IOError, font.get_variation_names)
+        self.assertRaises(IOError, font.get_variation_axes)
+
+        font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf")
+        self.assertEqual(
+            font.get_variation_names(),
+            [b'ExtraLight', b'Light', b'Regular', b'Semibold', b'Bold',
+             b'Black', b'Black Medium Contrast', b'Black High Contrast', b'Default'])
+        self.assertEqual(
+            font.get_variation_axes(),
+            [{'name': b'Weight', 'minimum': 200, 'maximum': 900, 'default': 389},
+             {'name': b'Contrast', 'minimum': 0, 'maximum': 100, 'default': 0}])
+
+        font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf")
+        self.assertEqual(
+            font.get_variation_names(),
+            [b'20', b'40', b'60', b'80', b'100', b'120', b'140', b'160', b'180',
+             b'200', b'220', b'240', b'260', b'280', b'300', b'Regular'])
+        self.assertEqual(
+            font.get_variation_axes(),
+            [{'name': b'Size', 'minimum': 0, 'maximum': 300, 'default': 0}])
+
+    def test_variation_set_by_name(self):
+        font = self.get_font()
+
+        freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version)
+        if freetype < '2.9.1':
+            self.assertRaises(NotImplementedError, font.set_variation_by_name, "Bold")
+            return
+
+        self.assertRaises(IOError, font.set_variation_by_name, "Bold")
+
+        def _check_text(font, path, epsilon):
+            im = Image.new("RGB", (100, 75), "white")
+            d = ImageDraw.Draw(im)
+            d.text((10, 10), "Text", font=font, fill="black")
+
+            expected = Image.open(path)
+            self.assert_image_similar(im, expected, epsilon)
+        font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
+        _check_text(font, "Tests/images/variation_adobe.png", 11)
+        for name in ["Bold", b"Bold"]:
+            font.set_variation_by_name(name)
+        _check_text(font, "Tests/images/variation_adobe_name.png", 11)
+
+        font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
+        _check_text(font, "Tests/images/variation_tiny.png", 40)
+        for name in ["200", b"200"]:
+            font.set_variation_by_name(name)
+        _check_text(font, "Tests/images/variation_tiny_name.png", 40)
+
+    def test_variation_set_by_axes(self):
+        font = self.get_font()
+
+        freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version)
+        if freetype < '2.9.1':
+            self.assertRaises(NotImplementedError, font.set_variation_by_axes, [100])
+            return
+
+        self.assertRaises(IOError, font.set_variation_by_axes, [500, 50])
+
+        def _check_text(font, path, epsilon):
+            im = Image.new("RGB", (100, 75), "white")
+            d = ImageDraw.Draw(im)
+            d.text((10, 10), "Text", font=font, fill="black")
+
+            expected = Image.open(path)
+            self.assert_image_similar(im, expected, epsilon)
+        font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
+        font.set_variation_by_axes([500, 50])
+        _check_text(font, "Tests/images/variation_adobe_axes.png", 5.1)
+
+        font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
+        font.set_variation_by_axes([100])
+        _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)
+
 
 @unittest.skipUnless(HAS_RAQM, "Raqm not Available")
 class TestImageFont_RaqmLayout(TestImageFont):