Articolo tecnico

Estrazione di testo, immagini e font dai PDF con PDF Library for Delphi

Estrarre testo, immagini e font da un PDF esistente sembra un problema risolto finché non ci fai passare attraverso un corpus reale. Punta un indicizzatore di ricerca a quarantamila file cliente e i guasti si ordinano in poche pile riconoscibili. Le parole si attaccano perché nessuno ha detto all'estrattore quanto largo conta come spazio una separazione. Altre pagine tornano come incomprensibili perché un font parziale non porta una mappatura dai suoi codici glifo ai caratteri reali. E "il logo dell'azienda" risulta essere nove oggetti immagine separati impilati dietro una maschera morbida. Nessuna di queste cose è un bug nella libreria. È la differenza tra chiamare una funzione di estrazione e capire cosa la funzione può e non può recuperare dai byte su disco

losLab PDF Library, l'edizione Pascal, dà al codice Delphi e C++Builder più di un modo per leggere ciascuno di quei tre flussi, e i livelli differiscono in ciò che garantiscono. Il trucco è abbinare il livello al lavoro: un indice di ricerca, un revisore di redazione e un passaggio di preflight PDF/A vogliono tutti cose diverse dalla stessa pagina, e ricorrere alla chiamata sbagliata spreca sforzo o produce un output di cui non ti puoi fidare

Livelli di estrazione del testo e cosa ciascuno promette

GetPageText prende un valore di opzioni da 0 a 8, e quel numero sceglie un motore invece di un formato. I valori da 0 a 2 eseguono un passaggio leggero che va bene per un'anteprima rapida. I valori da 3 a 8 sono instradati attraverso il motore con consapevolezza di layout, che ricostruisce righe e spaziatura da dove i glifi effettivamente siedono sulla pagina. Entro quel range le variazioni contano: 4 e 6 dividono l'output in parole, 5 e 6 emettono larghezze per glifo, e 7 restituisce testo piano con font, colore e metadati di blocco deliberatamente scartati. L'opzione 7 è quella da alimentare a un indice di ricerca, poiché l'indice vuole parole e nient'altro

Nessuna impostazione di opzione può salvare un documento che non ha mai portato l'informazione per iniziare. Il PDF mappa codici carattere a forme glifo, e l'unica cosa che mappa quei codici di nuovo a testo leggibile è un ToUnicode CMap del font (ISO 32000-1 §9.10). Quando un font parziale viene fornito senza uno, ogni estrattore è bloccato. Questa libreria, il copia-incolla in un visualizzatore, un toolkit concorrente: tutti sono ridotti a indovinare dai nomi glifo o a non restituire nulla. La risposta pratica è il rilevamento, non l'eroismo. Punteggia la pagina come a bassa confidenza e mandala a OCR, perché indicizzare la spazzatura silenziosamente è peggio che ammettere che non la sai leggere

Diagramma dei livelli di estrazione testo PDF in Delphi: le opzioni GetPageText da 0 a 8 instradano a una passata leggera o al motore consapevole del layout, e le pagine i cui font subsetti mancano di una CMap ToUnicode ricadono su OCR
I valori di opzione di GetPageText da 0 a 8 scelgono tra un passaggio di anteprima leggero e il motore consapevole del layout, con l'opzione 7 riservata all'indicizzazione per la ricerca e i CMap ToUnicode mancanti deviati verso l'OCR

Per i casi che le opzioni piatte non coprono, tokenizzazione personalizzata, forensica del flusso di contenuto, un imbuto di testo costruito secondo le tue regole, il decodificatore è disponibile uno strato più sotto. TPDFExtractor è costruito sul dizionario delle risorse e sulla collezione di font di una pagina. Il suo metodo ExtractTextW fa passare di nuovo le operazioni di testo del flusso di contenuto grezzo attraverso la stessa macchina dei font per recuperare Unicode, e il suo evento OnFindObject ti consegna ciascun oggetto mentre scorre via. La maggior parte del codice non ha mai bisogno di arrivare così in profondità. Le applicazioni che lo fanno sono quelle contente che lo strato sia pubblico invece che seppellito

Blocchi posizionati: l'unità dei risultati di ricerca e della revisione di redazione

Il testo piano ti dice cosa dice la pagina. Presto o tardi un prodotto ha anche bisogno di sapere dove lo dice, così può evidenziare un risultato di ricerca, disegnare una scatola attorno a un candidato di redazione o ancorare un'annotazione al punto giusto. ExtractPageTextBlocks restituisce un handle a una lista di run di testo, e ciascun run porta il suo testo, il suo riquadro di delimitazione, e il nome e la dimensione del font in cui era impostato:

var
  Pdf: TPDFlib;
  Blocks, I: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('contract.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Pdf.SelectPage(1);
    Blocks := Pdf.ExtractPageTextBlocks(0);
    for I := 0 to Pdf.GetTextBlockCount(Blocks) - 1 do
      Writeln(Format('%s  [%s %.1f pt at %.0f,%.0f]',
        [Pdf.GetTextBlockText(Blocks, I),
         Pdf.GetTextBlockFontName(Blocks, I),
         Pdf.GetTextBlockFontSize(Blocks, I),
         Pdf.GetTextBlockBound(Blocks, I, 0),
         Pdf.GetTextBlockBound(Blocks, I, 1)]));
    Pdf.ReleaseTextBlocks(Blocks);
  finally
    Pdf.Free;
  end;
end;

Un dettaglio in quest'area fa inciampare le integrazioni più di qualsiasi altro. SetTextExtractionArea, SetTextExtractionWordGap e SetTextExtractionOptions sono stato a livello di documento che persiste, non argomenti che passi per chiamata. Configura una restrizione di area per una caratteristica, diciamo leggere solo la banda di intestazione per classificare un documento, e essa tronca silenziosamente ogni estrazione che segue sullo stesso handle, inclusi i livelli GetPageText con consapevolezza di layout a cui ricorri più tardi. O resetti lo stato di estrazione tra compiti logici o dai a ciascun compito il suo handle di documento

La soglia del gap di parola è la leva per quella prima pila di fallimenti, le parole che si attaccano. SetTextExtractionWordGap dice al motore di layout quanto spazio orizzontale, misurato contro la spaziatura glifo propria della pagina, separa una parola dalla successiva. Una tabella densa vuole un gap più piccolo di una pagina di marketing con impostazione lasca, così una soglia sintonizzata per classe di documento batte una costante globale. Essa persiste sul documento come il resto dello stato di estrazione, quindi pianifica di impostarla deliberatamente invece di una volta e dimenticarla

Diagramma che mostra lo stato di estrazione PDF a livello documento in Delphi che persiste attraverso le chiamate su un handle fino al reset, il che impedisce il troncamento silenzioso delle estrazioni successive
Area di estrazione, word-gap e impostazioni delle opzioni persistono sull'handle del documento, così una regione delimitata per una funzione tronca silenziosamente ogni estrazione successiva finché lo stato non viene azzerato o l'handle sostituito

Immagini: flussi originali, non screenshot

Il modo sbagliato di ottenere immagini da un PDF è renderizzare la pagina e ritagliarla. Questo ricampiona i pixel, cuoce qualsiasi rotazione, e butta via qualsiasi cosa fosse l'originale. GetPageImageList invece enumera le risorse immagine effettive a cui la pagina fa riferimento, e ciascun elemento restituisce le sue proprietà e i suoi dati originali indisturbati:

var
  ImgList, I: Integer;
begin
  Pdf.SelectPage(1);
  ImgList := Pdf.GetPageImageList(0);
  for I := 0 to Pdf.GetImageListCount(ImgList) - 1 do
  begin
    Writeln(Pdf.GetImageListItemFormatDesc(ImgList, I, 0));
    Pdf.SaveImageListItemDataToFile(ImgList, I, 0,
      Format('page1-img%.2d.bin', [I]));
  end;
  Pdf.ReleaseImageList(ImgList);
end;

Controlla GetImageListItemFormatDesc prima di assumere qualsiasi cosa su un elemento, perché ciò che una pagina referenzia è raramente una foto ordinata per immagine visibile. Una maschera morbida appare come una sua voce separata. Lo stesso XObject spesso si ripete attraverso molte pagine, quindi deduplica per hash di contenuto prima di archiviare un'esportazione "tutte le immagini", o scriverai lo stesso logo cento volte. I JPEG CMYK necessitano di gestione del colore applicata a valle, o si renderizzano invertiti in visualizzatori che prendono i canali al valore nominale. Quando vuoi un inventario a livello di documento invece di una pagina alla volta, FindImages insieme a SetFindImagesMode scansiona l'intero file in un passaggio

C'è un confine che vale la pena sollevare con gli stakeholder prima che qualcuno scriva criteri di accettazione: l'estrazione delle immagini restituisce solo risorse raster. Un logo o grafico disegnato come tracciati vettoriali non è un'immagine nel senso di risorsa e non apparirà mai in nessuna lista di immagini, per quanto chiaramente si legga come un'immagine sullo schermo. Quando il requisito è davvero consegnare quel grafico come file, l'approccio onesto è renderizzare la regione della pagina a una bitmap, che è un'operazione diversa con fedeltà diversa. I due tipi di output non appartengono alla stessa cartella di esportazione senza un'etichetta che dice quale è quale

Confronto tra renderizzare una pagina PDF Delphi per afferrare immagini ed estrarre gli stream immagine originali con GetPageImageList, incluse le avvertenze su soft mask, XObject duplicati e CMYK
Renderizzare e ritagliare ricampiona i pixel e scarta i dati immagine originali, mentre GetPageImageList enumera le risorse immagine memorizzate con le loro proprietà e gli stream intatti

Font: una superficie di audit, non una caratteristica di esportazione

L'API dei font risponde a domande sui font. Non ti consegna i file dei font stessi, e quella distinzione modella tutto ciò che puoi costruirvi sopra. Dopo che FindFonts ha scansionato il documento, l'enumerazione percorre i font per ID, e le chiamate di proprietà riferiscono sul font attualmente selezionato:

var
  I: Integer;
begin
  Pdf.FindFonts;
  for I := 1 to Pdf.FontCount do        // gli indici dei font iniziano da 1, non da 0
    if Pdf.SelectFont(Pdf.GetFontID(I)) = 1 then
      Writeln(Format('%s  type=%d  embedded=%d  subset=%d',
        [Pdf.FontName, Pdf.FontType,
         Pdf.GetFontIsEmbedded, Pdf.GetFontIsSubsetted]));
end;

Controlla i limiti del ciclo. Gli indici dei font vanno da 1 a FontCount, mentre gli indici di blocco di testo e lista di immagini di qualche paragrafo sopra sono a base zero. Porta una convenzione nell'altra e ottieni un off-by-one che o salta il primo font o corre oltre la fine, e passerà test casuali perché la maggior parte dei documenti ha diversi font e quello sbagliato sembra ancora plausibile. Sii chiaro anche sulla portata. Questa API non ha esportazione a livello di byte. Nessuna chiamata restituisce il programma del font incorporato come un file TTF o OTF, e l'enumerazione più l'ispezione dei metadati è l'intero modello inteso. Quel modello copre comunque ciò che il lavoro di produzione chiede davvero ai font: rilevamento di parzializzazione per modello di nome, audit di incorporamento prima di una conversione d'archivio (un font non incorporato è un bloccante duro del PDF/A, come approfondisce preflight PDF/A e PDF/UA in Delphi), e diagnostica di codifica per quando la confidenza di estrazione cala. C'è anche una ragione di licenza per cui il confine siede qui. Un programma di font parziale è materiale concesso in licenza e, mancando la maggior parte dei suoi glifi, inutile come font installabile in ogni caso. Trattarlo come metadati di audit invece che come un asset estraibile è la posizione che puoi difendere

Quell'ultima chiamata tira il suo peso nel triage. Esegui GetFontEncoding su ciascun font, leggilo insieme alla bandiera di parzializzazione, e puoi predire la qualità di estrazione prima di pescare un singolo carattere. Una pagina i cui font sono tutti parzializzati con codifiche non standard è una candidata OCR sola con l'ispezione, che lascia che una pipeline a batch la instradi correttamente senza prima sprecare un passaggio di estrazione fallito su di essa

Estrazione su scala senza caricare documenti

In una pipeline a batch, caricare un intero documento solo per leggere una pagina è I/O sprecato, e si somma in fretta lungo un corpus. Le varianti a chiamata singola, ExtractFilePageText e ExtractFilePageTextBlocks, prendono un nome file, una password e un numero di pagina direttamente e saltano il carico completo. Per file su scala di gigabyte c'è una marcia ancora più bassa. Il percorso di accesso diretto apre un file attraverso letture xref in flusso, quindi DAOpenFileReadOnly seguito da DAExtractPageText tocca solo gli oggetti che quella pagina ha effettivamente bisogno. Viene con uno spostamento di convenzione che vale la pena memorizzare: le funzioni DA indirizzano le pagine per PageRef, un handle di riferimento a oggetto che ottieni da DAFindPage, mai per numero di pagina grezzo. Passa il numero dove va l'handle e la chiamata opera sull'oggetto sbagliato senza sollevare un errore, che è il peggior tipo di errore da debuggare. Il resto del toolkit di accesso diretto è esposto in fusione, divisione e accesso diretto di PDF grandi

Se c'è un'unica abitudine che separa il codice di estrazione che sopravvive a un corpus reale dal codice che zoppica, è trattare la pagina come input non fidato invece di una sorgente di dati pulita. Il testo che è in disaccordo con ciò che il visualizzatore renderizza è quasi sempre un problema di codifica, una legatura che collassa a un glifo o un font parziale a cui mancano le sue voci ToUnicode, e la correzione è misurare la confidenza e deviare le pagine cattive a OCR, non combattere i byte. L'API dei font non produrrà mai un TTF o un OTF, per scelta, quindi costruisci i flussi di lavoro dei font attorno a domande di audit. E lo stato di estrazione persistente, il rettangolo di area soprattutto, è un'impostazione che possiedi per la vita di un handle di documento, non un parametro che dimentichi dopo una chiamata. Fai bene quei tre riflessi e il resto dell'API si comporta

Le build di valutazione, i progetti demo e il riferimento completo dell'API di estrazione sono sulla pagina del prodotto losLab PDF Library per Delphi