简介:面向 Delphi/C++Builder 开发者的 PDF 处理组件套件,基于开源 PDFium 渲染引擎,提供查看、导航、文本提取与编辑等功能。支持 Delphi/C++Builder 5-10.3 与 Lazarus 2.0.6,且版本内置完整源代码,适合需要深度集成、跨平台或按需修改渲染细节的桌面应用项目。资源包共 1010 个文件,压缩后 23.75MB。671 个 dcu 为已编译单元,35 个 pas 对应 Delphi 源码,46 个 obj 与 44 个 hpp 便于 C++Builder 链接与引用,31 个 dpk/10 个 dpr 勾勒包与项目结构;同时包含 pdfium 核心 dll、示例工程、chm 帮助文档和说明文件,安装、查阅与二次编译都有据可依。当前已有 622 人学习/下载。完整源码交付意味着既可快速安装组件直接调用,也可顺藤摸瓜查看 PDFium 调用链,针对业务场景调整渲染策略;示例与文档覆盖环境配置到 API 使用,让桌面端 PDF 功能的集成、调试和维护更省力。
1. 源码版 PDFium 组件套件:Delphi 老项目终于能用上 Google 渲染引擎
接手过一个用 Delphi 7 维护了十多年的内部工具,所有 PDF 预览都靠 Shell 调外部小程序转成图片再显示,换一台机器就不是缺组件就是路径不对。后来换成了 Winsoft 的 PDFium Component Suite 5.4 源码版,把 Google 的 PDFium 渲染引擎直接集成进 VCL 程序里,预览、渲染、文本提取全在自己进程内完成,彻底甩掉了外部依赖。这可能是 Delphi/C++Builder 老项目接 PDF 能力最省心的一条路,因为 FULL_SOURCE 意味着每个 Pascal 声明都摊在你面前,DLL 可以自己编译,没有黑匣子。这篇笔记写给准备把 PDF 渲染和解析能力做进桌面应用的人,按我实际跑过的路径,从套件里有什么、怎么编译装进 IDE,到第一张图渲出来、五个典型翻车点,再到把渲染质量往上推一档的技巧。
2. PDFium 组件套件:套件里到底有什么,以及为什么值得换成它
2.1 PDFium 引擎带来的能力边界
PDFium 是 Google 开源的 PDF 渲染引擎,Chrome 内置的 PDF 阅读器就是基于它。它不止能渲染页面,还能解析目录、提取文本、读取元数据和注解。组件套件的价值在于把这些 C 接口封装成 VCL 组件,让 Delphi/C++Builder 开发者不用手动维护上百个外部函数声明,打开 IDE 拖一个组件就能起一个文档对象。
能力范围要心里有数:它能拿 PDF 做页面渲染、文本提取、页面导航、缩放旋转、生成位图快照,这些覆盖了绝大多数桌面端 PDF 工具需求。但 XFA 表单支持有限,带复杂动态表单的 PDF 打开后可能丢字段;部分 PDF 1.7 以后的高级特性也有兼容边界,遇到打不开的文档要先想到这个引擎的边界,而不是怀疑是代码写错了。
2.2 组件封装层:从 C API 到 VCL 控件的映射
拿到源码包后你会发现,它的结构基本分两层。底层是对 PDFium C API 的 Pascal 声明单元,把 FPDF_ 开头的函数按功能模块整理成若干 .pas 文件,比如文档加载、页面操作、位图渲染、文本提取各占一块;上层是真正的组件层,把底层的文档句柄和页面句柄包装成带生命周期管理的对象。
用的时候不需要直接碰 C API,核心映射关系是这样的:FPDF_LoadDocument 对应组件的 LoadFromFile 方法,FPDF_LoadPage 对应页面集合的索引或方法,FPDFBitmap_Create 和 FPDF_RenderPageBitmap 封装成 Page.Render 这类方法,最后把位图数据送到 TCanvas 上显示或保存。组件层替你管住了句柄释放顺序,这恰恰是自己裸调 C API 最容易出内存泄漏的地方。
2.3 版本兼容的边界:5.4 与 IDE 版本范围意味着什么
标题里的 "for 5-10.3" 指 Delphi/C++Builder 的版本区间,从 Delphi 5 一直到 RAD Studio 10.3。这意味着它维护了一条很宽的编译器兼容链:Delphi 5/6/7 时代的 AnsiString 代码、2007 到 XE 系列的单字节向 Unicode 过渡、以及 10.3 上默认的 UnicodeString,套件内部都用条件编译处理掉了。
值得注意两个边界:一是兼容范围是 VCL,不是 FMX,因为渲染上下文是给 Canvas 用的,FireMonkey 下要跑还得另想办法;二是 10.3 之后的版本不在官方保证里,虽然源码在手,自己加新版编译条件也能用,但没人保证零修改编译通过。我用它在 Delphi 10.3 上跑得很顺,Delphi 5 的环境没实际验证过,老编译器用户建议先拿 Demo 编译一遍再决定。
3. 安装与源码编译:把 FULL_SOURCE 变成自己能维护的一步
3.1 拿到源码包先做的事:摸清目录再动手
解压这类源码套件,我一般不会急着打开 IDE 安装,先看目录结构。常见的组件源码包会分成几个固定区域:源码单元放在 Source 目录,安装用的包文件放在 Packages 目录,示例工程单独放 Examples 或 Demo 目录,还有放资源文件和文档的目录。这么做的好处是,你能判断出三个关键问题:包文件名称里有没有写 IDE 版本,示例工程用什么版本编译的,以及源码单元之间有没有相对路径依赖。
确认目录结构之后,打开套件自带的说明文件看有没有额外的编译前提,比如需要先装某个底层库或者得有特定版本的 IDE 补丁。大多数情况直接编译即可,但花两分钟摸清目录结构,能省下后面排错的好几个小时。
3.2 IDE 内安装:一条路径把组件装进工具栏
安装 VCL 组件包的标准做法是走 IDE 的组件安装入口,Delphi 的 Component > Install Packages 菜单里点 Add 按钮,然后选中套件里对应你 IDE 版本的包文件。编译成功之后,组件会出现在工具面板的新分组里,拖到窗体上就能用。
这里有几个实操细节。第一,包文件如果按 IDE 版本分了目录,选中对应版本的准没错;如果没有分目录,说明套件靠编译条件自动适配,直接编译自动会选对分支。第二,把源码目录加进 IDE 的 Library 搜索路径,否则新建工程时拿到的是编译期需要的 .dcu 和 .bpl 而源码单元引用会报找不到文件。第三,安装过程如果爆出找不到某个单元的错误,多半是路径里有空格或者中文目录名,把整个源码包挪到纯英文路径下再试。
3.3 用命令行完成批量编译:不用打开 IDE 的路线
如果不想在 IDE 里频繁点按钮,或者你的构建流程走的是持续集成,可以用 Delphi 自带的命令行编译器 dcc32 手动编译。写一个简单的批处理脚本,按顺序清理产物、编译运行期包、再编译设计期包,最后把生成的 .bpl 复制到 IDE 的 bin 目录完成安装。
编译命令长这样,我用批处理演示,逻辑放在注释里:
rem 切到套件根目录,路径自己改 cd /d D:\Components\PDFiumSuite rem 清理上一次编译产物:删除所有 bpl、dcp、dcu 目录 for /r %%i in (*.bpl *.dcp) do del "%%i" for /d %%i in (__history) do rmdir /s /q "%%i" rem 编译运行期包:假设包文件为 .dpk,输出路径写到当前目录的 BPL 输出区 dcc32 -B -Q -JPHN -DRELEASE -U".\Source" -LE".\BPL" -LN".\DCP" -O".\Source" ".\Packages\RunTime.dpk" rem 编译设计期包:RUN 级包编译成功后 DLL 级包才能链上 dcc32 -B -Q -JPHN -DRELEASE -U".\Source;.\BPL" -LE".\BPL" -LN".\DCP" -O".\Source" ".\Packages\DesignTime.dpk"命令参数说明:-B 强制全量重建,防止旧 .dcu 干扰;-Q 静默模式,只输出错误不刷屏;-JPHN 生成 C 风格头文件,如果只给 Delphi 用可以去掉;-U 指定单元搜索路径,这里指向套件源码目录;-LE 和 -LN 分别指定编译出的 .bpl 和 .dcp 输出目录;-O 指定编译产物目录。包文件名我写的是通用示例,实际编译时把 RunTime.dpk 和 DesignTime.dpk 替换成套件里真实的包文件路径。
为什么不装进 IDE 也要编译一遍?因为哪怕你只是想在项目里引用源码单元而不是安装组件,编译一次也是一次完整的自检,能在几秒内暴露出编译器不认的语法、缺失的依赖单元和路径问题。
4. 最小可用示例:加载 PDF、渲染第一页、保存为 BMP
4.1 放置组件与初始化:三种常见集成方式
套件集成进你的工程不外乎三种方式:安装成组件从工具面板拖到窗体上,适合做有界面的预览程序;运行时动态创建组件对象,适合做无界面服务或批处理工具;直接引用底层源码单元跳过组件层,适合只需要渲染和提取文本不需要界面交互的场景。我的建议是,如果不是纯命令行工具,都走组件方式,生命周期由框架管理,省心。
初始化的时候我习惯把全局初始化放在程序主窗体的 OnCreate 里,退出时在 OnDestroy 里释放。PDFium 的底层库里有一个全局初始化调用,不调用的话后续创建文档对象会直接失败。组件套件通常会把这个初始化封装在某个单元里,你只要在工程里调用对应过程即可,具体过程名以套件源码为准,我这里是按常见做法描述。
4.2 渲染第一页:核心代码与参数解读
渲染一页并保存成 BMP 的最小代码大概是这样的,我给关键行加了注释:
uses PDFiumCore, // 底层 API 封装单元,名称以实际套件为准 PDFiumVCL, // 组件层单元,名称以实际套件为准 Vcl.Imaging.GraphUtil; procedure TForm1.RenderFirstPageToBmp(const AFileName: string; ABmp: TBitmap); var Doc: TpdfDocument; // 组件层文档对象 Page: TpdfPage; // 页面对象 BitmapInfo: FPDF_BITMAP; RawData: PByte; hDC: HDC; DPI_X, DPI_Y: Integer; PageW, PageH: Double; PixelW, PixelH: Integer; Stride: Integer; begin Doc := TpdfDocument.Create(nil); try Doc.LoadFromFile(AFileName); // 打开 PDF,内部会调用 FPDF_LoadDocument Page := Doc.Pages[0]; // 取第一页,Pages 是文档对象的页面集合 Page.LoadPage; // 显式加载页面,成功后句柄才有效 // 取渲染尺寸:把页面物理尺寸按屏幕 DPI 换算成像素。96 DPI 下直接按点转像素 hDC := GetDC(0); try DPI_X := GetDeviceCaps(hDC, LOGPIXELSX); DPI_Y := GetDeviceCaps(hDC, LOGPIXELSY); finally ReleaseDC(0, hDC); end; PageW := Page.Width; // 页面宽度,PDF 基本单位是点(1/72 英寸) PageH := Page.Height; // 页面高度 PixelW := Round(PageW * DPI_X / 72.0); PixelH := Round(PageH * DPI_Y / 72.0); // 创建 32 位 BGRA 位图,FPDFBitmap_Create 的参数是宽、高、是否 Alpha BitmapInfo := FPDFBitmap_Create(PixelW, PixelH, True); try // 渲染前把背景清成白色,避免透明区域叠加后发黑 FPDFBitmap_FillRect(BitmapInfo, 0, 0, PixelW, PixelH, $FFFFFFFF); // 渲染数组标记:从左上角开始,按行从上到下输出 Page.Render(BitmapInfo, 0, 0, PixelW, PixelH, 0, FPDF_RENDER_ANNOT or FPDF_RENDER_LIMITED_IMAGECACHE); RawData := FPDFBitmap_GetBuffer(BitmapInfo); // 取像素缓冲首地址 Stride := FPDFBitmap_GetStride(BitmapInfo); // 每行字节数 // 把 BGRA 缓冲按行拷贝进 VCL TBitmap 的 ScanLine ABmp.PixelFormat := pf32bit; ABmp.Width := PixelW; ABmp.Height := PixelH; CopyDIBToDC(ABmp.Canvas.Handle, RawData, PixelW, PixelH, Stride); finally FPDFBitmap_Destroy(BitmapInfo); end; finally Page.Free; // 释放顺序:先页面后文档 Doc.Free; end; end;代码逻辑拆解:先创建文档对象并加载文件,这一部对应 PDFium 的打开文件操作。Pages[0] 拿到首页对象后必须 LoadPage,这是容易漏掉的一步,不加载就渲染会得到空白图或者直接崩溃。尺寸换算用的是 PDFium 最基本的单位体系,PDF 页面尺寸以点为单位,一英寸等于 72 点,想获得屏幕上清晰的预览就要把它乘上当前显示器的 DPI 再除以 72。渲染函数里最关键的是最后的标志组合,FPDF_RENDER_ANNOT 表示把页面注解也画出来,不传的话 PDF 里的高亮和批注全部消失,这是新手最容易忽略的参数。
4.3 参数设置与内存释放的注意点
渲染清晰度取决于位图尺寸而不是渲染函数本身,把 PixelW 和 PixelH 放大两倍再渲染,得到的就是更高分辨率的预览。如果想做缩略图,把目标宽高设成 200 像素左右即可,PDFium 的缩放插值效果足够用。位图的 Alpha 通道参数设为 True 时,渲染前不清底的话透明区域会保持透明黑,保存成图片后预览是黑的,所以 FillRect 一步不要省。
释放顺序遵循后创建的先释放,先销毁页面对象再销毁文档对象,最后销毁位图缓冲。我在第一次写这个程序时把顺序写反了,页面句柄先释放后再给文档对象,结果每到第二页就访问违例。套件封装的组件对象如果实现了析构自动清理,你可以只释放最外层对象,但我建议还是手动按顺序做一次,因为这个顺序本身就是理解内部句柄关系的钥匙。
5. PDFium 组件套件避坑:5 个让新手翻车的真实场景
5.1 高 DPI 屏幕上渲染出来的图是糊的
现象是程序在自己电脑上显示正常,拿到 2K 高分屏或 150% 缩放的机器上,PDF 预览文字边缘明显发虚,放大看像低分辨率拉伸。
原因是渲染尺寸是按 PDF 页面原始点数直接用 1:1 换算的,没有乘当前显示器的缩放比。PDFium 默认渲染就是 96 DPI 的逻辑分辨率,高分屏需要更大的像素尺寸来匹配物理像素密度。
解决方法是把渲染目标尺寸乘以缩放系数,先 GetDeviceCaps 拿到 LOGPIXELSX,然后除以 96 得到缩放比,再应用到 PixelW 和 PixelH 上。代码在上一章已经有,核心就是把 72 除以当前 DPI 得到正确的换算比例。
5.2 中文和生僻字渲染成方块
现象是大部分中文正常,但部分生僻字、繁体字或特殊符号变成方块或空白,转成图片后也一样。
原因是渲染端缺少对应的字体,PDFium 在渲染文本时发现文档里没有嵌入字体子集,就会去系统字体里找替代,没找到就画方块。这跟套件关系不大,是所有 PDF 渲染引擎的共同问题。
解决分两步:一是能用带字库的 PDF 就尽量用;二是需要渲染动态生成的 PDF 时,确认生成端嵌入了字体子集。代码层面可以调节 FPDF_AddFont 注册自定义字体目录,把需要的中文字体文件放进套件查找路径里,渲染时通过字体管理接口加载。我遇到生僻字场景时,用系统自带的思源黑体注册后解决,普通公文场景用系统宋体也能覆盖。
5.3 渲染循环内存涨个不停,最后程序崩掉
现象是批量把几百个 PDF 转成图片,内存占用持续上涨,跑到一两百个文件后抛出内存不足或者访问违例。
原因是每个页面渲染都创建了位图缓冲和页面对象,释放时机不对。最常见的做法是在循环里每次都创建新的位图对象,用完没有释放;或者释放顺序错了,页面句柄比位图先被销毁,导致位图读取到悬空句柄。
解决方法是把资源获取和释放写成 try/finally 结构,页面、位图逐层缩进,确保异常时也能释放。另外,大循环里尽量复用位图缓冲对象,反复创建销毁不仅慢,还会让内存碎片化。我一般先创建一个最大页面尺寸的位图,循环里只改尺寸和内容,渲染完成后立即把缓冲拷走,最后统一释放。
5.4 64 位编译环境下源码包编译不过
现象是源码包在 32 位下编译安装一切正常,切到 64 位平台时报大量类型不匹配和函数指针错误。
原因是部分早期版本的组件源码在声明外部函数时,把指针类型硬编码成了 32 位尺寸,或者回调函数声明没有按 Windows 64 位调用约定修正。还有一类情况是包文件里没有为 Win64 单独配置编译条件,编译器选了默认分支。
解决方法是先看套件有没有为 Win64 提供独立的包文件或条件编译宏。如果没有,就得在源码单元里找到硬编码的指针类型定义,替换为 NativeInt 或 Pointer 这类平台无关类型。我在 Delphi 10.3 上把底层声明的 Cardinal 指针改成了 NativeUInt 后,64 位编译一次性通过。老项目如果暂时不需要 64 位,可以不折腾,但新项目一定优先 64 位,否则后面接第三方 64 位库会有兼容问题。
5.5 多文档同时打开时组件状态互相干扰
现象是程序里开了两个文档对象,切换页签或关闭一个文档时,另一个的渲染画面出错或者直接崩溃。
原因是组件内部某些全局状态没有隔离,常见的是渲染上下文或字体表是模块级共享的,多文档实例同时工作时互相覆盖。
解决方法是每个文档使用独立实例,不要共用组件对象;所有渲染操作在 UI 主线程同步执行,不要开线程并发渲染同一个文档。如果必须并发,给每个页面建独立位图渲染,并在渲染期间加锁。组件的底层接口没有提供线程安全承诺,我把两路文档同时渲染改成串行后,问题没有再出现。
6. 把渲染质量再往上推一档:清晰度、CJK 字体与批量渲染技巧
批量渲染 PDF 转图片是套件最常被用的场景,这里有一个很容易被忽略的性能技巧:渲染尺寸不要超过目标输出尺寸太多,否则浪费内存和 CPU。比如输出到 300 DPI 的 TIFF,就按 300 DPI 计算位图尺寸,PDFium 的插值质量在 1:1 以内足够应付印刷预览,没必要渲染 600 DPI 再缩小。
文本提取这个方向,套件的底层封装了 FPDFText_LoadPage 系列接口,可以用来做全文搜索和数据抽取。按页提取文本时注意,PDF 里文本可能是按绘制顺序存的,不是按阅读顺序,从表格或分栏文档里提出来的文本顺序可能很怪。这是 PDF 格式本身的问题,不是引擎缺陷。我会先把提取结果按坐标排序,再拼接段落,效果会好很多。
渲染标志位要专门拿出来调一遍。FPDF_RENDER_ANNOT 控制注解显示,FPDF_RENDER_LIMITED_IMAGECACHE 控制图片缓存上限,渲染大体积扫描 PDF 时会明显降低内存峰值。做打印输出时把 DPI 提高但加上 LIMITED_IMAGECACHE,能避免大图同时进内存导致的内存暴涨。
CJK 字体单独说一下,PDFium 官方对一些 CJK 字体的内置支持有限,遇到日文汉字或中文生僻字渲染异常,问题基本都在字体注册。套件源码里一般会有字体加载的接口,我在处理日文文档时手动注册了系统中文字体目录,生僻字覆盖率有明显改善。
我自己的教训是,第一次接入时为了赶进度直接拖组件用默认参数,结果生成的 PDF 预览图在客户的高分屏上糊成一片,后来才明白渲染尺寸必须与显示比例联动;还有一次批量任务跑了几千页后才注意到没有释放中间位图,内存直接顶到上限机器开始卡顿。这些细节不踩一遍很难记牢,现在每接到新项目,我第一件事就是把这几个参数和释放顺序写进规范文档里。希望帮到你。
本文还有配套的精品资源,点击获取