How to create Twitter screenshots with Python
Today I’d like to show you more of what you can achieve with Python and the Pillow library. For those unfamiliar, Pillow is an imaging library, that is, it helps you work with images programatically.
For this article, I want to show you how to automate the creation of Twitter screenshots, like this one:
As you can see, the image contains the user photo, as well as its name and Twitter handle, and the actual tweet. Not centering the contents was a conscious decision.
Just one last thing before the code. We’ll be making use of Pillow, which is a third-party library. Thus, you first need to install it with pip, the Python package manager. Simply go to your command line and enter
pip install pillow. In case you’re on Windows like me, don’t forget to run the command line as Administrator, otherwise you won’t be able to install the library.
And now, let’s dive into the code. We’ll go one step at a time to explain each part of the code, but there’s a code gist with the complete script at the end.
Starting with the imports, they are quite simple: three modules from Pillow, and a function from the built-in
from PIL import Image, ImageDraw, ImageFont
from textwrap import wrap
And that’s it. I’ll get to
wrap in a minute, but before that let me show some useful constant variables we can define to improve readibility further in the script.
FONT_USER_INFO = ImageFont.truetype("arial.ttf", 90, encoding="utf-8")
FONT_TEXT = ImageFont.truetype("arial.ttf", 110, encoding="utf-8")
WIDTH = 2376
HEIGHT = 2024
COLOR_BG = 'white'
COLOR_NAME = 'black'
COLOR_TAG = (64, 64, 64)
COLOR_TEXT = 'black'
COORD_PHOTO = (250, 170)
COORD_NAME = (600, 185)
COORD_TAG = (600, 305)
COORD_TEXT = (250, 510)
LINE_MARGIN = 15
Now let me explain what these will do. The first two variables are font settings, namely one for the user info text (the two lines next to the user photo), and another for the tweet body. We are using the Arial font, with a size of 90 and 110, respectively, with UTF-8 encoding. Due note that, according to how
ImageFont.truetype() looks up fonts, the font you choose should be either in the same folder as the script or in the default fonts directory of your operating system. Though you can also pass an absolute path instead. The remaining variables are just to set up sizes, colors and coordinates of where to draw stuff. (knowing that (0, 0) is the top left corner of the image).
Next up is the information for the image, all the text that will be drawn and a variable with the name to be used by the image file we will create.
user_name = "José Fernando Costa"
user_tag = "@soulsinporto"
text = "Go out there and do some fun shit, not because it makes money, but because it is fun for you!"
img_name = "work_towards_better_tomorrow"
And now for the setup of tracker variables and calculations. I’ll divide this part in three explanations, one for the aforementioned
wrap() function, one for the
y variables and a third for the line height calculations.
text_string_lines = wrap(text, 37)
wrap() function is doing here is splitting the single string that contains our tweet into smaller strings. We can’t really tell the script to draw that one string and expect it to be nicely split into lines of text. Instead, we have
wrap() do that for us, returning a list of strings. The second argument being
37 means that lines won’t be longer than 37 characters.
x = COORD_TEXT
y = COORD_TEXT
These are just tracker variables.
x actually never changes, I created it just to pass
(x,y) as the drawing coordinates later on, but
y on the other hand will keep track of the current vertical position at which to draw a line of text. As you can see, both variables hold the horizontal and vertical positions, respectively, at which the first line of the tweet will be drawn.
temp_img = Image.new('RGB', (0, 0))
temp_img_draw_interf = ImageDraw.Draw(temp_img)line_height = [
for i in range(len(text_string_lines))
(Apologies for the not so optimal code formatting in this sample)
And now for some brief calculations. To extract the height needed to draw each line of the tweet body, we first create a new
Image. Technically, the image doesn’t exist as it is zero pixels wide and tall, but we need to create an object of this type and its drawing interface so that we can call the
textsize() method to extract the height needed to draw each line of text using our font settings defined before.
textsize() returns a two-item tuple, the width and height needed to draw the line, but we are only interested in the second value, the one at index one (
If you’re not familiar with the syntax used for the
line_height variable, this is a special Python syntax called list comprehension (or list comp for short). In essence, this is a for loop where we loop through each line of text stored in the
text_string_lines list, and, for each line, call the
textsize() method to extract the height needed to draw that line. As a result,
line_height is a list of integers that represent the height neded to draw each line of text for the tweet body. For more on list comprehensions, I highly recommend reading the Python documentation on the matter.
Now, we are finally at the last part of the script, the actual image creation and drawing. Again, let’s go one chunk at a time.
img = Image.new('RGB', (WIDTH, HEIGHT), color='white')
draw_interf = ImageDraw.Draw(img)
This what you’ve seen before to create that temporary
Image object, but now to create our “real”
Image object. The image uses the RGB color model, it is
WIDTH pixels wide and
HEIGHT pixels tall, and its background image is white (white is a named color in the module, so we can use the name instead of passing in the RGB values). The next line creates the drawing interface for the image which, as the name suggests, is responsible for any drawing actions in the image.
draw_interf.text(COORD_NAME, user_name, font=FONT_USER_INFO, fill=COLOR_NAME)
draw_interf.text(COORD_TAG, user_tag, font=FONT_USER_INFO, fill=COLOR_TAG)
In these lines we simply draw the user name and then the user handle. The list of passed parameters is: the coordinates at which to start drawing the content, the content to be drawn (text in both cases), the font settings and the color to be used for the drawing. Just a detail,
COLOR_NAME is a named color, black, while
COLOR_TAG uses RGB values instead, passed as a three-value tuple.
for index, line in enumerate(text_string_lines):
draw_interf.text((x, y), line, font=FONT_TEXT, fill=COLOR_TEXT)
y += line_height[index] + LINE_MARGIN
The above block is where we draw those lines of text we’ve been talking about since the beginning. In short, we iterate through the list of lines and draw each line, just like we did with the user name and handle.
In detail, we use the neat
enumerate() function so that each iteration we have access to both the line of text (
line) and its index in the list (
index). In the first line of the loop we draw
line, exactly the same as with the user name and handle, but here the drawing coordinates are given by the
y tracker variables we’ve discussed before. In the following line, we update
y, the tracker of the vertical position at which draw text. We increment its current value with the height needed to draw the line of text that was just drawn, and then we also add some more distance, which works as the line spacing.
Now, all we are missing for the image to be complete is to add the user photo. Before explaining how to do this last part, let me preface it by saying that this user photo should be a 250x250 circle with transparency for the corners, as the values and dimensions used in this example were optimized for that size. Of course, you’re free to play around with all of this and create your own images. After all, that is the purpose of this article, I’m just giving you (new) tools to help realize your creativity!
user_photo = Image.open('user_photo.png', 'r')
So, first we load up the user photo. If the photo is in the same directory as the script, you can use the name of the file as in the example, but if it’s not, you need to provide its path. The second argument,
r, means that the photo is loaded for read-mode, that is, it can’t be modified.
img.paste(user_photo, COORD_PHOTO, mask=user_photo)
And this is the second-last line of the script. We take that photo we just loaded and paste it onto our working image. The first argument is the image to be pasted, the second is the coordinates at which it will be pasted and the third is the mask to be used in the process.
By now the first two arguments are easy to understand, but the third can cause confusion. You see, when you paste the image, if a mask is not specified, then the loaded image will be pasted on top of your working image, ignoring its transparencies. Thus, if we also use the image to be pasted as its own mask, then Pillow will only paste the image on top of the area covered by the mask. The mask does not count transparencies as drawing area, thus the result is that we only paste a 250x250 circle into the working image, not a 250x250 square.
And we have finally reached the end. The image’s modifications are complete, so we can just save it as new file on our computer. If you’re not familiar with that f before the string, that’s for f-strings, a better string formatting introduced in Python 3.6. I encourage you to check out this tutorial if you’re not familiar with it, as it is the best way to format strings in Python. In other words, we save the image as a PNG file (hence the .png extension), with the name (string) set in the
And that’s it. With this article, you’ve learned how to create new images with Pillow, how to draw text and how to paste images (with transparency). As you can see, this library is extremely useful to work with images programatically, and there’s more to it than just this. You can draw shapes and such, modify the size of images, add filters, and more.
The very last thing that I have for you is the code gist with the complete script.
Now go play around with this fantastic library and automate the creation of your own images.
As usual, any and all feedback is welcome to improve future articles :)