批量PPT文件夹原文件夹转换PDF GUI
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys,os,warnings,threading,tempfile,subprocess,time,shutil,platform
from pathlib import Path
from tkinter import *
from tkinter import ttk,filedialog,messagebox
from tkinter.scrolledtext import ScrolledText
from PIL import Image
os.environ['PYTHONWARNINGS']='ignore'
os.environ['PYTHONDONTWRITEBYTECODE']='1'
warnings.filterwarnings('ignore')
IS_WIN=platform.system()=='Windows'
HAS_WIN32COM=False
try:
import win32com.client,pythoncom
HAS_WIN32COM=True
except:pass
class Ctrl:
def __init__(self):
self.pause=threading.Event()
self.pause.set()
self.stop=False
self.running=False
def pause_(self):self.pause.clear()
def resume(self):self.pause.set()
def stop_(self):self.stop=True;self.pause.set()
def reset(self):self.pause.set();self.stop=False;self.running=False
def check(self):
if self.stop:return False
self.pause.wait()
return not self.stop
def should_stop(self):return self.stop
class PPTConv:
def __init__(self,log=None,prog=None):
self.log=log or (lambda x:None)
self.prog=prog or (lambda c,t:None)
self.ctrl=Ctrl()
self.tools={'ms_ppt':False,'libre':None}
self._checked=False
def _check_tools(self):
if self._checked:return
self._checked=True
self.log("检测Office组件...")
if HAS_WIN32COM:
try:pythoncom.CoInitialize()
except:pass
try:
pa=win32com.client.Dispatch("PowerPoint.Application")
pa.Quit()
self.tools['ms_ppt']=True
self.log(" Microsoft PowerPoint: 可用")
except:self.log(" Microsoft PowerPoint: 不可用")
libre_paths=[r"C:\Program Files\LibreOffice\program\soffice.exe",
r"C:\Program Files (x86)\LibreOffice\program\soffice.exe"]
for p in libre_paths:
if os.path.exists(p):
self.tools['libre']=p
self.log(" LibreOffice: 可用")
break
if not self.tools['ms_ppt'] and not self.tools['libre']:
self.log(" 警告: 未检测到可用的转换工具!")
self.log(" 请安装 Microsoft PowerPoint 或 LibreOffice")
def _ppt_export_pdf(self,ppt_path,pdf_path):
if self.tools['ms_ppt'] and HAS_WIN32COM:
pa=None
try:
self.log(" 使用 PowerPoint...")
pythoncom.CoInitialize()
pa=win32com.client.DispatchEx("PowerPoint.Application")
pres=pa.Presentations.Open(os.path.abspath(ppt_path),ReadOnly=True,Untitled=False,WithWindow=False)
pres.SaveAs(os.path.abspath(pdf_path),32)
pres.Close()
self.log(" PowerPoint导出完成")
return True
except Exception as e:
self.log(f" PowerPoint失败: {e}")
return False
finally:
if pa:
try:pa.Quit()
except:pass
try:pythoncom.CoUninitialize()
except:pass
if self.tools['libre']:
try:
self.log(" 使用 LibreOffice...")
od=os.path.dirname(os.path.abspath(pdf_path))
subprocess.run([self.tools['libre'],'--headless','--convert-to','pdf','--outdir',od,os.path.abspath(ppt_path)],capture_output=True,timeout=300)
exp=os.path.join(od,Path(ppt_path).stem+'.pdf')
if os.path.exists(exp):
if exp!=pdf_path:shutil.move(exp,pdf_path)
return True
except:pass
return False
def convert_file(self,ppt_path):
try:
ppt_path=os.path.abspath(ppt_path)
pdf_path=os.path.join(os.path.dirname(ppt_path),Path(ppt_path).stem+'.pdf')
self.log(f"转换: {os.path.basename(ppt_path)}")
self._check_tools()
if not self.tools['ms_ppt'] and not self.tools['libre']:
self.log(" 错误: 没有可用的转换工具")
return False
if self._ppt_export_pdf(ppt_path,pdf_path):
self.log(f" 成功: {pdf_path}")
return True
else:
self.log(" 失败: 导出失败")
return False
except Exception as e:
self.log(f"错误: {e}")
return False
def convert_folder(self,folder_path,recursive=False):
folder_path=os.path.abspath(folder_path)
self.log(f"="*50)
self.log(f"扫描文件夹: {folder_path}")
ppt_files=[]
if recursive:
for root,dirs,files in os.walk(folder_path):
for f in files:
if f.lower().endswith(('.pptx','.ppt')):
ppt_files.append(os.path.join(root,f))
else:
for f in os.listdir(folder_path):
fp=os.path.join(folder_path,f)
if os.path.isfile(fp) and f.lower().endswith(('.pptx','.ppt')):
ppt_files.append(fp)
if not ppt_files:
self.log(" 未找到PPT文件")
return 0
self.log(f" 找到 {len(ppt_files)} 个PPT文件")
self.log("-"*50)
success=0
for i,f in enumerate(ppt_files,1):
if not self.ctrl.check():break
self.prog(i,len(ppt_files))
if self.convert_file(f):success+=1
self.prog(i,len(ppt_files))
self.log("-"*50)
self.log(f"完成: {success}/{len(ppt_files)} 个文件转换成功")
return success
class MultiFolderSelector(Toplevel):
def __init__(self,parent,initial_path=None):
super().__init__(parent)
self.title("选择文件夹")
self.geometry("500x450")
self.resizable(True,True)
self.update_idletasks()
x=(self.winfo_screenwidth()//2)-(500//2)
y=(self.winfo_screenheight()//2)-(450//2)
self.geometry(f"500x450+{x}+{y}")
self.selected_folders=[]
self.initial_path=initial_path or os.path.expanduser("~")
self._build_ui()
self._load_folders(self.initial_path)
self.transient(parent)
self.grab_set()
def _build_ui(self):
mf=ttk.Frame(self,padding="10")
mf.pack(fill=BOTH,expand=True)
pf=ttk.Frame(mf)
pf.pack(fill=X,pady=(0,10))
ttk.Label(pf,text="选择父文件夹:").pack(side=LEFT)
self.path_var=StringVar(value=self.initial_path)
self.entry=ttk.Entry(pf,textvariable=self.path_var,width=40)
self.entry.pack(side=LEFT,padx=5,fill=X,expand=True)
ttk.Button(pf,text="浏览",command=self._browse,width=8).pack(side=LEFT)
ttk.Label(mf,text="按住 Ctrl/Shift 键可多选子文件夹,或双击进入子文件夹",foreground="gray",font=('微软雅黑',9)).pack(pady=(0,5))
lf=ttk.Frame(mf)
lf.pack(fill=BOTH,expand=True)
sy=ttk.Scrollbar(lf)
sy.pack(side=RIGHT,fill=Y)
sx=ttk.Scrollbar(lf,orient=HORIZONTAL)
sx.pack(side=BOTTOM,fill=X)
self.lb=Listbox(lf,font=('Consolas',10),selectmode=EXTENDED,yscrollcommand=sy.set,xscrollcommand=sx.set)
self.lb.pack(fill=BOTH,expand=True)
sy.config(command=self.lb.yview)
sx.config(command=self.lb.xview)
self.lb.bind('<Double-Button-1>',self._enter)
bf=ttk.Frame(mf)
bf.pack(fill=X,pady=(10,0))
ttk.Button(bf,text="添加选中文件夹",command=self._add_sel,width=15).pack(side=LEFT,padx=5)
ttk.Button(bf,text="添加全部",command=self._add_all,width=12).pack(side=LEFT,padx=5)
ttk.Button(bf,text="取消",command=self.destroy,width=10).pack(side=RIGHT,padx=5)
self.status=ttk.Label(bf,text="",foreground="blue")
self.status.pack(side=RIGHT,padx=10)
def _browse(self):
f=filedialog.askdirectory(initialdir=self.path_var.get())
if f:
self.path_var.set(f)
self._load_folders(f)
def _load_folders(self,parent):
self.lb.delete(0,END)
if not os.path.isdir(parent):return
subs=[]
try:
for i in os.listdir(parent):
if os.path.isdir(os.path.join(parent,i)):
subs.append(i)
except:pass
subs.sort(key=str.lower)
if subs:
self.lb.insert(END,".. (上级目录)")
for s in subs:self.lb.insert(END,s)
self.status.config(text=f"{len(subs)} 个子文件夹")
else:
self.lb.insert(END,"(无子文件夹)")
self.status.config(text="无子文件夹")
def _enter(self,e=None):
sel=self.lb.curselection()
if not sel:return
name=self.lb.get(sel[0])
if name==".. (上级目录)":
p=os.path.dirname(self.path_var.get())
if p and os.path.isdir(p):
self.path_var.set(p)
self._load_folders(p)
else:
np=os.path.join(self.path_var.get(),name)
if os.path.isdir(np):
self.path_var.set(np)
self._load_folders(np)
def _add_sel(self):
sel=self.lb.curselection()
if not sel:
messagebox.showwarning("提示","请先选择文件夹")
return
p=self.path_var.get()
for i in sel:
name=self.lb.get(i)
if name in [".. (上级目录)","(无子文件夹)"]:continue
fp=os.path.join(p,name)
if fp not in self.selected_folders:
self.selected_folders.append(fp)
self.destroy()
def _add_all(self):
p=self.path_var.get()
added=0
for i in range(self.lb.size()):
name=self.lb.get(i)
if name in [".. (上级目录)","(无子文件夹)"]:continue
fp=os.path.join(p,name)
if fp not in self.selected_folders:
self.selected_folders.append(fp)
added+=1
if added>0:self.destroy()
else:messagebox.showinfo("提示","没有可添加的文件夹")
class GUI:
def __init__(self,root):
self.root=root
self.root.title("PPT批量转PDF工具")
self.root.geometry("900x700")
self.root.minsize(800,600)
try:ttk.Style().theme_use('vista')
except:
try:ttk.Style().theme_use('clam')
except:pass
self.folders=[]
self.counts={}
self._build_ui()
self.conv=PPTConv(self.log,self._prog)
self.log("程序启动")
self.log("="*50)
threading.Thread(target=self._check_tools_thread,daemon=True).start()
def _check_tools_thread(self):
time.sleep(0.5)
self.conv._check_tools()
def _build_ui(self):
mf=ttk.Frame(self.root,padding="10")
mf.pack(fill=BOTH,expand=True)
ttk.Label(mf,text="PPT批量转PDF工具",font=('微软雅黑',16,'bold')).pack(pady=(0,10))
ff=ttk.LabelFrame(mf,text="文件夹列表",padding="10")
ff.pack(fill=BOTH,expand=True,pady=(0,10))
lf=ttk.Frame(ff)
lf.pack(fill=BOTH,expand=True)
sb=ttk.Scrollbar(lf)
sb.pack(side=RIGHT,fill=Y)
self.lb=Listbox(lf,font=('Consolas',10),yscrollcommand=sb.set,selectmode=EXTENDED)
self.lb.pack(fill=BOTH,expand=True)
sb.config(command=self.lb.yview)
bf=ttk.Frame(ff)
bf.pack(fill=X,pady=(10,0))
ttk.Button(bf,text="添加文件夹",command=self.add_folder,width=12).pack(side=LEFT,padx=5)
ttk.Button(bf,text="多选添加",command=self.add_multi,width=12).pack(side=LEFT,padx=5)
ttk.Button(bf,text="移除选中",command=self.remove_sel,width=12).pack(side=LEFT,padx=5)
ttk.Button(bf,text="清空列表",command=self.clear_all,width=12).pack(side=LEFT,padx=5)
self.stats=ttk.Label(bf,text="共 0 个文件夹",foreground="gray")
self.stats.pack(side=RIGHT,padx=10)
of=ttk.LabelFrame(mf,text="选项",padding="10")
of.pack(fill=X,pady=(0,10))
self.rec=BooleanVar(value=False)
ttk.Checkbutton(of,text="包含子文件夹",variable=self.rec).pack(side=LEFT,padx=20)
self.ov=BooleanVar(value=False)
ttk.Checkbutton(of,text="覆盖已存在的PDF",variable=self.ov).pack(side=LEFT,padx=20)
pf=ttk.Frame(mf)
pf.pack(fill=X,pady=(0,10))
self.pv=DoubleVar()
self.pb=ttk.Progressbar(pf,variable=self.pv,maximum=100)
self.pb.pack(fill=X,side=LEFT,expand=True,padx=(0,10))
self.pl=ttk.Label(pf,text="就绪",width=20)
self.pl.pack(side=RIGHT)
cf=ttk.Frame(mf)
cf.pack(fill=X,pady=(0,10))
self.start_btn=ttk.Button(cf,text="开始转换",command=self.start,width=15)
self.start_btn.pack(side=LEFT,padx=5)
ttk.Button(cf,text="暂停",command=self.pause,width=10).pack(side=LEFT,padx=5)
ttk.Button(cf,text="继续",command=self.resume,width=10).pack(side=LEFT,padx=5)
ttk.Button(cf,text="终止",command=self.stop,width=10).pack(side=LEFT,padx=5)
ttk.Button(cf,text="清除日志",command=lambda:self.log_text.delete(1.0,END),width=10).pack(side=RIGHT,padx=5)
lf2=ttk.LabelFrame(mf,text="日志",padding="5")
lf2.pack(fill=BOTH,expand=True)
self.log_text=ScrolledText(lf2,height=10,font=('Consolas',9),bg='#1e1e1e',fg='#d4d4d4')
self.log_text.pack(fill=BOTH,expand=True)
def add_folder(self):
f=filedialog.askdirectory(title="选择包含PPT文件的文件夹")
if f and f not in self.folders:
cnt=self._count_ppt(f,self.rec.get())
self.folders.append(f)
self.counts[f]=cnt
self.lb.insert(END,f"{os.path.basename(f)} ({cnt}个PPT)")
self.lb.itemconfig(END,fg='green' if cnt>0 else 'red')
self._upd_stats()
self.log(f"添加: {f} ({cnt}个PPT)")
elif f: self.log(f"跳过(已存在): {f}")
def add_multi(self):
d=MultiFolderSelector(self.root)
self.root.wait_window(d)
if d.selected_folders:
added=0
for f in d.selected_folders:
if f not in self.folders:
cnt=self._count_ppt(f,self.rec.get())
self.folders.append(f)
self.counts[f]=cnt
self.lb.insert(END,f"{f} ({cnt}个PPT)")
self.lb.itemconfig(END,fg='green' if cnt>0 else 'red')
self.log(f"添加: {f} ({cnt}个PPT)")
added+=1
else: self.log(f"跳过(已存在): {f}")
if added>0:
self._upd_stats()
messagebox.showinfo("提示",f"成功添加 {added} 个文件夹")
def _count_ppt(self,folder,recursive):
c=0
if recursive:
for r,_,fs in os.walk(folder):
for fn in fs:
if fn.lower().endswith(('.pptx','.ppt')):c+=1
else:
for fn in os.listdir(folder):
if os.path.isfile(os.path.join(folder,fn)) and fn.lower().endswith(('.pptx','.ppt')):c+=1
return c
def _upd_stats(self):
total=len(self.folders)
ppts=sum(self.counts.values())
self.stats.config(text=f"共 {total} 个文件夹, {ppts} 个PPT")
def remove_sel(self):
sel=self.lb.curselection()
if not sel:
messagebox.showwarning("提示","请先选择要移除的文件夹")
return
for i in reversed(sel):
f=self.folders[i]
self.log(f"移除: {f}")
del self.counts[f]
del self.folders[i]
self.lb.delete(i)
self._upd_stats()
def clear_all(self):
if self.folders and messagebox.askyesno("确认","确定要清空所有文件夹吗?"):
self.folders.clear()
self.counts.clear()
self.lb.delete(0,END)
self._upd_stats()
self.log("已清空所有文件夹")
def start(self):
if self.conv.ctrl.running:
messagebox.showwarning("提示","正在转换中,请稍候")
return
if not self.folders:
messagebox.showwarning("提示","请先添加文件夹")
return
self.conv.ctrl.reset()
self.pv.set(0)
self.pl.config(text="处理中...")
self.start_btn.config(state='disabled')
threading.Thread(target=self._conv_thread,daemon=True).start()
def _conv_thread(self):
try:
total_ppts=sum(self.counts.values())
if total_ppts==0:
self.log("没有找到任何PPT文件")
self._done()
return
self.log(f"开始转换 {len(self.folders)} 个文件夹中的 {total_ppts} 个PPT文件")
self.log("="*50)
processed=0
for f in self.folders:
if self.conv.ctrl.should_stop():break
cnt=self.counts.get(f,0)
if cnt==0:continue
self.conv.convert_folder(f,recursive=self.rec.get())
processed+=cnt
p=(processed/total_ppts)*100
self.pv.set(p)
self.pl.config(text=f"{processed}/{total_ppts} ({p:.0f}%)")
self.pv.set(100)
self.log("="*50)
self.log("所有转换完成!")
except Exception as e:
self.log(f"错误: {e}")
finally:
self._done()
def _done(self):
self.root.after(0,lambda:setattr(self.conv.ctrl,'running',False) or self.start_btn.config(state='normal') or self.pl.config(text="完成"))
def pause(self):
if self.conv.ctrl.running:
self.conv.ctrl.pause_()
self.log("已暂停")
def resume(self):
self.conv.ctrl.resume()
self.log("继续转换")
def stop(self):
if self.conv.ctrl.running:
self.conv.ctrl.stop_()
self.log("正在终止...")
def log(self,msg):
def _log():
try:
self.log_text.insert(END,msg+"\n")
self.log_text.see(END)
except:pass
try:self.root.after(0,_log)
except:print(msg)
def _prog(self,c,t):
def _up():
try:
p=(c/t*100) if t>0 else 0
self.pv.set(p)
self.pl.config(text=f"{c}/{t} ({p:.0f}%)")
except:pass
try:self.root.after(0,_up)
except:pass
def main():
root=Tk()
app=GUI(root)
root.update_idletasks()
w=root.winfo_width()
h=root.winfo_height()
x=(root.winfo_screenwidth()//2)-(w//2)
y=(root.winfo_screenheight()//2)-(h//2)
root.geometry(f'{w}x{h}+{x}+{y}')
root.mainloop()
if __name__=="__main__":
main()
一键打包成EXE
pyinstaller --onefile --windowed ppt_to_pdf_converter.py
11111
版权声明:本文为原创文章,版权归 零下一度 所有,转载请联系博主获得授权。
本文地址:https://www.lxyd.cn/PPT2PDF.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。
评论功能已关闭