Artigo Técnico

Texto Obsoleto Após Edição: o Cache FPDF_TEXTPAGE do PDFium

Você chama AddText para carimbar uma linha em uma página de PDF com o PDFiumPas, depois chama imediatamente FindFirst para confirmar que o carimbo pousou, e a busca volta vazia. O texto está na página — o Acrobat o mostra — mas o componente TPdf do PDFiumPas mantém uma estrutura FPDF_TEXTPAGE em cache separada, analisada uma vez a partir do content stream da página, e uma edição não atualiza retroativamente essa estrutura por conta própria. Consulte-a antes de ela ter sido atualizada e você lê a página exatamente como estava antes de sua mudança, não depois

Por que o PDFium retorna texto obsoleto logo após uma edição?

O PDFiumPas envolve o motor de renderização PDFium do Google para Delphi e C++Builder, e suas chamadas de texto e edição alcançam dois subsistemas diferentes dentro desse motor. FPDF_TEXTPAGE pertence ao lado de leitura: FPDFText_LoadPage percorre o content stream da página uma vez e constrói a página de texto — códigos de caractere, posições, métricas de fonte, limites de palavra — e o PDFiumPas mantém essa estrutura em cache enquanto a página permanecer carregada. Chamadas de edição como FPDFPage_InsertObject ou FPDFPage_GenerateContent operam sobre uma representação completamente diferente, o grafo de objetos e content stream da página, e o PDFium não empurra essas mudanças para uma página de texto já aberta por conta própria. Reconstruí-la a cada edição tornaria a edição em lote inaceitavelmente lenta, de modo que o design troca esse custo por uma regra, em vez disso — quem quer que detenha o handle o fecha após uma edição que muda conteúdo, e a próxima leitura constrói um novo

Dentro do cache de texto do TPdf: FTextPage, LoadTextPage e UnloadTextPage

TPdf rastreia o handle em cache em um único campo privado, FTextPage, e envolve seu ciclo de vida em dois métodos. LoadTextPage verifica se FTextPage é nil e, apenas nesse caso, chama FPDFText_LoadPage contra a página atual; se um handle já existe, LoadTextPage o reutiliza sem perguntar se a página mudou desde que foi construído. UnloadTextPage é a outra metade: fecha o handle nativo com FPDFText_ClosePage, redefine FTextPage para nil, e também descarta a lista de links web em cache e qualquer sessão de busca em andamento, já que ambas foram derivadas da mesma página de texto e ficam obsoletas pelo mesmo motivo

O comportamento de reutilização-sem-verificar do LoadTextPage é exatamente por que o sequenciamento importa. Toda consulta de texto em TPdfText, FindFirst, GetWebLinks — canaliza por LoadTextPage primeiro, de modo que, enquanto FTextPage ainda estiver segurando o handle pré-edição, nenhuma dessas chamadas tem como saber que uma mudança aconteceu. A navegação de página nunca foi o risco aqui: UnloadPage, que roda em trocas de página, recarregamentos e fechamento de documento, sempre fechou a página de texto junto com a própria página. A questão em aberto sempre foi sobre edições aplicadas à página em que você ainda está sentado

Quais métodos do PDFiumPas atualizam o cache automaticamente?

Os próprios métodos de edição de página do TPdfAddText, SetText, SetTextPositions, AddPath, RemoveObject e InsertFormObjectFromXObject — cada um chama UnloadTextPage antes de chamar UpdatePage (o FPDFPage_GenerateContent do PDFium) para serializar a mudança no content stream. Chame qualquer um desses e a próxíma chamada Text, FindFirst, ou GetWebLinks reconstrói a página de texto a partir do conteúdo como agora se apresenta, sem nenhuma chamada extra exigida de sua parte

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;

O padrão que ainda quebra: guardar o handle bruto do TextPage

TPdf expõe o handle vivo por meio de uma propriedade somente leitura TextPage, para o caso raro em que você precisa chamar uma função FPDFText_* que o PDFiumPas não empacotou. Essa válvula de escape também é o único lugar onde a invalidação automática não consegue ajudar: uma vez que você copia o valor FPDF_TEXTPAGE para fora da propriedade e para uma variável local, o PDFiumPas não tem como saber que você ainda o está segurando, nem como atualizar sua cópia quando UnloadTextPage roda em outro lugar do seu código

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;

Usar um handle depois que FPDFText_ClosePage rodou nele é comportamento indefinido no próprio PDFium, não uma convenção do PDFiumPas que você possa optar por ignorar — pode retornar o último dado conhecido, não retornar nada, ou quebrar o processo, e qual dessas coisas acontece em uma determinada build não é algo de que o código da aplicação deveria depender. A regra segura é restrita: leia Pdf.TextPage novo, imediatamente antes da chamada FPDFText_* que precisa dele, e nunca guarde uma cópia através de uma instrução que possa editar a página

Agrupe suas edições, depois consulte uma vez

Nada disso significa que toda chamada AddText ou RemoveObject precisa de uma consulta de texto defensiva logo depois para verificar o resultado. Cada método de edição já paga o custo de fechar a página de texto uma vez; consultar depois de cada edição individual dentro de um loop paga esse custo novamente sem benefício nenhum, já que FPDFText_LoadPage repercorre o content stream inteiro toda vez que roda

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;

A mesma lógica de agrupamento se aplica ao estado de busca especificamente. FindNext e FindPrevious continuam uma sessão iniciada por FindFirst, e essa sessão é desmontada por UnloadTextPage junto com tudo mais, de modo que chamar FindNext novamente depois de uma edição — em vez de chamar FindFirst novamente — levanta uma exceção em vez de silenciosamente retomar uma busca contra conteúdo que não existe mais. Trate qualquer edição como uma fronteira rígida tanto para o conteúdo de texto quanto para a posição de busca, e deixe um FindFirst novo do outro lado de suas edições retomar a busca

Onde isso se encaixa com extração e trabalho de anotação

Extração de texto simples — ler o texto de uma página sem mudar nada — nunca esbarra em nada disso, porque nada invalida um handle que nenhuma edição tocou. Para como Text, retângulos de caractere e limites de palavra funcionam em uma página não modificada, o artigo complementar sobre extração de texto com o PDFiumPas cobre esse terreno sem o ciclo de vida do cache de página de texto que este artigo adiciona por cima

O ciclo de vida do cache importa mais em fluxos de trabalho que editam e depois imediatamente agem sobre o resultado: carimbar uma correção e buscar por ela, redigir um parágrafo e confirmar que sumiu, ou localizar uma frase para ancorar uma anotação de marcação logo depois de inserir texto perto dela. Esse último caso vale a pena sinalizar por conta própria — anotações de marcação com quad-points são posicionadas a partir de retângulos de caractere lidos da página de texto, de modo que uma anotação construída a partir de coordenadas capturadas antes de uma edição acaba destacando o lugar errado uma vez que a edição pousa

As APIs de edição e texto de TPdf fazem parte do Componente PDFium para Delphi e C++Builder, e a página do produto traz a referência completa de método para as superfícies de edição, extração e busca cobertas aqui