Articolo tecnico

Estrazione di testo da file PDF con PDFium Component in Delphi

L'estrazione di testo da PDF sembra semplice finché non si incontra un documento in cui il livello di testo è assente, corrotto o suddiviso in decine di frammenti di caratteri privi di un ordine logico. Il Componente PDFium offre due punti di accesso: l'array Character[] per l'accesso diretto e basato su indici a ciascun glifo della pagina, e ReadablePageContent per una visualizzazione strutturata che ricostruisce paragrafi e intestazioni dall'albero dei tag del PDF o tramite analisi euristica. Nessuno dei due rappresenta la scelta ideale per ogni situazione, per cui è importante comprendere cosa ciascun metodo esponga

Apertura del documento e gestione degli errori silenti

TPdf apre un file impostando la proprietà FileName e attivando Active := True. L'aspetto critico da considerare è che l'assegnazione Active := True non solleva eccezioni. Se il file è mancante, protetto da password o corrotto, PDFium gestisce l'errore internamente e la proprietà Active rimane semplicemente su False. Di conseguenza, ogni procedura di estrazione deve prevedere questa verifica:

Pdf := TPdf.Create(nil);
try
  Pdf.FileName := 'report.pdf';
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    ShowMessage('Could not open PDF (damaged or wrong password)');
    Exit;
  end;
  // extraction follows here
finally
  Pdf.Active := False;
  Pdf.Free;
end;

I file protetti da password richiedono l'impostazione di Pdf.Password := '...' prima di attivare Active := True. Non sono consentiti tentativi successivi: se l'attivazione fallisce, occorre chiudere il documento e riaprirlo inserendo la password corretta

Estrazione pagina per pagina tramite Character[]

L'approccio di livello più basso esamina ogni singolo carattere di ciascuna pagina. Imposta Pdf.PageNumber per caricare il livello di testo della pagina desiderata, quindi scorri le voci di CharacterCount utilizzando la proprietà Character[]. È utile verificare due flag per ogni elemento: CharacterGenerated[i] identifica i glifi sintetici inseriti dal motore di rendering (ad esempio i trattini di a capo automatico) che non corrispondono a un valore Unicode reale, e CharacterMapError[i] segnala che PDFium non ha potuto mappare il glifo a un punto di codice, cosa che accade con codifiche dei font prive di tabella ToUnicode

procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
  Page, I: Integer;
  Line: string;
  Ch: WideChar;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    Line := '';
    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
        Continue;
      Ch := Pdf.Character[I];
      if Ch = #13 then
        Ch := #10;   // normalize CR to LF
      Line := Line + Ch;
    end;
    Output.Add(Line);
  end;
end;

Il risultato è una stringa lineare di punti di codice Unicode nell'ordine in cui PDFium li enumera, che corrisponde all'ordine con cui compaiono nel flusso di contenuti e non necessariamente all'ordine di lettura da sinistra a destra. Per la maggior parte dei documenti in caratteri latini prodotti con strumenti per ufficio standard questo è corretto. Per i PDF scansionati sottoposti a OCR con sequenze di glifi insolite, o per testi scritti da destra a sinistra, l'ordine può risultare errato. In questi casi, l'uso di ReadablePageContent si rivela più efficace

Estrazione strutturata tramite ReadablePageContent

Il metodo ReadablePageContent opera a un livello superiore: restituisce un record TPdfReadableContent il cui array Fragments contiene frammenti di testo associati a tag, ciascuno con una proprietà Kind che identifica paragrafi, intestazioni, elementi di elenchi, celle di tabelle e altro. Se il PDF contiene un albero di struttura (verificabile con Pdf.IsTagged), l'origine dei dati è rosStructure e l'ordine di lettura è garantito. Per i file non strutturati, PDFium ricorre al metodo rosHeuristic, che raggruppa i caratteri in base ai rispettivi box di delimitazione in unità di lettura plausibili, senza tuttavia garantire l'esattezza dell'ordine

procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
  Page: Integer;
  Content: TPdfReadableContent;
  Fragment: TPdfContentFragment;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Content := Pdf.ReadablePageContent(Page);
    for Fragment in Content.Fragments do
    begin
      case Fragment.Kind of
        cfHeading   : Output.Add('# ' + Fragment.Text);
        cfParagraph : Output.Add(Fragment.Text);
        cfListItem  : Output.Add('- ' + Fragment.Text);
      else
        Output.Add(Fragment.Text);
      end;
    end;
  end;
end;

Se la proprietà Content.Source è pari a rosHeuristic e l'output appare disordinato, probabilmente il livello di testo del documento non è stato generato tenendo conto dell'ordine di lettura. In tal caso, l'unica soluzione consiste nel riesportare il file dall'applicazione di origine configurando una corretta struttura dei tag, o nell'applicare un passaggio di post-elaborazione che ordini le coordinate di origine dei caratteri rispetto all'asse Y e successivamente all'asse X

Informazioni fornite da CharacterOrigin e CharacterRectangle

Entrambe le proprietà restituiscono la posizione di un carattere nello spazio della pagina (in punti, con origine nell'angolo in basso a sinistra e coordinata Y che cresce verso l'alto). CharacterOrigin[i] rappresenta il punto di ancoraggio della linea di base del glifo; CharacterRectangle[i] fornisce l'intero box di delimitazione (bounding box). Questi dati costituiscono le basi per elaborazioni complesse: individuare i margini delle colonne, raggruppare i caratteri in righe confrontando le coordinate Y entro un intervallo di tolleranza, o creare una mappa di rilevamento dei clic per la selezione del testo in un visualizzatore. Se è necessario individuare quale carattere si trovi sotto il puntatore del mouse, il metodo CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) esegue la ricerca direttamente, evitando di dover scorrere tutti i rettangoli

Configurazione delle DLL nel sistema

Il Componente PDFium delega l'intera analisi dei PDF a una DLL nativa, pdfium32.dll o pdfium64.dll a seconda della piattaforma di destinazione. Il componente include lo script CopyDlls.bat che copia il file appropriato nella directory di sistema di Windows. È sufficiente eseguirlo una volta come amministratore sul computer di sviluppo; per la distribuzione del software, la DLL deve essere posizionata nella stessa cartella dell'eseguibile dell'applicazione. Le varianti abilitate per V8 (pdfium32v8.dll, pdfium64v8.dll) hanno dimensioni notevolmente maggiori e sono necessarie solo se i PDF contengono script JavaScript da eseguire. Per la sola estrazione di testo, la build standard rappresenta la scelta corretta

Se la DLL non è presente al momento dell'esecuzione, l'attivazione di Active := True fallirà silenziosamente proprio come accade per un file mancante, poiché il componente intercetta internamente l'errore di caricamento della libreria. Si raccomanda di verificare sempre il comportamento su un sistema pulito prima della distribuzione

Utilizzo di FontSize[] insieme a Character[] per l'analisi del layout

Oltre al testo normale, l'API a livello di carattere espone FontSize[i], che restituisce la dimensione in punti di ciascun glifo visualizzato. Utilizzato in combinazione con CharacterOrigin[i] e CharacterRectangle[i], questo parametro consente di distinguere il testo principale dalle intestazioni senza dover fare affidamento sull'albero della struttura del documento. In un documento non strutturato, una sequenza di caratteri in cui la dimensione del carattere supera un certo valore rappresenta con ogni probabilità un'intestazione. La stessa logica si applica per individuare le didascalie (testo di piccole dimensioni posizionato al di sotto di un box immagine) o le note a piè di pagina (testo ridotto vicino al margine inferiore). Nessuna di queste operazioni richiede il rendering visivo: le tre proprietà leggono direttamente dal livello di testo che PDFium genera all'attivazione di Active := True

Un dettaglio importante: FontSize[i] riflette la dimensione dopo l'applicazione della matrice di trasformazione corrente (CTM) della pagina, per cui un documento in cui l'intera pagina è stata ridimensionata riporterà dimensioni adeguate proporzionalmente. Se si confrontano le dimensioni tra pagine con formati diversi, si consiglia di normalizzare i valori rispetto all'altezza di MediaBox di ciascuna pagina prima di definire i valori soglia di confronto

Scrittura dell'output su file

La classe TStringList di Delphi gestisce correttamente l'output UTF-8 a partire dalla versione XE. Imposta WriteBOM := False se è richiesto un file privo di marcatore BOM (molti sistemi di elaborazione successivi non supportano la presenza del BOM iniziale):

var
  Lines: TStringList;
begin
  Lines := TStringList.Create;
  try
    ExtractAllText(Pdf, Lines);
    Lines.WriteBOM := False;
    Lines.SaveToFile('output.txt', TEncoding.UTF8);
  finally
    Lines.Free;
  end;
end;

Per documenti di grandi dimensioni in cui l'uso della memoria è prioritario, si consiglia di scrivere i dati direttamente su un oggetto TStreamWriter configurando TEncoding.UTF8 all'interno del ciclo delle pagine, anziché accumulare preliminarmente l'intero testo in una lista in memoria

Le API Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent e CharacterIndexAtPos mostrate in questo articolo fanno parte del Componente PDFium per Delphi e C++Builder