diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 547172c39..0eb3e4325 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -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 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 `_ +for more information on the specific terms used. + + Example: Draw Partial Opacity Text ---------------------------------- @@ -295,18 +350,27 @@ Methods 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, the text is passed on to :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`. :param fill: Color to use for the text. :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 :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, the number of pixels between lines. :param align: If the text is passed on to :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 left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -347,12 +411,22 @@ Methods 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 fill: Color to use for the text. :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 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 left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 47d970aef..820dcc31d 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -320,6 +320,7 @@ class ImageDraw: features=features, language=language, stroke_width=stroke_width, + anchor=anchor, *args, **kwargs, ) @@ -333,6 +334,7 @@ class ImageDraw: features, language, stroke_width, + anchor, *args, **kwargs, ) @@ -353,7 +355,7 @@ class ImageDraw: draw_text(stroke_ink, stroke_width) # Draw normal text - draw_text(ink, 0, (stroke_width, stroke_width)) + draw_text(ink, 0) else: # Only draw normal text draw_text(ink) @@ -373,6 +375,16 @@ class ImageDraw: stroke_width=0, 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 = [] max_width = 0 lines = self._multiline_split(text) @@ -390,16 +402,33 @@ class ImageDraw: ) widths.append(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): + 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": - pass # left = x + pass elif align == "center": - left += (max_width - widths[idx]) / 2.0 + left += width_difference / 2.0 elif align == "right": - left += max_width - widths[idx] + left += width_difference else: raise ValueError('align must be "left", "center" or "right"') + self.text( (left, top), line, @@ -413,7 +442,6 @@ class ImageDraw: stroke_fill=stroke_fill, ) top += line_spacing - left = xy[0] def textsize( self, diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 5528108da..75ff485da 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -347,6 +347,7 @@ class FreeTypeFont: features=None, language=None, stroke_width=0, + anchor=None, ): """ Create a bitmap for the text. @@ -395,6 +396,12 @@ class FreeTypeFont: .. 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 :py:mod:`PIL.Image.core` interface module. """ @@ -405,6 +412,7 @@ class FreeTypeFont: features=features, language=language, stroke_width=stroke_width, + anchor=anchor, )[0] def getmask2( @@ -416,6 +424,7 @@ class FreeTypeFont: features=None, language=None, stroke_width=0, + anchor=None, *args, **kwargs, ): @@ -466,14 +475,21 @@ class FreeTypeFont: .. 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 :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ 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 + offset = offset[0] - stroke_width, offset[1] - stroke_width im = fill("L", size, 0) self.font.render( text, im.id, mode == "1", direction, features, language, stroke_width diff --git a/src/_imagingft.c b/src/_imagingft.c index 70817002e..2435fbab4 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -626,17 +626,25 @@ font_getsize(FontObject* self, PyObject* args) int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ const char *dir = NULL; const char *lang = NULL; + const char *anchor = NULL; PyObject *features = Py_None; PyObject *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; } 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); if (PyErr_Occurred()) { return NULL; @@ -720,11 +728,75 @@ font_getsize(FontObject* self, PyObject* args) x_anchor = y_anchor = 0; if (face) { if (horizontal_dir) { - x_anchor = 0; - y_anchor = PIXEL(self->face->size->metrics.ascender); + switch (anchor[0]) { + 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 { - x_anchor = x_min; - y_anchor = 0; + switch (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_anchor + x_min), -(-y_anchor + y_max) ); + +bad_anchor: + PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor); + return NULL; } static PyObject*