iT邦幫忙

0

[Day11]Python 桌面小工具:圖片批量壓縮器(Tkinter + Pillow)

  • 分享至 

  • xImage
  •  

今天做一個可視化的小工具,選資料夾就能批次壓縮/轉檔,完全離線、免部署、點兩下就能用

完成後,你可以:
一鍵壓縮整個資料夾(含子資料夾)
設定最長邊、JPEG 品質、輸出格式(保留/轉 JPG/轉 PNG)
選擇是否保留 EXIF、是否在檔名加 _compressed
支援 .jpg/.jpeg/.png/.webp 來源
進度條 + 不凍結(背景執行)

環境與安裝
只需安裝 Pillow:

pip install pillow

程式碼(存成 img_compressor.py)
執行:python img_compressor.py

# img_compressor.py — Day 11:圖片批量壓縮器(Tkinter + Pillow)
import os
import threading
from pathlib import Path
from tkinter import Tk, StringVar, IntVar, BooleanVar, filedialog, messagebox
from tkinter import ttk
from PIL import Image, ImageOps

SUPPORT = {".jpg", ".jpeg", ".png", ".webp"}

def resize_keep_ratio(img: Image.Image, max_side: int) -> Image.Image:
    """等比例縮放,把最長邊限制在 max_side 以內。"""
    w, h = img.size
    long_side = max(w, h)
    if long_side <= max_side:
        return img
    scale = max_side / long_side
    new_size = (int(w * scale), int(h * scale))
    return img.resize(new_size, Image.LANCZOS)

def process_folder(src: Path, dst: Path, max_side: int, quality: int, fmt: str,
                   keep_exif: bool, rename: bool, progress, status_label):
    """在背景執行的批次處理流程。"""
    files = [p for p in src.rglob("*") if p.suffix.lower() in SUPPORT]
    total = len(files)
    if total == 0:
        status_label.config(text="找不到可處理的圖片")
        return

    progress["maximum"] = total
    done = 0

    for fp in files:
        try:
            rel = fp.relative_to(src)
            target_dir = dst / rel.parent
            target_dir.mkdir(parents=True, exist_ok=True)

            # 決定輸出副檔名
            out_fmt = fmt.lower()
            ext_out = (
                fp.suffix.lower() if out_fmt == "keep"
                else (".jpg" if out_fmt in ("jpg", "jpeg") else ".png")
            )

            stem = fp.stem
            if rename:
                stem = f"{stem}_compressed"
            out_path = target_dir / f"{stem}{ext_out}"

            with Image.open(fp) as im:
                # 依 EXIF 自動修正旋轉
                try:
                    im = ImageOps.exif_transpose(im)
                except Exception:
                    pass

                # JPEG 不支援 alpha,要轉 RGB;PNG 可保留透明
                img_for_save = im.convert("RGB") if ext_out in (".jpg", ".jpeg") else im

                # 等比例縮圖
                img_for_save = resize_keep_ratio(img_for_save, max_side)

                # 儲存參數
                save_kw = {}
                if ext_out in (".jpg", ".jpeg"):
                    save_kw.update(dict(quality=quality, optimize=True, progressive=True))
                    # 保留 EXIF(若來源有)
                    if keep_exif and getattr(im, "info", {}).get("exif"):
                        save_kw["exif"] = im.info["exif"]
                elif ext_out == ".png":
                    save_kw.update(dict(optimize=True))

                img_for_save.save(out_path, **save_kw)

        except Exception as e:
            print(f"[跳過] {fp} -> {e}")

        done += 1
        progress["value"] = done
        status_label.config(text=f"處理中:{done}/{total}")
        progress.update_idletasks()

    status_label.config(text=f"完成:{done} 張,已輸出到 {dst}")

def start_worker(src_var, dst_var, max_var, q_var, fmt_var, exif_var, rn_var, progress, status):
    src = Path(src_var.get().strip())
    dst = Path(dst_var.get().strip())
    if not src.exists():
        messagebox.showerror("錯誤", "來源資料夾不存在")
        return
    if not dst.exists():
        try:
            dst.mkdir(parents=True, exist_ok=True)
        except Exception as e:
            messagebox.showerror("錯誤", f"無法建立輸出資料夾:{e}")
            return

    progress["value"] = 0
    status.config(text="開始處理…")

    t = threading.Thread(
        target=process_folder,
        args=(src, dst, max_var.get(), q_var.get(), fmt_var.get(),
              exif_var.get(), rn_var.get(), progress, status),
        daemon=True,
    )
    t.start()

def main():
    root = Tk()
    root.title("圖片批量壓縮器 (Day 11)")
    root.geometry("620x330")

    src_var = StringVar()
    dst_var = StringVar()
    max_var = IntVar(value=1280)
    q_var   = IntVar(value=85)
    fmt_var = StringVar(value="keep")   # keep / jpg / png
    exif_var = BooleanVar(value=True)
    rn_var   = BooleanVar(value=False)

    pad = {"padx": 8, "pady": 6}

    # 來源
    ttk.Label(root, text="來源資料夾").grid(row=0, column=0, sticky="e", **pad)
    ttk.Entry(root, textvariable=src_var, width=60).grid(row=0, column=1, **pad)
    ttk.Button(root, text="選擇…", command=lambda: src_var.set(filedialog.askdirectory() or src_var.get())
               ).grid(row=0, column=2, **pad)

    # 輸出
    ttk.Label(root, text="輸出資料夾").grid(row=1, column=0, sticky="e", **pad)
    ttk.Entry(root, textvariable=dst_var, width=60).grid(row=1, column=1, **pad)
    ttk.Button(root, text="選擇…", command=lambda: dst_var.set(filedialog.askdirectory() or dst_var.get())
               ).grid(row=1, column=2, **pad)

    # 參數
    ttk.Label(root, text="最長邊(px)").grid(row=2, column=0, sticky="e", **pad)
    ttk.Spinbox(root, from_=256, to=8000, textvariable=max_var, width=10
               ).grid(row=2, column=1, sticky="w", **pad)

    ttk.Label(root, text="JPEG 品質").grid(row=3, column=0, sticky="e", **pad)
    ttk.Spinbox(root, from_=30, to=95, textvariable=q_var, width=10
               ).grid(row=3, column=1, sticky="w", **pad)

    ttk.Label(root, text="輸出格式").grid(row=4, column=0, sticky="e", **pad)
    ttk.Combobox(root, values=["keep", "jpg", "png"],
                 textvariable=fmt_var, width=10, state="readonly"
                ).grid(row=4, column=1, sticky="w", **pad)

    # 選項
    ttk.Checkbutton(root, text="保留 EXIF(JPG)", variable=exif_var
                   ).grid(row=5, column=1, sticky="w", **pad)
    ttk.Checkbutton(root, text="檔名加 _compressed", variable=rn_var
                   ).grid(row=5, column=1, sticky="w", padx=160, pady=6)

    # 進度/狀態
    progress = ttk.Progressbar(root, length=560)
    progress.grid(row=6, column=0, columnspan=3, padx=8, pady=10)

    status = ttk.Label(root, text="等待開始…")
    status.grid(row=7, column=0, columnspan=3, padx=8, pady=4)

    # 開始按鈕
    ttk.Button(root, text="開始批次壓縮",
               command=lambda: start_worker(src_var, dst_var, max_var, q_var, fmt_var,
                                            exif_var, rn_var, progress, status)
               ).grid(row=8, column=0, columnspan=3, pady=8)

    root.mainloop()

if __name__ == "__main__":
    main()

怎麼用(步驟)
執行:python img_compressor.py
按「來源資料夾」→ 選你要壓縮的資料夾(含子資料夾會一併處理)
按「輸出資料夾」→ 選一個空資料夾來放輸出結果
設定參數:
最長邊(預設 1280 px)
JPEG 品質(預設 85)
輸出格式:keep(維持原副檔名)/ jpg / png
保留 EXIF(JPG)、檔名加 _compressed(避免覆蓋)
按「開始批次壓縮」,看進度條跑到 100% 即完成
打開輸出資料夾檢查結果
https://ithelp.ithome.com.tw/upload/images/20250926/20169368AJihEK380b.png
今日小結
我們把 Python 做成可視化桌面工具:不需要網頁、不需要伺服器
學到 Tkinter 版面的基本排法、背景執行避免 GUI 卡住、Pillow 的壓縮與轉檔技巧
明天可以延伸做 PDF 合併/分割器 或 檔名批次改名器。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言