Tehnični članak

Ekstrakcija besedila iz naloženega PDF-ja v Delphiju s HotPDF

HotPDF Component ekstrahira besedilo Unicode iz katerega koli naloženega PDF-ja v Delphiju prek dveh klicov: ExtractLoadedPageText vrne besedilo strani v vrstnem redu branja, ExtractLoadedPageTextLayout (dodano v v2.263.0) pa rekonstruira vizualno postavitev strani som navadno besedilo, tako da se v izhodu ohranijo stolpci, zamiki in poravnave tabel. Oba klica delujeta na dokumentih, ki jih HotPDF ni ustvaril, kar je pravzaprav tisto, kar šteje: račun, ki vam ga je stranka poslala po e-pošti, poročilo skenirnega urada ali pogodba, ki jo je ustvarila programska oprema, katere nihče več ne pozna po imenu

Doseganje tega je zahtevalo več mehanizmov, kot nakazujeta oba klica, saj PDF ne shranjuje besedila na enak način kot tekstovna datoteka. Ta članek obravnava oba načina ekstrakcije, nato pa odpira pokrov nad tremi deli pod njimi — bralnikom CMap, interpreterjem toka vsebine in verigo nadomestnega dekodiranja pisav — saj je razumevanje tega delovanja ključno za razlikovanje med slepim sprejemanjem popačenega izhoda in njegovo diagnostiko

Zakaj je ekstrakcija besedila težja od branja nizov iz datoteke?

Tok vsebine PDF beleži kode znakov in ne samih znakov. Operatorja Tj in TJ (ISO 32000-1 §9.4.3) prenašata nize bajtov, katerih pomen je v celoti odvisen od pisave, izbrane s predhodnim operatorjem Tf: bajt 0x41 je lahko črka A v kodiranju WinAnsi, poljuben glif v podnaboru pisave ali polovica dvobajtnega CID-ja v sestavljeni pisavi CJK. ISO 32000-1 §9.10 opredeljuje ekstrakcijo besedila natanko kot ta problem dekodiranja — preslikavo vsake kode nazaj v Unicode z uporabo vseh informacij, ki jih ponuja slovar pisave — standard pa izrecno navaja, da za skladno datoteko ni zahtevano, da zagotavlja dovolj informacij za to

Zadnji stavek pojasnjuje vsako poročilo o napaki tipa "zakaj kopiranje in lepljenje iz tega PDF-ja ustvari popačeno besedilo", kar ste kdaj videli. Ustvarjalec, ki vgradi podnabor pisave brez tabele /ToUnicode, zapiše datoteko, ki se upodobi popolnoma, ekstrahira pa se kot nesmisel, ker preslikava iz kode v glif obstaja, preslikava iz kode v Unicode pa nikoli ni bila poslana. Vsaj ena pisava ni vgrajena. Če gradite cevovode za podpisovanje, spremljevalni članek o podpisovanju in validaciji PAdES v Delphiju podrobno opisuje, kako medsebojno delujejo bajtni obsegi podpisov in inkrementalni deli

Ekstrakcija po vrstnem redu branja z ExtractLoadedPageText

Za indeksiranje iskanja, iskanje ključnih besed ali pošiljanje besedila v cevovod za analizo je klic ExtractLoadedPageText tisti, ki ga želite. Podpis je function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — indeksi strani so ničelni, rezultat pride kot izvorni delphi UnicodeString, funkcija pa vrne False, ko stran nima čitljivega toka vsebine, namesto da bi sprožila izjemo

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;

Prelomi vrstic v izhodu izhajajo iz namerno preproste hevristike: ko se navpično izhodišče glifa premakne za več kot polovico trenutne velikosti pisave — kar je podpis koraka Td ali T* v toku vsebine —, se vstavi nova vrstica. Znaki, ki jih dekoder ne more razrešiti, postanejo presledki, namesto da bi izginili, tako da se meje besed ohranijo, tudi če posamezni glifi ne. Ta način ne poskuša zaznati vrstnega reda branja ali več stolpcev: stran z dvema stolpcema se izpiše prepleteno v vrstnem redu toka vsebine, kar je običajno, a ne vedno, enako vizualnemu vrstnemu redu

Kdaj morate namesto tega uporabiti ekstrakcijo z ohranjanjem postavitve?

Klic ExtractLoadedPageTextLayout je prava izbira, kadar položaj nosi pomen: tabele, obrazci, izpisi kode ali karkoli, kar nameravate primerjati (diff), iskati (grep) ali analizirati po stolpcih. Namesto sploščenja glifov v tok jih združi v osnovne vrstice, razvrsti vsako osnovno vrstico po osi X in poustvari vodoravni in navpični prazen prostor na mreži znakov s fiksno širino, ki je določena na podlagi mediane širine glifa in velikosti pisave. Široke vrzel med deli na isti osnovni vrstici postanejo zaporedja presledkov; velike vrzel med osnovnimi vrsticami postanejo prazne vrstice. Rezultat se bere tako, kot je stran videti

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;

Oba načina si delita vsak bajt dekodirnega mehanizma in se razlikujeja le v tem, kako razporedita dekodirane glife, tako da izbira ne vpliva na zanesljivost. Izberite ExtractLoadedPageText, ko so pomembne le besede, in ExtractLoadedPageTextLayout, ko je pomembna postavitev. Zaznavanje večstolpčnega vrstnega reda branja ostaja izven obsega obeh — mrežno upodabljanje dvostolpčne strani zvesto prikazuje oba stolpca drug ob drugem, kar je za primerjavo (diff) natanko tisto pravo, za prelivanje proze pa ne

Kako HotPDF dekodira kode znakov v Unicode?

HotPDF Component razrešuje vsako kodo znaka prek prednostne verige nadomestnih možnosti: najprej se uporabi vgrajena CMap tabela pisave /ToUnicode, nato vnos /Encoding (tok ali poimenovana CMap), nato — za sestavljene pisave — standardne datoteke Adobe CMap za zbirke znakov, kot so Adobe-GB1, Adobe-CNS1, Adobe-Japan1 in Adobe-KR, in na koncu vgrajeni tabeli WinAnsi in MacRoman za preproste pisave. Strategija, ki ne more prinesti odgovora, se tiho preusmeri na naslednjo možnost, namesto da bi sprožila izjemo, koda, ki izčrpa celotno verigo, pa se razreši v 0, tako da lahko klicatelj prešteje zgrešene znake, namesto da ugiba

Tabela /ToUnicode CMap (ISO 32000-1 §9.10.3) je na prvem mestu, ker je to preslikava, ki jo je ustvarjalec napisal posebej za ekstrakcijo. Pot do standardnih Adobe CMap datotek je pomembna za dokumente CJK, ki uporabljajo vnaprej določene CMape, kot je UniGB-UTF16-H, namesto da bi karkoli vgrajevali: HotPDF hrani te zbirke v mapi resources\CMap, jih ob izvajanju poišče glede na izvedljivo datoteko in vsak analiziran zemljevid predpomni na ravni procesa — to je vredno vedeti, saj je največji med njimi, Adobe-GB1, velik približno 2 MB izvornega besedila, ki ga ne želite ponovno analizirati za vsako stran. Če mape ni, dekoder preprosto preskoči CMape na disku in dela z vgrajenimi tabelami ter vgrajenimi kodiranji. To je zrcalo na strani branja za problem oblikovanja, obravnavan v oblikovanju besedila v kompleksnih pisavah s HotPDF, kjer se z enako razliko med kodo in glifom srečujemo ob zapisovanju

Dve zanki v sintaksi CMap, ki ju je vredno poznati

Datoteke CMap so videti preproste za analizo, a niso, in dve podrobnosti sta razlog za večino neuspešnih poskusov pisanja analizatorjev. Prva je ta, da število zapisov pride pred ključno besedo razdelka: razdelek se glasi 2 beginbfchar in ne beginbfchar 2. Analizator, ki pričakuje število po ključni besedi, porabi številko kot odvečen žeton, nato pa v vsakem razdelku najde nič vnosov. Robusten pristop — tisti, ki ga uporablja bralnik HotPDF — je, da se število popolnoma prezre in se izvaja zanka do ujemajoče se ključne besede endbfchar / endbfrange, kar ima dodatno prednost, da tolerira realne datoteke, katerih štetje je preprosto napačno

Druga zanka je da so cilji bfchar in bfrange nizi UTF-16BE in ne cela števila. Cilj <D83DDE00> pomeni U+1F600 — par nadomestnih znakov (surrogate pair), ki ju je treba združiti v eno kodno točko — branje teh štirih bajtov kot celotnega števila z večjimi bajti na začetku (big-endian) pa ustvari nesmiselno vrednost za vsako kodno točko izven osnovne večjezične ravnine (Basic Multilingual Plane). Emotikoni v PDF-jih niso več eksotični, zato dekoder, ki preskoči združevanje nadomestnih znakov, ne uspe na datotekah, ki jih vaši uporabniki dejansko imajo. HotPDF najprej analizira heksadecimalni dobesedni niz v surove bajte, nato pa združi kodne enote UTF-16BE, kar pokriva tudi večznakovne cilje, ki jih ustvarijo preslikave ligatur

Spuščanje na raven glifov z ExtractLoadedPageGlyphs

Oba klica za besedilo sta zgrajena na ExtractLoadedPageGlyphs, pripadajoča matrika THPDFGlyphArray pa je na voljo tudi vaši kodi. Vsak THPDFGlyphRecord nosi razrešeno Unicode kodno točko poleg surove kode znaka, bajtne širine kode (1, 2 ali 4, ki jo določa codespacerange v CMap-u), ključa in velikosti aktivnega vira pisave, X in Y izhodišča v uporabniškem prostoru ter vodoravnega napredovanja. To je dovolj za izgradnjo zaznavanja meja besed, pozicioniranega označevanja ali lastnega algoritma postavitve, ne da bi se sami dotaknili toka vsebine

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;

Štetje zapisov z Unicode = 0, kot je prikazano zgoraj, je pošten način za merjenje kakovosti ekstrakcije na danem dokumentu, preden besedilu zaupate v nadaljnjih korakih. Zapisi glifov prav tako sidrajo vsak znak na izvorni operand v toku vsebine, kar omogoča iskanje in zamenjavo besedila v naloženem dokumentu HotPDF na vrhu istih temeljev

Kateri PDF-ji ne bodo dali svojega besedila?

Nekatere datoteke premagajo vsak ekstraktor in bolje jih je pravočasno zaznati kot pa poslati njihov izhod naprej. Skenirani dokumenti so najpreprostejši primer: stran, ki je ena sama velika slika, sploh ne vsebuje operaterjev besedila, zato ekstrakcija pravilno vrne prazen niz — rešitev je OCR, ekstrakcija slik strani iz naloženega PDF-ja pa je prvi korak tega cevovoda. Podnaborne pisave brez tabele /ToUnicode so težji primer: če pot /Encoding in standardni CMap-i prav tako ostanejo prazni, se ti glifi razrešijo v 0 in se v klicih besedila prikažejo kot presledki. Šifrirani dokumenti se ekstrahirajo normalno, če jih naložite z njihovim geslom prek preobremenjene metode LoadFromFile, tako da se tokovi dešifrirajo, preden jih interpreter sploh vidi

Eno ožjo omejitev si zasluži jasna navedba: dekodirna veriga bere tokove CMap in vsebine prek Flate poti HotPDF-ja, zato se pisava, katere tok ToUnicode uporablja nenavaden filter, preusmeri na naslednjo strategijo, namesto da bi stran odpovedala. V praksi FlateDecode pokriva skoraj vse, kar je bilo ustvarjeno v zadnjih dveh desetletjih, preusmeritev pa je namerna — dobite najboljše besedilo, ki ga datoteka omogoča, namesto izjeme. Enaki mehanizmi branja objektov, ki tukaj razrešujejo slovarje pisav, napajajo tudi urejanje metapodatkov na naloženih dokumentih, tako da lahko cevovod za sprejem dokumentov ekstrahira, pregleda in označi v enem koraku

Ekstrakcija besedila, upodabljanje z ohranjanjem postavitve, dostop na ravni glifov ter iskanje in zamenjava, zgrajeni na njih, so del standardne komponente HotPDF Component za Delphi in C++Builder — brez zunanjih knjižnic DLL, brez sistemskih storitev za besedilo, le Object Pascal, skozi katerega lahko stopate, ko v vaši vrsti pristane nenavadna datoteka