Chama-se AddText para carimbar uma linha numa página de PDF com o PDFiumPas, e depois chama-se imediatamente FindFirst para confirmar que o carimbo ficou lá, e a pesquisa devolve vazio. O texto está na página — o Acrobat mostra-o — mas o componente TPdf do PDFiumPas mantém uma estrutura FPDF_TEXTPAGE em cache separada, interpretada uma única vez a partir do fluxo de conteúdo da página, e uma edição não atualiza retroativamente essa estrutura por conta própria. Consultá-la antes de ser atualizada faz ler a página exatamente como era antes da alteração, não depois
Porque é que o PDFium devolve texto desatualizado logo após uma edição?
O PDFiumPas envolve o motor de renderização PDFium da Google para Delphi e C++Builder, e as suas chamadas de texto e de edição alcançam dois subsistemas diferentes dentro desse motor. O FPDF_TEXTPAGE pertence ao lado da leitura: o FPDFText_LoadPage percorre uma única vez o fluxo de conteúdo da página e constrói a página de texto — códigos de caracteres, posições, métricas de tipo de letra, limites de palavras — e o PDFiumPas mantém essa estrutura em cache enquanto a página permanecer carregada. As chamadas de edição, como FPDFPage_InsertObject ou FPDFPage_GenerateContent, operam sobre uma representação completamente diferente, o grafo de objetos e de fluxo de conteúdo da página, e o PDFium não propaga essas alterações por si só para uma página de texto já aberta. Reconstruí-la a cada edição tornaria a edição em lote inaceitavelmente lenta, pelo que o desenho troca esse custo por uma regra: quem detém o handle fecha-o depois de uma edição que altere o conteúdo, e a próxima leitura constrói um novo
Dentro da cache de texto do TPdf: FTextPage, LoadTextPage, e UnloadTextPage
O TPdf acompanha o handle em cache num único campo privado, FTextPage, e envolve o seu ciclo de vida em dois métodos. O LoadTextPage verifica se FTextPage é nil e, só nesse caso, chama FPDFText_LoadPage sobre a página atual; se já existir um handle, o LoadTextPage reutiliza-o sem verificar se a página mudou desde que foi construído. O UnloadTextPage é a outra metade: fecha o handle nativo com FPDFText_ClosePage, repõe FTextPage a nil, e também descarta a lista de ligações web em cache e qualquer sessão de pesquisa em curso, já que ambas derivavam da mesma página de texto e ficam desatualizadas pela mesma razão
O comportamento de reutilização sem verificação do LoadTextPage é exatamente a razão pela qual a sequência importa. Toda a consulta de texto no TPdf — Text, FindFirst, GetWebLinks — passa primeiro por LoadTextPage, pelo que, enquanto FTextPage continuar a segurar o handle pré-edição, nenhuma dessas chamadas tem forma de saber que ocorreu uma alteração. A navegação entre páginas nunca foi o risco aqui: o UnloadPage, que corre nas mudanças de página, recargas, e fecho de documento, sempre fechou a página de texto juntamente com a própria página. A questão em aberto foi sempre sobre edições aplicadas à página em que ainda se está posicionado
Que métodos do PDFiumPas atualizam a cache automaticamente?
Os próprios métodos de edição de página do TPdf — AddText, SetText, SetTextPositions, AddPath, RemoveObject, e InsertFormObjectFromXObject — chamam cada um UnloadTextPage antes de chamarem UpdatePage (o FPDFPage_GenerateContent do PDFium) para serializar a alteração no fluxo de conteúdo. Ao chamar qualquer um destes, a chamada seguinte a Text, FindFirst, ou GetWebLinks reconstrói a página de texto a partir do conteúdo tal como ele agora está, sem qualquer chamada extra necessária da parte de quem programa
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 continua a falhar: guardar o handle TextPage em bruto
O TPdf expõe o handle ativo através de uma propriedade só de leitura, TextPage, para o caso raro de ser necessário chamar uma função FPDFText_* que o PDFiumPas ainda não encapsulou. Essa via de escape é também o único ponto onde a invalidação automática não pode ajudar: assim que se copia o valor FPDF_TEXTPAGE da propriedade para uma variável local, o PDFiumPas não tem forma de saber que ainda se está a segurar nele, nem forma de atualizar essa cópia quando UnloadTextPage corre noutro ponto do 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 de o FPDFText_ClosePage ter corrido sobre ele é comportamento indefinido no próprio PDFium, não uma convenção do PDFiumPas que se possa optar por ignorar — pode devolver os últimos dados conhecidos, não devolver nada, ou levar o processo ao crash, e qual destes cenários ocorre numa dada compilação não é algo de que o código da aplicação se deva fiar. A regra segura é restrita: ler Pdf.TextPage de novo, imediatamente antes da chamada FPDFText_* que dele precisa, e nunca guardar uma cópia entre uma instrução que possa editar a página
Agrupe as suas edições e consulte uma única vez
Nada disto significa que cada chamada a AddText ou RemoveObject precisa de uma consulta de texto defensiva logo a seguir 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 ciclo paga esse custo outra vez sem qualquer benefício, já que o FPDFText_LoadPage volta a percorrer todo o fluxo de conteúdo sempre que corre
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 aplica-se especificamente ao estado de pesquisa. O FindNext e o FindPrevious dão continuidade a uma sessão iniciada por FindFirst, e essa sessão é desfeita por UnloadTextPage juntamente com tudo o resto, pelo que chamar FindNext outra vez depois de uma edição — em vez de chamar FindFirst de novo — levanta uma exceção em vez de retomar silenciosamente uma pesquisa contra conteúdo que já não existe. Trate qualquer edição como uma fronteira rígida tanto para o conteúdo de texto como para a posição de pesquisa, e deixe que um FindFirst novo, do outro lado das edições, retome a pesquisa
Como isto se encaixa com trabalho de extração e de anotação
A extração de texto simples — ler o texto de uma página sem alterar nada — nunca esbarra em nada disto, porque nada invalida um handle que nenhuma edição tocou. Para saber como Text, os retângulos de caracteres, e os limites de palavras funcionam numa página não modificada, o artigo relacionado sobre extração de texto com o PDFiumPas cobre esse terreno sem o ciclo de vida da cache de página de texto que este artigo acrescenta
O ciclo de vida da cache importa sobretudo em fluxos de trabalho que editam e depois agem de imediato sobre o resultado: carimbar uma correção e procurá-la, redigir um parágrafo e confirmar que desapareceu, ou localizar uma frase para ancorar uma anotação de marcação logo depois de inserir texto perto dela. Este último caso vale a pena assinalar por si só — as anotações de marcação por pontos quad são posicionadas a partir de retângulos de caracteres lidos da página de texto, pelo que uma anotação construída a partir de coordenadas capturadas antes de uma edição acaba por destacar o local errado assim que a edição é aplicada
As APIs de edição e de texto do TPdf fazem parte do PDFium Component para Delphi e C++Builder, e a página do produto contém a referência completa de métodos das superfícies de edição, extração, e pesquisa aqui abordadas