Watermarks, overlays, stickers and image collages are all compositing problems with easy-to-miss alpha rules.
base.paste(other, box, mask=None) copies other into base in place. box can be a 2-tuple (x, y) for the top-left corner or a 4-tuple region. Without a mask, transparent pixels of other are copied as-is, overwriting the base, so a PNG logo pasted onto a photo gets an opaque rectangle around it.
Pass a mask: an L or 1 image (or the A band of an RGBA image) where 255 means copy, 0 means keep the base and values in between blend. Using the image itself as mask, base.paste(logo, (x, y), logo), makes Pillow use its alpha band.
Image.alpha_composite(dst, src) does true Porter-Duff "over" compositing: both images must be RGBA and the same size, and the result's alpha is computed correctly. That matters when the base itself has transparency, where paste-with-mask gets alpha wrong. dst.alpha_composite(src, dest=(x, y)) places a smaller src at an offset in place.
Other tools:
Image.blend(a, b, alpha): linear mix of two same-size, same-mode images (0.0gives a,1.0gives b)Image.composite(a, b, mask): pick per pixel fromaorbvia a maskim.putalpha(band_or_value): add or replace the alpha band
A common watermark recipe: draw text on a transparent RGBA layer with a partly transparent fill, then alpha_composite it onto the photo converted to RGBA.
Example
from PIL import Image, ImageDraw
photo = Image.new("RGB", (200, 100), (0, 0, 255))
logo = Image.new("RGBA", (40, 40), (0, 0, 0, 0)) # transparent
ImageDraw.Draw(logo).ellipse([0, 0, 39, 39], fill=(255, 255, 0, 255))
a = photo.copy(); a.paste(logo, (10, 10)) # no mask
b = photo.copy(); b.paste(logo, (10, 10), logo) # alpha as mask
print(a.getpixel((11, 11)), b.getpixel((11, 11))) # (0, 0, 0) (0, 0, 255)
layer = Image.new("RGBA", photo.size, (0, 0, 0, 0))
ImageDraw.Draw(layer).rectangle([100, 0, 199, 99], fill=(255, 255, 255, 128))
marked = Image.alpha_composite(photo.convert("RGBA"), layer)
print(marked.getpixel((150, 50))) # (128, 128, 255, 255)
fade = Image.blend(photo, Image.new("RGB", (200, 100), "red"), 0.25)
print(fade.getpixel((0, 0))) # (63, 0, 191)Remember
pasteworks in place; pass a mask (often the image's own alpha) to respect transparencyalpha_compositedoes correct 'over' blending of two RGBA imagesblendcross-fades,compositepicks per pixel via a mask- Draw watermarks on a transparent layer, then composite
Watch out for
- Pasting a transparent PNG without a mask and getting a black or solid box around it.