Repository: python-pillow/Pillow

Issue:
ImageFont.truetype() function does not accept `Path` objects
<!--
Thank you for reporting an issue.

Follow these guidelines to ensure your issue is handled properly.

If you have a ...

1.  General question: consider asking the question on Stack Overflow
    with the python-imaging-library tag:

    * https://stackoverflow.com/questions/tagged/python-imaging-library

    Do not ask a question in both places.

    If you think you have found a bug or have an unexplained exception
    then file a bug report here.

2.  Bug report: include a self-contained, copy-pastable example that
    generates the issue if possible. Be concise with code posted.
    Guidelines on how to provide a good bug report:

    * https://stackoverflow.com/help/mcve

    Bug reports which follow these guidelines are easier to diagnose,
    and are often handled much more quickly.

3.  Feature request: do a quick search of existing issues
    to make sure this has not been asked before.

We know asking good questions takes effort, and we appreciate your time.
Thank you.
-->

### What did you do?

Pass `Path` object instead of `str` to `ImageFont.truetype()`. IMO this should work if I understand the docs correctly.

```python
from PIL import ImageFont
from pathlib import Path

font_path = Path("path/to/your/font")
font = ImageFont.truetype(font_path, 12)
```

### What did you expect to happen?

Get back a font object.

### What actually happened?

I get a type error saying that I cannot pass a `Path` object.

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/mauro/.pyenv/versions/test_env/lib/python3.10/site-packages/PIL/ImageFont.py", line 797, in truetype
    return freetype(font)
  File "/home/mauro/.pyenv/versions/test_env/lib/python3.10/site-packages/PIL/ImageFont.py", line 794, in freetype
    return FreeTypeFont(font, size, index, encoding, layout_engine)
  File "/home/mauro/.pyenv/versions/test_env/lib/python3.10/site-packages/PIL/ImageFont.py", line 226, in __init__
    self.font = core.getfont(
TypeError: argument 1 must be str, bytes or bytearray, not PosixPath
```

Funnily enough, the `__init__` function of `FreeTypeFont` checks if the passed `font` argument is of type `bytes`, `str` or `Path`: https://github.com/python-pillow/Pillow/blob/f9c7bd86115090ce2b110794485f138f0d5d5953/src/PIL/ImageFont.py#L215
The problem occurs later when the `core` object should load the font which presumably only works if `font` is a string:
https://github.com/python-pillow/Pillow/blob/f9c7bd86115090ce2b110794485f138f0d5d5953/src/PIL/ImageFont.py#L226-L228

This makes me think, that the `ImageFont.truetype()` function should indeed accept `Path` objects and internally convert the argument into a string before fowarding it to `core.getfont()`. 

### What are your OS, Python and Pillow versions?

* OS: Ubuntu 22.04.3 LTS
* Python: 3.10.7
* Pillow: 10.1.0

<!--
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.

The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as Plone, Django, or Buildout, try to replicate the issue just using Pillow.
-->



Discussion:

Resolving patch:
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -25,12 +25,16 @@
 # See the README file for information on usage and redistribution.
 #
 
+from __future__ import annotations
+
 import base64
 import os
 import sys
 import warnings
 from enum import IntEnum
 from io import BytesIO
+from pathlib import Path
+from typing import IO
 
 from . import Image
 from ._util import is_directory, is_path
@@ -185,7 +189,14 @@ def getlength(self, text, *args, **kwargs):
 class FreeTypeFont:
     """FreeType font wrapper (requires _imagingft service)"""
 
-    def __init__(self, font=None, size=10, index=0, encoding="", layout_engine=None):
+    def __init__(
+        self,
+        font: bytes | str | Path | IO | None = None,
+        size: float = 10,
+        index: int = 0,
+        encoding: str = "",
+        layout_engine: Layout | None = None,
+    ) -> None:
         # FIXME: use service provider instead
 
         if size <= 0:
@@ -217,6 +228,8 @@ def load_from_bytes(f):
             )
 
         if is_path(font):
+            if isinstance(font, Path):
+                font = str(font)
             if sys.platform == "win32":
                 font_bytes_path = font if isinstance(font, bytes) else font.encode()
                 try:
@@ -779,7 +792,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
                      This specifies the character set to use. It does not alter the
                      encoding of any text provided in subsequent operations.
     :param layout_engine: Which layout engine to use, if available:
-                     :data:`.ImageFont.Layout.BASIC` or :data:`.ImageFont.Layout.RAQM`.
+                     :attr:`.ImageFont.Layout.BASIC` or :attr:`.ImageFont.Layout.RAQM`.
                      If it is available, Raqm layout will be used by default.
                      Otherwise, basic layout will be used.
 


Test patch:
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -4,6 +4,7 @@
 import shutil
 import sys
 from io import BytesIO
+from pathlib import Path
 
 import pytest
 from packaging.version import parse as parse_version
@@ -76,8 +77,9 @@ def _render(font, layout_engine):
     return img
 
 
-def test_font_with_name(layout_engine):
-    _render(FONT_PATH, layout_engine)
+@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH)))
+def test_font_with_name(layout_engine, font):
+    _render(font, layout_engine)
 
 
 def test_font_with_filelike(layout_engine):