Python批量给图片添加文字或图片水印脚本
使用Python的Pillow库编写批量给图片添加文字与图片水印的高效脚本,详细讲解RGBA图层透明混合原理、九宫格坐标计算、自适应缩放与Exif方向校正等实战要点。
在网站运营、内容发布或数字资产归档过程中,为原始图像批量附加专属标识是一项高频需求。使用 Python 进行批量水印处理,核心在于借助成熟的 Pillow(PIL 图像处理库)实现底图解析、Alpha 透明通道混合与自动化目录遍历。本文将提供一套兼顾文字水印与图片水印的生产级脚本,并系统拆解坐标对齐、透明通道渲染、Exif 旋转校正与自适应缩放等关键实现细节。
一、整体处理架构与执行流程
批量水印脚本的核心逻辑并非简单地在原图上涂抹像素,而是建立一个标准化的图像流水线。完整的作业流程包括图片文件筛选、元数据方向矫正、临时 RGBA 透明图层创建、水印绘制与合成、以及最终的色彩空间还原与存储。
二、环境准备与依赖安装
处理图像需要安装 Pillow 库。Pillow 是 Python 原生 PIL 的现代分支,支持绝大多数通用图像格式的读写与矢量绘制:
pip install Pillow
同时需要注意中文字体文件。Windows 系统自带 simhei.ttf(黑体)或 msyh.ttc(微软雅黑);macOS 提供 PingFang.ttc;Linux 服务器推荐提前安装开源字体包,例如 fonts-noto-cjk 或提供本地 .ttf 文件路径,以避免中文渲染为乱码或矩形方块。
三、核心原理:RGBA 模式与透明图层混合
初学者容易直接在原始图像对象上调用 ImageDraw.Draw(img).text()。这种方式存在严重缺陷:如果原图是 RGB 格式,无法为其赋予半透明 Alpha 通道;即使原图是 RGBA,直接在原像素上覆写文字也会导致抗锯齿边缘产生杂色边缘。
标准解法是采用双图层混合策略:创建与底图尺寸完全相同的纯透明图层(全 0 的 RGBA 缓冲区),在该图层指定位置渲染文字或粘贴图片水印,最后调用 Image.alpha_composite() 进行无损 Alpha 合成。
四、坐标计算与九宫格对齐定位
批量处理面临的最大挑战在于输入图片的分辨率各不相同。硬编码坐标(例如 (100, 100))在 4K 大图上会显得微不足道,在小缩略图上则可能完全溢出。因此需要实现通用的九宫格定位算法与边距控制。
五、完整代码实现
下面是一个封装完备的 Python 脚本,支持文字水印与图片水印两种模式,具备 Exif 方向自动翻转、自适应缩放、多格式批量输出能力。
import os
import sys
from PIL import Image, ImageDraw, ImageFont, ImageOps
def calculate_position(base_size, mark_size, position="bottom-right", margin=20):
"""
计算水印在底图上的左上角放置坐标 (x, y)
"""
bw, bh = base_size
mw, mh = mark_size
pos_map = {
"top-left": (margin, margin),
"top-center": ((bw - mw) // 2, margin),
"top-right": (bw - mw - margin, margin),
"center-left": (margin, (bh - mh) // 2),
"center": ((bw - mw) // 2, (bh - mh) // 2),
"center-right": (bw - mw - margin, (bh - mh) // 2),
"bottom-left": (margin, bh - mh - margin),
"bottom-center": ((bw - mw) // 2, bh - mh - margin),
"bottom-right": (bw - mw - margin, bh - mh - margin)
}
return pos_map.get(position, pos_map["bottom-right"])
def add_text_watermark(image, text, font_path, font_size=36, color=(255, 255, 255, 128), position="bottom-right", margin=20):
"""
给单张 PIL 图像添加文字水印(返回 RGBA 模式图像)
"""
# 确保原图进入 RGBA 空间
base = image.convert("RGBA")
# 加载指定字体
try:
font = ImageFont.truetype(font_path, font_size)
except IOError:
# 回退至系统默认点阵字体(注意:默认字体不支持指定尺寸与中文)
font = ImageFont.load_default()
# 获取文本渲染外接矩形 (left, top, right, bottom)
bbox = font.getbbox(text)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
# 创建相同尺寸的全透明叠加图层
txt_layer = Image.new("RGBA", base.size, (255, 255, 255, 0))
draw = ImageDraw.Draw(txt_layer)
# 确定坐标并绘制文字
pos = calculate_position(base.size, (text_w, text_h), position=position, margin=margin)
# 补偿 bbox 左上角偏移量
actual_pos = (pos[0] - bbox[0], pos[1] - bbox[1])
draw.text(actual_pos, text, font=font, fill=color)
# 图层混合
return Image.alpha_composite(base, txt_layer)
def add_image_watermark(image, watermark_path, scale_ratio=0.15, opacity=0.8, position="bottom-right", margin=20):
"""
给单张 PIL 图像添加图片水印
"""
base = image.convert("RGBA")
wm = Image.open(watermark_path).convert("RGBA")
# 根据底图宽度按比例缩放水印图片
bw, bh = base.size
target_wm_w = max(int(bw * scale_ratio), 40)
w_ratio = target_wm_w / float(wm.size[0])
target_wm_h = int(float(wm.size[1]) * w_ratio)
wm = wm.resize((target_wm_w, target_wm_h), Image.Resampling.LANCZOS)
# 调整水印整体不透明度
if opacity < 1.0:
r, g, b, alpha = wm.split()
alpha = alpha.point(lambda p: int(p * opacity))
wm.putalpha(alpha)
# 创建透明合成层并粘贴水印
wm_layer = Image.new("RGBA", base.size, (0, 0, 0, 0))
pos = calculate_position(base.size, wm.size, position=position, margin=margin)
wm_layer.paste(wm, pos, mask=wm)
return Image.alpha_composite(base, wm_layer)
def batch_watermark(input_dir, output_dir, mode="text", **kwargs):
"""
批量扫描目录并处理图片
"""
valid_exts = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
if not os.path.exists(output_dir):
os.makedirs(output_dir, exist_ok=True)
success_count = 0
fail_count = 0
for root, _, files in os.walk(input_dir):
for file in files:
ext = os.path.splitext(file)[1].lower()
if ext not in valid_exts:
continue
src_path = os.path.join(root, file)
# 计算对应的相对路径并保持输出目录结构一致
rel_dir = os.path.relpath(root, input_dir)
target_sub_dir = os.path.join(output_dir, rel_dir)
os.makedirs(target_sub_dir, exist_ok=True)
dst_path = os.path.join(target_sub_dir, file)
try:
with Image.open(src_path) as raw_img:
# 关键步骤:依据 Exif 旋转标记矫正物理方向
img = ImageOps.exif_transpose(raw_img)
if mode == "text":
result = add_text_watermark(
img,
text=kwargs.get("text", "Official Watermark"),
font_path=kwargs.get("font_path", "msyh.ttc"),
font_size=kwargs.get("font_size", 36),
color=kwargs.get("color", (255, 255, 255, 128)),
position=kwargs.get("position", "bottom-right"),
margin=kwargs.get("margin", 25)
)
else:
result = add_image_watermark(
img,
watermark_path=kwargs.get("watermark_path"),
scale_ratio=kwargs.get("scale_ratio", 0.15),
opacity=kwargs.get("opacity", 0.75),
position=kwargs.get("position", "bottom-right"),
margin=kwargs.get("margin", 25)
)
# 根据目标格式保存
if ext in {".jpg", ".jpeg"}:
# JPEG 不支持 Alpha 通道,须转换回 RGB 并指定压缩质量
result.convert("RGB").save(dst_path, "JPEG", quality=92)
else:
result.save(dst_path, quality=95)
success_count += 1
except Exception as err:
print(f"[错误] 处理失败 {src_path}: {err}", file=sys.stderr)
fail_count += 1
print(f"批量任务完成: 成功 {success_count} 张, 失败 {fail_count} 张。")
if __name__ == "__main__":
# 使用示例:批量添加文字水印
# batch_watermark(
# input_dir="./images_raw",
# output_dir="./images_watermarked",
# mode="text",
# text="© 2026 技术实战专栏",
# font_path="msyh.ttc",
# font_size=40,
# color=(255, 255, 255, 140),
# position="bottom-right"
# )
# 使用示例:批量添加图片水印
# batch_watermark(
# input_dir="./images_raw",
# output_dir="./images_watermarked",
# mode="image",
# watermark_path="./logo.png",
# scale_ratio=0.18,
# opacity=0.7,
# position="bottom-right"
# )
pass
六、生产环境常见隐患与排查
- 照片被“意外颠倒”:智能手机拍摄的照片通常带有 Exif 方向标记(Orientation)。如果不调用
ImageOps.exif_transpose(),读取出来的像素仍是传感器物理排布方向,添加完水印保存后可能导致照片颠倒或水印位置错乱。 - JPEG 导出报错
OSError: cannot write mode RGBA as JPEG:JPEG 规范不包含透明度通道。保存为.jpg时必须先调用.convert("RGB")。 - 中文文字显示为方块豆腐块:未指定中文字体或使用的
.ttf文件缺少中文字符集。请务必使用系统包含完整 CJK 字符集的字体文件。 - 小图上的水印过度溢出:如果字体固定使用 48px,而原图尺寸仅有 80x80,水印将严重溢出。建议在正式执行前根据
min(bw, bh)动态缩放font_size。
七、核心技术决策对比与总结
对文字水印与图片水印两种场景的常见配置及关键考量点进行归纳,以便在不同业务场景中做出适配:
通过本文介绍的双图层 Alpha 混合机制、九宫格坐标映射与目录遍历管道,即可构建稳健可靠的自动化批量图片水印工具,满足生产环境下各种尺寸与格式图片的打标归档诉求。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









