.NET 采用开源软件OpenOffice 实现文档转码服务(word ppt excel)转PDF
前言
前几年做了个项目,里面有个需求,需要在浏览器中在线浏览word excel ppt pdf等文档。
最近又开始研究并记录下来
当时方案:
- 因为浏览器中阅读文档暂时只能通过pdf方式读取,所以就要想办法实现 word excel ppt 转为pdf文件实现在线浏览。
- 考虑到文件的安全性问题,一些在线的Saas服务就不考虑了,定制化本地安装的saas服务又不现实。
- .net 中已有一些组件可以实现word 转pdf了 如aspose.net , spire.doc for .net 等等,不过这些都是收费的。
- 微软的Office 也有提供com组件实现文档转码服务,前提是必须在Windows服务器上安装Office, 但Office同样需要license
- 考虑到成本问题。
最后采用了开源 OpenOffice +OpenOffice SDK 部署在Windows服务器中实现该需求
必要前提:
- 在windows服务器 framework 4 因为是好些年前的项目了,当时采用的是.net framework 4.6.1, Linux系统倒是没试过。
- OpenOffice 软件
- OpenOffice SDK 必须保证版本一致,否则会有问题。
正文:
以下是两个中间件服务
服务类型 | 服务名称 | 简称 | 描述 |
Windows Service | Convert trigger Service | CTS | 目的是来监控输入文件夹,当文件夹{InputFolder}中存在文件后,会出发转码操作。 |
Windows Console App | Convert Service | CS | 执行转码操作,会将{InputFolder}文件夹下的文件进行转码,并放置到{OutputFolder}目录下。 |
CTS服务采用Process类 调用CS 服务
以下是物理架构的关系图:
下面是CS服务中执行转码的核心代码。
1 public class OpenOfficeHelper : IOpenOffice 2 { 3 // For thread safety 4 private Mutex _openOfficeLock; 5 6 /// <summary> 7 /// constructor 8 /// </summary> 9 public OpenOfficeHelper() 10 { 11 _openOfficeLock = new Mutex(false, "OpenOfficeMutexLock-MiloradCavic"); 12 } 13 14 /// <summary> 15 /// Converts document to PDF 16 /// </summary> 17 /// <param name="sourcePath">Path to document to convert(e.g: C:\test.doc)</param> 18 /// <param name="destinationPath">Path on which to save PDF (e.g: C:\test.pdf)</param> 19 /// <returns>Path to destination file if operation is successful, or Exception text if it is not</returns> 20 public void ConvertDocToPDF(string sourcePath, string destinationPath) 21 { 22 bool obtained = _openOfficeLock.WaitOne(60 * 1000, false); 23 24 XComponent xComponent = null; 25 try 26 { 27 if (!obtained) 28 { 29 throw new System.Exception(string.Format("Request for using OpenOffice wasn't served after {0} seconds. Aborting...", 30)); 30 } 31 32 sourcePath = PathConverter(sourcePath); 33 destinationPath = PathConverter(destinationPath); 34 35 // 载入文件前属性设定,设定文件开启时隐藏 36 PropertyValue[] loadDesc = new PropertyValue[1]; 37 loadDesc[0] = new PropertyValue(); 38 loadDesc[0].Name = "Hidden"; 39 loadDesc[0].Value = new uno.Any(true); 40 41 //Get a ComponentContext 42 unoidl.com.sun.star.uno.XComponentContext xLocalContext = uno.util.Bootstrap.bootstrap(); 43 44 //Get MultiServiceFactory 45 unoidl.com.sun.star.lang.XMultiServiceFactory xRemoteFactory = (unoidl.com.sun.star.lang.XMultiServiceFactory)xLocalContext.getServiceManager(); 46 47 //Get a CompontLoader 48 XComponentLoader aLoader = (XComponentLoader)xRemoteFactory.createInstance("com.sun.star.frame.Desktop"); 49 50 //Load the sourcefile 51 xComponent = aLoader.loadComponentFromURL(sourcePath, "_blank", 0, new unoidl.com.sun.star.beans.PropertyValue[0]); 52 53 //Wait for loading 54 while (xComponent == null) 55 { 56 Thread.Sleep(3000); 57 } 58 59 SaveDocument(xComponent, destinationPath); 60 61 xComponent.dispose(); 62 63 } 64 catch (System.Exception ex) 65 { 66 throw ex; 67 } 68 finally 69 { 70 Process[] pt = Process.GetProcessesByName("soffice.bin"); 71 if (pt != null && pt.Length > 0) 72 { 73 foreach (var item in pt) 74 { 75 item.Kill(); 76 } 77 } 78 if (obtained) 79 { 80 _openOfficeLock.ReleaseMutex(); 81 } 82 } 83 } 84 85 /// <summary> 86 /// 执行保存 87 /// </summary> 88 /// <param name="xComponent">The x component.</param> 89 /// <param name="filePath">Name of the file.</param> 90 private void SaveDocument(XComponent xComponent, string filePath) 91 { 92 unoidl.com.sun.star.beans.PropertyValue[] propertyValue = new unoidl.com.sun.star.beans.PropertyValue[1]; 93 94 propertyValue[0] = new unoidl.com.sun.star.beans.PropertyValue(); 95 propertyValue[0].Name = "FilterName"; 96 propertyValue[0].Value = new uno.Any("writer_pdf_Export"); 97 98 ((XStorable)xComponent).storeToURL(filePath, propertyValue); 99 } 100 101 /// <summary> 102 /// Convert into OO file format 103 /// </summary> 104 /// <param name="file">The file.</param> 105 /// <returns>The converted file</returns> 106 private static string PathConverter(string file) 107 { 108 try 109 { 110 file = file.Replace(@"\", "/"); 111 112 return "file:///" + file; 113 } 114 catch (System.Exception ex) 115 { 116 throw ex; 117 } 118 } 119 120 121 }
原理其实就是 调用了OpenOffice 软件,另存为成PDF文件。
CTS服务的代码就不放出来,其实就是起一个Timer 定时器,定时监控 {InputFolder}文件夹下是否存在待转码文件, 存在,则起一个Process 实例 执行CS应用进行转码操作即可。
踩坑记录:
接下来就是遇到的坑了
- 当执行第一次转码操作时,CS服务会调用OpenOffice软件,界面屏幕会弹出一个弹窗(这个弹出只会弹出一次,不会弹出了),这个弹窗内容是需要填写的基本名称,否则会导致OpenOffice一致停留在这个界面
- 但我们CTS服务默认是以Local System 账户运行的,而CS服务的启动是由 Windows Service 触发的, 所以OpenOffice软件其实是由Local System用户打开的,但Local System 打开没有界面弹窗的,无法填写,也就导致无法转码了。如何证明呢,看第三点。
- 查看任务管理器发现其实 OpenOffice 软件已经打开(进程为soffice.bin进程),而且运行用户正好就是Local System。
解决方案有两种:
- 新建一个Windows用户DocConverter,将该用户放到管理员组下,然后以该用户登录windows后,打开OpenOffice,第一次弹窗后 填写对应的基本信息后,将Windows Service 启动用户改为DocCoverter用户,然后再启动转码服务。 这时候会发现已经能够正常工作了。
- 想办法以Local System用户身份打开一次OpenOffice,然后填写OpenOffice的基本信息即可,怎么打开呢,这里借助PsTools工具,以cmd命令行模式打开即可, 下载PSTools,ps工具包 点我下载
(1)打开压缩包,将里面的psexec.exe复制到System32文件夹下(64位用户请将psexec64.exe复制到SysWOW64文件夹下)
(2)以管理员身份运行命令提示符,输入"psexec -i -d -s cmd.exe"(64位用户类似),等待1~2秒后,就会出现以system权限运行的命令提示符了
(3)在被启动的命令提示符里输入命令"whoami"并回车,会发现返回一条信息为"nt authority\system",说明此命令提示符已以本地系统的身份运行了。
基本上就是这样。
参考: