mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-26 18:06:18 +03:00
Added variation font support
This commit is contained in:
parent
2334040f56
commit
da16b7ec45
BIN
Tests/fonts/AdobeVFPrototype.ttf
Normal file
BIN
Tests/fonts/AdobeVFPrototype.ttf
Normal file
Binary file not shown.
|
@ -1,12 +1,12 @@
|
||||||
|
|
||||||
NotoNastaliqUrdu-Regular.ttf, from https://github.com/googlei18n/noto-fonts
|
NotoNastaliqUrdu-Regular.ttf, from https://github.com/googlei18n/noto-fonts
|
||||||
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
|
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
|
10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base
|
||||||
|
|
||||||
(from https://packages.ubuntu.com/xenial/xfonts-base)
|
|
||||||
|
|
||||||
"Public domain font. Share and enjoy."
|
"Public domain font. Share and enjoy."
|
||||||
|
|
BIN
Tests/fonts/TINY5x3GX.ttf
Executable file
BIN
Tests/fonts/TINY5x3GX.ttf
Executable file
Binary file not shown.
BIN
Tests/images/variation_adobe.png
Normal file
BIN
Tests/images/variation_adobe.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/variation_adobe_axes.png
Normal file
BIN
Tests/images/variation_adobe_axes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/variation_adobe_name.png
Normal file
BIN
Tests/images/variation_adobe_name.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/variation_tiny.png
Normal file
BIN
Tests/images/variation_tiny.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 239 B |
BIN
Tests/images/variation_tiny_axes.png
Normal file
BIN
Tests/images/variation_tiny_axes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 937 B |
BIN
Tests/images/variation_tiny_name.png
Normal file
BIN
Tests/images/variation_tiny_name.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -570,6 +570,91 @@ class TestImageFont(PillowTestCase):
|
||||||
self.assertRaises(KeyError, t.getmask, 'абвг', features=['-kern'])
|
self.assertRaises(KeyError, t.getmask, 'абвг', features=['-kern'])
|
||||||
self.assertRaises(KeyError, t.getmask, 'абвг', language='sr')
|
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")
|
@unittest.skipUnless(HAS_RAQM, "Raqm not Available")
|
||||||
class TestImageFont_RaqmLayout(TestImageFont):
|
class TestImageFont_RaqmLayout(TestImageFont):
|
||||||
|
|
|
@ -417,6 +417,59 @@ class FreeTypeFont(object):
|
||||||
layout_engine=layout_engine or self.layout_engine,
|
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):
|
class TransposedFont(object):
|
||||||
"Wrapper for writing rotated or mirrored text"
|
"Wrapper for writing rotated or mirrored text"
|
||||||
|
|
162
src/_imagingft.c
162
src/_imagingft.c
|
@ -25,6 +25,8 @@
|
||||||
#include <ft2build.h>
|
#include <ft2build.h>
|
||||||
#include FT_FREETYPE_H
|
#include FT_FREETYPE_H
|
||||||
#include FT_GLYPH_H
|
#include FT_GLYPH_H
|
||||||
|
#include FT_MULTIPLE_MASTERS_H
|
||||||
|
#include FT_SFNT_NAMES_H
|
||||||
|
|
||||||
#define KEEP_PY_UNICODE
|
#define KEEP_PY_UNICODE
|
||||||
#include "py3.h"
|
#include "py3.h"
|
||||||
|
@ -877,6 +879,158 @@ font_render(FontObject* self, PyObject* args)
|
||||||
Py_RETURN_NONE;
|
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
|
static void
|
||||||
font_dealloc(FontObject* self)
|
font_dealloc(FontObject* self)
|
||||||
{
|
{
|
||||||
|
@ -892,6 +1046,14 @@ font_dealloc(FontObject* self)
|
||||||
static PyMethodDef font_methods[] = {
|
static PyMethodDef font_methods[] = {
|
||||||
{"render", (PyCFunction) font_render, METH_VARARGS},
|
{"render", (PyCFunction) font_render, METH_VARARGS},
|
||||||
{"getsize", (PyCFunction) font_getsize, 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}
|
{NULL, NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user