Tehnički članak

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

HotPDF Component izdvaja Unicode tekst iz bilo kog PDF-a koji učitate u Delphiju putem dva poziva: ExtractLoadedPageText vraća tekst stranice prema toku čitanja, a ExtractLoadedPageTextLayout (dodat u verziji v2.263.0) rekonstruiše vizuelni raspored stranice kao običan tekst, tako da kolone, uvlačenje i poravnanje tabela 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, izveštaj koji je dostavio biro za skeniranje, ugovor koji je generisao softver kome više niko ne zna ime

Dolazak do toga zahtevao je više mehanizama nego što to sugerišu dva potpisa, jer PDF ne čuva 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 zamenskih rešenja (fallback chain) za dekodiranje fontova — jer poznavanje načina na koji mapiranje funkcioniše čini razliku između slegnuti ramenima na neupotrebljiv izlaz i dijagnostikovanja istog

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

Tok sadržaja PDF-a (content stream) belež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 zavisi od fonta odabranog prethodnim operatorom Tf: bajt 0x41 može biti slovo A pod WinAnsi kodiranjem, proizvoljan glif u podskupu fonta ili polovina dvobajtnog CID-a u kompozitnom CJK fontu. ISO 32000-1 §9.10 definiše izdvajanje teksta upravo kao ovaj problem dekodiranja — mapiranje svakog koda nazad u Unicode koristeći bilo koje informacije koje pruža rečnik fonta — a standard izričito navodi da usaglašena datoteka ne mora nužno sadržati dovoljno informacija za to

Ta poslednja klauzula objašnjava svako izveštaje o grešci tipa "zašto kopiranje i lepljenje iz ovog PDF-a proizvodi besmislice" koje ste ikada videli. Proizvođač koji ugrađuje podskup fonta bez tablice /ToUnicode napisao je datoteku koja se renderuje 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 zamenskih rešenja (fallbacks) koji se trudi da učini najbolje što može, a korisno pitanje je koliko duboko taj lanac ide

Izdvajanje prema toku čitanja sa ExtractLoadedPageText

Za indeksiranje pretrage, podudaranje ključnih reči ili slanje teksta u pipeline 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 izuzetak

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 redova u izlazu dolaze iz namerno jednostavne heuristike: kada se vertikalno poreklo glifa pomeri za više od polovine trenutne veličine fonta — što je potpis koraka Td ili T* u toku sadržaja — umeće se novi red. Znakovi koje dekoder ne može razrešiti postaju razmaci umesto da nestanu, pa granice reči preživljavaju čak i kada pojedinačni glifovi ne prežive. Ono što ovaj režim ne pokušava jeste grupisanje prema redosledu čitanja ili detekcija više kolona: stranica sa dve kolone izlazi isprepletena redosledom toka sadržaja, što je obično, ali ne uvek, vizuelni redosled

Kada bi trebalo da koristite izdvajanje sa očuvanjem izgleda?

ExtractLoadedPageTextLayout je pravi poziv kad god pozicija nosi značenje: tabele, obrasci, popisi koda, sve što nameravate da upoređujete (diff), pretražujete (grep) ili analizirate po kolonama. Umesto da spljošti glifove u tok, on ih grupiše u osnovne linije (baselines), sortira svaku osnovnu liniju po X koordinati i reprodukuje vodoravne i okomite razmake na mreži znakova nepromenljive š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 režima dele svaki bajt meizma za dekodiranje i razlikuju se samo u načinu na koji raspoređuju dekodirane glifove, tako da izbor ne košta ništa u vernosti prikaza. Odaberite ExtractLoadedPageText kada su važne samo reči, a ExtractLoadedPageTextLayout kada je važan raspored. Detekcija redosleda čitanja sa više kolona ostaje van opsega za oba — prikaz mreže stranice sa dve kolone prikazuje vam obe kolone jedna pored druge, verno, što je za upoređivanje (diff) tačno, a za ponovni tok teksta nije

Kako HotPDF dekodira kodove znakova u Unicode?

HotPDF Component razrešava svaki kod znaka kroz prioritetno poređani lanac zamenskih rešenja (fallback chain): najpre 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 da pruži odgovor tiho se prebacuje na sledeću umesto da podiže izuzetak, a kod koji iscrpi ceo lanac razrešava se u 0 tako da pozivalac može brojati promašaje umesto da pogađa

CMap /ToUnicode (ISO 32000-1 §9.10.3) nalazi se na prvom mestu 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 unapred definisane CMap-ove poput UniGB-UTF16-H umesto ugradnje bilo čega: HotPDF isporučuje zbirke datoteka pod svojim direktorijumom resources\CMap, pronalazi ih u odnosu na izvršnu datoteku u vremenu izvršavanja (runtime) i kešira (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 ponovo analizirati po stranici. Ako direktorijum nije prisutan, dekoder jednostavno preskače CMap-ove sa diska i radi sa ugrađenim tablicama te ugrađenim kodiranjima. Ovo je zrcalna slika sa strane čitanja za problem oblikovanja pokriven u oblikovanju teksta složenog pisma sa HotPDF-om, gde se sa istom razlikom između koda i glifa susreće pri pisanju

Dve zamke sintakse CMap-a koje vredi znati

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

Druga zamka je da su ciljevi bfchar i bfrange UTF-16BE nizovi znakova, a ne celi brojevi (integers). Odredište <D83DDE00> znači U+1F600 — surogatni par koji se mora ponovo spojiti u jednu kodnu tačku — a čitanje ta dva bajta kao big-endian celog broja proizvodi besmislenu vrednost za svaku kodnu tačku van Osnovne višejezične ravni (Basic Multilingual Plane). Emotikoni u PDF-ovima više nisu egzotični, pa 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 ponovo spaja UTF-16BE kodne jedinice, što takođe pokriva višekarakterne ciljeve koje proizvode mapiranja ligatura

Spuštanje na nivo glifa sa ExtractLoadedPageGlyphs

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 sa Unicode = 0, kao što je prikazano gore, pošten je način merenja kvaliteta izdvajanja na određenom dokumentu pre nego što poverujete tekstu nizvodno. Zapisi glifova takođe sidre svaki znak za izvorni operand u toku sadržaja, što je ono što omogućava HotPDF-ovo pretraživanje i zamenu 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šte ne sadrži operatore teksta, pa izdvajanje ispravno vraća prazan niz — rešenje je OCR, a izdvajanje slika stranica iz učitanog PDF-a prvi je korak tog pipeline-a. Podskupovi fontova bez tablice /ToUnicode su teži slučaj: ako su put /Encoding i standardni CMap-ovi takođe prazni, ti se glifovi razrešavaju u 0 i pojavljuju se kao razmaci u pozivima teksta. Šifrovani dokumenti izdvajaju se normalno pod uslovom da ih učitate sa njihovom lozinkom putem preopterećenja LoadFromFile, tako da se tokovi dešifruju pre nego što ih interpretator uopšte vidi

Jednu užu granicu vredi 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 filter prebacuje na sledeću strategiju umesto da uzrokuje grešku na stranici. U praksi, FlateDecode pokriva gotovo sve proizvedeno u poslednje dve decenije, a prebacivanje na nižu razinu je tiho po dizajnu — dobijate najbolji tekst koji datoteka dopušta umesto izuzetka. Isti strojni deo sa čitalačke strane objekta koji ovde razrešava rečnike fontova takođe pokreće uređivanje metapodataka na učitanim dokumentima, pa pipeline za unos dokumenata može izdvojiti, pregledati i označiti u jednom prolazu

Izdvajanje teksta, prikaz sa očuvanjem izgleda, pristup na nivou glifa i funkcije pretraživanja i zamene izgrađene na njima deo su standardne komponente HotPDF Component za Delphi i C++Builder — bez spoljnih DLL-ova, bez tekstualnih usluga operativnog sistema, samo Object Pascal kroz koji možete prolaziti korak po korak kada neobična datoteka sleti u vaš red čekanja