2025, Oct 06 05:00

Fix Arcade text rendering: load custom TTFs in pyglet by using the font internal name

Learn how to load a custom TTF in Arcade/pyglet without fallback fonts: get the internal font name via FreeTypeFace, use it in font_name, and avoid OS quirks.

Loading a custom .ttf into an Arcade project can be deceptive: the font file loads without errors, yet the text renders with a fallback like Segoe UI. The typical suspects—file name, display name from a font viewer, or metadata strings—don’t necessarily match the identifier Arcade/pyglet actually use at draw time. The key is the internal font name embedded in the font itself.

Problem setup

You load a font file and pass what looks like a valid name when constructing an Arcade text object, but it still falls back:

arcade.load_font("Assets/Fonts/CopperplateGothic.ttf")

self.label_actions = arcade.Text(
    "Actions",
    font_name="CopperplateGothicStd-32BC",
    color=accent_color,
    font_size=32,
    x=0,
    y=btn_y + btn_h + 20,
    batch=self.title_batch,
)

print(self.label_actions.font_name)

Trying variants like the Windows font viewer label (for example, “Copperplate Std 32 BC”), the file property value (“CopperplateGothicStd-32BC”), or the file name with/without extension doesn’t help. Meanwhile, selecting a known system font such as “MS Comic Sans” works as expected, which makes it clear the font search itself is functional.

What’s actually going on

Arcade uses pyglet under the hood for font management. The string you pass in font_name is matched against the font’s internal name as parsed by pyglet, not arbitrary display names or file names. That internal name is accessible via pyglet’s FreeType-based loader and can differ from what UI tools show. If you pass any other string, pyglet can’t resolve the face and silently falls back.

You can inspect the internal name directly with pyglet’s FreeTypeFace:

import pyglet.font.freetype

font_path = "Copperplate-Gothic-Std-32-BC.ttf"
face = pyglet.font.freetype.FreeTypeFace.from_file(font_path)
print(face.name)

For the tested file, this prints “Copperplate Gothic Std”. That is the value that pyglet resolves when selecting the font, and the same mechanism is used by arcade.load_font (which internally relies on pyglet.font.add_file).

You can verify the name is usable from pyglet directly:

import pyglet

font_path = "Copperplate-Gothic-Std-32-BC.ttf"
pyglet.font.add_file(font_path)

family_name = "Copperplate Gothic Std"
resolved = pyglet.font.load(family_name)
print(resolved.name)

Fixing the Arcade usage

Once you have the internal font family, use it as the font_name when creating your Arcade text. Here is a minimal example that loads the file and renders with that exact name:

import arcade

WIN_W = 400
WIN_H = 300
APP_TITLE = "Example"

font_path = "Copperplate-Gothic-Std-32-BC.ttf"
family_name = "Copperplate Gothic Std"

class Demo(arcade.Window):
    def __init__(self):
        super().__init__(WIN_W, WIN_H, APP_TITLE)
        arcade.load_font(font_path)
        self.lbl = arcade.Text(
            "Actions",
            font_name=family_name,
            font_size=32,
            x=WIN_W // 2,
            y=WIN_H // 2,
            anchor_x="center",
            anchor_y="center",
        )
        print(self.lbl.font_name)

    def on_draw(self):
        self.clear()
        self.lbl.draw()

if __name__ == "__main__":
    Demo()
    arcade.run()

Platform notes

The loading path in pyglet can differ by OS. In particular, Windows may use Win32DirectWriteFont or GDIPlusFont, while the test above used Linux. In one case, the same font and code rendered correctly on Linux but failed to display on Windows, and simply switching to another font resolved the issue.

Why this matters

Text rendering is core UI. Using the wrong identifier leaves you with unpredictable fallbacks, inconsistent styling, and hard-to-reproduce bugs. Grabbing the internal font name ensures the renderer resolves the intended face and gives you deterministic results across runs. When troubleshooting across operating systems, knowing the exact selection mechanism shortens the feedback loop.

Takeaways

Load the .ttf via Arcade, but resolve the font by its internal name as reported by pyglet’s FreeTypeFace and pass that value into font_name. If the font still doesn’t render as expected on a specific platform, consider that the OS font backend may behave differently and test with an alternative font file.

The article is based on a question from StackOverflow by JediKnightBoB and an answer by furas.