Technical Article

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

Ekstrakcija teksta iz PDF-a izgleda jednostavno sve dok ne naiđete na dokument u kojem tekstualni sloj nedostaje, oštećen je ili je podeljen na desetine sićušnih segmenata karaktera bez ikakvog smislenog redosleda. PDFium VCL vam daje 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 zaglavlja iz stabla oznaka (tag tree) PDF-a ili heurističke analize. Nijedna od njih nije uvek pravi izbor, tako da je razumevanje onoga što svaka od njih izlaže veoma važno.

Otvaranje dokumenta i zamka tihog neuspeha

TPdf otvara datoteku postavljanjem FileName i prebacivanjem Active := True. Ključni detalj: 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 Active jednostavno ostaje False. To znači da svaka petlja za ekstrakciju mora da se zaštiti na sledeći način:

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;

Datoteke zaštićene lozinkom zahtevaju da Pdf.Password := '...' bude postavljeno pre Active := True. Nema druge šanse: kada Active ne uspe, zatvarate i ponovo otvarate sa ispravnom lozinkom.

Ekstrakcija stranicu po stranicu pomoću Character[]

Najniži nivo pristupa prolazi 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 CharacterCount koristeći svojstvo Character[]. Vredi proveriti dve zastavice na svakom unosu: CharacterGenerated[i] označava sintetičke glifove koje je umetnuo renderer (na primer, meke crtice na prelomima redova) koji nemaju stvarnu Unicode vrednost, i CharacterMapError[i] signalizira da PDFium nije mogao da mapira glif u kodnu tačku, što se dešava sa kodiranjima fontova kojima nedostaje ToUnicode tabela.

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 običan string Unicode kodnih tačaka redosledom kojim ih PDFium nabraja, što je redosled kojim se pojavljuju u toku sadržaja, a ne nužno redosled čitanja sleva nadesno. Za većinu dokumenata sa latiničnim pismom koje su proizveli 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. Tada svojstvo ReadablePageContent postaje korisnije.

Strukturirana ekstrakcija pomoću ReadablePageContent

ReadablePageContent ide jedan nivo više: vraća zapis TPdfReadableContent čiji niz Fragments nosi označene delove sadržaja, od kojih svaki ima Kind koji identifikuje pasuse, zaglavlja, stavke liste, ćelije tabele i tako dalje. Kada PDF sadrži stablo strukture (proverite Pdf.IsTagged), izvor je rosStructure i redosled čitanja je merodavan. Za neoznačene datoteke, PDFium se vraća na rosHeuristic, koji grupiše karaktere prema njihovim graničnim okvirima (bounding boxes) u verovatne jedinice za čitanje, ali ne može garantovati 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 napisan sa redosledom čitanja na umu. U tom trenutku, jedino pouzdano rešenje je ponovni izvoz iz izvorne aplikacije sa pravilnim označavanjem, ili pokretanje koraka postprocesiranja koji sortira koordinatne početke karaktera po Y, a zatim po X osi.

Šta vam pružaju CharacterOrigin i CharacterRectangle

Oba svojstva vraćaju poziciju karaktera u prostoru stranice (tačke, sa koordinatnim početkom u donjem levom uglu, Y raste nagore). CharacterOrigin[i] je osnovna tačka sidrenja glifa (glyph's baseline anchor point); CharacterRectangle[i] je puni granični okvir. Ovo su gradivni blokovi za bilo šta izvan običnog teksta: otkrivanje granica kolona, grupisanje karaktera u redove poređenjem Y koordinata unutar tolerancije, ili pravljenje mape za testiranje pogodaka (hit-test map) za izbor teksta u čitaču. Ako treba da pronađete koji se karakter nalazi pod klikom miša, funkcija CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) obavlja to traženje direktno, bez potrebe da prolazite kroz pravougaonike.

Postavljanje DLL-a na mesto

PDFium VCL delegira kompletno parsiranje PDF-a izvornoj DLL datoteci, bilo pdfium32.dll ili pdfium64.dll u zavisnosti od vaše ciljne platforme. Komponenta dolazi sa CopyDlls.bat skriptom koja kopira ispravnu datoteku u Windows sistemski direktorijum. Pokretanje ove skripte jednom kao Administrator na razvojnoj mašini je dovoljno; za distribuciju kopirate DLL pored izvršne datoteke aplikacije. Varijante sa omogućenim V8 motorom (pdfium32v8.dll, pdfium64v8.dll) su znatno veće i potrebne su samo ako vaši PDF-ovi sadrže JavaScript koji se mora izvršiti. Za čistu ekstrakciju teksta, standardni build je ispravan izbor.

Ako DLL nedostaje u vreme izvršavanja, postavljanje Active := True neće uspeti tiho, baš kao što se dešava i kod nedostajuće datoteke, jer komponenta interno hvata grešku pri učitavanju. Uvek testirajte na čistoj mašini pre isporuke.

Korišćenje FontSize[] zajedno sa Character[] za analizu rasporeda

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 zaglavlja bez oslanjanja na stablo strukture. Segment karaktera gde veličina fonta skače iznad praga je skoro sigurno zaglavlje u neoznačenom dokumentu. Ista tehnika se primenjuje na otkrivanje natpisa (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 direktno iz tekstualnog sloja koji PDFium gradi tokom Active := True.

Jedna nijansa: FontSize[i] odražava veličinu nakon što se primeni CTM (current transformation matrix) stranice, tako da će dokument u kojem je autor skalirao celu 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.

Zapisivanje izlaza u datoteku

Delphi-jev TStringList čisto rukuje UTF-8 izlazom počevši od verzije XE. Postavite WriteBOM := False ako vam je potrebna datoteka bez BOM-a (mnogi nizvodni potrošači to očekuju):

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 TEncoding.UTF8 unutar petlje stranica, umesto da najpre akumulirate sve u listu.

API-ji Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent i CharacterIndexAtPos koji su ovde prikazani deo su PDFium VCL komponente za Delphi i C++Builder.