Izvlačenje teksta, slika i fontova iz postojećeg PDF dokumenta zvuči kao rešen problem sve dok kroz taj proces ne provučete stvarne dokumente. Usmjerite indeksator pretrage na četrdeset hiljada korisničkih fajlova i problemi će se svrstati u nekoliko prepoznatljivih grupa. Reči se spajaju jer niko nije definisao koliki razmak se računa kao razmak između reči. Druge stranice se vraćaju kao nerazumljivi karakteri jer podskup fonta (subsetted font) ne sadrži mapu svojih kodova glifova ka stvarnim karakterima. A ispostavi se da je "logo kompanije" zapravo devet zasebnih objekata slika naslaganih iza meke maske (soft mask). Ništa od toga nije bag u biblioteci. To je razlika između jednostavnog pozivanja funkcije za ekstrakciju i razumevanja šta ta funkcija zapravo može, a šta ne može da povrati iz bajtova na disku.
losLab PDF Library, izdanje za Pascal, pruža Delphi i C++Builder kodu više načina za čitanje svakog od ova tri toka podataka, a nivoi se razlikuju po onome što garantuju. Trik je u usklađivanju nivoa sa zadatkom: indeks pretrage, revizija redakcije (redaction) i preflight provera za PDF/A zahtevaju različite stvari sa iste stranice, a korišćenje pogrešnog poziva uzalud troši resurse ili daje izlaz u koji ne možete imati poverenja.
Nivoi ekstrakcije teksta i šta svaki od njih obećava
Funkcija GetPageText prihvata vrednost opcije od 0 do 8, i taj broj bira mehanizam (engine) umesto samog formata. Vrednosti od 0 do 2 pokreću lagan prolaz koji je sasvim dovoljan za brz pregled. Vrednosti od 3 do 8 usmeravaju proces kroz mehanizam koji prepoznaje raspored (layout-aware), koji ponovo rekonstruiše linije i razmake na osnovu toga gde se glifovi zapravo nalaze na stranici. Unutar tog opsega razlike su važne: opcije 4 i 6 dele izlaz na reči, 5 i 6 emituju širine po pojedinačnom glifu, dok opcija 7 vraća čist tekst pri čemu namerno odbacuje font, boju i metapodatke o blokovima. Opcija 7 je ona kojom treba hraniti indeks pretrage, pošto indeks zahteva samo reči i ništa više.
Nijedno podešavanje opcija ne može spasiti dokument koji te informacije uopšte nije sadržao od samog početka. PDF mapira kodove karaktera u oblike glifova, a jedina stvar koja te kodove mapira nazad u čitljiv tekst jeste ToUnicode CMap tabela fonta (ISO 32000-1 §9.10). Kada se podskup fonta isporuči bez nje, svaki ekstraktor je nemoćan. Ova biblioteka, funkcija kopiraj-nalepi u pregledaču, konkurentski alati: svi oni su primorani da nagađaju na osnovu naziva glifova ili da ne vrate ništa. Praktično rešenje je detekcija, a ne herojski pokušaji. Označite stranicu kao nepouzdanu i pošaljite je na OCR, jer je tiho indeksiranje smeća gore nego priznanje da ga ne možete pročitati.
Za slučajeve koje jednostavne opcije ne pokrivaju, poput prilagođene tokenizacije, forenzike toka sadržaja ili filtera za tekst napravljenog po vašim pravilima, dekoder je dostupan jedan nivo niže. Klasa TPDFExtractor se kreira nad rečnikom resursa stranice i kolekcijom fontova. Metoda ExtractTextW vraća sirove tekstualne operacije toka sadržaja kroz isti mehanizam fontova kako bi se povratio Unicode, dok vam događaj OnFindObject prosleđuje svaki objekat dok prolazi kroz tok. Većina koda nikada ne mora da ide ovoliko duboko. Aplikacije koje to čine su one koje su zahvalne što je ovaj sloj javan, a ne sakriven.
Pozicionirani blokovi: jedinica rezultata pretrage i revizije redakcije
Čist tekst vam govori šta piše na stranici. Pre ili kasnije, aplikaciji je takođe potrebno da zna gde to tačno piše, kako bi mogla da istakne rezultat pretrage, nacrta okvir oko kandidata za uklanjanje osetljivih podataka (redaction) ili usidri anotaciju na pravo mesto. Funkcija ExtractPageTextBlocks vraća hendl liste tekstualnih segmenata, a svaki segment nosi svoj tekst, granični okvir, kao i naziv i veličinu fonta u kom je napisan:
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;
Jedan detalj u ovoj oblasti ometa integracije više od bilo kog drugog. Funkcije SetTextExtractionArea, SetTextExtractionWordGap i SetTextExtractionOptions predstavljaju stanja na nivou dokumenta koja ostaju aktivna, a ne argumente koje prosleđujete pri svakom pozivu. Konfigurišite ograničenje oblasti za jednu funkciju, recimo čitanje samo zaglavlja radi klasifikacije dokumenta, i to će tiho skratiti svaku sledeću ekstrakciju na istom hendlu, uključujući i GetPageText nivoe koji uzimaju u obzir raspored, a koje koristite kasnije. Ili resetujte stanje ekstrakcije između logičkih zadataka, ili svakom zadatku dodelite sopstveni hendl dokumenta.
Prag razmaka među rečima (word gap threshold) je rešenje za onu prvu grupu problema spojenih reči. SetTextExtractionWordGap govori mehanizmu za raspored koliki horizontalni prostor, meren u odnosu na razmak između glifova same stranice, odvaja jednu reč od sledeće. Zbijena tabela zahteva manji razmak od retko kucane marketinške stranice, pa je prag prilagođen klasi dokumenta bolji od jedne globalne konstante. On ostaje aktivan na dokumentu kao i ostatak stanja ekstrakcije, pa planirajte da ga postavljate ciljano, a ne samo jednom.
Slike: originalni tokovi, a ne snimci ekrana
Pogrešan način za izvlačenje slika iz PDF-a jeste renderovanje stranice i njeno isecanje. To vrši ponovno uzorkovanje piksela, trajno primenjuje rotaciju i odbacuje sve što je original predstavljao. Nasuprot tome, GetPageImageList popisuje stvarne resurse slika na koje stranica referencira, a svaka stavka vraća svoja svojstva i svoje originalne, neizmenjene podatke:
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;
Proverite GetImageListItemFormatDesc pre nego što bilo šta pretpostavite o stavci, jer ono na šta stranica referencira retko predstavlja jednu urednu sliku po vidljivom objektu. Meka maska se prikazuje kao sopstveni zasebni unos. Isti XObject se često ponavlja na mnogim stranicama, pa izvršite dedupliranje pomoću heša sadržaja pre nego što arhivirate izvoz svih slika, inače ćete isti logo upisati sto puta. CMYK JPEG slike zahtevaju primenu upravljanja bojama u kasnijoj fazi, jer se inače renderuju invertovano u pregledačima koji kanale tumače doslovno. Kada želite popis na nivou celog dokumenta umesto pojedinačnih stranica, funkcija FindImages zajedno sa SetFindImagesMode skenira ceo fajl u jednom prolazu.
Postoji jedno ograničenje koje vredi predočiti zainteresovanim stranama pre nego što bilo ko napiše kriterijume prihvatanja: ekstrakcija slika vraća isključivo rasterske resurse. Logo ili dijagram nacrtan kao skup vektorskih putanja nije slika u smislu resursa i nikada se neće pojaviti na bilo kojoj listi slika, bez obzira na to koliko jasno izgleda kao slika na ekranu. Kada je zahtev zaista isporuka tog dijagrama kao fajla, pravilan pristup je renderovanje regije stranice u bitmapu, što je drugačija operacija sa drugačijim nivoom vernosti. Te dve vrste izlaza ne pripadaju istoj izvoznoj fascikli bez jasne oznake koja je koja.
Fontovi: kontrolna površina, a ne funkcija izvoza
API za fontove daje odgovore na pitanja o fontovima. On vam ne isporučuje same fajlove fontova, i ta razlika oblikuje sve što možete da napravite na osnovu njega. Nakon što FindFonts skenira dokument, nabrajanje prolazi kroz fontove po ID-ju, a pozivi svojstava daju izveštaj o onom fontu koji je trenutno izabran:
var
I: Integer;
begin
Pdf.FindFonts;
for I := 1 to Pdf.FontCount do // font indexes start at 1, not 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;
Pazite na granice petlje. Indeksi fontova se kreću od 1 do FontCount, dok su indeksi tekstualnih blokova i lista slika pomenuti nekoliko pasusa iznad zasnovani na nuli. Ako prenesete jednu konvenciju u drugu, dobićete grešku "off-by-one" koja ili preskače prvi font ili ide van granica liste, a to će proći usputne testove jer većina dokumenata ima nekoliko fontova, pa i pogrešan font deluje uverljivo. Budite jasni i u vezi sa opsegom. Ovaj API nema mogućnost izvoza bajtova fontova. Nijedan poziv ne vraća ugrađeni program fonta kao TTF or OTF fajl; nabrajanje i inspekcija metapodataka predstavljaju jedini predviđeni model. Taj model i dalje pokriva ono što se u praksi traži od fontova: detekcija podskupova prema šablonu naziva, provera ugrađenosti (embedding) pre konverzije u arhivski format (neugrađeni font je apsolutna prepreka za PDF/A, kao što je detaljno opisano u članku o PDF/A i PDF/UA preflight proveri u Delphi-ju), kao i dijagnostika kodiranja kada opadne pouzdanost ekstrakcije. Postoji i licencni razlog zašto je granica ovde postavljena. Program podskupa fonta je licencirani materijal, a pošto mu nedostaje većina glifova, ionako je neupotrebljiv kao instalirani font. Tretiranje istog kao metapodataka za kontrolu umesto kao imovine za izvlačenje je stav koji možete odbraniti.
Taj poslednji poziv ima veliku ulogu u trijaži. Pokrenite GetFontEncoding na svakom fontu, pročitajte to uporedo sa zastavicom podskupa i možete predvideti kvalitet ekstrakcije pre nego što povučete ijedan karakter. Stranica čiji su svi fontovi u podskupovima sa nestandardnim kodiranjima je kandidat za OCR već na osnovu same inspekcije, što omogućava da je serijski proces pravilno usmeri, bez gubljenja vremena na neuspešan prolaz ekstrakcije.
Ekstrakcija u velikom obimu bez učitavanja dokumenata
U serijskom procesu, učitavanje celog dokumenta samo da bi se pročitala jedna stranica predstavlja uzaludan I/O rad, a to se brzo sabira na velikom broju dokumenata. Varijante sa jednim pozivom, ExtractFilePageText i ExtractFilePageTextBlocks, prihvataju naziv fajla, lozinku i broj stranice direktno i preskaču kompletno učitavanje. Za fajlove veličine više gigabajta postoji još brži režim. Putanja za direktan pristup otvara fajl kroz strimovanje xref čitanja, pa DAOpenFileReadOnly praćen funkcijom DAExtractPageText pristupa samo onim objektima koji su toj jednoj stranici zaista potrebni. To dolazi sa promenom konvencije koju vredi zapamtiti: DA funkcije adresiraju stranice preko PageRef-a, hendla objekta reference koji dobijate preko DAFindPage, nikada preko sirovog broja stranice. Prosledite broj tamo gde treba da stoji hendl i poziv će raditi na pogrešnom objektu bez prijavljivanja greške, što je najgora vrsta greške za otklanjanje bagova. Ostatak alata za direktan pristup opisan je u članku o spajanju, deljenju i direktnom pristupu velikim PDF fajlovima.
Ako postoji jedna navika koja razlikuje kod za ekstrakciju koji može da izdrži rad sa realnim dokumentima od onog koji stalno zapinje, to je tretiranje stranice kao nepouzdanog unosa, a ne kao čistog izvora podataka. Tekst koji se ne poklapa sa onim što pregledač prikazuje gotovo uvek je problem kodiranja, poput ligature koja se spaja u jedan glif ili podskupa fonta kojem nedostaju ToUnicode unosi, a rešenje je merenje pouzdanosti i usmeravanje loših stranica na OCR, a ne borba sa bajtovima. API za fontove nikada neće proizvesti TTF ili OTF, po dizajnu, tako da tokove rada sa fontovima gradite oko pitanja revizije. Pored toga, perzistentno stanje ekstrakcije, a naročito pravougaonik oblasti, jeste podešavanje koje posedujete tokom celog životnog veka hendla dokumenta, a ne parametar koji zaboravljate nakon jednog poziva. Usvojite ova tri refleksa i ostatak API-ja će raditi kako treba.
Evaluacione verzije, demo projekti i kompletna referenca API-ja za ekstrakciju nalaze se na stranici proizvoda losLab PDF Library for Delphi.