Repository: python-pillow/Pillow

Issue:
PermissionError when attempting to use importlib.resources on Windows to load a custom font with ImageFont
<!--
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.
-->

### Behavior

My code calls `ImageFont.truetype` to load a font. Because I want to make sure that font is present on the system, I commited it to my repo. When I want to make my package pip installable, I decided to package my font file using [`importlib.resources`](https://docs.python.org/3/library/importlib.html#module-importlib.resources). But now, when I pip install the package on Windows and try and run the code, I get a `PermissionError` when `importlib` attempts to clean up the temporary file - probably because Windows thinks a process is still using it:

```
Traceback (most recent call last):
  File "c:\users\me\appdata\local\programs\python\python39\lib\runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "c:\users\me\appdata\local\programs\python\python39\lib\runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "C:\Users\me\Code\PIL Resource Bug\pyenv\lib\site-packages\font_demo-0.0.0-py3.9.egg\font_demo\__main__.py", line 9, in <module>
  File "c:\users\me\appdata\local\programs\python\python39\lib\contextlib.py", line 124, in __exit__
    next(self.gen)
eader
  File "c:\users\me\appdata\local\programs\python\python39\lib\contextlib.py", line 124, in __exit__
    next(self.gen)
  File "c:\users\me\appdata\local\programs\python\python39\lib\importlib\_common.py", line 40, in _tempfile      
    os.remove(raw_path)
PermissionError: [WinError 5] Access is denied: 'C:\\Users\\me\\AppData\\Local\\Temp\\tmpd01q49pxOpenSans-Regular.ttf'
```

I don't get this with other kinds of resource files, so it seems Pillow is not releasing the file correctly. I have not tested this on other operating systems, but I suspect this is a Windows-only bug.

### Reproduction

1. Clone the [repo](https://github.com/geajack/pillow-resource-bug) I made to illustrate this.
2. Navigate to the root of the repo and optionally create an enter a virtual environment.
3. Install Pillow (`pip install pillow`).
4. Try running the code directly with `python -m font_demo`. You should see an image that says "Hello, world!".
5. Now run `python -m setup.py install` to install the package.
6. Enter a different directory and try running `python -m font_demo` again.
7. You get a`PermissionError` and the program halts without displaying the image.

### What are your OS, Python and Pillow versions?

* OS: Windows 10 (64 bit)
* Python: 3.9
* Pillow: 9.1.1

<!--
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:
Hi. If you'll permit me to ask a quick question, just to rule out the easy solution, does the problem persist if you don't use Pillow?

```python
import importlib.resources

if __name__ == "__main__":
    with importlib.resources.path("font_demo", "OpenSans-Regular.ttf") as path:
        pass
```
No, that works fine. However, this does cause an error:

```python
with importlib.resources.path("font_demo", "OpenSans-Regular.ttf") as path:
        font = ImageFont.truetype(str(path), 30)
```

So it seems my minimal example is not so minimal - you don't even need to use the font.
When using a truetype font, the font file itself is opened using the FreeType library. Looking at the code, it should be properly closed when the font object is deallocated, so adding a `del font` into the `with` block should solve this on CPython (but likely not PyPy which does not use reference counting).

If you need to remove the file before the font is deallocated, you should be able to load the font using a file-like object, perhaps like this:

```python
with importlib.resources.path("font_demo", "OpenSans-Regular.ttf") as path:
    with open(path, "rb") as f:
        font = ImageFont.truetype(f, 30)
```
Thanks @nulano. This is not the first problem we've had with FreeType keeping the font open - https://github.com/python-pillow/Pillow/issues/3730#issuecomment-480491568
Yeah, managing the file opening/closing myself seems like the best option. Thanks.

By the way, it's probably obvious to any PIL contributor, but this is the absolute simplest example I can get to reproduce the bug:

```python
import importlib.resources
from PIL import ImageFont
from PIL import _imagingft as core


if __name__ == "__main__":
    with importlib.resources.path("font_demo", "OpenSans-Regular.ttf") as path:
        font = core.getfont(
            str(path),
            30,
            0,
            "",
            layout_engine=ImageFont.Layout.BASIC
        )
        
```

After that, it seems like you're in native code.
@geajack are you happy for this issue to be closed?
If it's an upstream issue that you don't intend to try and workaround in Pillow then that seems fine.
The issue is that your code keeps a reference to the font (keeping the file open via FreeType) while trying to delete the file.

It is unclear whether FreeType keeping the file open while in use is an (upstream) issue or not. It seems reasonable to me to keep the file open while the font may still be used.

From the comment linked by @radarhere:

> From my understanding, it is FreeType that is actually keeping the file pointer open [...]. We call FT_Done_Face when we are done with the font, but that's not applicable here, since the fonts [...] might still be used.

> So I don't see any reasonable action to take to improve this. We could stop using FreeType, but that's extreme. We could load the [file] into memory, like you're doing with BytesIO, but there's no advantage over that being run externally by you. Feel free to add any more information or disagree with my thoughts.
I see. Maybe this is just a documentation issue? It's pretty non-obvious behavior after all - how often do you usually have to think about exactly when the garbage collector runs while programming in Python? Perhaps a note in the docs that the file will remain locked as long as the font object exists, and so if you need to do anything else with the file while the Python process is still running you should use the file-like object overload.

Resolving patch:
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -906,10 +906,12 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
     This function loads a font object from the given file or file-like
     object, and creates a font object for a font of the given size.
 
-    Pillow uses FreeType to open font files. If you are opening many fonts
-    simultaneously on Windows, be aware that Windows limits the number of files
-    that can be open in C at once to 512. If you approach that limit, an
+    Pillow uses FreeType to open font files. On Windows, be aware that FreeType
+    will keep the file open as long as the FreeTypeFont object exists. Windows
+    limits the number of files that can be open in C at once to 512, so if many
+    fonts are opened simultaneously and that limit is approached, an
     ``OSError`` may be thrown, reporting that FreeType "cannot open resource".
+    A workaround would be to copy the file(s) into memory, and open that instead.
 
     This function requires the _imagingft service.
 


Test patch: