一个扫描件存档可能会有数十 GB 大小,打开这种 PDF 时,阅读器通常只需要显示一页,例如目录页或书签跳转后的目标页。将整个文件一次性读入内存再渲染两页会浪费很多资源,增加启动等待,并且在 32 位 Delphi 进程里可能在首屏渲染前直接失败。PDFium 之所以设计了按需加载,就是为了避免这些问题:它不会一次读取整份文件,而是按需读取每次需要的字节范围
组件通过流适配器暴露这条路径。你只要传入任意 TStream,PDFium 就会按需从流中拉取数据块。文件可以来自磁盘、数据库 blob 字段或任何 TStream 子类,PDFium 不会预先拷贝全部内容到内存
PDFium 如何请求字节
PDFium 的 C API 接收调用方提供的 FPDF_FILEACCESS 结构后加载文档,它包含长度、读取回调和不透明用户参数三部分。入口函数是 FPDF_LoadCustomDocument。PDFium 在拿到该结构后先解析 trailer 并定位交叉引用表,然后只读取当前操作必须的内容。打开文档时通常会先触碰文件尾部和少量目录对象,渲染第 400 页时只读取该页的内容流与资源,不会触发全量读取
这就是缓冲加载和流式加载的区别,缓冲加载会在看到 byte 0 之前把文件全读一遍,流式加载则由 PDFium 按需驱动读取,未命中的字节根本不会被访问。对数十 GB 文件,这决定了可用加载与不可用加载的分界
流适配器
把 Delphi TStream 映射到 FPDF_FILEACCESS 的适配器是 TPdfStreamAdapter。它的构造函数接收流和所有权标志,保存流长度,填充 FPDF_FILEACCESS,并绑定读取回调。PDFium 回调时会传入偏移与长度,适配器据此 seek 到偏移位置并把对应范围精确拷贝进缓冲区
// Verbatim from the component: the stream-to-FPDF_FILEACCESS bridge
constructor TPdfStreamAdapter.Create(AStream: TStream; AOwnsStream: Boolean);
begin
inherited Create;
if AStream = nil then
raise EPdfError.Create('TPdfStreamAdapter: AStream is nil');
FStream := AStream;
FOwnsStream := AOwnsStream;
// FPDF_FILEACCESS.m_FileLen is a 32-bit unsigned long. Refuse a stream
// that would silently truncate past 4 GiB.
if AStream.Size > High(FPDF_DWORD) then
raise EPdfError.Create('TPdfStreamAdapter: stream exceeds the 4 GiB limit');
FillChar(FFileAccess, SizeOf(FFileAccess), 0);
FFileAccess.m_FileLen := FPDF_DWORD(AStream.Size);
FFileAccess.m_GetBlock := GetBlockCallback;
FFileAccess.m_Param := Self;
end;
所有权标志决定由谁释放流。传 False 时调用方继续持有并保持存活,直到文档生命周期结束;传 True 时适配器接管,在文档关闭时释放。无论何种方式,流都必须覆盖文档打开期间所有读取,因为 PDFium 保持 FPDF_FILEACCESS 指针并在任意时刻回调,不只在初始加载时
为什么回调必须是静态函数
m_GetBlock 中的读取回调采用 C 的 cdecl 调用约定。Delphi 方法带有隐式 Self,并不能直接作为 C 回调。组件将回调声明为带 cdecl; static 的 class function,因此它在二进制层面是无隐式 Self 的独立函数
这样解决了调用约定问题后,另一个问题随即出现:没有 Self 的回调如何定位具体流?答案是 m_Param。适配器在创建结构时把自身实例指针写入该字段,PDFium 回调时把它回传给回调函数,静态方法再将其转换为 TPdfStreamAdapter 并访问正确的流实例,这是标准 C 边界上下文中转接对象的 trampoline 方式
// Verbatim from the component: the cdecl trampoline back to the instance
class function TPdfStreamAdapter.GetBlockCallback(
param : Pointer;
position: FPDF_DWORD;
pBuf : PByte;
size : FPDF_DWORD): Integer; cdecl;
var
Adapter: TPdfStreamAdapter;
begin
Result := 0;
if (param = nil) or (pBuf = nil) or (size = 0) then
Exit;
Adapter := TPdfStreamAdapter(param); // recover the instance from m_Param
if Adapter.FStream = nil then
Exit;
try
Adapter.FStream.Position := Int64(position);
Adapter.FStream.ReadBuffer(pBuf^, Int64(size));
Result := 1;
except
Result := 0; // report failure by return value, never by raising
end;
end;
4 GiB 上限和为何要保护它
FPDF_FILEACCESS.m_FileLen 是 32 位无符号长度字段,最大可表示值略小于 4 GiB,而 TStream 长度是 Int64,能表示远大于该限制的大小。若不校验,超大流会被截断后伪装成看起来正常的值,后续解析很容易走向错误判断
构造函数会比较 Size 与 High(FPDF_DWORD),超过限制即抛出 EPdfError,并在真实输入点停止。这个失败点直观且容易定位;如果让截断静默发生,异常则会在后续解析阶段很晚才暴露,而且表现为交叉引用失败等与业务无关的问题
4 GiB 限制是这条加载路径的真实边界条件,超过后要么拆分文件、要么保留 64 位索引路径。推荐把它作为明确错误而不是靠算术绕过处理
失败不能越界
读取也可能失败,可能来自超时网络对象、已被关闭的句柄,或后续被截断的文件。PDFium 回调约定依赖返回码:非零成功、零失败。回调失败不能抛出 Pascal 异常,因为这是 C 框架,它不能处理这类异常传播
因此回调封装了 try/except 并返回 0 而不是抛出异常;否则异常会穿透 cdecl 调用栈导致未定义行为,严重时触发崩溃并且丢失堆栈。返回 0 后 PDFium 能将失败控制在协议内,并通过 FPDF_LoadCustomDocument 报告加载失败,由组件侧转为 Pascal 的 EPdfError
按这种方式打开文档
LoadCustomDocument 是此流式路径的驱动入口,它是独立方法,不会与 LoadDocument 混淆。该方法先创建适配器再调用 FPDF_LoadCustomDocument,并在文档生命周期内保留适配器引用
var
Pdf: TPdf;
FileStream: TFileStream;
begin
Pdf := TPdf.Create(nil);
FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
try
// Hand stream ownership to Pdf: it frees FileStream when the document closes.
Pdf.LoadCustomDocument(FileStream, True);
// PDFium has read only the trailer and catalog so far.
// Rendering a page pulls just that page's bytes through the callback.
// ... render or inspect pages here ...
finally
Pdf.Free; // closes the document, which frees the adapter and the stream
end;
end;
相同写法同样适用于 TMemoryStream、数据库数据集返回的 blob 流或其他自定义 TStream。在大文件只读少量内容的场景下,按需加载最有价值,比如归档查看、只抽样几页的缩略图场景或分页检索引擎;当文档较小或确需全量处理时,缓冲加载反而更直接。关键在于实际触达字节占比
页面按需流入后,下一步通常是保证缩放与滚动下的响应体验,见 渲染缓存与缩放性能说明。如果当前文档用于查看但不支持导出或编辑,可结合 安全 PDF 预览指南 使用;两者都建立在本篇说明的流式加载之上,并依赖 PDFium Component 在 Delphi 和 C++Builder 下的渲染、文本提取与批注 API