Repository: python-pillow/Pillow

Issue:
RTL Direction not having any effect
### What did you do?
 I'm trying to put text on top of an image and I also set the direction to RTL when doing this before I used to get some library not installed called libraqm but no with it install it still doesn't work I'm testing with the Arabic language 
### What did you expect to happen?
 the text goes RTL
### What actually happened?
the text went LTR
### What are your OS, Python and Pillow versions?
* OS: Ubuntu 18
* Python: 3.6
* Pillow: latest version

```python
def PhotoAlterer(name):
    image = Image.open('check.jpeg')

    draw = ImageDraw.Draw(image)

    font = ImageFont.truetype('din-next-lt-w23-medium.ttf', size=60)

    text_to_be_reshaped = '%s' % name
    reshaped_text = arabic_reshaper.reshape(text_to_be_reshaped)
    (x, y) = (1050, 427)
    color = 'rgb(255, 255, 255)'  # white color
    draw.text((x, y), reshaped_text , fill=color, font=font, direction="rtl")

    image.save('pImg/%s.png' % name)

```
Vertical text alignment ignores yOffset
While working on implementing the `anchor` parameter of `ImageDraw.text` (see https://github.com/python-pillow/Pillow/issues/4511#issuecomment-612940904), I noticed a bug with the handling of y_offset with raqm.

For the following code:

```python
from PIL import Image, ImageDraw, ImageFont
font = ImageFont.truetype("segoeui.ttf", 60, layout_engine=ImageFont.LAYOUT_RAQM)
def test(text):
    im = Image.new("RGB", (200,200), "white")
    d = ImageDraw.Draw(im)
    d.line(((0,100),(200,100)), "black")
    d.line(((100,0),(100,200)), "black")
    d.text((100,100),text,"black", font=font)
    im.show()
test("a" + ("\u030C" * 20))    # a + 20 * Unicode Character 'COMBINING CARON' (U+030C)
```

I expect to get the following image:
![image](https://user-images.githubusercontent.com/3819630/79150886-ced22b00-7dc9-11ea-8d7d-3a7da6a96db0.png)

But I get the following image (with master):
![image](https://user-images.githubusercontent.com/3819630/79151082-1ce72e80-7dca-11ea-9952-5262d59c039f.png)

This is because when raqm support was added, the `yoffset` variable was not updated to look at the `glyphinfo[i].y_offset` value: https://github.com/python-pillow/Pillow/blob/97280a045cc2544ef8855dac2765e765565772cd/src/_imagingft.c#L665-L674

Since `font_render` looks at the `bitmap.top` value anyway (see below), which should be equal to `bbox.yMax`, I believe `yoffset` is supposed to calculate the same value as `y_max`, so it can just be removed and `y_max` used instead (in second snippet below).
https://github.com/python-pillow/Pillow/blob/97280a045cc2544ef8855dac2765e765565772cd/src/_imagingft.c#L801-L804
https://github.com/python-pillow/Pillow/blob/97280a045cc2544ef8855dac2765e765565772cd/src/_imagingft.c#L710-L712

I am planning to include this change in the PR implementing the `anchor` parameter, but I wanted to document this as a separate issue.


Discussion:
Could you adjust the code that you've provided so that it's self-contained? At the moment, I don't know the value of `name` or what `arabic_reshaper` does.
This is the sequel to #4496

So from that, it seems reasonable to presume that `arabic_reshaper` is https://pypi.org/project/arabic-reshaper/, and that 'محمد' is a valid value for `text_to_be_reshaped`.

```python
from PIL import Image, ImageDraw, ImageFont
import arabic_reshaper

def PhotoAlterer(name):
    image = Image.open('check.jpeg')

    draw = ImageDraw.Draw(image)

    font = ImageFont.truetype('din-next-lt-w23-medium.ttf', size=60)

    text_to_be_reshaped = '%s' % name
    reshaped_text = arabic_reshaper.reshape(text_to_be_reshaped)
    (x, y) = (1050, 427)
    color = 'rgb(255, 255, 255)'  # white color
    draw.text((x, y), reshaped_text , fill=color, font=font, direction="rtl")

    image.save('pImg/%s.png' % name)

PhotoAlterer('محمد')
```

You may be interested in https://github.com/python-pillow/Pillow/issues/3081#issuecomment-557918892, but more likely, you're looking for https://github.com/python-pillow/Pillow/issues/3125#issuecomment-504790947

> The `direction` parameter sets the overall paragraph direction, the individual character's direction is determined by BiDi algorithm. Unicode bidi control characters can be to force text direction to be LTR or RTL and override the algorithm (though that is not usually something very useful todo, expect for ancient scripts that can be written in either direction or pseudo RTL localization strings).


Resolving patch:
diff --git a/docs/example/anchors.py b/docs/example/anchors.py
new file mode 100644
--- /dev/null
+++ b/docs/example/anchors.py
@@ -0,0 +1,28 @@
+from PIL import Image, ImageDraw, ImageFont
+
+font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 16)
+
+
+def test(anchor):
+    im = Image.new("RGBA", (200, 100), "white")
+    d = ImageDraw.Draw(im)
+    d.line(((100, 0), (100, 100)), "gray")
+    d.line(((0, 50), (200, 50)), "gray")
+    d.text((100, 50), f"{anchor} example", "black", font, anchor)
+    return im
+
+
+if __name__ == "__main__":
+    im = Image.new("RGBA", (600, 300), "white")
+    d = ImageDraw.Draw(im)
+    for y, row in enumerate(
+        (("ma", "mt", "mm"), ("ms", "mb", "md"), ("ls", "ms", "rs"))
+    ):
+        for x, anchor in enumerate(row):
+            im.paste(test(anchor), (x * 200, y * 100))
+            if x != 0:
+                d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3)
+            if y != 0:
+                d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3)
+    im.save("docs/example/anchors.png")
+    im.show()
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -320,6 +320,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None):
                     features=features,
                     language=language,
                     stroke_width=stroke_width,
+                    anchor=anchor,
                     *args,
                     **kwargs,
                 )
@@ -333,6 +334,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None):
                         features,
                         language,
                         stroke_width,
+                        anchor,
                         *args,
                         **kwargs,
                     )
@@ -353,7 +355,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None):
                 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 @@ def multiline_text(
         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 @@ def multiline_text(
             )
             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 @@ def multiline_text(
                 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
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -347,6 +347,7 @@ def getmask(
         features=None,
         language=None,
         stroke_width=0,
+        anchor=None,
     ):
         """
         Create a bitmap for the text.
@@ -395,6 +396,12 @@ def getmask(
 
                          .. 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 @@ def getmask(
             features=features,
             language=language,
             stroke_width=stroke_width,
+            anchor=anchor,
         )[0]
 
     def getmask2(
@@ -416,6 +424,7 @@ def getmask2(
         features=None,
         language=None,
         stroke_width=0,
+        anchor=None,
         *args,
         **kwargs,
     ):
@@ -466,14 +475,21 @@ def getmask2(
 
                          .. 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


Test patch:
diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt
--- a/Tests/fonts/LICENSE.txt
+++ b/Tests/fonts/LICENSE.txt
@@ -1,5 +1,6 @@
 
 NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
+NotoSans-Regular.ttf, from https://www.google.com/get/noto/
 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
diff --git a/Tests/fonts/NotoSans-Regular.ttf b/Tests/fonts/NotoSans-Regular.ttf
new file mode 100644
Binary files /dev/null and b/Tests/fonts/NotoSans-Regular.ttf differ
diff --git a/Tests/images/test_anchor_multiline_lm_center.png b/Tests/images/test_anchor_multiline_lm_center.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_multiline_lm_center.png differ
diff --git a/Tests/images/test_anchor_multiline_lm_left.png b/Tests/images/test_anchor_multiline_lm_left.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_multiline_lm_left.png differ
diff --git a/Tests/images/test_anchor_multiline_lm_right.png b/Tests/images/test_anchor_multiline_lm_right.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_multiline_lm_right.png differ
diff --git a/Tests/images/test_anchor_multiline_ma_center.png b/Tests/images/test_anchor_multiline_ma_center.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_multiline_ma_center.png differ
diff --git a/Tests/images/test_anchor_multiline_md_center.png b/Tests/images/test_anchor_multiline_md_center.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_multiline_md_center.png differ
diff --git a/Tests/images/test_anchor_multiline_mm_center.png b/Tests/images/test_anchor_multiline_mm_center.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_multiline_mm_center.png differ
diff --git a/Tests/images/test_anchor_multiline_mm_left.png b/Tests/images/test_anchor_multiline_mm_left.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_multiline_mm_left.png differ
diff --git a/Tests/images/test_anchor_multiline_mm_right.png b/Tests/images/test_anchor_multiline_mm_right.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_multiline_mm_right.png differ
diff --git a/Tests/images/test_anchor_multiline_rm_center.png b/Tests/images/test_anchor_multiline_rm_center.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_multiline_rm_center.png differ
diff --git a/Tests/images/test_anchor_multiline_rm_left.png b/Tests/images/test_anchor_multiline_rm_left.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_multiline_rm_left.png differ
diff --git a/Tests/images/test_anchor_multiline_rm_right.png b/Tests/images/test_anchor_multiline_rm_right.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_multiline_rm_right.png differ
diff --git a/Tests/images/test_anchor_quick_ls.png b/Tests/images/test_anchor_quick_ls.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_quick_ls.png differ
diff --git a/Tests/images/test_anchor_quick_ma.png b/Tests/images/test_anchor_quick_ma.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_quick_ma.png differ
diff --git a/Tests/images/test_anchor_quick_mb.png b/Tests/images/test_anchor_quick_mb.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_quick_mb.png differ
diff --git a/Tests/images/test_anchor_quick_md.png b/Tests/images/test_anchor_quick_md.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_quick_md.png differ
diff --git a/Tests/images/test_anchor_quick_mm.png b/Tests/images/test_anchor_quick_mm.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_quick_mm.png differ
diff --git a/Tests/images/test_anchor_quick_ms.png b/Tests/images/test_anchor_quick_ms.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_quick_ms.png differ
diff --git a/Tests/images/test_anchor_quick_mt.png b/Tests/images/test_anchor_quick_mt.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_quick_mt.png differ
diff --git a/Tests/images/test_anchor_quick_rs.png b/Tests/images/test_anchor_quick_rs.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_quick_rs.png differ
diff --git a/Tests/images/test_anchor_ttb_f_lt.png b/Tests/images/test_anchor_ttb_f_lt.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_lt.png differ
diff --git a/Tests/images/test_anchor_ttb_f_mm.png b/Tests/images/test_anchor_ttb_f_mm.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_mm.png differ
diff --git a/Tests/images/test_anchor_ttb_f_rb.png b/Tests/images/test_anchor_ttb_f_rb.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_rb.png differ
diff --git a/Tests/images/test_anchor_ttb_f_sm.png b/Tests/images/test_anchor_ttb_f_sm.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_sm.png differ
diff --git a/Tests/images/test_combine_caron.png b/Tests/images/test_combine_caron.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_caron.png differ
diff --git a/Tests/images/test_combine_caron_below.png b/Tests/images/test_combine_caron_below.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_caron_below.png differ
diff --git a/Tests/images/test_combine_caron_below_lb.png b/Tests/images/test_combine_caron_below_lb.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_caron_below_lb.png differ
diff --git a/Tests/images/test_combine_caron_below_ld.png b/Tests/images/test_combine_caron_below_ld.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_caron_below_ld.png differ
diff --git a/Tests/images/test_combine_caron_below_ls.png b/Tests/images/test_combine_caron_below_ls.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_caron_below_ls.png differ
diff --git a/Tests/images/test_combine_caron_below_ttb.png b/Tests/images/test_combine_caron_below_ttb.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_caron_below_ttb.png differ
diff --git a/Tests/images/test_combine_caron_below_ttb_lb.png b/Tests/images/test_combine_caron_below_ttb_lb.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_caron_below_ttb_lb.png differ
diff --git a/Tests/images/test_combine_caron_la.png b/Tests/images/test_combine_caron_la.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_caron_la.png differ
diff --git a/Tests/images/test_combine_caron_ls.png b/Tests/images/test_combine_caron_ls.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_caron_ls.png differ
diff --git a/Tests/images/test_combine_caron_lt.png b/Tests/images/test_combine_caron_lt.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_caron_lt.png differ
diff --git a/Tests/images/test_combine_caron_ttb.png b/Tests/images/test_combine_caron_ttb.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_caron_ttb.png differ
diff --git a/Tests/images/test_combine_caron_ttb_lt.png b/Tests/images/test_combine_caron_ttb_lt.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_caron_ttb_lt.png differ
diff --git a/Tests/images/test_combine_double_breve_below.png b/Tests/images/test_combine_double_breve_below.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_double_breve_below.png differ
diff --git a/Tests/images/test_combine_double_breve_below_ma.png b/Tests/images/test_combine_double_breve_below_ma.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ma.png differ
diff --git a/Tests/images/test_combine_double_breve_below_ra.png b/Tests/images/test_combine_double_breve_below_ra.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ra.png differ
diff --git a/Tests/images/test_combine_double_breve_below_ttb.png b/Tests/images/test_combine_double_breve_below_ttb.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb.png differ
diff --git a/Tests/images/test_combine_double_breve_below_ttb_mt.png b/Tests/images/test_combine_double_breve_below_ttb_mt.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb_mt.png differ
diff --git a/Tests/images/test_combine_double_breve_below_ttb_rt.png b/Tests/images/test_combine_double_breve_below_ttb_rt.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb_rt.png differ
diff --git a/Tests/images/test_combine_double_breve_below_ttb_st.png b/Tests/images/test_combine_double_breve_below_ttb_st.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb_st.png differ
diff --git a/Tests/images/test_combine_overline.png b/Tests/images/test_combine_overline.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_overline.png differ
diff --git a/Tests/images/test_combine_overline_la.png b/Tests/images/test_combine_overline_la.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_overline_la.png differ
diff --git a/Tests/images/test_combine_overline_ra.png b/Tests/images/test_combine_overline_ra.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_overline_ra.png differ
diff --git a/Tests/images/test_combine_overline_ttb.png b/Tests/images/test_combine_overline_ttb.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_overline_ttb.png differ
diff --git a/Tests/images/test_combine_overline_ttb_mt.png b/Tests/images/test_combine_overline_ttb_mt.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_overline_ttb_mt.png differ
diff --git a/Tests/images/test_combine_overline_ttb_rt.png b/Tests/images/test_combine_overline_ttb_rt.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_overline_ttb_rt.png differ
diff --git a/Tests/images/test_combine_overline_ttb_st.png b/Tests/images/test_combine_overline_ttb_st.png
new file mode 100644
Binary files /dev/null and b/Tests/images/test_combine_overline_ttb_st.png differ
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -1020,7 +1020,7 @@ def test_stroke():
         font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
 
         # Act
-        draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill)
+        draw.text((12, 12), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill)
 
         # Assert
         assert_image_similar_tofile(
@@ -1036,7 +1036,7 @@ def test_stroke_descender():
     font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
 
     # Act
-    draw.text((10, 0), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0")
+    draw.text((12, 2), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0")
 
     # Assert
     assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76)
@@ -1051,7 +1051,7 @@ def test_stroke_multiline():
 
     # Act
     draw.multiline_text(
-        (10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0"
+        (12, 12), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0"
     )
 
     # Assert
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -40,18 +40,21 @@ class TestImageFont:
             "textsize": 12,
             "getters": (13, 16),
             "mask": (107, 13),
+            "multiline-anchor": 6,
         },
         (">=2.7",): {
             "multiline": 6.2,
             "textsize": 2.5,
             "getters": (12, 16),
             "mask": (108, 13),
+            "multiline-anchor": 4,
         },
         "Default": {
             "multiline": 0.5,
             "textsize": 0.5,
             "getters": (12, 16),
             "mask": (108, 13),
+            "multiline-anchor": 4,
         },
     }
 
@@ -750,6 +753,93 @@ def test_variation_set_by_axes(self):
         font.set_variation_by_axes([100])
         self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)
 
+    @pytest.mark.parametrize(
+        "anchor",
+        (
+            # test horizontal anchors
+            "ls",
+            "ms",
+            "rs",
+            # test vertical anchors
+            "ma",
+            "mt",
+            "mm",
+            "mb",
+            "md",
+        ),
+    )
+    def test_anchor(self, anchor):
+        name, text = "quick", "Quick"
+        path = f"Tests/images/test_anchor_{name}_{anchor}.png"
+        f = ImageFont.truetype(
+            "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE
+        )
+
+        im = Image.new("RGB", (200, 200), "white")
+        d = ImageDraw.Draw(im)
+        d.line(((0, 100), (200, 100)), "gray")
+        d.line(((100, 0), (100, 200)), "gray")
+        d.text((100, 100), text, fill="black", anchor=anchor, font=f)
+
+        with Image.open(path) as expected:
+            assert_image_similar(im, expected, 7)
+
+    @pytest.mark.parametrize(
+        "anchor,align",
+        (
+            # test horizontal anchors
+            ("lm", "left"),
+            ("lm", "center"),
+            ("lm", "right"),
+            ("mm", "left"),
+            ("mm", "center"),
+            ("mm", "right"),
+            ("rm", "left"),
+            ("rm", "center"),
+            ("rm", "right"),
+            # test vertical anchors
+            ("ma", "center"),
+            # ("mm", "center"),  # duplicate
+            ("md", "center"),
+        ),
+    )
+    def test_anchor_multiline(self, anchor, align):
+        target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png"
+        text = "a\nlong\ntext sample"
+
+        f = ImageFont.truetype(
+            "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE
+        )
+
+        # test render
+        im = Image.new("RGB", (600, 400), "white")
+        d = ImageDraw.Draw(im)
+        d.line(((0, 200), (600, 200)), "gray")
+        d.line(((300, 0), (300, 400)), "gray")
+        d.multiline_text(
+            (300, 200), text, fill="black", anchor=anchor, font=f, align=align
+        )
+
+        with Image.open(target) as expected:
+            assert_image_similar(im, expected, self.metrics["multiline-anchor"])
+
+    def test_anchor_invalid(self):
+        font = self.get_font()
+        im = Image.new("RGB", (100, 100), "white")
+        d = ImageDraw.Draw(im)
+        d.font = font
+
+        for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]:
+            pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor))
+            pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor))
+            pytest.raises(
+                ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
+            )
+        for anchor in ["lt", "lb"]:
+            pytest.raises(
+                ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
+            )
+
 
 @skip_unless_feature("raqm")
 class TestImageFont_RaqmLayout(TestImageFont):
diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py
--- a/Tests/test_imagefontctl.py
+++ b/Tests/test_imagefontctl.py
@@ -1,6 +1,7 @@
 import pytest
+from packaging.version import parse as parse_version
 
-from PIL import Image, ImageDraw, ImageFont
+from PIL import Image, ImageDraw, ImageFont, features
 
 from .helper import assert_image_similar, skip_unless_feature
 
@@ -123,7 +124,7 @@ def test_text_direction_ttb_stroke():
     draw = ImageDraw.Draw(im)
     try:
         draw.text(
-            (25, 25),
+            (27, 27),
             "あい",
             font=ttf,
             fill=500,
@@ -206,3 +207,118 @@ def test_language():
     target = "Tests/images/test_language.png"
     with Image.open(target) as target_img:
         assert_image_similar(im, target_img, 0.5)
+
+
+@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
+def test_anchor_ttb(anchor):
+    if parse_version(features.version_module("freetype2")) < parse_version("2.5.1"):
+        # FreeType 2.5.1 README: Miscellaneous Changes:
+        # Improved computation of emulated vertical metrics for TrueType fonts.
+        pytest.skip("FreeType <2.5.1 has incompatible ttb metrics")
+
+    text = "f"
+    path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png"
+    f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120)
+
+    im = Image.new("RGB", (200, 400), "white")
+    d = ImageDraw.Draw(im)
+    d.line(((0, 200), (200, 200)), "gray")
+    d.line(((100, 0), (100, 400)), "gray")
+    try:
+        d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f)
+    except ValueError as ex:
+        if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
+            pytest.skip("libraqm 0.7 or greater not available")
+
+    with Image.open(path) as expected:
+        assert_image_similar(im, expected, 1)  # fails at 5
+
+
+combine_tests = (
+    # extends above (e.g. issue #4553)
+    ("caron", "a\u030C\u030C\u030C\u030C\u030Cb", None, None, 0.08),
+    ("caron_la", "a\u030C\u030C\u030C\u030C\u030Cb", "la", None, 0.08),
+    ("caron_lt", "a\u030C\u030C\u030C\u030C\u030Cb", "lt", None, 0.08),
+    ("caron_ls", "a\u030C\u030C\u030C\u030C\u030Cb", "ls", None, 0.08),
+    ("caron_ttb", "ca" + ("\u030C" * 15) + "b", None, "ttb", 0.3),
+    ("caron_ttb_lt", "ca" + ("\u030C" * 15) + "b", "lt", "ttb", 0.3),
+    # extends below
+    ("caron_below", "a\u032C\u032C\u032C\u032C\u032Cb", None, None, 0.02),
+    ("caron_below_ld", "a\u032C\u032C\u032C\u032C\u032Cb", "ld", None, 0.02),
+    ("caron_below_lb", "a\u032C\u032C\u032C\u032C\u032Cb", "lb", None, 0.02),
+    ("caron_below_ls", "a\u032C\u032C\u032C\u032C\u032Cb", "ls", None, 0.02),
+    ("caron_below_ttb", "a" + ("\u032C" * 15) + "b", None, "ttb", 0.03),
+    ("caron_below_ttb_lb", "a" + ("\u032C" * 15) + "b", "lb", "ttb", 0.03),
+    # extends to the right (e.g. issue #3745)
+    ("double_breve_below", "a\u035Ci", None, None, 0.02),
+    ("double_breve_below_ma", "a\u035Ci", "ma", None, 0.02),
+    ("double_breve_below_ra", "a\u035Ci", "ra", None, 0.02),
+    ("double_breve_below_ttb", "a\u035Cb", None, "ttb", 0.02),
+    ("double_breve_below_ttb_rt", "a\u035Cb", "rt", "ttb", 0.02),
+    ("double_breve_below_ttb_mt", "a\u035Cb", "mt", "ttb", 0.02),
+    ("double_breve_below_ttb_st", "a\u035Cb", "st", "ttb", 0.02),
+    # extends to the left (fail=0.064)
+    ("overline", "i\u0305", None, None, 0.02),
+    ("overline_la", "i\u0305", "la", None, 0.02),
+    ("overline_ra", "i\u0305", "ra", None, 0.02),
+    ("overline_ttb", "i\u0305", None, "ttb", 0.02),
+    ("overline_ttb_rt", "i\u0305", "rt", "ttb", 0.02),
+    ("overline_ttb_mt", "i\u0305", "mt", "ttb", 0.02),
+    ("overline_ttb_st", "i\u0305", "st", "ttb", 0.02),
+)
+
+
+# this tests various combining characters for anchor alignment and clipping
+@pytest.mark.parametrize(
+    "name,text,anchor,dir,epsilon", combine_tests, ids=[r[0] for r in combine_tests]
+)
+def test_combine(name, text, dir, anchor, epsilon):
+    if (
+        parse_version(features.version_module("freetype2")) < parse_version("2.5.1")
+        and dir == "ttb"
+    ):
+        # FreeType 2.5.1 README: Miscellaneous Changes:
+        # Improved computation of emulated vertical metrics for TrueType fonts.
+        pytest.skip("FreeType <2.5.1 has incompatible ttb metrics")
+
+    path = f"Tests/images/test_combine_{name}.png"
+    f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
+
+    im = Image.new("RGB", (400, 400), "white")
+    d = ImageDraw.Draw(im)
+    d.line(((0, 200), (400, 200)), "gray")
+    d.line(((200, 0), (200, 400)), "gray")
+    try:
+        d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f)
+    except ValueError as ex:
+        if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
+            pytest.skip("libraqm 0.7 or greater not available")
+
+    with Image.open(path) as expected:
+        assert_image_similar(im, expected, epsilon)
+
+
+def test_anchor_invalid_ttb():
+    font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
+    im = Image.new("RGB", (100, 100), "white")
+    d = ImageDraw.Draw(im)
+    d.font = font
+
+    for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]:
+        pytest.raises(
+            ValueError, lambda: font.getmask2("hello", anchor=anchor, direction="ttb")
+        )
+        pytest.raises(
+            ValueError, lambda: d.text((0, 0), "hello", anchor=anchor, direction="ttb")
+        )
+        pytest.raises(
+            ValueError,
+            lambda: d.multiline_text(
+                (0, 0), "foo\nbar", anchor=anchor, direction="ttb"
+            ),
+        )
+    # ttb multiline text does not support anchors at all
+    pytest.raises(
+        ValueError,
+        lambda: d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb"),
+    )