# Python实战:打造本地图片批量处理工具,高效解决日常图片整理痛点


一、背景介绍

日常工作生活中,我们经常需要处理大量图片:比如博主调整配图尺寸、摄影师压缩图片分享、设计师添加版权水印、普通用户整理相册格式。手动一张张处理效率低下,在线工具又存在隐私泄露风险。因此,一款轻量、本地、功能全面的批量图片处理工具成为刚需。

本文将基于Python的Tkinter(GUI)和Pillow(图像处理)库,手把手教你打造一款本地图片批量处理工具,覆盖尺寸调整、格式转换、质量压缩、文字水印、旋转等核心功能,完全独立运行,无需依赖在线服务。

二、实现思路

核心功能拆解

  1. GUI交互:设计直观界面,包含文件夹选择、参数输入、操作选项、进度反馈等组件;
  2. 文件操作:遍历源文件夹,过滤常见图片格式(JPG/PNG/BMP/GIF),创建输出目录;
  3. 图像处理
    • 尺寸调整:支持自定义宽/高+保持比例;
    • 格式转换:PNG↔JPG↔BMP互转;
    • 质量压缩:JPG格式专属,0-100可调;
    • 文字水印:支持字体、颜色、透明度、位置(右下角);
    • 旋转:90°顺时针/逆时针、180°;
  4. 进度反馈:实时显示处理进度,完成后提示结果。

技术栈选择

  • GUITkinter(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()确保

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注