Izvlačenje teksta, slika i fontova iz postojećeg PDF-a zvuči kao riješen problem sve dok kroz njega ne propustite stvarni korpus dokumenata. Usmjerite indeksator pretraživanja na četrdeset tisuća korisničkih datoteka i kvarovi će se razvrstati u nekoliko prepoznatljivih skupina. Riječi se spajaju jer nitko nije rekao ekstraktoru koliki se razmak računa kao razmaknica. Druge stranice vraćaju se kao nerazumljivi znakovi jer djelomično ugrađeni (subsetted) font ne sadrži mapu od svojih kodova glifova do stvarnih znakova. A 'logotip tvrtke' ispada kao devet zasebnih slikovnih objekata poslaganih iza meke maske (soft mask). Ništa od toga nije bug u knjižnici. To je razlika između pozivanja funkcije izdvajanja i razumijevanja onoga što funkcija može, a što ne može oporaviti iz bajtova na disku.
losLab PDF Library, izdanje za Pascal, daje kodu u Delphiju i C++Builderu više od jednog načina za čitanje svakog od ta tri toka, a razine se razlikuju po onome što jamče. Trik je u usklađivanju razine sa zadatkom: indeks pretraživanja, preglednik redigiranja i prolaz provjere (preflight) za PDF/A traže različite stvari iz iste stranice, a posezanje za pogrešnim pozivom uzalud troši trud ili proizvodi izlaz kojem ne možete vjerovati.
Razine izdvajanja teksta i što svaka od njih obećava
GetPageText prima vrijednost opcije od 0 do 8, i taj broj odabire pogon (engine) umjesto formata. Vrijednosti od 0 do 2 pokreću lagani prolaz koji je sasvim u redu za brzi pregled. Vrijednosti od 3 do 8 usmjeravaju se kroz pogon koji je svjestan izgleda (layout-aware engine), koji rekonstruira linije i razmake prema tome gdje se glifovi stvarno nalaze na stranici. Unutar tog raspona varijacije su važne: 4 i 6 dijele izlaz na riječi, 5 i 6 emitiraju širine po glifu, a 7 vraća običan tekst s namjerno odbačenim metapodacima o fontu, boji i blokovima. Opcija 7 je ona kojom se hrani indeks pretraživanja, budući da indeks traži riječi i ništa više.
Nikakva postavka opcije ne može spasiti dokument koji te informacije uopće nije nosio u startu. PDF preslikava znakovne kodove u oblike glifova, a jedina stvar koja te kodove vraća u čitljiv tekst je font ToUnicode CMap (ISO 32000-1 §9.10). Kada se djelomično ugrađeni (subsetted) font isporuči bez toga, svaki ekstraktor je blokiran. Ova knjižnica, opcija kopiraj-zalijepi u pregledniku, konkurentski alati: svi su oni svedeni na nagađanje iz naziva glifova ili ne vraćaju ništa. Praktičan odgovor je detekcija, a ne junaštvo. Označite stranicu kao nisko pouzdanu i pošaljite je na OCR, ove je tiho indeksiranje smeća gore nego priznavanje da ga ne možete pročitati.
Za slučajeve koje jednostavne opcije ne pokrivaju, kao što su prilagođena tokenizacija, forenzika toka sadržaja (content-stream forensics), ili tok teksta izgrađen prema vašim vlastitim pravilima, dekoder je dostupan jedan sloj dublje. TPDFExtractor se konstruira nad rječnikom resursa stranice i zbirkom fontova. Njegova metoda ExtractTextW pokreće neobrađene tekstualne operacije toka sadržaja natrag kroz isti mehanizam fonta za oporavak Unicodea, a njegov događaj OnFindObject predaje vam svaki objekt kako pristiže u toku. Većina koda nikada ne mora posezati ovoliko duboko. Aplikacije koje to čine su one kojima je drago što je taj sloj javan, a ne skriven.
Pozicionirani blokovi: jedinica rezultata pretraživanja i pregleda redigiranja
Običan tekst vam govori što na stranici piše. Prije ili kasnije proizvod također mora znati gdje to piše, kako bi mogao istaknuti rezultat pretraživanja, nacrtati okvir oko kandidata za redigiranje ili usidriti bilješku na pravo mjesto. ExtractPageTextBlocks vraća rukovatelj popisom tekstualnih nizova, a svaki niz nosi svoj tekst, svoj granični okvir (bounding box) te naziv i veličinu fonta u kojem je postavljen:
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 ovom području stvara probleme pri integraciji više od bilo kojeg drugog. Metode SetTextExtractionArea, SetTextExtractionWordGap i SetTextExtractionOptions predstavljaju trajno stanje na razini dokumenta, a ne argumente koje prenosite po pozivu. Ako konfigurirate ograničenje područja za jednu značajku, recimo čitanje samo trake zaglavlja za klasifikaciju dokumenta, ono tiho skraćuje svako izdvajanje koje slijedi na istom rukovatelju, uključujući razine GetPageText svjesne izgleda kojima pristupate kasnije. Ili resetirajte stanje izdvajanja između logičkih zadataka ili svakom zadatku dodijelite vlastiti rukovatelj dokumentom.
Prag razmaka među riječima je poluga za onu prvu skupinu kvarova, odnosno riječi koje se spajaju. SetTextExtractionWordGap govori pogonu izgleda koliko vodoravnog prostora, mjerenog u odnosu na vlastiti razmak glifova na stranici, odvaja jednu riječ od sljedeće. Gusta tablica zahtijeva manji razmak od labavo postavljene marketinške stranice, pa je prag prilagođen po klasi dokumenta bolji od jedne globalne konstante. On ostaje aktivan na dokumentu poput ostatka stanja izdvajanja, stoga ga planirajte postaviti namjerno, a ne jednom i potom zaboraviti.
Slike: izvorni tokovi, a ne snimke zaslona
Pogrešan način za izvlačenje slika iz PDF-a je renderiranje stranice i njezino obrezivanje. To ponovno uzorkuje piksele, ugrađuje bilo kakvu rotaciju i odbacuje sve što je bilo u izvorniku. Umjesto toga, GetPageImageList nabraja stvarne slikovne resurse na koje se stranica referira, a svaka stavka vraća svoja svojstva i svoje izvorne, nedirnute 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;Provjerite GetImageListItemFormatDesc prije nego što išta pretpostavite o stavci, ove ono na što se stranica referira rijetko je jedna uredna slika po vidljivoj slici. Meka maska (soft mask) prikazuje se kao zaseban unos. Isti se XObject često ponavlja na mnogim stranicama, pa napravite dedupliciranje prema sažetku sadržaja (content hash) prije nego što arhivirate izvoz 'svih slika', inače ćete isti logotip zapisati stotinu puta. CMYK JPEG slike zahtijevaju primjenu upravljanja bojama u kasnijoj fazi, inače se renderiraju obrnuto u preglednicima koji kanale prihvaćaju zdravo za gotovo. Kada želite popis na razini cijelog dokumenta umjesto stranice po stranicu, FindImages zajedno s SetFindImagesMode skenira cijelu datoteku u jednom prolazu.
Postoji jedna granica koju je vrijedno istaknuti dionicima prije nego što itko napiše kriterije prihvaćanja: izdvajanje slika vraća samo rasterske resurse. Logotip ili grafikon nacrtan kao vektorska putanja nije slika u smislu resursa i nikada se neće pojaviti na bilo kojem popisu slika, bez obzira na to koliko jasno izgledao kao slika na zaslonu. Kada je zahtjev doista isporuka tog grafikona kao datoteke, ispravan pristup je renderiranje regije stranice u bitmapu, što je drugačija operacija s drugačijom vjernošću prikaza. Te dvije vrste izlaza ne pripadaju u istu mapu za izvoz bez oznake koja objašnjava što je što.
Fontovi: površina za reviziju, a ne značajka izvoza
API za fontove daje odgovore na pitanja o fontovima. Ne predaje vam same datoteke fontova, i ta razlika oblikuje sve što možete izgraditi na tome. Nakon što FindFonts skenira dokument, nabrajanje prolazi kroz fontove po ID-u, a pozivi svojstava izvješćuju o fontu koji je trenutno odabran:
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;Pripazite na granice petlje. Indeksi fontova idu od 1 do FontCount, dok su indeksi tekstualnih blokova i popisa slika nekoliko odlomaka iznad na bazi nule. Prenesite jednu konvenciju u drugu i dobit ćete pogrešku 'off-by-one' koja ili preskače prvi font ili ide preko kraja, a to će proći ležerno testiranje jer većina dokumenata ima nekoliko fontova i pogrešan i dalje izgleda vjerojatno. Budite jasni i oko opsega. Ovaj API nema izvoz fontova na razini bajtova. Nijedan poziv ne vraća ugrađeni program fonta kao TTF or OTF datoteku, a nabrajanje plus provjera metapodataka je cijeli predviđeni model. Taj model i dalje pokriva ono što proizvodni rad stvarno traži od fontova: detekciju podskupa (subset) prema obrascu naziva, reviziju ugradnje prije arhivske konverzije (neugrađeni font je apsolutna prepreka za PDF/A, kao što je objašnjeno u članku PDF/A i PDF/UA provjera u Delphiju) i dijagnostiku kodiranja za slučajeve kada pouzdanost izdvajanja padne. Postoji i licencni razlog zašto je granica postavljena ovdje. Program podskupa fonta je licencirani materijal i, budući da mu nedostaje većina glifova, ionako je beskoristan kao instalacijski font. Tretiranje istog kao revizijskih metapodataka umjesto kao izvodljivog resursa je pozicija koju možete braniti.
Taj posljednji poziv nosi svoju težinu u trijaži. Pokrenite GetFontEncoding na svakom fontu, pročitajte ga zajedno sa zastavicom podskupa i možete predvidjeti kvalitetu izdvajanja prije nego što izvučete ijedan znak. Stranica čiji su svi fontovi podskupovi s nestandardnim kodiranjima kandidat je za OCR već pri samom pregledu, što omogućuje cjevovodu serijske obrade da je ispravno usmjeri bez prethodnog gubljenja vremena na neuspješan prolaz izdvajanja.
Izdvajanje u velikom opsegu bez učitavanja dokumenata
U cjevovodu serijske obrade, učitavanje cijelog dokumenta samo da bi se pročitala jedna stranica predstavlja uzaludan I/O, što se brzo nakuplja na cijelom korpusu. Varijante s jednim pozivom, ExtractFilePageText i ExtractFilePageTextBlocks, primaju izravno naziv datoteke, lozinku i broj stranice te preskaču potpuno učitavanje. Za datoteke veličine gigabajta postoji još niži stupanj prijenosa. Put izravnog pristupa otvara datoteku putem strujnog čitanja xref tablice, pa DAOpenFileReadOnly praćen s DAExtractPageText dotiče samo objekte koje ta jedna stranica stvarno treba. To dolazi s pomakom u konvenciji koji vrijedi zapamtiti: DA funkcije adresiraju stranice pomoću PageRef-a, rukovatelja referencom objekta koji dobivate iz DAFindPage, a nikada prema čistom broju stranice. Proslijedite broj tamo gdje rukovatelj pripada i poziv će raditi na pogrešnom objektu bez javljanja pogreške, što je najgora vrsta pogreške za uklanjanje bugova. Ostatak alata za izravan pristup opisan je u članku spajanje, dijeljenje i izravan pristup velikim PDF datotekama.
Ako postoji jedna navika koja odvaja kod za izdvajanje koji preživljava stvarni korpus od koda koji šepa, to je tretiranje stranice kao nepouzdanog unosa, a ne kao čistog izvora podataka. Tekst koji se ne podudara s onim što preglednik renderira gotovo je uvijek problem s kodiranjem, ligatura koja se spaja u jedan glif ili podskup fonta kojem nedostaju njegovi ToUnicode unosi, a rješenje je mjerenje pouzdanosti i preusmjeravanje loših stranica na OCR, a ne borba s bajtovima. API za fontove po dizajnu nikada neće proizvesti TTF or OTF, stoga gradite tokove rada s fontovima oko revizijskih pitanja. A trajno stanje izdvajanja, ponajviše pravokutnik područja (area rectangle), postavka je koju posjedujete tijekom vijeka trajanja rukovatelja dokumentom, a ne parametar koji zaboravite nakon jednog poziva. Usvojite ova tri refleksa ispravno i ostatak API-ja će se ponašati kako treba.
Evaluacijske verzije, demo projekti i potpuna referenca API-ja za izdvajanje nalaze se na stranici proizvoda losLab PDF Library za Delphi.