Create Text Watermarks with Pillow
Learn how to add a text watermark to an existing image with Python and Pillow, load a TrueType font, position text near the bottom-left, preview it, and save a new image.
A watermark is identifying text or graphics placed over an image. A text watermark can show branding, attribution, copyright information, or an ownership label. In this lesson, you will use Pillow to add a short text label near the lower-left corner of an existing image.
Pillow is a Python imaging library used to open, modify, display, and save image files. If you need a refresher on opening files, see the Pillow online course and Pillow image attributes.
How a text watermark workflow fits together
A watermark workflow has several separate parts:
- Source image: The existing raster image that you want to edit.
- Drawing context: An object created with
ImageDraw.Draw(image). It provides methods for drawing on the image. - Watermark text: A Python string such as a domain name or organization name.
- Font: A loaded TrueType font and a size, normally measured in pixels for this task.
- Position: An
(x, y)coordinate tuple indicating where the text drawing begins. - Result: The modified image shown in a preview or written to a new output file.
Drawing changes the image object in memory. It does not automatically create a file. To keep the result after the program ends, call image.save().
Pillow objects and methods
The three imported Pillow classes provide the main parts of this task.
| Object or method | Purpose | Typical input | Result |
|---|---|---|---|
Image | Opens and represents an image | A local filename or path | An in-memory image object |
Image.open() | Loads an existing image file | "photo.jpg" | A Pillow image |
ImageDraw | Provides drawing operations | The imported drawing module | Access to drawing tools |
ImageDraw.Draw(image) | Creates a drawing context attached to an image | A Pillow image object | An object used to render text and shapes |
ImageFont | Loads and works with fonts | The imported font module | Access to font-loading methods |
ImageFont.truetype() | Loads a TrueType font at a chosen size | A .ttf path and pixel size | A font object |
ImageDraw.text() | Renders a string onto the image | Position, text, and font | Pixels added to the image |
image.show() | Provides a quick local preview | The edited image object | A displayed preview |
image.save() | Exports the edited image | An output filename or path | A persistent image file |
Install and import Pillow
If Pillow is not installed in the active Python environment, install it with:
python -m pip install Pillow
Import the three classes needed for a text watermark:
from PIL import Image, ImageDraw, ImageFont
Image opens the source image, ImageDraw supplies text-rendering operations, and ImageFont loads the font used to render the watermark.
Open the source image
Use Image.open() with a local filename or filesystem path. In this example, the source file is named photo.jpg and is in the same directory as the Python script.
source_path = "photo.jpg"
image = Image.open(source_path)
The variable image now refers to the in-memory Pillow image. Drawing operations performed on this object modify it in memory. The original file is not changed merely by opening or drawing; it is changed only if you deliberately save over its path.
Define the watermark text
Keep the label in its own string variable. This separates content from drawing code and makes the label easy to change.
watermark_text = "example.org"
A short domain name, organization name, photographer credit, or attribution label usually works better than a long sentence. The text should identify the image without hiding too much of its content.
Load a TrueType font
A TrueType font is a font file commonly identified by the .ttf extension. Load it with ImageFont.truetype(), passing a font filename or an explicit filesystem path and a font size in pixels.
font_path = "DejaVuSans.ttf"
font_size = 32
font = ImageFont.truetype(font_path, size=font_size)
The font file must exist and be accessible to the Python process. A filename is resolved using the program's working directory and Pillow's available font lookup behavior. When that is unreliable, provide a complete path.
Font paths on different operating systems
| Operating system | Common font location | Path handling note | What to check when loading fails |
|---|---|---|---|
| Windows | C:\Windows\Fonts | Use a raw string for backslash-containing paths, such as r"C:\Windows\Fonts\Arial.ttf" | Confirm the file exists and that the filename matches the installed font file |
| Linux | Often under /usr/share/fonts | Use a normal absolute path with forward slashes | Verify that the font is installed and that the selected directory contains the requested .ttf file |
| macOS | Often under system or user font directories | Use the full path when a filename alone is not found | Check the installed font's exact filename and permissions |
For example, an explicit Windows path can be written as:
font_path = r"C:\Windows\Fonts\Arial.ttf"
font = ImageFont.truetype(font_path, size=32)
The r before the string creates a raw Python string. It prevents backslashes in a Windows path from being interpreted as escape sequences. You can also use doubled backslashes or forward slashes.
Positioning text with coordinates
ImageDraw.text() accepts a coordinate tuple such as (x, y). In the basic usage pattern, this coordinate describes the text drawing origin, or the placement near the text's upper-left area.
Pillow's image coordinate system starts at the upper-left corner. The x coordinate increases to the right, and the y coordinate increases downward.
position = (20, 650)
draw.text(position, watermark_text, font=font)
This fixed position uses a 20-pixel left margin and a y coordinate of 650. It is image-size dependent: it may be near the bottom for one photograph, but too high, too low, or outside another photograph.
Choosing a fixed bottom-left position
If you know the image dimensions and the watermark's approximate height, choose a small horizontal margin and a y coordinate close to the lower edge.
left_margin = 20
bottom_y = 650
position = (left_margin, bottom_y)
Fixed coordinates are useful for a quick example, but reusable code should calculate the position from the image size and the rendered text bounds.
Calculating a reusable lower-left position
The image's size property returns a tuple containing its width and height:
width, height = image.size
Use the drawing context to measure the text before drawing it. textbbox() returns a bounding box for the selected text and font. Its values are commonly represented as (left, top, right, bottom).
margin = 20
bbox = draw.textbbox((0, 0), watermark_text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = margin - bbox[0]
y = height - margin - text_height - bbox[1]
position = (x, y)
This calculation keeps the text near the bottom-left edge while leaving the same approximate margin on the left and bottom. Accounting for the bounding-box offsets is useful because font metrics can place visible pixels slightly away from the nominal origin.
Draw the watermark
Create a drawing context with ImageDraw.Draw(image). The returned object is attached to the image and is used to render text directly onto it.
draw = ImageDraw.Draw(image)
draw.text(position, watermark_text, font=font, fill="white")
The required arguments are the position, the text string, and the font. The optional fill argument selects the text color. A light color such as "white" may work on a dark region; a dark color such as "black" may work on a light region.
| Parameter | Meaning | Example type of value | Effect on output |
|---|---|---|---|
position | The text drawing origin as an x/y coordinate tuple | (20, 650) | Moves the watermark horizontally and vertically |
text | The string to render | "example.org" | Defines the visible label |
font | The loaded font and size | ImageFont font object | Controls typeface and text dimensions |
fill | The text color | "white" or an RGB tuple | Controls contrast and readability |
font_size | The requested size in pixels | 32 | Changes the apparent size and bounds of the watermark |
Complete bottom-left watermark example
This example opens a photograph, creates a drawing context, defines a domain watermark, loads a TrueType font, calculates a lower-left position, draws the text, previews the result, and saves a separate output file.
from PIL import Image, ImageDraw, ImageFont
source_path = "photo.jpg"
output_path = "photo-watermarked.jpg"
font_path = "DejaVuSans.ttf"
watermark_text = "example.org"
font_size = 32
margin = 20
# Open the existing image.
image = Image.open(source_path)
# Create a drawing context attached to the image.
draw = ImageDraw.Draw(image)
# Load a TrueType font.
font = ImageFont.truetype(font_path, size=font_size)
# Measure the rendered text.
bbox = draw.textbbox((0, 0), watermark_text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# Calculate a bottom-left position.
width, height = image.size
x = margin - bbox[0]
y = height - margin - text_height - bbox[1]
# Render the watermark directly onto the image.
draw.text((x, y), watermark_text, font=font, fill="white")
# Preview the edited image and save a persistent copy.
image.show()
image.save(output_path)
The text_width variable is measured here for clarity and possible future layout checks. The lower-left calculation mainly needs the text height, image height, and bounding-box offsets.
Previewing and exporting
image.show() is useful for a quick local preview. It normally opens the image using an operating-system image viewer. It does not create the output file you specify in your script.
image.show()
To persist the result, save it to a new filename:
image.save("photo-watermarked.jpg")
Saving to a new path protects the source image from accidental replacement. Overwrite the original only when that behavior is intentional:
image.save(source_path)
Troubleshooting
Font loading raises an error
Common causes include a font filename that is not in the process's working directory, a font that is not installed, a malformed path, or a file that the program cannot access.
- Verify that the requested
.ttffile exists. - Supply the complete font path instead of only the filename.
- On Windows, use a raw string such as
r"C:\Windows\Fonts\Arial.ttf". - On Linux, check available font directories, commonly including
/usr/share/fonts. - Check the exact capitalization and spelling of the font filename.
The watermark is too high, too low, or outside the image
A hard-coded y coordinate may not match the image dimensions. A different font size also changes the rendered text height.
- Inspect
image.sizeto obtain the current width and height. - Use a bottom margin and calculate y from the image height.
- Measure the text with
draw.textbbox()before choosing the final position. - Remember that y increases downward, so a larger y value moves text toward the bottom.
The watermark is difficult to read
The text may blend into the image, be too small, or overlap a visually busy area.
- Choose a contrasting
fillcolor. - Increase the font size if the image resolution supports it.
- Move the watermark to a clearer area while retaining an edge margin.
- Preview the result and inspect it at the intended display size.
The preview has a watermark but no output file exists
Image.show() only provides a preview. Add image.save(output_path) after drawing the text. Also check the program's working directory or use an explicit output path so you know where the file is written.
Summary
- Import
Image,ImageDraw, andImageFontfrom Pillow. - Open a local source image with
Image.open(). - Create a drawing context with
ImageDraw.Draw(image). - Store the branding or attribution label in a string variable.
- Load an accessible TrueType font with
ImageFont.truetype(). - Choose an
(x, y)coordinate or calculate a position from image dimensions and text bounds. - Render the label with
draw.text(), selecting a readable fill color. - Use
image.show()for a preview andimage.save()to create a persistent output file.
For related Pillow techniques, continue with cropping images, RGB channels, or rotating an image.