Pillow Watermark Generator & Compositor
Design professional, anti-theft text and logo watermarks in Python Pillow. Configure opacity, 9-quadrant positioning, diagonal tiling patterns, and shadow outlines live in your browser.
1. Configure Watermark Style
Adjust text, opacity, rotation, and anchor positioning:
2. Generated Python Script
Complete alpha compositing code with true opacity blending:
🎨 Alpha Compositing Theory
How Pillow blends transparent watermarks without distorting background colors:
Never draw directly on the source RGB photo. Instead, create a transparent overlay image (Image.new('RGBA', size, (255, 255, 255, 0))), render the watermark text onto the overlay, and blend with Image.alpha_composite().
Hardcoded font sizes like font_size=20 look fine on a 600px preview but become microscopic on a 24-megapixel camera photo. Always calculate font size dynamically: font_size = int(max(img.size) * 0.035).
📚 Production Watermarking Recipes
Tested snippets for text stamping, PNG logo overlays, anti-theft proofing, and parallel folder processing:
1. Dynamic Scaling Text Watermark with Drop Shadow
Recommended for PhotosRenders clean semi-transparent text with a subtle dark drop shadow, ensuring legibility on both light and dark photo areas.
from PIL import Image, ImageDraw, ImageFont
def add_text_watermark(input_path, output_path, text="© Photography", opacity=128):
"""
Adds semi-transparent text watermark with shadow to any image.
"""
with Image.open(input_path).convert("RGBA") as base:
# 1. Create transparent text layer
txt_layer = Image.new("RGBA", base.size, (255, 255, 255, 0))
draw = ImageDraw.Draw(txt_layer)
# 2. Dynamic font sizing (3.5% of longest edge)
font_size = max(18, int(max(base.size) * 0.035))
try:
font = ImageFont.truetype("arial.ttf", font_size)
except IOError:
font = ImageFont.load_default(size=font_size) # scalable built-in font (Pillow 10.1+)
# 3. Calculate bottom-right positioning with padding
bbox = draw.textbbox((0, 0), text, font=font)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
pad = int(font_size * 0.75)
x = base.width - text_w - pad
y = base.height - text_h - pad
# Draw soft shadow (black 40% opacity)
draw.text((x + 2, y + 2), text, font=font, fill=(0, 0, 0, int(opacity * 0.7)))
# Draw main watermark text
draw.text((x, y), text, font=font, fill=(255, 255, 255, opacity))
# 4. Composite & save as high quality JPEG/PNG
watermarked = Image.alpha_composite(base, txt_layer)
watermarked.convert("RGB").save(output_path, "JPEG", quality=95)
print(f"Watermarked: {output_path}")
# Usage
add_text_watermark("landscape.jpg", "landscape_watermarked.jpg")
2. Transparent PNG Logo Overlay with Custom Opacity
Branding & E-commerceOverlays a corporate PNG logo with proportional auto-scaling and adjustable alpha transparency.
from PIL import Image
def overlay_logo_watermark(base_path, logo_path, output_path, scale_pct=0.2, opacity=0.6):
"""
Overlays a PNG logo onto a base image with custom scale and opacity.
"""
with Image.open(base_path).convert("RGBA") as base, Image.open(logo_path).convert("RGBA") as logo:
# 1. Resize logo proportional to base image width
target_w = int(base.width * scale_pct)
target_h = int(logo.height * (target_w / logo.width))
logo_resized = logo.resize((target_w, target_h), Image.Resampling.LANCZOS)
# 2. Adjust logo transparency
r, g, b, alpha = logo_resized.split()
alpha = alpha.point(lambda p: int(p * opacity))
logo_resized.putalpha(alpha)
# 3. Paste in bottom-right corner with 20px padding
pos_x = base.width - target_w - 20
pos_y = base.height - target_h - 20
base.paste(logo_resized, (pos_x, pos_y), mask=logo_resized)
base.convert("RGB").save(output_path, "JPEG", quality=95)
# Usage
# overlay_logo_watermark("product.jpg", "company_logo.png", "product_branded.jpg")
3. Anti-Theft Diagonal Tiled Grid Pattern
Stock Photo ProtectionRepeats angled watermark text across the entire canvas in a grid, preventing unauthorized cropping or AI inpainting removal.
import math
from PIL import Image, ImageDraw, ImageFont
def add_tiled_watermark(input_path, output_path, text="PROOF — DO NOT COPY", opacity=60):
with Image.open(input_path).convert("RGBA") as base:
# Create transparent canvas
overlay = Image.new("RGBA", base.size, (255, 255, 255, 0))
draw = ImageDraw.Draw(overlay)
font_size = max(16, int(base.width * 0.025))
try:
font = ImageFont.truetype("arial.ttf", font_size)
except IOError:
font = ImageFont.load_default(size=font_size) # scalable built-in font (Pillow 10.1+)
step_x = int(font_size * 10)
step_y = int(font_size * 6)
for y in range(0, base.height + step_y, step_y):
for x in range(0, base.width + step_x, step_x):
draw.text((x, y), text, font=font, fill=(255, 255, 255, opacity))
# Rotate overlay by -30 degrees
rotated = overlay.rotate(-30, expand=False, resample=Image.Resampling.BICUBIC)
watermarked = Image.alpha_composite(base, rotated)
watermarked.convert("RGB").save(output_path, "JPEG", quality=90)
# Usage
# add_tiled_watermark("client_proof.jpg", "client_proof_watermarked.jpg")
4. Multithreaded Parallel Folder Batch Watermarker
Batch ProcessingProcesses thousands of gallery images in parallel across all CPU cores with automatic directory structuring.
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
from PIL import Image, ImageDraw, ImageFont
def process_file(src, dst_dir, text):
try:
dst = Path(dst_dir) / src.name
with Image.open(src).convert("RGBA") as base:
txt_layer = Image.new("RGBA", base.size, (255, 255, 255, 0))
draw = ImageDraw.Draw(txt_layer)
font_size = max(18, int(max(base.size) * 0.03))
try:
font = ImageFont.truetype("arial.ttf", font_size)
except IOError:
font = ImageFont.load_default(size=font_size) # scalable built-in font (Pillow 10.1+)
draw.text((30, base.height - font_size - 30), text, font=font, fill=(255, 255, 255, 140))
Image.alpha_composite(base, txt_layer).convert("RGB").save(dst, "JPEG", quality=92)
return f"Watermarked: {src.name}"
except Exception as e:
return f"Failed {src.name}: {e}"
def batch_watermark(input_folder, output_folder, text="© Brand 2026"):
Path(output_folder).mkdir(parents=True, exist_ok=True)
files = [p for p in Path(input_folder).glob("*") if p.suffix.lower() in (".jpg", ".jpeg", ".png")]
with ThreadPoolExecutor(max_workers=8) as ex:
results = ex.map(lambda f: process_file(f, output_folder, text), files)
for r in results:
print(r)
❓ Frequently Asked Questions
from PIL import Image; from PIL.ExifTags import Base; exif = img.getexif(); exif[Base.Copyright] = "Copyright 2026 YourName"; img.save('out.jpg', exif=exif). This preserves pristine visual pixels while storing legal metadata inside the file container.
rotated_layer = text_layer.rotate(45, expand=False, resample=Image.Resampling.BICUBIC), and composite onto the base image using Image.alpha_composite(base_rgba, rotated_layer).
save(), specify quality=95 and subsampling=0 (4:4:4 full chroma preservation) to eliminate chroma sub-sampling color bleed around sharp watermark edges.