#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import tkinter as tk
from tkinter import filedialog
from PIL import Image
import pillow_heif
import threading
import concurrent.futures
from queue import Queue
import datetime
# 注册 pillow-heif 插件,让 Pillow 能够打开 HEIF 文件
pillow_heif.register_heif_opener()
# 全局消息队列,用于日志显示
message_queue = Queue()
# 全局错误列表,记录转换失败的 (input_path, output_path) 文件对
error_files = []
# 全局控件引用,便于后续统一设置(在 UI 创建时赋值)
retry_button = None
src_button = None
dest_button = None
start_button = None
format_menu = None
target_format_var = None
def safe_print(message):
"""
将信息放入队列,由主线程更新日志显示
"""
message_queue.put(message)
def get_extension_for_format(target_format):
"""
根据目标格式返回适当的文件扩展名
"""
mapping = {
"JPEG": ".jpg",
"PNG": ".png",
"BMP": ".bmp",
"TIFF": ".tiff"
}
return mapping.get(target_format.upper(), ".jpg")
def convert_heif_to_format(input_path, output_path, target_format):
"""
将单个 HEIF/HEIC 文件转换为指定格式文件。
"""
global error_files
try:
with Image.open(input_path) as im:
if im.mode != "RGB":
im = im.convert("RGB")
im.save(output_path, target_format)
safe_print(f"转换成功: {os.path.basename(input_path)} -> {os.path.basename(output_path)}")
except Exception as e:
safe_print(f"转换失败: {os.path.basename(input_path)},错误: {e}")
error_files.append((input_path, output_path))
def process_files(src_folder, dest_folder, target_format):
"""
获取源文件夹中所有 HEIF/HEIC 文件,并使用多线程并发转换。
"""
global error_files
error_files.clear() # 清空之前的错误记录
heif_files = [f for f in os.listdir(src_folder) if f.lower().endswith((".heif", ".heic"))]
if not heif_files:
safe_print("所选文件夹中没有找到 HEIF/HEIC 文件!")
return
safe_print(f"共发现 {len(heif_files)} 个文件,开始转换...")
max_workers = min(8, len(heif_files))
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = []
for file in heif_files:
input_path = os.path.join(src_folder, file)
output_filename = os.path.splitext(file)[0] + get_extension_for_format(target_format)
output_path = os.path.join(dest_folder, output_filename)
futures.append(executor.submit(convert_heif_to_format, input_path, output_path, target_format))
concurrent.futures.wait(futures)
safe_print("初次转换完成。")
def retry_failed_conversion(target_format):
"""
对全局 error_files 中记录的失败文件进行自动重试转换。
"""
global error_files, retry_button
if not error_files:
safe_print("无失败文件,无需重试。")
return
to_retry = error_files.copy()
error_files.clear()
safe_print("开始自动重试转换失败的文件...")
max_workers = min(8, len(to_retry))
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = []
for input_path, output_path in to_retry:
futures.append(executor.submit(convert_heif_to_format, input_path, output_path, target_format))
concurrent.futures.wait(futures)
if error_files:
safe_print("自动重试后仍有以下文件转换失败:")
for inp, _ in error_files:
safe_print(f" {os.path.basename(inp)}")
safe_print("请点击‘重试转换失败文件’按钮进行手动重试。")
if retry_button:
retry_button.after(0, lambda: retry_button.config(state=tk.NORMAL))
else:
safe_print("自动重试后,所有文件转换成功!")
def start_conversion(src, dest, target_format):
"""
后台线程工作函数:执行初次转换及自动重试。
"""
safe_print("转换开始...")
process_files(src, dest, target_format)
if error_files:
safe_print("初次转换存在失败文件,启动自动重试...")
retry_failed_conversion(target_format)
if error_files:
safe_print("重试后仍有部分文件转换失败。")
safe_print("失败文件列表:")
for inp, _ in error_files:
safe_print(f" {os.path.basename(inp)}")
safe_print("请点击‘重试转换失败文件’按钮进行手动重试。")
else:
safe_print("所有文件均转换成功!")
def manual_retry_conversion():
"""
手动重试转换失败的文件(在后台线程中执行)。
"""
global retry_button
retry_button.config(state=tk.DISABLED)
current_format = target_format_var.get()
safe_print("手动重试开始...")
retry_failed_conversion(current_format)
if error_files:
safe_print("手动重试后仍有部分文件转换失败,请检查错误。")
else:
safe_print("手动重试后,所有失败文件已转换成功!")
def update_log(text_widget):
"""
定时检查消息队列,将新消息添加到日志文本框中。
"""
while not message_queue.empty():
msg = message_queue.get()
text_widget.insert(tk.END, msg + "\n")
text_widget.see(tk.END)
text_widget.after(100, update_log, text_widget)
def browse_folder(entry_widget, title):
"""
弹出文件夹选择对话框,并更新对应的文本输入框。
"""
folder = filedialog.askdirectory(title=title, initialdir=entry_widget.get())
if folder:
entry_widget.delete(0, tk.END)
entry_widget.insert(0, folder)
def create_gui():
"""
创建图形用户界面,包含文件夹选择、目标格式选择、日志展示及转换和重试按钮。
同时实现自动销毁功能:在 2025-12-31 之后,所有按钮失效。
"""
global target_format_var, retry_button, format_menu, src_button, dest_button, start_button
root = tk.Tk()
root.title("专供EMILY===HEIF 转 文件格式 批量转换工具")
root.geometry("600x500")
# --- 源文件夹选择 ---
src_frame = tk.Frame(root)
src_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label(src_frame, text="选择要转换的图片文件夹:").pack(side=tk.LEFT)
src_entry = tk.Entry(src_frame, width=50)
src_entry.pack(side=tk.LEFT, padx=5)
src_entry.insert(0, os.getcwd())
src_button = tk.Button(src_frame, text="浏览", command=lambda: browse_folder(src_entry, "选择含有 HEIF 图片的文件夹"))
src_button.pack(side=tk.LEFT)
# --- 目标文件夹选择 ---
dest_frame = tk.Frame(root)
dest_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label(dest_frame, text="选择转换后的图片文件夹:").pack(side=tk.LEFT)
dest_entry = tk.Entry(dest_frame, width=50)
dest_entry.pack(side=tk.LEFT, padx=5)
dest_entry.insert(0, os.getcwd())
dest_button = tk.Button(dest_frame, text="浏览", command=lambda: browse_folder(dest_entry, "选择保存转换后图片的文件夹"))
dest_button.pack(side=tk.LEFT)
# --- 目标格式选择 ---
format_frame = tk.Frame(root)
format_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label(format_frame, text="目标格式:").pack(side=tk.LEFT)
target_format_var = tk.StringVar(root)
format_options = ["JPEG", "PNG", "BMP", "TIFF"]
target_format_var.set("JPEG")
format_menu = tk.OptionMenu(format_frame, target_format_var, *format_options)
format_menu.pack(side=tk.LEFT, padx=5)
# --- 日志信息展示框 ---
log_text = tk.Text(root, wrap=tk.WORD, height=15)
log_text.pack(fill=tk.BOTH, padx=10, pady=10, expand=True)
# --- 开始转换按钮 ---
start_button = tk.Button(root, text="开始转换", font=("Arial", 12), width=20, height=2,
command=lambda: threading.Thread(
target=start_conversion,
args=(src_entry.get(), dest_entry.get(), target_format_var.get()),
daemon=True
).start())
start_button.pack(pady=5)
# --- 手动重试按钮 ---
retry_button = tk.Button(root, text="重试转换失败文件", font=("Arial", 12), width=25, height=2,
command=lambda: threading.Thread(target=manual_retry_conversion, daemon=True).start())
retry_button.pack(pady=5)
retry_button.config(state=tk.DISABLED)
update_log(log_text)
# --- 自动销毁功能:检查当前日期是否超过 2025-12-31 ---
if datetime.date.today() > datetime.date(2025, 12, 31):
safe_print("本软件已到期,所有功能已失效。")
src_button.config(state=tk.DISABLED)
dest_button.config(state=tk.DISABLED)
start_button.config(state=tk.DISABLED)
retry_button.config(state=tk.DISABLED)
format_menu.config(state=tk.DISABLED)
root.mainloop()
if __name__ == "__main__":
create_gui()