The Pillow package lets you draw text on images using Python. This includes using TrueType and OpenType fonts. You have a lot of flexibility when adding this text to your images. If you’d like to know more, you should check out Drawing Text on Images with Pillow and Python.
Note: The code and fonts used in this tutorial can be found in the Github repo for my book, Pillow: Image Processing with Python (chapter 9)
Of course, it’s nicer if you can actually see what your result will look like in real-time. You can accomplish that by writing a graphical user interface (GUI) that allows you to apply different fonts and tweak their settings. The GUI that you create will allow your users to modify the following properties of their text:
- Font type
- Font color
- Font size
- Text position
When your GUI is finished, it will look like this:
Let’s start writing some code!
Creating the GUI
For this tutorial, you will be using PySimpleGUI. You can use pip to install it. Once you’re done installing PySimpleGUI, you’re ready to go!
Now it’s time for you to code up the GUI. Open up your Python editor and create a new file. Then name it text_gui.py
and enter the following code:
# text_gui.py import glob import os import PySimpleGUI as sg import shutil import tempfile from PIL import Image from PIL import ImageColor from PIL import ImageDraw from PIL import ImageFont from PIL import ImageTk file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")] tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name def get_value(key, values): value = values[key] if value: return int(value) return 0 def apply_text(values, window): image_file = values["filename"] font_name = values["ttf"] font_size = get_value("font_size", values) color = values["colors"] x, y = get_value("text-x", values), get_value("text-y", values) text = values["text"] if image_file and os.path.exists(image_file): shutil.copy(image_file, tmp_file) image = Image.open(tmp_file) image.thumbnail((400, 400)) if text: draw = ImageDraw.Draw(image) if font_name == "Default Font": font = None else: font = ImageFont.truetype(font_name, size=font_size) draw.text((x, y), text=text, font=font, fill=color) image.save(tmp_file) photo_img = ImageTk.PhotoImage(image) window["image"].update(data=photo_img) def create_row(label, key, file_types): return [ sg.Text(label), sg.Input(size=(25, 1), enable_events=True, key=key), sg.FileBrowse(file_types=file_types), ] def get_ttf_files(directory=None): if directory is not None: ttf_files = glob.glob(directory + "/*.ttf") else: ttf_files = glob.glob("*.ttf") if not ttf_files: return {"Default Font": None} ttf_dict = {} for ttf in ttf_files: ttf_dict[os.path.basename(ttf)] = ttf return ttf_dict def save_image(values): save_filename = sg.popup_get_file( "File", file_types=file_types, save_as=True, no_window=True ) if save_filename == values["filename"]: sg.popup_error( "You are not allowed to overwrite the original image!") else: if save_filename: shutil.copy(tmp_file, save_filename) sg.popup(f"Saved: {save_filename}") def update_ttf_values(window): directory = sg.popup_get_folder("Get TTF Directory") if directory is not None: ttf_files = get_ttf_files(directory) new_values = list(ttf_files.keys()) window["ttf"].update(values=new_values, value=new_values[0]) def main(): colors = list(ImageColor.colormap.keys()) ttf_files = get_ttf_files() ttf_filenames = list(ttf_files.keys()) menu_items = [["File", ["Open Font Directory"]]] elements = [ [sg.Menu(menu_items)], [sg.Image(key="image")], create_row("Image File:", "filename", file_types), [sg.Text("Text:"), sg.Input(key="text", enable_events=True)], [ sg.Text("Text Position"), sg.Text("X:"), sg.Input("10", size=(5, 1), enable_events=True, key="text-x"), sg.Text("Y:"), sg.Input("10", size=(5, 1), enable_events=True, key="text-y"), ], [ sg.Combo(colors, default_value=colors[0], key='colors', enable_events=True), sg.Combo(ttf_filenames, default_value=ttf_filenames[0], key='ttf', enable_events=True), sg.Text("Font Size:"), sg.Input("12", size=(5, 1), key="font_size", enable_events=True), ], [sg.Button("Save Image", enable_events=True, key="save")], ] window = sg.Window("Draw Text GUI", elements) while True: event, values = window.read() if event == "Exit" or event == sg.WIN_CLOSED: break if event in ["filename", "colors", "ttf", "font_size", "text-x", "text-y", "text"]: apply_text(values, window) if event == "save" and values["filename"]: save_image(values) if event == "Open Font Directory": update_ttf_values(window) window.close() if __name__ == "__main__": main()
There are over 100 lines of code in this GUI! To make it easier to understand what’s going on, you will go over the code in smaller chunks.
Here are the first few lines of code:
# text_gui.py import glob import os import PySimpleGUI as sg import shutil import tempfile from PIL import Image from PIL import ImageColor from PIL import ImageDraw from PIL import ImageFont from PIL import ImageTk file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")] tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
This is the import section of your code. You import all the necessary modules and packages that you need to make your code work. You also set up a couple of variables there at the end. The file_types
defines what file types your user interface can load. The tmp_file
is a temporary file that you create to save your image changes to until the user is ready to actually save the file where they want it.
The first function in your code is called get_value()
. Here is its code:
def get_value(key, values): value = values[key] if value: return int(value) return 0
This function’s job is to convert strings to integers and to return zero if the user empties a control that requires a value.
Now you can move on to learn about apply_text()
:
def apply_text(values, window): image_file = values["filename"] font_name = values["ttf"] font_size = get_value("font_size", values) color = values["colors"] x, y = get_value("text-x", values), get_value("text-y", values) text = values["text"] if image_file and os.path.exists(image_file): shutil.copy(image_file, tmp_file) image = Image.open(tmp_file) image.thumbnail((400, 400)) if text: draw = ImageDraw.Draw(image) font = ImageFont.truetype(font_name, size=font_size) draw.text((x, y), text=text, font=font, fill=color) image.save(tmp_file) photo_img = ImageTk.PhotoImage(image) window["image"].update(data=photo_img)
Here is where the magic happens. This code runs whenever the user opens a new image, changes the font, changes the font size, change the font color, changes the text itself, or modifies the position of the text. If the user hasn’t entered any text, but they have chosen an image, only the image will change. If the user has entered text, then the other changes will be updated too.
Note: When the image is saved, it saves as the thumbnail version. So you will be saving a smaller version of the image, but with text on it.
The next function you will look at is called create_row()
:
def create_row(label, key, file_types): return [ sg.Text(label), sg.Input(size=(25, 1), enable_events=True, key=key), sg.FileBrowse(file_types=file_types), ]
You have seen this function before. It is used to create three Elements:
- A label (
sg.Text
) - A text box (
sg.Input
) - A file browse button (
sg.FileBrowse
)
These Elements are returned in a Python list. This will create a horizontal row of Elements in your user interface.
The next function to learn about is get_ttf_files()
:
def get_ttf_files(directory=None): if directory is not None: ttf_files = glob.glob(directory + "/*.ttf") else: ttf_files = glob.glob("*.ttf") if not ttf_files: return {"Default Font": None} ttf_dict = {} for ttf in ttf_files: ttf_dict[os.path.basename(ttf)] = ttf return ttf_dict
This code uses Python’s glob
module to find all the TrueType font files in the directory that your GUI file is in or by searching the directory
that you have passed in. If the GUI doesn’t find any TTF files, it will load Pillow’s default font.
No matter what happens, your code will return a Python dictionary that maps the name of the font to the absolute path tot the font file. In the event that your code does not find any TTF files, it will map “Default Font” to None
to indicate to your code that no fonts were found, so you will use Pillow’s default font instead.
The next function defines how to save your edited image:
def save_image(values): save_filename = sg.popup_get_file( "File", file_types=file_types, save_as=True, no_window=True ) if save_filename == values["filename"]: sg.popup_error( "You are not allowed to overwrite the original image!") else: if save_filename: shutil.copy(tmp_file, save_filename) sg.popup(f"Saved: {save_filename}")
This code asks the user what to name their new image using a popup dialog. Then it checks to make sure that the user doesn’t overwrite their original image. In fact, you prevent that from happening here by showing them an error message if they try to do that.
Otherwise, you save the file by copying the temporary file over to the new location that the user chose.
Now you are ready to learn about the update_ttf_values()
function:
def update_ttf_values(window): directory = sg.popup_get_folder("Get TTF Directory") if directory is not None: ttf_files = get_ttf_files(directory) new_values = list(ttf_files.keys()) window["ttf"].update(values=new_values, value=new_values[0])
This function is called from the File menu in your user interface. When it gets called, it will show a dialog asking the user where their TTF files are located. If the user presses Cancel, no directory is returned. But if the user does choose a directory and accepts the dialog, then you call gett_ttf_files()
and it will search for any TTF files that are in the folder.
Then it will update your TTF combobox’s contents so that you can then choose a TTF font to apply to your text.
The last function you’ll need to review is the main()
one:
def main(): colors = list(ImageColor.colormap.keys()) ttf_files = get_ttf_files() ttf_filenames = list(ttf_files.keys()) menu_items = [["File", ["Open Font Directory"]]] elements = [ [sg.Menu(menu_items)], [sg.Image(key="image")], create_row("Image File:", "filename", file_types), [sg.Text("Text:"), sg.Input(key="text", enable_events=True)], [ sg.Text("Text Position"), sg.Text("X:"), sg.Input("10", size=(5, 1), enable_events=True, key="text-x"), sg.Text("Y:"), sg.Input("10", size=(5, 1), enable_events=True, key="text-y"), ], [ sg.Combo(colors, default_value=colors[0], key='colors', enable_events=True), sg.Combo(ttf_filenames, default_value=ttf_filenames[0], key='ttf', enable_events=True), sg.Text("Font Size:"), sg.Input("12", size=(5, 1), key="font_size", enable_events=True), ], [sg.Button("Save Image", enable_events=True, key="save")], ] window = sg.Window("Draw Text GUI", elements)
The first three lines in this function create a Python list of colors
and a list of ttf_filenames
. Next, you create a nested list that represents your menu for your user interface. Then you create an elements
list, which you use to layout all the Elements that are used to create your user interface. Once you have that, you can pass it along to your sg.Window
, which will layout your Elements and show them to the user.
The last chunk of code is your user interface’s event handler:
while True: event, values = window.read() if event == "Exit" or event == sg.WIN_CLOSED: break if event in ["filename", "colors", "ttf", "font_size", "text-x", "text-y", "text"]: apply_text(values, window) if event == "save" and values["filename"]: save_image(values) if event == "Open Font Directory": update_ttf_values(window) window.close() if __name__ == "__main__": main()
Here you check if the user the user has closed the window. If they did, then you exit the loop and end the program.
The next conditional statement checks for all the other events except “save” and the menu events. If any of the other Elements that have events enabled are triggered, then you will run apply_text()
. This updates the image according to the setting(s) that you change.
The next conditional saves the image when you are finished editing it. It will call save_image()
which will ask the user where they want to save the image.
The last conditional is for your File menu event. If the user chooses the “Open Font Directory” option, it will end up calling update_ttf_values()
, which lets you choose a new TTF folder to search in.
Wrapping Up
You have created a nice GUI that you can use to learn how to draw text on your own images. This code teaches you how to use Pillow effectively for drawing text. It also teaches you how to create a functional graphical user interface using PySimpleGUI. This GUI works on Windows, Mac and Linux. Give it a try and add some cool new features to make it your own.