HotPDF Component estrae testo Unicode da qualsiasi PDF caricato in Delphi attraverso due chiamate: ExtractLoadedPageText restituisce il testo del flusso di lettura di una pagina, e ExtractLoadedPageTextLayout (introdotto nella v2.263.0) ricostruisce la disposizione visiva della pagina come testo semplice, preservando colonne, rientri e allineamento delle tabelle nell'output. Entrambi funzionano su documenti non creati da HotPDF, che è il caso di reale interesse: la fattura inviata da un cliente via e-mail, il rapporto consegnato da un ufficio di scansione, il contratto generato da un software sconosciuto
Raggiungere questo obiettivo ha richiesto più ingranaggi di quanto le due firme suggeriscano, poiché un PDF non memorizza il testo allo stesso modo di un file di testo. Questo articolo illustra entrambe le modalità di estrazione, per poi analizzare i tre componenti sottostanti — il lettore CMap, l'interprete del flusso di contenuti e la catena di fallback per la decodifica dei font — poiché sapere come funziona la mappatura fa la differenza tra rassegnarsi a un output corrotto e diagnosticarlo
Perché l'estrazione del testo è più difficile della semplice lettura delle stringhe dal file?
Un flusso di contenuti PDF registra codici di caratteri, non caratteri. Gli operatori Tj e TJ (ISO 32000-1 §9.4.3) trasportano stringhe di byte il cui significato dipende interamente dal font selezionato dal precedente Tf: il byte 0x41 potrebbe essere la lettera A in WinAnsi, un glifo arbitrario in un font subset o la metà di un CID a due byte in un font CJK composito. Lo standard ISO 32000-1 §9.10 definisce l'estrazione del testo proprio come questo problema di decodifica — mappare ogni codice a Unicode utilizzando qualsiasi informazione fornita dal dizionario del font — e lo standard dichiara esplicitamente che a un file conforme non è richiesto di fornire informazioni sufficienti per farlo
Quest'ultima clausola spiega ogni segnalazione di bug del tipo "perché il copia-incolla da questo PDF produce caratteri incomprensibili" che tu abbia mai visto. Un produttore che incorpora un font subset senza tabella /ToUnicode scrive un file che viene renderizzato perfettamente ma estratto come testo senza senso, poiché esiste la mappatura codice-glifo ma la mappatura codice-Unicode non è mai stata fornita. Qualsiasi API di estrazione onesta è quindi una catena di fallback di tipo "best-effort", e la domanda utile è quanto sia profonda tale catena
Estrazione del flusso di lettura con ExtractLoadedPageText
Per l'indicizzazione delle ricerche, la corrispondenza delle parole chiave o l'invio di testo a una pipeline di analisi, ExtractLoadedPageText è la chiamata corretta. La firma è function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — gli indici di pagina sono a base zero, il risultato viene restituito come una UnicodeString nativa di Delphi, e la funzione restituisce False quando la pagina non ha un flusso di contenuti leggibile, invece di sollevare un'eccezione
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText, AllText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('invoice.pdf');
AllText := '';
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
AllText := AllText + PageText + #13#10;
// AllText now holds the reading-flow text of the document
finally
Pdf.Free;
end;
end;
Le interruzioni di riga nell'output derivano da un'euristica volutamente semplice: quando l'origine verticale di un glifo si sposta di oltre la metà della dimensione del font corrente — la firma di un passaggio Td o T* nel flusso di contenuti — viene inserita una nuova riga. I caratteri che il decodificatore non riesce a risolvere diventano spazi anziché scomparire, per cui i confini delle parole si conservano anche quando i singoli glifi vanno persi. Questa modalità non tenta il raggruppamento per ordine di lettura o il rilevamento di più colonne: una pagina a due colonne viene prodotta con testo alternato nell'ordine del flusso dei contenuti, che di solito, ma non sempre, coincide con l'ordine visivo
Quando si dovrebbe preferire l'estrazione che preserva il layout?
ExtractLoadedPageTextLayout è la chiamata giusta ogni volta che la posizione ha un significato: tabelle, moduli, elenchi di codici, qualsiasi cosa si intenda confrontare, filtrare o analizzare per colonne. Invece di appiattire i glifi in un flusso, li raggruppa in linee di base, ordina ogni linea lungo l'asse X e riproduce gli spazi vuoti orizzontali e verticali su una griglia di caratteri a spaziatura fissa dimensionata in base all'avanzamento medio del glifo e alla dimensione del font. Le ampie lacune tra i segmenti sulla stessa linea di base diventano sequenze di spazi; le grandi distanze tra le linee di base diventano righe vuote. Il risultato si legge proprio come appare la pagina
var
Grid: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
// Columns, indentation and table alignment survive as
// spaces and blank lines on a character grid
end;
Le due modalità condividono ogni byte dei meccanismi di decodifica e differiscono solo nel modo in cui dispongono i glifi decodificati, quindi la scelta non influisce sulla fedeltà dell'output. Scegli ExtractLoadedPageText quando contano solo le parole ed ExtractLoadedPageTextLayout quando conta la disposizione. Il rilevamento dell'ordine di lettura a più colonne rimane al di fuori dell'ambito di entrambe — un rendering a griglia di una pagina a due colonne mostra entrambe le colonne affiancate in modo fedele, il che è perfetto per un confronto di differenze ma non per la ristrutturazione del testo
Come decodifica HotPDF i codici dei caratteri in Unicode?
HotPDF Component risolve ciascun codice di carattere attraverso una catena di fallback ordinata per priorità: prima la CMap /ToUnicode distribuita nel font, poi la voce /Encoding (flusso o CMap denominata), quindi — per i font compositi — i file CMap standard di Adobe per raccolte di caratteri quali Adobe-GB1, Adobe-CNS1, Adobe-Japan1 e Adobe-KR, e infine le tabelle integrate WinAnsi e MacRoman per i font semplici. Una strategia che non riesce a fornire una resposta degrada silenziosamente alla successiva anziché sollevare un'eccezione, e un codice che esaurisce l'intera catena si risolve in 0 in modo che il chiamante possa contare i fallimenti invece di tirare a indovinare
La CMap /ToUnicode (ISO 32000-1 §9.10.3) si trova al primo posto poiché è la mappatura scritta dal produttore specificamente per l'estrazione. Il percorso delle CMap standard di Adobe è importante per i documenti CJK che utilizzano CMap predefinite come UniGB-UTF16-H anziché incorporare elementi: HotPDF distribuisce i file della raccolta nella cartella resources\CMap, li individua in fase di esecuzione rispetto all'eseguibile e memorizza in cache ciascuna mappa analizzata per processo — dettaglio utile da sapere poiché la più grande di esse, la mappa Adobe-GB1, è composta da circa 2 MB di testo sorgente che si desidera evitare di analizzare nuovamente a ogni pagina. Se la cartella è assente, il decodificatore ignora semplicemente le CMap su disco e lavora con le tabelle incorporate e le codifiche integrate. Questo è lo specchio lato lettura del problema di shaping trattato in shaping del testo per script complessi con HotPDF, in cui la stessa distinzione codice-glifo viene affrontata in fase di scrittura
Due trappole di sintassi CMap che vale la pena conoscere
I file CMap sembrano facili da analizzare a livello sintattico ma non lo sono, e due dettagli spiegano la maggior parte dei fallimenti nei primi tentativi di scrittura di un analizzatore. Il primo è che il conteggio dei record precede sempre la parola chiave della sezione: una sezione riporta 2 beginbfchar, non beginbfchar 2. Un analizzatore che si aspetta il conteggio dopo la parola chiave consuma il numero como token estraneo, trovando poi zero voci in ogni sezione. L'approccio robusto — quello adottato dal lettore di HotPDF — consiste nell'ignorare completamente il conteggio ed eseguire un ciclo fino alla parola chiave corrispondente endbfchar / endbfrange, il che ha il vantaggio di tollerare file reali i cui conteggi sono semplicemente errati
La seconda trappola è che i target di bfchar e bfrange sono stringhe UTF-16BE, non interi. La destinazione <D83DDE00> indica U+1F600 — una coppia di surrogati che deve essere ricombinata in un unico code point — e leggere quei quattro byte come un intero big-endian produce un valore privo di significato per qualsiasi code point al di fuori del Basic Multilingual Plane. Le emoji nei PDF non sono più una rarità, per cui un decodificatore che salta la ricombinazione dei surrogati fallisce con i file reali degli utenti. HotPDF analizza prima il valore letterale esadecimale in byte grezzi, quindi ricombina le unità di codice UTF-16BE, coprendo anche i target a più caratteri generati dalle mappature delle legature
Scendere a livello di glifo con ExtractLoadedPageGlyphs
Entrambe le chiamate testuali sono basate su ExtractLoadedPageGlyphs, e l'array THPDFGlyphArray sottostante è utilizzabile anche nel tuo codice. Ogni record THPDFGlyphRecord trasporta il code point Unicode risolto insieme al codice di carattere grezzo, la larghezza in byte del codice (1, 2, o 4, decisa dal codespacerange della CMap), la chiave e dimensione della risorsa font attiva, l'origine X e Y dello spazio utente e l'avanzamento orizzontale. Ciò è sufficiente per implementare il rilevamento dei confini delle parole, l'evidenziazione posizionata o un algoritmo di layout personalizzato senza dover manipolare direttamente il flusso dei contenuti
var
Glyphs: THPDFGlyphArray;
I, Unresolved: Integer;
begin
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
begin
Unresolved := 0;
for I := 0 to High(Glyphs) do
if Glyphs[I].Unicode = 0 then
Inc(Unresolved);
if Unresolved > 0 then
ShowMessageFmt('%d of %d glyphs have no Unicode mapping',
[Unresolved, Length(Glyphs)]);
end;
end;
Il conteggio dei record Unicode = 0, come mostrato sopra, è il modo corretto per misurare la qualità dell'estrazione su un determinato documento prima di fare affidamento sul testo a valle. I record dei glifi ancorano inoltre ciascun carattere all'operando sorgente nel flusso di contenuti, il che rende possibili le funzioni di ricerca e sostituzione del testo nei documenti caricati di HotPDF sulla base della stessa struttura
Quali PDF non consentono l'estrazione del testo?
Alcuni file sconfiggono qualsiasi estrattore, ed è meglio rilevarli piuttosto che accettarne l'output. I documenti scansionati sono il caso più evidente: una pagina che consiste in una sola grande immagine non contiene alcun operatore di testo, quindi l'estrazione restituisce correttamente una stringa vuota — la soluzione è l'OCR, ed estrarre le immagini della pagina dal PDF caricato rappresenta il primo passo di questa pipeline. I font subset senza tabella /ToUnicode sono il caso più ostico: se anche il percorso di /Encoding e le CMap standard risultano vuoti, quei glifi si risolvono in 0 e appaiono come spazi nelle chiamate di testo. I documenti crittografati vengono estratti normalmente a condizione di caricarli con la relativa password tramite l'overload di LoadFromFile, in modo che i flussi vengano decrittografati prima dell'intervento dell'interprete
Un limite più specifico merita di essere esposto chiaramente: la catena di decodifica legge i flussi CMap e di contenuto attraverso il percorso Flate di HotPDF, quindi un font il cui flusso ToUnicode utilizza un filtro insolito degrada alla strategia successiva anziché far fallire la pagina. In pratica, FlateDecode copre quasi tutto ciò che è stato prodotto negli ultimi due decenni, e il degrado è silenzioso per progettazione — si ottiene il miglior testo consentito dal file anziché un'eccezione. Lo stesso meccanismo degli oggetti lato lettura che risolve i dizionari dei font in questo caso gestisce anche la modifica dei metadati nei documenti caricati, consentendo a una pipeline di acquisizione dei documenti di estrarre, ispezionare e annotare in un unico passaggio
L'estrazione del testo, il rendering con conservazione del layout, l'accesso a livello di glifo e le funzionalità di ricerca e sostituzione basate su di essi fanno tutti parte del componente standard HotPDF Component per Delphi e C++Builder — nessuna DLL esterna, nessun servizio di testo del sistema operativo, solo Object Pascal in cui è possibile eseguire il debug passo-passo quando un file insolito arriva nella coda di elaborazione