Windows服务-Office转PDF文件

一. 应用场景

开发一个课件在线学习功能,要求将WORD, EXCEL, PPT类型课件可在线打开学习;最初设想使用第三方office插件,无奈价格太高放弃使用;
我们最终的方案是:利用office自身的另存为功能,在服务器将上传的office文件转化为pdf格式,然后网页打开pdf文件实现在线学习功能;

项目实现方案:新建一个windows自启的service安装在服务器,这个服务里面会建立一个HttpListener,用于监听文件转换的请求,请求来了后交给ServiceHandle处理,ServiceHandle会调用ConvertHelper把指定目录下的office文件转化为pdf,并放在相同目录下,以供在线学习使用;

二. 工具及环境

  1. VS2019
  2. Office:本地开发2016版;服务器2010版
  3. 开发环境 win10
  4. 服务器环境 windows server 2012 R2
  5. VS开发使用到的nuget包:NetOffice

三. 创建windows server项目

VS2012下开发Windows服务, 参考地址:
https://www.cnblogs.com/zhy-1992/p/6515850.html

基本按照这个思路开发出一个基础service完全没问题;这里需要提醒下的是:项目的文件路径不要带有空格,否则在执行bat批处理操作时会出现问题。

四. 使用NetOffice实现文件转换

上面只是创建一个服务, 真正转换的核心功能还没开始,这时我们需要利用NetOffice插件;
项目目录:


image.png

利用nuget包管理器安装所需包,这是项目的packages.config文件:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="NetOffice.Core" version="1.7.4.4" targetFramework="net452" />
  <package id="NetOffice.Excel" version="1.7.4.4" targetFramework="net452" />
  <package id="NetOffice.PowerPoint" version="1.7.4.4" targetFramework="net452" />
  <package id="NetOffice.Word" version="1.7.4.4" targetFramework="net452" />
  <package id="Newtonsoft.Json" version="12.0.3" targetFramework="net45" />
</packages> 

转换核心代码:ConvertHelper.cs

public class ConvertHelper
    {
        public static void ConvetToPdf(string sourcePath, string targetPath)
        {
            if (!File.Exists(sourcePath))
            {
                throw new Exception(string.Format("文件{0}不存在", sourcePath));
            }

            if (File.Exists(targetPath))
            {
                throw new Exception(string.Format("目标文件{0}已存在", targetPath));
            }

            LogHelper.Info<Program>("开始转换:" + Path.GetFileName(sourcePath));

            var targetDirectory = Path.GetDirectoryName(targetPath);
            if (!Directory.Exists(targetDirectory))
            {
                Directory.CreateDirectory(targetDirectory);
            }

            var ext = Path.GetExtension(sourcePath).ToLower();

            if (ext == ".ppt" || ext == ".pptx")
            {
                ConvertPptToPdf(sourcePath, targetPath);
            }
            else if (ext == ".doc" || ext == ".docx")
            {
                ConvertDocumentToPdf(sourcePath, targetPath);
            }
            else if (ext == ".xls" || ext == ".xlsx")
            {
                ConvertExcelToPdf(sourcePath, targetPath);
            }
            else
            {
                LogHelper.Info<Program>("{0}不支持转换pdf", sourcePath);
                return;
            }

            if (File.Exists(targetPath))
            {
                LogHelper.Info<Program>("转换成功:" + Path.GetFileName(sourcePath));
            }
            else
            {
                LogHelper.Info<Program>("转换失败:" + Path.GetFileName(sourcePath));
            }
        }

        /// <summary>
        /// 转换ppt文件到pdf文件(不支持超时终止)
        /// </summary>
        /// <param name="sourcePath"></param>
        /// <param name="targetPath"></param>
        private static void ConvertPptToPdf(string sourcePath, string targetPath)
        {
            NetOffice.PowerPointApi.Application application = null;
            NetOffice.PowerPointApi.Presentation presentation = null;

            var cts = new CancellationTokenSource();
            var thread = new Thread(() =>
            {
                try
                {
                    application = new NetOffice.PowerPointApi.Application();
                    presentation = application.Presentations.Open(sourcePath, true, NetOffice.OfficeApi.Enums.MsoTriState.msoTrue, false);
                    presentation.SaveCopyAs(targetPath, NetOffice.PowerPointApi.Enums.PpSaveAsFileType.ppSaveAsPDF);
                }
                catch (Exception ex)
                {
                    if (ex.InnerException != null && ex.InnerException is ThreadAbortException)
                    {
                        LogHelper.Info<Program>("ConvertPptToPdf: {0}操作超时", Path.GetFileName(sourcePath));
                    }
                    else
                    {
                        LogHelper.Error<Program>(ex, "ConvertPptToPdf: {0}出现异常", Path.GetFileName(sourcePath));
                    }
                }
                finally
                {
                    if (presentation != null)
                    {
                        presentation.Close();
                        presentation = null;
                    }

                    if (application != null)
                    {
                        application.Quit();
                        application.Dispose();
                        application = null;
                    }

                    //killProccess("POWERPNT");
                }
            });

            cts.Token.Register(() =>
            {
                thread.Abort();
            });
            cts.CancelAfter(Program.MaxThreads * 1000);

            thread.Start();
            thread.Join();

            GC.Collect();
            GC.WaitForPendingFinalizers();
        }

        /// <summary>
        /// 转换word文件到pdf文件(支持超时终止)
        /// </summary>
        /// <param name="sourcePath"></param>
        /// <param name="targetPath"></param>
        private static void ConvertDocumentToPdf(string sourcePath, string targetPath)
        {
            NetOffice.WordApi.Application application = null;
            NetOffice.WordApi.Document document = null;

            var cts = new CancellationTokenSource();
            var thread = new Thread(() =>
            {
                try
                {
                    application = new NetOffice.WordApi.Application();
                    document = application.Documents.Open(sourcePath);
                    document.ExportAsFixedFormat(targetPath, NetOffice.WordApi.Enums.WdExportFormat.wdExportFormatPDF);
                }
                catch (Exception ex)
                {
                    if (ex.InnerException != null && ex.InnerException is ThreadAbortException)
                    {
                        LogHelper.Info<Program>("ConvertDocumentToPdf: {0}操作超时", Path.GetFileName(sourcePath));
                    }
                    else
                    {
                        LogHelper.Error<Program>(ex, "ConvertDocumentToPdf: {0}出现异常", Path.GetFileName(sourcePath));
                    }
                }
                finally
                {

                    if (document != null)
                    {
                        document.Close();
                        document = null;
                    }

                    if (application != null)
                    {
                        application.Quit();
                        application.Dispose();
                        application = null;
                    }

                    //killProccess("WINWORD");
                }
            });

            cts.Token.Register(() =>
            {
                thread.Abort();
            });
            cts.CancelAfter(Program.MaxThreads * 1000);

            thread.Start();
            thread.Join();


            GC.Collect();
            GC.WaitForPendingFinalizers();
        }

        private static void ConvertExcelToPdf(string sourcePath, string targetPath)
        {
            NetOffice.ExcelApi.Application application = null;
            NetOffice.ExcelApi.Workbook wookbook = null;

            var cts = new CancellationTokenSource();
            var thread = new Thread(() =>
            {
                try
                {
                    application = new NetOffice.ExcelApi.Application();
                    wookbook = application.Workbooks.Open(sourcePath);
                    wookbook.ExportAsFixedFormat(NetOffice.ExcelApi.Enums.XlFixedFormatType.xlTypePDF, targetPath);
                }
                catch (Exception ex)
                {
                    if (ex.InnerException != null && ex.InnerException is ThreadAbortException)
                    {
                        LogHelper.Info<Program>("ConvertExcelToPdf: {0}操作超时", Path.GetFileName(sourcePath));
                    }
                    else
                    {
                        LogHelper.Error<Program>(ex, "ConvertExcelToPdf: {0}出现异常", Path.GetFileName(sourcePath));
                    }
                }
                finally
                {
                    if (wookbook != null)
                    {
                        wookbook.Close();
                        wookbook = null;
                    }

                    if (application != null)
                    {
                        application.Quit();
                        application.Dispose();
                        application = null;
                    }

                    //killProccess("EXCEL");
                }
            });

            cts.Token.Register(() =>
            {
                thread.Abort();
            });
            cts.CancelAfter(Program.MaxThreads * 1000);

            thread.Start();
            thread.Join();

            GC.Collect();
            GC.WaitForPendingFinalizers();
        }


        public static void CleanProccess()
        {
            LogHelper.Info<Program>("CleanProccess Start");
            killProccess("WINWORD");
            killProccess("EXCEL");
            LogHelper.Info<Program>("CleanProccess Finish");
        }

        private static void killProccess(string appName)
        {
            // Store all running process in the system
            Process[] runingProcess = Process.GetProcesses();
            for (int i = 0; i < runingProcess.Length; i++)
            {
                // compare equivalent process by their name
                if (string.Equals(runingProcess[i].ProcessName, appName, StringComparison.OrdinalIgnoreCase))
                {
                    try
                    {
                        // kill  running process
                        runingProcess[i].Kill();
                        LogHelper.Info<Program>("Kill {0} [{1}]", appName, runingProcess[i].Id);
                    }
                    catch (Exception ex)
                    {
                        if (ex is System.InvalidOperationException)
                        {
                            //进程已经关闭
                        }
                        else
                        {
                            LogHelper.Error<Program>(ex, "Kill {0} [{1}] Error", appName, runingProcess[i].Id);
                        }
                    }
                }
            }
        }
    }

ServiceListener监听http请求,端口号默认8091;

        public ServiceListener()
        {
            _stop = new ManualResetEvent(false);
            _idle = new ManualResetEvent(false);
            _busy = new Semaphore(Program.MaxThreads, Program.MaxThreads);
            _listener = new HttpListener();
            _listenerThread = new Thread(HandleRequests);
        }

        public void Start()
        {
            var url = String.Format(@"http://localhost:{0}/", Program.ListenerPort); // Port=8091
            LogHelper.Info<ServiceListener>("Listenning Start:" + url);
            _listener.Prefixes.Add(url);
            _listener.Start();
            _listenerThread.Start();
        }

五. 系统调用http服务,转换文件

        public void ConvertToPdf(string serviceUrl, string sourcePath, string pdfPath)
        {
            var url = $"{serviceUrl}?srcPath={sourcePath}&tarPath={pdfPath}";
            using (var webClient = new WebClient())
            {
                var result = webClient.DownloadString(url);
            }
        }

至此,开发完毕,本地开发完成;

六. 本地测试功能

  1. 注册服务,执行注册服务.bat
  2. 启动服务,执行启动服务.bat
  3. 查看服务运行情况


    image.png

但是测试转换时遇到了问题,日志错误为 "Office 检测到该文件有问题。为帮助保护您的计算机,此文件无法打开。":

2020-05-02 09:04:22:248 Info [Thread9] 开始转换:a10a54166224453abdd8d4b809f15449.ppt
2020-05-02 09:04:25:379 Exception [Thread10] ConvertPptToPdf: a10a54166224453abdd8d4b809f15449.ppt出现异常 Exception:
System.Runtime.InteropServices.COMException (0x80004005): See inner exception(s) for details. ---> System.Reflection.TargetInvocationException: 调用的目标发生了异常。 ---> System.Runtime.InteropServices.COMException: Presentations.Open : Office 检测到该文件有问题。为帮助保护您的计算机,此文件无法打开。
   --- 内部异常堆栈跟踪的结尾 ---
   在 System.RuntimeType.InvokeDispMethod(String name, BindingFlags invokeAttr, Object target, Object[] args, Boolean[] byrefModifiers, Int32 culture, String[] namedParameters)
   在 System.RuntimeType.InvokeMember(String name, BindingFlags bindingFlags, Binder binder, Object target, Object[] providedArgs, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParams)
   在 NetOffice.Invoker.MethodReturn(COMObject comObject, String name, Object[] paramsArray)
   在 NetOffice.Invoker.MethodReturn(COMObject comObject, String name, Object[] paramsArray)
   在 NetOffice.PowerPointApi.Presentations.Open(String fileName, Object readOnly, Object untitled, Object withWindow)
   在 LMS.DocumentConvertService.ConvertHelper.LMS.DocumentConvertService\Service\ConvertHelper.cs:行号 75

2020-05-02 09:04:33:477 Info [Thread9] 转换失败:a10a54166224453abdd8d4b809f15449.ppt

问题原因:service不能没有权限进行桌面交互;因此需要开启此权限;
相关资料:
https://www.cnblogs.com/94cool/archive/2010/04/12/1710261.html
https://www.cnblogs.com/ymworkroom/articles/6673989.html
https://blog.csdn.net/jiangxinyu/article/details/5397060

我才用了其中2种较简单解决方案:

  1. 修改代码,在ProjectInstaller.cs中ProjectInstaller类中重载OnAfterInstall
[RunInstaller(true)]
    public partial class ProjectInstaller : System.Configuration.Install.Installer
    {
        public ProjectInstaller()
        {
            InitializeComponent();
        }

        protected override void OnAfterInstall(IDictionary savedState)
        {
            try
            {
                base.OnAfterInstall(savedState);
                System.Management.ManagementObject myService = new System.Management.ManagementObject(
                    string.Format("Win32_Service.Name='{0}'", this.serviceInstaller1.ServiceName));
                System.Management.ManagementBaseObject changeMethod = myService.GetMethodParameters("Change");
                changeMethod["DesktopInteract"] = true;
                System.Management.ManagementBaseObject OutParam = myService.InvokeMethod("Change", changeMethod, null);
            }
            catch (Exception)
            {
            }
        }
    }
  1. SC程序修改, 允许与桌面进行交互
    用批处理的方式实现,加入如下命令到>启动服务.bat文件中
sc config MonitorService type=interact type=own

至此,本地开发环境转换服务成功运行。
接下来准备上服务器!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355