小白一枚,不懂代碼,工作需要,自己用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]-證件照批量生成神器_輕松搞定各類證件照](http://www.oilmaxhydraulic.com.cn/wp-content/uploads/2025/07/d2b5ca33bd20250728164710.png)
? 版權聲明
THE END