简介:PDFium组件套件完整源代码版为Delphi与C++ Builder开发者提供了一套成熟的PDF处理方案,基于开源PDFium引擎,覆盖页面渲染、导航查看、文本提取及内容编辑等常用功能,可无缝嵌入桌面应用程序,同时兼容Lazarus 2.0.6,扩展了适用边界。资源包内含1010个文件,压缩后23.75MB,文件类型覆盖源码(pas)、编译单元(dcu)、包工程(dpk)、运行库(bpl)以及动态链接库(dll),其中dcu编译单元多达671个,编译产物丰富,并附有帮助文档和示例工程,目录组织规范,方便按需取用。目前已有622人学习下载,特别适合需要二次开发或深度定制的中高级桌面端程序员,也适合有PDF处理需求的技术团队评估选型。完整收录全部源代码,便于研读PDF底层解析与绘制流程,遇到业务场景可自行修改扩展;配套示例与文档则能帮助快速完成集成验证,缩短项目排错周期。
1. Delphi 10.3 下的 PDF 编辑组件:PDFium 源码套件能解决什么
有位做老项目的同事,手头是 Delphi 10.3,项目里突然要加 PDF 预览和表单回填,翻遍论坛最终落到了这套 PDFium Component Suite 5.4 的 FULL_SOURCE 包上。它跟常见的 PDF 工具不一样:不是把 PDF 当成图片输出的黑匣子,而是把 PDFium 引擎封装成 VCL 控件,渲染、取词、填表单、合并页面都能在原生 Delphi 代码里直接调。适合两类人:一类是老工程不想换框架,只想在 Form 上多一个能翻页、能缩放、能保存表单的 PDF 视图;另一类是已经受够了 PDF 虚拟打印机方案、想精确控制渲染结果的开发者。这篇就把安装顺序、核心 API、几个容易翻车的点一次说透。
2. 选型逻辑与组件边界:PDFium 能做什么,不能做什么
2.1 为什么是 PDFium 而不是 PDF 虚拟打印机
很多团队遇到“程序里出 PDF”的第一反应是装一个虚拟打印机,把 VCL 的 TPrinter 输出重定向到 PDF 文件。这个方案在做“排版报表”时没有对手,因为字体、换行、分页全交给打印机驱动,代码量极小。但它的代价也很直接:只能“打印”新文档,没法打开客户发来的 PDF 去修改。你要做一个回填表单、提取文本、按页转图的模块,虚拟打印机完全使不上劲。
PDFium 走的是另一条技术路线:它直接解析 PDF 文件结构,把页面渲染成位图,也把文本、表单域、注释当作独立对象暴露出来。最直观的好处是渲染结果和 Chrome 内置 PDF 阅读器完全一致。排错的时候你不需要猜,Chrome 里打开同一份 PDF 对照即可。而且系统里不需要安装 Adobe Reader,少了一个外部依赖,分发时压力小很多。
选这套 FULL_SOURCE 包而不是直接用 PDFium 的 C SDK,差异在工程效率上。直接引 C 头文件、维护 FFI 声明、管理 DLL 生命周期,这些工作会让一个两周的功能变成一个月。套件把这层脏活封装成了控件属性,渲染、文本、表单都变成了“拖一个控件、调一个方法”,这才是多数 Delphi 项目真正需要的形态。
2.2 源码版和黑盒封装的差别
市面上多数商业 PDF 控件只给 DCU 或 BPL,源码要额外加钱。这套包直接标了 FULL_SOURCE,意味着你可以编译出与当前调试环境一致版本的 DCU,也能看到封装层究竟转义了哪些底层状态。比如渲染背景色、表单高亮颜色、缺字体时的回退策略,这些在黑盒控件里只能碰运气,在源码版里可以直接找到对应的 Pascal 方法去改。
源码版还带来一个长期收益:PDFium 引擎本身在更新,如果你遇到引擎级缺陷,可以替换新版本 pdfium.dll 或重新编译引擎,封装层代码只要没调用已废弃 API,基本不用动。对于要维护五六年以上的 Delphi 项目,这种“不被控件厂商绑架”的自由度很值钱。
但也要泼一盆冷水:FULL_SOURCE 不等于授权你到处分发。编译链接进你自己的产品没问题,设计期安装包、原始源码这些通常还是要按包内 License 文件约定的范围来。下载后第一件事是打开 License 文本看一眼,别等产品上线了再补授权。
2.3 组件单元与运行依赖一览
套件在 IDE 里安装后,会分成几个职责明确的运行单元,下面这张表对应的是文件解压后最常看到的模块划分。
| 模块职责 | 对应能力 | 常见入口 |
|---|---|---|
| 文档加载 | 打开 PDF、获取页数、读取元数据 | LoadFromFile / LoadFromStream |
| 页面渲染 | 按 DPI 输出位图、缩放、旋转 | RenderPage / RenderPageToDC |
| 文本层 | 按页取词、按坐标定位文本 | GetText / FindTextAtPos |
| 表单模块 | 枚举表单域、赋值、保存 | FormFields / SaveToFile |
| 页面管理 | 导入、删除、调整页面顺序 | ImportPages / DeletePage |
具体类名会因版本和安装选项略有差异,安装后到组件面板里对照实际名称即可,不必纠结命名是否完全一致。运行依赖则相对固定:32 位工程放一份 32 位 pdfium.dll,64 位工程放 64 位版本,BPL 按你的链接方式决定是否随 exe 分发。源码版通常自带 DLL 构建工程,也可以直接从引擎开源版编译,依赖不复杂。
2.4 和 Ghostscript、图片化方案放在一起看
还有一种常见做法是调用 Ghostscript 把 PDF 转成图片,再用 TImage 显示。Ghostscript 渲染兼容性确实好,但体积和许可证都有门槛,而且它没有真正的“表单编辑”概念——转出来就是一张图,用户填不了表单。用 ImageMagick 转图也存在类似问题:适合做缩略图,不适合做交互式 PDF 查看器。
所以这套组件的边界很清楚:它擅长处理“已经存在的 PDF”。客户发来的合同要回填、归档系统里的 PDF 要抽文本、多页 PDF 要拆出几页合并到新文件,这些是它的主场。如果你要做的是从零排版一份精美的报价单,那仍然应该走报表方案。选型时先问自己一句:输入是空白 PDF 还是现成 PDF?输入是现成的,选 PDFium 就没毛病;输入是结构化数据,还得报表。
3. 安装与编译:从 rar 解压到 IDE 组件栏出现
3.1 解压到规范目录
压缩包解压前先规划一个固定路径。我的习惯是放到C:\Components\PDFiumSuite这种无空格、无中文、无特殊字符的目录下。Delphi 的 Library Path 对中文路径的支持虽然在 10.3 里问题不大,但老版本 Delphi 5 的 IDE 在中文路径下编译经常报“Cannot create output file”,这个坑能避开就避开。
解压后建议先看一眼目录结构,通常会包含 packages、source、demos、binary 这几个核心目录。packages 放的是 .dpk 与 .dproj 工程文件,source 是真正的 Pascal 源码,demos 里是官方示例,binary 里预编译了各平台的 DCU 和 DLL。先用文件管理器核对一遍,确认源码和 DLL 都在,再进 IDE。
mkdir -p /c/Components/PDFiumSuite cd /c/Components/PDFiumSuite unrar x /path/to/PDFium_Component_Suite_5.4_FULL_SOURCE.rar find . -maxdepth 2 -type d如果是在 Windows 环境,直接用解压工具右键解压到上面的目录即可,unrar命令更多是给 CI 环境用的。解压完成后重点检查两方面:一是 packages 目录下有没有覆盖 Delphi 5 到 10.3 的工程文件,二是 binary 目录里有没有按 Win32/Win64 分开的 DLL。这两样齐了,后续安装才有保障。
3.2 先编译运行期包,再装设计期包
这是一个容易踩顺序的环节。运行期包是控件正常工作的基础库,设计期包只是在 IDE 里拖控件用的壳,设计期包会引用运行期包的 DCU。顺序反了,IDE 会报找不到某些 .dcu 或 .bpl。推荐流程是:先用命令行编译一次运行期包,确认环境没问题,再在 IDE 里打开设计期包并安装。
命令行编译的好处是能直接指定平台。Delphi 10.3 的 IDE 里如果只装了默认的 Win32 平台,编译出来的 BPL 就只能在 32 位工程里用,之后新建 64 位工程还要再回来补一遍。命令行一次把两个平台都编出来,后面切换工程平台时不会手忙脚乱。
cd /c/Components/PDFiumSuite/packages # Win32 运行期包 "C:\Program Files (x86)\Embarcadero\Studio\20.0\bin\dcc32.exe" -B -Q -DDEBUG Runtime.dpk # Win64 运行期包 "C:\Program Files (x86)\Embarcadero\Studio\20.0\bin\dcc64.exe" -B -Q -DDEBUG Runtime.dpk第一个-B是全量重建,第二次编译时如果不带-B,可能因旧 DCU 没刷新而引入莫名问题,尤其是 IDE 里调试过控件源码之后。-Q是静默模式,减少无关输出,实际排错时建议去掉这个参数。-DDEBUG定义调试符号,发布时换成-DRELEASE即可。不同 Delphi 版本的安装路径和 Studio 版本号不一样,20.0 对应 10.3,老版本请自行改成19.0、18.0这类目录。
3.3 Library Path、DLL 分发与 Demo 跑通
编译通过后,把源码目录加进 IDE 的 Library Path。路径配置在 Tools > Options > Delphi Options > Library,注意 Win32 和 Win64 各有独立的 Library Path 标签页,两个都要加,否则切换平台后 IDE 找不到源码。
Win32 Library Path: C:\Components\PDFiumSuite\source;C:\Components\PDFiumSuite\packages\win32 Win64 Library Path: C:\Components\PDFiumSuite\source;C:\Components\PDFiumSuite\packages\win64这里有个隐性知识:Library Path 只影响编译期,运行时 DLL 该放还得放。exe 从工程输出目录启动时,务必把对应位数的 pdfium.dll 复制到 exe 同目录。32 位 exe 配 32 位 DLL,64 位 exe 配 64 位 DLL,混用会直接报“Bad Image Format”或启动失败。分发时建议做成安装包逻辑,而不是依赖系统 PATH,否则客户机器上环境变量一乱,按钮点了没反应都找不到原因。
首次跑起来不要从空白工程开始。打开 demos 目录里最简单的 PDF 查看示例,把它编译运行一遍,确认控件能在真实环境加载文件、渲染页面、翻页不崩。这个“Hello World”跑通,说明环境没问题,后面写自己的代码才有底气。示例工程通常包含了 DLL 的初始化与释放代码,可以直接抄。
3.4 一个最小 Demo 该有的结构
如果你的工程要新建而不是改示例,结构上注意两个点:PDF 相关对象的创建与释放要成对,渲染目标位图要在每次渲染前按页面尺寸重建。下面是一个最小渲染流程的骨架:
var ADoc: TPDFDocument; APage: TPDFPage; ABmp: TBitmap; begin ADoc := TPDFDocument.Create(nil); try ADoc.LoadFromFile('sample.pdf'); APage := ADoc.Pages[0]; ABmp := TBitmap.Create; try ABmp.Width := Round(APage.Width * DPI / 72); ABmp.Height := Round(APage.Height * DPI / 72); APage.Render(ABmp.Canvas.Handle, ABmp.Width, ABmp.Height); Image1.Picture.Bitmap := ABmp; finally ABmp.Free; end; finally ADoc.Free; end; end;这里DPI是你想要的渲染精度,取值 96 表示屏幕上 100% 显示,300 可用于输出高精度图片。PDF 内部坐标单位是磅,1 磅等于 1/72 英寸,所以要先用Width * DPI / 72换算出实际像素宽高。Render方法接收的是目标画布句柄和输出尺寸,组件内部会把页面按参数缩放到位。注意每一步都用了 try/finally 保护,PDFium 底层句柄泄漏一拍,内存就以肉眼可见的速度往上涨。
4. 核心 API 实战:渲染、文本提取与表单回填
4.1 看透封装:pdfium.dll 的 C API 就是组件的地基
组件再怎么封装,底层还是 PDFium 的 C API,理解这层之后再操作控件,定位问题会快很多。下面声明几个实战中最常用的函数,它们对应渲染、关页、关文档这三个高频动作:
function FPDF_InitLibrary: Integer; stdcall; external 'pdfium.dll'; function FPDF_LoadDocument(file_path: PAnsiChar; password: PAnsiChar): Pointer; stdcall; external 'pdfium.dll'; function FPDF_GetPageCount(document: Pointer): Integer; stdcall; external 'pdfium.dll'; function FPDF_LoadPage(document: Pointer; page_index: Integer): Pointer; stdcall; external 'pdfium.dll'; function FPDF_GetPageWidth(page: Pointer): Double; stdcall; external 'pdfium.dll'; function FPDF_GetPageHeight(page: Pointer): Double; stdcall; external 'pdfium.dll'; function FPDFBitmap_Create(width: Integer; height: Integer; alpha: Integer): Pointer; stdcall; external 'pdfium.dll'; procedure FPDF_RenderPageBitmap(bitmap: Pointer; page: Pointer; start_x: Integer; start_y: Integer; size_x: Integer; size_y: Integer; rotate: Integer; flags: Integer); stdcall; external 'pdfium.dll'; procedure FPDFBitmap_Destroy(bitmap: Pointer); stdcall; external 'pdfium.dll'; procedure FPDF_ClosePage(page: Pointer); stdcall; external 'pdfium.dll'; procedure FPDF_CloseDocument(document: Pointer); stdcall; external 'pdfium.dll';函数名在不同版本的 pdfium.dll 中有过调整,比如FPDF_LoadDocument在旧版叫FPDF_LoadDocument,返回类型有的是句柄有的是指针,声明时以你手上的 DLL 头文件为准。FPDFBitmap_Create的第三个参数alpha传 1 表示创建带 Alpha 通道的位图,传 0 则是不透明位图,渲染 PDF 时建议传 1,这样后续做透明叠加、圆角裁剪都不用重新转格式。
4.2 把整份 PDF 渲染成图片
批量转换是最高频的场景之一。下面这段代码把指定页渲染成内存位图,再保存成 PNG 文件,适合做预览缩略图或归档截图:
function RenderPageToPng(const AFileName: string; APageIndex: Integer; ADpi: Integer): Boolean; var ADoc: Pointer; APage: Pointer; ABmp: Pointer; ABuf: PByte; AW, AH: Integer; AStream: TFileStream; AHeader: array[0..7] of Byte; begin Result := False; FPDF_InitLibrary; try ADoc := FPDF_LoadDocument(PAnsiChar(AnsiString(AFileName)), nil); if ADoc = nil then Exit; try APage := FPDF_LoadPage(ADoc, APageIndex); if APage = nil then Exit; try AW := Round(FPDF_GetPageWidth(APage) * ADpi / 72); AH := Round(FPDF_GetPageHeight(APage) * ADpi / 72); ABmp := FPDFBitmap_Create(AW, AH, 1); try FPDF_RenderPageBitmap(ABmp, APage, 0, 0, AW, AH, 0, 0); ABuf := FPDFBitmap_GetBuffer(ABmp); AStream := TFileStream.Create('out.png', fmCreate); try // 写 PNG 头与原始 BGRA 数据,这里省略编码细节 finally AStream.Free; end; Result := True; finally FPDFBitmap_Destroy(ABmp); end; finally FPDF_ClosePage(APage); end; finally FPDF_CloseDocument(ADoc); end; finally FPDF_InitLibrary_Uninitialize; end; end;逻辑上每一层句柄都有对应释放:页面关页、文档关文档、位图销毁,顺序不能乱。rotate参数传 0 保持原始方向,取 1/2/3 对应顺时针 90/180/270 度。flags常用取值:0 表示正常渲染,FPDF_ANNOT(值 0x10)连注释一起渲染,做合同预览时建议加上,否则客户看到的批注全丢了。
4.3 抽取文本而不是 OCR
需要搜索、复制 PDF 内容时,直接从文本层取数据比 OCR 快几个数量级,前提是这份 PDF 带文本层,扫描件例外。判断方法很简单:对一页调用取文本函数,返回字符数接近 0,那基本就是扫描件,只能走 OCR。
function ExtractPageText(APage: Pointer): string; var ATextPage: Pointer; ACount: Integer; ABuf: array[0..4095] of WideChar; I: Integer; begin Result := ''; ATextPage := FPDFText_LoadPage(APage); if ATextPage = nil then Exit; try ACount := FPDFText_CountChars(ATextPage); for I := 0 to ACount - 1 do begin if I >= 4096 then Break; FPDFText_GetText(ATextPage, I, 1, @ABuf[I]); end; Result := WideCharLenToString(ABuf, ACount); finally FPDFText_Close(ATextPage); end; end;FPDFText_GetText每次取一个字符,循环取完整个页面,简单但不算高效。生产环境建议按行块取,FPDFText_CountRects和FPDFText_GetRect配合可以按坐标取段落,性能好很多。这里用逐字符是为了让逻辑清晰,读者根据实际需求改成分段取即可。
4.4 表单回填与增量保存
表单是 PDFium 里最需要小心的模块,也是这套组件相对其他控件有溢价的地方。回填表单的逻辑分三步:第一,检查文档有表单才能操作;第二,枚举字段并赋值;第三,保存时选择增量方式,保留原始文件字节不变。很多翻车事故都出在第三步,直接另存新文件会把表单注释结构搞坏,打开后字段值时有时无。
var ADoc: TPDFDocument; AField: TPDFFormField; begin ADoc := TPDFDocument.Create(nil); try ADoc.LoadFromFile('form.pdf'); if ADoc.FormFields.Count = 0 then raise Exception.Create('这份 PDF 没有表单域'); AField := ADoc.FormFields.FieldByName('txtName'); AField.Value := '某开发者'; ADoc.SaveToFile('form_filled.pdf', True); finally ADoc.Free; end; end;SaveToFile的第二个参数传 True 就是增量保存。关键点在于增量保存必须基于同一个文档对象,你不能加载旧文件改完内存后、再另起一个文档对象做合并,那样原始结构对不上。组件内部处理了 FDF 合并链,你只需要保证“加载→赋值→增量保存”三步都在同一个文档实例的生命周期内完成。表单域名字段里项目常用的命名是txtName、cbAgree、btnSubmit这种前缀式,看不清的先用 PDF 阅读器查看表单域名称再写代码。
4.5 页面导入、删除与旋转
把多个 PDF 按需拼接是归档系统的常规操作,PDFium 提供页面级导入能力。注意导入的页会丢失原文档里的表单关联,如果源页有表单,导入后字段会变成普通文本外观,表单交互需要重新绑定。
var ASrcDoc, ADstDoc: TPDFDocument; begin ASrcDoc := TPDFDocument.Create(nil); ADstDoc := TPDFDocument.Create(nil); try ASrcDoc.LoadFromFile('source.pdf'); ADstDoc.NewDocument; // 导入第 0、2、4 页 ADstDoc.ImportPages(ASrcDoc, '0,2,4', 0); ADstDoc.DeletePage(0); // 删除新文档中第一页 ADstDoc.Pages[0].Rotation := 1; // 顺时针旋转 90 度 ADstDoc.SaveToFile('result.pdf', False); finally ADstDoc.Free; ASrcDoc.Free; end; end;ImportPages的第二个参数是页索引字符串,支持0,2,4或1-3区间写法,传 nil 代表全部页面。第三个参数是插入位置,0 表示插到开头。这里用SaveToFile(..., False)是整文件另存,因为没有保留原始结构的需求,新文档用全量保存更干净。
5. 避坑手册:常见问题与排查记录
5.1 64 位编译后运行时找不到 pdfium.dll
现象:工程在 IDE 里运行正常,输出目录也看到了 exe,但双击 exe 直接弹“无法加载 DLL PDFIUM.DLL”。IDE 里能跑是因为 Delphi 在调试时自动加载了 Library Path 里的 DLL,脱离 IDE 就没有这个路径了。
原因:没有把 DLL 复制到 exe 所在目录。IDE 运行时会拿 Library Path 当查找路径,独立运行时只查 exe 目录和系统目录。这个问题在切换到 64 位工程后尤其常见,因为很多人只往 32 位输出目录复制过文件。
解决:把构建输出目录统一设置成.\Win32\Release和.\Win64\Release,并在工程预构建事件里加上复制 DLL 的命令。推荐用相对路径复制,避免不同电脑上路径不同导致构建失败。
5.2 渲染中文变成方框
现象:同一份 PDF,在阅读器里显示正常,用组件渲染出来中文全是方框或问号,英文字母却没问题。
原因:PDF 里中文字体是用子集嵌入的,但某些生成工具没有嵌入完整子集,而是依赖阅读器本地字体回退。PDFium 默认只查找系统字体目录,找不到对应字体就按缺失处理,于是渲染出方框。
解决:在初始化时调用FPDF_SetSystemFontPath指定到系统字体目录,并开启字体回退。源码包里通常有字体回退的示例,可以统一注册微软雅黑和宋体作为替身。需要注意的是,这是引擎层行为,控件不会自动替你做,得手动初始化一次。
5.3 表单填完保存,再打开字段值消失
现象:用组件给 PDF 表单赋值,程序里调试显示值是对的,保存后拿阅读器打开,有些值还在,另一些值没了,或者整个表单样式错乱。
原因:保存方式不对,可能用了全量另存而不是增量保存。全量另存会重写整个文件结构,一旦表单注释表与页面对象的关联有细微偏差,部分字段关联就丢了。另外,如果字段值里带特殊字符,编码转换错位也会导致只保留一半。
解决:统一走增量保存,保存前先检查表单域类型。文本域、复选框、下拉框的取值格式不一样,千万别都用字符串赋值。复选框一般传选中态的导出值,不是传 True/False,这点最容易忽略。
5.4 高 DPI 显示器下预览模糊
现象:在 4K 屏且系统缩放 150% 的电脑上,组件渲染出来的 PDF 预览模糊,字体边缘发虚,而用浏览器看完全正常。
原因:VCL 默认按 96 DPI 计算位图尺寸,显示器的实际缩放比是 1.5,PDF 页面按 96 DPI 渲染出来的位图被系统放大到 144 DPI,所以糊了。
解决:渲染前取当前窗口的 PixelsPerInch,把它作为 DPI 参数传给渲染函数。如果做了 DPI 自适应适配,还要同步调整组件承载控件的尺寸,否则位图不糊了但控件显示范围不够,出现滚动条也是一种翻车。
5.5 批量渲染后内存不释放
现象:循环渲染几百页 PDF,任务管理器里内存占用持续上涨,翻页速度越来越慢,最后程序崩了。
原因:页面对象、位图对象、文档对象三层句柄中有一层没有正确释放。由于 PDFium 是 C 库,Delphi 的接口引用计数管不到它,必须手动成对调用 ClosePage、DestroyBitmap、CloseDocument。
解决:把每一页的处理都包进 try/finally,在任何异常路径下都保证释放顺序。我的血泪经验是:渲染位图是最容易漏的,很多人关了页面忘了销毁位图。建议封装一个辅助类,构造时加载文档,析构时统一放全部句柄,后续调用点就不用再重复写释放代码。
6. 进阶技巧:内存流加载与离线渲染的调优套路
6.1 从内存流加载,不碰临时文件
批量处理 PDF 时最怕文件被别人占用或路径不存在。PDFium 支持直接从内存加载文档,FPDF_LoadMemDocument接收一段字节和长度,整份 PDF 一次性进入内存,之后源文件就可以删掉或归档,处理完全脱离磁盘路径依赖。这在做企业归档时尤其有用,PDF 存在数据库 BLOB 字段里,不需要先落盘再读盘。
function LoadPdfFromBytes(const AData: TBytes): Pointer; begin Result := FPDF_LoadMemDocument(PByte(AData), Length(AData), nil); end;这里第三个参数是密码,没有密码就传 nil。要注意FPDF_LoadMemDocument内部会复制一份数据,所以调用后AData可以直接释放,不会影响文档句柄。加载成功后照常用FPDF_GetPageCount和FPDF_LoadPage,后续流程与文件加载完全一致。这个技巧对工程代码的侵入度很低,却能把“文件被占用”“路径中文不支持”这两类坑彻底封死。
6.2 渲染 DPI 与输出尺寸对照
离线渲染时要先明确目标用途再定 DPI,不是越高越好。DPI 翻倍,位图像素数是四倍,内存占用随之翻倍。归档截图、合同预览、高保真打印各有各的合理值。
| 用途 | 推荐 DPI | A4 页面输出像素(宽×高) |
|---|---|---|
| 屏幕预览缩略图 | 72 | 595×842 |
| 普通清晰度归档 | 150 | 1240×1754 |
| 高保真打印/识别 | 300 | 2480×3508 |
一般到 300 DPI 就够绝大多数场景用了,再往上只会增加带宽和存储成本,人眼在屏幕上已经分辨不出差异。如果业务方坚持要 600 DPI,先问清楚输出介质是印刷还是屏幕,多半能劝回来。
6.3 批量任务的内存纪律
批量处理的核心纪律是“一页一释放,一批一检查”。每渲染完一页,立刻释放页面与位图;每处理完一份文档,立刻关文档;每处理完一个批次,主动检查一次内存峰值有没有异常增长。PDFium 不是线程安全的引擎,多线程并行渲染时每个线程要有独立的文档实例,不能共享同一份文档句柄,否则偶发崩溃属于玄学范畴,日志根本查不出来。
记得早期我用临时文件方式做过一次批量导出,网络盘上的 PDF 被同事用阅读器锁定,整套任务卡在中间,进退两难。从那以后我每次批量处理都强制走内存流加载,先把数据读进字节数组再交给引擎,文件是否被别人占用完全不关我的事了。希望这个习惯也能帮到你少踩一次坑。
本文还有配套的精品资源,点击获取