Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PermissionError when attempting to use importlib.resources on Windows to load a custom font with ImageFont #6324

Closed
jack-jjm opened this issue May 23, 2022 · 10 comments · Fixed by #6485

Comments

@jack-jjm
Copy link

jack-jjm commented May 23, 2022

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. 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 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 aPermissionError 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
@radarhere
Copy link
Member

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?

import importlib.resources

if __name__ == "__main__":
    with importlib.resources.path("font_demo", "OpenSans-Regular.ttf") as path:
        pass

@jack-jjm
Copy link
Author

No, that works fine. However, this does cause an error:

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.

@nulano
Copy link
Contributor

nulano commented May 23, 2022

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:

with importlib.resources.path("font_demo", "OpenSans-Regular.ttf") as path:
    with open(path, "rb") as f:
        font = ImageFont.truetype(f, 30)

@radarhere
Copy link
Member

radarhere commented May 23, 2022

Thanks @nulano. This is not the first problem we've had with FreeType keeping the font open - #3730 (comment)

@jack-jjm
Copy link
Author

jack-jjm commented May 23, 2022

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:

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.

@radarhere
Copy link
Member

@geajack are you happy for this issue to be closed?

@jack-jjm
Copy link
Author

jack-jjm commented May 23, 2022

If it's an upstream issue that you don't intend to try and workaround in Pillow then that seems fine.

@nulano
Copy link
Contributor

nulano commented May 23, 2022

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.

@jack-jjm
Copy link
Author

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.

@radarhere
Copy link
Member

I've created PR #6485 to add a note to the documentation, resolving this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants