You call AddText to stamp a line onto a PDF page with PDFiumPas, then immediately call FindFirst to confirm the stamp landed, and the search comes back empty. The text is on the page — Acrobat shows it — but PDFiumPas's TPdf component keeps a separate cached FPDF_TEXTPAGE structure, parsed once from the page's content stream, and an edit does not retroactively update that structure by itself. Query it before it has been refreshed and you read the page exactly as it looked before your change, not after
Why does PDFium return stale text right after an edit?
PDFiumPas wraps Google's PDFium rendering engine for Delphi and C++Builder, and its text and editing calls reach two different subsystems inside that engine. FPDF_TEXTPAGE belongs to the reading side: FPDFText_LoadPage walks the page's content stream once and builds the text page — character codes, positions, font metrics, word boundaries — and PDFiumPas keeps that structure cached for as long as the page remains loaded. Editing calls such as FPDFPage_InsertObject or FPDFPage_GenerateContent operate on a completely different representation, the page's object and content-stream graph, and PDFium does not push those changes into an already-open text page on its own. Rebuilding it on every edit would make batch editing unacceptably slow, so the design trades that cost for a rule instead — whoever holds the handle closes it after a content-changing edit, and the next read builds a fresh one
Inside TPdf's text cache: FTextPage, LoadTextPage, and UnloadTextPage
TPdf tracks the cached handle in a single private field, FTextPage, and wraps its lifecycle in two methods. LoadTextPage checks whether FTextPage is nil and, only in that case, calls FPDFText_LoadPage against the current page; if a handle already exists, LoadTextPage reuses it without asking whether the page changed since it was built. UnloadTextPage is the other half: it closes the native handle with FPDFText_ClosePage, sets FTextPage back to nil, and also drops the cached web-link list and any in-progress find session, since both were derived from the same text page and go stale for the same reason
LoadTextPage's reuse-without-checking behavior is exactly why the sequencing matters. Every text query on TPdf — Text, FindFirst, GetWebLinks — funnels through LoadTextPage first, so as long as FTextPage is still holding the pre-edit handle, none of those calls have any way to know a change happened. Page navigation was never the risk here: UnloadPage, which runs on page switches, reloads, and document close, has always closed the text page along with the page itself. The open question was always about edits applied to the page you are still sitting on
Which PDFiumPas methods refresh the cache automatically?
TPdf's own page-editing methods — AddText, SetText, SetTextPositions, AddPath, RemoveObject, and InsertFormObjectFromXObject — each call UnloadTextPage before they call UpdatePage (PDFium's FPDFPage_GenerateContent) to serialize the change into the content stream. Call any of these and the very next Text, FindFirst, or GetWebLinks call rebuilds the text page from the content as it now stands, with no extra call required on your part
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;
The pattern that still breaks: caching the raw TextPage handle
TPdf exposes the live handle through a read-only TextPage property, for the rare case where you need to call an FPDFText_* function PDFiumPas has not wrapped. That escape hatch is also the one place the automatic invalidation cannot help: once you copy the FPDF_TEXTPAGE value out of the property into a local variable, PDFiumPas has no way to know you are still holding it, and no way to update your copy when UnloadTextPage runs somewhere else in your code
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;
Using a handle after FPDFText_ClosePage has run on it is undefined behavior in PDFium itself, not a PDFiumPas convention you can choose to ignore — it may return the last-known data, return nothing, or crash the process, and which of those happens on a given build is not something application code should depend on. The safe rule is narrow: read Pdf.TextPage fresh, immediately before the FPDFText_* call that needs it, and never hold a copy across a statement that might edit the page
Batch your edits, then query once
None of this means every AddText or RemoveObject call needs a defensive text query right after it to check the result. Each editing method already pays the cost of closing the text page once; querying after every single edit inside a loop pays that cost again for no benefit, since FPDFText_LoadPage re-walks the whole content stream every time it runs
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;
The same batching logic applies to search state specifically. FindNext and FindPrevious continue a session started by FindFirst, and that session is torn down by UnloadTextPage along with everything else, so calling FindNext again after an edit — instead of calling FindFirst again — raises an exception rather than silently resuming a search against content that no longer exists. Treat any edit as a hard boundary for both text content and search position, and let one fresh FindFirst on the far side of your edits pick the search back up
Where this fits with extraction and annotation work
Plain text extraction — reading a page's text without changing anything — never runs into any of this, because nothing invalidates a handle that no edit has touched. For how Text, character rectangles, and word boundaries work on an unmodified page, the companion article on extracting text with PDFiumPas covers that ground without the text page cache lifecycle this article adds on top
The cache lifecycle matters most in workflows that edit and then immediately act on the result: stamping a correction and searching for it, redacting a paragraph and confirming it is gone, or locating a phrase to anchor a markup annotation right after inserting text near it. That last case is worth flagging on its own — quad-point markup annotations are positioned from character rectangles read off the text page, so an annotation built from coordinates captured before an edit ends up highlighting the wrong spot once the edit lands
TPdf's editing and text APIs are part of the PDFium Component for Delphi and C++Builder, and the product page carries the full method reference for the editing, extraction, and search surfaces covered here