Add Watermarks to Images with Python and Pillow
Pillow lets you insert text or image watermarks into photos and documents automatically — perfect for protecting photographs, signing screenshots, and batch processing.
Installation
pip install pillow
Simple Text Watermark
from PIL import Image, ImageDraw, ImageFont
def text_watermark(input_file, output_file, text, opacity=128):
img = Image.open(input_file).convert("RGBA")
layer = Image.new("RGBA", img.size, (255,255,255,0))
draw = ImageDraw.Draw(layer)
try:
font = ImageFont.truetype("arial.ttf", 36)
except:
font = ImageFont.load_default()
bbox = draw.textbbox((0,0), text, font=font)
tw = bbox[2] - bbox[0]
th = bbox[3] - bbox[1]
x = (img.width - tw) // 2
y = (img.height - th) // 2
draw.text((x, y), text, font=font, fill=(255,255,255,opacity))
Image.alpha_composite(img, layer).convert("RGB").save(output_file)
print(f"Watermark added: {output_file}")
text_watermark("photo.jpg", "photo_signed.jpg", "© My Company 2024")
Corner Watermark with Drop Shadow
from PIL import Image, ImageDraw, ImageFont
def corner_watermark(input_file, output_file, text, margin=20):
img = Image.open(input_file).convert("RGBA")
layer = Image.new("RGBA", img.size, (255,255,255,0))
draw = ImageDraw.Draw(layer)
try:
font = ImageFont.truetype("arial.ttf", 28)
except:
font = ImageFont.load_default()
bbox = draw.textbbox((0,0), text, font=font)
tw = bbox[2] - bbox[0]
th = bbox[3] - bbox[1]
x = img.width - tw - margin
y = img.height - th - margin
draw.text((x+2, y+2), text, font=font, fill=(0,0,0,160)) # shadow
draw.text((x, y), text, font=font, fill=(255,255,255,200))
Image.alpha_composite(img, layer).convert("RGB").save(output_file)
corner_watermark("landscape.jpg", "landscape_signed.jpg", "© 2024 MyPhotography")
Diagonal Tiled Watermark
from PIL import Image, ImageDraw, ImageFont
def diagonal_watermark(input_file, output_file, text, spacing=200, angle=30, opacity=60):
img = Image.open(input_file).convert("RGBA")
layer = Image.new("RGBA", img.size, (255,255,255,0))
try:
font = ImageFont.truetype("arial.ttf", 24)
except:
font = ImageFont.load_default()
for y in range(-img.height, img.height*2, spacing):
for x in range(-img.width, img.width*2, spacing):
txt_img = Image.new("RGBA", (300, 50), (255,255,255,0))
txt_draw = ImageDraw.Draw(txt_img)
txt_draw.text((0,0), text, font=font, fill=(128,128,128,opacity))
txt_img = txt_img.rotate(angle, expand=True)
layer.paste(txt_img, (x, y), txt_img)
Image.alpha_composite(img, layer).convert("RGB").save(output_file)
diagonal_watermark("document.jpg", "document_protected.jpg", "CONFIDENTIAL")
Logo / Image Watermark
from PIL import Image
def logo_watermark(base_image, logo_path, output, position="bottom-right", scale=0.15):
base = Image.open(base_image).convert("RGBA")
logo = Image.open(logo_path).convert("RGBA")
new_w = int(base.width * scale)
new_h = int(logo.height * (new_w / logo.width))
logo = logo.resize((new_w, new_h), Image.LANCZOS)
margin = 20
positions = {
"top-left": (margin, margin),
"top-right": (base.width - new_w - margin, margin),
"bottom-left": (margin, base.height - new_h - margin),
"bottom-right": (base.width - new_w - margin, base.height - new_h - margin),
"center": ((base.width - new_w) // 2, (base.height - new_h) // 2),
}
x, y = positions.get(position, positions["bottom-right"])
layer = Image.new("RGBA", base.size, (0,0,0,0))
layer.paste(logo, (x, y), logo)
Image.alpha_composite(base, layer).convert("RGB").save(output)
print(f"Logo watermark saved: {output}")
logo_watermark("photo.jpg", "logo.png", "photo_branded.jpg")
Batch Watermarking
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
def batch_watermark(folder, text, output_format="jpg"):
src = Path(folder)
dst = src / "watermarked"
dst.mkdir(exist_ok=True)
images = list(src.glob("*.jpg")) + list(src.glob("*.png"))
for i, path in enumerate(images, 1):
img = Image.open(path).convert("RGBA")
layer = Image.new("RGBA", img.size, (255,255,255,0))
draw = ImageDraw.Draw(layer)
try:
font = ImageFont.truetype("arial.ttf", max(20, img.width//40))
except:
font = ImageFont.load_default()
bbox = draw.textbbox((0,0), text, font=font)
x = img.width - (bbox[2]-bbox[0]) - 15
y = img.height - (bbox[3]-bbox[1]) - 15
draw.text((x+2,y+2), text, font=font, fill=(0,0,0,120))
draw.text((x,y), text, font=font, fill=(255,255,255,180))
out = dst / (path.stem + "." + output_format)
Image.alpha_composite(img, layer).convert("RGB").save(str(out))
print(f" [{i}/{len(images)}] {out.name}")
print(f"Batch complete: {len(images)} images in {dst}")
batch_watermark("photos/", "© MyPhotography.com")
Additional Resource
For converting images between JPG, PNG, WebP and other formats without any coding, use KaijuConverter — free and no registration required.
Related conversions
Most teams that read this guide convert images in one of these directions: