Delphi 10.3 全源码 PDFium 组件实战:VCL 中 PDF 显示、表单填写与注释保存
简介:Winsoft PDFium Component Suite 5.4 是面向 Delphi 与 C++ Builder 开发者的 PDF 处理组件包,基于开源 PDFium 渲染引擎,可在 Delphi/C++ Builder 5 至 10.3 及 Lazarus 2.0.6 环境中实现 PDF 查看、页面导航、文本提取与内容编辑等功能,适合需要在桌面应用中集成 PDF 能力的初中级开发者。压缩包共 1010 个文件,约 23.75MB,以 671 个 dcu 编译单元、35 个 pas 源码、31 个 dpk 包工程及 44 个 hpp 头文件为主体,另含 res/dcr 资源、dfm/lfm 窗体、dll 与 pdfium 运行库及示例工程,完整覆盖从源码到编译产物的组件结构。资源附带全部源代码,便于读者研读渲染封装与接口设计,按包工程组织方式二次编译或裁剪功能,并借助示例快速验证文本提取与编辑流程。目前已有 623 人学习下载,可作为 Delphi 平台 PDF 组件选型与源码研究的实用参考。
1. 拿到一份 Delphi 10.3 的 PDFium 全源码包,先别急着编译
上周有个做桌面端的朋友甩过来一个压缩包,文件名长得像乱码:Winsoft_PDFium_Component_Suite_5.4_for_5-10.3_FULL_SOURCE.rar。他原话是「客户要在 Delphi 10.3 的 ERP 里直接看 PDF、填表单、盖电子章,我找了一圈要么是壳要么是试用版,这个带 FULL SOURCE,你帮我看看能不能用」。这其实是很多 Delphi 老项目组的真实处境——VCL 界面跑了十几年,业务逻辑全在 Pascal 里,突然要加 PDF 渲染和编辑,用外部进程调 Adobe 太丑,用 WebView 嵌 PDF.js 又要处理一堆兼容性,最省事的路径就是找一个能直接拖到 Form 上的原生组件。
这份资源就是干这个的:Winsoft 的 PDFium Component Suite,版本 5.4,覆盖 Delphi 5 到 10.3 的源码全量包。PDFium 是 Chromium 里那个 PDF 渲染引擎,Google 维护,渲染质量和 Chrome 一致;Winsoft 做的事是把它包成 Delphi 的 VCL 组件,让你在 Object Inspector 里设几个属性就能显示 PDF、提取文字、做注释、填 AcroForm 字段。关键词里的「delphi 10.3」「PDF 编辑组件」说的就是它。适合谁?手上是 Delphi 5~10.3 的老项目、需要离线渲染、不想引入浏览器内核、又要求能改源码排查问题的团队。不适合谁?纯 FMX 跨平台或者已经上 11/12 的新项目,这套的 DCU 和包结构是按老版本 IDE 组织的,硬上会翻车。
2. 拆包看结构:源码、DCU 与 PDFium 动态库怎么摆
2.1 压缩包里到底有什么
解压之后不要急着双击.dpr,先按目录扫一遍。这类全源码包的典型结构是「组件源码 + 预编译 DCU + 原生 DLL + Demo」,四类东西混在一起,分不清就会在编译期和运行期各踩一次坑。我一般先跑一遍文件清单,把扩展名归类:
| 目录/文件类型 | 内容 | 用途 | 注意点 |
|---|---|---|---|
Source/下.pas | 组件包装单元,如 PDFium 主控件、注释、表单类 | 可编译、可改 | 依赖顺序有讲究 |
Source/下.inc | 版本条件编译开关 | 区分 Delphi 5~10.3 | 改错一个开关整包编不过 |
Lib/或Dcu/ | 预编译.dcu | 免编译直接用 | 必须和 IDE 版本严格对应 |
Bin/或根目录 | pdfium.dll(32/64 位) | 运行时渲染核心 | 位数必须和宿主进程一致 |
Demos/ | 示例工程 | 验证环境 | 先跑 Demo 再动自己的工程 |
这里第一个认知要立住:组件源码和 PDFium 原生库是两层东西。.pas编出来的是 Delphi 侧的包装,真正解析 PDF 字节流的是pdfium.dll。所以「编译通过」不等于「能显示」,DLL 没放对位置、位数不匹配,表现是控件一片空白或者一创建就抛异常,而不是编译错误。很多人卡在这一步以为是源码有问题,其实是运行期加载失败。
2.2 版本条件编译:5.4 覆盖 5~10.3 的开关在哪
全源码包为了兼容十几个 Delphi 版本,.inc里一定有一堆{$IFDEF VERxxx}。Delphi 10.3 的符号是VER330(Rio),10.2 是VER320,再往前 7 是VER150。打开主.inc先确认两件事:当前分支是否已经包含VER330,以及字符集相关开关(UNICODE)是否按 10.3 的默认走。老包常见的翻车点是作者在 10.3 上没测全,某个{$IFDEF}分支里引用了 10.3 已废弃的单元。
// 典型的版本开关片段,位置在各 .inc 文件顶部 {$IFDEF VER330} // Delphi 10.3 Rio {$DEFINE PDFIUM_DELPHI_10_3} {$DEFINE PDFIUM_UNICODE} {$ENDIF} {$IFDEF PDFIUM_DELPHI_10_3} // 10.3 下 System.AnsiStrings 的引用方式与老版本不同 uses System.SysUtils, System.Classes, System.AnsiStrings; {$ELSE} uses SysUtils, Classes; {$ENDIF}这段的逻辑是:先用 IDE 自带的VER330符号判定版本,再派生出包内自己的语义开关,后面的单元统一用语义开关做条件编译,避免到处写VERxxx。参数上你只需要关心一处——如果你的 10.3 装了更新补丁导致某些 RTL 单元行为变化,优先改语义开关而不是动VER330判定,否则会影响整个包的版本识别。改完.inc记得全量重编(Build 而非 Compile),增量编译对条件编译的感知并不可靠,这是血泪经验。
2.3 安装组件包的正确顺序
不要一个个.pas往工程里拖。正确做法是先编运行时包(Runtime Package),再编设计期包(Design Package),最后在 IDE 里 Install。顺序错了的典型现象是设计期能看到控件页签,但一拖到 Form 上就报「Class not found」。
# 以命令行编译为例,先运行时包后设计期包 # 32 位 Debug 先跑通,再切 Release / 64 位 dcc32 -B -Q ^ -U"Source;Source\Core;Source\Forms" ^ -I"Source" ^ -NU"Lib\Dcu\Win32" ^ PDFium_Runtime.dpk dcc32 -B -Q ^ -U"Source;Source\Core;Source\Forms" ^ -I"Source" ^ -LU"Lib\Dcu\Win32" ^ PDFium_Design.dpk-B强制全量编译,-U是单元搜索路径,-I是 include 路径(.inc靠它找),-NU指定 DCU 输出目录,-LU让设计期包链接到刚编出来的运行时包。参数里最容易漏的是-I,漏了就会报找不到.inc,而错误信息往往指向一个莫名其妙的标识符未定义,排查方向会被带偏。编完把Bin下的pdfium.dll拷到 Demo 的 exe 同目录,先跑官方 Demo 确认渲染正常,再动自己的工程——这一步是分界线,Demo 不通就别往下走。
3. 把 PDF 显示到 VCL 窗体:从加载到翻页的最小闭环
3.1 控件初始化与文档加载
最小可用闭环就四步:放控件、设 DLL 路径、Load 文档、设当前页。Winsoft 这套的控件命名各家版本略有差异,但属性语义稳定:一个Document或FileName入口负责加载,PageNumber/PageCount管翻页,Zoom管缩放。下面这段是通用写法,落到你手上的具体类名按源码里的声明微调。
procedure TFormMain.FormCreate(Sender: TObject); begin // 1) 指定原生库位置,避免依赖系统 PATH PDFView1.LibraryPath := ExtractFilePath(ParamStr(0)) + 'pdfium.dll'; // 2) 加载文档,失败要拿到错误码而不是只看布尔值 if not PDFView1.LoadFromFile('D:\data\contract.pdf') then begin ShowMessage(Format('加载失败,错误码 %d', [PDFView1.LastErrorCode])); Exit; end; // 3) 初始视图参数 PDFView1.Zoom := 100; // 百分比,100 为原始尺寸 PDFView1.PageNumber := 1; // 1-based,注意不是 0 PDFView1.FitMode := fmFitWidth; // 按宽度适配,长文档常用 end;逻辑说明:LibraryPath显式指定 DLL 是必须的,靠系统目录搜索在客户机上十有八九找不到;LoadFromFile返回布尔只是「有没有抛异常」的粗粒度结果,真正定位问题要看LastErrorCode,PDFium 的错误码能区分「文件损坏」「加密」「不支持的滤镜」几类。参数上PageNumber是 1-based,这点和很多 PDF 库的 0-based 不一样,翻页逻辑里for i := 0 to PageCount - 1再PageNumber := i + 1是常见写法,写反了会越界。FitMode建议默认按宽度,因为合同、报表类文档横向溢出比纵向溢出更烦人。
3.2 翻页、缩放与渲染性能的三个参数
渲染性能在 VCL 里主要受三件事影响:是否每次翻页都重新解析、位图缓存策略、以及高 DPI 下的缩放重绘。PDFium 本身有页面缓存,但包装层如果每次PageNumber赋值都触发一次完整LoadPage,翻页就会明显卡顿。实操里我一般把「预加载相邻页」打开,并把缩放做成离散档位(75/100/125/150),避免鼠标滚轮连续改Zoom导致高频重绘。
procedure TFormMain.NextPage; begin if PDFView1.PageNumber < PDFView1.PageCount then begin PDFView1.PageNumber := PDFView1.PageNumber + 1; // 预渲染下一页,减少翻页白屏 if PDFView1.PageNumber < PDFView1.PageCount then PDFView1.PreloadPage(PDFView1.PageNumber + 1); end; end; procedure TFormMain.SetZoomStep(AIndex: Integer); const ZOOMS: array[0..4] of Integer = (75, 100, 125, 150, 200); begin if (AIndex >= Low(ZOOMS)) and (AIndex <= High(ZOOMS)) then PDFView1.Zoom := ZOOMS[AIndex]; // 离散档位,避免连续重绘 end;PreloadPage是这套组件里比较实用的一个口子,代价是内存——一份 200 页的扫描件全预加载能吃掉几百 MB,所以只预加载相邻一页是平衡点。Zoom用离散档位而不是绑定到 TrackBar 的连续值,是因为 PDFium 每次缩放都要重新栅格化,连续值会让 UI 线程排队。如果你的场景是「大屏看图纸、要平滑缩放」,那就得把渲染放到后台线程,VCL 控件只在主线程贴位图,这个改造量不小,先评估值不值。
3.3 文字提取与搜索高亮
只显示不够,很多业务要「在 PDF 里搜关键词并高亮」。PDFium 的文字提取是按页返回文本块,包装层一般提供GetText(PageIndex)或逐字符回调。搜索高亮的实现路径是:提取当前页文本 → 定位匹配区间 → 用注释或矩形覆盖层画高亮。注意 PDF 里的文字坐标是页面坐标系(点,1/72 英寸),和屏幕像素不是一回事,要经过当前Zoom和 DPI 换算。
function TFormMain.HighlightKeyword(const AKeyword: string): Integer; var PageText: string; PosIdx: Integer; begin Result := 0; PageText := PDFView1.GetPageText(PDFView1.PageNumber - 1); // 0-based 取文本 PosIdx := Pos(AKeyword, PageText); while PosIdx > 0 do begin // 用文本区间建高亮注释,坐标换算由组件内部按当前 Zoom 处理 PDFView1.AddTextHighlight(PDFView1.PageNumber - 1, PosIdx - 1, Length(AKeyword)); Inc(Result); PosIdx := PosEx(AKeyword, PageText, PosIdx + Length(AKeyword)); end; end;这里有个容易忽略的边界:GetPageText的索引是 0-based,而PageNumber是 1-based,两个体系混用是这套组件最容易写错的地方,我见过有人在循环里减了两次 1,结果永远搜不到第一页。另外扫描件(图片型 PDF)提取出来是空串,这不是组件坏了,是 PDFium 不做 OCR,要文字得先过 OCR 引擎。高亮注释在保存时会写进 PDF 的注释层,如果客户要求「高亮不能改原文件」,那就得用覆盖层画矩形而不是真注释,这是需求层面的选择,动手前先问清楚。
4. 表单填写与注释保存:编辑能力的边界在哪
4.1 AcroForm 字段读写
PDF 表单分两类:AcroForm(标准表单域,有字段名)和 XFA(Adobe 私有,复杂)。PDFium 对 AcroForm 支持较好,对 XFA 基本无能为力——这是选型时必须先确认的,客户给的模板如果是 XFA,这套组件填不了,别硬扛。AcroForm 的读写路径是:枚举字段 → 按字段名设值 → 保存。
procedure TFormMain.FillForm; var I: Integer; Field: TPDFFormField; begin if not PDFView1.HasAcroForm then begin ShowMessage('该文档不含标准表单域'); Exit; end; for I := 0 to PDFView1.FormFieldCount - 1 do begin Field := PDFView1.FormField[I]; // 按字段名匹配业务数据,字段名区分大小写 if Field.Name = 'BuyerName' then Field.Value := '某公司' else if Field.Name = 'SignDate' then Field.Value := FormatDateTime('yyyy-mm-dd', Now); end; PDFView1.SaveToFile('D:\data\contract_filled.pdf'); end;逻辑上先判HasAcroForm,因为很多「看起来像表单」的 PDF 其实是画上去的线框,没有字段,直接遍历会拿到 0 个字段然后静默失败。Field.Name区分大小写,且可能带层级前缀(如form1[0].BuyerName),实际项目里我一般先导出全部字段名做成映射表,而不是在代码里硬编码。SaveToFile默认是另存,原文件不动,这点符合大多数合规要求;如果要原地保存,先写临时文件再替换,避免保存中途崩溃把原件写坏。
4.2 注释、印章与增量保存
注释和印章是「编辑」里最常被问的两件事。注释(高亮、批注、便签)走 PDFium 的注释 API,印章本质是往页面内容流里贴一张图或一段矢量。这里的关键概念是增量保存(incremental save):PDF 支持在原文件末尾追加变更,而不是全量重写,好处是快、且保留原始签名;坏处是文件会越来越大,多次保存后体积膨胀明显。
procedure TFormMain.AddStamp(const AImagePath: string; AX, AY: Double); begin // 坐标单位为 PDF 点,原点在页面左下角 PDFView1.AddImageStamp( PDFView1.PageNumber - 1, // 0-based 页索引 AImagePath, AX, AY, // 左下角坐标 120, 60 // 宽高,单位点 ); // 增量保存:保留原内容,追加变更 PDFView1.SaveToFile('D:\data\stamped.pdf', True); end;参数里AX, AY是页面坐标系左下角原点,和屏幕坐标(左上角原点)Y 轴方向相反,直接拿鼠标坐标往里塞会上下颠倒,这是新手最常见的翻车点。SaveToFile第二个参数True表示增量保存,适合「盖个章就完事」的场景;如果要做「批量改几十页再存」,增量保存会让文件膨胀到不可接受,这时应该走全量重写(传False),代价是原始数字签名会失效——有签名的合同千万别用全量重写,这是合规红线。
4.3 加密文档与权限位
带密码的 PDF 分两种:打开密码(user password)和权限密码(owner password)。PDFium 能处理前者,加载时传密码即可;后者限制的是「能不能打印、能不能复制、能不能改」,即使能打开,编辑操作也会被拒绝。包装层一般有Password属性和Permissions只读属性。
PDFView1.Password := 'userpwd'; if PDFView1.LoadFromFile('D:\data\secure.pdf') then begin // 检查权限位,别等用户点了保存才报错 if not (pAllowModify in PDFView1.Permissions) then btnSave.Enabled := False; end;权限位要在加载后立刻检查并反映到 UI 上,而不是等用户操作完再弹错误——这是体验问题,也是很多交付项目被挑刺的地方。另外注意:权限密码在 PDF 规范里是「君子协定」,技术上能绕过,所以如果你的业务真的要求「不可篡改」,光靠 PDF 权限位不够,得配合数字签名或服务端校验。
5. 避坑清单:这份全源码包最容易翻车的五个地方
5.1 现象:编译通过,运行时控件一片空白
原因:pdfium.dll没找到或位数不匹配。32 位宿主进程加载 64 位 DLL 会直接失败,但异常可能被包装层吞掉,表现为空白而非报错。解决:用 Dependency Walker 或 Process Monitor 看加载路径,确认 exe 同目录下的 DLL 位数与编译目标一致;LibraryPath显式指定绝对路径,别依赖 PATH。
5.2 现象:设计期能拖控件,运行期报「Class not found」
原因:设计期包和运行时包的 DCU 版本不一致,或者只装了设计期包没编运行时包。解决:删掉Lib\Dcu下所有旧 DCU,按「运行时包 → 设计期包 → IDE Install」顺序全量重编;检查 IDE 的 Library Path 里是否残留了其他版本的组件路径,路径顺序会决定加载哪个 DCU。
5.3 现象:中文路径下加载 PDF 失败,英文路径正常
原因:老版本包装层内部用了 ANSI 版文件 API,10.3 默认 Unicode 字符串传进去被截断。解决:确认.inc里PDFIUM_UNICODE已定义;如果源码里确实有AnsiString转换,改成WideString或先LoadFromStream再解析,绕开路径编码问题。这是 Delphi 老组件在 10.x 上的通病,不是这份包独有。
5.4 现象:翻页越来越卡,内存持续上涨
原因:每次翻页都新建页面对象却没释放,或者预加载开了但没设上限。解决:确认PreloadPage只预加载相邻页;在PageNumber变更前后用任务管理器看私有工作集,如果单调上涨就是泄漏,检查包装层是否在OnPageChanged里重复LoadPage。PDFium 的页面对象需要显式ClosePage,包装层漏掉是常见 bug,源码在手就直接补。
5.5 现象:保存后的 PDF 在别的阅读器里打不开
原因:增量保存时追加的内容流偏移计算错误,或者保存过程中异常中断留下半截文件。解决:保存一律先写临时文件,成功后原子替换;跨阅读器验证至少用两个不同引擎(比如系统自带的和浏览器内置的)各开一次;如果只有增量保存出问题,切全量重写对比,能快速定位是增量逻辑还是内容流本身的问题。
6. 进阶:把渲染搬出 UI 线程,以及一份可复用的验证清单
到这一步,基本功能都能跑了,真正决定这套组件能不能上生产的是两件事:大文档下的 UI 响应,以及交付前的验证覆盖。先说线程。VCL 控件只能在主线程碰,但 PDFium 的渲染本身可以在后台线程做——思路是后台线程用 PDFium 的文档对象渲染到位图,主线程只负责把位图贴到控件上。包装层如果没暴露线程安全的渲染接口,就得自己包一层,注意每个线程要有独立的文档实例,PDFium 的文档对象不是线程安全的,共享会随机崩溃。
type TRenderTask = class(TThread) private FDocPath: string; FPageIndex: Integer; FBitmap: TBitmap; protected procedure Execute; override; public constructor Create(const APath: string; APage: Integer); property Bitmap: TBitmap read FBitmap; end; procedure TRenderTask.Execute; var Doc: TPDFDocument; begin // 后台线程独立打开文档,不复用 UI 线程的实例 Doc := TPDFDocument.Create; try Doc.LoadFromFile(FDocPath); FBitmap := Doc.RenderPage(FPageIndex, 150); // 150 DPI finally Doc.Free; end; end;这段的关键约束是「独立实例」和「DPI 显式指定」。RenderPage的 DPI 参数决定位图分辨率,屏幕上 96 DPI 够用,但要打印或导出就得 150~300,提前想清楚用途再定,事后改会牵动整个缓存策略。线程里创建的TBitmap跨线程传给主线程时要Synchronize或走消息队列,直接赋值是未定义行为。
再说验证清单。这套组件涉及「Delphi 包装层 + 原生 DLL + PDF 规范」三层,任何一层出问题表现都相似,所以交付前我固定跑一遍下面这张表,比事后救火省事得多:
| 验证项 | 方法 | 通过标准 |
|---|---|---|
| DLL 加载 | 换一台没装开发环境的机器跑 exe | 不报缺库,控件正常创建 |
| 位数匹配 | 32/64 位各编一次 | 两个目标都能加载对应 DLL |
| 中文路径 | 文件放在含中文和空格的目录 | 正常加载、正常保存 |
| 加密文档 | 带 user password 和 owner password 各一份 | 前者能开,后者权限位正确反映 |
| 表单往返 | 填值保存后用另一阅读器打开 | 字段值保留、字段名不变 |
| 大文档 | 200 页以上扫描件连续翻页 | 内存稳定、翻页无明显卡顿 |
| 增量保存 | 连续保存 10 次 | 文件可打开,体积增长在预期内 |
这张表里「换机器验证」和「另一阅读器打开」是最容易被跳过、也最容易暴露问题的两项——开发机上什么都有,客户机上什么都没有,这个落差就是翻车的温床。从那以后我每次交付带原生库的 Delphi 组件,都强制在一台干净虚拟机上走一遍完整清单,宁可多花半小时,也不想半夜被电话叫起来查「为什么客户那边打不开」。希望帮到你。
本文还有配套的精品资源,点击获取