今天做一個可視化的小工具,選資料夾就能批次壓縮/轉檔,完全離線、免部署、點兩下就能用
完成後,你可以:
一鍵壓縮整個資料夾(含子資料夾)
設定最長邊、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% 即完成
打開輸出資料夾檢查結果
今日小結
我們把 Python 做成可視化桌面工具:不需要網頁、不需要伺服器
學到 Tkinter 版面的基本排法、背景執行避免 GUI 卡住、Pillow 的壓縮與轉檔技巧
明天可以延伸做 PDF 合併/分割器 或 檔名批次改名器。