diff --git a/Tests/fonts/AdobeVFPrototype.ttf b/Tests/fonts/AdobeVFPrototype.ttf new file mode 100644 index 00000000000..64f5ea8e1ed Binary files /dev/null and b/Tests/fonts/AdobeVFPrototype.ttf differ diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index ee9daee592c..90792915945 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -1,13 +1,11 @@ -NotoNastaliqUrdu-Regular.ttf: +NotoNastaliqUrdu-Regular.ttf, from https://github.com/googlei18n/noto-fonts +AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype +TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny -(from https://github.com/googlei18n/noto-fonts) +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. -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. - -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 index 00000000000..bd6e208dece 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 index 00000000000..71b879bc5a2 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 index 00000000000..9376c1d7b8f 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 index 00000000000..9e5fe70e539 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 index 00000000000..a0ff3f5946e 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 index 00000000000..d06ac7a60e7 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 index 00000000000..a0c6ffe3f1f 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 index 33f37916cb3..39231710df1 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -547,6 +547,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): diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 580aa87446a..fb47fa2baa2 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -206,6 +206,58 @@ def font_variant(self, font=None, size=None, index=None, encoding=None, 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' + 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" diff --git a/src/_imagingft.c b/src/_imagingft.c index 6b65e2c33bd..14f7dfc1275 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -25,6 +25,8 @@ #include #include FT_FREETYPE_H #include FT_GLYPH_H +#include FT_MULTIPLE_MASTERS_H +#include FT_SFNT_NAMES_H #define KEEP_PY_UNICODE #include "py3.h" @@ -824,6 +826,158 @@ font_render(FontObject* self, PyObject* args) Py_RETURN_NONE; } +#if FREETYPE_MAJOR > 2 ||\ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) ||\ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) + static PyObject* + font_getvarnames(FontObject* self, PyObject* args) + { + int error; + FT_UInt i, j, num_namedstyles, name_count; + FT_MM_Var *master; + FT_SfntName name; + PyObject *list_names, *list_name; + + error = FT_Get_MM_Var(self->face, &master); + if (error) + return geterror(error); + + num_namedstyles = master->num_namedstyles; + list_names = PyList_New(num_namedstyles); + + name_count = FT_Get_Sfnt_Name_Count(self->face); + for (i = 0; i < name_count; i++) { + error = FT_Get_Sfnt_Name(self->face, i, &name); + if (error) + return geterror(error); + + for (j = 0; j < num_namedstyles; j++) { + if (PyList_GetItem(list_names, j) != NULL) + continue; + + if (master->namedstyle[j].strid == name.name_id) { + list_name = Py_BuildValue(PY_ARG_BYTES_LENGTH, + name.string, name.string_len); + PyList_SetItem(list_names, j, list_name); + break; + } + } + } + + FT_Done_MM_Var(library, master); + + return list_names; + } + + static PyObject* + font_getvaraxes(FontObject* self, PyObject* args) + { + int error; + FT_UInt i, j, num_axis, name_count; + FT_MM_Var* master; + FT_Var_Axis axis; + FT_SfntName name; + PyObject *list_axes, *list_axis, *axis_name; + error = FT_Get_MM_Var(self->face, &master); + if (error) + return geterror(error); + + num_axis = master->num_axis; + name_count = FT_Get_Sfnt_Name_Count(self->face); + + list_axes = PyList_New(num_axis); + for (i = 0; i < num_axis; i++) { + axis = master->axis[i]; + + list_axis = PyDict_New(); + PyDict_SetItemString(list_axis, "minimum", + PyInt_FromLong(axis.minimum / 65536)); + PyDict_SetItemString(list_axis, "default", + PyInt_FromLong(axis.def / 65536)); + PyDict_SetItemString(list_axis, "maximum", + PyInt_FromLong(axis.maximum / 65536)); + + for (j = 0; j < name_count; j++) { + error = FT_Get_Sfnt_Name(self->face, j, &name); + if (error) + return geterror(error); + + if (name.name_id == axis.strid) { + axis_name = Py_BuildValue(PY_ARG_BYTES_LENGTH, + name.string, name.string_len); + PyDict_SetItemString(list_axis, "name", axis_name); + break; + } + } + + PyList_SetItem(list_axes, i, list_axis); + } + + FT_Done_MM_Var(library, master); + + return list_axes; + } + + static PyObject* + font_setvarname(FontObject* self, PyObject* args) + { + int error; + + int instance_index; + if (!PyArg_ParseTuple(args, "i", &instance_index)) + return NULL; + + error = FT_Set_Named_Instance(self->face, instance_index); + if (error) + return geterror(error); + + Py_INCREF(Py_None); + return Py_None; + } + + static PyObject* + font_setvaraxes(FontObject* self, PyObject* args) + { + int error; + + PyObject *axes, *item; + Py_ssize_t i, num_coords; + FT_Fixed *coords; + FT_Fixed coord; + if (!PyArg_ParseTuple(args, "O", &axes)) + return NULL; + + if (!PyList_Check(axes)) { + PyErr_SetString(PyExc_TypeError, "argument must be a list"); + return NULL; + } + + num_coords = PyObject_Length(axes); + coords = malloc(2 * sizeof(coords)); + for (i = 0; i < num_coords; i++) { + item = PyList_GET_ITEM(axes, i); + if (PyFloat_Check(item)) + coord = PyFloat_AS_DOUBLE(item); + else if (PyInt_Check(item)) + coord = (float) PyInt_AS_LONG(item); + else if (PyNumber_Check(item)) + coord = PyFloat_AsDouble(item); + else { + PyErr_SetString(PyExc_TypeError, "list must contain numbers"); + return NULL; + } + coords[i] = coord * 65536; + } + + error = FT_Set_Var_Design_Coordinates(self->face, num_coords, coords); + if (error) + return geterror(error); + + Py_INCREF(Py_None); + return Py_None; + } +#endif + static void font_dealloc(FontObject* self) { @@ -839,6 +993,14 @@ font_dealloc(FontObject* self) static PyMethodDef font_methods[] = { {"render", (PyCFunction) font_render, METH_VARARGS}, {"getsize", (PyCFunction) font_getsize, METH_VARARGS}, +#if FREETYPE_MAJOR > 2 ||\ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) ||\ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) + {"getvarnames", (PyCFunction) font_getvarnames, METH_VARARGS }, + {"getvaraxes", (PyCFunction) font_getvaraxes, METH_VARARGS }, + {"setvarname", (PyCFunction) font_setvarname, METH_VARARGS}, + {"setvaraxes", (PyCFunction) font_setvaraxes, METH_VARARGS}, +#endif {NULL, NULL} };