[Python] 纯文本查看 复制代码
#!/usr/bin/env python3
"""
PPT 瘦身 GUI 工具
可调节 JPEG 压缩质量 (1-100),限制图片最大边长(分辨率),
并选择性删除音视频,显著减小文件体积。
"""
import os
import sys
import shutil
import tempfile
import zipfile
import threading
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
try:
from PIL import Image
except ImportError:
# 如果缺少 Pillow,给出友好提示并退出
root = tk.Tk()
root.withdraw()
messagebox.showerror("缺少依赖", "请先安装 Pillow 库:\npip install Pillow")
sys.exit(1)
# 支持的图片类型
IMAGE_EXT = {'.jpg', '.jpeg', '.png'}
MEDIA_EXT = {'.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.mp3', '.wav', '.m4a', '.aac'}
def compress_image(img_path, quality=85, max_dimension=None):
"""
压缩单张图片,若新图片更小则替换原文件。
返回 (是否成功, 原大小, 新大小)
"""
orig_size = os.path.getsize(img_path)
try:
with Image.open(img_path) as img:
# 缩放
if max_dimension and (img.width > max_dimension or img.height > max_dimension):
ratio = max_dimension / max(img.width, img.height)
new_w = int(img.width * ratio)
new_h = int(img.height * ratio)
img.thumbnail((new_w, new_h), Image.Resampling.LANCZOS)
ext = os.path.splitext(img_path)[1].lower()
save_kwargs = {}
fmt = None
if ext in ('.jpg', '.jpeg'):
if img.mode in ('RGBA', 'P'):
img = img.convert('RGB')
save_kwargs = {'quality': quality, 'optimize': True, 'subsampling': 2}
fmt = 'JPEG'
elif ext == '.png':
save_kwargs = {'optimize': True, 'compress_level': 6}
fmt = 'PNG'
else:
return False, orig_size, orig_size
# 临时保存
with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp:
img.save(tmp, format=fmt, **save_kwargs)
tmp_path = tmp.name
new_size = os.path.getsize(tmp_path)
if new_size < orig_size:
shutil.move(tmp_path, img_path)
return True, orig_size, new_size
else:
os.unlink(tmp_path)
return False, orig_size, orig_size
except Exception as e:
print(f"压缩失败 {img_path}: {e}")
return False, orig_size, orig_size
def slim_pptx(input_path, output_path, quality, max_dim, remove_media, progress_callback=None):
"""
执行 PPTX 瘦身,支持进度回调 progress_callback(percent, message)
"""
if not os.path.isfile(input_path):
raise FileNotFoundError(f"文件不存在: {input_path}")
with tempfile.TemporaryDirectory() as tmpdir:
# 解压
if progress_callback:
progress_callback(5, "正在解压 PPTX...")
with zipfile.ZipFile(input_path, 'r') as zf:
zf.extractall(tmpdir)
# 收集 media 文件
media_files = []
for root, _, files in os.walk(tmpdir):
if os.path.basename(root) == 'media':
for f in files:
media_files.append(os.path.join(root, f))
images = [p for p in media_files if os.path.splitext(p)[1].lower() in IMAGE_EXT]
total_images = len(images)
# 压缩图片
saved_bytes = 0
for idx, img_path in enumerate(images):
if progress_callback:
percent = 10 + int(80 * idx / total_images) if total_images > 0 else 90
progress_callback(percent, f"压缩图片 {idx+1}/{total_images}")
success, old_sz, new_sz = compress_image(img_path, quality, max_dim)
if success:
saved_bytes += (old_sz - new_sz)
# 删除媒体
if remove_media:
to_remove = [p for p in media_files if os.path.splitext(p)[1].lower() in MEDIA_EXT]
for r in to_remove:
os.unlink(r)
if progress_callback:
progress_callback(90, f"已删除 {len(to_remove)} 个音视频文件")
# 重新打包
if progress_callback:
progress_callback(95, "正在重新打包 PPTX...")
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as new_zip:
# mimetype 必须未压缩且为第一项
mt = os.path.join(tmpdir, 'mimetype')
if os.path.exists(mt):
new_zip.write(mt, 'mimetype', compress_type=zipfile.ZIP_STORED)
for root, _, files in os.walk(tmpdir):
for f in files:
full = os.path.join(root, f)
arc = os.path.relpath(full, tmpdir)
if arc == 'mimetype':
continue
new_zip.write(full, arc, compress_type=zipfile.ZIP_DEFLATED)
if progress_callback:
progress_callback(100, "瘦身完成!")
return saved_bytes
class PPTXCompressorApp:
def __init__(self, root):
self.root = root
root.title("PPT 瘦身工具 - 可调压缩/分辨率")
root.geometry("500x450")
root.resizable(False, False)
# 变量
self.input_file = tk.StringVar()
self.output_file = tk.StringVar()
self.quality = tk.IntVar(value=75)
self.max_dim = tk.IntVar(value=1920)
self.remove_media = tk.BooleanVar(value=False)
# 界面布局
# 输入文件
tk.Label(root, text="原始 PPTX 文件:").grid(row=0, column=0, padx=10, pady=10, sticky='e')
tk.Entry(root, textvariable=self.input_file, width=40).grid(row=0, column=1, padx=5)
tk.Button(root, text="浏览...", command=self.browse_input).grid(row=0, column=2, padx=5)
# 输出文件
tk.Label(root, text="输出 PPTX 文件:").grid(row=1, column=0, padx=10, pady=5, sticky='e')
tk.Entry(root, textvariable=self.output_file, width=40).grid(row=1, column=1, padx=5)
tk.Button(root, text="另存为...", command=self.browse_output).grid(row=1, column=2, padx=5)
# 压缩质量滑块
tk.Label(root, text="JPEG 压缩质量 (1~100):").grid(row=2, column=0, padx=10, pady=5, sticky='e')
quality_scale = tk.Scale(root, from_=1, to=100, orient=tk.HORIZONTAL,
variable=self.quality, length=200)
quality_scale.grid(row=2, column=1, padx=5, pady=5, sticky='w')
self.quality_label = tk.Label(root, text="75")
self.quality_label.grid(row=2, column=2, padx=5, sticky='w')
quality_scale.configure(command=lambda v: self.quality_label.config(text=str(int(float(v)))))
# 最大边长(分辨率限制)
tk.Label(root, text="图片最大边长(像素):").grid(row=3, column=0, padx=10, pady=5, sticky='e')
dim_spin = tk.Spinbox(root, from_=100, to=10000, increment=100,
textvariable=self.max_dim, width=8)
dim_spin.grid(row=3, column=1, padx=5, pady=5, sticky='w')
tk.Label(root, text="(0 表示不限制)").grid(row=3, column=2, padx=5, sticky='w')
# 限制0表示不限制
# 删除媒体选项
tk.Checkbutton(root, text="删除所有音视频文件(注意:将无法播放)",
variable=self.remove_media).grid(row=4, column=0, columnspan=3, pady=10, sticky='w', padx=20)
# 开始按钮
self.start_btn = tk.Button(root, text="开始瘦身", command=self.start_slim,
bg="#4CAF50", fg="white", font=("", 12, "bold"))
self.start_btn.grid(row=5, column=0, columnspan=3, pady=20)
# 进度条
self.progress = ttk.Progressbar(root, orient=tk.HORIZONTAL, length=400, mode='determinate')
self.progress.grid(row=6, column=0, columnspan=3, pady=10, padx=20)
# 状态信息
self.status_var = tk.StringVar(value="就绪")
tk.Label(root, textvariable=self.status_var, relief=tk.SUNKEN, anchor='w').grid(
row=7, column=0, columnspan=3, sticky='ew', padx=10, pady=10)
# 帮助提示
tk.Label(root, text="提示:降低质量或限制最大边长可大幅减小体积,缩放对高分辨率照片效果最明显。",
fg="gray", font=("", 9)).grid(row=8, column=0, columnspan=3, pady=5)
def browse_input(self):
filename = filedialog.askopenfilename(filetypes=[("PowerPoint 文件", "*.pptx")])
if filename:
self.input_file.set(filename)
# 自动生成输出文件名
base, ext = os.path.splitext(filename)
self.output_file.set(f"{base}_瘦身后{ext}")
def browse_output(self):
filename = filedialog.asksaveasfilename(defaultextension=".pptx", filetypes=[("PowerPoint 文件", "*.pptx")])
if filename:
self.output_file.set(filename)
def start_slim(self):
input_path = self.input_file.get().strip()
out_path = self.output_file.get().strip()
if not input_path or not os.path.isfile(input_path):
messagebox.showerror("错误", "请选择有效的 PPTX 文件")
return
if not out_path:
messagebox.showerror("错误", "请指定输出文件路径")
return
if input_path == out_path:
messagebox.showerror("错误", "输入和输出文件不能相同,请另存为新文件")
return
quality = self.quality.get()
max_dim = self.max_dim.get()
if max_dim == 0:
max_dim = None
remove = self.remove_media.get()
# 禁止重复点击
self.start_btn.config(state=tk.DISABLED)
self.progress['value'] = 0
self.status_var.set("正在处理...")
# 在新线程中执行瘦身,避免界面卡死
def task():
try:
saved = slim_pptx(
input_path, out_path, quality, max_dim, remove,
progress_callback=self.update_progress
)
self.root.after(0, lambda: self.on_finish(True, saved, out_path))
except Exception as e:
self.root.after(0, lambda: self.on_finish(False, 0, str(e)))
thread = threading.Thread(target=task)
thread.daemon = True
thread.start()
def update_progress(self, percent, message):
self.root.after(0, lambda: self._update_gui(percent, message))
def _update_gui(self, percent, message):
self.progress['value'] = percent
self.status_var.set(message)
def on_finish(self, success, saved_bytes, extra):
self.start_btn.config(state=tk.NORMAL)
if success:
# 计算节省的 MB
saved_mb = saved_bytes / (1024 * 1024)
msg = f"瘦身完成!\n总共节省 {saved_mb:.2f} MB\n输出文件:{extra}"
messagebox.showinfo("完成", msg)
self.status_var.set(f"完成,节省 {saved_mb:.2f} MB")
else:
messagebox.showerror("错误", f"瘦身失败:\n{extra}")
self.status_var.set("失败")
if __name__ == "__main__":
root = tk.Tk()
app = PPTXCompressorApp(root)
root.mainloop()
安装依赖pip install Pillow 可调图片分辨率