Introduction
Pillow (PIL Fork) merges a transparent PNG overlay onto a background image using Image.paste() with the alpha channel as a mask, or Image.alpha_composite() for full alpha blending. The key is using the overlay's alpha channel (transparency information) so that transparent pixels show the background through. Without the mask parameter, paste() replaces pixels entirely, ignoring transparency.
Basic Overlay with paste()
1from PIL import Image
2
3# Open background and overlay images
4background = Image.open("background.jpg").convert("RGBA")
5overlay = Image.open("logo.png").convert("RGBA") # Must have alpha channel
6
7# Paste overlay at position (x, y) using its alpha channel as mask
8position = (50, 50)
9background.paste(overlay, position, mask=overlay)
10
11# Save result
12background.save("result.png")
The third argument mask=overlay tells Pillow to use the overlay's alpha channel as the transparency mask. Without it, transparent pixels would be pasted as opaque.
Using alpha_composite() (Full Alpha Blending)
1from PIL import Image
2
3background = Image.open("background.jpg").convert("RGBA")
4overlay = Image.open("watermark.png").convert("RGBA")
5
6# Resize overlay if needed
7overlay = overlay.resize((200, 200))
8
9# Create a blank image the same size as background
10temp = Image.new("RGBA", background.size, (0, 0, 0, 0))
11
12# Paste overlay onto temp at the desired position
13temp.paste(overlay, (100, 100))
14
15# Alpha composite: properly blends semi-transparent pixels
16result = Image.alpha_composite(background, temp)
17result.save("result.png")
alpha_composite() handles semi-transparent pixels (alpha values between 0 and 255) correctly, while paste() with a mask treats pixels as either fully transparent or fully opaque.
Centering the Overlay
1from PIL import Image
2
3background = Image.open("photo.jpg").convert("RGBA")
4overlay = Image.open("watermark.png").convert("RGBA")
5
6# Calculate center position
7bg_w, bg_h = background.size
8ov_w, ov_h = overlay.size
9x = (bg_w - ov_w) // 2
10y = (bg_h - ov_h) // 2
11
12background.paste(overlay, (x, y), mask=overlay)
13background.save("centered.png")
Resizing the Overlay
1from PIL import Image
2
3background = Image.open("background.jpg").convert("RGBA")
4overlay = Image.open("logo.png").convert("RGBA")
5
6# Resize to 20% of background width, maintaining aspect ratio
7target_width = background.width // 5
8aspect_ratio = overlay.height / overlay.width
9target_height = int(target_width * aspect_ratio)
10
11overlay = overlay.resize((target_width, target_height), Image.LANCZOS)
12
13# Place in bottom-right corner with padding
14padding = 20
15x = background.width - overlay.width - padding
16y = background.height - overlay.height - padding
17
18background.paste(overlay, (x, y), mask=overlay)
19background.save("with_logo.png")
Adjusting Overlay Opacity
1from PIL import Image, ImageEnhance
2
3background = Image.open("photo.jpg").convert("RGBA")
4overlay = Image.open("watermark.png").convert("RGBA")
5
6# Reduce overlay opacity to 50%
7alpha = overlay.split()[3] # Extract alpha channel
8alpha = ImageEnhance.Brightness(alpha).enhance(0.5) # 50% opacity
9overlay.putalpha(alpha)
10
11# Composite
12temp = Image.new("RGBA", background.size, (0, 0, 0, 0))
13temp.paste(overlay, (100, 100))
14result = Image.alpha_composite(background, temp)
15result.save("semi_transparent.png")
Or create a uniform semi-transparent overlay:
1from PIL import Image
2
3background = Image.open("photo.jpg").convert("RGBA")
4overlay = Image.open("overlay.png").convert("RGBA")
5
6# Set uniform alpha (0=transparent, 255=opaque)
7r, g, b, a = overlay.split()
8a = a.point(lambda x: int(x * 0.3)) # 30% opacity
9overlay = Image.merge("RGBA", (r, g, b, a))
10
11temp = Image.new("RGBA", background.size, (0, 0, 0, 0))
12temp.paste(overlay, (0, 0))
13result = Image.alpha_composite(background, temp)
14result.save("result.png")
Tiling an Overlay (Watermark Pattern)
1from PIL import Image
2
3background = Image.open("document.jpg").convert("RGBA")
4watermark = Image.open("watermark.png").convert("RGBA")
5
6# Reduce opacity
7r, g, b, a = watermark.split()
8a = a.point(lambda x: int(x * 0.15))
9watermark = Image.merge("RGBA", (r, g, b, a))
10
11# Tile across the entire image
12temp = Image.new("RGBA", background.size, (0, 0, 0, 0))
13for x in range(0, background.width, watermark.width + 50):
14 for y in range(0, background.height, watermark.height + 50):
15 temp.paste(watermark, (x, y), mask=watermark)
16
17result = Image.alpha_composite(background, temp)
18result.save("watermarked.png")
Batch Processing
1from PIL import Image
2from pathlib import Path
3
4def add_watermark(input_path, output_path, watermark_path, position="bottom-right", opacity=0.3):
5 background = Image.open(input_path).convert("RGBA")
6 watermark = Image.open(watermark_path).convert("RGBA")
7
8 # Scale watermark
9 scale = min(background.width, background.height) // 6
10 ratio = watermark.height / watermark.width
11 watermark = watermark.resize((scale, int(scale * ratio)), Image.LANCZOS)
12
13 # Adjust opacity
14 r, g, b, a = watermark.split()
15 a = a.point(lambda x: int(x * opacity))
16 watermark = Image.merge("RGBA", (r, g, b, a))
17
18 # Position
19 pad = 20
20 positions = {
21 "bottom-right": (background.width - watermark.width - pad,
22 background.height - watermark.height - pad),
23 "bottom-left": (pad, background.height - watermark.height - pad),
24 "top-right": (background.width - watermark.width - pad, pad),
25 "center": ((background.width - watermark.width) // 2,
26 (background.height - watermark.height) // 2),
27 }
28 pos = positions.get(position, positions["bottom-right"])
29
30 temp = Image.new("RGBA", background.size, (0, 0, 0, 0))
31 temp.paste(watermark, pos)
32 result = Image.alpha_composite(background, temp)
33
34 # Convert back to RGB for JPEG output
35 if output_path.suffix.lower() in (".jpg", ".jpeg"):
36 result = result.convert("RGB")
37 result.save(output_path)
38
39# Process all images in a directory
40for img_path in Path("photos/").glob("*.jpg"):
41 add_watermark(img_path, Path("output") / img_path.name, "logo.png")
Common Pitfalls
Forgetting to convert to RGBA: Image.open("photo.jpg") opens as RGB (no alpha channel). Both the background and overlay must be converted to RGBA with .convert("RGBA") before compositing, otherwise alpha_composite() raises a ValueError.
Not passing the mask parameter to paste(): background.paste(overlay, (x, y)) without mask=overlay replaces pixels entirely, making transparent areas appear as black or white rectangles instead of showing the background through.
Overlay larger than background: Pasting an overlay that extends beyond the background boundaries silently clips the overlay. Check sizes with overlay.size and resize or reposition if needed.
Saving RGBA to JPEG format: JPEG does not support transparency. Saving an RGBA image as JPEG either raises an error or drops the alpha channel. Convert to RGB with .convert("RGB") before saving as JPEG, or save as PNG to preserve transparency.
Using paste() instead of alpha_composite() for semi-transparent overlays: paste() with a mask treats each pixel as either transparent or opaque (binary mask). For smooth semi-transparent blending (like glass effects or gradient overlays), use alpha_composite() which properly handles alpha values between 0 and 255.
Summary
Use Image.paste(overlay, position, mask=overlay) for simple overlay with transparency
Use Image.alpha_composite(background, temp) for correct semi-transparent blending
Both images must be in RGBA mode — convert with .convert("RGBA")
Adjust overlay opacity by modifying the alpha channel with ImageEnhance.Brightness or point()
Convert to RGB before saving as JPEG since JPEG does not support transparency
Use Image.LANCZOS for high-quality resizing of overlays