[.NET][Office] 使用 Word 2010 在 Server 端將 DOC/DOCX 轉換成 PDF

這個需求真的是老需求了,只有使用者端有 Office,就難免會有這種需求,像是在 server 上產生 Word, Excel 或是將表格轉換成 Word/Excel 格式下載的,而這次碰到的需求是要將 Word 轉換成 PDF,只是目前市場上可用的免費工具如 itextsharp, pdfFactory 這種,都不能支援由 server 轉換文件為 PDF,而一些可轉換的元件要錢而且很貴 ($599 鎂以上,可轉散布的更貴),在一個預算有限的專案上,僅能使用最原始的方式來實作這個功能,畢竟 $399 還是比 $599 便宜多了...

前言:這個方法基本上是在想省錢,而且伺服器負載不大的情況才能做的,它本身有高度的風險,原因是 Office 2010 用戶端程式雖有強大的物件模型,但它並不是針對伺服器環境來設計,因此會有不少的副作用,故不能作為追求穩定的應用程式或服務的最佳解決方案,請參閱 Microsoft Support 上一篇文章:Office 伺服器端自動化的考量因素

這個需求真的是老需求了,只有使用者端有 Office,就難免會有這種需求,像是在 server 上產生 Word, Excel 或是將表格轉換成 Word/Excel 格式下載的,而這次碰到的需求是要將 Word 轉換成 PDF,只是目前市場上可用的免費工具如 itextsharp, pdfFactory 這種,都不能支援由 server 轉換文件為 PDF,而一些可轉換的元件要錢而且很貴 ($599 鎂以上,可轉散布的更貴),在一個預算有限的專案上,僅能使用最原始的方式來實作這個功能,畢竟 $399 還是比 $599 便宜多了。

在編寫程式之前,先來說明伺服器的設定,為了要將 DOC/DOCX 轉換成 PDF,在伺服器上安裝一套 Microsoft Word 2010 (或 Microsoft Office 2010) 是免不了的,如果是 Office 2007 的話,那可能還需要加裝一套 Save To PDF 的擴充套件,至於 Word 2003 以前的版本,則真的不支援存成 PDF,所以不在本文討論之列。

接著,為了能讓伺服器端程式 (本文以 ASP.NET 為例) 可存取物件模型,我們必須要在 Word 上設定 DCOM 的存取權限 (Word/Office 是 COM Automation Server),在 "執行" 中輸入 dcomcnfg:

image

會啟動 COM+ 的管理員,展開元件服務 > 電腦 > 我的電腦 > DCOM 設定,並找到 "Microsoft Word 97-2003 文件":

image

然後按右鍵,選 "內容",進入設定區:

image

選擇 "識別身份 (Identity)" 頁籤,並且選擇 "互動式使用者":

image

預設是 "執行啟動的使用者",這會讓 ASP.NET 的執行帳戶 (Network Service) 在呼叫物件模型時被拒絕,也就是若沒有設定這一項,會在程式執行時看到 "為具有 CLSID {000209FF-0000-0000-C000-000000000046} 的元件擷取 COM Class Factory 失敗: 80070005" 的錯誤訊息。

注意:如果是 IIS 6.0 的話,這個方法可能會失效,如果失效,請設定為 "使用下列使用者",並提供具有足夠權限的帳戶,或是直接使用 Administrator 帳戶,但實務上應極力避免使用 Administrator 帳戶。

除了這一項以外,在 "安全性" 頁籤中也要設定:

image

每一項均選自訂,並且按 "編輯" 進入編輯權限視窗:

image

將 ASP.NET 的執行帳戶加入,並且設定 "本機" 的部份允許即可。

存取權限部份設定也是相同:

image

設定權限也是相同 (亦可試試將完全控制取消,保留讀取權限):

image

這些工作完成後,我們就可以進入程式編寫的工作,程式本身其實並不困難,只要 Google 一下 "C# Word PDF",就可以找到不少有用的參考資料,而我自己寫的也給大家參考:


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Office;
using Microsoft.Office.Interop;
using Microsoft.Office.Interop.Word;

namespace OfficeToPDFConverter
{
    public enum WordFileFormat
    {
        WordDoc,
        WordDocx
    }

    public class WordConverter
    {
        public static byte[] Convert(byte[] DocFileData, WordFileFormat Format, string TempDirPath)
        {
            string docTempFileName = Guid.NewGuid().ToString();
            string pdfTempFileName = Guid.NewGuid().ToString() + ".pdf";

            if (Format == WordFileFormat.WordDoc)
                docTempFileName += ".doc";
            else if (Format == WordFileFormat.WordDocx)
                docTempFileName += ".docx";
            else
                throw new NotSupportedException("ERROR_DOC_FORMAT_NOT_SUPPORTED");

            object optionalNullParam = Type.Missing;
            MemoryStream stream = new MemoryStream();
            Application wordapp = WordAppInstance.GetInstance();
            object tempDocFilePath = TempDirPath + @"\" + docTempFileName;
            object tempPdfFilePath = TempDirPath + @"\" + pdfTempFileName;
            object wordDocSaveAs = WdSaveFormat.wdFormatPDF;
            object wordCloseOption = WdSaveOptions.wdDoNotSaveChanges;

            FileStream tempStream = new FileStream(tempDocFilePath.ToString(), FileMode.Create, FileAccess.Write);
            tempStream.Write(DocFileData, 0, DocFileData.Length);
            tempStream.Flush();
            tempStream.Close();

            Document docInstance = wordapp.Documents.Open(
                ref tempDocFilePath, ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, 
                ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, 
                ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, ref optionalNullParam);

            docInstance.Activate();
            docInstance.SaveAs(
                ref tempPdfFilePath, ref wordDocSaveAs, ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, 
                ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, 
                ref optionalNullParam, ref optionalNullParam, ref optionalNullParam, ref optionalNullParam);

            ((_Document)docInstance).Close(ref wordCloseOption, ref optionalNullParam, ref optionalNullParam);
            docInstance = null;

            FileStream pdfStream = new FileStream(tempPdfFilePath.ToString(), FileMode.Open, FileAccess.Read);
            byte[] pdfData = new byte[pdfStream.Length];
            pdfStream.Read(pdfData, 0, pdfData.Length);
            pdfStream.Close();

            // delete temp file.
            File.Delete(docTempFileName);
            File.Delete(pdfTempFileName);

            return pdfData;
        }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Office;
using Microsoft.Office.Interop;
using Microsoft.Office.Interop.Word;

namespace OfficeToPDFConverter
{
    public class WordAppInstance
    {
        private static Microsoft.Office.Interop.Word.Application _wordApplication = null;

        public static Microsoft.Office.Interop.Word.Application GetInstance()
        {
            if (_wordApplication == null)
            {
                _wordApplication = new Microsoft.Office.Interop.Word.ApplicationClass();
                _wordApplication.Visible = false;
                _wordApplication.ScreenUpdating = false;

                return _wordApplication;
            }
            else
            {
                return _wordApplication;
            }
        }

        public static void Free()
        {
            if (_wordApplication != null)
            {
                object optionalNullParam = Type.Missing;
                object wordSaveOption = WdSaveOptions.wdDoNotSaveChanges;

                ((_Application)_wordApplication).Quit(ref wordSaveOption, ref optionalNullParam, ref optionalNullParam);
                _wordApplication = null;

                GC.Collect();
            }
        }
    }
}

 

注意:專案中要加入 Microsoft.Office.Interop.Word (14.0) 的參考。

然後就可以在 ASP.NET 上寫程式了:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace WebTesting
{
    public partial class _Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        protected void cmdConvert_Click(object sender, EventArgs e)
        {
            if (this.upWordDoc.HasFile)
            {
                byte[] d = null;

                if (this.upWordDoc.FileName.ToLower().EndsWith(".doc"))
                {
                    d = OfficeToPDFConverter.WordConverter.Convert(
                        this.upWordDoc.FileBytes, 
                        OfficeToPDFConverter.WordFileFormat.WordDoc, 
                        Server.MapPath(VirtualPathUtility.ToAbsolute("~/PdfBuffer")));
                }
                else if (this.upWordDoc.FileName.ToLower().EndsWith(".docx"))
                {
                    d = OfficeToPDFConverter.WordConverter.Convert(
                        this.upWordDoc.FileBytes,
                        OfficeToPDFConverter.WordFileFormat.WordDocx,
                        Server.MapPath(VirtualPathUtility.ToAbsolute("~/PdfBuffer")));
                }

                Response.AddHeader("Content-Disposition", "attachment; filename=Test.pdf");
                Response.ContentType = "application/octet-stream";
                Response.BinaryWrite(d);
                Response.End();
            }
        }
    }
}

 

注意:因為 Office 物件模型不接受 Stream,必須要另存檔案才可用 Office 物件模型操作。

Reference:

http://support.microsoft.com/kb/257757/zh-tw

http://www.dotblogs.com.tw/nobel12/archive/2010/05/12/15170.aspx

http://stackoverflow.com/questions/607669/how-do-i-convert-word-files-to-pdf-programmatically

http://www.codeproject.com/KB/cs/sertf2pdf.aspx