Articolo tecnico

Cercare e sostituire testo in un PDF esistente con Delphi

HotPDF Component può cercare e sostituire testo all'interno di un PDF esistente da Delphi e C++Builder. SearchLoadedPageText ed SearchLoadedDocumentText individuano ogni occorrenza di una stringa con precisione a livello di glifo, mentre ReplaceLoadedPageText e ReplaceLoadedDocumentText riscrivono i byte corrispondenti sul posto — a condizione che ogni carattere di sostituzione possa essere ricodificato tramite il font originale, un vincolo fisico che questo articolo affronta onestamente anziché nasconderlo in una nota a piè di pagina

La richiesta alla base di questa funzionalità è sempre ordinaria. Un'azienda cambia nome e tremila fatture archiviate riportano ancora il vecchio nome. Un modello di contratto presenta la data di scadenza dello scorso anno. Un codice prodotto è stato ritirato e ogni scheda tecnica che lo menziona richiede invece il codice successivo. In un elaboratore di testi ognuno di questi è un lavoro da trenta secondi. In un PDF si tratta di un problema decisamente complesso, e capirne il motivo fa la differenza tra utilizzare correttamente le API e inviare una segnalazione di bug che in realtà è solo una citazione delle specifiche

Perché sostituire il testo in un PDF è così difficile?

La sostituzione del testo in un PDF è complessa perché una pagina PDF non contiene testo modificabile, bensì glifi posizionati. Secondo il modello di visualizzazione del testo di ISO 32000-1 §9.4, un flusso di contenuti guida operatori come Tj e TJ che disegnano sequenze di codici di caratteri alle coordinate stabilite dalla matrice del testo. Tali codici non sono Unicode; sono indici in qualsiasi codifica dichiari il font della pagina, e la mappatura inversa a caratteri leggibili può risiedere in una CMap /ToUnicode, in un array di differenze di codifica o in una catena di mappatura CID. Non esiste un oggetto paragrafo, nessun flusso di testo e nessuna garanzia che una parola visiva sia memorizzata come un'unica stringa

La sostituzione aggiunge un secondo livello di difficoltà oltre alla decodifica: è necessario sapere esattamente quali byte del flusso originale hanno prodotto ciascun glifo, in modo da poter inserire nuovi byte precisamente in quell'intervallo e in nessun altro punto. Un estrattore di testo può permettersi di scartare le posizioni dei byte una volta ottenuto l'output Unicode. Un sostitutore non può farlo. Ecco perché HotPDF ha diviso il lavoro in due versioni — la v2.251.0 ha introdotto il livello di tracciamento degli offset e di ricerca, e la v2.252.0 ha implementato il livello di riscrittura su di esso

Trovare il testo: ricerca a livello di glifo con tracciamento dell'offset dei byte

La funzione SearchLoadedDocumentText di HotPDF trova ogni occorrenza di una stringa cercata confrontandola con la sequenza di glifi Unicode decodificata di ciascuna pagina, non con i byte grezzi del flusso, quindi una corrispondenza è tale indipendentemente da come il font l'ha codificata. L'infrastruttura sottostante è stata introdotta nella v2.251.0: il tokenizer del flusso di contenuti registra un intervallo di byte StartOfs/EndOfs per ogni operando stringa — inclusi i suoi delimitatori ( ) o < > — e ogni glifo decodificato trasporta una tripla TokenIndex/ItemIndex/ByteOffset che rimanda all'esatto operando, all'elemento dell'array TJ e all'unità di codice che lo ha prodotto. Lo stesso interprete di glifi alimenta le API di estrazione descritte nell'estrazione di testo da un PDF caricato in Delphi; la ricerca conserva semplicemente l'origine che l'estrazione scarta

Ogni corrispondenza viene restituita come record THPDFTextMatch contenente l'indice di pagina, l'intervallo inclusivo di glifi, l'origine X/Y e la larghezza nello spazio utente del riscontro, il token e l'indice dell'elemento sorgente, e il testo corrispondente stesso. Questo è sufficiente per gestire una sovrapposizione di evidenziazione, un'interfaccia utente di revisione o la fase di sostituzione. Una ricerca che non trova nulla restituisce un array vuoto anziché fallire, mantenendo semplice il pattern di chiamata

var
  Pdf: THotPDF;
  Matches: THPDFTextMatchArray;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoices-2025.pdf') > 0 then
    begin
      if Pdf.SearchLoadedDocumentText('Acme Corp', False, Matches) then
        for I := 0 to Length(Matches) - 1 do
          WriteLn(Format('page %d at (%.1f, %.1f): "%s"',
            [Matches[I].PageIndex, Matches[I].X, Matches[I].Y,
             Matches[I].Text]));
    end;
  finally
    Pdf.Free;
  end;
end;

Una scelta di progettazione intenzionale merita una nota. Quando CaseSensitive è False, il confronto esegue il folding delle maiuscole/minuscole solo per i caratteri ASCII: il case folding Unicode completo si comporta in modo diverso tra le toolchain da Delphi 5 a XE supportate da HotPDF, e un'API di ricerca che trova corrispondenze diverse a seconda del compilatore con cui è stata creata l'applicazione è peggiore di una con un limite documentato e prevedibile. Per i testi aziendali latini — nomi, codici, date — la conversione ASCII copre i casi pratici

Sostituire il testo: codifica inversa e innesto chirurgico

La funzione ReplaceLoadedDocumentText, aggiunta in HotPDF v2.252.0, riscrive ogni occorrenza di una stringa cercata eseguendo al contrario i meccanismi di decodifica. La funzione HPDFEncodeUnicode è l'inverso del decodificatore dei codici dei caratteri: ripercorre al contrario la stessa catena di strategie — ricerca di bfchar e bfrange in /ToUnicode, mappatura CID del flusso di codifica, mappature di identità Type0 e tabelle integrate WinAnsi e MacRoman — per convertire ciascun carattere di sostituzione nei byte dei codici dei caratteri previsti dal font originale. I byte ricodificati vengono poi serializzati in una stringa letterale o esadecimale ben formata, rispecchiando le regole di escaping del tokenizer in modo che il ciclo di analisi → riserializzazione sia stabile

L'innesto stesso è chirurgico anziché all'ingrosso. Solo l'intervallo di byte di codice coperto dalla corrispondenza viene sostituito all'interno dell'operando stringa; i byte non corrispondenti nello stesso operando, gli spazi vuoti tra i token e ogni operatore circostante vengono conservati in modo letterale, byte per byte. La sostituzione di bca all'interno di abcabc produce a + sostituzione + bc, non un operando rovinato. Le sostituzioni possono essere più corte o più lunghe rispetto al testo cercato — il valore letterale viene riserializzato e il parametro /Length del flusso viene aggiornato — e ogni flusso /Contents di una pagina multi-flusso viene elaborato singolarmente in modo che la pagina rimanga corretta

var
  Pdf: THotPDF;
  ReplaceCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract-draft.pdf') > 0 then
    begin
      if Pdf.ReplaceLoadedDocumentText('2025-12-31', '2026-12-31',
        True, ReplaceCount) then
        WriteLn(Format('%d operand rewrites performed', [ReplaceCount]));
      Pdf.SaveLoadedDocument('contract-final.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Nota ciò che le API non fanno: non eseguono la composizione tipografica della pagina. Il formato PDF non prevede il reflow del testo, quindi una sostituzione visivamente più larga dell'originale occuperà semplicemente più spazio orizzontale e potrebbe sovrapporsi a ciò che è disegnato alla sua destra. Le sostituzioni di lunghezza uguale o simile — date, stringhe di versione, codici di parti, correzioni di nomi — rappresentano lo scenario ideale. Una riscrittura completa appartiene al documento sorgente, non al PDF

Perché non è possibile sostituire il testo con caratteri che il subset del font non ha mai incluso?

Non è possibile sostituire il testo con un carattere che il subset del font incorporato non ha mai incluso, poiché la sequenza di byte che selezionerebbe tale carattere semplicemente non esiste nelle tabelle di mappatura del font. Quando un produttore di PDF incorpora un font subset, la sua CMap /ToUnicode e le strutture di codifica coprono solo i glifi effettivamente utilizzati nel documento originale. HPDFEncodeUnicode può solo invertire una mappatura presente: se il documento non ha mai contenuto la lettera E in quel font, non esiste alcun codice di carattere a cui invertire E. Questa è una proprietà fisica del file, non una limitazione di una specifica libreria — nessuno strumento può evocare una mappatura di glifi che non è mai stata incorporata

HotPDF gestisce questo fallimento in modo conservativo. Se un singolo carattere della sostituzione non può essere ricodificato, l'intera occorrenza cercata viene saltata — nessuna eccezione, nessun testo parzialmente corrotto, e l'occorrenza semplicemente non viene conteggiata in ReplaceCount. Conseguenza pratica: confronta ReplaceCount con il conteggio delle corrispondenze di una ricerca precedente e considera un eventuale scostamento come un segnale. Nell'esempio della data precedente, la cifra 6 deve apparire da qualche parte nel testo del documento nello stesso font affinché la riscrittura abbia successo — probabile in una fattura, mai garantito in generale. Quando i caratteri necessari non sono disponibili e l'obiettivo è rimuovere testo sensibile anziché riformularlo, la rimozione effettiva dei contenuti rimane lo strumento migliore; si veda la guida su redigere e ristrutturare PDF caricati in Delphi per seguire questo percorso

var
  Matches: THPDFTextMatchArray;
  Expected, Replaced: Integer;
begin
  Pdf.SearchLoadedDocumentText('Acme Corp', True, Matches);
  Expected := Length(Matches);
  Pdf.ReplaceLoadedDocumentText('Acme Corp', 'Apex Corp', True, Replaced);
  if Replaced < Expected then
    WriteLn(Format('%d occurrence(s) skipped: characters missing ' +
      'from the font subset, or match spans multiple operands',
      [Expected - Replaced]));
end;

La seconda condizione di salto in quel messaggio è l'altro limite documentato: una stringa cercata che si estende su più operandi stringa — ad esempio, Hello diviso tra gli elementi [(He)(llo)] TJ — viene trovata dalla ricerca, perché quest'ultima corrisponde alla sequenza di glifi decodificata, ma viene saltata dalla sostituzione, poiché la riscrittura attraverso i confini degli operandi richiederebbe l'unione di intervalli di byte adiacenti. La ricerca seguita dalla verifica rende visibili entrambi i limiti invece di lasciarli silenziosi

Cosa cambia nel file al momento del salvataggio?

Un flusso /Contents sostituito viene salvato non compresso. I flussi compressi con FlateDecode vengono decompressi per la modifica e, quando HotPDF scrive i byte ricostruiti, rimuove la voce /Filter del flusso e aggiorna il parametro /Length invece di comprimere nuovamente. Il PDF risultante è pienamente valido e viene visualizzato normalmente nei comuni visualizzatori; il compromesso è un file più grande per ogni flusso modificato. Per una pipeline batch che elabora migliaia di documenti, pianifica questa crescita o esegui un passaggio di compressione separato a valle. Il modo in cui gli oggetti riscritti interagiscono con la struttura di riferimento incrociato del documento al salvataggio è un argomento a sé, trattato nell'articolo sui flussi di oggetti e aggiornamenti incrementali in HotPDF

Tutto il resto nel file viene lasciato inalterato. I flussi non toccati mantengono la loro compressione, i font e le immagini non vengono riscritti, e l'innesto a livello di operando significa che anche i flussi modificati differiscono dall'originale solo dove si è verificata una corrispondenza. Tale approccio conservativo è intenzionale: quanto più una libreria riscrive un documento caricato, tanto maggiori sono le opportunità di alterare una particolarità del produttore che non era stata prevista

La ricerca e sostituzione del testo si uniscono all'estrazione, alla redazione e al rendering delle pagine nel set di strumenti per i documenti caricati di HotPDF, tutti guidati dallo stesso interprete del flusso di contenuti e disponibili da Delphi 5 fino alle attuali versioni di RAD Studio senza dipendenze esterne. Il riferimento completo delle API e il download della versione di prova si trovano sulla pagina del prodotto HotPDF Component