Tehnički članak

Ekstrakcija teksta iz PDF datoteka pomoću PDFium komponente u Delphi-ju

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