证件照批量生成神器_轻松搞定各类证件照

小白一枚,不懂代码,工作需要,自己用Cursor写的一个证件照批量生成工具,不喜勿喷。

代码附上:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
批量证件照生成器 - 终极优化版
功能:批量处理图片生成各种规格的证件照
优化:去除背景颜色功能,专注核心功能,极致性能优化
"""

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import shutil
import tempfile
import threading
from PIL import Image
from pathlib import Path
import gc
import time

class IDPhotoGenerator:
    """批量证件照生成器主类"""

    # 类常量 - 预定义数据减少初始化开销
    PHOTO_SIZES = {
        "一寸证件照": (295, 413),
        "二寸证件照": (413, 579),
        "小二寸证件照": (413, 531),
        "护照照片": (354, 472),
        "签证照片": (354, 354),
        "驾照照片": (260, 378),
        "港澳通行证": (390, 567),
        "自定义": (0, 0)
    }

    SUPPORTED_FORMATS = frozenset(['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp'])
    CROP_MODES = ["中心裁剪", "顶部居中", "底部居中"]
    OUTPUT_FORMATS = ["jpg", "png", "bmp"]

    def __init__(self, root):
        self.root = root
        self._setup_window()
        self._init_variables()
        self._create_widgets()
        self._setup_cleanup()

    def _setup_window(self):
        """初始化窗口设置"""
        self.root.title("批量证件照生成器")
        self.root.geometry("750x550")
        self.root.resizable(True, True)
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)

    def _init_variables(self):
        """初始化变量"""
        self.input_files = []
        self.output_folder = ""
        self.temp_dir = tempfile.mkdtemp()
        self.processing = False

    def _create_widgets(self):
        """创建GUI界面"""
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky="nsew")
        main_frame.columnconfigure(1, weight=1)

        # 创建各个步骤面板
        self._create_file_selection_panel(main_frame, 0)
        self._create_size_selection_panel(main_frame, 1)
        self._create_output_settings_panel(main_frame, 2)
        self._create_processing_options_panel(main_frame, 3)
        self._create_processing_panel(main_frame, 4)

    def _create_file_selection_panel(self, parent, row):
        """创建文件选择面板"""
        frame = ttk.LabelFrame(parent, text="第一步:选择输入文件", padding="5")
        frame.grid(row=row, column=0, columnspan=2, sticky="ew", pady=5)
        frame.columnconfigure(0, weight=1)

        # 按钮框架
        btn_frame = ttk.Frame(frame)
        btn_frame.grid(row=0, column=0, sticky="ew")

        ttk.Button(btn_frame, text="选择文件夹", command=self.select_folder).pack(side="left", padx=5)
        ttk.Button(btn_frame, text="选择文件", command=self.select_files).pack(side="left", padx=5)
        ttk.Button(btn_frame, text="清空选择", command=self.clear_selection).pack(side="left", padx=5)

        # 文件列表
        list_frame = ttk.Frame(frame)
        list_frame.grid(row=1, column=0, sticky="ew", pady=5)
        list_frame.columnconfigure(0, weight=1)

        self.file_listbox = tk.Listbox(list_frame, height=6)
        self.file_listbox.grid(row=0, column=0, sticky="ew")

        scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.file_listbox.yview)
        scrollbar.grid(row=0, column=1, sticky="ns")
        self.file_listbox.configure(yscrollcommand=scrollbar.set)

    def _create_size_selection_panel(self, parent, row):
        """创建规格选择面板"""
        frame = ttk.LabelFrame(parent, text="第二步:选择证件照规格", padding="5")
        frame.grid(row=row, column=0, columnspan=2, sticky="ew", pady=5)
        frame.columnconfigure(1, weight=1)

        # 证件照类型选择
        ttk.Label(frame, text="证件照类型:").grid(row=0, column=0, padx=5, sticky="w")
        self.size_var = tk.StringVar(value="二寸证件照")
        size_combo = ttk.Combobox(frame, textvariable=self.size_var,
                                 values=list(self.PHOTO_SIZES.keys()), state="readonly")
        size_combo.grid(row=0, column=1, padx=5, sticky="ew")
        size_combo.bind('<<ComboboxSelected>>', self._on_size_selected)

        # 自定义尺寸
        custom_frame = ttk.Frame(frame)
        custom_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5)

        ttk.Label(custom_frame, text="自定义尺寸(像素):").grid(row=0, column=0, padx=5)
        ttk.Label(custom_frame, text="宽度:").grid(row=0, column=1, padx=5)

        self.width_var = tk.StringVar()
        self.width_entry = ttk.Entry(custom_frame, textvariable=self.width_var, width=8, state="disabled")
        self.width_entry.grid(row=0, column=2, padx=2)

        ttk.Label(custom_frame, text="高度:").grid(row=0, column=3, padx=5)
        self.height_var = tk.StringVar()
        self.height_entry = ttk.Entry(custom_frame, textvariable=self.height_var, width=8, state="disabled")
        self.height_entry.grid(row=0, column=4, padx=2)

    def _create_output_settings_panel(self, parent, row):
        """创建输出设置面板"""
        frame = ttk.LabelFrame(parent, text="第三步:输出设置", padding="5")
        frame.grid(row=row, column=0, columnspan=2, sticky="ew", pady=5)
        frame.columnconfigure(1, weight=1)

        # 输出文件夹
        ttk.Label(frame, text="输出文件夹:").grid(row=0, column=0, padx=5, sticky="w")
        self.output_var = tk.StringVar()
        ttk.Entry(frame, textvariable=self.output_var, state="readonly").grid(row=0, column=1, padx=5, sticky="ew")
        ttk.Button(frame, text="选择", command=self.select_output_folder).grid(row=0, column=2, padx=5)

        # 输出格式
        ttk.Label(frame, text="输出格式:").grid(row=1, column=0, padx=5, sticky="w")
        self.format_var = tk.StringVar(value="jpg")
        format_combo = ttk.Combobox(frame, textvariable=self.format_var,
                                   values=self.OUTPUT_FORMATS, state="readonly")
        format_combo.grid(row=1, column=1, padx=5, sticky="w")

    def _create_processing_options_panel(self, parent, row):
        """创建处理选项面板"""
        frame = ttk.LabelFrame(parent, text="第四步:处理选项", padding="5")
        frame.grid(row=row, column=0, columnspan=2, sticky="ew", pady=5)

        ttk.Label(frame, text="裁剪方式:").grid(row=0, column=0, padx=5, sticky="w")
        self.crop_mode_var = tk.StringVar(value="中心裁剪")
        crop_combo = ttk.Combobox(frame, textvariable=self.crop_mode_var,
                                 values=self.CROP_MODES, state="readonly")
        crop_combo.grid(row=0, column=1, padx=5, sticky="w")

    def _create_processing_panel(self, parent, row):
        """创建处理控制面板"""
        frame = ttk.Frame(parent)
        frame.grid(row=row, column=0, columnspan=2, sticky="ew", pady=10)
        frame.columnconfigure(0, weight=1)

        self.process_btn = ttk.Button(frame, text="开始生成证件照", command=self.start_processing)
        self.process_btn.grid(row=0, column=0, pady=5)

        self.progress = ttk.Progressbar(frame, mode='determinate')
        self.progress.grid(row=1, column=0, sticky="ew", pady=5)

        self.status_label = ttk.Label(frame, text="准备就绪")
        self.status_label.grid(row=2, column=0, pady=5)

    def _on_size_selected(self, event=None):
        """证件照规格选择事件处理"""
        is_custom = (self.size_var.get() == "自定义")
        state = "normal" if is_custom else "disabled"

        self.width_entry.config(state=state)
        self.height_entry.config(state=state)

        if not is_custom:
            width, height = self.PHOTO_SIZES[self.size_var.get()]
            self.width_var.set(str(width))
            self.height_var.set(str(height))

    def select_folder(self):
        """选择文件夹"""
        folder = filedialog.askdirectory(title="选择包含图片的文件夹")
        if not folder:
            return

        self.input_files.clear()
        # 使用生成器表达式优化性能
        folder_path = Path(folder)
        self.input_files = [
            str(file_path) for file_path in folder_path.rglob("*")
            if file_path.suffix.lower() in self.SUPPORTED_FORMATS
        ]
        self._update_file_list()

    def select_files(self):
        """选择文件"""
        filetypes = [
            ("图片文件", "*.jpg *.jpeg *.png *.bmp *.tiff *.webp"),
            ("所有文件", "*.*")
        ]
        files = filedialog.askopenfilenames(title="选择图片文件", filetypes=filetypes)
        if files:
            self.input_files.extend(files)
            self._update_file_list()

    def clear_selection(self):
        """清空文件选择"""
        self.input_files.clear()
        self._update_file_list()
        gc.collect()

    def _update_file_list(self):
        """更新文件列表显示"""
        self.file_listbox.delete(0, tk.END)
        # 批量处理文件名
        file_names = [os.path.basename(path) for path in self.input_files]
        for name in file_names:
            self.file_listbox.insert(tk.END, name)

        self.status_label.config(text=f"已选择 {len(self.input_files)} 个文件")

    def select_output_folder(self):
        """选择输出文件夹"""
        folder = filedialog.askdirectory(title="选择输出文件夹")
        if folder:
            self.output_folder = folder
            self.output_var.set(folder)

    def _get_target_size(self):
        """获取目标尺寸"""
        try:
            if self.size_var.get() == "自定义":
                width = int(self.width_var.get())
                height = int(self.height_var.get())
                if width <= 0 or height <= 0:
                    raise ValueError("尺寸必须大于0")
            else:
                width, height = self.PHOTO_SIZES[self.size_var.get()]
            return width, height
        except (ValueError, KeyError):
            messagebox.showerror("错误", "请输入有效的尺寸数值")
            return None, None

    def _crop_and_resize_image(self, image_path, target_width, target_height):
        """裁剪和调整图像尺寸"""
        try:
            with Image.open(image_path) as img:
                # 优化模式转换
                if img.mode == 'P':
                    img = img.convert('RGB')
                elif img.mode not in ('RGB', 'RGBA'):
                    img = img.convert('RGB')

                # 裁剪图片
                cropped_img = self._crop_image(img, target_width, target_height)

                # 调整到目标尺寸
                final_img = cropped_img.resize((target_width, target_height), Image.Resampling.LANCZOS)

                # 确保RGB模式
                if final_img.mode != 'RGB':
                    final_img = final_img.convert('RGB')

                return final_img

        except Exception as e:
            print(f"图片处理失败: {e}")
            return None

    def _crop_image(self, img, target_width, target_height):
        """根据裁剪模式裁剪图片"""
        width, height = img.size
        target_ratio = target_width / target_height
        current_ratio = width / height

        if current_ratio > target_ratio:
            # 裁剪宽度
            new_width = int(height * target_ratio)
            left = self._get_crop_position(width, new_width, True)
            crop_box = (left, 0, left + new_width, height)
        else:
            # 裁剪高度
            new_height = int(width / target_ratio)
            top = self._get_crop_position(height, new_height, False)
            crop_box = (0, top, width, top + new_height)

        return img.crop(crop_box)

    def _get_crop_position(self, original_size, new_size, is_horizontal):
        """计算裁剪位置"""
        crop_mode = self.crop_mode_var.get()

        if crop_mode == "中心裁剪":
            return (original_size - new_size) // 2
        elif crop_mode == "顶部居中":
            return 0 if not is_horizontal else (original_size - new_size) // 2
        else:  # 底部居中
            return original_size - new_size if not is_horizontal else (original_size - new_size) // 2

    def _process_single_image(self, input_path, output_folder, target_width, target_height, output_format):
        """处理单张图片"""
        try:
            processed_img = self._crop_and_resize_image(input_path, target_width, target_height)

            if processed_img is None:
                return False, f"处理失败: {input_path}"

            # 生成输出路径
            base_name = Path(input_path).stem
            output_path = Path(output_folder) / f"{base_name}.{output_format}"

            # 优化保存参数
            save_kwargs = {'optimize': True}
            if output_format.lower() == 'jpg':
                save_kwargs.update({'quality': 95, 'progressive': True})
                save_format = 'JPEG'
            else:
                save_format = output_format.upper()
                if save_format == 'PNG':
                    save_kwargs.pop('quality', None)

            # 保存图片
            processed_img.save(output_path, format=save_format, **save_kwargs)

            # 及时释放内存
            processed_img.close()

            return True, str(output_path)

        except Exception as e:
            return False, f"处理 {input_path} 时出错: {str(e)}"

    def start_processing(self):
        """开始处理"""
        if self.processing:
            return

        # 验证输入
        if not self.input_files:
            messagebox.showwarning("警告", "请先选择输入文件")
            return

        if not self.output_folder:
            messagebox.showwarning("警告", "请选择输出文件夹")
            return

        target_size = self._get_target_size()
        if target_size[0] is None:
            return

        self.processing = True
        self.process_btn.config(state="disabled")

        # 启动处理线程
        thread = threading.Thread(target=self._process_images, args=target_size, daemon=True)
        thread.start()

    def _process_images(self, target_width, target_height):
        """批量处理图片"""
        total_files = len(self.input_files)
        successful = 0
        failed = 0
        output_format = self.format_var.get()

        # 初始化进度条
        self.root.after(0, lambda: self.progress.config(maximum=total_files, value=0))

        # 计算UI更新间隔
        update_interval = max(1, total_files // 50)
        start_time = time.time()

        for i, input_path in enumerate(self.input_files):
            # 减少UI更新频率
            if i % update_interval == 0 or i == total_files - 1:
                filename = os.path.basename(input_path)
                self.root.after(0, self._update_progress, filename, i + 1)

            # 处理图片
            success, result = self._process_single_image(
                input_path, self.output_folder, target_width, target_height, output_format
            )

            if success:
                successful += 1
            else:
                failed += 1
                print(result)

            # 定期垃圾回收
            if i % 30 == 0:
                gc.collect()

        # 最终清理
        gc.collect()
        elapsed_time = time.time() - start_time

        # 处理完成
        self.root.after(0, self._processing_complete, successful, failed, elapsed_time)

    def _update_progress(self, filename, progress_value):
        """更新进度显示"""
        self.status_label.config(text=f"正在处理: {filename}")
        self.progress.config(value=progress_value)

    def _processing_complete(self, successful, failed, elapsed_time):
        """处理完成"""
        self.processing = False
        self.process_btn.config(state="normal")

        # 格式化时间
        time_str = f"{elapsed_time:.1f}秒" if elapsed_time < 60 else f"{elapsed_time/60:.1f}分钟"

        status_text = f"处理完成!成功: {successful}, 失败: {failed}, 用时: {time_str}"
        self.status_label.config(text=status_text)

        if successful > 0:
            avg_time = elapsed_time / successful
            message = f"成功处理 {successful} 张图片"
            if failed > 0:
                message += f"\n失败 {failed} 张图片"
            message += f"\n总用时: {time_str} (平均 {avg_time:.2f}秒/张)"
            message += f"\n输出文件夹: {self.output_folder}"
            messagebox.showinfo("处理完成", message)
        else:
            messagebox.showerror("处理失败", "没有成功处理任何图片,请检查输入文件和设置")

    def _setup_cleanup(self):
        """设置清理功能"""
        def cleanup():
            """清理资源"""
            try:
                # 等待处理完成
                while self.processing:
                    time.sleep(0.1)

                # 清理临时文件
                if os.path.exists(self.temp_dir):
                    shutil.rmtree(self.temp_dir)

                gc.collect()

            except Exception as e:
                print(f"清理资源失败: {e}")

        self.root.protocol("WM_DELETE_WINDOW", lambda: (cleanup(), self.root.destroy()))

def main():
    """主函数"""
    root = tk.Tk()
    root.withdraw()  # 预隐藏窗口

    try:
        app = IDPhotoGenerator(root)
        root.deiconify()  # 显示窗口
        root.mainloop()
    except Exception as e:
        messagebox.showerror("启动错误", f"程序启动失败: {e}")
        root.destroy()

if __name__ == "__main__":
    main()
图片[1]-证件照批量生成神器_轻松搞定各类证件照
------本页内容已结束,喜欢请分享------
温馨提示:由于项目或工具都有失效性,如遇到不能做的项目或不能使用的工具,可以根据关键词在站点搜索相关内容,查看最近更新的或者在网页底部给我们留言反馈。
© 版权声明
THE END
喜欢就支持一下吧
点赞1437 分享