[開發經驗分享]Excel檔轉PDF產生的問題與解決方法

Excel檔轉PDF產生的問題與解決方法

開發情境與問題描述

最近在開發的時候遇到一個匯出Excel的問題,原本使用的前端套件是將Html Table的內容直接轉成Html的Excel檔案,並沒有經過後端(backend)處理,但是這個套件在IE(又是)卻發生了嚴重的編譯問題,導致整個網站都沒辦法開啟,公司內部決議的解決方法是將匯出Excel的方法改為由後端產出Excel檔案。

後端產生Excel套件:EPPlus

如何產生Excel檔案的相關程式碼網路上都有,這邊就不贅述了,產生Excel表單還算簡單,至少有很多免費的套件可以使用,至於Excel轉PDF的話就遇到一堆問題,以下正片開始。

解決方案一:FreeSpire.XLS

首先我Google了一下,關鍵字epplus excel to pdf,找到了以下文章: 使用 epplus 產生檔案後轉成 PDF 文章內容中提到,可以用FreeSpire.XLS這個套件來將Excel轉為PDF檔,因為看到Free這個字眼,就直覺覺得是免費的套件,所以直接到Nuget搜尋並安裝,照著文章的寫法完成之後,很順利的產生了PDF檔,檢查了一下看似沒甚麼問題,這個功能就簽入到版控,打完收工準備驗收。 FreeSpire.XLS

結果隔天同事到客戶端驗收的時候,赫然發現最下面會出現需要Buy it now!!的文字(WTF)。 (開發端不會產生,所以開發時沒有察覺)

緊急求救Google大神後發現,許多Excel轉PDF的套件均需要付費才能使用,免費的都會有一些限制,不是只給你一頁,就是多了一堆要你付費的文字,果然天下沒有白吃的午餐,只好找其他的方案了。

解決方案二:Microsoft.Office.Interop.Excel

因為是Excel檔,自然就找上了微軟的Excel轉PDF解法,也就是這個套件,只要Server端也安裝Office,並且專案加入這個dll的參考,就能夠使用它內建的匯出PDF的功能,相當的簡單好用。

// Excel 檔案位置
string sourcexlsx = @"D:\Downloads\003-圖表-ok.xlsx";
// PDF 儲存位置
string targetpdf = @"D:\Downloads\003-圖表-ok.pdf";
//建立 Excel application instance
Microsoft.Office.Interop.Excel.Application appExcel = new Microsoft.Office.Interop.Excel.Application();
//開啟 Excel 檔案
var xlsxDocument = appExcel.Workbooks.Open(sourcexlsx);
//匯出為 pdf
xlsxDocument.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, targetpdf);
//關閉 Excel 檔
xlsxDocument.Close();
//結束 Excel
appExcel.Quit();

但是,等到完成,而且都要發布到Server的時候,才被告知:因為Microsoft Office要買授權才可以安裝,客戶不確定有沒有買,而且客戶是公家機關,之後也不會用MS Office而是會推OpenOffice,所以這個套件基本上又不能用了。

Microsoft.Office.Interop.Excel 使用 C# 將 Excel 檔(.xlsx .xls) 轉換為 PDF

解決方案三:OpenOffice的LibreOffice(.exe)

就在無計可施的時候,突然想到,xlsx檔用OpenOffice也可以編輯,於是又一次請教了Google大神,關鍵字openoffice excel to pdf c#,找到黑暗大的文章: LibreOffice docx 轉 pdf 評估筆記

文章內容是分享用LibreOffice將docx轉PDF檔的成果分享,讓我發現LibreOffice有跟MS Office一樣匯出PDF的功能(之後發現OpenOffice本身也有一樣轉PDF的功能),一樣只要在Server機器安裝就可以匯出,而且重點是免費!!!(歡呼)

再搭配LibreOfficeLibrary這個套件,把參數設定一下,輕輕鬆鬆就可以透過LibreOffice將Excel轉成PDF,在開發端測試完全沒問題,真是可喜可賀,問題就此解決,打完收工(?)

var documentConverter = new DocumentConverter();
documentConverter.ConvertToPdf(excelFilePath, pdfFilePath);

但就在開開心心的把LibreOffice安裝到客戶的Server上,並且把程式發布好,開啟站台測試時,一切瞬間風雲變色,原本0.1秒就可以順利看到PDF下載完成的訊息,放到正式機後跑了5分鐘還在轉圈圈,打開工作管理員查看執行中的程序,LibreOffice.exe這支執行檔一直在執行,我點幾次匯出就執行幾支程序,完全沒有結束的時候。

先就幾個方面進行問題排除:

  1. LibreOfficeLibrary套件問題 A. 也許是套件本身的問題,所以將套件匯出PDF的原始碼直接複製到專案。結果一樣,在開發端可以,發布上去就失敗 LibreOfficeLibrary匯出Pdf的部分 B. LibreOffice匯出PDF是下Command Line來執行,所以參考網路上的Command語法來執行。結果同上也是失敗
//執行LibreOffice匯出PDF的Command Line語法
public static void ConvertToPdfProcess(string filePath)
{
    //指定應用程式路徑
    string target = @"C:\Program Files\LibreOffice\program\soffice.exe";

    var pInfo = new ProcessStartInfo(target);
    pInfo.Arguments = $"-headless -convert-to pdf \"{filePath}\" -outdir \"{Path.GetDirectoryName(filePath)}\" ";
    using (var p = new Process())
    {
        p.StartInfo = pInfo;
        p.Start();
    }
}
  1. 資料夾或檔案權限問題 所以先把匯出檔案目錄以及LibreOffice.exe的權限先開放為Everyone。結果還是失敗

解決方案四:LibreOffice SDK

就這樣又卡關了,接著就像遊魂一樣,漫無目的地在Google亂逛亂搜尋LibreOffice Export PDF Fail、LibreOffice Stuck(?)、LibreOffice 執行權限...,完全沒有找到解法,發呆了好一陣子,回去看LibreOffice的官方資源,想起了LibreOffice有提供SDK,也許SDK可以突破盲點(雖然不知道盲點在哪),順利匯出PDF。

首先下載SDK並安裝,將sdk安裝路徑(C:\Program Files\LibreOffice\sdk\cli)下的五個dll加入到專案參考。
https://ithelp.ithome.com.tw/upload/images/20200102/2011620464BP6tTmVD.png

並在專案中加入以下程式碼:

/// <summary>
/// 匯出PDF
/// </summary>
/// <param name="inputFile">來源檔案路徑</param>
/// <param name="outputFile">匯出檔案路徑</param>
public static void ConvertToPdfSdk(string inputFile, string outputFile)
{
    if (ConvertExtensionToFilterType(Path.GetExtension(inputFile)) == null)
        throw new InvalidProgramException("Unknown file type for OpenOffice. File = " + inputFile);

    //Get a ComponentContext
    var xLocalContext =
        Bootstrap.bootstrap();
    //Get MultiServiceFactory
    var xRemoteFactory =
        (XMultiServiceFactory)
        xLocalContext.getServiceManager();
    //Get a CompontLoader
    var aLoader =
        (XComponentLoader)xRemoteFactory.createInstance("com.sun.star.frame.Desktop");
    //Load the sourcefile

    XComponent xComponent = null;
    try
    {
        xComponent = InitDocument(aLoader,
            PathConverter(inputFile), "_blank");
        //Wait for loading
        while (xComponent == null)
        {
            Thread.Sleep(1000);
        }

        // save/export the document
        SaveDocument(xComponent, inputFile, PathConverter(outputFile));
    }
    finally
    {
        if (xComponent != null) xComponent.dispose();
    }
}

/// <summary>
/// 文件初始化
/// </summary>
/// <param name="aLoader"></param>
/// <param name="file">來源檔案路徑</param>
/// <param name="target">目標檔案路徑</param>
/// <returns></returns>
private static XComponent InitDocument(XComponentLoader aLoader, string file, string target)
{
    var openProps = new PropertyValue[1];
    openProps[0] = new PropertyValue { Name = "Hidden", Value = new Any(true) };

    var xComponent = aLoader.loadComponentFromURL(
        file, target, 0,
        openProps);

    return xComponent;
}

/// <summary>
/// 儲存檔案
/// </summary>
/// <param name="xComponent">套件</param>
/// <param name="sourceFile">來源檔案路徑</param>
/// <param name="destinationFile">目標檔案路徑</param>
private static void SaveDocument(XComponent xComponent, string sourceFile, string destinationFile)
{
    var propertyValues = new PropertyValue[2];
    // Setting the flag for overwriting
    propertyValues[1] = new PropertyValue { Name = "Overwrite", Value = new Any(true) };
    //// Setting the filter name
    propertyValues[0] = new PropertyValue
    {
        Name = "FilterName",
        Value = new Any(ConvertExtensionToFilterType(Path.GetExtension(sourceFile)))
    };
    ((XStorable)xComponent).storeToURL(destinationFile, propertyValues);
}

/// <summary>
/// 檔案路徑字串格式
/// </summary>
/// <param name="file">檔案路徑</param>
/// <returns></returns>
private static string PathConverter(string file)
{
    if (string.IsNullOrEmpty(file))
        throw new NullReferenceException("Null or empty path passed to OpenOffice");

    return String.Format("file:///{0}", file.Replace(@"\", "/"));
}

/// <summary>
/// 對應檔案類型
/// </summary>
/// <param name="extension">副檔名</param>
/// <returns></returns>
public static string ConvertExtensionToFilterType(string extension)
{
    switch (extension)
    {
        case ".doc":
        case ".docx":
        case ".txt":
        case ".rtf":
        case ".html":
        case ".htm":
        case ".xml":
        case ".odt":
        case ".wps":
        case ".wpd":
            return "writer_pdf_Export";
        case ".xls":
        case ".xlsb":
        case ".xlsx":
        case ".ods":
            return "calc_pdf_Export";
        case ".ppt":
        case ".pptx":
        case ".odp":
            return "impress_pdf_Export";

        default:
            return null;
    }
}

來源參考:HOW TO: Convert office documents to PDF using Open Office/LibreOffice in C#

設定完畢之後,專案發行時,會發生無法載入檔案或組件 'cli_cppuhelper' 或其相依性的其中之一。 試圖載入格式錯誤的程式。原因是因為sdk是x64,所以需要將專案的目標平台改為x64

https://ithelp.ithome.com.tw/upload/images/20200102/201162046Yeh7PYBjK.png

設定後還會有一樣的錯誤,原因是專案的IIS Express也一樣要改為x64平台

https://ithelp.ithome.com.tw/upload/images/20200102/20116204IHv2BbsvGm.png

發布到IIS的時候,該站台的應用程式集區→進階設定→載入使用者設定檔,這邊一定要設定為True

到此,設定告一個段落,經過測試,開發端可以順利轉換,安裝在客戶的站台一樣可以順利執行!!!(歡呼~~~!!!),這回真的是可喜可賀可口可樂,以上就是達成Excel轉PDF功能的經過,過程雖然不算艱辛但也一波三折。

總結

目前還不知道LibreOffice.exe究竟出了甚麼問題導致無法順利執行,但是基本上這幾個作法在開發端都可以順利執行,如果各位看官有機會需要做到Excel匯出PDF的功能的話,不妨也從比較簡單的方式試試看,如果有MS Office的授權的話可以直接用MS Excel套件轉PDF,其次就是嘗試解決方案三OpenOffice的LibreOffice(.exe),說不定簡單的方式就可行了,而若是有遇到跟我一樣的情況的話,歡迎各位參考我的作法,也希望能夠幫到大家,最後感謝各位的收看。

參考來源整理

EPPlus套件
使用 epplus 產生檔案後轉成 PDF
Microsoft.Office.Interop.Excel
使用 C# 將 Excel 檔(.xlsx .xls) 轉換為 PDF
LibreOffice docx 轉 pdf 評估筆記
LibreOfficeLibrary
HOW TO: Convert office documents to PDF using Open Office/LibreOffice in C#