图片格式转换器
功能
格式转换
可指定转换后文件大小(指定长宽或指定空间占用)
可选择覆盖或另存
需要的包
需要安装pillow包
代码
import os
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
from PIL import Image, ImageOps
import sys
class ImageConverterApp:
def __init__(self, root):
self.root = root
self.root.title("图片格式转换工具")
self.root.geometry("750x650")
self.root.resizable(True, True)
# 设置中文字体支持
self.setup_fonts()
# 变量初始化
self.input_folder = tk.StringVar()
self.output_folder = tk.StringVar()
self.target_format = tk.StringVar(value="jpg")
self.width = tk.StringVar(value="800")
self.height = tk.StringVar(value="600")
self.max_filesize = tk.StringVar(value="400") # 默认400KB
self.overwrite = tk.BooleanVar(value=False)
self.preserve_ratio = tk.BooleanVar(value=True)
# 处理模式: 0-调整大小, 1-裁切, 2-限制文件大小(保持原图尺寸)
self.processing_mode = tk.IntVar(value=0)
# 支持的图片格式(包含avif)
self.supported_formats = ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp", "ico", "avif"]
# 创建UI
self.create_widgets()
def setup_fonts(self):
"""设置支持中文的字体"""
default_font = ('SimHei', 10)
self.root.option_add("*Font", default_font)
def create_widgets(self):
"""创建界面组件"""
# 创建主框架
main_frame = ttk.Frame(self.root, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)
# 输入文件夹选择
ttk.Label(main_frame, text="源文件夹:").grid(row=0, column=0, sticky=tk.W, pady=5)
ttk.Entry(main_frame, textvariable=self.input_folder, width=50).grid(row=0, column=1, pady=5)
ttk.Button(main_frame, text="浏览...", command=self.browse_input_folder).grid(row=0, column=2, padx=5, pady=5)
# 输出选项
output_frame = ttk.LabelFrame(main_frame, text="输出选项", padding="10")
output_frame.grid(row=1, column=0, columnspan=3, sticky=tk.W + tk.E, pady=10)
# 输出文件夹选择(仅在不覆盖时启用)
ttk.Label(output_frame, text="输出文件夹:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.output_entry = ttk.Entry(output_frame, textvariable=self.output_folder, width=40)
self.output_entry.grid(row=0, column=1, pady=5)
self.output_btn = ttk.Button(output_frame, text="浏览...", command=self.browse_output_folder)
self.output_btn.grid(row=0, column=2, padx=5, pady=5)
# 覆盖选项
ttk.Checkbutton(output_frame, text="覆盖原文件", variable=self.overwrite,
command=self.toggle_output_options).grid(row=1, column=0, sticky=tk.W, pady=5)
# 目标格式选择
ttk.Label(output_frame, text="目标格式:").grid(row=1, column=1, sticky=tk.W, pady=5)
format_combo = ttk.Combobox(output_frame, textvariable=self.target_format,
values=self.supported_formats, state="readonly", width=10)
format_combo.grid(row=1, column=2, sticky=tk.W, pady=5)
# 处理模式选择
mode_frame = ttk.LabelFrame(main_frame, text="图片处理模式", padding="10")
mode_frame.grid(row=2, column=0, columnspan=3, sticky=tk.W + tk.E, pady=10)
ttk.Radiobutton(mode_frame, text="调整大小", variable=self.processing_mode,
value=0, command=self.toggle_processing_options).grid(row=0, column=0, sticky=tk.W, padx=10)
ttk.Radiobutton(mode_frame, text="裁切到指定尺寸", variable=self.processing_mode,
value=1, command=self.toggle_processing_options).grid(row=0, column=1, sticky=tk.W, padx=10)
ttk.Radiobutton(mode_frame, text="保持原图尺寸,限制文件大小(KB)", variable=self.processing_mode,
value=2, command=self.toggle_processing_options).grid(row=0, column=2, sticky=tk.W, padx=10)
# 尺寸调整选项
size_frame = ttk.LabelFrame(main_frame, text="尺寸参数", padding="10")
size_frame.grid(row=3, column=0, columnspan=3, sticky=tk.W + tk.E, pady=10)
ttk.Label(size_frame, text="宽度:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.width_entry = ttk.Entry(size_frame, textvariable=self.width, width=10)
self.width_entry.grid(row=0, column=1, pady=5)
ttk.Label(size_frame, text="高度:").grid(row=0, column=2, sticky=tk.W, pady=5, padx=10)
self.height_entry = ttk.Entry(size_frame, textvariable=self.height, width=10)
self.height_entry.grid(row=0, column=3, pady=5)
self.ratio_check = ttk.Checkbutton(size_frame, text="保持宽高比", variable=self.preserve_ratio)
self.ratio_check.grid(row=0, column=4, sticky=tk.W, pady=5, padx=10)
# 文件大小限制选项
filesize_frame = ttk.LabelFrame(main_frame, text="文件大小限制", padding="10")
filesize_frame.grid(row=4, column=0, columnspan=3, sticky=tk.W + tk.E, pady=10)
ttk.Label(filesize_frame, text="最大文件大小 (KB):").grid(row=0, column=0, sticky=tk.W, pady=5)
self.filesize_entry = ttk.Entry(filesize_frame, textvariable=self.max_filesize, width=10)
self.filesize_entry.grid(row=0, column=1, pady=5)
ttk.Label(filesize_frame, text="建议值: JPG(100-800), PNG(200-1500), AVIF(50-500)").grid(row=0, column=2,
sticky=tk.W, pady=5,
padx=10)
# 转换按钮
ttk.Button(main_frame, text="开始转换", command=self.start_conversion, style="Accent.TButton").grid(row=5,
column=0,
columnspan=3,
pady=20)
# 进度条
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100)
self.progress_bar.grid(row=6, column=0, columnspan=3, sticky=tk.W + tk.E, pady=10)
# 状态区域
ttk.Label(main_frame, text="转换状态:").grid(row=7, column=0, sticky=tk.W, pady=5)
self.status_var = tk.StringVar(value="就绪")
ttk.Label(main_frame, textvariable=self.status_var).grid(row=7, column=1, sticky=tk.W, pady=5)
# 日志区域
ttk.Label(main_frame, text="转换日志:").grid(row=8, column=0, sticky=tk.NW, pady=5)
log_frame = ttk.Frame(main_frame)
log_frame.grid(row=8, column=1, columnspan=2, sticky=tk.NSEW, pady=5)
self.log_text = tk.Text(log_frame, height=10, width=60, state=tk.DISABLED)
self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(log_frame, command=self.log_text.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.log_text.config(yscrollcommand=scrollbar.set)
# 配置网格权重,使界面可伸缩
main_frame.columnconfigure(1, weight=1)
output_frame.columnconfigure(1, weight=1)
mode_frame.columnconfigure(2, weight=1)
size_frame.columnconfigure(4, weight=1)
filesize_frame.columnconfigure(2, weight=1)
main_frame.rowconfigure(8, weight=1)
# 初始设置控件状态
self.toggle_output_options()
self.toggle_processing_options()
# 设置按钮样式
style = ttk.Style()
style.configure("Accent.TButton", font=('SimHei', 10, 'bold'))
def toggle_output_options(self):
"""根据是否覆盖选项启用/禁用输出文件夹选项"""
if self.overwrite.get():
self.output_entry.config(state=tk.DISABLED)
self.output_btn.config(state=tk.DISABLED)
else:
self.output_entry.config(state=tk.NORMAL)
self.output_btn.config(state=tk.NORMAL)
def toggle_processing_options(self):
"""根据处理模式切换不同选项的可用性"""
mode = self.processing_mode.get()
if mode in (0, 1): # 调整大小或裁切模式
# 启用尺寸参数,禁用文件大小参数
self.width_entry.config(state=tk.NORMAL)
self.height_entry.config(state=tk.NORMAL)
self.ratio_check.config(state=tk.NORMAL if mode == 0 else tk.DISABLED)
self.filesize_entry.config(state=tk.DISABLED)
elif mode == 2: # 限制文件大小模式
# 禁用尺寸参数,启用文件大小参数
self.width_entry.config(state=tk.DISABLED)
self.height_entry.config(state=tk.DISABLED)
self.ratio_check.config(state=tk.DISABLED)
self.filesize_entry.config(state=tk.NORMAL)
def browse_input_folder(self):
"""选择输入文件夹"""
folder = filedialog.askdirectory(title="选择图片所在文件夹")
if folder:
self.input_folder.set(folder)
# 如果输出文件夹未设置,自动设置为输入文件夹的子文件夹
if not self.output_folder.get():
self.output_folder.set(os.path.join(folder, "converted_images"))
def browse_output_folder(self):
"""选择输出文件夹"""
folder = filedialog.askdirectory(title="选择保存转换后图片的文件夹")
if folder:
self.output_folder.set(folder)
def log(self, message):
"""向日志区域添加消息"""
self.log_text.config(state=tk.NORMAL)
self.log_text.insert(tk.END, message + "\n")
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
self.root.update_idletasks() # 更新界面
def get_image_files(self, folder):
"""获取文件夹中所有支持的图片文件"""
image_files = []
for file in os.listdir(folder):
ext = os.path.splitext(file)[1].lower()[1:] # 获取扩展名(不含点)
if ext in self.supported_formats:
image_files.append(os.path.join(folder, file))
return image_files
def process_image(self, image):
"""根据选择的模式处理图片"""
mode = self.processing_mode.get()
if mode == 0: # 调整大小
return self.resize_image(image)
elif mode == 1: # 裁切
return self.crop_image(image)
elif mode == 2: # 限制文件大小模式 - 保持原图尺寸
return image # 直接返回原图,不做尺寸改变
return image
def resize_image(self, image):
"""调整图片大小"""
try:
width = int(self.width.get())
height = int(self.height.get())
if width <= 0 or height <= 0:
raise ValueError("宽度和高度必须为正数")
# 如果保持比例,则计算新尺寸
if self.preserve_ratio.get():
original_width, original_height = image.size
ratio = min(width / original_width, height / original_height)
width = int(original_width * ratio)
height = int(original_height * ratio)
return image.resize((width, height), Image.Resampling.LANCZOS)
except ValueError as e:
self.log(f"尺寸错误: {str(e)},使用原图尺寸")
return image
def crop_image(self, image):
"""裁切图片到指定尺寸"""
try:
target_width = int(self.width.get())
target_height = int(self.height.get())
if target_width <= 0 or target_height <= 0:
raise ValueError("宽度和高度必须为正数")
# 使用中心裁切
return ImageOps.fit(
image,
(target_width, target_height),
method=Image.Resampling.LANCZOS,
bleed=0.0,
centering=(0.5, 0.5) # 中心位置
)
except ValueError as e:
self.log(f"裁切错误: {str(e)},使用原图")
return image
def save_with_filesize_limit(self, image, output_path, target_format):
"""保存图片并限制文件大小,保持原图尺寸"""
max_size_kb = int(self.max_filesize.get())
max_size_bytes = max_size_kb * 1024
# 对于不同格式使用不同的初始质量
if target_format.lower() in ['jpg', 'jpeg']:
quality = 90
format = 'JPEG'
elif target_format.lower() == 'png':
quality = 80
format = 'PNG'
elif target_format.lower() == 'webp':
quality = 80
format = 'WEBP'
elif target_format.lower() == 'avif':
quality = 60 # AVIF格式压缩效率高,初始质量设低一些
format = 'AVIF'
else:
quality = 80
format = target_format.upper()
# 先尝试保存一次看是否符合大小要求
temp_path = output_path + ".temp"
image.save(temp_path, format, quality=quality)
# 检查文件大小
current_size = os.path.getsize(temp_path)
# 如果已经符合要求,直接使用
if current_size <= max_size_bytes:
os.replace(temp_path, output_path)
return True, f"文件大小: {current_size / 1024:.1f}KB"
# 二分法寻找最佳质量
min_quality = 10
max_quality = quality
best_quality = quality
while max_quality - min_quality > 5:
mid_quality = (min_quality + max_quality) // 2
image.save(temp_path, format, quality=mid_quality)
current_size = os.path.getsize(temp_path)
if current_size <= max_size_bytes:
best_quality = mid_quality
min_quality = mid_quality # 尝试提高质量
else:
max_quality = mid_quality # 必须降低质量
# 使用找到的最佳质量保存
image.save(temp_path, format, quality=best_quality)
current_size = os.path.getsize(temp_path)
os.replace(temp_path, output_path)
if current_size > max_size_bytes:
return True, f"已尽力压缩: {current_size / 1024:.1f}KB (超过目标 {max_size_kb}KB)"
return True, f"文件大小: {current_size / 1024:.1f}KB"
def convert_image(self, input_path, output_path, target_format):
"""转换单张图片的格式和大小"""
try:
with Image.open(input_path) as img:
# 记录原图尺寸用于日志
original_size = img.size
# 处理透明通道
if target_format.lower() in ['jpg', 'jpeg', 'bmp'] and img.mode in ('RGBA', 'LA'):
background = Image.new(img.mode[:-1], img.size, (255, 255, 255))
background.paste(img, img.split()[-1])
img = background
elif img.mode == 'P': # 处理调色板模式
img = img.convert('RGB')
# 处理图片(调整大小或裁切)
processed_img = self.process_image(img)
# 保存图片
if self.processing_mode.get() == 2: # 限制文件大小模式
# 明确记录保持原图尺寸
success, size_msg = self.save_with_filesize_limit(processed_img, output_path, target_format)
return success, f"转换成功: {os.path.basename(input_path)} (保持原图尺寸: {original_size[0]}x{original_size[1]}) - {size_msg}"
else: # 其他模式
if target_format.lower() == 'jpg':
processed_img.save(output_path, 'JPEG', quality=95)
elif target_format.lower() == 'avif':
processed_img.save(output_path, 'AVIF', quality=80)
else:
processed_img.save(output_path)
size = os.path.getsize(output_path) / 1024
return True, f"转换成功: {os.path.basename(input_path)} (新尺寸: {processed_img.size[0]}x{processed_img.size[1]}) - 大小: {size:.1f}KB"
except Exception as e:
return False, f"转换失败 {os.path.basename(input_path)}: {str(e)}"
def start_conversion(self):
"""开始批量转换图片"""
input_folder = self.input_folder.get()
target_format = self.target_format.get().lower()
# 验证输入
if not input_folder or not os.path.isdir(input_folder):
messagebox.showerror("错误", "请选择有效的源文件夹")
return
if not self.overwrite.get():
output_folder = self.output_folder.get()
if not output_folder:
messagebox.showerror("错误", "请选择输出文件夹")
return
# 创建输出文件夹(如果不存在)
os.makedirs(output_folder, exist_ok=True)
# 验证文件大小参数(如果是限制文件大小模式)
if self.processing_mode.get() == 2:
try:
max_filesize = int(self.max_filesize.get())
if max_filesize <= 0:
raise ValueError
except ValueError:
messagebox.showerror("错误", "最大文件大小必须是正数")
return
# 获取所有图片文件
image_files = self.get_image_files(input_folder)
if not image_files:
messagebox.showinfo("信息", "所选文件夹中没有支持的图片文件")
return
# 开始转换
total = len(image_files)
success_count = 0
self.progress_var.set(0)
self.log("开始转换...")
self.status_var.set(f"正在转换: 0/{total}")
for i, input_path in enumerate(image_files, 1):
# 构建输出路径
filename = os.path.basename(input_path)
name_without_ext = os.path.splitext(filename)[0]
output_filename = f"{name_without_ext}.{target_format}"
if self.overwrite.get():
output_path = os.path.join(input_folder, output_filename)
else:
output_path = os.path.join(self.output_folder.get(), output_filename)
# 转换图片
success, msg = self.convert_image(input_path, output_path, target_format)
self.log(msg)
if success:
success_count += 1
# 更新进度
progress = (i / total) * 100
self.progress_var.set(progress)
self.status_var.set(f"正在转换: {i}/{total}")
self.root.update_idletasks()
# 转换完成
self.progress_var.set(100)
self.status_var.set("转换完成")
self.log(f"\n转换完成: 成功 {success_count}/{total}")
messagebox.showinfo("完成", f"转换完成!成功转换 {success_count}/{total} 张图片")
if __name__ == "__main__":
# 确保中文显示正常
root = tk.Tk()
app = ImageConverterApp(root)
root.mainloop()
默认评论
Halo系统提供的评论