Merge pull request #3693 from iwsfutcmd/text-set-language

Added ability to set language for text rendering
This commit is contained in:
Hugo 2019-03-12 11:32:33 +02:00 committed by GitHub
commit e2fda900cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 195 additions and 45 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

View File

@ -525,6 +525,15 @@ class TestImageFont(PillowTestCase):
self.assertEqual(t.getsize_multiline('ABC\nA'), (36, 36))
self.assertEqual(t.getsize_multiline('ABC\nAaaa'), (48, 36))
def test_complex_font_settings(self):
# Arrange
t = self.get_font()
# Act / Assert
if t.layout_engine == ImageFont.LAYOUT_BASIC:
self.assertRaises(KeyError, t.getmask, 'абвг', direction='rtl')
self.assertRaises(KeyError, t.getmask, 'абвг', features=['-kern'])
self.assertRaises(KeyError, t.getmask, 'абвг', language='sr')
@unittest.skipUnless(HAS_RAQM, "Raqm not Available")
class TestImageFont_RaqmLayout(TestImageFont):

View File

@ -130,3 +130,16 @@ class TestImagecomplextext(PillowTestCase):
target_img = Image.open(target)
self.assert_image_similar(im, target_img, .5)
def test_language(self):
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
draw.text((0, 0), 'абвг', font=ttf, fill=500,
language='sr')
target = 'Tests/images/test_language.png'
target_img = Image.open(target)
self.assert_image_similar(im, target_img, .5)

View File

@ -255,7 +255,7 @@ Methods
Draw a shape.
.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None)
.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None)
Draws the string at the given position.
@ -287,7 +287,17 @@ Methods
.. versionadded:: 4.2.0
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None)
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
.. versionadded:: 6.0.0
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None)
Draws the string at the given position.
@ -316,7 +326,17 @@ Methods
.. versionadded:: 4.2.0
.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None)
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
.. versionadded:: 6.0.0
.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None)
Return the size of the given string, in pixels.
@ -330,7 +350,6 @@ Methods
Requires libraqm.
.. versionadded:: 4.2.0
:param features: A list of OpenType font features to be used during text
layout. This is usually used to turn on optional
font features that are not enabled by default,
@ -343,8 +362,17 @@ Methods
Requires libraqm.
.. versionadded:: 4.2.0
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None)
.. versionadded:: 6.0.0
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None)
Return the size of the given string, in pixels.
@ -370,6 +398,16 @@ Methods
.. versionadded:: 4.2.0
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
.. versionadded:: 6.0.0
.. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None)
.. warning:: This method is experimental.

View File

@ -47,11 +47,45 @@ Functions
Methods
-------
.. py:method:: PIL.ImageFont.ImageFont.getsize(text)
.. py:method:: PIL.ImageFont.ImageFont.getsize(text, direction=None, features=[], language=None)
Returns width and height (in pixels) of given text if rendered in font with
provided direction, features, and language.
:param text: Text to measure.
:param direction: Direction of the text. It can be 'rtl' (right to
left), 'ltr' (left to right) or 'ttb' (top to bottom).
Requires libraqm.
.. versionadded:: 4.2.0
:param features: A list of OpenType font features to be used during text
layout. This is usually used to turn on optional
font features that are not enabled by default,
for example 'dlig' or 'ss01', but can be also
used to turn off default font features for
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
.. versionadded:: 4.2.0
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
.. versionadded:: 6.0.0
:return: (width, height)
.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[])
.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[], language=None)
Create a bitmap for the text.
@ -85,5 +119,15 @@ Methods
.. versionadded:: 4.2.0
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
.. versionadded:: 6.0.0
:return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module.

View File

@ -102,17 +102,32 @@ Use ``PIL.__version__`` instead.
API Additions
=============
DIB File Format
DIB file format
^^^^^^^^^^^^^^^
Pillow now supports reading and writing the DIB "Device Independent Bitmap" file format.
Pillow now supports reading and writing the Device Independent Bitmap file format.
Image.quantize
^^^^^^^^^^^^^^
The `dither` option is now a customisable parameter (was previously hardcoded to `1`). This parameter takes the same values used in `Image.convert`
The ``dither`` option is now a customisable parameter (was previously hardcoded to ``1``).
This parameter takes the same values used in ``Image.convert``.
PNG EXIF Data
New language parameter
^^^^^^^^^^^^^^^^^^^^^^
These text-rendering functions now accept a ``language`` parameter to request
language-specific glyphs and ligatures from the font:
* ``ImageDraw.ImageDraw.multiline_text()``
* ``ImageDraw.ImageDraw.multiline_textsize()``
* ``ImageDraw.ImageDraw.text()``
* ``ImageDraw.ImageDraw.textsize()``
* ``ImageFont.ImageFont.getmask()``
* ``ImageFont.ImageFont.getsize_multiline()``
* ``ImageFont.ImageFont.getsize()``
PNG EXIF data
^^^^^^^^^^^^^
EXIF data can now be read from and saved to PNG images. However, unlike other image
@ -130,4 +145,5 @@ Pillow can now read uncompressed RGB data from DDS images.
Reading TIFF with old-style JPEG compression
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Added support reading TIFF files with old-style JPEG compression through LibTIFF. All YCbCr TIFF images are now always read as RGB.
Added support reading TIFF files with old-style JPEG compression through LibTIFF. All YCbCr
TIFF images are now always read as RGB.

View File

@ -282,13 +282,17 @@ class ImageDraw(object):
self.draw.draw_bitmap(xy, mask, ink)
def multiline_text(self, xy, text, fill=None, font=None, anchor=None,
spacing=4, align="left", direction=None, features=None):
spacing=4, align="left", direction=None, features=None,
language=None):
widths = []
max_width = 0
lines = self._multiline_split(text)
line_spacing = self.textsize('A', font=font)[1] + spacing
for line in lines:
line_width, line_height = self.textsize(line, font)
line_width, line_height = self.textsize(line, font,
direction=direction,
features=features,
language=language)
widths.append(line_width)
max_width = max(max_width, line_width)
left, top = xy
@ -302,29 +306,30 @@ class ImageDraw(object):
else:
raise ValueError('align must be "left", "center" or "right"')
self.text((left, top), line, fill, font, anchor,
direction=direction, features=features)
direction=direction, features=features, language=language)
top += line_spacing
left = xy[0]
def textsize(self, text, font=None, spacing=4, direction=None,
features=None):
features=None, language=None):
"""Get the size of a given string, in pixels."""
if self._multiline_check(text):
return self.multiline_textsize(text, font, spacing,
direction, features)
direction, features, language)
if font is None:
font = self.getfont()
return font.getsize(text, direction, features)
return font.getsize(text, direction, features, language)
def multiline_textsize(self, text, font=None, spacing=4, direction=None,
features=None):
features=None, language=None):
max_width = 0
lines = self._multiline_split(text)
line_spacing = self.textsize('A', font=font)[1] + spacing
for line in lines:
line_width, line_height = self.textsize(line, font, spacing,
direction, features)
direction, features,
language)
max_width = max(max_width, line_width)
return max_width, len(lines)*line_spacing - spacing

View File

@ -158,17 +158,17 @@ class FreeTypeFont(object):
def getmetrics(self):
return self.font.ascent, self.font.descent
def getsize(self, text, direction=None, features=None):
size, offset = self.font.getsize(text, direction, features)
def getsize(self, text, direction=None, features=None, language=None):
size, offset = self.font.getsize(text, direction, features, language)
return (size[0] + offset[0], size[1] + offset[1])
def getsize_multiline(self, text, direction=None,
spacing=4, features=None):
def getsize_multiline(self, text, direction=None, spacing=4,
features=None, language=None):
max_width = 0
lines = self._multiline_split(text)
line_spacing = self.getsize('A')[1] + spacing
for line in lines:
line_width, line_height = self.getsize(line, direction, features)
line_width, line_height = self.getsize(line, direction, features, language)
max_width = max(max_width, line_width)
return max_width, len(lines)*line_spacing - spacing
@ -176,15 +176,15 @@ class FreeTypeFont(object):
def getoffset(self, text):
return self.font.getsize(text)[1]
def getmask(self, text, mode="", direction=None, features=None):
return self.getmask2(text, mode, direction=direction,
features=features)[0]
def getmask(self, text, mode="", direction=None, features=None, language=None):
return self.getmask2(text, mode, direction=direction, features=features,
language=language)[0]
def getmask2(self, text, mode="", fill=Image.core.fill, direction=None,
features=None, *args, **kwargs):
size, offset = self.font.getsize(text, direction, features)
features=None, language=None, *args, **kwargs):
size, offset = self.font.getsize(text, direction, features, language)
im = fill("L", size, 0)
self.font.render(text, im.id, mode == "1", direction, features)
self.font.render(text, im.id, mode == "1", direction, features, language)
return im, offset
def font_variant(self, font=None, size=None, index=None, encoding=None,

View File

@ -87,6 +87,10 @@ typedef bool (*t_raqm_set_text_utf8) (raqm_t *rq,
size_t len);
typedef bool (*t_raqm_set_par_direction) (raqm_t *rq,
raqm_direction_t dir);
typedef bool (*t_raqm_set_language) (raqm_t *rq,
const char *lang,
size_t start,
size_t len);
typedef bool (*t_raqm_add_font_feature) (raqm_t *rq,
const char *feature,
int len);
@ -106,6 +110,7 @@ typedef struct {
t_raqm_set_text set_text;
t_raqm_set_text_utf8 set_text_utf8;
t_raqm_set_par_direction set_par_direction;
t_raqm_set_language set_language;
t_raqm_add_font_feature add_font_feature;
t_raqm_set_freetype_face set_freetype_face;
t_raqm_layout layout;
@ -160,6 +165,7 @@ setraqm(void)
p_raqm.set_text = (t_raqm_set_text)dlsym(p_raqm.raqm, "raqm_set_text");
p_raqm.set_text_utf8 = (t_raqm_set_text_utf8)dlsym(p_raqm.raqm, "raqm_set_text_utf8");
p_raqm.set_par_direction = (t_raqm_set_par_direction)dlsym(p_raqm.raqm, "raqm_set_par_direction");
p_raqm.set_language = (t_raqm_set_language)dlsym(p_raqm.raqm, "raqm_set_language");
p_raqm.add_font_feature = (t_raqm_add_font_feature)dlsym(p_raqm.raqm, "raqm_add_font_feature");
p_raqm.set_freetype_face = (t_raqm_set_freetype_face)dlsym(p_raqm.raqm, "raqm_set_freetype_face");
p_raqm.layout = (t_raqm_layout)dlsym(p_raqm.raqm, "raqm_layout");
@ -176,6 +182,7 @@ setraqm(void)
p_raqm.set_text &&
p_raqm.set_text_utf8 &&
p_raqm.set_par_direction &&
p_raqm.set_language &&
p_raqm.add_font_feature &&
p_raqm.set_freetype_face &&
p_raqm.layout &&
@ -190,6 +197,7 @@ setraqm(void)
p_raqm.set_text = (t_raqm_set_text)GetProcAddress(p_raqm.raqm, "raqm_set_text");
p_raqm.set_text_utf8 = (t_raqm_set_text_utf8)GetProcAddress(p_raqm.raqm, "raqm_set_text_utf8");
p_raqm.set_par_direction = (t_raqm_set_par_direction)GetProcAddress(p_raqm.raqm, "raqm_set_par_direction");
p_raqm.set_language = (t_raqm_set_language)GetProcAddress(p_raqm.raqm, "raqm_set_language");
p_raqm.add_font_feature = (t_raqm_add_font_feature)GetProcAddress(p_raqm.raqm, "raqm_add_font_feature");
p_raqm.set_freetype_face = (t_raqm_set_freetype_face)GetProcAddress(p_raqm.raqm, "raqm_set_freetype_face");
p_raqm.layout = (t_raqm_layout)GetProcAddress(p_raqm.raqm, "raqm_layout");
@ -205,6 +213,7 @@ setraqm(void)
p_raqm.set_text &&
p_raqm.set_text_utf8 &&
p_raqm.set_par_direction &&
p_raqm.set_language &&
p_raqm.add_font_feature &&
p_raqm.set_freetype_face &&
p_raqm.layout &&
@ -332,8 +341,8 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out)
}
static size_t
text_layout_raqm(PyObject* string, FontObject* self, const char* dir,
PyObject *features ,GlyphInfo **glyph_info, int mask)
text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject *features,
const char* lang, GlyphInfo **glyph_info, int mask)
{
int i = 0;
raqm_t *rq;
@ -341,6 +350,7 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir,
raqm_glyph_t *glyphs = NULL;
raqm_glyph_t_01 *glyphs_01 = NULL;
raqm_direction_t direction;
size_t start = 0;
rq = (*p_raqm.create)();
if (rq == NULL) {
@ -360,6 +370,13 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir,
PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed");
goto failed;
}
if (lang) {
if (!(*p_raqm.set_language)(rq, lang, start, size)) {
PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed");
goto failed;
}
}
}
#if PY_VERSION_HEX < 0x03000000
else if (PyString_Check(string)) {
@ -372,6 +389,12 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir,
PyErr_SetString(PyExc_ValueError, "raqm_set_text_utf8() failed");
goto failed;
}
if (lang) {
if (!(*p_raqm.set_language)(rq, lang, start, size)) {
PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed");
goto failed;
}
}
}
#endif
else {
@ -498,8 +521,8 @@ failed:
}
static size_t
text_layout_fallback(PyObject* string, FontObject* self, const char* dir,
PyObject *features ,GlyphInfo **glyph_info, int mask)
text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObject *features,
const char* lang, GlyphInfo **glyph_info, int mask)
{
int error, load_flags;
FT_ULong ch;
@ -509,8 +532,8 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir,
FT_UInt last_index = 0;
int i;
if (features != Py_None || dir != NULL) {
PyErr_SetString(PyExc_KeyError, "setting text direction or font features is not supported without libraqm");
if (features != Py_None || dir != NULL || lang != NULL) {
PyErr_SetString(PyExc_KeyError, "setting text direction, language or font features is not supported without libraqm");
}
#if PY_VERSION_HEX >= 0x03000000
if (!PyUnicode_Check(string)) {
@ -564,15 +587,15 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir,
}
static size_t
text_layout(PyObject* string, FontObject* self, const char* dir,
PyObject *features, GlyphInfo **glyph_info, int mask)
text_layout(PyObject* string, FontObject* self, const char* dir, PyObject *features,
const char* lang, GlyphInfo **glyph_info, int mask)
{
size_t count;
if (p_raqm.raqm && self->layout_engine == LAYOUT_RAQM) {
count = text_layout_raqm(string, self, dir, features, glyph_info, mask);
count = text_layout_raqm(string, self, dir, features, lang, glyph_info, mask);
} else {
count = text_layout_fallback(string, self, dir, features, glyph_info, mask);
count = text_layout_fallback(string, self, dir, features, lang, glyph_info, mask);
}
return count;
}
@ -584,6 +607,7 @@ font_getsize(FontObject* self, PyObject* args)
FT_Face face;
int xoffset, yoffset;
const char *dir = NULL;
const char *lang = NULL;
size_t count;
GlyphInfo *glyph_info = NULL;
PyObject *features = Py_None;
@ -591,14 +615,14 @@ font_getsize(FontObject* self, PyObject* args)
/* calculate size and bearing for a given string */
PyObject* string;
if (!PyArg_ParseTuple(args, "O|zO:getsize", &string, &dir, &features))
if (!PyArg_ParseTuple(args, "O|zOz:getsize", &string, &dir, &features, &lang))
return NULL;
face = NULL;
xoffset = yoffset = 0;
y_max = y_min = 0;
count = text_layout(string, self, dir, features, &glyph_info, 0);
count = text_layout(string, self, dir, features, lang, &glyph_info, 0);
if (PyErr_Occurred()) {
return NULL;
}
@ -691,16 +715,17 @@ font_render(FontObject* self, PyObject* args)
int temp;
int xx, x0, x1;
const char *dir = NULL;
const char *lang = NULL;
size_t count;
GlyphInfo *glyph_info;
PyObject *features = NULL;
if (!PyArg_ParseTuple(args, "On|izO:render", &string, &id, &mask, &dir, &features)) {
if (!PyArg_ParseTuple(args, "On|izOz:render", &string, &id, &mask, &dir, &features, &lang)) {
return NULL;
}
glyph_info = NULL;
count = text_layout(string, self, dir, features, &glyph_info, mask);
count = text_layout(string, self, dir, features, lang, &glyph_info, mask);
if (PyErr_Occurred()) {
return NULL;
}