0

CONTEXT

I have a working python tkinter code in which I use a font I have previously downloaded to my computer (it is 'Trajan Pro Regular.ttf'). I want the font to be seen by anyone running the .exe program on any computer.

I have tried using pyglet, but it didn't work. I've looked at other answers but they didn't work for me either. Using pyglet, I got this error :

Traceback (most recent call last):
  File "ModifierTool.py", line 66, in <module>
  File "pyglet\font\__init__.py", line 156, in add_file
FileNotFoundError: [Errno 2] No such file or directory: 'Trajan Pro Regular.ttf'

It was fixed by placing the .ttf file in the same folder as the .exe app. This is of course not what I want, I want the font to be packed within the .exe. Even when placing the .ttf file inside the same folder as the .exe, despite clearing the error, the font is still not displayed on computer who don't have it downloaded.

I'm using auto-py-to-exe for packing.

Is there not an easy (or not) way to load embedded font files similarly to images or other files, using ressource_path ?

Sorry if I don't provide much information, this is kind of a general issue and I don't have much to add.

I tried using pyglet but it failed.

I previously tried this way (resource_path is also used to load .png and .pak files anyway). Here is a simplified snippet of code :

    import tkinter as tk
    import tkinter.font as tkfont

    def resource_path(relative_path):
            try:
                base_path = sys._MEIPASS
            except Exception:
                base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

    font_file = resource_path('Trajan Pro Regular.ttf')
    trajan_pro = tkfont.Font(family='Trajan Pro', file=font_file, size=10, weight='bold')

    def create_widgets(self):

    button_text = tk.Label(frame, text=option, font=trajan_pro, bg='#222222', fg='white')``

That didn't work either.

I also tried using tkextrafont but I couldn't get it to work, the font didn't even load when testing. See :

import tkinter as tk
import pathlib
import tkextrafont
from tkextrafont import Font
    
    def create_widgets(self):

        fontpath = pathlib.Path(__file__).parent/"Trajan Pro Regular.ttf"
        font = tkextrafont.Font(file=str(fontpath), family= "Trajan Pro Regular", size= 12, weight= 'bold')
...
    all_button_label = tk.Label(frame, text='All', font=font, bg='#222222', fg='white')
...
if __name__ == '__main__':
    root = tk.Tk()

EDIT 1

I now tried using the AddFontResourceEx function as per @relent95 's recommandation. I actually had a look at that prior to writing this post but scrapped the idea because I couldn't understand how to use it.

With this code (simplified):

import tkinter as tk
import tkinter.font as tkfont
import os

FR_PRIVATE  = 0x10
FR_NOT_ENUM = 0x20

def loadfont(fontpath, private=True, enumerable=False):
    if isinstance(fontpath, bytes):
        pathbuf = create_string_buffer(fontpath)
        AddFontResourceEx = windll.gdi32.AddFontResourceExA
    elif isinstance(fontpath, str):
        pathbuf = create_unicode_buffer(fontpath)
        AddFontResourceEx = windll.gdi32.AddFontResourceExW
    else:
        raise TypeError('fontpath must be of type str or unicode')
    
    flags = (FR_PRIVATE if private else 0) | (FR_NOT_ENUM if not enumerable else 0)
    numFontsAdded = AddFontResourceEx(byref(pathbuf), flags, 0)
    return bool(numFontsAdded)

class App(tk.Frame):
...
def create_widgets(self):

font_path = ("Trajan Pro Regular.ttf")
loadfont(font_path)

And then simply mentionning the font like this :

all_button_label = tk.Label(frame, text='All', font=tkfont.Font(family='Trajan Pro', size=12, weight='bold'), bg='#222222', fg='white')

I have managed to load the font correctly without having it downloaded onto my computer (when running the .py AND when running the .exe). Problem is, the file still needs to be in the same folder as the program to be loaded, which is something I don't want. I still don't know how to include the font within the executable.

I also tried replacing font_path = ("Trajan Pro Regular.ttf") with font_path = os.path.abspath("Trajan Pro Regular.ttf") and got the same result.

I'm using auto-py-to-exe to pack my python code and I do add --add-data "D:/Desktop/Modifier Tool/Trajan Pro Regular.ttf;." in the command. I tried just running the PyInstaller command in python but I got the same result as when I used auto-py-to-exe.

Reproducing the problem (AddFontResourceEx function)

Here is a simple code that highlights the problem. The font won't load unless it is in the same directory (or already downloaded), even after using --add-data with PyInstaller.

import tkinter as tk
import tkinter.font as tkfont
from ctypes import windll, byref, create_unicode_buffer, create_string_buffer

FR_PRIVATE  = 0x10
FR_NOT_ENUM = 0x20

def loadfont(fontpath, private=True, enumerable=False):
    if isinstance(fontpath, bytes):
        pathbuf = create_string_buffer(fontpath)
        AddFontResourceEx = windll.gdi32.AddFontResourceExA
    elif isinstance(fontpath, str):
        pathbuf = create_unicode_buffer(fontpath)
        AddFontResourceEx = windll.gdi32.AddFontResourceExW
    else:
        raise TypeError('fontpath must be of type str or unicode')
    
    flags = (FR_PRIVATE if private else 0) | (FR_NOT_ENUM if not enumerable else 0)
    numFontsAdded = AddFontResourceEx(byref(pathbuf), flags, 0)
    return bool(numFontsAdded)

class App(tk.Frame):
    def __init__(self, master=None, **kwargs):
        super().__init__(master, **kwargs)
        self.master = master
        self.create_widgets()

    def create_widgets(self):

        font_path = ("MyFont.ttf")
        loadfont(font_path)

        label = tk.Label(text='Hello', font=tkfont.Font(family='My Font'))
        label.pack()

root = tk.Tk()
app = App(root)
root.mainloop()

I hope it is clear.

EDIT 2

Reproducing the problem (pyglet)

With the following code, I get an error unless the .ttf file is in the same folder as the executable. The font does load correctly if it is placed in the same folder as the .exe app, unlike before.

Simplified code:

import tkinter as tk
import tkinter.font as tkfont
import pyglet

pyglet.options['win32_gdi_font'] = True

pyglet.font.add_file('MyFont.ttf')

class App(tk.Frame):
    def __init__(self, master=None, **kwargs):
        super().__init__(master, **kwargs)
        self.master = master
        self.create_widgets()

    def create_widgets(self):

        label = tk.Label(text='Hello', font=tkfont.Font(family='My Font'))
        label.pack()

if __name__ == '__main__':
    root = tk.Tk()
    app = App(root)
    root.mainloop()

Error displayed when running the program without the .ttf file in the same directory:

Traceback (most recent call last):
  File "Test.py", line 7, in <module>
  File "pyglet\font\__init__.py", line 156, in add_file
FileNotFoundError: [Errno 2] No such file or directory: 'MyFont.ttf'

I'm really hoping to fix that issue that has been bugging me for the past few days.

Basile
  • 13
  • 3
  • Does this answer your question? [Truly custom font in Tkinter](https://stackoverflow.com/questions/11993290/truly-custom-font-in-tkinter) – relent95 May 11 '23 at 01:09
  • You need to focus on a specific problem. Choose one method for using TTF in Tkinter and make your code work when not packed by PyInstaller. You seem to have succeeded that using Pyglet. Then, the problem is for using the PyInstaller, and you need to edit the question for the [mre] guide. – relent95 May 11 '23 at 01:15
  • You need to add the font file when using `auto-py-to-exe`. The command line for `PyInstaller` is `pyinstaller -F --add-data="Trajan Pro Regular.ttf;." ModifierTool.py`. – acw1668 May 11 '23 at 01:49
  • Thank you for your answers. I've updated my post to include something else I tried: the AddFontResourceEx as suggested by @relent95. I tried treating 1 problem after the other. Now I am stuck to the packing of the executable. The font won't load unless the .ttf file is present alongside the .exe in the same folder. I added a test code that allowed me to reproduce the problem. I hope I was clear. – Basile May 11 '23 at 14:37
  • None of your example codes uses `pyglet`, but the error traceback shows it is used. So better provide the code that raises the exception. – acw1668 May 11 '23 at 14:41
  • I couldn't get pyglet to work when I tried, so I moved on to other solutions. I mentioned it in my post to avoid anyone suggesting pyglet. To clarify, using pyglet, I had multiple problems (the error I provided, then after fixing the error, the font still not loading). Using the function you suggested, I'm down to only 1 problem, as I said. Sorry if I communicate in a wrong way, I've just started coding last week and signed in to Stack Overflow a few days ago. – Basile May 11 '23 at 15:05
  • That means you have already solved the error (caused by using `pyglet`) posted. Note that `pyget` starts from 2.0 has issue on Windows, see [my answer to this question](https://stackoverflow.com/a/76001345/5317403). – acw1668 May 11 '23 at 15:11
  • Yes, but I explained it in my post. I solved the issue by placing the ttf file in the same folder as the program. Then I was able to run the program but the font was still default. Using AddFontResourceEx suggested by @relent95 (not by you, my mistake), I was able to make the font load when it was placed in the same folder as the program. Now, the issue is that it won't load even after packing the font within the .exe with PyInstaller. I don't want to distribute my program with a font file alongside it, I want the file to be inside the app. Sorry if I'm not clear again. – Basile May 11 '23 at 15:17
  • I don't have the issue when running the executable (using `pyglet`) generated by `PyInstaller` without the TTF font alongside the executable. – acw1668 May 11 '23 at 15:41
  • Okay, I've tried using pyglet again and adding this line as you mentionned in your other answer: pyglet.options['win32_gdi_font'] = True I still get the error I mentionned when the ttf file isn't in the same folder. However, now the font does load when it is in the same folder. I'm basically faced with the same issue as with the other function. I'm guessing this is because of wrong packing. I will update my post with a code to reproduce my problem. – Basile May 11 '23 at 15:56

1 Answers1

2

Your code using pyglet does not use the correct path of the font when the script is converted to executable.

Below is the modified code which uses the correct path of the font:

from pathlib import Path
import tkinter as tk
import tkinter.font as tkfont
import pyglet

pyglet.options['win32_gdi_font'] = True
fontpath = Path(__file__).parent / 'Trajan Pro.ttf'
pyglet.font.add_file(str(fontpath))

class App(tk.Frame):
    def __init__(self, master=None, **kwargs):
        super().__init__(master, **kwargs)
        self.master = master
        self.create_widgets()

    def create_widgets(self):

        label = tk.Label(text='Hello', font=tkfont.Font(family='Trajan Pro', size=64))
        label.pack()

if __name__ == '__main__':
    root = tk.Tk()
    app = App(root)
    root.mainloop()

The result of the executable generated by PyInstaller without the font file alongside with the executable:

enter image description here

acw1668
  • 40,144
  • 5
  • 22
  • 34
  • Thank you for your help and your patience. It did fix the issue I was having. Appreciate it. – Basile May 11 '23 at 18:32