Tehnički članak

Izdvajanje teksta iz PDF datoteka pomoću PDFium komponente u Delphiju

Izdvajanje teksta iz PDF-a izgleda jednostavno sve dok ne naiđete na dokument u kojem tekstualni sloj nedostaje, oštećen je ili podijeljen na desetke sićušnih nizova znakova bez smislenog redoslijeda. PDFium komponenta nudi vam dvije pristupne točke: niz Character[] za sirovi pristup svakom glifu na stranici na temelju indeksa, te ReadablePageContent za strukturirani prikaz koji rekonstruira odlomke i naslove iz PDF-ovog stabla oznaka (tag tree) ili heurističke analize. Nijedan od njih nije uvijek pravi izbor, stoga je važno razumjeti što koji od njih izlaže

Otvaranje dokumenta i zamka tihih neuspjeha (silent failure)

TPdf otvara datoteku postavljanjem svojstva FileName i prebacivanjem na Active := True. Ključan detalj: Active := True nikada ne izaziva iznimku (exception). 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 izdvajanja 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 trebaju imati postavljeno svojstvo Pdf.Password := '...' prije nego što se postavi Active := True. Nema druge prilike: jednom kada Active zakaže, morate ga zatvoriti i ponovno otvoriti s ispravnom lozinkom

Izdvajanje stranicu po stranicu pomoću Character[]

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

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 ravan niz Unicode kodnih točaka redoslijedom kojim ih PDFium nabraja, što je redoslijed kojim se pojavljuju u struji sadržaja (content stream), a ne nužno redoslijed čitanja slijeva nadesno. Za većinu dokumenata s latiničnim pismom koje su proizveli standardni uredski alati to je u redu. Za skenirane PDF-ove koji su prošli OCR s neobičnim sekvencama glifova, ili za tekst zdesna nalijevo, redoslijed može biti pogrešan. U tim je slučajevima ReadablePageContent korisniji

Strukturirano izdvajanje pomoću ReadablePageContent

ReadablePageContent ide korak više: vraća zapis TPdfReadableContent čiji niz 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, koji grupira znakove prema njihovim graničnim okvirima (bounding boxes) u vjerojatne jedinice čitanja, 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 i vaš izlaz izgleda zbrkano, tekstualni sloj dokumenta vjerojatno nije napisan s redoslijedom čitanja na umu. U tom trenutku jedini pouzdani ispravak je ponovni izvoz iz izvorne aplikacije s ispravnim označavanjem, ili pokretanje koraka naknadne obrade koji razvrstava podrijetla (origins) znakova po Y pa po X koordinati

Što vam pružaju CharacterOrigin i CharacterRectangle

Oba svojstva vraćaju položaj znaka u prostoru stranice (točke, s ishodištem u donjem lijevom kutu, gdje Y raste prema gore). CharacterOrigin[i] je sidrišna točka osnovne linije (baseline) glifa; CharacterRectangle[i] je puni granični okvir (bounding box). To su gradivni blokovi za bilo što izvan običnog teksta: otkrivanje granica stupaca, grupiranje znakova u retke usporedbom Y koordinata unutar tolerancije ili izgradnja karte hit-testa za odabir teksta u pregledniku. Ako trebate pronaći koji se znak nalazi ispod klika mišem, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) obavlja to traženje izravno bez potrebe da prolazite kroz pravokutnike

Postavljanje DLL datoteke na mjesto

PDFium komponenta delegira svo raščlanjivanje PDF-a na izvorni DLL, bilo pdfium32.dll ili pdfium64.dll, ovisno o vašoj ciljnoj platformi. Komponenta isporučuje skriptu CopyDlls.bat skripta koja kopira ispravnu datoteku u sistemski direktorij Windowsa. Dovoljno ju je pokrenuti kao administrator jednom na razvojnom računalu; za isporuku kopirate DLL uz izvršnu datoteku aplikacije. V8-omogućene varijante (pdfium32v8.dll, pdfium64v8.dll) znatno su veće i potrebne su samo ako vaši PDF-ovi sadrže JavaScript koji se mora izvesti. Za čisto izdvajanje teksta, standardna verzija je ispravan izbor

Ako DLL nedostaje u vrijeme izvođenja, Active := True zatajit će tiho baš kao i za nedostajuću datoteku, jer komponenta interno hvata pogrešku učitavanja. Uvijek testirajte na čistom računalu prije isporuke

Korištenje FontSize[] uz Character[] za analizu izgleda (layout)

Osim običnog teksta, API na razini znakova izlaže FontSize[i], koji vraća iscrtanu veličinu svakog glifa u točkama. U kombinaciji s CharacterOrigin[i] i CharacterRectangle[i], to vam omogućuje razlikovanje tijela teksta od naslova bez oslanjanja na stablo strukture. Niz znakova u kojem veličina fonta skače iznad praga gotovo je sigurno naslov u neoznačenom dokumentu. Ista tehnika vrijedi i za otkrivanje opisa (mali tekst ispod graničnog okvira slike) ili bilješki (mali tekst blizu dna stranice). Ništa od ovoga ne zahtijeva iscrtavanje; sva tri svojstva čitaju se izravno iz sloja teksta koji PDFium gradi tijekom postavljanja Active := True

Jedna nijansa: FontSize[i] odražava veličinu nakon što se primijeni CTM (matrica trenutne transformacije) stranice, 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 ih u odnosu na visinu MediaBox-a svake stranice prije donošenja odluka o pragovima

Zapisivanje izlaza u datoteku

Delphijev TStringList čisto obrađuje UTF-8 izlaz od verzije XE. Postavite WriteBOM := False ako trebate datoteku bez BOM-a (mnogi primateljski sustavi imaju problema s vodećim BOM-om):

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 memorija predstavlja problem, pišite izravno u TStreamWriter s TEncoding.UTF8 unutar petlje stranice umjesto da prvo sve nakupljate u popisu

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