Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added variation font support #3802

Merged
merged 1 commit into from Jun 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added Tests/fonts/AdobeVFPrototype.ttf
Binary file not shown.
8 changes: 4 additions & 4 deletions 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."
Binary file added Tests/fonts/TINY5x3GX.ttf
Binary file not shown.
Binary file added Tests/images/variation_adobe.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/variation_adobe_axes.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/variation_adobe_name.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/variation_tiny.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/variation_tiny_axes.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/variation_tiny_name.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 85 additions & 0 deletions Tests/test_imagefont.py
Expand Up @@ -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):
Expand Down
53 changes: 53 additions & 0 deletions src/PIL/ImageFont.py
Expand Up @@ -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"
Expand Down
162 changes: 162 additions & 0 deletions src/_imagingft.c
Expand Up @@ -25,6 +25,8 @@
#include <ft2build.h>
#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"
Expand Down Expand Up @@ -877,6 +879,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)
{
Expand All @@ -892,6 +1046,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}
};

Expand Down