Python Pillow添加文字水印与绘制图形的方法
本文系统讲解使用 Python Pillow (PIL) 库实现文字水印与矢量图形绘制的方法。涵盖坐标体系、ImageDraw 接口规范、透明图层与 Alpha 混合原理、单点及旋转平铺水印实战与生产避坑注意事项。
ImageDraw 模块的图元绘制接口,以及通过构建独立的 RGBA 透明图层并配合 Image.alpha_composite 完成像素级透明度融合。本文从底层坐标机制讲起,详细演示基础几何图形绘制、半透明单角文字水印、倾斜平铺防盗水印的完整实现步骤,并对高频报错提供切实可行的排查方案。
一、运行环境与核心对象体系
Pillow 库在 Python 社区中继承自经典的 PIL(Python Imaging Library),提供了跨平台的图像加载、像素操作与格式转换能力。在开始编写代码前,确保本地 Python 环境已安装 Pillow(建议使用 10.0 及以上版本):
pip install pillow
在 Pillow 中实现画图与文字水印,主要依赖三个核心类:Image(负责图像数据的生命周期管理与色彩模式转换)、ImageDraw.Draw(提供在图像表面绘制线条、形状与文本的画笔上下文接口)以及 ImageFont.truetype(负责载入矢量字体并计算字形度量)。
Pillow 的像素直角坐标系以图像的左上角为原点 (0, 0),向右为 X 轴正方向,向下为 Y 轴正方向。图像的尺寸表示为二元组 (width, height),水平方向的最大有效下标为 width - 1,垂直方向为 height - 1。
二、图层叠加与 Alpha 混合原理解析
许多初学者在为图片添加文字水印时,习惯直接打开底图并调用 draw.text() 传入带有透明度的 RGBA 颜色元组,例如 (255, 255, 255, 128)。然而在实际运行中,往往会发现文字呈现为完全不透明的纯色块,或者产生突兀的锯齿边框。
造成这种现象的根源在于图像的色彩通道模式。标准 JPEG 照片采用 RGB 模式(仅有红、绿、蓝三个通道,无透明度通道),此时如果直接在底图上绘图,Pillow 无法直接就地改变底层像素的不透明度,Alpha 数值会被强制忽略或硬切断。
解决此问题的工业级标准方案是分层绘制与像素加权融合:
- 将原始图像转换为
RGBA模式; - 通过
Image.new("RGBA", base.size, (255, 255, 255, 0))创建一张与底图尺寸完全相同、背景完全透明(Alpha = 0)的独立图层; - 在独立透明图层上,使用带有 Alpha 值的颜色(例如
(255, 255, 255, 120))绘制文本或矢量图形; - 调用
Image.alpha_composite(base_rgba, watermark_layer)进行像素级透明度融合,计算公式为:\(C_{out} = C_{fg} imes lpha + C_{bg} imes (1 - lpha)\); - 若需要输出为 JPEG 格式,在保存前调用
.convert("RGB")剥离透明通道后再写入磁盘。
三、使用 ImageDraw 绘制基础与复合几何图形
在批量处理图像时,我们经常需要绘制引导线、标注框、聚焦圆或半透明遮罩背景。ImageDraw.Draw 提供了丰富的几何图形绘制方法。
下表整理了 ImageDraw 最常用的几何绘制接口及其核心参数:
| 方法名称 | 坐标传参格式 | 关键参数 | 用途说明 |
|---|---|---|---|
draw.line() |
[(x0, y0), (x1, y1), ...] |
fill, width, joint |
绘制连续线段或折线,支持线宽控制 |
draw.rectangle() |
[x0, y0, x1, y1] |
fill, outline, width |
以对角顶点绘制直角矩形,可单设填充与边框 |
draw.rounded_rectangle() |
[x0, y0, x1, y1] |
radius, fill, outline, width |
绘制圆角矩形,radius 控制四角平滑弧度 |
draw.ellipse() |
[x0, y0, x1, y1] |
fill, outline, width |
基于定界框绘制内切椭圆;宽高相等时为正圆 |
draw.polygon() |
[(x0, y0), (x1, y1), ...] |
fill, outline |
绘制任意多边形,末尾点自动与起始点闭合 |
下面是一个完整的 Python 示例,演示如何在一个新画布上绘制多种几何图形并导出:
from PIL import Image, ImageDraw
def draw_geometry_showcase(output_path="shapes_demo.png"):
# 1. 创建 600x400 的浅奶油白画布
canvas = Image.new("RGBA", (600, 400), "#FAF7F1")
draw = ImageDraw.Draw(canvas)
# 2. 绘制多段折线 (主色深灰蓝 #394957)
points = [(50, 80), (120, 40), (200, 100), (280, 50)]
draw.line(points, fill="#394957", width=3, joint="curve")
# 3. 绘制圆角矩形背景框 (带填充与边框)
rect_box = [50, 140, 280, 260]
draw.rounded_rectangle(rect_box, radius=12, fill="#EFE8D8", outline="#AD8B58", width=2)
# 4. 绘制正圆与内切椭圆 (中心定界法)
# 正圆: 定界框长宽均为 100 像素
draw.ellipse([340, 50, 440, 150], fill=None, outline="#394957", width=3)
# 扁椭圆
draw.ellipse([330, 180, 550, 260], fill="#AD8B58", outline="#394957", width=2)
# 5. 绘制三角形多边形
triangle = [(450, 300), (550, 380), (350, 380)]
draw.polygon(triangle, fill="#394957", outline="#AD8B58")
# 保存结果
canvas.save(output_path)
print(f"示例图形已成功导出至: {output_path}")
if __name__ == "__main__":
draw_geometry_showcase()
四、添加半透明文字水印的完整实现流程
为图像添加水印具有防盗图、注明版权归属、增加防伪认证等多重目的。实际生产中,最常用的两种水印形态为:单角固定文字水印(通常位于右下角)与全图倾斜平铺防盗水印。
1. 核心步骤:尺寸测量与位置定位
在 Pillow 中排版文字的关键在于测量渲染后的文本包围盒尺寸。需要特别注意:在旧版本中广泛使用的 draw.textsize() 方法在 Pillow 10.0.0 中已被彻底移除。
现代标准实现统一采用 draw.textbbox(),该方法接收初始坐标、文本内容与字体对象,返回一个四元组 (left, top, right, bottom)。文本的实际占用宽高计算方式如下:
# 获取文本的外接像素框
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
2. 实战实现:右下角半透明版权水印
以下代码演示了完整的单角半透明水印添加函数。脚本首先转换模式,创建透明图层,通过 textbbox 精确计算右下角边距偏移,最终完成像素融合:
import os
from PIL import Image, ImageDraw, ImageFont
def add_corner_watermark(image_path, output_path, text="© 2026 技术实战版权所有", font_path=None, font_size=28, margin=24, alpha=130):
# 1. 载入原始图像并转换为 RGBA
with Image.open(image_path) as base_img:
base_rgba = base_img.convert("RGBA")
img_w, img_h = base_rgba.size
# 2. 载入字体文件 (若未提供或不存在则使用回退字体)
try:
if font_path and os.path.exists(font_path):
font = ImageFont.truetype(font_path, font_size)
else:
# 尝试加载常见系统支持中文字体,若无则回退
font = ImageFont.truetype("Arial.ttf", font_size)
except Exception:
font = ImageFont.load_default()
# 3. 创建全透明水印图层 (Alpha = 0)
watermark_layer = Image.new("RGBA", (img_w, img_h), (255, 255, 255, 0))
draw = ImageDraw.Draw(watermark_layer)
# 4. 精确测算文字宽高并计算右下角坐标
bbox = draw.textbbox((0, 0), text, font=font)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
pos_x = img_w - text_w - margin
pos_y = img_h - text_h - margin
# 5. 在透明图层上绘制指定 Alpha 值的文字
# 填充色采用半透明白/暗金色调:RGBA 四元组
watermark_color = (255, 255, 255, alpha)
draw.text((pos_x, pos_y), text, font=font, fill=watermark_color)
# 6. 执行 Alpha 像素级混合
blended = Image.alpha_composite(base_rgba, watermark_layer)
# 7. 根据输出格式存储
if output_path.lower().endswith((".jpg", ".jpeg")):
# JPEG 不支持透明通道,导出前转换为 RGB
blended.convert("RGB").save(output_path, quality=95)
else:
blended.save(output_path)
print(f"水印添加完成,输出文件: {output_path}")
3. 进阶实战:全图旋转倾斜平铺防盗水印
对于合同文件扫描件、敏感数据凭据或图集防抓取,固定在角落的水印极易被裁剪篡改。此时必须使用倾斜 30°~45° 的全图平铺水印。
实现平铺水印的优雅技巧是:先创建一个贴合文字大小的小画布绘制单组文本,将其旋转指定角度,再通过行列步长循环,利用 paste() 带有透明通道遮罩将旋转后的文字块阵列印制到透明图层上,最后整体进行 alpha_composite。
def add_tiled_watermark(image_path, output_path, text="内部资料 · 严禁外传", font_path=None, font_size=24, angle=30, alpha=85, step_x=220, step_y=160):
with Image.open(image_path) as base_img:
base_rgba = base_img.convert("RGBA")
width, height = base_rgba.size
# 准备字体
try:
font = ImageFont.truetype(font_path, font_size) if font_path else ImageFont.load_default()
except Exception:
font = ImageFont.load_default()
# 测量单个文本尺寸
dummy_img = Image.new("RGBA", (1, 1))
dummy_draw = ImageDraw.Draw(dummy_img)
bbox = dummy_draw.textbbox((0, 0), text, font=font)
tw = bbox[2] - bbox[0]
th = bbox[3] - bbox[1]
# 创建单个文字贴片图层
pad = 20
txt_stamp = Image.new("RGBA", (tw + pad * 2, th + pad * 2), (255, 255, 255, 0))
stamp_draw = ImageDraw.Draw(txt_stamp)
stamp_draw.text((pad, pad), text, font=font, fill=(173, 139, 88, alpha)) # 暗金色半透明
# 旋转单个贴片 (expand=True 自动扩展边界保证旋转后不被裁剪)
rotated_stamp = txt_stamp.rotate(angle, expand=True, resample=Image.BICUBIC)
stamp_w, stamp_h = rotated_stamp.size
# 创建覆盖整张底图的透明大图层
watermark_layer = Image.new("RGBA", (width, height), (255, 255, 255, 0))
# 双重循环矩阵平铺 (从负步长开始以完全覆盖边缘)
for x in range(-stamp_w, width + stamp_w, step_x):
for y in range(-stamp_h, height + stamp_h, step_y):
# 关键:使用自身作为 mask,确保透明区域不产生黑框
watermark_layer.paste(rotated_stamp, (x, y), rotated_stamp)
# 融合与导出
final_img = Image.alpha_composite(base_rgba, watermark_layer)
if output_path.lower().endswith((".jpg", ".jpeg")):
final_img.convert("RGB").save(output_path, quality=92)
else:
final_img.save(output_path)
print(f"平铺水印处理完毕: {output_path}")
五、综合实战:制作图文复合水印徽章
在企业文档签署、商品防伪标牌等场景中,往往需要将几何图形(外边框、圆形印章、修饰线段)与文字内容有机组合,生成图文并茂的“水印徽章”。
下面的完整脚本演示了如何在透明图层上先绘制一个圆角矩形外框底托,再在其内部居中写入版权文字,最后叠加在底图左上角:
from PIL import Image, ImageDraw, ImageFont
def create_watermark_badge(base_path, output_path, badge_text="VERIFIED 官方审核"):
with Image.open(base_path) as base_img:
base_rgba = base_img.convert("RGBA")
W, H = base_rgba.size
# 创建透明图层
overlay = Image.new("RGBA", (W, H), (255, 255, 255, 0))
draw = ImageDraw.Draw(overlay)
# 字体初始化
try:
font = ImageFont.truetype("Arial.ttf", 20)
except Exception:
font = ImageFont.load_default()
# 1. 计算文字尺寸
bbox = draw.textbbox((0, 0), badge_text, font=font)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
# 2. 定义徽章位置与几何外框 (左上角留 30 像素边距)
margin_left, margin_top = 30, 30
pad_x, pad_y = 18, 10
badge_w = text_w + pad_x * 2
badge_h = text_h + pad_y * 2
badge_box = [margin_left, margin_top, margin_left + badge_w, margin_top + badge_h]
# 3. 绘制半透明圆角底托 (深灰蓝底色 + 半透明 Alpha=160)
draw.rounded_rectangle(badge_box, radius=8, fill=(57, 73, 87, 160), outline=(173, 139, 88, 220), width=2)
# 4. 绘制徽章左侧装饰小圆点 (暗金装饰)
dot_radius = 4
dot_cx = margin_left + pad_x // 2
dot_cy = margin_top + badge_h // 2
draw.ellipse([dot_cx - dot_radius, dot_cy - dot_radius, dot_cx + dot_radius, dot_cy + dot_radius], fill=(173, 139, 88, 255))
# 5. 在底托内部居中写入文字 (白色文本 Alpha=240)
text_x = margin_left + pad_x
text_y = margin_top + pad_y
draw.text((text_x, text_y), badge_text, font=font, fill=(250, 247, 241, 240))
# 6. 图层混合
result = Image.alpha_composite(base_rgba, overlay)
result.convert("RGB").save(output_path, quality=95)
print(f"图文徽章已成功生成并合成至: {output_path}")
六、高频问题排查与关键注意事项
在批量生产环境中运行 Pillow 水印脚本时,常会遇到由于平台差异、色彩模式或库版本升级引发的异常。以下为关键故障原因与修复建议。
原因:调用
ImageFont.truetype(font_path) 时,给定的字体文件名在当前运行目录下不存在,或者操作系统字体库中未收录该名称(例如 Linux 服务器往往默认不自带 Windows 下的 simhei.ttf 或 macOS 的 PingFang.ttc)。解决方案:在项目仓库中放置一份开源的中文字体文件(如思源黑体 SourceHanSansSC-Regular.otf 或阿里巴巴普惠体),代码中使用绝对路径或基于当前脚本的相对路径进行加载。
原因:JPEG/JPG 文件格式规范中不支持透明通道。合成完成后的图像对象仍然保持在
RGBA 模式,直接调用 save("output.jpg") 会导致编码器报错中断。解决方案:落盘前检查文件后缀,若是
.jpg 或 .jpeg,务必先调用 final_img.convert("RGB"),再执行 save()。
原因:
Image.paste(im, box, mask) 接受三个参数。如果省略了第三个 mask 遮罩参数,目标图像对应区域的原始像素会被源图像的 RGB 像素直接全覆盖,透明像素会被直接当作黑色 (0,0,0) 粘贴上去。解决方案:传入自身作为遮罩,即
target.paste(overlay, (x, y), overlay);或者优先使用标准 Image.alpha_composite。
七、总结
使用 Python Pillow 添加水印与绘制图形的核心可以提炼为以下三条黄金准则:
- 模式规范:参与透明度运算的图层必须统一为
RGBA模式,背景初始化为完全透明(0, 0, 0, 0),合成使用alpha_composite,导出 JPEG 前务必转回RGB。 - 精确定界:彻底抛弃已废弃的
textsize(),全量采用draw.textbbox(),利用四元组精确计算边距与旋转边界,防止文字错位与截断。 - 图层隔离:始终在独立的透明覆盖图层上绘制几何形状与文本,避免在原始数据上就地篡改,保证图层随时可替换、可重绘与高画质无损抗锯齿。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













