earn the White PyBites Ninja earn the Yellow PyBites Ninja earn the Orange PyBites Ninja right arrow earn more PyBites Ninja belts and certificates
The best way to learn to code in Python is to actually use the language.

Our platform offers effective Test Driven Learning which will be key to your progress.

Join thousands of Pythonistas and start coding!

Join us on our PyBites Platform
Click here to code!

Watermarking photos? "I can do that in Python!"

Posted by Anthony Lister on Mon 24 June 2019 in Concepts • 15 min read

Pillow or PIL

The Python Image Library (PIL), although not in the standard library, has been Python’s best-known 2-D image processing library. It predated installers such as pip, so a “friendly fork” called Pillow was created. Although the package is called Pillow, you import it as PIL to make it compatible with the older PIL.

Pillow’s documentation is good, check it out here: https://pillow.readthedocs.io/en/stable/

Set-up and install

As usual, a virtualenv is created and activated to do our work in.

Pillow can be installed on most platforms and has been tested for the latest Python version (3.7) across the most commonly used platforms. Simply install Pillow with pip:

(venv) $ pip install pillow

Let's begin...

Let's cover off some basic usage of Pillow; opening Images, resizing and saving images. If you want to follow along, find an image that you want to use and keep it handy in the same working directory of your program.py.

1) Opening (and saving) images

In [1]:
# program.py

from PIL import Image

# to load an image from a file, use the open() function in the image module. Note the capitalisation of image here. 
image = Image.open('images/original.jpg')

It's not unreasonable to have expected that to 'open' on your screen, but that's not what is meant here. You can test if the opening of the image in Python was successful by ensuring it returned an image object:

In [2]:
# if successful, should return an Image object:
print(type(image))  # output: <class 'PIL.JpegImagePlugin.JpegImageFile'>
<class 'PIL.JpegImagePlugin.JpegImageFile'>

Instead, to open, or show, the image on your screen use:

In [3]:
# to actually open and view it.

This is the image I am using for this work (it could be the PyBites corporate aircraft?):


Now we have opened the image in Python, let's inspect its image details from the object attributes:

In [5]:
# The file format of the source file:
print((image.format)) # Output: JPEG
In [6]:
# The pixel format, i.e. RGB
In [7]:
# Size in pixels
(6000, 4000)

Ok, let's save the image as something else, while also changing the file format from a JPEG to a PNG format:

In [8]:
# saving an image as a new image; can also change file format i.e. jpg -> png

You should end up with two image files in your working directory: 'original.jpg' and 'new_image.png'.

2) Resizing images

Here's where the fun really starts! Of course, it's possible to resize images using Pillow.

The previous image size was determined to be 6000 pixels x 4000 pixels. Let's change that to make the new_image.png smaller. Our watermarking will probably need to be smaller than the image to be watermarked.

To resize an image, we can pass a 2 integer tuple describing width and height to resize().

In [12]:
# lets start to use and modify new_image.png
img = Image.open('images/new_image.png')
#resizing it now to 400 pixels x 400 pixels:
img = image.resize((400, 400))
# save it as new_image_400x400 and as a JPG file:
(400, 400)

Let's look at our new image 'new_image_400x400.jpg'

In [13]:
img.show() # will open the image on your screen

This is what it looks like. Does it look right to you?


The thing to beware of using the resize() function is that is doesn't protect your aspect ratio. It's also therefore possible to blow out the image by making it bigger than the original and lose definition causing it to look a bit fuzzy.

Fortunately, this is where the thumbnail() function comes in handy!

In [14]:
# to keep aspect ratios instead, use thumbnail(),
# takes 2 ints max_width x max_height of the thumbnail.
image = Image.open('images/new_image.png')
image.thumbnail((400, 400))
(400, 266)

While we have just shrunk our image, another positive of thumbnail is that it cannot upscale the image above the pixel size of the original - no blow out! I.e. setting a pixel size of 8000 x 8000 pixels results in an image size of 6000 x 4000 pixels as per the original dimensions.

This looks much better:


In parts 1) and 2) we covered off how to open an image in Pillow, and on the screen. Then resize it using either the resize function or the preferred thumbnail function which preserves the image's aspect ratio.

Let's now look at how we can paste an image onto another image.

3) Pasting an image onto another image

Pasting an image onto another image using Pillow is straightforward and fundamental to creating a watermarked image. For this part, I'm going to use the original aircraft image, and paste the PyBites logo in the bottom right corner to it.

Quite often your second 'watermark' image will have a solid coloured background to it. That needs to be made transparent so that the 'watermark' background merges into the original image well. I'm using a Mac so I refererred to this Apple support page that showed me how to do this. There are methods easily 'Googled' for Windows using Paint too.

This is the PyBites logo from the #100DaysOfCode course, that I modified with a transparent background using the method described above. title

Let's code!

First we'll open the two images. Then make a copy of the original which we'll modify. Then code where we want the PyBites logo to go on the working image, paste it using the paste() function (easy as that!) and save our work:

In [15]:
from PIL import Image

image = Image.open('images/original.jpg')
logo = Image.open('images/pybites_trans.png')

image_copy = image.copy()

position = ((image_copy.width - logo.width), (image_copy.height - logo.height))

image_copy.paste(logo, position)


Here's the result:


But wait! We're using the Pybites logo with a transparent background, aren't we?

Yes we are, but let's look at the Pillow documentation...

Pasting an RGBA image and also using it as the mask will paste the opaque portion of the image but not its transparent background.

We need to pass in a third argument to the paste() function. This argument is the transparency mask Image object. A mask is an Image object where the alpha value is significant, but its green, red, and blue values are ignored. If a mask is given, paste() updates only the regions indicated by the mask.

So let's modify the paste as below to get a pasted logo with transparent pixels.

In [16]:
from PIL import Image

image = Image.open('images/original.jpg')
logo = Image.open('images/pybites_trans.png')

image_copy = image.copy()

position = ((image_copy.width - logo.width), (image_copy.height - logo.height))

# modify the paste by adding the logo as the third argument as per the explanation above.
image_copy.paste(logo, position, logo)


This is better! (Although we should have resized perhaps!)


In part 3) we covered how to paste a logo image onto a main image, and introduced how the paste function can also take a transparency mask as an optional argument.

4) A final program to paste multiple images over the original

As mentioned, the Pillow documentation is really quite good. Advanced users will want to understand how to add/replace alpha layers, or convert to black & white from RGB colours, and change many other parameters.

I encourage you to read the documentation and have a play!

The following code is a variant of the work above. It pastes multiple copies of the PyBites logo watermark all over the original image. While the PyBites logo is a bit of an excessive watermark it demonstrates that a formatted text, possibly with an accompanying logo, can be splashed all over an image you might want to protect online from being copied. This might prevent the image being copied and claimed as someone else's work, whereas a single watermarked image could be cropped out of the original picture.

In [17]:
from PIL import Image

def create_watermark(image_path, final_image_path, watermark):
    main = Image.open(image_path)
    mark = Image.open(watermark)

    mask = mark.convert('L').point(lambda x: min(x, 25))

    mark_width, mark_height = mark.size
    main_width, main_height = main.size
    aspect_ratio = mark_width / mark_height
    new_mark_width = main_width * 0.25
    mark.thumbnail((new_mark_width, new_mark_width / aspect_ratio), Image.ANTIALIAS)

    tmp_img = Image.new('RGB', main.size)

    for i in range(0, tmp_img.size[0], mark.size[0]):
        for j in range(0, tmp_img.size[1], mark.size[1]):
            main.paste(mark, (i, j), mark)
            main.thumbnail((8000, 8000), Image.ANTIALIAS)
            main.save(final_image_path, quality=100)

if __name__ == '__main__':

This results in the following output:


A few evenings play with Pillow increased my knowledge of how to manipulate images in Python with ease. However, my wife opted to use a $1.69 app on her phone to apply a watermark wherever on a photo she might have just taken. It suits her workflow, and I don't have to support it :) But she did later say she wanted a website... "I can do that in Python using Flask!", I said.

Thanks to Bob Belderbos for the inspiration to write this up.

Keep Calm and Code in Python!

-- Anthony

See an error in this post? Please submit a pull request on Github.