Technical Article

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

Ekstrakcija teksta iz PDF-a izgleda jednostavno sve dok ne naiđete na dokument u kojem je tekstualni sloj odsutan, oštećen ili podijeljen na desetke sitnih blokova znakova bez ikakvog smislenog redoslijeda. PDFium VCL vam pruža dvije pristupne točke: polje Character[] za izravan pristup svakom glifu na stranici na temelju indeksa i ReadablePageContent za strukturirani prikaz koji rekonstruira odlomke i naslove iz PDF-ovog stabla oznaka (tag tree) ili heurističke analize. Nijedna od njih nije uvijek jedini ispravan izbor, stoga je važno razumjeti što koja izlaže.

Otvaranje dokumenta i zamka s tihim neuspjehom

TPdf otvara datoteku postavljanjem svojstva FileName i prebacivanjem Active := True. Ključni detalj: Active := True nikada ne podiže iznimku. Ako datoteka nedostaje, zaštićena je lozinkom ili je oštećena, PDFium interno hvata pogrešku i 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;

Datoteke zaštićene lozinkom zahtijevaju da se postavi Pdf.Password := '...' prije prebacivanja na Active := True. Nema druge prilike: kada Active jednom ne uspije, zatvarate i ponovno otvarate dokument s ispravnom lozinkom.

Ekstrakcija stranicu po stranicu pomoću Character[]

Pristup najniže razine prolazi kroz svaki znak na svakoj stranici. Postavite Pdf.PageNumber kako biste učitali tekstualni sloj za tu stranicu, a zatim prođite kroz unose do CharacterCount pomoću svojstva Character[]. Dvije zastavice na svakom unosu vrijedi provjeriti: CharacterGenerated[i] označava sintetičke glifove koje je umetnuo renderer (primjerice, meke crtice kod prijeloma retka) koji nemaju stvarnu Unicode vrijednost, i CharacterMapError[i] koja signalizira da PDFium nije mogao mapirati glif u kodnu točku, što se događa s kodiranjima fontova kojima nedostaje tablica ToUnicode.

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 jednostavan niz Unicode kodnih točaka redoslijedom kojim ih PDFium nabraja, a to je redoslijed kojim se pojavljuju u toku sadržaja, a ne nužno redoslijed čitanja s lijeva na desno. Za većinu dokumenata s latiničnim pismom koje su izradili standardni uredski alati to je u redu. Za skenirane PDF-ove koji su prošli OCR s neobičnim sekvencama glifova ili za tekst s desna na lijevo, redoslijed može biti pogrešan. U tim je slučajevima ReadablePageContent korisniji.

Strukturirana ekstrakcija pomoću ReadablePageContent

Metoda ReadablePageContent ide korak više: vraća zapis TPdfReadableContent čije polje Fragments nosi označene fragmente sadržaja, od kojih svaki ima Kind koji identificira odlomke, naslove, stavke popisa, ćelije tablice i tako dalje. Kada PDF nosi stablo strukture (provjerite Pdf.IsTagged), izvor je rosStructure i redoslijed čitanja je mjerodavan. Za neoznačene datoteke, PDFium se vraća na rosHeuristic, što grupira znakove prema njihovim graničnim okvirima (bounding boxes) u uvjerljive cjeline za čitanje, ali ne može jamčiti toč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 vjerojatno nije napisan imajući na umu redoslijed čitanja. U tom je slučaju jedini pouzdani popravak ponovni izvoz iz izvorne aplikacije s pravilnim označavanjem ili pokretanje koraka naknadne obrade koji sortira ishodišta znakova po osi Y, a zatim X.

Što vam daju CharacterOrigin i CharacterRectangle

Oba svojstva vraćaju poziciju znaka u prostoru stranice (točke, ishodište u donjem lijevom kutu, os Y raste prema gore). CharacterOrigin[i] je sidrišna točka osnovne linije glifa; CharacterRectangle[i] je puni granični okvir. To su gradivni blokovi za sve što nadilazi običan tekst: prepoznavanje granica stupaca, grupiranje znakova u retke usporedbom Y koordinata unutar tolerancije ili izgradnja karte pogodaka za označavanje teksta u pregledniku. Ako trebate saznati koji se znak nalazi ispod klika mišem, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) dohvata tu pretragu izravno bez potrebe da prolazite kroz pravokutnike.

Postavljanje DLL-a na mjesto

PDFium VCL delegira cjelokupnu analizu PDF-a nativnom DLL-u, bilo pdfium32.dll ili pdfium64.dll, ovisno o vašoj ciljnoj platformi. Komponenta dolazi s CopyDlls.bat skriptom koja kopira ispravnu datoteku u sistemski direktorij sustava Windows. Pokretanje te skripte jednom kao administrator na razvojnom računalu je dovoljno; za isporuku kopirate DLL uz izvršnu datoteku aplikacije. Verzije s omogućenim V8 mehanizmom (pdfium32v8.dll, pdfium64v8.dll) znatno su veće i potrebne su samo ako vaši PDF-ovi sadrže JavaScript koji se mora izvršiti. Za čistu ekstrakciju teksta, standardna verzija je ispravan izbor.

Ako DLL nedostaje u vrijeme izvođenja, Active := True tiho će zakazati baš kao i kod nedostajuće datoteke jer komponenta interno hvata pogrešku učitavanja. Prije isporuke uvijek testirajte na čistom računalu.

Korištenje FontSize[] uz Character[] za analizu izgleda

Nakon običnog teksta, API na razini znakova izlaže FontSize[i], koji vraća veličinu svake iscrtane točke glifa. U kombinaciji s CharacterOrigin[i] i CharacterRectangle[i], to vam omogućuje da razlikujete glavni tekst od naslova bez oslanjanja na stablo strukture. Blok znakova u kojem veličina fonta skače iznad praga gotovo je sigurno naslov u neoznačenom dokumentu. Isti se postupak primjenjuje na otkrivanje opisa (mali tekst ispod graničnog okvira slike) ili fusnota (mali tekst blizu dna stranice). Ništa od ovoga ne zahtijeva renderiranje; sva tri svojstva čitaju izravno iz tekstualnog sloja koji PDFium gradi tijekom rada svojstva Active := True.

Jedna nijansa: FontSize[i] odražava veličinu nakon što se primijeni CTM stranice (current transformation matrix), pa će dokument u kojem je autor skalirao cijelu stranicu prijaviti proporcionalno prilagođene veličine. Ako uspoređujete veličine na stranicama s različitim dimenzijama, normalizirajte vrijednosti prema visini MediaBox-a svake stranice prije donošenja odluka o pragu.

Zapisivanje izlaza u datoteku

Delphijev TStringList čisto obrađuje UTF-8 izlaz od verzije XE. Postavite WriteBOM := False ako trebate datoteku bez BOM oznake (mnogi drugi alati 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 vrlo velike dokumente gdje je memorija problem, pišite izravno u TStreamWriter s TEncoding.UTF8 unutar petlje stranice umjesto da najprije sve skupljate u popis.

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