implement text anchor for truetype fonts

(cherry picked from commit bac9025918ccf944bac77addc130f33cf9d74701)
This commit is contained in:
nulano 2020-04-14 13:28:09 +02:00
parent c2367400fa
commit e6d4c2ce8f
4 changed files with 210 additions and 16 deletions

View File

@ -74,6 +74,61 @@ To load a OpenType/TrueType font, use the truetype function in the
:py:mod:`~PIL.ImageFont` module. Note that this function depends on third-party :py:mod:`~PIL.ImageFont` module. Note that this function depends on third-party
libraries, and may not available in all PIL builds. libraries, and may not available in all PIL builds.
Text anchors
^^^^^^^^^^^^
The ``anchor`` parameter determines the position of the ``xy`` coordinates relative to the text.
It consists of two characters, the horizontal and vertical alignment.
The default value is ``la`` for horizontal text and ``lt`` for vertical text.
This parameter is ignored for legacy PIL fonts, where the anchor is always top-left.
+---+-----------------------+-------------------------------------------------------+
| Horizontal anchor alignment |
+===+=======================+=======================================================+
| l | left | Anchor is to the left of the text. |
+---+-----------------------+-------------------------------------------------------+
| m | middle | Anchor is horizontally centered with the text. |
+---+-----------------------+-------------------------------------------------------+
| r | right | Anchor is to the right of the text. |
+---+-----------------------+-------------------------------------------------------+
| s | baseline | **(vertical text only)** |
| | | Anchor is at the baseline (middle) of the text. |
| | | The exact alignment depends on the font. |
+---+-----------------------+-------------------------------------------------------+
+---+-----------------------+-------------------------------------------------------+
| Vertical anchor alignment |
+===+=======================+=======================================================+
| a | ascender (top) | **(horizontal text only)** |
| | | Anchor is at the ascender line (top) |
| | | of the first line of text. |
+---+-----------------------+-------------------------------------------------------+
| t | top | **(single-line text only)** |
| | | Anchor is at the top of the text. |
+---+-----------------------+-------------------------------------------------------+
| m | middle | Anchor is vertically centered with the text. |
| | | For horizontal text this is the midpoint of the |
| | | first ascender line and the last descender line. |
+---+-----------------------+-------------------------------------------------------+
| s | baseline | **(horizontal text only)** |
| | | Anchor is at the baseline (bottom) |
| | | of the first line of text, only |
| | | descenders extend below the anchor. |
+---+-----------------------+-------------------------------------------------------+
| b | bottom | **(single-line text only)** |
| | | Anchor is at the bottom of the text. |
+---+-----------------------+-------------------------------------------------------+
| d | descender (bottom) | **(horizontal text only)** |
| | | Anchor is at the descender line (bottom) |
| | | of the last line of text. |
+---+-----------------------+-------------------------------------------------------+
See `Font metrics on Wikipedia <https://en.wikipedia.org/wiki/Typeface#Font_metrics>`_
for more information on the specific terms used.
Example: Draw Partial Opacity Text Example: Draw Partial Opacity Text
---------------------------------- ----------------------------------
@ -295,18 +350,27 @@ Methods
Draws the string at the given position. Draws the string at the given position.
:param xy: Top left corner of the text. :param xy: The anchor coordinates of the text.
:param text: Text to be drawn. If it contains any newline characters, :param text: Text to be drawn. If it contains any newline characters,
the text is passed on to the text is passed on to
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`. :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`.
:param fill: Color to use for the text. :param fill: Color to use for the text.
:param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
:param anchor: The text anchor alignment. Determines the relative location of
the anchor to the text. The default alignment is top left.
See :ref:`Text anchors` for valid values. This parameter is
ignored for legacy PIL fonts.
.. note:: This parameter was present in earlier versions
of Pillow, but implemented only in version 7.2.0.
:param spacing: If the text is passed on to :param spacing: If the text is passed on to
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`,
the number of pixels between lines. the number of pixels between lines.
:param align: If the text is passed on to :param align: If the text is passed on to
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`,
``"left"``, ``"center"`` or ``"right"``. ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
Use the ``anchor`` parameter to specify the alignment to ``xy``.
:param direction: Direction of the text. It can be ``"rtl"`` (right to :param direction: Direction of the text. It can be ``"rtl"`` (right to
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
Requires libraqm. Requires libraqm.
@ -347,12 +411,22 @@ Methods
Draws the string at the given position. Draws the string at the given position.
:param xy: Top left corner of the text. :param xy: The anchor coordinates of the text.
:param text: Text to be drawn. :param text: Text to be drawn.
:param fill: Color to use for the text. :param fill: Color to use for the text.
:param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
:param anchor: The text anchor alignment. Determines the relative location of
the anchor to the text. The default alignment is top left.
See :ref:`Text anchors` for valid values. This parameter is
ignored for legacy PIL fonts.
.. note:: This parameter was present in earlier versions
of Pillow, but implemented only in version 7.2.0.
:param spacing: The number of pixels between lines. :param spacing: The number of pixels between lines.
:param align: ``"left"``, ``"center"`` or ``"right"``. :param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
Use the ``anchor`` parameter to specify the alignment to ``xy``.
:param direction: Direction of the text. It can be ``"rtl"`` (right to :param direction: Direction of the text. It can be ``"rtl"`` (right to
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
Requires libraqm. Requires libraqm.

View File

@ -320,6 +320,7 @@ class ImageDraw:
features=features, features=features,
language=language, language=language,
stroke_width=stroke_width, stroke_width=stroke_width,
anchor=anchor,
*args, *args,
**kwargs, **kwargs,
) )
@ -333,6 +334,7 @@ class ImageDraw:
features, features,
language, language,
stroke_width, stroke_width,
anchor,
*args, *args,
**kwargs, **kwargs,
) )
@ -353,7 +355,7 @@ class ImageDraw:
draw_text(stroke_ink, stroke_width) draw_text(stroke_ink, stroke_width)
# Draw normal text # Draw normal text
draw_text(ink, 0, (stroke_width, stroke_width)) draw_text(ink, 0)
else: else:
# Only draw normal text # Only draw normal text
draw_text(ink) draw_text(ink)
@ -373,6 +375,16 @@ class ImageDraw:
stroke_width=0, stroke_width=0,
stroke_fill=None, stroke_fill=None,
): ):
if direction == "ttb":
raise ValueError("ttb direction is unsupported for multiline text")
if anchor is None:
anchor = "la"
elif len(anchor) != 2:
raise ValueError("anchor must be a 2 character string")
elif anchor[1] in "tb":
raise ValueError("anchor not supported for multiline text")
widths = [] widths = []
max_width = 0 max_width = 0
lines = self._multiline_split(text) lines = self._multiline_split(text)
@ -390,16 +402,33 @@ class ImageDraw:
) )
widths.append(line_width) widths.append(line_width)
max_width = max(max_width, line_width) max_width = max(max_width, line_width)
left, top = xy
top = xy[1]
if anchor[1] == "m":
top -= (len(lines) - 1) * line_spacing / 2.0
elif anchor[1] == "d":
top -= (len(lines) - 1) * line_spacing
for idx, line in enumerate(lines): for idx, line in enumerate(lines):
left = xy[0]
width_difference = max_width - widths[idx]
# first align left by anchor
if anchor[0] == "m":
left -= width_difference / 2.0
elif anchor[0] == "r":
left -= width_difference
# then align by align parameter
if align == "left": if align == "left":
pass # left = x pass
elif align == "center": elif align == "center":
left += (max_width - widths[idx]) / 2.0 left += width_difference / 2.0
elif align == "right": elif align == "right":
left += max_width - widths[idx] left += width_difference
else: else:
raise ValueError('align must be "left", "center" or "right"') raise ValueError('align must be "left", "center" or "right"')
self.text( self.text(
(left, top), (left, top),
line, line,
@ -413,7 +442,6 @@ class ImageDraw:
stroke_fill=stroke_fill, stroke_fill=stroke_fill,
) )
top += line_spacing top += line_spacing
left = xy[0]
def textsize( def textsize(
self, self,

View File

@ -347,6 +347,7 @@ class FreeTypeFont:
features=None, features=None,
language=None, language=None,
stroke_width=0, stroke_width=0,
anchor=None,
): ):
""" """
Create a bitmap for the text. Create a bitmap for the text.
@ -395,6 +396,12 @@ class FreeTypeFont:
.. versionadded:: 6.2.0 .. versionadded:: 6.2.0
:param anchor: The text anchor alignment. Determines the relative location of
the anchor to the text. The default alignment is top left.
See :ref:`Text anchors` for valid values.
.. versionadded:: 8.0.0
:return: An internal PIL storage memory instance as defined by the :return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module. :py:mod:`PIL.Image.core` interface module.
""" """
@ -405,6 +412,7 @@ class FreeTypeFont:
features=features, features=features,
language=language, language=language,
stroke_width=stroke_width, stroke_width=stroke_width,
anchor=anchor,
)[0] )[0]
def getmask2( def getmask2(
@ -416,6 +424,7 @@ class FreeTypeFont:
features=None, features=None,
language=None, language=None,
stroke_width=0, stroke_width=0,
anchor=None,
*args, *args,
**kwargs, **kwargs,
): ):
@ -466,14 +475,21 @@ class FreeTypeFont:
.. versionadded:: 6.2.0 .. versionadded:: 6.2.0
:param anchor: The text anchor alignment. Determines the relative location of
the anchor to the text. The default alignment is top left.
See :ref:`Text anchors` for valid values.
.. versionadded:: 8.0.0
:return: A tuple of an internal PIL storage memory instance as defined by the :return: A tuple of an internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module, and the text offset, the :py:mod:`PIL.Image.core` interface module, and the text offset, the
gap between the starting coordinate and the first marking gap between the starting coordinate and the first marking
""" """
size, offset = self.font.getsize( size, offset = self.font.getsize(
text, mode == "1", direction, features, language text, mode == "1", direction, features, language, anchor
) )
size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 size = size[0] + stroke_width * 2, size[1] + stroke_width * 2
offset = offset[0] - stroke_width, offset[1] - stroke_width
im = fill("L", size, 0) im = fill("L", size, 0)
self.font.render( self.font.render(
text, im.id, mode == "1", direction, features, language, stroke_width text, im.id, mode == "1", direction, features, language, stroke_width

View File

@ -626,17 +626,25 @@ font_getsize(FontObject* self, PyObject* args)
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
const char *dir = NULL; const char *dir = NULL;
const char *lang = NULL; const char *lang = NULL;
const char *anchor = NULL;
PyObject *features = Py_None; PyObject *features = Py_None;
PyObject *string; PyObject *string;
/* calculate size and bearing for a given string */ /* calculate size and bearing for a given string */
if (!PyArg_ParseTuple(args, "O|izOz:getsize", &string, &mask, &dir, &features, &lang)) { if (!PyArg_ParseTuple(args, "O|izOzz:getsize", &string, &mask, &dir, &features, &lang, &anchor)) {
return NULL; return NULL;
} }
horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
if (anchor == NULL) {
anchor = horizontal_dir ? "la" : "lt";
}
if (strlen(anchor) != 2) {
goto bad_anchor;
}
count = text_layout(string, self, dir, features, lang, &glyph_info, mask); count = text_layout(string, self, dir, features, lang, &glyph_info, mask);
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
return NULL; return NULL;
@ -720,11 +728,75 @@ font_getsize(FontObject* self, PyObject* args)
x_anchor = y_anchor = 0; x_anchor = y_anchor = 0;
if (face) { if (face) {
if (horizontal_dir) { if (horizontal_dir) {
x_anchor = 0; switch (anchor[0]) {
y_anchor = PIXEL(self->face->size->metrics.ascender); case 'l': // left
x_anchor = 0;
break;
case 'm': // middle (left + right) / 2
x_anchor = PIXEL(position / 2);
break;
case 'r': // right
x_anchor = PIXEL(position);
break;
case 's': // vertical baseline
default:
goto bad_anchor;
}
switch (anchor[1]) {
case 'a': // ascender
y_anchor = PIXEL(self->face->size->metrics.ascender);
break;
case 't': // top
y_anchor = y_max;
break;
case 'm': // middle (ascender + descender) / 2
y_anchor = PIXEL((self->face->size->metrics.ascender + self->face->size->metrics.descender) / 2);
break;
case 's': // horizontal baseline
y_anchor = 0;
break;
case 'b': // bottom
y_anchor = y_min;
break;
case 'd': // descender
y_anchor = PIXEL(self->face->size->metrics.descender);
break;
default:
goto bad_anchor;
}
} else { } else {
x_anchor = x_min; switch (anchor[0]) {
y_anchor = 0; case 'l': // left
x_anchor = x_min;
break;
case 'm': // middle (left + right) / 2
x_anchor = (x_min + x_max) / 2;
break;
case 'r': // right
x_anchor = x_max;
break;
case 's': // vertical baseline
x_anchor = 0;
break;
default:
goto bad_anchor;
}
switch (anchor[1]) {
case 't': // top
y_anchor = 0;
break;
case 'm': // middle (top + bottom) / 2
y_anchor = PIXEL(position / 2);
break;
case 'b': // bottom
y_anchor = PIXEL(position);
break;
case 'a': // ascender
case 's': // horizontal baseline
case 'd': // descender
default:
goto bad_anchor;
}
} }
} }
@ -733,6 +805,10 @@ font_getsize(FontObject* self, PyObject* args)
(x_max - x_min), (y_max - y_min), (x_max - x_min), (y_max - y_min),
(-x_anchor + x_min), -(-y_anchor + y_max) (-x_anchor + x_min), -(-y_anchor + y_max)
); );
bad_anchor:
PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor);
return NULL;
} }
static PyObject* static PyObject*