Resizing and Watermarking images with Python and PIL

Resizing and Watermarking images with Python and PIL

Hi everyone,

Today, we will be seeing how to resize and watermark an image using Python and the PIL library. This is very simple once I went through a few issues I experienced myself and a deprecation warning. This code is based on the code from the following page:

I have recently switched to Ubuntu Linux and am looking for alternative software to those I normally use in Windows. To watermark and resize my images there, I used IrfanView. This software is Windows-only although the FAQ says it can be used in Linux using WINE.

On Linux, we can use the ImageMagick toolkit to manipulate images. Unfortunately, I found it to be a bit complicated to use. This led me to write a Python script to handle these steps.

Python has the wonderful PIL library which is used to write code to do stuff with images. An advantage of this is we can update the code as we like in the future shall we want it to do other stuff. In my code, I’m doing 3 steps:

  1. Rotate the image by using the EXIF image metadata if it is specified.
  2. Resize the image.
  3. Apply the text-based watermark.

Step 1 is needed because otherwise, PIL will ignore the image orientation tag, discard it and proceed to steps 2 and 3, therefore having an incorrectly rotated image with the watermark correctly placed. This happens because when we take a picture with our phones, it stores the orientation in its metadata. This metadata standard is called EXIF. The image itself is not rotated, and it’s up to photo viewing software to respect this rotation metadata. Do note that none of these steps affects the original image with our code.

Other Python libraries we will be using is the os and the glob libraries.

  • os will be used to get the file name and split the file extension.
  • glob will be used to get a list of JPEG files (with the *.jpg extension) in the directory that holds our photos.

Now, let’s start with the code.

Step 1: The Python imports

Before we begin writing our code, we must import some libraries that will allow us to do the operations that we will do to our images. This is important. Otherwise, Python will fail to execute our code.

import os
import glob
from PIL import Image, ImageDraw, ImageFont, ImageOps

The first 2 imports are straightforward. The 3rd import specified which specific classes we want to use from the PIL library.

  • Image is the main image class responsible from opening and saving our image.
  • ImageDraw allow us to draw in the image. We will be adding a text to it, which is why we need it.
  • ImageFont will allow us to use a TrueType font installed in our system to render the text.
  • ImageOps allows performing operations in the image. We will be using it to rotate and resize them.

Step 2: Creating the main image function

We will be defining a function that will be in charge of running the above steps. It will be responsible for generating and parsing the file names using the os library. We will be calling this function resize_and_watermark:

def resize_and_watermark(file: str, path: str) -> None:
    print("Resizing and Watermarking file {}".format(file))
    filename = os.path.basename(file)
    filename_without_extension, extension = os.path.splitext(filename)
    output_filename = "{}-out-watermark{}".format(filename_without_extension, extension)
  1. In the def line, we pass the file argument as a string (str), because the file path itself is a string. We also pass the path argument as a string too because we need the directory path to save our image later. This functon does not return anything, so the expected output is None.
  2. We then print a message along with the file that it is processing (line 2).
  3. Moving forward to the next 3 lines, the first one parses the file name out of the full file path (os.path.basename).
  4. Then, we split the file name with os.path.splitext. This returns us 2 strings: The first is the filename without the extension and the 2nd string is the extension itself.
  5. The last line is the formatted output filename, appending out-watermark and the extension to it. Note that we pass the splitted filename and extension to it from the previous lines to the .format function.

Step 3: Opening the image, performing the rotation, and resizing it.

Now that we parsed the file names, it’s time to add the code to open, rotate the image according to its EXIF metadata orientation and to resize it to a specified width and height in pixels.

    img = Image.open(file)
    img = ImageOps.exif_transpose(img)
    img = ImageOps.contain(img, (1920, 1080))

The first line will open the image file and the img variable will hold it. This variable will keep the image as we process it with our code.

If an image has an EXIF orientation tag, we want to use it to rotate our images properly before we do any further processing to it. This is what the ImageOps.exif_transpose(img) line does. Without it, we would have a non-rotated image like this:

Donuts - Not Rotated

A rotated image using the above line will look like this:

Donuts - Rotated

The line ImageOps.contain(img, (1920, 1080)) will resize the image to 1920×1080 pixels maintaining its aspect ratio. This will help us keep our images small too, ideal to host on a website like this. You can change those 2 numbers to another resolution that fits your needs. It needs to be in the (x, y) format and be in pixels.

At this time, we have rotated and resized our image.

Step 4: Drawing the text

Now it’s the time to place the text watermark in our image. This piece of code is more complicated, so we will go in more details here:

    draw = ImageDraw.Draw(img)
    text = "https://moisescardona.me"
    font = ImageFont.truetype('/home/moisespr123/.local/share/fonts/micross.ttf', 48)
    textbbox = draw.textbbox((0, 0), text, font)

In line 14, we make a draw variable that will hold the image data and allow to modify the img object directly.

In line 15, we have the text variable that holds the text to draw to our image

In line 16, we use the ImageFont class to open a TrueType font installed in our system along with the text size (48 in this case). The first argument is the path to the TrueType font to use and the 2nd argument is the text size to use. The result is returned and is held in the font variable.

Finally, in line 17, we draw the text at the pixels (0, 0). The textbbox variable holds a slice with the size of our text along all 4 corners.

We can use a print statement to get this information:

    print(textbbox)
    # prints the following
    #  (0, 11, 550, 55)

These are the bounding pixels where the text starts and ends. We need the last 2 items in order to subtract the image width and height with them to place our text correctly.

This brings us to the next piece of our code:

    textwidth = textbbox[2]
    textheight = textbbox[3]
    width, height = img.size
    x = width - textwidth - 10
    y = height - textheight - 10

The first 2 lines holds the last 2 items from the textbbox variable, the text width and text height, respectively. We do not need the starting information in this case, since we will be placing our watermark in the lower right by subtracting the image width an height with these values.

Line 20 gets the image width and height and stores it in their respective variables.

The last 2 lines subtracts this to get the x and y location to place our text in the image. Iḿ also subtracting 10 more pixels to make it look better placed in the image.

Now, we can draw our text in our image:

    draw.text((x, y), text, font=font, stroke_width=3, stroke_fill="#000")

draw.text is the function that will draw the text in our image.

  1. The first argument is the x and y location to place our text. It is the upper-left coordinate in pixels as PIL’s coordinate system starts there.
  2. The text variable holds the text we previously wrote in line 15.
  3. font=font tells the font argument to use our font variable holding the TrueType font and the text size from line 16.
  4. stroke_with=3 is the size of the stroke. It is basically the border of our text. The 3 is the size.
  5. stroke_fill="#000" is the color to use for the stroke. #000 is the hex number for black.

At this point, we have drawn the text to our image. We now just need to save it.

    img.save(path + "/" + output_filename)

This line is self-explanatory. It will save the data from the img object to a specified path. Note that we use here the path and output_filename variables.

Now, we need to write the loop function to go over each JPEG file in a directory.

Step 5: Looping through each image in the folder

All we need to do now is write some code that will go through each image in a folder. For this, we will be defining a path variable that will hold the directory path and we will get the file list using glob.glob.

if __name__ == "__main__":
    path = '/home/moisespr123/Donuts'
    for filepath in glob.glob(path + '/*.jpg'):
        resize_and_watermark(filepath, path)
    print("Done Resizing and Watermarking files.")

Because this is basically the code that will loop and call our resizing and watermark function, it is basically the main code part of our script. We want it to execute if it is called directly from our interpreter, which is the purpose of the first line.

Next, we see the path variable defined with a directory path.

Now, we will loop through the file list it generates on that same for loop by using glob.glob and specifying the path along with a wildcard to get only the *.jpg files.

For each JPEG with the *.jpg extension found, we will run the resize_and_watermark function, passing the complete file path (filepath) and the directory path (path) as arguments .

Once we have processed every file of the directory, it goes out of the for loop and prints the message: Done Resizing and Watermarking files.

Running the script.

Now, we are ready to run the script. Here is the folder before running this script:

Python Resize and Watermark Script - Folder before running the script

To run the script, we simply open the terminal, go to our scripts folder, and run the python3 command followed by the script name:

Python Resize and Watermark Script - Running the Script

As we can see, the script ran, but gave us a warning. We’ll see how to disable that below. It doesn’t affect the output but if you try to process huge files, you get that message by default. Regardless of it, the script ran successfully and here is the output:

Donuts - Watermarked

The watermark is located in the lower right corner. It is simple and ideal to simply watermark our images.

Here is the folder with the watermarked image. Note that the file size is also way smaller than the original:

Python Resize and Watermark Script - Folder after running the script

We can also see that the image follows our resizing limits as it has a height of 1080 pixels:

Python Resize and Watermark Script - Image dimensions

Congratulations! We have successfully resized and watermarked our image!

Bonus: Disabling the DecompressionBombWarning message

Because our image was taken with a resolution of 108MP, it exceeds a limit of pixels that PIL has in place. It just warns us, and we can ignore it, but also we can disable this warning in the code by adding the following:

    Image.MAX_IMAGE_PIXELS = None

Now, when we run the script again, the warning no longer appears:

Python Resize and Watermark Script - Running the Script - No Warning

Congratulations! We have written our Python resizing and watermarking script. I hope you enjoyed this content and look up for more programming content!

The full script is below. Note that you need to change the following variables:

  • text to your desired watermark text.
  • font to your desired TrueType Font and text size.
  • path to the path containing your JPEG files to resize and watermark

Optionally, change the 1920 and 1080 values in line 13 to resize to another resolution. The watermark placement can be adjusted by changing lines 21 and 22 for x and y, respectively.

import os
import glob
from PIL import Image, ImageDraw, ImageFont, ImageOps


def resize_and_watermark(file: str, path: str) -> None:
    print("Resizing and Watermarking file {}".format(file))
    filename = os.path.basename(file)
    filename_without_extension, extension = os.path.splitext(filename)
    output_filename = "{}-out-watermark{}".format(filename_without_extension, extension)
    img = Image.open(file)
    img = ImageOps.exif_transpose(img)
    img = ImageOps.contain(img, (1920, 1080))
    draw = ImageDraw.Draw(img)
    text = "Your Text"
    font = ImageFont.truetype('/path/to/truetype/font.ttf', 48)
    textbbox = draw.textbbox((0, 0), text, font)
    textwidth = textbbox[2]
    textheight = textbbox[3]
    width, height = img.size
    x = width - textwidth - 10
    y = height - textheight - 10
    draw.text((x, y), text, font=font, stroke_width=3, stroke_fill="#000")
    img.save(path + "/" + output_filename)


if __name__ == "__main__":
    Image.MAX_IMAGE_PIXELS = None
    path = '/path/to/jpg/files'
    for filepath in glob.glob(path + '/*.jpg'):
        resize_and_watermark(filepath, path)
    print("Done Resizing and Watermarking files.")

FAQ

  • Q: I get an import error saying PIL cannot be imported.
    A: Be sure to install the Pillow library with pip3 install -U Pillow.
Python Resize and Watermark Script - Install Pillow