Articol tehnic

Extragerea textului dintr-un PDF încărcat în Delphi cu HotPDF

HotPDF Component extrage text Unicode din orice PDF pe care îl încărcați în Delphi prin intermediul a două apeluri: ExtractLoadedPageText returnează textul în ordinea fluxului de citire a paginii, iar ExtractLoadedPageTextLayout (adăugat în v2.263.0) reconstruiește aranjamentul vizual al paginii sub formă de text simplu, astfel încât coloanele, indentarea și alinierea tabelelor sunt păstrate în rezultatul final. Ambele funcționează pe documentele pe care HotPDF nu le-a creat, acesta fiind cazul cel mai important: factura trimisă prin e-mail de un client, raportul livrat de un birou de scanare sau contractul generat de un program pe care nimeni nu îl mai poate identifica

Realizarea acestui lucru a necesitat mai multe mecanisme decât sugerează cele două semnături, deoarece un PDF nu stochează textul în modul în care o face un fișier text. Acest articol prezintă ambele moduri de extragere, apoi explică cele trei componente de bază — cititorul CMap, interpretul fluxului de conținut și lanțul de alternative pentru decodarea fonturilor — deoarece înțelegerea modului în care funcționează maparea face diferența între a ignora un rezultat neinteligibil și a-l diagnostica

De ce este extragerea textului mai dificilă decât simpla citire a șirurilor din fișier?

Un flux de conținut PDF înregistrează coduri de caractere, nu caractere. Operatorii Tj și TJ (ISO 32000-1 §9.4.3) conțin șiruri de octeți a căror semnificație depinde în întregime de fontul selectat de operatorul Tf precedent: octetul 0x41 ar putea fi litera A conform WinAnsi, o glifă arbitrară într-un font subsetat sau jumătatea unui CID de doi octeți într-un font compus CJK. ISO 32000-1 §9.10 definește extragerea textului exact ca această problemă de decodare — maparea fiecărui cod înapoi la Unicode utilizând orice informație oferită de dicționarul de fonturi — iar standardul precizează explicit că un fișier conform nu are obligația de a oferi suficiente informații pentru a face acest lucru

Această ultimă clauză explică fiecare raport de eroare de tipul „de ce copierea și lipirea din acest PDF produce caractere neinteligibile” pe care l-ați întâlnit vreodată. Un program de generare care încorporează un font subsetat fără tabelul /ToUnicode a scris un fișier care se afișează perfect, dar se extrage ca un text lipsit de sens, deoarece maparea de la cod la glifă există, dar maparea de la cod la Unicode nu a fost furnizată niciodată. Prin urmare, orice API de extragere corect este un lanț de alternative bazat pe cel mai bun efort, iar întrebarea utilă este cât de profund este acest lanț

Extragerea fluxului de citire cu ExtractLoadedPageText

Pentru indexarea căutării, potrivirea cuvintelor cheie sau trimiterea textului către o linie de analiză, apelul potrivit este ExtractLoadedPageText. Semnătura este function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — indecșii paginilor pornesc de la zero, rezultatul este returnat ca un tip nativ Delphi UnicodeString, iar funcția returnează False atunci când pagina nu are un flux de conținut lizibil, în loc să genereze o excepție

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;

Salturile de rând în rezultatul obținut provin dintr-o euristică deliberat simplă: când originea verticală a unei glife se deplasează cu mai mult de jumătate din dimensiunea fontului curent — semnalul unui pas Td sau T* în fluxul de conținut — este inserat un rând nou. Caracterele pe care decodorul nu le poate rezolva devin spații în loc să dispară, astfel încât limitele cuvintelor sunt păstrate chiar și atunci când unele glife lipsesc. Ceea ce nu încearcă acest mod este gruparea în ordinea de citire sau detectarea coloanelor multiple: o pagină cu două coloane este extrasă intercalat în ordinea fluxului de conținut, care corespunde de obicei, dar nu întotdeauna, cu ordinea vizuală

Când ar trebui să folosiți extragerea cu păstrarea structurii?

ExtractLoadedPageTextLayout este apelul corect ori de câte ori poziția are o anumită semnificație: tabele, formulare, liste de coduri, orice doriți să comparați (diff), să căutați (grep) sau să analizați pe coloane. În loc să aplatizeze glifele într-un flux, le grupează în linii de bază, sortează fiecare linie după coordonata X și reproduce spațiul gol orizontal și vertical pe o grilă de caractere monospațiate, dimensionată pe baza avansului mediu al glifelor și a dimensiunii fontului. Spațiile mari dintre secțiunile de pe aceeași linie devin serii de spații, iar distanțele mari dintre liniile de bază devin rânduri goale. Rezultatul se citește exact așa cum arată pagina vizual

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;

Cele două moduri partajează fiecare octet din mecanismul de decodare și diferă doar prin modul în care aranjează glifele decodificate, astfel încât alegerea nu costă nimic în termeni de fidelitate. Alegeți ExtractLoadedPageText când contează doar cuvintele și ExtractLoadedPageTextLayout când contează aranjamentul. Detectarea ordinii de citire pe mai multe coloane rămâne în afara scopului ambelor moduri — o redare în grilă a unei pagini cu două coloane vă arată ambele coloane una lângă alta, cu fidelitate, ceea ce pentru comparare (diff) este exact ceea ce trebuie, dar nu și pentru rearanjarea fluxului de text ca proză

Cum decodifică HotPDF codurile de caractere în Unicode?

HotPDF Component rezolvă fiecare cod de caracter printr-un lanț de alternative ordonat după prioritate: mai întâi CMap-ul /ToUnicode încorporat în font, apoi intrarea /Encoding (flux sau CMap numit), apoi — pentru fonturile compuse — fișierele CMap standard Adobe pentru colecții de caractere precum Adobe-GB1, Adobe-CNS1, Adobe-Japan1 și Adobe-KR, și în final tabelele încorporate WinAnsi și MacRoman pentru fonturile simple. O strategie care nu poate oferi un răspuns trece în mod silențios la următoarea în loc să genereze o excepție, iar un cod care epuizează întregul lanț se rezolvă la valoarea 0, astfel încât apelantul să poată contoriza omisiunile în loc să ghicească

CMap-ul /ToUnicode (ISO 32000-1 §9.10.3) se află pe primul loc deoarece reprezintă maparea pe care programul de generare a scris-o special pentru extragere. Calea CMap standard Adobe contează pentru documentele CJK care utilizează CMap-uri predefinite precum UniGB-UTF16-H în loc să încorporeze ceva: HotPDF livrează fișierele colecției în directorul său resources\CMap directory, le localizează în raport cu fișierul executabil la rulare și stochează în cache fiecare hartă analizată per proces — un aspect util de știut deoarece cea mai mare dintre ele, harta Adobe-GB1, are aproximativ 2 MB de text sursă pe care nu doriți să îl reanalizați pentru fiecare pagină. Dacă directorul lipsește, decodorul omite pur și simplu CMap-urile stocate pe disc și funcționează cu tabelele încorporate alături de codificările integrate. Aceasta este oglinda pe partea de citire a problemei de modelare (shaping) acoperite în modelarea textului cu scripturi complexe în HotPDF, unde aceeași distincție între cod și glifă este întâlnită la scriere

Două capcane ale sintaxei CMap care merită știute

Fișierele CMap par ușor de analizat sintactic, dar nu sunt, iar două detalii explică majoritatea eșecurilor la prima încercare de scriere a unui parser. Primul este că numărul de înregistrări vine înainte de cuvântul cheie al secțiunii: o secțiune se citește 2 beginbfchar, nu beginbfchar 2. Un parser care se așteaptă la număr după cuvântul cheie consumă numărul ca un token rătăcit, apoi găsește zero intrări în fiecare secțiune. Abordarea robustă — cea aleasă de cititorul HotPDF — constă în ignorarea completă a numărului și rularea unei bucle până la întâlnirea cuvântului cheie corespunzător endbfchar / endbfrange, ceea ce are avantajul de a tolera fișierele reale ale căror numere sunt pur și simplu greșite

A doua capcană este că destinațiile bfchar și bfrange sunt șiruri UTF-16BE, nu numere întregi. Destinația <D83DDE00> înseamnă U+1F600 — o pereche de surogate care trebuie recombinată într-un singur punct de cod — iar citirea acelor patru octeți ca un număr întreg big-endian produce o valoare lipsită de sens pentru orice punct de cod din afara Planului Multilingv de Bază. Caracterele emoji din PDF-uri nu mai sunt o raritate, așa că un decodor care omite recombinarea surogatelor va eșua pe fișierele pe care utilizatorii dvs. le folosesc de fapt. HotPDF analizează mai întâi literalul hexazecimal în octeți bruts, apoi recombină unitățile de cod UTF-16BE, ceea ce acoperă și destinațiile cu mai multe caractere produse de mapările de ligaturi

Accesarea nivelului de glifă cu ExtractLoadedPageGlyphs

Ambele apeluri de text sunt construite pe ExtractLoadedPageGlyphs, iar tipul de date subiacent THPDFGlyphArray este de asemenea disponibil pentru codul dvs. Fiecare THPDFGlyphRecord conține punctul de cod Unicode rezolvat alături de codul de caracter brut, lățimea în octeți a codului (1, 2 sau 4, stabilită de codespacerange al CMap-ului), cheia și dimensiunea resursei de font active, originea X și Y în spațiul utilizatorului și avansul orizontal. Acest lucru este suficient pentru a construi detectarea limitelor cuvintelor, evidențierea poziționată sau un algoritm de structurare personalizat, fără a atinge manual fluxul de conținut

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;

Contorizarea înregistrărilor Unicode = 0, ca mai sus, reprezintă modul corect de a măsura calitatea extragerii pe un document dat, înainte de a utiliza textul în aval. Înregistrările de glife ancorează de asemenea fiecare caracter de operandul sursă din fluxul de conținut, lucru care face posibilă căutarea și înlocuirea textului în documentul încărcat din HotPDF pe aceeași fundație

Care PDF-uri nu își vor dezvălui textul?

Unele fișiere împiedică funcționarea oricărui extractor, iar detectarea lor este de preferat în locul livrării unui rezultat greșit. Documentele scanate reprezintă cel mai clar caz: o pagină care este o singură imagine mare nu conține deloc operatori de text, așa că extragerea returnează corect un șir gol — remedierea constă în OCR, iar extragerea imaginilor paginii din PDF-ul încărcat este primul pas al acestei linii de procesare. Fonturile subsetate fără tabelul /ToUnicode reprezintă cazul mai dificil: dacă nici calea /Encoding și nici CMap-urile standard nu oferă rezultate, acele glife se rezolvă la 0 și apar ca spații în apelurile de text. Documentele criptate se extrag normal dacă le încărcați cu parola corespunzătoare prin intermediul supraîncărcării LoadFromFile, astfel încât fluxurile să fie decriptate înainte ca interpretul să le proceseze

O limită mai strânsă merită menționată clar: lanțul de decodare citește fluxurile CMap și de conținut prin intermediul căii Flate a HotPDF, astfel încât un font al cărui flux ToUnicode utilizează un filtru neobișnuit trece la următoarea strategie în loc să blocheze analiza paginii. În practică, FlateDecode acoperă aproape tot ceea ce s-a produs în ultimele două decenii, iar această degradare este silențioasă prin proiectare — obțineți cel mai bun text pe care îl permite fișierul, în loc de o excepție. Același mecanism de obiecte de citire care rezolvă dicționarele de fonturi aici asigură de asemenea editarea metadatelor în documentele încărcate, permițând unei linii de procesare a documentelor să extragă, să verifice și să adnoteze într-o singură etapă

Extragerea textului, afișarea cu păstrarea structurii, accesul la nivel de glifă și caracteristicile de căutare și înlocuire construite pe baza acestora fac parte din pachetul standard HotPDF Component pentru Delphi și C++Builder — fără DLL-uri externe, fără servicii de text ale sistemului de operare, doar cod Object Pascal prin care puteți naviga pas cu pas când un fișier neobișnuit ajunge în procesare