Chiami AddText per stampigliare una riga su una pagina PDF con PDFiumPas, poi chiami immediatamente FindFirst per confermare che il timbro sia atterrato, e la ricerca torna vuota. Il testo è sulla pagina — Acrobat lo mostra — ma il componente TPdf di PDFiumPas mantiene una struttura FPDF_TEXTPAGE in cache separata, analizzata una volta dal content stream della pagina, e una modifica non aggiorna retroattivamente da sé quella struttura. Interrogala prima che sia stata aggiornata e leggi la pagina esattamente come appariva prima della tua modifica, non dopo
Perché PDFium restituisce testo obsoleto subito dopo una modifica?
PDFiumPas avvolge il motore di rendering PDFium di Google per Delphi e C++Builder, e le sue chiamate di testo e modifica raggiungono due sottosistemi diversi dentro quel motore. FPDF_TEXTPAGE appartiene al lato di lettura: FPDFText_LoadPage percorre una volta il content stream della pagina e costruisce la text page — codici carattere, posizioni, metriche del font, confini di parola — e PDFiumPas mantiene quella struttura in cache finché la pagina resta caricata. Le chiamate di modifica come FPDFPage_InsertObject o FPDFPage_GenerateContent operano su una rappresentazione completamente diversa, il grafo di oggetti e content-stream della pagina, e PDFium non spinge da sé quelle modifiche in una text page già aperta. Ricostruirla a ogni modifica renderebbe inaccettabilmente lenta la modifica in batch, quindi il design scambia quel costo con una regola invece — chiunque tenga l'handle lo chiude dopo una modifica che cambia il contenuto, e la lettura successiva ne costruisce uno fresco
Dentro la cache di testo di TPdf: FTextPage, LoadTextPage e UnloadTextPage
TPdf traccia l'handle in cache in un singolo campo privato, FTextPage, e ne avvolge il ciclo di vita in due metodi. LoadTextPage verifica se FTextPage è nil e, solo in quel caso, chiama FPDFText_LoadPage contro la pagina corrente; se un handle esiste già, LoadTextPage lo riutilizza senza chiedersi se la pagina sia cambiata da quando è stato costruito. UnloadTextPage è l'altra metà: chiude l'handle nativo con FPDFText_ClosePage, riporta FTextPage a nil, e scarta anche l'elenco di web-link in cache e qualsiasi sessione di ricerca in corso, poiché entrambi derivavano dalla stessa text page e diventano obsoleti per lo stesso motivo
Il comportamento di riutilizzo-senza-controllo di LoadTextPage è esattamente il motivo per cui la sequenza conta. Ogni query di testo su TPdf — Text, FindFirst, GetWebLinks — confluisce prima in LoadTextPage, quindi finché FTextPage continua a portare l'handle pre-modifica, nessuna di quelle chiamate ha modo di sapere che è avvenuto un cambiamento. La navigazione tra pagine non è mai stata il rischio qui: UnloadPage, che gira ai cambi di pagina, ricaricamenti e chiusura del documento, ha sempre chiuso la text page insieme alla pagina stessa. La domanda aperta riguardava sempre le modifiche applicate alla pagina su cui sei ancora seduto
Quali metodi di PDFiumPas aggiornano automaticamente la cache?
I metodi di modifica pagina propri di TPdf — AddText, SetText, SetTextPositions, AddPath, RemoveObject e InsertFormObjectFromXObject — chiamano ciascuno UnloadTextPage prima di chiamare UpdatePage (l'FPDFPage_GenerateContent di PDFium) per serializzare la modifica nel content stream. Chiama uno qualsiasi di questi e la successiva chiamata a Text, FindFirst o GetWebLinks ricostruisce la text page dal contenuto così com'è ora, senza alcuna chiamata extra richiesta da parte tua
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;
Lo schema che si rompe ancora: mettere in cache l'handle TextPage grezzo
TPdf espone l'handle vivo tramite una proprietà di sola lettura TextPage, per il raro caso in cui devi chiamare una funzione FPDFText_* che PDFiumPas non ha avvolto. Quella via di fuga è anche l'unico punto in cui l'invalidazione automatica non può aiutare: una volta che copi il valore FPDF_TEXTPAGE fuori dalla proprietà in una variabile locale, PDFiumPas non ha modo di sapere che lo stai ancora tenendo, e nessun modo di aggiornare la tua copia quando UnloadTextPage gira da qualche altra parte nel tuo codice
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;
Usare un handle dopo che FPDFText_ClosePage è già stata eseguita su di esso è comportamento non definito in PDFium stesso, non una convenzione di PDFiumPas che puoi scegliere di ignorare — può restituire l'ultimo dato noto, non restituire nulla, o mandare in crash il processo, e quale di questi accada in una data build non è qualcosa su cui il codice applicativo dovrebbe fare affidamento. La regola sicura è ristretta: leggi Pdf.TextPage fresco, immediatamente prima della chiamata FPDFText_* che ne ha bisogno, e non tenere mai una copia attraverso un'istruzione che potrebbe modificare la pagina
Raggruppa le tue modifiche, poi interroga una volta sola
Nulla di tutto ciò significa che ogni chiamata AddText o RemoveObject necessiti di una query di testo difensiva subito dopo per verificarne il risultato. Ogni metodo di modifica paga già il costo di chiudere la text page una volta; interrogare dopo ogni singola modifica dentro un ciclo paga di nuovo quel costo senza alcun beneficio, poiché FPDFText_LoadPage ripercorre l'intero content stream ogni volta che viene eseguita
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;
La stessa logica di raggruppamento si applica specificamente allo stato di ricerca. FindNext e FindPrevious continuano una sessione avviata da FindFirst, e quella sessione viene smantellata da UnloadTextPage insieme a tutto il resto, quindi chiamare di nuovo FindNext dopo una modifica — invece di chiamare di nuovo FindFirst — solleva un'eccezione invece di riprendere silenziosamente una ricerca contro un contenuto che non esiste più. Tratta ogni modifica come un confine rigido sia per il contenuto testuale sia per la posizione di ricerca, e lascia che un FindFirst fresco dall'altro lato delle tue modifiche riprenda la ricerca
Dove questo si inserisce con estrazione e lavoro sulle annotazioni
L'estrazione di puro testo — leggere il testo di una pagina senza cambiare nulla — non incontra mai nulla di tutto ciò, perché nulla invalida un handle che nessuna modifica ha toccato. Per come funzionano Text, i rettangoli dei caratteri e i confini di parola su una pagina non modificata, l'articolo di approfondimento sull'estrazione di testo con PDFiumPas tratta quel terreno senza il ciclo di vita della cache della text page che questo articolo aggiunge sopra
Il ciclo di vita della cache conta di più nei flussi di lavoro che modificano e poi agiscono immediatamente sul risultato: stampigliare una correzione e cercarla, oscurare un paragrafo e confermare che sia sparito, o localizzare una frase per ancorarvi un'annotazione di markup subito dopo aver inserito testo nelle vicinanze. Quest'ultimo caso vale la pena segnalarlo a parte — le annotazioni di markup a quad-point sono posizionate a partire dai rettangoli dei caratteri letti dalla text page, quindi un'annotazione costruita da coordinate catturate prima di una modifica finisce per evidenziare il punto sbagliato una volta che la modifica atterra
Le API di modifica e testo di TPdf fanno parte del componente PDFium per Delphi e C++Builder, e la pagina del prodotto porta il riferimento completo dei metodi per le superfici di modifica, estrazione e ricerca trattate qui