Python怎么给图片添加文字或图片水印
详细讲解在Python中使用Pillow库为图片添加文字水印与图片水印的方法,涵盖RGBA透明图层合成、坐标计算、字体加载与批量处理技巧。
Image.alpha_composite 完成逐像素的双图层混合。这种处理方式能够确保半透明边缘平滑过渡、杜绝毛刺黑边,并保护原始底图不受破损。
一、开发环境准备与核心依赖
处理图片水印无需安装庞大的计算机视觉库(如 OpenCV),标准 Python 环境下只需安装轻量且功能全面的 Pillow 即可:
# 在终端或虚拟环境中安装最新版 Pillow
pip install pillow
在 Python 代码中,主要依赖以下几个核心模块与类:
- Image:负责图像文件的载入、尺寸获取、格式与色彩模式转换(RGB、RGBA)、以及高精度的图层混合(
alpha_composite)。 - ImageDraw:提供在图像画布上绘制几何图形与排版文字的方法(
Draw.text与包围盒计算Draw.textbbox)。 - ImageFont:负责加载系统 TrueType(.ttf)或 OpenType(.otf)字体文件,支持精确指定像素字号。
- ImageOps:提供
exif_transpose工具,专门用于依据 EXIF 元数据自动校正手机或单反所拍摄图片的翻转角度。
二、Pillow 处理水印的核心流程与工作原理
很多初学者容易犯的一个错误,是直接打开一张普通的 JPG 格式图片,在其上调用 ImageDraw.Draw(img).text(...) 试图画出半透明文字。这种做法通常会导致两个问题:要么指定的透明度参数(Alpha 通道)被直接忽略,文字显示为生硬的纯色;要么在抗锯齿过渡边缘生成一圈黑色杂边。
其根本原因在于:普通 JPG 图片属于 RGB 三通道模式,没有记录透明度的第四个通道(Alpha 通道)。正确的图像水印处理流程包含标准六个阶段:
- 读取原图与朝向校正:使用
Image.open()读取原图,并立即使用ImageOps.exif_transpose()解析相机方向标记,避免原图在后续操作中发生倒置。 - 转换色彩模式为 RGBA:执行
base_img.convert('RGBA'),将原图提升为 4 通道,为透明度叠加做好准备。 - 创建全透明独立画布:使用
Image.new('RGBA', base_img.size, (0, 0, 0, 0))创建一张尺寸与底图完全相等的纯透明图层。这一步实现了原图与水印的内容隔离。 - 绘制或放置水印内容:在全透明画布上,通过
ImageDraw.Draw绘制带有 Alpha 透明度(例如 Alpha=120,约 47% 不透明度)的文字,或将预先调整好透明度的 Logo 图片贴在目标坐标。 - 执行双图层 Alpha 混合:调用
Image.alpha_composite(base_img, watermark_layer)。Pillow 会通过底层的 C 语言加速代码,对两个图层的像素执行精确的透明度加权融合。 - 转回目标格式并存储:若目标文件为 JPEG/JPG 格式,必须先调用
final_img.convert('RGB')剥离 Alpha 通道,再执行保存;若为 PNG,则可直接保留透明通道输出。
三、添加文字水印:从基础到半透明定位
实现文字水印时,有两个关键细节直接决定了水印的美观度与跨平台稳定性:一是字体的正确加载,二是坐标锚点与边距的动态计算。
3.1 字体加载与中文字体支持
Pillow 默认的 ImageFont.load_default() 只内置了一套极小的英文字符位图,不仅无法调整字号大小,而且在遇到中文字符时会出现方块或乱码。因此,必须通过 ImageFont.truetype(font_path, font_size) 显式指定系统中存在的 TrueType/OpenType 字体文件(例如 Windows 的 msyh.ttc、macOS 的 PingFang.ttc,或项目内置的开源字体 SourceHanSans.ttf)。
3.2 坐标计算与九宫格定位
为了让水印自然地呈现在图片的右下角、居中或左上角,必须根据原图的宽高(W, H)以及文字在当前字体下的实际渲染宽高(w, h)推导坐标。
font.getsize() 和 draw.textsize() 方法,取而代之的是更加严谨的 draw.textbbox((0, 0), text, font=font)。它返回一个四元组 (left, top, right, bottom),文字宽度为 right - left,高度为 bottom - top。
3.3 完整文字水印代码示例
以下为一个封装完整的 Python 函数,支持自定义文字内容、透明度(0~255)、九宫格对齐位置,并包含字号相对底图的自适应机制:
from PIL import Image, ImageDraw, ImageFont, ImageOps
import os
def add_text_watermark(
input_path: str,
output_path: str,
text: str,
font_path: str = None,
opacity: int = 120, # 透明度范围 0(全透) ~ 255(不透明)
position: str = "bottom-right", # 可选: center, bottom-right, bottom-left, top-right
margin_ratio: float = 0.03, # 边距占底图宽度的比例
font_scale: float = 0.04 # 字号占底图宽度的比例
):
"""
为图片添加高质量半透明文字水印
"""
# 1. 打开原图并校正 EXIF 旋转
with Image.open(input_path) as raw_img:
base_img = ImageOps.exif_transpose(raw_img).convert("RGBA")
width, height = base_img.size
# 2. 动态计算字号与内边距,确保大小图水印比例一致
font_size = max(16, int(width * font_scale))
margin = max(15, int(width * margin_ratio))
# 3. 载入字体,提供回退策略
font = None
candidates = [font_path] if font_path else []
candidates.extend([
"/System/Library/Fonts/PingFang.ttc", # macOS 默认中文字体
"C:\Windows\Fonts\msyh.ttc", # Windows 微软雅黑
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" # Linux 常见无衬线字体
])
for p in candidates:
if p and os.path.exists(p):
try:
font = ImageFont.truetype(p, font_size)
break
except Exception:
continue
if font is None:
font = ImageFont.load_default()
# 4. 创建与底图等大的独立透明画布
txt_layer = Image.new("RGBA", (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(txt_layer)
# 5. 精确测量文本的包围盒尺寸
bbox = draw.textbbox((0, 0), text, font=font)
txt_w = bbox[2] - bbox[0]
txt_h = bbox[3] - bbox[1]
# 6. 计算九宫格定位坐标
if position == "center":
x = (width - txt_w) // 2
y = (height - txt_h) // 2
elif position == "bottom-right":
x = width - txt_w - margin
y = height - txt_h - margin
elif position == "bottom-left":
x = margin
y = height - txt_h - margin
elif position == "top-right":
x = width - txt_w - margin
y = margin
else: # top-left
x = margin
y = margin
# 7. 在透明图层上绘制带透明度的纯白文字(可依需附带微弱投影)
# 填充色格式为 (R, G, B, Alpha)
text_color = (255, 255, 255, opacity)
draw.text((x, y), text, font=font, fill=text_color)
# 8. 图层混合并导出
watermarked = Image.alpha_composite(base_img, txt_layer)
# 判断保存格式:JPEG 不支持 Alpha,需先转 RGB
ext = os.path.splitext(output_path)[1].lower()
if ext in [".jpg", ".jpeg"]:
final_output = watermarked.convert("RGB")
final_output.save(output_path, "JPEG", quality=95)
else:
watermarked.save(output_path)
print(f"文字水印已成功添加并保存至: /Users/kukukuwl/Library/Application Support/Antigravity++/BusinessWorkspaces/B98DAABF-FBE0-4CC0-8492-798944C68276/ku-aifram-ipython-com-svg/project/instances/ipython-com-svg/.state/tasks/502dcf8d-28cb-496e-9086-9e2b86b576e9/svg-1-body/body.html")
# 调用示例:
# add_text_watermark("input.jpg", "output_watermarked.jpg", text="© 摄影师小明作品")
四、添加图片水印:Logo 缩放、透明度调整与图层融合
当需要将企业品牌图标或特定 Logo 添加到原图时,其操作要求更高:Logo 本身可能已经拥有透明背景,但也可能需要整体降低不透明度(例如让 Logo 呈现 40% 的微弱印记),以免喧宾夺主。
4.1 Logo 尺寸缩放与 Alpha 通道重分配
通常情况下,作为水印的 Logo 图标尺寸不能直接固定为绝对像素。如果原图是一张 4000x3000 的单反照片,而 Logo 只有 200x50 像素,贴上去会几乎难以辨认;反之如果原图是一张 600x400 的缩略图,Logo 又会占据整张画面。因此,应按照底图宽度的百分比(通常为 15%~25%)等比例缩放 Logo。
修改图片水印本身的透明度,不能直接通过颜色值修改,而是需要获取其 Alpha 通道矩阵,将每个像素的原有透明度乘以一个缩放系数(alpha_factor = target_opacity / 255),使原有半透明边缘保持平滑衰减。
4.2 完整图片水印代码示例
以下代码展示了如何读取一张 PNG 透明格式的 Logo,按底图宽度等比例缩放、重新计算整体 Alpha 通道透明度,并精确叠加到底图指定位置:
def add_image_watermark(
base_image_path: str,
logo_image_path: str,
output_path: str,
scale_ratio: float = 0.18, # Logo 宽度占原图宽度的比例
opacity: int = 150, # Logo 透明度 (0~255)
position: str = "bottom-right",
margin_ratio: float = 0.03
):
"""
为图片添加高质量半透明 Logo 图片水印
"""
# 1. 载入底图与 Logo 图
with Image.open(base_image_path) as raw_base:
base_img = ImageOps.exif_transpose(raw_base).convert("RGBA")
with Image.open(logo_image_path) as raw_logo:
logo_img = raw_logo.convert("RGBA")
base_w, base_h = base_img.size
# 2. 等比例调整 Logo 尺寸
target_logo_w = max(40, int(base_w * scale_ratio))
logo_aspect = logo_img.height / logo_img.width
target_logo_h = int(target_logo_w * logo_aspect)
# 使用 Resampling.LANCZOS 获得最高品质的高清缩放
logo_resized = logo_img.resize((target_logo_w, target_logo_h), Image.Resampling.LANCZOS)
# 3. 调节 Logo 的透明度:修改 Alpha 通道
# 分离通道: r, g, b, a
r, g, b, a = logo_resized.split()
# 将现有 Alpha 矩阵按 opacity / 255 比例缩放
alpha_factor = opacity / 255.0
a = a.point(lambda p: int(p * alpha_factor))
# 重新合并通道
logo_adjusted = Image.merge("RGBA", (r, g, b, a))
# 4. 计算粘贴坐标
margin = max(15, int(base_w * margin_ratio))
if position == "bottom-right":
pos_x = base_w - target_logo_w - margin
pos_y = base_h - target_logo_h - margin
elif position == "center":
pos_x = (base_w - target_logo_w) // 2
pos_y = (base_h - target_logo_h) // 2
elif position == "bottom-left":
pos_x = margin
pos_y = base_h - target_logo_h - margin
else: # top-right
pos_x = base_w - target_logo_w - margin
pos_y = margin
# 5. 构建与底图等大的透明图层,将调整好的 Logo 粘贴在指定位置
watermark_layer = Image.new("RGBA", (base_w, base_h), (0, 0, 0, 0))
# paste 第三个参数传 logo_adjusted 作为 mask,保证其自身原本的透明区域不被涂黑
watermark_layer.paste(logo_adjusted, (pos_x, pos_y), mask=logo_adjusted)
# 6. 利用 alpha_composite 双层合成
result_img = Image.alpha_composite(base_img, watermark_layer)
# 7. 导出处理
ext = os.path.splitext(output_path)[1].lower()
if ext in [".jpg", ".jpeg"]:
final_output = result_img.convert("RGB")
final_output.save(output_path, "JPEG", quality=95)
else:
result_img.save(output_path)
print(f"图片水印已成功合成至: /Users/kukukuwl/Library/Application Support/Antigravity++/BusinessWorkspaces/B98DAABF-FBE0-4CC0-8492-798944C68276/ku-aifram-ipython-com-svg/project/instances/ipython-com-svg/.state/tasks/502dcf8d-28cb-496e-9086-9e2b86b576e9/svg-1-body/body.html")
# 调用示例:
# add_image_watermark("photo.jpg", "logo.png", "watermarked_photo.jpg")
五、高阶需求:全屏倾斜铺满平铺水印
在证件照上传验证、版权图库展示或防止屏幕截图外泄等场景中,角落的单一水印很容易被恶意裁切掉。此时最有效的方案是制作全屏倾斜 30°~45° 的密集平铺文字水印。
平铺水印的实现核心包括三步:
- 先在较小的局部画布上绘制一行文字,并调用
small_canvas.rotate(30, expand=True)将其旋转 30 度; - 建立一个覆盖全图的网格步长(步长通常为旋转后文字块宽度的 1.5~2 倍);
- 在双重
for循环中遍历整张底图的 X、Y 坐标网格,将旋转文字贴入全透明大图层,最后一次性执行alpha_composite。
def add_tiled_watermark(
input_path: str,
output_path: str,
text: str,
font_path: str = None,
angle: int = 30,
opacity: int = 40, # 平铺水印建议透明度较低(30~50),避免破坏主体可读性
step_x_ratio: float = 0.28,
step_y_ratio: float = 0.22
):
"""
全屏铺满倾斜文字水印
"""
with Image.open(input_path) as raw_img:
base_img = ImageOps.exif_transpose(raw_img).convert("RGBA")
W, H = base_img.size
font_size = max(18, int(W * 0.028))
# 加载系统字体
font = None
if font_path and os.path.exists(font_path):
font = ImageFont.truetype(font_path, font_size)
else:
font = ImageFont.load_default()
# 1. 在临时小画布上绘制单体文字
dummy_draw = ImageDraw.Draw(base_img)
bbox = dummy_draw.textbbox((0, 0), text, font=font)
tw = bbox[2] - bbox[0] + 10
th = bbox[3] - bbox[1] + 10
single_txt = Image.new("RGBA", (tw, th), (0, 0, 0, 0))
d = ImageDraw.Draw(single_txt)
d.text((5, 5), text, font=font, fill=(255, 255, 255, opacity))
# 2. 旋转单体文字,expand=True 会自适应放大画布容纳对角线
rotated_txt = single_txt.rotate(angle, expand=True, resample=Image.Resampling.BICUBIC)
rw, rh = rotated_txt.size
# 3. 建立等大全透明图层并网格平铺
tiled_layer = Image.new("RGBA", (W, H), (0, 0, 0, 0))
step_x = max(rw + 40, int(W * step_x_ratio))
step_y = max(rh + 40, int(H * step_y_ratio))
for y in range(-rh, H + rh, step_y):
for x in range(-rw, W + rw, step_x):
tiled_layer.paste(rotated_txt, (x, y), mask=rotated_txt)
# 4. 图层合成与存储
result = Image.alpha_composite(base_img, tiled_layer)
ext = os.path.splitext(output_path)[1].lower()
if ext in [".jpg", ".jpeg"]:
result.convert("RGB").save(output_path, "JPEG", quality=95)
else:
result.save(output_path)
六、生产环境中容易忽视的四个常见问题与解决方案
在将水印脚本部署到后端服务器或批处理流水线时,常常会遇到几种典型异常,以下是排查与规避方法:
1. 保存 JPEG 时报 OSError: cannot write mode RGBA as JPEG
原因:JPEG 图像标准本身不支持 Alpha 透明通道。经过 alpha_composite 处理后的图像对象模式为 RGBA,若直接 .save("out.jpg"),Pillow 底层的 JPEG 编码器会抛出此错误。
解决方法:在调用 save 之前,执行 result.convert("RGB")。此时 Pillow 会自动将剩余透明区域与默认背景混合并移除 Alpha 通道,从而顺利保存。
2. 中文文字渲染显示为方块或乱码
原因:调用了 ImageFont.load_default() 或传入了仅包含 ASCII 字符集的英文字体(如部分系统默认的 Courier/Arial)。
解决方法:必须下载并打包一份开源中文字体(如思源黑体 SourceHanSansSC-Regular.otf 或阿里巴巴普惠体)到项目资源目录中,使用绝对路径或相对路径加载,杜绝依赖不可控的宿主机操作系统字体环境。
3. 照片添加水印后方向颠倒或横竖反转
原因:现代智能手机与数码相机拍摄照片时,图像的实际像素矩阵可能始终以传感器物理方向存储,真正的水平/竖直朝向记录在 EXIF 的 Orientation 标签中。如果直接读图,读取的是未翻转的原始像素,但保存时若丢失了 EXIF,照片看起来就会旋转了 90 度或 180 度。
解决方法:使用 ImageOps.exif_transpose(raw_img)。该函数会自动检测并按照 EXIF 标记将像素点物理旋转到正确的观感方向,后续水印坐标计算将完全基于正确的朝向。
4. 固定像素水印在不同尺寸图片上忽大忽小
原因:手机实拍原图尺寸常为 4000x3000 像素,而网页截图可能仅有 800x600 像素。如果硬编码 font_size = 24,在 4K 图上只有微小的一个点。
解决方法:摒弃绝对像素思维,全部采用基准宽度的相对比例计算,如 font_size = max(16, int(width * 0.035)),并对最小尺寸设置保底值(max(16, ...)),保证不同分辨率下的视觉呈现一致。
七、方法选型对比与总结
针对不同的业务应用场景,文字水印与图片水印各有千秋。下表梳理了两种方案在实际应用中的核心考量维度:
| 对比维度 | 文字水印 (Text Watermark) | 图片 Logo 水印 (Image Logo) |
|---|---|---|
| 适用场景 | 摄影签名、动态时间戳、用户名防截图追溯 | 企业品牌形象展示、官方机构资质认证、防伪印章 |
| 素材依赖 | 只需 .ttf 字体文件,不依赖额外图片资源 | 必须提供高分辨率、无杂色的透明背景 PNG 文件 |
| 动态灵活性 | 极高(文字内容、日期、文字长度可随时动态变动) | 较低(通常为固定的矢量导出位图) |
| 核心实现 API | ImageDraw.Draw.text() + textbbox() |
Image.resize() + split() 调节透明度通道 |
| 关键注意事项 | 避免默认字体乱码;注意换行与边界裁剪 | 保持纵横比缩放;严禁在缩放时产生噪点与黑边 |
总结而言,在 Python 中给图片添加水印的成熟工程方法,是建立在独立图层与 Alpha 通道混合机制之上的。遵循“读取校正朝向 → 模式转为 RGBA → 构建全透明独立画布 → 动态比例计算并绘制水印 → alpha_composite 混合 → 转回 RGB 导出”这一标准六步法,不仅能兼顾文字水印与图片 Logo 水印的灵活调用,更能彻底避开黑边、方块乱码与 JPEG 保存报错等常见陷阱,实现稳定、高品质的图像自动化处理。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









