一直以來,我都想為 PDF 補丁丁添加一個 PDF 渲染引擎。可是,目前並沒有可以在 .NET 框架上運行的免費 PDF 渲染引擎。經過網上的搜索,有人使用 C++/CLI 調用 XPDF 或 Mupdf,實現了不安裝 Adobe 系列軟件而渲染出 PDF 文件的功能。
Mupdf 是一個開源的 PDF 渲染引擎,使用 C 語言編寫,可編譯成能讓 C# 調用的動態鏈接庫。因此,只要編寫合適的調用代碼,就能使用該渲染引擎,將 PDF 文檔轉換為一頁一頁的圖片,或者在程序界面顯示 PDF 文檔的內容。
要使用 Mupdf 渲染 PDF 文檔,有幾個步驟:
- 獲取 Mupdf 的動態鏈接庫。
- 了解該庫中的相關導出函數。
- 為導出函數撰寫 P/Invoke 代碼。
- 撰寫 C# 代碼,調用 Mupdf 的導出函數。將渲染后的數據(Pixmap)轉換為位圖,或直接在控件的設備句柄(HDC)繪制渲染后的文檔。
獲取 Mupdf 動態鏈接庫
Mupdf 的源代碼沒有提供直接編譯生成動態鏈接庫的 Make 文件。幸好,從另一個基於 Mupdf 的開源項目——SumatraPDF——能編譯生成 Mupdf 動態鏈接庫。在 SumatraPDF 的源代碼網站下載源代碼和工程文件,使用 Visual C++(免費的速成版就可以了)編譯該工程,生成配置選“Release”,就能生成 Mupdf 的動態鏈接庫。
了解 Mupdf 的概念和導出函數
Mupdf 的導出函數可通過查看 Mupdf 源代碼的頭文件得到。頭文件可在 Mupdf 官方網站的 Documentation 區在線查閱。
Mupdf 最通用的函數放在頭文件“Fitz.h”里。如果只是使用 C# 函數來渲染 PDF 文檔,只使用 Fitz.h 文件中提供的結構和函數即可。在渲染 PDF 文檔時用到的結構主要有五個:
- fz_context:存放渲染引擎所用的全局數據。
- fz_document:存放文檔的信息。
- fz_page:存放頁面的數據。
- fz_device:用於放置渲染結果的目標設備。
- fz_pixmap:存放渲染結果的畫布。
Fitz.h 文件中提供的函數均以“fz_”開頭,這些函數可用於處理上述五個結構。以上述五個結構為基礎,調用相應的函數,就能完成渲染 PDF 文檔的任務。
沒有 C 語言基礎的開發人員請注意:部分預定義處理指令——即 #define 指令,也使用“fz_”開頭,這些處理指令並不是導出函數。在使用 P/Invoke 技術調用函數庫時不能使用 #define 指令定義的替換函數。例如,fz_try、fz_catch、fz_finally 就是這類型的預定義處理指令。
為導出函數撰寫 P/Invoke 代碼
Fitz.h 提供的導出函數中,下列函數在渲染 PDF 文檔時是必須使用的。
- fz_new_context:創建渲染文檔時的上下文變量。
- fz_free_context:釋放上下文變量所占用的資源。
- fz_open_file_w:打開文件流(傳入的文件名變量為 Unicode)
- fz_open_document_with_stream:打開文件流對應的文檔(PDF 或其它支持的文件格式)。
- fz_close_document:關閉文檔。
- fz_close:關閉文件流。
- fz_count_pages:獲得文檔的頁數。
- fz_load_page:加載文檔指定的頁面。
- fz_free_page:釋放文檔頁面占用的資源。
- fz_bound_page:確定文檔頁面的尺寸。
- fz_new_pixmap:創建渲染頁面所用的圖形畫紙。
- fz_clear_pixmap_with_value:清除畫紙(通常用於將 PDF 文檔的背景色設置為純白色)。
- fz_new_draw_device:從畫紙創建繪圖設備。
- fz_find_device_colorspace:獲取渲染頁面所用的顏色域(彩色或灰色)。
- fz_run_page:將頁面渲染到指定的設備上。
- fz_free_device:釋放設備所占用的資源。
- fz_drop_pixmap:釋放畫紙占用的資源。
- fz_pixmap_samples:獲取畫紙的數據(用於將已渲染的畫紙內容轉換為 Bitmap)。
在撰寫 P/Invoke 代碼的過程中,我們還會遇到幾個結構,“BBox”表示邊框結構,包含 x0、y0、x1 和 y1 四個整數坐標變量;“Rectangle”與“BBox”類似,但坐標變量為浮點數;“Matrix”用於渲染過程中的拉伸、平移等操作(詳見 Mupdf 代碼中的頭文件)。最后,我們得到與下列代碼類似的 P/Invoke C# 代碼。
public struct BBox { public int Left, Top, Right, Bottom; } public struct Rectangle { public float Left, Top, Right, Bottom; } public struct Matrix { public float A, B, C, D, E, F; } class NativeMethods { const string DLL = "libmupdf.dll"; [DllImport (DLL, EntryPoint="fz_new_context")] public static extern IntPtr NewContext (IntPtr alloc, IntPtr locks, uint max_store); [DllImport (DLL, EntryPoint = "fz_free_context")] public static extern IntPtr FreeContext (IntPtr ctx); [DllImport (DLL, EntryPoint = "fz_open_file_w", CharSet = CharSet.Unicode)] public static extern IntPtr OpenFile (IntPtr ctx, string fileName); [DllImport (DLL, EntryPoint = "fz_open_document_with_stream")] public static extern IntPtr OpenDocumentStream (IntPtr ctx, string magic, IntPtr stm); [DllImport (DLL, EntryPoint = "fz_close")] public static extern IntPtr CloseStream (IntPtr stm); [DllImport (DLL, EntryPoint = "fz_close_document")] public static extern IntPtr CloseDocument (IntPtr doc); [DllImport (DLL, EntryPoint = "fz_count_pages")] public static extern int CountPages (IntPtr doc); [DllImport (DLL, EntryPoint = "fz_bound_page")] public static extern Rectangle BoundPage (IntPtr doc, IntPtr page); [DllImport (DLL, EntryPoint = "fz_clear_pixmap_with_value")] public static extern void ClearPixmap (IntPtr ctx, IntPtr pix, int byteValue); [DllImport (DLL, EntryPoint = "fz_find_device_colorspace")] public static extern IntPtr FindDeviceColorSpace (IntPtr ctx, string colorspace); [DllImport (DLL, EntryPoint = "fz_free_device")] public static extern void FreeDevice (IntPtr dev); [DllImport (DLL, EntryPoint = "fz_free_page")] public static extern void FreePage (IntPtr doc, IntPtr page); [DllImport (DLL, EntryPoint = "fz_load_page")] public static extern IntPtr LoadPage (IntPtr doc, int pageNumber); [DllImport (DLL, EntryPoint = "fz_new_draw_device")] public static extern IntPtr NewDrawDevice (IntPtr ctx, IntPtr pix); [DllImport (DLL, EntryPoint = "fz_new_pixmap")] public static extern IntPtr NewPixmap (IntPtr ctx, IntPtr colorspace, int width, int height); [DllImport (DLL, EntryPoint = "fz_run_page")] public static extern void RunPage (IntPtr doc, IntPtr page, IntPtr dev, Matrix transform, IntPtr cookie); [DllImport (DLL, EntryPoint = "fz_drop_pixmap")] public static extern void DropPixmap (IntPtr ctx, IntPtr pix); [DllImport (DLL, EntryPoint = "fz_pixmap_samples")] public static extern IntPtr GetSamples (IntPtr ctx, IntPtr pix); }
撰寫代碼調用導出函數
在上述 P/Invoke 代碼已經准備好之后,需要撰寫代碼調用導出函數並渲染出頁面。為簡單起見,示例中並不使用類封裝結構,而是直接調用上述 P/Invoke 函數。上述函數中,名稱中包含“close”、“drop”、“free”的函數是用來釋放資源的。在實際開發過程中,應撰寫相應的類來保存對這些資源的指針引用。而且,這些類應實現 IDisposable 接口,並將釋放資源的函數放在 Dispose 方法中。在完成操作后,應調用類實例的 Dispose 方法,釋放相關的資源。
渲染頁面的流程如下,按步驟逐個調用上述的函數即可:
- 加載文檔。
- 加載頁面。
- 預備好繪圖畫紙(Pixmap)。
- 從繪圖畫紙創建繪圖設備。
- 將頁面繪制到繪圖設備(即畫紙)上。
- 將畫紙的數據轉換為 Bitmap。
- 保存 Bitmap 或將 Bitmap 繪制到程序界面。
- 釋放 Bitmap 的資源。
- 釋放畫紙、繪圖設備、頁面和文檔的資源。
代碼如下所示。
static void Main (string[] args) { const uint FZ_STORE_DEFAULT = 256 << 20; IntPtr ctx = NativeMethods.NewContext (IntPtr.Zero, IntPtr.Zero, FZ_STORE_DEFAULT); // 創建上下文 IntPtr stm = NativeMethods.OpenFile (ctx, "test.pdf"); // 打開 test.pdf 文件流 IntPtr doc = NativeMethods.OpenDocumentStream (ctx, ".pdf", stm); // 從文件流創建文檔對象 int pn = NativeMethods.CountPages (doc); // 獲取文檔的頁數 for (int i = 0; i < pn; i++) { // 遍歷各頁 IntPtr p = NativeMethods.LoadPage (doc, i); // 加載頁面(首頁為 0) Rectangle b = NativeMethods.BoundPage (doc, p); // 獲取頁面尺寸 using (var bmp = RenderPage (ctx, doc, p, b)) { // 渲染頁面並轉換為 Bitmap bmp.Save ((i+1) + ".png"); // 將 Bitmap 保存為文件 } NativeMethods.FreePage (doc, p); // 釋放頁面所占用的資源 } NativeMethods.CloseDocument (doc); // 釋放其它資源 NativeMethods.CloseStream (stm); NativeMethods.FreeContext (ctx); }
其中,RenderPage 方法用來渲染圖片,代碼如下。
static Bitmap RenderPage (IntPtr context, IntPtr document, IntPtr page, Rectangle pageBound) { Matrix ctm = new Matrix (); IntPtr pix = IntPtr.Zero; IntPtr dev = IntPtr.Zero; int width = (int)(pageBound.Right - pageBound.Left); // 獲取頁面的寬度和高度 int height = (int)(pageBound.Bottom - pageBound.Top); ctm.A = ctm.D = 1; // 設置單位矩陣 (1,0,0,1,0,0) // 創建與頁面相同尺寸的繪圖畫布(Pixmap) pix = NativeMethods.NewPixmap (context, NativeMethods.FindDeviceColorSpace (context, "DeviceRGB"), width, height); // 將 Pixmap 的背景設為白色 NativeMethods.ClearPixmap (context, pix, 0xFF); // 創建繪圖設備 dev = NativeMethods.NewDrawDevice (context, pix); // 將頁面繪制到以 Pixmap 生成的繪圖設備上 NativeMethods.RunPage (document, page, dev, ctm, IntPtr.Zero); NativeMethods.FreeDevice (dev); // 釋放繪圖設備對應的資源 dev = IntPtr.Zero; // 創建與 Pixmap 相同尺寸的彩色 Bitmap Bitmap bmp = new Bitmap (width, height, PixelFormat.Format24bppRgb); var imageData = bmp.LockBits (new System.Drawing.Rectangle (0, 0, width, height), ImageLockMode.ReadWrite, bmp.PixelFormat); unsafe { // 將 Pixmap 的數據轉換為 Bitmap 數據 // 獲取 Pixmap 的圖像數據 byte* ptrSrc = (byte*)NativeMethods.GetSamples (context, pix); byte* ptrDest = (byte*)imageData.Scan0; for (int y = 0; y < height; y++) { byte* pl = ptrDest; byte* sl = ptrSrc; for (int x = 0; x < width; x++) { // 將 Pixmap 的色彩數據轉換為 Bitmap 的格式 pl[2] = sl[0]; //b-r pl[1] = sl[1]; //g-g pl[0] = sl[2]; //r-b //sl[3] 是透明通道數據,在此忽略 pl += 3; sl += 4; } ptrDest += imageData.Stride; ptrSrc += width * 4; } } NativeMethods.DropPixmap (context, pix); // 釋放 Pixmap 占用的資源 return bmp; }
好了,渲染 PDF 文檔的代碼雛形就此完成了。
在實際項目開發中,我們還需要考慮以下幾個首要問題:
- 處理 Mupdf 拋出的異常:捕獲 AccessViolationException 異常。
- 記住釋放資源:可考慮將相應的資源封裝為實現 IDisposible 接口的類。
- 擴展程序的功能:可參考使用 Mupdf 的開放源代碼項目,其中最著名的一個項目莫過於 SumatraPDF。
- 在64位機器上運行:可將 .NET 項目的 CPU 平台設置為 x86,強制程序用 32 位 .NET Framework 運行。
本文及源代碼項目發布在 CodeProject 網站,有興趣的同好可閱讀《Rendering PDF Documents with Mupdf and P/Invoke in C#》。