一、背景介绍
日常工作生活中,我们经常需要处理大量图片:比如博主调整配图尺寸、摄影师压缩图片分享、设计师添加版权水印、普通用户整理相册格式。手动一张张处理效率低下,在线工具又存在隐私泄露风险。因此,一款轻量、本地、功能全面的批量图片处理工具成为刚需。
本文将基于Python的Tkinter(GUI)和Pillow(图像处理)库,手把手教你打造一款本地图片批量处理工具,覆盖尺寸调整、格式转换、质量压缩、文字水印、旋转等核心功能,完全独立运行,无需依赖在线服务。
二、实现思路
核心功能拆解
- GUI交互:设计直观界面,包含文件夹选择、参数输入、操作选项、进度反馈等组件;
- 文件操作:遍历源文件夹,过滤常见图片格式(JPG/PNG/BMP/GIF),创建输出目录;
- 图像处理:
- 尺寸调整:支持自定义宽/高+保持比例;
- 格式转换:PNG↔JPG↔BMP互转;
- 质量压缩:JPG格式专属,0-100可调;
- 文字水印:支持字体、颜色、透明度、位置(右下角);
- 旋转:90°顺时针/逆时针、180°;
- 进度反馈:实时显示处理进度,完成后提示结果。
技术栈选择
- GUI:
Tkinter(Python内置,轻量无依赖); - 图像处理:
Pillow(强大的Python图像处理库,支持所有核心操作); - 文件操作:Python标准库
os+shutil。
三、代码实现
1. 环境准备
首先安装依赖库:
pip install pillow
2. 完整代码
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageDraw, ImageFont, ImageOps
class ImageBatchProcessor:
def __init__(self, root):
self.root = root
self.root.title("本地图片批量处理工具")
self.root.geometry("800x600")
self.root.resizable(False, False)
# 初始化变量
self.source_dir = ""
self.output_dir = ""
self.image_files = []
self.total_images = 0
self.processed_count = 0
# 构建UI
self.build_ui()
def build_ui(self):
# 1. 源文件夹选择区
frame_source = ttk.LabelFrame(self.root, text="源文件夹")
frame_source.grid(row=0, column=0, padx=10, pady=5, sticky="ew", columnspan=2)
self.source_entry = ttk.Entry(frame_source, width=60)
self.source_entry.grid(row=0, column=0, padx=5, pady=5)
ttk.Button(frame_source, text="选择", command=self.select_source).grid(row=0, column=1, padx=5, pady=5)
# 2. 操作选项区
frame_ops = ttk.LabelFrame(self.root, text="操作选项")
frame_ops.grid(row=1, column=0, padx=10, pady=5, sticky="nsew", columnspan=2)
# 2.1 调整尺寸
ttk.Label(frame_ops, text="调整尺寸:").grid(row=0, column=0, padx=5, pady=2, sticky="w")
self.width_entry = ttk.Entry(frame_ops, width=10)
self.width_entry.grid(row=0, column=1, padx=5, pady=2)
ttk.Label(frame_ops, text="宽(px)").grid(row=0, column=2, padx=0, pady=2)
self.height_entry = ttk.Entry(frame_ops, width=10)
self.height_entry.grid(row=0, column=3, padx=5, pady=2)
ttk.Label(frame_ops, text="高(px)").grid(row=0, column=4, padx=0, pady=2)
self.keep_ratio = tk.BooleanVar(value=True)
ttk.Checkbutton(frame_ops, text="保持比例", variable=self.keep_ratio).grid(row=0, column=5, padx=5, pady=2)
# 2.2 格式转换
ttk.Label(frame_ops, text="格式转换:").grid(row=1, column=0, padx=5, pady=2, sticky="w")
self.format_var = tk.StringVar(value="保持原格式")
ttk.Combobox(frame_ops, textvariable=self.format_var, values=["保持原格式", "JPG", "PNG", "BMP"]).grid(row=1, column=1, padx=5, pady=2, columnspan=2)
# 2.3 质量压缩(仅JPG)
ttk.Label(frame_ops, text="JPG质量:").grid(row=2, column=0, padx=5, pady=2, sticky="w")
self.quality_entry = ttk.Entry(frame_ops, width=10)
self.quality_entry.insert(0, "85")
self.quality_entry.grid(row=2, column=1, padx=5, pady=2)
ttk.Label(frame_ops, text="(0-100)").grid(row=2, column=2, padx=0, pady=2)
# 2.4 文字水印
ttk.Label(frame_ops, text="水印文字:").grid(row=3, column=0, padx=5, pady=2, sticky="w")
self.watermark_text = ttk.Entry(frame_ops, width=20)
self.watermark_text.grid(row=3, column=1, padx=5, pady=2, columnspan=2)
ttk.Label(frame_ops, text="透明度(0-1):").grid(row=3, column=3, padx=5, pady=2)
self.watermark_alpha = ttk.Entry(frame_ops, width=5)
self.watermark_alpha.insert(0, "0.7")
self.watermark_alpha.grid(row=3, column=4, padx=5, pady=2)
# 2.5 旋转
ttk.Label(frame_ops, text="旋转:").grid(row=4, column=0, padx=5, pady=2, sticky="w")
self.rotate_var = tk.StringVar(value="不旋转")
ttk.Combobox(frame_ops, textvariable=self.rotate_var, values=["不旋转", "90°顺时针", "90°逆时针", "180°"]).grid(row=4, column=1, padx=5, pady=2, columnspan=2)
# 3. 输出设置区
frame_output = ttk.LabelFrame(self.root, text="输出设置")
frame_output.grid(row=2, column=0, padx=10, pady=5, sticky="ew", columnspan=2)
ttk.Label(frame_output, text="输出文件夹:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.output_entry = ttk.Entry(frame_output, width=60)
self.output_entry.grid(row=0, column=1, padx=5, pady=5)
ttk.Button(frame_output, text="选择", command=self.select_output).grid(row=0, column=2, padx=5, pady=5)
ttk.Label(frame_output, text="文件名后缀:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
self.suffix_entry = ttk.Entry(frame_output, width=20)
self.suffix_entry.insert(0, "_processed")
self.suffix_entry.grid(row=1, column=1, padx=5, pady=5)
# 4. 进度显示区
frame_progress = ttk.LabelFrame(self.root, text="处理进度")
frame_progress.grid(row=3, column=0, padx=10, pady=5, sticky="ew", columnspan=2)
self.progress_label = ttk.Label(frame_progress, text="等待开始...")
self.progress_label.grid(row=0, column=0, padx=5, pady=5)
# 5. 执行按钮
ttk.Button(self.root, text="开始批量处理", command=self.start_process).grid(row=4, column=0, padx=10, pady=10, columnspan=2)
def select_source(self):
"""选择源文件夹"""
dir_path = filedialog.askdirectory()
if dir_path:
self.source_dir = dir_path
self.source_entry.delete(0, tk.END)
self.source_entry.insert(0, dir_path)
# 预加载图片文件
self.image_files = [f for f in os.listdir(dir_path) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif'))]
self.total_images = len(self.image_files)
self.progress_label.config(text=f"找到{self.total_images}张图片")
def select_output(self):
"""选择输出文件夹"""
dir_path = filedialog.askdirectory()
if dir_path:
self.output_dir = dir_path
self.output_entry.delete(0, tk.END)
self.output_entry.insert(0, dir_path)
def validate_inputs(self):
"""验证用户输入"""
# 检查源文件夹
if not self.source_dir:
messagebox.showwarning("警告", "请选择源文件夹!")
return False
# 检查输出文件夹(默认用源文件夹下的processed)
if not self.output_dir:
self.output_dir = os.path.join(self.source_dir, "processed")
os.makedirs(self.output_dir, exist_ok=True)
self.output_entry.delete(0, tk.END)
self.output_entry.insert(0, self.output_dir)
# 验证尺寸输入(如果有)
width = self.width_entry.get().strip()
height = self.height_entry.get().strip()
if width or height:
if width and not width.isdigit():
messagebox.showwarning("警告", "宽度必须为正整数!")
return False
if height and not height.isdigit():
messagebox.showwarning("警告", "高度必须为正整数!")
return False
# 验证质量输入
quality = self.quality_entry.get().strip()
if not quality.isdigit() or not (0 <= int(quality) <=100):
messagebox.showwarning("警告", "质量必须为0-100的整数!")
return False
# 验证水印透明度
alpha = self.watermark_alpha.get().strip()
try:
alpha_val = float(alpha)
if not (0 <= alpha_val <= 1):
raise ValueError
except ValueError:
messagebox.showwarning("警告", "透明度必须为0-1的浮点数!")
return False
return True
def process_single_image(self, img_path):
"""处理单张图片"""
try:
# 打开图片
with Image.open(img_path) as img:
# 1. 旋转操作
rotate_opt = self.rotate_var.get()
if rotate_opt == "90°顺时针":
img = img.rotate(-90, expand=True)
elif rotate_opt == "90°逆时针":
img = img.rotate(90, expand=True)
elif rotate_opt == "180°":
img = img.rotate(180, expand=True)
# 2. 调整尺寸
width = self.width_entry.get().strip()
height = self.height_entry.get().strip()
if width or height:
w = int(width) if width else img.width
h = int(height) if height else img.height
if self.keep_ratio.get():
# 保持比例计算
ratio = min(w/img.width, h/img.height)
new_w = int(img.width * ratio)
new_h = int(img.height * ratio)
else:
new_w = w
new_h = h
img = img.resize((new_w, new_h), Image.Resampling.LANCZOS)
# 3. 添加水印
watermark_text = self.watermark_text.get().strip()
if watermark_text:
alpha_val = float(self.watermark_alpha.get())
# 创建水印层
watermark_layer = Image.new("RGBA", img.size, (255,255,255,0))
draw = ImageDraw.Draw(watermark_layer)
# 设置字体(默认Arial,若不存在则用系统默认)
try:
font = ImageFont.truetype("arial.ttf", 24)
except IOError:
font = ImageFont.load_default()
# 计算文字位置(右下角,边距20)
text_bbox = draw.textbbox((0,0), watermark_text, font=font)
text_w = text_bbox[2] - text_bbox[0]
text_h = text_bbox[3] - text_bbox[1]
x = img.width - text_w - 20
y = img.height - text_h -20
# 绘制文字(白色,透明度alpha_val)
draw.text((x,y), watermark_text, font=font, fill=(255,255,255,int(255*alpha_val)))
# 合成水印
if img.mode != "RGBA":
img = img.convert("RGBA")
img = Image.alpha_composite(img, watermark_layer).convert("RGB")
# 4. 保存图片
filename, ext = os.path.splitext(os.path.basename(img_path))
suffix = self.suffix_entry.get().strip()
new_filename = f"{filename}{suffix}"
# 确定输出格式
output_format = self.format_var.get()
if output_format != "保持原格式":
new_ext = f".{output_format.lower()}"
new_filename += new_ext
save_format = output_format
else:
new_filename += ext
save_format = ext[1:].upper()
output_path = os.path.join(self.output_dir, new_filename)
# 质量压缩(仅JPG)
save_params = {}
if save_format == "JPG":
save_params["quality"] = int(self.quality_entry.get())
save_params["optimize"] = True
img.save(output_path, format=save_format, **save_params)
return True
except Exception as e:
print(f"处理失败 {img_path}: {str(e)}")
return False
def start_process(self):
"""开始批量处理"""
if not self.validate_inputs():
return
if self.total_images ==0:
messagebox.showwarning("警告", "源文件夹中无有效图片!")
return
self.processed_count =0
self.progress_label.config(text=f"处理中: 0/{self.total_images}")
self.root.update_idletasks()
# 遍历处理图片
for idx, img_file in enumerate(self.image_files):
img_path = os.path.join(self.source_dir, img_file)
success = self.process_single_image(img_path)
if success:
self.processed_count +=1
# 更新进度
self.progress_label.config(text=f"处理中: {self.processed_count}/{self.total_images}")
self.root.update_idletasks()
# 处理完成
messagebox.showinfo("完成", f"处理结束! 成功{self.processed_count}张,失败{self.total_images - self.processed_count}张。")
self.progress_label.config(text="处理完成!")
if __name__ == "__main__":
root = tk.Tk()
app = ImageBatchProcessor(root)
root.mainloop()
三、代码解释
1. GUI设计
- 主窗口:使用
Tk()创建,设置标题和尺寸; - 组件布局:通过
LabelFrame分组,grid布局管理各个输入框、按钮、下拉框; - 交互逻辑:文件夹选择按钮调用
filedialog.askdirectory(),参数输入框绑定变量存储用户选择。
2. 输入验证
- 检查源文件夹是否为空;
- 验证尺寸、质量、透明度等参数是否合法;
- 若未选择输出文件夹,自动创建
processed子文件夹。
3. 图像处理核心
- 旋转:使用
rotate()方法,expand=True确保图片不被裁剪; - 尺寸调整:
resize()方法,Resampling.LANCZOS保证高质量缩放; - 水印:创建透明层
RGBA,绘制文字后与原图合成; - 格式转换:通过
save()方法的format参数指定输出格式; - 质量压缩:仅对JPG有效,
quality参数控制压缩程度。
4. 进度反馈
- 实时更新
progress_label显示当前处理数量; root.update_idletasks()确保