Technical Article

Stale Text After Editing: the PDFium FPDF_TEXTPAGE Cache

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 behaviour is exactly why the sequencing matters. Every text query on TPdfText, 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 serialise 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 behaviour, 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 behaviour 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