How to Load a Custom font Using Pillow in Python

Have you ever needed to generate thousands of images with text on them? Maybe you've tried to launch a new meme-generating app only to realize JavaScript doesn't handle imaging the way you'd like. Python's Pillow library may just be the imaging workhorse you have been looking for!
pillow custom font python overcoded

The Pillow library is a popular imaging library available for Python. Among its many utilities is the ability to use custom fonts for text manipulation. In this article, we’re going to take a look at just how easy using a custom font with Pillow can be.

Pillow’s text manipulation toolsets can be used for general text manipulation or analytical purposes like measuring text size and attributes. Pillow’s text utilities cater for more visual use cases and shouldn’t be regarded as a text processing or natural language processing toolset.

TL;DR – Using Custom Fonts in Pillow

pillow image text custom font overcoded
Image generated using the following TL;DR Pillow ImageFont and ImageDraw code.

This article will walk you through the basics of loading a custom font in the Pillow imaging library for Python. For those that just want a copy/paste solution check out the code below.

# Load a OpenFont format font from a local directory as an ImageFont object
# In this case, a TrueType font using the truetype method.
font = ImageFont.truetype(font='path/to/your/font.ttf', size=42)

# Create a new image onto which the text will be added
image = Image.new(mode='RGB', size=(1024, 256), color='#ffaa00')

# Create an ImageDraw object onto which the font text will be placed
draw = ImageDraw.Draw(im=image)

# Draw the text onto our image
draw.text(xy=(512, 128), text="overcoded - @}", font=font, fill='black', anchor='mm')

# Open the image via system standard image viewer
image.show()

This code isn’t very flexible but demonstrates the basic steps involved in using a custom font with the Pillow imaging library:

  1. Load a FreeType compatible font (.ttf, .otf, etc.)
  2. Create a Pillow Image object to use as a background for the text
  3. Create a Pillow ImageDraw object to access the text drawing features of Pillow
  4. Add the custom text to the ImageDraw object
  5. Use the Image.show() method to view results in system default image viewer

There are plenty of options that can be specified during these steps and several notable gotchas that might pop up. The remainder of this article will attempt to cover the basics as well as advise on how to handle some common issues that are known to pop up.

Project Structure + Setup

For the examples in this article, I’ll be using the Source Code Pro font available from Google Fonts. I have downloaded it and saved it to a local fonts subdirectory in the project folder I created for these examples. Below is the structure of the project.

.
└── ProjectRoot
    ├── fonts
    │   └── SourceCodePro-Bold.ttf
    └── pillow_custom_font_example.py

Pillow is an actively maintained fork of the now outdated Python Imaging Library (PIL). Pillow can be installed using pip as such: pip install Pillow. Refer to the official Pillow installation docs for more guidance on various system installs, upgrading pip, and etc.

Note: Pillow and PIL cannot coexist within the same Python installation. You must uninstall PIL if you have it installed before Pillow will be functional.

The ImageFont Module

Among Pillow’s 30ish modules, the ImageFont module allows easy specification of custom fonts. As per the documentation, this module provides utility for bitmap fonts and fonts within the FreeType family (OpenType, TrueType, etc.).

The ImageFont module provides custom font support to be used along with the ImageDraw module’s text method. We’ll get to the ImageDraw.text() method shortly but for now let’s start by using the ImageFont module’s ImageFont class to specify our custom Source Code Pro font.

Supported Font Types

Fonts come in a variety of types including bitmap, raster, vector, TrueType, OpenType, and several others. Bitmap fonts are mostly outdated but can still be found out in the wild roaming around sometimes.

Pillow has supported bitmap fonts since its inception and modern releases support OpenType and TrueType with native ImageFont.truetype() function. The ImageFont.load_path() and ImageFont.load() functions will only work with bitmap fonts containing related glyph data and will generate errors for other formats.

from PIL import ImageFont
import os

# Define the path to our custom .ttf font
free_type_compatible_font = os.path.abspath(
    os.path.join(
        os.path.dirname(__file__), 'fonts/SourceCodePro-Black.ttf'
    )
)

# Open as TrueType native
font = ImageFont.truetype(font=free_type_compatible_font, size=42)

>>> <PIL.ImageFont.FreeTypeFont object at 0x0000021736AE94C0>

# Define the path to our custom .otf font
free_type_compatible_font = os.path.abspath(
    os.path.join(
        os.path.dirname(__file__), 'fonts/Montserrat-Regular.otf'
    )
)

# Open as OpenType font
font = ImageFont.truetype(font=free_type_compatible_font, size=42)

>>> <PIL.ImageFont.FreeTypeFont object at 0x00000249E03C94C0>

As you can see, both the .otf and .ttf fonts load successfully into memory via the ImageFont. Let’s see what happens when we try to load one of these fonts with the .load() or load_path() methods:

# Load using the load method
font = ImageFont.load(free_type_compatible_font)

>>> OSError: cannot find glyph data file

# Load using the load_path method
font = ImageFont.load_path(free_type_compatible_font)

>>> OSError: cannot find font file

While the last error might seem a bit odd, the Pillow documentation lends the following explanation:

Same as ~PIL.ImageFont.load, but searches for a bitmap font along the Python path. Raises OSError if the file could not be read.

I got a little tripped up the first time I got the “cannot find font file” error when trying to [incorrectly] load a .ttf file with the load_path() method. If you see this error just know that it can result from either a missing file or a file not being read properly.

Image & ImageDraw Objects

Much of the utility provided by the Pillow library revolves around the use of the Image class. This class is a rollover from the original PIL library and one of the primary reasons one can’t use the Pillow library if PIL is installed in the same environment. From the documentation:

The Image module provides a class with the same name which is used to represent a PIL image. The module also provides a number of factory functions, including functions to load images from files, and to create new images.

The Image module, being so fundamental to the utilities of Pillow, has a LOT of functionality. We aren’t going to cover much of that here—just know that you can do a lot other than simply load, save, and show images (our use case.) Let’s create an image object to use as a background to display our text:

# Create an 1024px x 256px image in solid yellow RGB mode
image = Image.new(mode='RGB', size=(1024, 256), color='#ffaa00')

Our next step is to create an ImageDraw object that will allow us to use our custom font to create some text. I’ll go ahead and load our font again since it’s been a minute since the top of this article.

# Load the font from local storage
font = ImageFont.truetype(font='fonts/SourceCodePro-Regular.ttf', size=42)

# Create an ImageDraw object onto which the font text will be placed
draw = ImageDraw.Draw(im=image)

# Draw the text onto our image
draw.text(xy=(512, 128), text="feeling overcoded? - @}", font=font, fill='black', anchor='mm')

# Display the image using system standard photo viewer (it's a .png file)
image.show()

I’ve kind of hit the gas peddle here and done several things. Let’s break this one down a bit.

  1. Font – the font we loaded from local storage
  2. The ImageDraw object, bound to our Image background
  3. The ImageDraw.text function sets the size, text, font, and color of our text
  4. The command that displays the end result

Note: I’m using the anchor=”mm” argument in the draw.text() method. This is a built-in helper method that controls the positioning (anchoring) of text. It’s a super-convenient group of functions that saves a ton of time placing text on images. Check out the full documentation for how to use text anchors with Pillow. For now, just know that “mm” means center the text in the middle. Here’s what all our hard work has created:

overcoded pillow text on image tutorial initial result
Our text displays in a size 42 font of our choosing “anchored” to the middle of the background.

Keep in mind this is a very basic example of what the Pillow ImageFont class can achieve when coupled with other utilities the Pillow library provides. Pillow can help with text wrapping, creating ASCII text, or just about any other application involving the manipulation of text, images, or both that one can imagine.

Final Thoughts

Pillow is the defacto imaging library for more artistic applications in my opinion. The OpenCV wrapper for Python is certainly an industrial-strength imaging library with a lot to offer. In my experience, OpenCV is more suited towards imaging data applications such as machine learning, statistical analysis, or processing large swaths of visual data. Pillow is just one of the many libraries that makes Python one of the most popular programming languages out there. I find few other languages capable of achieving such a high workload with such minimal syntax.

Zαck West
Full-Stack Software Engineer with 10+ years of experience. Expertise in developing distributed systems, implementing object-oriented models with a focus on semantic clarity, driving development with TDD, enhancing interfaces through thoughtful visual design, and developing deep learning agents.