一、背景:AI 爆火,它到底能为我们做什么?
AI 领域可谓是热闹非凡,ChatGPT、DeepSeek 等各种 AI 工具如雨后春笋般涌现,朋友圈里到处都是大家用 AI 生成的文案、图片,仿佛一夜之间,AI 就渗透到了我们生活的方方面面。然而,在这股热潮之下,很多人心里都有这样的疑问:AI 到底能做什么?我能用它来做什么呢?看着别人用 AI 做出各种酷炫的东西,自己却不知道从何下手,仿佛 AI 离我们很近,又很远。
二、生活中的一件小事:打印PDF太大,打印机无法支持超过20MB的PDF打印。
就在今天,我就遇到了一个让人头疼的小问题。孩子学校需要打印一份 PDF学习材料,可当我把文件发送到打印机时,却收到提示:文件超过 20 MB,无法直接打印,需要去电脑端。但是电脑端也试过,根本也不好用。孩子今天放学后就要用到,我试了市面上的很多工具,尝试进行PDF压缩,很遗憾,大部分压缩功能都需要收费。之前也遇到过这种太大无法打印的问题,当时我是采取的最笨的办法,一张张截图,然后一张张打印图片,费时又费力。不知道大家遇没遇到过这种情况,虽然我也是搞软件的,但是遇到这个问题也很头疼,网上也有很多在线压缩的,要么就收费、要么就是做推广需要验证码加好友什么的。
然后就突然想起最近一直在关注的 AI 编程,说不定它能帮我解决这个难题。抱着试试看的心态,我决定向 AI 求助。
三、具体如何做:豆包 AI 助力,生成 Python 程序
因为最近一直在使用豆包,其编程能力很不错。利用豆包AI工具,将需求描述给AI,让其帮我用Python写一个合并DPF的程序。对话如下“我是一个C#开发人员,对.net很熟悉,现在想学习python,请帮我用python写一个可以合并PDF的程序,要求可以自定义上传文件,然后进行合并,同时支持多页合并。”
四、一步步让AI帮你调试:在尝试中找到正确的方法
豆包很快就给出了回应,它为我提供了一段基于 PyMuPDF 库的 Python 代码,这个代码直接复制后拿到Visual Studio Code中
进行运行调试,输出界面如下
但是第一次运行报错了
将报错信息直接反馈豆包,让其进行修改,豆包很快进行了反馈
但是运行后还是报错,继续将错误反馈豆包,进行反复修改
经过多次调试、更换其他思路后最终可以了,但是输出的结果不太对,将结果反馈豆包后,它可以继续帮你修改
最终经过多次的问题修复反馈,成功完成了PDF压缩程序。压缩后的PDF大小大幅降低。同时PDF质量没有受到影响。
以下为PDF压缩前后对比图,压缩前23MB,压缩后9MB
下面是最终完成代码,拿去后直接保存为"合并PDF.py"文件,如果你有Python环境,即可直接运行;后续我会将这个py打包为exe程序,可直接双击运行。
创作不易,欢迎点赞和关注,你的支持是最好的动力~
import os
import fitz # PyMuPDF
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from tkinter import font
import sys
from typing import Optional, Tuple
import tempfile
import traceback
from PIL import Image # 用于图片处理
class PDFImageCompressorApp:
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("PDF图片压缩工具")
self.root.geometry("600x400")
self.root.minsize(500, 350)
# 设置中文字体支持
self.default_font = font.nametofont("TkDefaultFont")
self.default_font.configure(family="SimHei", size=10)
self.root.option_add("*Font", self.default_font)
# 初始化变量
self.input_file = ""
self.output_file = ""
self.quality = tk.IntVar(value=50) # 默认质量为50%
self.is_processing = False # 标记是否正在处理
self._create_widgets()
self._setup_layout()
def _create_widgets(self) -> None:
# 创建菜单栏
self.menu_bar = tk.Menu(self.root)
self.file_menu = tk.Menu(self.menu_bar, tearoff=0)
self.file_menu.add_command(label="打开", command=self._select_input_file, accelerator="Ctrl+O")
self.file_menu.add_separator()
self.file_menu.add_command(label="退出", command=self.root.quit)
self.menu_bar.add_cascade(label="文件", menu=self.file_menu)
self.help_menu = tk.Menu(self.menu_bar, tearoff=0)
self.help_menu.add_command(label="使用说明", command=self._show_help)
self.help_menu.add_separator()
self.help_menu.add_command(label="关于", command=self._show_about)
self.menu_bar.add_cascade(label="帮助", menu=self.help_menu)
self.root.config(menu=self.menu_bar)
# 创建主框架
self.main_frame = ttk.Frame(self.root, padding="20")
# 文件选择区域
self.file_frame = ttk.LabelFrame(self.main_frame, text="文件选择", padding="10")
self.file_path_label = ttk.Label(self.file_frame, text="未选择文件", width=40)
self.browse_button = ttk.Button(self.file_frame, text="浏览...", command=self._select_input_file)
# 质量设置区域
self.quality_frame = ttk.LabelFrame(self.main_frame, text="压缩质量", padding="10")
self.quality_scale = ttk.Scale(self.quality_frame, from_=10, to=100, orient=tk.HORIZONTAL,
variable=self.quality, length=300)
self.quality_value_label = ttk.Label(self.quality_frame, text="50%")
self.quality_scale.bind("<Motion>", self._update_quality_label)
self.quality_scale.bind("<ButtonRelease-1>", self._update_quality_label)
# 压缩说明
self.info_label = ttk.Label(self.quality_frame,
text="较低的质量会产生更小的文件,但可能降低可读性",
foreground="blue")
# 预设按钮
self.preset_frame = ttk.Frame(self.quality_frame)
self.low_quality_button = ttk.Button(self.preset_frame, text="低质量 (小文件)",
command=lambda: self.quality.set(30))
self.medium_quality_button = ttk.Button(self.preset_frame, text="中等质量",
command=lambda: self.quality.set(50))
self.high_quality_button = ttk.Button(self.preset_frame, text="高质量 (大文件)",
command=lambda: self.quality.set(80))
# 压缩按钮
self.compress_button = ttk.Button(self.main_frame, text="开始压缩",
command=self._compress_pdf,
style='Accent.TButton')
# 状态栏
self.status_bar = ttk.Label(self.root, text="就绪", anchor=tk.W, padding="5")
def _setup_layout(self) -> None:
self.menu_bar.add_cascade(label="文件", menu=self.file_menu)
self.menu_bar.add_cascade(label="帮助", menu=self.help_menu)
self.main_frame.pack(fill=tk.BOTH, expand=True)
self.file_frame.pack(fill=tk.X, pady=10)
self.file_path_label.pack(side=tk.LEFT, padx=5)
self.browse_button.pack(side=tk.RIGHT)
self.quality_frame.pack(fill=tk.X, pady=10)
self.quality_scale.pack(side=tk.LEFT, padx=5)
self.quality_value_label.pack(side=tk.LEFT, padx=5)
self.info_label.pack(anchor=tk.W, pady=5)
self.preset_frame.pack(fill=tk.X, pady=5)
self.low_quality_button.pack(side=tk.LEFT, padx=5)
self.medium_quality_button.pack(side=tk.LEFT, padx=5)
self.high_quality_button.pack(side=tk.LEFT, padx=5)
self.compress_button.pack(pady=20)
self.status_bar.pack(fill=tk.X, side=tk.BOTTOM)
# 绑定快捷键
self.root.bind("<Control-o>", lambda event: self._select_input_file())
# 设置样式
style = ttk.Style()
style.configure('Accent.TButton', font=('SimHei', 12, 'bold'))
def _update_quality_label(self, event=None) -> None:
"""更新质量标签显示"""
value = self.quality.get()
self.quality_value_label.config(text=f"{value}%")
def _select_input_file(self, event=None) -> None:
"""选择输入PDF文件"""
file_path = filedialog.askopenfilename(
title="选择PDF文件",
filetypes=[("PDF文件", "*.pdf"), ("所有文件", "*.*")]
)
if file_path:
self.input_file = file_path
self.file_path_label.config(text=os.path.basename(file_path))
self._update_status(f"已选择文件: {os.path.basename(file_path)}")
def _update_status(self, message: str) -> None:
"""更新状态栏消息"""
self.status_bar.config(text=message)
self.root.update_idletasks()
def _show_help(self) -> None:
"""显示帮助信息"""
help_text = """PDF图片压缩工具使用说明:
1. 选择文件:点击"浏览..."按钮选择要压缩的PDF文件
2. 设置质量:使用滑块或预设按钮设置压缩质量
3. 开始压缩:点击"开始压缩"按钮开始处理
4. 保存文件:选择压缩后PDF的保存位置
注意:
- 此工具通过将PDF转换为图片再重新组合来压缩文件
- 压缩质量越低,生成的文件越小,但可能会降低PDF的可读性
- 处理大型PDF文件可能需要较长时间"""
messagebox.showinfo("使用说明", help_text)
def _show_about(self) -> None:
"""显示关于信息"""
about_text = "PDF图片压缩工具 v1.0\n\n使用Python、PyMuPDF和PIL库开发的PDF压缩应用程序"
messagebox.showinfo("关于", about_text)
def _get_output_file_path(self) -> Optional[str]:
"""获取输出文件路径"""
if not self.input_file:
messagebox.showerror("错误", "请先选择要压缩的PDF文件")
return None
default_output = os.path.splitext(self.input_file)[0] + "_compressed.pdf"
output_file = filedialog.asksaveasfilename(
title="保存压缩后的PDF文件",
defaultextension=".pdf",
initialfile=os.path.basename(default_output),
filetypes=[("PDF文件", "*.pdf"), ("所有文件", "*.*")]
)
return output_file if output_file else None
def _get_file_size(self, file_path: str) -> str:
"""获取文件大小的友好显示"""
size = os.path.getsize(file_path)
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.2f} {unit}"
size /= 1024.0
return f"{size:.2f} TB"
def _try_repair_pdf(self, input_path: str) -> Optional[str]:
"""尝试修复损坏的PDF文件"""
try:
# 创建临时修复文件
temp_path = os.path.splitext(input_path)[0] + "_repaired.pdf"
# 尝试打开并保存文件以修复
with fitz.open(input_path) as doc:
doc.save(temp_path, garbage=4, deflate=True)
return temp_path
except Exception as e:
self._show_detailed_error("修复PDF文件时出错", e)
return None
def _show_detailed_error(self, title: str, error: Exception) -> None:
"""显示详细的错误信息,包括错误类型和代码行号"""
# 获取堆栈跟踪信息
exc_type, exc_value, exc_traceback = sys.exc_info()
stack_summary = traceback.extract_tb(exc_traceback)
# 提取最相关的错误行信息
error_line = None
for frame in reversed(stack_summary):
if frame.filename.endswith("pdf_image_compressor.py"):
error_line = frame
break
# 构建错误消息
error_message = f"错误类型: {type(error).__name__}\n\n"
error_message += f"错误信息: {str(error)}\n\n"
if error_line:
error_message += f"错误位置: 第 {error_line.lineno} 行\n"
error_message += f"代码: {error_line.line.strip()}\n\n"
# 添加建议的修复方法或相关信息
if "PIL" in str(error):
error_message += "提示: 此错误与图像处理有关。请确保已安装Pillow库: pip install pillow"
elif "PyMuPDF" in str(error):
error_message += "提示: 此错误与PDF处理有关。请确保已安装PyMuPDF库: pip install pymupdf"
error_message += "\n请复制此错误信息并提供给开发者以获取进一步帮助。"
# 显示错误对话框
messagebox.showerror(title, error_message)
def _pdf_to_images(self, pdf_path: str, quality: int) -> Tuple[Optional[str], int]:
"""将PDF转换为图片并返回临时文件夹路径"""
try:
# 创建临时文件夹
temp_dir = tempfile.mkdtemp(prefix="pdf_compress_")
# 打开PDF文件
doc = fitz.open(pdf_path)
total_pages = len(doc)
# 计算缩放因子
if quality < 40: # 低质量
zoom_x = 1.0 # 水平缩放因子
zoom_y = 1.0 # 垂直缩放因子
elif quality < 70: # 中等质量
zoom_x = 1.5 # 水平缩放因子
zoom_y = 1.5 # 垂直缩放因子
else: # 高质量
zoom_x = 2.0 # 水平缩放因子
zoom_y = 2.0 # 垂直缩放因子
mat = fitz.Matrix(zoom_x, zoom_y) # 缩放矩阵
# 逐页转换为图片
for page_num in range(total_pages):
page = doc.load_page(page_num)
pix = page.get_pixmap(matrix=mat)
# 构建图片路径
img_path = os.path.join(temp_dir, f"page_{page_num+1}.jpg")
# 保存图片
pix.save(img_path)
# 释放资源
del pix
# 更新状态
self._update_status(f"正在转换第 {page_num+1}/{total_pages} 页为图片...")
doc.close()
return temp_dir, total_pages
except Exception as e:
self._show_detailed_error("将PDF转换为图片时出错", e)
return None, 0
def _compress_images(self, img_dir: str, total_pages: int, quality: int) -> None:
"""压缩指定目录中的图片"""
try:
# 获取所有图片文件
img_files = sorted(
[f for f in os.listdir(img_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg'))],
key=lambda x: int(x.split('_')[1].split('.')[0])
)
# 压缩每张图片
for i, img_file in enumerate(img_files):
img_path = os.path.join(img_dir, img_file)
# 打开图片
img = Image.open(img_path)
# 转换为RGB模式(如果不是)
if img.mode != 'RGB':
img = img.convert('RGB')
# 计算目标尺寸
width, height = img.size
# 根据质量调整尺寸
if quality < 40: # 低质量:大幅缩小
scale = 0.5
elif quality < 70: # 中等质量:适度缩小
scale = 0.8
else: # 高质量:轻微缩小或不缩小
scale = 0.95
new_width = max(100, int(width * scale))
new_height = max(100, int(height * scale))
# 调整图片尺寸
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
# 保存压缩后的图片
img.save(img_path, quality=quality, optimize=True)
# 释放资源
img.close()
# 更新状态
self._update_status(f"正在压缩第 {i+1}/{total_pages} 页图片...")
except Exception as e:
self._show_detailed_error("压缩图片时出错", e)
raise
def _images_to_pdf(self, img_dir: str, output_path: str, total_pages: int) -> None:
"""将图片合并为PDF"""
try:
# 创建新的PDF文档
doc = fitz.open()
# 获取所有图片文件
img_files = sorted(
[f for f in os.listdir(img_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg'))],
key=lambda x: int(x.split('_')[1].split('.')[0])
)
# 将每张图片添加到PDF
for i, img_file in enumerate(img_files):
img_path = os.path.join(img_dir, img_file)
# 获取图片尺寸
img = Image.open(img_path)
width, height = img.size
img.close()
# 添加新页面
page = doc.new_page(width=width, height=height)
# 在页面上插入图片
page.insert_image(fitz.Rect(0, 0, width, height), filename=img_path)
# 更新状态
self._update_status(f"正在合并第 {i+1}/{total_pages} 页到PDF...")
# 保存PDF
doc.save(output_path)
doc.close()
except Exception as e:
self._show_detailed_error("将图片合并为PDF时出错", e)
raise
def _compress_pdf(self) -> None:
"""压缩PDF文件"""
# 防止重复处理
if self.is_processing:
return
output_file = self._get_output_file_path()
if not output_file:
return
try:
if not self.input_file:
messagebox.showerror("错误", "请先选择要压缩的PDF文件")
return
self.is_processing = True
self.compress_button.config(state=tk.DISABLED)
self._update_status("正在检查PDF文件...")
# 尝试打开PDF文件,检查是否有错误
pdf_to_process = self.input_file
repair_attempted = False
try:
# 先尝试普通打开
with fitz.open(self.input_file) as doc:
pass
except Exception as e:
# 尝试修复PDF
self._update_status("检测到PDF文件可能损坏,正在尝试修复...")
pdf_to_process = self._try_repair_pdf(self.input_file)
repair_attempted = True
if not pdf_to_process:
messagebox.showerror("错误", f"无法打开PDF文件: {str(e)}\n\n可能是文件已损坏。")
self.is_processing = False
self.compress_button.config(state=tk.NORMAL)
return
else:
messagebox.showinfo("提示", "已尝试修复PDF文件。将继续处理修复后的版本。")
self._update_status("正在准备压缩PDF文件...")
# 获取原始文件大小
original_size = self._get_file_size(self.input_file)
# 质量设置
quality = self.quality.get()
# 1. 将PDF转换为图片
self._update_status("开始将PDF转换为图片...")
img_dir, total_pages = self._pdf_to_images(pdf_to_process, quality)
if not img_dir:
raise Exception("无法创建临时图片文件夹")
# 2. 压缩图片
self._update_status("开始压缩图片...")
self._compress_images(img_dir, total_pages, quality)
# 3. 将图片合并为PDF
self._update_status("开始将图片合并为PDF...")
self._images_to_pdf(img_dir, output_file, total_pages)
# 删除临时文件夹
for file_name in os.listdir(img_dir):
file_path = os.path.join(img_dir, file_name)
os.remove(file_path)
os.rmdir(img_dir)
# 删除临时修复文件(如果有)
if repair_attempted and pdf_to_process != self.input_file:
os.remove(pdf_to_process)
# 获取压缩后文件大小
compressed_size = self._get_file_size(output_file)
# 计算压缩比例
original_bytes = os.path.getsize(self.input_file)
compressed_bytes = os.path.getsize(output_file)
reduction_percentage = (1 - compressed_bytes / original_bytes) * 100
result_message = (f"PDF处理完成!\n\n"
f"原始大小: {original_size}\n"
f"处理后大小: {compressed_size}\n"
f"大小变化: {reduction_percentage:.2f}%\n\n"
f"文件已保存到: {output_file}")
# 检查是否有实际压缩
if reduction_percentage < 5:
result_message += "\n\n注意: 文件大小变化较小,可能是因为PDF中主要是文本内容。"
self._update_status(f"PDF处理完成,文件已保存到: {output_file}")
messagebox.showinfo("成功", result_message)
except Exception as e:
# 显示详细的全局错误
self._show_detailed_error("处理PDF时发生错误", e)
self._update_status(f"错误: 处理PDF时发生错误: {str(e)}")
finally:
# 重置处理状态
self.is_processing = False
self.compress_button.config(state=tk.NORMAL)
def main() -> None:
"""主函数"""
try:
# 尝试导入必要的库
import fitz
from PIL import Image
except ImportError:
print("错误: 缺少必要的库。请安装PyMuPDF和Pillow库: pip install pymupdf pillow")
return
root = tk.Tk()
app = PDFImageCompressorApp(root)
root.mainloop()
if __name__ == "__main__":
main()
五、总结
AI 可以帮助我们做任何事情,只要你能想到。它就像一个无所不能的助手,等待着我们去挖掘它的潜力。这次用 AI 解决打印问题只是一个开始,未来我还会继续探索 AI 在工作和生活中的更多应用。
如果你也有类似的问题,或者想了解更多 AI 的实战经历,欢迎在下方留言关注。让我们一起探索 AI 的世界,利用 AI 工具创造更多的价值,期待与你一起进步!后面我会把PDF压缩的源码放到下载资源里,有需要的可以去下载。