Tehnički članak

Izdvajanje teksta iz učitanog PDF-a u Delphiju pomoću HotPDF-a

HotPDF Component izdvaja Unicode tekst iz bilo kojeg PDF-a koji učitate u Delphiju putem dva poziva: ExtractLoadedPageText vraća tekst stranice prema toku čitanja, a ExtractLoadedPageTextLayout (dodan u verziji v2.263.0) rekonstruira vizualni raspored stranice kao običan tekst, tako da stupci, uvlačenje i poravnanje tablica preživljavaju u izlazu. Oba rade na dokumentima koje HotPDF nije stvorio, što je slučaj koji je zapravo važan: račun koji vam je poslao kupac e-poštom, izvješće koje je dostavio ured za skeniranje, ugovor koji je generirao softver kojemu više nitko ne zna ime

Dolazak do toga zahtijevao je više mehanizama nego što to sugeriraju dva potpisa, jer PDF ne pohranjuje tekst na način na koji to čini tekstualna datoteka. Ovaj članak prolazi kroz oba načina izdvajanja, a zatim otvara poklopac na tri komponente ispod — čitač CMap-a, interpretator toka sadržaja i lanac zamjenskih rješenja (fallback chain) za dekodiranje fontova — jer poznavanje načina na koji mapiranje funkcionira čini razliku između slijeganja ramenima na neupotrebljiv izlaz i dijagnosticiranja istog

Zašto je izdvajanje teksta teže od čitanja nizova znakova iz datoteke?

Tok sadržaja PDF-a (content stream) bilježi kodove znakova, a ne same znakove. Operatori Tj i TJ (ISO 32000-1 §9.4.3) prenose nizove bajtova čije značenje u potpunosti ovisi o fontu odabranom prethodnim operatorom Tf: bajt 0x41 može biti slovo A pod WinAnsi kodiranjem, proizvoljan glif u podskupu fonta ili polovica dvobajtnog CID-a u kompozitnom CJK fontu. ISO 32000-1 §9.10 definira izdvajanje teksta upravo kao ovaj problem dekodiranja — mapiranje svakog koda natrag u Unicode koristeći bilo koje informacije koje pruža rječnik fonta — a standard izričito navodi da sukladna datoteka ne mora nužno sadržavati dovoljno informacija za to

Ta posljednja klauzula objašnjava svako izvješće o pogrešci tipa "zašto kopiranje i lijepljenje iz ovog PDF-a proizvodi besmislice" koje ste ikada vidjeli. Proizvođač koji ugrađuje podskup fonta bez tablice /ToUnicode napisao je datoteku koja se renderira savršeno, ali se izdvaja kao besmislica, jer mapiranje koda u glif postoji, ali mapiranje koda u Unicode nikada nije isporučeno. Stoga je svaki pošten API za izdvajanje lanac zamjenskih rješenja (fallbacks) koji se trudi učiniti najbolje što može, a korisno pitanje je koliko duboko taj lanac ide

Izdvajanje prema toku čitanja s ExtractLoadedPageText

Za indeksiranje pretraživanja, podudaranje ključnih riječi ili slanje teksta u cjevovod za analizu, ExtractLoadedPageText je poziv koji želite. Potpis je function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — indeksi stranica počinju od nule, rezultat stiže kao izvorni Delphi UnicodeString, a funkcija vraća False kada stranica nema čitljiv tok sadržaja, radije nego da podiže iznimku

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;

Prijelomi redaka u izlazu dolaze iz namjerno jednostavne heuristike: kada se vertikalno podrijetlo glifa pomakne za više od polovice trenutne veličine fonta — što je potpis koraka Td ili T* u toku sadržaja — umeće se novi redak. Znakovi koje dekoder ne može razriješiti postaju razmaci umjesto da nestanu, pa granice riječi preživljavaju čak i kada pojedinačni glifovi ne prežive. Ono što ovaj način ne pokušava jest grupiranje prema redoslijedu čitanja ili otkrivanje više stupaca: stranica s dva stupca izlazi isprepletena redoslijedom toka sadržaja, što je obično, ali ne uvijek, vizualni redoslijed

Kada biste umjesto toga trebali koristiti izdvajanje s očuvanjem izgleda?

ExtractLoadedPageTextLayout je pravi poziv kad god pozicija nosi značenje: tablice, obrasci, popisi koda, sve što namjeravate uspoređivati (diff), pretraživati (grep) ili analizirati po stupcima. Umjesto da spljošti glifove u tok, on ih grupiru u osnovne linije (baselines), sorts svaku osnovnu liniju po X koordinati i reproducira vodoravne i okomite razmake na mreži znakova nepromjenjive širine (monospaced) čija je veličina određena srednjim pomakom glifa i veličinom fonta. Široki razmaci između nizova na istoj osnovnoj liniji postaju nizovi razmaka; veliki razmaci između osnovnih linija postaju prazni redovi. Rezultat se čita onako kako stranica izgleda

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;

Dva načina dijele svaki bajt mehanizma za dekodiranje i razlikuju se samo u načinu na koji raspoređuju dekodirane glifove, tako da izbor ne košta ništa u vjernosti prikaza. Odaberite ExtractLoadedPageText kada su važne samo riječi, a ExtractLoadedPageTextLayout kada je važan raspored. Otkrivanje redoslijeda čitanja s više stupaca ostaje izvan opsega za oba — prikaz mreže stranice s dva stupca prikazuje vam oba stupca jedan pored drugog, vjerno, što je za uspoređivanje (diff) točno, a za ponovni tok teksta nije

Kako HotPDF dekodira kodove znakova u Unicode?

HotPDF Component razrješava svaki kod znaka kroz prioritetno poredani lanac zamjenskih rješenja (fallback chain): najprije ugrađeni /ToUnicode CMap fonta, zatim unos /Encoding (tok ili imenovani CMap), potom — za kompozitne fontove — standardne Adobe CMap datoteke za zbirke znakova kao što su Adobe-GB1, Adobe-CNS1, Adobe-Japan1 i Adobe-KR, te na kraju ugrađene WinAnsi i MacRoman tablice za jednostavne fontove. Strategija koja ne može dati odgovor tiho se prebacuje na sljedeću umjesto da podiže iznimku, a kod koji iscrpi cijeli lanac razrješava se u 0 tako da pozivatelj može brojati promašaje umjesto da pogađa

CMap /ToUnicode (ISO 32000-1 §9.10.3) nalazi se na prvom mjestu jer je to mapiranje koje je proizvođač napisao posebno za izdvajanje. Put prema Adobe standardnim CMap datotekama važan je za CJK dokumente koji koriste unaprijed definirane CMap-ove poput UniGB-UTF16-H umjesto ugradnje bilo čega: HotPDF isporučuje zbirke datoteka pod svojim direktorijem resources\CMap, pronalazi ih u odnosu na izvršnu datoteku u vremenu izvršavanja (runtime) i predmemorira (caches) svaku analiziranu kartu po procesu — što je korisno znati jer je najveća od njih, Adobe-GB1 karta, otprilike 2 MB izvornog teksta koji ne želite ponovno analizirati po stranici. Ako direktorij nije prisutan, dekoder jednostavno preskače CMap-ove s diska i radi s ugrađenim tablicama te ugrađenim kodiranjima. Ovo je zrcalna slika sa strane čitanja za problem oblikovanja pokriven u oblikovanju teksta složenog pisma s HotPDF-om, gdje se s istom razlikom između koda i glifa susreće pri pisanju

Dvije zamke sintakse CMap-a koje vrijedi znati

Datoteke CMap izgledaju trivijalno za analiziranje, ali nisu, i dva detalja uzrokuju većinu neuspjeha analizatora (parser) pri prvom pokušaju. Prvi je da broj zapisa dolazi prije ključne riječi odjeljka: odjeljak glasi 2 beginbfchar, a ne beginbfchar 2. Analizator koji očekuje broj nakon ključne riječi konzumira broj kao zalutali token, a zatim pronalazi nula unosa u svakom odjeljku. Robustan pristup — onaj na kojem se zaustavio čitač u HotPDF-u — jest potpuno zanemarivanje broja i petlja dok se ne pronađe odgovarajuća ključna riječ endbfchar / endbfrange, što ima dodatnu prednost toleriranja stvarnih datoteka čiji su brojevi zapisa jednostavno pogrešni

Druga zamka je da su ciljevi bfchar i bfrange UTF-16BE nizovi znakova, a ne cijeli brojevi (integers). Odredište <D83DDE00> znači U+1F600 — surogatni par koji se mora ponovno spojiti u jednu kodnu točku — a čitanje ta kita bajta kao big-endian cijelog broja proizvodi besmislenu vrijednost za svaku kodnu točku izvan Osnovne višejezične ravnine (Basic Multilingual Plane). Emotikoni u PDF-ovima više nisu egzotični, so a dekoder koji preskače spajanje surogata zakazuje na datotekama koje vaši korisnici doista imaju. HotPDF prvo analizira heksadecimalni literal u sirove bajtove, a zatim ponovno spaja UTF-16BE kodne jedinice, što također pokriva višekarakterne ciljeve koje proizvode mapiranja ligatura

Spuštanje na razinu glifa s ExtractLoadedPageGlyphs

Oba poziva za tekst izgrađena su na ExtractLoadedPageGlyphs, a temeljni THPDFGlyphArray dostupan je i vašem kodu. Svaki THPDFGlyphRecord nosi razriješenu Unicode kodnu točku zajedno sa sirovim kodom znaka, širinom bajta koda (1, 2 ili 4, određenu s codespacerange CMap-a), aktivnim ključem i veličinom resursa fonta, koordinatama X i Y podrijetla u korisničkom prostoru te vodoravnim pomakom. To je dovoljno za izgradnju otkrivanja granica riječi, pozicioniranog označavanja ili prilagođenog algoritma izgleda bez potrebe da sami dirate tok sadržaja

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;

Brojanje zapisa s Unicode = 0, kao što je prikazano gore, pošten je način mjerenja kvalitete izdvajanja na određenom dokumentu prije nego što povjerujete tekstu nizvodno. Zapisi glifova također sidre svaki znak za izvorni operand u toku sadržaja, što je ono što omogućuje HotPDF-ovo pretraživanje i zamjenu teksta u učitanom dokumentu na istom temelju

Koji PDF-ovi neće otkriti svoj tekst?

Skenirani dokumenti su najjednostavniji slučaj: stranica koja je jedna velika slika uopće ne sadrži operatore teksta, pa izdvajanje ispravno vraća prazan niz — rješenje je OCR, a izdvajanje slika stranica iz učitanog PDF-a prvi je korak tog cjevovoda. Podskupovi fontova bez tablice /ToUnicode su teži slučaj: ako su put /Encoding i standardni CMap-ovi također prazni, ti se glifovi razrješavaju u 0 i pojavljuju se kao razmaci u pozivima teksta. Šifrirani dokumenti izdvajaju se normalno pod uvjetom da ih učitate s njihovom lozinkom putem preopterećenja LoadFromFile, tako da se tokovi dešifriraju prije nego što ih interpretator uopće vidi

Jednu užu granicu vrijedi jasno reći: dekoderski lanac čita CMap i tokove sadržaja kroz HotPDF-ovu Flate stazu, pa se font čiji ToUnicode tok koristi neobičan filtar prebacuje na sljedeću strategiju umjesto da uzrokuje pogrešku na stranici. U praksi, FlateDecode pokriva gotovo sve proizvedeno u posljednja dva desetljeća, a prebacivanje na nižu razinu je tiho po dizajnu — dobivate najbolji tekst koji datoteka dopušta umjesto iznimke. Isti strojni dio s čitačke strane objekta koji ovdje razrješava rječnike fontova također pokreće uređivanje metapodataka na učitanim dokumentima, pa cjevovod za unos dokumenata može izdvojiti, pregledati i označiti u jednom prolazu

Izdvajanje teksta, prikaz s očuvanjem izgleda, pristup na razini glifa i značajke pretraživanja i zamjene izgrađene na njima dio su standardne komponente HotPDF Component za Delphi i C++Builder — bez vanjskih DLL-ova, bez tekstualnih usluga operativnog sustava, samo Object Pascal kroz koji možete prolaziti korak po korak kada neobična datoteka sleti u vaš red čekanja