PYTHON实用代码合集

FengYuchen
2025-10-14
点 赞
0
热 度
4
评 论
0

文章摘要

智阅GPT

图片格式转换器

功能

  1. 格式转换

  2. 可指定转换后文件大小(指定长宽或指定空间占用)

  3. 可选择覆盖或另存

需要的包

需要安装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()


用键盘敲击出的不只是字符,更是一段段生活的剪影、一个个心底的梦想。希望我的文字能像一束光,在您阅读的瞬间,照亮某个角落,带来一丝温暖与共鸣。

FengYuchen

estj 总经理

具有版权性

请您在转载、复制时注明本文 作者、链接及内容来源信息。 若涉及转载第三方内容,还需一同注明。

具有时效性

目录

欢迎来到我的博客

17 文章数
7 分类数
2 评论数
9标签数