你通过 PDFiumPas 使用 AddText 在 PDF 页面上盖印一行文字,随后立即调用 FindFirst 确认印章已经写入,结果搜索却返回空值。文字明明在页面上,Acrobat 也能显示,但 PDFiumPas 的 TPdf 组件还单独缓存着一个 FPDF_TEXTPAGE 结构,它只从页面内容流解析一次,编辑本身不会自动追溯更新这个结构。如果在刷新前查询它,读到的会是修改前的页面,而不是修改后的页面
为什么 PDFium 会在编辑后立即返回过时文本
PDFiumPas 为 Delphi 和 C++Builder 封装 Google 的 PDFium 渲染引擎,而其中的文本调用和编辑调用会进入引擎内部两个不同的子系统。FPDF_TEXTPAGE 属于读取侧:FPDFText_LoadPage 会遍历页面内容流一次并构建文本页,包括字符编码、位置、字体度量和单词边界,PDFiumPas 会在页面保持加载期间缓存这个结构。FPDFPage_InsertObject 或 FPDFPage_GenerateContent 等编辑调用操作的是完全不同的表示,即页面对象和内容流图,PDFium 不会自行把这些变化推送到已经打开的文本页。每次编辑都重建文本页会让批量编辑慢到无法接受,因此设计选择了以一条规则换取这项成本:持有该句柄的一方在内容变化后关闭它,下一次读取时再创建全新的文本页
TPdf 的文本缓存内部:FTextPage、LoadTextPage 与 UnloadTextPage
TPdf 在一个私有字段 FTextPage 中跟踪缓存句柄,并通过两个方法封装其生命周期。LoadTextPage 会检查 FTextPage 是否为 nil,只有在 nil 时才针对当前页面调用 FPDFText_LoadPage;如果句柄已经存在,LoadTextPage 就会直接复用它,而不会确认页面在创建句柄后是否发生过变化。另一半是 UnloadTextPage:它通过 FPDFText_ClosePage 关闭原生句柄,将 FTextPage 重新设为 nil,同时丢弃缓存的网页链接列表和正在进行的查找会话,因为两者都源自同一个文本页,也会因同一个原因失效
LoadTextPage 这种不检查就复用的行为,正是调用顺序重要的原因。TPdf 上每个文本查询——Text、FindFirst、GetWebLinks——都会先经过 LoadTextPage,因此只要 FTextPage 仍持有编辑前的句柄,这些调用就无法知道页面发生过变化。这里的风险从来不是页面导航:页面切换、重新加载和文档关闭时都会运行的 UnloadPage,一直会连同页面本身一起关闭文本页。真正需要回答的问题始终是:如何处理仍停留在当前页面上时对页面执行的编辑
哪些 PDFiumPas 方法会自动刷新缓存
TPdf 自带的页面编辑方法——AddText、SetText、SetTextPositions、AddPath、RemoveObject 和 InsertFormObjectFromXObject——都会先调用 UnloadTextPage,再调用 UpdatePage(PDFium 的 FPDFPage_GenerateContent)将变更序列化到内容流中。调用其中任何一个方法后,下一次 Text、FindFirst 或 GetWebLinks 都会根据当前内容重建文本页,无需你额外调用任何方法
var
Pdf: TPdf;
Index: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
Pdf.AddText('Reviewed by J. Alvarez', 'Helvetica', 10, 72, 40, clBlack, 255, 0);
// AddText already closed the cached text page, so this FindFirst
// call rebuilds it fresh before it searches
Index := Pdf.FindFirst('Reviewed by J. Alvarez');
if Index >= 0 then
ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
finally
Pdf.Free;
end;
end;
仍然会出问题的模式:缓存原始 TextPage 句柄
TPdf 通过只读的 TextPage 属性暴露当前句柄,供少数需要调用尚未封装的 FPDFText_* 函数的场景使用。但这个逃生口也是自动失效机制无法帮助的唯一位置:一旦你把属性中的 FPDF_TEXTPAGE 值复制到局部变量,PDFiumPas 就无法知道你仍持有它,也无法在代码其他位置运行 UnloadTextPage 时更新你的副本
var
Pdf: TPdf;
RawHandle: FPDF_TEXTPAGE;
StaleCount: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
RawHandle := Pdf.TextPage; // FPDFText_LoadPage handle, cached in FTextPage
Pdf.SetText(0, 'Amended Clause 4.2');
// SetText already closed RawHandle and set Pdf.TextPage back to nil.
// Calling any FPDFText_* function against the old value now touches a
// handle PDFium has already freed — undefined behavior, not a bug you
// can catch with a nil check
StaleCount := FPDFText_CountChars(RawHandle);
finally
Pdf.Free;
end;
end;
在 FPDFText_ClosePage 已经作用于句柄后继续使用它,是 PDFium 本身定义的未定义行为,不是可以选择忽略的 PDFiumPas 约定——它可能返回最近一次已知的数据,也可能什么都不返回,或者让进程崩溃,而某个构建版本究竟出现哪一种结果,并不是应用代码应当依赖的行为。安全规则很简单:重新读取 Pdf.TextPage,并在即将调用需要它的 FPDFText_* 函数前完成这一步,绝不要让副本跨过可能编辑页面的语句
批量完成编辑,然后只查询一次
这并不意味着每次 AddText 或 RemoveObject 调用后都要立即执行防御性文本查询来检查结果。每个编辑方法本身已经承担了一次关闭文本页的成本;在循环中每次编辑后查询,都会无益地再次付出这项成本,因为每次运行 FPDFText_LoadPage 都要重新遍历整个内容流
var
Pdf: TPdf;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'watermarked.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
// Strip every text object that looks like a draft watermark. Each
// RemoveObject call already invalidates the cache on its own, so
// nothing needs refreshing by hand between iterations
for I := Pdf.ObjectCount - 1 downto 0 do
if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
Pdf.RemoveObject(I, True);
// Query once, after the whole batch is done, not once per removal
if Pdf.FindFirst('DRAFT') < 0 then
ShowMessage('Watermark cleared');
finally
Pdf.Free;
end;
end;
同样的批处理逻辑也适用于搜索状态。FindNext 和 FindPrevious 会继续由 FindFirst 开始的会话,而 UnloadTextPage 会连同其他状态一起拆除该会话,因此编辑后再次调用 FindNext,而不是重新调用 FindFirst,会抛出异常,而不会悄悄在已经不存在的内容上继续搜索。把任何编辑都视为文本内容和搜索位置的硬边界,让编辑完成后另一侧的一次全新 FindFirst 重新开始搜索
它与文本提取和注释工作的关系
纯文本提取——读取页面文本而不进行任何修改——不会遇到这些问题,因为没有编辑会使句柄失效。关于未修改页面上的 Text、字符矩形和单词边界如何工作,请参阅使用 PDFiumPas 提取文本的配套文章,其中不涉及本文进一步介绍的文本页缓存生命周期
在先编辑、再立即处理结果的工作流中,缓存生命周期最为重要:盖章修正后搜索该修正,遮盖一段文字后确认它已经消失,或者插入文字后立即定位短语以锚定标记注释。最后一种情况尤其值得单独提醒——四边形点标记注释依据从文本页读取的字符矩形定位,因此如果使用编辑前捕获的坐标创建注释,编辑完成后高亮就会落在错误位置
TPdf 的编辑和文本 API 属于面向 Delphi 和 C++Builder 的 PDFium Component,产品页面提供本文涉及的编辑、提取和搜索接口的完整方法参考