Stamping Images w/ Watermarks

Watermark added!
Need a watermark on an image? You got it! Imagine you’re an artist, and your company needs a particular image on its website soon. The image has to be watermarked before it goes out to the internet.
You’re welcome to spin up the beast of an art application of your choice, wait for it to load, choose the file, add a layer, save the file and then send it where it needs to go. But is your license for that software out of date? And how much time do you have?
With the small Python application we build in this project, you can knock a few steps off that list.
Today we’ll cover the basics of opening an image, adding a watermark to it and saving the result as a new image.
Ready for a Soft Landing?
What do we need for this project? Here are the resources to get started:
- An integrated development environment (IDE)
- Python 3
- pip
- Pillow, installed with
python3 -m pip install Pillow - An image. Any size works, but today we’re using a 500 x 500 px PNG file. You can use grads.png, attached to this post.
Once you have everything installed, let’s go over the project’s requirements.
You’ll need an image of a reasonable size; ours is 500 x 500 px. Feel free to test other image types and sizes.
The script takes two arguments: the image, and the text to add to it. We could prompt the user for input to make it friendlier, but we’re keeping it simple today.
h2omark.py
How do you like that name? Let’s see what’s going on…
#!/usr/bin/env python3
# Import the modules needed.
from PIL import Image, ImageDraw, ImageFont
import os
import sys
# Point this to a TrueType font installed on your computer.
FONT_PATH = "Hind-Light.ttf"
# Our function, which is called at the bottom of the script.
def h2o(strImg, strTxt):
# Create an Image object from the image file.
objImg = Image.open(strImg)
# Save under a different filename, such as grads_h2o.png.
strName, strExt = os.path.splitext(strImg)
strFile = strName + "_h2o" + strExt
# Prepare the image for drawing.
width, height = objImg.size
draw = ImageDraw.Draw(objImg)
text = strTxt
font = ImageFont.truetype(FONT_PATH, 36)
# Measure the text. textbbox() returns the left, top, right and bottom edges.
left, top, right, bottom = draw.textbbox((0, 0), text, font=font)
textwidth = right - left
textheight = bottom - top
# Calculate the x,y coordinates of the text.
margin = 10
x = width - textwidth - margin - left
y = height - textheight - margin - top
# Draw the watermark in the bottom-right corner.
draw.text((x, y), text, font=font)
# Save the watermarked image.
objImg.save(strFile)
# Call our function with the two arguments given at runtime.
h2o(sys.argv[1], sys.argv[2])
h2omark.py
I believe the comments describe what’s going on. One thing to change: FONT_PATH must point to a font installed on your computer. I used Hind Light here. Fonts such as Tahoma aren’t installed on every operating system, so check what you have first.
The file needs to be executable:
$ chmod +x h2omark.py
Pay attention to the order of the arguments. As written, the script expects the image filename first and the watermark text second, like so:
$ ./h2omark.py grads.png iServeData
This creates grads_h2o.png next to the original. If you change the order of the arguments, you’ll need to change the sys.argv[] index numbers to match.
The original version of this script split the filename into a list on the “.” character. That breaks on filenames with more than one period, such as my.photo.png. os.path.splitext() splits off only the final extension, and it keeps the “.” with the extension, so we don’t have to add it back.
Closing Bytes and Bits
Python and the Pillow library are a great unlicensed way to speed up a workflow and increase productivity. This programmatic approach won’t suit everyone: it takes a little math and some data-type juggling to get a simple watermark.
This post was more about learning to work with these modules and functions. There was some troubleshooting along the way. For example, the save() call at the end kept throwing errors until I remembered that I was passing it the image object instead of a filename. That’s how the filename block was born.
Keep going and take Pillow for a spin. Its documentation is well written, with a slew of examples.
Written with Python 3.10 and Pillow 9.
Edited in October 2026 for grammar and accuracy. The code uses textbbox(), available since Pillow 8.0, and os.path.splitext() to handle filenames with more than one period.
Note, October 2026: Since this post, Pillow 10 (July 2023) removed textsize(). The code above already uses textbbox(), so it works in both old and new versions.