iT邦幫忙

4

[開發經驗分享][.NET C#]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。結果還是失敗

[最終]解決方案三之二:使用OpenOffice的dll

前置作業

  • 安裝LibreOffice
  • 安裝後,將下列五個dll複製出來

C:\Windows\Microsoft.NET\assembly\GAC_MSIL\cli_basetypes\v4.0_1.0.20.0__ce2cb7e279207b9e\cli_basetypes.dll
C:\Windows\Microsoft.NET\assembly\GAC_MSIL\cli_oootypes\v4.0_1.0.9.0__ce2cb7e279207b9e\cli_oootypes.dll
C:\Windows\Microsoft.NET\assembly\GAC_MSIL\cli_ure\v4.0_1.0.23.0__ce2cb7e279207b9e\cli_ure.dll
C:\Windows\Microsoft.NET\assembly\GAC_MSIL\cli_uretypes\v4.0_1.0.9.0__ce2cb7e279207b9e\cli_uretypes.dll
C:\Windows\Microsoft.NET\assembly\GAC_64\cli_cppuhelper\v4.0_1.0.23.0__ce2cb7e279207b9e\cli_cppuhelper.dll

若是懶得找下面我把他壓縮放到Google雲端,請自行取用。

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' 或其相依性的其中之一。 試圖載入格式錯誤的程式。原因是因為cli_cppuhelper.dll是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功能的經過,過程雖然不算艱辛但也一波三折。

補充(2021/1/23)

LibreOffice另外還有提供許多轉檔的指令,目前比較常用的有

  • Ods轉Xlsx:Calc MS Excel 2007 XML
  • Ods轉Xls:MS Excel 97
  • Excel轉Ods:calc8
  • 試算表轉Pdf:calc_pdf_Export
  • Odt轉Docx:MS Word 2007 XML
  • Odt轉Doc:MS Word 97
  • Word轉Odt:writer8
  • Word轉Pdf:writer_pdf_Export
    其餘請參考官網列表
/// <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("Calc MS Excel 2007 XML")
	};
	((XStorable)xComponent).storeToURL(destinationFile, propertyValues);
}

總結

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

參考來源整理

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#

Bootstrap.bootstrap()補充

在開發環境下沒問題,但是部屬到IIS之後會卡在Bootstrap.bootstrap();那行,這時候請於站台的應用程式集區的進階設定的載入使用者設定檔設定成True後,方可解決。
https://ithelp.ithome.com.tw/upload/images/20200505/20116204JmalLgkI1s.png


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

2
長庚
iT邦新手 3 級 ‧ 2020-01-05 17:38:41

原來客戶會有把excel轉pdf的需求!!!
謝謝大大的分享,以後遇到這種需求就知道要怎麼處理了 :)

1
圓頭人
iT邦研究生 5 級 ‧ 2020-04-15 09:16:09

請教大大~
跑到這行時,一直轉圈圈,跑不出來
var xLocalContext = Bootstrap.bootstrap();

環境
VS2013
安裝x86LibreOffice
安裝x86LibreOffice的SDK

protected void Button1_Click(object sender, EventArgs e)
{
    ConvertToPdfSdk(@"e:\result.xlsx", @"e:\result.pdf");
}
 /// <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;
    }
看更多先前的回應...收起先前的回應...
威廉蕭 iT邦新手 5 級 ‧ 2020-04-15 09:22:21 檢舉

Hi, 您好
https://dotblogs.com.tw/WillianHsiaoDotNetBLog/2020/01/07/ExcelToPdf
這邊文章下面的留言有相關的解決方法

圓頭人 iT邦研究生 5 級 ‧ 2020-04-15 09:33:46 檢舉

我跟他一樣
網頁畫面一直等候中.......然後就沒下文了。
救人哦~~

威廉蕭 iT邦新手 5 級 ‧ 2020-05-05 19:28:35 檢舉

請問你們是發佈上去之後才出現問題的嗎? 還是在開發的時候就出現了呢?

威廉蕭 iT邦新手 5 級 ‧ 2020-05-05 19:31:31 檢舉

IIS的應用程式集區/進階設定/載入使用者設定檔/True
請試試看此方法

我是發佈上去之後才出現轉圈的問題

我要留言

立即登入留言