implement text anchor for truetype fonts

This commit is contained in:
nulano 2020-04-14 13:28:09 +02:00
parent 5e271e2a6c
commit bd693f7ecf
4 changed files with 216 additions and 20 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
---------------------------------- ----------------------------------
@ -277,18 +332,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.
@ -329,12 +393,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

@ -314,6 +314,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,
) )
@ -327,6 +328,7 @@ class ImageDraw:
features, features,
language, language,
stroke_width, stroke_width,
anchor,
*args, *args,
**kwargs, **kwargs,
) )
@ -347,7 +349,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)
@ -367,6 +369,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)
@ -384,16 +396,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,
@ -407,7 +436,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

@ -345,6 +345,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.
@ -393,6 +394,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:: 7.2.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.
""" """
@ -403,6 +410,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(
@ -414,6 +422,7 @@ class FreeTypeFont:
features=None, features=None,
language=None, language=None,
stroke_width=0, stroke_width=0,
anchor=None,
*args, *args,
**kwargs **kwargs
): ):
@ -464,14 +473,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:: 7.2.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

@ -612,12 +612,13 @@ font_getsize(FontObject* self, PyObject* args)
int position, advanced; int position, advanced;
int x_max, x_min, y_max, y_min; int x_max, x_min, y_max, y_min;
FT_Face face; FT_Face face;
int xoffset, yoffset; int x_anchor, y_anchor;
int horizontal_dir; int horizontal_dir;
int mask = 0; int mask = 0;
int load_flags; int load_flags;
const char *dir = NULL; const char *dir = NULL;
const char *lang = NULL; const char *lang = NULL;
const char *anchor = NULL;
size_t i, count; size_t i, count;
GlyphInfo *glyph_info = NULL; GlyphInfo *glyph_info = NULL;
PyObject *features = Py_None; PyObject *features = Py_None;
@ -625,10 +626,19 @@ font_getsize(FontObject* self, PyObject* args)
/* calculate size and bearing for a given string */ /* calculate size and bearing for a given string */
PyObject* string; PyObject* string;
if (!PyArg_ParseTuple(args, "O|izOz:getsize", &string, &mask, &dir, &features, &lang)) { if (!PyArg_ParseTuple(args, "O|izOzzz:getsize", &string, &mask, &dir, &features, &lang, &anchor)) {
return NULL; 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); count = text_layout(string, self, dir, features, lang, &glyph_info, mask);
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
return NULL; return NULL;
@ -636,7 +646,6 @@ font_getsize(FontObject* self, PyObject* args)
face = NULL; face = NULL;
position = x_max = x_min = y_max = y_min = 0; position = x_max = x_min = y_max = y_min = 0;
horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
for (i = 0; i < count; i++) { for (i = 0; i < count; i++) {
int index, error, offset; int index, error, offset;
FT_BBox bbox; FT_BBox bbox;
@ -733,21 +742,90 @@ font_getsize(FontObject* self, PyObject* args)
glyph_info = NULL; glyph_info = NULL;
} }
x_anchor = y_anchor = 0;
if (face) { if (face) {
if (horizontal_dir) { if (horizontal_dir) {
xoffset = 0; switch (anchor[0]) {
yoffset = self->face->size->metrics.ascender - y_max; case 'l': // left
x_anchor = 0;
break;
case 'm': // middle (left + right) / 2
x_anchor = position / 2;
break;
case 'r': // right
x_anchor = position;
break;
case 's': // vertical baseline
default:
goto bad_anchor;
}
switch (anchor[1]) {
case 'a': // ascender
y_anchor = self->face->size->metrics.ascender;
break;
case 't': // top
y_anchor = y_max;
break;
case 'm': // middle (ascender + descender) / 2
y_anchor = (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 = self->face->size->metrics.descender;
break;
default:
goto bad_anchor;
}
} else { } else {
xoffset = 0; switch (anchor[0]) {
yoffset = -y_max; 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 = position / 2;
break;
case 'b': // bottom
y_anchor = position;
break;
case 'a': // ascender
case 's': // horizontal baseline
case 'd': // descender
default:
goto bad_anchor;
}
} }
} }
return Py_BuildValue( return Py_BuildValue(
"(ii)(ii)", "(ii)(ii)",
PIXEL(x_max - x_min), PIXEL(y_max - y_min), PIXEL(x_max - x_min), PIXEL(y_max - y_min),
PIXEL(xoffset), PIXEL(yoffset) PIXEL(-x_anchor + x_min), PIXEL(y_anchor - y_max)
); );
bad_anchor:
PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor);
return NULL;
} }
static PyObject* static PyObject*