一、转换解决方案
Office文件(doc、docx、xls、xlsx、ppt、pptx、txt七种格式)转换为PDF有很多办法,本文只从已实践的四种作解析说明。
1、调用Office自带组件服务转换
前提要求
- 服务器上必须装Office,最低2007(注:2007安装完成后,需要单独下载PDF转换插件,附件里有),最好是2010以上。
- 还需要在服务器上开启php的dcom扩展,参考网址如下:
注:32位的office配置office组件服务时,输入comexp.msc -32这个命令而不是dcomcnfg。
调用方法
此种方法本质上是PHP与C++语言的交互。Office从2010版以后,增加了转换PDF文件的COM插件(就是office的C++ SDK库),可以另存为PDF文件。如下图所示:
我们需要做的获取这个组件的API,然后调用即可。至于用什么语言,看自己需求吧。PHP的此处已经说过,文末会提供额外的python方式代码。
API地址及参考如下所示
https://docs.microsoft.com/zh-cn/office/vba/api/word.document.exportasfixedformat
缺陷
这种方式在实践中发现,带特殊对象的word文件,譬如公章等的非图片对象,用这个转换之后出现黑框的情况,所以只适用于普通的文档转换。而且转换文档偏慢。
2、采用开源Office软件libreOffice软件进行转换。
前提要求
服务器上需要安装libreOffice软件。下载地址:
https://zh-cn.libreoffice.org/download/download 在Path下配置环境变量
调用方法
参考:
缺陷
毕竟是开源软件,执行标准跟Microsoft Office不一样,所以转换之后颜色渲染粗细上会有差别,文字页数会有一定的偏移但并不影响(跟文档里的图片布局排版有关)。另外,对非图片对象的图片会自动转换为图片对象来保存,这点不错。
3、调用PDF虚拟打印机的方式转换
前提要求
客户端上安装PDF虚拟打印机,注意是客户端安装
目前虚拟打印机的厂商有很多家,除了Microsoft自带的,最权威的有Adobe acrobat即Adobe PDFMaker。
安装完成后,在office里会自动加载插件,保存选项里会多出一项:
还有一家是PDFCreator。官网可下载。下载地址:
https://www.pdfforge.org/pdfcreator
调用方法
- 客户端上安转打印机后,由客户端触发调用,然后再动态的传到服务器端。
- 目前调用的API暂时未找着,查到的都只是对PDF编辑、合并一类。如果有哪位大大找到了,还请劳烦告知我一声。此处先标记一下,以后再补充。
缺陷
几乎没有什么缺陷,最好的一种方式。唯一遗憾的是笔者未找到在服务器端安装后提供的API可供调用的。
二、 注意事项
- 转换完成后,如果字体乱码或不一致,则应该是服务器缺少字体所致,安装后即可解决。
- 带签章的文件转完后,签章盖住下方文字。原因是盖章之前未选择衬于文字下方。此处需告知客户在盖章前,必须选择“衬于文字下方”。
- PHP的exec函数执行错误。原因是需要在php.ini里开启exec扩展即可。
三、扩展
根据研究,Office转换为html展示,或html转换为word、pdf展示,皆可参考以上方案。
四、Python代码示例
参考了这位大大的文章,做了多线程的优化且增加了日志记录,如下:
import os
import pythoncom
import threading
import logging
from win32com.client import constants, gencache, DispatchEx
_Log = logging.getLogger("PDFConverter")
class PDFConverter:
def __init__(self, _pathname, _export_path=None, _is_async=True):
"""
:param _pathname: 输入的文件夹或文件路径
:param _export_path: 导出目录路径
:param _is_async: 是否异步执行
"""
self._pathname = _pathname
self._export_path = _export_path
self._is_async = _is_async
self._handle_postfix = ['doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'txt']
self._filename_list = list()
def _parse_filenames(self):
"""
解析路径,获取当前路径下的所有文件
"""
full_pathname = os.path.abspath(self._pathname)
if os.path.isfile(full_pathname):
if self._is_legal_postfix(self._pathname):
self._filename_list.append(full_pathname)
else:
raise TypeError('文件 {} 后缀名不合法!仅支持如下文件类型:{}。'.format(self._pathname, '、'.join(self._handle_postfix)))
elif os.path.isdir(full_pathname):
# 遍历的目录的地址, 返回的是一个三元组(root,dirs,files)
# real_path为当前正在遍历的这个文件夹的地址
# files为real_path文件夹中所有的文件
for real_path, _, files in os.walk(full_pathname):
for name in files:
filename = os.path.join(full_pathname, real_path, name)
if self._is_legal_postfix(filename):
self._filename_list.append(os.path.join(filename))
else:
raise TypeError('文件/文件夹 {} 不存在或不合法!'.format(self._pathname))
def _is_legal_postfix(self, filename):
return filename.split('.')[-1].lower() in self._handle_postfix and not os.path.basename(filename).startswith(
'~')
@staticmethod
def doc(input_file, export_file):
"""
doc 和 docx 文件转换
"""
pythoncom.CoInitialize()
# gencache.EnsureModule('{00020905-0000-0000-C000-000000000046}', 0, 8, 4)
# 使用启动独立的进程
wordApp = DispatchEx("PDFMakerAPI.PDFMakerApp")
try:
# wordApp.Visible = 0
# wordApp.DisplayAlerts = 0
# doc = wordApp.Documents.Open(input_file)
# doc.ExportAsFixedFormat(export_file, constants.wdExportFormatPDF,
# OptimizeFor=constants.wdExportOptimizeForPrint,
# Item=constants.wdExportDocumentWithMarkup,
# IncludeDocProps=True,
# CreateBookmarks=constants.wdExportCreateHeadingBookmarks)
# # 关闭
# doc.Close()
wordApp.CreatePDF(input_file, export_file)
except Exception as e:
_Log.error(input_file, "文件转换失败:", e)
finally:
if wordApp:
wordApp.Quit()
# 释放资源
pythoncom.CoUninitialize()
def docx(self, input_file, export_file):
self.doc(input_file, export_file)
def txt(self, input_file, export_file):
self.doc(input_file, export_file)
@staticmethod
def xls(input_file, export_file):
"""
xls 和 xlsx 文件转换
"""
pythoncom.CoInitialize()
# gencache.EnsureModule('{00020905-0000-0000-C000-000000000046}', 0, 8, 4)
xlsApp = DispatchEx("Excel.Application")
try:
xlsApp.Visible = 0
xlsApp.DisplayAlerts = 0
books = xlsApp.Workbooks.Open(input_file, False)
books.ExportAsFixedFormat(0, export_file)
books.Close(False)
except Exception as e:
_Log.error(input_file, "文件转换失败:", e)
finally:
if xlsApp:
xlsApp.Quit()
# 释放资源
pythoncom.CoUninitialize()
def xlsx(self, input_file, export_file):
self.xls(input_file, export_file)
@staticmethod
def ppt(input_file, export_file):
"""
ppt 和 pptx 文件转换
"""
pythoncom.CoInitialize()
pptApp = DispatchEx("PowerPoint.Application")
try:
pptApp.Visible = 0
pptApp.DisplayAlerts = 0
ppt = pptApp.Presentations.Open(input_file, False, False, False)
ppt.ExportAsFixedFormat(export_file, 2, PrintRange=None)
ppt.Close()
except Exception as e:
_Log.error(input_file, "文件转换失败:", e)
finally:
if pptApp:
pptApp.Quit()
# 释放资源
pythoncom.CoUninitialize()
def pptx(self, input_file, export_file):
self.ppt(input_file, export_file)
def converter(self):
"""
进行批量处理,根据后缀名调用函数执行转换
"""
_flag = 1
_export_file_path = ""
if self._export_path and os.path.isdir(self._export_path):
_export_file_path = self._export_path
else:
_flag = 0
# 解析目标文件路径,存入list,并确认最终导出的文件夹路径_export_file_path
try:
self._parse_filenames()
except TypeError as e:
_Log.error("解析目标文件/路径报错", e)
return 1
_Log.info('需要转换的文件数:%d' % len(self._filename_list))
# threads = []
for filename in self._filename_list:
postfix = filename.split('.')[-1].lower()
funcCall = getattr(self, postfix)
# self._input_file = filename
if _flag is 0:
_export_file_path = os.path.dirname(filename)
export_file = os.path.join(_export_file_path, str(os.path.splitext(filename)[0]) + '.pdf')
if self._is_async:
t = threading.Thread(target=funcCall, args=(filename, export_file))
# threads.append(t)
t.start()
else:
funcCall(filename, export_file)
# 开始调用start方法,同时开始所有线程
# for i in range(0, len(self._filename_list)):
# threads[i].start()
_Log.info('转换完成!')
return 0
if __name__ == "__main__":
# 支持文件夹批量导入,也支持单个文件的转换
# 默认异步导出到每个文件的对应文件下,也可指定相应目录
folder = 'tmp'
pathname = os.path.join(os.path.abspath('.'), folder)
# pathname = 'test.doc'
PDF = PDFConverter(pathname)
PDF.converter()
其中
gencache.EnsureModule('{00020905-0000-0000-C000-000000000046}', 0, 8, 4)
并未用到,也可以用gencache去启动进程,此处不做探究。
不足之处,还请各位多多指教