Articolo tecnico

Testo Obsoleto Dopo la Modifica: la Cache FPDF_TEXTPAGE di PDFium

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

Le modifiche PDFium scrivono nello stream di contenuto della pagina mentre la FPDF_TEXTPAGE in cache resta un'istantanea presa al caricamento, così una query FindFirst Delphi subito dopo AddText legge la pagina pre-modifica e non vede il timbro
Modificare e leggere sono due sottosistemi separati dentro PDFium; la pagina di testo in cache è uno snapshot dal momento del caricamento e nessuna modifica la rinfresca da sola

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 TPdfText, 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 TPdfAddText, 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 ha già chiuso la text page in cache, quindi questa chiamata FindFirst
    // la ricostruisce da zero prima di cercare
    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;    // handle di FPDFText_LoadPage, in cache in FTextPage
    Pdf.SetText(0, 'Amended Clause 4.2');
    // SetText ha già chiuso RawHandle e ha riportato Pdf.TextPage a nil.
    // Chiamare qualsiasi funzione FPDFText_* contro il vecchio valore tocca ora un
    // handle che PDFium ha già liberato — comportamento indefinito, non un bug che
    // puoi intercettare con un controllo su nil
    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

I metodi di modifica TPdf come AddText, SetText e RemoveObject chiamano UnloadTextPage prima di UpdatePage così la successiva query Delphi Text, FindFirst o GetWebLinks ricostruisce la FPDF_TEXTPAGE dal contenuto modificato
Ogni modifica con wrapper abbandona prima la pagina di testo obsoleta e genera il contenuto dopo; la successiva query di testo ricostruisce quindi FPDF_TEXTPAGE automaticamente
var
  Pdf: TPdf;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'watermarked.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    // Rimuovi ogni oggetto di testo che sembra un watermark di bozza. Ogni
    // chiamata RemoveObject invalida già la cache da sola, quindi
    // niente deve essere aggiornato a mano tra le iterazioni
    for I := Pdf.ObjectCount - 1 downto 0 do
      if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
        Pdf.RemoveObject(I, True);

    // Esegui la query una volta, dopo l'intero batch, non una per ogni rimozione
    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

Copiare l'handle grezzo FPDF_TEXTPAGE fuori dalla proprietà TextPage di TPdf e chiamare FPDFText_CountChars su di esso dopo SetText lascia il codice Delphi usare un handle PDFium già liberato, che è comportamento indefinito
Un valore FPDF_TEXTPAGE copiato continua a puntare a un handle che il percorso di modifica ha già chiuso; leggere invece Pdf.TextPage fresco immediatamente prima di ogni chiamata FPDFText_* senza wrapper

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