65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Mupdf 和 P/Invoke 在 C# 中渲染 PDF 文档

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (30投票s)

2012年11月24日

CPL

16分钟阅读

viewsIcon

275209

downloadIcon

8931

无需安装额外组件即可将 PDF 转换为位图。

Notice

本文档出于历史原因保留。如需 P/Invoke 最新的 MuPDF 版本,请阅读本文档

将 MuPDF DLL 编译成渲染和编辑 PDF 文档

引言

多年来,我一直在 .NET Framework 中寻找 PDF 渲染引擎。然而,这个世界上还没有免费的原生 .NET 引擎。最终,我被一个 GNU 许可、快速、轻量级的 PDF 渲染引擎——Mupdf 所吸引,并为其编写了自己的 C# 包装器。

本文将介绍编写 Mupdf 引擎的 P/Invoke C# 包装器的初步步骤,并将 PDF 页面渲染成图片文件。

背景和相关项目

在搜索 PDF 渲染引擎的过程中,我发现了两个提供良好 C# 接口的项目。这两个项目提供的库可以用来使用 C# 渲染 PDF 页面。

第一个项目最初发布在 CodeProject.com 上,标题为使用 Xpdf 和 muPDF 在 C# 中查看 PDF 文件,最终托管在 GoogleCode 上,名为pdfviewer-win32。它使用 C++/CLI 来桥接 C# 和 Mupdf。C++/CLI 方法的优点是允许开发者访问 Mupdf 库的几乎所有内部部分。缺点是开发者需要编写许多 C++/CLI 包装类。编写和调试这类类并非易事,而且许多 C# 开发者甚至不了解 C++。

第二个项目托管在 GoogleCode 上,名为mupdf-converter。它有 2 层:第一层引入了一些 C++ 类来封装库;第二层提供了一些 C 函数作为这些类公开的静态方法。最后,这些 C 函数以 DLL 的形式导出,供 P/Invoke 调用。简而言之,包装器的流程是:MuPDF -> C++ 包装器 -> C (导出为 DLL 函数) -> .NET P/Invoke。

既然存在像pdfviewer-win32mupdf-converter这样的现有项目,为什么不直接使用它们呢?只要您不追求最新的开发进展或使用 Mupdf 库提供的所有功能,直接使用它们是可以的。在撰写本文时,pdfviewer-win32项目提供了很多功能,但已经一年多没有更新了。mupdf-converter库提供的功能非常有限——除了渲染 PDF 页面,您无法用它做其他事情——而且它仅在 .NET 4.0 或更高版本上运行。

我研究了mupdf-converter的源代码,发现它相当简单,其两层包装器实现也相当臃肿。实际上,没有必要使用 C++ 类来包装 Mupdf 的函数。由于 Mupdf 是用 C 编写的,因此可以从中创建一个 DLL,并通过 P/Invoke 直接调用其导出函数。也就是说,调用路径可以缩短为:MuPDF -> .NET P/Invoke。受这个库的启发,我开始制作自己的库,并因此写了这篇文章。

警告:本文档的以下部分假设您知道如何在 C# 中编写 P/Invoke 函数。如果您对此一无所知,可以从 MSDN 上学习。如果您不想费力,请使用pdfviewer-win32mupdf-converter提供的现有库。

“制作”步骤

制作我们的 P/Invoke 专用的MupdfSharp库的步骤包括以下几点。

  1. 获取 Mupdf DLL 库。
  2. 学习 Mupdf 的基本概念和导出函数。
  3. 编写 P/Invoke 函数。

获取 Mupdf DLL

如果您曾经下载并编译过 Mupdf 的源代码,您可能会发现编译的输出是几个 EXE 文件,根本没有 DLL 库。因此,如果您不熟悉 MAKE 文件,可能需要花费几个小时来学习和修改 MAKE 文件才能获得 DLL 库。

幸运的是,这个世界上总有热心人。SumatraPDF 的开发者(一个利用 Mupdf 功能的轻量级 PDF 阅读器程序)是我们的救星。他们将代码文件发布在 Github 上,并且他们的项目编译后确实生成了一个供您重用的 DLL 库。因此,步骤可以很简单。

  1. 前往 SumatraPDF 的项目主页:https://github.com/sumatrapdfreader/sumatrapdf
  2. 下载源代码包(或使用 SVN 工具与他们的最新工作同步)。
  3. 打开 Visual C++(您可以在此处使用免费的 Express 版本)并加载项目。
  4. 选择“Release”作为构建配置并编译项目。
  5. 在 release 文件夹中找到 "libmupdf.dll" 文件。
  6. 您现在拥有了 Mupdf DLL。

SumatraPDF 的开发者是非常勤奋的程序员。他们紧密跟踪 Mupdf 的最新开发并频繁更新他们的代码。因此,您可以完全信任他们,并使用他们的库,而不是尝试从官方代码中自行编译 Mupdf DLL。

Notice

今年春天,我发现 SumatraPDF 的开发者似乎停止了与 MuPDF 库的同步,早在几个月前。

正如他们所说,我检查了 MuPDF 的 API 并理解了他们的原因:“通常,这个问题需要大量的工作。Sumatra 对 mupdf 有相当多的更改,mupdf 变化很大,升级需要大量工作,但这(至少对我来说)不是优先事项。”

因此,我写了一篇文章,介绍如何编译我们自己的 MuPDF 版本

2017-11-18

学习基本概念和函数

一旦您获得了libmupdf.dll,就可以开始研究 Mupdf 库提供的函数了。

该库是用 C 编写的。在 C 编程世界中,函数定义放在头文件中,文件扩展名为“.h”。

在 SumatraPDF 项目中,MuPDF 库的头文件位于mupdf\include\mupdf文件夹中。

MuPDF 官方网站的文档部分,列出了五个头文件——“fitz.h”、“pdf.h”、“html.h”、“svg.h”和“xps.h”。我们只需要开始学习前两个。然后我们将跟随它们,找出所需的函数和结构。

以下行来自“fitz.h”文件。当我们在探索 MuPDF 库的功能时,我们将跟随 #include 指令,这表明引用的头文件也在库中使用,我们将逐一检查它们。

#ifndef MUDPF_FITZ_H
#define MUDPF_FITZ_H

#include "mupdf/fitz/version.h"
#include "mupdf/fitz/system.h"
#include "mupdf/fitz/context.h"

// the rest of the file is omitted here

当我们打开 fitz.hpdf.h 文件中列出的文件时,例如 context.h,我们将看到一些函数和结构的定义。函数和结构的定义对于以后编写 P/Invoke 函数至关重要。

要了解 MuPDF 的工作机制,我们可以从 MuPDF 文档网站上列出的示例文件“source/tools/mudraw.c”开始。通过研究代码,我们可以了解 MuPDF 的工作原理。

保存文档内容的结构

fitz.h中的五个关键结构在代码中使用,如下所列。

  1. fz_context结构(又名context.h中定义的fz_context_s):用于在处理 PDF 文件(或其他支持的文档格式)时保存信息。在处理之前必须准备好 context。
  2. fz_document结构(又名document.h中定义的fz_document_s):用于保存已打开的 PDF 文档。
  3. fz_page结构(又名document.h中定义的fz_page_s):用于处理 PDF 页面。一旦打开文档,就可以加载其页面并进行处理。
  4. fz_pixmap结构(又名pixmap.h中定义的fz_pixmap_s):代表页面的渲染视觉结果。如果您想渲染 PDF 文档,必须设置一个 pixmap 并在其上绘制。
  5. fz_device结构(又名device.h中定义的fz_device_s):代表渲染结果绘制的设备。通常,我们会从 pixmap 准备一个设备进行绘制。

您不必关心这些结构的内部布局。这意味着,当我们编写 P/Invoke 代码时,不必在 C# 代码中设置相应的 struct。使用 IntPtr 来保存它们的引用即可。了解这一点可以大大简化我们之后在 P/Invoke 部分的工作。

操作结构的函数

通过了解上述关键结构,我们在示例文件中找到了以下有用的函数。

  1. fz_new_context_imp:创建 fz_context
  2. fz_free_context:释放 fz_context 使用的资源。
  3. fz_open_document_with_stream:从给定的 fz_stream 创建 fz_document 实例。
  4. fz_open_file_w:使用 Unicode 编码的文件名打开 fz_stream。注意:如果您处理的文件名中没有非 ASCII 字符,可以使用 fz_open_document 函数打开文档,而不是组合使用 fz_open_file_wfz_open_document_with_stream
  5. fz_close_document:关闭 fz_document
  6. fz_close:关闭 fz_stream
  7. fz_count_pages:获取文档中的页数。
  8. fz_load_page:为给定的页码创建 fz_page 实例。
  9. fz_free_page:释放 fz_page 使用的资源。
  10. fz_bound_page:获取页面的尺寸。
  11. fz_new_pixmap:创建 fz_pixmap 实例以保存渲染的视觉结果。
  12. fz_clear_pixmap_with_value:用颜色(通常是白色)填充 fz_pixmap
  13. fz_new_draw_device:创建 fz_device 以绘制渲染结果。
  14. fz_lookup_device_colorspace:获取 fz_colorspace 结构,用于 fz_run_page 函数。
  15. fz_run_page:将页面渲染到指定的 fz_device
  16. fz_free_device:释放 fz_device 使用的资源。
  17. fz_drop_pixmap:释放 fz_pixmap 使用的资源。
  18. fz_pixmap_samples:获取渲染的 fz_pixmap 的数据,用于渲染 Bitmap。

注意:如果您使用从 SumatraPDF 编译的 DLL

在 SumatraPDF 的最新版本中,为了缩小 DLL 库文件的大小,开发者决定在编译 libmupdf.dll 文件时剥离一些函数。因此,fitz.h文件中的一些函数没有被编译到 DLL文件中,我们必须改用pdf.h文件中等效的导出函数。

编写 P/Invoke 函数

注意:本章将展示 P/Invoke 函数是如何组成的。如果您不关心它们是如何来的,可以跳过本章。

一旦我们了解了要使用的函数,就可以开始在 C# 中编写 P/Invoke 函数了。首先,我们从 fitz\context.h 中的 fz_new_context_imp 函数开始,因为它创建了 fz_context 实例,并且后续的代码将依赖于该对象。

fz_context *fz_new_context_imp(fz_alloc_context *alloc, fz_locks_context *locks, unsigned int max_store, const char *version);
  1. fz_context * 是函数的返回值,指向 fz_context 结构的指针。在我们的 P/Invoke 函数中,将使用 IntPtr,因为我们不关心 fz_context 的内部结构。
  2. fz_new_context_imp 自然是函数名。
  3. fz_alloc_context *fz_locks_context * 是函数前两个参数的类型。在使用 fz_new_context 函数初始化 fz_context 时,可以将 IntPtr.Zero 传递给这两个参数,因此我们将在 P/Invoke 函数的相应位置使用 IntPtr
  4. max_store 决定了引擎缓存资源的数量。头文件提供的推荐大小(256 << 20)等于 256MB。
  5. version 是传递给函数的字符串。

这是 fz_new_context_imp 函数的 P/Invoke 函数代码。

[DllImport (DLL, EntryPoint="fz_new_context_imp")]
static extern IntPtr NewContext (IntPtr alloc, IntPtr locks, uint max_store, string version); 

由于调用者实际上不必了解 alloclocksmax_store,我们可以添加一个重载函数,使方法看起来更简洁。

const uint FZ_STORE_DEFAULT = 256 << 20;
const string MuPDFVersion = "1.6";

public static IntPtr NewContext () {
    return NewContext (IntPtr.Zero, IntPtr.Zero, FZ_STORE_DEFAULT, MuPDFVersion);
}

但是……你可能会问,“等等。你怎么知道我们可以将两个 IntPtr.Zero 和一个 FZ_STORE_DEFAULT 传递给 NewContext 函数?还有那个神秘字符串 '1.6' 是什么意思?”好问题。第一个问题的答案写在 fitz\context.h 中——“当您不需要预分配内存或设置锁时,可以使用 NULL”。在 P/Invoke 的世界里,NULL 表示前两个参数的 IntPtr.ZeroFZ_STORE_DEFAULT 的值也在那里提到。

第二个问题的答案可以在 fitz\context.hfitz\version.h 中找到。首先,我们在 context.h 中看到以下行:

#define fz_new_context(alloc, locks, max_store) fz_new_context_imp(alloc, locks, max_store, FZ_VERSION)

如果您研究 MuPDF 或 SumatraPDF 提供的示例代码,您会看到它们使用 fz_new_context 而不是 fz_new_context_imp。但是,正如上面的代码行所示,fz_new_context 只是 C 中的一个宏定义,它没有从 DLL 导出到 C# 世界。当我们在 P/Invoke 库时,必须改用 fz_new_context_imp

接下来,我们可以在 version.h 中找到 FZ_VERSION 的定义。

警告:如果您传递的版本号与引擎版本不匹配,上述函数将返回 IntPtr.Zero 而不是 fz_context 实例。在编译自己的 libmupdf.dll 库时,至少应查看 fitz\version.h 文件,并检查 FZ_VERSION 定义是否与此处传递给函数的 version 参数匹配。

如果您有进一步的问题,请参阅 fitz 和 pdf 文件夹中的那些 *.h 文件。

我们对剩余的函数做同样的事情,并相应地编写 P/Invoke 函数。在此过程中,我们不可避免地会遇到另外三个新结构:fz_bboxfz_rectanglefz_matrix,它们由上面列出的函数引用。它们都非常简单。我们只需根据 fitz.h 中的定义,将它们定义为代码中的 struct。最终,我们将得到类似下面的代码,可以供 P/Invoke 使用。

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 uint FZ_STORE_DEFAULT = 256 << 20;
    const string DLL = "libmupdf.dll";
    const string MuPDFVersion = "1.6";
 
    [DllImport (DLL, EntryPoint="fz_new_context")]
    static extern IntPtr NewContext (IntPtr alloc, IntPtr locks, uint max_store, string version);
    public static IntPtr NewContext () {
        return NewContext (IntPtr.Zero, IntPtr.Zero, FZ_STORE_DEFAULT, MuPDFVersion);
    }

    [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 = "pdf_open_document_with_stream")]
    public static extern IntPtr OpenDocumentStream (IntPtr ctx, IntPtr stm);

    [DllImport (DLL, EntryPoint = "fz_close")]
    public static extern IntPtr CloseStream (IntPtr stm);
 
    [DllImport (DLL, EntryPoint = "pdf_close_document")]
    public static extern IntPtr CloseDocument (IntPtr doc);
 
    [DllImport (DLL, EntryPoint = "pdf_count_pages")]
    public static extern int CountPages (IntPtr doc);
 
    [DllImport (DLL, EntryPoint = "pdf_bound_page")]
    public static extern void BoundPage (IntPtr doc, IntPtr page, ref Rectangle bound);

    [DllImport (DLL, EntryPoint = "fz_clear_pixmap_with_value")]
    public static extern void ClearPixmap (IntPtr ctx, IntPtr pix, int byteValue);
 
    [DllImport (DLL, EntryPoint = "fz_lookup_device_colorspace")]
    public static extern IntPtr LookupDeviceColorSpace (IntPtr ctx, string colorspace);
 
    [DllImport (DLL, EntryPoint = "fz_free_device")]
    public static extern void FreeDevice (IntPtr dev);
 
    [DllImport (DLL, EntryPoint = "pdf_free_page")]
    public static extern void FreePage (IntPtr doc, IntPtr page);
 
    [DllImport (DLL, EntryPoint = "pdf_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 = "pdf_run_page")]
    public static extern void RunPage (IntPtr doc, IntPtr page, IntPtr dev, ref 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);
 
}
  

使用代码 - 程序流程

本文档的目标是将 PDF 文档渲染成图片。过程非常直接。

  1. 加载文档。
  2. 遍历文档中的每一页。
  3. 将每一页渲染为 Bitmap 并保存到磁盘。
  4. 在操作过程中释放分配的资源。

代码流程骨架

代码如下。

static void Main (string[] args) {
    IntPtr ctx = NativeMethods.NewContext (); // Creates the context
    IntPtr stm = NativeMethods.OpenFile (ctx, "test.pdf"); // opens file test.pdf as a stream
    IntPtr doc = NativeMethods.OpenDocumentStream (ctx, ".pdf", stm); // opens the document
    int pn = NativeMethods.CountPages (doc); // gets the number of pages in the document
    for (int i = 0; i < pn; i++) { // iterate through each pages
        IntPtr p = NativeMethods.LoadPage (doc, i); // loads the page (first page number is 0)
        Rectangle b = new Rectangle ();
        b = NativeMethods.BoundPage (doc, p, ref b); // gets the page size
        using (var bmp = RenderPage (ctx, doc, p, b)) { // renders the page and converts the result to Bitmap
            bmp.Save ((i+1) + ".png"); // saves the bitmap to a file
        }
        NativeMethods.FreePage (doc, p); // releases the resources consumed by the page
    }
    NativeMethods.CloseDocument (doc); // releases the resources
    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); // gets the size of the page
    int height = (int)(pageBound.Bottom - pageBound.Top);
    ctm.A = ctm.D = 1; // sets the matrix as the identity matrix (1,0,0,1,0,0)

    // creates a pixmap the same size as the width and height of the page
    pix = NativeMethods.NewPixmap (context, 
      NativeMethods.LookupDeviceColorSpace (context, "DeviceRGB"), width, height);
    // sets white color as the background color of the pixmap
    NativeMethods.ClearPixmap (context, pix, 0xFF);
 
    // creates a drawing device
    dev = NativeMethods.NewDrawDevice (context, pix);
    // draws the page on the device created from the pixmap
    NativeMethods.RunPage (document, page, dev, ctm, IntPtr.Zero);
 
    NativeMethods.FreeDevice (dev); // frees the resources consumed by the device
    dev = IntPtr.Zero;
 
    // creates a colorful bitmap of the same size of the pixmap
    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 { // converts the pixmap data to Bitmap data
        // gets the rendered data from the 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++) {
                //Swap these here instead of in MuPDF because most pdf images will be rgb or cmyk.
                //Since we are going through the pixels one by one
                //anyway swap here to save a conversion from rgb to bgr.
                pl[2] = sl[0]; //b-r
                pl[1] = sl[1]; //g-g
                pl[0] = sl[2]; //r-b
                //sl[3] is the alpha channel, we will skip it here
                pl += 3;
                sl += 4;
            }
            ptrDest += imageData.Stride;
            ptrSrc += width * 4;
        }
    }
    // free bitmap in memory
    bmp.UnlockBits (imageData);
    NativeMethods.DropPixmap (context, pix);
    return bmp;
}

好了,一切都*几乎*完成了。只需运行程序,您就可以看到 test.pdf 中的每个 PDF 页面都已转换为 PNG 文件。

关注点

在实际开发中,您需要注意几个问题。

首先需要注意的是异常:文档可能已损坏、被占用等。Mupdf 库在发生此类问题时会抛出异常。但是,由于我们正在进行 P/Invoke。我们所能做的就是捕获 AccessViolationException,并重新抛出特定的、重新定义的异常,例如,在捕获加载文档时发生的异常时,可以重新抛出 PdfDocumentException。关键在于找出异常何时会抛出。您可以在 *.h 文件中找到信息。

其次要注意的是释放资源。您可以开发类来封装对象的创建和销毁。例如,您可以编写一个 MupdfPage 类来处理 fz_page 相关事宜。在 MupdfPage 的构造函数中,它创建 fz_page 实例。该类应实现 IDisposable 接口,并将 fz_free_page 的 P/Invoke 代码放在 Dispose 方法中以释放资源。

第三件需要考虑的事情是扩展功能。目前本文档介绍的包装器允许您将 PDF 页面转换为图片。您可能想做更多的事情,并从 Mupdf 中获得更多乐趣。有几种方法:

  1. 深入研究 pdf.hpdf\*.h。该文件中还有许多本文档未涵盖的函数。例如,您可以找到打开密码保护文档的函数(搜索 pdf_needs_passwordpdf_authenticate_password)。
  2. 从示例和现有的开源项目中学习。官方 Mupdf 网站提供了几个示例,并且上述 C 头文件在线提供。本文档中提到的三个项目也是您进一步学习的好参考。
  3. 再次注意:SumatraPDF 项目编译的 libmupdf.dll 文件不包含 fitz.h 文件中的所有“fz_”函数,但您可以在 pdf.h 中找到实际的实现。要找出哪些函数缺失,可以在 SumatraPDF 源代码的libmupdf.def文件中查找函数名。如果函数名列在那里,它可能可以在您的 P/Invoke 代码中使用;否则,请改用 pdf.h 中的函数。

第四个需要考虑的是在 64 位机器上运行。这是使用 P/Invoke 时最常见的问题之一。本文档下载中提供的 DLL,从 SumatraPDF 源代码编译而来,是 32 位的。它必须从 32 位的 .NET Framework 调用。在 64 位机器上,默认的 .NET Framework 是 64 位的,这在 P/Invoke 32 位 Mupdf DLL 时会失败。与其重新编译 Mupdf DLL 为 64 位 DLL,不如将 C# 项目的 CPU 平台从*Any CPU*更改为*x86*。因此,编译的 .NET 程序将被强制在 32 位 .NET Framework 上运行,并与 32 位 Mupdf DLL 正常工作。

第五个要考虑的可能是图像处理基础。在我们的示例中,我们使用默认分辨率渲染 PDF 页面。但是如果我们想让它更小(例如页面缩略图)或更大(方便阅读)怎么办?图像的“缩放”因子在于我们传递给 RunPage 函数的 Matrix 结构。如果我们修改其 AD 数字,图像将分别水平和垂直缩放(修改其他值会导致图像旋转、倾斜或平移)。如果我们设置 AD 为负值,渲染的图像将水平或垂直翻转。顺便说一句,别忘了调整渲染的 pixmap 和 Bitmap 的尺寸以容纳缩放后的图像。以下是供您参考的代码。您可能会将 zoomXzoomY 编程为 RenderPage 函数的参数,并将原始函数中的第二个代码块替换为下面的其余代码行。

float zoomX = 1.0, zoomY = 1.0;

int width = (int)(zoomX * (pageBound.Right - pageBound.Left)); // gets the size of the scaled page
int height = (int)(zoomY * (pageBound.Bottom - pageBound.Top));
ctm.A = zoomX;
ctm.D = zoomY; // sets the matrix as (zoomX,0,0,zoomY,0,0) 

鸣谢

  • pdfviewer-win32 项目让我接触了 XPDF 和 Mupdf。
  • mupdf-converter 项目启发我不用 C++/CLI 编写这个包装器。
  • SumatraPDF 项目在编译 DLL 文件方面提供了很大帮助。

历史 

  • 添加了关于 SumatraPDF 和 MuPDF 同步停止的说明。2017-9-15
  • 更新以反映 MuPDF 网站的变化。2017-1-24
  • 更新了 SumatraPDF 项目的位置。2014-12-30
  • 再次修订,以反映 SumatraPDF 和 MuPDF 的最新变化。2014-10-9
  • 修订以反映 SumatraPDF 和 MuPDF 的最新变化。2013-9-29
  • 许可证已更改为 GPL3(与 MuPDF 兼容)。2013-3-28。
© . All rights reserved.