ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Delphi 10.3 PDFium组件套件:渲染、表单回填与避坑指南

Delphi 10.3 PDFium组件套件:渲染、表单回填与避坑指南

简介: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 翻倍,位图像素数是四倍,内存占用随之翻倍。归档截图、合同预览、高保真打印各有各的合理值。

用途推荐 DPIA4 页面输出像素(宽×高)
屏幕预览缩略图72595×842
普通清晰度归档1501240×1754
高保真打印/识别3002480×3508

一般到 300 DPI 就够绝大多数场景用了,再往上只会增加带宽和存储成本,人眼在屏幕上已经分辨不出差异。如果业务方坚持要 600 DPI,先问清楚输出介质是印刷还是屏幕,多半能劝回来。

6.3 批量任务的内存纪律

批量处理的核心纪律是“一页一释放,一批一检查”。每渲染完一页,立刻释放页面与位图;每处理完一份文档,立刻关文档;每处理完一个批次,主动检查一次内存峰值有没有异常增长。PDFium 不是线程安全的引擎,多线程并行渲染时每个线程要有独立的文档实例,不能共享同一份文档句柄,否则偶发崩溃属于玄学范畴,日志根本查不出来。

记得早期我用临时文件方式做过一次批量导出,网络盘上的 PDF 被同事用阅读器锁定,整套任务卡在中间,进退两难。从那以后我每次批量处理都强制走内存流加载,先把数据读进字节数组再交给引擎,文件是否被别人占用完全不关我的事了。希望这个习惯也能帮到你少踩一次坑。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进