While working on the Udacity project `Meme Generator`, that takes in images and captions them with quotes at a random position, I went extra miles to implement a functionality that will wrap the quote’s body if it is longer than the image width. This functionality is not required in the project rubric since the default quotes are short enough to fit the image in one line. Trying to achieve this with Python Pillow didn’t work since it doesn’t automatically draw and push the text to a new line. In other to do this manually, I needed to calculate the width and height of the text, determine the random positions then, fit the text properly so that it doesn’t overflow

code in this article can be found in this repo. Also, check the `Meme Generator` project to see how I implemented the technique in this project. I recommend you follow along with this article and run the code yourself. This way, you will understand and even achieve better results.

Explaining the technique

As we dirty our hands with code, I’ll explain the technique I used to get it done. I considered the following criteria:

Load the Image

We will be using the extension library pillow to draw text on an image. We shall use the following classes from pillow:

Image : to create an image object for our text

ImageDraw: to create a drawing context

ImageFont: font of the text we will be drawing on the image

So, I loaded the image with the code

from PIL import Image, ImageDraw, ImageFont #create image object from the input image path

try:

image = Image.open('./eyong.jpg')

except IOError as e:

print(e)

Make sure Pillow is installed. Also, you can replace ./eyong.jpg with the path to your own image. Consider using .jpg for a start

Random positioning

The project(Meme Generator) requires us to randomly select a location on an image where the quote will be drawn. This is very important as it will determine how I will split the text. As we can see from the diagram above, the white rectangle is the area on the image where a random position will be selected. I decided to make it more to the left because the text will be left-aligned and we won’t want to have our text starting at the extreme right of our image.

The bottom is made 90% because by default the quote will have a body and an author, so if the body occupies just one line, then the author will be pushed down one line. If we had made it 100%, and it happens that the randomly selected y-axis falls at the extreme bottom, then the author will not be visible on the image. As we will see later, the Y-axis will vary base on two other criteria.

Resize the Image

The image is resized with a width of 500px and automatically generates the height while maintaining aspect ratio(Required by the project)

# Resize the image

width = 500

img_w = image.size[0]

img_h = image.size[1]

wpercent = (width/float(img_w))

hsize = int((float(img_h)*float(wpercent)))

rmg = image.resize((width,hsize), Image.ANTIALIAS)

You can use any width here, but it’s important that the height is generated from the width and the aspect ratio is preserved.

Calculate X boundary, Split text to multiple line and Calculate Y boundary

Later, I calculated the x-axis boundary and then randomly generate the x -point:

# Set x boundry

# Take 10% to the left for min and 50% to the left for max

x_min = (rmg.size[0] * 8) // 100

x_max = (rmg.size[0] * 50) // 100

# Randomly select x-axis

ran_x = randint(x_min, x_max)

We need to import randint in other to use it from random import randint

Calculating the Y-axis boundary is a bit tricky. We need to know the number of lines of text and also the line height.

The number of lines of text is determined by the text length and the randomly selected X-axis. Consider the images below:

Consider the image to the left, imagine we randomly selected the x-point and y-point at that position. Given that the text is about 40 characters long and the distance from the x-point to the right boundary of the image is 30px. We notice that the text will overflow. At this point, we can split our text at a maximum length of 30 characters. Hence, our resulting split will have 3 lines of texts of length 30, 30 and 10.

However, after splitting we see that the lines of text overflow to the bottom of the image(Image to the right). This can happen when the height from the y-point to the bottom of the image is shorter compared to the number of lines generated from the split times the line height

The upshot will be to define a new boundary for the Y-axis so that any randomly generated point won’t result in this overflow to the bottom. I did that with the following lines of code:

Split text base on randomly selected X-point and font size of the text

# Create font object with the font file and specify desired size

# Font style is `arial` and font size is 20

font_path = 'font/arialbd.ttf'

font = ImageFont.truetype(font=font_path, size=20) text = "This could be a single line text but its too long to fit in one." lines = text_wrap(text, font, rmg.size[0]-ran_x)

line_height = font.getsize('hg')[1]

In the line of code line_height = font.getsize(‘hg’)[1] , we find the line weight. This will be used to add the appropriate line spacing. We chose h as it extends upward just like t , l , etc. We also choice g as it extends downward just as y , p etc. Hence hg covers the height range of all the English characters.

Lets define text_wrap which does the splitting

The logic is pretty straightforward:

If text is shorter than the maximum width, then it can fit in one line. Return without splitting

Split the text using spaces to get each words

Create short texts by appending words while the width is smaller than the maximum width.

Now that we have the split text and the line weight, we are ready to find the vertical(Y-axis) position boundary and the randomly generated Y-point

y_min = (rmg.size[1] * 4) // 100 # 4% from the top

y_max = (rmg.size[1] * 90) //100 # 90% to the bottom

y_max -= (len(lines)*line_height) # Adjust

ran_y = randint(y_min, y_max) # Generate random point

Now we have every parameter to draw the text on the image. I used a loop to increment the current vertical position with the vertical position and the line height each time a new line is inserted.

#Create draw object

draw = ImageDraw.Draw(rmg) #Draw text on image color = 'rgb(255,0,0)' # Red color

x = ran_x

y = ran_y

for line in lines:

draw.text((x,y), line, fill=color, font=font)



y = y + line_height # update y-axis for new line # Redefine x and y-axis to insert author's name

author = "- Eyong Kevin"

y += 5 # Add some line space

x += 20 # Indent it a bit to the right

draw.text((x,y), author, fill=color, font=font)

rmg.show()

The output image will look something like this:

Captioned image using Python(Image of Eyong Kevin)

Conclusion

We see that the text in the image is readable and well-formatted. I believe there are so many ways and even better ways to solve this problem. I am open to any suggestion to improve on this technique or any other technique better than this one. So, let me know in the comments below.

Happy Xmas and New year in advance.

References

Putting Text on Images using Python — Part 2