497 lines
12 KiB
Python
497 lines
12 KiB
Python
"""
|
||
图形化自动处理:
|
||
1. Word -> PDF -> 图片版PDF
|
||
2. PDF -> 图片版PDF
|
||
|
||
依赖:
|
||
pip install pymupdf pywin32
|
||
|
||
说明:
|
||
- 支持 .doc / .docx / .pdf
|
||
- 支持一次选择多个文件
|
||
- 输出图片版 PDF
|
||
- 自动清理临时文件
|
||
"""
|
||
|
||
import os
|
||
import time
|
||
import shutil
|
||
import threading
|
||
import tkinter as tk
|
||
from tkinter import filedialog, messagebox, scrolledtext
|
||
import fitz
|
||
import win32com.client
|
||
|
||
|
||
# =========================
|
||
# 核心转换函数
|
||
# =========================
|
||
|
||
def word_to_pdf_by_wps(word_path, pdf_path, log_func=print):
|
||
"""
|
||
WPS 转 PDF
|
||
"""
|
||
|
||
wps = None
|
||
doc = None
|
||
|
||
try:
|
||
try:
|
||
wps = win32com.client.Dispatch("KWPS.Application")
|
||
except Exception:
|
||
wps = win32com.client.Dispatch("wps.Application")
|
||
|
||
wps.Visible = False
|
||
|
||
doc = wps.Documents.Open(os.path.abspath(word_path))
|
||
|
||
time.sleep(1)
|
||
|
||
# 17 = PDF
|
||
doc.ExportAsFixedFormat(os.path.abspath(pdf_path), 17)
|
||
|
||
log_func(f"[OK] Word 转 PDF 完成: {pdf_path}")
|
||
|
||
finally:
|
||
try:
|
||
if doc:
|
||
doc.Close(False)
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
if wps:
|
||
wps.Quit()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def pdf_to_images(pdf_path, image_dir, zoom=2, log_func=print):
|
||
"""
|
||
PDF 每页转图片
|
||
"""
|
||
|
||
os.makedirs(image_dir, exist_ok=True)
|
||
|
||
doc = fitz.open(pdf_path)
|
||
|
||
image_paths = []
|
||
|
||
try:
|
||
for i in range(len(doc)):
|
||
page = doc.load_page(i)
|
||
|
||
pix = page.get_pixmap(
|
||
matrix=fitz.Matrix(zoom, zoom),
|
||
alpha=False
|
||
)
|
||
|
||
img_path = os.path.join(
|
||
image_dir,
|
||
f"page_{i + 1}.png"
|
||
)
|
||
|
||
pix.save(img_path)
|
||
|
||
image_paths.append(img_path)
|
||
|
||
log_func(f"[OK] 截图完成: {img_path}")
|
||
|
||
finally:
|
||
doc.close()
|
||
|
||
return image_paths
|
||
|
||
|
||
def images_to_pdf(image_paths, output_pdf_path, log_func=print):
|
||
"""
|
||
图片重新合成 PDF
|
||
"""
|
||
|
||
pdf = fitz.open()
|
||
|
||
try:
|
||
for img_path in image_paths:
|
||
img = fitz.open(img_path)
|
||
|
||
try:
|
||
rect = img[0].rect
|
||
|
||
page = pdf.new_page(
|
||
width=rect.width,
|
||
height=rect.height
|
||
)
|
||
|
||
page.insert_image(rect, filename=img_path)
|
||
|
||
finally:
|
||
img.close()
|
||
|
||
pdf.save(output_pdf_path, deflate=True)
|
||
|
||
finally:
|
||
pdf.close()
|
||
|
||
log_func(f"[OK] 图片版 PDF 已生成: {output_pdf_path}")
|
||
|
||
|
||
def safe_delete(path, log_func=print):
|
||
"""
|
||
删除文件或目录
|
||
"""
|
||
|
||
if not os.path.exists(path):
|
||
return
|
||
|
||
try:
|
||
if os.path.isfile(path):
|
||
os.remove(path)
|
||
log_func(f"[OK] 文件已删除: {path}")
|
||
|
||
elif os.path.isdir(path):
|
||
shutil.rmtree(path)
|
||
log_func(f"[OK] 文件夹已删除: {path}")
|
||
|
||
except Exception as e:
|
||
log_func(f"[警告] 删除失败: {path}")
|
||
log_func(f"原因: {e}")
|
||
|
||
|
||
def convert_one_file(input_path, output_dir, zoom=2, log_func=print):
|
||
"""
|
||
处理单个文件
|
||
"""
|
||
|
||
input_path = os.path.abspath(input_path)
|
||
|
||
if not os.path.exists(input_path):
|
||
raise FileNotFoundError(f"文件不存在: {input_path}")
|
||
|
||
ext = os.path.splitext(input_path)[1].lower()
|
||
|
||
base_name = os.path.splitext(os.path.basename(input_path))[0]
|
||
|
||
work_dir = output_dir if output_dir else os.path.dirname(input_path)
|
||
os.makedirs(work_dir, exist_ok=True)
|
||
|
||
image_dir = os.path.join(
|
||
work_dir,
|
||
base_name + "_images"
|
||
)
|
||
|
||
output_pdf_path = os.path.join(
|
||
work_dir,
|
||
base_name + "_图片版.pdf"
|
||
)
|
||
|
||
temp_pdf_path = os.path.join(
|
||
work_dir,
|
||
base_name + "_temp.pdf"
|
||
)
|
||
|
||
generated_temp_pdf = False
|
||
|
||
try:
|
||
if ext in [".doc", ".docx"]:
|
||
log_func("=" * 80)
|
||
log_func(f"[1] 检测到 Word 文档: {input_path}")
|
||
|
||
word_to_pdf_by_wps(
|
||
input_path,
|
||
temp_pdf_path,
|
||
log_func
|
||
)
|
||
|
||
pdf_path = temp_pdf_path
|
||
generated_temp_pdf = True
|
||
|
||
elif ext == ".pdf":
|
||
log_func("=" * 80)
|
||
log_func(f"[1] 检测到 PDF 文件: {input_path}")
|
||
|
||
pdf_path = input_path
|
||
|
||
else:
|
||
raise ValueError(f"不支持的文件类型: {ext}")
|
||
|
||
log_func("[2] 正在按页截图...")
|
||
|
||
image_paths = pdf_to_images(
|
||
pdf_path,
|
||
image_dir,
|
||
zoom=zoom,
|
||
log_func=log_func
|
||
)
|
||
|
||
if not image_paths:
|
||
raise RuntimeError("PDF 没有成功生成任何图片")
|
||
|
||
log_func("[3] 正在生成图片版 PDF...")
|
||
|
||
images_to_pdf(
|
||
image_paths,
|
||
output_pdf_path,
|
||
log_func=log_func
|
||
)
|
||
|
||
log_func("[4] 清理临时文件...")
|
||
|
||
safe_delete(image_dir, log_func)
|
||
|
||
if generated_temp_pdf:
|
||
safe_delete(temp_pdf_path, log_func)
|
||
|
||
log_func(f"[完成] 最终文件: {output_pdf_path}")
|
||
|
||
return output_pdf_path
|
||
|
||
except Exception as e:
|
||
log_func(f"[失败] {input_path}")
|
||
log_func(f"原因: {e}")
|
||
raise
|
||
|
||
|
||
# =========================
|
||
# 图形化界面
|
||
# =========================
|
||
|
||
class PdfImageApp:
|
||
|
||
def __init__(self, root):
|
||
self.root = root
|
||
self.root.title("Word / PDF 转图片版 PDF 工具")
|
||
self.root.geometry("900x600")
|
||
|
||
self.selected_files = []
|
||
self.output_dir = ""
|
||
|
||
self.build_ui()
|
||
|
||
def build_ui(self):
|
||
title_label = tk.Label(
|
||
self.root,
|
||
text="需要本地有wps \nWord / PDF 转图片版 PDF 工具",
|
||
font=("微软雅黑", 18, "bold")
|
||
)
|
||
title_label.pack(pady=10)
|
||
|
||
desc_label = tk.Label(
|
||
self.root,
|
||
text="支持 .doc / .docx / .pdf,可一次选择多个文件,自动生成图片版 PDF",
|
||
font=("微软雅黑", 10)
|
||
)
|
||
desc_label.pack()
|
||
|
||
button_frame = tk.Frame(self.root)
|
||
button_frame.pack(pady=10)
|
||
|
||
select_file_btn = tk.Button(
|
||
button_frame,
|
||
text="选择文件",
|
||
width=18,
|
||
command=self.select_files
|
||
)
|
||
select_file_btn.grid(row=0, column=0, padx=10)
|
||
|
||
select_output_btn = tk.Button(
|
||
button_frame,
|
||
text="选择输出目录",
|
||
width=18,
|
||
command=self.select_output_dir
|
||
)
|
||
select_output_btn.grid(row=0, column=1, padx=10)
|
||
|
||
start_btn = tk.Button(
|
||
button_frame,
|
||
text="开始转换",
|
||
width=18,
|
||
bg="#4CAF50",
|
||
fg="white",
|
||
command=self.start_convert
|
||
)
|
||
start_btn.grid(row=0, column=2, padx=10)
|
||
|
||
clear_btn = tk.Button(
|
||
button_frame,
|
||
text="清空日志",
|
||
width=18,
|
||
command=self.clear_log
|
||
)
|
||
clear_btn.grid(row=0, column=3, padx=10)
|
||
|
||
self.file_label = tk.Label(
|
||
self.root,
|
||
text="当前未选择文件",
|
||
fg="blue",
|
||
wraplength=850,
|
||
justify="left"
|
||
)
|
||
self.file_label.pack(pady=5)
|
||
|
||
self.output_label = tk.Label(
|
||
self.root,
|
||
text="输出目录:默认输出到原文件所在目录",
|
||
fg="purple",
|
||
wraplength=850,
|
||
justify="left"
|
||
)
|
||
self.output_label.pack(pady=5)
|
||
|
||
zoom_frame = tk.Frame(self.root)
|
||
zoom_frame.pack(pady=5)
|
||
|
||
# tk.Label(
|
||
# zoom_frame,
|
||
# text="清晰度 zoom:"
|
||
# ).pack(side=tk.LEFT)
|
||
#
|
||
# self.zoom_var = tk.StringVar(value="2")
|
||
#
|
||
# zoom_entry = tk.Entry(
|
||
# zoom_frame,
|
||
# textvariable=self.zoom_var,
|
||
# width=8
|
||
# )
|
||
# zoom_entry.pack(side=tk.LEFT)
|
||
#
|
||
# tk.Label(
|
||
# zoom_frame,
|
||
# text="建议:2 正常,3 更清晰但文件更大"
|
||
# ).pack(side=tk.LEFT, padx=10)
|
||
|
||
self.log_box = scrolledtext.ScrolledText(
|
||
self.root,
|
||
width=110,
|
||
height=25,
|
||
font=("Consolas", 10)
|
||
)
|
||
self.log_box.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
|
||
|
||
def log(self, msg):
|
||
self.log_box.insert(tk.END, msg + "\n")
|
||
self.log_box.see(tk.END)
|
||
self.root.update_idletasks()
|
||
|
||
def clear_log(self):
|
||
self.log_box.delete("1.0", tk.END)
|
||
|
||
def select_files(self):
|
||
files = filedialog.askopenfilenames(
|
||
title="请选择 Word 或 PDF 文件",
|
||
filetypes=[
|
||
("支持的文件", "*.doc *.docx *.pdf"),
|
||
("Word 文件", "*.doc *.docx"),
|
||
("PDF 文件", "*.pdf"),
|
||
("所有文件", "*.*")
|
||
]
|
||
)
|
||
|
||
if files:
|
||
self.selected_files = list(files)
|
||
|
||
self.file_label.config(
|
||
text=f"已选择 {len(self.selected_files)} 个文件:\n" +
|
||
"\n".join(self.selected_files)
|
||
)
|
||
|
||
self.log(f"[选择文件] 共选择 {len(self.selected_files)} 个文件")
|
||
|
||
def select_output_dir(self):
|
||
output_dir = filedialog.askdirectory(
|
||
title="请选择输出目录"
|
||
)
|
||
|
||
if output_dir:
|
||
self.output_dir = output_dir
|
||
self.output_label.config(
|
||
text=f"输出目录:{self.output_dir}"
|
||
)
|
||
self.log(f"[输出目录] {self.output_dir}")
|
||
|
||
def start_convert(self):
|
||
if not self.selected_files:
|
||
messagebox.showwarning(
|
||
"提示",
|
||
"请先选择一个或多个 Word / PDF 文件"
|
||
)
|
||
return
|
||
|
||
try:
|
||
zoom = float(self.zoom_var.get())
|
||
if zoom <= 0:
|
||
raise ValueError
|
||
except Exception:
|
||
messagebox.showwarning(
|
||
"提示",
|
||
"zoom 必须是大于 0 的数字,例如 2 或 3"
|
||
)
|
||
return
|
||
|
||
t = threading.Thread(
|
||
target=self.convert_files_thread,
|
||
args=(zoom,),
|
||
daemon=True
|
||
)
|
||
t.start()
|
||
|
||
def convert_files_thread(self, zoom):
|
||
success_files = []
|
||
failed_files = []
|
||
|
||
self.log("")
|
||
self.log("========== 开始批量转换 ==========")
|
||
|
||
for index, file_path in enumerate(self.selected_files, start=1):
|
||
self.log("")
|
||
self.log(f"[任务 {index}/{len(self.selected_files)}] {file_path}")
|
||
|
||
try:
|
||
output_pdf = convert_one_file(
|
||
input_path=file_path,
|
||
output_dir=self.output_dir,
|
||
zoom=zoom,
|
||
log_func=self.log
|
||
)
|
||
|
||
success_files.append(output_pdf)
|
||
|
||
except Exception as e:
|
||
failed_files.append((file_path, str(e)))
|
||
|
||
self.log("")
|
||
self.log("========== 批量转换结束 ==========")
|
||
self.log(f"[成功] {len(success_files)} 个")
|
||
self.log(f"[失败] {len(failed_files)} 个")
|
||
|
||
if success_files:
|
||
self.log("")
|
||
self.log("生成的图片版 PDF:")
|
||
for f in success_files:
|
||
self.log(f)
|
||
|
||
if failed_files:
|
||
self.log("")
|
||
self.log("失败文件:")
|
||
for f, reason in failed_files:
|
||
self.log(f"{f} -> {reason}")
|
||
|
||
if failed_files:
|
||
messagebox.showwarning(
|
||
"转换完成",
|
||
f"处理完成。\n\n成功:{len(success_files)} 个\n失败:{len(failed_files)} 个\n\n请查看日志。\n\n请使用生成的“图片版 PDF”文件。"
|
||
)
|
||
else:
|
||
messagebox.showinfo(
|
||
"转换完成",
|
||
f"全部处理完成。\n\n成功生成 {len(success_files)} 个图片版 PDF。\n\n请使用生成的“图片版 PDF”文件。"
|
||
)
|
||
|
||
|
||
def main():
|
||
root = tk.Tk()
|
||
app = PdfImageApp(root)
|
||
root.mainloop()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |