Title: Label images at the bottom in Python
This is the third in a four-part series of posts leading to a flash card application that you can use to learn about Japanese hiragana characters.
My previous post Split images into halves in Python split images into two halves, one containing a hiragana character and one containing the same image overlaid with a mnemonic image. For example, the following images show the hiragana character む, which is pronounced "mu" as in "moo."
Of course you may not remember what the mnemonic means, so I want to add a label at the bottom of the second image like this.
That's what this example does.
Using the Program
To use the program, enter the name of the directory that contains the split images and enter an output directory.
Click list Files to see the list of files in the input directory. Click on a file to select it and add a label to its bottom. Use the controls in the window's middle column to set the font size, label background color, font color, message area height, and message for the image. For example, click on the BG Color swatch to select the background color. After you have a result that you like, click Save to save a copy of the labeled image in the output directory.
Repeat until you've labeled all of the images.
The code that builds the user interface is fairly long but not super innovative, so I'm going to skip discussing that. Download the example to read it for yourself. Instead, let's jump into the code that actually does stuff.
Listing Files
When you click List Files, the following code springs into action.
def draw_sample(self, *args):
'''Draw the labeled sample image.'''
# Discard any previously saved image.
self.full_scale_image = None
self.canvas.delete(tk.ALL)
try:
# Get parameters.
if len(self.file_list.curselection()) == 0:
return
index = self.file_list.curselection()[0]
filename = self.file_items[index]
font_size = float(self.font_hgt_var.get())
fg_color = self.fg_label['bg']
bg_color = self.bg_label['bg']
height = int(self.msg_hgt_var.get())
message = self.msg_var.get().strip()
# Draw the sample.
# Load the image.
pil_image = Image.open(filename)
# Add space at the bottom.
size = (pil_image.width, pil_image.height + height)
resized_image = Image.new(pil_image.mode, size, bg_color)
resized_image.paste(pil_image, (0, 0))
# Draw the label.
dr = ImageDraw.Draw(resized_image)
x = pil_image.width // 2
y = pil_image.height + height // 2
font_path = "arial.ttf"
text_font = ImageFont.truetype(font_path, font_size)
dr.text((x, y), message, font=text_font, fill=fg_color,
anchor='mm')
# Save the full-scale image in case we want to save it.
self.full_scale_image = resized_image
# Display the result.
self.canvas.update()
wid = self.canvas.winfo_width()
hgt = self.canvas.winfo_height()
display_image, x, y = fit_image(resized_image, 0, 0, wid, hgt)
self.photo_image = ImageTk.PhotoImage(display_image)
self.canvas.create_image(x, y, anchor=tk.NW,
image=self.photo_image)
except ValueError as ex:
if f'{ex}' != 'font size must be greater than 0, not 0.0':
print(f'{type(ex).__name__}: {ex}')
except Exception as ex:
print(f'{type(ex).__name__}: {ex}')
This method deletes any saved full scale image and clears the canvas that displays the sample.
The code then enters a try except block where it does all of its interesting work. This block is necessary because there will be times when the parameters will be invalid. For example, if you select the font size and then type 9, the Entry widget will first delete its current value leaving it blank. A blank value cannot be converted into a font size, so that will cause an error. The try except block ignores that kind of error.
Inside the block, the code gets the parameters that you selected. To draw the sample, the code first loads the file into a PIL image.
Next, the code creates a new image that is taller than the input image by the amount you entered in the Msg Height entry widget. It sets the background color of the new image to the BG Color that you selected.
The code then pastes the original image onto the new, taller image and draws the message text at the bottom. The text method's anchor parameter lets you determine how the text is aligned with respect to the target location. In this example, that location (x, y) is the center of the message area and the anchor value mm centers the text horizontally and vertically.
The method saves the full-scale image in self.full_scale_image. It then uses the fit_image function described in my post Fit an image to a target rectangle and center it in Python to resize the image to fit the program's canvas. It converts the resized result into a PhotoImage, saves it somewhere that the garbage collector can't steal it, and draws the image on the canvas.
Saving Results
When you click Save, the program uses the following code to save the image.
def save_image(self):
'''Save the labeled image.'''
if self.full_scale_image is None:
return
# Compose the output file name.
index = self.file_list.curselection()[0]
filename = self.file_items[index]
from_file = Path(filename)
to_path = Path(self.to_dir_var.get())
# Make the output dirtcory if necessary.
to_path.mkdir(parents=True, exist_ok=True)
# Save the file.
output_filename = to_path / f'{from_file.name}'
self.full_scale_image.save(output_filename)
If there isn't a full-scale image, because you haven't selected a file or one of the parameters (like font size) is invalid, the method simply returns.
If there is a full-scale image, the code gets the input file name and makes a Path object for it and the output directory. It creates that directory if it doesn't exist.
The code uses the output directory and the input file's name to compose the output file name and saves the image there.
Note that the output file's name is the same as the input file's name, it's just in a different directory. One consequence of that is that the saved file has the same format as the original. For example, if the input file is a JPG file, then the output file is also a JPG file.
Conclusion
This example lets you add messages at the bottom of an image. Some of its key points that you may find useful in other programs include:
- It loops through the image files in a directory and adds them to a list box, also saving their file items for easy use later
- It lets you select foreground and background colors
- It makes an enlarged image, copies the original image onto it, and then draws the message at the bottom
- It saves result files in an output directory with the same names and formats as the original files
Download the example to see additional details.
My next post will be the final flash card app that we've been working toward for the past few posts.
|