平时处理图片时,你是不是总遇到这样的麻烦:想给一批图片加水印,单张处理太费时间,用专业软件又觉得小题大做?今天就带大家亲手做一个实用的python小工具,既能选单个图片加水印,也能批量处理整个文件夹的图片,小白跟着步骤走也能搞定!
一、工具核心功能与准备工作
先明确下这个工具能帮我们解决什么问题,以及动手前需要准备哪些东西。
1. 核心功能
咱们做的这个工具,主打“灵活”和“高效”,具体能实现这3个功能:
- 单图处理:选一张图片,手动调整水印位置、透明度,预览效果后再保存
- 批量处理:选一个文件夹,一键给里面所有图片(支持jpg、png、jpeg格式)加统一水印
- 自定义设置:自己改水印文字、字体大小、颜色,还能调水印的透明度,避免遮挡图片内容
2. 环境准备
做这个工具不用复杂的环境,只要装2个python库就行,新手直接按步骤来:
- 先确认电脑装了python(建议3.7及以上版本,没装的话去官网下载,记得勾“add python to path”)
- 打开命令提示符(windows按win+r,输cmd;mac打开终端),输入下面两行命令,安装需要的库:
- 安装处理图片的库:
pip install pillow - 安装做图形界面的库(让工具更直观):
pip install tkinter
(注:tkinter在python3里通常是自带的,要是安装报错,直接跳过这步试试,大概率能正常用)
- 安装处理图片的库:
二、代码拆解与实现(附完整代码)
我把整个工具的代码分成了3个部分,每部分都标了注释,大家可以跟着理解,也能直接复制用。
1. 导入需要的库
先把后面要用到的工具包导进来,就像做饭前把调料准备好一样:
from pil import image, imagedraw, imagefont # 处理图片和添加文字水印 import tkinter as tk # 做图形界面 from tkinter import filedialog, messagebox, ttk # 界面里的文件选择、提示框等组件 import os # 处理文件夹和文件路径
2. 核心功能函数(加水印的关键)
这部分是工具的“心脏”,负责实现图片读取、水印添加、保存等核心操作,我拆成了3个函数,每个函数干一件事,逻辑更清楚:
(1)单个图片加水印函数
def add_watermark_to_single_image():
# 1. 让用户选择要处理的单个图片
img_path = filedialog.askopenfilename(
title="选一张要加水印的图片",
filetypes=[("图片文件", "*.jpg;*.png;*.jpeg"), ("所有文件", "*.*")]
)
if not img_path: # 要是用户没选图片,直接退出函数
return
# 2. 获取用户设置的水印参数(文字、大小、颜色、透明度)
watermark_text = entry_text.get().strip()
if not watermark_text: # 没填水印文字的话,弹出提示
messagebox.showwarning("提示", "请先输入水印文字哦!")
return
try:
font_size = int(entry_font_size.get().strip())
opacity = int(entry_opacity.get().strip())
# 简单判断参数是否合理,避免出错
if font_size <= 0 or opacity < 0 or opacity > 100:
raise valueerror
except valueerror:
messagebox.showerror("错误", "字体大小要填正整数,透明度要在0-100之间哦!")
return
# 3. 读取图片,处理png透明格式(避免透明图片加水印后背景变黑色)
img = image.open(img_path).convert("rgba")
# 创建一个和图片一样大的透明图层,用来放水印
watermark_layer = image.new("rgba", img.size, (255, 255, 255, 0))
draw = imagedraw.draw(watermark_layer)
# 4. 加载字体(这里用系统默认字体,windows和mac路径不一样,做了兼容)
try:
if os.name == "nt": # windows系统
font = imagefont.truetype("arial.ttf", font_size)
else: # mac或linux系统
font = imagefont.truetype("/library/fonts/arial.ttf", font_size)
except ioerror:
# 要是没找到arial字体,就用默认字体,虽然丑点但不影响用
font = imagefont.load_default(size=font_size)
# 5. 计算水印位置(默认放在右下角,距离边缘20像素,也可以自己改这里的数值)
text_width, text_height = draw.textbbox((0, 0), watermark_text, font=font)[2:]
img_width, img_height = img.size
x = img_width - text_width - 20 # 右边距20
y = img_height - text_height - 20 # 下边距20
# 6. 添加水印(处理透明度)
# 把用户输入的0-100透明度,转成pil需要的0-255范围
alpha = int(255 * (opacity / 100))
# 这里默认水印是黑色,想改颜色的话,把(0,0,0,alpha)里的前三个数换成rgb值
draw.text((x, y), watermark_text, font=font, fill=(0, 0, 0, alpha))
# 7. 合并图片和水印图层,保存结果
result = image.alpha_composite(img, watermark_layer)
# 处理png转jpg的情况(jpg不支持透明,要加白色背景)
if img_path.lower().endswith(('.jpg', '.jpeg')):
result = result.convert("rgb")
# 让用户选保存路径
save_path = filedialog.asksaveasfilename(
defaultextension=os.path.splitext(img_path)[1],
filetypes=[("图片文件", "*.jpg;*.png;*.jpeg"), ("所有文件", "*.*")],
title="保存加水印后的图片"
)
if save_path:
result.save(save_path)
messagebox.showinfo("成功", f"图片已保存到:\n{save_path}")
(2)批量处理文件夹图片函数
批量处理和单图逻辑差不多,主要多了“遍历文件夹”的步骤:
def batch_add_watermark():
# 1. 让用户选要批量处理的文件夹
folder_path = filedialog.askdirectory(title="选要批量加水印的文件夹")
if not folder_path:
return
# 2. 获取用户设置的水印参数(和单图函数一样,避免重复写代码)
watermark_text = entry_text.get().strip()
if not watermark_text:
messagebox.showwarning("提示", "请先输入水印文字哦!")
return
try:
font_size = int(entry_font_size.get().strip())
opacity = int(entry_opacity.get().strip())
if font_size <= 0 or opacity < 0 or opacity > 100:
raise valueerror
except valueerror:
messagebox.showerror("错误", "字体大小要填正整数,透明度要在0-100之间哦!")
return
# 3. 遍历文件夹里的所有文件,只处理图片
# 先定义支持的图片格式,避免处理非图片文件
supported_formats = ('.jpg', '.jpeg', '.png')
# 统计成功处理的图片数量
success_count = 0
# 加载字体(和单图函数一样,做了系统兼容)
try:
if os.name == "nt":
font = imagefont.truetype("arial.ttf", font_size)
else:
font = imagefont.truetype("/library/fonts/arial.ttf", font_size)
except ioerror:
font = imagefont.load_default(size=font_size)
# 4. 逐个处理图片
for filename in os.listdir(folder_path):
# 只处理支持格式的文件
if filename.lower().endswith(supported_formats):
img_path = os.path.join(folder_path, filename)
try:
# 读取图片,和单图处理逻辑一致
img = image.open(img_path).convert("rgba")
watermark_layer = image.new("rgba", img.size, (255, 255, 255, 0))
draw = imagedraw.draw(watermark_layer)
# 计算水印位置(同样默认右下角)
text_width, text_height = draw.textbbox((0, 0), watermark_text, font=font)[2:]
img_width, img_height = img.size
x = img_width - text_width - 20
y = img_height - text_height - 20
# 添加水印
alpha = int(255 * (opacity / 100))
draw.text((x, y), watermark_text, font=font, fill=(0, 0, 0, alpha))
# 合并图层,保存图片
result = image.alpha_composite(img, watermark_layer)
if filename.lower().endswith(('.jpg', '.jpeg')):
result = result.convert("rgb")
# 保存路径:在原文件夹下加“_watermarked”后缀
name, ext = os.path.splitext(filename)
save_path = os.path.join(folder_path, f"{name}_watermarked{ext}")
result.save(save_path)
success_count += 1
except exception as e:
# 遇到错误不崩溃,只是提示哪个文件处理失败
messagebox.showerror("处理失败", f"文件 {filename} 处理出错:\n{str(e)}")
# 批量处理结束后,提示结果
messagebox.showinfo("批量处理完成", f"总共处理了 {success_count} 张图片\n结果保存在原文件夹,文件名带“_watermarked”后缀")
(3)创建图形界面函数
有了核心功能,再做个简单的界面,不用记命令,点鼠标就能操作:
def create_gui():
# 1. 初始化窗口
root = tk.tk()
root.title("python图片水印工具")
root.geometry("500x300") # 窗口大小,也可以自己改
root.resizable(true, true) # 允许拉伸窗口
# 2. 创建标签和输入框(按网格布局,整齐好看)
# 水印文字设置
ttk.label(root, text="水印文字:").grid(row=0, column=0, padx=10, pady=15, sticky="w")
global entry_text
entry_text = ttk.entry(root, width=40)
entry_text.grid(row=0, column=1, padx=10, pady=15)
entry_text.insert(0, "我的图片") # 默认文字,可修改
# 字体大小设置
ttk.label(root, text="字体大小:").grid(row=1, column=0, padx=10, pady=5, sticky="w")
global entry_font_size
entry_font_size = ttk.entry(root, width=40)
entry_font_size.grid(row=1, column=1, padx=10, pady=5)
entry_font_size.insert(0, "20") # 默认20号字,可修改
# 透明度设置
ttk.label(root, text="水印透明度(0-100):").grid(row=2, column=0, padx=10, pady=5, sticky="w")
global entry_opacity
entry_opacity = ttk.entry(root, width=40)
entry_opacity.grid(row=2, column=1, padx=10, pady=5)
entry_opacity.insert(0, "50") # 默认半透明,可修改
# 3. 创建功能按钮
# 单图处理按钮
btn_single = ttk.button(root, text="处理单个图片", command=add_watermark_to_single_image)
btn_single.grid(row=3, column=0, padx=20, pady=20, sticky="ew")
# 批量处理按钮
btn_batch = ttk.button(root, text="批量处理文件夹", command=batch_add_watermark)
btn_batch.grid(row=3, column=1, padx=20, pady=20, sticky="ew")
# 4. 运行窗口
root.mainloop()
3. 主程序入口
最后加一句代码,让脚本运行时自动打开界面:
if __name__ == "__main__":
create_gui()
三、升级版图片水印工具:支持多参数自定义配置
在前一版工具的基础上,我们新增了文字颜色选择、水印位置自由切换、重复水印次数设置等功能,让水印效果更灵活可控。以下是完整的升级版代码,包含所有参数配置功能。
3.1 功能升级说明
- 新增文字颜色选择:支持通过颜色选择器自定义水印文字颜色
- 水印位置可选:提供9个常用位置(如左上角、中心、右下角等)
- 重复水印次数:可设置水印在图片中重复出现的行数和列数(如3x3网格分布)
- 保留原功能:兼容单图/批量处理、字体大小/透明度调整
3.2 完整代码实现(含参数配置)
from pil import image, imagedraw, imagefont
import tkinter as tk
from tkinter import filedialog, messagebox, ttk, colorchooser
import os
import math
class watermarktool:
def __init__(self, root):
# 初始化主窗口
self.root = root
self.root.title("图片水印批量工具")
self.root.geometry("700x550")
self.root.resizable(true, true)
self.root.configure(bg="#f0f0f0") # 浅灰背景
# 初始化参数
self.current_color = (0, 0, 0) # 默认黑色
self.fullscreen_mode = false # 全屏水印模式开关
# 设置全局样式
self.setup_styles()
# 创建界面
self.create_widgets()
def setup_styles(self):
"""配置ttk样式,统一美化界面"""
style = ttk.style()
style.theme_use("clam") # 使用clam主题增强跨平台一致性
# 标题样式
style.configure("title.tlabel",
font=("微软雅黑", 12, "bold"),
background="#f0f0f0",
foreground="#333333")
# 标签样式
style.configure("label.tlabel",
font=("微软雅黑", 10),
background="#f0f0f0",
foreground="#555555",
padding=5)
# 输入框样式
style.configure("tentry",
font=("微软雅黑", 10),
padding=5,
fieldbackground="#ffffff",
borderwidth=1,
focusthickness=2,
focuscolor="#4a90d9")
# 按钮样式
style.configure("tbutton",
font=("微软雅黑", 10, "bold"),
padding=8,
background="#4a90d9",
foreground="#ffffff")
style.map("tbutton",
background=[("active", "#357abd"), ("pressed", "#2a6099")])
# 颜色预览样式(不依赖height,用padding控制大小)
self.color_style = ttk.style()
self.color_style.configure("color.tlabel",
background="#000000", # 默认黑色
borderwidth=1,
relief="solid",
padding=(10, 5)) # 用内边距控制宽高(水平10,垂直5)
# 分组框标题样式(解决-font错误)
style.configure("tlabelframe.label",
font=("微软雅黑", 10, "bold"),
foreground="#333333")
def create_widgets(self):
"""创建界面组件,采用卡片式布局"""
# 主容器(带边距)
main_frame = ttk.frame(self.root, padding=20)
main_frame.pack(fill=tk.both, expand=true)
# 参数配置卡片(白色背景,阴影效果)
config_card = ttk.labelframe(main_frame, text="水印参数配置", padding=15)
config_card.pack(fill=tk.x, pady=(0, 20))
# 1. 水印文字
ttk.label(config_card, text="水印文字:", style="label.tlabel").grid(
row=0, column=0, padx=10, pady=10, sticky="w")
self.entry_text = ttk.entry(config_card, width=50)
self.entry_text.grid(row=0, column=1, columnspan=3, padx=10, pady=10, sticky="ew")
self.entry_text.insert(0, "我的图片")
# 2. 字体大小 + 透明度(横向排列)
ttk.label(config_card, text="字体大小:", style="label.tlabel").grid(
row=1, column=0, padx=10, pady=10, sticky="w")
self.entry_font_size = ttk.entry(config_card, width=10)
self.entry_font_size.grid(row=1, column=1, padx=(10, 30), pady=10, sticky="w")
self.entry_font_size.insert(0, "20")
ttk.label(config_card, text="透明度(0-100):", style="label.tlabel").grid(
row=1, column=2, padx=10, pady=10, sticky="w")
self.entry_opacity = ttk.entry(config_card, width=10)
self.entry_opacity.grid(row=1, column=3, padx=10, pady=10, sticky="w")
self.entry_opacity.insert(0, "50")
# 3. 文字颜色选择(修复-height错误:用padding控制预览框大小)
ttk.label(config_card, text="文字颜色:", style="label.tlabel").grid(
row=2, column=0, padx=10, pady=10, sticky="w")
color_frame = ttk.frame(config_card)
color_frame.grid(row=2, column=1, columnspan=3, padx=10, pady=10, sticky="w")
# 移除-height参数,依赖样式中的padding控制大小
self.color_label = ttk.label(color_frame, style="color.tlabel")
self.color_label.pack(side=tk.left, padx=5)
ttk.button(color_frame, text="选择颜色", command=self.choose_color).pack(side=tk.left)
# 4. 水印位置选择
ttk.label(config_card, text="水印位置:", style="label.tlabel").grid(
row=3, column=0, padx=10, pady=10, sticky="w")
self.position_var = tk.stringvar(value="右下")
positions = ["左上", "中上", "右上", "左中", "中心", "右中", "左下", "中下", "右下"]
self.position_combo = ttk.combobox(
config_card, textvariable=self.position_var, values=positions, width=10, state="readonly"
)
self.position_combo.grid(row=3, column=1, padx=10, pady=10, sticky="w")
# 5. 重复分布(行数+列数)
ttk.label(config_card, text="重复分布:", style="label.tlabel").grid(
row=4, column=0, padx=10, pady=10, sticky="w")
ttk.label(config_card, text="行数:", style="label.tlabel").grid(
row=4, column=1, padx=(10, 5), pady=10, sticky="e")
self.entry_rows = ttk.entry(config_card, width=5)
self.entry_rows.grid(row=4, column=2, padx=(0, 20), pady=10, sticky="w")
self.entry_rows.insert(0, "1")
ttk.label(config_card, text="列数:", style="label.tlabel").grid(
row=4, column=2, padx=(20, 5), pady=10, sticky="e")
self.entry_cols = ttk.entry(config_card, width=5)
self.entry_cols.grid(row=4, column=3, padx=10, pady=10, sticky="w")
self.entry_cols.insert(0, "1")
# 6. 全屏水印模式(新增功能)
fullscreen_frame = ttk.frame(config_card)
fullscreen_frame.grid(row=5, column=0, columnspan=4, padx=10, pady=10, sticky="w")
self.fullscreen_check = ttk.checkbutton(
fullscreen_frame, text="全屏水印模式(忽略位置和行列设置)",
command=self.toggle_fullscreen_mode
)
self.fullscreen_check.pack(side=tk.left)
# 7. 全屏水印参数(仅在全屏模式下启用)
self.fullscreen_params_frame = ttk.frame(config_card)
self.fullscreen_params_frame.grid(row=6, column=0, columnspan=4, padx=10, pady=5, sticky="w")
self.disable_frame(self.fullscreen_params_frame) # 默认禁用(自定义禁用方法)
ttk.label(self.fullscreen_params_frame, text="水印旋转角度:", style="label.tlabel").pack(side=tk.left, padx=5)
self.entry_rotation = ttk.entry(self.fullscreen_params_frame, width=5)
self.entry_rotation.pack(side=tk.left, padx=5)
self.entry_rotation.insert(0, "30") # 默认旋转30度
ttk.label(self.fullscreen_params_frame, text="水平间距(像素):", style="label.tlabel").pack(side=tk.left, padx=5)
self.entry_h_spacing = ttk.entry(self.fullscreen_params_frame, width=5)
self.entry_h_spacing.pack(side=tk.left, padx=5)
self.entry_h_spacing.insert(0, "100")
ttk.label(self.fullscreen_params_frame, text="垂直间距(像素):", style="label.tlabel").pack(side=tk.left, padx=5)
self.entry_v_spacing = ttk.entry(self.fullscreen_params_frame, width=5)
self.entry_v_spacing.pack(side=tk.left, padx=5)
self.entry_v_spacing.insert(0, "80")
# 按钮区域(底部居中)
btn_frame = ttk.frame(main_frame)
btn_frame.pack(fill=tk.x, pady=10)
ttk.button(
btn_frame, text="处理单个图片", command=self.process_single_image, width=20
).pack(side=tk.left, padx=(0, 30), pady=10, anchor=tk.center)
ttk.button(
btn_frame, text="批量处理文件夹", command=self.batch_process_images, width=20
).pack(side=tk.left, padx=30, pady=10, anchor=tk.center)
# 状态标签(底部显示提示信息)
self.status_label = ttk.label(
main_frame, text="请配置参数后选择图片处理", style="label.tlabel", anchor=tk.center
)
self.status_label.pack(fill=tk.x, pady=10)
# 让列自适应宽度
config_card.columnconfigure(1, weight=1)
config_card.columnconfigure(3, weight=1)
def disable_frame(self, frame):
"""自定义禁用框架内所有组件的方法(替代state=disabled)"""
for widget in frame.winfo_children():
widget.config(state="disabled")
def enable_frame(self, frame):
"""自定义启用框架内所有组件的方法"""
for widget in frame.winfo_children():
widget.config(state="normal")
def toggle_fullscreen_mode(self):
"""切换全屏水印模式,启用/禁用相关参数"""
self.fullscreen_mode = not self.fullscreen_mode
if self.fullscreen_mode:
self.enable_frame(self.fullscreen_params_frame)
self.update_status("已启用全屏水印模式")
else:
self.disable_frame(self.fullscreen_params_frame)
self.update_status("已禁用全屏水印模式")
def choose_color(self):
"""选择水印颜色并更新预览"""
color = colorchooser.askcolor(title="选择水印颜色")[0]
if color:
self.current_color = (int(color[0]), int(color[1]), int(color[2]))
hex_color = f"#{int(color[0]):02x}{int(color[1]):02x}{int(color[2]):02x}"
self.color_style.configure("color.tlabel", background=hex_color)
self.update_status(f"已选择颜色:{hex_color}")
def get_watermark_params(self):
"""获取并校验水印参数"""
watermark_text = self.entry_text.get().strip()
if not watermark_text:
messagebox.showwarning("提示", "请输入水印文字")
return none
try:
font_size = int(self.entry_font_size.get().strip())
opacity = int(self.entry_opacity.get().strip())
rows = int(self.entry_rows.get().strip())
cols = int(self.entry_cols.get().strip())
if not (font_size > 0 and 0 <= opacity <= 100 and rows > 0 and cols > 0):
raise valueerror
except valueerror:
messagebox.showerror("参数错误", "请检查参数:\n- 字体大小:正整数\n- 透明度:0-100\n- 行数/列数:正整数")
return none
# 全屏模式参数校验
fullscreen_params = none
if self.fullscreen_mode:
try:
rotation = int(self.entry_rotation.get().strip())
h_spacing = int(self.entry_h_spacing.get().strip())
v_spacing = int(self.entry_v_spacing.get().strip())
if not (h_spacing > 0 and v_spacing > 0):
raise valueerror
fullscreen_params = (rotation, h_spacing, v_spacing)
except valueerror:
messagebox.showerror("参数错误", "全屏模式参数需为正整数")
return none
return (
watermark_text, font_size, opacity,
self.current_color, self.position_var.get(),
rows, cols, fullscreen_params
)
def load_font(self, size):
"""加载字体(兼容中文)"""
try:
if os.name == "nt": # windows系统
return imagefont.truetype("simsun.ttc", size) # 宋体支持中文
else: # mac/linux
return imagefont.truetype("/system/library/fonts/pingfang.ttc", size) # 苹方字体
except ioerror:
self.update_status("未找到指定字体,使用默认字体")
return imagefont.load_default(size=size)
def calculate_position(self, position, img_w, img_h, text_w, text_h):
"""计算水印位置坐标"""
margin = 20 # 边缘距离
pos_map = {
"左上": (margin, margin),
"中上": ((img_w - text_w) // 2, margin),
"右上": (img_w - text_w - margin, margin),
"左中": (margin, (img_h - text_h) // 2),
"中心": ((img_w - text_w) // 2, (img_h - text_h) // 2),
"右中": (img_w - text_w - margin, (img_h - text_h) // 2),
"左下": (margin, img_h - text_h - margin),
"中下": ((img_w - text_w) // 2, img_h - text_h - margin),
"右下": (img_w - text_w - margin, img_h - text_h - margin)
}
return pos_map.get(position, (margin, margin)) # 默认左上
def draw_fullscreen_watermark(self, draw, watermark_text, font, img_width, img_height, fill_color):
"""绘制全屏水印(优化旋转逻辑)"""
# 获取全屏模式参数
params = self.get_watermark_params()
if not params:
return
fullscreen_params = params[-1]
rotation, h_spacing, v_spacing = fullscreen_params
# 计算文字基础尺寸
text_bbox = draw.textbbox((0, 0), watermark_text, font=font)
text_width = text_bbox[2] - text_bbox[0]
text_height = text_bbox[3] - text_bbox[1]
# 计算网格覆盖范围(避免边缘空白)
grid_width = img_width + text_width * 2
grid_height = img_height + text_height * 2
# 遍历网格绘制旋转水印
for x in range(-text_width, grid_width, h_spacing):
for y in range(-text_height, grid_height, v_spacing):
# 创建临时图层(尺寸足够容纳旋转后的文字)
temp_size = max(text_width, text_height) * 2
temp_layer = image.new("rgba", (temp_size, temp_size), (255, 255, 255, 0))
temp_draw = imagedraw.draw(temp_layer)
# 在临时图层中心绘制文字
temp_draw.text(
(temp_size//2 - text_width//2, temp_size//2 - text_height//2),
watermark_text,
font=font,
fill=fill_color
)
# 旋转临时图层(expand=true确保完整显示)
rotated_layer = temp_layer.rotate(rotation, expand=true)
rotated_w, rotated_h = rotated_layer.size
# 计算最终绘制位置(文字中心对齐网格点)
draw_x = x - rotated_w // 2
draw_y = y - rotated_h // 2
# 绘制旋转后的水印(用paste替代bitmap,避免透明度问题)
draw._image.paste(rotated_layer, (draw_x, draw_y), rotated_layer)
def process_single_image(self):
"""处理单个图片"""
img_path = filedialog.askopenfilename(
title="选择需要加水印的图片",
filetypes=[("图片文件", "*.jpg;*.png;*.jpeg"), ("所有文件", "*.*")]
)
if not img_path:
self.update_status("已取消选择图片")
return
# 获取参数
params = self.get_watermark_params()
if not params:
return
watermark_text, font_size, opacity, color, position, rows, cols, fullscreen_params = params
try:
self.update_status(f"正在处理图片:{os.path.basename(img_path)}")
# 处理图片
img = image.open(img_path).convert("rgba")
watermark_layer = image.new("rgba", img.size, (255, 255, 255, 0))
draw = imagedraw.draw(watermark_layer)
font = self.load_font(font_size)
alpha = int(255 * (opacity / 100))
fill_color = (*color, alpha)
# 全屏水印模式
if self.fullscreen_mode and fullscreen_params:
self.draw_fullscreen_watermark(draw, watermark_text, font, img.width, img.height, fill_color)
else:
# 普通模式:计算文字尺寸和分布
text_bbox = draw.textbbox((0, 0), watermark_text, font=font)
text_width = text_bbox[2] - text_bbox[0]
text_height = text_bbox[3] - text_bbox[1]
x_step = math.floor(img.width / (cols - 1)) if cols > 1 else 0
y_step = math.floor(img.height / (rows - 1)) if rows > 1 else 0
# 绘制水印
for i in range(rows):
for j in range(cols):
base_x, base_y = self.calculate_position(
position, img.width, img.height, text_width, text_height
)
x = base_x + (j * x_step) if cols > 1 else base_x
y = base_y + (i * y_step) if rows > 1 else base_y
x = max(0, min(x, img.width - text_width))
y = max(0, min(y, img.height - text_height))
draw.text((x, y), watermark_text, font=font, fill=fill_color)
# 合并保存
result = image.alpha_composite(img, watermark_layer)
if img_path.lower().endswith(('.jpg', '.jpeg')):
result = result.convert("rgb")
save_path = filedialog.asksaveasfilename(
defaultextension=os.path.splitext(img_path)[1],
filetypes=[("图片文件", "*.jpg;*.png;*.jpeg"), ("所有文件", "*.*")],
title="保存加水印后的图片"
)
if save_path:
result.save(save_path)
messagebox.showinfo("成功", f"图片已保存到:\n{save_path}")
self.update_status(f"处理完成:{os.path.basename(save_path)}")
except exception as e:
messagebox.showerror("处理失败", f"错误信息:\n{str(e)}")
self.update_status("处理失败,请检查图片是否正常")
def batch_process_images(self):
"""批量处理文件夹图片"""
folder_path = filedialog.askdirectory(title="选择批量处理的文件夹")
if not folder_path:
self.update_status("已取消选择文件夹")
return
params = self.get_watermark_params()
if not params:
return
watermark_text, font_size, opacity, color, position, rows, cols, fullscreen_params = params
supported_formats = ('.jpg', '.jpeg', '.png')
success_count = 0
font = self.load_font(font_size)
alpha = int(255 * (opacity / 100))
fill_color = (*color, alpha)
self.update_status(f"开始批量处理:{os.path.basename(folder_path)}")
for filename in os.listdir(folder_path):
if filename.lower().endswith(supported_formats):
img_path = os.path.join(folder_path, filename)
try:
img = image.open(img_path).convert("rgba")
watermark_layer = image.new("rgba", img.size, (255, 255, 255, 0))
draw = imagedraw.draw(watermark_layer)
# 全屏水印模式
if self.fullscreen_mode and fullscreen_params:
self.draw_fullscreen_watermark(draw, watermark_text, font, img.width, img.height, fill_color)
else:
# 普通模式绘制
text_bbox = draw.textbbox((0, 0), watermark_text, font=font)
text_width = text_bbox[2] - text_bbox[0]
text_height = text_bbox[3] - text_bbox[1]
x_step = math.floor(img.width / (cols - 1)) if cols > 1 else 0
y_step = math.floor(img.height / (rows - 1)) if rows > 1 else 0
for i in range(rows):
for j in range(cols):
base_x, base_y = self.calculate_position(
position, img.width, img.height, text_width, text_height
)
x = base_x + (j * x_step) if cols > 1 else base_x
y = base_y + (i * y_step) if rows > 1 else base_y
x = max(0, min(x, img.width - text_width))
y = max(0, min(y, img.height - text_height))
draw.text((x, y), watermark_text, font=font, fill=fill_color)
# 保存处理结果
result = image.alpha_composite(img, watermark_layer)
if filename.lower().endswith(('.jpg', '.jpeg')):
result = result.convert("rgb")
name, ext = os.path.splitext(filename)
save_path = os.path.join(folder_path, f"{name}_watermarked{ext}")
result.save(save_path)
success_count += 1
self.update_status(f"已处理:{filename}({success_count}张)")
except exception as e:
messagebox.showerror("单个文件失败", f"文件 {filename} 处理出错:\n{str(e)}")
messagebox.showinfo("批量完成", f"批量处理结束!\n共成功处理 {success_count} 张图片")
self.update_status(f"批量处理完成,成功 {success_count} 张")
def update_status(self, text):
"""更新底部状态提示"""
self.status_label.config(text=text)
self.root.update_idletasks() # 立即刷新界面
if __name__ == "__main__":
root = tk.tk()
app = watermarktool(root)
root.mainloop()
3.3 参数配置说明
3.3.1. 基础参数
- 水印文字:输入需要添加的水印内容(支持中文)
- 字体大小:设置文字大小(建议10-50之间,根据图片尺寸调整)
- 透明度:0-100的数值(0为完全透明,100为完全不透明)
3.3.2. 高级参数
- 文字颜色:点击"选择颜色"打开调色板,可自定义任意颜色(默认黑色)
- 水印位置:下拉选择9个常用位置(如"中心"会将水印放在图片正中间)
- 重复分布:通过"行数"和"列数"设置水印重复次数:
- 1x1:只显示一个水印(默认)
- 2x2:显示4个水印(2行2列均匀分布)
- 3x3:显示9个水印(适合大图片全屏水印)
3.4 使用示例和效果演示
- 制作版权水印:文字"© 2025 xcleigh工作室",白色半透明(透明度30),右下角1x1分布
- 制作全屏水印:文字"内部资料",灰色(rgb 128,128,128),透明度20,3x3中心分布

批量生成效果

生成水印前的图片

生成水印后的图片

四、工具使用步骤(小白友好版)
代码写好后,怎么用呢?跟着这4步来,保证能成功:
1. 保存代码
把上面所有代码复制下来,粘贴到记事本里,然后点“文件-另存为”,注意2个细节:
- 文件名:结尾必须是
.py,比如watermark_tool.py - 保存类型:选“所有文件”,编码选“utf-8”(避免中文乱码)
2. 运行工具
找到保存好的watermark_tool.py文件,双击它就能打开工具(前提是已经装了python)。打开后会看到这样的界面:
- 上面三个输入框,分别填“水印文字”“字体大小”“透明度”
- 下面两个按钮,选“处理单个图片”或“批量处理文件夹”
3. 单图处理操作
- 在输入框里填好参数(比如水印文字填“我的旅行照”,字体20,透明度50)
- 点“处理单个图片”,在弹出的窗口里选要加水印的图片(比如桌面上的
photo.jpg) - 选好后,会弹出“保存”窗口,选个保存位置(比如还是桌面),点“保存”
- 提示“成功”后,去保存位置找图片,就能看到右下角多了水印
4. 批量处理操作
- 同样先填好参数(批量处理时,所有图片会用同一个参数)
- 点“批量处理文件夹”,选要处理的文件夹(比如“d盘-图片集”)
- 工具会自动处理文件夹里所有jpg、png图片,不用手动等
- 处理完会提示“完成”,去原文件夹看,每张图片都会多一个带“_watermarked”后缀的副本,比如
photo_watermarked.jpg
五、常见问题解决(避坑指南)
用的时候可能会遇到小问题,这里整理了3个常见情况,教你怎么解决:
1. 双击脚本没反应/报错“no module named xxx”
原因:没装需要的库,或者python没添加到环境变量。
解决办法:
- 按前面“准备工作”的步骤,重新打开cmd/终端,输入
pip install pillow和pip install tkinter - 要是还没反应,右键点击脚本,选“打开方式”,手动选python.exe(通常在
c:\users\你的用户名\appdata\local\programs\python\pythonxx文件夹里)
2. 处理png图片后,背景变黑色
原因:png图片有透明背景,保存成jpg时不支持透明,默认会填黑色。
解决办法:
- 处理png图片时,保存的时候选“保存类型”为png,不要选jpg
- 要是必须存jpg,可以在代码里改“填充颜色”:把
fill=(0,0,0,alpha)改成fill=(255,255,255,alpha),水印会变成白色,背景也会变成白色
3. 水印文字显示乱码/方块
原因:系统里没有arial字体,导致字体加载失败。
解决办法:
- 手动换个系统有的字体,比如windows里的“微软雅黑”,把代码里加载字体的行改成:
font = imagefont.truetype("msyh.ttc", font_size) - 或者直接用默认字体,虽然不好看,但不会乱码,代码里保留
font = imagefont.load_default(size=font_size)就行
六、功能扩展建议(进阶玩法)
要是你觉得基础功能不够用,还能给工具加这些小功能,难度也不大:
- 加水印位置选择:现在默认右下角,可以加个下拉框,让用户选“左上角”“中间”“左下角”等位置
- 加图片水印:不光能加文字,还能加图片水印(比如自己的logo),用
image.open("logo.png")读取logo,再贴到原图上 - 批量修改保存路径:现在批量处理只能存在原文件夹,可加个“选择输出文件夹”按钮,把结果统一存到新文件夹里
- 预览功能:加水印前先显示预览图,用户确认没问题再保存,避免反复修改
七、总结
这个python水印工具虽然简单,但特别实用,不管是处理日常照片,还是工作中给素材加水印,都能省不少时间。关键是代码全在上面,自己能改,想加什么功能就加什么。
新手不用怕,跟着步骤一步步来,先跑通基础版本,再慢慢改细节,既能学会python实用技能,又能做出自己能用的工具,这不比单纯看教程有意思多了?
以上就是基于python实现一个图片水印批量添加工具的详细内容,更多关于python图片水印批量添加的资料请关注代码网其它相关文章!
发表评论