Ekstrakcija teksta iz PDF-a izgleda jednostavno sve dok ne naiđete na dokument u kome tekstualni sloj nedostaje, oštećen je ili je podeljen na desetine sitnih sekvenci karaktera bez ikakvog smislenog redosleda. PDFium komponenta vam pruža dve ulazne tačke: niz Character[] za sirovi pristup svakom glifu na stranici na osnovu indeksa, i ReadablePageContent za strukturirani prikaz koji rekonstruiše pasuse i naslove iz stabla oznaka (tag tree) PDF-a ili na osnovu heurističke analize. Nijedan od njih nije uvek pravi izbor, pa je razumevanje onoga što svaki od njih izlaže veoma važno
Otvaranje dokumenta i zamka tihog otkazivanja
Klasa TPdf otvara datoteku postavljanjem svojstva FileName i prebacivanjem Active := True. Kritičan detalj: operacija Active := True nikada ne podiže izuzetak. Ako datoteka nedostaje, zaštićena je lozinkom ili je oštećena, PDFium interno hvata grešku i svojstvo Active jednostavno ostaje na False. To znači da se svaka petlja za ekstrakciju mora zaštititi od ovoga:
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True;
if not Pdf.Active then
begin
ShowMessage('Could not open PDF (damaged or wrong password)');
Exit;
end;
// extraction follows here
finally
Pdf.Active := False;
Pdf.Free;
end;
Za datoteke zaštićene lozinkom potrebno je postaviti Pdf.Password := '...' pre prebacivanja Active := True. Nema druge šanse: kada Active jednom ne uspe, morate zatvoriti i ponovo otvoriti dokument sa ispravnom lozinkom
Ekstrakcija stranicu po stranicu pomoću niza Character[]
Najniži nivo pristupa podrazumeva prolazak kroz svaki karakter na svakoj stranici. Postavite Pdf.PageNumber da biste učitali tekstualni sloj za tu stranicu, a zatim prođite kroz unose do broja CharacterCount koristeći svojstvo Character[]. Vredi proveriti dve zastavice na svakom unosu: CharacterGenerated[i] označava sintetičke glifove koje je umetnuo mehanizam za renderovanje (na primer, meke crtice na prelomima redova) koji nemaju stvarnu Unicode vrednost, i CharacterMapError[i] koja signalizira da PDFium nije mogao da mapira glif u kodnu tačku (code point), što se dešava kod kodiranja fontova koji nemaju ToUnicode tabelu
procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
Page, I: Integer;
Line: string;
Ch: WideChar;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
Line := '';
for I := 0 to Pdf.CharacterCount - 1 do
begin
if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
Continue;
Ch := Pdf.Character[I];
if Ch = #13 then
Ch := #10; // normalize CR to LF
Line := Line + Ch;
end;
Output.Add(Line);
end;
end;
Rezultat je a flat string of Unicode kodnih tačaka po redosledu kojim ih PDFium nabraja, a to je redosled kojim se pojavljuju u toku sadržaja (content stream), što nije nužno redosled čitanja sleva nadesno. Za većinu dokumenata sa latiničnim pismom koje proizvode standardni kancelarijski alati, ovo je sasvim u redu. Za skenirane PDF-ove koji su prošli OCR sa neobičnim sekvencama glifova, ili za tekst zdesna nalevo, redosled može biti pogrešan. U tim slučajevima, ReadablePageContent postaje korisniji
Strukturirana ekstrakcija pomoću ReadablePageContent
Metoda ReadablePageContent ide jedan nivo više: ona vraća zapis tipa TPdfReadableContent čiji niz Fragments nosi označene fragmente sadržaja, od kojih svaki ima Kind koji identifikuje pasuse, naslove, stavke liste, ćelije tabele i tako dalje. Kada PDF sadrži stablo strukture (proverite Pdf.IsTagged), izvor je rosStructure i redosled čitanja je autoritativan. Za neoznačene datoteke, PDFium se vraća na rosHeuristic, koji grupiše karaktere prema njihovim okvirima (bounding boxes) u verovatne jedinice čitanja, ali ne može da garantuje apsolutnu tačnost
procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
Page: Integer;
Content: TPdfReadableContent;
Fragment: TPdfContentFragment;
begin
for Page := 1 to Pdf.PageCount do
begin
Content := Pdf.ReadablePageContent(Page);
for Fragment in Content.Fragments do
begin
case Fragment.Kind of
cfHeading : Output.Add('# ' + Fragment.Text);
cfParagraph : Output.Add(Fragment.Text);
cfListItem : Output.Add('- ' + Fragment.Text);
else
Output.Add(Fragment.Text);
end;
end;
end;
end;
Ako je Content.Source = rosHeuristic a vaš izlaz izgleda zbrkano, tekstualni sloj dokumenta verovatno nije zapisan sa redosledom čitanja na umu. U tom trenutku, jedina pouzdana ispravka je ponovni izvoz iz izvorne aplikacije sa pravilnim označavanjem, ili pokretanje koraka naknadne obrade koji sortira ishodišta karaktera po Y, a zatim po X koordinatama
Šta vam pružaju CharacterOrigin i CharacterRectangle
Ova oba svojstva vraćaju poziciju karaktera u prostoru stranice (u tačkama, sa koordinatnim početkom u donjem levom uglu, gde Y raste nagore). CharacterOrigin[i] je sidrišna tačka osnovne linije (baseline) glifa. CharacterRectangle[i] je pun granični okvir. Ovo su gradivni blokovi za bilo šta van običnog teksta: otkrivanje granica kolona, grupisanje karaktera u redove poređenjem Y koordinata unutar određene tolerancije, ili izgradnja mape za hit-test kod selekcije teksta u pregledaču. Ako treba da pronađete koji karakter se nalazi ispod klika mišem, funkcija CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) obavlja tu pretragu direktno, tako da ne morate sami da prolazite kroz sve pravougaonike
Postavljanje DLL-a na mesto
PDFium komponenta delegira svo parsiranje PDF-a nativnoj DLL datoteci, bilo pdfium32.dll or pdfium64.dll u zavisnosti od vaše ciljne platforme. Komponenta dolazi sa skriptom CopyDlls.bat koja kopira odgovarajući fajl u Windows sistemski direktorijum. Pokretanje ove skripte kao administrator jednom na razvojnoj mašini je dovoljno. Za distribuciju, kopirajte DLL pored izvršne datoteke aplikacije. Varijante sa omogućenim V8 mehanizmom (pdfium32v8.dll, pdfium64v8.dll) su znatno veće i potrebne su samo ako vaši PDF dokumenti sadrže JavaScript koji se mora izvršiti. Za čistu ekstrakciju teksta, standardna verzija je pravi izbor
Ako DLL nedostaje u vreme izvršavanja, poziv Active := True će tiho propasti baš kao i za nedostajuću datoteku, jer komponenta interno hvata grešku pri učitavanju. Uvek testirajte na čistoj mašini pre isporuke softvera
Korišćenje FontSize[] uz Character[] za analizu izgleda
Pored običnog teksta, API na nivou karaktera izlaže FontSize[i], koji vraća renderovanu veličinu svakog glifa u tačkama. Kombinovano sa CharacterOrigin[i] i CharacterRectangle[i], ovo vam omogućava da razlikujete osnovni tekst od naslova bez oslanjanja na stablo strukture. Sekvenca karaktera gde veličina fonta skače iznad određenog praga je skoro sigurno naslov u neoznačenom dokumentu. Ista tehnika se primenjuje na otkrivanje opisa slika (mali tekst ispod graničnog okvira slike) ili fusnota (mali tekst blizu dna stranice). Ništa od ovoga ne zahteva renderovanje. Sva tri svojstva čitaju podatke direktno iz tekstualnog sloja koji PDFium gradi tokom stanja Active := True
Jedna nijansa: FontSize[i] odražava veličinu nakon što se primeni CTM (current transformation matrix) stranice, tako da će dokument u kome je autor skalirao čitavu stranicu prijaviti proporcionalno prilagođene veličine. Ako poredite veličine na stranicama sa različitim dimenzijama, normalizujte ih u odnosu na visinu MediaBox-a svake stranice pre donošenja odluka o pragu
Upisivanje izlaza u datoteku
Klasa TStringList u Delphi-ju čisto rukuje UTF-8 izlazom još od verzije XE. Postavite WriteBOM := False ako vam je potrebna datoteka bez BOM-a (mnogi nizvodni potrošači se bune na vodeći BOM):
var
Lines: TStringList;
begin
Lines := TStringList.Create;
try
ExtractAllText(Pdf, Lines);
Lines.WriteBOM := False;
Lines.SaveToFile('output.txt', TEncoding.UTF8);
finally
Lines.Free;
end;
end;
Za veoma velike dokumente gde je memorija problem, pišite direktno u TStreamWriter sa kodiranjem TEncoding.UTF8 unutar petlje stranica, umesto da najpre akumulirate sve u listu
API-ji Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent i CharacterIndexAtPos prikazani ovde su deo PDFium komponente za Delphi i C++Builder