在 C# 中使用 P/Invoke 調用 Mupdf 函數庫顯示 PDF 文檔


一直以來,我都想為 PDF 補丁丁添加一個 PDF 渲染引擎。可是,目前並沒有可以在 .NET 框架上運行的免費 PDF 渲染引擎。經過網上的搜索,有人使用 C++/CLI 調用 XPDF 或 Mupdf,實現了不安裝 Adobe 系列軟件而渲染出 PDF 文件的功能。

Mupdf 是一個開源的 PDF 渲染引擎,使用 C 語言編寫,可編譯成能讓 C# 調用的動態鏈接庫。因此,只要編寫合適的調用代碼,就能使用該渲染引擎,將 PDF 文檔轉換為一頁一頁的圖片,或者在程序界面顯示 PDF 文檔的內容。

要使用 Mupdf 渲染 PDF 文檔,有幾個步驟:

  1. 獲取 Mupdf 的動態鏈接庫。
  2. 了解該庫中的相關導出函數。
  3. 為導出函數撰寫 P/Invoke 代碼。
  4. 撰寫 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 文檔時用到的結構主要有五個:

  1. fz_context:存放渲染引擎所用的全局數據。
  2. fz_document:存放文檔的信息。
  3. fz_page:存放頁面的數據。
  4. fz_device:用於放置渲染結果的目標設備。
  5. fz_pixmap:存放渲染結果的畫布。

Fitz.h 文件中提供的函數均以“fz_”開頭,這些函數可用於處理上述五個結構。以上述五個結構為基礎,調用相應的函數,就能完成渲染 PDF 文檔的任務。

沒有 C 語言基礎的開發人員請注意:部分預定義處理指令——即 #define 指令,也使用“fz_”開頭,這些處理指令並不是導出函數。在使用 P/Invoke 技術調用函數庫時不能使用 #define 指令定義的替換函數。例如,fz_try、fz_catch、fz_finally 就是這類型的預定義處理指令。

為導出函數撰寫 P/Invoke 代碼

Fitz.h 提供的導出函數中,下列函數在渲染 PDF 文檔時是必須使用的。

  1. fz_new_context:創建渲染文檔時的上下文變量。
  2. fz_free_context:釋放上下文變量所占用的資源。
  3. fz_open_file_w:打開文件流(傳入的文件名變量為 Unicode)
  4. fz_open_document_with_stream:打開文件流對應的文檔(PDF 或其它支持的文件格式)。
  5. fz_close_document:關閉文檔。
  6. fz_close:關閉文件流。
  7. fz_count_pages:獲得文檔的頁數。
  8. fz_load_page:加載文檔指定的頁面。
  9. fz_free_page:釋放文檔頁面占用的資源。
  10. fz_bound_page:確定文檔頁面的尺寸。
  11. fz_new_pixmap:創建渲染頁面所用的圖形畫紙。
  12. fz_clear_pixmap_with_value:清除畫紙(通常用於將 PDF 文檔的背景色設置為純白色)。
  13. fz_new_draw_device:從畫紙創建繪圖設備。
  14. fz_find_device_colorspace:獲取渲染頁面所用的顏色域(彩色或灰色)。
  15. fz_run_page:將頁面渲染到指定的設備上。
  16. fz_free_device:釋放設備所占用的資源。
  17. fz_drop_pixmap:釋放畫紙占用的資源。
  18. 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 方法,釋放相關的資源。

渲染頁面的流程如下,按步驟逐個調用上述的函數即可:

  1. 加載文檔。
  2. 加載頁面。
  3. 預備好繪圖畫紙(Pixmap)。
  4. 從繪圖畫紙創建繪圖設備。
  5. 將頁面繪制到繪圖設備(即畫紙)上。
  6. 將畫紙的數據轉換為 Bitmap。
  7. 保存 Bitmap 或將 Bitmap 繪制到程序界面。
  8. 釋放 Bitmap 的資源。
  9. 釋放畫紙、繪圖設備、頁面和文檔的資源。

代碼如下所示。

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 文檔的代碼雛形就此完成了。

在實際項目開發中,我們還需要考慮以下幾個首要問題:

  1. 處理 Mupdf 拋出的異常:捕獲 AccessViolationException 異常。
  2. 記住釋放資源:可考慮將相應的資源封裝為實現 IDisposible 接口的類。
  3. 擴展程序的功能:可參考使用 Mupdf 的開放源代碼項目,其中最著名的一個項目莫過於 SumatraPDF。
  4. 在64位機器上運行:可將 .NET 項目的 CPU 平台設置為 x86,強制程序用 32 位 .NET Framework 運行。

本文及源代碼項目發布在 CodeProject 網站,有興趣的同好可閱讀《Rendering PDF Documents with Mupdf and P/Invoke in C#》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM